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

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


核心配置代码:

1. pom

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <artifactId>spring-boot-mybatis-multiple-datasource</artifactId>
    <packaging>jar</packaging>

    <version>0.0.1-SNAPSHOT</version>
    <name>spring-boot-mybatis-multiple-datasource</name>
    <description>Demo Multiple Datasource for Spring Boot</description>

    <parent>
        <groupId>com.along</groupId>
        <artifactId>spring-boot-all</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
        <!--mybatis-->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>1.3.2</version>
        </dependency>
        <!-- 分页插件 -->
        <dependency>
            <groupId>com.github.pagehelper</groupId>
            <artifactId>pagehelper-spring-boot-starter</artifactId>
            <version>1.2.10</version>
        </dependency>
        <!-- mybatis-generator-core 反向生成java代码-->
        <dependency>
            <groupId>org.mybatis.generator</groupId>
            <artifactId>mybatis-generator-core</artifactId>
            <version>1.3.5</version>
        </dependency>
        <!-- alibaba的druid数据库连接池 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.1.10</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
            <!--mybatis逆向工程maven插件-->
            <plugin>
                <groupId>org.mybatis.generator</groupId>
                <artifactId>mybatis-generator-maven-plugin</artifactId>
                <version>1.3.5</version>
                <configuration>
                    <!--允许移动生成的文件-->
                    <verbose>true</verbose>
                    <!--允许覆盖生成的文件-->
                    <overwrite>true</overwrite>
                    <!--配置文件的路径 默认resources目录下-->
                    <configurationFile>src/main/resources/generatorConfig.xml</configurationFile>
                </configuration>
                <!--插件依赖的jar包-->
                <dependencies>
                    <dependency>
                        <groupId>mysql</groupId>
                        <artifactId>mysql-connector-java</artifactId>
                        <version>8.0.13</version>
                    </dependency>
                </dependencies>
            </plugin>
        </plugins>

    </build>
</project>

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<Object, Object> 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<String> 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<Person> findAllPerson(int pageNum, int pageSize) {
        PageHelper.startPage(pageNum, pageSize);
        PersonExample example = new PersonExample();
        List<Person> personList = personMapper.selectByExample(example);
        return new PageInfo<>(personList);
    }

    @DataSource(ContextConst.DataSourceType.PROD) // 指定该方法使用prod数据源
    @Override
    public PageInfo<Person> findByName(String name) {
        PersonExample example = new PersonExample();
        PersonExample.Criteria criteria = example.createCriteria();
        criteria.andNameEqualTo(name);
        List<Person> 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<Person> list) {
        return personMapper.insertBatchSelective(list);
    }

    @Override
    public int updateBatch(List<Person> 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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,084评论 6 503
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,623评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,450评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,322评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,370评论 6 390
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,274评论 1 300
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,126评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,980评论 0 275
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,414评论 1 313
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,599评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,773评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,470评论 5 344
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,080评论 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,713评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,852评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,865评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,689评论 2 354

推荐阅读更多精彩内容

  • spring官方文档:http://docs.spring.io/spring/docs/current/spri...
    牛马风情阅读 1,672评论 0 3
  • 命令行操作 xcrun simctl list 查看模拟器列表open /Applications/Xcode.a...
    cocoaroger阅读 725评论 0 0
  • 对你的爱一直都未曾改变,只是没有了当初的勇气。 很想很想参与关于你的一切,可是我不能对你说。 也曾幻想着拥有你的甜...
    简约为伴阅读 235评论 0 0
  • 当我站在拥挤的地铁车厢 我总觉得 自己在发光 闪闪发光 挤在我旁边的大叔也是这样想的。
    留子尧阅读 173评论 0 4