目录

  • 一、快速入门
    • 1.1 创建class文件
    • 1.2 ClassPool的相关方法
    • 1.3 CtClass的相关方法
    • 1.4 CtMethod的相关方法
    • 1.5 调用生成的类对象
      • 1.5.1 通过反射调用
      • 1.5.2 通过接口调用
    • 1.6 修改现有的类对象
  • 二、将类冻结
  • 三、类搜索路径
  • 四、$开头的特殊字符
  • 五、ProxyFactory的使用

我们知道Java字节码以二进制的形式存储在class文件中,每一个class文件包含一个Java类或接口。Javaassist 就是一个用来处理Java字节码的类库。在Javassist 中,类CtClass表示class文件。

我们可以用javassist类库实现动态创建类、添加类的属性和方法、设置类的父类,以及修改类的方法等操作。Javassist不允许删除方法或字段,但它允许更改名称。所以,如果一个方法是没有必要的,可以通过调用CtMethod的setName和setModifiers中将其改为一个私有方法。Javassist不允许向现有方法添加额外的参数。你可以通过新建一个方法达到同样的效果。

使用前需要先引入javassist库

以gradle工程为例,添加如下依赖:

 implementation 'org.javassist:javassist:3.28.0-GA'

一、快速入门

1.1 创建class文件

假设我们需要在当前工程的/out/class目录内创建一个Person.class文件,例如:

idea反编译查看的源码内容如下:

package com.test;public class Person {private String name = "小明";private int age = 20;public void setName(String var1) {this.name = var1;}public String getName() {return this.name;}public void setAge(int var1) {this.age = var1;}public int getAge() {return this.age;}public Person() {this.name = "老王";this.age = 30;}public Person(String var1, int var2) {this.name = var1;this.age = var2;}public void printName() {System.out.println(this.name);}public void printAge() {System.out.println(this.age);}
}

使用javassist可以很方便的实现:

package com.demoimport javassist.*object CreatePersonTest {@JvmStaticfun main(args: Array<String>) {createPerson()}fun createPerson() {// 1.创建默认的ClassPool,ClassPool是一个存储CtClass的Hash表val pool: ClassPool = ClassPool.getDefault()// 2.新建一个空类,叫Person类val person: CtClass = pool.makeClass("com.test.Person")// 3.新增一个字段 private String name;// 字段名为nameval nameField: CtField = CtField(pool.get("java.lang.String"), "name", person)// 设置字段的访问类型为privatenameField.modifiers = Modifier.PRIVATE// 新增age字段,也可以使用make方法快速创建val ageField = CtField.make("private int age;", person)// 给字段进行默认初始化,并添加到Person类中person.addField(nameField, CtField.Initializer.constant("小明"))person.addField(ageField, CtField.Initializer.constant(20))// 3.生成getter、setter方法person.addMethod(CtNewMethod.setter("setName", nameField))person.addMethod(CtNewMethod.getter("getName", nameField))person.addMethod(CtNewMethod.setter("setAge",ageField))person.addMethod(CtNewMethod.getter("getAge",ageField))// 4.添加无参数的构造方法val cons: CtConstructor = CtConstructor(arrayOf<CtClass>(), person)// 设置构造方法的方法体cons.setBody("{name=\"老王\";age=30;}")// 添加到person类中person.addConstructor(cons)// 5.添加有参的构造函数val cons2: CtConstructor = CtConstructor(arrayOf(pool.get("java.lang.String"), CtClass.intType), person)// $0=this, $1,$2,$3...代表第几个参数cons2.setBody("{$0.name=$1;$0.age=$2;}")// 添加到person类中person.addConstructor(cons2)// 6.创建一个名为printName的方法,无参数,无返回值,输出name值val printName: CtMethod = CtMethod(CtClass.voidType, "printName", arrayOf<CtClass>(), person)// 设置方法访问类型printName.modifiers = Modifier.PUBLICprintName.setBody("{System.out.println(name);}")// 上面的方式可以换成这种快速创建val printAge: CtMethod = CtMethod.make("public void printAge(){System.out.println(age);}", person)// 添加到person类中person.addMethod(printName)person.addMethod(printAge)// 7.将创建的类对象编译成.class文件,输出到指定到路径为当前路径下的out/class路径person.writeFile("./out/class/")}
}

跟咱们预想的一样。在 Javassist 中,类 Javaassit.CtClass 表示 class 文件。一个 GtClass (编译时类)对象可以处理一个 class 文件,ClassPool是 CtClass 对象的容器。它按需读取类文件来构造 CtClass 对象,并且保存 CtClass 对象以便以后使用。

需要注意的是 ClassPool 会在内存中维护所有被它创建过的 CtClass,当 CtClass 数量过多时,会占用大量的内存,API中给出的解决方案是 有意识的调用CtClass的detach()方法以释放内存。

1.2 ClassPool的相关方法

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

1.3 CtClass的相关方法

freeze: 冻结一个类,使其不可修改;
isFrozen : 判断一个类是否已被冻结;
prune : 删除类不必要的属性,以减少内存占用。调用该方法后,许多方法无法将无法正常使用,慎用;
defrost : 解冻一个类,使其可以被修改。如果事先知道一个类会被defrost, 则禁止调用 prune 方法;
detach : 将该class从ClassPool中删除;
writeFile : 根据CtClass生成 .class 文件;
toClass : 通过类加载器加载该CtClass。
setInterfaces: 添加父接口
setSuperclass: 添加父类

1.4 CtMethod的相关方法

上面我们创建一个新的方法使用了CtMethod类。CtMthod代表类中的某个方法,可以通过CtClass提供的API获取或者CtNewMethod新建,通过CtMethod对象可以实现对方法的修改。

insertBefore : 在方法的起始位置插入代码;
insterAfter : 在方法的所有 return 语句前插入代码以确保语句能够被执行,除非遇到exception;
insertAt : 在指定的位置插入代码;
setBody: 将方法的内容设置为要写入的代码,当方法被 abstract修饰时,该修饰符被移除;
make : 创建一个新的方法。

注意到在上面代码中的:setBody()的时候我们使用了一些符号:
cons2.setBody("{$0.name=$1;$0.age=$2;}")
具体还有很多的符号可以使用,但是不同符号在不同的场景下会有不同的含义,后面会介绍,也可以看javassist 的说明文档。

1.5 调用生成的类对象

上面的案例是创建一个类对象然后输出该对象编译完之后的 .class 文件。那如果我们想调用生成的类对象中的属性或者方法应该怎么去做呢?

1.5.1 通过反射调用

通过CtClass的toClass方法可以转化成Class对象,然后就可以通过反射的操作来操作目标类的成员了。

fun callByReflect() {// 1.创建classPoolval pool: ClassPool = ClassPool.getDefault()// 2.添加目标类的搜索路径pool.insertClassPath("./out/class/")// 3.获取Person.class的CtClass对象val person: CtClass = pool.get("com.test.Person")// 4.创建Person对象// 先通过CtClass的toClass让类加载器加载该CtClass,然后使用反射创建对象val personObj = person.toClass().newInstance()// 5.反射调用setter方法val setName = personObj.javaClass.getMethod("setName", String::class.java)setName.invoke(personObj, "老王")// 6.反射执行printName方法val printName = personObj.javaClass.getMethod("printName")printName.invoke(personObj)
}

1.5.2 通过接口调用

上面两种其实都是通过反射的方式去调用,问题在于我们的工程中其实并没有这个类对象,所以反射的方式比较麻烦,并且开销也很大。那么如果你的类对象可以抽象为一些方法得合集,就可以考虑为该类生成一个接口类。这样在newInstance()的时候我们就可以强转为接口,可以将反射的那一套省略掉了。

还拿上面的Person类来说,新建一个IPerson接口类:

package com.demo/*** @author  chenyousheng* @date  2022/2/12* @desc Person类接口*/
interface IPerson {fun setName(name: String)fun getName(): Stringfun printName()
}

通过CtClass的setInterfaces方法可以给目标类添加一个父接口,也就是上面我们定义的接口类。

fun callByInterface() {// 1.创建classPoolval pool: ClassPool = ClassPool.getDefault()// 2.添加类搜索路径,这里是IPerson接口class文件所在的路径,// 如果IPerson源码文件的根文件是src目录,那么也可以不需要指定class文件的目录,因为ClassPool默认搜索路径能够找到// pool.appendClassPath("./build/classes/kotlin/main/com/demo/")// 这里是Person类的class所在的路径,由于这个路径不是默认搜索路径,所以需要指定pool.appendClassPath("./out/class")// 3.获取接口类val personInter: CtClass = pool.get("com.demo.IPerson")// 4.获取Person类val person: CtClass = pool.get("com.test.Person")// 5.让Person类实现IPerson接口person.interfaces = arrayOf<CtClass>(personInter)// 6.下面创建Person对象,就可以强转成接口类了val personObj: IPerson = person.toClass().newInstance() as IPerson// 然后就可以愉快的调用方法了,不需要反射来操作了println(personObj.getName())personObj.setName("老王2")personObj.printName()}

1.6 修改现有的类对象

前面说到新增一个类对象。这个使用场景目前还没有遇到过,一般会遇到的使用场景应该是修改已有的类。比如常见的日志切面,权限切面。我们利用javassist来实现这个功能。

有如下类对象:

package com.demo;public class PersonService {public void fly() {System.out.println("我飞起来了");}
}

下面的例子是演示如何来操作PersonService.class文件

package com.demoimport javassist.ClassPool
import javassist.CtClass
import javassist.CtMethod
import javassist.Modifier/*** @author  chenyousheng* @date  2022/2/12* @desc 修改已存在的class*/
object UpdatePersonTest {@JvmStaticfun main(args: Array<String>) {update()}fun update() {// 1.创建classPool,使用的是默认系统的类搜索路径。val pool: ClassPool = ClassPool.getDefault()// 2.获取要修改的classval cc: CtClass = pool.get("com.demo.PersonService")// 3.获取目标类的方法val fly: CtMethod = cc.getDeclaredMethod("fly")// 在方法执行前插入代码fly.insertBefore("System.out.println(\"起飞前准备降落伞\");")// 在方法执行后插入代码fly.insertAfter("System.out.println(\"成功落地\");")// 4.新增一个方法val newMethod: CtMethod = CtMethod(CtClass.voidType, "joinFriend",arrayOf<CtClass>(), cc)// 设置方法访问权限newMethod.modifiers = Modifier.PUBLIC// 设置方法的方法体newMethod.setBody("{System.out.println(\"加个好友吧\");}")// 加到目标类中cc.addMethod(newMethod)// 5.实例化目标类val personService = cc.toClass().newInstance()// 调用fly方法personService.javaClass.getMethod("fly").invoke(personService)// 调用joinFriend方法personService.javaClass.getMethod("joinFriend").invoke(personService)}
}

运行结果如下:

起飞前准备降落伞
我飞起来了
成功落地
加个好友吧

另外需要注意的是:上面的insertBefore() 和 setBody()中的语句,如果你是单行语句可以直接用双引号,但是有多行语句的情况下,你需要将多行语句用{}括起来。javassist只接受单个语句或用大括号括起来的语句块。

二、将类冻结

如果一个 CtClass 对象通过 writeFile(), toClass(), toBytecode() 被转换成一个类文件,此 CtClass 对象会被冻结起来,不允许再修改。因为一个类只能被 JVM 加载一次。

但是,一个冷冻的 CtClass 也可以被解冻,例如:

CtClasss cc = ...;:
cc.writeFile();
cc.defrost();
cc.setSuperclass(...);    // 因为类已经被解冻,所以这里可以调用成功

调用 defrost() 之后,此 CtClass 对象又可以被修改了。

如果 ClassPool.doPruning 被设置为 true,Javassist 在冻结 CtClass 时,会修剪 CtClass 的数据结构。为了减少内存的消耗,修剪操作会丢弃 CtClass 对象中不必要的属性。例如,Code_attribute 结构会被丢弃。一个 CtClass 对象被修改之后,方法的字节码是不可访问的,但是方法名称、方法签名、注解信息可以被访问。修剪过的 CtClass 对象不能再次被解冻。ClassPool.doPruning 的默认值为 false。

stopPruning() 可以用来驳回修剪操作。

CtClasss cc = ...;
cc.stopPruning(true);:
cc.writeFile(); // 转换成一个 class 文件
// cc is not pruned.

这个 CtClass 没有被修剪,所以在 writeFile() 之后,可以被解冻。

三、类搜索路径

通过ClassPool.getDefault()获取的ClassPool使用 JVM 的类搜索路径。如果程序运行在JBoss或者Tomcat等 Web 服务器上,ClassPool可能无法找到用户的类,因为 Web 服务器使用多个类加载器作为系统类加载器。在这种情况下,ClassPool 必须添加额外的类搜索路径。

1)通过ClassClassPath添加搜索路径

pool.insertClassPath(new ClassClassPath(Person.getClass()));

上面的语句将Person类添加到pool的类加载路径中。但在实践中,我发现通过这个可以将Person类所在的整个jar包添加到类加载路径中。

2)通过指定目录来添加搜索路径
也可以注册一个目录作为类搜索路径:
pool.insertClassPath("/usr/javalib");则是将 /usr/javalib目录添加到类搜索路径中。

3)通过URL指定搜索路径

