基于byteBuddy的java agent设计

参考资料: java classLoader问题: https://segmentfault.com/a/1190000040364396

                 java agent基础原理:https://blog.csdn.net/ancinsdn/article/details/58276945?ivk_sa=1024320u

                  使用 javaagent 和 动态 Attach两种方式实现类的动态修改和增强: https://www.cnblogs.com/756623607-zhang/p/12575509.html
                  javaagent学习笔记: https://segmentfault.com/a/1190000016601560?utm_source=tag-newest

参考项目 : 基于bytebuddy的agent简单实现:https://gitee.com/yanghuijava/agent-tutorial

                  http-plugin:  https://github.com/alibaba/jvm-sandbox-repeater

                   完整版设计参考: https://github.com/apache/skywalking.git

https://gitee.com/beetle082/bee-apm.git

agent和attach模式

agent项目: https://github.com/jakubhalun/tt2016_byte_buddy_agent_demo.git

应用案例: https://github.com/jakubhalun/tt2016_byte_buddy_agent_demo_instrumented_app.git

attach命令:  https://github.com/jakubhalun/tt2016_byte_buddy_agent_loader.git

可以使用agent和attach两种模式。

classloader相关的地方: 类的继承、代码中使用class或者使用class初始化对象、对于传入的已经生成的对象无影响。

1 项目介绍

基于ByteBuddy开发的java探针工具,提供http、dubbo等协议的流量复制功能。
支持 JavaAgent 和 Attach 两种模式使用。
参考项目:apache-skywalking、jvm-sandbox-repeater。

2 开发注意事项

  • 尽量不要使用第三方jar包,常规基础包如apache.commons、guava、fastjson等可考虑使用。但必须使用maven-shade-plugin统一更改包路径,防止跟应用中的jar包冲突。
  • 不能使用log4j等日志框架,自己需实现log功能。
  • 字节码增强类需要打包成jar包,自定义classLoader来加载jar包,否则无法加载

3 使用方式

  • JavaAgent 模式


    java -javaagent:<agentJarPath>[=<agentOptions>] [-D<agentKey>=<agentValue>...] -jar <projectJarPath>


example: java -javaagent:agent-1.0.0-SNAPSHOT.jar -Dtraffic.agent.http.sample.rate=100 -Dtraffic.agent.http.ignore.ips=127.0.0.1,123.*.1.1 -jar springMvc-example.jar

  • Attach 模式


    java -jar <attachJarPath> <agentJarPath> <pid> [<agentOptions>]


example: java -jar attacher-1.0.0-SNAPSHOT.jar agent-1.0.0-SNAPSHOT.jar 3280 "traffic.agent.http.sample.rate=2;traffic.agent.http.ignore.ips=127.0.0.1,123.*.1.1"

4  系统参数

traffic.agent.logging.dir 日志文件路径 logs/apicollection
traffic.agent.http.sample.rate HTTP采样率,代表每N个请求采样1次 100
traffic.agent.http.ignore.ips HTTP忽略的ip,格式为:127.0.0.1, 127.*, 127..12.22
traffic.agent.http.response.bytes.max HTTP响应体数据最大值限制,超过该值则不记录ResponseBody 204800 (200K)
traffic.agent.plugins.mount plugins默认路径 plugins, activations

系统参数优先级说明, Agent Options > Jvm Options > System Properties,容器变量认为是System Properties

5 支持的plugins

  • http-plugin

6 性能测试

6.1 http-plugin性能测试

测试接口: get请求,返回一个字符串
测试命令: ab -n100000 -c50 url(10万请求 50并发量)

  • 不使用agent,qps: 11765.16
  • 使用agent,qps: 10760.62


7 遇到的问题总结

7.1 java.lang.NoClassDefFoundError

该异常的主要 原因是classloader问题,agent运行的classloader为app classloader(默认),springboot的classloader为JarLauncher(parent为app classloader)。

因为双亲委派机制,如果agent使用了JarLauncher加载的class,会报上述错误。

