数据源切换方法
Spring对数据源的管理类似于策略模式,不懂策略模式也没关系,其实就是有一个全局的键值对,类型是Map<String, DataSource>
。当JDBC
操作数据库之时,会根据不同的key值选择不同的数据源。而这个key值可以放到方法的注解里。
所以切换数据源的思路就是让JDBC
在获取数据源时根据key获取到要切换的数据源。
JDBC
提供了AbstractRoutingDataSource
抽象类,类名意思是数据源路由,该类提供了一个抽象方法determineCurrentLookupKey()
,切换数据源时JDBC
会调用这个方法获取数据源的key,所以只需要实现该方法,改变该方法中返回的key值即可。
源码解读
1.从类关系图中可以看出AbstractRoutingDataSource
类实现了DataSource
接口,后者有两个getConnection()
方法,即获取DB连接的作用。
2.AbstractRoutingDataSource
实现了这两个方法
其中determineTargetDataSource()
方法的作用就是获取实际的数据源,其内部调用了determineCurrentLookupKey()
方法,取到当前设定的key,通过key在上下文this.resolvedDataSources
属性中尝试获取DataSource对象,这个对象即当前连接的数据源
3.那么this.resolvedDataSources
在哪里维护呢? 继续在AbstractRoutingDataSource
类里找,可以找到afterPropertiesSet()
方法,这个方法是InitializingBean
接口的,作用是在bean的所有属性设置完成后便会调用此方法。可以看到this.resolvedDataSources
是从this.targetDataSources
取的信息。
所以只需要改变this.targetDataSources,即可改变this.resolvedDataSources;后续改变determineCurrentLookupKey()的返回值(key),在调用getConnection()时即可获取到指定的数据源
实现方式:注解+切面
别看步骤挺多,但其实很容易理解和使用
1.配置文件示例:
spring: datasource: master: # 数据源1 jdbc-url: jdbc:mysql://localhost:3306/master?characterEncoding=utf8&useUnicode=true&useSSL=false&serverTimezone=GMT%2B8 username: root password: root driver-class-name: com.mysql.cj.jdbc.Driver db1: # 数据源1 jdbc-url: jdbc:mysql://localhost:3306/db2?characterEncoding=utf8&useUnicode=true&useSSL=false&serverTimezone=GMT%2B8 username: root password: root driver-class-name: com.mysql.cj.jdbc.Driver
2.创建数据源配置类
创建数据源配置类(我这里为了方便区分就为每一个数据源创建了一个配置类,当然也可以把所有的数据源配置在一个类里)
@Configuration @EnableConfigurationProperties({MasterDataSourceProperties.class}) public class MasterDataSourceConfig { /** * 这个MasterDataSourceProperties是读取配置文件的类,我这里为了省篇幅就不展示了 **/ @Autowired private MasterDataSourceProperties masterDataSourceProperties; @Bean @Primary public DataSource masterDataSource() { DruidDataSource datasource = new DruidDataSource(); datasource.setUrl(masterDataSourceProperties.getUrl()); datasource.setUsername(masterDataSourceProperties.getUsername()); datasource.setPassword(AESUtil.aesDecode(masterDataSourceProperties.getPassword())); datasource.setDriverClassName(masterDataSourceProperties.getDriverClassName()); ...... return datasource; } }
@Configuration @EnableConfigurationProperties({OdsDataSourceProperties.class}) public class DB1DataSourceConfig { /** * 这个DB1DataSourceProperties是读取配置文件的类,我这里为了省篇幅就不展示了 **/ @Autowired private DB1DataSourceProperties dB1DataSourceProperties; @Bean public DataSource db1DataSource() { DruidDataSource datasource = new DruidDataSource(); datasource.setUrl(dB1DataSourceProperties.getUrl()); datasource.setUsername(dB1DataSourceProperties.getUsername()); datasource.setPassword(AESUtil.aesDecode(dB1DataSourceProperties.getPassword())); datasource.setDriverClassName(dB1DataSourceProperties.getDriverClassName()); ...... return datasource; } }
3.创建DynamicDataSource
创建自己的一个DynamicDataSource
类(名字任意)继承AbstractRoutingDataSource
,维护数据源,提供切换方法。
public class DynamicDataSource extends AbstractRoutingDataSource { /** * 如果不希望数据源在启动配置时就加载好,可以定制这个方法,从任何你希望的地方读取并返回数据源 * 比如从数据库、文件、外部接口等读取数据源信息,并最终返回一个DataSource实现类对象即可 */ @Override protected DataSource determineTargetDataSource() { return super.determineTargetDataSource(); } /** * 如果希望所有数据源在启动配置时就加载好,然后通过设置数据源Key值来切换数据,定制这个方法 */ @Override protected Object determineCurrentLookupKey() { return DynamicDataSourceContextHolder.getDataSourceKey(); } /** * 设置默认数据源 * * @param defaultDataSource */ public void setDefaultDataSource(Object defaultDataSource) { super.setDefaultTargetDataSource(defaultDataSource); } /** * 设置数据源 * * @param dataSources */ public void setDataSources(Map<Object, Object> dataSources) { super.setTargetDataSources(dataSources); // 将数据源的 key 放到数据源上下文的 key 集合中,用于切换时判断数据源是否有效 DynamicDataSourceContextHolder.addDataSourceKeys(dataSources.keySet()); } }
4.创建数据源上下文处理器DynamicDataSourceContextHolder
创建数据源上下文处理器DynamicDataSourceContextHolder
用以存储当前线程需要使用的数据源名称。
public class DynamicDataSourceContextHolder { private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>() { /** * 将 master 数据源的 key作为默认数据源的 key */ @Override protected String initialValue() { return "master"; } }; /** * 数据源的 key集合,用于切换时判断数据源是否存在 */ public static List<Object> dataSourceKeys = new ArrayList<>(); /** * 切换数据源 * * @param key */ public static void setDataSourceKey(String key) { contextHolder.set(key); } /** * 获取数据源 * * @return */ public static String getDataSourceKey() { return contextHolder.get(); } /** * 重置数据源 */ public static void clearDataSourceKey() { contextHolder.remove(); } /** * 判断是否包含数据源 * * @param key 数据源key * @return */ public static boolean containDataSourceKey(String key) { return dataSourceKeys.contains(key); } /** * 添加数据源keys * * @param keys * @return */ public static boolean addDataSourceKeys(Collection<? extends Object> keys) { return dataSourceKeys.addAll(keys); } }
5.创建数据源配置类DataSourceConfig
创建数据源配置类DataSourceConfig
,将所有数据源注入到spring容器
@Configuration @EnableAutoConfiguration(exclude = { DataSourceAutoConfiguration.class }) // 排除 DataSourceAutoConfiguration 的自动配置,避免环形调用 public class DataSourceConfig { @Autowired private MasterDataSourceConfig masterDataSourceConfig; @Autowired private DB1DataSourceConfig dB1DataSourceConfig; /** * 设置动态数据源为主数据源 * * @return */ @Bean @Primary public DynamicDataSource dataSource() { DynamicDataSource dynamicDataSource = new DynamicDataSource(); // 默认指定的数据源 dynamicDataSource.setDefaultDataSource(masterDataSourceConfig.masterDataSource()); // 将数据源设置进map Map<Object, Object> dataSourceMap = new HashMap<>(8); dataSourceMap.put(DataSourceEnum.MASTER.toString(), masterDataSourceConfig.masterDataSource()); dataSourceMap.put(DataSourceEnum.DB1.toString(), dB1DataSourceConfig.db1DataSource()); // 使用 Map 保存多个数据源,并设置到动态数据源对象中,这个值最终会在afterPropertiesSet中被设置到resolvedDataSources上 dynamicDataSource.setDataSources(dataSourceMap); return dynamicDataSource; } }
6.创建数据源类型枚举DataSourceEnum
public enum DataSourceEnum { /**默认类型*/ MASTER, /**DB1类型*/ DB1, ; }
7.创建自定义注解@DataSource
@Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface DataSource { /** * 数据源key值 * @return */ DataSourceEnum value(); }
8.创建切面DynamicDataSourceAspect
@Slf4j @Aspect @Order(-1) @Component public class DynamicDataSourceAspect { /** * 切换数据源 * * @param point * @param dataSource */ @Before("@annotation(dataSource))") public void switchDataSource(JoinPoint point, DataSource dataSource) { if (!DynamicDataSourceContextHolder.containDataSourceKey(dataSource.value().toString())) { log.info("DataSource [{}] doesn't exist, use default DataSource", dataSource.value()); } else { // 切换数据源 DynamicDataSourceContextHolder.setDataSourceKey(dataSource.value().toString()); log.info("Switch DataSource to [{}] in Method [{}]", DynamicDataSourceContextHolder.getDataSourceKey(), point.getSignature()); } } /** * 重置数据源 * * @param point * @param dataSource */ @After("@annotation(dataSource))") public void restoreDataSource(JoinPoint point, DataSource dataSource) { // 将数据源置为默认数据源 DynamicDataSourceContextHolder.clearDataSourceKey(); log.info("Restore DataSource to [{}] in Method [{}]", DynamicDataSourceContextHolder.getDataSourceKey(), point.getSignature()); } }
如何使用
@Override @DataSource(DataSourceEnum.DB1) public Page<AuditTaskDto> queryAuditTask(AuditTaskQuery query) { Page<AuditTaskDto> page = baseMapper.queryAuditTask(query); return page; }
如此就可以直接使用自定义的@DataSource注解来切换数据源啦~~~