我们知道Java字节码以二进制的形式存储在class文件中,每一个class文件包含一个Java类或接口。Javaassist 就是一个用来处理Java字节码的类库。在Javassist 中,类CtClass表示class文件。
我们可以用javassist类库实现动态创建类、添加类的属性和方法、设置类的父类,以及修改类的方法等操作。Javassist不允许删除方法或字段,但它允许更改名称。所以,如果一个方法是没有必要的,可以通过调用CtMethod的setName和setModifiers中将其改为一个私有方法。Javassist不允许向现有方法添加额外的参数。你可以通过新建一个方法达到同样的效果。
ClassPool pool = ClassPool.getDefault(); CtClass cc = pool.get("com.javassit.test.Student"); cc.setSuperclass(pool.get("com.javassit.test.Person")); cc.writeFile(); |
这段代码首先获取一个ClassPool对象,ClassPool是一个存储 CtClass的 Hash 表,类的名称为key。ClassPool的get函数用于从Hash表中查找key对应的CtClass对象。如果没有找到,get() 函数会创建并返回一个新的CtClass对象,这个新对象会保存在Hash表中。
调用writeFile()后,这项修改会被写入原始类文件,这个方法会将CtClass对象转换成类文件并写到本地磁盘。也可以使用 byte[] b = cc.toBytecode()来获取修改过的字节码,还也可以通过Class clazz = cc.toClass()直接将CtClass转换成Class对象。toClass请求当前线程的ClassLoader加载CtClass所代表的类文件。
定义新类
将类冻结
如果一个CtClass对象通过 writeFile(), toClass(), toBytecode() 被转换成一个类文件,此CtClass 对象会被冻结起来,不允许再修改。因为一个类只能被 JVM 加载一次。但是,一个冷冻的CtClass也可以通过defrost方法进行解冻,解冻完成后又可以被修改了。
类搜索路径
通过ClassPool.getDefault()获取的ClassPool使用 JVM 的类搜索路径。如果程序运行在JBoss或者Tomcat等 Web 服务器上,ClassPool可能无法找到用户的类,因为 Web 服务器使用多个类加载器作为系统类加载器。在这种情况下,ClassPool 必须添加额外的类搜索路径。
pool.insertClassPath(new ClassClassPath(Person.getClass())); |
上面的语句将Person类添加到pool的类加载路径中。但在实践中,我发现通过这个可以将Person类所在的整个jar包添加到类加载路径中。
也可以注册一个目录作为类搜索路径。如pool.insertClassPath("/usr/javalib");则是将 /usr/javalib目录添加到类搜索路径中。
类搜索路径还可以是 URL :
ClassPool pool = ClassPool.getDefault(); ClassPath cp = new URLClassPath("www.123.com", 80, "/java/", "org.javassist."); pool.insertClassPath(cp); |
上述代码将http://www.123.com:80/java添加到类搜索路径。并且这个URL只能搜索 org.javassist 包里面的类。例如,为了加载 org.javassist.test.Main,它的类文件会从获取http://www.123.com:80/java/org/javassist/test/Main.class获取。
此外,也可以直接传递一个byte数组给ClassPool来构造一个 CtClass对象来完成这项操作,需要使用ByteArrayPath类。示例:
ClassPool cp = ClassPool.getDefault(); byte[] b = a byte array; String name = class name; cp.insertClassPath(new ByteArrayClassPath(name, b)); CtClass cc = cp.get(name); |
示例中的 CtClass 对象表示字节数据b代表的class文件。将对应的类名传递给ClassPool的get()方法,就可以从字节数组中读取到对应的类文件。
如果你不知道类的全名,可以使用makeClass()方法:
ClassPool cp = ClassPool.getDefault(); InputStream ins = an input stream for reading a class file; CtClass cc = cp.makeClass(ins); |
makeClass()返回从给定输入流构造的CtClass对象。你可以使用makeClass()将类文件提供给ClassPool对象。如果搜索路径包含大的jar文件,这可能会提高性能。由于ClassPool对象按需读取类文件,它可能会重复搜索整个jar文件中的每个类文件。makeClass()可以用于优化此搜索。由makeClass()构造的CtClass保存在ClassPool对象中,从而使得类文件不会再被读取。
在方法体的开始、结尾处添加代码或者增加try、catch代码块
CtMethod和CtConstructor提供了insertBefore、insertAfter和addCatch方法。它们可以将用 Java 编写的代码片段插入到现有方法中。insertAt方法可以将编译后的代码插入到指定行号位置。这几个方法接收一个表示语句或语句块的 String 对象。
传递给方法这几个方法的String对象是由Javassist编译器编译的。由于编译器支持语言扩展,以 $ 开头的几个标识符有特殊的含义:
符号 | 含义 |
$0, $1, $2, ... | $0=this,$1表示方法的第一个参数,依次类推, 如果方法是静态的,则 $0 不可用 |
$args | 方法参数数组.它的类型为 Object[],$args[0]=$1,$args[1]=$2 |
$r | 返回结果的类型,用于强制类型转换 |
$w | 包装器类型,用于强制类型转换 |
$_ | 返回值,一般在insertAfter中用到 |
$sig | 参数类型数组,$sig[0]表示第一个参数类型 |
$type | 返回值类型,即$_的类型 |
$class | $0或this的类型 |
$e | 异常类型 |
修改方法体
CtMethod和CtConstructor提供setBody()来替换整个方法体。他将新的源代码编译成Java字节码,并用它替换原方法体。如果给定的源文本为null,则替换后的方法体仅包含返回语句,返回零或空值,除非结果类型为void。在传递给setBody()的源代码中,以$开头的标识符和上面insertBefore、insertAfter和addCatch方法一样有特殊含义,但注意$_不可用。
注解 (Annotations)
CtClass,CtMethod,CtField和CtConstructor 提供获取注解的方法getAnnotations()用于读取注解。它返回一个注解类型的对象(但这种不好用,返回的是一个object类型的代理对象)。
对注解的查询和修改比较好的实现方式如下:
package net.thinktrader.fundta.test.controller.javassist.ann; import javax.persistence.EntityManager; import javax.persistence.Id; import javax.persistence.PersistenceContext; import org.springframework.stereotype.Component; import org.springframework.stereotype.Service; import javassist.ClassPool; import javassist.CtClass; import javassist.CtConstructor; import javassist.CtField; import javassist.CtMethod; import javassist.NotFoundException; import javassist.bytecode.AnnotationsAttribute; import javassist.bytecode.ClassFile; import javassist.bytecode.ConstPool; import javassist.bytecode.FieldInfo; import javassist.bytecode.MethodInfo; import javassist.bytecode.annotation.Annotation; import javassist.bytecode.annotation.StringMemberValue; @Service(value = "service") @Component(value = "service") public class CollectionBase { @PersistenceContext(unitName = "em-field") @Id protected EntityManager em; @Id @PersistenceContext(unitName = "em-method") protected EntityManager getEntityManager() { return this.em; } @Deprecated public CollectionBase() { } public static void main(String[] args) throws Exception { readAndUpdateClassAnnocation(); readFieldAndMethodAnnocation(); updateFieldAndMethodAnnocation(); } public static void readAndUpdateClassAnnocation() throws Exception { ClassPool pool = ClassPool.getDefault(); // 获取要修改的类的所有信息 CtClass ct = pool.get("net.thinktrader.fundta.test.controller.javassist.ann.CollectionBase"); ClassFile cf = ct.getClassFile2(); disPlayClassAnnotation(cf, "org.springframework.stereotype.Component", "value"); AnnotationsAttribute attribute = (AnnotationsAttribute) cf.getAttribute(AnnotationsAttribute.visibleTag); ConstPool cp = cf.getConstPool(); Annotation annotation = new Annotation("org.springframework.stereotype.Component", cp); // 修改名称为unitName的注解 annotation.addMemberValue("value", new StringMemberValue("em-update", cp)); attribute.addAnnotation(annotation); cf.addAttribute(attribute); disPlayClassAnnotation(cf, "org.springframework.stereotype.Component", "value"); } public static void updateFieldAndMethodAnnocation() throws NotFoundException { ClassPool pool = ClassPool.getDefault(); // 获取需要修改的类 CtClass ct = pool.get("net.thinktrader.fundta.test.controller.javassist.ann.CollectionBase"); // 获取类里的em属性 CtField cf = ct.getField("em"); FieldInfo fieldInfo = cf.getFieldInfo(); // 获取类里的方法 CtMethod[] cms = ct.getDeclaredMethods("getEntityManager"); // 获取第一个方法(因为该方法没有重载) CtMethod cm = cms[0]; MethodInfo minInfo = cm.getMethodInfo(); ConstPool cp = fieldInfo.getConstPool(); // 获取注解信息 AnnotationsAttribute attribute2 = new AnnotationsAttribute(cp, AnnotationsAttribute.visibleTag); Annotation annotation = new Annotation("javax.persistence.PersistenceContext", cp); // 修改名称为unitName的注解 annotation.addMemberValue("unitName", new StringMemberValue("em-update", cp)); attribute2.setAnnotation(annotation); minInfo.addAttribute(attribute2); fieldInfo.addAttribute(attribute2); disPlayFieldAnnotation(fieldInfo, "javax.persistence.PersistenceContext", "unitName"); disPlayMethodAnnotation(minInfo, "javax.persistence.PersistenceContext", "unitName"); } public static void readFieldAndMethodAnnocation() throws NotFoundException { ClassPool pool = ClassPool.getDefault(); // 获取要修改的类的所有信息 CtClass ct = pool.get("net.thinktrader.fundta.test.controller.javassist.ann.CollectionBase"); // 获取类里的em属性 CtField cf = ct.getField("em"); // 获取属性信息 FieldInfo fieldInfo = cf.getFieldInfo(); // 获取类中的方法 CtMethod[] cms = ct.getDeclaredMethods("getEntityManager"); // 获取第一个方法(因为该方法没有重载) CtMethod cm = cms[0]; // 获取方法信息 MethodInfo methodInfo = cm.getMethodInfo(); // 构造方法 CtConstructor[] structs = ct.getConstructors(); for (CtConstructor s : structs) { MethodInfo mi = s.getMethodInfo(); disPlayMethodAnnotation(mi, "java.lang.Deprecated", ""); } disPlayFieldAnnotation(fieldInfo, "javax.persistence.PersistenceContext", "unitName"); disPlayMethodAnnotation(methodInfo, "javax.persistence.PersistenceContext", "unitName"); } public static void disPlayFieldAnnotation(FieldInfo fieldInfo, String annotationName, String annotationValue) { System.out.println("~~~~~~~~~~~~~~~~~~~~~"); System.out.println("属性名称:" + fieldInfo.getName()); // 获取注解属性 AnnotationsAttribute attribute = (AnnotationsAttribute) fieldInfo.getAttribute(AnnotationsAttribute.visibleTag); System.out.println("属性所有注解:" + attribute); // 获取某种类型的注解 Annotation annotation = attribute.getAnnotation(annotationName); System.out.println("某种类型注解:" + annotation); // 获取注解中某个选项的值 String text = ((StringMemberValue) annotation.getMemberValue(annotationValue)).getValue(); System.out.println("注解某选项的值:" + text); } public static void disPlayMethodAnnotation(MethodInfo methodInfo, String annotationName, String annotationValue) { System.out.println("~~~~~~~~~~~~~~~~~~~~~"); System.out.println("方法名称:" + methodInfo.getName()); // 获取注解属性 AnnotationsAttribute attribute = (AnnotationsAttribute) methodInfo .getAttribute(AnnotationsAttribute.visibleTag); System.out.println("方法所有注解:" + attribute); // 获取某种类型的注解 Annotation annotation = attribute.getAnnotation(annotationName); System.out.println("方法某种类型注解:" + annotation); // 获取注解中某个选项的值 String text = ((StringMemberValue) annotation.getMemberValue(annotationValue)).getValue(); System.out.println("方法注解某选项的值:" + text); } public static void disPlayClassAnnotation(ClassFile cf, String annotationName, String annotationValue) { System.out.println("~~~~~~~~~~~~~~~~~~~~~"); AnnotationsAttribute attribute = (AnnotationsAttribute) cf.getAttribute(AnnotationsAttribute.visibleTag); System.out.println("类上的所有注解:" + attribute); // 获取某种类型的注解 Annotation annotation = attribute.getAnnotation(annotationName); System.out.println("类上的某个注解:" + annotation); // 获取注解中某个选项的值 String text = ((StringMemberValue) annotation.getMemberValue(annotationValue)).getValue(); System.out.println("类上的某个注解某选项的值:" + text); } } |
高级接口实现AOP操作
用javassist实现AOP相关操作的方法很多,如前面介绍到的insertBefore、insertAfter、addCatch方法外,javassist还提供了一个高级接口来实现AOP,代码如下:
package net.thinktrader.fundta.test.controller.javassist; import java.lang.reflect.Method; import javassist.ClassPool; import javassist.CtClass; import javassist.util.proxy.MethodFilter; import javassist.util.proxy.MethodHandler; import javassist.util.proxy.ProxyFactory; import javassist.util.proxy.ProxyObject; public class Test { public static void main(String[] args) throws Exception { ClassPool pool = ClassPool.getDefault(); //导入包 pool.importPackage("java.util.ArrayList"); CtClass cc = pool.get("net.thinktrader.fundta.test.controller.javassist.Point"); // 实例化代理类工厂 ProxyFactory factory = new ProxyFactory(); //设置父类,ProxyFactory将会动态生成一个类,继承该父类 factory.setSuperclass(cc.toClass()); //设置过滤器,判断哪些方法调用需要被拦截 factory.setFilter(new MethodFilter() { @Override public boolean isHandled(Method m) { return m.getName().startsWith("set"); } }); //创建代理类型 Class> clazz = factory.createClass(); //创建代理实例 Point proxy = (Point) clazz.newInstance(); //设置代理处理方法 ((ProxyObject)proxy).setHandler(new MethodHandler() { @Override public Object invoke(Object self, Method thisMethod, Method proceed, Object[] args) throws Throwable { //实际情况可根据需求修改 System.out.println(thisMethod.getName() + "被调用"); try { //thisMethod为被代理方法 proceed为代理方法 self为代理实例 args为方法参数 Object ret = proceed.invoke(self, args); System.out.println("返回值: " + ret); return ret; } finally { System.out.println(thisMethod.getName() + "调用完毕"); } } }); //测试 proxy.setX(10); proxy.getX(); } } |