警告
本文最后更新于 2020-04-20 ,文中内容可能已过时。
由上文引出的Javassist学习
Java中所有的类都被编译为class文件来运行,在编译完class文件之后,类不能再被显示修改,而Javassist
就是用来处理编译后的class文件,它可以用来修改方法或者新增方法,并且不需要深入了解字节码,还可以生成一个新的类对象。
创建maven项目,引入Javassist库
1
2
3
4
5
6
<!-- https://mvnrepository.com/artifact/javassist/javassist -->
<dependency>
<groupId> javassist</groupId>
<artifactId> javassist</artifactId>
<version> 3.12.1.GA</version>
</dependency>
使用javassist来创建一个Person类
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
package com.y4er.learn ;
import javassist.* ;
public class CreateClass {
public static void main ( String [] args ) throws Exception {
// 获取javassist维护的类池
ClassPool pool = ClassPool . getDefault ();
// 创建一个空类com.y4er.learn.Person
CtClass ctClass = pool . makeClass ( "com.y4er.learn.Person" );
// 给ctClass类添加一个string类型的字段为name
CtField name = new CtField ( pool . get ( "java.lang.String" ), "name" , ctClass );
// 设置private权限
name . setModifiers ( Modifier . PRIVATE );
// 初始化name字段为zhangsan
ctClass . addField ( name , CtField . Initializer . constant ( "zhangsan" ));
// 生成get、set方法
ctClass . addMethod ( CtNewMethod . getter ( "getName" , name ));
ctClass . addMethod ( CtNewMethod . setter ( "setName" , name ));
// 添加无参构造函数
CtConstructor ctConstructor = new CtConstructor ( new CtClass [] {}, ctClass );
ctConstructor . setBody ( "{name=\"xiaoming\";}" );
ctClass . addConstructor ( ctConstructor );
// 添加有参构造
CtConstructor ctConstructor1 = new CtConstructor ( new CtClass [] { pool . get ( "java.lang.String" )}, ctClass );
ctConstructor1 . setBody ( "{$0.name=$1;}" );
ctClass . addConstructor ( ctConstructor1 );
// 创建一个public方法printName() 无参无返回值
CtMethod printName = new CtMethod ( CtClass . voidType , "printName" , new CtClass [] {}, ctClass );
printName . setModifiers ( Modifier . PUBLIC );
printName . setBody ( "{System.out.println($0.name);}" );
ctClass . addMethod ( printName );
// 写入class文件
ctClass . writeFile ();
ctClass . detach ();
}
}
执行完之后生成了Person.class
从上文的demo中可以看到部分使用方法,在javassist中CtClass代表的就是类class,ClassPool就是CtClass的容器,ClassPool维护了所有创建的CtClass对象,需要注意的是当CtClass数量过大会占用大量内存,需要调用CtClass.detach()释放内存。
ClassPool重点有以下几个方法:
getDefault() 单例获取ClassPool appendClassPath() 将目录添加到ClassPath insertClassPath() 在ClassPath插入jar get() 根据名称获取CtClass对象 toClass() 将CtClass转为Class 一旦被转换则不能修改 makeClass() 创建新的类或接口 更多移步官方文档:http://www.javassist.org/html/javassist/ClassPool.html
CtClass需要关注的方法:
addConstructor() 添加构造函数 addField() 添加字段 addInterface() 添加接口 addMethod() 添加方法 freeze() 冻结类使其不能被修改 defrost() 解冻使其能被修改 detach() 从ClassPool中删除类 toBytecode() 转字节码 toClass() 转Class对象 writeFile() 写入.class文件 setModifiers() 设置修饰符 移步:http://www.javassist.org/html/javassist/CtClass.html
CtMethod继承CtBehavior,需要关注的方法:
insertBefore 在方法的起始位置插入代码 insterAfter 在方法的所有 return 语句前插入代码 insertAt 在指定的位置插入代码 setBody 将方法的内容设置为要写入的代码,当方法被 abstract修饰时,该修饰符被移除 make 创建一个新的方法 更多移步:http://www.javassist.org/html/javassist/CtBehavior.html
在setBody()中我们使用了$
符号代表参数
1
2
// $0代表this $1代表第一个传入的参数 类推
printName . setBody ( "{System.out.println($0.name);}" );
上文我们生成了一个ctClass对象对应的是Person.class,怎么调用Person类生成对象、调用属性或方法?
三种方法:
反射方式调用 加载class文件 通过接口 1
2
3
4
5
6
// 实例化
Object o = ctClass . toClass (). newInstance ();
Method setName = o . getClass (). getMethod ( "setName" , String . class );
setName . invoke ( o , "Y4er" );
Method printName1 = o . getClass (). getMethod ( "printName" );
printName1 . invoke ( o );
1
2
3
4
5
ClassPool pool = ClassPool . getDefault ();
pool . appendClassPath ( "E:\\code\\java\\javassist-learn\\com\\y4er\\learn" );
CtClass PersonClass = pool . get ( "com.y4er.learn.Person" );
Object o = PersonClass . toClass (). newInstance ();
//接下来反射调用
新建一个接口IPerson,将Person类的方法全部抽象出来
1
2
3
4
5
6
7
8
9
package com.y4er.learn ;
public interface IPerson {
String getName ();
void setName ( String name );
void printName ();
}
1
2
3
4
5
6
7
8
9
10
11
12
ClassPool pool = ClassPool . getDefault ();
pool . appendClassPath ( "E:\\code\\java\\javassist-learn\\com\\y4er\\learn\\Person.class" );
CtClass IPerson = pool . get ( "com.y4er.learn.IPerson" );
CtClass Person = pool . get ( "com.y4er.learn.Person" );
Person . defrost ();
Person . setInterfaces ( new CtClass [] { IPerson });
IPerson o = ( IPerson ) Person . toClass (). newInstance ();
o . setName ( "aaa" );
System . out . println ( o . getName ());
o . printName ();
将Person类实现IPerson接口,然后创建实例时直接强转类型,就可以直接调用了。
javassist大多数情况下用户修改已有的类,比如常见的日志切面。我仍然使用Person类来讲解:
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
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package com.y4er.learn ;
public class Person implements IPerson {
private String name = "zhangsan" ;
public String getName () {
return this . name ;
}
public void setName ( String var1 ) {
this . name = var1 ;
}
public Person () {
this . name = "xiaoming" ;
}
public Person ( String var1 ) {
this . name = var1 ;
}
public void printName () {
System . out . println ( this . name );
}
}
此时我想在printName方法的执行效果如下
1
2
3
------ printName start ------
xiaoming
------ printName over ------
写一下代码:
1
2
3
4
5
6
7
8
9
10
11
pool . appendClassPath ( "E:\\code\\java\\javassist-learn\\com\\y4er\\learn\\Person.class" );
CtClass Person = pool . get ( "com.y4er.learn.Person" );
Person . defrost ();
CtMethod printName1 = Person . getDeclaredMethod ( "printName" , null );
printName1 . insertBefore ( "System.out.println(\"------ printName start ------\");" );
printName1 . insertAfter ( "System.out.println(\"------ printName over ------\");" );
Object o = Person . toClass (). newInstance ();
Method printName2 = o . getClass (). getMethod ( "printName" );
printName2 . invoke ( o , null );
很轻松实现了切面
文笔垃圾,措辞轻浮,内容浅显,操作生疏。不足之处欢迎大师傅们指点和纠正,感激不尽。