在 Arthas 中调用 Spring Bean 方法

获取 Spring 应用上下文

使用工具类

如果你的项目中有一个工具类实现了 ApplicationContextAware 接口,如 cn.shutdown.pf.utils.SpringContextUtils,可以使用该类获取 ApplicationContext

@Component
public final class SpringContextUtils implements ApplicationContextAware {
    private static ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        SpringContextUtils.applicationContext = applicationContext;
    }

    public static ApplicationContext getApplicationContext() {
        return applicationContext;
    }
}

使用 OGNL 获取

在 Arthas 中,使用 ognl 命令结合工具类获取 ApplicationContext

ognl -c <classLoaderHash> '#context=@cn.shutdown.pf.performance.utils.SpringContextUtils@getApplicationContext()'

获取类加载器哈希值

使用 sc 命令

在 Arthas 中,使用 sc 命令可以查看类的详细信息,包括类加载器的哈希值。例如,查看 SpringContextUtils 类的类加载器哈希值:

sc -d cn.shutdown.pf.performance.utils.SpringContextUtils

输出示例:

 class-info        cn.shutdown.pf.performance.utils.SpringContextUtils
 code-source       nested:/app/web.jar/!BOOT-INF/classes/!
 name              cn.shutdown.pf.performance.utils.SpringContextUtils
 ...
 class-loader      +-org.springframework.boot.loader.launch.LaunchedClassLoader@660591fb
                     +-jdk.internal.loader.ClassLoaders$AppClassLoader@251a69d7
                       +-jdk.internal.loader.ClassLoaders$PlatformClassLoader@89c10b7
 classLoaderHash   660591fb

在输出中,classLoaderHash 字段显示了类加载器的哈希值,这里是 660591fb

使用 ognl 命令

你也可以使用 ognl 命令直接获取类加载器的哈希值。例如:

ognl '@cn.shutdown.pf.performance.utils.SpringContextUtils@getClass().getClassLoader().hashCode()'

这将返回类加载器的哈希值。

调用 Bean 方法

调用无参数方法

获取到 ApplicationContext 后,可以获取 Bean 并调用其无参数方法:

ognl -c <classLoaderHash> '#context=@cn.shutdown.pf.performance.utils.SpringContextUtils@getApplicationContext(), #bean=#context.getBean("brokerService"), #bean.someMethod()'

调用有参数方法

如果方法有参数,直接在调用时传入参数值:

ognl -c <classLoaderHash> '#context=@cn.shutdown.pf.performance.utils.SpringContextUtils@getApplicationContext(), #bean=#context.getBean("brokerService"), #bean.orderChildPay("CYD2024122923468019")'

调用方法并传入对象参数

当方法需要一个对象作为参数时,需要在 OGNL 表达式中创建该对象并设置其属性:

ognl -c <classLoaderHash> '#context=@cn.shutdown.pf.utils.SpringContextUtils@getApplicationContext(), #param=new com.yourpackage.BrokerOrderChildUpdateMqParam(), #param.setChildNo("DD2024122923468019"), #param.setOrderNo("12345"), #bean=#context.getBean("brokerServiceImpl"), #bean.orderChildPay(#param)'

查看 Spring Bean 名称

使用 vmtool 命令

如果你的 Arthas 支持 vmtool 命令,可以使用以下命令查看所有注册的 Bean 名称:

vmtool --action getInstances --className org.springframework.context.ApplicationContext --express 'instances[0].getBeanDefinitionNames()'

使用 ognl 命令

如果不支持 vmtool 命令,可以使用 ognl 命令结合 ApplicationContext 获取 Bean 名称:

ognl -c <classLoaderHash> '#context=@cn.shutdown.pf.utils.SpringContextUtils@getApplicationContext(), #context.getBeanDefinitionNames()'

常见问题及解决方法

ClassNotFoundException

  • 确保类路径正确,类已编译并打包到应用中。
  • 使用 sc 命令检查类加载器信息,确保使用正确的类加载器。

NoSuchBeanDefinitionException

  • 确认 Bean 名称正确,检查类上的注解(如 @Component@Service)。
  • 确保 Spring 组件扫描路径包含了 Bean 类所在的包路径。
  • 使用 Arthas 的 bean 命令(如果支持)或 ognl 命令列出所有 Bean 名称,确认 Bean 是否存在。

方法调用返回 null

  • 方法逻辑导致返回 null,检查方法内部逻辑和业务条件。
  • 方法业务逻辑设计为返回 null,需要根据业务需求处理返回值。

注意事项

  • 确保 Arthas 版本与操作系统和应用架构兼容。
  • 使用正确的类加载器哈希值,可以通过 sc 命令获取。
  • 调用方法时,确保方法签名与传入的参数类型和数量匹配。
  • 在生产环境中使用 Arthas 时,注意安全和性能影响,谨慎操作。
在做 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
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值