一、前言
Javassist 是一个开源,用于分析、编辑和创建Java字节码的类库,由东京工业大学数学和计算机科学系的 Shigeru Chiba (千叶滋)所创建。目前已加入了开放源代码JBoss 应用服务器项目,通过使用Javassist对字节码操作为JBoss实现动态"AOP"框架。
通过使用Javassist可以使Java程序在运行时定义一个新的类,并且在JVM加载类文件时修改它
Javassist提供两个级别的API:源码级别和字节码级别。
如果使用源码级的API,开发人员可以在不知道Java字节码的情况下编辑Java类文件,就像我们编写Java源代码一样方便。如果使用字节码级别的API,那么需要详细了解Java字节码和类文件格式,因为字节码级别的API允许我们对类文件进行任意修改。
官网地址:Javassist官方教程
二、Javassist 中几个重要的类
在使用javassist进行编码之前,有必要对javassist理论知识做一个全面的了解和学习;
Javassist 中最为重要的是 ClassPool,CtClass ,CtMethod 以及 CtField 这几个类;
- ClassPool:基于Hashtable 实现的CtClass 对象容器,其中键是类名称,值是表示该类的 CtClass 对象;
- CtClass:CtClass 表示类,一个 CtClass (编译时类)对象可以处理一个 class 文件,这些 CtClass 对象可以从 ClassPool 获得;
- CtMethods:表示类中的方法;
- CtFields :表示类中的字段;
ClassPool
CtClass对象的容器,常用的API如下:
- getDefault () —— 返回默认的ClassPool ,单例模式,一般通过该方法创建ClassPool;
- appendClassPath(ClassPath cp), insertClassPath(ClassPath cp) —— 将一个ClassPath加到类搜索路径末尾位置,或插入到起始位置。通过该方法写入额外的类搜索路径,以解决多个类加载器环境中找不到类问题;
- importPackage(String packageName) —— 导入包;
- makeClass(String classname) —— 创建一个空类,里面没有变量或方法,后面通过CtClass函数进行添加;
- get(String classname)、getCtClass(String classname) —— 根据类路径名获取该类的CtClass对象,用于后续编辑;
1、获取 ClassPool 对象操作
-
// 获取 ClassPool 对象,使用系统默认类路径
-
ClassPool
pool
=
new
ClassPool(
true);
-
// 效果与 new ClassPool(true) 一致
-
ClassPool
pool1
= ClassPool.getDefault();
2、获取类操作
-
// 通过类名获取 CtClass,未找到会抛出异常
-
CtClass
ctClass
= pool.get(
"com.congge.service.DemoService");
-
// 通过类名获取 CtClass,未找到返回 null,不会抛出异常
-
CtClass
ctClass1
= pool.getOrNull(
"com.congge.service.DemoService");
3、 创建新类操作
-
// 复制一个类,创建一个新类
-
CtClass
ctClass2
= pool.getAndRename(
"com.congge.DemoService",
"com.congge.DemoCopyService");
-
// 通过类名,创建一个新类
-
CtClass
ctClass3
= pool.makeClass(
"com.congge.NewDemoService");
-
// 通过文件流,创建一个新类,注意文件必须是编译后的 class 文件,不是源代码文件。
-
CtClass
ctClass4
= pool.makeClass(
new
FileInputStream(
new
File(
"./customize/DemoBeforeHandler.class")));
CtClass
通过 CtClass 对象,开发人员可以得到很多关于类的信息,就可以对类进行修改等操作,常用的API如下:
- debugDump;String类型,如果生成。class文件,保存在这个目录下;
- setName(String name):给类重命名;
- setSuperclass(CtClass clazz):设置父类;
- addField(CtField f, Initializer init):添加字段(属性),初始值见CtField;
- addMethod(CtMethod m):添加方法(函数);
- toBytecode(): 返回修改后的字节码。需要注意的是一旦调用该方法,则无法继续修改CtClass;
- toClass(): 将修改后的CtClass加载至当前线程的上下文类加载器中,CtClass的
toClass
方法是通过调用本方法实现。需要注意的是一旦调用该方法,则无法继续修改已经被加载的CtClass; - writeFile(String directoryName):根据CtClass生成
.class
文件; - defrost():解冻类,用于使用了toclass()、toBytecode、writeFile(),类已经被JVM加载,Javassist冻结CtClass后;
- detach():避免内存溢出,从ClassPool中移除一些不需要的CtClass;
获取类属性
-
// 类名
-
String
simpleName
= ctClass.getSimpleName();
-
// 类全名
-
String
name
= ctClass.getName();
-
// 包名
-
String
packageName
= ctClass.getPackageName();
-
// 接口
-
CtClass[] interfaces = ctClass.getInterfaces();
-
// 继承类
-
CtClass
superclass
= ctClass.getSuperclass();
-
// 获取字节码文件,可以通过 ClassFile 对象进行字节码级操作
-
ClassFile
classFile
= ctClass.getClassFile();
-
// 获取带参数的方法,第二个参数为参数列表数组,类型为 CtClass
-
CtMethod
ctMethod
= ctClass.getDeclaredMethod(
"selectOrder",
new
CtClass[] {pool.get(String.class.getName()), pool.get(String.class.getName())});
-
// 获取字段
-
CtField
ctField
= ctClass.getField(
"salary");
类型判断
-
// 判断数组类型
-
ctClass.isArray();
-
// 判断原生类型
-
ctClass.isPrimitive();
-
// 判断接口类型
-
ctClass.isInterface();
-
// 判断枚举类型
-
ctClass.isEnum();
-
// 判断注解类型
-
ctClass.isAnn
添加类属性
-
// 添加接口
-
ctClass.addInterface(...);
-
// 添加构造器
-
ctClass.addConstructor(...);
-
// 添加字段
-
ctClass.addField(...);
-
// 添加方法
-
ctClass.addMethod(...);
编译类
-
// 编译成字节码文件,使用当前线程上下文类加载器加载类,如果类已存在或者编译失败将抛出异常
-
Class
clazz
= ctClass.toClass();
-
// 编辑成字节码文件,返回 byte 数组
-
byte[] bytes = ctClass.toBytecode();
CtMethod
方法相关相关,常用的API如下:
- insertBefore(String src) —— 在方法的起始位置插入代码;
- insertAfter(String src) —— 在方法的所有 return 语句前插入代码以确保语句能够被执行,除非遇到exception;
- insertAt(int lineNum, String src): —— 在指定的位置插入代码;
- addCatch(String src, CtClass exceptionType) —— 将方法内语句作为try的代码块,插入catch代码块src;
- setBody(String src) —— 将方法的内容设置为要写入的代码,当方法被 abstract修饰时,该修饰符被移除;
- setModifiers(int mod) —— 设置访问级别,一般使用Modifier调用常量;
- invoke(Object obj, Object... args) —— 反射调用字节码生成类的方法;
获取方法属性
-
CtClass
ctClass5
= pool.get(TestService.class.getName());
-
CtMethod
ctMethod
= ctClass5.getDeclaredMethod(
"selectOrder");
-
// 方法名
-
String
methodName
= ctMethod.getName();
-
// 返回类型
-
CtClass
returnType
= ctMethod.getReturnType();
-
// 方法参数,通过此种方式得到方法参数列表 格式:com.congge.UserService.selectUser(java.lang.String,java.util.List,com.entity.User)
-
ctMethod.getLongName();
-
// 方法签名 格式:(Ljava/lang/String;Ljava/util/List)Ljava/lang/Integer;
-
ctMethod.getSignature();
-
-
// 获取方法参数名称,可以通过这种方式得到方法真实参数名称
-
List<String> argKeys =
new
ArrayList<>();
-
MethodInfo
methodInfo
= ctMethod.getMethodInfo();
-
CodeAttribute
codeAttribute
= methodInfo.getCodeAttribute();
-
LocalVariableAttribute
attr
= (LocalVariableAttribute) codeAttribute.getAttribute(LocalVariableAttribute.tag);
-
int
len
= ctMethod.getParameterTypes().length;
-
// 非静态的成员函数的第一个参数是this
-
int
pos
= Modifier.isStatic(ctMethod.getModifiers()) ?
0 :
1;
-
for (
int
i
= pos; i < len; i++) {
-
argKeys.add(attr.variableName(i));
-
}
方法操作
-
// 在方法体前插入代码块
-
ctMethod.insertBefore(
"");
-
// 在方法体后插入代码块
-
ctMethod.insertAfter(
"");
-
// 在某行 字节码 后插入代码块
-
ctMethod.insertAt(
10,
"");
-
// 添加参数
-
ctMethod.addParameter(CtClass);
-
// 设置方法名
-
ctMethod.setName(
"newName");
-
// 设置方法体
-
ctMethod.setBody(
"");
对于setBody $0代表this $1、$2、...代表方法的第几个参数,$符号含义总结如下:
符号 | 含义 |
---|---|
$0, $1, $2, ... | this,第几个参数 |
$args | 参数列表. $args的类型是Object[]. |
$$ | 所有实参.例如, m($$) 等价于 m($1,$2,...) |
$cflow(...) | cflow变量 |
$r | 结果类型. 用于表达式转换. |
$w | 包装类型. 用于表达式转换. |
$_ | 结果值 |
$sig | java.lang.Class列表,代表正式入参类型 |
$type | java.lang.Class对象,代表正式入参值. |
$class | java.lang.Class对象,代表传入的代码段. |
CtField
字段相关,常用的API如下:
- CtField(CtClass type, String name, CtClass declaring) —— 构造函数,添加字段类型,名称,所属的类;
- CtField.Initializer constant() —— CtClass使用addField时初始值的设置;
- setModifiers(int mod) —— 设置访问级别,一般使用Modifier调用常量;
Javassist API操作综合使用案例
导入依赖
-
<dependency>
-
<groupId>org.javassist</groupId>
-
<artifactId>javassist</artifactId>
-
<version>
3.27
.0-GA</version>
-
</dependency>
1、使用javassist创建类
-
public
static
void
main
(String[] args)
throws Exception {
-
ClassPool
pool
=
new
ClassPool(
true);
-
CtClass
targetClass
= pool.get(
"com.congge.test.HelloServiceImpl");
-
CtMethod
method
= targetClass.getDeclaredMethod(
"sayHello");
-
// 复制方法生成一个新的代理方法
-
CtMethod
agentMethod
= CtNewMethod.copy(method, method.getName()+
"$agent", targetClass,
null);
-
agentMethod.setModifiers(Modifier.PRIVATE);
-
// 添加方法
-
targetClass.addMethod(agentMethod);
-
// 构建新的方法体,并使用代理方法
-
String
source
=
"{"
-
+
"System.out.println(\"before handle > ...\" + $type);"
-
+ method.getName() +
"$agent($$);"
-
+
"System.out.println(\"after handle ...\");"
-
+
"}"
-
;
-
// 设置方法体
-
method.setBody(source);
-
targetClass.toClass();
-
IHello
hello
=
new
HelloServiceImpl();
-
hello.sayHello(
"javassist");
-
}
运行上面的代码,观察输出结果,通过该案例就动态创建出了一个接口实现类
2、创建代理方法
-
import javassist.*;
-
-
public
class
JavaSisstWord {
-
-
public
static
void
main
(String[] args)
throws NotFoundException, CannotCompileException, IllegalAccessException, InstantiationException {
-
ClassPool
pool
=
new
ClassPool(
true);
-
pool.insertClassPath(
new
LoaderClassPath(JavaSisstWord.class.getClassLoader()));
-
-
//构建一个新的类
-
CtClass
targetClass
= pool.makeClass(
"com.congge.hello");
-
targetClass.addInterface(pool.get(IHello.class.getName()));
-
-
//将方法添加进去
-
CtClass
returnType
= pool.get(
void.class.getName());
-
String
name
=
"sayHello";
-
CtClass[] parameters =
new
CtClass[]{pool.get(String.class.getName())};
-
CtMethod
method
=
new
CtMethod(returnType,name,parameters,targetClass);
-
String
src
=
"{System.out.println(\"hello :\" + $1);}";
-
method.setBody(src);
-
targetClass.addMethod(method);
-
-
//装载class
-
Class
aClass
= targetClass.toClass();
-
IHello
hello
= (IHello) aClass.newInstance();
-
hello.sayHello(
"新的class的参数");
-
}
-
-
public
interface
IHello{
-
void
sayHello
(String name);
-
}
-
-
}
可以结合下面这张图总结一下javassist的运行流程
三、Javaagent
在上一篇,用较大的篇幅总结了javaagent的使用,java agent使用 ,对于Java 程序员来说,Java Intrumentation、Java agent 这些技术可能平时接触的很少。事实上,在我们日常开发中接触到的各种工具中,有很多都是基于javaagent原理实现的,如热部署(JRebel, spring-loaded)、IDE debug、各种线上诊断工具(btrace,Arthas,skywalking)等。
java agent实现技术也很多,比如本篇接下去要讲的javassist,asm等,都是可以实现的,关于java agent,先介绍几个重要的底层接口类;
Instrumentation
使用 java.lang.instrument.Instrumentation,开发者可以构建一个独立于应用程序的代理程序(Agent),用来监测和协助运行在 JVM 上的程序,甚至能够替换和修改某些类的定义。
有了这样的功能,开发者就可以实现更为灵活的运行时虚拟机监控和 Java 类操作,这样的特性实际上提供了一种虚拟机级别支持的 AOP 实现方式,使得开发者无需对 JDK 做任何升级和改动,就可以实现某些 AOP 的功能了。
Instrumentation 的最大作用,就是类定义动态改变和操作
Instrumentation的一些主要方法如下:
-
public
interface
Instrumentation {
-
/**
-
* 注册一个Transformer,从此之后的类加载都会被 transformer 拦截。
-
* ClassFileTransformer 的 transform 方法可以直接对类的字节码进行修改,但是只能修改方法体,不能变更方法签名、增加和删除方法/类的成员属性
-
*/
-
void
addTransformer
(ClassFileTransformer transformer);
-
-
/**
-
* 对JVM已经加载的类重新触发类加载,使用上面注册的 ClassFileTransformer 重新对类进行修饰。
-
*/
-
void
retransformClasses
(Class<?>... classes)
throws UnmodifiableClassException;
-
-
/**
-
* 重新定义类,不是使用 transformer 修饰,而是把处理结果(bytecode)直接给JVM。
-
* 调用此方法同样只能修改方法体,不能变更方法签名、增加和删除方法/类的成员属性
-
*/
-
void
redefineClasses
(ClassDefinition... definitions)
throws ClassNotFoundException, UnmodifiableClassException;
-
-
/**
-
* 获取一个对象的大小
-
*/
-
long
getObjectSize
(Object objectToSize);
-
-
/**
-
* 将一个jar加入到bootstrap classloader 的 classpath 里
-
*/
-
void
appendToBootstrapClassLoaderSearch
(JarFile jarfile);
-
-
/**
-
* 将一个jar加入到 system classloader 的 classpath 里
-
*/
-
void
appendToSystemClassLoaderSearch
(JarFile jarfile);
-
-
/**
-
* 获取当前被JVM加载的所有类对象
-
*/
-
Class[] getAllLoadedClasses();
-
}
Javaagent
- Java agent 是一种特殊的Java程序(Jar文件),它是 Instrumentation 的客户端具体实现;
- 与普通 Java 程序通过main方法启动不同,agent 并不是一个可以单独启动的程序,而必须依附在一个Java应用程序(JVM)上,与它运行在同一个进程中,通过 Instrumentation API 与虚拟机交互;
- Java agent 与 Instrumentation 密不可分,二者也需要在一起使用。因为JVM 会把 Instrumentation 的实例会作为参数注入到 Java agent 的启动方法中。因此如果想使用 Instrumentation 功能,拿到 Instrumentation 实例,我们必须通过Java agent;
Java agent 有两个启动时机,一个是在程序启动时通过 -javaagent 参数启动代理程序,另一个是在程序运行期间通过 Java Tool API 中的 attach api 动态启动代理程序;
JVM启动时静态加载
对于JVM启动时加载的 agent,Instrumentation 会通过 premain 方法传入代理程序,premain 方法会在程序 main 方法执行之前被调用。
此时大部分Java类都没有被加载(“大部分”是因为,agent类本身和它依赖的类还是无法避免的会先加载的),是一个对类加载埋点做手脚(addTransformer)的好机会。但这种方式有很大的局限性,Instrumentation 仅限于 main 函数执行前,此时有很多类还没有被加载,如果想为其注入 Instrumentation 就无法办到。
这种方式的应用:例如在 IDEA 启动 debug 模式时,就是以 -javaagent 的形式启动 debug 代理程序实现的
-
/**
-
* agentArgs 是 premain 函数得到的程序参数,通过 -javaagent 传入。这个参数是个字符串,如果程序参数有多个,需要程序自行解析这个字符串。
-
* inst 是一个 java.lang.instrument.Instrumentation 的实例,由 JVM 自动传入。
-
*/
-
public
static
void
premain
(String agentArgs, Instrumentation inst) {
-
-
}
-
-
/**
-
* 带有 Instrumentation 参数的 premain 优先级高于不带此参数的 premain。
-
* 如果存在带 Instrumentation 参数的 premain,不带此参数的 premain 将被忽略。
-
*/
-
public
static
void
premain
(String agentArgs) {
-
-
}
如下面这段代码,按照上一篇文章,将MyPreMainAgent 配置并打包后,其他类启动参数配置了这个jar就会先于方法输出这段结果;
-
public
class
MyPreMainAgent {
-
-
public
static
void
premain
(String agentArgs, Instrumentation inst) {
-
System.out.println(
"hello javaAgent");
-
}
-
-
}
JVM 启动后动态加载
对于VM启动后动态加载的 agent,Instrumentation 会通过 agentmain 方法传入代理程序,agentmain 在 main 函数开始运行后才被调用;
这种方式,比如在使用 Arthas 进行诊断线上问题时,通过 attach api,来动态加载代理程序到目标VM;
-
/**
-
* agentArgs 是 agentmain 函数得到的程序参数,在 attach 时传入。这个参数是个字符串,如果程序参数有多个,需要程序自行解析这个字符串。
-
* inst 是一个 java.lang.instrument.Instrumentation 的实例,由 JVM 自动传入。
-
*/
-
public
static
void
agentmain
(String agentArgs, Instrumentation inst) {
-
-
}
-
-
/**
-
* 带有 Instrumentation 参数的 agentmain 优先级高于不带此参数的 agentmain。
-
* 如果存在带 Instrumentation 参数的 agentmain,不带此参数的 agentmain 将被忽略。
-
*/
-
public
static
void
agentmain
(String agentArgs) {
-
-
}
MANIFEST.MF
编写好的代理类想要运行,在打 jar 包前,还需在 MANIFEST.MF 中指定代理程序入口(当然,也可以在maven的pom文件中进行插件化形式的配置,效果类似);
大多数 JAR 文件会包含一个 META-INF 目录,它用于存储包和扩展的配置数据,如安全性和版本信息。其中会有一个 MANIFEST.MF 文件,该文件包含了该 Jar 包的版本、创建人和类搜索路径等信息,如果是可执行Jar 包,会包含Main-Class属性,表明 Main 方法入口;
例如下面是通过 mvn clean package 命令打包后的 Jar 包中的 MANIFEST.MF 文件,从中可以看出 jar 的版本、创建者、SpringBoot 版本、程序入口、类搜索路径等信息。
其中涉及到与agent相关的参数
- Premain-Class:JVM 启动时指定了代理,此属性指定代理类,即包含 premain 方法的类;
- Agent-Class:JVM动态加载代理,此属性指定代理类,即包含 agentmain 方法的类;
- Boot-Class-Path:设置引导类加载器搜索的路径列表,列表中的路径由一个或多个空格分开;
- Can-Redefine-Classes:布尔值(true 或 false)。是否能重定义此代理所需的类;
- Can-Retransform-Classes:布尔值(true 或 false)。是否能重转换此代理所需的类;
- Can-Set-Native-Method-Prefix:布尔值(true 或 false)。是否能设置此代理所需的本机方法前缀;
四、基于javassit实现对coontroller层的监控
通常在实际的业务开发中,我们可能会碰到类似下面这样的需求
- 拦截指定包下的所有业务类,进行方法参数合规性校验;
- 对特定的接口请求进行限流;
- 对特定的方法进行参数的日志审计;
- ...
遇到这样的需求,很多同学第一反应大多会想到AOP,没毛病,使用aop来解决这个问题是个不错的思路,但还是那句话,有了javaagent之后,可以尽可能的让开发人员少改动现有的代码,接下来,考虑下如果在业务中要实现对某个controller进行参数,返回值的监控,该如何做呢?接下来看完整的实现步骤;
1、导入相关依赖
-
<dependencies>
-
-
<dependency>
-
<groupId>junit</groupId>
-
<artifactId>junit</artifactId>
-
<version>
4.12</version>
-
</dependency>
-
-
<dependency>
-
<groupId>org.springframework.boot</groupId>
-
<artifactId>spring-boot-starter-web</artifactId>
-
</dependency>
-
-
<dependency>
-
<groupId>org.javassist</groupId>
-
<artifactId>javassist</artifactId>
-
<version>
3.27
.0-GA</version>
-
</dependency>
-
-
<dependency>
-
<groupId>com.alibaba</groupId>
-
<artifactId>fastjson</artifactId>
-
<version>
1.2
.67</version>
-
</dependency>
-
-
</dependencies>
-
-
<build>
-
<plugins>
-
<plugin>
-
<groupId>org.apache.maven.plugins</groupId>
-
<artifactId>maven-jar-plugin</artifactId>
-
<version>
3.1
.0</version>
-
<configuration>
-
<archive>
-
<manifest>
-
<addClasspath>
true</addClasspath>
-
</manifest>
-
<manifestEntries>
-
<Premain-Class>com.congge.agent.jvm.AgentMain2</Premain-Class>
-
<Can-Redefine-Classes>
true</Can-Redefine-Classes>
-
<Can-Retransform-Classes>
true</Can-Retransform-Classes>
-
<Can-Set-Native-Method-Prefix>
true</Can-Set-Native-Method-Prefix>
-
</manifestEntries>
-
</archive>
-
</configuration>
-
</plugin>
-
</plugins>
-
</build>
2、提供一个测试用的接口类
-
import org.springframework.web.bind.annotation.GetMapping;
-
import org.springframework.web.bind.annotation.RestController;
-
-
@RestController
-
public
class
UserController {
-
-
@GetMapping("/queryUserInfo")
-
public String
queryUserInfo
(String userId){
-
return
"hello :" + userId;
-
}
-
-
}
3、编写agent类
-
import java.io.IOException;
-
import java.lang.instrument.ClassFileTransformer;
-
import java.lang.instrument.IllegalClassFormatException;
-
import java.lang.instrument.Instrumentation;
-
import java.security.ProtectionDomain;
-
import java.util.ArrayList;
-
import java.util.HashSet;
-
import java.util.List;
-
import java.util.Set;
-
-
public
class
AgentMain2 {
-
-
private
static
final Set<String> classNameSet =
new
HashSet<>();
-
-
static {
-
classNameSet.add(
"com.congge.controller.UserController");
-
}
-
-
public
static
void
premain
(String agentArgs, Instrumentation instrumentation) {
-
-
final
ClassPool
pool
=
new
ClassPool();
-
pool.appendSystemPath();
-
-
//基于工具,在运行的时候修改class字节码,即动态插装
-
instrumentation.addTransformer(
new
ClassFileTransformer() {
-
@Override
-
public
byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain,
byte[] classfileBuffer)
throws IllegalClassFormatException {
-
-
String
currentClassName
= className.replaceAll(
"/",
".");
-
if (!classNameSet.contains(currentClassName)) {
// 提升classNameSet中含有的类
-
return
null;
-
}
-
-
if(classNameSet.contains(currentClassName)){
-
// 获取类
-
//CtClass ctClass = ClassPool.getDefault().get(currentClassName);
-
CtClass
ctClass
=
null;
-
try {
-
ctClass = pool.getDefault().get(currentClassName);
-
}
catch (NotFoundException e) {
-
e.printStackTrace();
-
}
-
String
clazzName
= ctClass.getName();
-
-
// 获取方法
-
CtMethod
ctMethod
=
null;
-
try {
-
ctMethod = ctClass.getDeclaredMethod(
"queryUserInfo");
-
}
catch (NotFoundException e) {
-
e.printStackTrace();
-
}
-
String
methodName
= ctMethod.getName();
-
-
// 方法信息:methodInfo.getDescriptor();
-
MethodInfo
methodInfo
= ctMethod.getMethodInfo();
-
-
// 方法:入参信息
-
CodeAttribute
codeAttribute
= methodInfo.getCodeAttribute();
-
LocalVariableAttribute
attr
= (LocalVariableAttribute) codeAttribute.getAttribute(LocalVariableAttribute.tag);
-
CtClass[] parameterTypes =
new
CtClass[
0];
-
try {
-
parameterTypes = ctMethod.getParameterTypes();
-
}
catch (NotFoundException e) {
-
e.printStackTrace();
-
}
-
-
-
boolean
isStatic
= (methodInfo.getAccessFlags() & AccessFlag.STATIC) !=
0;
// 判断是否为静态方法
-
int
parameterSize
= isStatic ? attr.tableLength() : attr.tableLength() -
1;
// 静态类型取值
-
List<String> parameterNameList =
new
ArrayList<>(parameterSize);
// 入参名称
-
List<String> parameterTypeList =
new
ArrayList<>(parameterSize);
// 入参类型
-
StringBuilder
parameters
=
new
StringBuilder();
// 参数组装;$1、$2...,$$可以获取全部,但是不能放到数组初始化
-
-
for (
int
i
=
0; i < parameterSize; i++) {
-
parameterNameList.add(attr.variableName(i + (isStatic ?
0 :
1)));
// 静态类型去掉第一个this参数
-
parameterTypeList.add(parameterTypes[i].getName());
-
if (i +
1 == parameterSize) {
-
parameters.append(
"$").append(i +
1);
-
}
else {
-
parameters.append(
"$").append(i +
1).append(
",");
-
}
-
}
-
-
// 方法:出参信息
-
CtClass
returnType
=
null;
-
try {
-
returnType = ctMethod.getReturnType();
-
}
catch (NotFoundException e) {
-
e.printStackTrace();
-
}
-
String
returnTypeName
= returnType.getName();
-
-
// 方法:生成方法唯一标识ID
-
int
idx
= Monitor.generateMethodId(clazzName, methodName, parameterNameList, parameterTypeList, returnTypeName);
-
-
// 定义属性
-
try {
-
ctMethod.addLocalVariable(
"startNanos", CtClass.longType);
-
}
catch (CannotCompileException e) {
-
e.printStackTrace();
-
}
-
try {
-
ctMethod.addLocalVariable(
"parameterValues", ClassPool.getDefault().get(Object[].class.getName()));
-
}
catch (CannotCompileException e) {
-
e.printStackTrace();
-
}
catch (NotFoundException e) {
-
e.printStackTrace();
-
}
-
-
// 方法前加强
-
try {
-
ctMethod.insertBefore(
"{ startNanos = System.nanoTime(); parameterValues = new Object[]{" + parameters.toString() +
"}; }");
-
}
catch (CannotCompileException e) {
-
e.printStackTrace();
-
}
-
-
// 方法后加强
-
try {
-
ctMethod.insertAfter(
"{ com.congge.agent.jvm.Monitor.point(" + idx +
", startNanos, parameterValues, $_);}",
false);
// 如果返回类型非对象类型,$_ 需要进行类型转换
-
}
catch (CannotCompileException e) {
-
e.printStackTrace();
-
}
-
-
// 方法;添加TryCatch
-
try {
-
ctMethod.addCatch(
"{ com.congge.agent.jvm.Monitor.point(" + idx +
", $e); throw $e; }", ClassPool.getDefault().get(
"java.lang.Exception"));
// 添加异常捕获
-
}
catch (CannotCompileException e) {
-
e.printStackTrace();
-
}
catch (NotFoundException e) {
-
e.printStackTrace();
-
}
-
-
try {
-
return ctClass.toBytecode();
-
}
catch (IOException e) {
-
e.printStackTrace();
-
}
catch (CannotCompileException e) {
-
e.printStackTrace();
-
}
-
}
-
return
null;
-
}
-
});
-
-
}
-
-
}
该类的主要实现思路,就是重写premain方法,并覆盖其中的instrumentation的实现,在instrumentation的实现中,充分利用javassist提供的相关API,拦截并获取目标UserController的方法的参数,以及执行结果;
4、将上面的agent所在的类配置到pom下并打包
5、启动springboot工程并在VM中配置如下参数
-javaagent:E:\code-self\spi\java-agent\target\java-agent-1.0-SNAPSHOT.jar=com.congge.agent.User
6、测试结果
启动完成后,浏览器访问下接口,并观察控制台输出结果;
通过控制台结果输出,在agent中需要监控拦截的信息就可以拿到了,那么拿到这些信息之后,理论上来说,就可以做更多的事情了,比如,上报异常参数,执行结果等等;
本段代码中,逻辑是写在一起的,而且只监控了UserController这一个类,在实际开发中,可以通过更灵活的方式去做,比如写到配置文件读取,通过自定义注解,或者扫描某个包路径等等;
代码优化改进
按照上面的思路,为了让这段代码更具通用性,我们可以直针对特定注解的类进行监控,同时对这样的目标类下的所有方法进行拦截,改进后的代码如下:
-
import javassist.*;
-
import javassist.bytecode.AccessFlag;
-
import javassist.bytecode.CodeAttribute;
-
import javassist.bytecode.LocalVariableAttribute;
-
import javassist.bytecode.MethodInfo;
-
-
import java.io.IOException;
-
import java.lang.instrument.ClassFileTransformer;
-
import java.lang.instrument.IllegalClassFormatException;
-
import java.lang.instrument.Instrumentation;
-
import java.security.ProtectionDomain;
-
import java.util.*;
-
-
public
class
AgentMain3 {
-
-
private
static
final Set<String> classNameSet =
new
HashSet<>();
-
-
static {
-
classNameSet.add(
"com.congge.controller.UserController");
-
}
-
-
public
static
void
premain
(String agentArgs, Instrumentation instrumentation) {
-
-
final
ClassPool
pool
=
new
ClassPool();
-
pool.appendSystemPath();
-
-
//基于工具,在运行的时候修改class字节码,即动态插装
-
instrumentation.addTransformer(
new
ClassFileTransformer() {
-
@Override
-
public
byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain,
byte[] classfileBuffer)
throws IllegalClassFormatException {
-
-
String
currentClassName
= className.replaceAll(
"/",
".");
-
if (!classNameSet.contains(currentClassName)) {
// 提升classNameSet中含有的类
-
return
null;
-
}
-
-
if(classNameSet.contains(currentClassName)){
-
// 获取类
-
//CtClass ctClass = ClassPool.getDefault().get(currentClassName);
-
CtClass
ctClass
=
null;
-
try {
-
ctClass = pool.getDefault().get(currentClassName);
-
try {
-
Object[] annotations = ctClass.getAnnotations();
-
for (Object obj : annotations) {
-
if (!obj.toString().startsWith(
"@org.springframework.web.bind.annotation.RestController")) {
-
continue;
-
}
-
}
-
}
catch (ClassNotFoundException e) {
-
e.printStackTrace();
-
}
-
}
catch (NotFoundException e) {
-
e.printStackTrace();
-
}
-
String
clazzName
= ctClass.getName();
-
// 获取方法
-
//CtMethod ctMethod = null;
-
try {
-
CtMethod[] declaredMethods = ctClass.getDeclaredMethods();
-
if(Objects.nonNull(declaredMethods) && declaredMethods.length >
0){
-
for(CtMethod ctMethod1 : declaredMethods){
-
CtMethod
ctMethod
= ctClass.getDeclaredMethod(ctMethod1.getName());
-
doHandleMethod(clazzName, ctMethod);
-
}
-
}
-
}
catch (NotFoundException e) {
-
e.printStackTrace();
-
}
-
try {
-
return ctClass.toBytecode();
-
}
catch (IOException e) {
-
e.printStackTrace();
-
}
catch (CannotCompileException e) {
-
e.printStackTrace();
-
}
-
}
-
return
null;
-
}
-
});
-
-
}
-
-
private
static
void
doHandleMethod
(String clazzName, CtMethod ctMethod) {
-
String
methodName
= ctMethod.getName();
-
// 方法信息:methodInfo.getDescriptor();
-
MethodInfo
methodInfo
= ctMethod.getMethodInfo();
-
-
// 方法:入参信息
-
CodeAttribute
codeAttribute
= methodInfo.getCodeAttribute();
-
LocalVariableAttribute
attr
= (LocalVariableAttribute) codeAttribute.getAttribute(LocalVariableAttribute.tag);
-
CtClass[] parameterTypes =
new
CtClass[
0];
-
try {
-
parameterTypes = ctMethod.getParameterTypes();
-
}
catch (NotFoundException e) {
-
e.printStackTrace();
-
}
-
-
boolean
isStatic
= (methodInfo.getAccessFlags() & AccessFlag.STATIC) !=
0;
// 判断是否为静态方法
-
int
parameterSize
= isStatic ? attr.tableLength() : attr.tableLength() -
1;
// 静态类型取值
-
List<String> parameterNameList =
new
ArrayList<>(parameterSize);
// 入参名称
-
List<String> parameterTypeList =
new
ArrayList<>(parameterSize);
// 入参类型
-
StringBuilder
parameters
=
new
StringBuilder();
// 参数组装;$1、$2...,$$可以获取全部,但是不能放到数组初始化
-
-
for (
int
i
=
0; i < parameterSize; i++) {
-
parameterNameList.add(attr.variableName(i + (isStatic ?
0 :
1)));
// 静态类型去掉第一个this参数
-
parameterTypeList.add(parameterTypes[i].getName());
-
if (i +
1 == parameterSize) {
-
parameters.append(
"$").append(i +
1);
-
}
else {
-
parameters.append(
"$").append(i +
1).append(
",");
-
}
-
}
-
-
// 方法:出参信息
-
CtClass
returnType
=
null;
-
try {
-
returnType = ctMethod.getReturnType();
-
}
catch (NotFoundException e) {
-
e.printStackTrace();
-
}
-
String
returnTypeName
= returnType.getName();
-
-
// 方法:生成方法唯一标识ID
-
int
idx
= Monitor.generateMethodId(clazzName, methodName, parameterNameList, parameterTypeList, returnTypeName);
-
-
// 定义属性
-
try {
-
ctMethod.addLocalVariable(
"startNanos", CtClass.longType);
-
}
catch (CannotCompileException e) {
-
e.printStackTrace();
-
}
-
try {
-
ctMethod.addLocalVariable(
"parameterValues", ClassPool.getDefault().get(Object[].class.getName()));
-
}
catch (CannotCompileException e) {
-
e.printStackTrace();
-
}
catch (NotFoundException e) {
-
e.printStackTrace();
-
}
-
-
// 方法前加强
-
try {
-
ctMethod.insertBefore(
"{ startNanos = System.nanoTime(); parameterValues = new Object[]{" + parameters.toString() +
"}; }");
-
}
catch (CannotCompileException e) {
-
e.printStackTrace();
-
}
-
-
// 方法后加强
-
try {
-
ctMethod.insertAfter(
"{ com.congge.agent.jvm.Monitor.point(" + idx +
", startNanos, parameterValues, $_);}",
false);
// 如果返回类型非对象类型,$_ 需要进行类型转换
-
}
catch (CannotCompileException e) {
-
e.printStackTrace();
-
}
-
-
// 方法;添加TryCatch
-
try {
-
ctMethod.addCatch(
"{ com.congge.agent.jvm.Monitor.point(" + idx +
", $e); throw $e; }", ClassPool.getDefault().get(
"java.lang.Exception"));
// 添加异常捕获
-
}
catch (CannotCompileException e) {
-
e.printStackTrace();
-
}
catch (NotFoundException e) {
-
e.printStackTrace();
-
}
-
}
-
-
}
为了模拟出效果,我们在UserController中再增加一个方法
-
@RestController
-
public
class
UserController {
-
-
@GetMapping("/queryUserInfo")
-
public String
queryUserInfo
(String userId){
-
return
"hello :" + userId;
-
}
-
-
@GetMapping("/queryUserInfo2")
-
public String
queryUserInfo2
(String userName){
-
return
"hello :" + userName;
-
}
-
-
}
按照上面的步骤再次完成配置之后,再次启动工程进行测试,依次访问下面的接口,,观察控制台输出效果:
-
http:
//localhost:8087/queryUserInfo?userId=222
-
http:
//localhost:8087/queryUserInfo2?userName=javassist
如果需要监控更多的业务类,或者特定注解的类,也可以尝试类似的思路,比如我们要监控业务实现层的方法等,均可借鉴。
转载:https://blog.csdn.net/zhangcongyi420/article/details/128266912