官方文档 http://www.javassist.org/tutorial/tutorial.html
在java8以上版本直接运行会报错
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 Exception in thread "main" java.lang.reflect.InaccessibleObjectException: Unable to make protected final java.lang.Class java.lang.ClassLoader.defineClass(java.lang.String, byte[],int,int,java.security.ProtectionDomain) throws java.lang.ClassFormatError accessible: module java.base does not "opens java.lang" to unnamed module @d2cc05a at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:354) at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:297) at java.base/java.lang.reflect.Method.checkCanSetAccessible(Method.java:199) at java.base/java.lang.reflect.Method.setAccessible(Method.java:193) at javassist.util.proxy.SecurityActions.setAccessible(SecurityActions.java:159) at javassist.util.proxy.DefineClassHelper$JavaOther .defineClass(DefineClassHelper.java:213) at javassist.util.proxy.DefineClassHelper$Java11 .defineClass(DefineClassHelper.java:52) at javassist.util.proxy.DefineClassHelper.toClass(DefineClassHelper.java:260) at javassist.ClassPool.toClass(ClassPool.java:1232) at javassist.ClassPool.toClass(ClassPool.java:1090) at javassist.ClassPool.toClass(ClassPool.java:1048) at javassist.CtClass.toClass(CtClass.java:1290) at com.ssdmbbl.javassist.DemoTest.main(DemoTest.java:19)
需要加上VM参数
1 --add-opens=java.base/java.lang=ALL-UNNAMED
一 基本概念 1.1 基础知识 Javassist 是一个开源的分析、编辑和创建Java字节码的类库. 其主要优点在于简单快速. 直接使用 java 编码的形式, 而不需要了解虚拟机指令, 就能动态改变类的结构, 或者动态生成类.
Javassist中最为重要的是ClassPool
,CtClass
, CtMethod
以及CtField
这几个类.
ClassPool
: 一个基于Hashtable
实现的CtClass
对象容器, 其中键是类名称, 值是表示该类的CtClass
对象
CtClass
: CtClass
表示类, 一个CtClass
(编译时类)对象可以处理一个class文件, 这些CtClass
对象可以从ClassPool
获得
CtMethods
: 表示类中的方法
CtFields
: 表示类中的字段
1.2 ClassPool的相关方法
getDefault
: 返回默认的ClassPool
是单例模式的,一般通过该方法创建我们的ClassPool
;
appendClassPath
, insertClassPath
: 将一个ClassPath
加到类搜索路径的末尾位置 或 插入到起始位置。通常通过该方法写入额外的类搜索路径,以解决多个类加载器环境中找不到类的尴尬;
toClass
: 将修改后的CtClass
加载至当前线程的上下文类加载器中,CtClass
的toClass
方法是通过调用本方法实现。需要注意的是一旦调用该方法,则无法继续修改已经被加载的class;
get
, getCtClass
: 根据类路径名获取该类的CtClass
对象,用于后续的编辑。
1 2 3 4 ClassPool pool = new ClassPool(true ); ClassPool pool1 = ClassPool.getDefault();
为减少ClassPool可能导致的内存消耗. 可以从ClassPool中删除不必要的CtClass对象. 或者每次创建新的ClassPool对象.
1 2 3 4 ctClass.detach(); ClassPool pool2 = new ClassPool(true );
1.3 CtClass的相关方法
freeze: 冻结一个类,使其不可修改;
isFrozen : 判断一个类是否已被冻结;
prune : 删除类不必要的属性,以减少内存占用。调用该方法后,许多方法无法将无法正常使用,慎用;
defrost : 解冻一个类,使其可以被修改。如果事先知道一个类会被defrost, 则禁止调用 prune 方法;
detach : 将该class从ClassPool中删除;
writeFile : 根据CtClass生成 .class 文件;
toClass : 通过类加载器加载该CtClass。
setInterfaces: 添加父接口
setSuperclass: 添加父类
1.3.1 获取CtClass 1 2 3 4 CtClass ctClass = pool.get("com.kawa.ssist.JustRun" ); CtClass ctClass1 = pool.getOrNull("com.kawa.ssist.JustRun" );
1.3.2 创建CtClass 1 2 3 4 5 6 CtClass ctClass2 = pool.getAndRename("com.kawa.ssist.JustRun" , "com.kawa.ssist.JustRunq" ); CtClass ctClass3 = pool.makeClass("com.kawa.ssist.JustRuna" ); CtClass ctClass4 = pool.makeClass(new FileInputStream(new File("/home/un/test/JustRun.class" )));
1.3.3 CtClass基础信息 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 String simpleName = ctClass.getSimpleName(); String name = ctClass.getName(); String packageName = ctClass.getPackageName(); CtClass[] interfaces = ctClass.getInterfaces(); CtClass superclass = ctClass.getSuperclass(); CtMethod ctMethod = ctClass.getDeclaredMethod("getName()" , new CtClass[] {pool.get(String.class .getName ()), pool .get (String .class .getName ())}) ; CtField ctField = ctClass.getField("name" ); ctClass.isArray(); ctClass.isPrimitive(); ctClass.isInterface(); ctClass.isEnum(); ctClass.isAnnotation(); ctClass.freeze () ctClass.isFrozen() ctClass.prune() ctClass.defrost()
1.3.4 CtClass类操作 1 2 3 4 5 6 7 8 ctClass.addInterface(...); ctClass.addConstructor(...); ctClass.addField(...); ctClass.addMethod(...);
1.3.5 CtClass类编译 1 2 3 4 5 6 Class clazz = ctClass.toClass(); ClassFile classFile = ctClass.getClassFile(); byte [] bytes = ctClass.toBytecode();
1.4 CtMethod的相关方法 上面我们创建一个新的方法使用了CtMethod
类。CtMthod
代表类中的某个方法,可以通过CtClass
提供的API获取或者CtNewMethod
新建,通过CtMethod
对象可以实现对方法的修改。
1.4.1 获取CtMethod属性 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 CtClass ctClass5 = pool.get(TestService.class .getName ()) ; CtMethod ctMethod = ctClass5.getDeclaredMethod("selectOrder" ); String methodName = ctMethod.getName(); CtClass returnType = ctMethod.getReturnType(); ctMethod.getLongName(); ctMethod.getSignature(); List<String> argKeys = new ArrayList<>(); MethodInfo methodInfo = ctMethod.getMethodInfo(); CodeAttribute codeAttribute = methodInfo.getCodeAttribute(); LocalVariableAttribute attr = (LocalVariableAttribute) codeAttribute.getAttribute(LocalVariableAttribute.tag); int len = ctMethod.getParameterTypes().length;int pos = Modifier.isStatic(ctMethod.getModifiers()) ? 0 : 1 ;for (int i = pos; i < len; i++) { argKeys.add(attr.variableName(i)); }
1.4.2 CtMethod方法体修改 1 2 3 4 5 6 7 8 9 10 11 12 13 14 ctMethod.insertBefore("" ); ctMethod.insertAfter("" ); ctMethod.insertAt(10 , "" ); ctMethod.addParameter(CtClass); ctMethod.setName("newName" ); ctMethod.setBody("{$0.name = $1;}" ); ctMethod.make("kawa" ,CtClass);
1.4.3 异常块 addCatch() 在方法中加入try catch块, 需要注意的是, 必须在插入的代码中, 加入return值$e代表异常信息.插入的代码片段必须以throw或return语句结束
1 2 3 4 5 6 7 8 9 10 CtMethod m = ...; CtClass etype = ClassPool.getDefault().get("java.io.IOException" ); m.addCatch("{ System.out.println($e); throw $e; }" , etype); try { } catch (java.io.IOException e) { System.out.println(e); throw e; }
二 类搜索路径 通过ClassPool.getDefault()
获取的ClassPool
使用 JVM 的类搜索路径。如果程序运行在JBoss或者Tomcat等 Web 服务器上,ClassPool
可能无法找到用户的类,因为 Web 服务器使用多个类加载器作为系统类加载器。在这种情况下,ClassPool
必须添加额外的类搜索路径。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 ClassPool pool = ClassPool.getDefault(); pool.insertClassPath("/usr/local/javalib" ) ClassPath cp = new URLClassPath("www.javassist.org" , 80 , "/java/" , "org.javassist." ); pool.insertClassPath(cp); byte [] b = a byte array;String name = class name ; cp.insertClassPath(new ByteArrayClassPath(name, b)); InputStream ins = an input stream for reading a class file ; CtClass cc = cp.makeClass(ins);
2.1 通过ClassClassPath
添加搜索路径 1 2 3 4 5 6 7 8 9 pool.insertClassPath(new ClassClassPath(Person.getClass())); pool.insertClassPath(new ClassClassPath(this .getClass())); pool.appendClassPath(new ClassClassPath(this .getClass())); pool.insertClassPath("/xxx/lib" );
上面的语句将Person类添加到pool的类加载路径中。但在实践中,我发现通过这个可以将Person类所在的整个jar包添加到类加载路径中。
2.2 通过指定目录来添加搜索路径 也可以注册一个目录作为类搜索路径:如pool.insertClassPath("/usr/javalib")
;则是将 /usr/javalib
目录添加到类搜索路径中。
2.3 通过URL指定搜索路径 1 2 3 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
获取。
2.4 通过ByteArrayPath
添加搜索路径 1 2 3 4 5 ClassPool cp = ClassPool.getDefault(); byte [] buf = 字节数组;String name = 类名; cp.insertClassPath(new ByteArrayClassPath(name, buf)); CtClass cc = cp.get(name);
示例中的 CtClass
对象是字节数据buf
代表的class文件。将对应的类名传递给ClassPool
的get()
方法,就可以从字节数组中读取到对应的类文件。
2.5 通过输入流加载class 如果你不知道类的全名,可以使用makeClass()
方法:
1 2 3 ClassPool cp = ClassPool.getDefault(); InputStream ins = class 文件对应的输入流 ; CtClass cc = cp.makeClass(ins);
makeClass()
返回从给定输入流构造的CtClass
对象。你可以使用makeClass()
将类文件提供给ClassPool
对象。如果搜索路径包含大的jar文件,这可能会提高性能。由于ClassPool
对象按需读取类文件,它可能会重复搜索整个jar文件中的每个类文件。makeClass()
可以用于优化此搜索。由makeClass()
构造的CtClass
保存在ClassPool
对象中,从而使得类文件不会再被读取。
三 读写字节码 3.1 bytecode读写 Javassist是用来处理java字节码的类库, java字节码一般存放在后缀名称为class的二进制文件中。每个二进制文件都包含一个java类或者是java接口。
Javasist.CtClass是对类文件的抽象,处于编译中的此对象可以用来处理类文件。下面的代码用来展示一下其简单用法:
1 2 3 4 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()
方法来直接获取修改的字节码:
1 byte [] b = cc.toBytecode();
你可以通过如下代码直接加载CtClass:
1 Class clazz = cc.toClass();
toClass()
方法被调用,将会使得当前线程中的context class loader加载此CtClass类,然后生成java.lang.Class
对象。更多的细节 ,请参见this section below .
3.1.1 新建类 新建一个类,可以使用ClassPool.makeClass()
方法来实现:
1 2 ClassPool pool = ClassPool.getDefault(); CtClass cc = pool.makeClass("Point" );
上面的代码展示的是创建无成员方法的Point
类,如果需要附带方法的话,我们可以用CtNewMethod
附带的工厂方法创建,然后利用CtClass.addMethod()
将其追加就可以了 。
makeClass()
不能用于创建新的接口。但是makeInterface()
可以。接口的方法可以用CtNewmethod.abstractMethod()
方法来创建,需要注意的是,在这里,一个接口方法其实是一个abstract方法。
3.1.2 冻结类 如果CtClass
对象被writeFile()
,toClass()
或者toBytecode()
转换成了类对象,Javassist将会冻结此CtClass
对象。任何对此对象的后续更改都是不允许的。之所以这样做,主要是因为此类已经被JVM加载,由于JVM本身不支持类的重复加载操作,所以不允许更改。
一个冻结的CtClass
对象,可以通过如下的代码进行解冻,如果想更改类的话,代码如下:
1 2 3 4 5 CtClasss cc = ...; : cc.writeFile(); cc.defrost(); cc.setSuperclass(...);
调用了defrost()
方法之后,CtClass
对象就可以随意修改了。
如果ClassPool.doPruning
被设置为true,那么Javassist将会把已冻结的CtClass
对象中的数据结构进行精简,此举主要是为了防止过多的内存消耗。而精简掉的部分,都是一些不必要的属性(attriute_info结构)。因此,当一个CtClass
对象被精简之后,方法是无法被访问和调用的,但是方法名称,签名,注解可以被访问。被精简过的CtClass
对象可以被再次解冻。需要注意的是,ClassPool.doPruning
的默认值为false。
为了防止CtClass
类被无端的精简,需要优先调用stopPruning()
方法来进行阻止:
1 2 3 4 5 CtClasss cc = ...; cc.stopPruning(true ); : cc.writeFile();
这样,CtClass
对象就不会被精简了。当writeFile()
方法调用之后,我们就可以进行解冻,然后为所欲为了。
需要注意的是:在调试的时候,debugWriteFile()
方法可以很方便的防止CtClass
对象精简和冻住。
3.2类搜索路径 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 2 ClassPool pool = ClassPool.getDefault(); pool.insertClassPath("/usr/local/javalib" );
也可以使用url来作为搜索路径:
1 2 3 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类,此类将会从如下路径获取:
1 http://www.javassist.org:80/java/org/javassist/test /Main.class
此外,你甚至可以直接使用一串字节码,然后创建出CtClass
对象。示例如下:
1 2 3 4 5 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()
来实现:
1 2 3 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
即可。这种做法可以允许其他资源被包含到搜索路径中。
3.3 ClassPool 一个ClassPool
里面包含了诸多的CtClass
对象。每当一个CtClass
对象被创建的时候,都会在ClassPool
中做记录。之所以这样做,是因为编译器后续的源码编译操作可能会通过此类关联的CtClass
来获取。
比如,一个代表了Point类的CtClass
对象,新加一个getter()
方法。之后,程序将会尝试编译包含了getter()
方法的Point类,然后将编译好的getter()
方法体,添加到另外一个Line类上面。如果CtClass
对象代表的Point类不存在的话,那么编译器就不会成功的编译getter()
方法。需要注意的是原来的类定义中并不包含getter()
方法 。因此,要想正确的编译此方法,ClassPool
对象必须包含程序运行时候的所有的CtClass
对象。
3.3.1避免内存溢出 CtClass
对象非常多的时候,ClassPool
将会消耗内存巨大。为了避免个问题,你可以移除掉一些不需要的CtClass
对象。你可以通过调用CtClass.detach()
方法来实现,那样的话此CtClass
对象将会从ClassPool
移除。代码如下:
1 2 3 CtClass cc = ... ; cc.writeFile(); cc.detach();
此CtClass
对象被移除后,不能再调用其任何方法。但是你可以调用ClassPool.get()
方法来创建一个新的CtClass
实例。
另一个方法就是用新的ClassPool
对象来替代旧的ClassPool
对象。如果旧的ClassPool
对象被垃圾回收了,那么其内部的CtClass
对象也都会被垃圾回收掉。下面的代码可以用来创建一个新的ClassPool
对象:
1 2 ClassPool cp = new ClassPool(true );
上面的代码和ClassPool.getDefault()
来创建ClassPool
,效果是一样的。需要注意的是,ClasssPool.getDefault()
是一个单例工厂方法,它能够创建出一个唯一的ClassPool
对象并进行重复利用。new ClassPool(true)
是一个很快捷的构造方法,它能够创建一个ClassPool
对象然后追加系统搜索路径到其中。和如下的代码创建行为表现一致:
1 2 ClassPool cp = new ClassPool(); cp.appendSystemPath();
3.3.2 级联ClassPools 如果应用运行在JBOSS/Tomcat上, 那么创建多个ClassPool
对象将会很有必要。因为每个类加载其都将会持有一个ClassPool
的实例。应用此时最好不用getDefault()
方法来创建ClassPool
对象,而是使用构造来创建。
多个ClassPool
对象像java.lang.ClassLoader
一样做级联,代码如下:
1 2 3 ClassPool parent = ClassPool.getDefault(); ClassPool child = new ClassPool(parent); child.insertClassPath("./classes" );
如果child.get()
被调用,子ClassPool
将会首先从父ClassPool
进行查找。当父ClassPool
查找不到后,然后将会尝试从./classes
目录进行查找。
如果child.childFirstLookup = true
, 子ClassPool
将会首先查找自己的目录,然后查找父ClassPool
,代码如下:
1 2 3 4 ClassPool parent = ClassPool.getDefault(); ClassPool child = new ClassPool(parent); child.appendSystemPath(); child.childFirstLookup = true ;
3.3.3为新类重命名 可以从已有类创建出新的类,代码如下:
1 2 3 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
对象已经不存在了,请看如下代码:
1 2 3 4 5 6 ClassPool pool = ClassPool.getDefault(); CtClass cc = pool.get("Point" ); CtClass cc1 = pool.get("Point" ); cc.setName("Pair" ); CtClass cc2 = pool.get("Pair" ); CtClass cc3 = pool.get("Point" );
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
对象来生成不同的类。
3.3.4 从冻结类中创建新类 当CtClass
对象通过writeFile()
方法或者toBytecode()
转变成类文件的时候,Javassist将不允许对这个CtClass
对象有任何修改。因此,当代表Point类的CtClass
对象被转换成了类文件,你不能够先拷贝Point类,然后修改名称为Pair类,因为Point类中的setName()
方法是无法被执行的,错误使用示例如下:
1 2 3 4 ClassPool pool = ClassPool.getDefault(); CtClass cc = pool.get("Point" ); cc.writeFile(); cc.setName("Pair" );
为了能够避免这种限制,你应该使用getAndRename()
方法,正确示例如下:
1 2 3 4 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.3 类加载器 如果预先知道需要修改什么类,最简单的修改方式如下:
调用ClassPool.get()方法获取CtClass对象
修改此对象
调用CtClass对象的writeFile()方法或者toBytecode()方法来生成类文件。
如果检测类是否修改行为发生在程序加载的时候,那么对于用户说来,Javassist最好提供这种与之匹配的类加载检测行为。事实上,javassist可以做到在类加载的时候来修改二进制数据。使用Javassist的用户可以定义自己的类加载器,当然也可以采用Javassist自身提供的。
3.3.1 CtClass中的toClass方法 CtClass
提供的toClass()方法,可以很方便的加载当前线程中通过CtClass
对象创建的类。但是为了使用此方法,调用方必须拥有足够的权限才行,否则将会报SecurityException
错误。
下面的代码段展示了如何使用toClass()方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 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()
方法中的代码如下:
1 2 3 4 5 6 public static void main(String[] args) throws Exception { Hello orig = new Hello(); ClassPool cp = ClassPool.getDefault(); CtClass cc = cp.get("Hello"); : }
main方法中,第一行的Hello类会被加载,之后调用toClass()将会报错,因为一个类加载器无法在同一时刻加载两个不同的Hello类版本。
如果程序跑在JBoss/Tomcat上,利用toClass()方法可能会有些问题。在这种情况下,你将会遇到ClassCastException
错误,为了避免这种错误,你必须为toClass()
方法提供非常明确的类加载器。比如,在如下代码中,bean代表你的业务bean对象的时候:
1 2 CtClass cc = ...; Class c = cc.toClass(bean.getClass().getClassLoader());
则就不会出现上述问题。你应当为toClass()
方法提供已经加载过程序的类加载器才行。
toClass()
的使用会带来诸多方便,但是如果你需要更多更复杂的功能,你应当实现自己的类加载器。
3.4 java中的类加载 在java中,多个类加载器可以共存,不同的类加载器会创建自己的应用区域。不同的类加载器可以加载具有相同类名称但是内容不尽相同的类文件。这种特性可以让我们在一个JVM上并行运行多个应用。
需要注意的是JVM不支持动态的重新加载一个已加载的类。一旦类加载器加载了一个类,那么这个类或者基于其修改的类,在JVM运行时,都不能再被加载。因此,你不能够修改已经被JVM加载的类。但是,JPDA(Java Platform Debugger Architecture)支持这种做法。具体请见 Section 3.6 .
如果一个类被两个不同的类加载器加载,那么JVM会将此类分成两个不同的类,但是这两个类具有相同的类名和定义。我们一般把这两个类当做是不同的类,所以一个类不能够被转换成另一个类,一旦这么做,那么这种强转操作将会抛出错误ClassCastException
。
比如,下面的例子会抛错:
1 2 3 4 MyClassLoader myLoader = new MyClassLoader(); Class clazz = myLoader.loadClass("Box" ); Object obj = clazz.newInstance(); Box b = (Box)obj;
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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public class Point { private int x, y; public int getX () { return x; } : } public class Box { private Point upperLeft, size; public int getBaseX () { return upperLeft.x; } : } public class Window { 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类。
接下来,让我们看一个少量更改过的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public class Point { private int x, y; public int getX () { return x; } : } public class Box { private Point upperLeft, size; public Point getSize () { return size; } : } public class Window { private Box box; public boolean widthIs (int w) { Point p = box.getSize(); return w == p.getX(); } }
现在看来,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 2 3 4 5 public class Point { public int x, y; public int getX () { return x; } : }
想要了解java中更多的类加载器信息,以下信息也许有帮助:
1 2 Sheng Liang and Gilad Bracha, "Dynamic Class Loading in the Java Virtual Machine", ACM OOPSLA'98, pp.36-44, 1998.
3.5 使用javassist.Loader Javassist提供了javassist.Loader
这个类加载器。它使用javassist.ClassPool
对象来读取类文件。
举个例子,使用javassist.Loader
来加载Javassist修改过的类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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
类的实例出来。
如果一个类被加载后,用户想要修改成自己想要的东西进来,那么用户可以通过添加事件监听器到javassist.Loader上。每当类加载器加载了类进来,那么事件监听器将会发出通知。此监听器必须实现如下的接口:
1 2 3 4 5 6 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
:
1 2 3 4 5 6 7 8 9 10 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
会调用这些方法来获取类文件。
为了能够运行MyApp类中的MyTranslator
对象,写了一个主方法如下:
1 2 3 4 5 6 7 8 9 10 11 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); } }
想要运行它,可以按照如下命令来:
1 % java Main2 arg1 arg2...
MyApp类和其他的一些类,会被MyTranslator所翻译。
需要注意的是,类似MyApp这种应用类,是不能够访问Main2,MyTranslator,ClassPool这些类的,因为这些类是被不同加载器所加载的。应用类是被javassist.Loader所加载,而Main2这些是被java的默认类加载器所加载的。
javassist.Loader搜寻需要加载的类的时候,和java.lang.ClassLoader.ClassLoader是截然不同的。后者先使用父类加载器进行加载,如果父类加载器找不到类,则尝试用当前加载器进行加载。而javassist.Load在如下情况下,则尝试直接加载:
Javassist可以按照搜索的顺序来加载已修改的类,但是,如果它无法找到已修改的类,那么将会由父类加载器进行加载操作。一旦当一个类被父加载器所加载,那么指向此类的其他类,也将被此父加载器所加载,因为,这些被加载类是不会被修改的。如果你的程序无法加载一个已修改的类,你需要确认所有的类是否是被javassist.Loader所加载。
3.5 打造一个类加载器 用javassist打造一个简单的类加载器,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 import javassist.*;public class SampleLoader extends ClassLoader { 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" ); } protected Class findClass (String name) throws ClassNotFoundException { try { CtClass cc = pool.get(name); 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方法中必须要有所体现,当然此目录名称是可以随意改变的。接下来我们运行如下命令:
此时,类加载器将会加载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 2 3 4 5 6 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
文件。
为了能够让更改的String类在MyApp中运行,可以按照如下的方式来进行:
1 % java -Xbootclasspath/p:. MyApp arg1 arg2...
假设MyApp的代码如下:
1 2 3 4 5 public class MyApp { public static void main (String[] args) throws Exception { System.out.println(String.class.getField("hiddenValue").getName()); } }
此更改的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文档。
四 定制化 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 2 3 4 void move (int newX, int newY, int 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文档。
4.1 方法体前/后穿插代码段 CtMethod
和CtConstructor
提供了insertBefore
,insertAfter
,addCatch
三个方法,它们用于在已存在的方法中插入代码段。使用者可以插入java代码段是因为Javassist内置了一个简易的java编译器来处理这些源码。此编译器会将java源码编译成字节码,然后插入到方法体中。
同时,在指定行号的位置插入代码段也是允许的(只有当行号在当前类中存在)。CtMethod
和CtConstructor
中的insertAt
方法带有源码输入和行号的定义,它能够将编译后的代码段插入到指定了行号的位置。
insertBefore``,insertAfter
,addCatch
和insertAt
方法均接受一个String类型的代表源码块的入参。此代码段可以是简单的控制类语句if和while,也可以是以分号结尾的表达式,都需要用左右大括号{}
进行包装。因此,下面的示例源码都是符合要求的代码段:
1 2 3 System.out.println("Hello" ); { System.out.println("Hello" ); } if (i < 0 ) { i = -i; }
代码段可以指向字段和方法,也可以为编译器添加-g选项来让其指向插入的方法中的参数。否则,只能利用$0,$1,$2...
这种如下的变量来进行访问。虽然不允许访问方法中的本地变量,但是在方法体重定义一个新的本地变量是允许的。例外的是,编译器开启了-g
选项的话,insertAt
方法是允许代码段访问本地变量的。
insertBefore
,insertAfter
,addCatch
和insertAt
入参中的String对象,也就是用户输入的代码段,会被Javassist中的编译器编译,由于此编译器支持语言扩展,不同的$
符号有不同的含义:
1 2 3 4 5 6 7 8 9 10 11 12 $0 , $1 , $2 , ... this and actual parameters$args An array of parameters. The type of $args is Object[].$$ All actual parameters. For example, m($$) is equivalent to m($1 ,$2 ,...) $cflow (...) cflow variable$r The result type . It is used in a cast expression.$w The wrapper type . It is used in a cast expression.$_ The resulting value$sig An array of java.lang.Class objects representing the formal parameter types.$type A java.lang.Class object representing the formal result type .$class A java.lang.Class object representing the class currently edited.
4.1.1 $0, $1, $2, ...
传给目标方法的参数$1,$2...
将会替换掉原始的参数名称。$1
代表第一个参数,$2
代表第二个参数,以此类推。这些参数的类型和原始的参数类型是一致的。$0
等价于this
关键字,如果方法为static
,那么$0
将不可用。
这些变量的使用方法如下,以Point类为例:
1 2 3 4 class Point { int x, y; void move (int dx, int dy) { x += dx; y += dy; } }
调用move
方法,打印dx
和dy
的值,执行如下的程序
1 2 3 4 5 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
方法中的代码段是被大括号{}包围的,此方法只接受一个被大括号包围的代码段入参。
更改之后的Point类如下:
1 2 3 4 5 6 7 class Point { int x, y; void move(int dx, int dy) { { System.out.println(dx); System.out.println(dy); } x += dx; y += dy; } }
$1
和$2
被dx
和dy
替换掉。
从这里可以看出,$1
,$2
,$3
…是可以被更新的。如果一个新的值被赋予了这几个变量中的任意一个,那么这个变量对应的参数值也会被更新。下面来说说其他的参数。
4.1.2 $args
$args
变量代表所有参数列表。其类型为Object数组类型。如果一个参数类型基础数据类型,比如int
,那么将会被转换为java.lang.Integer
并放到$args
中。因此,$args[0]
一般情况下等价于$1
,除非第一个参数的类型为基础数据类型。需要注意的是,$args[0]
和$0
是不等价的,因为$0
代表this
关键字。
如果object
列表被赋值给$args
,那么列表中的每个元素将会被分配给对应的参数。如果一个参数的类型为基础数据类型,那么对应的正确的数据类型为包装过的类型。此转换会发生在参数被分配之前。
4.1.3 $$$$ $$$$是被逗号分隔的所有参数列表的缩写。比如,如果move
方法中的参数数量有三个,那么
等价于:
如果move()
无入参,那么move($$)
等价于move().
$$$$也可以被用于其他的场景,如果你写了如下的表达式:
那么此表达式等价于:
1 exMove($1 ,$2 ,$3 ,context)
需要注意的是,$$$$虽说是方法调用的通用符号,但是一般和$proceed
联合使用,后面会讲到。
4.1.4 $cflow
代表着“流程控制”。这个只读变量会返回方法的递归调用深度。
假设如下的方法代表CtMethod
中的对象cm
:
1 2 3 4 5 6 int fact (int n) { if (n <= 1 ) return n; else return n * fact(n - 1 ); }
为了使用$cflow
,首先需要引用$cflow
,用于监听fact方法的调用
1 2 CtMethod cm = ...; cm.useCflow("fact" );
useCflow()
方法就是用来声明$cflow
变量。任何可用的java命名都可以用来进行识别。此名称也可以包含.
(点号),比如"my.Test.face"
也是可以的。
然后,$cflow(fact)
代表着方法cm递归调用的深度。当方法第一次被调用的时候,$cflow(fact)
的值为0,再调用一次,此值将会变为1.比如:
1 2 cm.insertBefore("if ($cflow(fact) == 0)" + " System.out.println(\"fact \" + $1);" );
代码段将fact
方法进行编译以便于能够看到对应的参数。由于$cflow(fact)
被选中,那么对fact
方法的递归调用将不会显示参数。
$cflow
的值是当前线程中,从cm方法中,最上层栈帧到当前栈帧的值。$cflow
同时和cm方法在同一个方法内部的访问权限也是不一样的。
4.1.5 $r
代表着结果类型,必须在转换表达式中用作类型转换。比如,如下用法
1 2 Object result = ... ; $_ = ($r)result;
如果结果类型为基础数据类型,那么($r)
需要遵循如下的规则:
首先,如果操作数类型是基础数据类型,($r)
将会被当做普通的转义符。相反的,如果操作数类型是包装类型,那么($r)
将会把此包装类型转换为结果类型,比如如果结果类型是int
,那么($r)
会将java.lang.Integer
转换为intl
;如果结果类型是void
,那么($r)
将不会进行类型转换;如果当前操作调用了void
方法,那么($r)
将会返回null
。举个例子,如果foo
方法是void
方法,那么:
是一个有效的申明。
转换符号($r)
同时也用于return申明中,即便返回类型是void,如下的return申明也是有效的:
这里,result是一个本地变量,由于($r)
这里做了转换,那么返回结果是无效的。此时的return申明和没有任何返回的return申明是等价的:
4.1.6 $w
代表包装类型。必须在转义表达式中用于类型转换。($w)
将基础类型转换为对应的包装类型,如下代码示例
结果类型依据($w)
后面的表达式来确定,如果表达式是double类型,那么包装类型则为java.lang.Double
。如果($w)
后面的表达式不是基础类型,那么($w)
将不进行任何转换。
4.1.7 $_
CtMethod
和CtConstructor
中的insertAfter
方法将编译过的代码插入到方法的尾部。之前给过的一些例子有关insertAfter
的例子中,不仅包括$0.$1这种例子的讲解,而且包括$_
的这种例子。说道$_
变量,它用来代表方法的结果值。其变量类型是方法返回的结果类型。如果返回的结果类型是void,那么$_
的类型是Object类型,但是其值为null
。
尽管利用insertAfter
插入的编译过的代码,是在方法返回之前被执行的,但是这种代码也可以在在方法抛出的exception
中执行。为了能够让其在方法抛出的exception
中执行,insertAfter
方法中的第二个参数asFinally
必须为true。
当exception
被抛出的时候,利用insertAfter
方法插入的代码段将会和作为finally
代码块来执行。此时在编译过的代码中,$_
的值为0或者null。当此代码段执行完毕后,exception
会被重新抛给调用端。需要注意的是,$_
是永远不会被抛给调用端的,它会直接被抛弃掉。
4.1.8 $sig
$type
的值是java.lang.Class
对象,代表着返回值的正确的类型。如果它指向的是构造器,那么其值为Void.class
。
4.1.9 $class
$class
的值是java.lang.Class
对象,代表着当前编辑的方法,此时和$0
是等价的。
4.1.10 addCatch()
此方法用于将代码段插入到方法体中进行执行,在执行过程中一旦方法体抛出exception
,可以控制给发送给客户端的返回。下面的源码展示了利用特殊的变量$e
来指向exception
1 2 3 CtMethod m = ...; CtClass etype = ClassPool.getDefault().get("java.io.IOException" ); m.addCatch("{ System.out.println($e); throw $e; }" , etype);
此方法体m被翻译出来后,展示如下:
1 2 3 4 5 6 7 try { the original method body } catch (java.io.IOException e) { System.out.println(e); throw e; }
需要注意的是,插入的代码段必须以throw
或者return
命令结尾。
4.2 修改方法体 CtMethod
和CtContructor
提供setBody
方法来取代整个方法体。此方法能够将传入的代码段编译为Java字节码,然后用此字节码将其原有的方法体给替换掉。如果给定代码段为空,那么被替换的方法体将只有return 0
声明,如果结果类型为void
,那么则只有return null
声明。
外部传入给setBody
方法的代码段,会包含如下的以$
开头的识别码,这些识别码有不同的含义:
1 2 3 4 5 6 7 8 9 10 $0 , $1 , $2 , ... this and actual parameters$args An array of parameters. The type of $args is Object[].$$ All actual parameters. $cflow (...) cflow variable$r The result type . It is used in a cast expression.$w The wrapper type . It is used in a cast expression.$sig An array of java.lang.Class objects representing the formal parameter types.$type A java.lang.Class object representing the formal result type .$class A java.lang.Class object representing the class that declares the methodcurrently edited (the type of $0 ).
需要注意的是,此时$_
是不可用的。
4.2.1 利用源文本替换现有表达式 Javassist允许修改方法体中的表达式。可以利用javassist.expr.ExprEditor
类来进行替换操作。用户可以通过定义ExprEditor
的子类来修改表达式。为了运行ExprEditor
对象,用户必须调用CtMethod
或者CtClass
中的instrument
方法来进行,示例如下
1 2 3 4 5 6 7 8 9 10 11 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 { $1 = 0; $_ = $proceed($$); }
move
方法中的第一个参数将永远为0
,需要注意的替换的代码不仅仅是表达式,也可以是声明或者代码块,但是不能是try-catch
声明。
instrument
方法可以用来搜索方法体,如果找到了待替换的表达式,比如说方法体,字段,创建的类等,之后它会调用ExprEditor
对象中的edit
方法来进行修改。传递给edit
方法的参数是找寻到的表达式对象,然后edit
方法就可以通过此表达式对象来进行替换操作。
通过调用传递给edit
方法的表达式对象中的replace
方法,可以用来替换成给定的的表达式声明或者代码段。如果给定的代码段是空的,那么也就是说,将会执行replace("{}")
方法,那么之前的代码段将会在方法体中被移除。如果你仅仅是想在表达式之前或者之后插入代码段操作,那么你需要将下面的代码段传递给replace
方法:
1 2 3 { before-statements; $_ = $proceed($$); after-statements; }
此代码段可以是方法调用,字段访问,对象创建等等。
再来看看第二行声明:
上面表达式代表着读访问操作,也可以用如下声明来代表写访问操作:
目标表达式中的本地变量是可以通过replace
方法传递到被instrument
方法查找到的代码段中的,如果编译的时候开启了-g
选项的话。
4.2.2 javassist.expr.MethodCall MethodCall
对象代表了一个方法调用,它里面的replace
方法可以对方法调用进行替换,它通过接收准备传递给insertBefore
方法中的以$
开头的识别符号来进行替换操作:
1 2 3 4 5 6 7 8 9 10 11 12 $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
不是字符串,而是特殊的语法,它后面必须利用小括号()
来包上参数列表。
4.2.3 javassist.expr.ConstructorCall 代表构造器调用,比如this()
调用和构造体中的super
调用。其中的replace
方法可以用来替换代码段。它通过接收insertBefore
方法中传入的含有以$
开头的代码段来进行替换操作:
1 2 3 4 5 $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
不是字符串,而是特殊的语法,它后面必须利用小括号()
来包上参数列表。
4.2.4 javassist.expr.FieldAccess 此对象代表着字段访问。ExprEditor
中的edit
方法中如果有字段访问被找到,那么就会接收到这个对象。FieldAccess
中的replace
方法接收待替换的字段。
在代码段中,以$
开头的识别码有如下特殊的含义:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 $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和
$$$都是可用的。如果表达式是可访问的,代码段中,$_
必须被赋值,且$_
的类型就是此字段的类型。
4.2.5 javassist.expr.NewExpr NewExpr
对象代表利用new
操作符来进行对象创建。其edit
方法接收对象创建行为,其replace
方法则可以接收传入的代码段,将现有的对象创建的表达式进行替换。
在代码段中,以$
开头的识别码有如下含义:
1 2 3 4 5 6 7 8 9 $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和
$$$也都是可用的。
4.2.6 javassist.expr.NewArray 此对象表示利用new
操作符进行的数组创建操作。其edit
方法接收数组创建操作的行为,其replace
方法则可以接收传入的代码段,将现有的数组创建的表达式进行替换。
在代码段中,以$
开头的识别码有如下含义:
1 2 3 4 5 6 7 8 $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和
$$$也是可用的。
比如,如果数组创建的表达式如下:
1 String[][] s = new String[3 ][4 ];
那么,$1
和$2
的值将分别为3
和4
,而$3
则是不可用的。
但是,如果数组创建的表达式如下:
1 String[][] s = new String[3 ][];
那么,$1
的值为3
,而$2
是不可用的。
4.2.7 javassist.expr.Instanceof 此对象代表instanceof
表达式。其edit
方法接收instanceof
表达式行为,其replace
方法则可以接收传入的代码段,将现有的表达式进行替换。
在代码段中,以$
开头的识别码有如下含义:
1 2 3 4 5 6 7 8 9 $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 ofthe original instanceof operator. Otherwise, it returns false .
其他的识别码,比如$$w,
$args和
$$$也是可用的。
4.2.8 javassist.expr.Cast 此对象代表显式类型转换。其edit
方法接收显式类型转换的行为,其replace
方法则可以接收传入的代码段,将现有的代码段进行替换。
在代码段中,以$
开头的识别码有如下的含义:
1 2 3 4 5 6 7 8 9 10 $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和
$$$也是可用的。
4.2.9 javassist.expr.Handler 此对象代表try-catch
申明中的catch
子句。其edit
方法接收catch
表达式行为,其insertBefore
方法将接收的代码段进行编译,然后将其插入到catch
子句的开始部分。
在代码段中,以$
开头的识别码有如下的含义:
1 2 3 4 5 $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 representingthe type of the exception caught by the catch clause.
如果一个新的exception
对象被赋值给$1
,那么它将会将此exception
传递给原有的catch
子句并被捕捉。
4.3 添加新方法或字段 4.3.1 添加一个方法 Javassist一开始就允许用户创建新的方法和构造,CtNewMethod
和CtNewConstructor
提供了多种静态工厂方法来创建CtMethod
或者CtConstructor
对象。特别说明一下,其make
方法可以从给定的代码段中创建CtMethod
或者CtContructor
对象。
比如,如下程序:
1 2 3 4 5 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字段。
make
方法中的代码段可以包含以$
开头的识别码,但是setBydy
方法中的$_
除外。如果目标对象和目标方法的名字也传递给了make
方法,那么此方法也可以包含$proceed
。比如:
1 2 3 4 CtClass point = ClassPool.getDefault().get("Point" ); CtMethod m = CtNewMethod.make( "public int ymove(int dy) { $proceed(0, dy); }" , point, "this" , "move" );
上面代码创建如下ymove
方法定义:
1 public int ymove (int dy) { this .move(0 , dy); }
需要注意的是,$proceed
已经被this.move
替换掉了。
Javassist也提供另一种方式来添加新方法,你可以首先创建一个abstract
方法,然后赋予它方法体:
1 2 3 4 5 6 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
状态。
4.3.2 相互递归调用方法 当一个方法调用另一个为添加到操作类中的方法时,Javassist是无法编译此方法的(Javassist可以编译自己调用自己的递归方法)。为了添加相互递归调用的方法到类中,你需要如下的窍门来进行。假设你想添加m和n方法到cc中:
1 2 3 4 5 6 7 8 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
方法并把他们添加到类中。
然后,为方法设置方法体,方法体内部可以实现相互调用。
最后,将类变为非abstract
的,因为addMethod
添加abstract
方法的时候,会自动将类变为abstract
的。
4.3.3 添加字段 Javassist允许用户创建一个新的字段:
1 2 3 CtClass point = ClassPool.getDefault().get("Point" ); CtField f = new CtField(CtClass.intType, "z" , point); point.addField(f);
上面的diam会添加z字段到Point类中。
如果添加的字段需要设定初始值的话,代码需要被改为如下方式来进行:
1 2 3 CtClass point = ClassPool.getDefault().get("Point" ); CtField f = new CtField(CtClass.intType, "z" , point); point.addField(f, "0" );
现在,addField
方法接收了第二个用于计算初始值的参数。此参数可以为任何符合要求的java表达式。需要注意的是,此表达式不能够以分号结束(;)
。
此外,上面的代码可以被重写为如下更简单的方式:
1 2 3 CtClass point = ClassPool.getDefault().get("Point" ); CtField f = CtField.make("public int z = 0;" , point); point.addField(f);
4.3.4 成员移除 为了移除字段或者方法,可以调用CtClass
类中的removeField
或者removeMethod
来进行。而移除CtConstructor
,可以通过调用removeConstructor
方法来进行。
4.4 Annotations CtClass
,CtMethod
,CtField
和CtConstructor
提供了getAnnotations
这个快捷的方法来进行注解的读取操作,它会返回注解类型对象。
比如,如下注解方式:
1 2 3 4 public @interface Author { String name () ; int year () ; }
可以按照如下方式来使用:
1 2 3 4 @Author (name="Chiba" , year=2005 )public class Point { int x, y; }
此时,这些注解的值就可以用getAnnotations方法来获取,此方法将会返回包含了注解类型的对象列表。
1 2 3 4 5 6 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);
上面代码打印结果如下:
由于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 2 3 4 5 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
。
需要注意的是,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 2 3 4 5 6 7 8 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)
。
推荐用户使用#
号分隔符来分隔类名和静态方法或者字段名。比如在java中,正常情况下我们会这么调用:
1 javassist.CtClass.intType.getName()
我们会访问javassist.Ctclass
中的静态字段intType
,然后调用其getName
方法。而在Javassist中,我们可以按照如下的表达式来书写:
1 javassist.CtClass#intType.getName()
这样编译器就能够快速的解析此表达式了。
五 字节码API 为了直接修改类文件,Javassist也提供了底层的API,想使用这些API的话,你需要有良好的Java字节码知识储备和类文件格式的认知,这样,你使用这些API修改类文件的时候,才可以随心所欲而不逾矩。
如果你只是想生成一个简单的类文件,那么javassist.bytecode.ClassFileWriter
类可以做到。它虽然体积小,但是是比javassist.bytecode.ClassFile
更为快速的存在。
5.1 获取ClassFile对象 一个javassist.bytecode.ClassFile
对象就代表着一个类文件,为了获取这个对象,CtClass
中的getClassFile
方法可以做到。如果不想这么做的话,你也可以直接在类文件中构造一个javassist.bytecode.ClassFile
,代码如下:
1 2 3 BufferedInputStream fin = new BufferedInputStream(new FileInputStream("Point.class" )); ClassFile cf = new ClassFile(new DataInputStream(fin));
这个代码片段展示了从Point.class类中创建出一个ClassFile对象出来。
既然可以从类文件中创建出ClassFile
,那么也能将ClassFile
回写到类文件中。ClassFile
中的write
方法就可以将类文件内容回写到给定的DataOutputStream
中。让我们全程展示一下这种做法:
1 2 3 4 5 6 7 8 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" )));
上面的代码生成了Foo.class
这个类文件,它包含了对如下类的扩展:
1 2 3 4 package test;class Foo implements Cloneable { public int width; }
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 2 3 4 ClassFile cf = ... ; MethodInfo minfo = cf.getMethod("move" ); CodeAttribute ca = minfo.getCodeAttribute(); CodeIterator i = ca.iterator();
CodeIterator
对象允许你从前到后挨个访问字节码指令。如下的方法是CodeIterator
中的一部分:
void begin()
移到第一个指令处.
void move(int index)
移到指定索引处
boolean hasNext()
如果存在指令的话,返回true
int next()
返回下一个指令的索引需要注意的是,此方法并不会返回下一个指令的操作码
int byteAt(int index)
返回指定索引处的无符号8bit位长值.
int u16bitAt(int index)
返回指定索引处的无符号16bit位长值.
int write(byte[] code, int index)
在指定索引处写入字节数组.
void insert(int index, byte[] code)
在指定索引处写入字节数组,其他字节码的offset等将会自适应更改。
下面的代码段展示了方法体中的所有指令:
1 2 3 4 5 6 CodeIterator ci = ... ; while (ci.hasNext()) { int index = ci.next(); int op = ci.byteAt(index); System.out.println(Mnemonic.OPCODE[op]); }
5.4 字节码序列的生成 Bytecode对象代表了字节码序列,它是一组在持续不断进行增长的字节码的简称,来看看下面简单的代码片段:
1 2 3 4 5 ConstPool cp = ...; Bytecode b = new Bytecode(cp, 1 , 0 ); b.addIconst(3 ); b.addReturn(CtClass.intType); CodeAttribute ca = b.toCodeAttribute();
代码将会产生如下的序列:
你也可以利用Bytecode
中的get
方法来获取一个字节码数组序列,之后可以将此数组插入到另一个代码段中。
虽然Bytecode
提供了一系列的方法添加特殊的指令到序列中,它同时也提供了addOpcode
方法来添加8bit操作码,提供了addIndex
方法来添加索引。8bit操作码的值是在Opcode
接口中被定义的。
addOpcode
方法和其他添加特殊指令的方法可以自动的维持堆栈的深度,除非操作流程出现了分歧,在这里,我们可以使用Bytecode
的getMaxStack
方法来获取堆栈最大深度。同时,堆栈深度和Bytecode
对象内创建的CodeAtrribute
对象也有关系,为了重新计算方法体中的最大堆栈深度,可以使用CodeAttribute
中的computeMaxStack
来进行。
Bytecode
可以用来构建一个方法,示例如下:
1 2 3 4 5 6 7 8 9 10 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);
上面的代码流程是创建了默认的构造函数后,然后将其添加到cf指向的类中。具体说来就是,Bytecode对象首先被转换成了CodeAttribute对象,接着被添加到minfo所指向的方法中。此方法最终被添加到cf类文件中。
注解在运行时态,作为一个可见或者不可见的属性被保存在类文件中。它们可以从ClassFile
,MethodInfo
或者FieldInfo
对象中通过getAttribute(AnnotationsAttribute.invisibleTag)
方法来获取。更多的谢洁,可以看看javadoc中关于javassist.bytecode.AnnotationsAttribute
类和javassist.bytecode.annotation
包的描述。
Javassist也能够让你利用一些应用层的API来访问注解。只需要利用CtClass
或者CtBehavior
中的的getAnnotations方法接口。
六 泛型 Javassist底层的API可以完全支持Java5中的泛型。另一方面,其更高级别的API,诸如CtClass
是无法直接支持泛型的。对于字节码转换来说,这也不是什么大问题。
Java的泛型,采用的是擦除技术。当编译完毕后,所有的类型参数都将会被擦掉。比如,假设你的源码定义了一个参数类型Vector<String>
:
1 2 3 Vector<String> v = new Vector<String>(); : String s = v.get(0 );
编译后的字节码等价于如下代码:
1 2 3 Vector v = new Vector(); : String s = (String)v.get(0 );
所以,当你写了一套字节码转换器后,你可以移除掉所有的类型参数。由于嵌入在Javassist的编译器不支持泛型,所以利用其编译的时候,你不得不在调用端做显式的类型转换。比如,CtMethod.make
方法。但是如果源码是利用常规的Java编译器,比如javac,来编译的话,是无需进行类型转换的。
如果你有一个类,示例如下:
1 2 3 4 public class Wrapper <T > { T value; public Wrapper (T t) { value = t; } }
想添加Getter<T>
接口到Wrapper<T>
类中:
1 2 3 public interface Getter<T> { T get(); }
那么实际上,你需要添加的接口是Getter
(类型参数<T>
已经被抹除),需要添加到Wrapper
中的方法如下:
1 public Object get() { return value; }
需要注意的是,非类型参数是必须的。由于get
方法返回了Object
类型,那么调用端如果用Javassist编译的话,就需要进行显式类型转换。比如,如下例子,类型参数T
是String
类型,那么(String
)就必须被按照如下方式插入:
1 2 Wrapper w = ... String s = (String)w.get();
当使用常规的Java编译器编译的时候,类型转换是不需要的,因为编译器会自动进行类型转换。
如果你想在运行时态,通过反射来访问类型参数,那么你不得不在类文件中添加泛型符号。更多详细信息,请参阅API文档CtClass
中的setGenericSignature
方法。
七 可变参数 目前,Javassist无法直接支持可变参数。为了让方法可以支持它,你需要显式设置方法修改器,其实很简单,假设你想生成如下的方法:
1 public int length(int... args) { return args.length; }
下面的Javassist代码将会生成如上的方法:
1 2 3 4 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
被添加到了方法修改器中。
为了能够在Javassist编译器中调用此方法,你需要这样来:
1 length(new int [] { 1 , 2 , 3 });
而不是这样来:
八 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
来进行。
九 装箱/拆箱 在Java中,装箱和拆箱操作是语法糖。对于字节码说来,是不存在装箱和拆箱的。所以Javassist的编译器不支持装箱拆箱操作。比如,如下的描述,在java中是可行的:
可以看出,此装箱操作是隐式的。但是在Javassist中,你必须显式的将值类型从int转为Integer:
1 Integer i = new Integer(3);
十 调试 将CtClass.debugDump
设置为目录名称之后,所有被Javassist生成或修改的类文件将会被保存到此目录中。如果不想这么做,可以将CtClass.debugDump
设置为null,需要注意的是,它的默认值就是null。
示例代码:
1 CtClass.debugDump = "./dump";
此时,所有的被修改的类文件将会被保存到./dump
目录中。
十一 javassist使用示例代码 11.1 生成字段和方法 原始代码如下
1 2 3 4 5 6 package com.ssdmbbl.javassist;public class Hello { public Hello () { } }
使用以下代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 package com.ssdmbbl.javassist;import javassist.*;public class JavassistTest2 { public static void main (String[] args) throws Exception { final ClassPool pool = ClassPool.getDefault(); final CtClass makeClass = pool.makeClass("com.ssdmbbl.javassist.Hello" ); final CtField nameField = new CtField(pool.get("java.lang.String" ), "name" , makeClass); nameField.setModifiers(Modifier.PRIVATE); final CtField ageField = CtField.make("private int age;" , makeClass); makeClass.addField(nameField, CtField.Initializer.constant("小明" )); makeClass.addField(ageField, CtField.Initializer.constant(20 )); makeClass.addMethod(CtNewMethod.setter("setName" , nameField)); makeClass.addMethod(CtNewMethod.getter("getName" , nameField)); makeClass.addMethod(CtNewMethod.setter("setAge" , ageField)); makeClass.addMethod(CtNewMethod.getter("getAge" , ageField)); final CtConstructor constructor = new CtConstructor(new CtClass[]{}, makeClass); constructor.setBody("{name=\"老王\";age=30;}" ); makeClass.addConstructor(constructor); final CtConstructor constructor1 = new CtConstructor(new CtClass[]{pool.get("java.lang.String" ), CtClass.intType}, makeClass); constructor1.setBody("{$0.name=$1;$0.age=$2;}" ); makeClass.addConstructor(constructor1); final CtMethod printName = new CtMethod(CtClass.voidType, "printName" , new CtClass[]{}, makeClass); printName.setModifiers(Modifier.PUBLIC); printName.setBody("{System.out.println(name);}" ); final CtMethod printAge = CtMethod.make("public void printAge(){System.out.println(age);}" , makeClass); makeClass.addMethod(printName); makeClass.addMethod(printAge); makeClass.writeFile("./aa" ); } }
VM参数
1 --add-opens=java.base/java.lang=ALL-UNNAMED
生成构造方法时可以用以下代码:
1 2 3 CtConstructor constructor = CtNewConstructor .make("public User(String name, String age) { this.name = name;this.age = age;}" , ctClass);
生成普通方法可以使用以下代码
1 CtMethod ctMethod = CtMethod.make("public int calcute(int a,int b){ return a + b ;}" , ctClass);
运行后,会在aa
目录下生成Hello.class
文件,该文件反编译后的内容为
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 package com.ssdmbbl.javassist;public class Hello { 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 Hello () { this .name = "老王" ; this .age = 30 ; } public Hello (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); } }
11.2 通过反射调用 通过CtClass的toClass方法可以转化成Class对象,然后就可以通过反射的操作来操作目标类的成员了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public class ClassUse { public static void main (String[] args) throws Exception { final ClassPool pool = ClassPool.getDefault(); pool.insertClassPath("./aa" ); final CtClass ctClass = pool.get("com.ssdmbbl.javassist.Hello" ); Object personObj = ctClass.toClass().newInstance(); Method setName = personObj.getClass().getMethod("setName" , String.class ) ; setName.invoke(personObj, "老王" ); Method printName = personObj.getClass().getMethod("printName" ); printName.invoke(personObj); } }
VM参数
1 --add-opens=java.base/java.lang=ALL-UNNAMED
运行代码,得到的结果为
11.3 通过接口调用 以上面生成的class为例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 package com.ssdmbbl.javassist;public class Hello { 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 Hello () { this .name = "老王" ; this .age = 30 ; } public Hello (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); } }
接着新建一个IPerson接口类:
1 2 3 4 5 6 7 8 9 10 package com.demo;public interface IPerson { void setName (String name) ; String getName () ; void printName () ; }
针对以下代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 package com;import com.demo.IPerson;import javassist.ClassPool;import javassist.CtClass;public class JavassistTest2 { public static void main (String[] args) throws Exception { final ClassPool pool = ClassPool.getDefault(); CtClass codeClassI = pool.get("com.demo.IPerson" ); CtClass ctClass = pool.get("com.ssdmbbl.Hello" ); ctClass.setInterfaces(new CtClass[]{codeClassI}); IPerson person = (IPerson) ctClass.toClass().newInstance(); System.out.println(person.getName()); } }
VM参数
1 --add-opens=java.base/java.lang=ALL-UNNAMED
运行结果为
11.4 修改现有的类对象 有如下类对象
1 2 3 4 5 6 7 8 package com.demo; public class PersonService { public void fly() { System.out.println("我飞起来了"); } }
针对以下代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 package com.ssdmbbl.javassist;import javassist.*;public class JavassistTest2 { public static void main (String[] args) throws Exception { final ClassPool pool = ClassPool.getDefault(); final CtClass makeClass = pool.makeClass("com.ssdmbbl.javassist.Hello" ); final CtField nameField = new CtField(pool.get("java.lang.String" ), "name" , makeClass); nameField.setModifiers(Modifier.PRIVATE); final CtField ageField = CtField.make("private int age;" , makeClass); makeClass.addField(nameField, CtField.Initializer.constant("小明" )); makeClass.addField(ageField, CtField.Initializer.constant(20 )); makeClass.addMethod(CtNewMethod.setter("setName" , nameField)); makeClass.addMethod(CtNewMethod.getter("getName" , nameField)); makeClass.addMethod(CtNewMethod.setter("setAge" , ageField)); makeClass.addMethod(CtNewMethod.getter("getAge" , ageField)); final CtConstructor constructor = new CtConstructor(new CtClass[]{}, makeClass); constructor.setBody("{name=\"老王\";age=30;}" ); makeClass.addConstructor(constructor); final CtConstructor constructor1 = new CtConstructor(new CtClass[]{pool.get("java.lang.String" ), CtClass.intType}, makeClass); constructor1.setBody("{$0.name=$1;$0.age=$2;}" ); makeClass.addConstructor(constructor1); final CtMethod printName = new CtMethod(CtClass.voidType, "printName" , new CtClass[]{}, makeClass); printName.setModifiers(Modifier.PUBLIC); printName.setBody("{System.out.println(name);}" ); final CtMethod printAge = CtMethod.make("public void printAge(){System.out.println(age);}" , makeClass); makeClass.addMethod(printName); makeClass.addMethod(printAge); makeClass.writeFile("./aa" ); } }
VM参数
1 --add-opens=java.base/java.lang=ALL-UNNAMED
运行结果为
1 2 3 4 起飞前准备降落伞 我飞起来了 成功落地 加个好友吧
11.5 实现动态代理 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 package com.ssdmbbl.javassist;import com.demo.PersonService;import javassist.ClassPool;import javassist.CtClass;import javassist.util.proxy.ProxyFactory;import javassist.util.proxy.ProxyObject;public class ClassUse { public static void main (String[] args) throws Exception { final ClassPool pool = ClassPool.getDefault(); final CtClass ctClass = pool.get("com.demo.PersonService" ); ProxyFactory factory = new ProxyFactory(); factory.setSuperclass(ctClass.toClass()); factory.setFilter(m -> m.getName() == "fly" ); final Class<?> aClass = factory.createClass(); final PersonService personService = (PersonService) aClass.newInstance(); ((ProxyObject) personService).setHandler((self, thisMethod, proceed, args1) -> { System.out.println((thisMethod.getName() + "被调用前输出" )); try { Object ret = proceed.invoke(self, args1); System.out.println(thisMethod.getName() + "正在调用,返回值: " + ret); return ret; } finally { System.out.println(thisMethod.getName() + "被调用后输出" ); } }); personService.fly(); } }
VM参数
1 --add-opens=java.base/java.lang=ALL-UNNAMED
运行结果为
1 2 3 4 fly被调用前输出 我飞起来了 fly正在调用,返回值: null fly被调用后输出
十二 重新生成类的字节码文件 原始类
1 2 3 4 5 6 7 8 9 10 11 package com.demo;public class Person { public int hello (String s) { return s.length(); } public String hello2 (String s) { return s; } }
12.1 给类添加方法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 package com.ssdmbbl.javassist;import javassist.*;public class ClassUse { public static void main (String[] args) throws Exception { ClassPool pool = ClassPool.getDefault(); CtClass cc = pool.get("com.demo.Person" ); CtMethod cm1 = CtMethod.make("public int add1(int a, String b){return a+b.length();}" , cc); cc.addMethod(cm1); CtClass[] parameters = new CtClass[]{CtClass.intType, pool.get("java.lang.String" )}; CtMethod cm2 = new CtMethod(CtClass.intType, "add2" , parameters, cc); cm2.setModifiers(Modifier.PUBLIC); cm2.setBody("{return $1+$2.length();}" ); cc.addMethod(cm2); cc.writeFile("./demo" ); } }
输出结果反编译为
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 package com.demo;public class Person { public Person () { } public int hello (String s) { return s.length(); } public String hello2 (String s) { return s; } public int add1 (int var1, String var2) { return var1 + var2.length(); } public int add2 (int var1, String var2) { return var1 + var2.length(); } }
12.2 给类添加属性 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 package com.ssdmbbl.javassist;import javassist.*;public class ClassUse { public static void main (String[] args) throws Exception { ClassPool pool = ClassPool.getDefault(); CtClass cc = pool.get("com.demo.Person" ); CtField cf = new CtField(CtClass.intType, "age" , cc); cf.setModifiers(Modifier.PRIVATE); cc.addField(cf); cc.addMethod(CtNewMethod.getter("getAge" , cf)); cc.addMethod(CtNewMethod.setter("setAge" , cf)); CtField cf2 = CtField.make("private String name;" , cc); cc.addField(cf2); cc.addMethod(CtNewMethod.getter("getName" , cf2)); cc.addMethod(CtNewMethod.setter("setName" , cf2)); cc.writeFile("./demo" ); } }
输出结果反编译为
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 package com.demo;public class Person { private int age; private String name; public Person () { } public int hello (String s) { return s.length(); } public String hello2 (String s) { return s; } public int getAge () { return this .age; } public void setAge (int var1) { this .age = var1; } public String getName () { return this .name; } public void setName (String var1) { this .name = var1; } }
12.3 修改类的方法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 package com.ssdmbbl.javassist;import javassist.*;public class ClassUse { public static void main (String[] args) throws Exception { ClassPool pool = ClassPool.getDefault(); CtClass cc = pool.get("com.demo.Person" ); CtMethod cm = cc.getDeclaredMethod("hello" , new CtClass[]{pool.get("java.lang.String" )}); cm.insertBefore("System.out.println(\"调用前2\");" ); cm.insertBefore("System.out.println(\"调用前1\");" ); cm.insertAt(20094 , "System.out.println(\"在指定行插入代码\");" ); cm.insertAfter("System.out.println(\"调用后1\");" ); cm.insertAfter("System.out.println(\"调用后2\");" ); CtMethod cm2 = cc.getDeclaredMethod("hello2" , new CtClass[]{pool.get("java.lang.String" )}); cm2.setBody("{" + "if ($1 == null) {\n" + "\treturn \"\";\n" + "}\n" + "return \"你好:\" + $1;" + "}" ); cc.writeFile("./demo" ); } }
输出结果反编译为
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 package com.demo;public class Person { public Person () { } public int hello (String s) { System.out.println("调用前1" ); System.out.println("调用前2" ); System.out.println("在指定行插入代码" ); int var3 = s.length(); System.out.println("调用后1" ); System.out.println("调用后2" ); return var3; } public String hello2 (String s) { return s == null ? "" : "你好:" + s; } }
12.4 修改注解的值 原始类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 package com.demo;import javax.persistence.Column;public class DemoPerson { @Column (name = "AGE" ) public int age = 28 ; }
修改代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 package com.ssdmbbl.javassist;import javassist.ClassPool;import javassist.CtClass;import javassist.CtField;import javassist.bytecode.AnnotationsAttribute;import javassist.bytecode.ConstPool;import javassist.bytecode.FieldInfo;import javassist.bytecode.annotation.Annotation;import javassist.bytecode.annotation.StringMemberValue;import javax.persistence.Column;public class ClassUse { public static void main (String[] args) throws Exception { ClassPool pool = ClassPool.getDefault(); CtClass cc = pool.get("com.demo.DemoPerson" ); CtField cf = cc.getField("age" ); FieldInfo fInfo = cf.getFieldInfo(); ConstPool cp = cc.getClassFile().getConstPool(); Annotation annotation = new Annotation(Column.class .getName (), cp ) ; annotation.addMemberValue("name" , new StringMemberValue("_AGE" , cp)); AnnotationsAttribute annotationsAttribute = new AnnotationsAttribute(cp, AnnotationsAttribute.visibleTag); annotationsAttribute.setAnnotation(annotation); fInfo.addAttribute(annotationsAttribute); cc.writeFile("./demo" ); } }
输出结果反编译后为
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package com.demo;import javax.persistence.Column;public class DemoPerson { @Column ( name = "_AGE" ) public int age = 28 ; public DemoPerson () { } }
12.5 修改已加载过的类 原始类
1 2 3 4 5 6 7 package com.demo;public class Person { public void hello (String s) { System.out.println(s); } }
使用以下代码
1 2 3 4 5 6 public class ClassUse { public static void main (String[] args) throws Exception { new Person().hello("包青天" ); ClassPool.getDefault().get("com.demo.Person" ).toClass(); } }
运行结果如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 包青天 Exception in thread "main" javassist.CannotCompileException: by java.lang.reflect.InvocationTargetException at javassist.util.proxy.DefineClassHelper$JavaOther .defineClass(DefineClassHelper.java:220) at javassist.util.proxy.DefineClassHelper$Java11 .defineClass(DefineClassHelper.java:52) at javassist.util.proxy.DefineClassHelper.toClass(DefineClassHelper.java:260) at javassist.ClassPool.toClass(ClassPool.java:1232) at javassist.ClassPool.toClass(ClassPool.java:1090) at javassist.ClassPool.toClass(ClassPool.java:1048) at javassist.CtClass.toClass(CtClass.java:1290) at com.ssdmbbl.javassist.ClassUse.main(ClassUse.java:14) Caused by: java.lang.reflect.InvocationTargetException at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77) at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.base/java.lang.reflect.Method.invoke(Method.java:568) at javassist.util.proxy.DefineClassHelper$JavaOther .defineClass(DefineClassHelper.java:214) ... 7 more Caused by: java.lang.LinkageError: loader 'app' attempted duplicate class definition for com.demo.Person. (com.demo.Person is in unnamed module of loader 'app' ) at java.base/java.lang.ClassLoader.defineClass1(Native Method) at java.base/java.lang.ClassLoader.defineClass(ClassLoader.java:1012) ... 12 more
解决方法:指定一个未加载的 ClassLoader,为了方便,Javassist 也提供一个 Classloader 供使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 package com.ssdmbbl.javassist;import com.demo.Person;import javassist.*;public class ClassUse { public static void main (String[] args) throws Exception { new Person().hello("包青天" ); ClassPool pool = ClassPool.getDefault(); CtClass cc = pool.get("com.demo.Person" ); CtMethod cm = cc.getDeclaredMethod("hello" , new CtClass[]{pool.get("java.lang.String" )}); cm.setBody("{" + "System.out.println(\"你好鸭:\" + $1);" + "}" ); cc.writeFile("./demo" ); Translator translator = new Translator() { @Override public void start (ClassPool classPool) throws NotFoundException, CannotCompileException { System.out.println("start" ); } @Override public void onLoad (ClassPool classPool, String paramString) throws NotFoundException, CannotCompileException { System.out.println("onLoad:" + paramString); new Person().hello("张三飞111" ); } }; Loader classLoader = new Loader(pool); classLoader.addTranslator(pool, translator); Class clazz = classLoader.loadClass("com.demo.Person" ); new Person().hello("张思飞222" ); clazz.getDeclaredMethod("hello", String.class).invoke(clazz.newInstance(), "调用的是新类的方法"); Class clazz2 = Class.forName("com.demo.Person" ); clazz2.getDeclaredMethod("hello", String.class).invoke(clazz2.newInstance(), "调用原始类的方法"); } }
运行结果
1 2 3 4 5 6 7 包青天 start onLoad:com.demo.Person 张三飞111 张思飞222 你好鸭:调用的是新类的方法 调用原始类的方法
12.6 修改未加载过的类 1 2 3 4 5 6 7 8 9 ClassPool pool = ClassPool.getDefault(); CtClass cc = pool.get("com.demo.Person" ); CtMethod cm = cc.getDeclaredMethod("hello" , new CtClass[] { pool.get("java.lang.String" ) }); cm.setBody("{" + "System.out.println(\"你好:\" + $1);" + "}" ); cc.writeFile("./demo" ); cc.toClass(); new Person().hello("包青天" );
12.7 获取类基本信息 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 ClassPool pool = ClassPool.getDefault(); CtClass cc = pool.get("com.demo.Person" ); byte [] bytes = cc.toBytecode();System.out.println(bytes.length); System.out.println(cc.getName()); System.out.println(cc.getSimpleName()); System.out.println(cc.getSuperclass().getName()); System.out.println(Arrays.toString(cc.getInterfaces())); for (CtConstructor con : cc.getConstructors()) { System.out.println("构造方法 " +con.getLongName()); } for (CtMethod method : cc.getMethods()) { System.out.println(method.getLongName()); }
打印内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 562 com.demo.Person Person java.lang.Object [] 构造方法 com.demo.Person() java.lang.Object.equals(java.lang.Object) java.lang.Object.finalize() com.demo.Person.hello2(java.lang.String) java.lang.Object.toString() java.lang.Object.getClass() java.lang.Object.notifyAll() java.lang.Object.hashCode() java.lang.Object.wait() java.lang.Object.notify() com.demo.Person.hello(java.lang.String) java.lang.Object.wait(long) java.lang.Object.wait(long,int) java.lang.Object.clone()
获取注解信息
1 2 3 Object[] annotations = cf.getAnnotations(); //获取类、方法、字段等上面定义的注解信息 SerializedName annotation = (SerializedName) annotations[0]; //遍历判断注解类型 System.out.println(annotation.value()); //获取注解的值
12.8 创建一个新类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 ClassPool pool = ClassPool.getDefault(); CtClass cc = pool.makeClass("com.demo.Person" ); cc.addField(CtField.make("private int id;" , cc)); cc.addField(CtField.make("private String name;" , cc)); cc.addMethod(CtMethod.make("public String getName(){return name;}" , cc)); cc.addMethod(CtMethod.make("public void setName(String name){this.name = name;}" , cc)); CtConstructor constructor = new CtConstructor(new CtClass[] { CtClass.intType, pool.get("java.lang.String" ) }, cc); constructor.setBody("{this.id=id;this.name=name;}" ); cc.addConstructor(constructor); cc.writeFile("./demo" );
输出结果反编译后的信息为
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 package com.demo;public class Person { private int id; private String name; public String getName () { return this .name; } public void setName (String var1) { this .name = var1; } public Person (int var1, String var2) { this .id = this .id; this .name = this .name; } }
12.9 使用 Javassist 解析方法信息 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 ClassPool classPool = new ClassPool(true ); classPool.insertClassPath(new ClassClassPath(this .getClass())); CtClass ctClass = classPool.get(classMetadata.getClassName()); if (ctClass.isInterface() || ctClass.isAnnotation() || ctClass.isEnum() || ctClass.isPrimitive() || ctClass.isArray() || ctClass.getSimpleName().contains("$" )) { return ; } CtMethod[] methods = ctClass.getDeclaredMethods(); for (CtMethod method : methods) { if (method.getName().contains("$" )) { continue ; } String packageName = ctClass.getPackageName(); String className = ctClass.getSimpleName(); String methodName = method.getName(); String methodSignature = StringUtils.defaultIfBlank(StringUtils.substringBetween(method.getLongName(), "(" , ")" ), null ); }
十三 springboot整合javassist 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 package com.yishuifengxiao.app.file_browser.support;import org.aspectj.lang.JoinPoint;import org.aspectj.lang.annotation.AfterReturning;import org.aspectj.lang.annotation.Aspect;import org.aspectj.lang.annotation.Before;import org.aspectj.lang.annotation.Pointcut;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.stereotype.Component;import com.alibaba.fastjson.JSON;import javassist.ClassClassPath;import javassist.ClassPool;import javassist.CtClass;import javassist.CtMethod;import javassist.Modifier;import javassist.NotFoundException;import javassist.bytecode.CodeAttribute;import javassist.bytecode.LocalVariableAttribute;import javassist.bytecode.MethodInfo;@Aspect @Component public class MethodLogAop { @Pointcut ("(execution(* com.yishuifengxiao.app.file_browser.service..*.*(..)))" ) public void webLog () { } @Before ("webLog()" ) public void doBefore (JoinPoint joinPoint) throws Throwable { String classType = joinPoint.getTarget().getClass().getName(); Class<?> clazz = Class.forName(classType); String clazzName = clazz.getName(); System.out.println(" doBefore 类名:" + clazzName); String methodName = joinPoint.getSignature().getName(); System.out.println(" doBefore 方法名:" + methodName); String[] paramNames = getFieldsName(this .getClass(), clazzName, methodName); Object[] args = joinPoint.getArgs(); for (int k = 0 ; k < args.length; k++) { System.out.println(" doBefore 参数名:" + paramNames[k] + ",参数值:" + JSON.toJSONString(args[k])); } } private static String[] getFieldsName(Class<?> cls, String clazzName, String methodName) throws NotFoundException { ClassPool pool = ClassPool.getDefault(); ClassClassPath classPath = new ClassClassPath(cls); pool.insertClassPath(classPath); CtClass cc = pool.get(clazzName); CtMethod cm = cc.getDeclaredMethod(methodName); MethodInfo methodInfo = cm.getMethodInfo(); CodeAttribute codeAttribute = methodInfo.getCodeAttribute(); LocalVariableAttribute attr = (LocalVariableAttribute) codeAttribute.getAttribute(LocalVariableAttribute.tag); String[] paramNames = new String[cm.getParameterTypes().length]; int pos = Modifier.isStatic(cm.getModifiers()) ? 0 : 1 ; for (int i = 0 ; i < paramNames.length; i++) { paramNames[i] = attr.variableName(i + pos); } return paramNames; } @AfterReturning (returning = "ret" , pointcut = "webLog()" ) public void doAfterReturning (JoinPoint joinPoint, Object ret) throws Throwable { String classType = joinPoint.getTarget().getClass().getName(); Class<?> clazz = Class.forName(classType); String clazzName = clazz.getName(); System.out.println("doAfterReturning 类名:" + clazzName); String methodName = joinPoint.getSignature().getName(); System.out.println("doAfterReturning 方法名:" + methodName); System.out.println("doAfterReturning 返回值 : " + JSON.toJSONString(ret)); } }
运行结果为:
1 2 3 4 5 6 7 doBefore 类名:com.yishuifengxiao.app.file_browser.service.impl.UserServiceImpl doBefore 方法名:findUserByUsername doBefore 参数名:username,参数值:"admin" doAfterReturning 类名:com.yishuifengxiao.app.file_browser.service.impl.UserServiceImpl doAfterReturning 方法名:findUserByUsername doAfterReturning 返回值 : {"id":"1","name":"系统超级管理员","pwd":"473A1E15AEB92A70","role":"role","stat":1,"username":"admin"}
execution语法
语法表达式: execution(<修饰符> <返回类型> <类路径> <方法名>(<参数列表>) <异常模式> )
其中,修饰符和异常是可选的,如果不加类路径,则默认对所有的类生效。
常用实例:
1.通过方法签名、返回值定义切点:
execution(public * *Service(..))
:定位于所有类下返回值任意、方法入参类型、数量任意,public类型的方法
execution(public String *Service(..))
:定位于所有类下返回值为String、方法入参类型、数量任意,public类型的方法
2.通过类包定义切点:
execution(* com.yc.controller.BaseController+.*(..))
:匹配任意返回类型,对应包下BaseController类及其子类等任意方法。
execution(* com.*.(..))
:匹配任意返回类型,com包下所有类的所有方法
execution(* com..*.(..))
:匹配任意返回类型,com包、子包下所有类的所有方法
(注意:.
表示该包下所有类,..
则涵括其子包。)
3.通过方法入参定义切点
(这里*
表示任意类型的一个参数,..
表示任意类型任意数量的参数)
execution(* speak(Integer,*))
:匹配任意返回类型,所有类中只有两个入参,第一个入参为Integer,第二个入参任意的方法
execution(* speak(..,Integer,..))
:匹配任意返回类型,所有类中至少有一个Integer入参,但位置任意的方法。