A series of questions raised by Mybatis - spring multi data source configuration

In daily development, we use a single database for development, which can fully meet the needs in small projects. However, when we involve large-scale projects like Taobao and JD.com, a single database can hardly bear the CRUD operations of users. At this time, we need to use multiple data sources to separate read and write operations, which is also a popular data management method at present.

1 Spring Boot configures multiple data sources

Define the data required by the datasource in a YAML file:

spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    default-datasource:
      username: root
      password: root
      url: jdbc:mysql://localhost:3306/default?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
      driver-class-name: com.mysql.jdbc.Driver
    inspur-zs-datasource:
      username: root
      password: root
      url: jdbc:mysql://localhost:3306/inspur-zs?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
      driver-class-name: com.mysql.jdbc.Driver

    druid:
      initial-size: 5
      min-idle: 1
      max-active: 20
  profiles:
    active: dev

mybatis:
  mapper-locations: classpath:/mapper/*.xml
  type-aliases-package: com.inspur.pojo
  configuration:
    mapUnderscoreToCamelCase: true
    log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl

Define multiple data sources:

package com.inspur.spring.config.datasource;

import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.sql.DataSource;

/**
 * 数据源配置
 * ConfigurationProperties注解用于将YAML中指定的数据创建成指定的对象,
 * 但是,YAML中的数据必须要与对象对象中的属性同名,不然无法由Spring Boot完成赋值。
 *
 * @author zhaoshuai-lc
 * @date 2023/07/11
 */
@Configuration
public class DataSourceConfig {
    
    

    @Bean(name = "defaultDatasource")
    @ConfigurationProperties(prefix = "spring.datasource.default-datasource")
    public DataSource defaultDatasource() {
    
    
        return DruidDataSourceBuilder.create().build();
    }

    @Bean(name = "inspurZsDatasource")
    @ConfigurationProperties(prefix = "spring.datasource.inspur-zs-datasource")
    public DataSource inspurZsDatasource() {
    
    
        return DruidDataSourceBuilder.create().build();
    }
}

Since we need to define multiple data sources, it is impossible to determine which data source to import to complete the initialization in the Spring Boot data source automatic configuration class, so we need to disable Spring Boot's data source automatic configuration class, and then use our custom The data source configuration class to complete the initialization and management of the data source.

package com.inspur;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

/**
 * WtMybatisStudyApplication
 * 由于我们要定义多个数据源,所以在Spring Boot数据源自动配置类中就无法确定导入哪个数据源来完成初始化,
 * 所以我们就需要禁用掉Spring Boot的数据源自动配置类,然后使用我们自定义的数据源配置类来完成数据源的初始化与管理。
 *
 * @author zhaoshuai-lc
 * @date 2023/07/11
 */
@SpringBootApplication(exclude = {
    
    DataSourceAutoConfiguration.class})
@EnableAspectJAutoProxy
public class WtMybatisStudyApplication {
    
    
    public static void main(String[] args) {
    
    
        SpringApplication.run(WtMybatisStudyApplication.class, args);
    }
}

1.1 Specify the data source - implement the DataSource interface

Disadvantages: A lot of code redundancy is generated, and there is hard coding in the code.

package com.inspur.spring.config.datasource;

import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import javax.sql.DataSource;
import java.io.PrintWriter;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.util.logging.Logger;

/**
 * 方式一
 * DynamicDataSource1
 * 实现DataSource接口我们本质上只使用了一个方法就是getConnection()这个无参的方法
 *
 * @author zhaoshuai-lc
 * @date 2023/07/11
 */
@Component
@Primary
public class DynamicDataSource1 implements DataSource {
    
    
    public static ThreadLocal<String> flag = new ThreadLocal<>();

    @Resource
    private DataSource defaultDatasource;

    @Resource
    private DataSource inspurZsDatasource;

    public DynamicDataSource1() {
    
    
        flag.set("defaultDatasource");
    }


    @Override
    public Connection getConnection() throws SQLException {
    
    
        if (flag.get().equals("defaultDatasource")) {
    
    
            return defaultDatasource.getConnection();
        } else if (flag.get().equals("inspurZsDatasource")) {
    
    
            return inspurZsDatasource.getConnection();
        }
        return defaultDatasource.getConnection();
    }

    @Override
    public Connection getConnection(String username, String password) throws SQLException {
    
    
        return null;
    }

    @Override
    public <T> T unwrap(Class<T> iface) throws SQLException {
    
    
        return null;
    }

    @Override
    public boolean isWrapperFor(Class<?> iface) throws SQLException {
    
    
        return false;
    }

    @Override
    public PrintWriter getLogWriter() throws SQLException {
    
    
        return null;
    }

    @Override
    public void setLogWriter(PrintWriter out) throws SQLException {
    
    

    }

    @Override
    public void setLoginTimeout(int seconds) throws SQLException {
    
    

    }

    @Override
    public int getLoginTimeout() throws SQLException {
    
    
        return 0;
    }

    @Override
    public Logger getParentLogger() throws SQLFeatureNotSupportedException {
    
    
        return null;
    }
}

In essence, we only use one method to implement the DataSource interface, which is getConnection() without parameters, but we also need to implement all the methods in the DataSource interface, but we don’t need to write the method body, that is, there are a lot of "waste". method" .
@Primary annotation == @Order(1), used to set the injection order of this class.

use:

    @Override
    public PageData<BsFactoryCalendar> selectByExample(BsFactoryCalendarExample example) {
    
    
        PageData<BsFactoryCalendar> pageData = new PageData<>();
		DynamicDataSource1.flag.set("default-datasource");
        List<BsFactoryCalendar> bsFactoryCalendars = bsFactoryCalendarMapper.selectByExample(example);
        PageInfo<BsFactoryCalendar> pageInfo = new PageInfo<>(bsFactoryCalendars);
        pageData.setRows(bsFactoryCalendars);
        pageData.setTotal(pageInfo.getTotal());
        return pageData;
    }

1.2 Specify the data source - inherit the AbstrictRoutingDataSource class

The redundancy of the code is reduced, but there will still be hard coding.

package com.inspur.spring.config.datasource;

import cn.hutool.core.map.MapUtil;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import javax.sql.DataSource;
import java.util.Map;

/**
 * DynamicDataSource2
 * AbstrictRoutingDataSource的本质就是利用一个Map将数据源存储起来,然后通过Key来得到Value来修改数据源。
 *
 * @author zhaoshuai-lc
 * @date 2023/07/11
 */

@Component
@Primary
public class DynamicDataSource2 extends AbstractRoutingDataSource {
    
    
    public static ThreadLocal<String> flag = new ThreadLocal<>();
    @Resource
    private DataSource defaultDatasource;

    @Resource
    private DataSource inspurZsDatasource;

    public DynamicDataSource2() {
    
    
        flag.set("defaultDatasource");
    }

    @Override
    protected Object determineCurrentLookupKey() {
    
    
        return flag.get();
    }

    @Override
    public void afterPropertiesSet() {
    
    
        Map<Object, Object> targetDataSource = MapUtil.newConcurrentHashMap();
        // 将第一个数据源设置为默认的数据源
        super.setDefaultTargetDataSource(defaultDatasource);
        targetDataSource.put("defaultDatasource", defaultDatasource);
        targetDataSource.put("inspurZsDatasource", inspurZsDatasource);
        super.setTargetDataSources(targetDataSource);
        super.afterPropertiesSet();
    }
}

The essence of AbstractRoutingDataSource is to use a Map to store the data source, and then use the Key to get the Value to modify the data source.

use:

    @Override
    public PageData<BsFactoryCalendar> selectByExample(BsFactoryCalendarExample example) {
    
    
        PageData<BsFactoryCalendar> pageData = new PageData<>();
		DynamicDataSource2.flag.set("default-datasource");
        List<BsFactoryCalendar> bsFactoryCalendars = bsFactoryCalendarMapper.selectByExample(example);

        PageInfo<BsFactoryCalendar> pageInfo = new PageInfo<>(bsFactoryCalendars);
        pageData.setRows(bsFactoryCalendars);
        pageData.setTotal(pageInfo.getTotal());
        return pageData;
    }

1.3 Specify the data source - use Spring AOP + custom annotation form

The form of Spring AOP + custom annotation is a recommended way of writing, which reduces code redundancy and does not have hard coding. This method is suitable for manipulating the schema of the specified database for the specified functionality.

Import dependencies:

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

Enable AOP support:

package com.inspur;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

@SpringBootApplication(exclude = {
    
    DataSourceAutoConfiguration.class})
@EnableAspectJAutoProxy
public class WtMybatisStudyApplication {
    
    

    public static void main(String[] args) {
    
    
        SpringApplication.run(WtMybatisStudyApplication.class, args);
    }

}

Define an enum to represent the identity of the data source:

package com.inspur.spring.config.datasource.enums;

public enum DataSourceType {
    
    
    DEFAULT_DATASOURCE,
    INSPURZS_DATASOURCE,
}

Inherit the AbstractRoutingDataSource class:

package com.inspur.spring.config.datasource;

import cn.hutool.core.map.MapUtil;
import com.inspur.spring.config.datasource.enums.DataSourceType;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import javax.sql.DataSource;
import java.util.Map;

@Primary
@Component
public class DynamicDataSource3 extends AbstractRoutingDataSource {
    
    
    public static ThreadLocal<String> flag = new ThreadLocal<>();
    @Resource
    private DataSource defaultDatasource;

    @Resource
    private DataSource inspurZsDatasource;

    public DynamicDataSource3() {
    
    
        flag.set(DataSourceType.DEFAULT_DATASOURCE.name());
    }

    @Override
    protected Object determineCurrentLookupKey() {
    
    
        return flag.get();
    }

    @Override
    public void afterPropertiesSet() {
    
    
        Map<Object, Object> targetDataSource = MapUtil.newConcurrentHashMap();
        // 将第一个数据源设置为默认的数据源
        super.setDefaultTargetDataSource(defaultDatasource);
        targetDataSource.put(DataSourceType.DEFAULT_DATASOURCE.name(), defaultDatasource);
        targetDataSource.put(DataSourceType.INSPURZS_DATASOURCE.name(), inspurZsDatasource);
        super.setTargetDataSources(targetDataSource);
        super.afterPropertiesSet();
    }
}

Custom annotations:

package com.inspur.spring.config.datasource.annotation;

import com.inspur.spring.config.datasource.enums.DataSourceType;

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;

@Target({
    
    ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface TargetDataSource {
    
    
    DataSourceType value() default DataSourceType.DEFAULT_DATASOURCE;
}

Define the implementation class of the annotation:

package com.inspur.spring.config.datasource.annotation;

import com.inspur.spring.config.datasource.DynamicDataSource3;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;

/**
 * TargetDataSourceAspect
 *
 * @author zhaoshuai-lc
 * @date 2023/07/11
 */
@Component
@Aspect
@Slf4j
public class TargetDataSourceAspect {
    
    
    @Before("@within(com.inspur.spring.config.datasource.annotation.TargetDataSource) || " +
            "@annotation(com.inspur.spring.config.datasource.annotation.TargetDataSource)")
    public void beforeNoticeUpdateDataSource(JoinPoint joinPoint) {
    
    
        TargetDataSource annotation = null;
        Class<? extends Object> target = joinPoint.getTarget().getClass();
        if (target.isAnnotationPresent(TargetDataSource.class)) {
    
    
            // 判断类上是否标注着注解
            annotation = target.getAnnotation(TargetDataSource.class);
            log.info("类: {}, 标注了注解@TargetDataSource", target);
        } else {
    
    
            Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
            if (method.isAnnotationPresent(TargetDataSource.class)) {
    
    
                // 判断方法上是否标注着注解,如果类和方法上都没有标注,则报错
                annotation = method.getAnnotation(TargetDataSource.class);
                log.info("方法: {}, 标注了注解@TargetDataSource", method);
            } else {
    
    
                log.error("注解@TargetDataSource只能用于类或者方法上, error: {} {}", target, method);
                throw new RuntimeException("注解@TargetDataSource使用错误");
            }
        }
        // 切换数据源
        DynamicDataSource3.flag.set(annotation.value().name());
    }
}

use:

package com.inspur.spring.service;

import com.github.pagehelper.PageInfo;
import com.inspur.spring.common.interfaceResult.PageData;
import com.inspur.spring.config.datasource.annotation.TargetDataSource;
import com.inspur.spring.config.datasource.enums.DataSourceType;
import com.inspur.spring.dao.BsFactoryCalendarMapper;
import com.inspur.spring.pojo.BsFactoryCalendar;
import com.inspur.spring.pojo.BsFactoryCalendarExample;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;
import java.util.List;

@Service
@TargetDataSource(value = DataSourceType.DEFAULT_DATASOURCE) // 方式三 多数据源设置
public class BsFactoryCalendarServiceImpl implements BsFactoryCalendarService {
    
    

    @Resource
    private BsFactoryCalendarMapper bsFactoryCalendarMapper;

    @Override
    @Transactional
    public PageData<BsFactoryCalendar> selectByExample(BsFactoryCalendarExample example) {
    
    
        PageData<BsFactoryCalendar> pageData = new PageData<>();
        List<BsFactoryCalendar> bsFactoryCalendars = bsFactoryCalendarMapper.selectByExample(example);

        PageInfo<BsFactoryCalendar> pageInfo = new PageInfo<>(bsFactoryCalendars);
        pageData.setRows(bsFactoryCalendars);
        pageData.setTotal(pageInfo.getTotal());
        return pageData;
    }
}

1.4 Operate the XML file in the specified directory through the data source specified by SqlSessionFactory

Using this method will not have any relationship with the class mentioned above, this method will redefine the class. This method is also a recommended method, which is suitable for the operation of the specified database, that is, suitable for read-write separation. There will be no code redundancy and no hardcoding.

insert image description here

Configure the YAML file:

spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    default-datasource:
      username: root
      password: root
      jdbc-url: jdbc:mysql://localhost:3306/default?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
      driver-class-name: com.mysql.jdbc.Driver
    inspur-zs-datasource:
      username: root
      password: root
      jdbc-url: jdbc:mysql://localhost:3306/inspur-zs?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
      driver-class-name: com.mysql.jdbc.Driver
  main:
    allow-bean-definition-overriding : true
    
    druid:
      initial-size: 5
      min-idle: 1
      max-active: 20
  profiles:
    active: dev

mybatis:
  mapper-locations: classpath:/mapper/*.xml
  type-aliases-package: com.inspur.pojo
  configuration:
    mapUnderscoreToCamelCase: true
    log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl

For the Mapper layer, specify the data source through SqlSessionFactory to operate:

package com.inspur.spring.config.datasource;

import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;

import javax.sql.DataSource;

@Configuration
@MapperScan(basePackages = "com.inspur.spring.dao.defaultzs", sqlSessionFactoryRef = "DefaultSqlSessionFactory")
public class DefaultDatasourceConfig {
    
    

    @Primary
    @Bean(name = "DefaultDatasource")
    @ConfigurationProperties(prefix = "spring.datasource.default-datasource")
    public DataSource getDateSource1() {
    
    
        return DataSourceBuilder.create().build();
    }

    @Primary
    @Bean(name = "DefaultSqlSessionFactory")
    public SqlSessionFactory sqlSessionFactory(@Qualifier("DefaultDatasource") DataSource datasource) throws Exception {
    
    
        SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
        bean.setDataSource(datasource);
        // 设置mybatis的xml所在位置
        bean.setMapperLocations(new PathMatchingResourcePatternResolver()
                .getResources("classpath*:mapper/defaultzs/*.xml"));
        return bean.getObject();
    }


    @Bean("DefaultSqlSessionTemplate")
    @Primary
    public SqlSessionTemplate sqlSessionTemplate(@Qualifier("DefaultSqlSessionFactory") SqlSessionFactory factory) {
    
    
        return new SqlSessionTemplate(factory);
    }

    @Bean
    public PlatformTransactionManager transactionManager(@Qualifier("DefaultDatasource") DataSource dataSource) {
    
    
        return new DataSourceTransactionManager(dataSource);
    }
}

package com.inspur.spring.config.datasource;

import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;

import javax.sql.DataSource;

@Configuration
@MapperScan(basePackages = "com.inspur.spring.dao.inspurzs", sqlSessionFactoryRef = "InspurZsSqlSessionFactory")
public class InspurZsDatasourceConfig {
    
    

    @Primary
    @Bean(value = "InspurZsDatasource")
    @ConfigurationProperties(prefix = "spring.datasource.inspur-zs-datasource")
    public DataSource getDateSource1() {
    
    
        return DataSourceBuilder.create().build();
    }

    @Primary
    @Bean(value = "InspurZsSqlSessionFactory")
    public SqlSessionFactory sqlSessionFactory(@Qualifier("InspurZsDatasource") DataSource datasource) throws Exception {
    
    
        SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
        bean.setDataSource(datasource);
        // 设置mybatis的xml所在位置
        bean.setMapperLocations(new PathMatchingResourcePatternResolver()
                .getResources("classpath*:mapper/inspurzs/*.xml"));
        return bean.getObject();
    }


    @Bean(value = "InspurZsSqlSessionTemplate")
    @Primary
    public SqlSessionTemplate sqlSessionTemplate(@Qualifier("InspurZsSqlSessionFactory") SqlSessionFactory factory) {
    
    
        return new SqlSessionTemplate(factory);
    }

    @Bean
    public PlatformTransactionManager transactionManager(@Qualifier("InspurZsDatasource") DataSource dataSource) {
    
    
        return new DataSourceTransactionManager(dataSource);
    }
}

The basePackages in the @MapperScan annotation points to the specified Dao layer.
The sqlSessionFactoryRef in the @MapperScan annotation is used to specify the use of a SqlSessionFactory to operate the data source.

 bean.setMapperLocations(new PathMatchingResourcePatternResolver()
                .getResources("classpath*:mapper/inspurzs/*.xml"));

Using this method will not have any code redundancy and hard-coding, but it needs to be layered and clear. The only downside is that adding a data source requires rewriting a class, and most of the code in this class is the same.

Guess you like

Origin blog.csdn.net/zs18753479279/article/details/132096035
Recommended