Spring多数据源配置(2)[PageHelper插件下应用bug修复]

BUG

基于前一篇文章关于Sping多数据源实现,已经被我运用到实际项目中。但最近开始出现一些问题,服务刚启动,能看到数据源切换混乱的场景。由于项目中设计,服务启动会去从库查一些配置项数据,需要切换数据源,但经常数据查询失败,发现跑到主库去了,但随后又正常。

本着总想搞点大新闻的心态,开始了Debug之旅。

每次的坑,通常是我无意间挖的,这次也不例外。debug发现,一次操作,数据源被获取了两次。其中第一次是被分页插件PageHelper消耗了。看了下源码,是由于我干掉了一个配置。新项目这边有人说需要mysql、oracle多库同存的业务需求,我把PageHelper的方言配置,原本写死的 【dialect=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 = true

/**
     * 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判断方言。

回到前面的问题,为什么多获取了一次数据库连接,就导致后面数据源不正常,就得看第一版代码的坑爹之处:
标记存上下文:

    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:

@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();
    }
}

获取数据源:

    @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(),返回并移除,存一次用一次,之前分页插件配置了方言,所以不会中间获取一次数据源,一切正常。当我删除了方言配置,中间获取了一次,就导致消耗掉了一次标记,到了正式使用的时候,就再拿不到对应数据源。

为什么之后又正常,那是因为分页插件,加载了一次方言后,就不再加载。所以之后获取数据源就正常了。

修复

要解决上面的问题,就需要解决数据源标记丢失的问题,所以修改了上下文队列获取标记的方法,将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
原文地址:https://www.cnblogs.com/coderzl/p/7490495.html