基於前一篇文章關於Sping多數據源實現,已經被我運用到實際項目中。但最近開始出現一些問題,服務剛啓動,能看到數據源切換混亂的場景。因爲項目中設計,服務啓動會去從庫查一些配置項數據,須要切換數據源,但常常數據查詢失敗,發現跑到主庫去了,但隨後又正常。html
本着總想搞點大新聞的心態,開始了Debug之旅。java
每次的坑,一般是我無心間挖的,此次也不例外。debug發現,一次操做,數據源被獲取了兩次。其中第一次是被分頁插件PageHelper消耗了。看了下源碼,是因爲我幹掉了一個配置。新項目這邊有人說須要mysql、oracle多庫同存的業務需求,我把PageHelper的方言配置,本來寫死的 【dialect=mysql】給幹掉了。mysql
/** * 設置屬性值 * * @param p 屬性值 */ public void setProperties(Properties p) { //MyBatis3.2.0版本校驗 try { Class.forName("org.apache.ibatis.scripting.xmltags.SqlNode");//SqlNode是3.2.0以後新增的類 } catch (ClassNotFoundException e) { throw new RuntimeException("您使用的MyBatis版本過低,MyBatis分頁插件PageHelper支持MyBatis3.2.0及以上版本!"); } //數據庫方言 String dialect = p.getProperty("dialect"); if (dialect == null || dialect.length() == 0) { autoDialect = true; this.properties = p; } else { autoDialect = false; sqlUtil = new SqlUtil(dialect); sqlUtil.setProperties(p); } }
加載時,判斷沒有設置方言,則 autoDialect = truesql
/** * Mybatis攔截器方法 * * @param invocation 攔截器入參 * @return 返回執行結果 * @throws Throwable 拋出異常 */ public Object intercept(Invocation invocation) throws Throwable { if (autoDialect) { initSqlUtil(invocation); } return sqlUtil.processPage(invocation); } /** * 初始化sqlUtil * * @param invocation */ public synchronized void initSqlUtil(Invocation invocation) { if (sqlUtil == null) { String url = null; try { MappedStatement ms = (MappedStatement) invocation.getArgs()[0]; MetaObject msObject = SystemMetaObject.forObject(ms); DataSource dataSource = (DataSource) msObject.getValue("configuration.environment.dataSource"); url = dataSource.getConnection().getMetaData().getURL(); } catch (SQLException e) { throw new RuntimeException("分頁插件初始化異常:" + e.getMessage()); } if (url == null || url.length() == 0) { throw new RuntimeException("沒法自動獲取jdbcUrl,請在分頁插件中配置dialect參數!"); } String dialect = Dialect.fromJdbcUrl(url); if (dialect == null) { throw new RuntimeException("沒法自動獲取數據庫類型,請經過dialect參數指定!"); } sqlUtil = new SqlUtil(dialect); sqlUtil.setProperties(properties); properties = null; autoDialect = false; } }
沒有意外,而後就須要獲取數據庫鏈接,根據url判斷方言。數據庫
回到前面的問題,爲何多獲取了一次數據庫鏈接,就致使後面數據源不正常,就得看初版代碼的坑爹之處:
標記存上下文:apache
public class DataRouteContext { private static ThreadLocal<Deque<String>> route = new ThreadLocal<>(); public static String getRoute(){ Deque<String> deque = route.get(); if (deque == null || deque.size() == 0) { return null; } return deque.pop(); }
Aspect:oracle
@Aspect @Component @Order(1) public class DataRouteAspect { // @Around("execution(public * *(..)) && @annotation(dataRoute))") @Around("@annotation(dataRoute)") public Object setRouteName(ProceedingJoinPoint jp, DataRoute dataRoute) throws Throwable { String routeKey = dataRoute.value(); DataRouteLogger.info("Aspect 數據路由設置爲:"+routeKey); if (StringUtils.isNotBlank(routeKey)) { DataRouteContext.setRoute(routeKey); } return jp.proceed(); } }
獲取數據源:app
@Override public Connection getConnection(String username, String password) throws SQLException { DataSource ds = null; String routeName = DataRouteContext.getRoute(); if (routeName != null) { DataRouteLogger.info("dataSource changed , current dataSource is:"+routeName); ds = sourceMap.get(routeName); } else { DataRouteLogger.info("current dataSource is:defaultSource"); ds = this.defaultSource; } if (ds == null){ DataRouteLogger.error("dataSource is:" + routeName + " not found"); throw new IllegalArgumentException("dataSource is: " + routeName + "not found"); } if(username == null || password == null) { return ds.getConnection(); } return ds.getConnection(username, password); }
AOP將在須要切換數據源的方法前,往線程上下文隊列裏放一個數據源名稱,而後獲取數據源時,會根據上下文隊列裏取到的數據源名稱,切換不一樣的數據源,取不到,則爲默認數據源。
標記存放方式是隊列,取是用pop(),返回並移除,存一次用一次,以前分頁插件配置了方言,因此不會中間獲取一次數據源,一切正常。當我刪除了方言配置,中間獲取了一次,就致使消耗掉了一次標記,到了正式使用的時候,就再拿不到對應數據源。ide
爲何以後又正常,那是由於分頁插件,加載了一次方言後,就再也不加載。因此以後獲取數據源就正常了。this
要解決上面的問題,就須要解決數據源標記丟失的問題,因此修改了上下文隊列獲取標記的方法,將pop()改爲peek()。返回數據,不移除。
public class DataRouteContext { private static ThreadLocal<Deque<String>> route = new ThreadLocal<>(); public static String getRoute(){ Deque<String> deque = route.get(); if (deque == null || deque.size() == 0) { return null; } return deque.peek(); }
而後修改了AOP邏輯,增長了reset動做
@Aspect @Component @Order(1) public class DataRouteAspect { // @Around("execution(public * *(..)) && @annotation(dataRoute))") @Around("@annotation(dataRoute)") public Object setRouteName(ProceedingJoinPoint jp, DataRoute dataRoute) throws Throwable { String routeKey = dataRoute.value(); DataRouteLogger.info("Aspect 數據路由設置爲:"+routeKey); if (StringUtils.isNotBlank(routeKey)) { DataRouteContext.setRoute(routeKey); } Object result = jp.proceed(); DataRouteContext.reset(); return result; } }
至此,以前的BUG就解決了。
分頁插件多數據源配置,還須要新增一個參數
autoRuntimeDialect=true