Spring多數據源配置(2)[PageHelper插件下應用bug修復]

BUG

基於前一篇文章關於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
相關文章
相關標籤/搜索