java高级开发

Advanced Java Class Tutorial: A

2022-12-23  本文已影响0人  老鼠AI大米_Java全栈

在Java开发中,一个典型的工作流程是在每次类更改时重新启动服务器,没有人对此抱怨?解决这个问题是否既具有挑战性又令人兴奋?在本文中,我将尝试解决这个问题,帮助您获得动态类重载的所有好处,并极大地提高您的生产力。

在Java开发中,一个典型的工作流程是在每次类更改时重新启动服务器,没有人对此抱怨?解决这个问题是否既具有挑战性又令人兴奋?在本文中,我将尝试解决这个问题,帮助您获得动态类重载的所有好处,并极大地提高您的生产力。

Java类重载不常被讨论,而且很少有文档探讨这个过程。我是来改变这一点的。本Java课程教程将逐步解释这一过程,并帮助您掌握这一令人难以置信的技术。请记住,实现Java类重载需要非常小心,但学习如何实现它将使您成为Java开发人员和软件架构师的一员。了解如何避免最常见的10个Java错误也不会有什么害处。

Work-Space Setup

All source code for this tutorial is uploaded on GitHub here.

To run the code while you follow this tutorial, you will need Maven, Git and either Eclipse or IntelliJ IDEA.

If you are using Eclipse:

If you are using IntelliJ:

Example 1: Reloading a Class with Java Class Loader

The first example will give you a general understanding of the Java class loader. Here is the source code.

Given the following User class definition:

public static class User {
  public static int age = 10;
}

We can do the following:

public static void main(String[] args) {
  Class<?> userClass1 = User.class;
  Class<?> userClass2 = new DynamicClassLoader("target/classes")
      .load("qj.blog.classreloading.example1.StaticInt$User");
  ...

在本教程示例中,内存中将加载两个User类。userClass1将由JVM的默认类加载器加载,userClass2将使用DynamicClassLoader加载,DynamicClass加载器是一个自定义类加载器,其源代码也在GitHub项目中提供,我将在下面详细描述。

Here is the rest of the main method:

  out.println("Seems to be the same class:");
  out.println(userClass1.getName());
  out.println(userClass2.getName());
  out.println();

  out.println("But why there are 2 different class loaders:");
  out.println(userClass1.getClassLoader());
  out.println(userClass2.getClassLoader());
  out.println();

  User.age = 11;
  out.println("And different age values:");
  out.println((int) ReflectUtil.getStaticFieldValue("age", userClass1));
  out.println((int) ReflectUtil.getStaticFieldValue("age", userClass2));
}

And the output:

Seems to be the same class:
qj.blog.classreloading.example1.StaticInt$User
qj.blog.classreloading.example1.StaticInt$User

But why there are 2 different class loaders:
qj.util.lang.DynamicClassLoader@3941a79c
sun.misc.Launcher$AppClassLoader@1f32e575

And different age values:
11
10

正如您在这里看到的,尽管User类具有相同的名称,但它们实际上是两个不同的类,可以独立地管理和操作它们。age值虽然声明为静态,但存在于两个版本中,分别附加到每个类,也可以独立更改。

在正常的Java程序中,ClassLoader是将类引入JVM的门户。当一个类需要加载另一个类时,ClassLoader的任务是加载。

然而,在这个Java类示例中,名为DynamicClassLoader的自定义ClassLoader用于加载User类的第二个版本。如果不是DynamicClassLoader,而是再次使用默认的类加载器(使用命令StaticInt.class.getClassLoader()),那么将使用相同的User类,因为所有加载的类都将被缓存。

image.png

The DynamicClassLoader

在一个普通的Java程序中可以有多个类加载器。加载主类ClassLoader的类是默认类,您可以从代码中创建和使用任意多的类加载器。因此,这是在Java中重新加载类的关键。DynamicClassLoader可能是整个教程中最重要的部分,因此我们必须了解动态类加载是如何工作的,然后才能实现我们的目标。

与ClassLoader的默认行为不同,我们的DynamicClassLoader继承了更具侵略性的策略。一个普通的类加载器会给其父类加载器优先权,只加载其父类无法加载的类。这适用于正常情况,但不适用于我们的情况。相反,DynamicClassLoader将尝试查看其所有类路径,并在放弃其父类的权限之前解析目标类。

在上面的示例中,DynamicClassLoader仅使用一个类路径创建:“target/classes”(在当前目录中),因此它能够加载驻留在该位置的所有类。对于所有不在其中的类,它必须引用父类加载器。例如,我们需要在StaticInt类中加载String类,而我们的类加载器无法访问JRE文件夹中的rt.jar,因此将使用父类加载器的String类。

The following code is from AggressiveClassLoader, the parent class of DynamicClassLoader, and shows where this behavior is defined.

byte[] newClassData = loadNewClass(name);
if (newClassData != null) {
  loadedClasses.add(name);
  return loadClass(newClassData, name);
} else {
  unavaiClasses.add(name);
  return parent.loadClass(name);
}

