Spring Boot and multiple data sources

catalogue

  • preface

  • The purpose of writing this article

  • What are multiple data sources?

  • When to use multiple data sources?

  • Consolidate a single data source

  • Integrate Mybatis

  • How to integrate multiple data sources?

    • What is a dynamic data source?

    • How does data source switching ensure thread isolation?

    • How to construct a dynamic data source?

    • Define an annotation

    • How to integrate with Mybatis?

    • demonstration

  • summary

preface

At this time in about 19 years, the old colleague company was working on the medical system and needed to connect some information with the HIS system, such as patient, medical care, doctor's orders, departments and so on. But at first, I didn't know how to seamlessly connect with HIS, so I asked for lessons.

Finally, after discussion, the view docking method is adopted, which is roughly the view provided by HIS system for docking.

The purpose of writing this article

This article will involve the integration of Spring Boot with Mybatis and database. There are many articles similar to the integration of Mybatis and database on the Internet. The author also wrote an article before to introduce some integration routines in detail: Spring Boot integrates multi-point routines and takes fewer detours~ , you can have a look if you are interested.

What are multiple data sources?

The most common single application involves at most one database, that is, a data source. As the name suggests, multiple data sources involve two or more databases in a single application.

In fact, this definition is already clear when configuring the data source, such as the following code:

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

