龙空技术网

JavaAgent实现调用链收集

冷叶 2174

前言:

而今大家对“java实验抽取样本”都比较注重,兄弟们都想要分析一些“java实验抽取样本”的相关内容。那么小编在网上网罗了一些对于“java实验抽取样本””的相关文章,希望朋友们能喜欢,大家快快来了解一下吧!

背景

部门内部的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 主要代码:

@Overridepublic 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

Javaassist

bytebuddy

  有很多关于三者之间的对比文章,大家可自行搜索看下。

  目前来说,asm的使用门槛最高,而且调试门槛也很高,idea有款插件ASM Bytecode Outline非常给力,能根据当前java类生成对应的asm指令,效果图如下:

  不过使用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后置,当调用的时候再去实时反射获取。

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);}@Overridepublic 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的接口,可以看出来,切面相关的日志已经打印出来了

标签: #java实验抽取样本