MyBatis整合双数据源
2012 年 1 月 9 日
有时候在项目中会遇到需要连接两个数据库的情况。本文就结合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; }