使用javassist实现aop

1 篇文章 0 订阅

使用javassist实现aop

该博客仅为个人学习记录,采用javassist实现前置增强,后置增强以及异常增强,如果能帮到各位不胜荣幸,如果有不对,也可提出更改意见,整个过程中,最核心的部分为第四步:自定义实现类文件转换器

一、目录结构

java-agent-demo
│  
├─agent-api
│  ├─pom.xml
│  └─src
│      └─main
│          └─java
│              └─com.java.agent.api
│                          		  └─ Advice.java
│                                  
└─agent-demo
    ├─pom.xml
    └─src
        └─main
           └─java
               └─com.java.agent.demo
								   ├─MyAgent.java
								   │  
								   ├─transformer
								   │   		   └─ MyTransformer.java
								   └─util
										└─ StringUtil.java

二、maven依赖

父依赖:定义maven插件以及各种依赖版本

<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.learning.demo</groupId>
    <artifactId>java-agent-demo</artifactId>
    <version>1.0.0-SNAPSHOT</version>

    <packaging>pom</packaging>
    <url>http://maven.apache.org</url>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <modules>
        <module>agent-demo</module>
        <module>agent-api</module>
    </modules>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.javassist</groupId>
                <artifactId>javassist</artifactId>
                <version>3.26.0-GA</version>
            </dependency>
            <dependency>
                <groupId>junit</groupId>
                <artifactId>junit</artifactId>
                <version>3.8.1</version>
                <scope>test</scope>
            </dependency>
            <dependency>
                <groupId>org.slf4j</groupId>
                <artifactId>slf4j-api</artifactId>
                <version>1.7.6</version>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <version>2.4</version>
                <configuration>
                	<!-- jar包输出目录 -->
                    <outputDirectory>D:/</outputDirectory>
                    <finalName>${artifactId}</finalName>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <encoding>utf-8</encoding>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-shade-plugin</artifactId>
                <version>3.0.0</version>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>shade</goal>
                        </goals>
                        <configuration>
                            <createDependencyReducedPom>false</createDependencyReducedPom>
                            <transformers>
                                <transformer
                                        implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                                    <manifestEntries>
                                        <Premain-Class>com.java.agent.demo.MyAgent</Premain-Class>
                                    </manifestEntries>
                                </transformer>
                            </transformers>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

子依赖1:agent-demo(aop具体实现)

<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>com.learning.demo</groupId>
        <artifactId>java-agent-demo</artifactId>
        <version>1.0.0-SNAPSHOT</version>
    </parent>

    <artifactId>agent-demo</artifactId>
    <packaging>jar</packaging>

    <dependencies>
        <dependency>
            <groupId>org.javassist</groupId>
            <artifactId>javassist</artifactId>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
        </dependency>
    </dependencies>
</project>

子依赖2:agent-api(对外提供依赖,用户可自定义增强实现)

<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>com.learning.demo</groupId>
        <artifactId>java-agent-demo</artifactId>
        <version>1.0.0-SNAPSHOT</version>
    </parent>

    <artifactId>agent-api</artifactId>
    <version>1.0.0-SNAPSHOT</version>

</project>

三、对外提供的增强实现接口

该接口放在agent-api包中,供外部引入,并实现需要的的增强,其中所有方法均提供默认实现,只要需要重写,所需增强即可

package com.java.agent.api;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

/**
 * 增强接口
 *
 * @author zyc
 * @date 2021-10-10 14:46:52
 */
public interface Advice {

    /**
     * 前置增强
     *
     * @param args
     * @param obj
     * @param method
     * @return
     * @throws InvocationTargetException
     * @throws IllegalAccessException
     */
    default Object before(Object[] args, Object obj, Method method) throws InvocationTargetException, IllegalAccessException {
        return method.invoke(obj, args);
    }

    /**
     * 后置增强
     *
     * @param args
     * @param method
     * @param startTimeMs
     */
    default void after(Object[] args, Method method, long startTimeMs){
    }

