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ê有14年使用Java技术构建web应用程序的经验. 在过去的5年里,他一直在使用React和Angular.
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错误.
本教程的所有源代码都上传到GitHub上 here.
要在学习本教程的同时运行代码,您需要 Maven, Git and either Eclipse or IntelliJ IDEA.
mvn eclipse:月食
来生成Eclipse的项目文件.target/classes
.pom
file.Alt+B E
run_example*.bat
. 将IntelliJ编译器的自动编译设置为true. 然后,每次更改任何java文件时,IntelliJ都会自动编译它.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
类将被使用,因为所有加载的类都被缓存.
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
:
DynamicClassLoader
是否可以与其所有加载的类和对象一起被垃圾收集.能够加载和使用同一个类的两个版本, we are now thinking of dumping the old version and loading the new one to replace it. 在下一个例子中,我们将持续地.
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 DynamicClassLoader
s and old User
类将被解除链接,并受到垃圾回收器的处理.
如果您熟悉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. 让我们在下一个例子中尝试一下.
这个示例的输出将与示例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.
随着类和对象数量的增长, 我们“删除旧版本”的步骤也将变得更加复杂. 这也是类重载如此困难的最大原因. 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:
With this example, we see that reloading all application’s classes is actually rather easy. 目标仅仅是保持苗条, 从活动线程到正在使用的动态类装入器的可放下连接. 但是如果我们希望一些对象(和它们的类) not 被重新加载,并在重新加载周期之间被重用? 让我们看下一个例子.
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”. 我们必须非常清楚哪些对象或类驻留在哪个空间, 因此在这两个空间之间画了一条分隔线.
从图中可以看出,不仅是 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类加载工作变得非常混乱. 我们如何确定哪些类应该在持久化空间中, 哪些类属于可重载空间? 以下是规则:
Context
类引用持久化 ConnectionPool
class, but ConnectionPool
没有提到 Context
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类.
This example will be very similar to what a normal web application should look like. 它是一个单页应用程序,使用AngularJS, SQLite, Maven和 Jetty嵌入式Web服务器.
下面是web服务器结构中的可重载空间:
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.
The ReloadingWebContext
将是实际上下文的包装器,并且:
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
in Context
DynamicClassLoader
.Class java.sql.Connection
for object public F0
in Context
DynamicClassLoader
的类路径,所以它不会被拾取.在这个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.
祝我的朋友们好运,享受你们新发现的超能力!
Lê有14年使用Java技术构建web应用程序的经验. 在过去的5年里,他一直在使用React和Angular.
世界级的文章,每周发一次.
世界级的文章,每周发一次.
Join the Toptal® community.