由上文引出的Javassist学习

前言

Java中所有的类都被编译为class文件来运行,在编译完class文件之后,类不能再被显示修改,而Javassist就是用来处理编译后的class文件,它可以用来修改方法或者新增方法,并且不需要深入了解字节码,还可以生成一个新的类对象。

创建class

创建maven项目,引入Javassist库

1<!-- https://mvnrepository.com/artifact/javassist/javassist -->
2        <dependency>
3            <groupId>javassist</groupId>
4            <artifactId>javassist</artifactId>
5            <version>3.12.1.GA</version>
6        </dependency>

使用javassist来创建一个Person类

 1package com.y4er.learn;
 2
 3import javassist.*;
 4
 5
 6public class CreateClass {
 7    public static void main(String[] args) throws Exception {
 8        // 获取javassist维护的类池
 9        ClassPool pool = ClassPool.getDefault();
10
11        // 创建一个空类com.y4er.learn.Person
12        CtClass ctClass = pool.makeClass("com.y4er.learn.Person");
13
14        // 给ctClass类添加一个string类型的字段为name
15        CtField name = new CtField(pool.get("java.lang.String"), "name", ctClass);
16
17        // 设置private权限
18        name.setModifiers(Modifier.PRIVATE);
19
20        // 初始化name字段为zhangsan
21        ctClass.addField(name, CtField.Initializer.constant("zhangsan"));
22
23        // 生成get、set方法
24        ctClass.addMethod(CtNewMethod.getter("getName",name));
25        ctClass.addMethod(CtNewMethod.setter("setName",name));
26
27        // 添加无参构造函数
28        CtConstructor ctConstructor = new CtConstructor(new CtClass[]{}, ctClass);
29        ctConstructor.setBody("{name=\"xiaoming\";}");
30        ctClass.addConstructor(ctConstructor);
31
32        // 添加有参构造
33        CtConstructor ctConstructor1 = new CtConstructor(new CtClass[]{pool.get("java.lang.String")}, ctClass);
34        ctConstructor1.setBody("{$0.name=$1;}");
35        ctClass.addConstructor(ctConstructor1);
36
37        // 创建一个public方法printName() 无参无返回值
38        CtMethod printName = new CtMethod(CtClass.voidType, "printName", new CtClass[]{}, ctClass);
39        printName.setModifiers(Modifier.PUBLIC);
40        printName.setBody("{System.out.println($0.name);}");
41        ctClass.addMethod(printName);
42
43        // 写入class文件
44        ctClass.writeFile();
45        ctClass.detach();
46    }
47}

执行完之后生成了Person.class

image.png

使用方法

从上文的demo中可以看到部分使用方法,在javassist中CtClass代表的就是类class,ClassPool就是CtClass的容器,ClassPool维护了所有创建的CtClass对象,需要注意的是当CtClass数量过大会占用大量内存,需要调用CtClass.detach()释放内存。

ClassPool重点有以下几个方法:

  1. getDefault() 单例获取ClassPool
  2. appendClassPath() 将目录添加到ClassPath
  3. insertClassPath() 在ClassPath插入jar
  4. get() 根据名称获取CtClass对象
  5. toClass() 将CtClass转为Class 一旦被转换则不能修改
  6. makeClass() 创建新的类或接口

更多移步官方文档:http://www.javassist.org/html/javassist/ClassPool.html

CtClass需要关注的方法:

  1. addConstructor() 添加构造函数
  2. addField() 添加字段
  3. addInterface() 添加接口
  4. addMethod​() 添加方法
  5. freeze() 冻结类使其不能被修改
  6. defrost() 解冻使其能被修改
  7. detach() 从ClassPool中删除类
  8. toBytecode() 转字节码
  9. toClass() 转Class对象
  10. writeFile() 写入.class文件
  11. setModifiers​() 设置修饰符

移步:http://www.javassist.org/html/javassist/CtClass.html

