Java 9+ 的 exports 指令不支持热替换,它仅声明包可见性,与类动态加载无关;热替换需依赖模块封装边界+自定义类加载器+接口契约稳定实现,核心是换实现不换接口。

Java 9+ 的 exports 指令本身**不支持热替换**,它只是模块系统中用于控制包可见性的编译期/运行期访问策略声明,和类的动态加载、卸载、替换没有直接关系。想通过 exports 实现“热替换”,属于对模块指令的误用——它解决的是“谁能看到我的包”,而不是“如何换掉正在跑的类”。
真正起作用的是模块化 + 类加载器隔离
要在模块化(JPMS)环境中安全实现模块内实现类的热替换,关键不是 exports,而是利用模块系统的封装边界配合自定义类加载器,让新旧实现互不干扰。此时 exports 的作用仅限于:确保你对外暴露的接口包(如 com.example.service)被正确导出,供其他模块稳定调用。
例如,假设你有一个服务模块:
-
接口定义在
com.example.api包(导出给所有模块):module com.example.service.api { exports com.example.api; } -
具体实现放在
com.example.service.impl包(不导出,仅本模块内使用) - 主程序或插件管理器通过
ServiceLoader<myservice></myservice>或工厂类获取实例,且只依赖com.example.api.MyService接口
热替换落地靠三步:接口稳、加载器新、实例切换
保持变量/接口稳定,本质是保证调用方代码完全不用改——只换实现,不碰契约。实战要点如下:
- 接口必须定义在独立、导出的 API 模块中,且禁止修改方法签名、返回类型、异常列表;字段若为 public static final 常量可保留,但避免暴露可变状态
-
每次热替换都创建全新类加载器(不能复用),加载新版本的
impl类;旧加载器及其加载的类可被 GC 回收(前提是无强引用) -
切换时只更新实现对象引用,例如把单例工厂中的
currentImpl字段原子更新为新实例;所有上层代码仍调用同一接口方法,底层已指向新版逻辑
为什么 exports 不能“做热替换”?
exports 是静态声明,写在 module-info.java 里,编译后固化在模块描述符中。JVM 启动后无法运行时修改某个模块是否导出某包——这不属于模块系统设计目标。试图用它控制“哪个实现生效”,就像想用门牌号来开关房间里的灯,方向错了。
真正需要动态控制的是:
• 类字节码从哪来(文件路径 / 网络 / 内存)
• 由哪个 ClassLoader 加载
• 新旧实例如何解耦与交接
这些都发生在模块加载之后,与 exports 无关。
一个轻量可行的结构示意
假设你要热更订单处理器:
-
order-api模块:导出com.example.order,含OrderProcessor接口 -
order-impl-v1和order-impl-v2:各自打包为 JAR,不声明模块(或声明但不导出任何包),仅提供实现类 - 主模块持有
AtomicReference<orderprocessor></orderprocessor>,启动时用自定义加载器加载 v1;检测到新 JAR 后,新建加载器加载 v2 类,构建新实例,原子替换引用 - 所有业务代码始终写
processor.process(order),完全感知不到版本变化

















