(原創(chuàng))spring aop無法攔截接口上的注解

目錄

  • 問題背景
  • 問題現(xiàn)場(aop代碼)
  • 源碼
  • 初步解決方案
    • 重寫事務(wù)攔截器
    • 設(shè)置攔截器
  • 通用解決方案(自定義的方法攔截器)
    • demo 乞丐版
    • Pro版
    • 注意細(xì)節(jié)

問題背景

最近在spring-boot項目中做mysql讀寫分離時遇到了一些奇葩問題预伺,問題現(xiàn)象:通過常規(guī)的spring aop去攔截帶有自定義注解的方法時雕拼,發(fā)現(xiàn)只有注解寫在實現(xiàn)類上面時才有效愁憔,寫在接口上時卻不生效焰手。所用的spring-boot版本為1.x版本

問題現(xiàn)場(aop代碼)

@Aspect
@Component
@EnableAspectJAutoProxy
public class DataSourceAspect {
 
    @Around("@annotation(com.xxx.DataSource)")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        // 業(yè)務(wù)方法執(zhí)行之前設(shè)置數(shù)據(jù)源...
        doingSomthingBefore();

        // 執(zhí)行業(yè)務(wù)方法
        Object result = joinPoint.proceed();

        // 業(yè)務(wù)方法執(zhí)行之后清除數(shù)據(jù)源設(shè)置...
        doingSomthingAfter();
        return result;
    }
}

這是一段非常普通的spring aop攔截器代碼,由于項目中使用的事務(wù)注解全部都是寫在接口的方法上的荠藤,所以我也就習(xí)慣性的把注解@DataSource寫在接口的方法上伙单,一調(diào)試代碼,這時候發(fā)現(xiàn)spring aop根本就不鳥你哈肖,攔截器沒生效吻育。網(wǎng)上一通搜索后,發(fā)現(xiàn)遇到這個問題的人非常多淤井,答案也是五花八門布疼,有的說是spring-boot 1.x版本的bug,升級到2.x版本就可以了币狠。然后就屁顛屁顛的把spring-boot版本換成最新的2.3.0.RELEASE版本游两,根本就沒用;也有人分析說aop代理的是spring的bean實例漩绵,然而接口很顯然是不能實例化的贱案,所以aop無法生效。查了很多止吐,都是分析為什么不起作用的轰坊,可能是我搜索的關(guān)鍵字不對的原因,就沒怎么看到有解決方案的帖子祟印。
同樣的寫在接口方法上的@Transactional為什么就能生效呢(至于spring事務(wù)原理的解析這里就不講了,網(wǎng)上一大把)粟害?

源碼

通過@EnableTransactionManagement進(jìn)去看了下spring事務(wù)的源碼蕴忆,

@EnableTransactionManagement

上圖中看到@EnableTransactionManagement注解上導(dǎo)入了一個類,不知道干什么的悲幅,點進(jìn)去看看

TransactionManagementConfigurationSelector

TransactionManagementConfigurationSelector繼承了AdviceModeImportSelector套鹅,就是想加載別的類,在selectImports方法返回的內(nèi)容就是要加載的類汰具,這里可以看到分別加載了AutoProxyRegistrar卓鹿,ProxyTransactionManagementConfiguration這兩個類,通過名字能猜出ProxyTransactionManagementConfiguration這個類應(yīng)該是一個事務(wù)相關(guān)的配置類留荔,繼續(xù)點進(jìn)去看下

image.png

點開ProxyTransactionManagementConfiguration類后吟孙,果然是一個配置類,在這個類中其實它主要是干了一件事,配置spring的advisor(增強(qiáng)器)杰妓。這里的TransactionAttributeSource表示事務(wù)屬性源藻治,它是用來生成事務(wù)相關(guān)的屬性的,比如什么事務(wù)是否為只讀啊巷挥,傳播特性啊等等桩卵,都是通過這個接口來獲取的,那這個接口有很多實現(xiàn)類倍宾,如圖:

image.png

這里默認(rèn)是用的AnnotationTransactionAttributeSource注解事務(wù)屬性源雏节,換句話說,這個類就是用來處理@Transactional注解的高职。
剛剛的ProxyTransactionManagementConfiguration配置類中還有一個bean钩乍,TransactionInterceptor事務(wù)攔截器产禾,這個類才是真正的處理事務(wù)相關(guān)的一切邏輯的,可以看下一它的類圖結(jié)構(gòu),

