本文译自Getting Started with Javassist,若是谬误之处,还请指出。html
bytecode读写java
ClassPoolexpress
Class loader数组
自有和定制服务器
Bytecode操控接口数据结构
Genericsapp
Varargs框架
J2MEide
装箱和拆箱函数
调试
Javassist是用来处理java字节码的类库, java字节码通常存放在后缀名称为class的二进制文件中。每一个二进制文件都包含一个java类或者是java接口。
Javasist.CtClass是对类文件的抽象,处于编译中的此对象能够用来处理类文件。下面的代码用来展现一下其简单用法:
ClassPool pool = ClassPool.getDefault(); CtClass cc = pool.get("test.Rectangle"); cc.setSuperclass(pool.get("test.Point")); 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()方法来直接获取修改的字节码:
byte[] b = cc.toBytecode();
你能够经过以下代码直接加载CtClass:
Class clazz = cc.toClass();
toClass()方法被调用,将会使得当前线程中的context class loader加载此CtClass类,而后生成 java.lang.Class对象。更多的细节 ,请参见this section below.
新建类
新建一个类,可使用ClassPool.makeClass()方法来实现:
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.makeClass("Point");
上面的代码展现的是建立无成员方法的Point类,若是须要附带方法的话,咱们能够用CtNewMethod附带的工厂方法建立,而后利用CtClass.addMethod()将其追加就能够了 。
makeClass()不能用于建立新的接口。可是makeInterface()能够。接口的方法能够用CtNewmethod.abstractMethod()方法来建立,须要注意的是,在这里,一个接口方法实际上是一个abstract方法。
冻结类
若是CtClass对象被writeFile(),toClass()或者toBytecode()转换成了类对象,Javassist将会冻结此CtClass对象。任何对此对象的后续更改都是不容许的。之因此这样作,主要是由于此类已经被JVM加载,因为JVM自己不支持类的重复加载操做,因此不容许更改。
一个冻结的CtClass对象,能够经过以下的代码进行解冻,若是想更改类的话,代码以下:
CtClasss cc = ...; : cc.writeFile(); cc.defrost(); 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()方法来进行阻止:
CtClasss cc = ...; cc.stopPruning(true); : cc.writeFile(); //转换为类文件. //cc不会被精简.
这样,CtClass对象就不会被精简了。当writeFile()方法调用以后,咱们就能够进行解冻,而后随心所欲了。
须要注意的是:在调试的时候, debugWriteFile()方法能够很方便的防止CtClass对象精简和冻住。
类搜索路径
ClassPool.getDefault()方法的搜索路径和JVM的搜索路径是一致的。若是程序运行在JBoss或者Tomcat服务器上,那么ClassPool对象也许不可以找到用户类,缘由是应用服务器用的是多个class loader,其中包括系统的class loader来加载对象。正因如此,ClassPool须要 附加特定的类路径才行。 假设以下的pool实例表明ClassPool对象:
pool.insertClassPath(new ClassClassPath(this.getClass()));
上面的代码段注册了this所指向的类路径下面的类对象。你能够用其余的类对象来代替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包下面的类。好比加载org.javassist.test.Main类,此类将会从以下路径获取:
http://www.javassist.org:80/java/org/javassist/test/Main.class
此外,你甚至能够直接使用一串字节码,而后建立出CtClass对象。示例以下:
ClassPool cp = ClassPool.getDefault(); byte[] b = a byte array; String name = class name; cp.insertClassPath(new ByteArrayClassPath(name, b)); CtClass cc = cp.get(name);
从上面代码能够看出,ClassPool加载了ByteArrayClasPath构建的对象,而后利用get()方法并经过类名,将对象赋值给了CtClass对象。
若是你不知道类的全名,你也能够用makeClass()来实现:
ClassPool cp = ClassPool.getDefault(); InputStream ins = an input stream for reading a class file; CtClass cc = cp.makeClass(ins);
makeClass()方法利用给定的输入流构建出CtClass对象。你能够用饿汉方式直接建立出ClassPool对象,这样当搜索路径中有大点的jar文件须要加载的时候,能够提高一些性能,之因此 这样作,缘由是ClassPool对象按需加载类文件,因此它可能会重复搜索整个jar包中的每一个类文件,正由于如此,makeClass()能够用于优化查找的性能。被makeClass()方法加载过的CtClass对象将会留存于ClassPool对象中,不会再进行读取。
用户能够扩展类搜索路径。能够经过定义一个新的类,扩展自ClassPath接口,而后返回一个insertClassPath便可。这种作法能够容许其余资源被包含到搜索路径中。
一个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移除。代码以下:
CtClass cc = ... ; cc.writeFile(); cc.detach();
此CtClass对象被移除后,不能再调用其任何方法。可是你能够调用ClassPool.get()方法来建立一个新的CtClass实例。
另外一个方法就是用新的ClassPool对象来替代旧的ClassPool对象。若是旧的ClassPool对象被垃圾回收了,那么其内部的CtClass对象也都会被垃圾回收掉。下面的代码能够用来建立一个新的ClassPool对象:
ClassPool cp = new ClassPool(true); //若是须要的话,利用appendClassPath()来添加额外的搜索路径
上面的代码和ClassPool.getDefault()来建立ClassPool,效果是同样的。须要注意的是,ClasssPool.getDefault()是一个单例工厂方法,它可以建立出一个惟一的ClassPool对象并进行重复利用。new ClassPool(true)是一个很快捷的构造方法,它可以建立一个ClassPool对象而后追加系统搜索路径到其中。和以下的代码建立行为表现一致:
ClassPool cp = new ClassPool(); cp.appendSystemPath(); // or append another path by appendClassPath()
级联ClassPools
若是应用运行在JBOSS/Tomcat上, 那么建立多个ClassPool对象将会颇有必要。由于每一个类加载其都将会持有一个ClassPool的实例。应用此时最好不用getDefault()方法来建立ClassPool对象,而是使用构造来建立。
多个ClassPool对象像java.lang.ClassLoader同样作级联,代码以下:
ClassPool parent = ClassPool.getDefault(); ClassPool child = new ClassPool(parent); child.insertClassPath("./classes");
若是child.get()被调用,子ClassPool将会首先从父ClassPool进行查找。当父ClassPool查找不到后,而后将会尝试从./classes目录进行查找。
若是child.childFirstLookup = true, 子ClassPool将会首先查找本身的目录,而后查找父ClassPool,代码以下:
ClassPool parent = ClassPool.getDefault(); ClassPool child = new ClassPool(parent); child.appendSystemPath(); //和默认的搜索地址一致. child.childFirstLookup = true; //修改子类搜索行为.
为新类重命名
能够从已有类建立出新的类,代码以下:
ClassPool pool = ClassPool.getDefault(); CtClass cc = pool.get("Point"); 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对象已经不存在了,请看以下代码:
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是不一致的.
cc1和cc2将会指向cc,可是cc3却不会。须要注意的是,在cc.setName("Pair")执行后,cc和cc1指向的CtClass对象都变成了指向Pair类。
ClassPool对象用来维护类之间和CtClass对象之间一对一的映射关系。Javassist不容许两个不一样的CtClass对象指向同一个类,除非两个独立的ClassPool存在的状况下。这是为实现程序转换而保证其一致性的最鲜明的特色。
咱们知道,能够利用ClassPool.getDefault()方法建立ClassPool的实例,代码片断以下(以前已经展现过):
ClassPool cp = new ClassPool(true);
若是你有两个ClassPool对象,那么你能够从这两个对象中分别取出具备相同类文件,可是隶属于不一样的CtClass对象生成的,此时能够经过修改这俩CtClass对象来生成不一样的类。
从冻结类中建立新类
当CtClass对象经过writeFile()方法或者toBytecode()转变成类文件的时候,Javassist将不容许对这个CtClass对象有任何修改。所以,当表明Point类的CtClass对象被转换成了类文件,你不可以先拷贝Point类,而后修更名称为Pair类,由于Point类中的setName()方法是没法被执行的,错误使用示例以下:
ClassPool pool = ClassPool.getDefault(); CtClass cc = pool.get("Point"); cc.writeFile(); cc.setName("Pair"); // wrong since writeFile() has been called.
为了可以避免这种限制,你应该使用getAndRename()方法,正确示例以下:
ClassPool pool = ClassPool.getDefault(); CtClass cc = pool.get("Point"); cc.writeFile(); 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()方法:
public class Hello { public void say() { System.out.println("Hello"); } } public class Test { public static void main(String[] args) throws Exception { ClassPool cp = ClassPool.getDefault(); CtClass cc = cp.get("Hello"); CtMethod m = cc.getDeclaredMethod("say"); m.insertBefore("{ System.out.println(\"Hello.say():\"); }"); Class c = cc.toClass(); Hello h = (Hello)c.newInstance(); h.say(); } }
Test.main()方法中, say()方法被插入了println()方法,以后这个被修改的Hello类实例被建立,say()方法被调用。
须要注意的是,上面代码中,Hello类是放在toClass()以后被调用的,若是不这么作的话,JVM将会先加载Hello类,而不是在toClass()方法加载Hello类以后再调用Hello类,这样作会致使加载失败(会抛出LinkageError错误)。好比,若是Test.main()方法中的代码以下:
public static void main(String[] args) throws Exception { Hello orig = new Hello(); ClassPool cp = ClassPool.getDefault(); CtClass cc = cp.get("Hello");
CtMethod m = cc.getDeclaredMethod("say"); m.insertBefore("{ System.out.println(\"Hello.say():\"); }"); Class c = cc.toClass(); Hello h = (Hello)c.newInstance(); h.say();
}
main方法中,第一行的Hello类会被加载,以后调用toClass()将会报错,由于一个类加载器没法在同一时刻加载两个不一样的Hello类版本。
若是程序跑在JBoss/Tomcat上,利用toClass()方法可能会有些问题。在这种状况下,你将会遇到ClassCastException错误,为了不这种错误,你必须为toClass()方法提供很是明确的类加载器。好比,在以下代码中,bean表明你的业务bean对象的时候:
CtClass cc = ...; 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。
好比,下面的例子会抛错:
MyClassLoader myLoader = new MyClassLoader(); Class clazz = myLoader.loadClass("Box"); Object obj = clazz.newInstance(); Box b = (Box)obj; //会抛出ClassCastException错误.
Box类被两个类加载器所加载,试想一下,假设CL类加载器加载的类包含此代码段,因为此代码段指向MyClassLoader,Class,Object,Box,因此CL加载器也会将这些东西加载进来(除非它是其它类加载器的代理)。所以变量b就是CL中的Box类。从另外一方面说来,myLoader也加载了Box类,obj对象是Box类的实例,所以,代码的最后一行将一直抛出ClassCastException错误,由于obj和b是Box类的不一样实例副本。
public class Point { // 被PL加载 private int x, y; public int getX() { return x; } : } public class Box { // 初始化器为L可是实际加载器为PL private Point upperLeft, size; public int getBaseX() { return upperLeft.x; } : } public class Window { // 被L加载器所加载 private Box box; public int getBaseX() { return box.getBaseX(); } }假如Window类被L加载器所加载,那么Window的虚拟加载器和实际加载器都是L。因为Window类中引用了Box类,JVM将会加载Box类,这里,假设L将此加载任务代理给了其父加载器PL,那么Box的类加载器将会变成L,可是其实际加载器将会是PL。所以,在此种状况下,Point类的虚拟加载器将不是L,而是PL,由于它和Box的实际加载器是同样的。所以L加载器将永远不会加载Point类。
public class Point { private int x, y; public int getX() { return x; } : } public class Box { // the initiator is L but the real loader is PL private Point upperLeft, size; public Point getSize() { return size; } : } public class Window { // loaded by a class loader L private Box box; public boolean widthIs(int w) { Point p = box.getSize(); return w == p.getX(); } }如今看来,Window类指向了Point,所以类加载器L要想加载Point的话,它必须代理PL。必须杜绝的状况是,两个类加载器加载同一个类的状况。其中一个类加载器必须可以代理另外一个才行。
Point p = box.getSize();没有抛出错误,Window将会破坏Point对象的包装。举个例子吧,被PL加载的Point类中,x字段是私有的。可是,若是L利用以下的定义加载了Point类的话,那么Window类是能够直接访问x字段的:
public class Point { public int x, y; // not private public int getX() { return x; } : }想要了解java中更多的类加载器信息,如下信息也许有帮助:
import javassist.*; import test.Rectangle; public class Main { public static void main(String[] args) throws Throwable { ClassPool pool = ClassPool.getDefault(); Loader cl = new Loader(pool); CtClass ct = pool.get("test.Rectangle"); ct.setSuperclass(pool.get("test.Point")); Class c = cl.loadClass("test.Rectangle"); Object rect = c.newInstance(); : } }上面的程序就修改了test.Rectangle类,先是test.Point类被设置成了test.Rectangle类的父类,以后程序会加载这个修改的类并建立test.Rectangle类的实例出来。
public interface Translator { public void start(ClassPool pool) throws NotFoundException, CannotCompileException; public void onLoad(ClassPool pool, String classname) throws NotFoundException, CannotCompileException; }当利用javassist.Loader.addTranslator()将事件监听器添加到javassist.Loader对象上的时候,上面的start()方法将会被触发。而onLoad()方法的触发先于javassist.Loader加载一个类,所以onLoad()方法能够改变已加载的类的定义。
public class MyTranslator implements Translator { void start(ClassPool pool) throws NotFoundException, CannotCompileException {} void onLoad(ClassPool pool, String classname) throws NotFoundException, CannotCompileException { CtClass cc = pool.get(classname); cc.setModifiers(Modifier.PUBLIC); } }须要注意的是,onLoad()方法不须要调用toBytecode方法或者writeFile方法,由于javassistLoader会调用这些方法来获取类文件。
import javassist.*; public class Main2 { public static void main(String[] args) throws Throwable { Translator t = new MyTranslator(); ClassPool pool = ClassPool.getDefault(); Loader cl = new Loader(); cl.addTranslator(pool, t); cl.run("MyApp", args); } }想要运行它,能够按照以下命令来:
% java Main2 arg1 arg2...MyApp类和其余的一些类,会被MyTranslator所翻译。
import javassist.*; public class SampleLoader extends ClassLoader { /* Call MyApp.main(). */ public static void main(String[] args) throws Throwable { SampleLoader s = new SampleLoader(); Class c = s.loadClass("MyApp"); c.getDeclaredMethod("main", new Class[] { String[].class }) .invoke(null, new Object[] { args }); } private ClassPool pool; public SampleLoader() throws NotFoundException { pool = new ClassPool(); pool.insertClassPath("./class"); // MyApp.class must be there. } /* Finds a specified class. * The bytecode for that class can be modified. */ protected Class findClass(String name) throws ClassNotFoundException { try { CtClass cc = pool.get(name); // modify the CtClass object here byte[] b = cc.toBytecode(); return defineClass(name, b, 0, b.length); } catch (NotFoundException e) { throw new ClassNotFoundException(); } catch (IOException e) { throw new ClassNotFoundException(); } catch (CannotCompileException e) { throw new ClassNotFoundException(); } } }MyApp类是一个应用程序。为了执行这个应用,咱们首先须要将类文件放到./class文件夹下,须要确保当前文件夹不在类搜索目录下,不然将会被SampleLoader的父类加载器,也就是系统默认的类加载器所加载。./class目录名称在insertClassPath方法中必需要有所体现,固然此目录名称是能够随意改变的。接下来咱们运行以下命令:
% java SampleLoader
此时,类加载器将会加载MyApp类(./class/MyApp.class)并调用MyApp.main方法。
ClassPool pool = ClassPool.getDefault(); CtClass cc = pool.get("java.lang.String"); CtField f = new CtField(CtClass.intType, "hiddenValue", cc); f.setModifiers(Modifier.PUBLIC); cc.addField(f); cc.writeFile(".");此段代码会产生"./java/lang/String.class"文件。
% java -Xbootclasspath/p:. MyApp arg1 arg2...假设MyApp的代码以下:
public class MyApp { public static void main(String[] args) throws Exception { System.out.println(String.class.getField("hiddenValue").getName()); } }此更改的String类成功的被加载,而后打印出了hiddenValue。
好比,Point类有一个move方法,其子类ColorPoint不会重写move方法, 那么在这里,两个move方法,将会被CtMethod对象正确的识别。若是CtMethod对象的方法定义被修改,那么此修改将会反映到两个方法上。若是你想只修改ColorPoint类中的move方法,你须要首先建立ColorPoint的副本,那么其CtMethod对象将也会被复制,CtMethod对象可使用CtNewMethod.copy方法来实现。
Javassist不支持移除方法或者字段,可是支持修更名字。因此若是一个方法再也不须要的话,能够在CtMethod中对其进行重命名并利用setName方法和setModifiers方法将其设置为私有方法。
Javassist不支持为已有的方法添加额外的参数。可是能够经过为一个新的方法建立额外的参数。好比,若是你想添加一个额外的int参数newZ到Point类的方法中:
void move(int newX, int newY) { x = newX; y = newY; }
你应当在Point类中添加以下方法:
void move(int newX, int newY, int newZ) { // do what you want with newZ. move(newX, newY); }
Javassist同时也提供底层的API来直接修改原生的类文件。好比,CtClass类中的getClassFile方法能够返回一个ClassFile对象来表明一个原生的类文件。而CtMethod中的getMethodInfo方法则返回MethodInfo对象来表明一个类中的method_info结构。底层的API单词大多数来自于JVM,因此用于用起来不会感受到陌生。更多的内容,能够参看 javassist.bytecode
package.
Javassist修改类文件的时候,通常不须要javassist.runtime包,除非一些特别的以$符号开头的。这些特殊符号会在后面进行讲解。更多的内容,能够参考javassist.runtime包中的API文档。
System.out.println("Hello"); { System.out.println("Hello"); } if (i < 0) { i = -i; }代码段能够指向字段和方法,也能够为编译器添加-g选项来让其指向插入的方法中的参数。不然,只能利用$0,$1,$2...这种以下的变量来进行访问。虽然不容许访问方法中的本地变量,可是在方法体重定义一个新的本地变量是容许的。例外的是,编译器开启了-g选项的话,insertAt方法是容许代码段访问本地变量的。
$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对象,表明传入的代码段 . |
ClassPool pool = ClassPool.getDefault(); CtClass cc = pool.get("Point"); CtMethod m = cc.getDeclaredMethod("move"); m.insertBefore("{ System.out.println($1); System.out.println($2); }"); cc.writeFile();须要注意的是,insertBefore方法中的代码段是被大括号{}包围的,此方法只接受一个被大括号包围的代码段入参。
int fact(int n) { if (n <= 1) return n; else return n * fact(n - 1); }为了使用$cflow,首先须要引用$cflow,用于监听fact方法的调用:
CtMethod cm = ...; cm.useCflow("fact");useCflow()方法就是用来声明$cflow变量。任何可用的java命名均可以用来进行识别。此名称也能够包含.(点号),好比"my.Test.face"也是能够的。
cm.insertBefore("if ($cflow(fact) == 0)" + " System.out.println(\"fact \" + $1);");代码段将fact方法进行编译以便于可以看到对应的参数。因为$cflow(fact)被选中,那么对fact方法的递归调用将不会显示参数。
Object result = ... ; $_ = ($r)result;若是结果类型为基础数据类型,那么($r)须要遵循以下的规则:
$_ = ($r)foo();是一个有效的申明。
return ($r)result;这里,result是一个本地变量,因为($r)这里作了转换,那么返回结果是无效的。此时的return申明和没有任何返回的return申明是等价的:
return;
Integer i = ($w)5;结果类型依据($w)后面的表达式来肯定,若是表达式是double类型,那么包装类型则为java.lang.Double。若是($w)后面的表达式不是基础类型,那么($w)将不进行任何转换。
CtMethod m = ...; CtClass etype = ClassPool.getDefault().get("java.io.IOException"); m.addCatch("{ System.out.println($e); throw $e; }", etype);此方法体m被翻译出来后,展现以下:
try {
the original method body
}
catch (java.io.IOException e) {
System.out.println(e);
throw e;
}
须要注意的是,插入的代码段必须以throw或者return命令结尾。
$0 , $1 , $2 , ... |
this 和实参 |
$args |
参数列表.$args类型为 Object数组 . |
$$ |
全部参数. |
$cflow( ...) |
cflow变量 |
$r |
结果类型. 用于表达式转换. |
$w |
包装类型. 用于表达式转换. |
$sig |
java.lang.Class对象数组,表明正式的参数类型 . |
$type |
java.lang.Class 对象,表明正式的结果类型. |
$class |
java.lang.Class对象,表明当前操做的方法 (等价于$0的类型). |
CtMethod cm = ... ; cm.instrument( new ExprEditor() { public void edit(MethodCall m) throws CannotCompileException { if (m.getClassName().equals("Point") && m.getMethodName().equals("move")) m.replace("{ $1 = 0; $_ = $proceed($$); }"); } });上面例子能够看出,经过搜索cm方法体中,经过替换掉Point类中的move方法为以下代码后,
{ $1 = 0; $_ = $proceed($$); }move方法中的第一个参数将永远为0,须要注意的替换的代码不只仅是表达式,也能够是声明或者代码块,可是不能是try-catch声明。
{ before-statements; $_ = $proceed($$); after-statements; }此代码段能够是方法调用,字段访问,对象建立等等。
$_ = $proceed();上面表达式表明着读访问操做,也能够用以下声明来表明写访问操做:
$proceed($$);目标表达式中的本地变量是能够经过replace方法传递到被instrument方法查找到的代码段中的,若是编译的时候开启了-g选项的话。
$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. |
$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. |
$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. . |
$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. . |
$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. . |
String[][] s = new String[3][4];那么,$1和$2的值将分别为3和4,而$3则是不可用的。
String[][] s = new String[3][];那么,$1的值为3,而$2是不可用的。
$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. |
$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. |
$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. |
CtClass point = ClassPool.getDefault().get("Point"); CtMethod m = CtNewMethod.make( "public int xmove(int dx) { x += dx; }", point); point.addMethod(m);添加了一个公共方法xmove到Point类中,此例子中,x是Point类中的int字段。
CtClass point = ClassPool.getDefault().get("Point"); CtMethod m = CtNewMethod.make( "public int ymove(int dy) { $proceed(0, dy); }", point, "this", "move");上面代码建立以下ymove方法定义:
public int ymove(int dy) { this.move(0, dy); }须要注意的是,$proceed已经被this.move替换掉了。
CtClass cc = ... ; CtMethod m = new CtMethod(CtClass.intType, "move", new CtClass[] { CtClass.intType }, cc); cc.addMethod(m); m.setBody("{ x += $1; }"); cc.setModifiers(cc.getModifiers() & ~Modifier.ABSTRACT);若是一个abstract方法被添加到了类中,此时Javassist会将此类也变为abstract,为了解决这个问题,你不得不利用setBody方法将此类变回非abstract状态。
CtClass cc = ... ; CtMethod m = CtNewMethod.make("public abstract int m(int i);", cc); CtMethod n = CtNewMethod.make("public abstract int n(int i);", cc); cc.addMethod(m); cc.addMethod(n); m.setBody("{ return ($1 <= 0) ? 1 : (n($1 - 1) * $1); }"); n.setBody("{ return m($1); }"); cc.setModifiers(cc.getModifiers() & ~Modifier.ABSTRACT);首先,你须要建立两个abstract方法并把他们添加到类中。
CtClass point = ClassPool.getDefault().get("Point"); CtField f = new CtField(CtClass.intType, "z", point); point.addField(f);上面的diam会添加z字段到Point类中。
CtClass point = ClassPool.getDefault().get("Point"); CtField f = new CtField(CtClass.intType, "z", point); point.addField(f, "0"); // initial value is 0.如今,addField方法接收了第二个用于计算初始值的参数。此参数能够为任何符合要求的java表达式。须要注意的是,此表达式不可以以分号结束(;)。
CtClass point = ClassPool.getDefault().get("Point"); CtField f = CtField.make("public int z = 0;", point); point.addField(f);
public @interface Author { String name(); int year(); }能够按照以下方式来使用:
@Author(name="Chiba", year=2005) public class Point { int x, y; }此时,这些注解的值就能够用getAnnotations方法来获取,此方法将会返回包含了注解类型的对象列表。
CtClass cc = ClassPool.getDefault().get("Point"); Object[] all = cc.getAnnotations(); Author a = (Author)all[0]; String name = a.name(); int year = a.year(); System.out.println("name: " + name + ", year: " + year);上面代码打印结果以下:
name: Chiba, year: 2005因为Point类的注解只有@Author,因此all列表的长度只有一个,且all[0]就是Author对象。名字和年龄这俩注解字段值能够经过调用Author对象中的name方法和year来获取。
ClassPool pool = ClassPool.getDefault(); pool.importPackage("java.awt"); CtClass cc = pool.makeClass("Test"); CtField f = CtField.make("public Point p;", cc); cc.addField(f);第二行表明引入java.awt包,那么第三行就不会抛出错误,由于编译器能够将Point类识别为java.awt.Point。
class A {} class B extends A {} class C extends B {} class X { void foo(A a) { .. } void foo(B b) { .. } }若是编译的表达式是x.foo(new C()),其中x变量指向了X类实例,此时编译器尽管能够正确的编译foo((B)new C()),可是它依旧会将会调用foo(A)。
javassist.CtClass.intType.getName()咱们会访问javassist.Ctclass中的静态字段intType,而后调用其getName方法。而在Javassist中,咱们能够按照以下的表达式来书写:
javassist.CtClass#intType.getName()这样编译器就可以快速的解析此表达式了。
BufferedInputStream fin = new BufferedInputStream(new FileInputStream("Point.class")); ClassFile cf = new ClassFile(new DataInputStream(fin));这个代码片断展现了从Point.class类中建立出一个ClassFile对象出来。
上面的代码生成了Foo.class这个类文件,它包含了对以下类的扩展:ClassFile cf = new ClassFile(false, "test.Foo", null); cf.setInterfaces(new String[] { "java.lang.Cloneable" }); FieldInfo f = new FieldInfo(cf.getConstPool(), "width", "I"); f.setAccessFlags(AccessFlag.PUBLIC); cf.addField(f); cf.write(new DataOutputStream(new FileOutputStream("Foo.class")));
ClassFile提供了addField方法和addMethod方法来添加字段或者方法(须要注意的是,在字节码层面上说来,构造器也被视为方法),同时也提供了addAttribute方法来为类文件添加属性。package test; class Foo implements Cloneable { public int width; }5.2 添加和删除成员
ClassFile cf = ... ; MethodInfo minfo = cf.getMethod("move"); // we assume move is not overloaded. CodeAttribute ca = minfo.getCodeAttribute(); 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)
下面的代码段展现了方法体中的全部指令:
CodeIterator ci = ... ; while (ci.hasNext()) { int index = ci.next(); int op = ci.byteAt(index); System.out.println(Mnemonic.OPCODE[op]); }
ConstPool cp = ...; // constant pool table Bytecode b = new Bytecode(cp, 1, 0); b.addIconst(3); b.addReturn(CtClass.intType); CodeAttribute ca = b.toCodeAttribute();代码将会产生以下的序列:
iconst_3 ireturn你也能够利用Bytecode中的get方法来获取一个字节码数组序列,以后能够将此数组插入到另外一个代码段中。
上面的代码流程是建立了默认的构造函数后,而后将其添加到cf指向的类中。具体说来就是,Bytecode对象首先被转换成了CodeAttribute对象,接着被添加到minfo所指向的方法中。此方法最终被添加到cf类文件中。ClassFile cf = ... Bytecode code = new Bytecode(cf.getConstPool()); code.addAload(0); code.addInvokespecial("java/lang/Object", MethodInfo.nameInit, "()V"); code.addReturn(null); code.setMaxLocals(1); MethodInfo minfo = new MethodInfo(cf.getConstPool(), MethodInfo.nameInit, "()V"); minfo.setCodeAttribute(code.toCodeAttribute()); cf.addMethod(minfo);
Vector<String> v = new Vector<String>(); : String s = v.get(0);编译后的字节码等价于以下代码:
Vector v = new Vector(); : String s = (String)v.get(0);因此,当你写了一套字节码转换器后,你能够移除掉全部的类型参数。因为嵌入在Javassist的编译器不支持泛型,因此利用其编译的时候,你不得不在调用端作显式的类型转换。好比,CtMethod.make方法。可是若是源码是利用常规的Java编译器,好比javac,来编译的话,是无需进行类型转换的。
public class Wrapper<T> { T value; public Wrapper(T t) { value = t; } }想添加Getter<T>接口到Wrapper<T>类中:
public interface Getter<T> { T get(); }那么实际上,你须要添加的接口是Getter(类型参数<T>已经被抹除),须要添加到Wrapper中的方法以下:
public Object get() { return value; }须要注意的是,非类型参数是必须的。因为get方法返回了Object类型,那么调用端若是用Javassist编译的话,就须要进行显式类型转换。好比,以下例子,类型参数T是String类型,那么(String)就必须被按照以下方式插入:
Wrapper w = ... String s = (String)w.get();当使用常规的Java编译器编译的时候,类型转换是不须要的,由于编译器会自动进行类型转换。
public int length(int... args) { return args.length; }下面的Javassist代码将会生成如上的方法:
CtClass cc = /* target class */; CtMethod m = CtMethod.make("public int length(int[] args) { return args.length; }", cc); m.setModifiers(m.getModifiers() | Modifier.VARARGS); cc.addMethod(m);参数类型int...变成了int[]数组,Modifier.VARARGS被添加到了方法修改器中。
length(new int[] { 1, 2, 3 });而不是这样来:
length(1, 2, 3);
m.getMethodInfo().rebuildStackMapForME(cpool);这里,cpool是ClassPool对象,此对象能够利用CtClass对象中的getClassPool来获取,它负责利用给定的类路径来找寻类文件。为了获取全部的CtMethods对象,能够经过调用CtClass对象的getDeclaredMethods来进行。
Integer i = 3;能够看出,此装箱操做是隐式的。可是在Javassist中,你必须显式的将值类型从int转为Integer:
Integer i = new Integer(3);
CtClass.debugDump = "./dump";此时,全部的被修改的类文件将会被保存到./dump目录中。