The three attributes url, username and password have uniquely identified a database, and the DataSource is created by relying on these three attributes. Multiple data sources means configuring multiple data sources (let's understand this for the time being).

When to use multiple data sources?

As a scenario introduced in the preface, I believe most of those who have worked in the medical system will deal with HIS. In order to simplify the operation process of nurses and doctors, it is necessary to connect the necessary information from the HIS system. As far as I know, there are roughly two schemes as follows:

  1. HIS provides views, such as medical view, patient view, etc. at this time, other systems only need to regularly read data from HIS view and synchronize it to their own database.

  2. The interface provided by HIS, whether in the form of webService or HTTP, is feasible. At this time, other systems only need to adjust the interface as required.

Obviously, the first scheme involves at least two databases, one is HIS database and one of its own system. In a single application, it is necessary to switch multiple data sources to achieve the purpose.

Of course, there are many usage scenarios for multiple data sources. The above is just a simple scenario.

Consolidate a single data source

This article uses Alibaba's database connection pool druid to add dependencies as follows:

<!--druid Connection pool-->
<dependency>
   <groupId>com.alibaba</groupId>
   <artifactId>druid-spring-boot-starter</artifactId>
   <version>1.1.9</version>
</dependency>

Ali's database connection pool is very powerful, such as data monitoring, database encryption and so on. This article only demonstrates the process of integration with Spring Boot. Some other functions can be added later.

The automatic configuration class of the starter of the Druid connection pool is DruidDataSourceAutoConfigure, and the class is marked with the following line of comments:

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

@The EnableConfigurationProperties annotation validates the configuration in the configuration file and maps to the properties of the specified class.

The prefix specified in DruidStatProperties is spring.datasource.druid. This configuration is mainly used to set some parameters of the connection pool.

The prefix specified in DataSourceProperties is spring.datasource, which is mainly used to set the url, username, password and other information of the database.

Therefore, we only need to specify some configurations of the database and some configuration information of the connection pool in the global configuration file. The prefixes are spring.datasource.druid and spring.datasource respectively. The following are applications. Properties configured by individuals:

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
#Initialize connection size
spring.datasource.druid.initial-size=0
#Maximum number of connections used in connection pool
spring.datasource.druid.max-active=20
#Connection pool minimum idle
spring.datasource.druid.min-idle=0
#Gets the maximum connection wait time
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
#Configure how often to detect idle connections that need to be closed. The unit is milliseconds
spring.datasource.druid.time-between-eviction-runs-millis=60000
#Sets the minimum lifetime of a connection in the pool, in milliseconds
spring.datasource.druid.min-evictable-idle-time-millis=25200000
#spring.datasource.druid.max-evictable-idle-time-millis=
#When the removeAbandoned function is enabled, how long must the connection be closed
spring.datasource.druid.removeAbandoned=true
#1800 seconds, or 30 minutes
spring.datasource.druid.remove-abandoned-timeout=1800
#<!--  1800 seconds, or 30 minutes -- >
spring.datasource.druid.log-abandoned=true
spring.datasource.druid.filters=mergeStat

Configure the above information in the global configuration file application.properties file to inject a data source into Spring Boot. In fact, this is only one way. Here is another way.

In the automatic configuration class, DruidDataSourceAutoConfigure has the following code:

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

@The combination of ConditionalOnMissingBean and @ Bean annotation means that we can override it. We only need to inject a Bean of DataSource type into the IOC in advance.

Therefore, we can define the following configurations in the custom configuration class:

/**
     * @Bean: Inject a Bean into the IOC container
     * @ConfigurationProperties: Make the attributes prefixed with spring.datasource in the configuration file map to the attributes of the Bean
     * @return
     */
    @ConfigurationProperties(prefix = "spring.datasource")
    @Bean
    public DataSource dataSource(){
        //Do some other custom configurations, such as password encryption
        return new DruidDataSource();
    }

The above describes two data source configuration methods. The first is relatively simple, and the second is suitable for expansion and can be selected as needed.

Integrate Mybatis

Integrating Mybatis with Spring Boot is actually very simple. It can be done in a few steps. First, add dependencies:

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

Step 2: find the automatic configuration class MybatisAutoConfiguration, which has the following line of code:

@EnableConfigurationProperties(MybatisProperties.class)

The old routine. The configuration with the prefix mybatis in the global configuration file will be mapped to the attributes in this class.

There are many things that can be configured, such as the location of XML files, type processor, etc. the following is a simple configuration:

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

If Mapper needs to be injected through package scanning, you need to add an annotation on the configuration class: @ MapperScan, where the value attribute specifies the package to be scanned.

It is a relatively simple way to directly configure various attributes in the global configuration file. In fact, there are no less than two configuration methods for the integration of any component. The following describes how to configure the configuration class.

The MybatisAutoConfiguration auto configuration class has the following code:

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

@ConditionalOnMissingBean and @ Bean are really old partners, which means that we can overwrite again. We only need to inject SqlSessionFactory (one of the six swordsmen of Mybatis) into the IOC container.

Inject in the custom configuration class, as follows:

 /**
     * Inject 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();
        //  Automatically convert underscores in the database to hump format
        configuration.setMapUnderscoreToCamelCase(true);
        configuration.setDefaultFetchSize(100);
        configuration.setDefaultStatementTimeout(30);
        sqlSessionFactoryBean.setConfiguration(configuration);
        return sqlSessionFactoryBean.getObject();
    }

The above describes the two ways to configure Mybatis. In fact, the first is enough in most scenarios. Why introduce the second? Of course, it is prepared for the integration of multiple data sources.

There is a very important line of code in MybatisAutoConfiguration, as follows:

@ConditionalOnSingleCandidate(DataSource.class)

@The annotation ConditionalOnSingleCandidate means that only one candidate Bean instance in the IOC container will take effect.

What does this line of code mean in Mybatis's auto configuration class? Let's introduce it, ha ha ha~

How to integrate multiple data sources?

The question left above: why is the following line of code marked on the automatic configuration of Mybatis:

@ConditionalOnSingleCandidate(DataSource.class)

The implication of the above line of code: this automatic configuration class will take effect only when there is only one data source DataSource in the IOC container.

oh In this way, can't multiple data sources use Mybatis?

There may be a misunderstanding that multiple data sources coexist with multiple data sources. Of course, this is not incorrect.

In the case of multiple data sources, multiple data sources do not coexist. Spring provides an abstract class such as AbstractRoutingDataSource, which enables arbitrary switching in the case of multiple data sources, which is equivalent to the role of a dynamic routing. The author calls it a dynamic data source. Therefore, Mybatis only needs to configure this dynamic data source.

What is a dynamic data source?

Dynamic data source is simply a data source that can be switched freely, which is similar to the feeling of a dynamic routing. Spring provides an abstract class AbstractRoutingDataSource, which has an attribute as follows:

private Map<Object, Object> targetDataSources;

targetDataSources is a Map structure in which all data sources to be switched are stored and switched according to the specified KEY. Of course, there is a default data source.

AbstractRoutingDataSource is an abstract class in which an abstract method needs to be implemented by a subclass, as follows:

protected abstract Object determineCurrentLookupKey();

The return value of the determineCurrentLookupKey() method determines the KEY of the data source to be switched, which is used to get the value (data source) from targetDataSources.

How does data source switching ensure thread isolation?

The data source belongs to a common resource. How to ensure thread isolation in the case of multithreading? I can't switch. It affects the execution of other threads.

When it comes to thread isolation, you naturally think of ThreadLocal. Store the KEY of switching data sources (used to get values from targetDataSources) in ThreadLocal and clear it after execution.

A DataSourceHolder is encapsulated separately, and ThreadLocal is used internally to isolate threads. The code is as follows:

/**
 * Use ThreadLocal to store the KEY after switching the data source
 */
public class DataSourceHolder {

    //thread     Local environment
    private static final ThreadLocal<String> dataSources = new InheritableThreadLocal();

    //set up data sources
    public static void setDataSource(String datasource) {
        dataSources.set(datasource);
    }

    //Get data source
    public static String getDataSource() {
        return dataSources.get();
    }

    //Clear data source
    public static void clearDataSource() {
        dataSources.remove();
    }
}

How to construct a dynamic data source?

As mentioned above, you only need to inherit an abstract class AbstractRoutingDataSource and override one of the methods determineCurrentLookupKey(). The code is as follows:

/**
 * Dynamic data source, inheriting AbstractRoutingDataSource
 */
public class DynamicDataSource extends AbstractRoutingDataSource {

    /**
     * Return the key of the data source to be used, and the corresponding data source will be obtained from the Map according to this key (SWITCHING)
     * @return
     */
    @Override
    protected Object determineCurrentLookupKey() {
        //Remove the KEY from ThreadLocal
        return DataSourceHolder.getDataSource();
    }

    /**
     * The construction method fills the Map and constructs multiple data sources
     */
    public DynamicDataSource(DataSource defaultTargetDataSource, Map<Object, Object> targetDataSources) {
        //The default data source can be used as the master data source
        super.setDefaultTargetDataSource(defaultTargetDataSource);
        //Target data source
        super.setTargetDataSources(targetDataSources);
        //Execute the afterpropertieset method to complete the property setting
        super.afterPropertiesSet();
    }
}

The above code is very simple. The analysis is as follows:

  1. A multi parameter construction method that specifies the default data source and target data source.

  2. Override the determineCurrentLookupKey() method to return the KEY corresponding to the data source. Here, the value is directly obtained from ThreadLocal, which is the DataSourceHolder encapsulated above.

Define an annotation

For easy operation and low coupling, you can't manually adjust the interface every time you need to switch the data source. You can define an annotation for switching the data source, as follows:

/**
 * Switch annotation of data source
 */
@Target(value = ElementType.METHOD)
@Retention(value = RetentionPolicy.RUNTIME)
@Documented
public @interface SwitchSource {

    /**
     * Default switched data source KEY
     */
    String DEFAULT_NAME = "hisDataSource";

    /**
     * You need to switch to the KEY of the data
     */
    String value() default DEFAULT_NAME;
}

There is only one value attribute in the annotation, which specifies the KEY to switch the data source.

It's not enough to have comments. Of course, there should be sections. The code is as follows:

@Aspect
//Priority set to highest
@Order(Ordered.HIGHEST_PRECEDENCE)
@Component
@Slf4j
public class DataSourceAspect {


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

    /**
     * Switch to the specified data source before method execution
     * @param joinPoint
     */
    @Before(value = "pointcut()")
    public void beforeOpt(JoinPoint joinPoint) {
        /*Because the annotation is cut, there is no need to make too many decisions here. Directly obtain the annotation value, surround it, set the data source to remote, and then clear the current thread data source after the end*/
        Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
        SwitchSource switchSource = method.getAnnotation(SwitchSource.class);
        log.info("[Switch DataSource]:" + switchSource.value());
        DataSourceHolder.setDataSource(switchSource.value());
    }

    /**
     * After the method is executed, clear the KEY stored in ThreadLocal, so that the dynamic data source will use the default data source
     */
    @After(value = "pointcut()")
    public void afterOpt() {
        DataSourceHolder.clearDataSource();
        log.info("[Switch Default DataSource]");
    }
}

This ASPECT is easy to understand. beforeOpt() is executed before the method, and the value attribute in the value @ SwitchSource is set to ThreadLocal; After the method is executed, the afterOpt() method clears the KEY in ThreadLocal to ensure that the default data source is used if the data source is not switched.

How to integrate with Mybatis?

The integration of single data source and Mybatis has been explained in detail above. The SqlSessionFactory is constructed with the data source as a parameter. The same idea is that you only need to replace this data source with a dynamic data source. The injected code is as follows:

/**
     * Create the SqlSessionFactory of the dynamic data source, and the dynamic data source is passed in
     * @Primary This annotation is very important. If there are multiple sqlsessionfactories in the project, this annotation must be added
     */
    @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();
    }

