tio-boot
整合 hotswap-classloader
实现热加载
简介
在现代软件开发中,快速迭代和高效测试是提升开发效率的关键。传统的开发流程中,每次代码变更后都需要重启应用程序,这不仅耗时且影响开发体验。为了优化这一过程,本文介绍了如何将 hotswap-classloader
与 tio-boot
结合使用,实现 Java 应用的热加载(Hot Swapping),从而在不重启 JVM 的情况下动态更新类定义。
什么是 hotswap-classloader
?
hotswap-classloader
是由开发者 litongjava 开发的一款 Java 动态类加载器。其核心功能是在 Java 应用运行时动态更换或更新类定义,无需重启整个 JVM。这种热替换能力在开发过程中尤为有用,因为它显著减少了等待应用重启的时间,加快了开发迭代和测试的效率。
为什么将 hotswap-classloader
与 tio-boot
结合使用?
将 hotswap-classloader
与 tio-boot
结合使用,为 Java 网络应用开发带来了多项关键优势:
- 快速迭代和测试:开发者可以在不重启服务器的情况下实时更新类文件,快速迭代和即时测试,提高开发效率。
- 提高开发效率:减少了重启应用程序的时间,使开发者能够更专注于代码的编写和改进。
- 支持敏捷开发:在敏捷开发模式下,频繁的代码更改和测试是常态。
hotswap-classloader
的动态加载能力使这一过程更加流畅和高效。
结合使用 hotswap-classloader
和 tio-boot
不仅提升了开发效率,还增强了网络应用开发的灵活性和便利性,对于希望快速迭代和改进其网络应用的开发团队而言,这是一个极具价值的组合。
整合热加载
整合热加载的步骤
以下步骤将指导您如何将 hotswap-classloader
与 tio-boot
进行整合,实现热加载功能:
1. 添加依赖
在项目的 pom.xml
文件中添加 hotswap-classloader
的依赖:
<dependency>
<groupId>com.litongjava</groupId>
<artifactId>hotswap-classloader</artifactId>
<!-- 依赖地址:https://central.sonatype.com/artifact/com.litongjava/hotswap-classloader -->
<version>${hotswap-classloader.version}</version>
</dependency>
请确保将 ${hotswap-classloader.version}
替换为实际的版本号。
2. 使用 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");
}
}
3. 启动热加载
- 通过程序参数:在启动类的程序参数(Program arguments)中添加启动参数
--mode=dev
。 - 通过配置文件:在配置文件中添加相应的配置以启用开发模式。
TioApplicationWrapper
会自动判断是否启用热加载。判断逻辑为:如果 ClassLoader 的 URL 中包含 /target/classes/
,则认为是开发环境,开启热加载;否则,认为是生产环境,不启用热加载。
测试热加载效果
- 在 Eclipse IDE 中:只需保存文件即可触发热加载,实时查看效果。
- 在 IntelliJ IDEA 环境中:需要在运行时手动编译(
Build
->Recompile
)文件才能看到效果。
日志示例
以下是启动和热加载过程中的日志示例:
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 thread 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 thread 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 thread 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 thread 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 thread 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 thread 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
实现了在运行时动态加载和替换类的功能。当检测到类文件发生变化时,hotswap-classloader
会创建一个新的 ClassLoader
来加载更新后的类定义,并使用反射机制重新初始化应用上下文。这确保了新版本的类能够在不重启 JVM 的情况下被应用。
2. 文件监控
利用 Java NIO 的 WatchService
实时监控类文件的变动。当检测到文件修改事件时,触发热加载流程,确保类的更新能够及时应用。
3. 自定义线程池
由于 AIO 的默认线程池不支持热加载,为避免线程池中的线程持有旧的 ClassLoader
导致类无法卸载的问题,hotswap-classloader
使用了自定义的线程池 tio-group
。在热加载时,旧的线程池会被安全地关闭,新的线程池会随新的 ClassLoader
一起创建,确保类的正确加载和卸载。
4. 生产环境优化
在生产环境中,TioApplicationWrapper
会自动检测运行环境,避免加载热加载相关的功能,从而不引入额外的性能损耗。生产环境仍然使用 AIO 的默认线程池,确保应用的性能和稳定性。
更多使用说明
有关热加载的更多使用细节和高级配置,请参考官方文档:
部署注意事项
在将应用部署到生产环境时,需要注意以下事项,以确保应用的稳定性和性能:
- 禁用热加载:在生产环境中,不要在程序参数中添加
--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 会将它们视为不同的类,从而导致类型转换失败。
原因分析
类重复加载:
启用了
hotswap-classloader
后,每次热加载都会创建一个新的ClassLoader
来加载类定义。这样,同名的类在不同的ClassLoader
下被加载,JVM 认为它们是不同的类,导致ClassCastException
。类加载器的影响:
类加载器直接影响类的唯一性。Java 遵循双亲委派模型,即子类加载器在加载类时会首先委托给父类加载器。如果同一个类由不同的加载器加载,即使类名相同,JVM 也会将它们视为不同的类。
解决方案
为了避免上述问题,需要确保特定的类由系统类加载器(父类加载器)加载,而不是由 hotswap-classloader
重复加载。具体步骤如下:
1. 指定系统类加载器加载特定类
在启动类中,配置 HotSwapResolver
以指定需要由系统类加载器加载的类的包前缀。例如:
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");
}
}
2. 解释配置
HotSwapResolver.addSystemClassPrefix("com.litongjava.maxkb.vo.");
:该配置的作用是告知热加载器,对于包名以
com.litongjava.maxkb.vo.
开头的类,不进行热加载,而是由系统类加载器加载。这样可以确保这些类只被加载一次,避免因重复加载导致的ClassCastException
。
总结
- 避免类重复加载:确保关键类仅由一个类加载器加载,尤其是涉及类型转换和序列化的类,避免由多个类加载器加载同名类。
- 合理配置热加载器:在使用热加载功能时,仔细配置哪些类需要热加载,哪些类不需要。对于不需要热加载的类,尤其是关键类,应该由系统类加载器加载。
- 理解类加载机制:深入理解 Java 的类加载机制和双亲委派模型,有助于在开发过程中避免类似的问题。
通过以上方法,您可以解决 ClassCastException
问题,确保应用程序在启用热加载功能后正常运行。
总结
通过整合 hotswap-classloader
,开发者可以在不重启应用的情况下实时更新代码,大大提高了开发效率。同时,得益于对生产环境的优化处理,热加载机制不会对线上应用的性能和稳定性造成影响。
使用建议:
- 在开发环境中,如 Eclipse IDE,保持文件保存即可测试加载效果。
- 在 IntelliJ IDEA 环境中,需要在运行时手动编译(
Build
->Recompile
)文件才能看到热加载效果。
正确配置和使用 hotswap-classloader
与 tio-boot
的整合,将为您的 Java 网络应用开发带来显著的效率提升和更好的开发体验。