Spring Boot Dynamic Data Source example (Automatic switch of multiple data sources)

  • 2020-06-12 09:16:27
  • OfStack

This paper implements the case scenario:

In addition to the need to read and manage data from its own main database system, there is also a part of the business involved in other multiple databases, which requires that any method can be flexible to specify the specific database to operate.

In order to use it in the simplest way in the development, this paper is based on annotations and AOP's method implementation. In the project of spring boot framework, after adding the code classes implemented in this paper, you only need to configure the data source to use directly through annotations, which is simple and convenient.

1 Configuration 2 used

1. Start class registration for dynamic data source

2. Configure multiple data sources in the configuration file

3. Use annotations to specify the data source on the required method

1. Add @Import ({DynamicDataSourceRegister. class, MProxyTransactionManagementConfiguration. class}) to startup class


@SpringBootApplication
@Import({DynamicDataSourceRegister.class}) //  Register dynamic multiple data sources 
public class SpringBootSampleApplication {

  //  Omit other code 
}

2. Configuration file configuration content is: (excluding other configuration in the project, here is only the data source related)


#  Master data source, default 
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/test
spring.datasource.username=root
spring.datasource.password=123456

#  More data sources 
custom.datasource.names=ds1,ds2
custom.datasource.ds1.driver-class-name=com.mysql.jdbc.Driver
custom.datasource.ds1.url=jdbc:mysql://localhost:3306/test1
custom.datasource.ds1.username=root
custom.datasource.ds1.password=123456

custom.datasource.ds2.driver-class-name=com.mysql.jdbc.Driver
custom.datasource.ds2.url=jdbc:mysql://localhost:3306/test2
custom.datasource.ds2.username=root
custom.datasource.ds2.password=123456

3. Usage


package org.springboot.sample.service;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;

import org.springboot.sample.datasource.TargetDataSource;
import org.springboot.sample.entity.Student;
import org.springboot.sample.mapper.StudentMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Service;

/**
 * Student Service
 *
 * @author   ChanHongYu (365384722)
 * @myblog http://blog.csdn.net/catoop/
 * @create  2016 years 1 month 12 day 
 */
@Service
public class StudentService {

  @Autowired
  private JdbcTemplate jdbcTemplate;

  // MyBatis the Mapper Method definition interface 
  @Autowired
  private StudentMapper studentMapper;

  @TargetDataSource(name="ds2")
  public List<Student> likeName(String name){
    return studentMapper.likeName(name);
  }

  public List<Student> likeNameByDefaultDataSource(String name){
    return studentMapper.likeName(name);
  }

  /**
   *  Use the default data source without specifying the data source 
   *
   * @return
   * @author SHANHY
   * @create 2016 years 1 month 24 day 
   */
  public List<Student> getList(){
    String sql = "SELECT ID,NAME,SCORE_SUM,SCORE_AVG, AGE  FROM STUDENT";
    return (List<Student>) jdbcTemplate.query(sql, new RowMapper<Student>(){

      @Override
      public Student mapRow(ResultSet rs, int rowNum) throws SQLException {
        Student stu = new Student();
        stu.setId(rs.getInt("ID"));
        stu.setAge(rs.getInt("AGE"));
        stu.setName(rs.getString("NAME"));
        stu.setSumScore(rs.getString("SCORE_SUM"));
        stu.setAvgScore(rs.getString("SCORE_AVG"));
        return stu;
      }

    });
  }

  /**
   *  Specify data source 
   *
   * @return
   * @author SHANHY
   * @create 2016 years 1 month 24 day 
   */
  @TargetDataSource(name="ds1")
  public List<Student> getListByDs1(){
    String sql = "SELECT ID,NAME,SCORE_SUM,SCORE_AVG, AGE  FROM STUDENT";
    return (List<Student>) jdbcTemplate.query(sql, new RowMapper<Student>(){

      @Override
      public Student mapRow(ResultSet rs, int rowNum) throws SQLException {
        Student stu = new Student();
        stu.setId(rs.getInt("ID"));
        stu.setAge(rs.getInt("AGE"));
        stu.setName(rs.getString("NAME"));
        stu.setSumScore(rs.getString("SCORE_SUM"));
        stu.setAvgScore(rs.getString("SCORE_AVG"));
        return stu;
      }

    });
  }

  /**
   *  Specify data source 
   *
   * @return
   * @author SHANHY
   * @create 2016 years 1 month 24 day 
   */
  @TargetDataSource(name="ds2")
  public List<Student> getListByDs2(){
    String sql = "SELECT ID,NAME,SCORE_SUM,SCORE_AVG, AGE  FROM STUDENT";
    return (List<Student>) jdbcTemplate.query(sql, new RowMapper<Student>(){

      @Override
      public Student mapRow(ResultSet rs, int rowNum) throws SQLException {
        Student stu = new Student();
        stu.setId(rs.getInt("ID"));
        stu.setAge(rs.getInt("AGE"));
        stu.setName(rs.getString("NAME"));
        stu.setSumScore(rs.getString("SCORE_SUM"));
        stu.setAvgScore(rs.getString("SCORE_AVG"));
        return stu;
      }

    });
  }
}

Note that when using MyBatis, the @TargetDataSource annotation cannot be used directly on the interface class Mapper.

According to the above code StudentMapper as the interface, the code is as follows:


package org.springboot.sample.mapper;

import java.util.List;

import org.springboot.sample.entity.Student;

/**
 * StudentMapper , the mapping SQL Statement interface, no logical implementation 
 *
 * @author  ChanHongYu (365384722)
 * @myblog http://blog.csdn.net/catoop/
 * @create 2016 years 1 month 20 day 
 */
public interface StudentMapper {

  //  annotations  @TargetDataSource  It can't be used here 
  List<Student> likeName(String name);

  Student getById(int id);

  String getNameById(int id);

}

Place the following classes in the Spring Boot project.

DynamicDataSource.Java

DynamicDataSourceAspect.java

DynamicDataSourceContextHolder.java

DynamicDataSourceRegister.java

TargetDataSource.java


package org.springboot.sample.datasource;

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

/**
 *  Dynamic data source 
 *
 * @author   ChanHongYu (365384722)
 * @create  2016 years 1 month 22 day 
 */
public class DynamicDataSource extends AbstractRoutingDataSource {

  @Override
  protected Object determineCurrentLookupKey() {
    return DynamicDataSourceContextHolder.getDataSourceType();
  }

}


package org.springboot.sample.datasource;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

/**
 *  Switching data sources Advice
 *
 * @author  ChanHongYu (365384722)
 * @create 2016 years 1 month 23 day 
 */
@Aspect
@Order(-1)//  Ensure that the AOP in @Transactional Before performing 
@Component
public class DynamicDataSourceAspect {

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

  @Before("@annotation(ds)")
  public void changeDataSource(JoinPoint point, TargetDataSource ds) throws Throwable {
    String dsId = ds.name();
    if (!DynamicDataSourceContextHolder.containsDataSource(dsId)) {
      logger.error(" The data source [{}] Does not exist, use the default data source  > {}", ds.name(), point.getSignature());
    } else {
      logger.debug("Use DataSource : {} > {}", ds.name(), point.getSignature());
      DynamicDataSourceContextHolder.setDataSourceType(ds.name());
    }
  }

  @After("@annotation(ds)")
  public void restoreDataSource(JoinPoint point, TargetDataSource ds) {
    logger.debug("Revert DataSource : {} > {}", ds.name(), point.getSignature());
    DynamicDataSourceContextHolder.clearDataSourceType();
  }

}


package org.springboot.sample.datasource;

import java.util.ArrayList;
import java.util.List;

public class DynamicDataSourceContextHolder {

  private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>();
  public static List<String> dataSourceIds = new ArrayList<>();

  public static void setDataSourceType(String dataSourceType) {
    contextHolder.set(dataSourceType);
  }

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

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

  /**
   *  Determine the specified DataSrouce Does it currently exist 
   *
   * @param dataSourceId
   * @return
   * @author SHANHY
   * @create 2016 years 1 month 24 day 
   */
  public static boolean containsDataSource(String dataSourceId){
    return dataSourceIds.contains(dataSourceId);
  }
}


package org.springboot.sample.datasource;

import java.util.HashMap;
import java.util.Map;

import javax.sql.DataSource;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.MutablePropertyValues;
import org.springframework.beans.PropertyValues;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.GenericBeanDefinition;
import org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder;
import org.springframework.boot.bind.RelaxedDataBinder;
import org.springframework.boot.bind.RelaxedPropertyResolver;
import org.springframework.context.EnvironmentAware;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.core.env.Environment;
import org.springframework.core.type.AnnotationMetadata;

/**
 *  Dynamic data source registration <br/>
 *  Start the dynamic data source in the start class (for example SpringBootSampleApplication ) 
 *  add  @Import(DynamicDataSourceRegister.class)
 *
 * @author  ChanHongYu (365384722)
 * @create 2016 years 1 month 24 day 
 */
public class DynamicDataSourceRegister
    implements ImportBeanDefinitionRegistrar, EnvironmentAware {

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

  private ConversionService conversionService = new DefaultConversionService(); 
  private PropertyValues dataSourcePropertyValues;

  //  Use this default value if the data source type is not specified in the configuration file 
  private static final Object DATASOURCE_TYPE_DEFAULT = "org.apache.tomcat.jdbc.pool.DataSource";
  // private static final Object DATASOURCE_TYPE_DEFAULT =
  // "com.zaxxer.hikari.HikariDataSource";

  //  The data source 
  private DataSource defaultDataSource;
  private Map<String, DataSource> customDataSources = new HashMap<>();

  @Override
  public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
    Map<Object, Object> targetDataSources = new HashMap<Object, Object>();
    //  Add the master data source to more data sources 
    targetDataSources.put("dataSource", defaultDataSource);
    DynamicDataSourceContextHolder.dataSourceIds.add("dataSource");
    //  Add more data sources 
    targetDataSources.putAll(customDataSources);
    for (String key : customDataSources.keySet()) {
      DynamicDataSourceContextHolder.dataSourceIds.add(key);
    }

    //  create DynamicDataSource
    GenericBeanDefinition beanDefinition = new GenericBeanDefinition();
    beanDefinition.setBeanClass(DynamicDataSource.class);
    beanDefinition.setSynthetic(true);
    MutablePropertyValues mpv = beanDefinition.getPropertyValues();
    mpv.addPropertyValue("defaultTargetDataSource", defaultDataSource);
    mpv.addPropertyValue("targetDataSources", targetDataSources);
    registry.registerBeanDefinition("dataSource", beanDefinition);

    logger.info("Dynamic DataSource Registry");
  }

  /**
   *  create DataSource
   *
   * @param type
   * @param driverClassName
   * @param url
   * @param username
   * @param password
   * @return
   * @author SHANHY
   * @create 2016 years 1 month 24 day 
   */
  @SuppressWarnings("unchecked")
  public DataSource buildDataSource(Map<String, Object> dsMap) {
    try {
      Object type = dsMap.get("type");
      if (type == null)
        type = DATASOURCE_TYPE_DEFAULT;//  The default DataSource

      Class<? extends DataSource> dataSourceType;
      dataSourceType = (Class<? extends DataSource>) Class.forName((String) type);

      String driverClassName = dsMap.get("driver-class-name").toString();
      String url = dsMap.get("url").toString();
      String username = dsMap.get("username").toString();
      String password = dsMap.get("password").toString();

      DataSourceBuilder factory = DataSourceBuilder.create().driverClassName(driverClassName).url(url)
          .username(username).password(password).type(dataSourceType);
      return factory.build();
    } catch (ClassNotFoundException e) {
      e.printStackTrace();
    }
    return null;
  }

  /**
   *  Load the multi-data source configuration 
   */
  @Override
  public void setEnvironment(Environment env) {
    initDefaultDataSource(env);
    initCustomDataSources(env);
  }

  /**
   *  Initializes the master data source 
   *
   * @author SHANHY
   * @create 2016 years 1 month 24 day 
   */
  private void initDefaultDataSource(Environment env) {
    //  Read the master data source 
    RelaxedPropertyResolver propertyResolver = new RelaxedPropertyResolver(env, "spring.datasource.");
    Map<String, Object> dsMap = new HashMap<>();
    dsMap.put("type", propertyResolver.getProperty("type"));
    dsMap.put("driver-class-name", propertyResolver.getProperty("driver-class-name"));
    dsMap.put("url", propertyResolver.getProperty("url"));
    dsMap.put("username", propertyResolver.getProperty("username"));
    dsMap.put("password", propertyResolver.getProperty("password"));

    defaultDataSource = buildDataSource(dsMap);

    dataBinder(defaultDataSource, env);
  }

  /**
   *  for DataSource Bind more data 
   *
   * @param dataSource
   * @param env
   * @author SHANHY
   * @create 2016 years 1 month 25 day 
   */
  private void dataBinder(DataSource dataSource, Environment env){
    RelaxedDataBinder dataBinder = new RelaxedDataBinder(dataSource);
    //dataBinder.setValidator(new LocalValidatorFactory().run(this.applicationContext));
    dataBinder.setConversionService(conversionService);
    dataBinder.setIgnoreNestedProperties(false);//false
    dataBinder.setIgnoreInvalidFields(false);//false
    dataBinder.setIgnoreUnknownFields(true);//true
    if(dataSourcePropertyValues == null){
      Map<String, Object> rpr = new RelaxedPropertyResolver(env, "spring.datasource").getSubProperties(".");
      Map<String, Object> values = new HashMap<>(rpr);
      //  Exclude properties that have been set 
      values.remove("type");
      values.remove("driver-class-name");
      values.remove("url");
      values.remove("username");
      values.remove("password");
      dataSourcePropertyValues = new MutablePropertyValues(values);
    }
    dataBinder.bind(dataSourcePropertyValues);
  }

  /**
   *  Initialize more data sources 
   *
   * @author SHANHY
   * @create 2016 years 1 month 24 day 
   */
  private void initCustomDataSources(Environment env) {
    //  Read configuration files for more data sources, also available via defaultDataSource Read the database for more data sources 
    RelaxedPropertyResolver propertyResolver = new RelaxedPropertyResolver(env, "custom.datasource.");
    String dsPrefixs = propertyResolver.getProperty("names");
    for (String dsPrefix : dsPrefixs.split(",")) {//  Multiple data sources 
      Map<String, Object> dsMap = propertyResolver.getSubProperties(dsPrefix + ".");
      DataSource ds = buildDataSource(dsMap);
      customDataSources.put(dsPrefix, ds);
      dataBinder(ds, env);
    }
  }

}


package org.springboot.sample.datasource;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 *  Used on a method to specify which data source to use 
 *
 * @author   ChanHongYu (365384722)
 * @create  2016 years 1 month 23 day 
 */
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface TargetDataSource {
  String name();
}

This article's code blogger has been tested and has no problems before sending it out for sharing. The connection pool parameter configuration is applied to all data sources.
For example, configure 1:


spring.datasource.maximum-pool-size=80

So all of our data sources are automatically applied.

Supplement:

If you are using SpringMVC and have integrated Shiro, 1 in general you may be:


#  Master data source, default 
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/test
spring.datasource.username=root
spring.datasource.password=123456

#  More data sources 
custom.datasource.names=ds1,ds2
custom.datasource.ds1.driver-class-name=com.mysql.jdbc.Driver
custom.datasource.ds1.url=jdbc:mysql://localhost:3306/test1
custom.datasource.ds1.username=root
custom.datasource.ds1.password=123456

custom.datasource.ds2.driver-class-name=com.mysql.jdbc.Driver
custom.datasource.ds2.url=jdbc:mysql://localhost:3306/test2
custom.datasource.ds2.username=root
custom.datasource.ds2.password=123456

0

Then please do not do so, please follow the following method to configure:


#  Master data source, default 
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/test
spring.datasource.username=root
spring.datasource.password=123456

#  More data sources 
custom.datasource.names=ds1,ds2
custom.datasource.ds1.driver-class-name=com.mysql.jdbc.Driver
custom.datasource.ds1.url=jdbc:mysql://localhost:3306/test1
custom.datasource.ds1.username=root
custom.datasource.ds1.password=123456

custom.datasource.ds2.driver-class-name=com.mysql.jdbc.Driver
custom.datasource.ds2.url=jdbc:mysql://localhost:3306/test2
custom.datasource.ds2.username=root
custom.datasource.ds2.password=123456

1

Related articles: