利用javaagent修改字节码实现aop

利用javaagent修改字节码实现aop

起因:
在用seata AT模式时想在全局事务提交后触发事件,解析undo_log执行一些操作,比如异步写入es。由于seata没有提供代码切入点,那么只能在项目中建一个和框架代码相同的包,再拷贝框架中的类源码放到包下,重写其中的源码,classpath优先加载原则达到覆盖原类目的。这种做法实属不够『优雅』。
那么怎么才能做到无声无息的对源码做到增强呢?
想到spring aop,但是源代码是new出来的,并且不受spring工厂托管,那么这条路走不通了。
又想到利用ClassLoader,自定义加载我们复写的代码,但是框架没法用到我们自定义的ClassLoader,此路也行不通。
那么只能在jvm加载该class解析成字节码的过程中,偷偷给原字节码替换掉,『偷天换日』计划就此形成。
利用java5提供的agent特性来完成这一操作。这篇文章:Java 5 特性 Instrumentation 实践 介绍了如何使用。
那么开始吧!~

  1. 准备工作,定义 Advice 接口、pom 引入 javassist 依赖

     public interface Advice {
     	void before(Object[] params);
     	void after(Object[] params);
     }
     
     // pom.xml 引入依赖
     <dependency>
         <groupId>org.javassist</groupId>
         <artifactId>javassist</artifactId>
         <version>3.27.0-GA</version>
     </dependency>
    
  2. 在应用中添加 premain 方法,为 agent 提供入口。类名随意,类似于main方法,放在哪个类里不重要。

     public class Premain {
         public static void premain(String agentArgs, Instrumentation inst) {
         	 //  Transformer 类提供转换方法
             inst.addTransformer(new Transformer());
         }
     }
    
  3. 添加Transformer类,按照规定重写 transform 方法

     public class Transformer implements ClassFileTransformer {
     				
     	 // 缓存的所有配置
         private volatile Map<String, Pack> map = null;
         
         /**
          * 参数:
          * loader - 定义要转换的类加载器;如果是引导加载器,则为 null
          * className - 完全限定类内部形式的类名称和 The Java Virtual Machine Specification 中定义的接口名称。例如,"java/util/List"。
          * classBeingRedefined - 如果是被重定义或重转换触发,则为重定义或重转换的类;如果是类加载,则为 null
          * protectionDomain - 要定义或重定义的类的保护域
          * classfileBuffer - 类文件格式的输入字节缓冲区(不得修改)
          * 返回:
          * 一个格式良好的类文件缓冲区(转换的结果),如果未执行转换,则返回 null。
          * 抛出:
          * IllegalClassFormatException - 如果输入不表示一个格式良好的类文件
          */
         public byte[] transform(ClassLoader l, String className, Class<?> c, ProtectionDomain pd, byte[] b) {
             if (map == null) {
                 // 加载配置,全局只加载一次
                 scanProps();
             }
             // 包含配置的类需要做aop
             if (map.containsKey(className)) {
             	// 拿出当前类的配置
                 Pack p = map.get(className);
                 // 调用字节码翻译工具,返回做好aop的字节码
                 return TransformUtil.getNewByteCode(p.targetClassName, p.targetMethodName);
             }
             // 返回null 表示使用默认字节码,不做转换
             return null;
         }
     
         /**
          * 初始化所有工程中的advice.properties配置文件,加载到本类的缓存中
          * 配置示例:
          * io.seata.rm.datasource.undo.AbstractUndoLogManager.batchDeleteUndoLog=com.a.b.c.AbstractUndoLogManagerAdvice
     	  * key 配置到方法,value 配置 Advice 的实现类
          */
         private synchronized void scanProps() {
             if (map != null) {
             	// double check
                 return;
             }
             Map<String, Pack> m = new HashMap<>();
             Enumeration<URL> urls = null;
             try {
             	 // 搜索所有项目下的 advice.properties 配置文件
                 urls = Thread.currentThread().getContextClassLoader().getResources("advice.properties");
             } catch (IOException e) {
                 e.printStackTrace();
             }
             if (urls != null) {
                 while (urls.hasMoreElements()) {
                     InputStream inStream = null;
                     Properties property = new Properties();
                     try {
                         inStream = urls.nextElement().openStream();
                         property.load(inStream);
                         property.stringPropertyNames().forEach(key -> {
                         	 // 取出 key 的 类名
                             String targetClassName = key.substring(0, key.lastIndexOf("."));
                             // 取出 key 的 方法名
                             String targetMethodName = key.substring(key.lastIndexOf(".") + 1);
                             // 取出 advice 实现类名
                             String adviceClassName = (String) property.get(key);
                             // 缓存的key为 io/seata/rm.datasource/undo/AbstractUndoLogManager 格式
                             String k = targetClassName.replaceAll("\\.", "/");
                             Pack p = m.get(k);
                             // 添加到配置缓存中
                             if (p == null) {
                                 p = new Pack(targetClassName, targetMethodName, adviceClassName);
                                 m.put(k, p);
                             } else {
                                 p.add(targetMethodName, adviceClassName);
                             }
                         });
                     } catch (Exception e) {
                         e.printStackTrace();
                     } finally {
                         if (inStream != null) {
                             try {
                                 inStream.close();
                             } catch (IOException e) {
                                 e.printStackTrace();
                             }
                         }
                     }
                 }
             }
             // map 为 volatile 声明,保证全局可见
             map = m;
        }
    
     	static class Pack {
         	String targetClassName;
         	// 因为一个类可能有多个方法需要做 advice,这里用<methodName, AdviceImplName>保存
         	Map<String, String> targetMethodName = new HashMap<>();
    
            public Pack(String targetClassName, String targetMethodName, String adviceClassName) {
                 this.targetClassName = targetClassName;
                 this.targetMethodName.put(targetMethodName, adviceClassName);
            }
    
            public void add(String targetMethodName, String adviceClassName) {
                 this.targetMethodName.put(targetMethodName, adviceClassName);
            }
    	}
     }
    
  4. 现在知道了哪些类、哪些方法、需要用哪些Advice 做代理,现在需要修改字节码把代理用TransformUtil 支持进去,javassist 如何使用可以参考这个文档:
    http://www.javassist.org/tutorial/tutorial.html

     public class TransformUtil {
         /**
          * 目标类的目标方法做代理
          * 支持普通方法、static方法。
          * 不支持构造方法、重载方法、抽象方法
          *
          * @param targetClassName 目标类全名 Examples: java.lang.String
          * @param method_advice <目标方法名 Examples: getBytes, Advice实现类的全名 Examples: java.lang.String>
          * @return 重写的类的字节码
          */
         public static byte[] getNewByteCode(String targetClassName, Map<String, String> method_advice) {
             try {
                 ClassPool pool = ClassPool.getDefault();
                 CtClass ctClass = pool.get(targetClassName);
     
                 // 循环代理一个类中的多个方法
                 for (Map.Entry<String, String> entry : method_advice.entrySet()) {
                     String targetMethodName = entry.getKey();
                     String adviceClassName = entry.getValue();
                     CtMethod originalMethod = ctClass.getDeclaredMethod(targetMethodName);
                     CtClass originalReturnType = originalMethod.getReturnType();
                     // advice name
                     String adviceFieldName = targetMethodName + "$advice";
                     // 原始方法重命名后的名字
                     String newMethodName = "original$" + targetMethodName;
     
                     // 添加Advice到类的成员变量
                     String adviceField = "private" +
                     		// 判断目标方法是否是static的
                             ((originalMethod.getMethodInfo().getAccessFlags() & AccessFlag.STATIC) == AccessFlag.STATIC ? " static " : " ") 
                             // 需要改成Advice接口存放的路径
                             + "final com.aop.javassist.Advice "
                             + adviceFieldName + " = new " + adviceClassName + "(); ";
                     ctClass.addField(CtField.make(adviceField, ctClass));
     
                     // ===================组装方法体开始===================
                     // 拼装新方法的方法体
                     StringBuilder methodBody = new StringBuilder();
                     methodBody.append("{ ");
                     methodBody.append("Object[] params = $args; ");
                     // 调用before
                     methodBody.append("try { ");
                     methodBody.append(adviceFieldName)
                     		   .append(".before(params); ");
                     methodBody.append("} catch (Exception e) { ");
                     methodBody.append("e.printStackTrace(); ");
                     methodBody.append("} ");
                     // 获取原始方法的返回值类型,调用原始方法
                     if (!CtClass.voidType.equals(originalReturnType)) {
                         methodBody.append(originalReturnType.getName())
                         		   .append(" result = ")
                         		   .append(newMethodName)
                         		   .append("($$); ");
                     } else {
                         methodBody.append(newMethodName)
                         		   .append("($$); ");
                     }
                     // 调用after
                     methodBody.append("try { ");
                     methodBody.append(adviceFieldName)
                     				    .append(".after(params); ");
                     methodBody.append("} catch (Exception e) { ");
                     methodBody.append("e.printStackTrace(); ");
                     methodBody.append("} ");
                     // 如果原方法有返回值 那么将它返回出去
                     if (!CtClass.voidType.equals(originalReturnType)) {
                         methodBody.append("return result; ");
                     }
                     methodBody.append("} ");
                     // ==================组装方法体结束=================
                     // 更改原始方法名
                     originalMethod.setName(newMethodName);
                     // 复制一个原始方法的镜像方法,并将新的方法体填充到镜像方法体中
                     CtMethod copyMethod = CtNewMethod.copy(originalMethod, targetMethodName, ctClass, null);
                     copyMethod.setBody(methodBody.toString());
                     // 将新生成的方法添加到类中
                     ctClass.addMethod(copyMethod);
                 }
                 byte[] b = ctClass.toBytecode();
                 // 释放加载的CtClass内存
                 ctClass.detach();
                 return b;
             } catch (Exception e) {
                 e.printStackTrace();
             }
             return null;
         }
     }
    

    这里做增强的思路是把原class方法改成一个新名字,再添加一个原方法声明相同的方法,在新方法中依次调用Advice的before、原方法、Advice的after,如果原方法有返回值,在调用after后把结果返回出去。
    如果对 String 类的 split 方法 做 aop,那么String类大概会被改成这个样子:

     private final a.b.c.Advice split$advice = new a.b.c.AdviceImpl();
     
     public String[] split(String regex) {
     	Object[] params = new Object[]{regex}; 
     	try { 
     		split$advice.before(params);
     	} catch (Exception e) { 
     		e.printStackTrace(); 
     	} 
     	String[] result =  original$split(regex);
     	try { 
     		split$advice.after(params);
     	} catch (Exception e) { 
     		e.printStackTrace(); 
     	}
     	return result;
     }
     
     /**
     * 原来的 split 方法,现在已被更名
     */
     public String[] original$split(String regex) {
     	return split(regex, 0);
     }
    

