javassist基础以及如何使用javassist实现AOP

我们知道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所代表的类文件。

定义新类

javassist基础以及如何使用javassist实现AOP_第1张图片

将类冻结

如果一个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 异常类型

javassist基础以及如何使用javassist实现AOP_第2张图片

修改方法体

CtMethod和CtConstructor提供setBody()来替换整个方法体。他将新的源代码编译成Java字节码,并用它替换原方法体。如果给定的源文本为null,则替换后的方法体仅包含返回语句,返回零或空值,除非结果类型为void。在传递给setBody()的源代码中,以$开头的标识符和上面insertBefore、insertAfter和addCatch方法一样有特殊含义,但注意$_不可用。

javassist基础以及如何使用javassist实现AOP_第3张图片

注解 (Annotations)

CtClass,CtMethod,CtField和CtConstructor 提供获取注解的方法getAnnotations()用于读取注解。它返回一个注解类型的对象(但这种不好用,返回的是一个object类型的代理对象)。

javassist基础以及如何使用javassist实现AOP_第4张图片

对注解的查询和修改比较好的实现方式如下:

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();
 }
}

 

你可能感兴趣的:(javassist)