惊人!Spring5 AOP 默认使用 CGLIB ?从现象到源码的深度分析

2019 Java 开发者跳槽指南.pdf (吐血整理)….>>>

Spring5 AOP 默认使用 Cglib 了?我第一次听到这个说法是在一个微信群里:

惊人!Spring5 AOP 默认使用 CGLIB ?从现象到源码的深度分析
群聊天

真的假的?查阅文档

刚看到这个说法的时候,我是保持怀疑态度的。

大家都知道 Spring5 之前的版本 AOP 在默认情况下是使用 JDK 动态代理的,那是不是 Spring5 版本真的做了修改呢?于是我打开 Spring Framework 5.x 文档,再次确认了一下:

文档地址:https://docs.spring.io/spring/docs/5.2.0.RELEASE/spring-framework-reference/core.html#aop

惊人!Spring5 AOP 默认使用 CGLIB ?从现象到源码的深度分析
Spring Framework 5.x 文档

简单翻译一下。Spring AOP 默认使用 JDK 动态代理,如果对象没有实现接口,则使用 CGLIB 代理。当然,也可以强制使用 CGLIB 代理。

什么?文档写错了?!

当我把官方文档发到群里之后,又收到了这位同学的回复:

惊人!Spring5 AOP 默认使用 CGLIB ?从现象到源码的深度分析
文档写错了?!

SpringBoot 2.x 代码示例

为了证明文档写错了,这位同学还写了一个 DEMO。下面,就由我来重现一下这个 DEMO 程序:

运行环境:SpringBoot 2.2.0.RELEASE 版本,内置 Spring Framework 版本为 5.2.0.RELEASE 版本。同时添加 spring-boot-starter-aop 依赖,自动装配 Spring AOP。

public interface UserService {
void work();
}

@Service
public class UserServiceImpl implements UserService {

@Override
public void work() {
System.out.println("开始干活...coding...");
}
}
@Component
@Aspect
public class UserServiceAspect {
@Before("execution(* com.me.aop.UserService.work(..))")
public void logBefore(JoinPoint joinPoint) {
System.out.println("UserServiceAspect.....()");
}
}
惊人!Spring5 AOP 默认使用 CGLIB ?从现象到源码的深度分析
默认使用Cglib代理了?

UserServiceImpl实现了UserService接口,同时使用UserServiceAspectUserService#work方法进行前置增强拦截。

从运行结果来看,这里的确使用了 CGLIB 代理而不是 JDK 动态代理。

难道真的是文档写错了?!

@EnableAspectJAutoProxy 源码注释

在 Spring Framework 中,是使用@EnableAspectJAutoProxy注解来开启 Spring AOP 相关功能的。

Spring Framework 5.2.0.RELEASE 版本@EnableAspectJAutoProxy注解源码如下:

惊人!Spring5 AOP 默认使用 CGLIB ?从现象到源码的深度分析
@EnableAspectJAutoProxy源码

通过源码注释我们可以了解到:在 Spring Framework 5.2.0.RELEASE 版本中,proxyTargetClass的默认取值依旧是false,默认还是使用 JDK 动态代理。

难道文档和源码注释都写错了?!

@EnableAspectJAutoProxy 的 proxyTargetClass 无效了?

接下来,我尝试使用@EnableAspectJAutoProxy来强制使用 JDK 动态代理。

运行环境:SpringBoot 2.2.0.RELEASE 版本,内置 Spring Framework 版本为 5.2.0.RELEASE 版本。

惊人!Spring5 AOP 默认使用 CGLIB ?从现象到源码的深度分析
proxyTargetClass设置无效了?

通过运行发现,还是使用了 CGLIB 代理。难道@EnableAspectJAutoProxyproxyTargetClass设置无效了?

Spring Framework 5.x

整理一下思路

  1. 有人说 Spring5 开始 AOP 默认使用 CGLIB 了
  2. Spring Framework 5.x 文档和 @EnableAspectJAutoProxy源码注释都说了默认是使用 JDK 动态代理
  3. 程序运行结果说明,即使继承了接口,设置proxyTargetClassfalse,程序依旧使用 CGLIB 代理

