文章采集调用(*敏*感*词*代理中增强类Enhancer应该是核心配置功能类的)

优采云 发布时间: 2022-02-21 23:25

  文章采集调用(*敏*感*词*代理中增强类Enhancer应该是核心配置功能类的)

  背景

  通过开发插件包采集部门内的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的阵痛。另外,由于多版本兼容,数据不能一致,处理起来很麻烦。为此,您唯一能想到的就是对相应的集合和模拟进行代理增强操作。

  *敏*感*词*

  代理中的增强类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 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; }

  类名匹配在这里进行了两次。增强器映射保存了需要增强的类名与增强的扩展类的关系。Enhancer中的变量很简单,如下:

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

  只有匹配到对应的增强器后,才会进行增强处理,比如后面提到的DubboProviderEnhancer。

  字节码操作工具

  目前主流的字节码操作工具包括以下asmJavaassistbytebuddy

  三个文章的比较有很多,大家可以自己搜索看看。

  目前asm的使用门槛最高,调试的门槛也很高。Idea有一个非常强大的插件ASM Bytecode Outline。它可以根据当前的java类生成相应的asm指令。效果图如下:

  

  但是,使用 asm 还是需要开发者对字节码指令、局部变量表、操作树栈有很好的理解,才能写好相关的代码。

  bytebuddy 完全以链式编程的方式构建了一套用于方法切面编织的字节码操作。从编码的角度来看,它是比较简单的。目前,bytebuddy的代理操作已经很完善了。它是根据类名和方法名过滤的。提供了一套链式操作,如果业务逻辑不复杂,推荐使用。

  代理实现

  代理类的实现大家一定不会陌生。我们有许多代理类的入口点。我们可以在方法的before、after、afterReturn、afterThrowing等进行相应的操作。当然,所有这些都可以通过模板来实现。这里我稍微简化一下,整体实现代理类的增强操作。类似于java动态代理,大家都知道实现java动态代理必须实现的类InvocationHandler,改写方法:

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

  大致思路是比较动态代理的方式,封装需要代理的类,将类类型、入参和对象带入代理方法,重写需要增强的方法,将重写前的方法作为基类和修改方法名,这里分两步操作。

  1.重写之前的方法

  2.添加新方法并复制之前的方法体

  需要注意的是这里没有违反retransformClasses的规则,没有添加属性,也没有修改方法声明

  RPC中间件相关类的增强实现效果如下:

  代码显示如下:

<p>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 = "System.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

0 个评论

要回复文章请先登录注册


官方客服QQ群

微信人工客服

QQ人工客服


线