小工具      在线工具  汉语词典  dos游戏  css  js  c++  java

Spring Boot 优雅配置多数据源

Java,spring boot,java,spring 额外说明

收录于:52天前

大约在19年的这个时候,老同事公司在做医疗系统,需要和HIS系统对接一些信息,比如患者、医护、医嘱、科室等信息。但是起初并不知道如何与HIS无缝对接,于是向我取经。

最终经过讨论采用了视图对接的方式,大致就是HIS系统提供视图,他们进行对接。

什么是多数据源?

最常见的单一应用中最多涉及到一个数据库,即是一个数据源(Datasource)。那么顾名思义,多数据源就是在一个单一应用中涉及到了两个及以上的数据库了。

其实这个定义在配置数据源的时候就已经很明确了,比如下面的代码:

@Bean(name = "dataSource")
    public DataSource dataSource() {
        DruidDataSource druidDataSource = new DruidDataSource();
        druidDataSource.setUrl(url);
        druidDataSource.setUsername(username);
        druidDataSource.setDriverClassName(driverClassName);
        druidDataSource.setPassword(password);
        return druidDataSource;
    }

urlusernamepassword这三个属性已经唯一确定了一个数据库了,DataSource则是依赖这三个创建出来的。则多数据源即是配置多个DataSource(暂且这么理解)。

什么时候使用多个数据源?

正如前言介绍到的一个场景,相信大多数做过医疗系统的都会和HIS打交道,为了简化护士以及医生的操作流程,必须要将必要的信息从HIS系统对接过来,据我了解的大致有两种方案如下:

  1. HIS提供视图,比如医护视图、患者视图等,而此时其他系统只需要定时的从HIS视图中读取数据同步到自己数据库中即可。

  2. HIS提供接口,无论是webService还是HTTP形式都是可行的,此时其他系统只需要按照要求调接口即可。

很明显第一种方案涉及到了至少两个数据库了,一个是HIS数据库,一个自己系统的数据库,在单一应用中必然需要用到多数据源的切换才能达到目的。

当然,多数据源的使用场景有很多种,以上只是一个简单的场景。

集成单一数据源

本文使用阿里的数据库连接池druid,添加依赖如下:

<!--druid连接池-->
<dependency>
   <groupId>com.alibaba</groupId>
   <artifactId>druid-spring-boot-starter</artifactId>
   <version>1.1.9</version>
</dependency>

阿里的数据库连接池非常强大,比如数据监控数据库加密等等内容,本文仅仅演示与Spring Boot整合的过程,一些其他的功能后续可以自己研究添加。

Druid连接池的starter的自动配置类是DruidDataSourceAutoConfigure,类上标注如下一行注解:

@EnableConfigurationProperties({DruidStatProperties.class, DataSourceProperties.class})

@EnableConfigurationProperties这个注解使得配置文件中的配置生效并且映射到指定类的属性。

DruidStatProperties中指定的前缀是spring.datasource.druid,这个配置主要是用来设置连接池的一些参数。

DataSourceProperties中指定的前缀是spring.datasource,这个主要是用来设置数据库的urlusernamepassword等信息。

因此我们只需要在全局配置文件中指定数据库的一些配置以及连接池的一些配置信息即可,前缀分别是spring.datasource.druidspring.datasource,以下是个人随便配置的(application.properties):

spring.datasource.url=jdbc\:mysql\://120.26.101.xxx\:3306/xxx?useUnicode\=true&characterEncoding\=UTF-8&zeroDateTimeBehavior\=convertToNull&useSSL\=false&allowMultiQueries\=true&serverTimezone=Asia/Shanghai
spring.datasource.username=root
spring.datasource.password=xxxx
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
#初始化连接大小
spring.datasource.druid.initial-size=0
#连接池最大使用连接数量
spring.datasource.druid.max-active=20
#连接池最小空闲
spring.datasource.druid.min-idle=0
#获取连接最大等待时间
spring.datasource.druid.max-wait=6000
spring.datasource.druid.validation-query=SELECT 1
#spring.datasource.druid.validation-query-timeout=6000
spring.datasource.druid.test-on-borrow=false
spring.datasource.druid.test-on-return=false
spring.datasource.druid.test-while-idle=true
#配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
spring.datasource.druid.time-between-eviction-runs-millis=60000
#置一个连接在池中最小生存的时间,单位是毫秒
spring.datasource.druid.min-evictable-idle-time-millis=25200000
#spring.datasource.druid.max-evictable-idle-time-millis=
#打开removeAbandoned功能,多少时间内必须关闭连接
spring.datasource.druid.removeAbandoned=true
#1800秒,也就是30分钟
spring.datasource.druid.remove-abandoned-timeout=1800
#<!-- 1800秒,也就是30分钟 -->
spring.datasource.druid.log-abandoned=true
spring.datasource.druid.filters=mergeStat

在全局配置文件application.properties文件中配置以上的信息即可注入一个数据源到Spring Boot中。其实这仅仅是一种方式,下面介绍另外一种方式。

在自动配置类中DruidDataSourceAutoConfigure中有如下一段代码:

 @Bean(initMethod = "init")
    @ConditionalOnMissingBean
    public DataSource dataSource() {
        LOGGER.info("Init DruidDataSource");
        return new DruidDataSourceWrapper();
    }

@ConditionalOnMissingBean@Bean这两个注解的结合,意味着我们可以覆盖,只需要提前在IOC中注入一个DataSource类型的Bean即可。

因此,我们可以在自定义配置类中定义如下配置:

/**
     * @Bean:向IOC容器中注入一个Bean
     * @ConfigurationProperties:使得配置文件中以spring.datasource为前缀的属性映射到Bean的属性中
     * @return
     */
    @ConfigurationProperties(prefix = "spring.datasource")
    @Bean
    public DataSource dataSource(){
        //做一些其他的自定义配置,比如密码加密等......
        return new DruidDataSource();
    }

以上介绍了两种数据源的配置方法。第一种比较简单,第二种适合扩展,可以根据需要选择。

集成Mybatis

将Mybatis与Spring Boot集成其实非常简单。只需几个简单的步骤即可完成。首先,添加依赖:

<dependency>
     <groupId>org.mybatis.spring.boot</groupId>
     <artifactId>mybatis-spring-boot-starter</artifactId>
     <version>2.0.0</version>
</dependency>

第二步找到自动配置类MybatisAutoConfiguration,有如下一行代码:

@EnableConfigurationProperties(MybatisProperties.class)

老套路了,全局配置文件中配置前缀为mybatis的配置将会映射到该类中的属性。

可配置的东西很多,比如XML文件的位置类型处理器等等,如下简单的配置:

mybatis.type-handlers-package=com.demo.typehandler
mybatis.configuration.map-underscore-to-camel-case=true

如果需要通过包扫描的方式注入Mapper,则需要在配置类上加入一个注解:@MapperScan,其中的value属性指定需要扫描的包。

直接在全局配置文件中配置各种属性是一种比较简单的方式。事实上,任何组件的集成至少有两种配置方法。下面介绍如何配置配置类。

MybatisAutoConfiguration自动配置类有如下一断代码:

 @Bean
  @ConditionalOnMissingBean
  public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {}

@ConditionalOnMissingBean@Bean真是老搭档了,意味着我们又可以覆盖,只需要在IOC容器中注入SqlSessionFactory(Mybatis六剑客之一生产者)

只需将其注入到自定义配置类中即可,如下:

/**
     * 注入SqlSessionFactory
     */
    @Bean("sqlSessionFactory1")
    public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dataSource);
        sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:/mapper/**/*.xml"));
        org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration();
        // 自动将数据库中的下划线转换为驼峰格式
        configuration.setMapUnderscoreToCamelCase(true);
        configuration.setDefaultFetchSize(100);
        configuration.setDefaultStatementTimeout(30);
        sqlSessionFactoryBean.setConfiguration(configuration);
        return sqlSessionFactoryBean.getObject();
    }

以上介绍了Mybatis的两种配置方式。其实大多数场景下第一种方法就足够了。至于为什么要引入第二种方法呢?当然,它是为多数据源的整合做好准备的。

MybatisAutoConfiguration中有一行很重要的代码,如下:

@ConditionalOnSingleCandidate(DataSource.class)

@ConditionalOnSingleCandidate这个注解的意思是当IOC容器中只有一个候选Bean的实例才会生效。

Mybatis自动配置类中这行代码是什么意思?下面介绍一下,哈哈哈~

如何整合多个数据源?

上面留下的问题:为什么Mybatis自动配置上标注了下面这行代码:

@ConditionalOnSingleCandidate(DataSource.class)

上面一行代码的含义是:这个自动配置类只有在IOC容器中只有一个数据源DataSource时才会生效。

哦?如果我们这样做的话,是不是就不能使用Mybatis进行多数据源了呢?

可能大家会有一个误解,认为多数据源就是多个的DataSource并存的,当然这样说也不是不正确。

多数据源的情况下并不是多个数据源并存的,Spring提供了AbstractRoutingDataSource这样一个抽象类,使得能够在多数据源的情况下任意切换,相当于一个动态路由的作用,作者称之为动态数据源。因此Mybatis只需要配置这个动态数据源即可。

什么是动态数据源?

动态数据源简单的说就是能够自由切换的数据源,类似于一个动态路由的感觉,Spring 提供了一个抽象类AbstractRoutingDataSource,这个抽象类中哟一个属性,如下:

私有 Map<Object, Object> targetDataSources;

targetDataSources是一个Map结构,所有需要切换的数据源都存放在其中,根据指定的KEY进行切换。当然还有一个默认的数据源。

AbstractRoutingDataSource这个抽象类中有一个抽象方法需要子类实现,如下:

受保护的抽象对象确定当前查找密钥();

determineCurrentLookupKey()这个方法的返回值决定了需要切换的数据源的KEY,就是根据这个KEYtargetDataSources取值(数据源)。

切换数据源时如何保证线程隔离?

数据源是公共资源。多线程情况下如何保证线程隔离?我不能在这里切换并影响其他线程的执行。

说到线程隔离,自然会想到ThreadLocal了,将切换数据源的KEY(用于从targetDataSources中取值)存储在ThreadLocal中,执行结束之后清除即可。

单独封装了一个DataSourceHolder,内部使用ThreadLocal隔离线程,代码如下:

/**
 * 使用ThreadLocal存储切换数据源后的KEY
 */
public class DataSourceHolder {

    //线程  本地环境
    private static final ThreadLocal<String> dataSources = new InheritableThreadLocal();

    //设置数据源
    public static void setDataSource(String datasource) {
        dataSources.set(datasource);
    }

    //获取数据源
    public static String getDataSource() {
        return dataSources.get();
    }

    //清除数据源
    public static void clearDataSource() {
        dataSources.remove();
    }
}

如何构建动态数据源?

上文说过只需继承一个抽象类AbstractRoutingDataSource,重写其中的一个方法determineCurrentLookupKey()即可。代码如下:

/**
 * 动态数据源,继承AbstractRoutingDataSource
 */
public class DynamicDataSource extends AbstractRoutingDataSource {

    /**
     * 返回需要使用的数据源的key,将会按照这个KEY从Map获取对应的数据源(切换)
     * @return
     */
    @Override
    protected Object determineCurrentLookupKey() {
        //从ThreadLocal中取出KEY
        return DataSourceHolder.getDataSource();
    }

    /**
     * 构造方法填充Map,构建多数据源
     */
    public DynamicDataSource(DataSource defaultTargetDataSource, Map<Object, Object> targetDataSources) {
        //默认的数据源,可以作为主数据源
        super.setDefaultTargetDataSource(defaultTargetDataSource);
        //目标数据源
        super.setTargetDataSources(targetDataSources);
        //执行afterPropertiesSet方法,完成属性的设置
        super.afterPropertiesSet();
    }
}

上面的代码很简单,分析如下:

  1. 指定默认数据源和目标数据源的多参数构造函数。

  2. 重写determineCurrentLookupKey()方法,返回数据源对应的KEY,这里是直接从ThreadLocal中取值,就是上文封装的DataSourceHolder

定义一个注释

为了操作方便、耦合性低,每次需要切换数据源时,不能手动调整接口。您可以定义用于切换数据源的注解,如下:

/**
 * 切换数据源的注解
 */
@Target(value = ElementType.METHOD)
@Retention(value = RetentionPolicy.RUNTIME)
@Documented
public @interface SwitchSource {

    /**
     * 默认切换的数据源KEY
     */
    String DEFAULT_NAME = "hisDataSource";

    /**
     * 需要切换到数据的KEY
     */
    String value() default DEFAULT_NAME;
}

注解中只有一个value属性,指定了需要切换数据源的KEY

仅有注释是不够的。当然,你还需要有方面。代码如下:

@Aspect
//优先级要设置在事务切面执行之前
@Order(1)
@Component
@Slf4j
public class DataSourceAspect {


    @Pointcut("@annotation(SwitchSource)")
    public void pointcut() {
    }

