MO_or掰泡馍式教学实现多数据源

一、引言 本文主要介绍一种优雅、安全、易用,支持事务管理的Spring Boot整合多数据源的方式,本文中不针对多数据源是什么、为什么用、什么时候用做介绍,小伙伴可根据自身情况酌情采纳
温馨提示:
基于以下知识有一定应用与实践后,能更好地理解本篇文章

  • Lambda、ThreadLocal、栈、队列、自定义注解
  • IoC、AOP、Druid、Maven、Spring Boot
由于本文主要讲解代码的具体实现,代码与注释较多,若感到阅读体验不佳,可配合开源代码,使用代码编辑器进行阅读
多数据源Gitee地址
对应项目模块为hei-dynamic-datasource
二、大致思路
  1. 通过配置类与yml配置文件先装配好默认数据源与多数据源
  2. 再通过自定义注解与AOP,找到目标类或方法,并指定其使用的数据源Key值
  3. 最后通过继承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); }}

四、项目结构 MO_or掰泡馍式教学实现多数据源
文章图片

五、代码示例及解析 5.1、maven相关依赖
org.springframework.boot spring-boot-starter-aop

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 = https://www.it610.com/article/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 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 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 getDynamicDataSource(){ Map dataSourcePropertiesMap = properties.getDatasource(); Map 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 datasource = new LinkedHashMap<>(); public Map getDatasource() { return datasource; }public void setDatasource(Map datasource) { this.datasource = datasource; } }

六、最后 【MO_or掰泡馍式教学实现多数据源】以上代码均已提交到开源项目中,对应项目模块为hei-dynamic-datasource
有需要的小伙伴可点击下方链接,clone代码到本地
多数据源Gitee地址

    推荐阅读