等一下,我们是不是遗漏了什么?

示例程序是使用 SpringBoot 来运行的,那如果不用 SpringBoot,只用 Spring Framework 会怎么样呢?

运行环境:Spring Framework 5.2.0.RELEASE 版本。UserServiceImpl 和 UserServiceAspect 类和上文一样,这里不在赘述。

惊人!Spring5 AOP 默认使用 CGLIB ?从现象到源码的深度分析
Spring Framework 5.x
惊人!Spring5 AOP 默认使用 CGLIB ?从现象到源码的深度分析
Spring Framework 5.x使用CGLIB

运行结果表明:在 Spring Framework 5.x 版本中,如果类实现了接口,AOP 默认还是使用 JDK 动态代理。

再整理思路

  1. Spring5 AOP 默认依旧使用 JDK 动态代理,官方文档和源码注释没有错。
  2. SpringBoot 2.x 版本中,AOP 默认使用 cglib,且无法通过proxyTargetClass进行修改。
  3. 那是不是 SpringBoot 2.x 版本做了一些改动呢?

再探 SpringBoot 2.x

结果上面的分析,很有可能是 SpringBoot2.x 版本中,修改了 Spring AOP 的相关配置。那就来一波源码分析,看一下内部到底做了什么。

源码分析

源码分析,找对入口很重要。那这次的入口在哪里呢?

@SpringBootApplication是一个组合注解,该注解中使用@EnableAutoConfiguration实现了大量的自动装配。

