拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Arthas ognl 命令实战:在线修改 Java 对象属性而不重启 JVM

线上服务还在跑某个对象的状态就是不对但你又不确定问题具体出在哪一行代码。最头疼的情况是你想改一个内存里已经存在的对象属性但又不能重启 JVM重启意味着缓存预热、连接池重建、用户请求中断这个代价在核心链路上根本承受不起。这时候 Arthas 的ognl命令就是一把手术刀——不用停服务不用改代码重新发布直接在线上把运行中实例的属性值改掉然后接着观察现象。这篇文章会从一个具体的生产问题切入完整讲清楚怎么用 Arthas 配合 OGNL 表达式定位实例、拿到对象引用、修改属性值以及这个过程中你一定会踩到的坑。适合正在用 Arthas 做线上排查的 Java 开发、运维同学也适合刚接触 Arthas 想系统了解ognl命令能力的初学者。我会把原理、表达式拆解、完整实操步骤和避坑经验都写出来让你看完就能直接上手。1. 为什么需要动态修改线上对象属性1.1 线上问题的特殊性和重启的代价先还原一下我遇到过的场景。某个订单处理服务突然出现了大量超时日志里看到业务状态是PAY_SUCCESS但实际资金已经原路退回用户投诉说没收到货。我怀疑是某个订单实例的status字段在内存里被错误地改写了但日志只记录了最终结果根本没有中间过程。这时候摆在我面前的选择是第一在代码里加日志重新发布等下一次触发第二直接连上 Arthas看看现在内存里这个对象到底长什么样把它改回正确值让业务先恢复再慢慢查原因。第一种方案的问题是发布一次要经过构建、测试、审批、灰度至少半小时起步而且这个 bug 不是每次请求都会触发加日志也不一定能抓到现场。第二种方案虽然看起来暴力但它能立刻验证我的假设——如果我把状态字段纠正过来下游重试就成功了那就说明内存状态确实是根因。重启服务在这个场景下是最后的选项。一个运行了大半个月的 JVM 进程堆里有几 GB 的缓存数据有各种线程池的连接状态有分布式锁的持有信息重启意味着所有这些全部归零。特别是在没有做优雅停机的情况下强行kill进程还可能造成数据不一致。所以对线上问题来说能原地修改就绝不重启解决这是排查过程中非常重要的一条原则。1.2 Arthas 与 OGNL 的能力组合Arthas 是阿里开源的一款 Java 诊断工具它可以在不修改代码、不重启服务的情况下对运行中的 JVM 进行在线诊断。它最强大的地方不在于某个单点命令而在于多个命令组合起来的排障链路dashboard看全局状态thread看线程栈watch看方法入参和返回值而ognl命令则能让你直接执行一段 OGNL 表达式读取甚至修改目标对象的内部状态。OGNL全称是 Object-Graph Navigation Language对象图导航语言。它最早是 Struts 2 框架的表达式引擎核心能力就是通过简单的点号语法访问对象的属性、调用对象的方法、遍历集合。Arthas 把 OGNL 嵌入了命令行等于给了你在线操作 JVM 内任意对象的入口。这两个工具组合起来能做的事情远超改一个属性值这么简单。你可以动态调用某个 Spring Bean 的方法可以查看某个 Map 里缓存了哪些数据可以修改某个配置对象的开关字段也可以直接触发一段业务逻辑。换句话说Arthas 的ognl命令是整个排查工具箱里最灵活的瑞士军刀前提是你真的会写 OGNL 表达式。2. 动手前的必备基础OGNL 表达式怎么写2.1 对象图导航的核心语法OGNL 的语法核心就是导航。你有一个对象通过.运算符可以一层层往里走。比如order.user.name表示从order对象拿到user属性再拿到它的name属性。这个过程对应到 Java 代码其实就是order.getUser().getName()的调用。但 OGNL 比反射更聪明的一点是它不必显式调用getter方法。当你写user.name时OGNL 会自动尝试查找getName()方法或者在字段可访问时直接读取字段值。这就意味着即使某个类没有提供getter只要字段本身在堆内存中存在OGNL 也能拿到值。除了属性导航OGNL 还支持方法调用和静态字段访问。方法调用写法是直接追加括号比如order.refreshStatus()就会触发refreshStatus方法的执行。静态字段访问需要用到类全限定名字段名的格式这是 Arthas 里非常高频的用法。比如你想拿到某个常量类里的阈值配置可以写com.example.OrderConstantsMAX_TIMEOUT。集合操作也有对应的语法支持。访问 Map 里的元素用map[key]或map.key访问数组或 List 里的元素用list[0]这样的下标形式。特别是当你需要遍历一批对象时OGNL 提供了.{...}投影语法和.{? ...}选择语法这跟 Java 8 的 Stream 操作有几分神似。2.2 在 Arthas 中执行 ognl 命令的完整格式Arthas 里执行 OGNL 表达式的入口是ognl命令。最简单的用法是直接跟一段表达式ognl com.example.OrderfindById(1001L)这个命令会调用Order类的静态方法findById并打印返回结果。如果想查看返回对象的某个属性可以继续导航ognl com.example.OrderfindById(1001L).status这里有个关键问题Arthas 的ognl命令本质上是在一个指定的 ClassLoader 里执行表达式。如果你的目标对象是被某个 Web 应用的自定义 ClassLoader 加载的直接用上面这种写法大概率会报ClassNotFoundException。解决办法是通过-c参数指定 ClassLoader 的 hash 值。ognl -c 1b6d3586 com.example.OrderfindById(1001L).status要拿到目标 ClassLoader 的 hash 值可以先执行classloader命令它会列出所有 ClassLoader 及其 hash。如果嫌麻烦也可以直接用-c或者更粗暴地直接用ognl命令的--classLoaderClass参数指定类名前缀。实际生产中我一般会先跑一次classloader找到准确的 hash 再操作因为这个值一旦错后面全是白费功夫。另外一个实用参数是-x它控制结果的展开深度。默认情况下如果返回的对象层级很深Arthas 只会打印一层结构。当你想看到对象内部嵌套的属性值时加大展开深度非常有用ognl -x 3 com.example.OrderfindById(1001L)这会把Order对象内部三层嵌套的结构全部打印出来方便你确认要修改的属性到底在哪一层。3. 定位线上实例从哪去拿对象引用3.1 通过静态字段和单例直接获取实例拿到了 OGNL 表达式能力下一个核心问题就是怎么拿到目标对象的引用。这里要分情况讨论第一种也是最幸运的情况——目标对象藏在某个静态字段里或者它是一个单例。比如StatusHolder类有一个静态字段INSTANCE保存了全局状态对象public class StatusHolder { public static final StatusHolder INSTANCE new StatusHolder(); private volatile OrderStatus status OrderStatus.INIT; }那么我可以直接在 Arthas 里修改这个实例的属性ognl -c 1b6d3586 com.example.StatusHolderINSTANCE.status先用这个表达式读取当前值确认对象能访问到之后再执行赋值操作。静态字段访问的格式是类名字段名对于单例类来说这通常是最快的一条路径。Spring 项目里还有一种常见情况Bean 本身是单例的对象的引用在 Spring 容器里。但 Spring 容器内部并不是以静态字段形式保存 Bean 引用的它挂在ApplicationContext的singletonObjects这个 Map 里。这就引出第二种方法。3.2 从 Spring 容器中获取 Bean 实例在 Spring Boot 或传统 Spring 项目中线上对象大多数是 Bean。要在 Arthas 里拿到某个 Bean 的实例常规思路是通过ApplicationContext的getBean方法。但问题来了怎么拿到ApplicationContext的引用比较实用的方式是利用 Spring 工具类。如果你项目里暴露了ApplicationContext的静态持有者比如:Component public class SpringContextUtil implements ApplicationContextAware { private static ApplicationContext applicationContext; // 省略 setter }那就可以这样获取 Beanognl -c 1b6d3586 com.example.SpringContextUtilapplicationContext.getBean(orderService)拿到 Bean 实例之后后续的属性和方法操作就完全在你掌控之下了。这里我踩过一个坑getBean返回的是Object类型如果你直接在表达式后面继续点某个子属性OGNL 编译时可能因为找不到对应的方法报错。解决办法是先做一次类型转换比如#orderService com.example.SpringContextUtilapplicationContext.getBean(orderService), #orderService.status。逗号在 OGNL 里相当于依次执行的意思前面的赋值结果会保存到变量#orderService中后面就能正常导航了。如果项目里没有暴露ApplicationContext的静态持有者还有一招直接在 spring 容器内部查singletonObjects。这个字段定义在DefaultSingletonBeanRegistry里我们可以这样拿到所有单例 Bean 的 Mapognl -c 1b6d3586 #ctx com.example.SpringContextUtilapplicationContext, #ctx.getBeanFactory().singletonObjectssingletonObjects是一个MapString, Object你可以在表达式里用[beanName]的方式直接索引ognl -c 1b6d3586 #ctx com.example.SpringContextUtilapplicationContext, #ctx.getBeanFactory().singletonObjects[orderService].status这个写法比getBean更底层有时能绕开某些代理类包装导致的方法访问异常因为在singletonObjects里存的是原始 Bean 实例除非是循环引用提前暴露的早期引用。3.3 获取类的所有实例并筛选目标有些场景里你会面对更棘手的情况目标对象既不是单例也没有挂在静态字段上。它是一个普通的new出来的对象活跃在某个线程的调用栈里或者作为集合元素挂在某个服务里。这时候要找它就需要另外一个武器——vmtool或instances能力。Arthas 在较新版本中提供了vmtool命令可以通过getInstances动作获取指定类的所有实例。拿我上面的例子来说我可以这样拿到Order类的所有实例vmtool -c 1b6d3586 --action getInstances --className com.example.Order --limit 100拿到实例列表之后结合ognl命令就能筛选出我要的目标。但通常实例数量可能很大全量打印不现实所以我一般会先--limit 10看看实例大概长什么样然后根据某个标志性字段做过滤。比如我只想找orderNo等于A10001的那个Ordervmtool -c 1b6d3586 --action getInstances --className com.example.Order --express instances.length 0 ? instances.{? #this.orderNo A10001}[0] : null在--express参数里instances代表获取到的实例数组{? ...}是 OGNL 的选择语法#this引用当前遍历元素。这条命令会精确返回那个目标订单对象。拿到之后你可以把它的地址记住或者直接在这个表达式后面接属性操作把它改掉。vmtool是定位修改目标最有用的工具没有之一。如果你用的版本比较老没有vmtool可以通过ognl加静态方法的方式凑合。比如找一个服务类里持有订单列表的字段遍历这个列表找到目标对象。但这种方式要求你对业务类结构非常了解不如vmtool直接。所以我建议能升级到带vmtool的版本就别用老版本硬扛。4. 修改属性值的实操流程与表达式拆解4.1 先读后写确认实例唯一性和当前状态我在实际操作中总结了一个铁律任何线上修改动作之前必须先读取确认再执行修改。哪怕你对自己的表达式非常有把握也要先跑一次读取操作。这不只是为了验证表达式正确更是为了确认目标实例只有一个。如果存在多个实例而你只改了一个后续排查依然会被误导。拿前面的StatusHolder举例第一步是读取当前值ognl -c 1b6d3586 com.example.StatusHolderINSTANCE.status假设返回结果是INIT你自己心里清楚正确值应该是RUNNING那接下来就可以执行修改了。修改属性的方式有两种一是调用 setter 方法二是直接字段赋值具体用哪种要看类的封装程度。4.2 通过 setter 方法与直接赋值修改属性调用 setter 方法的写法很直观适合那些字段是私有的、但有公开setter的业务类ognl -c 1b6d3586 com.example.StatusHolderINSTANCE.setStatus(com.example.OrderStatusRUNNING)这条表达式会执行StatusHolder.INSTANCE.setStatus(OrderStatus.RUNNING)。注意OrderStatus是一个枚举这里通过com.example.OrderStatusRUNNING的方式引用枚举常量。枚举常量本质上是静态字段所以这种写法在 OGNL 里是正确的。如果目标类没有setter方法或者字段是public的可以直接用字段赋值ognl -c 1b6d3586 com.example.StatusHolderINSTANCE.status com.example.OrderStatusRUNNING在 OGNL 中就是赋值运算符支持直接修改对象的属性字段。这个能力在 Arthas 中最让我惊喜因为有些内部类的字段根本没有setter用反射写工具类才能改的值现在一行表达式就搞定了。还有一种更精细的操作场景你要修改的不是对象本身的引用而是对象内部某个嵌套对象或者集合里的某个元素。比如order.items[0].price表示订单里第一个商品的价格。OGNL 的导航能力在这里就能发挥出来从根对象一层层延伸下去直接定位到要修改的最深路径。4.3 修改 String、Integer、枚举等不可变类型时的替代方案这里有个重要的坑要提前说明OGNL 表达式里如果你要赋的值是一个字符串需要用引号包裹。比如修改订单备注字段ognl -c 1b6d3586 com.example.OrderServicefindOrder(1001L).remark 线上临时修改这种写法在命令行里直接传参数时引号嵌套经常容易出错。我的经验是要么用单引号包裹整个表达式内部用双引号表示字符串要么在表达式内部用单引号表示字符串外层用双引号传给命令。根据 shell 的解析规则不同需要灵活调整。对于 String、Integer 这类不可变类型修改引用本体没什么难度真正可能出问题的是引用传递的问题。假如某个类有一个String字段被多处共享你在 Arthas 里重新赋值这个字段其实只是把引用指向了一个新的字符串对象并不会影响其他持有旧引用的地方。这个认知很重要否则你会以为改完了对象属性怎么没生效其实是因为有别的变量还指着旧的字符串对象。我在一个线上问题里就遇到过这个陷阱某个配置对象保存了一个maxRetry的Integer值我通过 Arthas 把它从3改成了5。但业务代码里有一段逻辑启动时把maxRetry复制到了另一个局部变量里后续只读局部变量导致我的修改完全不生效。排查到半夜才发现是引用传递的问题。所以要记住Arthas 修改的是对象图的结构不是全局的数据流。4.4 修改嵌套集合与 Map 元素的表达式示例集合对象在业务代码里出现频率极高特别是Map和List。修改集合元素比修改普通属性稍微复杂一点因为你不仅要写对导航路径还要知道集合里存的到底是什么。以下是一些高频操作示例。修改 List 中特定位置的元素ognl -c 1b6d3586 #orderList com.example.OrderCacheorderList, #orderList[0].status com.example.OrderStatusRUNNING修改 Map 中某个 key 对应的对象属性ognl -c 1b6d3586 #orderMap com.example.OrderCacheorderMap, #orderMap[A10001].status com.example.OrderStatusRUNNING按条件过滤集合后再修改目标元素这个是我用得最多的场景。比如把所有超时订单的状态都改成TIMEOUTognl -c 1b6d3586 #orders com.example.OrderCacheorderList, #orders.{? #this.timeout true}.{ status com.example.OrderStatusTIMEOUT }这段表达式的逻辑是先拿到订单列表用选择语法{? ...}过滤出timeout为true的订单再用投影语法.{ ... }对每个元素执行赋值操作。实际使用中我会尽量少用这种批量修改因为一旦过滤条件写错副作用范围会很大。如果确实需要批量效果建议先用选择语法读取一遍符合条件的元素数量确认无误后再动手。在表达式里创建新对象并赋值也是可行的。比如需要把一个 List 字段替换成新的 ArrayListognl -c 1b6d3586 #orderService com.example.SpringContextUtilapplicationContext.getBean(orderService), #orderService.orderList new java.util.ArrayList()OGNL 支持通过new 全限定类名()创建任意可访问的对象这在某些需要清空集合的场景里特别顺手。5. 高频报错和排查技巧实录5.1 ClassNotFoundExceptionClassLoader 不匹配这是我在使用ognl命令时遇到频率最高的错误没有之一。报错信息一般是java.lang.ClassNotFoundException: com.example.Order但你的类明明就在 classpath 里为什么找不到原因在于 JVM 里的类加载机制是分层的同一个类可以被不同的 ClassLoader 加载多次Arthas 默认在系统 ClassLoader 下执行表达式找不到 Web 应用自定义 ClassLoader 里的类。解决办法也很明确通过-c参数指定目标类的 ClassLoader。先执行classloader找到加载了你业务类的那个 ClassLoader 的 hash 值比如是1b6d3586然后重新执行ognl -c 1b6d3586 com.example.OrderfindById(1001L)如果你的目标类不在业务 ClassLoader 里而是 JDK 自带的类比如java.lang.String那不需要指定-c默认 ClassLoader 就能访问到。判断依据很简单报ClassNotFoundException了就去找 ClassLoader。5.2 表达式语法错误注意逗号、引号与赋值OGNL 表达式写错最常见的表现是返回一个语法错误或者 Arhthas 直接提示表达式无法解析。我总结下来主要有三个原因。第一表达式里混用了中英文标点特别是逗号和引号。OGNL 里逗号用于分隔多个表达式中文逗号会直接破坏解析。这个看起来低级但在命令行里粘贴代码时非常容易出错。第二多表达式序列的返回结果不符合预期。OGNL 里用逗号连接多个表达式时整个表达式的返回值是最后一个子表达式的值。如果你写成a 1, b 2最终返回的是2而不是赋值的结果。所以如果你要验证修改后的字段值需要把读取操作放在最后一个ognl -c 1b6d3586 #orderService com.example.SpringContextUtilapplicationContext.getBean(orderService), #orderService.status com.example.OrderStatusRUNNING, #orderService.status第三赋值运算符在 shell 中被解析成了环境变量赋值。如果你用的是 bash命令里的com.example.OrderStatusRUNNING这类包含和的表达式可能需要整体用引号包裹避免 shell 提前解析。5.3 修改不生效从引用、代理和缓存三个方向排查修改表达式执行成功返回结果也显示新值但业务代码运行起来还是旧行为这是让人最崩溃的情况。我排查过这种假成功问题一般有三个方向。一是引用被基本类型缓存了。比如你把Integer字段从 3 改成 5但业务代码在别处用基本类型int接了旧值引用传递断裂你改的是对象业务读的是值副本。这种只能通过找到所有引用的源头来解决。二是 Spring 代理对象的问题。你通过getBean拿到的可能是 JDK 动态代理或 CGLIB 增强后的代理对象调用方法时走的是代理逻辑而你直接给目标字段赋值可能在代理层就丢失了。这种情况下建议操作业务方法调用而不是直接改字段或者绕过代理对象拿到原始target再改。三是缓存和本地副本导致修改值被覆盖。比如一个配置对象启动时被加载到了本地变量后续循环一直用这个本地变量。这种要定位源头通常是找到真正存储配置的那个容器对象再改否则就是白忙活。我在实践中总结出一个判断技巧修改完立刻用watch命令观察后续方法调用时该字段的真实值。如果watch看到的值还是旧的说明你的修改路径不对如果值是新的但行为没变说明有其他地方在做二次覆盖。5.4 常用命令组合与实战速查在线上定位问题时我经常把几个命令串起来用形成一个稳定的排障闭环。先是dashboard确认进程基本状态然后thread看有没有业务线程卡住再用watch或trace盯一个关键方法的入参和返回值。一旦确认问题出在对象数据上就轮到vmtoolognl登场。速查表如下操作命令示例查看所有 ClassLoaderclassloader获取类的所有实例vmtool --action getInstances --className com.example.Order --limit 10读取静态字段值ognl -c 1b6d3586 com.example.StatusHolderINSTANCE.status调用 setter 修改属性ognl -c 1b6d3586 com.example.StatusHolderINSTANCE.setStatus(com.example.OrderStatusRUNNING)直接字段赋值ognl -c 1b6d3586 com.example.StatusHolderINSTANCE.status com.example.OrderStatusRUNNING从 Spring 容器获取 Beanognl -c 1b6d3586 #ctx com.example.SpringContextUtilapplicationContext, #ctx.getBean(orderService)按条件筛选并修改ognl -c 1b6d3586 #orders com.example.OrderCacheorderList, #orders.{? #this.timeout true}[0].status com.example.OrderStatusTIMEOUT5.5 线上操作的安全红线最后必须强调安全问题。用 Arthas 修改线上对象属性是一把双刃剑用好了快速止血用不好可能在生产环境制造新的故障。我自己定了几条红线也建议你严格遵守。第一修改前必须备份原始值。无论改什么字段先把原始值记录在本地或者聊天工具里万一修改引发了级联问题要有能力立刻恢复。恢复方式很简单再执行一次反向赋值即可。第二明确修改的影响范围。一个对象属性被修改可能会被多个业务链路读取甚至在异步任务里被持久化到数据库。你改的虽然是内存值但它可能通过定时任务、消息监听、事务提交等方式扩散到下游系统。在不确定影响范围的情况下优先做小范围验证观察一段时间再继续。第三禁止在高峰期执行非必要修改。修改线上对象导致的短暂状态不一致在某些强一致场景下会引发脏读或并发异常。如果业务允许最好安排在低峰期进行操作。第四记录完整的操作日志。包括执行时间、目标对象、表达式命令、修改前后的值以及操作人的标识。这些东西不只是为了追溯更是为了后续复盘时能回答当时到底改了什么。我见过一些同事线上问题紧张的时候手忙脚乱地敲命令改完属性就跑路结果第二天问题复现谁也说不清昨天改了什么。这种操作习惯比 bug 本身更可怕因为不可追溯的变更会直接摧毁团队的信心。Arthas 本身提供了history命令能查看当前会话历史但换了终端就没了所以我建议有条件的话直接在综合平台或内部文档里记录操作清单。实际用下来Arthas 的ognl命令在修改线上对象属性这件事上已经是我排查复杂问题的首选工具了。它能让你绕过代码发布周期直接在运行中的 JVM 里观察和修正状态但这种能力同时也要求你对 JVM 类加载机制、对象引用传递和业务数据流有足够深的理解。否则表达式报错了都不知道从哪查起。刚开始用的时候建议先在预发环境或者本地写个简单 Demo 多练几遍把 OGNL 的导航、赋值、筛选语法都摸熟再到生产环境用。等你熟悉了这套组合技再遇到线上状态不对但又不能重启的问题时你就知道自己手里其实一直握着一把非常好用的手术刀。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门