ClassPool pool = ClassPool.getDefault();
ClassPath cp = new URLClassPath("www.sample.com", 80, "/out/", "com.test.");
pool.insertClassPath(cp);

上述代码将http://www.sample.com:80/out添加到类搜索路径。并且这个URL只能搜索 com.test包里面的类。例如,为了加载 com.test.Person,它的类文件会从获取http://www.sample.com:80/out/com/test/Person.class获取。

4)通过ByteArrayPath添加搜索路径

ClassPool cp = ClassPool.getDefault();
byte[] buf = 字节数组;
String name = 类名;
cp.insertClassPath(new ByteArrayClassPath(name, buf));
CtClass cc = cp.get(name);

示例中的 CtClass 对象是字节数据buf代表的class文件。将对应的类名传递给ClassPool的get()方法,就可以从字节数组中读取到对应的类文件。

5)通过输入流加载class
如果你不知道类的全名,可以使用makeClass()方法:

ClassPool cp = ClassPool.getDefault();
InputStream ins =  class文件对应的输入流;
CtClass cc = cp.makeClass(ins);

makeClass()返回从给定输入流构造的CtClass对象。你可以使用makeClass()将类文件提供给ClassPool对象。如果搜索路径包含大的jar文件,这可能会提高性能。由于ClassPool对象按需读取类文件,它可能会重复搜索整个jar文件中的每个类文件。makeClass()可以用于优化此搜索。由makeClass()构造的CtClass保存在ClassPool对象中,从而使得类文件不会再被读取。