    /**
     * 在方法执行之前切换到指定的数据源
     * @param joinPoint
     */
    @Before(value = "pointcut()")
    public void beforeOpt(JoinPoint joinPoint) {
        /*因为是对注解进行切面,所以这边无需做过多判定,直接获取注解的值,进行环绕,将数据源设置成远方,然后结束后,清楚当前线程数据源*/
        Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
        SwitchSource switchSource = method.getAnnotation(SwitchSource.class);
        log.info("[Switch DataSource]:" + switchSource.value());
        DataSourceHolder.setDataSource(switchSource.value());
    }

    /**
     * 方法执行之后清除掉ThreadLocal中存储的KEY,这样动态数据源会使用默认的数据源
     */
    @After(value = "pointcut()")
    public void afterOpt() {
        DataSourceHolder.clearDataSource();
        log.info("[Switch Default DataSource]");
    }
}

这个ASPECT很容易理解,beforeOpt()在方法之前执行,取值@SwitchSource中value属性设置到ThreadLocal中;afterOpt()方法在方法执行之后执行,清除掉ThreadLocal中的KEY,保证了如果不切换数据源,则用默认的数据源。

如何与Mybatis集成?

单一数据源与Mybatis整合上文已经详细讲解了,数据源DataSource作为参数构建了SqlSessionFactory,同样的思想,只需要把这个数据源换成动态数据源即可。注入的代码如下:

/**
     * 创建动态数据源的SqlSessionFactory,传入的是动态数据源
     * @Primary这个注解很重要,如果项目中存在多个SqlSessionFactory,这个注解一定要加上
     */
    @Primary
    @Bean("sqlSessionFactory2")
    public SqlSessionFactory sqlSessionFactoryBean(DynamicDataSource dynamicDataSource) throws Exception {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dynamicDataSource);
        sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:/mapper/**/*.xml"));
        org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration();
        configuration.setMapUnderscoreToCamelCase(true);
        configuration.setDefaultFetchSize(100);
        configuration.setDefaultStatementTimeout(30);
        sqlSessionFactoryBean.setConfiguration(configuration);
        return sqlSessionFactoryBean.getObject();
    }

与Mybatis整合很简单,只需要把数据源替换成自定义的动态数据源DynamicDataSource

那么动态数据源如何注入到IOC容器中呢?看上文自定义的DynamicDataSource构造方法,肯定需要两个数据源了,因此必须先注入两个或者多个数据源到IOC容器中,如下:

/**
     * @Bean:向IOC容器中注入一个Bean
     * @ConfigurationProperties:使得配置文件中以spring.datasource为前缀的属性映射到Bean的属性中
     */
    @ConfigurationProperties(prefix = "spring.datasource")
    @Bean("dataSource")
    public DataSource dataSource(){
        return new DruidDataSource();
    }

    /**
     * 向IOC容器中注入另外一个数据源
     * 全局配置文件中前缀是spring.datasource.his
     */
    @Bean(name = SwitchSource.DEFAULT_NAME)
    @ConfigurationProperties(prefix = "spring.datasource.his")
    public DataSource hisDataSource() {
        return DataSourceBuilder.create().build();
    }

以上构建的两个数据源,一个是默认的数据源,一个是需要切换到的数据源(targetDataSources),这样就组成了动态数据源了。数据源的一些信息,比如urlusername需要自己在全局配置文件中根据指定的前缀配置即可,代码不再贴出。

动态数据源注入代码如下:

/**
     * 创建动态数据源的SqlSessionFactory,传入的是动态数据源
     * @Primary这个注解很重要,如果项目中存在多个SqlSessionFactory,这个注解一定要加上
     */
    @Primary
    @Bean("sqlSessionFactory2")
    public SqlSessionFactory sqlSessionFactoryBean(DynamicDataSource dynamicDataSource) throws Exception {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dynamicDataSource);
        org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration();
        configuration.setMapUnderscoreToCamelCase(true);
        configuration.setDefaultFetchSize(100);
        configuration.setDefaultStatementTimeout(30);
        sqlSessionFactoryBean.setConfiguration(configuration);
        return sqlSessionFactoryBean.getObject();
    }

这里还有一个问题:IOC中有多个数据源,那么事务管理器应该怎么做呢?也很困惑,选择哪个数据源?因此,事务管理器仍然必须重新配置。

事务管理器此时管理的数据源将是动态数据源DynamicDataSource,配置如下:

/**
     * 重写事务管理器,管理动态数据源
     */
    @Primary
    @Bean(value = "transactionManager2")
    public PlatformTransactionManager annotationDrivenTransactionManager(DynamicDataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }

至此,Mybatis与多数据源的集成就完成了。

演示

使用也是很简单,在需要切换数据源的方法上方标注@SwitchSource切换到指定的数据源即可,如下:

 //不开启事务
    @Transactional(propagation = Propagation.NOT_SUPPORTED)
    //切换到HIS的数据源
    @SwitchSource
    @Override
    public List<DeptInfo> list() {
        return hisDeptInfoMapper.listDept();
    }

这样只要执行到这方法将会切换到HIS的数据源,方法执行结束之后将会清除,执行默认的数据源。

总结

本文讲的是Spring Boot与单数据源、Mybatis、多数据源的集成。希望本文能够帮助读者理解多数据源的整合。虽然用得不多,但在某些领域仍然很重要。的。

. . .

相关推荐

额外说明

Linux模拟网络连接超时

最近在设置http请求超时setReadTimeout和setConnectTimeout,其中ReadTimeout很好模拟,ConnectTimeout模拟有点麻烦。 HTTP协议是建立在TCP协议基础之上的,HTTP会通过TCP建立连接。 TCP协

额外说明

Linux init进程(从内核态转为用户态)

init进程,也就是内核启动3个进程中的进程1; init进程完成了从内核态向用户态的转变; (1)init进程是比较特殊,一个进程两个状态,init刚开始运行时是内核态,他属于内核线程,然后他自己运行了一个用户态下面的程序后,他自己强行转成了用户态,因

额外说明

如何杀掉一个进程?

1、根据端口杀进程 查询端口号:netstat -ano|findstr "8080" 通过进程id查看对应的应用 tasklist |findstr "进程id号" 通过命令杀进程 taskkill  /f /t /im  "进程id或者应用名称" 2

额外说明

《PostgreSQL开发指南》第08章备份与恢复

《PostgreSQL 开发指南》专栏目录 第 01 篇 课程介绍 第 02 篇 PostgreSQL 简介 第 03 篇 PostgreSQL 安装 第 04 篇 角色与用户 第 05 篇 数据库与模式 第 06 篇 管理数据表 第 07 篇 管理表空

额外说明

C语言:文件操作

文件操作 1. 为什么使用文件 2.文件的打开和关闭 2.1文件指针 2..2 文件的打开和关闭 3.文件的顺序读写 3.1 对比一组函数 4.文件的随机读写 4.1 fseek 4.2 ftell 4.3 rewind 5. 文本文件和二进制文件 6.

额外说明

HNU-计算机系统-实验2-DataLab

写在前面 首先要感谢A橙_大佬,在之前的实验以及学习中,许多地方参考与学习了大佬的思路。 包括惊喜地发现,本次实验的验收助教居然就是A橙_大佬。……  虽然最后没用勇气去找大佬验收,换了另外一位助教验收嘿嘿…… 以及,这个datalab,真的很考验对于位

额外说明

初学maven时在命令行窗口构建maven项目时出现 Generating project in Interactive mode

1.我输入的命令是: mvn archetype:generate 2.出现以下情况: 3.原因: 是在外网,访问速度很慢,导致创建过程卡住。 4.解决方法是: 加个参数 -DarchetypeCatalog=internal 让它不要从远程服务器上取

额外说明

变分自编码器(VAE)详解与实现(tensorflow2.x)

变分自编码器(VAE)详解与实现(tensorflow2.x) VAE介绍 VAE原理 变分推理 VAE核心方程 优化方式 重参数化技巧(Reparameterization trick) VAE实现 导入库 重参数技巧 绘制测试图片函数 加载数据与超参

额外说明

linux 安装redis redis-cli报错问题 下载问题(亲测有效)

官网给的方法很好: https://redis.io/download 方法一:可以选择直接上传,然后解压, 可以使用(xftp工具进行上传)或者命令:“rz” 方法二:也可以使用命令行进行下载 因为redis使用c编写,并且centos上面的gcc版

额外说明

【nmon】linux :服务器性能监控工具 nmon 安装和使用

目录 一、查看linux系统服务器版本信息 ​二、nmon下载 三、nmon安装 ​四、安装成功校检 五、测试监控 六、监控数据采集 一、查看linux系统服务器版本信息 (Linux查看版本当前操作系统内核信息):uname -a (Linux查看当前

ads via 小工具