Javassist使用教程【译】

本文详细介绍了Javassist库的使用,包括读写字节码、ClassPool管理、ClassLoader交互,以及如何在运行时修改类。重点讲述了如何定义新类、冷冻与解冻类、类路径搜索,以及在方法开头和结尾插入代码。此外,还提到了字节码API、泛型和可变参数支持等关键概念。
摘要由CSDN通过智能技术生成

说明

  1. 由于后续的 agent 相关文章要用到 Javassist,为了不和agent掺和所以这里把Javassist单独拿出来。
  2. 本文是对Javassist官网指南的翻译! ,原文:https://www.javassist.org/tutorial/tutorial.html,作者:Shigeru Chiba (抛开民族ch来说,日本的这个教授开发的这个Javassist还是不错的毕竟没有ASM那么不易入手。)
  3. 由于我个人并没有把Javassist的所有功能和特性都使用一遍,所以有些功能我也不是很熟悉,翻译/解释不到位的地方就当混个脸熟或者说了解吧。
  4. 本文没有过多的个人情感在里边,99%都是翻译原文,另外个人水平有限翻译不正确的地方请多多海涵。当然或许也没人看哈哈。
  5. 更多的,本文是一篇 备忘/工具介绍 类文章。
  6. 注意:本文中有很多 \$符号,不使用转义的话会被识别为数学符号排版很难受,所以全部使用 \转义了,如果见到 \ \$ 这种的 请忽略 \ 看成 $ 就好了。

1、读写字节码

Javassist是一个用于处理Java字节码的库。Java字节码存储在一个class结尾的二进制文件中。每一个class文件都包含了一个Java类或接口。

javassist.CtClass是class文件的一个抽象代表。一个CtClass(编译期类)对象处理一个class文件。例如:

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

这个程序首先定义了个ClassPool对象,它控制着字节码的修改。ClassPool对象是CtClass对象的一个容器,它代表一个Class文件。它会读取Class(test.Rectangle)文件,然后构造并返回一个CtClass对象。为了修改一个类,用户必须用ClassPool对象的get() 方法来获取CtClass对象。上面展示的例子中,CtClass的实例cc代表类test.RectanleClassPool实例通过 getDefault() 方法实例化,它采用默认的搜索路径方式。

从实现的角度看,ClassPoolCtClass对象的一个Hash表,ClassPool使用全类名作为键。当使用classPool.get() 方法时,会搜索Hash表,根据类名找出相应的CtClass对象。如果该对象没找到,就会读取类文件,然后构造一个CtClass对象,将其存到Hash表中,并返回结果。

CtClass对象可以被修改(下边会详细介绍)。上面的例子中,它将test.Point作为自己的父类。在调用writeFile() 后,该修改就会更新到源class文件中。

writeFile()CtClass对象转化为一个Class文件,并把它写到本地磁盘上。Javassist也提供了一个方法,用于直接获取被修改的字节码。可以调用toBytecode() 方法获取:

byte[] b = cc.toBytecode();

你也可以直接加载CtClass:

Class clazz = cc.toClass();

toClass() 请求当前线程的上下文类加载器来加载CtClass代表的class文件,它返回java.lang.Class对象。更多细节见第三章。

1.1、 定义一个新的类(重要)

要定义一个新的类,必须使用ClassPool对象,调用其makeClass() 方法:

ClassPool pool = ClassPool.getDefault(); CtClass cc = pool.makeClass("Point");

这段代码定义了一个类名为Point的类,它没有任何成员,Point的成员方法可以通过CtNewMethod声明的工厂方法make创建,然后通过CtClass的addMethod方法添加到Point类中。

makeClass() 不能创建一个新的接口,想创建一个接口需要用makeInterface() 。接口的成员方法是使用CtNewMethodabstractMethod() 来创建的 。注意接口的方法是抽象方法。

1.2、 冻结类(重要)

如果一个CtClass对象已经转化成了class文件,比如通过writeFile()toClass()toBytecode() , Javassist会冻结CtClass对象。之后对于CtClass对象的修改都是不允许的。这个是为了警告开发者,他们尝试修改的Class文件已经被加载了,JVM不允许再次加载该Class。

冻结的类可以如果想要修改,可以进行解冻,这样就允许修改了,如下:

CtClasss cc = ...;   : cc.writeFile(); // 会引起类冻结 cc.defrost();   // 解冻 cc.setSuperclass(...);   // OK 因为这个类已经被解冻了

defrost() 被调用之后,该CtClass对象可以再次被修改。

如果ClassPool.doPruning被设置为true,当CtClass被冻结时,Javassist会修剪它的数据结构。为了减少内存消耗,会删除那个对象中不需要的属性(attributeinfo structures)。例如,Codeattribute结构(方法体)会被删除。因此,在CtClass对象被修剪之后,方法的字节码是不可访问的,除了方法名称,签名和注解。被修剪的CtClass对象不能再次被解冻(defrost)。ClassPool.doPruning 的默认是false.

CtClasss cc = ...; cc.stopPruning(true);   : cc.writeFile();     // 转化为一个Class文件 // cc 没有被修剪.

CtClass对象cc没有被修剪。因此它还可以在调用writeFile() 之后调用defrost() 解冻。

在调试时,你可能想要临时停止修剪和冻结,并将修改后的字节码写回磁盘的类文件。使用debutWriteFile()方法可以方便的达到该目的。该方法停止修剪,写类文件,然后解冻CtClass对象,重新打开修剪开关(如果原来已经打开)

1.3、 类路径搜索(重要)

ClassPool.getDefault 默认会搜索JVM下面相同路径的类,并返回ClassPool。但是,如果一个程序运行在Web应用服务器上,像JBoss和Tomcat那种,ClassPool对象可能就找不到用户指定的类了,因为web应用服务使用了多个系统类加载器。这种情况下,需要给ClassPool注册一个额外的Class路径。如下:

pool.insertClassPath(new ClassClassPath(this.getClass())); // 假设pool是ClassPool的一个实例

这句代码注册了一个类的类路径,这个类是this指向的那个类。你可以使用任意Class代替this.getClass()

你也可以注册一个文件夹作为类路径。例如,下面这段代码增添可以了文件夹 /usr/local/javalib到搜索路径中:

ClassPool pool = ClassPool.getDefault(); pool.insertClassPath("/usr/local/javalib");

搜索路径不仅可以是目录,甚至可以是URL:

ClassPool pool = ClassPool.getDefault(); ClassPath cp = new URLClassPath("www.javassist.org", 80, "/java/", "org.javassist."); pool.insertClassPath(cp);

该代码增添了http://www.javassist.org:80/java/ 到类文件搜索路径下。该URL仅仅搜索org.javassist. 包下的class文件。例如,要加载org.javassist.test.Main 这个类,javassist会从这个地址下获取该类文件:

http://www.javassist.org:80/java/org/javassist/test/Main.class

此外,你也可以直接给ClassPool对象一个byte数组,然后用这个数组构建CtClass对象。要这样做,用ByteArrayClassPath, 例如:

ClassPool cp = ClassPool.getDefault(); byte[] b = a byte array; String name = class name; cp.insertClassPath(new ByteArrayClassPath(name, b)); CtClass cc = cp.get(name);

获得的CtClass对象表示一个由b指定的类文件定义的类。如果调用get()ClassPool会从ByteArrayClassPath中读取一个Class文件,指定的Class的名字就是上面的name变量。

如果你不知道这个类的全限定名,你可以使用ClassPool中的makeClass() :

ClassPool cp = ClassPool.getDefault(); InputStream ins = an input stream for reading a class file; CtClass cc = cp.makeClass(ins);

makeClass() 返回一个通过输入流构建出来的CtClass。你可以使用makeClass()ClassPool 对象提供一个Class文件。如果搜索路径包含了一个很大的jar包,这可以提高性能。因为ClassPool对象会一个一个找,它可能会重复搜索整个jar包中的每一个class文件。makeClass() 可以优化这个搜索。makeClass() 构造出来的类会保存在ClassPool对象中,你下次再用的时候,不会再次读Class文件。

2、ClassPool(重要)

2.1、 ClassPool简介(重要)

ClassPool对象是多个CtClass对象的容器。一旦CtClass对象被创建,它就会永远被记录在ClassPool对象中。这是因为编译器之后在编译源码的时候可能需要访问CtClass对象。

例如,假定有一个新方法getter() 被增添到了Point类的CtClass对象。稍后,程序会试图编译代码,它包含了对Point方法的getter() 调用,并会使用编译后代码作为一个方法的方法体。如果表示Point类的CtClass对象丢了的话,编译器就不能编译调用getter() 的方法了(注意:原始类定义中不包含getter() )。因此,为了正确编译这样一个方法调用,ClassPool在程序过程中必须始终包含所有的CtClass对象。

2.2、 避免内存溢出(重要)

某种特定的ClassPool可能造成巨大的内存消耗,导致OOM,比如CtClass对象变得非常的大(这个情况发生的很少,因为Javassist已经尝试用不同的方法减少内存消耗了,比如冻结类)。为了避免该问题,你可以从ClassPool中移除不需要的CtClass对象。只需要调用CtClassdetach() 方法就行了:

CtClass cc = ... ; cc.writeFile(); cc.detach(); // 该CtClass已经不需要了,从ClassPool中移除

在调用detach() 之后,这个CtClass对象就不能再调用任何方法了。但是你可以依然可以调用classPool.get() 方法来创建(没有则创建)一个相同的类。如果你调用get()ClassPool会再次读取class文件,然后创建一个新的CtClass对象并返回。

另一种方式是new一个新的ClassPool,旧的就不要了。这样旧的ClassPool就会被垃圾回收,它的CtClass也会被跟着垃圾回收。可以使用以下代码完成:

ClassPool cp = new ClassPool(true); // true代表使用默认路径 // 如果需要的话,可以用appendClassPath()添加一个额外的搜索路径。

上面这个new ClassPoolClassPool.getDefault() 的效果是一样。注意,ClassPool.getDefault() 是一个单例的工厂方法,它只是为了方便用户创建提供的方法。这两种创建方式是一样的,源码也基本是一样的,只不过ClassPool.getDefault() 是单例的。

注意,new ClassPool(true) 是一个很方便的构造函数,它构造了一个ClassPool对象,然后给他增添了系统搜索路径。它构造方法的调用就等同于下面的这段代码:

ClassPool cp = new ClassPool(); cp.appendSystemPath(); // 你也可以通过appendClassPath()增添其他路径

2.3、 级联ClassPool

如果一个程序运行在Web应用服务器上,你可能需要创建多个ClassPool实例。为每一个类加载器(ClassLoader)创建一个ClassPool(也就是容器)。这时程序在创建ClassPool对象的时候就不能再用getDefault() 了,而是要用ClassPool的构造函数。

多个ClassPool对象可以像java.lang.ClassLoader那样进行级联。例如:

ClassPool parent = ClassPool.getDefault(); ClassPool child = new ClassPool(parent); child.insertClassPath("./classes");

如果调用了child.get() ,child的ClassPool首先会代理parent的ClassPool,如果parent的ClassPool中没有找到要找的类,才会试图到child中的 ./classes目录下找。

如果child.childFirstLookup设置为了true,child的ClassPool就会首先到自己路径下面找,之后才会到parent的路径下面找。

ClassPool parent = ClassPool.getDefault(); ClassPool child = new ClassPool(parent); child.appendSystemPath();         // 这默认使用相同的类路径 child.childFirstLookup = true;   // 改变child的行为。

2.4、 更改类名的方式定义新类(重要)

一个“新类”可以从一个已经存在的类copy出来。可以使用以下代码:

ClassPool pool = ClassPool.getDefault(); CtClass cc = pool.get("Point"); cc.setName("Pair");

这段代码首先获取了PointCtClass对象。然后调用setName() 方法给对象一个新的名字Pair。在这个调用之后,CtClass表示的类中的所有Point都会替换为Pair。类定义的其他部分不会变。

既然setName() 改变了ClassPool对象中的记录。从实现的角度看,ClassPool是一个hash表,setName() 改变了关联这个CtClass对象的key值。这个key值从原名称Point变为了新名称Pair

因此,如果之后调用get("Point") ,就不会再返回上面的cc引用的对象了。ClassPool对象会再次读取class文件,然后构造一个新的CtClass对象。这是因为Point这个CtClassClassPool中已经不存在了。请看下面代码:

ClassPool pool = ClassPool.getDefault(); CtClass cc = pool.get("Point"); CtClass cc1 = pool.get("Point");   // 此时,cc1和cc是完全一样的。 cc.setName("Pair"); CtClass cc2 = pool.get("Pair");   // cc2和cc是完全一样的 CtClass cc3 = pool.get("Point");   // cc3和cc是不一样的,因为cc3是重新读取的class文件

cc1cc2引用的是相同的CtClass实例,与cc一样,cc3是另外一个实例对象。注意:cc.setName(“Pair”) 执行后,CtClass对象,cc和cc1引用的都都变成Pair类。

ClassPool对象用于维护CtClass对象和类之间的一一映射关系。Javassist不允许两个不同的CtClass对象代表相同的类,除非你用两个ClassPool。这个是程序转换一致性的重要特性。

要创建ClassPool的副本,可以使用下面的代码片段(这个上面已经提到过了):

ClassPool cp = new ClassPool(true);

如果你又两个ClassPool对象,那么你就可以从这两个对象中获取到相同class文件但是不同的CtClass对象。你可以对那两个CtClass进行不同方式的修改,然后生成两个版本的Class。

2.5、重命名冻结类的方式定义新类(重要)

一旦CtClass对象转化为Class文件后,比如writeFile() 或是 toBytecode() 之后,Javassist会拒绝CtClass对象进一步的修改。因此,在CtClass对象转为Class文件之后,你将不能再通过setNme() 的方式将该类拷贝成一个新的类了。比如,下面的这段错误代码:

ClassPool pool = ClassPool.getDefault(); CtClass cc = pool.get("Point"); cc.writeFile(); cc.setName("Pair");   // 错, 因为cc已经调用了writeFile()

为了解除这个限制,你应该调用ClassPoolgetAndRename() 方法。 例如:

ClassPool pool = ClassPool.getDefault(); CtClass cc = pool.get("Point"); cc.writeFile(); CtClass cc2 = pool.getAndRen

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值