Skip to content

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 网络应用开发带来了多项关键优势:

  1. 快速迭代和测试:开发者可以在不重启服务器的情况下实时更新类文件,快速迭代和即时测试,提高开发效率。
  2. 提高开发效率:减少了重启应用程序的时间,使开发者能够更专注于代码的编写和改进。
  3. 支持敏捷开发:在敏捷开发模式下,频繁的代码更改和测试是常态。hotswap-classloader 的动态加载能力使这一过程更加流畅和高效。

结合使用 hotswap-classloader 和 tio-boot 不仅提升了开发效率,还增强了网络应用开发的灵活性和便利性,对于希望快速迭代和改进其网络应用的开发团队而言,这是一个极具价值的组合。

整合热加载 ​

整合热加载的步骤 ​

以下步骤将指导您如何将 hotswap-classloader 与 tio-boot 进行整合,实现热加载功能:

1. 添加依赖 ​

在项目的 pom.xml 文件中添加 hotswap-classloader 的依赖:

xml
<dependency>
  <groupId>nexus.io</groupId>
  <artifactId>hotswap-classloader</artifactId>
  <!-- 依赖地址:https://central.sonatype.com/artifact/nexus.io/hotswap-classloader -->
  <version>${hotswap-classloader.version}</version>
</dependency>

请确保将 ${hotswap-classloader.version} 替换为实际的版本号。

2. 使用 TioApplicationWrapper 启动服务 ​

在启动类中使用 TioApplicationWrapper 来启动应用,以启用热加载功能:

java
import nexus.io.hotswap.wrapper.tio.boot.TioApplicationWrapper;
import nexus.io.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/,则认为是开发环境,开启热加载;否则,认为是生产环境,不启用热加载。

4. 测试热加载效果 ​

  • 在 Eclipse IDE 中:只需保存文件即可触发热加载,实时查看效果。
  • 在 IntelliJ IDEA 环境中:需要在运行时手动编译(Build -> Recompile)文件才能看到效果。

5. 日志示例 ​

以下是启动和热加载过程中的日志示例:

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:nexus.io.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:nexus.io.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:nexus.io.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:nexus.io.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:nexus.io.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:nexus.io.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 (^_^)

6. 关闭文件监听(可选) ​

默认开启 class 文件监听,保存文件后自动重启并热加载。控制台也可以输入 r 后回车手动重启。

如果只希望手动重启,可以关闭文件监听:

properties
hotswap.watch.file.enabled=false

说明:

  1. 默认值是 true(等价于 EnvUtils.getBoolean("hotswap.watch.file.enabled", true))。
  2. 设置为 false 后,不再监听文件变化。
  3. 设置为 false 后,控制台仍会提示并支持 r + Enter 手动重启。

Tio-boot 启动示例:

java
import nexus.io.annotation.AComponentScan;
import nexus.io.diamond.boradcast.config.AdminAppConfig;
import nexus.io.hotswap.wrapper.tio.boot.TioApplicationWrapper;
import nexus.io.tio.utils.environment.EnvUtils;

@AComponentScan("nexus.io.diamond.boradcast.controller")
public class BoradcastAdminApp {
  public static void main(String[] args) {
    EnvUtils.load(args);
    long start = System.currentTimeMillis();
    AdminAppConfig config = new AdminAppConfig();
    TioApplicationWrapper.run(BoradcastAdminApp.class, config, args);
    long end = System.currentTimeMillis();
    System.out.println((end - start) + "(ms)");
  }
}

必须先执行 EnvUtils.load(args) 读取配置文件,否则加载不到 hotswap.watch.file.enabled 配置。

热加载的实现原理 ​

热加载功能的实现依赖于以下几个关键组件:

1. 动态类加载 ​

hotswap-classloader 通过自定义 ClassLoader 实现了在运行时动态加载和替换类的功能。当检测到类文件发生变化时,hotswap-classloader 会创建一个新的 ClassLoader 来加载更新后的类定义,并使用反射机制重新初始化应用上下文。这确保了新版本的类能够在不重启 JVM 的情况下被应用。

2. 文件监控 ​

利用 Java NIO 的 WatchService实时监控类文件的变动。当检测到文件修改事件时,触发热加载流程,确保类的更新能够及时应用。

3. 自定义线程池 ​

由于 AIO 的默认线程池不支持热加载,为避免线程池中的线程持有旧的 ClassLoader 导致类无法卸载的问题,hotswap-classloader 使用了自定义的线程池 tio-group。在热加载时,旧的线程池会被安全地关闭,新的线程池会随新的 ClassLoader 一起创建,确保类的正确加载和卸载。

4. 生产环境优化 ​

