开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 1 天,点击查看活动详情 项目中一直在使用数据源,就一个注解,实现数据源切换,感觉很屌。特此,学习一下,自己从 0 搭建一个支持多数据源切换的学习 demo。
搭建基础的 SpringBoot 项目,然后引入下面的 jar 包
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.8</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.3.0</version>
</dependency>
以下的代码是基于项目已经能正常访问数据库,只是想多一个数据源的情况。
工具类
通过实例名获取实例 Bean
@Component
public final class SpringUtils implements BeanFactoryPostProcessor, ApplicationContextAware
{
/** Spring应用上下文环境 */
private static ConfigurableListableBeanFactory beanFactory;
private static ApplicationContext applicationContext;
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException
{
SpringUtils.beanFactory = beanFactory;
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException
{
SpringUtils.applicationContext = applicationContext;
}
/**
* 获取对象
*
*
*/
@SuppressWarnings("unchecked")
public static <T> T getBean(String name) throws BeansException
{
return (T) beanFactory.getBean(name);
}
}
多数据源枚举
public enum DataSourceType {
master,
slave
}
配置类
DruidConfig
- 实现多数据源的 Bean,几个数据源就得写几个
- 设置到基于实现AbstractRoutingDataSource抽象类的 DynamicDataSource 类,注入容器。
@Configuration
public class DruidConfig {
@Bean
@ConfigurationProperties("spring.datasource.druid.master")
public DataSource masterDataSource(DruidProperties druidProperties) {
DruidDataSource dataSource = DruidDataSourceBuilder.create().build();
return druidProperties.dataSource(dataSource);
}
/**
* @param druidProperties
* @return
* @ConditionalOnProperty 通过配置文件的属性值,来判定此 Bean 是否会被注入
*/
//如果有多个其他数据源,则写多个即可
@Bean
@ConfigurationProperties("spring.datasource.druid.slave")
@ConditionalOnProperty(prefix = "spring.datasource.druid.slave", name = "enabled", havingValue = "true")
public DataSource slaveDataSource(DruidProperties druidProperties) {
DruidDataSource dataSource = DruidDataSourceBuilder.create().build();
return druidProperties.dataSource(dataSource);
}
@Bean(name = "dynamicDataSource")
@Primary
public DynamicDataSource dataSource(DataSource masterDataSource) {
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put(DataSourceType.master.name(), masterDataSource);
//若有多个,则下面 set 多个
setDataSource(targetDataSources, DataSourceType.slave.name(), "slaveDataSource");
return new DynamicDataSource(masterDataSource, targetDataSources);
}
/**
* 设置数据源
*
* @param targetDataSources
* @param sourceName
* @param beanName
*/
public void setDataSource(Map<Object, Object> targetDataSources, String sourceName, String beanName) {
try {
DataSource dataSource = SpringUtils.getBean(beanName);
targetDataSources.put(sourceName, dataSource);
} catch (Exception e) {
}
}
}
DynamicDataSource
继承了由 Spring 提供的轻量级多数据源切换类:AbstractRoutingDataSource,并实现了determineCurrentLookupKey()方法
public class DynamicDataSource extends AbstractRoutingDataSource {
public DynamicDataSource(DataSource defaultTargetDataSource, Map<Object, Object> targetDataSources) {
super.setDefaultTargetDataSource(defaultTargetDataSource);
super.setTargetDataSources(targetDataSources);
super.afterPropertiesSet();
}
//确定当前查找键
@Override
protected Object determineCurrentLookupKey() {
return DynamicDataSourceContextHolder.getDataSourceType();
}
}
DynamicDataSourceContextHolder
提供一个 ThreadLocal 保证每个使用切换数据源的线程拥有独立的副本
public class DynamicDataSourceContextHolder {
public static final Logger log = LoggerFactory.getLogger(DynamicDataSourceContextHolder.class);
/**
* ThreadLocal 为每个使用该变量的线程提供独立的变量副本
* 所以每一个线程都可以独立的改变自己的副本,而不会影响其他线程所对应的副本
*/
private static final ThreadLocal<String> CONTEXT_HOLDER=new ThreadLocal<>();
/**
* 设置数据源的变量
* @param dsType
*/
public static void setDataSourceType(String dsType){
log.info("切换到 {} 数据源",dsType);
CONTEXT_HOLDER.set(dsType);
}
/**
* 获得数据源的变量
* @return
*/
public static String getDataSourceType(){
return CONTEXT_HOLDER.get();
}
/**
* 清空数据源变量
*/
public static void clearDataSourceType()
{
CONTEXT_HOLDER.remove();
}
}
注解实现
juejin.cn/post/718618… 可以参考以前写切面实战内容
注解 DataSource
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface DataSource {
public DataSourceType value() default DataSourceType.master;
}
切面实现
@Aspect
@Order(1)
@Component
public class DataSourceAspect {
@Pointcut("@annotation(com.practice.springdemo.datasourceConfig.annotation.DataSource)"
+ "|| @within(com.practice.springdemo.datasourceConfig.annotation.DataSource)")
public void dsPointCut() {
}
/**
* 获取需要切换的数据源
*
* @param point
* @return
*/
public DataSource getDataSource(ProceedingJoinPoint point) {
MethodSignature signature = (MethodSignature) point.getSignature();
DataSource dataSource = AnnotationUtils.findAnnotation(signature.getMethod(), DataSource.class);
if (Objects.nonNull(dataSource)) {
return dataSource;
}
return AnnotationUtils.findAnnotation(signature.getDeclaringType(), DataSource.class);
}
@Around("dsPointCut()")
public Object around(ProceedingJoinPoint point) throws Throwable {
DataSource dataSource = getDataSource(point);
if (Objects.nonNull(dataSource)) {
DynamicDataSourceContextHolder.setDataSourceType(dataSource.value().name());
}
try {
return point.proceed();
} finally {
DynamicDataSourceContextHolder.clearDataSourceType();
}
}
}
使用方式
直接在方法上面加上你指定的数据源即可 @DataSource(DataSourceType.slave)
@DataSource(DataSourceType.slave)
@Override
public void dataSlave() {
Map map = dataSourceMapper.dataSlave();
System.out.println("slave:" + map);
}
总结
- AbstractRoutingDataSource 类实现可动态路由的数据源
- DruidConfig 在注入动态数据源的时候必须使用 @Primary 修饰,这样才能先初始化。否则报错 Property 'sqlSessionFactory' or 'sqlSessionTemplate' are required。
- @ConditionalOnProperty 注解的使用
- 实现 InitializingBean 类的 afterPropertiesSet() 方法能在项目初始化的时候运行(AbstractRoutingDataSource 实现了)