使用javaAgent进行调用链收集

背景

部门内部的trace的链路信息通过开发插件包进行收集的,包括RPC的mock工具。收集下来的调用链类似如下:

1589802250554|0b51063f15898022505134582ec1dc|RPC|com.service.bindReadService#getBindModelByUserId|[2988166812]|{"@type":"com.service.models.ResultVOModel","succeed":true,"valueObject":{"@type":"com.service.models.bind1688Model","accountNo":"2088422864957283","accountType":3,"bindFrom":"activeAccount","enable":true,"enableStatus":1,"memberId":"b2b-2988166812dc3ef","modifyDate":1509332355000,"userId":2988166812}}|2|0.1.1.4.4|11.181.112.68|C:membercenterhost|DPathBaseEnv|N||

不仅打印了trace,还有出入参,目标IP以及源IP,可以看出来还是非常清晰的,在我们联调和排查的问题的时候起到了很大的效率提升。
  不过,随着产品的不断迭代,以jar的形式还是遇到了很多问题,首先就是接入成本高,版本不稳定导致升级迅速,相应服务得不断升级,相信大家都有过升级fastjson的痛苦。再一个,因为多版本兼容导致数据也不能一致,处理起来十分麻烦。为此,能想的到的就是对相应的收集和mock做agent增强操作。

结构图

代理中增强类Enhancer应该是核心配置功能类,通过继承或者SPI扩展,我们可以实现不同的增强点的配置。
在这里插入图片描述

相关代码

BootStrapAgent 入口类:

/**
 * @author wanghao
 * @date 2020/5/6
 */
public class BootStrapAgent {

    public static void main(String[] args) {
        System.out.println("====main 方法执行");
    }

    public static void premain(String agentArgs, Instrumentation inst) {
        System.out.println("====premain 方法执行");
        new BootInitializer(inst, true).init();
    }

    public static void agentmain(String agentOps, Instrumentation inst) {
        System.out.println("====agentmain 方法执行");
        new BootInitializer(inst, false).init();
    }
}

agent的入口类,premain支持的agent挂载方式,agentmain支持的是attach api 方式,agent方式需要指定-javaagent参数,需要工程当中的docker文件进行配置,仍然是是有侵入成本的,长远看还是需要用attach api的方式。

BootInitializer 主要代码:

public class BootInitializer {

    public BootInitializer(Instrumentation instrumentation, boolean isPreAgent) {
        this.instrumentation = instrumentation;
        this.isPreAgent = isPreAgent;
    }