Take note of the following properties of DynamicClassLoader:

Example 2: Reloading a Class Continuously

下一个Java示例将向您展示JRE可以永远加载和重新加载类,将旧类转储并回收垃圾,并从硬盘加载全新的类并投入使用

Here is the main loop:

public static void main(String[] args) {
  for (;;) {
    Class<?> userClass = new DynamicClassLoader("target/classes")
      .load("qj.blog.classreloading.example2.ReloadingContinuously$User");
    ReflectUtil.invokeStatic("hobby", userClass);
    ThreadUtil.sleep(2000);
  }
}

Every two seconds, the old User class will be dumped, a new one will be loaded and its method hobby invoked.

Here is the User class definition:

@SuppressWarnings("UnusedDeclaration")
public static class User {
  public static void hobby() {
    playFootball(); // will comment during runtime
    //  playBasketball(); // will uncomment during runtime
  }
  
  // will comment during runtime
  public static void playFootball() {
    System.out.println("Play Football");
  }
  
  //  will uncomment during runtime
  //  public static void playBasketball() {
  //    System.out.println("Play Basketball");
  //  }
}

When running this application, you should try to comment and uncomment the code indicated code in the User class. You will see that the newest definition will always be used.

Here is some example output:

...
Play Football
Play Football
Play Football
Play Basketball
Play Basketball
Play Basketball

每次创建DynamicClassLoader的新实例时,它都会从target/classes文件夹中加载User类,我们已将Eclipse或IntelliJ设置为输出最新的类文件。所有旧的DynamicClassLoader和旧的User类都将被取消链接,并接受垃圾收集器的处理。


image.png

如果您熟悉JVM HotSpot,那么这里值得注意的是,类结构也可以更改和重新加载:将删除playFootball方法,并添加playBasketball方法。这与HotSpot不同,HotSpot只允许更改方法内容,或者不能重新加载类。

既然我们已经能够重新加载一个类,那么是时候尝试一次重新加载多个类了。让我们在下一个示例中尝试一下。

Example 3: Reloading Multiple Classes

此示例的输出将与示例2相同,但将展示如何在具有上下文、服务和模型对象的更类似于应用程序的结构中实现此行为。这个例子的源代码相当大,所以我在这里只展示了它的一部分。

Here is is the main method:

public static void main(String[] args) {
  for (;;) {
    Object context = createContext();
    invokeHobbyService(context);
    ThreadUtil.sleep(2000);
  }
}

And the method createContext:

private static Object createContext() {
  Class<?> contextClass = new DynamicClassLoader("target/classes")
    .load("qj.blog.classreloading.example3.ContextReloading$Context");
  Object context = newInstance(contextClass);
  invoke("init", context);
  return context;
}

The method invokeHobbyService:

private static void invokeHobbyService(Object context) {
  Object hobbyService = getFieldValue("hobbyService", context);
  invoke("hobby", hobbyService);
}

And here is the Context class:

public static class Context {
  public HobbyService hobbyService = new HobbyService();
  
  public void init() {
    // Init your services here
    hobbyService.user = new User();
  }
}

And the HobbyService class:

public static class HobbyService {
  public User user;
  
  public void hobby() {
    user.hobby();
  }
}

本示例中的Context类比前面示例中的User类复杂得多:它具有指向其他类的链接,并且每次实例化时都会调用init方法。基本上,它非常类似于真实世界应用程序的上下文类(它跟踪应用程序的模块并执行依赖注入)。因此,能够将此Context类及其所有链接类一起重新加载,是将此技术应用于现实生活的一大步。


image.png

随着类和对象数量的增加,我们“删除旧版本”的步骤也将变得更加复杂。这也是类重载如此困难的最大原因。为了可能删除旧版本,我们必须确保在创建新上下文后,删除对旧类和对象的所有引用。我们如何优雅地处理这个问题?

这里的main方法将持有上下文对象,这是需要删除的所有内容的唯一链接。如果我们断开该链接,上下文对象、上下文类和服务对象……都将受到垃圾收集器的处理。

关于为什么类通常如此持久,并且不会收集垃圾的一点解释:

在这个示例中,我们看到重新加载所有应用程序的类实际上相当容易。目标仅仅是保持从活动线程到使用中的动态类加载器的瘦的、可丢弃的连接。但是,如果我们希望某些对象(及其类)不被重新加载,并且在重新加载周期之间被重用呢?让我们看下一个示例。

Example 4: Separating Persisted and Reloaded Class Spaces

The main method:

public static void main(String[] args) {
  ConnectionPool pool = new ConnectionPool();

  for (;;) {
    Object context = createContext(pool);

    invokeService(context);

    ThreadUtil.sleep(2000);
  }
}

因此,您可以看到这里的技巧是加载ConnectionPool类,并在重载循环外实例化它,将其保存在持久化空间中,并将引用传递给Context对象.

The createContext method is also a little bit different:

private static Object createContext(ConnectionPool pool) {
  ExceptingClassLoader classLoader = new ExceptingClassLoader(
      (className) -> className.contains(".crossing."),
      "target/classes");
  Class<?> contextClass = classLoader.load("qj.blog.classreloading.example4.reloadable.Context");
  Object context = newInstance(contextClass);
  
  setFieldValue(pool, "pool", context);
  invoke("init", context);

  return context;
}

从现在起,我们将在每个循环中重新加载的对象和类称为“可重新加载空间”,而其他对象和类-在重新加载循环中未回收和未更新的对象和类别-称为“持久化空间”。我们必须非常清楚哪些对象或类停留在哪个空间中,从而在这两个空间之间画出一条分隔线。


image.png

从图中可以看出,不仅Context对象和UserService对象引用ConnectionPool对象,而且Context和UserService类也引用了ConnectionPool类。这是一种非常危险的情况,经常导致混乱和失败。ConnectionPool类不能由我们的DynamicClassLoader加载,内存中只能有一个ConnectionPool类别,即默认ClassLoader所加载的类别。这是一个例子,说明了在Java中设计类重载体系结构时,必须小心谨慎。

如果DynamicClassLoader意外加载了ConnectionPool类怎么办?然后,无法将持久化空间中的ConnectionPool对象传递给Context对象,因为Context对象需要一个不同类的对象,该对象也被命名为ConnectionPool,但实际上是一个不同的类!

那么我们如何防止DynamicClassLoader加载ConnectionPool类呢?本示例不使用DynamicClassLoader,而是使用它的一个子类:ExceptingClassLoader。该子类将根据条件函数将加载传递给超级类加载器:

(className) -> className.contains("$Connection")

如果我们在这里不使用ExceptingClassLoader,那么DynamicClassLoader将加载ConnectionPool类,因为该类位于“target/classes”文件夹中。防止ConnectionPool类被DynamicClassLoader获取的另一种方法是将ConnectionPool类别编译到不同的文件夹中,可能在不同的模块中,并且将单独编译。

Rules for Choosing Space

现在,Java类加载作业变得非常混乱。我们如何确定哪些类应该在持久化空间中,哪些类在可重载空间中?

Here are the rules:

  1. 可重载空间中的类可以引用持久化空间中的一个类,但持久化空间内的类可能永远不会引用可重载空间内的某个类。在前面的示例中,可重新加载的Context类引用了持久化的ConnectionPool类,但ConnectionPool没有对Context的引用
  2. 如果一个类不引用另一个空间中的任何类,则该类可以存在于这两个空间中。例如,具有所有静态方法(如StringUtils)的实用程序类可以在持久化空间中加载一次,然后在可重新加载空间中单独加载。

所以你可以看到这些规则并不是很严格。除了在两个空间中引用对象的交叉类之外,所有其他类都可以在持久化空间或可重载空间中自由使用,或者两者都可以。当然,只有可重新加载空间中的类才会享受重新加载循环的乐趣。

因此,类重载最具挑战性的问题就得到了解决。在下一个示例中,我们将尝试将此技术应用于一个简单的web应用程序,并像任何脚本语言一样享受重新加载Java类的乐趣。

Example 5: Little Phone Book

This example will be very similar to what a normal web application should look like. It is a Single Page Application with AngularJS, SQLite, Maven, and Jetty Embedded Web Server.

Here is the reloadable space in the web server’s structure:


image.png

web服务器不会保存对真实servlet的引用,这些引用必须保留在可重新加载的空间中,以便重新加载。它保存的是存根servlet,每次调用它的服务方法时,都会解析实际上下文中要运行的实际servlet。

这个示例还引入了一个新的对象ReloadingWebContext,它向web服务器提供了与普通Context类似的所有值,但在内部保存了对可由DynamicClassLoader重新加载的实际上下文对象的引用。正是这个ReloadingWebContext为web服务器提供存根servlet。


image.png

The ReloadingWebContext will be the wrapper of the actual context, and:

Because it’s very important to understand how we isolate the persisted space and reloadable space, here are the two classes that are crossing between the two spaces:

Class qj.util.funct.F0 for object public F0<Connection> connF in Context

Class java.sql.Connection for object public F0<Connection> connF in Context

Normal SQL connection object. This class does not reside in our DynamicClassLoader’s class path so it won’t be picked up.

Summary

在本Java类教程中,我们看到了如何重新加载单个类、连续重新加载单个个类、重新加载多个类的整个空间,以及如何从必须持久化的类中单独重新加载多类。使用这些工具,实现可靠的类重载的关键因素是拥有一个超干净的设计。然后可以自由地操纵类和整个JVM。

实现Java类重载并不是世界上最简单的事情。但如果你尝试一下,并且在某个时刻发现你的类正在快速加载,那么你就已经快到了。在您可以为系统实现完美的清洁设计之前,您只需要做很少的事情。

Good luck my friends and enjoy your newfound superpower!

参考:https://www.toptal.com/java/java-wizardry-101-a-guide-to-java-class-reloading

上一篇下一篇

猜你喜欢

热点阅读