image.png

可以看到TransactionInterceptor繼承了TransactionAspectSupport類和實現(xiàn)了MethodInterceptor接口黄伊,其中TransactionAspectSupport是提供事務(wù)支持的,MethodInterceptor是用來攔截加了@Transactional注解的方法的,職責(zé)分明帕膜。那這里知道了這個方法攔截器后我們就可以做一些騷操作了危纫。

這里我們先回到我們的需求點上螃征,我們要做的是實現(xiàn)程序自動讀寫分離酗电,那么讀寫分離的本質(zhì)是啥,不就是切換數(shù)據(jù)源么划滋,我不會告訴你怎么實現(xiàn)多數(shù)據(jù)源切換的(我也不知道处坪,動態(tài)數(shù)據(jù)源方案網(wǎng)上又是一大把的根资,但是有的是有坑的,比如為什么你配了動態(tài)數(shù)據(jù)源加上事務(wù)注解之后就無效了呢稻薇,去掉事務(wù)注解又可以了,是不是很蛋疼胶征。動態(tài)切換數(shù)據(jù)源的關(guān)鍵點在于:在適當(dāng)?shù)臅r機(jī)切換數(shù)據(jù)源)塞椎。那我這里的遇到的問題是無法攔截接口上的注解(其實你把注解放到實現(xiàn)類的方法上,啥事兒都沒了睛低。但我這個人就是喜歡杠案狠,非要放到接口方法上)

那怎么搞定這個問題呢服傍,其實通過上面對事務(wù)源碼的簡單分析之后大致可以得出以下結(jié)論:

重寫事務(wù)攔截器,在事務(wù)處理的前后加上自己的邏輯骂铁,切換數(shù)據(jù)源吹零。然后將自己重寫的事務(wù)攔截器設(shè)置到剛開始的 advisor 中就可以了

初步解決方案

重寫事務(wù)攔截器

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注解的有缆,拿到它并獲取相關(guān)屬性的榛,
        //如果事務(wù)屬性為只讀的督弓,那毫無疑問可以把它對數(shù)據(jù)的請求打到從庫
        Transactional transactional = method.getAnnotation(Transactional.class);
        boolean readOnly = transactional.readOnly();
        if (readOnly) {
            // 只讀事務(wù)猖吴,切換到mysql的從庫
            changeDatasource(DatasourceType.SLAVE);
        } else {
            // 非只讀事務(wù)囊扳,切換到mysql主庫
            changeDatasource(DatasourceType.MASTER);
        }
    }

    public void after() {
        // 清除數(shù)據(jù)源設(shè)置
        changeDatasource(DatasourceType.CLEAN);
    }

    private void changeDatasource(DatasourceType type) {
        //模擬數(shù)據(jù)源切換
        System.out.println("\n\n\n===========================================================");
        System.out.println("Datasource = " + type);
        System.out.println("===========================================================\n\n\n");
    }
}

enum DatasourceType {
    MASTER, SLAVE, CLEAN
}

設(shè)置攔截器

將自己重寫后的事務(wù)攔截器設(shè)置到advisor中烁挟,將它默認(rèn)的覆蓋掉

@Configuration
public class TransactionConfig implements InitializingBean, BeanFactoryAware {
    @Override
    public void afterPropertiesSet() throws Exception {
        // 獲取增強(qiáng)器
        BeanFactoryTransactionAttributeSourceAdvisor advisor = factory.getBean(BeanFactoryTransactionAttributeSourceAdvisor.class);
        PlatformTransactionManager platformTransactionManager = factory.getBean(PlatformTransactionManager.class);
        // spring原有的事務(wù)攔截器用的就是注解類型的事務(wù)屬性源婴洼,那我們也用這個,不然你的事務(wù)注解就失效了撼嗓,那不就白忙活了么
        TransactionAttributeSource attributeSource = new AnnotationTransactionAttributeSource();
        // 實例化自己的事務(wù)攔截器
        CustomInterceptor advice = new CustomInterceptor(platformTransactionManager, attributeSource);
        // 把它原有的事務(wù)攔截器替換成自己的柬采,因為你重寫的事務(wù)攔截是繼承它原有的,所以可以這么搞
        advisor.setAdvice(advice);
    }

