Arthas redefine(加载外部的.class文件,redefine到JVM里 )

在这里插入图片描述

二、命令列表

2.2 class/classloader相关命令

2.2.3 redefine(加载外部的.class文件,redefine到JVM里 )

提示:

目的:常用来解决线上服务不停止,修改代码操作

注意点1:redefine 的 class 不能修改、添加、删除类的 field 和 method,包括方法参数、方法名称及返回值。
注意点2:如果 mc 失败,可以在本地开发环境编译好 class 文件,上传到目标系统,使用 redefine 热加载 class

注意点3:目前 redefine 和 watch/trace/jad/tt 等命令冲突,以后重新实现 redefine 功能会解决此问题

注意点4:redefine 后的原来的类不能恢复,redefine 有可能失败(比如增加了新的 field),参考 jdk 本身的文档。

注意点5reset命令对redefine的类无效。如果想重置,需要redefine原始的字节码。

注意点6redefine命令和jad/watch/trace/monitor/tt等命令会冲突。执行完redefine之后,如果再执行上面提到的命令,则会把redefine的字节码重置。 原因是 jdk 本身 redefine 和 Retransform 是不同的机制,同时使用两种机制来更新字节码,只有最后修改的会生效。

参数说明:

参数名称参数说明
[c:]ClassLoader 的 hashcode
[classLoaderClass:]指定执行表达式的 ClassLoader 的 class name
举例1:加载新的代码,jad/mc 命令使用
# 1、使用jad反编译 demo.MathGame 输出到 /opt/MathGame.java
jad --source-only demo.MathGame > /opt/MathGame.java
# 2、把上一步反编译的代码修改完成后,使用mc在内存中对新的代码进行编译
mc /opt/MathGame.java -d /opt
# 3、使用redefine命令加载新的字节码
redefine /opt/demo/MathGame.class
举例2:上传 .class 文件到服务器的技巧

使用mc命令来编译jad的反编译的代码有可能失败。可以在本地修改代码,编译好后再上传到服务器上。有的服务器不允许直接上传文件,可以使用base64命令来绕过。

  1. 在本地先转换.class文件为 base64,再保存为 result.txt

    base64 < Test.class > result.txt
    
  2. 到服务器上,新建并编辑result.txt,复制本地的内容,粘贴再保存

  3. 把服务器上的 result.txt还原为.class

    base64 -d < result.txt > Test.class
    
  4. 用 md5 命令计算哈希值,校验是否一致

本人其他相关文章链接

1.Arthas 全攻略:让调试变得简单
2.Arthas dashboard(当前系统的实时数据面板)
3.Arthas thread(查看当前JVM的线程堆栈信息)
4.Arthas jvm(查看当前JVM的信息)
5.Arthas sysprop(查看和修改JVM的系统属性)
6.Arthas sysenv(查看JVM的环境变量)
7.Arthas vmoption(查看和修改 JVM里诊断相关的option)
8.Arthas getstatic(查看类的静态属性 )
9.Arthas heapdump(dump java heap, 类似 jmap 命令的 heap dump 功能)
10.Arthas logger(查看 logger 信息,更新 logger level)
11.Arthas mbean(查看 Mbean 的信息)
12.Arthas memory(查看 JVM 内存信息)
13.Arthas ognl(执行ognl表达式)
14.Arthas perfcounter(查看当前 JVM 的 Perf Counter 信息)
15.Arthas vmtool(从 jvm 里查询对象,执行 forceGc)
16.Arthas jad(字节码文件反编译成源代码 )
17.Arthas mc(Memory Compiler/内存编译器 )
18.Arthas redefine(加载外部的.class文件,redefine到JVM里 )
19.Arthas classloader (查看 classloader 的继承树,urls,类加载信息)
20.Arthas sc(查看JVM已加载的类信息 )
21.Arthas sm(查看已加载类的方法信息 )
22.Arthas monitor(方法执行监控)
23.Arthas stack (输出当前方法被调用的调用路径)
24.Arthas trace (方法内部调用路径,并输出方法路径上的每个节点上耗时)
25.Arthas tt(方法执行数据的时空隧道,记录下指定方法每次调用的入参和返回信息,并能对这些不同的时间下调用进行观测)
26.Arthas watch (方法执行数据观测)
27.Arthas profiler(使用async-profiler对应用采样,生成火焰图)

