MyBatis整合双数据源
有时候在项目中会遇到需要连接两个数据库的情况。本文就结合Spring和Mybatis来讲下怎么使用双数据源(或者是多数据源)。
背景知识介绍
本文中实现多数据源的关键是Spring提供的AbstractRoutingDataSource。这个类可以根据lookup key来实现底层数据源的动态转换。
public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {
@Nullable
private Map<Object, Object> targetDataSources;
@Nullable
private Object defaultTargetDataSource;
private boolean lenientFallback = true;
private DataSourceLookup dataSourceLookup = new JndiDataSourceLookup();
@Nullable
private Map<Object, DataSource> resolvedDataSources;
@Nullable
private DataSource resolvedDefaultDataSource;
public void setTargetDataSources(Map<Object, Object> 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> T unwrap(Class<T> 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<String> dataSourceNameHolder = new ThreadLocal<>();
public DynamicDataSource(DataSource defaultTargetDataSource, Map<Object, Object> 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<Object, Object> 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;
}
人生的主旋律其实是苦难,快乐才是稀缺资源。在困难中寻找快乐,才显得珍贵~