解决方法: 将 使用JarLauncher加载的class的class文件打包成jar包,自己新建个AgentClassLoader(parent为JarLauncher)来加载jar包 和 创建对象。

7.2 agent无法attach问题

bytebuddy有2套annotation api来处理aop逻辑,一套是implemetaion下的注解,另一套是adivce api 。

解决方法: 使用advice api, 只有这套注解是支持attach的

7.3 agent如何detach

 ResettableClassFileTransformer resettable = new AgentBuilder.Default()
        .disableClassFormatChanges()
        //忽略classes的匹配规则(不拦截)
        .ignore(IgnoreTypes.INSTANCE.get())        
        .with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION)
        .with(new AgentListener())
        //定义被拦截class的匹配规则
        .type(MatchTypes.INSTANCE.get(enhanceClassDefineList))        //拦截class做增强处理
        .transform(new Transformers(enhanceClassDefineList))        
         .installOn(inst); 
//attach模式agent卸载
if (isRuntimeAttach) {    ProcessWatcher.watch(AgentSpyApi.getAttacherPid())
            .checkIntervalMills(1000)
            .onShutdown(() -> {
                //agent卸载
                resettable.reset(inst, AgentBuilder.RedefinitionStrategy.RETRANSFORMATION);
                //重置attach相关的值
                AgentSpyApi.destroy();
                System.out.println("TrafficCaptureAgent stopped successfully!");
                LOGGER.info("TrafficCaptureAgent stopped successfully!");
            }).start();
} 

resettable.reset(inst, AgentBuilder.RedefinitionStrategy.RETRANSFORMATION);  

触发机制: 监听attacher进程号

7.4 advice api使用坑

绑定参数案例:

newBuilder.visit(Advice.withCustomMapping()
                .bind(AInterceptorClass.class, methodsInterceptPoint.interceptorClass())
                //拦截器代理
                .to(MethodsInterceptorDelegator.class)                //被拦截的方法匹配规则
                .on(methodsInterceptPoint.methodMatcher())); 


具体使用案例:
@Advice.OnMethodEnter
public static InterceptorContext enter(@Advice.This Object target,
                                       @Advice.Origin Method method,
                                       @Advice.AllArguments(readOnly = false, typing = Assigner.Typing.DYNAMIC) Object[] allArguments,
                                       @AInterceptorClass String interceptorClass) {    
InterceptorContext context = new InterceptorContext(target, method, allArguments, method.getParameterTypes());
    try {        
         MethodsInterceptor interceptor = InterceptorClassLoader.load(interceptorClass,
                Thread.currentThread().getContextClassLoader());
        interceptor.beforeMethod(context);
        allArguments = context.getArguments();
    } catch (Throwable t) {        
         LOGGER.error(t, "class[%s] before method[%s] intercept failure", target.getClass(), method.getName());
    }    return context;
}
@Advice.OnMethodExit(onThrowable = Throwable.class)
public static void exit(@Advice.Enter InterceptorContext context,
                        @Advice.Return(readOnly = false, typing = Assigner.Typing.DYNAMIC) Object ret,
                        @Advice.Thrown Throwable thrown,
                        @AInterceptorClass String interceptorClass) {    
try {
        MethodsInterceptor interceptor = InterceptorClassLoader.load(interceptorClass,
                Thread.currentThread().getContextClassLoader());
        Object newRet = ret;
        interceptor.afterMethod(context, newRet, thrown);
        ret = newRet;
    } catch (Throwable t) {        
        LOGGER.error(t, "class[%s] after method[%s] intercept failure", context.getObjInst().getClass(),
                context.getMethod().getName());
    }
}


  @Advice.AllArguments(readOnly = false, typing = Assigner.Typing.DYNAMIC) Object[] allArguments readOnly=false 代表可修改该对象;
typing = Assigner.Typing.DYNAMIC 不用指定具体类型来接收参数,每次获取到的allArguments是一份浅拷贝数据

发表评论 / Comment

用心评论~