前言
一般常见的动态方法调用使用Reflection或者字节码生成技术。虽然JDK已对反射进行了优化但在追求性能的场景中仍然显得性能不佳。本文即是介绍一个面向程序员友好的字节码操作类库javassist。根据benchmark其展现的性能已几乎无异于直接调用。
开源地址:javassist,简单地看一下官方介绍:
Javassist 使 Java 字节码操作变得简单。它是一个用于在 Java 中编辑字节码的类库。它使 Java 程序可以在运行时定义新类,并在 JVM 加载它时修改类文件。与其他类似的字节码编辑器不同,Javassist 提供了两个级别的 API:源级别和字节代码级别。如果用户使用源代码级 API,则他们可以在不了解 Java 字节码规范的情况下编辑类文件。整个 API 仅使用 Java 语言的词汇表进行设计。您甚至可以以源文本的形式指定插入的字节码。Javassist 可以即时对其进行编译。另一方面,字节码级别的 API 允许用户像其他编辑器一样直接编辑类文件。
说得直白一点就是,我们可以通过它的API来生成我们想要的字节码。下面演示如何进行使用。
正文
生成
首先需要引入Jar包,仓库地址为:
<dependency><groupId>org.javassist</groupId><artifactId>javassist</artifactId><version>3.23.1-GA</version>
</dependency>
为了演示,我们直接创建一个main函数,用来构建一个不存在的对象。
public class JavassistInvoker {public static void main(String[] args) throws Exception {//创建类,这是一个单例对象ClassPool pool = ClassPool.getDefault();//我们需要构建的类CtClass ctClass = pool.makeClass("io.github.pleuvoir.prpc.invoker.Person");//新增字段CtField field$name = new CtField(pool.get("java.lang.String"), "name", ctClass);//设置访问级别field$name.setModifiers(Modifier.PRIVATE);//也可以给个初始值ctClass.addField(field$name, CtField.Initializer.constant("pleuvoir"));//生成get/set方法ctClass.addMethod(CtNewMethod.setter("setName", field$name));ctClass.addMethod(CtNewMethod.getter("getName", field$name));//新增构造函数//无参构造函数CtConstructor cons$noParams = new CtConstructor(new CtClass[]{}, ctClass);cons$noParams.setBody("{name = \"pleuvoir\";}");ctClass.addConstructor(cons$noParams);//有参构造函数CtConstructor cons$oneParams = new CtConstructor(new CtClass[]{pool.get("java.lang.String")}, ctClass);// $0=this $1,$2,$3... 代表方法参数cons$oneParams.setBody("{$0.name = $1;}");ctClass.addConstructor(cons$oneParams);// 创建一个名为 print 的方法,无参数,无返回值,输出name值CtMethod ctMethod = new CtMethod(CtClass.voidType, "print", new CtClass[]{}, ctClass);ctMethod.setModifiers(Modifier.PUBLIC);ctMethod.setBody("{System.out.println(name);}");ctClass.addMethod(ctMethod);//当前工程的target目录final String targetClassPath = Thread.currentThread().getContextClassLoader().getResource("").toURI().getPath();//生成.class文件ctClass.writeFile(targetClassPath);}
}
可以看到生成的文件如下:
需要注意的是,如果我们大量地使用该方法生成类,可能会造成内存压力。因为ClassPool中存在哈希表用来缓存生成的CtClass对象。我们可以通过调用CtClass#detach()方法清除缓存。
到此为止,我们的文件是生成了。需要如何调用呢?难道是通过ClassLoader加载吗?接着往下看。
调用
反射调用
有两种常用的方式,调用toClass()方法或者读取.class文件。
修改如上的方法:
这里可以看到已经获得了我们期待的对象,但是由于我们编译器并无此对象所以不能完成强制转换。也就是说还得通过反射来调用。
也可以读取文件进行加载:
//如果生成的类没有放在classpath下需要自己指定类加载器加载的位置,否则加载不到。这里我们不需要设置
// pool.appendClassPath(Thread.currentThread().getContextClassLoader().getResource("").toURI().getPath());
final CtClass loadCtClass = pool.get("io.github.pleuvoir.prpc.invoker.Person");
loadCtClass.toClass().newInstance();
注意:toClass不要重复调用。
以上两种形式都是通过反射调用,我们又不存在这样的实体。那么怎么样才能完成转换呢?聪明的小伙伴肯定想到了,那就是定义接口。生成后转换为接口就可以直接调用了。
OK,话不多说。我们先定义一个接口正好来演示一下使用javassist实现动态代理。
接口调用
在实现动态代理前,我们先来回顾一组静态代理。
我们先来定义目标接口,用来被代理。等会这也是我们程序强制转换的接口。
public interface HelloService {String sayHello(String name);
}
定义代理实现,传入接口实现类。
public class HelloServiceProxy implements HelloService {private HelloService helloService;public HelloServiceProxy() {}public HelloServiceProxy(HelloService helloService) {this.helloService = helloService;}// 这里会做修改@Overridepublic String sayHello(String name) {System.out.println("静态代理前 ..");helloService.sayHello(name);System.out.println("静态代理后 ..");return name;}}
很简单,我们来测试一下:
public class ProxyTest {public static void main(String[] args) {final HelloServiceProxy serviceProxy = new HelloServiceProxy(new HelloService() {@Overridepublic String sayHello(String name) {System.out.println("目标接口实现:name=" + name);return "null";}});serviceProxy.sayHello("pleuvoir");}
}
输出的结果是:
静态代理前 ..
目标接口实现:name=pleuvoir
静态代理后 ..
不错,符合我们的预期。接下来我们就要开始生成真正的动态代理了。
先来思考一下,我们的静态代理是使用有参构造函数传入真正的实现类。如果我们动态生成的类去调用构造函数还是必须使用反射,所以我们增加一个新接口用来设置实现类。如下:
public interface IProxy {void setProxy(Object t);
}
我们期望生成的类是这样的:
public class HelloSericeProxyV2 implements IProxy, HelloService {private HelloService helloService;public HelloSericeProxyV2() {}@Overridepublic void setProxy(Object t) {this.helloService = (HelloService) t;}// 这里会做修改@Overridepublic String sayHello(String name) {System.out.println("静态代理前 ..");helloService.sayHello(name);System.out.println("静态代理后 ..");return name;}}
通过调用setProxy设置实现类,然后调用目标方法sayHello。获取到对象后可以分别转换为这两个接口进行方法调用。可能有的朋友会问,为什么void setProxy(Object t)不使用泛型。原因是对其支持不是很好,设置起来有点麻烦,所以就没设置。看一下具体的实现:
public class ProxyTest {public static void main(String[] args) throws Exception {// final HelloServiceProxy serviceProxy = new HelloServiceProxy(new HelloService() {
// @Override
// public String sayHello(String name) {
// System.out.println("目标接口实现:name=" + name);
// return "null";
// }
// });
//
// serviceProxy.sayHello("pleuvoir");//创建类,这是一个单例对象ClassPool pool = ClassPool.getDefault();pool.appendClassPath(Thread.currentThread().getContextClassLoader().getResource("").toURI().getPath());//我们需要构建的类CtClass ctClass = pool.makeClass("io.github.pleuvoir.prpc.invoker.HelloServiceJavassistProxy");//这个类实现了哪些接口ctClass.setInterfaces(new CtClass[]{pool.getCtClass("io.github.pleuvoir.prpc.invoker.HelloService"),pool.getCtClass("io.github.pleuvoir.prpc.invoker.IProxy")});//新增字段CtField field$name = new CtField(pool.get("io.github.pleuvoir.prpc.invoker.HelloService"), "helloService", ctClass);//设置访问级别field$name.setModifiers(Modifier.PRIVATE);ctClass.addField(field$name);//新增构造函数//无参构造函数CtConstructor cons$noParams = new CtConstructor(new CtClass[]{}, ctClass);cons$noParams.setBody("{}");ctClass.addConstructor(cons$noParams);//重写sayHello方方法,可以通过构造字符串的形式CtMethod m = CtNewMethod.make(buildSayHello(), ctClass);ctClass.addMethod(m);// 创建一个名为 setProxy 的方法CtMethod ctMethod = new CtMethod(CtClass.voidType, "setProxy",new CtClass[]{pool.getCtClass("java.lang.Object")}, ctClass);ctMethod.setModifiers(Modifier.PUBLIC);// // $0=this $1,$2,$3... 代表方法参数ctMethod.setBody("{$0.helloService = $1;}");ctClass.addMethod(ctMethod);ctClass.writeFile(Thread.currentThread().getContextClassLoader().getResource("").toURI().getPath());//获取实例对象final Object instance = ctClass.toClass().newInstance();System.out.println(Arrays.toString(instance.getClass().getDeclaredMethods()));//设置目标方法if (instance instanceof IProxy) {IProxy proxy = (IProxy) instance;proxy.setProxy(new HelloService() {@Overridepublic String sayHello(String name) {System.out.println("目标接口实现:name=" + name);return "null";}});}if (instance instanceof HelloService) {HelloService service = (HelloService) instance;service.sayHello("pleuvoir");}}private static String buildSayHello() {String methodString = " public String sayHello(String name) {\n"+ " System.out.println(\"静态代理前 ..\");\n"+ " helloService.sayHello(name);\n"+ " System.out.println(\"静态代理后 ..\");\n"+ " return name;\n"+ " }";return methodString;}
}
生成的.class和我们预期的一样。
参考
- javassist tutorial
- javassist 使用全解析
后语
OK,我们看到了javassist在生成代码上还是很方便高效的。注意事项是尽量不要使用泛型比较麻烦,另外如果生成的字节码会被其它对象缓存可以选择new ClassPool(true)的方式进行创建,而不是使用ClassPool.getDefault()单例对象。