Javassist

什么是Javassist

Javassist是由东京工业大学的数学和计算机科学系的 Shigeru Chiba (千叶滋)所创建的。
Javassist是一个开源分析编辑创建Java字节码的类库。它已加入开源的JBoss 应用服务器项目,通过使用Javassist对字节码操作为JBoss实现动态”AOP”框架。

java字节码的处理,有很多工具,如bcel,asm。不过这些都需要直接跟虚拟机指令打交道。如果你不想了解虚拟机指令,可以采用javassist。javassist是jboss的一个子项目,其主要的优点,在于简单,而且快速。直接使用java编码的形式,而不需要了解虚拟机指令,就能动态改变类的结构,或者动态生成类。

Java 字节码以二进制的形式存储在 .class 文件中,每一个.class 文件包含一个 Java 类或接口。
Javassist 就是一个用来处理 Java 字节码的类库。它可以在一个已经编译好的类中添加新的方法,或者是修改已有的方法,并且不需要对字节码方面有深入的了解。同时也可以去生成一个新的类对象,通过完全手动的方式。

一句话来讲Javassist:Javassist允许Java程序可以在运行时定义一个新的class、在JVM加载时修改class文件。

Javassist的用法

Javassist官网
Javassist使用指南

Javassist使用指南按如下目录介绍(后面只介绍一点目前比较重要的点,详细的请看官方文档)

  1. Reading and writing bytecode
  2. ClassPool
  3. Class loader
  4. Introspection and customization
  5. Bytecode level API
  6. Generics
  7. Varargs
  8. J2ME
  9. Boxing/Unboxing
  10. Debug

Maven依赖:

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

1. 读、写字节码

Javassist is a class library for dealing with Java bytecode. Java bytecode is stored in a binary file called a class file. Each class file contains one Java class or interface.

ClassPool对象:代表class文件的CtClass对象的容器,可以通过该对象来获取想要读取或者修改的类。
javassist.CtClass代表一个class文件的抽象类表示形式。一个CtClass(compile-time class编译时的类)是一个处理class文件的句柄
以下是一个简单的程序:

1
2
3
4
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get("test.Rectangle");
cc.setSuperclass(pool.get("test.Point"));
cc.writeFile();

这段程序首先包含一个ClassPool对象,通过javassist控制字节码的修改。
ClassPool对象是代表class文件的CtClass对象的容器。为了构建CtClass对象,它按要求读取class文件,并记录所构建的对象以供以后的访问。
为了修改一个类的定义,用户必须首先从ClassPool对象的.get(className)方法获取一个CtClass引用。
在上述示例中,CtClass对象表示ClassPool中的类test.Rectangle,并且将其分配给变量cc。
ClassPool对象由静态方法getDefault方法查找默认的系统检索path返回。

从实现上来看,ClassPool是一个CtClass的哈希表,使用class name作为key
ClassPool.get()方法通过检索这个哈希表找到一个CtClass对象关联指定的key
如果CtClass对象没有找到,get()方法会读取class文件去构造一个CtClass对象,记录在哈希表中然后作为get()的返回值返回。

ClassPool中获取到的CtClass对象是可以被修改的。在上述示例中,它被修改了,test.Rectangle的父类变更为test.Point,这个修改将会在最后CtClass.writeFile()方法调用后反映在class文件中。

writeFile()方法将CtClass对象转换到class文件并且将其写入本地磁盘。Javassist也提供了一个方法用于直接获取修改后的字节码:toBytecode():

1
byte[] b = cc.toBytecode();

也可以像这样直接加载CtClass:

1
2
//toClass请求当前线程的上下文类加载器去加载class文件,返回一个java.lang.Class对象。
Class clazz = cc.toClass();//

