spring aop无法拦截接口上的注解

本文探讨了Spring AOP在拦截接口上注解的方法时遇到的问题,指出只有注解写在实现类上才有效。文章通过源码分析,提出两种解决方案:1. 重写事务拦截器并设置到Advisor中;2. 创建自定义方法拦截器。通用解决方案涉及创建Advisor和MethodInterceptor,实现在事务管理之外拦截接口方法上的注解。
摘要由CSDN通过智能技术生成

问题背景

最近在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

TransactionManagementConfigurationSelector继承了AdviceModeImportSelector,就是想加载别的类,在selectImports方法返回的内容就是要加载的类,这里可以看到分别加载了AutoProxyRegistrarProxyTransactionManagementConfiguration这两个类,通过名字能猜出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表示事务属性源,它是用来生成事务相关的属性的,比如什么事务是否为只读啊,传播特性啊等等,都是通过这个接口来获取的,那这个接口有很多实现类,如图:
image.png

这里默认是用的AnnotationTransactionAttributeSource注解事务属性源,换句话说,这个类就是用来处理@Transactional注解的。
刚刚的ProxyTransactionManagementConfiguration配置类中还有一个bean,TransactionInterceptor事务拦截器,这个类才是真正的处理事务相关的一切逻辑的,可以看下一它的类图结构,
image.png

可以看到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 {
   
       
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值