在做 javaagent 时 ,使用 agentMain 对已经运行中的 springboot 应用中的 restController 的 postmapping 修饰的方法 做切面,注意这是 agentMain ,以下方法执行之前大部分类已经加载结束并且对应的controller生成了单例的bean,package org.sunyaxing.transflow.agent.agentplugin; import net.bytebuddy.agent.ByteBuddyAgent; import net.bytebuddy.agent.builder.AgentBuilder; import net.bytebuddy.implementation.MethodDelegation; import net.bytebuddy.matcher.ElementMatchers; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.lang.instrument.Instrumentation; public class TransflowAgent { private static final Logger log = LoggerFactory.getLogger(TransflowAgent.class); public static void agentmain(String agentArgs, Instrumentation instrumentation) { ByteBuddyAgent.install(); new AgentBuilder.Default() .with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION) .with(AgentBuilder.TypeStrategy.Default.REDEFINE) .type(ElementMatchers.isAnnotatedWith(RestController.class)) .transform((builder, typeDescription, classLoader, javaModule) -> { log.info("agentmain transform class {} {}", typeDescription, classLoader); return builder .method( ElementMatchers.isAnnotatedWith(RequestMapping.class) .or(ElementMatchers.isAnnotatedWith(GetMapping.class)) .or(ElementMatchers.isAnnotatedWith(PostMapping.class)) ) .intercept(MethodDelegation.to(MonitorInterceptor.class)); }).installOnByteBuddyAgent(); } 然而,注入对已经运行的serverA 注入agent后,调用postmapping所修饰的方法并没有触发切面,是什么原因,如何解决
04-03
<think>嗯,我现在要解决的是JavaAgent通过agentMain方法拦截已经启动的SpringBoot应用中RestController的PostMapping方法切面失效的问题。首先,我得回忆一下JavaAgent的工作原理。JavaAgent可以在类加载时或者类加载后进行字节码增强,agentMain方法是在JVM启动后动态加载Agent时调用的,通常用于对已加载的类进行重新转换(retransformation)。 但为什么拦截PostMapping会失败呢?可能的原因有几个。首先,SpringBoot在启动时可能已经加载了相关的Controller类,如果Agent是在应用启动之后才加载的,那么这些类可能已经被加载过了,而默认情况下,ByteBuddy可能无法对这些已加载的类进行重新转换,导致切面没有生效。这时候需要使用ByteBuddy的retransform方法,显式地指定需要重新转换的类。 另外,Spring的AOP和RestController的处理机制可能也有影响。比如,PostMapping注解的处理可能是在类初始化时完成的,如果切面是在类初始化之后添加的,那么注解可能已经被处理过了,导致拦截不到。这时候可能需要确保切面在Spring处理注解之前就被应用。 还有可能是ByteBuddy的匹配规则不够准确,没有正确匹配到目标类或方法。需要检查字节码增强的逻辑是否正确,是否正确地定位到了RestController的类和方法,特别是带有PostMapping注解的方法。 接下来,我需要考虑JavaAgent的配置是否正确。比如,在MANIFEST.MF文件中是否设置了Can-Redefine-Classes和Can-Retransform-Classes为true,这样才能允许重新定义和重新转换类。如果没有设置这些属性,Agent可能没有权限进行retransformation。 另外,使用ByteBuddy进行retransformation的时候,可能需要使用AgentBuilder的enableBootstrapInjection或者installOn方法,确保Instrumentation实例被正确应用。还有可能需要使用RedefinitionStrategy.RETRANSFORMATION或者ReBASE策略,来允许重新转换已加载的类。 还有一个可能性是,SpringBoot使用了CGLIB或者JDK动态代理来创建代理对象,这样原始的类可能已经被代理类替换,导致直接增强原始类的方法无法拦截到代理类的方法调用。这时候可能需要调整增强策略,针对代理类进行处理,或者使用其他方式如AspectJ的Load-Time Weaving(LTW)来实现更彻底的切面。 解决方案方面,首先应该确认是否正确地进行了retransformation,确保相关的类被重新加载并应用了切面。可以通过在Agent的transform方法中打印日志,确认是否被调用。然后检查ByteBuddy的匹配规则,确保目标类和方法被正确匹配。此外,可能需要调整Spring的组件扫描顺序,或者在Spring应用完全启动之后,再手动触发一次类的重新转换,确保切面被应用。 最后,还需要考虑版本兼容性问题,比如使用的ByteBuddy版本是否与SpringBoot或Java版本兼容,是否存在已知的bug导致retransformation失效。这时候可以尝试升级相关依赖,或者查阅社区是否有类似的问题和解决方案。</think>### JavaAgent拦截SpringBoot PostMapping失效排查指南 #### 一、核心原因分析 1. **类加载时机问题** SpringBoot启动时已加载RestController类,常规JavaAgent的`agentmain`方法默认仅作用于新加载的类。未显式触发retransformation时,已加载类无法被修改[^1]。 2. **字节码匹配精度不足** ByteBuddy的类/方法匹配规则未精确覆盖Spring生成的代理类(如CGLIB代理),典型症状是增强逻辑仅对原始类生效,但实际调用的是代理对象[^2]。 3. **注解处理阶段冲突** PostMapping注解的元数据处理在类初始化阶段完成,若增强逻辑在注解解析后执行,会导致切面与路由映射表不同步[^3]。 4. **Instrumentation权限配置** Agent的`MANIFEST.MF`文件缺少关键声明: ```manifest Can-Redefine-Classes: true Can-Retransform-Classes: true ``` #### 二、解决方案步骤 1. **强制触发类重转换** ```java instrumentation.retransformClasses(targetClass); ``` 通过`Instrumentation`API显式指定需要重转换的Controller类。 2. **精准匹配代理类** 使用ByteBuddy的复合匹配策略: ```java new AgentBuilder.Default() .type(ElementMatchers.isAnnotatedWith(RestController.class)) .and(ElementMatchers.nameEndsWith("$$EnhancerBySpringCGLIB")) .transform(new Transformer()) .installOn(instrumentation); ``` 3. **增强时重置注解缓存** 在transform阶段清除Spring的注解缓存: ```java public static byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) { AnnotationUtils.clearCache(); return enhanceBytes(classfileBuffer); // 字节码增强逻辑 } ``` 4. **验证Agent配置** ```bash jar tf your-agent.jar | grep MANIFEST.MF ``` 确保包含`Premain-Class`和`Agent-Class`双入口声明。 #### 三、完整代码示例 ```java public class AspectAgent { public static void agentmain(String args, Instrumentation inst) { new AgentBuilder.Default() .with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION) .with(AgentBuilder.InitializationStrategy.NoOp.INSTANCE) .with(AgentBuilder.TypeStrategy.Default.REDEFINE) .type(nameMatches("com.example.controller.*Controller")) .transform((builder, type, cl, pd) -> builder.method(isAnnotatedWith(PostMapping.class)) .intercept(MethodDelegation.to(SpringAspect.class)) ).installOn(inst); // 强制重转换已加载Class[] loaded = inst.getAllLoadedClasses(); Arrays.stream(loaded).filter(c -> c.getName().contains("Controller")) .forEach(c -> { try { inst.retransformClasses(c); } catch (Exception e) { e.printStackTrace(); } }); } } // 切面逻辑类 public class SpringAspect { @RuntimeType public static Object intercept(@Origin Method method, @SuperCall Callable<?> callable) { System.out.println("Intercepted: " + method.getName()); return callable.call(); } } ``` #### 四、关键验证点 1. **字节码验证** 使用`-XX:+TraceClassLoading`参数观察目标类是否被重新加载: ``` [Loaded com.example.controller.UserController in __VM_Enhanced__] ``` 2. **代理类检测** 通过Arthas检查类结构: ```bash sc -d com.example.controller.UserController jad com.example.controller.UserController ``` 3. **Annotation元数据检查** 使用反射验证切面是否生效: ```java Arrays.stream(TargetClass.class.getDeclaredMethods()) .filter(m -> m.isAnnotationPresent(PostMapping.class)) .forEach(m -> System.out.println(m.getAnnotation(Around.class))); ``` #### 五、进阶优化方案 1. **混合使用Javassist** 对复杂AOP场景,可结合Javassist修改方法体: ```java CtClass ctClass = ClassPool.getDefault().makeClass(new ByteArrayInputStream(classfileBuffer)); CtMethod method = ctClass.getDeclaredMethod("targetMethod"); method.insertBefore("System.out.println(\"Before advice\");"); return ctClass.toBytecode(); ``` 2. **Spring Hook技巧** 通过劫持`RequestMappingHandlerMapping`强制刷新路由表: ```java ConfigurableApplicationContext ctx = (ConfigurableApplicationContext) SpringApplication.run(Application.class); RequestMappingHandlerMapping mapping = ctx.getBean(RequestMappingHandlerMapping.class); mapping.afterPropertiesSet(); // 触发路由重建 ``` [^1]: Java SE 8 Instrumentation API规范 [^2]: ByteBuddy官方文档关于retransformation的说明 [^3]: Spring Framework注解处理机制白皮书
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

刘大猫.

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值