前言:多数据源跟读写分离,没啥区别的,只是数据库连接不一样。。
一、配置原理
1.认识AbstractRoutingDataSource类
说实话,mybatis配置多数据源,AbstractRoutingDataSource类起到一个决定性的作用,读者可以直接在项目中找到这个类,打开后,找到最后这个方法:
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;
}
protected abstract Object determineCurrentLookupKey();
该方法字面意思就是:决定目标数据源!看,多么清晰的名字,再看最后的一个方法,determineCurrentLookupKey
这个方法就是决定当前的数据源的key, this.resolvedDataSources.get(lookupKey)(determineTargetDataSource()方法里面的代码)
,这个resolvedDataSources就是个Map,所以你就可以知道,determineCurrentLookupKey就是获取一个Map的key名称。
注意留意这个方法:determineCurrentLookupKey
是一个抽象类,在spring的框架中,凡是抽象类又没有实现的方法,一般都是给使用者自定义的扩展方法,切记切记!!!
所以,我们配置多数据源,就应该从这个扩展方法开始!!!
2.实现AbstractRoutingDataSource
实现AbstractRoutingDataSource的目的很简单,就是为了扩展determineCurrentLookupKey
方法,只要实现了这个方法,配置多数据源,就成功了一半!
具体方法如下:
/**
* 动态数据源
**/
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.getDb();
}
}
/** 当前线程数据源 */
public class DataSourceContextHolder {
private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();
// 设置数据源名
public static void setDb(String dbName) {
CONTEXT_HOLDER.set(dbName);
}
// 获取数据源名
public static String getDb() {
return CONTEXT_HOLDER.get();
}
// 清除数据源名
public static void clearDb() {
CONTEXT_HOLDER.remove();
}
}
该步骤,我们使用了ThreadLocal 保证当前线程的数据源具有线程独占性,与其他线程无关;
读者如果进行到这里,恭喜,你已经成功了一半。接下来,我们做点业务上的代码。
3.业务关联代码
3.1 创建个注解,用来方便指定数据源名称的
/** 数据源注解 */
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE, ElementType.METHOD })
public @interface TargetDataSource {
//默认是主库
String value() default "master_db";
}
3.2 创建切面
切面的目的是为了获取类或者方法上的TargetDataSource 注解的数据源名称:
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
@Aspect
@Order(1)
@Component
public class DynamicDataSourceAspect {
/**
* 数据源优先级
* 1.方法上的TargetDataSource注解
* 2.类上的TargetDataSource注解
* 3.默认数据源
* */
@Around("execution(* com.xxx.data.xxx.xxx.service..*.*Impl.*(..))")//扫描路径:你需要切换数据源的包名,通常是实现类所在的包
public Object switchDs(ProceedingJoinPoint point) throws Throwable {
Class<?> className = point.getTarget().getClass();
//默认的数据源,其实可以把写死的放到一个枚举中
String dataSource = "master_db";
// 得到访问的方法对象
String methodName = point.getSignature().getName();
Class[] argClass = ((MethodSignature) point.getSignature()).getParameterTypes();
Method method = className.getMethod(methodName, argClass);
// 判断是否存在@TargetDataSource注解
if (method.isAnnotationPresent(TargetDataSource.class)) {
TargetDataSource annotation = method.getAnnotation(TargetDataSource.class);
// 取出注解中的数据源名
dataSource = annotation.value();
} else if (className.isAnnotationPresent(TargetDataSource.class)) {
TargetDataSource ds = className.getAnnotation(TargetDataSource.class);
dataSource = ds.value();
}
// 切换数据源
DataSourceContextHolder.setDb(dataSource);
try {
return point.proceed();
} finally {
DataSourceContextHolder.clearDb();
}
}
}
仔细看下,其目的就是为了走: DataSourceContextHolder.setDb(dataSource);–把当前线程持有的数据源名称替换成指定的名称!
3.3 创建多个数据源DataSource
之前我们提到,determineCurrentLookupKey
这个方法,就是得到一个key,这个key,就是指定的数据源的名称,而这个名称,其实,就可以从DataSourceContextHolder.getDb()
中获取!
假如,创建多个数据源,放到一个Map<String,DataSource>,然后用这个key,是不是就可以轻松获取到一个DataSource了? 正好,前面讲过的 AbstractRoutingDataSource 这个类,就有一个对象,专门存放多个数据源的Map,那就是:
private Map<Object, Object> targetDataSources;
所以,可以参考如下类,创建多个数据源以及SqlSessionFactoryBean
:
@MapperScan(basePackages = { "com.xx.data.xx.xx.datamodel.dao" })//你们自己的dao
@Configuration
public class DataModelConfig {
@Value("${druid.masterdb.url}")
private String fmasterdbUrl;
@Value("${druid.masterdb.username}")
private String masterdbserName;
@Value("${druid.masterdb.password}")
private String masterdbPwd;
@Value("${druid.slave1.url}")
private String slave1Url;
@Value("${druid.slave1.username}")
private String slave1serName;
@Value("${druid.slave1.password}")
private String slave1Pwd;
@Value("${druid.slave2.url}")
private String slave2Url;
@Value("${druid.slave2.username}")
private String slave2UserName;
@Value("${druid.slave2.password}")
private String slave2Pwd;
@Autowired
private DateSourceConfigBean dateSourceConfigBean;
//很关键:创建的数据源,一定是自己建的那个动态数据源
@Bean(name = "dynamicDataSource")
public DataSource dynamicDataSource() {
DynamicDataSource dynamicDataSource = new DynamicDataSource();
DataSource defaultdb= defaultDb();
// 默认数据源
dynamicDataSource.setDefaultTargetDataSource(defaultdb);
// 配置多数据源
Map<Object, Object> dsMap = new HashMap<>(6);
dsMap.put("master_db",defaultdb);
dsMap.put("slave2",slave2Db());
dsMap.put("slave1", slave1Db());
//你看,这里把N个数据源放到了一个上面说的Map
dynamicDataSource.setTargetDataSources(dsMap);
return dynamicDataSource;
}
private DataSource defaultDb() {
return createDruidDataSource(masterdbUrl, masterdbUserName, masterdbPwd);
}
private DataSource slave1Db() {
return createDruidDataSource(slave1Url, slave1UserName, slave1Pwd);
}
private DataSource slave2Db() {
return createDruidDataSource(slave2Url, slave2UserName, slave2Pwd);
}
private DruidDataSource createDruidDataSource(String url,String dbUserName,String dbUserPwd ){
try{
DruidDataSource dataSource = new DruidDataSource();
dataSource.setUrl(url);
dataSource.setUsername(username);
dataSource.setPassword(password);
return dataSource;
}catch (Exception e){
System.exit(-1);
}
return null;
}
@Bean
public SqlSessionFactoryBean sqlSessionFactory(DataSource dynamicDataSource) {
SqlSessionFactoryBean sqlSessionFactory = new SqlSessionFactoryBean();
sqlSessionFactory.setDataSource(dynamicDataSource);
try {
// spring获取classpath下通配的文件列表解析器
ResourcePatternResolver resourceLoader = new PathMatchingResourcePatternResolver();
Resource[] resources = resourceLoader.getResources("com/xx/data/xxx/xxx/xxx/mapper/**/*.xml");//你们mapper的xml文件地址
sqlSessionFactory.setMapperLocations(resources);
return sqlSessionFactory;
} catch (IOException e) {
logger.error("Load mybatis mapper xml exception, ", e);
}
return null;
}
3.4 集成业务
//连接从库1
import java.util.List;
@TargetDataSource("master_db")
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper userMapper;
@Override
public List<User> selectList(Map<String,String> condition) {
return userMapper.selectList(condition);
}
}
//连接从库2
import java.util.List;
@TargetDataSource("slave1")
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper userMapper;
@Override
public List<User> selectList(Map<String,String> condition) {
return userMapper.selectList(condition);
}
}
//连接主库
import java.util.List;
@TargetDataSource("slave2")
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper userMapper;
@Override
public List<User> selectList(Map<String,String> condition) {
return userMapper.selectList(condition);
}
@TargetDataSource("slave1")//方法上也可以直接加注解
@Override
public List<User> selectList1(Map<String,String> condition) {
return userMapper.selectList1(condition);
}
}
以上,所有配置动态数据源的方法!读写分离一模一样,只是数据库连接不同而已!!
二、实战代码
篇幅多长,可以看下一篇。