JRebel热重载会导致Spring注解如@Scheduled、@EventListener失效,因类重载后Class实例变更而容器未同步更新注册;可通过排除关键包、改用DevTools重启、运行时动态注册等策略缓解。

这个问题很实际——JRebel 频繁重载类,确实会让基于运行期反射的注解逻辑“突然失灵”,比如 Spring 的 @Scheduled、@EventListener、自定义注解驱动的 AOP 切面、甚至手动调用 clazz.getDeclaredAnnotations() 获取的元数据都可能变为空或错乱。这不是 bug,而是热重载与 JVM 类生命周期、Spring 容器注册机制之间天然存在的张力。
核心原因:重载破坏了“类身份”与“容器注册”的一致性
JRebel 替换的是 class 字节码,但不会自动重建 Spring 容器中对该类的 Bean 定义、代理对象、事件监听器注册、定时任务调度器中的 Method 对象引用。一旦类被 reload,原 Class 实例被卸载,新 Class 实例虽同名,但 == 不成立,hashCode() 不同,所有基于旧 Class 引用的反射缓存、代理绑定、调度注册全部失效。
关键场景与对应表现
-
Scheduled 方法消失:修改带
@Scheduled的类后,定时任务不再触发,日志里也看不到调度器注册该方法的痕迹 -
@EventListener 不再响应:事件发布后,监听方法完全不执行,debug 发现
ApplicationEventMulticaster的监听器列表里已无该实例 -
自定义注解增强失效:比如你用
@Audit触发日志审计,重载后切面没织入,方法执行时 audit 日志静默 -
反射获取注解返回 null:代码中写
method.getAnnotation(MyAnn.class),重载后始终为 null,哪怕源码里明明写着
实用缓解策略(非根治,但足够稳定)
-
限制重载范围:在 JRebel 配置中禁用对含核心注解类的自动 reload。例如,在
rebel.xml中用<exclude>排除**/config/**、**/scheduler/**、**/listener/**等包路径,让这些类只在重启时加载 -
改用 Spring Boot DevTools + Restart 方案:对注解敏感模块,主动放弃 JRebel,改用 DevTools 的轻量级 restart(配合
spring.devtools.restart.additional-paths缩小扫描范围),它会重建整个 ApplicationContext,保证注解注册完整 -
延迟初始化 + 运行时注册兜底:把注解解析逻辑从类加载期移到运行期。例如监听
ContextRefreshedEvent,遍历所有 Bean,重新扫描并注册@Scheduled方法;或用DefaultListableBeanFactory.registerSingleton()动态补注册 -
避免在重载热点类上放关键注解:把
@Scheduled、@EventListener拆到独立的、极少改动的配置类中;业务逻辑放 Controller/Service,注解载体保持稳定
开发阶段可验证的检查点
每次怀疑注解失效时,不要只看代码,直接查运行态:
- 在 debug 模式下,执行
applicationContext.getBeanNamesForAnnotation(Scheduled.class),确认返回非空 - 调用
scheduledTaskRegistrar.getScheduledTasks()(需暴露该 bean),看任务列表是否包含你的方法 - 打印
YourClass.class.getClassLoader(),观察重载前后是否为HotSwapClassLoader实例——如果是,就说明当前确实是 JRebel 加载的“新类”,原有注册大概率已断连

















