MyBatis整合双数据源

有时候在项目中会遇到需要连接两个数据库的情况。本文就结合Spring和Mybatis来讲下怎么使用双数据源(或者是多数据源)。

背景知识介绍

本文中实现多数据源的关键是Spring提供的AbstractRoutingDataSource。这个类可以根据lookup key来实现底层数据源的动态转换。

public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {

    @Nullable
    private Map targetDataSources;

    @Nullable
    private Object defaultTargetDataSource;

    private boolean lenientFallback = true;

    private DataSourceLookup dataSourceLookup = new JndiDataSourceLookup();

    @Nullable
    private Map resolvedDataSources;

    @Nullable
    private DataSource resolvedDefaultDataSource;

    public void setTargetDataSources(Map targetDataSources) {
        this.targetDataSources = targetDataSources;
    }

    public void setDefaultTargetDataSource(Object defaultTargetDataSource) {
        this.defaultTargetDataSource = defaultTargetDataSource;
    }

    public void setLenientFallback(boolean lenientFallback) {
        this.lenientFallback = lenientFallback;
    }

    public void setDataSourceLookup(@Nullable DataSourceLookup dataSourceLookup) {
        this.dataSourceLookup = (dataSourceLookup != null ? dataSourceLookup : new JndiDataSourceLookup());
    }


    @Override
    public void afterPropertiesSet() {
        if (this.targetDataSources == null) {
            throw new IllegalArgumentException("Property 'targetDataSources' is required");
        }
        this.resolvedDataSources = new HashMap(this.targetDataSources.size());
        this.targetDataSources.forEach((key, value) -> {
            Object lookupKey = resolveSpecifiedLookupKey(key);
            DataSource dataSource = resolveSpecifiedDataSource(value);
            this.resolvedDataSources.put(lookupKey, dataSource);
        });
        if (this.defaultTargetDataSource != null) {
            this.resolvedDefaultDataSource = resolveSpecifiedDataSource(this.defaultTargetDataSource);
        }
    }

    protected Object resolveSpecifiedLookupKey(Object lookupKey) {
        return lookupKey;
    }

    protected DataSource resolveSpecifiedDataSource(Object dataSource) throws IllegalArgumentException {
        if (dataSource instanceof DataSource) {
            return (DataSource) dataSource;
        }
        else if (dataSource instanceof String) {
            return this.dataSourceLookup.getDataSource((String) dataSource);
        }
        else {
            throw new IllegalArgumentException(
                    "Illegal data source value - only [javax.sql.DataSource] and String supported: " + dataSource);
        }
    }


    @Override
    public Connection getConnection() throws SQLException {
        return determineTargetDataSource().getConnection();
    }

    @Override
    public Connection getConnection(String username, String password) throws SQLException {
        return determineTargetDataSource().getConnection(username, password);
    }

    @Override
    @SuppressWarnings("unchecked")
    public  T unwrap(Class iface) throws SQLException {
        if (iface.isInstance(this)) {
            return (T) this;
        }
        return determineTargetDataSource().unwrap(iface);
    }

    @Override
    public boolean isWrapperFor(Class iface) throws SQLException {
        return (iface.isInstance(this) || determineTargetDataSource().isWrapperFor(iface));
    }
    
    protected DataSource determineTargetDataSource() {
        Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
        Object lookupKey = determineCurrentLookupKey();
        DataSource dataSource = this.resolvedDataSources.get(lookupKey);
        if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
            dataSource = this.resolvedDefaultDataSource;
        }
        if (dataSource == null) {
            throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
        }
        return dataSource;
    }

    @Nullable
    //一般只需要用户实现这个方法。
    protected abstract Object determineCurrentLookupKey();

}

实现流程

step1:实现一个自定义的AbstractRoutingDataSource

public class DynamicDataSource extends AbstractRoutingDataSource {

    //这边定义了一个和线程绑定的ThreadLocal变量,用于存放需要使用的数据源的名称
    private static final ThreadLocal dataSourceNameHolder = new ThreadLocal();

