20260506-Arthas 真的可以“在线发 Jar 包”吗?一次线上热修复实践记录
1. 起因:运维说可以用 Arthas 发包
最近线上遇到一个发票明细匹配状态异常的问题。
缺陷信息如下:
- • 缺陷名称:发票明细
match_data_blank赋值逻辑错误 - • 缺陷位置:
AdInvoicesStatsService#update - • 代码位置:
AdInvoicesStatsService.java:L239-L247 - • 影响现象:
amzn_ad_invoices_stats_detail_match_relationship 表中已经插入了有效关联记录:
is_delete = 0
但是 amzn_ad_invoices_stats_detail.match_data_blank 仍然被标记为:
match_data_blank = 1
也就是说,数据明明已经存在关联关系,却仍然被系统判断为“未关联”。
这个字段又会影响主表的 all_match 计算,最终导致主表匹配状态异常。
代码修完之后,我原本以为要重新打包、发布、重启服务。但运维说了一句:
这个可以用 Arthas 发到线上。
一开始我没太理解。
我的第一反应是:
Arthas 不是线上诊断工具吗?还能发 Jar 包?发完不用重启?Spring Bean 不需要重新初始化吗?
于是就有了这次实践。
2. 先澄清:Arthas 不是发布整个 Jar 包
这里运维说的“发包”,严格来说不是把整个 Spring Boot Jar 包重新上传、替换、重启。
它更准确的含义是:
用 Arthas 对线上 JVM 中某个已经加载的类进行热替换。
也就是说,它不是替换整个应用,而是替换某一个 .class 文件。
比如这次的问题只发生在:
AdInvoicesStatsService#update
而且缺陷只是方法内部逻辑错误,例如某个判断条件、某个赋值逻辑写错了。
这种场景非常适合 Arthas 热更新。
3. 为什么不需要重启?
普通发布流程是:
修改代码↓打 Jar 包↓上传服务器↓停止旧服务↓启动新服务↓Spring 重新扫描 Bean↓Bean 重新初始化
但 Arthas 的热更新不是走这个流程。
它是直接连接到正在运行的 JVM 进程,然后通过 JVM 提供的 Instrumentation 能力,把新的 .class 字节码替换到 JVM 内存中。
也就是说:
线上 JVM 不停Spring 容器不重启Bean 对象不重新创建只替换类的方法字节码
所以后续再调用这个方法时,执行的就是新的方法逻辑。
4. Spring 初始化怎么办?
这个问题一开始我也很困惑。
比如 AdInvoicesStatsService 是 Spring 管理的 Bean,它可能有:
@Service@Transactional@Resource@Autowired
那 Arthas 热更新之后,Spring 会重新扫描吗?
Bean 会重新实例化吗?
事务代理会失效吗?
答案是:不会重新初始化,也不需要重新初始化。
因为 Arthas 替换的是类的字节码,不是重新创建 Spring Bean。
假设 Spring 容器中已经有一个 Bean:
adInvoicesStatsService
这个 Bean 对象依然是原来的对象,引用也没有变。
但是它所属的类:
AdInvoicesStatsService
其中某个方法的字节码被 JVM 替换了。
所以后续调用:
adInvoicesStatsService.update(...)
执行的就是替换后的新逻辑。
Spring 容器本身并不知道这件事,也不需要知道。
前提是:
你只修改方法内部逻辑,不修改类结构。
5. 这次缺陷是否适合用 Arthas 修复?
这次问题的本质是:
已经存在有效关联记录,但
match_data_blank仍然被错误赋值为1。
缺陷集中在:
AdInvoicesStatsService#update
也就是一个方法内部的业务判断和赋值逻辑错误。
这种修改一般属于:
修改 if 判断修改变量赋值修改查询结果判断修改状态字段计算
没有涉及:
新增字段新增方法修改方法参数修改返回值修改类继承结构新增 Spring Bean修改注解扫描逻辑
所以它是适合 Arthas 热修复的。
6. 实际操作流程
下面以 AdInvoicesStatsService 为例,整理一下完整操作流程。
假设类全路径为:
com.xxx.service.AdInvoicesStatsService
实际操作时需要替换成项目中的真实包名。
第一步:连接线上 JVM
登录线上服务器后,启动 Arthas:
java -jar arthas-boot.jar
然后选择目标 Java 进程。
进入 Arthas 控制台后,大概是这种形式:
[arthas@12345]$
第二步:查看目标类是否已经加载
sc -d com.xxx.service.AdInvoicesStatsService
重点关注两个信息:
classLoaderHashcode-source
classLoaderHash 后面编译时会用到,避免 classloader 不一致导致编译或加载失败。
第三步:反编译线上正在运行的类
jad --source-only com.xxx.service.AdInvoicesStatsService > /tmp/AdInvoicesStatsService.java
这一步非常关键。
不要直接拿本地代码就改,因为线上代码可能和本地分支不完全一致。
最稳妥的方式是:
基于线上 JVM 中真实运行的字节码反编译出来的源码进行修改。
第四步:备份原始文件
先保留一份原始源码:
cp /tmp/AdInvoicesStatsService.java /tmp/AdInvoicesStatsService.java.bak
如果已经有原始 .class 文件,也建议备份。
线上热修复必须留后路。
第五步:修改缺陷代码
编辑反编译出来的源码:
vim /tmp/AdInvoicesStatsService.java
找到 update 方法中对应的逻辑。
这次要修的是:
match_data_blank 赋值逻辑错误
目标是保证:
当 amzn_ad_invoices_stats_detail_match_relationship 已存在有效记录:
is_delete = 0
对应明细不应该继续标记为:
match_data_blank = 1
而应该修正为:
match_data_blank = 0
也就是:
有有效关联关系,则不是空匹配。
这里只改方法内部逻辑,不改类结构。
第六步:使用 mc 编译源码
先拿到 classloader:
sc -d com.xxx.service.AdInvoicesStatsService | grep classLoaderHash
假设输出为:
classLoaderHash 3d4eac69
然后使用 Arthas 的 mc 命令编译:
mc -c 3d4eac69 /tmp/AdInvoicesStatsService.java -d /tmp
编译成功后,会生成类似文件:
/tmp/com/xxx/service/AdInvoicesStatsService.class
如果这里编译失败,通常是因为依赖类路径不完整。
这种情况下可以在本地完整项目环境中编译出 .class 文件,再上传到服务器。
第七步:使用 redefine 热替换
执行:
redefine /tmp/com/xxx/service/AdInvoicesStatsService.class
如果成功,会看到类似结果:
redefine success, size: 1
这就说明 JVM 内存中的 AdInvoicesStatsService 类已经被替换。
此时不需要重启 Spring Boot 服务。
第八步:验证修复结果
可以用业务数据验证,也可以借助 Arthas 观察方法调用。
例如观察 update 方法:
watch com.xxx.service.AdInvoicesStatsService update '{params, returnObj, throwExp}' -x 3 -n 5
然后触发一次发票明细更新逻辑,确认:
amzn_ad_invoices_stats_detail_match_relationship
存在有效关联记录时:
is_delete = 0
对应明细表:
amzn_ad_invoices_stats_detail.match_data_blank
不再错误保持为:
1
而是被正确修正为:
0
最终主表 all_match 计算也恢复正常。
7. Arthas 热更新的核心原理
Arthas 的热更新能力,本质上依赖 JVM 的 Instrumentation 机制。
大致过程是:
Arthas attach 到目标 JVM↓向目标 JVM 注入 Agent↓获取 JVM 中已经加载的类↓加载新的 .class 字节码↓通过 redefine / retransform 替换类定义↓后续方法调用走新逻辑
它不是修改磁盘上的 Jar 包。
它修改的是 JVM 内存中已经加载的类定义。
所以它的特点是:
不重启服务不重新加载 Spring 容器不重新创建 Bean只影响当前 JVM 进程
8. 需要特别注意的限制
Arthas 热更新不是万能的。
它适合修“小而明确”的线上逻辑问题,不适合做完整发版。
不能修改类结构
一般不能做这些操作:
新增字段删除字段新增方法删除方法修改方法参数修改方法返回值修改类名修改继承关系修改实现接口
否则 JVM 很可能拒绝替换。
不适合改 Spring 初始化逻辑
比如下面这些修改不适合通过 Arthas 热更新:
新增 @Service新增 @Component新增 Mapper 方法新增配置类修改 @Bean 初始化逻辑修改构造方法初始化逻辑修改 static 静态初始化逻辑
因为 Spring 容器已经启动完成了。
Arthas 不会让 Spring 重新扫描 Bean,也不会重新执行完整初始化流程。
构造方法和 static 逻辑要谨慎
即使你修改了构造方法或 static 代码块,也不代表它会重新执行。
因为这些逻辑通常在类加载或对象创建时已经执行过了。
线上已有 Bean 不会因为 redefine 就重新 new 一次。
重启后热修复会丢失
这是非常重要的一点。
Arthas 替换的是当前 JVM 内存中的类。
如果服务重启,JVM 会重新从磁盘上的 Jar 包加载类。
所以如果磁盘 Jar 包还是旧代码,那么重启后修复会丢失。
因此正确流程应该是:
Arthas 临时热修线上问题↓验证问题修复↓代码合并到 Git↓走正式发布流程↓确保下次重启后仍然是修复后的代码
Arthas 适合应急止血,不应该替代标准发布流程。
9. 这类场景什么时候适合用 Arthas?
适合:
线上紧急 Bug只改某个方法内部逻辑不涉及表结构变更不涉及接口签名变更不涉及 Spring Bean 新增不涉及依赖新增影响范围明确需要快速止血
不适合:
大范围重构新增功能模块新增字段或方法新增依赖修改配置文件修改 Spring 初始化逻辑修改数据库结构需要完整回归测试的复杂发布
10. 最终结论
这次实践之后,我对运维说的“用 Arthas 发包”有了更准确的理解。
它并不是传统意义上的发 Jar 包。
更准确地说,它是:
使用 Arthas 将修复后的
.class文件热替换到线上 JVM 中,让当前服务在不重启的情况下立即执行新逻辑。
对于这次 AdInvoicesStatsService#update 中 match_data_blank 赋值错误的问题,它非常合适。
因为这类问题本质上只是方法内部逻辑错误,不涉及类结构变化,也不需要 Spring 重新初始化。
不过它也有明显边界:
它是热修复,不是正式发布。它是应急止血,不是常规发版。它只影响当前 JVM,重启后会回到 Jar 包里的代码。
所以最稳妥的处理方式是:
线上用 Arthas 快速修复代码仓库同步提交修复后续通过正常 CI/CD 正式发布
这样既能快速恢复线上业务,又能保证后续版本一致性。