使用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、存储方法名的方式比较复杂,实际用时,可以自行修改实现
不过我也仅仅是试验了下,如果有兴趣解决的小伙伴也可自己解决哦