APM(3)javassist使用

通过前两节,了解了javaagent相关的知识,无论静态agent还是动态agent都可以在对应用无侵入的情况下实现对应用的监控(拦截),那么我们拦截应用的执行有什么用呢?答案是可以实现应用的监控,对类字节码的修改,动态代理等等,如果要修改类字节码则需要一个工具,这个工具就是javassist
Javassist是一个开源的分析、编辑和创建Java字节码的类库。其主要的优点,在于简单,而且快速。直接使用java编码的形式,而不需要了解虚拟机指令,就能动态改变类的结构,或者动态生成

javassist使用

关于javassist直接通过代码注释方式来理解,他的主要功能是可以动态构建一个新类,或者修改已经存在的类的相关属性(成员变量,构造方法,普通方法),使用之前先引入相关jar包

1
2
3
4
5
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.20.0-GA</version>
</dependency>

动态创建类

下面是使用javassist创建一个类的实例相关api使用都已在代码中注释

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
/**
* @throws Exception
* 使用javassist动态创建一个类
*/
public static void makeClass() throws Exception{
ClassPool pool = ClassPool.getDefault();
//定义类
CtClass stuClass = pool.makeClass("com.pojo.Student");
//创建成员变量--两种方式
//id属性--通过构造方法定义
CtField idField = new CtField(CtClass.longType, "id", stuClass);
idField.setModifiers(Modifier.PRIVATE);//设置访问类型为私有
//name属性--通过make方法类似写类那样定义
CtField nameField = CtField.make("private String name;", stuClass);
stuClass.addField(idField);
stuClass.addField(nameField);
//创建方法
CtMethod nameGetMethod = CtMethod.make("public String getName(){return name;}", stuClass);
CtMethod nameSetMethod = CtMethod.make("public void setName(String name){this.name = name;}", stuClass);
stuClass.addMethod(nameGetMethod);
stuClass.addMethod(nameSetMethod);
//创建构造方法
//添加有参构造器
CtConstructor constructor = new CtConstructor(new CtClass[]{CtClass.longType,pool.get("java.lang.String")},stuClass);
constructor.setBody("{this.id=id;this.name=name;}");
stuClass.addConstructor(constructor);
//无参构造器
CtConstructor cons = new CtConstructor(null,stuClass);
cons.setBody("{}");
stuClass.addConstructor(cons);
//生成到文件中
//stuClass.writeFile("C:/workproject");
//如果要使用生成的类,可以通过如下方式加载并获取
//输出并加载class 类,默认加载到当前线程的ClassLoader中,也可以选择输出的ClassLoader。
Class clazz = stuClass.toClass();
// 下面是反射相关api
System.out.println("class:"+clazz.getName());
System.out.println("------------属性列表------------");
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
System.out.println(field.getType()+"\t"+field.getName());
}
System.out.println("------------方法列表------------");
//方法
Method[] methods = clazz.getDeclaredMethods();
for (Method method: methods){
System.out.println(method.getReturnType()+"\t"+method.getName()+"\t"+ Arrays.toString(method.getParameterTypes()));
}
}

若将该类生成到文件中,可以得到Student.class文件,将该文件反编译可得到如下图片结果,正是我们所创建的类

mark

动态修改类方法

用于被修改的类

1
2
3
4
5
6
7
8
9
class Calculator {
public void getSum(long n) {
long sum = 0;
for (int i = 0; i < n; i++) {
sum += i;
}
System.out.println("n="+n+",sum="+sum);
}
}

使用javassist修改类实例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
/**
* 使用javassist动态修改一个类
* 修改getSum的方法名为getSum$impl,并在方法体上加上一句输出
* 创建一个方法复制原来的getSum方法,并在其内调用修改后的getSum方法
* 其实这相当于在使用javassist实现动态代理,因为用户使用的仍然是getSum方法,而该方法实际上已经被我们篡改
*/
public static void modifyClass() throws Exception{
ClassPool classPool = ClassPool.getDefault();
//获取类
CtClass ctClass = classPool.get("com.ssist.Calculator");
// 需要修改的方法名称
String mname = "getSum";
CtMethod mold = ctClass.getDeclaredMethod(mname);
// 修改原有的方法名称
String nname = mname + "$impl";
mold.setName(nname);
//在方法执行前插入一条语句
mold.insertBefore("System.out.println(\"这是插入的内容\");");
//可以直接设置方法内容
//mold.setBody("");
//创建新的方法,复制原来的方法
CtMethod mnew = CtNewMethod.copy(mold, mname, ctClass, null);
// 主要的注入代码
StringBuffer body = new StringBuffer();
body.append("{\nlong start = System.currentTimeMillis();\n");
// 调用原有代码,类似于method();($$)表示所有的参数
body.append(nname + "($$);\n");
body.append("System.out.println(\"Call to method " + mname
+ " took \" +\n (System.currentTimeMillis()-start) + " + "\" ms.\");\n");
body.append("}");
// 替换新方法
mnew.setBody(body.toString());
// 增加新方法
ctClass.addMethod(mnew);
//执行修改后的getSum方法--此时该方法名称已改为getSum$Impl
System.out.println("-----这是在执行原来的getSum方法,修改后是getSum$Impl方法-------");
Calculator calculator =(Calculator)ctClass.toClass().newInstance();
Class clazz = Calculator.class;
Method declaredMethod = clazz.getDeclaredMethod(nname, long.class);
declaredMethod.invoke(calculator,10000);
//执行新添加的方法getSum,该方法为复制原来的getSum方法,并修改了部分方法体内容
System.out.println("-----这是在执行新添加的getSum方法(原来的getSum已经改名)-----");
calculator.getSum(10000);
}

javassist执行流程图和UML类图

执行流程图:
mark
类图:
mark

javassist特殊符号说明及注意点

mark
注意点:

1
2
3
javassist 特殊语法说明:
a) 不能引用在方法中其它地方定义的局部变量
b) 不会对类型进行强制检查:如 int start = System.currentTimeMillis(); 或 String i=”abc”;

参考博客

Java学习之javassist
Java动态编程之javassist
鲁班大师pdf文档


-------------本文结束感谢您的阅读-------------