Java代理(六)当前主流动态代理框架性能对比
0.测试范围
-
Java proxies
Java 类库带有一个代理工具包,允许创建实现一组给定接口的类。这个内置的代理供应商很方便,但也很有限。例如,上面提到的安全框架不能以这种方式实现,因为我们想要扩展类而不是接口。
-
cglib
代码生成库是在 Java 早期实现的,遗憾的是它没有跟上 Java 平台的发展。尽管如此,cglib 仍然是一个非常强大的库,但它的积极发展变得相当模糊。出于这个原因,它的许多用户离开了 cglib。
-
Javassist
该库带有一个编译器,该编译器采用包含 Java 源代码的字符串,这些字符串在应用程序运行时被翻译成 Java 字节码。这是非常雄心勃勃的,原则上是一个好主意,因为 Java 源代码显然是描述 Java 类的好方法。但是,Javassist 编译器的功能无法与 javac 编译器相比,并且在动态组合字符串以实现更复杂的逻辑时容易出错。此外,Javassist 带有一个代理库,它类似于 JCL 的代理实用程序,但允许扩展类并且不限于接口。然而,Javassist 的代理工具的范围在其 API 和功能方面同样受到限制。
-
ByteBuddy
ByteBuddy 是一个强大的 Java 字节码操作库,它允许开发者在运行时动态地创建、修改和操作 Java 类。它类似于其他字节码操作工具(如 ASM 或 CGLIB),但提供了更高级的抽象和更易用的 API。ByteBuddy 广泛应用于 AOP(面向切面编程)、动态代理、插件系统、测试框架等领域。
1. ByteBuddy官网上的benchmark
在ByteBuddy的官网上给了一个benchmark,号称ByteBuddy是当前最快的动态测试框架。
上表中的第一个基准测试用于在不实现或覆盖任何方法的情况下为 Object 子类化的库的运行时间。这给了我们一个库在代码生成中的一般开销的印象。在这个基准测试中,由于只有在假设总是扩展接口时才有可能进行优化,Java 代理的性能优于其他库。 Byte Buddy 还检查类中的泛型类型和注解是什么导致了额外的运行时。在创建类的其他基准测试中也可以看到这种性能开销。
基准测试 (2a) 显示了用于创建(和加载)实现具有 18 个方法的单个接口的类的测量运行时间,(2b)显示了为此类生成的方法的执行时间。类似地,(3a) 显示了使用相同的 18 种已实现方法扩展类的基准。 Byte Buddy 提供了两个基准测试,这是由于对始终执行超级方法的拦截器可能进行的优化。在类创建期间牺牲一些时间,Byte Buddy 创建的类的执行时间通常达到基线,这意味着检测根本不会产生任何开销。应该注意的是,如果元数据处理被禁用,Byte Buddy 在类创建期间也优于任何其他代码生成库。然而,由于代码生成的运行时间与程序的总运行时间相比非常小,因此这种选择退出是不可用的,因为它会以牺牲库代码的复杂性为代价获得很少的性能。
英文版:Byte Buddy - runtime code generation for the Java virtual machine
中文版:https://juejin.cn/post/7031748974285422629
2.基于JMH的benchmark
2.1 各测试库版本
<dependencies>
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.3.0</version>
</dependency>
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy</artifactId>
<version>1.17.5</version>
</dependency>
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.30.2-GA</version>
</dependency>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-core</artifactId>
<version>1.37</version>
</dependency>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>1.37</version>
</dependency>
</dependencies>
2.2 各Proxy源码
为了方便测试,简化了各Proxy类,代码全部放到一个类里,同时方便观察benchmark日志,拦截BusinessCalculator类时不打印输出日志。
JdkProxy源码如下:
public class JdkProxy implements InvocationHandler {
private Calculator calculator;
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// System.out.println("invoke method: "+method.getName());
long startTime = System.currentTimeMillis();
Object result = method.invoke(calculator, args);
long endTime = System.currentTimeMillis();
// System.out.println("cost time: ["+(endTime-startTime)+"]ms");
long cost = endTime - startTime;
return result;
}
public Object getProxy(Calculator calculator){
this.calculator = calculator;
return Proxy.newProxyInstance(
this.calculator.getClass().getClassLoader(),
this.calculator.getClass().getInterfaces(),
this);
}
}
CglibProxy源码如下:
public class CglibProxy implements MethodInterceptor {
private Calculator calculator;
public Object getProxy(Calculator calculator) {
this.calculator = calculator;
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(calculator.getClass());
enhancer.setCallback(this);
return enhancer.create();
}
@Override
public Object intercept(Object proxyObject, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
// System.out.println("invoke method: "+method.getName());
long startTime = System.currentTimeMillis();
Object result = methodProxy.invoke(calculator, objects);
long endTime = System.currentTimeMillis();
// System.out.println("cost time: ["+(endTime-startTime)+"]ms");
long cost = endTime - startTime;
return result;
}
}
ByteBuddyProxy源码如下:
public class ByteBuddyProxy {
private Calculator calculator;
@RuntimeType
public Object intercept(@This Object obj, @Origin Method method, @AllArguments Object[] args) throws Throwable {
// System.out.println("invoke method: "+method.getName());
long startTime = System.currentTimeMillis();
Object result = method.invoke(calculator, args);
long endTime = System.currentTimeMillis();
// System.out.println("cost time: ["+(endTime-startTime)+"]ms");
long costTime = endTime-startTime;
return result;
}
public Object getProxy(Calculator calculator){
this.calculator = calculator;
try {
return new ByteBuddy()
.subclass(BusinessCalculator.class)
.method(ElementMatchers.any())
.intercept(MethodDelegation.to(this))
.make()
.load(getClass().getClassLoader())
.getLoaded()
.getDeclaredConstructor()
.newInstance();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
JavassistProxy源码如下:
public class JavassistProxy {
public Object getProxy(Calculator calculator){
ProxyFactory proxyFactory = new ProxyFactory();
if(calculator.getClass().isInterface()){
Class[] clz = new Class[1];
clz[0] = calculator.getClass();
proxyFactory.setInterfaces(clz);
} else {
proxyFactory.setSuperclass(calculator.getClass());
}
proxyFactory.setHandler(new MethodHandler() {
public Object invoke(Object proxy, Method method, Method method1, Object[] args) throws Throwable {
// System.out.println("invoke method: "+method.getName());
long startTime = System.currentTimeMillis();
Object result = method1.invoke(proxy,args);
long endTime = System.currentTimeMillis();
// System.out.println("cost time: ["+(endTime-startTime)+"]ms");
long costTime = endTime - startTime;
return result;
}
});
try{
return proxyFactory.createClass().newInstance();
} catch(Exception e) {
e.printStackTrace();
return null;
}
}
}
2.3 测试的benchmark
共测试两个过程:
1)对象的创建过程,参考源码:CreateProxyBenchmark.java;
2)方法的调用过程, 参考源码:CallProxyBenchmark.java;
CreateProxyBenchmark.java 源码如下:
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@State(Scope.Benchmark)
public class CreateProxyBenchmark {
public static void main(String[] args) throws Exception{
Options ops = new OptionsBuilder().include(CreateProxyBenchmark.class.getSimpleName())
.forks(1).build();
new Runner(ops).run();
}
@Benchmark
public void JdkProxyCreate(){
Calculator proxyService = (Calculator) new JdkProxy().getProxy(new BusinessCalculator());
}
@Benchmark
public void CglibProxyCreate(){
Calculator proxyService = (Calculator) new CglibProxy().getProxy(new BusinessCalculator());
}
@Benchmark
public void ByteBuddyProxyCreate(){
Calculator proxyService = (Calculator) new ByteBuddyProxy().getProxy(new BusinessCalculator());
}
@Benchmark
public void JavassistProxyCreate(){
Calculator proxyService = (Calculator) new JavassistProxy().getProxy(new BusinessCalculator());
}
}
CallProxyBenchmark.java如下:
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@State(Scope.Benchmark)
public class CallProxyBenchmark {
private final Calculator jdkProxy = (Calculator) new JdkProxy().getProxy(new BusinessCalculator());
private final Calculator cglibProxy = (Calculator) new CglibProxy().getProxy(new BusinessCalculator());
private final Calculator byteBuddyProxy = (Calculator) new ByteBuddyProxy().getProxy(new BusinessCalculator());
private final Calculator javassistProxy = (Calculator) new JavassistProxy().getProxy(new BusinessCalculator());
public static void main(String[] args) throws Exception{
Options ops = new OptionsBuilder().include(CallProxyBenchmark.class.getSimpleName())
.forks(1).build();
new Runner(ops).run();
}
@Benchmark
public void jdkProxyCall(){
jdkProxy.add(9, 2);
jdkProxy.subtract(9, 2);
jdkProxy.multiply(9, 2);
jdkProxy.divide(9, 2);
}
@Benchmark
public void cglibProxyCall(){
cglibProxy.add(9, 2);
cglibProxy.subtract(9, 2);
cglibProxy.multiply(9, 2);
cglibProxy.divide(9, 2);
}
@Benchmark
public void byteBuddyProxyCall(){
byteBuddyProxy.add(9, 2);
byteBuddyProxy.subtract(9, 2);
byteBuddyProxy.multiply(9, 2);
byteBuddyProxy.divide(9, 2);
}
@Benchmark
public void javassistProxyCall(){
javassistProxy.add(9, 2);
javassistProxy.subtract(9, 2);
javassistProxy.multiply(9, 2);
javassistProxy.divide(9, 2);
}
}
2.4 创建代理对象benchmark结果
JMH用来做基准测试,由于JIT编译器会根据代码运行情况进行优化,代码在第一次执行的时候,速度相对较慢,随着运行的次数增加,JIT编译器会对代码进行优化,以达到最佳的性能状态。
JMH可以对代码进行预热,让代码达到最佳的性能状态,再进行性能测试。
在基准测试前会预热5个迭代,每个迭代10s。测试也会进行5个迭代,每个迭代10s。测试结果如下:
Benchmark | Mode | Cnt | Score | Units |
CreateProxyBenchmark.ByteBuddyProxyCreate | avgt | 5 | 2368439.951 ± 533202.838 | ns/op |
CreateProxyBenchmark.CglibProxyCreate | avgt | 5 | 263.011 ± 72.902 | ns/op |
CreateProxyBenchmark.JavassistProxyCreate | avgt | 5 | 621833.137 ± 110444.823 | ns/op |
CreateProxyBenchmark.JdkProxyCreate | avgt | 5 | 39.257 ± 11.334 | ns/op |
创建代理对象性能如下:(耗时越短,性能越好,单位纳秒)
JDK Dynamic Proxy > CGLIB Proxy > Javassist Proxy > ByteBuddy Proxy.
详细日志如下:
# JMH version: 1.37
# VM version: JDK 17.0.12, Java HotSpot(TM) 64-Bit Server VM, 17.0.12+8-LTS-286
# VM invoker: C:\Program Files\Java\jdk-17\bin\java.exe
# VM options: --add-opens=java.base/java.lang=ALL-UNNAMED -javaagent:C:\Program Files\JetBrains\IntelliJ IDEA Community Edition 2024.3.3\lib\idea_rt.jar=49860:C:\Program Files\JetBrains\IntelliJ IDEA Community Edition 2024.3.3\bin -Dfile.encoding=UTF-8
# Blackhole mode: compiler (auto-detected, use -Djmh.blackhole.autoDetect=false to disable)
# Warmup: 5 iterations, 10 s each
# Measurement: 5 iterations, 10 s each
# Timeout: 10 min per iteration
# Threads: 1 thread, will synchronize iterations
# Benchmark mode: Average time, time/op
# Benchmark: org.derek.CreateProxyBenchmark.ByteBuddyProxyCreate# Run progress: 0.00% complete, ETA 00:06:40
# Fork: 1 of 1
# Warmup Iteration 1: 2209820.406 ns/op
# Warmup Iteration 2: 1339412.661 ns/op
# Warmup Iteration 3: 1385975.457 ns/op
# Warmup Iteration 4: 1631787.208 ns/op
# Warmup Iteration 5: 2300534.974 ns/op
Iteration 1: 2182113.184 ns/op
Iteration 2: 2258255.671 ns/op
Iteration 3: 2477342.521 ns/op
Iteration 4: 2476037.021 ns/op
Iteration 5: 2448451.357 ns/op
Result "org.derek.CreateProxyBenchmark.ByteBuddyProxyCreate":
2368439.951 ±(99.9%) 533202.838 ns/op [Average]
(min, avg, max) = (2182113.184, 2368439.951, 2477342.521), stdev = 138471.084
CI (99.9%): [1835237.113, 2901642.788] (assumes normal distribution)# Run progress: 25.00% complete, ETA 00:05:03
# Fork: 1 of 1
# Warmup Iteration 1: 176.052 ns/op
# Warmup Iteration 2: 255.153 ns/op
# Warmup Iteration 3: 276.424 ns/op
# Warmup Iteration 4: 273.844 ns/op
# Warmup Iteration 5: 252.013 ns/op
Iteration 1: 239.986 ns/op
Iteration 2: 249.230 ns/op
Iteration 3: 269.832 ns/op
Iteration 4: 288.434 ns/op
Iteration 5: 267.576 ns/opResult "org.derek.CreateProxyBenchmark.CglibProxyCreate":
263.011 ±(99.9%) 72.902 ns/op [Average]
(min, avg, max) = (239.986, 263.011, 288.434), stdev = 18.932
CI (99.9%): [190.110, 335.913] (assumes normal distribution)...
2.5 调用代理方法benchmark结果
Benchmark | Mode | Cnt | Score | Units |
CallProxyBenchmark.byteBuddyProxyCall | avgt | 5 | 123726968.263 ± 20871995.449 | ns/op |
CallProxyBenchmark.cglibProxyCall | avgt | 5 | 263954955.266 ± 220931004.099 | ns/op |
CallProxyBenchmark.javassistProxyCall | avgt | 5 | 237567867.221 ± 74435415.052 | ns/op |
CallProxyBenchmark.jdkProxyCall | avgt | 5 | 168462962.111 ± 337985782.973 | ns/op |
从代理的方法调用性能来看:(调用时间越小,性能越好,单位纳秒)
ByteBuddy Proxy > JDK Dynamic Proxy> Javassist Proxy > CGLIB Proxy
详细日志如下:
# JMH version: 1.37
# VM version: JDK 17.0.12, Java HotSpot(TM) 64-Bit Server VM, 17.0.12+8-LTS-286
# VM invoker: C:\Program Files\Java\jdk-17\bin\java.exe
# VM options: --add-opens=java.base/java.lang=ALL-UNNAMED -javaagent:C:\Program Files\JetBrains\IntelliJ IDEA Community Edition 2024.3.3\lib\idea_rt.jar=56325:C:\Program Files\JetBrains\IntelliJ IDEA Community Edition 2024.3.3\bin -Dfile.encoding=UTF-8
# Blackhole mode: compiler (auto-detected, use -Djmh.blackhole.autoDetect=false to disable)
# Warmup: 5 iterations, 10 s each
# Measurement: 5 iterations, 10 s each
# Timeout: 10 min per iteration
# Threads: 1 thread, will synchronize iterations
# Benchmark mode: Average time, time/op
# Benchmark: org.derek.CallProxyBenchmark.byteBuddyProxyCall# Run progress: 0.00% complete, ETA 00:06:40
# Fork: 1 of 1
# Warmup Iteration 1: 108579593.548 ns/op
# Warmup Iteration 2: 108119516.129 ns/op
# Warmup Iteration 3: 113436715.730 ns/op
# Warmup Iteration 4: 107387134.043 ns/op
# Warmup Iteration 5: 154345370.769 ns/op
Iteration 1: 132647452.632 ns/op
Iteration 2: 121247436.145 ns/op
Iteration 3: 124925249.383 ns/op
Iteration 4: 119137738.095 ns/op
Iteration 5: 120676965.060 ns/opResult "org.derek.CallProxyBenchmark.byteBuddyProxyCall":
123726968.263 ±(99.9%) 20871995.449 ns/op [Average]
(min, avg, max) = (119137738.095, 123726968.263, 132647452.632), stdev = 5420390.936
CI (99.9%): [102854972.814, 144598963.712] (assumes normal distribution)# Run progress: 25.00% complete, ETA 00:05:05
# Fork: 1 of 1
# Warmup Iteration 1: 116294859.770 ns/op
# Warmup Iteration 2: 123856798.765 ns/op
# Warmup Iteration 3: 123760371.605 ns/op
# Warmup Iteration 4: 118871832.941 ns/op
# Warmup Iteration 5: 123794728.395 ns/op
Iteration 1: 164936025.806 ns/op
Iteration 2: 272795927.027 ns/op
Iteration 3: 301178502.941 ns/op
Iteration 4: 274668105.405 ns/op
Iteration 5: 306196215.152 ns/op
Result "org.derek.CallProxyBenchmark.cglibProxyCall":
263954955.266 ±(99.9%) 220931004.099 ns/op [Average]
(min, avg, max) = (164936025.806, 263954955.266, 306196215.152), stdev = 57375080.168
CI (99.9%): [43023951.167, 484885959.365] (assumes normal distribution)
# Run progress: 50.00% complete, ETA 00:03:24
# Fork: 1 of 1
# Warmup Iteration 1: 125305170.370 ns/op
# Warmup Iteration 2: 286637622.857 ns/op
# Warmup Iteration 3: 278121966.667 ns/op
# Warmup Iteration 4: 285848186.111 ns/op
# Warmup Iteration 5: 234395393.023 ns/op
Iteration 1: 258945625.641 ns/op
Iteration 2: 216181610.638 ns/op
Iteration 3: 249889931.707 ns/op
Iteration 4: 218131984.783 ns/op
Iteration 5: 244690183.333 ns/op
Result "org.derek.CallProxyBenchmark.javassistProxyCall":
237567867.221 ±(99.9%) 74435415.052 ns/op [Average]
(min, avg, max) = (216181610.638, 237567867.221, 258945625.641), stdev = 19330640.909
CI (99.9%): [163132452.168, 312003282.273] (assumes normal distribution)# Run progress: 75.00% complete, ETA 00:01:42
# Fork: 1 of 1
# Warmup Iteration 1: 123866507.407 ns/op
# Warmup Iteration 2: 205614083.673 ns/op
# Warmup Iteration 3: 233406518.605 ns/op
# Warmup Iteration 4: 237938358.140 ns/op
# Warmup Iteration 5: 241431535.714 ns/op
Iteration 1: 286094022.222 ns/op
Iteration 2: 239724626.190 ns/op
Iteration 3: 103729624.742 ns/op
Iteration 4: 107708296.774 ns/op
Iteration 5: 105058240.625 ns/op
Result "org.derek.CallProxyBenchmark.jdkProxyCall":
168462962.111 ±(99.9%) 337985782.973 ns/op [Average]
(min, avg, max) = (103729624.742, 168462962.111, 286094022.222), stdev = 87773834.518
CI (99.9%): [≈ 0, 506448745.083] (assumes normal distribution)# Run complete. Total time: 00:06:48
Benchmark Mode Cnt Score Error Units
CallProxyBenchmark.byteBuddyProxyCall avgt 5 123726968.263 ± 20871995.449 ns/op
CallProxyBenchmark.cglibProxyCall avgt 5 263954955.266 ± 220931004.099 ns/op
CallProxyBenchmark.javassistProxyCall avgt 5 237567867.221 ± 74435415.052 ns/op
CallProxyBenchmark.jdkProxyCall avgt 5 168462962.111 ± 337985782.973 ns/op
总结:
在代理对象创建过程中,JDK Dynamic Proxy 和 CGLIB Proxy 排名第一和第二。
在方法调用过程中,ByteBuddy Proxy 和 JDK Dynamic Proxy 排名第一和第二。
参考资料:
JMH 快速入门 | JAVA-TUTORIAL
https://github.com/topics/jmh
bytebuddy基本使用bytebuddy基本使用 为什么要生成运行时代码? Java 语言带有相对严格的类型系统。 - 掘金
Byte Buddy - runtime code generation for the Java virtual machine