问题背景
最近在spring-boog项目中做mysql读写分离时遇到了一些奇葩问题,问题现象:通过常规的spring aop去拦截带有自定义注解的方法时,发现只有注解写在实现类上面时才有效,写在接口上时却不生效。所用的spring-boot版本为1.x版本
问题现场(aop代码)
@Aspect
@Component
@EnableAspectJAutoProxy
public class DataSourceAspect {
@Around("@annotation(com.xxx.DataSource)")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
// 业务方法执行之前设置数据源...
doingSomthingBefore();
// 执行业务方法
Object result = joinPoint.proceed();
// 业务方法执行之后清除数据源设置...
doingSomthingAfter();
return result;
}
}
这是一段非常普通的spring aop拦截器代码,由于项目中使用的事务注解全部都是写在接口的方法上的,所以我也就习惯性的把注解@DataSource
写在接口的方法上,一调试代码,这时候发现spring aop根本就不鸟你,拦截器没生效。网上一通搜索后,发现遇到这个问题的人非常多,答案也是五花八门,有的说是spring-boot 1.x版本的bug,升级到2.x版本就可以了。然后就屁颠屁颠的把spring-boot版本换成最新的2.3.0.RELEASE
版本,根本就没用;也有人分析说aop代理的是spring的bean实例,然而接口很显然是不能实例化的,所以aop无法生效。查了很多,都是分析为什么不起作用的,可能是我搜索的关键字不对的原因,就没怎么看到有解决方案的帖子。
同样的写在接口方法上的@Transactional
为什么就能生效呢(至于spring事务原理的解析这里就不讲了,网上一大把)?
源码
通过@EnableTransactionManagement
进去看了下spring事务的源码,
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3k1bkINx-1589795912549)(https://upload-images.jianshu.io/upload_images/8810368-ce3cbe1fb6a52c45.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)]
上图中看到@EnableTransactionManagement
注解上导入了一个类,不知道干什么的,点进去看看
TransactionManagementConfigurationSelector
继承了AdviceModeImportSelector
,就是想加载别的类,在selectImports
方法返回的内容就是要加载的类,这里可以看到分别加载了AutoProxyRegistrar
,ProxyTransactionManagementConfiguration
这两个类,通过名字能猜出ProxyTransactionManagementConfiguration
这个类应该是一个事务相关的配置类,继续点进去看下
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6veBWfQv-1589795912557)(https://upload-images.jianshu.io/upload_images/8810368-c5654bb8ac3e7dc3.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)]
点开ProxyTransactionManagementConfiguration
类后,果然是一个配置类,在这个类中其实它主要是干了一件事,配置spring的advisor
(增强器)。这里的TransactionAttributeSource
表示事务属性源,它是用来生成事务相关的属性的,比如什么事务是否为只读啊,传播特性啊等等,都是通过这个接口来获取的,那这个接口有很多实现类,如图:
这里默认是用的AnnotationTransactionAttributeSource
注解事务属性源,换句话说,这个类就是用来处理@Transactional
注解的。
刚刚的ProxyTransactionManagementConfiguration
配置类中还有一个bean,TransactionInterceptor
事务拦截器,这个类才是真正的处理事务相关的一切逻辑的,可以看下一它的类图结构,
可以看到TransactionInterceptor
继承了TransactionAspectSupport
类和实现了MethodInterceptor
接口,其中TransactionAspectSupport
是提供事务支持的,MethodInterceptor
是用来拦截加了@Transactional
注解的方法的,职责分明。那这里知道了这个方法拦截器后我们就可以做一些骚操作了。
这里我们先回到我们的需求点上,我们要做的是实现程序自动读写分离,那么读写分离的本质是啥,不就是切换数据源么,我不会告诉你怎么实现多数据源切换的(我也不知道,动态数据源方案网上又是一大把的,但是有的是有坑的,比如为什么你配了动态数据源加上事务注解之后就无效了呢,去掉事务注解又可以了,是不是很蛋疼。动态切换数据源的关键点在于:在适当的时机切换数据源
)。那我这里的遇到的问题是无法拦截接口上的注解
(其实你把注解放到实现类的方法上,啥事儿都没了。但我这个人就是喜欢杠,非要放到接口方法上)
那怎么搞定这个问题呢,其实通过上面对事务源码的简单分析之后大致可以得出以下结论:
重写事务拦截器,在事务处理的前后加上自己的逻辑,切换数据源。然后将自己重写的事务拦截器设置到刚开始的 advisor 中就可以了
初步解决方案
重写事务拦截器
public class CustomInterceptor extends TransactionInterceptor {
private static final long serialVersionUID = 1154144110124764905L;
public CustomInterceptor(PlatformTransactionManager ptm, TransactionAttributeSource tas) {
super(ptm, tas);
}
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
before(invocation.getMethod());
Object invoke = null;
try {
invoke = super.invoke(invocation);
} finally {
after();
}
return invoke;
}
public void before(Method method) {
//这里都拿到method对象了,那通过反射可以做的事情就很多了,
//能到这里来的,那方法上面肯定是有Transactional注解的,拿到它并获取相关属性,
//如果事务属性为只读的,那毫无疑问可以把它对数据的请求打到从库
Transactional transactional = method.getAnnotation(Transactional.class);
boolean readOnly = transactional.readOnly();
if (readOnly) {
// 只读事务,切换到mysql的从库
changeDatasource(DatasourceType.SLAVE);
} else {