The integration with Mybatis is very simple. You only need to replace the data source with a custom dynamic data source, DynamicDataSource.

So how can dynamic data sources be injected into the IOC container? Looking at the above customized DynamicDataSource construction method, two data sources are definitely required. Therefore, two or more data sources must be injected into the IOC container first, as follows:

 /**
     * @Bean: Inject a Bean into the IOC container
     * @ConfigurationProperties: Make the attributes prefixed with spring.datasource in the configuration file map to the attributes of the Bean
     */
    @ConfigurationProperties(prefix = "spring.datasource")
    @Bean("dataSource")
    public DataSource dataSource(){
        return new DruidDataSource();
    }

    /**
     * Inject another data source into the IOC container
     * The prefix in the global configuration file is spring.datasource.his
     */
    @Bean(name = SwitchSource.DEFAULT_NAME)
    @ConfigurationProperties(prefix = "spring.datasource.his")
    public DataSource hisDataSource() {
        return DataSourceBuilder.create().build();
    }

The two data sources constructed above, one is the default data source and the other is the data source to be switched to (targetdata sources), which constitutes a dynamic data source. Some information about the data source, such as url, username needs to configure it according to the specified prefix in the global configuration file, and the code will not be posted.

The injection code of dynamic data source is as follows:

/**
     * Create the SqlSessionFactory of the dynamic data source, and the dynamic data source is passed in
     * @Primary This annotation is very important. If there are multiple sqlsessionfactories in the project, this annotation must be added
     */
    @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();
    }

Here is another problem: there are multiple data sources in the IOC, so what about the transaction manager? It is also confused. Which data source to choose? Therefore, the transaction manager must still be reconfigured.

The data source managed by the transaction manager at this time will be the dynamic data source. The configuration is as follows:

   /**
     * Rewrite the transaction manager to manage dynamic data sources
     */
    @Primary
    @Bean(value = "transactionManager2")
    public PlatformTransactionManager annotationDrivenTransactionManager(DynamicDataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }

So far, the integration of Mybatis and multiple data sources has been completed.

demonstration

It is also easy to use. Mark @ SwitchSource above the method that needs to switch data sources. Switch to the specified data source, as follows:

    //Do not open transaction
    @Transactional(propagation = Propagation.NOT_SUPPORTED)
    //Switch to HIS data source
    @SwitchSource
    @Override
    public List<DeptInfo> list() {
        return hisDeptInfoMapper.listDept();
    }

 

In this way, as long as the method is executed, it will switch to the data source of HIS. After the method is executed, it will be cleared and the default data source will be executed.

summary

This article talks about the integration of Spring Boot with single data source, Mybatis and multiple data sources. I hope this article can help readers understand the integration of multiple data sources. Although it is not used much, it is still important in some fields.

Keywords: Java Spring Spring Boot

Added by 44justin on Wed, 17 Nov 2021 03:46:55 +0200