Java中常见的动态代理有:JDK动态代理、cglib、ASM和bytebuddy等,如下所示:
- JDK动态代理:运行期动态的创建代理类,只支持接口;
- ASM:一个 Java 字节码操控框架。它能够以二进制形式修改已有类或者动态生成类。不过ASM在创建class字节码的过程中,操纵的级别是底层JVM的汇编指令级别,这要求ASM使用者要对class组织结构和JVM汇编指令有一定的了解;
- javassist:一个开源的分析、编辑和创建Java字节码的类库(源码级别的类库)。javassist是jboss的一个子项目,其主要的优点,在于简单,而且快速。直接使用java编码的形式,而不需要了解虚拟机指令,就能动态改变类的结构,或者动态生成类;
- bytebuddy:一个更高层次操作字节码的工具包。
JDK动态代理
Java在JDK1.3后引入的动态代理机制,使我们可以在运行期动态的创建代理类。动态代理是在运行期间通过接口生成代理类的,与静态代理相比更加灵活,但是也有一定的限制,第一是代理对象必须实现一个接口,否则会报异常,因为其原理就是根据接口来生成代理对象的。第二是有性能问题,因为是通过反射来实现调用的,所以比正常的直接调用来得慢,并且通过生成类文件也会多消耗部分方法区空间,可能引起Full GC。
一个JDK动态代理示例如下:
public static interface Hello {
void hi(String msg);
}
public static class HelloImpl implements Hello {
@Override
public void hi(String msg) {
System.out.println("hello " + msg);
}
}
/**
* 代理类
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public static class HelloProxy implements InvocationHandler {
private Object proxied = null;
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("hello proxy");
return method.invoke(proxied, args);
}
}
public static void main(String[] args) {
Hello hello = (Hello) Proxy.newProxyInstance(Hello.class.getClassLoader(), new Class[]{Hello.class}, new HelloProxy(new HelloImpl()));
hello.hi("world");
}
JDK动态代理生成proxy class的流程,以上述示例代码为例,从Proxy.newProxyInstance()
方法开始:首先加载被代理类的接口的class,然后通过执行方法java.lang.reflect.Proxy.ProxyClassFactory#apply
创建对应proxy class,其内部会调用ProxyGenerator.generateProxyClass()
方法生成二进制class数据,然后执行java.lang.reflect.Proxy#defineClass0
返回对应class实例,至此生成proxy class流程就完成了。
关于Java动态代理更多资料可参考: https:// mp.weixin.qq.com/s/0dLv gsDo32OvuWYI3wESJw
ASM
ASM 是一个 Java 字节码操控框架。它能够以二进制形式修改已有类或者动态生成类。ASM 可以直接产生二进制 class 文件,也可以在类被加载入 Java 虚拟机之前动态改变类行为(也就是生成的代码可以覆盖原来的类也可以是原始类的子类)。ASM 从类文件中读入信息后,能够改变类行为,分析类信息,甚至能够根据用户要求生成新类。
不过ASM在创建class字节码的过程中,操纵的级别是底层JVM的汇编指令级别,这要求ASM使用者要对class组织结构和JVM汇编指令有一定的了解。ASM提供了两组API:Core API 和Tree API,Core API是基于访问者模式来操作类的,而Tree是基于树节点来操作类的
注意:Spring和Hibernate中的cglib是一个基于ASM的更高层次的自动代码生成工具。关于ASM更多资料可参考: https://www. ibm.com/developerworks/ cn/java/j-lo-asm30/index.html
javassist
Javassist是一个开源的分析、编辑和创建Java字节码的类库。是由东京工业大学的数学和计算机科学系的 Shigeru Chiba (千叶 滋)所创建的。它已加入了开放源代码JBoss 应用服务器项目,通过使用Javassist对字节码操作为JBoss实现动态"AOP"框架。一句话总结就是:Javassit其实就是一个二方包,提供了运行时操作Java字节码的方法。
Java代码编译完生成.class文件,就JVM(准确说是JIT)会解释执行这些字节码(转换为机器码并执行),由于字节码的解释执行是在运行时进行的,那我们能否手工编写或者更改字节码,再由JVM执行呢?答案是肯定的,而Javassist就提供了一些方便的方法,让我们通过这些方法生成字节码,而不用关注class文件数据格式。
类似字节码操作方法还有ASM。几种动态编程方法相比较,在性能上Javassist高于反射,但低于ASM,因为Javassist增加了一层抽象。在实现成本上Javassist和反射都很低,而ASM由于直接操作字节码,相比Javassist源码级别的api实现成本高很多。几个方法有自己的应用场景,比如Kryo使用的是ASM,追求性能的最大化。而NBeanCopyUtil采用的是Javassist,在对象拷贝的性能上也已经明显高于其他的库,并保持高易用性。实际项目中推荐先用Javassist实现原型,若在性能测试中发现Javassist成为了性能瓶颈,再考虑使用其他字节码操作方法做优化。
注意;上述说的在性能上Javassist高于反射,但低于ASM是指生成字节码流程的这个性能,而不是生成class的执行性能,由于生成的都是class,二者的执行性能理论上是一样的。
javassist实例代码:
package com.test;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.CtNewMethod;
public class Account {
public void operation(String value) {
System.out.println("operation... " + value);
}
public static void main(String[] args) throws Exception {
ClassPool classPool = ClassPool.getDefault();
CtClass ctClass = classPool.makeClass("test.SubAccount");
ctClass.setSuperclass(classPool.get("com.test.Account"));
// 调用方法
((Account) ctClass.toClass().newInstance()).operation("");
// 添加方法并调用
ctClass = classPool.makeClass("test.SubAccount2");
ctClass.setSuperclass(classPool.get("com.test.Account"));
ctClass.addMethod(CtNewMethod.make(
"public void operation(String value) { System.out.println("operation... from Sub"); }", ctClass));
((Account) ctClass.toClass().newInstance()).operation("");
// 更改现有方法
ctClass = classPool.get("com.test.Account");
CtMethod method = ctClass.getDeclaredMethod("operation");
method.insertBefore("{ System.out.println($1); }"); // $1 表示函数入栈第一个参数
new Account().operation("param1");
}
}
输出:
operation...
operation... from Sub
operation... param1
bytebuddy
Byte Buddy是致力于解决字节码操作和 instrumentation API 的复杂性的开源框架。Byte Buddy 所声称的目标是将显式的字节码操作隐藏在一个类型安全的领域特定语言背后。通过使用 Byte Buddy,任何熟悉 Java 编程语言的人都有望非常容易地进行字节码操作。
Byte Buddy是一个较高层级的抽象的字节码操作工具,相较于ASM而言。其实吧,Byte Buddy 本身也是基于 ASM API 实现的。更多资料请参考: https://www. infoq.cn/article/Easily -Create-Java-Agents-with-ByteBuddy
如下的示例展现了如何生成一个简单的类,这个类是 Object 的子类,并且重写了 toString 方法,用来返回“Hello World!”。与原始的 ASM 类似,“intercept”会告诉 Byte Buddy 为拦截到的指令提供方法实现:
Class<?> dynamicType = new ByteBuddy()
.subclass(Object.class)
.method(ElementMatchers.named("toString"))
.intercept(FixedValue.value("Hello World!"))
.make()
.load(Object.class.getClassLoader(), ClassLoadingStrategy.Default.WRAPPER)
.getLoaded();
System.out.println(dynamicType.getSimpleName());
// 输出:Object$ByteBuddy$ilIxkTl1
小结
从Java动态代理实现上来看,可分为两种策略:一种是操作字节码,创建新类或者修改现有类,比如ASM/byte-buddy/Java动态代理;另一种是使用Java编码方式创建新类或者修改现有类,比如javassist。
关于动态代理的性能问题,如果是生成的class是被代理类的子类或者就是(添加了代理逻辑的)被代理类本身,理论上性能和直接直接被代理类逻辑是一样的(代理本身逻辑忽略不计情况下);如果生成的class是通过反射等方式进行调用被代理类逻辑的,那么性能上相对来说是比较弱的,比如Java动态代理。
推荐阅读
- 别再问我ConcurrentHashMap了
- ConcurrentHashMap竟然也有死循环问题?
- 深入浅出动态代理
- 深入浅出Java反射