一、引言

本文次要介绍一种优雅、平安、易用,反对事务管理的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)@SpringBootTestpublic 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注解@Inheritedpublic @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地址