在讨论动态代理机制时,一个不可避免的话题是性能。无论采用JDK动态代理还是CGLIB动态代理,本质上都是在原有目标对象上进行了封装和转换,这个过程需要消耗资源和性能。而JDK动态代理和CGLIB动态代理的内部实现过程本身也存在很大的差异性。本节将讨论这两种动态代理机制对系统运行性能所带来的影响。
为了量化不同动态代理机制对性能的影响程度,我们将设计一个案例,在该案例中同样使用3.1.2节中介绍的AccountService接口以及它的实现类AccountServiceImpl。我们将通过创建一定数量的AccountServiceImpl实例并调用它的doAccountTransaction()方法,触发动态代理机制。
为了能够基于不同的代理机制来创建代理对象,需要引入Spring中一个非常有用的注解,即@Scope注解。我们已经在第2章中了解到该注解可以用来设置Bean的作用域。其实,@Scope注解还可以用来指定代理模式ScopedProxyMode。在Spring中,Scoped-ProxyMode是一个枚举,如代码清单3-15所示。
代码清单3-15 ScopedProxyMode枚举类代码
public enum ScopedProxyMode { DEFAULT, NO, INTERFACES, TARGET_CLASS; }
请注意,ScopedProxyMode中的INTERFACES代表的就是JDK动态代理,而TARGET_CLASS使用的则是CGLIB动态代理。
现在,让我们创建两个配置类JDKProxyConfig和CGLIBProxyConfig,分别针对AccountServiceImpl指定两种不同的代理机制。其中,JDKProxyConfig如代码清单3-16所示。
代码清单3-16 JDKProxyConfig类代码
@Configuration @EnableAspectJAutoProxy public class JDKProxyConfig { @Bean @Scope(proxyMode=ScopedProxyMode.INTERFACES) public AccountService accountService(){ return new AccountServiceImpl(); } }
而CGLIBProxyConfig则如代码清单3-17所示。
代码清单3-17 CGLIBProxyConfig类代码
@Configuration @EnableAspectJAutoProxy public class CGLIBProxyAppConfig { @Bean @Scope(proxyMode=ScopedProxyMode.TARGET_CLASS) public AccountService accountService(){ return new AccountServiceImpl(); } }
借助于这两个配置文件,我们就可以通过AnnotationConfigApplicationContext这个基于注解配置的应用上下文对象来获取添加了不同代理机制的AccountServiceImpl对象,实现方式如代码清单3-18所示。
代码清单3-18 基于动态代理获取AccountService实现类代码
//基于JDKProxyConfig获取AccountServiceImpl对象 AccountService accountService = new AnnotationConfigApplicationContext(JDKProxyConfig.class).getBean(AccountService.class); //基于CGLIBProxyConfig获取AccountServiceImpl对象 AccountService accountService = new AnnotationConfigApplicationContext(CGLIBProxyConfig.class).getBean(AccountService.class);
现在,准备工作已经完成,让我们编写一个测试用例来对不同代理机制的性能进行量化。测试用例如代码清单3-19所示。
代码清单3-19 动态代理性能测试用例代码
@Test public void testAopProxyPerformance() { int countofObjects = 5000; AccountServiceImpl[] unproxiedClasses = new AccountServiceImpl[countofObjects]; for (int i = 0; i < countofObjects; i++) { unproxiedClasses[i] = new AccountServiceImpl(); } AccountService[] cglibProxyClasses = new AccountService[countofObjects]; AccountService accountService = null; for (int i = 0; i < countofObjects; i++) { accountService = new AnnotationConfigApplicationContext(CGLIBProxyAppConfig.class).getBean(AccountService.class); cglibProxyClasses[i] = accountService; } AccountService[] jdkProxyClasses = new AccountService[countofObjects]; for (int i = 0; i < countofObjects; i++) { accountService = new AnnotationConfigApplicationContext(JDKProxyAppConfig.class).getBean(AccountService.class); jdkProxyClasses[i] = accountService; } long timeTookForUnproxiedObjects = invokeTargetObjects(countofObjects, unproxiedClasses); displayResults("NOProxy", timeTookForUnproxiedObjects); long timeTookForJdkProxiedObjects = invokeTargetObjects(countofObjects, jdkProxyClasses); displayResults("JDKProxy", timeTookForJdkProxiedObjects); long timeTookForCglibProxiedObjects = invokeTargetObjects(countofObjects, cglibProxyClasses); displayResults("CGLIBProxy", timeTookForCglibProxiedObjects); }
可以看到,我们分别针对不使用代理、使用JDK代理以及使用CGLIB代理的场景,创建了5000个AccountServiceImpl对象实例,并记录它们的创建时间。完整的代码可以参考:https://github.com/tianminzheng/spring-boot-examples/tree/main/SpringAopProxyExample。
现在,让我们执行这个测试用例,得到的结果如下所示:
代码清单3-20 测试用例结果
NOProxy: 562900(ns) 0(ms) JDKProxy: 39113600(ns) 39(ms) CGLIBProxy: 46222000(ns) 46(ms)
以上量化结果取决于不同的机器配置,但不影响我们得出结论。从结果中不难看出,JDK动态代理在性能上优于CGLIB动态代理,但相差并不大。事实上,通常情况下,我们不需要对上述结果有太多的担忧,因为相比代理机制带来的优势,添加代理的时间往往可以忽略不计。