热更新
本文是 Zeze 热更新(Hot Reload) 的完整参考——阐述其模块级不停服更新的原理、核心组件、发布流程、状态迁移与回滚机制,以及工程上的注意事项,供设计热更方案、排查热更问题时查阅。事务与配置基础见 事务、配置参考,上线前的热更验收见 上线清单。
Zeze 提供完整的模块级热更新能力:不停服更新业务逻辑、基于 Java Agent 的类重定义能力和自定义 ClassLoader 支持模块独立升级、状态迁移与回滚。整套机制以「一个 jar 一个热更模块」为边界,模块之间相互隔离,同一模块的不同版本可在升级窗口内共存。
核心组件一览
Section titled “核心组件一览”| 组件 | 所在包 | 职责 |
|---|---|---|
ClassReloader | Zeze.Util | Java Agent 入口类,提供运行时类重定义的底层能力 |
HotModule | Zeze.Hot | 自定义 ClassLoader,每个热更模块一个实例,加载该模块 jar 内的所有 class(接口除外) |
HotService | Zeze.Hot | 热更模块生命周期接口:start / stop / upgrade |
HotManager | Zeze.Hot | 热更管理器,协调整个加载、卸载、升级、回滚流程 |
HotAgent | Zeze.Hot | 热更客户端,连接 HotDistribute 上传发布文件 |
HotDistribute | Zeze.Hot | 热更发布控制台,管理发布状态机和文件传输 |
辅助组件:HotModuleContext(跨模块引用的版本化上下文)、HotUpgrade / HotBeanFactory(缓存刷新)、HotGuard(热更期间与 Raft 等操作的并发保护)、HotTransaction(安装过程的事务包装)。
ClassReloader:类重定义底层能力
Section titled “ClassReloader:类重定义底层能力”Zeze.Util.ClassReloader 使用 Java Instrumentation API 进行运行时类重定义。它支持两种加载方式:
1. -javaagent 参数启动加载
在 MANIFEST.MF 中声明(Premain-Class 可换成 Agent-Class),并以启动参数注入:
// Premain-Class: Zeze.Util.ClassReloader// Can-Redefine-Classes: truejava -javaagent:zeze.jar ......premain(args, inst) 将注入的 Instrumentation 保存到静态字段 inst。
2. 运行时自动 attach
调用 ClassReloader.getInst() 时若发现 inst 为 null,则自动创建一个临时 agent jar 并注入当前 JVM:
public static Instrumentation getInst() { return inst != null ? inst : loadAgent();}loadAgent() 的关键步骤:写一个临时 agent.jar(包含 Agent-Class、Premain-Class、Can-Redefine-Classes、Can-Retransform-Classes),获取当前 JVM pid(ManagementFactory.getRuntimeMXBean().getName() 取 @ 之前的部分),再用 VirtualMachine.attach(pid).loadAgent(jarPath) 注入:
String pid = nameOfRunningVM.substring(0, nameOfRunningVM.indexOf('@'));int r = Runtime.getRuntime().exec(new String[]{"java", "-cp", path, fullClassName, pid, path}).waitFor();类重定义 API
| 方法 | 说明 |
|---|---|
reloadClass(byte[] classData, ClassLoader classLoader) | 热更单个 class |
reloadClasses(Collection<byte[]> classDatas, ClassLoader classLoader) | 批量热更多个 class |
reloadClasses(ZipFile zipFile) | 从 zip/jar 批量加载,自动跳过同版本,避免不必要的重定义 |
getClassPathFromData(byte[] classData) | 直接解析 class 二进制的常量池获取完整类名,不依赖 ClassLoader |
reloadClasses(ZipFile, classLoader, log) 重载会逐项与 classLoader.getResourceAsStream(name) 的现有字节比对,一致则跳过(buf0.equals(buf1)),从而避免不必要的重定义。getClassPathFromData 自行解析 .class 常量池(CONSTANT_Class、CONSTANT_Utf8 等)得到类名,因此重定义前不需要先通过 ClassLoader 找到类。
HotModule:模块级 ClassLoader
Section titled “HotModule:模块级 ClassLoader”HotModule extends ClassLoader implements Closeable。每个热更模块一个实例,以一个 jar 文件为边界,加载其中所有 class(接口除外,接口由父 ClassLoader 加载)。
- 模块隔离:模块间相互隔离;同一模块不同版本可在升级窗口内共存。
- 入口类命名:
{namespace}.Module{lastPart},其中last(namespace)取 namespace 最后一段。例如 namespace 为MySolution.MyName时,入口类为MySolution.MyName.ModuleMyName:
var moduleClassName = namespace + ".Module" + last(namespace);this.moduleClass = loadClass(moduleClassName);- 加载规则:重写
findClass/loadClass,从该模块的JarFile读取class字节后defineClass。接口(.interface.jar)由HotManager这一层负责装载,不会被HotModule替换。
创建 HotModule → 注册 HotManager → start() → startLast() → 运行中 │ upgrade(newModule) ▲ stopBefore() ─┘ stop()| 阶段 | 做什么 |
|---|---|
start() | 初始化资源、注册协议数据表;重新为 contexts 设置当前模块引用 |
startLast() | 依赖就绪后的二次初始化(在所有模块 start 之后再统一调用) |
stopBefore() | 停机前调用,此时应用环境完整,可做依赖性的预清理 |
stop() | 执行 UnRegister(注销协议)、释放资源、关闭 JarFile。不清除本地进程状态——有状态需保留供 upgrade 读取 |
upgrade(HotModule old) | 把旧模块的 contexts 迁移到新模块,并调用 service.upgrade(old.service) 迁移状态 |
版本化 Context
Section titled “版本化 Context”HotModuleContext<T extends HotService> 用于管理外部模块对本模块服务的引用。升级时 context 自动迁移到新模块:
public void upgrade(HotModule old) throws Exception { contexts.putAll(old.contexts); // 继承旧模块的全部 context for (var context : contexts.values()) context.setModule(this); // 把 context 指向新模块 service.upgrade(old.service); // 业务状态迁移}stop 时(实为 disable())会把每个 context 的 module 置为 null,防止外部模块持有过期引用:
void disable() { for (var context : contexts.values()) context.setModule(null);}获取引用:HotModule.getContext(Class<T>),内部用 ConcurrentHashMap 懒初始化。
HotService:生命周期接口
Section titled “HotService:生命周期接口”public interface HotService { void start() throws Exception; default void startLast() throws Exception {} default void stopBefore() throws Exception {} void stop() throws Exception; void upgrade(HotService old) throws Exception;}| 方法 | 语义要点 |
|---|---|
start() | 初始化资源、注册协议数据表 |
startLast() | 默认空实现;start 全部完成后的二次初始化 |
stopBefore() | 默认空实现;停机前、应用环境完整时调用 |
stop() | 必须保留有状态的数据,后面 upgrade 时由新实例读取。负责 UnRegister 与释放资源 |
upgrade(HotService old) | 从旧实例迁移状态到新实例 |
stop与upgrade是配合使用的:stop只断开外部接入并保留状态,upgrade把保留的状态搬过来。实现HotService时务必遵守这一约定。
HotManager:安装与升级编排
Section titled “HotManager:安装与升级编排”HotManager extends ClassLoader,全局一般一个实例,负责装载所有模块接口、监视发布目录、协调升级与回滚。关键字段:
private final String workingDir; // 工作目录private final String distributeDir; // 发布文件存放子目录private final FewModifySortedMap<String, HotModule> modules; // namespace -> HotModuleprivate final ReentrantReadWriteLock hotLock; // 热更读写锁private final ConcurrentHashSet<HotUpgrade> hotUpgrades; // 缓存刷新private final ConcurrentHashSet<HotBeanFactory> hotBeanFactories;private final DistributeManager distributeManager;private final HotDistribute hotDistribute;发布目录监视:start() 后用调度线程周期性执行 tryDistribute(false):
Task.getScheduledThreadPool().scheduleAtFixedRate( () -> tryDistribute(false), 10000, 10000, TimeUnit.MILLISECONDS);Task.hotGuard = this::enterReadLock; // 热更期间保护运行中的操作tryDistribute 检查 {distributeDir}/ready 标记文件是否存在;存在则读取其中的模块列表并触发 installReadies(atomicAll)。
模块升级步骤(install 流程)
Section titled “模块升级步骤(install 流程)”install(namespaces, atomicAll) 是核心,步骤如下:
- 锁外执行
stopBefore:对每个待升级的现有模块调用stopBefore()。 - 进入写锁:执行
checkpointRun()先持久化现有数据;从modules中移除待升级 namespace。 - 逆序
stop:从后往前对旧模块调用stop()(UnRegister、释放资源、保留状态)。若中途异常,调用recoverModules恢复。 - 加载 Schemas:
loadSchemas()从__hot_schemas__{SolutionName}.jar加载并切换 schemas(旧 schemas 保存用于回滚)。 - 安装新模块:为每个 namespace 从
{distributeDir}/{namespace}.jar与{namespace}.interface.jar创建新HotModule并put进modules;接口 jar 备份旧文件以便回滚。 createModuleInstance:批量装载 redirect 模块,创建IModule实例。__install_alter__():在切换后的 schemas 下执行结构变更。upgrade(old):对已存在的旧模块,调用module.upgrade(exist)把状态迁移到新模块(事务外运行)。stopInternal旧模块:内部停止、不可恢复——清理contexts、触发stopEvents。- 内部
upgrade:对hotUpgrades(缓存了其他模块数据)调用HotUpgrade.upgrade(retreatFunc);对HotBeanFactory刷新 Bean 类型注册;对__get_upgrade_memory_table__()执行内存表升级。 start():按序启动新模块,启动失败的模块会被停止并注销。sendCommitResultAndWaitCommit2:原子发布时通知发布控制台最终提交。startLast():最后统一调用startLast()(忽略错误)。
关键设计:步骤 8 之后进入「不能出错阶段」——任何异常将
Runtime.getRuntime().halt(111222)强制停机,因为此时数据结构变更已部分落地,无法回滚。这也是为什么热更要充分测试。
install 用 HotTransaction 包装,whileRollback 注册 MainRollbackAction:
- 恢复旧 schemas:
zeze.__upgrade_schemas__(oldSchemas) - 恢复被停止的模块:
recoverModules(exists, -1)(重新Register、start、加回modules) - 重新 alter:
zeze.__install_alter__() - 清理内存表升级记录:
zeze.__get_upgrade_memory_table__().clear()
模块文件层面的回滚:安装前把旧 module.jar / interface.jar 重命名为 .backup,whileRollback 时还原,whileCommit 时删除备份。
发布流程(HotAgent ⇄ HotDistribute)
Section titled “发布流程(HotAgent ⇄ HotDistribute)”完整发布是一个带状态机的文件传输过程:
1. HotAgent 连接 HotDistribute2. PrepareDistribute 进入准备,锁定发布通道3. 文件传输 openFile → appendFile(多次)→ closeFile(MD5 校验)4. TryDistribute HotManager 执行 tryDistribute,触发模块升级5. Commit / Commit2 确认成功;失败则 TryRollback 回滚- 准备阶段:
PrepareDistribute锁定发布通道,防止并发发布冲突。 - 文件传输:
openFile打开目标文件,appendFile分块上传(多次),closeFile做 MD5 校验确认完整。 - 触发升级:
HotManager.tryDistribute(true)在原子模式下执行installReadies(atomicAll),并在关键节点通过hotDistribute.sendTryDistributeResultAndWaitCommit/sendCommitResultAndWaitCommit2与发布控制台同步状态。 - 提交/回滚:成功走
Commit/Commit2;失败走TryRollback,HotManager把distributes目录里的文件renameDistributes到backup/{时间戳}子目录。
模块识别规则:loadExistDistributes 扫描 distributeDir 下的 .jar,要求成对出现 {namespace}.jar 与 {namespace}.interface.jar 才视为 ready;可选的 start.order.txt 指定加载顺序。
热更新相关配置位于 <zeze> 根元素:
| 属性 | 默认值 | 说明 |
|---|---|---|
HotWorkingDir | ""(空串,即当前目录) | 工作目录,运行态的 modules/ 与 interfaces/ 存放于此 |
HotDistributeDir | distributes | 发布文件存放子目录名 |
<zeze HotWorkingDir="./hot" HotDistributeDir="distributes" ...></zeze>HotManager 构造时会校验目录关系:distributeDir 不能是 workingDir/interfaces/、workingDir/modules/ 的子目录,二者也不能互相包含。
- 接口不能修改。接口由父
ClassLoader(HotManager)加载,热更不会替换接口 class。新增接口方法会破坏兼容。 upgrade处理新旧数据兼容。Bean 结构变化时需实现HotUpgrade.upgrade的retreatFunc——当缓存中仍持有旧模块创建的 Bean 时,通过序列化/反序列化把它「撤退」为新模块的类(HotManager.retreat会用新模块的ClassLoader重新构造一个同结构的 Bean)。- BeanFactory 注册。模块自定义 Bean 类型须
BeanFactory.register注册;由于持久化保存的是类名,升级后类名变化会影响反序列化,热更时框架会通过BeanFactory.resetHot重建注册表。 stop事件。HotModule.stopEvents用于通知依赖方(如Online)清理旧模块引用。注册本地数据setLocalBean会自动注册对应的 stop 事件。- 不建议频繁热更。每次热更都会创建新的
ClassLoader和JarFile句柄,频繁热更可能导致 Metaspace 泄漏(旧模块类未及时卸载)。建议低峰期批量热更。 - Raft 线程安全。热更过程通过
hotGuard保护(Task.hotGuard = this::enterReadLock,install持写锁),确保与运行中的 Raft 等操作不冲突。