    public DynamicDataSource(DataSource defaultTargetDataSource, Map targetDataSources) {
        super.setDefaultTargetDataSource(defaultTargetDataSource);
        super.setTargetDataSources(targetDataSources);
        super.afterPropertiesSet();
    }

    @Override
    //重写了AbstractRoutingDataSource的determineCurrentLookupKey方法
    protected Object determineCurrentLookupKey() {
        return getDataSource();
    }

    public static void setDataSource(String dataSource) {
        dataSourceNameHolder.set(dataSource);
    }

    public static String getDataSource() {
        return dataSourceNameHolder.get();
    }

    public static void clearDataSource() {
        dataSourceNameHolder.remove();
    }

}

step2:实现一个AOP对Service层方法进行AOP拦截,调用DynamicDataSource中的ThreadLocal变量,将当前请求需要使用的数据源名称设置进去。

//定义一个DataSource注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSource {
    String name() default "";
}
//这边再定义一个常量
public interface DataSourceNames {
    String FIRST = "first";
    String SECOND = "second";

}

定义AOP处理DataSource注解

@Aspect
@Component
public class DataSourceAspect implements Ordered {
    protected Logger logger = LoggerFactory.getLogger(getClass());

    @Pointcut("@annotation(com.xx.yy.annotation.DataSource)")
    public void dataSourcePointCut() {

    }

    @Around("dataSourcePointCut()")
    public Object around(ProceedingJoinPoint point) throws Throwable {
        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod();
        
        DataSource ds = method.getAnnotation(DataSource.class);
        //如果未指定数据源就使用第一个数据源
        if(ds == null){
            DynamicDataSource.setDataSource(DataSourceNames.FIRST);
            logger.debug("set datasource is " + DataSourceNames.FIRST);
        }else {
            DynamicDataSource.setDataSource(ds.name());
            logger.debug("set datasource is " + ds.name());
        }
        try {
            return point.proceed();
        } finally {
            DynamicDataSource.clearDataSource();
            logger.debug("clean datasource");
        }
    }
    @Override
    public int getOrder() {
        return 1;
    }
}

step3:对数据源进行配置

@Configuration
public class DynamicDataSourceConfig {

    @Bean
    @ConfigurationProperties("spring.datasource.druid.first")
    public DataSource firstDataSource(){
        return DruidDataSourceBuilder.create().build();
    }

    @Bean
    @ConfigurationProperties("spring.datasource.druid.second")
    public DataSource secondDataSource(){
        DataSource dataSource = DruidDataSourceBuilder.create().build();
        return dataSource;
    }

    @Bean
    @Primary
    @DependsOn(value = {"firstDataSource","secondDataSource"})
    public DynamicDataSource dataSource(DataSource firstDataSource,DataSource secondDataSource) {
        Map targetDataSources = new HashMap();
        targetDataSources.put(DataSourceNames.FIRST, firstDataSource);
        targetDataSources.put(DataSourceNames.SECOND,  secondDataSource);
        return new DynamicDataSource(firstDataSource, targetDataSources);
    }
}

以上就是实现双数据源的全部配置。

使用

使用的时候非常简单,只需要在Service层的方法上加上@DataSource注解就可以了。

@DataSource(name = DataSourceNames.SECOND)
public String selectByInfoName(String name){
   //...
}

一些注意点

如果你使用了pageHelper等分页插件,请将方言设置成自动模式, autoRuntimeDialect: true

pagehelper:
  reasonable: false
  supportMethodsArguments: true
  params: count=countSql
  autoRuntimeDialect: true

如果你使用了Druid数据源,并通过下面的形式创建数据源,要保障数据源的用户名和密码字段不为null。不然DruidDataSourceWrapper这个Bean会检测这个字段的值,导致启动失败。

@Bean
    @ConfigurationProperties("spring.datasource.druid.first")
    public DataSource firstDataSource(){
        return DruidDataSourceBuilder.create().build();
    }

    @Bean
    @ConfigurationProperties("spring.datasource.druid.second")
    public DataSource secondDataSource(){
        DataSource dataSource = DruidDataSourceBuilder.create().build();
        return dataSource;
    }