Spring Boot 聲明式事務結合相關攔截器


  我這項目的讀寫分離方式在使用ThreadLocal實現的讀寫分離在遷移后的偶發錯誤里提了,我不再說一次了,這次是有要求讀寫分離與事務部分要完全脫離配置文件,程序員折騰了很久,於是我就查了一下,由於我還是比較喜歡使用xml的方式,所以就隨便。。。(過程省略吧),然而,似乎是一定要聲明式的方式,所以,無奈之下就只好干了。

  首先,在之前的博客里提到過,我們的讀寫分離方式要求我們自己的AOP攔截器必須在事務攔截器之前執行,在配置文件的方式下很容易,在aop的配置里設置一下Order就好了。然而,Spring Boot的@EnableTransactionManagement注解中已經把這部分固定了,官方文檔似乎說它是和@Transactional配合使用的,總之幾乎沒有留下什么插手的余地(如果大家有好辦法,希望能告訴我一下):

  這個ProxyTransactionManagementConfiguration類中,就直接手new了TransactionInterceptor:

    public TransactionInterceptor transactionInterceptor() {
        TransactionInterceptor interceptor = new TransactionInterceptor();
        interceptor.setTransactionAttributeSource(transactionAttributeSource());
        if (this.txManager != null) {
            interceptor.setTransactionManager(this.txManager);
        }
        return interceptor;
    }

  雖然這里可以通過上圖的方式加點什么,但這個事務體系本身是非常獨立的,而且在啟動過程中就已經確定下來了,既然要調節它和自定義的aop的執行順序,我想只能先統一他們的執行策略。之前自定義的aop是在啟動過程中就被加入到一個攔截器的調用鏈中:

  由於Spring Boot很多東西並沒有留什么擴展的余地(就好像前面那個new),如果完全不用配置文件,使用Spring Boot的方式,雖然沒有什么順眼的方法,其實也還是能做的,先提個建議,能不用的情況下,最好不要用。方法是自定義一個事務攔截器,拋棄@EnableTransactionManagement,測試代碼如下,不要在意命名,圖方便直接在原來的上面改的:

    @Bean(name = "newDataSourceAop")
    public NewDataSourceAop newDataSourceAop(){
        return new NewDataSourceAop();
    }

    @Bean(name = "tInterceptor")
    public TInterceptor tInterceptor(){
        return new TInterceptor();
    }

    /**
     * 代理
     * @return
     */
    @Bean
    public BeanNameAutoProxyCreator transactionAutoProxy() {
        BeanNameAutoProxyCreator autoProxy = new BeanNameAutoProxyCreator();
        autoProxy.setProxyTargetClass(true);// 這個屬性為true時,表示被代理的是目標類本身而不是目標類的接口
        autoProxy.setBeanNames("*ServiceImpl");
        autoProxy.setInterceptorNames("newDataSourceAop", "tInterceptor");
        return autoProxy;
    }

  注意,攔截器的順序依賴於名字字符串傳入的先后順序,@Order什么的是完全沒用的,因為保存這些攔截器的是一個字符串數組。自定義的事務AOP Advice:

public class TInterceptor extends TransactionAspectSupport implements MethodInterceptor, Serializable {
    
    public TInterceptor() {
        setTransactionAttributes(getAttrs());
    }
    
    private Properties getAttrs(){
        Properties attributes = new Properties();
        // 新增
        attributes.setProperty("create*", "PROPAGATION_REQUIRED,ISOLATION_DEFAULT");
        // 修改
        attributes.setProperty("update*", "PROPAGATION_REQUIRED,ISOLATION_DEFAULT");
        // 刪除
        attributes.setProperty("delete*", "PROPAGATION_REQUIRED,ISOLATION_DEFAULT");
        //查詢
        attributes.setProperty("query*", "PROPAGATION_REQUIRED,ISOLATION_DEFAULT");
        return attributes;
    }
    
    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        Class<?> targetClass = (invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null);
    
        // Adapt to TransactionAspectSupport's invokeWithinTransaction...
        return invokeWithinTransaction(invocation.getMethod(), targetClass, () -> invocation.proceed());
    }
}

  自定義的讀寫分離Advice:

@EnableConfigurationProperties(ReadDBPathProperties.class)
public class NewDataSourceAop implements MethodBeforeAdvice {
    private static final Logger log = LoggerFactory.getLogger(NewDataSourceAop.class);
    
    @Autowired
    private ReadDBPathProperties readDBPathProperties;

    @Override
    public void before(Method method, Object[] args, Object target) throws Throwable {
        String clazzName = method.getDeclaringClass().getSimpleName();
        String runner = clazzName + "." + method.getName();
        this.chooseDataSource(runner);
    }

    private void chooseDataSource(String runner){
        runner += ",";
        String read = readDBPathProperties.getReadPath()+",";
        log.info("case : read path, vo : readPath = {}", read);
        int index = read.indexOf(runner);
        if (index == -1){
            log.info("case : choose write DB, runner : {}, tid={}", runner, Thread.currentThread().getId());
            HandleDataSource.putDataSource("write");
            return;
        }
        log.info("case : choose read DB, runner : {}, tid={}", runner, Thread.currentThread().getId());
        HandleDataSource.putDataSource("read");
    }
}

  最后再特別說一下,關於這個功能的單元測試,這個單元測試有一點意思,如果是直接運行測試方法,啟動過程和執行過程在同一個線程是測試不出效果的,因為啟動過程中加載Bean的時候會對下圖中的resources初始化,寫入默認的數據源:

  就會導致后續執行的讀寫分離攔截器失效,只要保證執行線程和啟動線程不在同一線程就好。

 

==========================================================

咱最近用的github:https://github.com/saaavsaaa

微信公眾號:

                      


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM