preface

Around this time in 2013, my old colleague company was working on a medical system and needed to connect some information with HIS system, such as information of patients, medical care, medical orders, departments and so on. But at first, I didn’t know how to work seamlessly with HIS, so I learned from him.

Finally, the view docking method was adopted after discussion, which basically means that HIS system provides views and they do docking.

The purpose of writing this article

This article will involve the integration of Spring Boot, Mybatis and database. There are many articles similar to the integration of Mybatis and database on the Internet. The author has also written a detailed article about some integration routines before: Spring Boot integration multi-point routine, less detours ~, interested can see.

What is multi-data source?

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

This definition is already clear when configuring the data source, as shown in the following code:

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

The url, username, and password attributes uniquely identify the database. The DataSource is created using these attributes. Multiple data sources means configuring multiple datasources.

When to use multiple data sources?

Just as the scene introduced in the preface, it is believed that most people who have worked in the medical system will deal with HIS. In order to simplify the operation process of nurses and doctors, necessary information must be connected to HIS system. As far as I know, there are roughly two schemes as follows:

  1. HISProvide views, such as medical view, patient view, etc., while other systems only need periodic slaveHISView read data to its own database can be synchronized.
  2. HISProvide interface, bothwebServiceorHTTPThe form is feasible, at this time other systems only need to adjust the interface according to the requirements.

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

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

Consolidate a single data source

SQL > select druid, druid, druid, druid, druid

<! --druid connection pool -->
<dependency>
   <groupId>com.alibaba</groupId>
   <artifactId>druid-spring-boot-starter</artifactId>
   <version>1.1.9</version>
</dependency>
Copy the code

Ali’s database connection pool is very powerful, such as data monitoring, database encryption and so on. This paper only demonstrates the integration process with Spring Boot, and some other functions can be added in the future.

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

@EnableConfigurationProperties({DruidStatProperties.class, DataSourceProperties.class})
Copy the code

@ EnableConfigurationProperties this makes effective configuration and mapping in the configuration file to specify the properties of the class.

DruidStatProperties specifies the prefix spring.datasource.druid, which is used to set the connection pool parameters.

The DataSourceProperties prefix is Spring. datasource. This is used to set the URL, username, password, etc of the database.

Druid = spring.datasource; druid = spring.datasource; druid = spring.datasource; The following is a personal configuration (application.properties) :

spring.datasource.url=JDBC \ : mysql \ : / / 120.26.101. XXX \ : 3306 / XXX? useUnicode\=true&characterEncoding\=UTF-8&zeroDateTimeBehavior\=convertToNull&useSSL\=false&allowMultiQueries\=true&serv erTimezone=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 the connection size
spring.datasource.druid.initial-size=0
# Maximum number of connections in connection pool
spring.datasource.druid.max-active=20
The connection pool is minimum idle
spring.datasource.druid.min-idle=0
Get 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, in milliseconds
spring.datasource.druid.time-between-eviction-runs-millis=60000
Set the minimum time for a connection to live in the pool in milliseconds
spring.datasource.druid.min-evictable-idle-time-millis=25200000
#spring.datasource.druid.max-evictable-idle-time-millis=
# Enable removeAbandoned. How long must the connection be closed
spring.datasource.druid.removeAbandoned=true
#1800 seconds. That's 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
Copy the code

Configure the above information in the global configuration file application.properties to inject a data source into Spring Boot. That’s just one way. Here’s another way.

In the automatic configuration class DruidDataSourceAutoConfigure in following a piece of code:

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

The combination of @conditionalonmissingBean and @bean annotations means that we can override it by injecting a DataSource Bean into IOC ahead of time.

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

/ * * *@BeanInject a Bean * into the IOC container@ConfigurationProperties: causes properties prefixed with spring. Datasource in the configuration file to be mapped to properties of the Bean@return* /
    @ConfigurationProperties(prefix = "spring.datasource")
    @Bean
    public DataSource dataSource(a){
        // Do some other custom configurations, such as password encryption......
        return new DruidDataSource();
    }
Copy the code

The above two data source configuration methods are introduced. The first one is relatively simple, and the second one is suitable for expansion and selection on demand.

Integration of Mybatis

Spring Boot Mybatis integration is actually very simple, simple steps to fix, first add dependency:

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

The second step is to find the MybatisAutoConfiguration class, which has the following line:

@EnableConfigurationProperties(MybatisProperties.class)
Copy the code

Global configuration files with the prefix mybatis will map to properties in this class.

There are many things you can configure, such as the location of XML files, type handlers, and so on, as simple as:

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

If you want to inject Mapper as a packet scan, you need to add an annotation to the configuration class: @mapperscan, where the value property specifies which packet to scan.

It is an easy way to configure various properties directly in the global configuration file. In fact, any component integration has at least two configuration methods. Here is how to configure the configuration class.

MybatisAutoConfiguration automatic configuration class has the following code:

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

ConditionalOnMissingBean and @bean are really old partners, which means we can override it again, just inject the SqlSessionFactory into the IOC container.

Inject into a 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();
    }
Copy the code

There are two ways to configure Mybatis. In most scenarios, the first is sufficient. Why introduce the second? Of course, to prepare for the integration of multiple data sources.

There is an important line of code in MybatisAutoConfiguration as follows:

@ConditionalOnSingleCandidate(DataSource.class)
Copy the code

@ ConditionalOnSingleCandidate this annotation mean when IOC container is only one candidate Bean instance will only take effect.

What does this line of code mean in Mybatis auto-configuration class? The following introduction, ha ha ha ~

How to integrate multiple data sources?

Why Mybatis auto-configuration is marked with the following code:

@ConditionalOnSingleCandidate(DataSource.class)
Copy the code

The implication of this line of code is that this auto-configuration class only works if there is only one DataSource in the IOC container.

Oh? Mybatis cannot be used for multiple data sources.

There is a misconception that a multisource DataSource is a multidatasource, but that is not incorrect.

Multiple source is not under the condition of the coexistence of multiple data sources, Spring provides AbstractRoutingDataSource such an abstract class, can make in the case of multiple source arbitrary switching, equivalent to the role of a dynamic routing, the author called dynamic data source. So Mybatis only needs to configure this dynamic data source.

What is a dynamic data source?

Dynamic data source in simple terms is to be able to free switch data source, similar to the feeling of a dynamic routing, Spring provides an abstract class AbstractRoutingDataSource, yo an attribute in this abstract class, as follows:

private Map<Object, Object> targetDataSources;
Copy the code

TargetDataSources is a Map structure where all data sources that need to be switched are stored, according to the specified KEY. There is also a default data source.

AbstractRoutingDataSource this abstract class with an abstract method need to subclass implementation, as follows:

protected abstract Object determineCurrentLookupKey(a);
Copy the code

DetermineCurrentLookupKey (), the return value of this method determines the need to switch the KEY data source, it is according to the KEY from targetDataSources values (data sources).

How do data source switches ensure thread isolation?

The data source is a common resource. How do you ensure thread isolation in the case of multiple threads? Can’t switch my side to affect the execution of other threads.

When it comes to thread isolation, ThreadLocal is a natural place to store the KEY that switches the data source (used to value the data from the targetDataSources) in the ThreadLocal and then clean it up at the end of execution.

A separate DataSourceHolder is encapsulated, and ThreadLocal is used internally to isolate threads.

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

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

    // Set the data source
    public static void setDataSource(String datasource) {
        dataSources.set(datasource);
    }

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

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

How to construct a dynamic data source?

Said just inherited an abstract class AbstractRoutingDataSource, rewrite one of the methods determineCurrentLookupKey (). The code is as follows:

/ * * * dynamic data source, inheritance AbstractRoutingDataSource * /
public class DynamicDataSource extends AbstractRoutingDataSource {

    /** * returns the key of the data source to be used, which will be used to retrieve the corresponding data source from the Map (toggle) *@return* /
    @Override
    protected Object determineCurrentLookupKey(a) {
        // Fetch the KEY from ThreadLocal
        return DataSourceHolder.getDataSource();
    }

    /** * The constructor populates the Map to build multiple data sources */
    public DynamicDataSource(DataSource defaultTargetDataSource, Map<Object, Object> targetDataSources) {
        // The default data source can be used as the primary data source
        super.setDefaultTargetDataSource(defaultTargetDataSource);
        // Target data source
        super.setTargetDataSources(targetDataSources);
        // Execute the afterPropertiesSet method to set the properties
        super.afterPropertiesSet(); }}Copy the code

