SoFunction
Updated on 2025-03-01

Spring+MyBatis instance code to implement data read and write separation

This article introduces Spring Boot + MyBatis read and write separation. Those who need to know Spring + MyBatis read and write separation can refer to it. Hope this article is helpful to you.

Its final implementation functions:

  1. The default update operation uses the write data source
  2. Read operations use slave data source
  3. Special settings: You can specify the type and name of the data source to be used (if there is a name, the corresponding data source will be used according to the name)

The implementation principle is as follows:

  1. Intercept the dao layer interface through Spring AOP, and set the data source type and name of the interface that needs to specify the data source in ThradLocal
  2. Through MyBatsi plug-in, set the data source in ThreadLocal based on update or query operations (if the dao layer is not specified)
  3. Inherit the AbstractRoutingDataSource class.

Here we write it directly and use HikariCP as the data source

The implementation steps are as follows:

  1. Define its data source configuration file and parse it into a data source
  2. Define AbstractRoutingDataSource class and other annotations
  3. Define Aop Intercept
  4. Define MyBatis plugin
  5. Integrate together

1. Configuration and parsing classes

Its configuration parameters are directly used by HikariCP configuration, and the specific parameters can be used as reference.HikariCP

Used hereyamlFormat, name, the content is as follows:

dds:
 write:
  jdbcUrl: jdbc:mysql://localhost:3306/order
  password: liu123
  username: root
  maxPoolSize: 10
  minIdle: 3
  poolName: master
 read:
  - jdbcUrl: jdbc:mysql://localhost:3306/test
   password: liu123
   username: root
   maxPoolSize: 10
   minIdle: 3
   poolName: slave1
  - jdbcUrl: jdbc:mysql://localhost:3306/test2
   password: liu123
   username: root
   maxPoolSize: 10
   minIdle: 3
   poolName: slave2

Define the bean corresponding to this configuration, with the name DBConfig, and the content is as follows:

@Component
@ConfigurationProperties(locations = "classpath:", prefix = "dds")
public class DBConfig {
  private List<HikariConfig> read;
  private HikariConfig write;

  public List<HikariConfig> getRead() {
    return read;
  }

  public void setRead(List<HikariConfig> read) {
     = read;
  }

  public HikariConfig getWrite() {
    return write;
  }

  public void setWrite(HikariConfig write) {
     = write;
  }
}

The tool class that converts configuration to DataSource, name:DataSourceUtil, the content is as follows:

import ;
import ;

import ;
import ;
import ;

public class DataSourceUtil {
  public static DataSource getDataSource(HikariConfig config) {
    return new HikariDataSource(config);
  }

  public static List<DataSource> getDataSource(List<HikariConfig> configs) {
    List<DataSource> result = null;
    if (configs != null && () > 0) {
      result = new ArrayList<>(());
      for (HikariConfig config : configs) {
        (getDataSource(config));
      }
    } else {
      result = new ArrayList<>(0);
    }

    return result;
  }
}

2. Annotations and dynamic data sources

Define annotation @DataSource, which is used to specify the data source to be used for individual methods (such as a read operation needs to be executed on the master, but another read method b needs to be executed on the specific one of the read data source)

@Retention()
@Target()
public @interface DataSource {
  /**
    * Type, which means whether to use read or write
    * @return
    */
  DataSourceType type() default ;

  /**
    * Specify the name of the DataSource to use
    * @return
    */
  String name() default "";
}

Define the data source type, which is divided into two types: READ, WRITE, and the content is as follows:

public enum DataSourceType {
  READ, WRITE;
}

Define the class that holds this shared informationDynamicDataSourceHolder, in which two are definedThreadLocaland a map,holderUsed to save the data source type (read or write) of the current thread.poolUsed to save the data source name (if specified), its content is as follows:

import ;
import ;

public class DynamicDataSourceHolder {
  private static final Map<String, DataSourceType> cache = new ConcurrentHashMap<>();
  private static final ThreadLocal<DataSourceType> holder = new ThreadLocal<>();
  private static final ThreadLocal<String> pool = new ThreadLocal<>();

  public static void putToCache(String key, DataSourceType dataSourceType) {
    (key,dataSourceType);
  }

  public static DataSourceType getFromCach(String key) {
    return (key);
  }

  public static void putDataSource(DataSourceType dataSourceType) {
    (dataSourceType);
  }

  public static DataSourceType getDataSource() {
    return ();
  }

  public static void putPoolName(String name) {
    if (name != null && () > 0) {
      (name);
    }
  }

  public static String getPoolName() {
    return ();
  }

  public static void clearDataSource() {
    ();
    ();
  }
}

Dynamic data source class isDynamicDataSoruce, it inherits fromAbstractRoutingDataSource, you can switch to the corresponding data source according to the returned key, and the content is as follows:

import ;
import ;

import ;
import ;
import ;
import ;
import ;
import ;

public class DynamicDataSource extends AbstractRoutingDataSource {
  private DataSource writeDataSource;
  private List<DataSource> readDataSource;
  private int readDataSourceSize;
  private Map<String, String> dataSourceMapping = new ConcurrentHashMap<>();

  @Override
  public void afterPropertiesSet() {
    if ( == null) {
      throw new IllegalArgumentException("Property 'writeDataSource' is required");
    }
    setDefaultTargetDataSource(writeDataSource);
    Map<Object, Object> targetDataSource = new HashMap<>();
    ((), writeDataSource);
    String poolName = ((HikariDataSource)writeDataSource).getPoolName();
    if (poolName != null && () > 0) {
      (poolName,());
    }
    if ( == null) {
      readDataSourceSize = 0;
    } else {
      for (int i = 0; i < (); i++) {
        (() + i, (i));
        poolName = ((HikariDataSource)(i)).getPoolName();
        if (poolName != null && () > 0) {
          (poolName,() + i);
        }
      }
      readDataSourceSize = ();
    }
    setTargetDataSources(targetDataSource);
    ();
  }


  @Override
  protected Object determineCurrentLookupKey() {
    DataSourceType dataSourceType = ();
    String dataSourceName = null;
    if (dataSourceType == null ||dataSourceType ==  || readDataSourceSize == 0) {
      dataSourceName = ();
    } else {
      String poolName = ();
      if (poolName == null) {
        int idx = ().nextInt(0, readDataSourceSize);
        dataSourceName = () + idx;
      } else {
        dataSourceName = (poolName);
      }
    }
    ();
    return dataSourceName;
  }

  public void setWriteDataSource(DataSource writeDataSource) {
     = writeDataSource;
  }

  public void setReadDataSource(List<DataSource> readDataSource) {
     = readDataSource;
  }
}

Intercept

If custom configuration is done in the corresponding dao layer (specifying the data source), some processing is performed. Analyze the corresponding method@DataSourceNote, if it exists, and save the corresponding information to the aboveDynamicDataSourceHoldermiddle. Here's theThe package is intercepted. The content is as follows:

import ;
import ;
import ;
import ;
import ;
import ;
import ;
import ;
import ;

import ;

/**
  * Use AOP interception, you can specify the data source name to use (corresponding to the connection pool name) for special methods.
  */
@Aspect
@Component
public class DynamicDataSourceAspect {

  @Pointcut("execution(public * .*.*(*))")
  public void dynamic(){}

  @Before(value = "dynamic()")
  public void beforeOpt(JoinPoint point) {
    Object target = ();
    String methodName = ().getName();
    Class&lt;?&gt;[] clazz = ().getInterfaces();
    Class&lt;?&gt;[] parameterType = ((MethodSignature)()).getMethod().getParameterTypes();
    try {
      Method method = clazz[0].getMethod(methodName,parameterType);
      if (method != null &amp;&amp; ()) {
        DataSource datasource = ();
        (());
        String poolName = ();
        (poolName);
        (clazz[0].getName() + "." + methodName, ());
      }
    } catch (Exception e) {
      ();
    }
  }

  @After(value = "dynamic()")
  public void afterOpt(JoinPoint point) {
    ();
  }
}

Plugin

If the corresponding data source to be used is not specified at the dao layer, intercepting is performed here. According to whether the data source type is updated or queryed, the content is as follows:

import ;
import ;
import ;
import .*;
import ;
import ;

import ;

@Intercepts({
    @Signature(type = , method = "update", args = {, }),
    @Signature(type = , method = "query", args = {, ,
        , })
})
public class DynamicDataSourcePlugin implements Interceptor {

  @Override
  public Object intercept(Invocation invocation) throws Throwable {
    MappedStatement ms = (MappedStatement)()[0];
    DataSourceType dataSourceType = null;
    if ((dataSourceType = (())) == null) {
      if (().equals()) {
        dataSourceType = ;
      } else {
        dataSourceType = ;
      }
      ((), dataSourceType);
    }
    (dataSourceType);
    return ();
  }

  @Override
  public Object plugin(Object target) {
    if (target instanceof Executor) {
      return (target, this);
    } else {
      return target;
    }
  }

  @Override
  public void setProperties(Properties properties) {

  }
}

5. Integration

Define what MyBatis is to use andDataSource, the content is as follows:

import ;
import ;
import ;
import ;
import ;
import ;
import ;
import ;
import ;
import ;
import ;
import ;

import ;
import ;

@Configuration
@MapperScan(value = "", sqlSessionFactoryRef = "sqlSessionFactory")
public class DataSourceConfig {
  @Resource
  private DBConfig dbConfig;

  @Bean(name = "dataSource")
  public DynamicDataSource dataSource() {
    DynamicDataSource dataSource = new DynamicDataSource();
    ((()));
    ((()));
    return dataSource;
  }

  @Bean(name = "transactionManager")
  public DataSourceTransactionManager dataSourceTransactionManager(@Qualifier("dataSource") DataSource dataSource) {
    return new DataSourceTransactionManager(dataSource);
  }

  @Bean(name = "sqlSessionFactory")
  public SqlSessionFactory sqlSessionFactory(@Qualifier("dataSource") DataSource dataSource) throws Exception {
    SqlSessionFactoryBean sessionFactoryBean = new SqlSessionFactoryBean();
    (new ClassPathResource(""));
    (new PathMatchingResourcePatternResolver()
        .getResources("classpath*:mapper/*.xml"));
    (dataSource);
    return ();
  }
}

If you are not clear, you can check the source code on githuborderdemo

The above is all the content of this article. I hope it will be helpful to everyone's study and I hope everyone will support me more.