前言:
而今大家对“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实验抽取样本