前言
一般常见的动态方法调用使用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;
}
// 这里会做修改
@Override
public 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() {
@Override
public 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() {
}
@Override
public void setProxy(Object t) {
this.helloService = (HelloService) t;
}
// 这里会做修改
@Override
public 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() {
@Override
public 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()单例对象。