四、$开头的特殊字符

符号 含义
$0, $1, $2, … $0=this,$1表示方法的第一个参数,依次类推,如果方法是静态的,则 $0 不可用
$args 方法参数数组.它的类型为 Object[],$args[0]=1,1,1,args[1]=$2
$r 返回结果的类型,用于强制类型转换
$w 包装器类型,用于强制类型转换,当放回值是包装类型时,可以用此来强转
$_ 返回值,一般在insertAfter中用到,用于得到原方法的返回值
$sig 参数类型数组,$sig[0]表示第一个参数类型
$type 返回值类型,一般在insertAfter中用到,即$_的类型
$class $0或this的类型
$e 异常类型

1)$e的使用场景
在给方法添加catch语句块的时候就会用到了,例如

// 创建printAge方法
val printAge: CtMethod = CtMethod.make("public void printAge(){System.out.println(age);}", person)
// 给printAge添加catch语句块,在kotlin中$需要转义
printAge.addCatch("{System.out.println(\$e);throw \$e;}", pool.get("java.lang.Exception"))
// 添加到person类中
person.addMethod(printAge)

效果如下:

public void printAge() {try {System.out.println(this.age);} catch (Exception var2) {System.out.println(var2);throw var2;}
}

2)$r的使用场景
例如给Person类添加一个convert方法

val convert = CtNewMethod.make("public int convert(){ Double d= 12.5;return (\$r)d;} ", person)
person.addMethod(convert)

效果如下:

public int convert() {double var1 = 12.5D;return (Integer)var1;
}

五、ProxyFactory的使用

通过ProxyFactory可以实现动态代理的方式在处理目标类的方法。假设要被代理的类定义如下:

package com.demo;public class PersonService {public int fly() {System.out.println("我飞起来了");return 0;}
}

通过ProxyFactory动态代理PersonService的fly方法,具体如下:

package com.demoimport javassist.ClassPool
import javassist.util.proxy.MethodHandler
import javassist.util.proxy.ProxyFactory
import javassist.util.proxy.ProxyObject/*** @author  chenyousheng* @date  2022/2/13* @desc ProxyFactory动态代理目标类方法*/
object ProxyFactoryTest {@JvmStaticfun main(args: Array<String>) {// 获取ClassPoolval pool = ClassPool.getDefault()// 获取目标类val cc = pool.get("com.demo.PersonService")// 实例化代理类工厂val factory = ProxyFactory()// 设置代理类的父类,ProxyFactory将会动态生成一个类,继承该父类factory.superclass = cc.toClass()// 设置过滤器,判断哪些方法调用需要被拦截factory.setFilter {return@setFilter it.name == "fly"}// 创建代理类型val proxy = factory.createClass()// 创建代理实例,强转成父类val personService: PersonService = proxy.newInstance() as PersonService// 设置代理处理方法(personService as ProxyObject).handler = MethodHandler { self, thisMethod, proceed, args ->//thisMethod为被代理方法 proceed为代理方法 self为代理实例 args为方法参数println(thisMethod.name + "被调用前输出")try {val ret = proceed.invoke(self, *args)println(thisMethod.name + "正在调用,返回值: " + ret)return@MethodHandler ret} finally {println(thisMethod.name + "被调用后输出")}}// 调用代理类的fly方法personService.fly()}
}

输出结果如下:

fly被调用前输出
我飞起来了
fly正在调用,返回值: 0
fly被调用后输出

javassist使用指南相关推荐

