Java常见几种动态代理的对比

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/0dLvgsDo32OvuWYI3wESJw

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动态代理。

本文分享自微信公众号 - TopCoder(gh_12e4a74a5c9c)

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-10-17

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏泰斗贤若如

我们一起学Python之——认识Python"规则"

开学后,跟预想的一样,开学第一天我们就开了Python,虽然之前早就预料到了,但对于一直学Java的我来说,内心还是有一些涟漪的。总归还是要接受的,还不如振作...

5810
来自专栏CVer

杀手级AI补全代码工具问世,支持7种编程语言及10种主流IDE,程序员沸腾了

平时写代码的时候多少会依赖编辑器里面的代码补全,敲几个字母就能补全一整个词。可是这么多年过去了,为什么代码补全还是像最开始那样,只能限定于一个词,而且毫无意义地...

19440
来自专栏北京宏哥

Java自动化测试框架-02 - TestNG之理论到实践 - 纸上得来终觉浅,绝知此事要躬行(详细教程)

TestNG,即Testing, NextGeneration,下一代测试技术,是一套根据JUnit 和NUnit思想而构建的利用注释来强化测试功能的一个测试框...

11830
来自专栏软件测试testclass

Jmeter基于windows性能测试环境搭建

Apache JMeter™应用程序是开放源码软件,是一个100%纯Java应用程序,用于加载测试功能行为和度量性能。它最初是为测试Web应用程序而设计的,但后...

8630
来自专栏程序员的梦想

超详细:6种常用的设计模式总结

简单点说,就是一个应用程序中,某个类的实例对象只有一个,你没有办法去new,因为构造器是被private修饰的,一般通过getInstance()的方法来获取它...

8200
来自专栏软件测试testclass

Katalon Studio关键字的调用

Katalon Studio预封装了大量的公共方法和关键字,Katalon Studio允许使用Groovy或Java语言自定义关键字。所以脚本开发人员自己封装...

10330
来自专栏一猿小讲

Java 反编译

To copy others is necessary, but to copy oneself is pathetic.【模仿别人是必要的,但重复自己是可悲的...

20730
来自专栏Java架构沉思录

这份3万字的Spring Boot知识清单,请查收!

在过去两三年的Spring生态圈,最让人兴奋的莫过于Spring Boot框架。或许从命名上就能看出这个框架的设计初衷:快速的启动Spring应用。因而Spri...

9320
来自专栏Jerry的SAP技术分享

SAP UI5 app view router - Haiji's application

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 ...

10920
来自专栏开发架构二三事

netty源码分析一之NioServerSocketChannel

这里调用的也是SelectorProvider.provider()的openServerSocketChannel方法。

9420

扫码关注云+社区

领取腾讯云代金券

年度创作总结 领取年终奖励