示例:

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
public class WriteRead {
public static void main(String[] args){
// 获取ClassPool
ClassPool pool = ClassPool.getDefault();
CtClass cc = null;
try {
// 通过ClassPool获取CtClass
cc = pool.get("com.alter.Javassist.test.Rectangle");
// 设置父类
cc.setSuperclass(pool.get("com.alter.Javassist.test.Point"));
// 更新到class文件中(仅在JVM中)
cc.writeFile();

// 获取修改后的字节码
byte[] b = cc.toBytecode();
System.out.println(new String(b));

// 加载类(请求当前线程的上下文加载器加载CtClass代表的类)
Class clazz = cc.toClass();
System.out.println("cc.getClass() is :"+cc.getClass());
System.out.println("superClass is :" + clazz.getSuperclass());
} catch (NotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (CannotCompileException e) {
e.printStackTrace();
}

}
}

1
执行之后会自动生成如下文件
2
cc.writeFile();可以填一个dirctory Name的参数,有这个参数,则com.alter.Javassist.test.Rectangle被包含在这个文件下。

2. Frozen class

冻结类的含义:
如果一个CtClass对象通过writeFile()toBytecodetoClass方法被转换到class文件中,javassist则会冻结这个CtClass对象。再对这个CtClass对象进行操作则会不允许,这在尝试去修改一个已经被JVM加载过的class文件的时候会发出警告,因为JVM不允许重加载一个class。
一个冻结的CtClass可以通过其defrost()方法解冻,解冻后可以允许对这个CtClass修改:

1
2
3
4
5
6
7
8
// 被冻结了,不能再修改(Exception in thread "main" java.lang.RuntimeException: com.alter.Javassist.test.Rectangle class is frozen)
// 解冻后可以修改
cc2.toClass();// 被冻结
cc2.defrost();// 解冻
System.out.println(cc2.getFields().length);
cc2.addField(CtField.make("private String name;", cc2));// 解冻后允许修改
cc2.writeFile();
System.out.println(cc2.getFields().length);

3. 定义一个新的class

使用ClassPool.makeClass,重新定义一个新的类

1
2
3
ClassPool pool1 = ClassPool.getDefault();
CtClass cc2 = pool1.makeClass("hello.make.Point");
System.out.println(cc2.toClass()); // 输出class hello.make.Point

这个程序定义了一个Point类,未包含任何成员。
成员方法可以通过使用CtClass的addMethod()方法传入一个CtMethod的工厂方法创建的对象作为参数来追加。

1
2
3
4
5
6
7
8
9
10
// 定义一个新的类
ClassPool pool1 = ClassPool.getDefault();
CtClass cc2 = pool1.makeClass("hello.make.Point");
//System.out.println(cc2.toClass().getMethods().length); // 9

// 追加方法
cc2.addMethod(CtMethod.make("public void sayHello(){\n" +
" System.out.println(\"Hello!\");\n" +
" }",cc2));
System.out.println(cc2.toClass().getMethods().length); // 10

makeClass()方法不能创建一个新的接口,需要使用makeInterface()方法才可以。
接口中的成员方法可以通过CtMethod的abstractMethod方法创建。

4. 添加成员字段属性

1
2
3
4
5
6
7
8
9
10
11
ClassPool pool = ClassPool.getDefault(); //获取一个类池

// 动态添加一个字段
CtClass ctClass = pool.makeClass("com.java.javassist.pojo.People");
CtField ageField = new CtField(pool.get("java.lang.String"), "name", ctClass);
ageField.setModifiers(Modifier.PRIVATE); // 还可以设置访问权限
ctClass.addField(ageField);

// 动态添加getter setter方法
ctClass.addMethod(CtNewMethod.setter("setAge", ageField));
ctClass.addMethod(CtNewMethod.getter("getAge", ageField));

5. 构造函数的创建

1
2
3
4
5
6
7
8
9
// 添加无参构造函数
CtConstructor ctConstructor = new CtConstructor(new CtClass[]{}, ctClass);
ctConstructor.setBody("$0.name = \"alter\";");
ctClass.addConstructor(ctConstructor);

// 添加有参构造函数
CtConstructor ctConstructor1 = new CtConstructor(new CtClass[]{pool.get("java.lang.String")}, ctClass);
ctConstructor1.setBody("$0.name = $1;");
ctClass.addConstructor(ctConstructor1);

当如果想要实现有参和无参构造函数方法的时候,对于$0$1...$n ,这里的$0代表着这个类的this对象,$1则是当前方法的第一个参数,以此类推!
需要注意:在实现有参和无参构造的时候,一定要setbody

6. 添加成员方法

这里实现成员方法,然后进行保存字节码

1
2
3
4
5
6
7
// 添加成员方法  如果没有setBody则是一个抽象方法
CtMethod ctMethod = new CtMethod(CtClass.voidType,"hello", new CtClass[]{pool.get("java.lang.String")}, ctClass);
ctMethod.setModifiers(Modifier.PUBLIC);
ctMethod.setBody("{System.out.println($0.name);}");
ctClass.addMethod(ctMethod);

ctClass.writeFile("javassistExample");

4 5 6合起来
执行结果为

3

7. 修剪prune类

如果CtClass.prune()方法被调用,则Javassist会在CtClass被冻结的时候(调用writeFile()doBytecodetoClass方法的时候)修剪CtClass对象的数据结构。
为了降低内存消耗,修剪时会放弃对象中的不必要的属性。当一个CtClass对象被修剪后,方法的字节码则不能被访问,除了方法名称、方法签名和注解。修剪过的CtClass对象不会被解冻。默认修剪值是false

1
2
3
4
5
6
// 修剪ctClass
cc2.prune();// 设置修剪为true
cc2.writeFile();// 冻结的时候,会进行修剪
//System.out.println(cc2);//修剪后不能访问方法

//error:Exception in thread "main" java.lang.RuntimeException: toBytecode(): hello.make.Point was pruned.

禁止修剪stopPruning(true),必须在对象的前面调用:

1
2
3
4
CtClasss cc = ...;
cc.stopPruning(true);// 前面调用禁止修剪
:
cc.writeFile();

注意:
当debugging的时候,你可能想临时禁止修剪、冻结和修改一个class文件到磁盘中,那么debugWriteFile是一个简便的方法。该方法禁止修剪、写入class文件、解冻。

8. 附加

(1) 关于ClassPool
ClassPool需要关注的方法:
getDefault: 返回默认的ClassPool 是单例模式的,一般通过该方法创建我们的ClassPool;
appendClassPath, insertClassPath: 将一个ClassPath加到类搜索路径的末尾位置 或 插入到起始位置。通常通过该方法写入额外的类搜索路径,以解决多个类加载器环境中找不到类的尴尬;
toClass: 将修改后的CtClass加载至当前线程的上下文类加载器中,CtClass的toClass方法是通过调用本方法实现。需要注意的是一旦调用该方法,则无法继续修改已经被加载的class。
get,getCtClass: 根据类路径名获取该类的CtClass对象,用于后续的编辑。

(2) 关于CtMethod
CtMethod继承CtBehavior,需要关注的方法:
insertBefore 在方法的起始位置插入代码
insterAfter 在方法的所有 return 语句前插入代码
insertAt 在指定的位置插入代码
setBody 将方法的内容设置为要写入的代码,当方法被abstract修饰时,该修饰符被移除
make 创建一个新的方法

(3) 对象实例化
这里提供了三种方法:
1、反射方式调用
2、加载class文件
3、通过接口

9. AOP编程

想要实现的效果是能够在hello方法的前后进行打印字符==================

insertBefore 在方法的起始位置插入代码
insterAfter 在方法的所有 return 语句前插入代码

要知道,class在没有进行toClass之前,我们可以进行任意修改,我们就拿class字节码文件来继续进行修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Aop1 {
public static void main(String[] args) throws NotFoundException, CannotCompileException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException, InvocationTargetException {
ClassPool classPool = ClassPool.getDefault();
classPool.insertClassPath("/路径/javassistExample");
CtClass ctClass = classPool.get("com.java.javassist.pojo.People");
ctClass.defrost();
CtMethod ctMethod = ctClass.getDeclaredMethod("hello", new CtClass[]{classPool.get("java.lang.String")});
ctMethod.insertBefore("{System.out.println(\"------ hello before ------\");}");
ctMethod.insertAfter("{System.out.println(\"------ printName after ------\");}");

Object o = ctClass.toClass().newInstance();
Method hello = o.getClass().getMethod("hello",String.class);
hello.invoke(o, "1111");
}
}

这里有个注意点:如果当前web路径中也存在一个com.java.javassist.pojo.People,那么就需要用到insertClassPath,当java加载的时候首先找的是class字节码,而不是当前web路径中的类,如果你把insertClassPath改成了appendClassPath那么他就会加载失败!
如果只有一个com.java.javassist.pojo.People,那么使用insertClassPath还是appendClassPath都可以(至少这个例子是可以的)。
4

Reference

http://www.javassist.org/tutorial/tutorial.html
https://www.cnblogs.com/zpchcbd/p/14835338.html
https://blog.csdn.net/zixiao217/article/details/88803631