松哥最近正在录制 TienChin 项目视频~采用 Spring Boot Vue3 技术栈,里边会涉及到各种好玩的技术,小伙伴们来和松哥一起做一个完成率超 90% 的项目,戳戳戳这里-->TienChin 项目配套视频来啦。
这是小伙伴们在微信上问的一个问题:
这个问题比较典型,让我想到面试时有一个 Spring 事务失效的问题,跟这个原因以及解决方案是一模一样的,因此,抽空整篇文章和小伙伴们分享下。
1. AOP 的原理
小伙伴们知道,AOP 底层就是动态代理,动态代理有两种实现方式:
- JDK 动态代理:利用拦截器(必须实现 InvocationHandler)加上反射机制生成一个代理接口的匿名类,在调用具体方法前调用 InvokeHandler 来处理。举个例子,假设有一个接口 A,A 有一个实现类 B,现在要给 B 生成代理对象,那么实际上是给 A 接口自动生成了一个匿名实现类,并且在这个匿名实现类中调用到 B 中的方法。
- CGLIB 动态代理:利用 ASM 框架,对代理对象类生成的 class 文件加载进来,通过修改其字节码生成子类来处理。举个例子,现在有一个类 A,A 没有接口,现在想给 A 生成一个代理对象,那么实际上是自动给 A 生成了一个子类,在这个子类中覆盖了 A 中的方法,所以,小伙伴们要注意,A 类以及它里边的方法不能是 final 类型的,否则无法生成代理。
如果被代理的对象有接口,则可以使用 JDK 动态代理,没有接口就可以使用 CGLIB 动态代理。
在 Spring 中,默认情况下,如果被代理的对象有接口,就使用 JDK 动态代理,如果被代理的对象没有接口,则使用 CGLIB 动态代理。
在 Spring Boot 中,2.0 之前也跟 Spring 中的规则一样,2.0 之后则统一都使用 CGLIB 动态代理。
不过这些都是默认的规则,如果有接口,但是你又希望使用 CGLIB 动态代理,通过修改配置,也都是可以实现的:
如果是 XML 配置,想使用 CGLIB 动态代理,可以按如下方式实现:
代码语言:javascript复制<aop:config proxy-target-class="true">
<aop:pointcut id="pc1" expression="。。。"/>
<aop:aspect ref="logAdvice">
。。。
</aop:aspect>
</aop:config>
如果是 Java 配置,想使用 CGLIB 动态代理,可以按如下方式实现:
代码语言:javascript复制@Component
@Aspect
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class LogAspect {
}
当然,在新版 Spring Boot 项目中,有接口的类默认就是使用 CGLIB 动态代理的。但是此时如果有接口的类你又想使用 JDK 动态代理,那么可以通过如下配置:
代码语言:javascript复制spring.aop.proxy-target-class=false
关于 Spring Boot 中的 AOP 代理问题,可以参考去年松哥写的文章:Spring Boot 中的 AOP,到底是 JDK 动态代理还是 Cglib 动态代理?。
2. 实际用的类
基于第一小节的讲解,小伙伴们知道,当你在项目中用到了 AOP 之后,其实你所以见到的类,并不是原本的类了。
松哥前面写了好几篇 AOP 相关的文章,如下:
- 手把手教你玩多数据源动态切换!
- Redis 做接口限流,一个注解的事!
- 处理接口幂等性的两种常见方案|手把手教你
- 数据权限,一个注解搞定!
虽然是解决不同的问题,但是有一个共同的点,那就是都是通过自定义注解 AOP 解决问题的。
现在我就以手把手教你玩多数据源动态切换!为例,来和大家说说这里的动态代理到底是咋回事,没看过这篇文章的小伙伴可以先看下。
小伙伴们看下,我的 UserService 大致上是下面这样:
代码语言:javascript复制@Service
public class UserService {
@Autowired
UserMapper userMapper;
@DS("master")
public Integer count() {
return userMapper.getCount();
}
}
小伙伴们看到,count() 方法上加了 @DS 注解,所以这个 count() 方法将来是要被自动代理的。换言之,当你在另外一个类中注入 UserService 的时候,其实不是这个 UserService,我 DEBUG 小伙伴们来看一下:
小伙伴们从图中可以看到,此时我注入的 UserService 并不是真正的 UserService,而是一个通过 CGLIB 动态代理为 UserService 生成的子类,这个子类里边的 count 方法大致逻辑类似下面这样(其实就是 AOP 中的代码,具体小伙伴们可以参考 手把手教你玩多数据源动态切换!一文):
代码语言:javascript复制# 切换数据源
# 去数据库查询 count
# 清空 ThreadLocal 中的变量
# ...
但是,如果我的调用逻辑是这样呢:
代码语言:javascript复制@Service
public class UserService {
@Autowired
UserMapper userMapper;
public Integer count2() {
return count();
}
@DS("master")
public Integer count() {
return userMapper.getCount();
}
}
小伙伴们来看,count2 方法,这个时候直接在 count2 方法中调用了 count 方法,当然,count2() 方法中的调用也可以写作 this.count();
,这样看起来就更明确了,我们调用 count 方法,使用的是当前对象,而当前对象是不包含代理对象中的代码的,我们通过 DEBUG 来看下:
所以,当我们在 count2 中直接调用 count 方法的时候,那么加在 count 方法上的注解就会失效。
3. 问题解决
这个问题存在于所有使用了 AOP 的地方,存在的原因第二小节已经分析的很清楚了。
解决办法其实也有很多种,最为简单省事的一种,就是在当前类中注入代理对象,然后通过代理对象去调用其他方法,如下:
代码语言:javascript复制@Service
public class UserService {
@Autowired
UserMapper userMapper;
@Autowired
UserService userService;
public Integer count2() {
return userService.count();
}
@Transactional
@DS("master")
public Integer count() {
return userMapper.getCount();
}
}
虽然问题解决了,不过这毕竟不是一个好的解决办法(因为自己中注入自己,在新版 Spring Boot 中要开启循环依赖才能实现),大家在实际开发中,还是要从设计上尽量避免这种问题。
好啦,这个问题搞明白了,那么事务失效这个问题,也不用我多说了吧!
松哥最近正在录制 TienChin 项目视频~采用 Spring Boot Vue3 技术栈,里边会涉及到各种好玩的技术,小伙伴们来和松哥一起做一个完成率超 90% 的项目,戳戳戳这里-->TienChin 项目配套视频来啦。