(原创)spring aop无法拦截接口上的注解

目录

  • 问题背景
  • 问题现场(aop代码)
  • 源码
  • 初步解决方案
    • 重写事务拦截器
    • 设置拦截器
  • 通用解决方案(自定义的方法拦截器)
    • demo 乞丐版
    • Pro版
    • 注意细节

问题背景

最近在spring-boot项目中做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事务的源码,

@EnableTransactionManagement

上图中看到@EnableTransactionManagement注解上导入了一个类,不知道干什么的,点进去看看

TransactionManagementConfigurationSelector

TransactionManagementConfigurationSelector继承了AdviceModeImportSelector,就是想加载别的类,在selectImports方法返回的内容就是要加载的类,这里可以看到分别加载了AutoProxyRegistrarProxyTransactionManagementConfiguration这两个类,通过名字能猜出ProxyTransactionManagementConfiguration这个类应该是一个事务相关的配置类,继续点进去看下

image.png

点开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 {
            // 非只读事务,切换到mysql主库
            changeDatasource(DatasourceType.MASTER);
        }
    }

    public void after() {
        // 清除数据源设置
        changeDatasource(DatasourceType.CLEAN);
    }

    private void changeDatasource(DatasourceType type) {
        //模拟数据源切换
        System.out.println("\n\n\n===========================================================");
        System.out.println("Datasource = " + type);
        System.out.println("===========================================================\n\n\n");
    }
}

enum DatasourceType {
    MASTER, SLAVE, CLEAN
}

设置拦截器

将自己重写后的事务拦截器设置到advisor中,将它默认的覆盖掉

@Configuration
public class TransactionConfig implements InitializingBean, BeanFactoryAware {
    @Override
    public void afterPropertiesSet() throws Exception {
        // 获取增强器
        BeanFactoryTransactionAttributeSourceAdvisor advisor = factory.getBean(BeanFactoryTransactionAttributeSourceAdvisor.class);
        PlatformTransactionManager platformTransactionManager = factory.getBean(PlatformTransactionManager.class);
        // spring原有的事务拦截器用的就是注解类型的事务属性源,那我们也用这个,不然你的事务注解就失效了,那不就白忙活了么
        TransactionAttributeSource attributeSource = new AnnotationTransactionAttributeSource();
        // 实例化自己的事务拦截器
        CustomInterceptor advice = new CustomInterceptor(platformTransactionManager, attributeSource);
        // 把它原有的事务拦截器替换成自己的,因为你重写的事务拦截是继承它原有的,所以可以这么搞
        advisor.setAdvice(advice);
    }

    private DefaultListableBeanFactory factory;

    @Override
    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
        if (beanFactory instanceof DefaultListableBeanFactory) {
            this.factory = (DefaultListableBeanFactory) beanFactory;
        }
    }
}

到这里,对于接口上有事务注解的方法,我们已经可以动态的切换它的数据源了,而且还可以不用自定注解,直接用spring自带的注解就好。
那经过上面的一顿操作后,终于可以在事务的前后做自己的事情了。

从某种意义上来将,这个方案确实解决了接口方法上的注解问题,但也只是仅限于spring的事务注解。那对于本文标题所述的问题,在本质上并没有得到解决,因为事务这里是spring-transaction模块实现的注解处理,我们这里只是用了一种投机取巧的方法达到了目的而已。

通用解决方案(自定义的方法拦截器)

所谓通用解决方案就是模仿spring-transaction写一个自己的方法拦截器,那这里就不限于注解了,通过注解也是可以的,只不过除了接口方法上的注解无法直接通过spring aop拦截外,其他的方式好像都可以通过spring aop直接实现。

实现一个自定义的方法拦截器:

  1. 你的bean需要是一个被ProxyFactoryBean创建的bean
  2. 需要有一个Advisor对象(AbstractBeanFactoryPointcutAdvisor),然后把这个advisor对象设置到ProxyFactoryBean
  3. 需要有一个PointCut对象(StaticMethodMatcherPointcut),将其设置到 advisor 对象中
  4. 需要有一个Advice对象(MethodInterceptor),将其设置到 advisor 对象中

demo 乞丐版

/**
 * 业务接口
 */
interface Service {
    void test1();
    /**
     * 打上标记,需要被拦截的方法
     */
    @DataSource
    void test2();
}
/**
 * 业务实现
 */
class ServiceImpl implements Service {
    @Override
    public void test1() {
        System.out.println("hello world");
    }
    @Override
    public void test2() {
        System.out.println("I'm doing something in DB");
    }
}

/**
 * 方法拦截标记
 */
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
@interface DataSource {
}

class DataSourceAdvisor extends AbstractBeanFactoryPointcutAdvisor {

    private DataSourcePointCut pointCut;

    public void setPointCut(DataSourcePointCut pointCut) {
        this.pointCut = pointCut;
    }

    @Override
    public Pointcut getPointcut() {
        return pointCut;
    }
}

class DataSourceInterceptor implements MethodInterceptor {
    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        String name = invocation.getMethod().getName();
        System.out.println("==============" + name + " before ================");
        Object result = invocation.proceed();
        System.out.println("==============" + name + " after ================");
        return result;
    }
}

class DataSourcePointCut extends StaticMethodMatcherPointcut {

    /**
     * 方法匹配器,这个才是真正起作用的主
     *
     * @param method
     * @param targetClass
     * @return
     */
    @Override
    public boolean matches(Method method, Class<?> targetClass) {
        return method.isAnnotationPresent(DataSource.class);
    }
}