    private DefaultListableBeanFactory factory;

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

到這里且警,對于接口上有事務(wù)注解的方法粉捻,我們已經(jīng)可以動態(tài)的切換它的數(shù)據(jù)源了,而且還可以不用自定注解振湾,直接用spring自帶的注解就好杀迹。
那經(jīng)過上面的一頓操作后,終于可以在事務(wù)的前后做自己的事情了押搪。

從某種意義上來將树酪,這個方案確實解決了接口方法上的注解問題,但也只是僅限于spring的事務(wù)注解大州。那對于本文標(biāo)題所述的問題续语,在本質(zhì)上并沒有得到解決,因為事務(wù)這里是spring-transaction模塊實現(xiàn)的注解處理厦画,我們這里只是用了一種投機(jī)取巧的方法達(dá)到了目的而已疮茄。

通用解決方案(自定義的方法攔截器)

所謂通用解決方案就是模仿spring-transaction寫一個自己的方法攔截器,那這里就不限于注解了根暑,通過注解也是可以的力试,只不過除了接口方法上的注解無法直接通過spring aop攔截外,其他的方式好像都可以通過spring aop直接實現(xiàn)排嫌。

實現(xiàn)一個自定義的方法攔截器:

  1. 你的bean需要是一個被ProxyFactoryBean創(chuàng)建的bean
  2. 需要有一個Advisor對象(AbstractBeanFactoryPointcutAdvisor)畸裳,然后把這個advisor對象設(shè)置到ProxyFactoryBean
  3. 需要有一個PointCut對象(StaticMethodMatcherPointcut),將其設(shè)置到 advisor 對象中
  4. 需要有一個Advice對象(MethodInterceptor)淳地,將其設(shè)置到 advisor 對象中

demo 乞丐版

/**
 * 業(yè)務(wù)接口
 */
interface Service {
    void test1();
    /**
     * 打上標(biāo)記怖糊,需要被攔截的方法
     */
    @DataSource
    void test2();
}
/**
 * 業(yè)務(wù)實現(xiàn)
 */
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");
    }
}

/**
 * 方法攔截標(biāo)記
 */
@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. 創(chuàng)建一個代理工廠bean
        ProxyFactoryBean pfb = new ProxyFactoryBean();
        // 2. 設(shè)置哪個對象需要被代理
        pfb.setTarget(service);

        // 3. 初始化 advisor
        DataSourceAdvisor advisor = new DataSourceAdvisor();
        // 4. 設(shè)置pointcut
        advisor.setPointCut(new DataSourcePointCut());
        // 5. 設(shè)置方法攔截器
        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();
    }
}

測試結(jié)果如下:

test-result.png

可以看到成功的攔截到了service#test2方法伍伤。實現(xiàn)方法攔截就這么幾個步驟并徘。

這是一個bean的情況,但是在實際的企業(yè)級開發(fā)中扰魂,這么寫很顯然不現(xiàn)實麦乞,在實際開發(fā)中要是這么寫,那就離拎盒飯不遠(yuǎn)了...

Pro版

在這個版本中我們只需要解決一件事情阅爽,那就是讓spring能夠自動為我們創(chuàng)建ProxyFactoryBean

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

public interface Test2Service {
    /**
     * 被標(biāo)記的方法
     */
    @Tx
    void a();

    void b();

    void c();
}

@Service
public class Test2ServiceImpl implements Test2Service {

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