原谅我并没有对String类的split方法做过测试,这里只是举一个栗子。
这个转换器目前不支持 构造方法、重载方法、抽象方法 转换,并且也没做校验、也算是不足之处吧。

  1. 接下来使以上的『一顿操作』生效
    在工程中加入 advice.properties 配置文件,配置如下内容:

     需要代理的类名.需要代理的方法名=AdviceImpl类全路径名
     例如 对 Test 类的 operation 方法 配置代理:
     	a.b.c.d.Test.operation=d.c.b.a.AdviceImpl
    

    工程中加入 Advice 实现类:

     public class AdviceImpl implements Advice {
         @Override
         public void before(Object[] params) {
             System.out.println("before");
         }
     
         @Override
         public void after(Object[] params) {
             System.out.println("after");
         }
     }
    

    工程的MANIFEST.MF 需要配置以下两项:

     Manifest-Version: 1.0
     Premain-Class: com.aop.premain.Premain
    

    如果用mvn打包,可以在pom中添加插件做到打包后自动加入上述配置:

     	<plugin>
     		<artifactId>maven-jar-plugin</artifactId>
     		<version>3.0.2</version>
     		<configuration>
     			<archive>
     				<manifestEntries>
     					<!-- 本篇开始写的那个Premain类的全名-->
     					<Premain-Class>com.aop.premain.Premain</Premain-Class>
     				</manifestEntries>
     			</archive>
     		</configuration>
     	</plugin>
    

    打包工程
    在工程的启动参数中添加:

     -javaagent:jar路径/test-aop.jar
    

这样,在工程启动后使用的Test类实例就是被修改过的了。

ps:-javaagent 配置的jar包可以是一个外部包含 premain 方法的jar,并且在外部jar中加入MANIFEST.MF配置。 在工程启动后会在这个jar中寻找 premain 方法并执行后续逻辑,得到的字节码加载在当前jvm中。这里为了方便,将本工程自己作为外部的 jar,自己 agent 自己。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值