我這項目的讀寫分離方式在使用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
微信公眾號: