一、引言

本文主要介绍一种优雅、安全、易用,支持事务管理的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);
    }

}

四、项目结构

多数据源项目结构

五、代码示例及解析

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构造 本地线程变量
     * 用于避免多次创建数据库连接或者多线程使用同一个数据库连接
     * 减少数据库连接创建关闭对程序执行效率的影响与服务器压力
     *
     * 这里使用数组队列实现栈数据结构
     * 主要为了解决调用链执行顺序问题,比如这里有ABC三个方法,它们分别使用的是不同数据源
     * A调用B,B调用C,这里数据源切换时就要保证,C方法数据源要最先被清除,再是B,最后A,即后进先出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地址


MO_or
25 声望75 粉丝

小菜鸟的成长地。