EnableAutoConfiguration也是一个组合注解,在该注解上被标志了@Import。关于@Import注解的详细用法,可以参看笔者之前的文章:https://mp.weixin.qq.com/s/7arh4sVH1mlHE0GVVbZ84Q

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {

AutoConfigurationImportSelector实现了DeferredImportSelector接口。

在 Spring Framework 4.x 版本中,这是一个空接口,它仅仅是继承ImportSelector接口而已。而在 5.x 版本中拓展了DeferredImportSelector接口,增加了一个getImportGroup方法:

惊人!Spring5 AOP 默认使用 CGLIB ?从现象到源码的深度分析
AutoConfigurationImportSelector#getImportGroup

在这个方法中返回AutoConfigurationGroup类。这是AutoConfigurationImportSelector中的一个内部类,他实现了DeferredImportSelector.Group接口。

在 SpringBoot 2.x 版本中,就是通过AutoConfigurationImportSelector.AutoConfigurationGroup#process方法来导入自动配置类的。

惊人!Spring5 AOP 默认使用 CGLIB ?从现象到源码的深度分析
导入配置类

通过断点调试可以看到,和 AOP 相关的自动配置是通过org.springframework.boot.autoconfigure.aop.AopAutoConfiguration来进行配置的。

惊人!Spring5 AOP 默认使用 CGLIB ?从现象到源码的深度分析
AopAutoConfiguration源码

真相大白

看到这里,可以说是真相大白了。在 SpringBoot2.x 版本中,通过AopAutoConfiguration来自动装配 AOP。

默认情况下,是肯定没有spring.aop.proxy-target-class这个配置项的。而此时,在 SpringBoot 2.x 版本中会默认使用 Cglib 来实现。

SpringBoot 2.x 中如何修改 AOP 实现

通过源码我们也就可以知道,在 SpringBoot 2.x 中如果需要修改 AOP 的实现,需要通过spring.aop.proxy-target-class这个配置项来修改。

#在application.properties文件中通过spring.aop.proxy-target-class来配置
spring.aop.proxy-target-class=false
惊人!Spring5 AOP 默认使用 CGLIB ?从现象到源码的深度分析
spring-configuration-metadata.json

这里也提一下spring-configuration-metadata.json文件的作用:在使用application.propertiesapplication.yml文件时,IDEA 就是通过读取这些文件信息来提供代码提示的,SpringBoot 框架自己是不会来读取这个配置文件的。

SringBoot 1.5.x 又是怎么样的

惊人!Spring5 AOP 默认使用 CGLIB ?从现象到源码的深度分析
SringBoot 1.5.x

可以看到,在 SpringBoot 1.5.x 版本中,默认还是使用 JDK 动态代理的。

SpringBoot 2.x 为何默认使用 Cglib

SpringBoot 2.x 版本为什么要默认使用 Cglib 来实现 AOP 呢?这么做的好处又是什么呢?笔者从网上找到了一些资料,先来看一个 issue。

Spring Boot issue #5423

Use @EnableTransactionManagement(proxyTargetClass = true) #5423

https://github.com/spring-projects/spring-boot/issues/5423

在这个 issue 中,抛出了这样一个问题:

惊人!Spring5 AOP 默认使用 CGLIB ?从现象到源码的深度分析
image.png

翻译一下:我们应该使用@EnableTransactionManagement(proxyTargetClass = true)来防止人们不使用接口时出现讨厌的代理问题。

这个"不使用接口时出现讨厌的代理问题"是什么呢?思考一分钟。

讨厌的代理问题

假设,我们有一个UserServiceImplUserService类,此时需要在UserContoller中使用UserService。在 Spring 中通常都习惯这样写代码:

@Autowired
UserService userService;

在这种情况下,无论是使用 JDK 动态代理,还是 CGLIB 都不会出现问题。

但是,如果你的代码是这样的呢:

@Autowired
UserServiceImpl userService;

这个时候,如果我们是使用 JDK 动态代理,那在启动时就会报错:

惊人!Spring5 AOP 默认使用 CGLIB ?从现象到源码的深度分析
启动报错

因为 JDK 动态代理是基于接口的,代理生成的对象只能赋值给接口变量。

而 CGLIB 就不存在这个问题。因为 CGLIB 是通过生成子类来实现的,代理对象无论是赋值给接口还是实现类这两者都是代理对象的父类。

SpringBoot 正是出于这种考虑,于是在 2.x 版本中,将 AOP 默认实现改为了 CGLIB。

更多的细节信息,读者可以自己查阅上述 issue。

总结

  1. Spring 5.x 中 AOP 默认依旧使用 JDK 动态代理。
  2. SpringBoot 2.x 开始,为了解决使用 JDK 动态代理可能导致的类型转化异常而默认使用 CGLIB。
  3. 在 SpringBoot 2.x 中,如果需要默认使用 JDK 动态代理可以通过配置项spring.aop.proxy-target-class=false来进行修改,proxyTargetClass配置已无效。

延伸阅读

issue:Default CGLib proxy setting default cannot be overridden by using core framework annotations (@EnableTransactionManagement, @EnableAspectJAutoProxy) #12194

https://github.com/spring-projects/spring-boot/issues/12194

这个 issue 也聊到了关于proxyTargetClass设置失效的问题,讨论内容包括:@EnableAspectJAutoProxy@EnableCaching@EnableTransactionManagement。感兴趣的读者可以自行查阅 issue。

最后,欢迎大家在公众号留言,我们一起探讨学习!感谢你的阅读~

END

Java面试题专栏

【20期】你知道为什么HashMap是线程不安全的吗?

【19期】为什么Java线程没有Running状态?

【18期】Java序列化与反序列化三连问:是什么?为什么要?如何做?

【17期】什么情况用ArrayList or LinkedList呢?

【16期】你能谈谈HashMap怎样解决hash冲突吗

【15期】谈谈这几个常见的多线程面试题

【14期】你能说说进程与线程的区别吗

【13期】谈谈 Redis 的过期策略

【12期】谈谈项目中单点登录的实现原理?

【11期】分布式系统接口,如何避免表单的重复提交?


惊人!Spring5 AOP 默认使用 CGLIB ?从现象到源码的深度分析


欢迎长按下图关注公众号后端技术精选

惊人!Spring5 AOP 默认使用 CGLIB ?从现象到源码的深度分析

原文始发于微信公众号(后端技术精选):惊人!Spring5 AOP 默认使用 CGLIB ?从现象到源码的深度分析