热加载 hotswap-classloader
简介
什么是 hotswap-classloader
?
hotswap-classloader
是由笔者 litongjava 开发的一款 Java 动态类加载器。其核心功能是在 Java 应用运行时动态地更换或更新类定义,而无需重启整个 JVM。这种热替换(Hot Swapping)能力对于开发过程中的快速迭代和测试尤为有用,因为它显著减少了等待应用重启的时间。
为什么将 hotswap-classloader
与 tio-boot
结合使用?
将 hotswap-classloader
与 tio-boot
结合使用,可以为 Java 网络应用开发带来以下关键优势:
- 快速迭代和测试:通过使用
hotswap-classloader
,开发者可以在不重启服务器的情况下实时更新类文件,实现快速迭代和即时测试。 - 提高开发效率:减少了重启应用程序所需的时间,开发者可以更加专注于代码的编写和改进,从而提高工作效率。
- 支持敏捷开发:在敏捷开发模式下,频繁的更改和测试是常态。
hotswap-classloader
的动态加载能力使得这一过程更加流畅和高效。
总的来说,结合使用 hotswap-classloader
和 tio-boot
不仅提高了开发效率,而且增强了网络应用开发的灵活性和便利性。这对于希望快速迭代和改进其网络应用的开发团队来说,是一个非常有价值的组合。
整合热加载
整合热加载的步骤
添加依赖
在项目的
pom.xml
文件中添加以下依赖:<dependency> <groupId>com.litongjava</groupId> <artifactId>hotswap-classloader</artifactId> <!-- https://central.sonatype.com/artifact/com.litongjava/hotswap-classloader --> <version>${hotswap-classloader.version}</version> </dependency>
使用
TioApplicationWrapper
启动服务在启动类中使用
TioApplicationWrapper
来启动应用:import com.litongjava.hotswap.wrapper.tio.boot.TioApplicationWrapper; import com.litongjava.jfinal.aop.annotation.AComponentScan; @AComponentScan public class HelloApp { public static void main(String[] args) { long start = System.currentTimeMillis(); TioApplicationWrapper.run(HelloApp.class, args); long end = System.currentTimeMillis(); System.out.println((end - start) + "ms"); } }
启动热加载
- 在启动类的程序参数(Program arguments)中添加启动参数
--mode=dev
,或者在配置文件中添加对应的配置。 TioApplicationWrapper
会自动判断是否启用热加载。判断逻辑是:如果 ClassLoader 的 URL 中包含/target/classes/
,则是开发环境,开启热加载;否则不是生产环境,不开启
- 在启动类的程序参数(Program arguments)中添加启动参数
测试热加载效果
- 在 Eclipse IDE 中:保存文件即可触发热加载,实时查看效果。
- 在 IDEA 环境中:需要在运行时手动编译(
Build
->Recompile
)文件才能看到效果。
日志示例
```log
2024-09-15 13:48:09.531 [main] INFO c.l.h.w.t.b.TioApplicationWrapper.runDev:75 - start hotswap watcher:Thread[HotSwapWatcher,10,main]
2024-09-15 13:48:09.540 [main] INFO c.l.h.w.t.b.TioApplicationWrapper.runDev:82 - new hotswap class loader:com.litongjava.hotswap.classloader.HotSwapClassLoader@7cf10a6f
2024-09-15 13:48:09.578 [main] INFO c.l.t.u.e.EnvUtils.load:171 - app.env:null
2024-09-15 13:48:09.611 [main] INFO c.l.j.a.s.ComponentScanner.findClasses:72 - resource:file:/E:/code/java/project-litongjava/tio-boot-web-hello/target/classes/com/litongjava/tio/web/hello
2024-09-15 13:48:09.671 [main] INFO c.l.t.u.Threads.getTioExecutor:93 - new worker thead pool:com.litongjava.tio.utils.thread.pool.SynThreadPoolExecutor@612679d6[Running, pool size = 1, active threads = 1, queued tasks = 0, completed tasks = 0]
2024-09-15 13:48:09.676 [main] INFO c.l.t.u.Threads.getGroupExecutor:51 - new group thead pool:java.util.concurrent.ThreadPoolExecutor@52f759d7[Running, pool size = 0, active threads = 0, queued tasks = 0, completed tasks = 0]
2024-09-15 13:48:09.698 [main] INFO c.l.t.b.c.TioApplicationContext.run:278 - init:57(ms),scan class:17(ms),config:50(ms),server:13(ms),http route:16(ms)
2024-09-15 13:48:09.698 [main] INFO c.l.t.b.c.TioApplicationContext.printUrl:295 - port:80
http://localhost
363ms
2024-09-15 13:48:46.892 [pool-1-thread-1] INFO c.l.h.w.HotSwapWatcher.process:156 - watch event ENTRY_MODIFY,IndexController.class
2024-09-15 13:48:46.893 [pool-1-thread-1] INFO c.l.h.w.HotSwapWatcher.process:165 - restart server:com.litongjava.hotswap.wrapper.tio.boot.TioBootRestartServer@126afc98
loading
2024-09-15 13:48:46.893 [pool-1-thread-1] INFO c.l.t.b.c.TioApplicationContext.close:317 - stop server
2024-09-15 13:48:46.894 [tio-group-2] INFO c.l.t.s.AcceptCompletionHandler.failed:132 - The server will be shut down and no new requests will be accepted:0.0.0.0:80
2024-09-15 13:48:46.895 [pool-1-thread-1] INFO c.l.t.u.Threads.close:177 - shutdown group thead pool:java.util.concurrent.ThreadPoolExecutor@52f759d7[Terminated, pool size = 0, active threads = 0, queued tasks = 0, completed tasks = 7]
2024-09-15 13:48:46.896 [pool-1-thread-1] INFO c.l.t.u.Threads.close:188 - shutdown worker thead pool:com.litongjava.tio.utils.thread.pool.SynThreadPoolExecutor@612679d6[Terminated, pool size = 0, active threads = 0, queued tasks = 0, completed tasks = 2]
2024-09-15 13:48:46.896 [pool-1-thread-1] INFO c.l.t.s.TioServer.stop:141 - 0.0.0.0:80 stopped
2024-09-15 13:48:46.898 [pool-1-thread-1] INFO c.l.h.w.t.b.TioBootRestartServer.restart:33 - new classLoader:com.litongjava.hotswap.classloader.HotSwapClassLoader@5ff60e5f
2024-09-15 13:48:46.904 [pool-1-thread-1] INFO c.l.t.u.e.EnvUtils.load:171 - app.env:null
2024-09-15 13:48:46.904 [pool-1-thread-1] INFO c.l.j.a.s.ComponentScanner.findClasses:72 - resource:file:/E:/code/java/project-litongjava/tio-boot-web-hello/target/classes/com/litongjava/tio/web/hello
2024-09-15 13:48:46.912 [pool-1-thread-1] INFO c.l.t.u.Threads.getTioExecutor:93 - new worker thead pool:com.litongjava.tio.utils.thread.pool.SynThreadPoolExecutor@6bb1b24e[Running, pool size = 1, active threads = 1, queued tasks = 0, completed tasks = 0]
2024-09-15 13:48:46.913 [pool-1-thread-1] INFO c.l.t.u.Threads.getGroupExecutor:51 - new group thead pool:java.util.concurrent.ThreadPoolExecutor@6c0d059c[Running, pool size = 0, active threads = 0, queued tasks = 0, completed tasks = 0]
2024-09-15 13:48:46.917 [pool-1-thread-1] INFO c.l.t.b.c.TioApplicationContext.run:278 - init:5(ms),scan class:3(ms),config:5(ms),server:1(ms),http route:4(ms)
2024-09-15 13:48:46.917 [pool-1-thread-1] INFO c.l.t.b.c.TioApplicationContext.printUrl:295 - port:80
http://localhost
Loading complete in 24 ms (^_^)
热加载的实现原理
1. 动态类加载
hotswap-classloader
通过自定义 ClassLoader,实现了在运行时动态加载和替换类的功能。当检测到 class 文件发生变化时,创建一个新的 ClassLoader 来加载更新后的类定义,并使用反射机制重新初始化应用上下文。
2. 文件监控
使用 Java NIO 的 WatchService 监控 class 文件的变动。当检测到文件修改事件时,触发热加载流程。
3. 自定义线程池
由于 NIO2 的默认线程池不支持热加载,为了避免线程池中的线程持有旧的 ClassLoader 导致类无法卸载的问题,hotswap-classloader
使用了自定义的线程池 tio-group
。在热加载时,旧的线程池会被安全地关闭,新的线程池会随新的 ClassLoader 一起创建。
4. 生产环境优化
在生产环境中,TioApplicationWrapper
会自动检测运行模式,避免加载热加载相关的功能,从而不引入额外的性能损耗。生产环境中,仍然使用 NIO 的默认线程池,确保性能和稳定性。
更多使用说明
有关热加载的更多使用细节和高级配置,请参考官方文档:
https://github.com/litongjava/hotswap-classloader
部署注意事项
- 禁用热加载:在部署到生产环境时,不要在程序参数中添加
--mode=dev
。TioApplicationWrapper
会调用TioApplication
来启动项目,不会启用热加载功能。 - 性能考虑:理论上,生产环境不会有多余的性能损耗,因为
TioApplicationWrapper
仅在启动时判断了运行模式和 classpath 路径。
增加对 fastjson2 的支持
问题描述:
在启用了 TioApplicationWrapper
后,出现了以下错误:
java.lang.ClassCastException: com.litongjava.maxkb.vo.MaxKbApplicationNoReferencesSetting cannot be cast to com.litongjava.maxkb.vo.MaxKbApplicationNoReferencesSetting
at com.alibaba.fastjson2.reader.ORG_9_5_MaxKbDatasetSettingVo.readObject(Unknown Source)
at com.alibaba.fastjson2.reader.ORG_8_18_MaxKbApplicationVo.readObject(Unknown Source)
at com.alibaba.fastjson2.JSON.parseObject(JSON.java:864)
at com.litongjava.tio.utils.json.FastJson2.parse(FastJson2.java:42)
at com.litongjava.tio.utils.json.MixedJson.parse(MixedJson.java:28)
at com.litongjava.tio.utils.json.JsonUtils.parse(JsonUtils.java:20)
原因分析:
错误原因:
这个
ClassCastException
异常是由于同一个类com.litongjava.maxkb.vo.MaxKbApplicationNoReferencesSetting
被不同的类加载器加载了多次。即使类的全限定名相同,但如果由不同的类加载器加载,JVM 会认为它们是不同的类,因而无法进行类型转换。与类加载器的关系:
与类加载器直接相关。启用了
hotswapclassloader
后,为了实现热加载,每次都会创建一个新的加载器实现类的加载。这样,在类型转换时,JVM 会检测到类型不兼容,从而抛出ClassCastException
。关于父类加载器的影响:
正常情况下,如果类都是由同一个类加载器或其父类加载器加载的,那么 JVM 会认为这些类是相同的,类型转换也不会出现问题。这是因为 Java 的类加载机制遵循父委派模型,父类加载器加载的类对子类加载器是可见的。
解决方案:
为了避免上述问题,需要确保特定的类由父类加载器加载,而不是 hotswapclassloader 加载器重复加载。可以通过以下方式指定需要由系统类加载器(父类加载器)加载的类:
package com.litongjava.maxkb;
import com.litongjava.annotation.AComponentScan;
import com.litongjava.hotswap.watcher.HotSwapResolver;
import com.litongjava.hotswap.wrapper.tio.boot.TioApplicationWrapper;
@AComponentScan
public class MaxKbApp {
public static void main(String[] args) {
long start = System.currentTimeMillis();
// 指定需要由系统类加载器加载的类的包名前缀
HotSwapResolver.addSystemClassPrefix("com.litongjava.maxkb.vo.");
TioApplicationWrapper.run(MaxKbApp.class, args);
// 如果不需要热加载功能,可以直接使用以下方式启动
// TioApplication.run(MaxKbApp.class, args);
long end = System.currentTimeMillis();
System.out.println((end - start) + "ms");
}
}
解释:
HotSwapResolver.addSystemClassPrefix("com.litongjava.maxkb.vo.");:
这行代码的作用是告诉热加载器,对于包名以
com.litongjava.maxkb.vo.
开头的类,不要进行热加载,而是由系统类加载器(父类加载器)加载。这样可以避免这些类被重复加载,从而避免ClassCastException
。
总结:
避免类重复加载:确保关键的类只被一个类加载器加载,特别是避免由多个并行的类加载器加载同名的类。
合理配置热加载器:在使用热加载功能时,需要仔细配置哪些类需要热加载,哪些类不需要。对于不需要热加载的类,尤其是涉及到类型转换和序列化的类,应该由系统类加载器加载。
理解类加载机制:深入理解 Java 的类加载机制和双亲委派模型,有助于在开发过程中避免类似的问题。
通过以上方法,您应该能够解决 ClassCastException
问题,确保应用程序正常运行。
总结
通过整合 hotswap-classloader
,开发者可以在不重启应用的情况下实时更新代码,大大提高了开发效率。同时,得益于对生产环境的优化处理,热加载机制不会对线上应用的性能和稳定性造成影响。
如果是 Eclipse IDE,保持一个文件即可测试加载效果,如果是 IDEA 环境需要再运行时手动编译(Build-->Recompile)文件才可以看到效果