The above code is very simple and can be analyzed as follows:

  1. A multi-parameter constructor that specifies the default and target data sources.
  2. rewritedetermineCurrentLookupKey()Method to return the corresponding data sourceKEYThis is directly fromThreadLocalMedium value, which is encapsulated aboveDataSourceHolder.

Define an annotation

For ease of operation and low coupling, you can’t manually adjust the interface every time you need to switch data sources. You can define a switch data source annotation as follows:

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

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

    /** * need to switch to data KEY */
    String value(a) default DEFAULT_NAME;
}
Copy the code

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

Annotations are not enough, of course, there should be a section, the code is as follows:

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


    @Pointcut("@annotation(SwitchSource)")
    public void pointcut(a) {}/** * Switches to the specified data source * before the method executes@param joinPoint
     */
    @Before(value = "pointcut()")
    public void beforeOpt(JoinPoint joinPoint) {
        /* Get the value of the annotation directly, wrap around it, set the data source to a distance, and then, when finished, know the current thread data source */
        Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
        SwitchSource switchSource = method.getAnnotation(SwitchSource.class);
        log.info("[Switch DataSource]:" + switchSource.value());
        DataSourceHolder.setDataSource(switchSource.value());
    }

    The /** * method clears the KEY stored in ThreadLocal so that the dynamic data source uses the default data source */
    @After(value = "pointcut()")
    public void afterOpt(a) {
        DataSourceHolder.clearDataSource();
        log.info("[Switch Default DataSource]"); }}Copy the code

This ASPECT is easy to understand, beforeOpt() is executed before the method, with the value attribute in @switchSource set to ThreadLocal; The afterOpt() method is executed after the method executes, removing the KEY from ThreadLocal and ensuring that the default data source is used if the data source is not switched.

How to integrate with Mybatis?

SqlSessionFactory = SqlSessionFactory = SqlSessionFactory = SqlSessionFactory = SqlSessionFactory The injected code looks like this:

/** * create dynamic data source SqlSessionFactory, pass dynamic data source *@PrimaryThis is an important annotation, and if there are multiple SQlsessionFactories in the project, this annotation must be followed by */
    @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();
    }
Copy the code

Integration with Mybatis is simple, just replace the data source with a custom DynamicDataSource.

So how do dynamic data sources get injected into the IOC container? The DynamicDataSource constructor must inject two or more data sources into the IOC container.

 / * * *@BeanInject a Bean * into the IOC container@ConfigurationProperties: causes properties prefixed with spring. Datasource in the configuration file to be mapped to the Bean properties */
    @ConfigurationProperties(prefix = "spring.datasource")
    @Bean("dataSource")
    public DataSource dataSource(a){
        return new DruidDataSource();
    }

    /** * inject another datasource into the IOC container * the global configuration file is prefixed with spring.datasource. His */
    @Bean(name = SwitchSource.DEFAULT_NAME)
    @ConfigurationProperties(prefix = "spring.datasource.his")
    public DataSource hisDataSource(a) {
        return DataSourceBuilder.create().build();
    }
Copy the code

The two data sources built above, one is the default data source and the one you want to switch to (targetDataSources), make up the dynamic data source. Some data source information, such as URL and username, need to be configured in the global configuration file according to the specified prefix, the code will not be posted.

The injection code for the dynamic data source is as follows:

/** * create dynamic data source SqlSessionFactory, pass dynamic data source *@PrimaryThis is an important annotation, and if there are multiple SQlsessionFactories in the project, this annotation must be followed by */
    @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();
    }
Copy the code

Here’s another problem: with multiple data sources in IOC, what about the transaction manager? It’s also confused, which data source to choose? So the transaction manager will definitely need to be reconfigured.

The transaction manager will manage the DynamicDataSource data source as follows:

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

At this point, the integration of Mybatis with multiple data sources is complete.

demo

It is also very easy to use, in the need to switch the data source above the method with the tag @switchSource switch to the specified data source, as follows:

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

So as long as this method is executed, it will switch to HIS data source, and after the method is executed, it will be cleared and the default data source will be executed.

conclusion

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

Original is not easy, a little like to share a wave, thank you for your support ~

Source code has been uploaded, the need for source code of friends public number reply keywordsMultiple data sources.