CtMethod继承CtBehavior,需要关注的方法:

  1. insertBefore 在方法的起始位置插入代码
  2. insterAfter 在方法的所有 return 语句前插入代码
  3. insertAt 在指定的位置插入代码
  4. setBody 将方法的内容设置为要写入的代码,当方法被 abstract修饰时,该修饰符被移除
  5. make 创建一个新的方法

更多移步:http://www.javassist.org/html/javassist/CtBehavior.html

在setBody()中我们使用了$符号代表参数

1// $0代表this $1代表第一个传入的参数 类推
2printName.setBody("{System.out.println($0.name);}");

使用CtClass生成对象

上文我们生成了一个ctClass对象对应的是Person.class,怎么调用Person类生成对象、调用属性或方法?

三种方法:

  1. 反射方式调用
  2. 加载class文件
  3. 通过接口

反射调用

1// 实例化
2Object o = ctClass.toClass().newInstance();
3Method setName = o.getClass().getMethod("setName", String.class);
4setName.invoke(o,"Y4er");
5Method printName1 = o.getClass().getMethod("printName");
6printName1.invoke(o);

加载class文件

1ClassPool pool = ClassPool.getDefault();
2pool.appendClassPath("E:\\code\\java\\javassist-learn\\com\\y4er\\learn");
3CtClass PersonClass = pool.get("com.y4er.learn.Person");
4Object o = PersonClass.toClass().newInstance();
5//接下来反射调用

通过接口调用

新建一个接口IPerson,将Person类的方法全部抽象出来

1package com.y4er.learn;
2
3public interface IPerson {
4    String getName();
5
6    void setName(String name);
7
8    void printName();
9}
 1ClassPool pool = ClassPool.getDefault();
 2pool.appendClassPath("E:\\code\\java\\javassist-learn\\com\\y4er\\learn\\Person.class");
 3
 4CtClass IPerson = pool.get("com.y4er.learn.IPerson");
 5CtClass Person = pool.get("com.y4er.learn.Person");
 6Person.defrost();
 7Person.setInterfaces(new CtClass[]{IPerson});
 8
 9IPerson o = (IPerson) Person.toClass().newInstance();
10o.setName("aaa");
11System.out.println(o.getName());
12o.printName();

将Person类实现IPerson接口,然后创建实例时直接强转类型,就可以直接调用了。

修改现有的类

javassist大多数情况下用户修改已有的类,比如常见的日志切面。我仍然使用Person类来讲解:

 1//
 2// Source code recreated from a .class file by IntelliJ IDEA
 3// (powered by Fernflower decompiler)
 4//
 5
 6package com.y4er.learn;
 7
 8public class Person implements IPerson {
 9    private String name = "zhangsan";
10
11    public String getName() {
12        return this.name;
13    }
14
15    public void setName(String var1) {
16        this.name = var1;
17    }
18
19    public Person() {
20        this.name = "xiaoming";
21    }
22
23    public Person(String var1) {
24        this.name = var1;
25    }
26
27    public void printName() {
28        System.out.println(this.name);
29    }
30}

此时我想在printName方法的执行效果如下

1------ printName start ------
2xiaoming
3------ printName  over ------

写一下代码:

 1pool.appendClassPath("E:\\code\\java\\javassist-learn\\com\\y4er\\learn\\Person.class");
 2CtClass Person = pool.get("com.y4er.learn.Person");
 3Person.defrost();
 4
 5CtMethod printName1 = Person.getDeclaredMethod("printName", null);
 6printName1.insertBefore("System.out.println(\"------ printName start ------\");");
 7printName1.insertAfter("System.out.println(\"------ printName  over ------\");");
 8
 9Object o = Person.toClass().newInstance();
10Method printName2 = o.getClass().getMethod("printName");
11printName2.invoke(o, null);

很轻松实现了切面

image.png

文笔垃圾,措辞轻浮,内容浅显,操作生疏。不足之处欢迎大师傅们指点和纠正,感激不尽。