一、引言
本文次要介绍一种 优雅、平安、易用,反对事务管理 的 Spring Boot 整合多数据源的形式,本文中不针对多数据源是什么、为什么用、什么时候用做介绍,小伙伴可依据本身状况酌情驳回
舒适提醒:
基于以下常识有肯定利用与实际后,能更好地了解本篇文章
- Lambda、ThreadLocal、栈、队列、自定义注解
- IoC、AOP、Druid、Maven、Spring Boot
因为本文次要解说代码的具体实现,代码与正文较多,若感到浏览体验不佳,可配合开源代码,应用代码编辑器进行浏览
多数据源 Gitee 地址
对应我的项目模块为 hei-dynamic-datasource
二、大抵思路
- 通过配置类与 yml 配置文件先拆卸好默认数据源与多数据源
- 再通过自定义注解与 AOP,找到指标类或办法,并指定其应用的数据源 Key 值
- 最初通过继承 AbstractRoutingDataSource 类,返回经 AOP 解决后的数据源 Key 值,从第一步拆卸好的数据源中找到对应配置并利用
三、测试用例
在类或办法上加上 @DataSource(“value”)就能够指定不同数据源
@Service
// 办法上的注解比类上注解优先级更高
@DataSource("slave2")
public class DynamicDataSourceTestService {
@Autowired
private SysUserDao sysUserDao;
@Transactional
public void updateUser(Long id){SysUserEntity user = new SysUserEntity();
user.setUserId(id);
user.setMobile("13500000002");
sysUserDao.updateById(user);
}
@Transactional
@DataSource("slave1")
public void updateUserBySlave1(Long id){SysUserEntity user = new SysUserEntity();
user.setUserId(id);
user.setMobile("13500000001");
sysUserDao.updateById(user);
}
@DataSource("slave2")
@Transactional
public void updateUserBySlave2(Long id){SysUserEntity user = new SysUserEntity();
user.setUserId(id);
user.setMobile("13500000003");
sysUserDao.updateById(user);
// 测试事务
int i = 1/0;
}
}
@RunWith(SpringRunner.class)
@SpringBootTest
public class DynamicDataSourceTest {
@Autowired
private DynamicDataSourceTestService dynamicDataSourceTestService;
@Test
public void test(){
Long id = 1L;
dynamicDataSourceTestService.updateUser(id);
dynamicDataSourceTestService.updateUserBySlave1(id);
dynamicDataSourceTestService.updateUserBySlave2(id);
}
}
四、我的项目构造
五、代码示例及解析
5.1、maven 相干依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
5.2、yml 配置
dynamic:
datasource:
slave1:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/hei?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
username: root
password: 123456
slave2:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/hei?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
username: root
password: 123456
5.3、自定义注解(DataSource)
// 定义作用范畴为(办法、接口、类、枚举、注解)@Target({ElementType.METHOD, ElementType.TYPE})
// 保障运行时能被 JVM 或应用反射的代码应用
@Retention(RetentionPolicy.RUNTIME)
// 生成 Javadoc 时让应用了 @DataSource 这个注解的中央输入 @DataSource 这个注解或不同内容
@Documented
// 类继承中让子类继承父类 @DataSource 注解
@Inherited
public @interface DataSource {// @DataSource 注解里传的参,这里次要传配置文件中不同数据源的标识,如 @DataSource("slave1")
String value() default "";}
5.4、切面类(DataSourceAspect)
// 申明、定义切面类
@Aspect
@Component
/**
* 让该 bean 的执行程序优先级最高,并不能管制加载入 IoC 的程序
* 如果一个办法被多个 @Around 加强,那就能够应用该注解指定程序
*/
@Order(Ordered.HIGHEST_PRECEDENCE)
public class DataSourceAspect {protected Logger logger = LoggerFactory.getLogger(getClass());
// 指明告诉在应用 @DataSource 注解标注下才触发
@Pointcut("@annotation(io.renren.commons.dynamic.datasource.annotation.DataSource)" +
"|| @within(io.renren.commons.dynamic.datasource.annotation.DataSource)")
public void dataSourcePointCut() {}
// 对告诉办法的具体实现并采纳盘绕告诉设定办法与切面的执行程序,即在办法执行前和后触发
@Around("dataSourcePointCut()")
/**
* ProceedingJoinPoint 继承了 JoinPoint,相较于 JoinPoint 裸露了 proceed 办法,该类仅配合实现 around 告诉
* JoinPoint 类,用来获取代理类和被代理类的信息
* 调用 proceed 办法,示意继续执行指标办法(即加了 @DataSource 注解的办法)*/
public Object around(ProceedingJoinPoint point) throws Throwable {
// 通过反射取得被代理类(指标对象)Class targetClass = point.getTarget().getClass();
System.out.println("targetClass:" + targetClass);
/**
* 取得被代理类(指标对象)的办法签名
* signature 加签是一种简略、低成本、保障数据安全的形式
*/
MethodSignature signature = (MethodSignature) point.getSignature();
/**
* 取得被代理类(指标对象)的办法
* 这里取得办法也能够通过反射和 getTarget(),但步骤更多更简单
*/
Method method = signature.getMethod();
System.out.println("method:" + method);
// 取得被代理类(指标对象)的注解对象
DataSource targetDataSource = (DataSource) targetClass.getAnnotation(DataSource.class);
System.out.println("targetDataSource:" + targetDataSource);
// 取得被代理类(指标对象)的办法的注解对象
DataSource methodDataSource = method.getAnnotation(DataSource.class);
System.out.println("methodDataSource:" + methodDataSource);
// 判断被代理类(指标对象)的注解对象或者被代理类(指标对象)的办法的注解对象不为空
if (targetDataSource != null || methodDataSource != null) {
String value;
// 优先用被代理类(指标对象)的办法的注解对象的值进行后续赋值
if (methodDataSource != null) {value = methodDataSource.value();
} else {value = targetDataSource.value();
}
/**
* DynamicContextHolder 是本人实现的栈数据结构
* 将注解对象的值入栈
*/
DynamicContextHolder.push(value);
logger.debug("set datasource is {}", value);
}
try {
// 继续执行被代理类(指标对象)的办法
return point.proceed();} finally {
// 清空栈中数据
DynamicContextHolder.poll();
logger.debug("clean datasource");
}
}
}
5.5、多数据源上下文操作反对类(DynamicContextHolder)
public class DynamicContextHolder {
/**
* Lambda 结构 本地线程变量
* 用于防止屡次创立数据库连贯或者多线程应用同一个数据库连贯
* 缩小数据库连贯创立敞开对程序执行效率的影响与服务器压力
*
* 这里应用数组队列实现栈数据结构,实现函数部分状态所需的后进先出 "LIFO" 环境
*/
private static final ThreadLocal<Deque<String>> CONTEXT_HOLDER = ThreadLocal.withInitial(ArrayDeque::new);
/**
* 取得以后线程数据源
*
* @return 数据源名称
*/
public static String peek() {return CONTEXT_HOLDER.get().peek();}
/**
* 设置以后线程数据源
*
* @param dataSource 数据源名称
*/
public static void push(String dataSource) {CONTEXT_HOLDER.get().push(dataSource);
}
/**
* 清空以后线程数据源
*/
public static void poll() {Deque<String> deque = CONTEXT_HOLDER.get();
deque.poll();
if (deque.isEmpty()) {CONTEXT_HOLDER.remove();
}
}
}
5.6、多数据源类(DynamicDataSource)
public class DynamicDataSource extends AbstractRoutingDataSource {
/**
* 返回以后上下文环境的数据源 key
* 后续会依据这个 key 去找到对应的数据源属性
*/
@Override
protected Object determineCurrentLookupKey() {return DynamicContextHolder.peek();
}
}
5.7、多数据源配置类(DynamicDataSourceConfig)
/**
* 通过 @EnableConfigurationProperties(DynamicDataSourceProperties.class)
* 将 DynamicDataSourceProperties.class 注入到 Spring 容器中
*/
@Configuration
@EnableConfigurationProperties(DynamicDataSourceProperties.class)
public class DynamicDataSourceConfig {
// 这里 properties 曾经蕴含了 yml 配置中所对应的多数据源的属性了
@Autowired
private DynamicDataSourceProperties properties;
/**
* 通过 @ConfigurationProperties 与 @Bean,将 yml 配置文件对于 druid 中的属性配置,转化成 bean,并将 bean 注入到容器中
* 这里作用是通过 autowire 作为参数利用到上面的 dynamicDataSource()办法中
*/
@Bean
@ConfigurationProperties(prefix = "spring.datasource.druid")
public DataSourceProperties dataSourceProperties() {return new DataSourceProperties();
}
/**
* 通过 @Bean 告知 Spring 容器,该办法会返回 DynamicDataSource 对象
* 通过 dynamicDataSource()配置多数据源抉择逻辑,次要配置指标数据源和默认数据源
*/
@Bean
public DynamicDataSource dynamicDataSource(DataSourceProperties dataSourceProperties) {
// 实例化本人实现的多数据源,其中实现了获取以后线程数据源名称的办法
DynamicDataSource dynamicDataSource = new DynamicDataSource();
// 设置多数据源属性
dynamicDataSource.setTargetDataSources(getDynamicDataSource());
// 工厂办法创立 Druid 数据源
DruidDataSource defaultDataSource = DynamicDataSourceFactory.buildDruidDataSource(dataSourceProperties);
// 设置默认数据源属性
dynamicDataSource.setDefaultTargetDataSource(defaultDataSource);
return dynamicDataSource;
}
private Map<Object, Object> getDynamicDataSource(){Map<String, DataSourceProperties> dataSourcePropertiesMap = properties.getDatasource();
Map<Object, Object> targetDataSources = new HashMap<>(dataSourcePropertiesMap.size());
dataSourcePropertiesMap.forEach((k, v) -> {DruidDataSource druidDataSource = DynamicDataSourceFactory.buildDruidDataSource(v);
targetDataSources.put(k, druidDataSource);
});
return targetDataSources;
}
}
5.8、多数据源工厂类(DynamicDataSourceFactory)
// 这里拜访权限是包公有
class DynamicDataSourceFactory {static DruidDataSource buildDruidDataSource(DataSourceProperties properties) {DruidDataSource druidDataSource = new DruidDataSource();
druidDataSource.setDriverClassName(properties.getDriverClassName());
druidDataSource.setUrl(properties.getUrl());
druidDataSource.setUsername(properties.getUsername());
druidDataSource.setPassword(properties.getPassword());
druidDataSource.setInitialSize(properties.getInitialSize());
druidDataSource.setMaxActive(properties.getMaxActive());
druidDataSource.setMinIdle(properties.getMinIdle());
druidDataSource.setMaxWait(properties.getMaxWait());
druidDataSource.setTimeBetweenEvictionRunsMillis(properties.getTimeBetweenEvictionRunsMillis());
druidDataSource.setMinEvictableIdleTimeMillis(properties.getMinEvictableIdleTimeMillis());
druidDataSource.setMaxEvictableIdleTimeMillis(properties.getMaxEvictableIdleTimeMillis());
druidDataSource.setValidationQuery(properties.getValidationQuery());
druidDataSource.setValidationQueryTimeout(properties.getValidationQueryTimeout());
druidDataSource.setTestOnBorrow(properties.isTestOnBorrow());
druidDataSource.setTestOnReturn(properties.isTestOnReturn());
druidDataSource.setPoolPreparedStatements(properties.isPoolPreparedStatements());
druidDataSource.setMaxOpenPreparedStatements(properties.getMaxOpenPreparedStatements());
druidDataSource.setSharePreparedStatements(properties.isSharePreparedStatements());
try {druidDataSource.setFilters(properties.getFilters());
druidDataSource.init();} catch (SQLException e) {e.printStackTrace();
}
return druidDataSource;
}
}
5.9、数据源属性类(DataSourceProperties)
public class DataSourceProperties {
/**
* 可动静配置的数据库连贯属性
*/
private String driverClassName;
private String url;
private String username;
private String password;
/**
* Druid 默认参数
*/
private int initialSize = 2;
private int maxActive = 10;
private int minIdle = -1;
private long maxWait = 60 * 1000L;
private long timeBetweenEvictionRunsMillis = 60 * 1000L;
private long minEvictableIdleTimeMillis = 1000L * 60L * 30L;
private long maxEvictableIdleTimeMillis = 1000L * 60L * 60L * 7;
private String validationQuery = "select 1";
private int validationQueryTimeout = -1;
private boolean testOnBorrow = false;
private boolean testOnReturn = false;
private boolean testWhileIdle = true;
private boolean poolPreparedStatements = false;
private int maxOpenPreparedStatements = -1;
private boolean sharePreparedStatements = false;
private String filters = "stat,wall";
public String getDriverClassName() {return driverClassName;}
public void setDriverClassName(String driverClassName) {this.driverClassName = driverClassName;}
public String getUrl() {return url;}
public void setUrl(String url) {this.url = url;}
public String getUsername() {return username;}
public void setUsername(String username) {this.username = username;}
public String getPassword() {return password;}
public void setPassword(String password) {this.password = password;}
public int getInitialSize() {return initialSize;}
public void setInitialSize(int initialSize) {this.initialSize = initialSize;}
public int getMaxActive() {return maxActive;}
public void setMaxActive(int maxActive) {this.maxActive = maxActive;}
public int getMinIdle() {return minIdle;}
public void setMinIdle(int minIdle) {this.minIdle = minIdle;}
public long getMaxWait() {return maxWait;}
public void setMaxWait(long maxWait) {this.maxWait = maxWait;}
public long getTimeBetweenEvictionRunsMillis() {return timeBetweenEvictionRunsMillis;}
public void setTimeBetweenEvictionRunsMillis(long timeBetweenEvictionRunsMillis) {this.timeBetweenEvictionRunsMillis = timeBetweenEvictionRunsMillis;}
public long getMinEvictableIdleTimeMillis() {return minEvictableIdleTimeMillis;}
public void setMinEvictableIdleTimeMillis(long minEvictableIdleTimeMillis) {this.minEvictableIdleTimeMillis = minEvictableIdleTimeMillis;}
public long getMaxEvictableIdleTimeMillis() {return maxEvictableIdleTimeMillis;}
public void setMaxEvictableIdleTimeMillis(long maxEvictableIdleTimeMillis) {this.maxEvictableIdleTimeMillis = maxEvictableIdleTimeMillis;}
public String getValidationQuery() {return validationQuery;}
public void setValidationQuery(String validationQuery) {this.validationQuery = validationQuery;}
public int getValidationQueryTimeout() {return validationQueryTimeout;}
public void setValidationQueryTimeout(int validationQueryTimeout) {this.validationQueryTimeout = validationQueryTimeout;}
public boolean isTestOnBorrow() {return testOnBorrow;}
public void setTestOnBorrow(boolean testOnBorrow) {this.testOnBorrow = testOnBorrow;}
public boolean isTestOnReturn() {return testOnReturn;}
public void setTestOnReturn(boolean testOnReturn) {this.testOnReturn = testOnReturn;}
public boolean isTestWhileIdle() {return testWhileIdle;}
public void setTestWhileIdle(boolean testWhileIdle) {this.testWhileIdle = testWhileIdle;}
public boolean isPoolPreparedStatements() {return poolPreparedStatements;}
public void setPoolPreparedStatements(boolean poolPreparedStatements) {this.poolPreparedStatements = poolPreparedStatements;}
public int getMaxOpenPreparedStatements() {return maxOpenPreparedStatements;}
public void setMaxOpenPreparedStatements(int maxOpenPreparedStatements) {this.maxOpenPreparedStatements = maxOpenPreparedStatements;}
public boolean isSharePreparedStatements() {return sharePreparedStatements;}
public void setSharePreparedStatements(boolean sharePreparedStatements) {this.sharePreparedStatements = sharePreparedStatements;}
public String getFilters() {return filters;}
public void setFilters(String filters) {this.filters = filters;}
}
5.10、多数据源属性类(DynamicDataSourceProperties)
/**
* 通过 @ConfigurationProperties 指定读取 yml 的前缀关键字
* 配合 setDatasource(),即读取 dynamic.datasource 下的配置,将配置属性转化成 bean
* 容器执行程序是,在 bean 被实例化后,会调用后置解决,递归的查找属性,通过反射注入值
*
* 因为该类只在 DynamicDataSourceConfig 类中应用,没有其它中央用到,所以没有应用 @Component
* 而是在 DynamicDataSourceConfig 类中用 @EnableConfigurationProperties 定义为 bean
*/
@ConfigurationProperties(prefix = "dynamic")
public class DynamicDataSourceProperties {private Map<String, DataSourceProperties> datasource = new LinkedHashMap<>();
public Map<String, DataSourceProperties> getDatasource() {return datasource;}
public void setDatasource(Map<String, DataSourceProperties> datasource) {this.datasource = datasource;}
}
六、最初
以上代码均已提交到开源我的项目中,对应我的项目模块为 hei-dynamic-datasource
有须要的小伙伴可点击下方链接,clone 代码到本地
多数据源 Gitee 地址