    public void init() {
        this.instrumentation.addTransformer(new EnhanceClassTransfer(), true);
        if (!isPreAgent) {
            try {
                // TODO 此处暂硬编码,后续修改
                this.instrumentation.retransformClasses(Class.forName("com.abb.ReflectInvocationHandler"));
            } catch (UnmodifiableClassException e) {
                e.printStackTrace();
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
        }
    }
}

这里需要注意一点的是,addTransformer中的参数canRetransform需要设置为true,意思表名可重转换器,否则即使调用retransformClasses方法同样也不能对指定的类进行重定义。需要注意的是,新加载的类不能修改旧有的类声明,譬如不能增加属性、不能修改方法声明。

EnhanceClassTransfer 主要代码:

@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {

    if (className == null) {
        return null;
    }
    String name = className.replace("/", ".");
    byte[] bytes = enhanceEngine.getEnhancedByteByClassName(name, classfileBuffer, loader);
    return bytes;
}

EnhanceClassTransfer做的事情很简单,直接去调用EnhanceEngine生成字节码

EnhanceEngine 主要代码:

private static Map<String, Enhancer> enhancerMap = new ConcurrentHashMap<>();

public byte[] getEnhancedByteByClassName(String className, byte[] bytes, ClassLoader classLoader, Enhancer enhancerProvide) {
        byte[] classBytes = bytes;

        boolean isNeedEnhance = false;

        Enhancer enhancer = null;

        // 两次enhancer匹配校验
        // 具体类名匹配
        enhancer = enhancerMap.get(className);
        if (enhancer != null) {
            isNeedEnhance = true;
        }
        // 类名正则匹配
        if (!isNeedEnhance) {
            for (Enhancer classNamePtnEnhancer : classNamePtnEnhancers) {
                if (classNamePtnEnhancer.isClassMatch(className)) {
                    enhancer = classNamePtnEnhancer;
                    break;
                }
            }
        }
        if (enhancer != null) {
            System.out.println(enhancer.getClassName());
            MethodAopContext methodAopContext =
                    GlobalAopContext.buildMethodAopContext(enhancer.getClassName(), enhancer.getInvocationInterceptor()
                    ,classLoader, enhancer.getMethodFilter());
            try {
                classBytes = ClassProxyUtil.buildInjectByteCodeByJavaAssist(methodAopContext, bytes);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return classBytes;
    }

这里做了两次的类名匹配,enhancerMap中保存了需要增强的类名与增强扩展类之间的关系,Enhancer当中变量非常简单,如下:

private String className;
private Pattern classNamePattern;
private Pattern methodPattern;
private InvocationInterceptor invocationInterceptor;

只有匹配到了相应的enhancer才会做增强处理,比如后续会提到的DubboProviderEnhancer

字节码操作工具

目前主流的字节码操作工具有如下几种,附带几种方式的文档手册
asm
操作手册:asm4-guide.pdf
Javaassist
文档地址:http://www.javassist.org/tutorial/tutorial.html
bytebuddy
文档地址:https://bytebuddy.net/#/tutorial

有很多关于三者之间的对比文章,大家可自行搜索看下。
  目前来说,asm的使用门槛最高,而且调试门槛也很高,idea有款插件ASM Bytecode Outline非常给力,能根据当前java类生成对应的asm指令,效果图如下:
image.png
  不过使用asm还是需要开发者对字节码指令、局部变量表、操作树栈很清楚才能撸好相关代码。
  bytebuddy完全是以链式编程的方式构建了一套方法切面编织的字节码操作,编码角度来说较简单,目前bytebuddy的agent操作已经很很全了,基于类名过过滤,方法名过滤的一套链式操作都有提供,如果业务逻辑不复杂的话推荐使用。

代理实现

对于代理类的实现,想必一定都不会陌生,对一个类做代理我们会有很多的切入点,在method的before、after、afterReturn、afterThrowing等都可以进行相应的操作。当然这些都可以通过模板实现,这里我做的稍微简化一点儿,将代理类的增强操作整体实现。类比java动态代理,大家都清楚要实现java动态代理必须要实现的类InvocationHandler,其中复写的方法:

public Object invoke(Object proxy, Method method, Object[] args)
        throws Throwable;

大致的思路,类比动态代理的方式,对将需要代理的类进行封装,将class类型、入参、对象带入到代理方法中,对需要增强的方法重写,将重写之前的方法作为基底类并且修改方法名,这边是两步操作。
1.重写之前的方法
2.新增新的方法,并且复制之前的方法体
需要注意的是,这里并没有违反retransformClasses的规则,没有增加属性和修改方法声明
对于RPC中间件相关类增强的实现的效果如下:

代码如下:

public static byte[] buildInjectByteCodeByJavaAssist(MethodAopContext methodAopContext, byte[] classBytes) throws Exception {

    CtClass ctclass = null;
    try {
        ClassPool classPool = new ClassPool();
        // 使用加载该类的classLoader进行classPool的构造,而不能使用ClassPool.getDefault()的方式
        classPool.appendClassPath(new LoaderClassPath(methodAopContext.getLoader()));
        ctclass = classPool.get(methodAopContext.getClassName());
        CtMethod[] declaredMethods = ctclass.getDeclaredMethods();

        for (CtMethod method : declaredMethods) {
            String methodName = method.getName();
            if (methodAopContext.matchs(methodName)) {
                System.out.println("methodName:" + methodName);
                String outputStr = "\nSystem.out.println(\"this method " + methodName
                        + " cost:\" +(endTime - startTime) +\"ms.\");";
                // 定义新方法名,修改原名
                String oldMethodName = methodName + "$old";
                // 将原来的方法名字修改
                method.setName(oldMethodName);

                // 创建新的方法,复制原来的方法,名字为原来的名字
                CtMethod newMethod = CtNewMethod.copy(method, methodName, ctclass, null);

                int modifiers = newMethod.getModifiers();
                String type = newMethod.getReturnType().getName();
                CtClass[] parameterJaTypes = newMethod.getParameterTypes();

                // 获取参数
                Class<?>[] parameterTypes = new Class[parameterJaTypes.length];
                for (int var1 = 0; var1 <= parameterJaTypes.length - 1; var1++) {
                    parameterTypes[var1] = methodAopContext.getLoader().loadClass(parameterJaTypes[var1].getName());
                }
                // 构建新的方法体
                StringBuilder bodyStr = new StringBuilder();
                bodyStr.append("{");
                bodyStr.append(prefix);

                MethodAopContext.MethodInfo methodInfo = new MethodAopContext.MethodInfo();
                methodInfo.methodName = oldMethodName;
                methodInfo.params = parameterTypes;
                methodAopContext.setMethodInfo(methodInfo);

                // 判断是否是静态方法
                boolean isStaticMethod = Modifier.isStatic(modifiers);
                if (isStaticMethod) {
                    bodyStr.append("com.client.bootstrap.aop.bytecode.ReturnWrapper returnWrapper = " +
                            "com.client.bootstrap.aop.bytecode.GlobalAopContext.onMethodEnter("
                            + methodAopContext.getIndex() + "," + methodAopContext.getClassName().concat(".class") + ","
                            + "null" + "," + "$args);");
                } else {
                    bodyStr.append("com.client.bootstrap.aop.bytecode.ReturnWrapper returnWrapper = " +
                            "com.client.bootstrap.aop.bytecode.GlobalAopContext.onMethodEnter("
                            + methodAopContext.getIndex() + "," + methodAopContext.getClassName().concat(".class") + ","
                            + "$0" + "," + "$args);");
                }

                // 调用原有代码,类似于method();($$)表示所有的参数
                if (!"void".equals(type)) {
                    // 强制转换
                    bodyStr.append(type).append(" result = (" + type + ")returnWrapper.getReturnObject();");
                }

                bodyStr.append(postfix);
                bodyStr.append(outputStr);
                if(!"void".equals(type)) {
                    bodyStr.append("return result;\n");
                }
                bodyStr.append("}");
                // 替换新方法
                newMethod.setBody(bodyStr.toString());
                // 增加新方法
                ctclass.addMethod(newMethod);
            }
        }
        byte[] bytes = ctclass.toBytecode();
        CommonUtils.writeByteToFile(methodAopContext.getClassName().concat(".class"), bytes);
        return bytes;
    } catch (Exception e) {
        e.printStackTrace();
        return classBytes;
    }
}

这里并不将Method对象直接传递到onMethodEnter方法中,而只将Method信息包裹成信息对象放至数组中,用index来维系上线文之间的对象获取,为什么这么做呢,因为按照我们编写字节码操作时,新生成的方法的字节码类还未被重新载入,这是,classLoader将找不到你的方法。所以,将method后置,当调用的时候再去实时反射获取。
字节码构件图.png

RPC入口类增强

书归上文,我们想做的是什么呢?打印RPC的Trace链路日志,实现链路收集。很好办的就是在RPC的入口类进行增强就行了,com.rpc.remoting.provider.ReflectInvocationHandler就是我们的目标类,ReflectInvocationHandler是RPC的过滤器的最后一环,也就是最靠近方法的那层逻辑,再往下就是反射RPC的服务端的具体方法了。

public DubboProviderEnhancer() {
    this.setClassName("com.abb.ReflectInvocationHandler");
    this.setClassMethodPatters(new String[]{"handleRequest0"});
    this.setInvocationInterceptor(this);
}

@Override
public Object invoke(Object proxy, Method method, Object[] args, int idx) throws Throwable {
    // 最小粒度的获取切面
    LogRecord logRecord = new LogRecord();
    // 调用真实的方式
    Object rpcResponse = method.invoke(proxy, args);
    // 抽取参数至logRecord
    extractParam(logRecord, args, rpcResponse);
    return rocResponse;
}

打包

最后进行agent打包,maven配置清单,最主要的打包plugin就是maven-assembly-plugin,注意指定Premain-Class和Agent-Class

<plugin>
    <artifactId>maven-assembly-plugin</artifactId>
    <version>3.0.0</version>
    <configuration>
        <archive>
            <manifest>
                <addClasspath>true</addClasspath>
                <mainClass>
                    com.client.bootstrap.BootStrapAgent
                </mainClass>
            </manifest>
            <manifestEntries>
                <Premain-Class>
                    com.client.bootstrap.BootStrapAgent
                </Premain-Class>
                <Agent-Class>
                    com.client.bootstrap.BootStrapAgent
                </Agent-Class>
                <Can-Redefine-Classes>true</Can-Redefine-Classes>
                <Can-Retransform-Classes>true</Can-Retransform-Classes>
            </manifestEntries>
        </archive>
        <descriptorRefs>
            <descriptorRef>jar-with-dependencies</descriptorRef>
        </descriptorRefs>
    </configuration>
    <executions>
        <execution>
            <id>make-assembly</id>
            <phase>package</phase>
            <goals>
                <goal>single</goal>
            </goals>
        </execution>
    </executions>
</plugin>

##改造结果
首先可以看到,jvm启动的参数已经挂载了我们的agent
在这里插入图片描述
调用一个RPC的接口,可以看出来,切面相关的日志已经打印出来了
image.png

展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 编程工作室 设计师: CSDN官方博客
应支付0元
点击重新获取
扫码支付

支付成功即可阅读