authors are vetted experts in their fields and write on topics in which they have demonstrated experience. All of our content is peer reviewed and validated by Toptal experts in the same field.
Lê Anh qun的个人资料图片

Lê Anh Quân

Lê有14年使用Java技术构建web应用程序的经验. 在过去的5年里,他一直在使用React和Angular.

Expertise

Previously At

FPT Software
Share

In Java开发项目, 典型的工作流涉及到每次更改类时重新启动服务器, 没有人抱怨. 这是Java开发的一个事实. 从使用Java的第一天起,我们就一直这样工作. 但是Java类的重新加载很难实现吗? 这个问题的解决既具有挑战性又令人兴奋吗 熟练的Java开发人员? 在这个Java类教程中, 我将设法解决这个问题, 帮助您获得动态类重新加载的所有好处, 并极大地提高你的工作效率.

Java类重载不常被讨论, 关于这个过程的文献很少. 我是来改变这一切的. This Java classes tutorial will provide a step by step explanation of this process and help you master this incredible technique. Keep in mind that implementing Java class reloading requires a great deal of care, 但是学会如何去做会让你进入大联盟, 作为Java开发人员, 作为一个软件架构师. 理解也不会有什么坏处 如何避免10个最常见的Java错误.

Work-Space Setup

本教程的所有源代码都上传到GitHub上 here.

要在学习本教程的同时运行代码,您需要 Maven, Git and either Eclipse or IntelliJ IDEA.

如果您正在使用Eclipse:

  • Run the command mvn eclipse:月食 来生成Eclipse的项目文件.
  • 加载生成的项目.
  • 将输出路径设置为 target/classes.

如果您正在使用IntelliJ:

  • 导入项目的 pom file.
  • IntelliJ will not auto-compile when you are running any example, so you have to either:
  • Run the examples inside IntelliJ, then every time you want to compile, you’ll have to press Alt+B E
  • 在IntelliJ外部运行示例 run_example*.bat. 将IntelliJ编译器的自动编译设置为true. 然后,每次更改任何java文件时,IntelliJ都会自动编译它.

示例1:用Java类装入器重新装入一个类

The first example will give you a general understanding of the Java class loader. 这里是源代码.

假设如下 User class definition:

公共静态类User {
  Public static int = 10;
}

我们可以这样做:

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 using the DynamicClassLoader, 一个自定义类加载器,其源代码也在GitHub项目中提供, 我将在下面详细描述.

这是剩下的 main method:

  out.println("似乎是同一个类:");
  out.println (userClass1.getName());
  out.println (userClass2.getName());
  out.println();

  out.println("但是为什么有两个不同的类加载器:");
  out.println (userClass1.getClassLoader ());
  out.println (userClass2.getClassLoader ());
  out.println();

  User.age = 11;
  out.println("不同年龄的值:");
  out.println (ReflectUtil (int).getStaticFieldValue(“年龄”,userClass1));
  out.println (ReflectUtil (int).getStaticFieldValue(“年龄”,userClass2));
}

And the output:

似乎是同一类:
qj.blog.classreloading.example1.StaticInt$User
qj.blog.classreloading.example1.StaticInt$User

但为什么有两个不同的类加载器:
qj.util.lang.DynamicClassLoader@3941a79c
sun.misc.发射器AppClassLoader@1f32e575美元

以及不同的年龄值:
11
10

正如你所看到的,尽管 User 类具有相同的名称, 它们实际上是两个不同的类, 而且它们是可以控制的, and manipulated, independently. The age value, 尽管声明为静态, 存在两个版本, 分别附加到每个类, 也可以独立改变.

在一个普通的Java程序中, ClassLoader 门户是否将类引入JVM. 当一个类需要加载另一个类时,它是 ClassLoader他的任务是装载.

然而,在这个Java类示例中,自定义 ClassLoader named DynamicClassLoader 用于加载第二个版本的 User class. If instead of DynamicClassLoader,我们将再次使用默认的类装入器(使用命令 StaticInt.class.getClassLoader() ) then the same User 类将被使用,因为所有加载的类都被缓存.

Examining the way the default Java ClassLoader works versus DynamicClassLoader is key to benefiting from this Java classes tutorial.

The DynamicClassLoader