  1. javassist编程指南(一)

    javassist编程指南(主译) javassist是什么? Javassist(Java 编程辅助)使得Java字节码操作更简单. Javassist可用于编辑字节码的类库. 允许Java程序可以 ...

  2. Javassist 使用指南(一)

    本文译自: Javassist Tutorial-1 原作者: Shigeru Chiba 完成时间:2016年11月 1. 读写字节码 我们知道 Java 字节码以二进制的形式存储在 class 文 ...

  3. Javassist使用指南1

    1.创建了一个非默认的classpool,加入当前线程的上下文类加载器作为额外的类搜索路径 val classPool = ClassPool(false) classPool.appendClass ...

  4. javassist编程指南==读、写字节码

    读.写字节码 Javassist是一个处理字节码的类库.Java字节码存储在一个叫做*.class的二进制文件中.每个class文件包含一个java类或者接口. javassist.CtClass代表 ...

  5. Javassist 使用指南

    说明:翻译的太好,怕原文丢失就转载了. 1. 读写字节码 我们知道 Java 字节码以二进制的形式存储在 class 文件中,每一个 class 文件包含一个 Java 类或接口.Javaassist ...

  6. Javassist 使用指南 侵立删

    1. 读写字节码 我们知道 Java 字节码以二进制的形式存储在 class 文件中,每一个 class 文件包含一个 Java 类或接口.Javaassist 就是一个用来处理 Java 字节码的类 ...

  7. Javassist 指南1

    1.读写字节码 Javassist 是一个能处理 Java字节码 的类库,Java字节码存储在class文件中,每一个class文件都包含了一个Java类或一个接口类. 在Javassist中,使用J ...

  8. Java降落伞_javassist使用指南

    Java 字节码以二进制的形式存储在 .class 文件中,每一个 .class 文件包含一个 Java 类或接口.Javaassist 就是一个用来 处理 Java 字节码的类库.它可以在一个已经编 ...

  9. Android 中使用Javassist

    Javassist Javassist 是一个执行字节码操作的库.它可以在一个已经编译好的类中添加新的方法,或者是修改已有的方法,并且不需要对字节码方面有深入的了解. Javassist 可以绕过编译 ...

  10. Java字节码instrument研究

    MyAgent项目 <?xml version="1.0" encoding="UTF-8"?> <project xmlns="h ...

最新文章

  1. 【Python基础】Python的深浅拷贝讲解
  2. CSAPP实验二进制炸弹
  3. Vue源码后记-更多options参数(1)
  4. jdbc mysql 远程数据库_jdbc 连接远程mysql数据库的有关问题
  5. Class的getInterfaces与getGenericInterface区别
  6. 高并发场景下,如何保证生产者投递到消息中间件的消息不丢失?
  7. 判断字符是否包含有特殊字符
  8. 用idea创建vue项目
  9. 归并排序时间复杂度分析
  10. 关于web服务器硬件配置
  11. mysql允许连接表为空_mysql – 选择一个表中的所有项并与另一个表连接,允许空值...
  12. 给SpringBoot Web应用配上JavaFx漂亮衣服
  13. 第一次发,可能不太好,别喷我
  14. arduino智能跟随小车
  15. 快速求一个字符串的非空子串(不相同)的数量
  16. 英语十大词性之二 - 动词
  17. UE4学习(一)C++编程官方文档解读
  18. 【新书速递】图解IT-用Python轻松设计控制系统
  19. boston数据集预测房价
  20. CLRS第二章思考题

热门文章

  1. Foxpro 简体转繁体的一种方式(代码)
  2. 文件MD5查看linuxwindows
  3. 学校计算机和网络保密管理规定,计算机信息系统安全保密管理规定
  4. 基于CompactRIO的嵌入式车载电性能测试系统研发
  5. java与eclipse不匹配_【JAVA小白】 用eclipse输入格式不匹配的问题
  6. 国内第一款企业集中管理平台--极通EWEBS3.0
  7. Photoshop插件-删除亮调通道蒙板-脚本开发-PS插件
  8. 找不到好看的电影就看《IMDB排名前500电影》
  9. Redis入门指南笔记
  10. win10自带的打印机服务器,win10系统打印服务器安装设置的详细方法