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.
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.