    /**
     * 异常增强
     *
     * @param args
     * @param obj
     * @param method
     * @param e
     * @return
     */
    default Object exception(Object[] args, Object obj, Method method, Exception e) throws Exception {
        throw e;
    }
}

四、自定义实现类文件转换器

其中自定义方法体时涉及到的 $args、$sig 等特殊字符,具体释义请参考:https://www.jianshu.com/p/b9b3ff0e1bf8

package com.java.agent.demo.transformer;

import com.java.agent.demo.util.StringUtil;
import javassist.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.io.InputStream;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.reflect.Method;
import java.net.URL;
import java.security.ProtectionDomain;
import java.util.*;
import java.util.stream.Collectors;

/**
 * 自己实现的类文件转换器
 *
 * @author zyc
 * @date 2021-10-10 17:51:36
 */
public class MyTransformer implements ClassFileTransformer {
    private static final Logger LOGGER = LoggerFactory.getLogger(MyTransformer.class);

    /**
     * 类名与方法名之间的分割符
     * 如:com.xx.xx.MyTransformer#fillAllMethodByClassName
     */
    private static final String CLASS_SPLIT = "#";

    /**
     * 方法名与参数类型之间的分割符
     * 如:fillAllMethodByClassName@java.lang.String
     */
    private static final String METHOD_SPLIT = "@";


    /**
     * 被处理的方法列表
     * key:类名  value:该类下被增强的方法列表
     */
    private final static Map<String, Set<String>> METHOD_MAP = new HashMap<>();

    /**
     * 增强实现类
     */
    private static String agentAdvice = null;

    /**
     * 调用构造方法时加载配置文件
     */
    public MyTransformer() {

        // 搜索所有项目下的 agent.properties 配置文件
        URL resource = Thread.currentThread().getContextClassLoader().getResource("agent.properties");
        if (resource == null) {
            throw new RuntimeException("agent.properties cannot be found!");
        }

        //配置文件映射成Properties对象
        Properties properties = new Properties();
        try (InputStream inStream = resource.openStream();) {

            //从输入流中读取配置文件
            properties.load(inStream);
        } catch (IOException e) {
            throw new RuntimeException("agent.properties cannot be found!", e);
        }

        String agentClasses = properties.getProperty("agent.classes");
        String agentMethods = properties.getProperty("agent.methods");

        //如果未配置需要增强的类或方法,则跳过后续配置
        if (StringUtil.isEmpty(agentClasses) && StringUtil.isEmpty(agentMethods)) {
            return;
        }

        //获取增强实现类
        agentAdvice = properties.getProperty("agent.advice");

        String[] classes = agentClasses.split(",");
        for (String clazzUrl : classes) {
            fillAllMethodByClassName(clazzUrl);
        }

        String[] methods = agentMethods.split(",");
        for (String methodUrl : methods) {
            fillMethodByMethodName(methodUrl);
        }
    }

    /**
     * 核心实现步骤如下:
     * 1、根据老的方法名生成新的方法名      "queryUser" --> "queryUser$user"
     * 2、使用新的方法名copy出一个老的方法  queryUser ===> queryUser$user
     * 3、将老方法的方法实现改为代理实现    queryUser$user的实现为原方法的实现
     * 4、老方法的代理实现中调用copy出的   queryUser的实现改为bodyBuilder拼接出的新的实现
     * 5、将queryUser$user添加到原类中
     *
     * @param loader              定义要转换的类加载器;如果是引导加载器,则为 null
     * @param className           完全限定类内部形式的类名称和 The Java Virtual Machine Specification 中定义的接口名称。例如,"java/util/List"。
     * @param classBeingRedefined 如果是被重定义或重转换触发,则为重定义或重转换的类;如果是类加载,则为 null
     * @param protectionDomain    要定义或重定义的类的保护域
     * @param classfileBuffer     类文件格式的输入字节缓冲区(不得修改)
     * @return 一个格式良好的类文件缓冲区(转换的结果),如果未执行转换,则返回 null。
     * @throws IllegalClassFormatException 如果输入不表示一个格式良好的类文件
     */
    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {

        //javassist的包名是用点分割的,需要转换下
        className = className.replace("/", ".");

        // 判断加载的class的包路径是不是需要监控的类
        if (METHOD_MAP.containsKey(className)) {

            try {
                // 使用全称,用于取得字节码类<使用javassist>
                CtClass ctClass = ClassPool.getDefault().get(className);

                // 添加Advice到类的成员变量
                String adviceField = "private static final com.java.agent.api.Advice advice = new " + agentAdvice + "();\n";
                ctClass.addField(CtField.make(adviceField, ctClass));

                for (String methodName : METHOD_MAP.get(className)) {

                    //分割存储的methodName, 获取到实际方法名和方法参数列表
                    String[] methodInfo = methodName.split(METHOD_SPLIT);
                    String oldMethodName = methodInfo[0];
                    String oldMethodParams = methodInfo[1];

                    //得到原方法实例,遍历防止方法重载
                    CtMethod[] ctMethods = ctClass.getDeclaredMethods(oldMethodName);
                    for (CtMethod oldMethod : ctMethods) {

                        //比较参数类型
                        String ctmInfo = Arrays.stream(oldMethod.getParameterTypes())
                                .map(CtClass::getName)
                                .collect(Collectors.joining(","));

                        //参数类型一致,则新增代理方法
                        if (oldMethodParams.equals(ctmInfo)) {

                            //从老方法复制出来新的方法
                            String newMethodName = oldMethodName + "$new";
                            CtMethod newMethod = CtNewMethod.copy(oldMethod, newMethodName, ctClass, null);

                            //新的方法中调用原方法
                            StringBuilder bodyBuilder = new StringBuilder();
                            bodyBuilder.append("{\n")
                                    //获取参数列表
                                    .append("Object[] params = $args;\n")
                                    //获取新增的方法,即原实现方法
                                    .append("java.lang.reflect.Method method = this.getClass().getMethod(\"")
                                    .append(newMethodName)
                                    .append("\", $sig);\n")
                                    .append("long startTime = System.currentTimeMillis();\n")
                                    .append("try {\n")
                                    //调用前置增强
                                    .append("return advice.before(params, this, method);\n")
                                    .append("} catch (Exception e) {\n")
                                    //调用异常增强
                                    .append("return advice.exception(params, this, method, e);\n")
                                    .append("} finally {\n")
                                    //调用后置增强
                                    .append("advice.after(params, method, startTime);\n")
                                    .append("}\n}\n");

                            oldMethod.setBody(bodyBuilder.toString());
                            ctClass.addMethod(newMethod);
                            break;
                        }
                    }
                }

                byte[] b = ctClass.toBytecode();
                // 释放加载的CtClass内存
                ctClass.detach();
                return b;
            } catch (Exception e) {
                LOGGER.error("system is error!", e);
            }
        }
        return null;
    }

    /**
     * 从类中获取到所有的方法
     *
     * @param classUrl
     * @return
     */
    private static void fillAllMethodByClassName(String classUrl) {
        try {
            Class<?> clazz = Class.forName(classUrl);
            Method[] methods = clazz.getDeclaredMethods();

            Set<String> methodSet = Arrays.stream(methods)
                    .map(Method::getName)
                    .map(methodNames -> classUrl.concat(CLASS_SPLIT).concat(methodNames))
                    .collect(Collectors.toSet());

            for (String mName : methodSet) {
                fillMethodByMethodName(mName);
            }
        } catch (ClassNotFoundException e) {
            throw new RuntimeException(classUrl.concat(" cannot found! please check it!"), e);
        }
    }

    /**
     * 从方法中获取到需要增强的方法
     *
     * @param methodUrl
     * @return
     */
    private static void fillMethodByMethodName(String methodUrl) {

        //获取最后一个#号位置,#号之后表示方法名
        int splitIndex = methodUrl.lastIndexOf(CLASS_SPLIT);

        //如果#分隔符下标小于1,认为是格式有误
        if (splitIndex < 1) {
            throw new RuntimeException("agent.methods format is error! example:com.xxx.xxx.ClassName" + CLASS_SPLIT + "methodName");
        }

        String className = methodUrl.substring(0, splitIndex);
        String methodName = methodUrl.substring(splitIndex + 1);

        //查询出该类下所有与入参方法名一致的方法
        Class<?> clazz = null;
        try {
            clazz = Class.forName(className);
        } catch (ClassNotFoundException e) {
            throw new RuntimeException(className.concat(" cannot found! please check it!"), e);
        }

        Method[] methods = clazz.getDeclaredMethods();

        //避免方法重载时方法丢失的问题,方法名后拼接入参类名
        for (Method method : methods) {
            if (method.getName().equals(methodName)) {
                String paramsClazz = Arrays.stream(method.getParameterTypes())
                        .map(Class::getName)
                        .collect(Collectors.joining(","));

                METHOD_MAP.computeIfAbsent(className, k -> new HashSet<>())
                        .add(methodName.concat(METHOD_SPLIT).concat(paramsClazz));
            }
        }
    }
}

五、操作原理解释

经过了上面一系列的操作之后,就完成的方法的拷贝、重命名、添加新的属性、添加新的方法等操作,大致原理如下,以String.split(String regex) 方法举例吧

原方法是这样的:

public String[] split(String regex) {
	return split(regex, 0);
}

1、我们先添加了一个静态属性,如下

private static final com.java.agent.api.Advice advice = new com.web.demo.aspect.AdviceImpl();

2、之后我们copy出了一个新的方法split$new,这是String中出现以下情况:

public String[] split(String regex) {
	return split(regex, 0);
}

public String[] split$new(String regex) {
	return split(regex, 0);
}

3、再然后我们将原方法的方法体进行了改变,变成了如下情况:

public String[] split(String regex) {
	Object[] params = $args;
	java.lang.reflect.Method method = this.getClass().getMethod("split$new", $sig);
	long startTime = System.currentTimeMillis();
	try {
		return advice.before(params, this, method);
	} catch (Exception e) {
		return advice.exception(params, this, method, e);
	} finally {
		advice.after(params, method, startTime);
	}
}

public String[] split$new(String regex) {
	return split(regex, 0);
}

4、此时目标类以及目标方法均已修改完毕啦

六、自定义一个类新增静态方法

package com.java.agent.demo;

import com.java.agent.demo.transformer.MyTransformer;

import java.lang.instrument.Instrumentation;

/**
 * @author zyc
 * @date 2021-10-09 15:41:18
 */
public class MyAgent {

    /**
     * JVM 首先尝试在代理类上调用以下方法
     */
    public static void premain(String agentArgs, Instrumentation inst) {
        inst.addTransformer(new MyTransformer());
    }
}

七、其他工具类

保证agent不再依赖其他的三方依赖,所以自定义了一个简单实现

package com.java.agent.demo.util;

/**
 * String工具类
 *
 * @author zyc
 * @date 2021-10-09 14:20:36
 */
public class StringUtil {
    public static final String EMPTY = "";

    /**
     * 判断字符串是否为空
     *
     * @param str
     * @return
     */
    public static boolean isEmpty(String str) {
        return str == null || str.length() < 1;
    }


    /**
     * 判断单词首字母是否为大写
     *
     * @return
     */
    public static boolean isHeadUpperCase(String word) {
        if (isEmpty(word)) {
            return false;
        }

        int asciiNum = word.toCharArray()[0];
        return asciiNum > 64 && asciiNum < 91;
    }
}

八、生成agent-demo的jar包

命令行中执行如下命令即可:

mvn clean install -Dmaven.test.skip=true

注意:我的demo中,将打成的jar输出到了D:/ 下,你们也可以自定义你们的输出目录

九、服务中使用

1、服务中引入maven依赖

服务端引入agent提供的增强实现依赖

<dependency>
   <groupId>com.learning.demo</groupId>
    <artifactId>agent-api</artifactId>
    <version>1.0.0-SNAPSHOT</version>
</dependency>

2、服务中resources下新增配置文件

文件名必须是:agent.properties(因为MyTransformer 类的构造方法中找的文件为该名称)

内容格式如下:

#设置增强实现类
agent.advice=com.web.demo.aspect.AdviceImpl

#需要被增强的类
agent.classes=com.web.demo.controller.TestController

#需要被增强的方法
agent.methods=com.web.demo.controller.ZycController#queryDelayList

3、自定义增强实现

package com.web.demo.aspect;

import com.alibaba.fastjson.JSONObject;
import com.java.agent.api.Advice;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

/**
 * 自定义的增强实现
 */
public class AdviceImpl implements Advice {
    private static final Logger LOGGER = LoggerFactory.getLogger(AdviceImpl.class);


    @Override
    public Object before(Object[] args, Object obj, Method method) throws InvocationTargetException, IllegalAccessException {
        LOGGER.info("执行前置增强==》, methodName:{}", method.getName());
        return method.invoke(obj, args);
    }

    @Override
    public void after(Object[] args, Method method, long startTimeMs) {
        LOGGER.info("执行后置增强==》, methodName:{}, cost:{}ms", method.getName(), System.currentTimeMillis() - startTimeMs);
    }

    @Override
    public Object exception(Object[] args, Object obj, Method method, Exception e) throws Exception {
        LOGGER.warn("执行异常增强==》, methodName:" + method.getName() + " ,", e);

        JSONObject result = new JSONObject();
        result.put("code", -1);
        result.put("msg", "出现了异常");
        return result;
    }
}

4、添加启动参数

如果在IDEA中运行,项目的启动参数是添加在:Run Configuration – VM options 中

-javaagent:D:\agent-demo.jar=agentargs 

5、测试

调用被增强的方法,看打印的日志信息
输出如下:

2021-10-11 20:50:57.228 [http-nio-8080-exec-1] INFO  com.web.demo.aspect.AdviceImpl[before:19] -执行前置增强==, methodName:testAjax$new
2021-10-11 20:50:57.260 [http-nio-8080-exec-1] INFO  com.web.demo.controller.TestController[testAjax$new:33] -入参参数为:{"test":"测试"}
2021-10-11 20:51:06.862 [http-nio-8080-exec-1] WARN  com.web.demo.aspect.AdviceImpl[exception:30] -执行异常增强==, methodName:testAjax$new ,
java.lang.reflect.InvocationTargetException: null
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at com.web.demo.aspect.AdviceImpl.before(AdviceImpl.java:20)
	at com.web.demo.controller.TestController.testAjax(TestController.java)
	......
Caused by: java.lang.NullPointerException: null
	at com.web.demo.controller.TestController.testAjax$new(TestController.java:36)
	... 60 common frames omitted
2021-10-11 20:51:06.863 [http-nio-8080-exec-1] INFO  com.web.demo.aspect.AdviceImpl[after:25] -执行后置增强==, methodName:testAjax$new, cost:5ms

从结果上看,已经执行了增强,差不多大功告成了,但是还是有一些小的瑕疵吧,比如:1、Advice接口入参的Method的对象的名称为 xx$new
2、METHOD_MAP 这个常量可以进行缓存
3、存储方法名的方式比较复杂,实际用时,可以自行修改实现
不过我也仅仅是试验了下,如果有兴趣解决的小伙伴也可自己解决哦

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值