在生产环境中,TioApplicationWrapper 会自动检测运行环境,避免加载热加载相关的功能,从而不引入额外的性能损耗。生产环境仍然使用 AIO 的默认线程池,确保应用的性能和稳定性。

更多使用说明 ​

有关热加载的更多使用细节和高级配置,请参考官方文档:

hotswap-classloader 官方文档

部署注意事项 ​

在将应用部署到生产环境时,需要注意以下事项,以确保应用的稳定性和性能:

  • 禁用热加载:在生产环境中,不要在程序参数中添加 --mode=dev。TioApplicationWrapper 会调用 TioApplication 启动项目,不会启用热加载功能。
  • 性能考虑:理论上,生产环境不会有额外的性能损耗,因为 TioApplicationWrapper 仅在启动时判断了运行模式和 classpath 路径。

增加对 fastjson2 的支持 ​

在启用了 TioApplicationWrapper 后,部分开发者可能会遇到如下错误:

java.lang.ClassCastException: nexus.io.maxkb.vo.MaxKbApplicationNoReferencesSetting cannot be cast to nexus.io.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 nexus.io.tio.utils.json.FastJson2.parse(FastJson2.java:42)
    at nexus.io.tio.utils.json.MixedJson.parse(MixedJson.java:28)
    at nexus.io.tio.utils.json.JsonUtils.parse(JsonUtils.java:20)

问题描述 ​

上述 ClassCastException 异常是由于同一个类 nexus.io.maxkb.vo.MaxKbApplicationNoReferencesSetting 被不同的类加载器加载了多次。尽管类的全限定名相同,但如果由不同的类加载器加载,JVM 会将它们视为不同的类,从而导致类型转换失败。

原因分析 ​

  1. 类重复加载:

    启用了 hotswap-classloader 后,每次热加载都会创建一个新的 ClassLoader 来加载类定义。这样,同名的类在不同的 ClassLoader 下被加载,JVM 认为它们是不同的类,导致 ClassCastException。

  2. 类加载器的影响:

    类加载器直接影响类的唯一性。Java 遵循双亲委派模型,即子类加载器在加载类时会首先委托给父类加载器。如果同一个类由不同的加载器加载,即使类名相同,JVM 也会将它们视为不同的类。

解决方案 ​

为了避免上述问题,需要确保特定的类由系统类加载器(父类加载器)加载,而不是由 hotswap-classloader 重复加载。具体步骤如下:

1. 指定系统类加载器加载特定类 ​

在启动类中,配置 HotSwapResolver 以指定需要由系统类加载器加载的类的包前缀。例如:

java
package nexus.io.maxkb;

import nexus.io.annotation.AComponentScan;
import nexus.io.hotswap.watcher.HotSwapResolver;
import nexus.io.hotswap.wrapper.tio.boot.TioApplicationWrapper;

@AComponentScan
public class MaxKbApp {
    public static void main(String[] args) {
        long start = System.currentTimeMillis();
        // 指定需要由系统类加载器加载的类的包名前缀
        HotSwapResolver.addSystemClassPrefix("nexus.io.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("nexus.io.maxkb.vo.");:

    该配置的作用是告知热加载器,对于包名以 nexus.io.maxkb.vo. 开头的类,不进行热加载,而是由系统类加载器加载。这样可以确保这些类只被加载一次,避免因重复加载导致的 ClassCastException。

总结 ​

  • 避免类重复加载:确保关键类仅由一个类加载器加载,尤其是涉及类型转换和序列化的类,避免由多个类加载器加载同名类。
  • 合理配置热加载器:在使用热加载功能时,仔细配置哪些类需要热加载,哪些类不需要。对于不需要热加载的类,尤其是关键类,应该由系统类加载器加载。
  • 理解类加载机制:深入理解 Java 的类加载机制和双亲委派模型,有助于在开发过程中避免类似的问题。

通过以上方法,您可以解决 ClassCastException 问题,确保应用程序在启用热加载功能后正常运行。

总结 ​

通过整合 hotswap-classloader,开发者可以在不重启应用的情况下实时更新代码,大大提高了开发效率。同时,得益于对生产环境的优化处理,热加载机制不会对线上应用的性能和稳定性造成影响。

使用建议:

  • 在开发环境中,如 Eclipse IDE,保持文件保存即可测试加载效果。
  • 在 IntelliJ IDEA 环境中,需要在运行时手动编译(Build -> Recompile)文件才能看到热加载效果。

正确配置和使用 hotswap-classloader 与 tio-boot 的整合,将为您的 Java 网络应用开发带来显著的效率提升和更好的开发体验。

参考资料 ​

MIT Licensed