/**
 * 单元测试
 */
public class MethodInterceptorTest {

    private ProxyFactoryBean proxyFactoryBean;

    @BeforeEach
    public void before() {
        // 0. 通过某种手段拿到一个bean实例,这里简单点new一个
        Service service = new ServiceImpl();
        // 1. 创建一个代理工厂bean
        ProxyFactoryBean pfb = new ProxyFactoryBean();
        // 2. 设置哪个对象需要被代理
        pfb.setTarget(service);

        // 3. 初始化 advisor
        DataSourceAdvisor advisor = new DataSourceAdvisor();
        // 4. 设置pointcut
        advisor.setPointCut(new DataSourcePointCut());
        // 5. 设置方法拦截器
        advisor.setAdvice(new DataSourceInterceptor());

        // 6. 将advisor添加到代理中
        pfb.addAdvisor(advisor);

        proxyFactoryBean = pfb;
    }

    @Test
    public void test() {
        // 通过代理生成 service 实例
        Service proxy = (Service) proxyFactoryBean.getObject();
        proxy.test1();
        System.out.println("\n\n");
        proxy.test2();
    }
}

测试结果如下:

test-result.png

可以看到成功的拦截到了service#test2方法。实现方法拦截就这么几个步骤。

这是一个bean的情况,但是在实际的企业级开发中,这么写很显然不现实,在实际开发中要是这么写,那就离拎盒饭不远了...

Pro版

在这个版本中我们只需要解决一件事情,那就是让spring能够自动为我们创建ProxyFactoryBean

@SpringBootApplication
public class DefaultProxyCreatorApplication {
    public static void main(String[] args) {
        SpringApplication.run(DefaultProxyCreatorApplication.class, args);
    }
}

public interface Test2Service {
    /**
     * 被标记的方法
     */
    @Tx
    void a();

    void b();

    void c();
}

@Service
public class Test2ServiceImpl implements Test2Service {

    @Override
    public void a() {
        System.out.println("test2 method a");
    }

    /**
     * 被标记的方法
     */
    @Tx
    @Override
    public void b() {
        System.out.println("test2 method b");
    }

    @Override
    public void c() {
        System.out.println("test2 method c");
    }
}


@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface Tx {
}

public class TxInterceptor implements MethodInterceptor {
    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        String name = invocation.getMethod().getName();
        System.out.println(String.format("------------%s: before----------", name));
        Object object = invocation.proceed();
        System.out.println(String.format("------------%s: after----------", name));
        return object;
    }
}

public class TxMethodPointcutAdvisor extends StaticMethodMatcherPointcutAdvisor {
    /**
     * 拦截规则:
     * 1: 接口类名上有 @Tx 注解
     * 2: 接口方法名上有 @Tx 注解
     *
     * @param method
     * @param targetClass
     * @return
     */
    @Override
    public boolean matches(Method method, Class<?> targetClass) {
        return methodCanPass(method) || classCanPass(method.getDeclaringClass());
    }

    private boolean methodCanPass(Method method) {
        return method.isAnnotationPresent(Tx.class);
    }

    private boolean classCanPass(Class<?> clazz) {
        return clazz.isAnnotationPresent(Tx.class);
    }
}

@Configuration
public class AopConfig {

    @Bean
    @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
        // 这个类就是自动代理创建器,能够自动的为每个bean生成代理
        return new DefaultAdvisorAutoProxyCreator();
    }

    @Bean
    public TxMethodPointcutAdvisor methodPointcutAdvisor(TxInterceptor txInterceptor) {
        TxMethodPointcutAdvisor advisor = new TxMethodPointcutAdvisor();
        advisor.setAdvice(txInterceptor);
        return advisor;
    }

    @Bean
    public TxInterceptor methodInterceptor() {
        return new TxInterceptor();
    }
}

创建一个单元测试验证功能

@SpringBootTest
class TxInterceptorTest {

    @Autowired
    private Test2Service test2Service;

    @Test
    void test1() {
        test2Service.a();
        System.out.println("\n");
        test2Service.b();
        System.out.println("\n");
        test2Service.c();
    }
}

单元测试结果如下:

test-result-pro.png

可以看到service#aservice#b这两个方法都被拦截到了。其中方法 a 的注解在接口上,方法b的注解在实现类上,可见这已经达到了我的目的,成功的拦截到了接口方法上的注解。

注意细节

那如果说仅仅将上面的配置代码复制到项目中去用的话,是可以拦截接口方法注解的,但是如果要和spring的事务注解一起用的话,那么你可能要失望了,因为它会先经过事务的拦截,然后才到你的自定义拦截器,要解决这个问题很简单,将advisor设置一个执行顺序就可以了

    @Bean
    public TxMethodPointcutAdvisor methodPointcutAdvisor(TxInterceptor txInterceptor) {
        TxMethodPointcutAdvisor advisor = new TxMethodPointcutAdvisor();
        advisor.setAdvice(txInterceptor);
        advisor.setOrder(1);//设置顺序,值越小,优先级越高,也就是越被先执行
        return advisor;
    }

那这个值是怎么取的呢,难道设置成1就一定会被先执行么,

order.png
max.png

从这里可以看到spring事务的advisor执行顺序值为Integer的最大值,所以也就是你随便设置一个值(只要它不是Integer.MAX_VALUE),它都会比spring事务拦截器先执行。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,530评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,403评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,120评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,770评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,758评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,649评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,021评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,675评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,931评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,751评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,410评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,004评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,969评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,042评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,493评论 2 343