    /**
     * 被標(biāo)記的方法
     */
    @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 {
    /**
     * 攔截規(guī)則:
     * 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() {
        // 這個類就是自動代理創(chuàng)建器路幸,能夠自動的為每個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();
    }
}

創(chuàng)建一個單元測試驗證功能

@SpringBootTest
class TxInterceptorTest {

    @Autowired
    private Test2Service test2Service;

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

單元測試結(jié)果如下:

test-result-pro.png

可以看到service#aservice#b這兩個方法都被攔截到了。其中方法 a 的注解在接口上付翁,方法b的注解在實現(xiàn)類上简肴,可見這已經(jīng)達(dá)到了我的目的,成功的攔截到了接口方法上的注解百侧。

注意細(xì)節(jié)

那如果說僅僅將上面的配置代碼復(fù)制到項目中去用的話砰识,是可以攔截接口方法注解的,但是如果要和spring的事務(wù)注解一起用的話佣渴,那么你可能要失望了辫狼,因為它會先經(jīng)過事務(wù)的攔截,然后才到你的自定義攔截器辛润,要解決這個問題很簡單膨处,將advisor設(shè)置一個執(zhí)行順序就可以了

    @Bean
    public TxMethodPointcutAdvisor methodPointcutAdvisor(TxInterceptor txInterceptor) {
        TxMethodPointcutAdvisor advisor = new TxMethodPointcutAdvisor();
        advisor.setAdvice(txInterceptor);
        advisor.setOrder(1);//設(shè)置順序,值越小砂竖,優(yōu)先級越高真椿,也就是越被先執(zhí)行
        return advisor;
    }

那這個值是怎么取的呢,難道設(shè)置成1就一定會被先執(zhí)行么乎澄,

order.png
max.png

從這里可以看到spring事務(wù)的advisor執(zhí)行順序值為Integer的最大值突硝,所以也就是你隨便設(shè)置一個值(只要它不是Integer.MAX_VALUE),它都會比spring事務(wù)攔截器先執(zhí)行置济。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
禁止轉(zhuǎn)載解恰,如需轉(zhuǎn)載請通過簡信或評論聯(lián)系作者。
  • 序言:七十年代末浙于,一起剝皮案震驚了整個濱河市护盈,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌羞酗,老刑警劉巖腐宋,帶你破解...
    沈念sama閱讀 211,042評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡脏款,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,996評論 2 384
  • 文/潘曉璐 我一進(jìn)店門裤园,熙熙樓的掌柜王于貴愁眉苦臉地迎上來撤师,“玉大人,你說我怎么就攤上這事拧揽√甓埽” “怎么了?”我有些...
    開封第一講書人閱讀 156,674評論 0 345
  • 文/不壞的土叔 我叫張陵淤袜,是天一觀的道長痒谴。 經(jīng)常有香客問我,道長铡羡,這世上最難降的妖魔是什么积蔚? 我笑而不...
    開封第一講書人閱讀 56,340評論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮烦周,結(jié)果婚禮上尽爆,老公的妹妹穿的比我還像新娘。我一直安慰自己读慎,他們只是感情好漱贱,可當(dāng)我...
    茶點故事閱讀 65,404評論 5 384
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著夭委,像睡著了一般幅狮。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上株灸,一...
    開封第一講書人閱讀 49,749評論 1 289
  • 那天崇摄,我揣著相機(jī)與錄音,去河邊找鬼蚂且。 笑死配猫,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的杏死。 我是一名探鬼主播泵肄,決...
    沈念sama閱讀 38,902評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼淑翼!你這毒婦竟也來了腐巢?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,662評論 0 266
  • 序言:老撾萬榮一對情侶失蹤玄括,失蹤者是張志新(化名)和其女友劉穎冯丙,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,110評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡胃惜,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,451評論 2 325
  • 正文 我和宋清朗相戀三年泞莉,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片船殉。...
    茶點故事閱讀 38,577評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡鲫趁,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出利虫,到底是詐尸還是另有隱情挨厚,我是刑警寧澤,帶...
    沈念sama閱讀 34,258評論 4 328
  • 正文 年R本政府宣布糠惫,位于F島的核電站疫剃,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏硼讽。R本人自食惡果不足惜巢价,卻給世界環(huán)境...
    茶點故事閱讀 39,848評論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望固阁。 院中可真熱鬧蹄溉,春花似錦、人聲如沸您炉。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,726評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽赚爵。三九已至棉胀,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間冀膝,已是汗流浹背唁奢。 一陣腳步聲響...
    開封第一講書人閱讀 31,952評論 1 264
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留窝剖,地道東北人。 一個月前我還...
    沈念sama閱讀 46,271評論 2 360
  • 正文 我出身青樓赐纱,卻偏偏與公主長得像脊奋,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,452評論 2 348