在一个普通的Java程序中可以有多个类加载器. 加载主类的那个, ClassLoader, is the default one, and from your code, you can create and use as many classloaders as you like. 这就是在Java中重载类的关键. The DynamicClassLoader 可能是整个教程中最重要的部分吗, so we must understand how dynamic class loading works before we can accomplish our goal.

的默认行为不同 ClassLoader, our DynamicClassLoader 继承了更激进的策略. 一个普通的类装入器会给出它的父类 ClassLoader 优先级和只加载其父类无法加载的类. 这在正常情况下是合适的,但在我们的情况下不合适. Instead, the DynamicClassLoader will try to look through all its class paths and resolve the target class before it gives up the right to its parent.

在上面的示例中, DynamicClassLoader 只使用一个类路径创建: "target/classes" (in our current directory), so it’s capable of loading all the classes that reside in that location. For all the classes not in there, it will have to refer to the parent classloader. 例如,我们需要加载 String class in our StaticInt 类,并且我们的类装入器无法访问 rt.jar 在JRE文件夹中,所以 String 将使用父类装入器的类.

下面的代码来自 AggressiveClassLoader的父类 DynamicClassLoader,并显示此行为的定义位置.

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

请注意下列性质 DynamicClassLoader:

  • The loaded classes have the same performance and other attributes as other classes loaded by the default class loader.
  • The DynamicClassLoader 是否可以与其所有加载的类和对象一起被垃圾收集.

能够加载和使用同一个类的两个版本, we are now thinking of dumping the old version and loading the new one to replace it. 在下一个例子中,我们将持续地.

例2:连续重新加载一个类

This next Java example will show you that the JRE can load and reload classes forever, 旧的类被丢弃,垃圾被收集, 全新的类从硬盘中加载并投入使用. 这里是源代码.

下面是主循环:

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

每隔两秒,老去 User 类将被转储,将加载一个新的类及其方法 hobby invoked.

Here is the User class definition:

@SuppressWarnings(“UnusedDeclaration”)
公共静态类User {
  Public static void hobby() {
    playFootball(); //将在运行时注释
    //  playBasketball(); // will uncomment during runtime
  }
  
  //将在运行时注释
  public static void playFootball() {
    System.out.println(“踢足球”);
  }
  
  //将在运行时取消注释
  // public static void playBasketball() {
  //    System.out.println(“打篮球”);
  //  }
}

运行此应用程序时, 中指示的代码应该尝试注释和取消注释 User class. 您将看到总是使用最新的定义.

下面是一些输出示例:

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

每次都有新的实例 DynamicClassLoader ,它将加载 User class from the target/classes 文件夹,我们已经将Eclipse或IntelliJ设置为输出最新的类文件. All old DynamicClassLoaders and old User 类将被解除链接,并受到垃圾回收器的处理.

高级Java开发人员理解动态类重载是至关重要的, 活动或未链接.

如果您熟悉JVM HotSpot, then it’s noteworthy here that the class structure can also be changed and reloaded: the playFootball 方法是要去除和 playBasketball method added. 这与HotSpot不同, 哪一个只允许改变方法内容, 否则类无法重新加载.

Now that we are capable of reloading a class, it is time to try reloading many classes at once. 让我们在下一个例子中尝试一下.

示例3:重载多个类

这个示例的输出将与示例2相同, but will show how to implement this behavior in a more application-like structure with context, 服务和模型对象. This example’s source code is rather large, so I have only shown parts of it here. 完整的源代码是 here.

Here is is the main method:

public static void main(String[] args) {
  for (;;) {
    对象上下文= createContext();
    invokeHobbyService(上下文);
    ThreadUtil.sleep(2000);
  }
}

And the method createContext:

createContext() {
  Class contextClass = new DynamicClassLoader("target/classes")
    .load("qj.blog.classreloading.example3.美元ContextReloading上下文”);
  对象context = newInstance(contextClass);
  调用(“init”、上下文);
  return context;
}

The method invokeHobbyService:

私有静态void invokeHobbyService(对象上下文){
  对象hobyservice = getFieldValue(" hobyservice ", context);
  调用(“爱好”,hobbyService);
}

And here is the Context class:

公共静态类Context {
  public hobyservice = new hobyservice ();
  
  Public void init() {
    //在这里初始化你的服务
    hobbyService.user = new user ();
  }
}

And the HobbyService class:

公共静态类HobbyService {
  public User user;
  
  Public void hobby() {
    user.hobby();
  }
}

The Context 类要复杂得多 User 类:它具有指向其他类的链接,并且具有 init 方法在每次实例化时调用. Basically, it’s very similar to real world application’s context classes (which keeps track of the application’s modules and does dependency injection). 所以能够重新加载这个 Context class together with all it’s linked classes is a great step toward applying this technique to real life.

即使是高级Java工程师也很难重新加载Java类.

随着类和对象数量的增长, 我们“删除旧版本”的步骤也将变得更加复杂. 这也是类重载如此困难的最大原因. To possibly drop old versions we will have to make sure that, once the new context is created, all 对旧类和对象的引用将被删除. 我们如何优雅地处理这个问题?

The main 方法将持有上下文对象,并且 这是唯一的联系 所有需要丢掉的东西. 如果我们切断这个连接, 上下文对象和上下文类, 服务对象…都将受到垃圾收集器的影响.

A little explanation about why normally classes are so persistent, and do not get garbage collected:

  • 通常,我们将所有类加载到默认的Java类加载器中.
  • 类-类加载器关系是一种双向关系, 类装入器还缓存它所装入的所有类.
  • 所以只要类加载器仍然连接到任何活动线程, 所有东西(所有加载的类)都不受垃圾收集器的影响.
  • That said, unless we can separate the code we want to reload from the code already loaded by the default class loader, 我们的新代码更改将永远不会在运行时期间应用.

With this example, we see that reloading all application’s classes is actually rather easy. 目标仅仅是保持苗条, 从活动线程到正在使用的动态类装入器的可放下连接. 但是如果我们希望一些对象(和它们的类) not 被重新加载,并在重新加载周期之间被重用? 让我们看下一个例子.

例4:分离持久化和重新加载的类空间

这是源代码..

The main method:

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

  for (;;) {
    对象context = createContext(pool);

    invokeService(上下文);

    ThreadUtil.sleep(2000);
  }
}

你可以看到这里的技巧是加载 ConnectionPool 类并在重新加载周期之外实例化它, 将其保存在持久化空间中, 并将引用传递给 Context objects

The createContext 方法也有一点不同:

createContext(ConnectionPool pool) {
  classLoader = new ExceptingClassLoader(
      (className) -> className.contains(".crossing."),
      “目标/类”);
  Class contextClass = classLoader.load("qj.blog.classreloading.example4.reloadable.Context");
  对象context = newInstance(contextClass);
  
  setFieldValue(pool, "pool", context);
  调用(“init”、上下文);

  return context;
}

From now on, we will call the objects and classes that are reloaded with every cycle the “reloadable space” and others - the objects and classes not recycled and not renewed during the reloading cycles - the “persisted space”. 我们必须非常清楚哪些对象或类驻留在哪个空间, 因此在这两个空间之间画了一条分隔线.

Unless handled properly, this separation of Java class loading can lead to failure.

从图中可以看出,不仅是 Context object and the UserService 对象引用 ConnectionPool object, but the Context and UserService 类也引用 ConnectionPool class. 这是一个非常危险的情况,经常导致混乱和失败. The ConnectionPool 类不能被 DynamicClassLoader必须只有一个 ConnectionPool 类,该类是默认加载的 ClassLoader. This is one example of why it is so important to be careful when designing a class-reloading architecture in Java.

What if our DynamicClassLoader 意外载入 ConnectionPool class? Then the ConnectionPool 对象不能从持久化空间传递到 Context 对象,因为 Context 对象正在等待另一个类的对象,该对象也命名为 ConnectionPool,但实际上是一个不同的类!

那么我们如何预防我们的 DynamicClassLoader from loading the ConnectionPool class? Instead of using DynamicClassLoader,这个例子使用了它的一个子类,名为: ExceptingClassLoader, which will pass the loading to super classloader based on a condition function:

(className) -> className.包含(" $连接”)

If we don’t use ExceptingClassLoader here, then the DynamicClassLoader would load the ConnectionPool 类,因为这个类驻留在target/classes” folder. 另一种防止 ConnectionPool 班被我们的 DynamicClassLoader is to compile the ConnectionPool class to a different folder, maybe in a different module, and it will be compiled separately.

选择空间的规则

现在,Java类加载工作变得非常混乱. 我们如何确定哪些类应该在持久化空间中, 哪些类属于可重载空间? 以下是规则:

  1. 可重载空间中的类可以引用持久化空间中的类, 而是持久化空间中的类 may never 在可重载空间中引用一个类. 在前面的例子中,可重新加载的 Context 类引用持久化 ConnectionPool class, but ConnectionPool 没有提到 Context
  2. A class can exist in either space if it does not reference any class in the other space. 例如,具有所有静态方法的实用程序类,如 StringUtils can be loaded once in the persisted space, and loaded separately in the reloadable space.

所以你可以看到这些规则并不是很严格. Except for the crossing classes that have objects referenced across the two spaces, all other classes can be freely used in either the persisted space or the reloadable space or both. Of course, only classes in the reloadable space will enjoy being reloaded with reloading cycles.

这样就解决了类重载中最具挑战性的问题. 在下一个例子中, 我们将尝试将这种技术应用到一个简单的web应用程序中, 并像任何脚本语言一样重新加载Java类.

例5:小电话簿

这是源代码..

This example will be very similar to what a normal web application should look like. 它是一个单页应用程序,使用AngularJS, SQLite, Maven和 Jetty嵌入式Web服务器.

下面是web服务器结构中的可重载空间:

A thorough understanding of the reloadable space in the web server’s structure will help you master Java class loading.

web服务器不会保存对实际servlet的引用, 哪些必须留在可重新加载的空间, 以便重新装填. 它保存的是存根servlet, which, 每次调用它的服务方法, 将解析实际的servlet在实际上下文中运行.

这个例子还引入了一个新对象 ReloadingWebContext, 它提供给web服务器的所有值像一个正常的上下文, but internally holds references to an actual context object that can be reloaded by a DynamicClassLoader. It is this ReloadingWebContext 哪个向web服务器提供存根servlet.

ReloadingWebContext handles stub servlets to the web server in the Java class reloading process.

The ReloadingWebContext 将是实际上下文的包装器,并且:

  • 当调用对“/”的HTTP GET时,会重新加载实际上下文吗.
  • 会向web服务器提供存根servlet吗.
  • Will set values and invoke methods every time the actual context is initialized or destroyed.
  • Can be configured to reload the context or not, and which classloader is used for reloading. 这将有助于在生产环境中运行应用程序.

Because it’s very important to understand how we isolate the persisted space and reloadable space, 下面是在两个空间之间交叉的两个类:

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

  • 函数对象,每次调用该函数时将返回一个Connection. 该类驻留在qj中.Util包,它被排除在 DynamicClassLoader.

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

  • 普通SQL连接对象. 这个类不在我们的 DynamicClassLoader的类路径,所以它不会被拾取.

Summary

在这个Java类教程中, 我们已经看到了如何重新加载单个类, 连续重新加载单个类, 重载包含多个类的整个空间, 并从必须持久化的类中分别重新加载多个类. With these tools, the key factor to achieve reliable class reloading is to have a super clean design. 然后,您可以自由地操作您的类和整个JVM.

实现Java类重载并不是世界上最简单的事情. 但如果你试一试, 并且在某些时候发现您的类正在动态加载, 那你就快成功了. There will be very little left to do before you can achieve totally superb clean design for your system.

祝我的朋友们好运,享受你们新发现的超能力!

聘请Toptal这方面的专家.
Hire Now
Lê Anh qun的个人资料图片
Lê Anh Quân

Located in Hanoi, Vietnam

Member since August 26, 2014

About the author

Lê有14年使用Java技术构建web应用程序的经验. 在过去的5年里,他一直在使用React和Angular.

Toptalauthors are vetted experts in their fields and write on topics in which they have demonstrated experience. All of our content is peer reviewed and validated by Toptal experts in the same field.

Expertise

Previously At

FPT Software

世界级的文章,每周发一次.

订阅意味着同意我们的 privacy policy

世界级的文章,每周发一次.

订阅意味着同意我们的 privacy policy

Toptal Developers

Join the Toptal® community.