本文译自Getting Started with Javassist,若是谬误之处,还请指出。html
bytecode读写java
ClassPoolexpress
Class loader数组
自有和定制服务器
Bytecode操控接口数据结构
Genericsapp
Varargs框架
J2MEless
装箱和拆箱ide
调试
1. bytecode读写
Javassist是用来处理java字节码的类库, java字节码通常存放在后缀名称为class的二进制文件中。每一个二进制文件都包含一个java类或者是java接口。
Javasist.CtClass是对类文件的抽象,处于编译中的此对象能够用来处理类文件。下面的代码用来展现一下其简单用法:
1: ClassPool pool = ClassPool.getDefault();
2: CtClass cc = pool.get("test.Rectangle");
3: cc.setSuperclass(pool.get("test.Point"));
4: cc.writeFile();
这段程序首先获取ClassPool的实例,它主要用来修改字节码的,里面存储着基于二进制文件构建的CtClass对象,它可以按需建立出CtClass对象并提供给后续处理流程使用。当须要进行类修改操做的时候,用户须要经过ClassPool实例的.get()方法,获取CtClass对象。从上面代码中咱们能够看出,ClassPool的getDefault()方法将会查找系统默认的路径来搜索test.Rectable对象,而后将获取到的CtClass对象赋值给cc变量。
从易于扩展使用的角度来讲,ClassPool是由装载了不少CtClass对象的HashTable组成。其中,类名为key,CtClass对象为Value,这样就能够经过搜索HashTable的Key来找到相关的CtClass对象了。若是对象没有被找到,那么get()方法就会建立出一个默认的CtClass对象,而后放入到HashTable中,同时将当前建立的对象返回。
从ClassPool中获取的CtClass对象,是能够被修改的。从上面的 代码中,咱们能够看到,原先的父类,由test.Rectangle被改为了test.Point。这种更改能够经过调用CtClass().writeFile()将其持久化到文件中。同时,Javassist还提供了toBytecode()方法来直接获取修改的字节码:
1: byte[] b = cc.toBytecode();
你能够经过以下代码直接加载CtClass:
1: Class clazz = cc.toClass();
toClass()方法被调用,将会使得当前线程中的context class loader加载此CtClass类,而后生成 java.lang.Class对象。更多的细节 ,请参见this section below.
新建类
新建一个类,可使用ClassPool.makeClass()方法来实现:
1: ClassPool pool = ClassPool.getDefault();
2: CtClass cc = pool.makeClass("Point");
上面的代码展现的是建立无成员方法的Point类,若是须要附带方法的话,咱们能够用CtNewMethod附带的工厂方法建立,而后利用CtClass.addMethod()将其追加就能够了 。
makeClass()不能用于建立新的接口。可是makeInterface()能够。接口的方法能够用CtNewmethod.abstractMethod()方法来建立,须要注意的是,在这里,一个接口方法实际上是一个abstract方法。
冻结类
若是CtClass对象被writeFile(),toClass()或者toBytecode()转换成了类对象,Javassist将会冻结此CtClass对象。任何对此对象的后续更改都是不容许的。之因此这样作,主要是由于此类已经被JVM加载,因为JVM自己不支持类的重复加载操做,因此不容许更改。
一个冻结的CtClass对象,能够经过以下的代码进行解冻,若是想更改类的话,代码以下:
1: CtClasss cc = ...;
2: :
3: cc.writeFile();
4: cc.defrost();
5: cc.setSuperclass(...); // OK since the class is not frozen.
调用了defrost()方法以后,CtClass对象就能够随意修改了。
若是ClassPool.doPruning被设置为true,那么Javassist将会把已冻结的CtClass对象中的数据结构进行精简,此举主要是为了防止过多的内存消耗。而精简掉的部分,都是一些没必要要的属性(attriute_info结构)。所以,当一个CtClass对象被精简以后,方法是没法被访问和调用的,可是方法名称,签名,注解能够被访问。被精简过的CtClass对象能够被再次解冻。须要注意的是,ClassPool.doPruning的默认值为false。
为了防止CtClass类被无故的精简,须要优先调用stopPruning()方法来进行阻止:
1: CtClasss cc = ...;
2: cc.stopPruning(true);
3: :
4: cc.writeFile(); //转换为类文件,cc不会被精简.
这样,CtClass对象就不会被精简了。当writeFile()方法调用以后,咱们就能够进行解冻,而后随心所欲了。
须要注意的是:在调试的时候, debugWriteFile()方法能够很方便的防止CtClass对象精简和冻住。
类搜索路径
ClassPool.getDefault()方法的搜索路径和JVM的搜索路径是一致的。若是程序运行在JBoss或者Tomcat服务器上,那么ClassPool对象也许不可以找到用户类,缘由是应用服务器用的是多个class loader,其中包括系统的class loader来加载对象。正因如此,ClassPool须要 附加特定的类路径才行。 假设以下的pool实例表明ClassPool对象:
1: pool.insertClassPath(new ClassClassPath(this.getClass()));
上面的代码段注册了this所指向的类路径下面的类对象。你能够用其余的类对象来代替this.getClass()。这样就能够加载其余不一样的类对象了。
你也能够注册一个目录名字来做为类搜索路径。好比下面代码中,使用/usr/local/javalib目录做为搜索路径:
1: ClassPool pool = ClassPool.getDefault();
2: pool.insertClassPath("/usr/local/javalib");
也可使用url来做为搜索路径:
1: ClassPool pool = ClassPool.getDefault();
2: ClassPath cp = new URLClassPath("www.javassist.org", 80, "/java/", "org.javassist.");
3: pool.insertClassPath(cp);
上面这段代码将会添加“http://www.javassist.org:80/java/”到类搜索路径。这个URL主要用来搜索org.javassist包下面的类。好比加载org.javassist.test.Main类,此类将会从以下路径获取:
1: http://www.javassist.org:80/java/org/javassist/test/Main.class
此外,你甚至能够直接使用一串字节码,而后建立出CtClass对象。示例以下:
1: ClassPool cp = ClassPool.getDefault();
2: byte[] b = a byte array;
3: String name = class name;
4: cp.insertClassPath(new ByteArrayClassPath(name, b));
5: CtClass cc = cp.get(name);
从上面代码能够看出,ClassPool加载了ByteArrayClasPath构建的对象,而后利用get()方法并经过类名,将对象赋值给了CtClass对象。
若是你不知道类的全名,你也能够用makeClass()来实现:
1: ClassPool cp = ClassPool.getDefault();
2: InputStream ins = an input stream for reading a class file;
3: CtClass cc = cp.makeClass(ins);
makeClass()方法利用给定的输入流构建出CtClass对象。你能够用饿汉方式直接建立出ClassPool对象,这样当搜索路径中有大点的jar文件须要加载的时候,能够提高一些性能,之因此 这样作,缘由是ClassPool对象按需加载类文件,因此它可能会重复搜索整个jar包中的每一个类文件,正由于如此,makeClass()能够用于优化查找的性能。被makeClass()方法加载过的CtClass对象将会留存于ClassPool对象中,不会再进行读取。
用户能够扩展类搜索路径。能够经过定义一个新的类,扩展自ClassPath接口,而后返回一个insertClassPath便可。这种作法能够容许其余资源被包含到搜索路径中。
2. ClassPool
一个ClassPool里面包含了诸多的CtClass对象。每当一个CtClass对象被建立的时候,都会在ClassPool中作记录。之因此这样作,是由于编译器后续的源码编译操做可能会经过此类关联的CtClass来获取。
好比,一个表明了Point类的CtClass对象,新加一个getter()方法。以后,程序将会尝试编译包含了getter()方法的Point类,而后将编译好的getter()方法体,添加到另一个Line类上面。若是CtClass对象表明的Point类不存在的话,那么编译器就不会成功的编译getter()方法。须要注意的是原来的类定义中并不包含getter()方法 。所以,要想正确的编译此方法,ClassPool对象必须包含程序运行时候的全部的CtClass对象。
避免内存溢出
CtClass对象很是多的时候,ClassPool将会消耗内存巨大。为了不个问题,你能够移除掉一些不须要的CtClass对象。你能够经过调用CtClass.detach()方法来实现,那样的话此CtClass对象将会从ClassPool移除。代码以下:
1: CtClass cc = ... ;
2: cc.writeFile();
3: cc.detach();
此CtClass对象被移除后,不能再调用其任何方法。可是你能够调用ClassPool.get()方法来建立一个新的CtClass实例。
另外一个方法就是用新的ClassPool对象来替代旧的ClassPool对象。若是旧的ClassPool对象被垃圾回收了,那么其内部的CtClass对象也都会被垃圾回收掉。下面的代码能够用来建立一个新的ClassPool对象:
1: ClassPool cp = new ClassPool(true);
2: //若是须要的话,利用appendClassPath()来添加额外的搜索路径
上面的代码和ClassPool.getDefault()来建立ClassPool,效果是同样的。须要注意的是,ClasssPool.getDefault()是一个单例工厂方法,它可以建立出一个惟一的ClassPool对象并进行重复利用。new ClassPool(true)是一个很快捷的构造方法,它可以建立一个ClassPool对象而后追加系统搜索路径到其中。和以下的代码建立行为表现一致:
1: ClassPool cp = new ClassPool();
2: cp.appendSystemPath(); // or append another path by appendClassPath()
级联ClassPools
若是应用运行在JBOSS/Tomcat上, 那么建立多个ClassPool对象将会颇有必要。由于每一个类加载其都将会持有一个ClassPool的实例。应用此时最好不用getDefault()方法来建立ClassPool对象,而是使用构造来建立。
多个ClassPool对象像java.lang.ClassLoader同样作级联,代码以下:
1: ClassPool parent = ClassPool.getDefault();
2: ClassPool child = new ClassPool(parent);
3: child.insertClassPath("./classes");
若是child.get()被调用,子ClassPool将会首先从父ClassPool进行查找。当父ClassPool查找不到后,而后将会尝试从./classes目录进行查找。
若是child.childFirstLookup = true, 子ClassPool将会首先查找本身的目录,而后查找父ClassPool,代码以下:
1: ClassPool parent = ClassPool.getDefault();
2: ClassPool child = new ClassPool(parent);
3: child.appendSystemPath(); //和默认的搜索地址一致.
4: child.childFirstLookup = true; //修改子类搜索行为.
为新类重命名
能够从已有类建立出新的类,代码以下:
1: ClassPool pool = ClassPool.getDefault();
2: CtClass cc = pool.get("Point");
3: cc.setName("Pair");
此代码首先从Point类建立了CtClass对象,而后调用setName()重命名为Pair。以后,全部对CtClass对象的引用,将会由Point变成Pair。
须要注意的是setName()方法改变ClassPool对象中的标记。从可扩展性来看,ClassPool对象是HashTable的合集,setName()方法只是改变了key和Ctclass对象的关联。
所以,对于get("Point")方法以后的全部调用,将不会返回CtClasss对象。ClassPool对象再次读取Point.class的时候,将会建立一个新的CtClass,这是由于和Point关联的CtClass对象已经不存在了,请看以下代码:
1: ClassPool pool = ClassPool.getDefault();
2: CtClass cc = pool.get("Point");
3: CtClass cc1 = pool.get("Point"); //cc1和cc是一致的.
4: cc.setName("Pair");
5: CtClass cc2 = pool.get("Pair"); //cc2和cc是一致的.
6: CtClass cc3 = pool.get("Point"); //cc3和cc是不一致的.
cc1和cc2将会指向cc,可是cc3却不会。须要注意的是,在cc.setName("Pair")执行后,cc和cc1指向的CtClass对象都变成了指向Pair类。
ClassPool对象用来维护类之间和CtClass对象之间一对一的映射关系。Javassist不容许两个不一样的CtClass对象指向同一个类,除非两个独立的ClassPool存在的状况下。这是为实现程序转换而保证其一致性的最鲜明的特色。
咱们知道,能够利用ClassPool.getDefault()方法建立ClassPool的实例,代码片断以下(以前已经展现过):
1: ClassPool cp = new ClassPool(true);
若是你有两个ClassPool对象,那么你能够从这两个对象中分别取出具备相同类文件,可是隶属于不一样的CtClass对象生成的,此时能够经过修改这俩CtClass对象来生成不一样的类。
从冻结类中建立新类
当CtClass对象经过writeFile()方法或者toBytecode()转变成类文件的时候,Javassist将不容许对这个CtClass对象有任何修改。所以,当表明Point类的CtClass对象被转换成了类文件,你不可以先拷贝Point类,而后修更名称为Pair类,由于Point类中的setName()方法是没法被执行的,错误使用示例以下:
1: ClassPool pool = ClassPool.getDefault();
2: CtClass cc = pool.get("Point");
3: cc.writeFile();
4: cc.setName("Pair"); // wrong since writeFile() has been called.
为了可以避免这种限制,你应该使用getAndRename()方法,正确示例以下:
1: ClassPool pool = ClassPool.getDefault();
2: CtClass cc = pool.get("Point");
3: cc.writeFile();
4: CtClass cc2 = pool.getAndRename("Point", "Pair");
若是getAndRename()方法被调用,那么ClassPool首先会基于Point.class来建立一个新的CtClass对象。以后,在CtClass对象被放到HashTable前,它将CtClass对象名称从Point修改成Pair。所以,getAndRename()方法能够在writeFile()方法或者toBytecode()方法执行后去修改CtClass对象。
3. 类加载器
若是预先知道须要修改什么类,最简单的修改方式以下:
1. 调用ClassPool.get()方法获取CtClass对象
2. 修改此对象
3. 调用CtClass对象的writeFile()方法或者toBytecode()方法来生成类文件。
若是检测类是否修改行为发生在程序加载的时候,那么对于用户说来,Javassist最好提供这种与之匹配的类加载检测行为。事实上,javassist能够作到在类加载的时候来修改二进制数据。使用Javassist的用户能够定义本身的类加载器,固然也能够采用Javassist自身提供的。
3.1 CtClass中的toClass方法
CtClass提供的toClass()方法,能够很方便的加载当前线程中经过CtClass对象建立的类。可是为了使用此方法,调用方必须拥有足够的权限才行,不然将会报SecurityException错误。
下面的代码段展现了如何使用toClass()方法:
1: public class Hello {
2: public void say() {
3: System.out.println("Hello");
4: }
5: }
6:
7: public class Test {
8: public static void main(String[] args) throws Exception {
9: ClassPool cp = ClassPool.getDefault();
10: CtClass cc = cp.get("Hello");
11: CtMethod m = cc.getDeclaredMethod("say");
12: m.insertBefore("{ System.out.println(\"Hello.say():\"); }");
13: Class c = cc.toClass();
14: Hello h = (Hello)c.newInstance();
15: h.say();
16: }
17: }
Test.main()方法中, say()方法被插入了println()方法,以后这个被修改的Hello类实例被建立,say()方法被调用。
须要注意的是,上面代码中,Hello类是放在toClass()以后被调用的,若是不这么作的话,JVM将会先加载Hello类,而不是在toClass()方法加载Hello类以后再调用Hello类,这样作会致使加载失败(会抛出LinkageError错误)。好比,若是Test.main()方法中的代码以下:
1: public static void main(String[] args) throws Exception {
2: Hello orig = new Hello();
3: ClassPool cp = ClassPool.getDefault();
4: CtClass cc = cp.get("Hello"); CtMethod m = cc.getDeclaredMethod("say");
5: m.insertBefore("{ System.out.println(\"Hello.say():\"); }");
6: Class c = cc.toClass();
7: Hello h = (Hello)c.newInstance();
8: h.say();}
main方法中,第一行的Hello类会被加载,以后调用toClass()将会报错,由于一个类加载器没法在同一时刻加载两个不一样的Hello类版本。
若是程序跑在JBoss/Tomcat上,利用toClass()方法可能会有些问题。在这种状况下,你将会遇到ClassCastException错误,为了不这种错误,你必须为toClass()方法提供很是明确的类加载器。好比,在以下代码中,bean表明你的业务bean对象的时候:
1: CtClass cc = ...;
2: Class c = cc.toClass(bean.getClass().getClassLoader());
则就不会出现上述问题。你应当为toClass()方法提供已经加载过程序的类加载器才行。
toClass()的使用会带来诸多方便,可是若是你须要更多更复杂的功能,你应当实现本身的类加载器。
3.2 java中的类加载
在java中,多个类加载器能够共存,不一样的类加载器会建立本身的应用区域。不一样的类加载器能够加载具备相同类名称可是内容不尽相同的类文件。这种特性可让咱们在一个JVM上并行运行多个应用。
须要注意的是JVM不支持动态的从新加载一个已加载的类。一旦类加载器加载了一个类,那么这个类或者基于其修改的类,在JVM运行时,都不能再被加载。所以,你不可以修改已经被JVM加载的类。可是,JPDA(Java Platform Debugger Architecture)支持这种作法。具体请见 Section 3.6.
若是一个类被两个不一样的类加载器加载,那么JVM会将此类分红两个不一样的类,可是这两个类具备相同的类名和定义。咱们通常把这两个类当作是不一样的类,因此一个类不可以被转换成另外一个类,一旦这么作,那么这种强转操做将会抛出错误ClassCastException。
好比,下面的例子会抛错:
1: MyClassLoader myLoader = new MyClassLoader();
2: Class clazz = myLoader.loadClass("Box");
3: Object obj = clazz.newInstance();
4: Box b = (Box)obj; //会抛出ClassCastException错误.
Box类被两个类加载器所加载,试想一下,假设CL类加载器加载的类包含此代码段,因为此代码段指向MyClassLoader,Class,Object,Box,因此CL加载器也会将这些东西加载进来(除非它是其它类加载器的代理)。所以变量b就是CL中的Box类。从另外一方面说来,myLoader也加载了Box类,obj对象是Box类的实例,所以,代码的最后一行将一直抛出ClassCastException错误,由于obj和b是Box类的不一样实例副本。
多个类加载器会造成树状结构,除了底层引导的类加载器外,每个类加载器都有可以正常的加载子加载器的父加载器。因为加载类的请求能够被类加载器所代理,因此一个类可能会被你所不但愿看到的类加载器所加载。所以,类C可能会被你所不但愿看到的类加载器所加载,也可能会被你所但愿的加载器所加载。为了区分这种现象,咱们称前一种加载器为类C的虚拟引导器,后一种加载器为类C的真实加载器。
此外,若是类加载器CL(此类加载器为类C的虚拟引导器)让其父加载器PL来加载类C,那么至关于CL没有加载任何类C相关的东西。此时,CL就不能称做虚拟引导器。相反,其父类加载器PL将会变成虚拟引导器。全部指向类C定义的类,都会被类C的真实加载器所加载。
为了理解这种行为,让咱们看看以下的例子:
1: public class Point { // 被PL加载
2: private int x, y;
3: public int getX() { return x; }
4: :
5: }
6:
7: public class Box { // 初始化器为L可是实际加载器为PL
8: private Point upperLeft, size;
9: public int getBaseX() { return upperLeft.x; }
10: :
11: }
12:
13: public class Window { // 被L加载器所加载
14: private Box box;
15: public int getBaseX() { return box.getBaseX(); }
16: }
假如Window类被L加载器所加载,那么Window的虚拟加载器和实际加载器都是L。因为Window类中引用了Box类,JVM将会加载Box类,这里,假设L将此加载任务代理给了其父加载器PL,那么Box的类加载器将会变成L,可是其实际加载器将会是PL。所以,在此种状况下,Point类的虚拟加载器将不是L,而是PL,由于它和Box的实际加载器是同样的。所以L加载器将永远不会加载Point类。
接下来,让咱们看一个少许更改过的例子:
1: public class Point {
2: private int x, y;
3: public int getX() { return x; }
4: :
5: }
6:
7: public class Box { // the initiator is L but the real loader is PL
8: private Point upperLeft, size;
9: public Point getSize() { return size; }
10: :
11: }
12:
13: public class Window { // loaded by a class loader L
14: private Box box;
15: public boolean widthIs(int w) {
16: Point p = box.getSize();
17: return w == p.getX();
18: }
19: }
如今看来,Window类指向了Point,所以类加载器L要想加载Point的话,它必须代理PL。必须杜绝的状况是,两个类加载器加载同一个类的状况。其中一个类加载器必须可以代理另外一个才行。
当Point类加载后,L没有代理PL,那么widthIs()将会抛出ClassCastExceptioin。因为Box类的实际加载器是PL,因此指向Box类的Point类将也会被PL所加载。所以,getSize()方法的最终结果将是被PL加载的Point对象的实例。反之,widthIs()方法中的p变量的类型将是被L所加载的Point类。对于这种状况,JVM会将其视为不一样的类型,从而由于类型不匹配而抛出错误。
这种状况,虽然不方便,可是却颇有必要,来看一下以下代码段:
1: Point p = box.getSize();
没有抛出错误,Window将会破坏Point对象的包装。举个例子吧,被PL加载的Point类中,x字段是私有的。可是,若是L利用以下的定义加载了Point类的话,那么Window类是能够直接访问x字段的:
1: public class Point {
2: public int x, y; // not private
3: public int getX() { return x; }
4: :
5: }
想要了解java中更多的类加载器信息,如下信息也许有帮助:
Sheng Liang and Gilad Bracha, "Dynamic Class Loading in the Java Virtual Machine",
ACM OOPSLA'98, pp.36-44, 1998.
3.3 使用javassist.Loader
Javassist提供了javassist.Loader这个类加载器。它使用javassist.ClassPool对象来读取类文件。
举个例子,使用javassist.Loader来加载Javassist修改过的类:
1: import javassist.*;
2: import test.Rectangle;
3:
4: public class Main {
5: public static void main(String[] args) throws Throwable {
6: ClassPool pool = ClassPool.getDefault();
7: Loader cl = new Loader(pool);
8:
9: CtClass ct = pool.get("test.Rectangle");
10: ct.setSuperclass(pool.get("test.Point"));
11:
12: Class c = cl.loadClass("test.Rectangle");
13: Object rect = c.newInstance();
14: :
15: }
16: }
上面的程序就修改了test.Rectangle类,先是test.Point类被设置成了test.Rectangle类的父类,以后程序会加载这个修改的类并建立test.Rectangle类的实例出来。
若是一个类被加载后,用户想要修改为本身想要的东西进来,那么用户能够经过添加事件监听器到javassist.Loader上。每当类加载器加载了类进来,那么事件监听器将会发出通知。此监听器必须实现以下的接口:
1: public interface Translator {
2: public void start(ClassPool pool)
3: throws NotFoundException, CannotCompileException;
4: public void onLoad(ClassPool pool, String classname)
5: throws NotFoundException, CannotCompileException;
6: }
当利用javassist.Loader.addTranslator()将事件监听器添加到javassist.Loader对象上的时候,上面的start()方法将会被触发。而onLoad()方法的触发先于javassist.Loader加载一个类,所以onLoad()方法能够改变已加载的类的定义。
举个例子,下面的事件监听器将会在类被加载器加载以前,修改其类型为public:
1: public class MyTranslator implements Translator {
2: void start(ClassPool pool)
3: throws NotFoundException, CannotCompileException {}
4: void onLoad(ClassPool pool, String classname)
5: throws NotFoundException, CannotCompileException
6: {
7: CtClass cc = pool.get(classname);
8: cc.setModifiers(Modifier.PUBLIC);
9: }
10: }
须要注意的是,onLoad()方法不须要调用toBytecode方法或者writeFile方法,由于javassistLoader会调用这些方法来获取类文件。
为了可以运行MyApp类中的MyTranslator对象,写了一个主方法以下:
1: import javassist.*;
2:
3: public class Main2 {
4: public static void main(String[] args) throws Throwable {
5: Translator t = new MyTranslator();
6: ClassPool pool = ClassPool.getDefault();
7: Loader cl = new Loader();
8: cl.addTranslator(pool, t);
9: cl.run("MyApp", args);
10: }
11: }
想要运行它,能够按照以下命令来:
1: % java Main2 arg1 arg2...
MyApp类和其余的一些类,会被MyTranslator所翻译。
须要注意的是,相似MyApp这种应用类,是不可以访问Main2,MyTranslator,ClassPool这些类的,由于这些类是被不一样加载器所加载的。应用类是被javassist.Loader所加载,而Main2这些是被java的默认类加载器所加载的。
javassist.Loader搜寻须要加载的类的时候,和java.lang.ClassLoader.ClassLoader是大相径庭的。后者先使用父类加载器进行加载,若是父类加载器找不到类,则尝试用当前加载器进行加载。而javassist.Load在以下状况下,则尝试直接加载:
ClassPool对象上,没法找到get方法
或者
父类使用delegateLoadingOf()方法进行加载
Javassist能够按照搜索的顺序来加载已修改的类,可是,若是它没法找到已修改的类,那么将会由父类加载器进行加载操做。一旦当一个类被父加载器所加载,那么指向此类的其余类,也将被此父加载器所加载,由于,这些被加载类是不会被修改的。若是你的程序没法加载一个已修改的类,你须要确认全部的类是不是被javassist.Loader所加载。
3.4 打造一个类加载器
用javassist打造一个简单的类加载器,代码以下:
1: import javassist.*;
2:
3: public class SampleLoader extends ClassLoader {
4: /* Call MyApp.main().
5: */
6: public static void main(String[] args) throws Throwable {
7: SampleLoader s = new SampleLoader();
8: Class c = s.loadClass("MyApp");
9: c.getDeclaredMethod("main", new Class[] { String[].class })
10: .invoke(null, new Object[] { args });
11: }
12:
13: private ClassPool pool;
14:
15: public SampleLoader() throws NotFoundException {
16: pool = new ClassPool();
17: pool.insertClassPath("./class"); // MyApp.class must be there.
18: }
19:
20: /* Finds a specified class.
21: * The bytecode for that class can be modified.
22: */
23: protected Class findClass(String name) throws ClassNotFoundException {
24: try {
25: CtClass cc = pool.get(name);
26: // modify the CtClass object here
27: byte[] b = cc.toBytecode();
28: return defineClass(name, b, 0, b.length);
29: } catch (NotFoundException e) {
30: throw new ClassNotFoundException();
31: } catch (IOException e) {
32: throw new ClassNotFoundException();
33: } catch (CannotCompileException e) {
34: throw new ClassNotFoundException();
35: }
36: }
37: }
MyApp类是一个应用程序。为了执行这个应用,咱们首先须要将类文件放到./class文件夹下,须要确保当前文件夹不在类搜索目录下,不然将会被SampleLoader的父类加载器,也就是系统默认的类加载器所加载。./class目录名称在insertClassPath方法中必需要有所体现,固然此目录名称是能够随意改变的。接下来咱们运行以下命令:
1: % java SampleLoader
此时,类加载器将会加载MyApp类(./class/MyApp.class)并调用MyApp.main方法。
这是使用基于Javassist类加载器最简单的方式。然而,若是你想写一个更加复杂的类加载器,你须要对Java的类加载器机制有足够的了解。好比,上面的代码中,MyApp类的命名空间和SampleLoader类的命名空间是不一样的,是由于这两个类是被不一样的类加载器锁加载的。所以,MyApp类没法直接访问SampleLoader类。
3.5 修改系统类
系统类,好比java.lang.String,会优先被系统的类加载器所加载。所以,上面展现的SampleLoader或者javassist.Loader在进行类加载的时候,是没法修改系统类的。
若是须要进行修改的话,系统类必须被静态的修改。好比,下面的代码将会给java.lang.String添加一个hiddenValue的字段:
1: ClassPool pool = ClassPool.getDefault();
2: CtClass cc = pool.get("java.lang.String");
3: CtField f = new CtField(CtClass.intType, "hiddenValue", cc);
4: f.setModifiers(Modifier.PUBLIC);
5: cc.addField(f);
6: cc.writeFile(".");
此段代码会产生"./java/lang/String.class"文件。
为了可以让更改的String类在MyApp中运行,能够按照以下的方式来进行:
1: % java -Xbootclasspath/p:. MyApp arg1 arg2...
假设MyApp的代码以下:
1: public class MyApp {
2: public static void main(String[] args) throws Exception {
3: System.out.println(String.class.getField("hiddenValue").getName());
4: }
5: }
此更改的String类成功的被加载,而后打印出了hiddenValue。
须要注意的是:用如上的方式来修改rt.jar中的系统类并进行部署,会违反Java 2 Runtime Environment binary code license.
3.6 运行状态下从新加载类
若是JVM中的JPDA(Java Platform Debugger Architecture)是可用状态,那么一个类是能够被动态加载的。JVM加载类后,此类的以前版本将会被卸载,而新版本将会被加载。因此,从这里看出,在运行时状态,类是能够被动态更改的。然而,新的类必须可以和旧的类兼容,是由于JVM不容许直接更改类的总体框架,他们必须有相同的方法和字段。
Javassist提供了简单易用的方式来从新加载运行时的类。想要获取更多内容,请翻阅javassist.tools.HotSwapper的API文档。
4. 定制化
CtClass提供了不少方法来用进行定制化。Javassist能够和Java的反射API进行联合定制。CtClass提供了getName方法,getSuperclass方法,getMethods方法等等。CtClass同时也提供了方法来修改类定义,容许添加新的字段,构造,方法等。即使对于检测方法体这种事情来讲,也是可行的。
方法都是被CtMethod对象所表明,它提供了多个方法用于改变方法的定义,须要注意的是,若是方法继承自父类,那么在父类中的一样方法将也会被CtMethod所表明。CtMethod对象能够正确的表明任何方法声明。
好比,Point类有一个move方法,其子类ColorPoint不会重写move方法, 那么在这里,两个move方法,将会被CtMethod对象正确的识别。若是CtMethod对象的方法定义被修改,那么此修改将会反映到两个方法上。若是你想只修改ColorPoint类中的move方法,你须要首先建立ColorPoint的副本,那么其CtMethod对象将也会被复制,CtMethod对象可使用CtNewMethod.copy方法来实现。
Javassist不支持移除方法或者字段,可是支持修更名字。因此若是一个方法再也不须要的话,能够在CtMethod中对其进行重命名并利用setName方法和setModifiers方法将其设置为私有方法。
Javassist不支持为已有的方法添加额外的参数。可是能够经过为一个新的方法建立额外的参数。好比,若是你想添加一个额外的int参数newZ到Point类的方法中:
1: void move(int newX, int newY) { x = newX; y = newY; }
你应当在Point类中添加以下方法
1: void move(int newX, int newY, int newZ) {
2: // do what you want with newZ.
3: move(newX, newY);
4: }
Javassist同时也提供底层的API来直接修改原生的类文件。好比,CtClass类中的getClassFile方法能够返回一个ClassFile对象来表明一个原生的类文件。而CtMethod中的getMethodInfo方法则返回MethodInfo对象来表明一个类中的method_info结构。底层的API单词大多数来自于JVM,因此用于用起来不会感受到陌生。更多的内容,能够参看 javassist.bytecode
package.
Javassist修改类文件的时候,通常不须要javassist.runtime包,除非一些特别的以$符号开头的。这些特殊符号会在后面进行讲解。更多的内容,能够参考javassist.runtime包中的API文档。
4.1 方法体前/后穿插代码段
CtMethod和CtConstructor提供了insertBefore,insertAfter,addCatch三个方法,它们用于在已存在的方法中插入代码段。使用者能够插入java代码段是由于Javassist内置了一个简易的java编译器来处理这些源码。此编译器会将java源码编译成字节码,而后插入到方法体中。
同时,在指定行号的位置插入代码段也是容许的(只有当行号在当前类中存在)。CtMethod和CtConstructor中的insertAt方法带有源码输入和行号的定义,它可以将编译后的代码段插入到指定了行号的位置。
insertBefore,insertAfter,addCatch和insertAt方法均接受一个String类型的表明源码块的入参。此代码段能够是简单的控制类语句if和while,也能够是以分号结尾的表达式,都须要用左右大括号{}进行包装。所以,下面的示例源码都是符合要求的代码段:
1: System.out.println("Hello");
2: { System.out.println("Hello"); }
3: if (i < 0) { i = -i; }
代码段能够指向字段和方法,也能够为编译器添加-g选项来让其指向插入的方法中的参数。不然,只能利用$0,$1,$2...这种以下的变量来进行访问。虽然不容许访问方法中的本地变量,可是在方法体重定义一个新的本地变量是容许的。例外的是,编译器开启了-g选项的话,insertAt方法是容许代码段访问本地变量的。
insertBefore,insertAfter,addCatch和insertAt入参中的String对象,也就是用户输入的代码段,会被Javassist中的编译器编译,因为此编译器支持语言扩展,不一样的$符号有不一样的含义:
$0
, $1
, $2
, ... this 和实参
$args
参数列表. $args的类型是
Object[]
.
$$
全部实参.例如, m($$)
等价于 m($1,$2,
...)
$cflow(
...)
cflow变量
$r
结果类型. 用于表达式转换.
$w
包装类型. 用于表达式转换.
$_
结果值
$sig
java.lang.Class列表,表明正式入参类型
$type
java.lang.Class对象,表明正式入参值
.
$class
java.lang.Class对象,表明传入的代码段
.
传给目标方法的参数$1,$2...将会替换掉原始的参数名称。$1表明第一个参数,$2表明第二个参数,以此类推。这些参数的类型和原始的参数类型是一致的。$0等价于this关键字,若是方法为static,那么$0将不可用。
这些变量的使用方法以下,以Point类为例:
1: class Point {
2: int x, y;
3: void move(int dx, int dy) { x += dx; y += dy; }
4: }
调用move方法,打印dx和dy的值,执行以下的程序
1: ClassPool pool = ClassPool.getDefault();
2: CtClass cc = pool.get("Point");
3: CtMethod m = cc.getDeclaredMethod("move");
4: m.insertBefore("{ System.out.println($1); System.out.println($2); }");
5: cc.writeFile();
须要注意的是,insertBefore方法中的代码段是被大括号{}包围的,此方法只接受一个被大括号包围的代码段入参。
更改以后的Point类以下:
1: class Point {
2: int x, y;
3: void move(int dx, int dy) {
4: { System.out.println(dx); System.out.println(dy); }
5: x += dx; y += dy;
6: }
7: }
$1和$2被dx和dy替换掉。
从这里能够看出,$1,$2,$3...是能够被更新的。若是一个新的值被赋予了这几个变量中的任意一个,那么这个变量对应的参数值也会被更新。下面来讲说其余的参数。
$args变量表明全部参数列表。其类型为Object数组类型。若是一个参数类型基础数据类型,好比int,那么将会被转换为java.lang.Integer并放到$args中。所以,$args[0]通常状况下等价于$1,除非第一个参数的类型为基础数据类型。须要注意的是,$args[0]和$0是不等价的,由于$0表明this关键字。
若是object列表被赋值给$args,那么列表中的每一个元素将会被分配给对应的参数。若是一个参数的类型为基础数据类型,那么对应的正确的数据类型为包装过的类型。此转换会发生在参数被分配以前。
$$是被逗号分隔的全部参数列表的缩写。好比,若是move方法中的参数数量有三个,那么
move($$)
等价于:
move($1,$2,$3)
若是move()无入参,那么move($$)等价于move().
$$也能够被用于其余的场景,若是你写了以下的表达式:
exMove($$,context)
那么此表达式等价于:
exMove($1,$2,$3,context)
须要注意的是,$$虽然说是方法调用的通用符号,可是通常和$proceed联合使用,后面会讲到。
表明着“流程控制”。这个只读变量会返回方法的递归调用深度。
假设以下的方法表明CtMethod中的对象cm:
1: int fact(int n) {
2: if (n <= 1)
3: return n;
4: else
5: return n * fact(n - 1);
6: }
为了使用$cflow,首先须要引用$cflow,用于监听fact方法的调用
1: CtMethod cm = ...;
2: cm.useCflow("fact");
useCflow()方法就是用来声明$cflow变量。任何可用的java命名均可以用来进行识别。此名称也能够包含.(点号),好比"my.Test.face"也是能够的。
而后,$cflow(fact)表明着方法cm递归调用的深度。当方法第一次被调用的时候,$cflow(fact)的值为0,再调用一次,此值将会变为1.好比:
1: cm.insertBefore("if ($cflow(fact) == 0)"
2: + " System.out.println(\"fact \" + $1);");
代码段将fact方法进行编译以便于可以看到对应的参数。因为$cflow(fact)被选中,那么对fact方法的递归调用将不会显示参数。
$cflow的值是当前线程中,从cm方法中,最上层栈帧到当前栈帧的值。$cflow同时和cm方法在同一个方法内部的访问权限也是不同的。
表明着结果类型,必须在转换表达式中用做类型转换。好比,以下用法
1: Object result = ... ;
2: $_ = ($r)result;
若是结果类型为基础数据类型,那么($r)须要遵循以下的规则:
首先,若是操做数类型是基础数据类型,($r)将会被当作普通的转义符。相反的,若是操做数类型是包装类型,那么($r)将会把此包装类型转换为结果类型,好比若是结果类型是int,那么($r)会将java.lang.Integer转换为intl;若是结果类型是void,那么($r)将不会进行类型转换;若是当前操做调用了void方法,那么($r)将会返回null。举个例子,若是foo方法是void方法,那么:
1: $_ = ($r)foo();
是一个有效的申明。
转换符号($r)同时也用于return申明中,即使返回类型是void,以下的return申明也是有效的:
1: return ($r)result;
这里,result是一个本地变量,因为($r)这里作了转换,那么返回结果是无效的。此时的return申明和没有任何返回的return申明是等价的:
1: return;
表明包装类型。必须在转义表达式中用于类型转换。($w)将基础类型转换为对应的包装类型,以下代码示例
1: Integer i = ($w)5;
结果类型依据($w)后面的表达式来肯定,若是表达式是double类型,那么包装类型则为java.lang.Double。若是($w)后面的表达式不是基础类型,那么($w)将不进行任何转换。
CtMethod和CtConstructor中的insertAfter方法将编译过的代码插入到方法的尾部。以前给过的一些例子有关insertAfter的例子中,不只包括$0.$1这种例子的讲解,并且包括$_的这种例子。说道$_变量,它用来表明方法的结果值。其变量类型是方法返回的结果类型。若是返回的结果类型是void,那么$_的类型是Object类型,可是其值为null。
尽管利用insertAfter插入的编译过的代码,是在方法返回以前被执行的,可是这种代码也能够在在方法抛出的exception中执行。为了可以让其在方法抛出的exception中执行,insertAfter方法中的第二个参数asFinally必须为true。
当exception被抛出的时候,利用insertAfter方法插入的代码段将会和做为finally代码块来执行。此时在编译过的代码中,$_的值为0或者null。当此代码段执行完毕后,exception会被从新抛给调用端。须要注意的是,$_是永远不会被抛给调用端的,它会直接被抛弃掉。
$type的值是java.lang.Class对象,表明着返回值的正确的类型。若是它指向的是构造器,那么其值为Void.class。
$class的值是java.lang.Class对象,表明着当前编辑的方法,此时和$0是等价的。
此方法用于将代码段插入到方法体中进行执行,在执行过程当中一旦方法体抛出exception,能够控制给发送给客户端的返回。下面的源码展现了利用特殊的变量$e来指向exception
1: CtMethod m = ...;
2: CtClass etype = ClassPool.getDefault().get("java.io.IOException");
3: m.addCatch("{ System.out.println($e); throw $e; }", etype);
此方法体m被翻译出来后,展现以下:
1: try {
2: the original method body
3: }
4: catch (java.io.IOException e) {
5: System.out.println(e);
6: throw e;
7: }
须要注意的是,插入的代码段必须以throw或者return命令结尾。
4.2 修改方法体
CtMethod和CtContructor提供setBody方法来取代整个方法体。此方法可以将传入的代码段编译为Java字节码,而后用此字节码将其原有的方法体给替换掉。若是给定代码段为空,那么被替换的方法体将只有return 0声明,若是结果类型为void,那么则只有 return null声明。
外部传入给setBody方法的代码段,会包含以下的以$开头的识别码,这些识别码有不一样的含义:
$0
, $1
, $2
, ... this
和实参
$args
参数列表.$args类型为
Object数组
.
$$
全部参数.
$cflow(
...)
cflow变量
$r
结果类型. 用于表达式转换.
$w
包装类型. 用于表达式转换.
$sig
java.lang.Class对象数组,表明正式的参数类型
.
$type
java.lang.Class
对象,表明正式的结果类型.
$class
java.lang.Class对象,表明当前操做的方法
(等价于$0的类型).
须要注意的是,此时$_是不可用的。
Javassist容许修改方法体中的表达式。能够利用javassist.expr.ExprEditor类来进行替换操做。用户能够经过定义ExprEditor的子类来修改表达式。为了运行ExprEditor对象,用户必须调用CtMethod或者CtClass中的instrument方法来进行,示例以下
1: CtMethod cm = ... ;
2: cm.instrument(
3: new ExprEditor() {
4: public void edit(MethodCall m)
5: throws CannotCompileException
6: {
7: if (m.getClassName().equals("Point")
8: && m.getMethodName().equals("move"))
9: m.replace("{ $1 = 0; $_ = $proceed($$); }");
10: }
11: });
上面例子能够看出,经过搜索cm方法体中,经过替换掉Point类中的move方法为以下代码后,
1: { $1 = 0; $_ = $proceed($$); }
move方法中的第一个参数将永远为0,须要注意的替换的代码不只仅是表达式,也能够是声明或者代码块,可是不能是try-catch声明。
instrument方法能够用来搜索方法体,若是找到了待替换的表达式,好比说方法体,字段,建立的类等,以后它会调用ExprEditor对象中的edit方法来进行修改。传递给edit方法的参数是找寻到的表达式对象,而后edit方法就能够经过此表达式对象来进行替换操做。
经过调用传递给edit方法的表达式对象中的replace方法,能够用来替换成给定的的表达式声明或者代码段。若是给定的代码段是空的,那么也就是说,将会执行replace("{}")方法,那么以前的代码段将会在方法体中被移除。若是你仅仅是想在表达式以前或者以后插入代码段操做,那么你须要将下面的代码段传递给replace方法:
1: { before-statements;
2: $_ = $proceed($$);
3: after-statements; }
此代码段能够是方法调用,字段访问,对象建立等等。
再来看看第二行声明:
1: $_ = $proceed();
上面表达式表明着读访问操做,也能够用以下声明来表明写访问操做:
1: $proceed($$);
目标表达式中的本地变量是能够经过replace方法传递到被instrument方法查找到的代码段中的,若是编译的时候开启了-g选项的话。
MethodCall对象表明了一个方法调用,它里面的replace方法能够对方法调用进行替换,它经过接收准备传递给insertBefore方法中的以$开头的识别符号来进行替换操做:
$0
The target object of the method call.
This is not equivalent to this
, which represents the caller-side this
object.$0
is null
if the method is static.
$1
, $2
, ...
The parameters of the method call.
$_
The resulting value of the method call.
$r
The result type of the method call.
$class
A java.lang.Class
object representing the class declaring the method.
$sig
An array of java.lang.Class
objects representing the formal parameter types.
$type
A java.lang.Class
object representing the formal result type.
$proceed
The name of the method originally called in the expression.
这里,方法调用是指MethodCall对象。$w,$args和$$在这里都是可用的,除非方法调用的结果类型为void,此时,$_必须被赋值且$_的类型就是返回类型。若是调用的结果类型为Object,那么$_的类型就是Object类型且赋予$_的值能够被忽略。
$proceed不是字符串,而是特殊的语法,它后面必须利用小括号()来包上参数列表。
表明构造器调用,好比this()调用和构造体中的super调用。其中的replace方法能够用来替换代码段。它经过接收insertBefore方法中传入的含有以$开头的代码段来进行替换操做:
$0
The target object of the constructor call. This is equivalent to this
.
$1
, $2
, ...
The parameters of the constructor call.
$class
A java.lang.Class
object representing the class declaring the constructor.
$sig
An array of java.lang.Class
objects representing the formal parameter types.
$proceed
The name of the constructor originally called in the expression.
这里,构造器调用表明着ContructorCall对象,其余的符号,好比$w,$args和$$也是可用的。
因为构造器调用,要么是父类调用,要么是类中的其余构造器调用,因此被替换的方法体必须包含构造器调用操做,通常状况下都是调用$proceed().
$proceed不是字符串,而是特殊的语法,它后面必须利用小括号()来包上参数列表。
此对象表明着字段访问。ExprEditor中的edit方法中若是有字段访问被找到,那么就会接收到这个对象。FieldAccess中的replace方法接收待替换的字段。
在代码段中,以$开头的识别码有以下特殊的含义:
$0
The object containing the field accessed by the expression. This is not equivalent to this
.this
represents the object that the method including the expression is invoked on.$0
is null
if the field is static.
$1
The value that would be stored in the field if the expression is write access.
Otherwise, $1
is not available.
$_
The resulting value of the field access if the expression is read access.
Otherwise, the value stored in $_
is discarded.
$r
The type of the field if the expression is read access.
Otherwise, $r
is void
.
$class
A java.lang.Class
object representing the class declaring the field.
$type
A java.lang.Class
object representing the field type.
$proceed
The name of a virtual method executing the original field access. .
其余的识别符,例如$w,$args和$$都是可用的。若是表达式是可访问的,代码段中,$_必须被赋值,且$_的类型就是此字段的类型。
NewExpr对象表明利用new操做符来进行对象建立。其edit方法接收对象建立行为,其replace方法则能够接收传入的代码段,将现有的对象建立的表达式进行替换。
在代码段中,以$开头的识别码有以下含义:
$0
null
.
$1
, $2
, ...
The parameters to the constructor.
$_
The resulting value of the object creation.
A newly created object must be stored in this variable.
$r
The type of the created object.
$sig
An array of java.lang.Class
objects representing the formal parameter types.
$type
A java.lang.Class
object representing the class of the created object.
$proceed
The name of a virtual method executing the original object creation. .
其余的识别码,好比$w,$args和$$也都是可用的。
此对象表示利用new操做符进行的数组建立操做。其edit方法接收数组建立操做的行为,其replace方法则能够接收传入的代码段,将现有的数组建立的表达式进行替换。
在代码段中,以$开头的识别码有以下含义:
$0
null
.
$1
, $2
, ...
The size of each dimension.
$_
The resulting value of the array creation.
A newly created array must be stored in this variable.
$r
The type of the created array.
$type
A java.lang.Class
object representing the class of the created array.
$proceed
The name of a virtual method executing the original array creation. .
其余的识别码,好比$w,$args和$$也是可用的。
好比,若是数组建立的表达式以下:
String[][] s = new String[3][4];
那么,$1和$2的值将分别为3和4,而$3则是不可用的。
可是,若是数组建立的表达式以下:
String[][] s = new String[3][];
那么,$1的值为3,而$2是不可用的。
此对象表明instanceof表达式。其edit方法接收instanceof表达式行为,其replace方法则能够接收传入的代码段,将现有的表达式进行替换。
在代码段中,以$开头的识别码有以下含义:
$0
null
.
$1
The value on the left hand side of the original instanceof
operator.
$_
The resulting value of the expression. The type of $_
is boolean
.
$r
The type on the right hand side of the instanceof
operator.
$type
A java.lang.Class
object representing the type on the right hand side of the instanceof
operator.
$proceed
The name of a virtual method executing the original instanceof
expression.
It takes one parameter (the type is java.lang.Object
) and returns true
if the parameter value is an instance of the type on the right hand side of
the original instanceof
operator. Otherwise, it returns false.
其余的识别码,好比$w,$args和$$也是可用的。
此对象表明显式类型转换。其edit方法接收显式类型转换的行为,其replace方法则能够接收传入的代码段,将现有的代码段进行替换。
在代码段中,以$开头的识别码有以下的含义:
$0
null
.
$1
The value the type of which is explicitly cast.
$_
The resulting value of the expression. The type of $_
is the same as the type
after the explicit casting, that is, the type surrounded by ( )
.
$r
the type after the explicit casting, or the type surrounded by ( )
.
$type
A java.lang.Class
object representing the same type as $r
.
$proceed
The name of a virtual method executing the original type casting.
It takes one parameter of the type java.lang.Object
and returns it after
the explicit type casting specified by the original expression.
其余的识别码,好比$w,$args和$$也是可用的。
此对象表明try-catch申明中的catch子句。其edit方法接收catch表达式行为,其insertBefore方法将接收的代码段进行编译,而后将其插入到catch子句的开始部分。
在代码段中,以$开头的识别码有以下的含义:
$1
The exception object caught by the catch
clause.
$r
the type of the exception caught by the catch
clause. It is used in a cast expression.
$w
The wrapper type. It is used in a cast expression.
$type
A java.lang.Class
object representing
the type of the exception caught by the catch
clause.
若是一个新的exception对象被赋值给$1,那么它将会将此exception传递给原有的catch子句并被捕捉。
4.3 添加新方法或字段
Javassist一开始就容许用户建立新的方法和构造,CtNewMethod和CtNewConstructor提供了多种静态工厂方法来建立CtMethod或者CtConstructor对象。特别说明一下,其make方法能够从给定的代码段中建立CtMethod或者CtContructor对象。
好比,以下程序:
1: CtClass point = ClassPool.getDefault().get("Point");
2: CtMethod m = CtNewMethod.make(
3: "public int xmove(int dx) { x += dx; }",
4: point);
5: point.addMethod(m);
添加了一个公共方法xmove到Point类中,此例子中,x是Point类中的int字段。
make方法中的代码段能够包含以$开头的识别码,可是setBydy方法中的$_除外。若是目标对象和目标方法的名字也传递给了make方法,那么此方法也能够包含$proceed。好比:
1: CtClass point = ClassPool.getDefault().get("Point");
2: CtMethod m = CtNewMethod.make(
3: "public int ymove(int dy) { $proceed(0, dy); }",
4: point, "this", "move");
上面代码建立以下ymove方法定义:
1: public int ymove(int dy) { this.move(0, dy); }
须要注意的是,$proceed已经被this.move替换掉了。
Javassist也提供另外一种方式来添加新方法,你能够首先建立一个abstract方法,而后赋予它方法体:
1: CtClass cc = ... ;
2: CtMethod m = new CtMethod(CtClass.intType, "move",
3: new CtClass[] { CtClass.intType }, cc);
4: cc.addMethod(m);
5: m.setBody("{ x += $1; }");
6: cc.setModifiers(cc.getModifiers() & ~Modifier.ABSTRACT);
若是一个abstract方法被添加到了类中,此时Javassist会将此类也变为abstract,为了解决这个问题,你不得不利用setBody方法将此类变回非abstract状态。
当一个方法调用另外一个为添加到操做类中的方法时,Javassist是没法编译此方法的(Javassist能够编译本身调用本身的递归方法)。为了添加相互递归调用的方法到类中,你须要以下的窍门来进行。假设你想添加m和n方法到cc中:
1: CtClass cc = ... ;
2: CtMethod m = CtNewMethod.make("public abstract int m(int i);", cc);
3: CtMethod n = CtNewMethod.make("public abstract int n(int i);", cc);
4: cc.addMethod(m);
5: cc.addMethod(n);
6: m.setBody("{ return ($1 <= 0) ? 1 : (n($1 - 1) * $1); }");
7: n.setBody("{ return m($1); }");
8: cc.setModifiers(cc.getModifiers() & ~Modifier.ABSTRACT);
首先,你须要建立两个abstract方法并把他们添加到类中。
而后,为方法设置方法体,方法体内部能够实现相互调用。
最后,将类变为非abstract的,由于addMethod添加abstract方法的时候,会自动将类变为abstract的。
Javassist容许用户建立一个新的字段:
1: CtClass point = ClassPool.getDefault().get("Point");
2: CtField f = new CtField(CtClass.intType, "z", point);
3: point.addField(f);
上面的diam会添加z字段到Point类中。
若是添加的字段须要设定初始值的话,代码须要被改成以下方式来进行:
1: CtClass point = ClassPool.getDefault().get("Point");
2: CtField f = new CtField(CtClass.intType, "z", point);
3: point.addField(f, "0"); // initial value is 0.
如今,addField方法接收了第二个用于计算初始值的参数。此参数能够为任何符合要求的java表达式。须要注意的是,此表达式不可以以分号结束(;)。
此外,上面的代码能够被重写为以下更简单的方式:
1: CtClass point = ClassPool.getDefault().get("Point");
2: CtField f = CtField.make("public int z = 0;", point);
3: point.addField(f);
为了移除字段或者方法,能够调用CtClass类中的removeField或者removeMethod来进行。而移除CtConstructor,能够经过调用removeConstructor方法来进行。
4.4 Annotations
CtClass,CtMethod,CtField和CtConstructor提供了getAnnotations这个快捷的方法来进行注解的读取操做,它会返回注解类型对象。
好比,以下注解方式:
1: public @interface Author {
2: String name();
3: int year();
4: }
能够按照以下方式来使用:
1: @Author(name="Chiba", year=2005)
2: public class Point {
3: int x, y;
4: }
此时,这些注解的值就能够用getAnnotations方法来获取,此方法将会返回包含了注解类型的对象列表。
1: CtClass cc = ClassPool.getDefault().get("Point");
2: Object[] all = cc.getAnnotations();
3: Author a = (Author)all[0];
4: String name = a.name();
5: int year = a.year();
6: System.out.println("name: " + name + ", year: " + year);
上面代码打印结果以下:
name: Chiba, year: 2005
因为Point类的注解只有@Author,因此all列表的长度只有一个,且all[0]就是Author对象。名字和年龄这俩注解字段值能够经过调用Author对象中的name方法和year来获取。
为了使用getAnnotations方法,相似Author这种注解类型必须被包含在当前的类路径中,同时必须可以被ClassPool对象所访问,若是类的注解类型没法被找到,Javassist就没法获取此注解类型的默认注解值。
4.5 运行时类支持
在大部分状况下,在Javassist中修改类并不须要Javassist运行时的支持。可是,有些基于Javassist编译器生成的字节码,则须要javassist.runtime这种运行时支持类包的支持(更多细节请访问此包的API)。须要注意的是,javassist.runtime包是Javassist中进行类修改的时候,惟一可能须要调用的包。
4.6导入
全部的源码中的类名,必须是完整的(必须包含完整的包名),可是java.lang包例外,好比,Javassist编译器能够将java.lang包下的Object转换为java.lang.Object。
为了让编译器可以找到类名锁对应的包,能够经过调用ClassPool的importPackage方法来进行,示例以下:
1: ClassPool pool = ClassPool.getDefault();
2: pool.importPackage("java.awt");
3: CtClass cc = pool.makeClass("Test");
4: CtField f = CtField.make("public Point p;", cc);
5: cc.addField(f);
第二行表明引入java.awt包,那么第三行就不会抛出错误,由于编译器能够将Point类识别为java.awt.Point。
须要注意的是,importPckage方法不会影响到ClassPool中的get方法操做,只会影响到编译器的包导入操做。get方法中的参数在任何状况下,必须是完整的,包含包路径的。
4.7限制
在当前扩展中,Javassist中的Java编译器有语言层面的几大限制,具体以下:
不支持J2SE 5.0中的新语法(包括enums和generics)。Javassist底层API才会支持注解,具体内容能够查看javassist.bytecode.annotation包(CtClass和CtBehavior中的getAnnotations方法)。泛型被部分支持,能够查看后面的章节来了解更详细的内容。
数组初始化,也就是被双括号包围的以逗号分隔的表达式,不支持同时初始化多个。
不支持内部类或者匿名类。须要注意的是,这仅仅是由于编译器不支持,因此没法编译匿名表达式。可是Javassist自己是能够读取和修改内部类或者匿名类的。
continue和break关键字不支持。
编译器不可以正确的识别java的方法派发模型,若是使用了这种方式,将会形成编译器解析的混乱。好比:
1: class A {}
2: class B extends A {}
3: class C extends B {}
4:
5: class X {
6: void foo(A a) { .. }
7: void foo(B b) { .. }
8: }
若是编译的表达式是x.foo(new C()),其中x变量指向了X类实例,此时编译器尽管能够正确的编译foo((B)new C()),可是它依旧会将会调用foo(A)。
推荐用户使用#号分隔符来分隔类名和静态方法或者字段名。好比在java中,正常状况下咱们会这么调用:
javassist.CtClass.intType.getName()
咱们会访问javassist.Ctclass中的静态字段intType,而后调用其getName方法。而在Javassist中,咱们能够按照以下的表达式来书写:
javassist.CtClass#intType.getName()
这样编译器就可以快速的解析此表达式了。
5. 字节码API
为了直接修改类文件,Javassist也提供了底层的API,想使用这些API的话,你须要有良好的Java字节码知识储备和类文件格式的认知,这样,你使用这些API修改类文件的时候,才能够为所欲为而不逾矩。
若是你只是想生成一个简单的类文件,那么javassist.bytecode.ClassFileWriter类能够作到。它虽然体积小,可是是比javassist.bytecode.ClassFile更为快速的存在。
5.1 获取ClassFile对象
一个javassist.bytecode.ClassFile对象就表明着一个类文件,为了获取这个对象,CtClass中的getClassFile方法能够作到。若是不想这么作的话,你也能够直接在类文件中构造一个javassist.bytecode.ClassFile,代码以下:
1: BufferedInputStream fin
2: = new BufferedInputStream(new FileInputStream("Point.class"));
3: ClassFile cf = new ClassFile(new DataInputStream(fin));
这个代码片断展现了从Point.class类中建立出一个ClassFile对象出来。
既然能够从类文件中建立出ClassFile,那么也能将ClassFile回写到类文件中。ClassFile中的write方法就能够将类文件内容回写到给定的DataOutputStream中。让咱们全程展现一下这种作法:
1: ClassFile cf = new ClassFile(false, "test.Foo", null);
2: cf.setInterfaces(new String[] { "java.lang.Cloneable" });
3:
4: FieldInfo f = new FieldInfo(cf.getConstPool(), "width", "I");
5: f.setAccessFlags(AccessFlag.PUBLIC);
6: cf.addField(f);
7:
8: cf.write(new DataOutputStream(new FileOutputStream("Foo.class")));
上面的代码生成了Foo.class这个类文件,它包含了对以下类的扩展:
1: package test;
2: class Foo implements Cloneable {
3: public int width;
4: }
5.2 添加和删除成员
ClassFile提供了addField方法和addMethod方法来添加字段或者方法(须要注意的是,在字节码层面上说来,构造器也被视为方法),同时也提供了addAttribute方法来为类文件添加属性。
须要注意的是FiledInfo,MethodInfo和AttributeInfo对象包含了对ConstPool(const pool table)对象的指向。此ConstPool对象被添加到ClassFile对象中后,在ClassFile对象和FiledInfo对象(或者是MethodInfo对象等)中必须是共享的。换句话说,FiledInfo对象(或者MethodInfo对象等)在不一样的ClassFile中是不能共享的。
为了从ClassFile对象中移除字段或者方法,你必须首先经过类的getFields方法获取全部的字段以及getMethods方法获取全部的方法来生成java.util.List对象,而后将此对象返回。以后就能够经过List对象上的remove方法来移除字段或者方法了,属性的移除方式也不例外,只须要经过FiledInfo或者MethodInfo中的getAttributes方法来获取到属性列表后,而后将相关属性从中移除便可。
5.3 遍历方法体
为了校验方法体中的每一个字节码指令,CodeIterator则很是有用。想要获取这个对象的话,须要以下步骤:
1: ClassFile cf = ... ;
2: MethodInfo minfo = cf.getMethod("move"); // we assume move is not overloaded.
3: CodeAttribute ca = minfo.getCodeAttribute();
4: CodeIterator i = ca.iterator();
CodeIterator对象容许你从前到后挨个访问字节码指令。以下的方法是CodeIterator中的一部分:
void begin()
void move(int index)
boolean hasNext()
int next()
int byteAt(int index)
int u16bitAt(int index)
int write(byte[] code, int index)
void insert(int index, byte[] code)
下面的代码段展现了方法体中的全部指令:
1: CodeIterator ci = ... ;
2: while (ci.hasNext()) {
3: int index = ci.next();
4: int op = ci.byteAt(index);
5: System.out.println(Mnemonic.OPCODE[op]);
6: }
5.4 字节码序列的生成
Bytecode对象表明了字节码序列,它是一组在持续不断进行增加的字节码的简称,来看看下面简单的代码片断:
1: ConstPool cp = ...; // constant pool table
2: Bytecode b = new Bytecode(cp, 1, 0);
3: b.addIconst(3);
4: b.addReturn(CtClass.intType);
5: CodeAttribute ca = b.toCodeAttribute();
代码将会产生以下的序列:
iconst_3 ireturn
你也能够利用Bytecode中的get方法来获取一个字节码数组序列,以后能够将此数组插入到另外一个代码段中。
虽然Bytecode提供了一系列的方法添加特殊的指令到序列中,它同时也提供了addOpcode方法来添加8bit操做码,提供了addIndex方法来添加索引。8bit操做码的值是在Opcode接口中被定义的。
addOpcode方法和其余添加特殊指令的方法能够自动的维持堆栈的深度,除非操做流程出现了分歧,在这里,咱们可使用Bytecode的getMaxStack方法来获取堆栈最大深度。同时,堆栈深度和Bytecode对象内建立的CodeAtrribute对象也有关系,为了从新计算方法体中的最大堆栈深度,可使用CodeAttribute中的computeMaxStack来进行。
Bytecode能够用来构建一个方法,示例以下:
1: ClassFile cf = ...
2: Bytecode code = new Bytecode(cf.getConstPool());
3: code.addAload(0);
4: code.addInvokespecial("java/lang/Object", MethodInfo.nameInit, "()V");
5: code.addReturn(null);
6: code.setMaxLocals(1);
7:
8: MethodInfo minfo = new MethodInfo(cf.getConstPool(), MethodInfo.nameInit, "()V");
9: minfo.setCodeAttribute(code.toCodeAttribute());
10: cf.addMethod(minfo);
上面的代码流程是建立了默认的构造函数后,而后将其添加到cf指向的类中。具体说来就是,Bytecode对象首先被转换成了CodeAttribute对象,接着被添加到minfo所指向的方法中。此方法最终被添加到cf类文件中。
5.5 注解 (Meta tags)
注解在运行时态,做为一个可见或者不可见的属性被保存在类文件中。它们能够从ClassFile,MethodInfo或者FieldInfo对象中经过getAttribute(AnnotationsAttribute.invisibleTag)方法来获取。更多的谢洁,能够看看javadoc中关于javassist.bytecode.AnnotationsAttribute类和javassist.bytecode.annotation包的描述。
Javassist也可以让你利用一些应用层的API来访问注解。只须要利用CtClass或者CtBehavior中的的getAnnotations方法接口。
6.泛型
Javassist底层的API能够彻底支持Java5中的泛型。另外一方面,其更高级别的API,诸如CtClass是没法直接支持泛型的。对于字节码转换来讲,这也不是什么大问题。
Java的泛型,采用的是擦除技术。当编译完毕后,全部的类型参数都将会被擦掉。好比,假设你的源码定义了一个参数类型Vector<String>:
1: Vector<String> v = new Vector<String>();
2: :
3: String s = v.get(0);
编译后的字节码等价于以下代码:
1: Vector v = new Vector();
2: :
3: String s = (String)v.get(0);
因此,当你写了一套字节码转换器后,你能够移除掉全部的类型参数。因为嵌入在Javassist的编译器不支持泛型,因此利用其编译的时候,你不得不在调用端作显式的类型转换。好比,CtMethod.make方法。可是若是源码是利用常规的Java编译器,好比javac,来编译的话,是无需进行类型转换的。
若是你有一个类,示例以下:
1: public class Wrapper<T> {
2: T value;
3: public Wrapper(T t) { value = t; }
4: }
想添加Getter<T>接口到Wrapper<T>类中:
1: public interface Getter<T> {
2: T get();
3: }
那么实际上,你须要添加的接口是Getter(类型参数<T>已经被抹除),须要添加到Wrapper中的方法以下:
1: public Object get() { return value; }
须要注意的是,非类型参数是必须的。因为get方法返回了Object类型,那么调用端若是用Javassist编译的话,就须要进行显式类型转换。好比,以下例子,类型参数T是String类型,那么(String)就必须被按照以下方式插入:
1: Wrapper w = ...
2: String s = (String)w.get();
当使用常规的Java编译器编译的时候,类型转换是不须要的,由于编译器会自动进行类型转换。
若是你想在运行时态,经过反射来访问类型参数,那么你不得不在类文件中添加泛型符号。更多详细信息,请参阅API文档CtClass中的setGenericSignature方法。
7.可变参数
目前,Javassist没法直接支持可变参数。为了让方法能够支持它,你须要显式设置方法修改器,其实很简单,假设你想生成以下的方法:
1: public int length(int... args) { return args.length; }
下面的Javassist代码将会生成如上的方法:
1: CtClass cc = /* target class */;
2: CtMethod m = CtMethod.make("public int length(int[] args) { return args.length; }", cc);
3: m.setModifiers(m.getModifiers() | Modifier.VARARGS);
4: cc.addMethod(m);
参数类型int...变成了int[]数组,Modifier.VARARGS被添加到了方法修改器中。
为了可以在Javassist编译器中调用此方法,你须要这样来:
1: length(new int[] { 1, 2, 3 });
而不是这样来:
1: length(1, 2, 3);
8. J2ME
若是你想在J2ME执行环境中修改类文件,你须要进行预校验操做,此操做会产生栈Map对象,此对象和JDK1.6中的J2SE栈map表有些类似。当且仅当javassist.bytecode.MethodInfo.doPreverify为true的时候,Javassist会维护J2ME中的栈map。
你也能够为修改的方法手动生成一个栈map,好比,一个给定的CtMethod对象中的m,你能够调用以下方法来生成一个栈map:
1: m.getMethodInfo().rebuildStackMapForME(cpool);
这里,cpool是ClassPool对象,此对象能够利用CtClass对象中的getClassPool来获取,它负责利用给定的类路径来找寻类文件。为了获取全部的CtMethods对象,能够经过调用CtClass对象的getDeclaredMethods来进行。
9.装箱/拆箱
在Java中,装箱和拆箱操做是语法糖。对于字节码说来,是不存在装箱和拆箱的。因此Javassist的编译器不支持装箱拆箱操做。好比,以下的描述,在java中是可行的:
1: Integer i = 3;
能够看出,此装箱操做是隐式的。可是在Javassist中,你必须显式的将值类型从int转为Integer:
1: Integer i = new Integer(3);
10.调试
将CtClass.debugDump设置为目录名称以后,全部被Javassist生成或修改的类文件将会被保存到此目录中。若是不想这么作,能够将CtClass.debugDump设置为null,须要注意的是,它的默认值就是null。
示例代码:
1: CtClass.debugDump = "./dump";
此时,全部的被修改的类文件将会被保存到./dump目录中。