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进行数据源切换成功了
目前所有的操作都是读操作,不考虑事务的