SpringBoot2(四):多数据源自动切换

类似的实现百度一大把,不过别人实现了的照搬到自己电脑上不见的也可以运行,依然遇到很多问题,这回自己实现一把,亲测可用,文末附源码地址。


核心配置代码:

1. pom



    4.0.0
    spring-boot-mybatis-multiple-datasource
    jar

    0.0.1-SNAPSHOT
    spring-boot-mybatis-multiple-datasource
    Demo Multiple Datasource for Spring Boot

    
        com.along
        spring-boot-all
        0.0.1-SNAPSHOT
    

    
        UTF-8
        UTF-8
        1.8
    

    
        
            org.springframework.boot
            spring-boot-starter-jdbc
        
        
            org.springframework.boot
            spring-boot-starter-web
        
        
            org.springframework.boot
            spring-boot-starter-aop
        
        
        
            org.mybatis.spring.boot
            mybatis-spring-boot-starter
            1.3.2
        
        
        
            com.github.pagehelper
            pagehelper-spring-boot-starter
            1.2.10
        
        
        
            org.mybatis.generator
            mybatis-generator-core
            1.3.5
        
        
        
            com.alibaba
            druid-spring-boot-starter
            1.1.10
        

        
            mysql
            mysql-connector-java
            runtime
        
        
            org.springframework.boot
            spring-boot-starter-test
            test
        
    

    
        
            
                org.springframework.boot
                spring-boot-maven-plugin
            
            
            
                org.mybatis.generator
                mybatis-generator-maven-plugin
                1.3.5
                
                    
                    true
                    
                    true
                    
                    src/main/resources/generatorConfig.xml
                
                
                
                    
                        mysql
                        mysql-connector-java
                        8.0.13
                    
                
            
        

    

2. application.yml

注意:springboot2开始mysql驱动变为com.mysql.cj.jdbc.Driver

server:
  port: 8080

spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    primary:
      driver-class-name: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://localhost:3306/test?useSSL=false&useUnicode=true&characterEncoding=utf8&autoReconnect=true&rewriteBatchedStatements=TRUE&serverTimezone=UTC&allowMultiQueries=true
      username: root
      password: root
      type: com.alibaba.druid.pool.DruidDataSource
      #druid相关配置
      druid:
        #监控统计拦截的filters
        filters: stat
        #配置初始化大小/最小/最大
        initial-size: 1
        min-idle: 1
        max-active: 20
        #获取连接等待超时时间
        max-wait: 60000
        #间隔多久进行一次检测,检测需要关闭的空闲连接
        time-between-eviction-runs-millis: 60000
        #一个连接在池中最小生存的时间
        min-evictable-idle-time-millis: 300000
        validation-query: SELECT 'x'
        test-while-idle: true
        test-on-borrow: false
        test-on-return: false
        #打开PSCache,并指定每个连接上PSCache的大小。oracle设为true,mysql设为false。分库分表较多推荐设置为false
        pool-prepared-statements: false
        max-pool-prepared-statement-per-connection-size: 20
    local:
      driver-class-name: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://localhost:3306/test2?useSSL=false&useUnicode=true&characterEncoding=utf8&autoReconnect=true&rewriteBatchedStatements=TRUE&serverTimezone=UTC&allowMultiQueries=true
      username: root
      password: root
      type: com.alibaba.druid.pool.DruidDataSource
    prod:
      driver-class-name: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://localhost:3306/test3?useSSL=false&useUnicode=true&characterEncoding=utf8&autoReconnect=true&rewriteBatchedStatements=TRUE&serverTimezone=UTC&allowMultiQueries=true
      username: root
      password: root
      type: com.alibaba.druid.pool.DruidDataSource

mybatis:
  #映射文件所在路径
  mapper-locations: classpath:com.along.dao/*.xml
  #pojo类所在包路径
  type-aliases-package: com.along.entity
  configuration:
    #配置项:开启下划线到驼峰的自动转换. 作用:将数据库字段根据驼峰规则自动注入到对象属性。
    map-underscore-to-camel-case: true

#pagehelper
pagehelper:
  helperDialect: mysql
  reasonable: true
  supportMethodsArguments: true
  params: count=countSql

logging:
  level:
    #打印SQL信息
    com.along.dao: debug

3. 数据源配置类 MultipleDataSourceConfig.java

/**
 * 数据源配置
 */
@Configuration
public class MultipleDataSourceConfig {

    @Bean(name = "dataSourcePrimary")
    @ConfigurationProperties(prefix = "spring.datasource.primary")
    public DataSource primaryDataSource() {
        return new DruidDataSource();
    }

    @Bean(name = "dataSourceLocal")
    @ConfigurationProperties(prefix = "spring.datasource.local")
    public DataSource localDataSource() {
        return new DruidDataSource();
    }

    @Bean(name = "dataSourceProd")
    @ConfigurationProperties(prefix = "spring.datasource.prod")
    public DataSource prodDataSource() {
        return new DruidDataSource();
    }

    @Primary
    @Bean(name = "dynamicDataSource")
    public DataSource dynamicDataSource() {
        DynamicDataSource dynamicDataSource = new DynamicDataSource();
        //配置默认数据源
        dynamicDataSource.setDefaultTargetDataSource(primaryDataSource());

        //配置多数据源
        HashMap dataSourceMap = new HashMap<>();
        dataSourceMap.put(ContextConst.DataSourceType.PRIMARY.name(), primaryDataSource());
        dataSourceMap.put(ContextConst.DataSourceType.LOCAL.name(), localDataSource());
        dataSourceMap.put(ContextConst.DataSourceType.PROD.name(), prodDataSource());
        dynamicDataSource.setTargetDataSources(dataSourceMap); // 该方法是AbstractRoutingDataSource的方法
        return dynamicDataSource;
    }

    /**
     * 配置@Transactional注解事务
     *
     * @return
     */
    @Bean
    public PlatformTransactionManager transactionManager() {
        return new DataSourceTransactionManager(dynamicDataSource());
    }
}

4. 数据源持有类 DataSourceContextHolder.java

/**
 * 数据源持有类
 */
public class DataSourceContextHolder {

    private static final Logger logger = LoggerFactory.getLogger(DataSourceContextHolder.class);

    private static final ThreadLocal contextHolder = new ThreadLocal<>();

    public static void setDataSource(String dbType){
        logger.info("切换到[{}]数据源",dbType);
        contextHolder.set(dbType);
    }

    public static String getDataSource(){
        return contextHolder.get();
    }

    public static void clearDataSource(){
        contextHolder.remove();
    }
}

5. 数据源路由实现类 DynamicDataSource.java

这是实现动态数据源切换的核心

/**
 * 数据源路由实现类
 */
public class DynamicDataSource extends AbstractRoutingDataSource {

    private static final Logger logger = LoggerFactory.getLogger(DynamicDataSource.class);

    @Override
    protected Object determineCurrentLookupKey() {
        String dataSource = DataSourceContextHolder.getDataSource();
        if (dataSource == null) {
            logger.info("当前数据源为[primary]");
        } else {
            logger.info("当前数据源为{}", dataSource);
        }
        return dataSource;
    }

}

6. 自定义切换数据源的注解

/**
 * 切换数据源的注解
 */
@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSource {

    ContextConst.DataSourceType value() default ContextConst.DataSourceType.PRIMARY;

}

7. 数据源枚举类

/**
 * 上下文常量
 */
public interface ContextConst {

    /**
     * 数据源枚举
     */
    enum DataSourceType {
        PRIMARY, LOCAL, PROD, TEST
    }
}

8. 定义切换数据源的切面,为注解服务

/**
 * 切换数据源的切面
 */
@Component
@Aspect
@Order(1) //这是关键,要让该切面调用先于AbstractRoutingDataSource的determineCurrentLookupKey()AbstractRoutingDataSource的determineCurrentLookupKey()
public class DynamicDataSourceAspect {

    /**
     * within 对象级别,用在类上
     * annotation 方法级别,用在方法上
     */
    @Pointcut("@annotation(com.along.annotation.DataSource)")
    public void dataSourcePointCut() {

    }

    @Around("dataSourcePointCut()")
    public Object around(ProceedingJoinPoint point) throws Throwable {
        // 切换数据源
        try {
            // 获取类上定义的DataSource注解
            DataSource annotationOfClass = point.getTarget().getClass().getAnnotation(DataSource.class);
            // 获取方法名
            String methodName = point.getSignature().getName();
            // 拿到方法对应的参数类型
            Class[] parameterTypes = ((MethodSignature) point.getSignature()).getParameterTypes();
            // 根据类、方法、参数类型(重载)获取到方法的具体信息
            Method method = point.getTarget().getClass().getMethod(methodName, parameterTypes);
            // 拿到方法定义的DataSource注解信息
            DataSource methodAnnotation = method.getAnnotation(DataSource.class);
            // 方法上的注解优先于类上的注解
            methodAnnotation = methodAnnotation == null ? annotationOfClass : methodAnnotation;

            // 获取DataSource注解指定的数据源
            ContextConst.DataSourceType dataSourceType = methodAnnotation != null ? methodAnnotation.value() : ContextConst.DataSourceType.JK;

            // 设置数据源
            DataSourceContextHolder.setDataSource(dataSourceType.name());
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        }

        // 执行方法
        try {
            return point.proceed();
        } finally {
            DataSourceContextHolder.clearDataSource();
        }
    }
}

9. 修改启动类

//排除DataSource自动配置类,否则会默认自动配置,不会使用我们自定义的DataSource,并且启动报错
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
@MapperScan({"com.along.dao"}) // 扫描包路径
public class SpringBootMybatisMultipleDatasourceApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringBootMybatisMultipleDatasourceApplication.class, args);
    }

}

10. 排除DataSource自动配置类

//排除DataSource自动配置类,否则会默认自动配置,不会使用我们自定义的DataSource,并且启动报错
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
@MapperScan({"com.along.dao"}) // 扫描包路径
public class SpringBootMybatisMultipleDatasourceApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringBootMybatisMultipleDatasourceApplication.class, args);
    }

}

11. 正常使用

在方法上通过注解@DataSource指定该方法所用的数据源,如果没有使用注解指定则使用默认数据源
下面是在service实现类中的应用:

/**
 * @Description: service实现
 * @Author along
 * @Date 2018/12/28 17:44
 */
@Service(value = "personService")
@Transactional
public class PersonServiceImpl implements PersonService {

    private PersonMapper personMapper;

    @Autowired
    public PersonServiceImpl(@Qualifier("personMapper") PersonMapper personMapper) {
        this.personMapper = personMapper;
    }

    @Override
    public Integer add(Person person) {
        return personMapper.insert(person);
    }

    @Override
    public PageInfo findAllPerson(int pageNum, int pageSize) {
        PageHelper.startPage(pageNum, pageSize);
        PersonExample example = new PersonExample();
        List personList = personMapper.selectByExample(example);
        return new PageInfo<>(personList);
    }

    @DataSource(ContextConst.DataSourceType.PROD) // 指定该方法使用prod数据源
    @Override
    public PageInfo findByName(String name) {
        PersonExample example = new PersonExample();
        PersonExample.Criteria criteria = example.createCriteria();
        criteria.andNameEqualTo(name);
        List personList = personMapper.selectByExample(example);
        return new PageInfo<>(personList);
    }

    @DataSource(ContextConst.DataSourceType.LOCAL) // 指定该方法使用local数据源
    @Override
    public int insert(Person person) {

        return personMapper.insert(person);
    }

    @Override
    public int insertBatch(List list) {
        return personMapper.insertBatchSelective(list);
    }

    @Override
    public int updateBatch(List list) {
        return personMapper.updateBatchByPrimaryKeySelective(list);

    }

}

12. 遇到的问题与解决办法

1. spring中加注解方法被同一个类内部方法调用导致AOP失效

场景描述:如下面代码所示

public class A {
    //......
    
    @DataSource(ContextConst.DataSourceType.LOCAL) // 指定该方法使用local数据源
    public void serviceA() {
        ......
    }

     @DataSource(ContextConst.DataSourceType.PROD) // 指定该方法使用prod数据源
    public void serviceB() {
        ......
        serviceA()
        ......
    }
}

上述代码如果直接请求serviceA(),能成功切换数据源正常调用,但是如果请求serviceB(),serviceB()里调用serviceA(),那么serivceB的数据源能正常切换,但是调用serviceA()时切换数据源就会失败。
解决办法:使用AopContext.currentProxy()调用

public class A {
    public void serviceB() {
            ......
            //此处调用的就是代理后的方法
            ((A)AopContext.currentProxy()).serviceA();
            ......
    }
}

使用AopContext.currentProxy()注意必须在程序启动时开启EnableAspectJAutoProxy注解,设置代理暴露方式为true,如下面所示:

/**
 * EnableAspectJAutoProxy注解两个参数作用分别为:
 *
 * 一个是控制aop的具体实现方式,为true的话使用cglib,为false的话使用java的Proxy,默认为false。
 * 第二个参数控制代理的暴露方式,解决内部调用不能使用代理的场景,默认为false。
 */
@EnableAspectJAutoProxy(proxyTargetClass = true, exposeProxy = true)
@SpringBootApplication
public class SpringBootMybatisMultipleDatasourceApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringBootMybatisMultipleDatasourceApplication.class, args);
    }
}

这是我觉得比较方便的解决方案,当然解决方案不止一种,详细解决思路参考链接:spring中加注解方法被同一个类内部方法调用导致AOP失效的解决方案

13.终极大招

上方的方案终究比较捡漏,现实中已经有人给我们造好了轮子,点击下方传送门
dynamic-datasource-spring-boot-starter: 基于 SpringBoot 多数据源 动态数据源 主从分离 快速启动器 支持分布式事务 (gitee.com)
如果配置后项目启动失败,在启动类上加上如下配置,使得项目启动不会默认加载数据源,使用时才加载。

@SpringBootApplication(exclude = DruidDataSourceAutoConfigure.class)

源码地址

https://github.com/alonglong/spring-boot-all/tree/master/spring-boot-mybatis-multiple-datasource

你可能感兴趣的:(SpringBoot2(四):多数据源自动切换)