AbstractRoutingDataSource
spring-jdbc
的包中,提供了AbstractRoutingDataSource
用于数据源路由操作
public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {
该类实现了InitializingBean
接口,说明初始化时会调用afterPropertiesSet
方法
@Override
public void afterPropertiesSet() {
if (this.targetDataSources == null) {
throw new IllegalArgumentException("Property 'targetDataSources' is required");
}
this.resolvedDataSources = CollectionUtils.newHashMap(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);
}
}
由此可知,targetDataSources属性不能为空,需要给这个属性赋值
查看resolveSpecifiedDataSource
方法
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);
}
}
如果dataSource是字符串,会生成一个DataSource,由此可见,如果数据源连接使用的是jndi,可以直接将jndi的名称传入
所以这个初始化的操作实际就是将targetDataSources中value,处理为DataSource,并按我们存入的映射关系进行映射,默认数据源不为空时,将其处理成一个DataSource
该抽象类拥有一个唯一的抽象方法
/**
* Determine the current lookup key. This will typically be
* implemented to check a thread-bound transaction context.
* <p>Allows for arbitrary keys. The returned key needs
* to match the stored lookup key type, as resolved by the
* {@link #resolveSpecifiedLookupKey} method.
*/
@Nullable
protected abstract Object determineCurrentLookupKey();
在determineTargetDataSource
方法中调用了该抽象方法
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;
}
由此可见该方法是获取一个key,通过key来获取需要的数据源,如果找不到对应的数据源,则返回设置的默认数据源
添加数据源配置
我们先创建一个配置类继承AbstractRoutingDataSource
@Configuration
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return null;
}
}
获取DataSource
的操作应该是线程隔离的,所以使用ThreadLocal
进行key的获取
AbstractRoutingDataSource
中已经存在了初始化方法,所以我们重写afterPropertiesSet
方法,进行targetDataSources
和defaultTargetDataSource
的赋值操作
创建DBContextManage来操作TheadLocal
public class DBContextManage {
private DBContextManage(){}
private static final ThreadLocal<String> DB_CONTEXT = new ThreadLocal<>();
// 设置key
public static void set(String dbName){
DB_CONTEXT.set(dbName);
}
// 获取key
public static String get(){
return DB_CONTEXT.get();
}
// 清除ey
public static void clear(){
DB_CONTEXT.remove();
}
}
可以基于数据库记录或配置文件来获取DataSource
的配置
基于数据库
基于数据库时,需用@Primary
指定默认DataSource
,然后通过数据库查询获得其他数据源的链接信息
@Configuration
public class DynamicDataSource extends AbstractRoutingDataSource {
@Resource
private DbDao dbDao;
@Override
public void afterPropertiesSet() {
List<Db> list = dbDao.selectAll();
Map<Object, Object> targetDataSources = list.stream().collect(Collectors.toMap(e -> e.getDbId(), e -> e.getJndi()));
setTargetDataSources(targetDataSources);
super.afterPropertiesSet();
}
@Override
protected Object determineCurrentLookupKey() {
return DBContextManage.get();
}
}
基于配置文件
datasource:
configs:
- jdbcUrl: jdbc:oracle:thin:@10.10.200.42:1521:orcl
driverClassName: oracle.jdbc.OracleDriver
userName: aaaaaaa
password: aaaaaaa
key: db1
- jdbcUrl: jdbc:oracle:thin:@10.10.200.42:1521:orcl
driverClassName: oracle.jdbc.OracleDriver
userName: bbbbbbbb
password: bbbbbbbb
key: db2
配置对应类
@Data
@Configuration
@ConfigurationProperties(prefix = "datasource")
public class DynamicDataSourceConfig {
private List<Config> configs = Collections.emptyList();
@Data
public static class Config{
private String jdbcUrl;
private String driverClassName;
private String userName;
private String password;
private String key;
}
}
数据源配置
@Configuration
public class DynamicDataSource extends AbstractRoutingDataSource {
@Autowired
private DynamicDataSourceConfig dynamicDataSourceConfig;
@Override
public void afterPropertiesSet() {
Map<Object, Object> targetDataSourcesMap = dynamicDataSourceConfig.getConfigs().stream()
.collect(Collectors.toMap(DynamicDataSourceConfig.Config::getKey,
e -> DataSourceBuilder.create().driverClassName(e.getDriverClassName()).url(e.getJdbcUrl())
.username(e.getUserName()).password(e.getPassword()).build()));
super.setTargetDataSources(targetDataSourcesMap);
super.afterPropertiesSet();
}
@Override
protected Object determineCurrentLookupKey() {
return DBContextManage.get();
}
}
测试
创建一个测试Dao
public interface TestDao { List<String> test();}<select id="test" resultType="string"> select distinct applied_by from changelog</select>
创建一个测试Controller
@RestController
public class TestController {
@Autowired
private TestDao testDao;
@RequestMapping("/test")
public void test(){
System.out.println("====>>dynamic datasource test");
DBContextManage.set("db1");
System.out.println(testDao.test());
DBContextManage.set("db2");
System.out.println(testDao.test());
}
}
由此可见数据源的切换已经成功了
但是我们开发的过程中不会这么操作,一般采用自定义注解加AOP的方式来实现
基于注解加AOP进行数据源切换
创建自定义注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface DBRoute {
String value();
}
创建AOP
@EnableAspectJAutoProxy@Configuration@Aspectpublic class DBRouteAspectJ { @Pointcut("@annotation(com.example.dynamicdatasource.DBRoute)") public void pointCut() {} @Around(value = "pointCut()&&@annotation(annotation)") public Object dbRoute(ProceedingJoinPoint joinPoint,DBRoute annotation) throws Throwable{ // 设置key DBContextManage.set(annotation.value()); // 执行方法 Object proceed = joinPoint.proceed(); // 清理ThreadLocal,避免内存泄露 DBContextManage.clear(); return proceed; }}
测试
创建测试Service
@Service
public class TestService {
@Autowired
private TestDao testDao;
@DBRoute("db1")
public void test1(){
System.out.println("======>test1");
System.out.println(testDao.test());
}
@DBRoute("db2")
public void test2(){
System.out.println("======>test2");
System.out.println(testDao.test());
}
}
修改测试Controller
@RestController
public class TestController {
@Autowired
private TestService testService;
@RequestMapping("/test")
public void test(){
System.out.println("====>>dynamic datasource test");
testService.test1();
testService.test2();
}
}
由日志可见,基于AOP进行数据源切换成功了
目前所有的操作都是读操作,不考虑事务的