项目总结68:Springboot集成动态数据源示例
START
代码示例
POM文件
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jdbc</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.1.2</version> </dependency>
RoutingDataSource类:继承AbstractRoutingDataSource 类;重写determineCurrentLookupKey()方法和setTargetDataSources()方法
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource; import java.util.Map; public class RoutingDataSource extends AbstractRoutingDataSource { @Override protected Object determineCurrentLookupKey() { return RoutingDataSourceContext.getDataSourceRouteKey(); } @Override public void setTargetDataSources(Map<Object, Object> targetDataSources) { super.setTargetDataSources(targetDataSources); RoutingDataSourceContext.putDataSourceKeys(targetDataSources.keySet()); } }
RoutingDataSourceContext 类
import java.util.Collection; import java.util.HashSet; import java.util.Set; public class RoutingDataSourceContext { private static final Set<Object> dataSourceKeys = new HashSet<>(); private static final ThreadLocal<DataSourceRouteEnums> threadLocalDataSourceKey = new ThreadLocal<DataSourceRouteEnums>(){ @Override protected DataSourceRouteEnums initialValue(){ return DataSourceRouteEnums.DEFAULT_MYSQL; } }; public static DataSourceRouteEnums getDataSourceRouteKey(){ return threadLocalDataSourceKey.get(); } public static void setThreadLocalDataSourceKey(DataSourceRouteEnums dataSourceRoutekey){ threadLocalDataSourceKey.set(dataSourceRoutekey); } public static void remove(){ threadLocalDataSourceKey.remove();; } public static void putDataSourceKeys(Collection<Object> keys){ dataSourceKeys.addAll(keys); } public static boolean containKey(DataSourceRouteEnums dataSourceRoutekey){ return dataSourceKeys.contains(dataSourceRoutekey); } }
RoutingDataSourceConfig类
import com.alibaba.druid.pool.DruidDataSource; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Primary; import javax.sql.DataSource; import java.util.HashMap; import java.util.Map; @Configuration public class RoutingDataSourceConfig { @Bean("defaultMysql") @ConfigurationProperties("spring.datasource") @Lazy public DataSource defaultMysql(){ return new DruidDataSource(); } @Bean("specMysql") @ConfigurationProperties("spring.datasource-mysql") public DataSource specMysql(){ return new DruidDataSource(); } @Bean("dynamicDataSource") @Primary public DataSource dynamicDataSource(){ RoutingDataSource routingDataSource = new RoutingDataSource(); routingDataSource.setDefaultTargetDataSource(defaultMysql());//默认数据源 Map<Object,Object> targetDataSourceMap = new HashMap<>(); targetDataSourceMap.put(DataSourceRouteEnums.DEFAULT_MYSQL,defaultMysql());//数据源1(默认数据源) targetDataSourceMap.put(DataSourceRouteEnums.SPEC_MYSQL,specMysql());//数据源2 routingDataSource.setTargetDataSources(targetDataSourceMap); return routingDataSource; } }
DataSourceRouteEnums枚举类
public enum DataSourceRouteEnums { //默认数据源 DEFAULT_MYSQL, //数据源2 SPEC_MYSQL, }
DataSourceAnno注解类
@Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE,ElementType.METHOD}) public @interface DataSourceAnno { DataSourceRouteEnums value(); }
RoutingDataSourceAspect类
import com.tyj.study.dynamicdatasource.config.DataSourceRouteEnums; import com.tyj.study.dynamicdatasource.config.RoutingDataSourceContext; import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.springframework.stereotype.Component; @Aspect @Slf4j @Component public class RoutingDataSourceAspect { @Before("@annotation(dataSourceAnno)") public void before(JoinPoint point, DataSourceAnno dataSourceAnno){ DataSourceRouteEnums dataSourceKey = dataSourceAnno.value(); if(!RoutingDataSourceContext.containKey(dataSourceKey)){ log.info("RoutingDataSource AOP before : method[{}],datasource [{}] not exist, use default",point.getSignature(),dataSourceKey); }else{ RoutingDataSourceContext.setThreadLocalDataSourceKey(dataSourceKey); log.info("RoutingDataSource AOP before : method[{}],use default [{}]",point.getSignature(),dataSourceKey); } } @Before("@annotation(dataSourceAnno)") public void after(JoinPoint point, DataSourceAnno dataSourceAnno){ RoutingDataSourceContext.remove(); log.info("RoutingDataSource AOP after : restore DataSource to [{}] in [{}]",dataSourceAnno.value(),point.getSignature()); } }
DynamicDataSourceTestController类
import com.tyj.study.dynamicdatasource.aop.DataSourceAnno; import com.tyj.study.dynamicdatasource.service.DynamicDataSourceService; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.List; import java.util.Map; @Api(tags = "动态数据源测试") @RestController @RequestMapping("dynamicdatasource") public class DynamicDataSourceTestController { @Autowired private DynamicDataSourceService dynamicDataSourceService; @ApiOperation("默认数据库") @GetMapping("default") public List<Map> testDefault(){ List<Map> maps = dynamicDataSourceService.listTables(); return maps; } @ApiOperation("第二数据库") @GetMapping("spec") @DataSourceAnno(DataSourceRouteEnums.SPEC_MYSQL) public List<Map> testSpecMysql(){ List<Map> maps = dynamicDataSourceService.listTables(); return maps; } }
import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; @SpringBootApplication(exclude = {DataSourceAutoConfiguration.class}) @MapperScan("com.tyj.study.dynamicdatasource.mapper") public class StudyApplication { public static void main(String[] args) { SpringApplication.run(StudyApplication.class, args); } }
server.port=8080 spring.datasource.url=jdbc:mysql://XXX.XX.XXX.XXA:3306/lop_project?useUnicode=true&characterEncoding=utf8&characterSetResults=utf8 spring.datasource.username=wobuchifanqie spring.datasource.password=wobuchifanqie123 spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver spring.datasource.type=com.alibaba.druid.pool.DruidDataSource spring.datasource-mysql.url=jdbc:mysql://XXX.XX.XXX.XXB:3306/mall?useUnicode=true&characterEncoding=utf8&characterSetResults=utf8 spring.datasource-mysql.username=wobuchifanqie spring.datasource-mysql.password=wobuchifanqie1234 spring.datasource-mysql.driver-class-name=com.mysql.cj.jdbc.Driver spring.datasource-mysql.type=com.alibaba.druid.pool.DruidDataSource mybatis.mapper-locations=classpath:/mapper/**/*.xml
非重点类
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.tyj.study.dynamicdatasource.mapper.DynamicDataSourceMapper"> <select id="listTables" parameterType="java.lang.String" resultType="java.util.Map"> show tables </select> </mapper>
public interface DynamicDataSourceMapper { List<Map> listTables(); } public interface DynamicDataSourceService { List<Map> listTables(); } @Service public class DynamicDataSourceServiceImpl implements DynamicDataSourceService{ @Autowired private DynamicDataSourceMapper dynamicDataSourceMapper; @Override public List<Map> listTables() { return dynamicDataSourceMapper.listTables(); } }
执行逻辑
1- 项目启动
1-1-启动类启动,自动扫描需要加载的类配置,这里需要手动排除DataSourceAutoConfiguration类;否则会报循环依赖异常
1-2-加载多数据源配置类RoutingDataSourceConfig类;将多个数据源加载到IOC;具体细节去如下:
(1) routingDataSource.setDefaultTargetDataSource(defaultMysql()); 配置默认数据源
(2) routingDataSource.setTargetDataSources(targetDataSourceMap); 记录多个数据源,与此同时,多个数据源会被放在RoutingDataSourceContext类的Set<Object> dataSourceKeys中;
(3)
2-请求接口(自动切换数据源)
2-1- 如果接口方法使用非默认数据源,加上@DataSourceAnno(DataSourceRouteEnums.SPEC_MYSQL);
2-2-Controller收到请求时,AOP根据@DataSourceAnno(DataSourceRouteEnums.SPEC_MYSQL)匹配执行before通知,在before通知中,执行RoutingDataSourceContext.setThreadLocalDataSourceKey(dataSourceKey),即读取当前选择的数据源-放在ThreadLocal中;
2-3-执行数据库请求;
2-4-Controller返回请求结果后,AOP根据@DataSourceAnno(DataSourceRouteEnums.SPEC_MYSQL)匹配执行after通知,在after通知中,执行RoutingDataSourceContext.remove();,即移除数据源-从ThreadLocal中移除;
附录1-异常:循环依赖异常
The dependencies of some of the beans in the application context form a cycle: dynamicDataSourceTestController (field private com.tyj.study.dynamicdatasource.service.DynamicDataSourceService com.tyj.study.dynamicdatasource.config.DynamicDataSourceTestController.dynamicDataSourceService) ↓ dynamicDataSourceServiceImpl (field private com.tyj.study.dynamicdatasource.mapper.DynamicDataSourceMapper com.tyj.study.dynamicdatasource.service.DynamicDataSourceServiceImpl.dynamicDataSourceMapper) ↓ dynamicDataSourceMapper defined in file [D:\workspace\study\target\classes\com\tyj\study\dynamicdatasource\mapper\DynamicDataSourceMapper.class] ↓ sqlSessionFactory defined in class path resource [org/mybatis/spring/boot/autoconfigure/MybatisAutoConfiguration.class] ┌─────┐ | dynamicDataSource defined in class path resource [com/tyj/study/dynamicdatasource/config/RoutingDataSourceConfig.class] ↑ ↓ | defaultMysql defined in class path resource [com/tyj/study/dynamicdatasource/config/RoutingDataSourceConfig.class] ↑ ↓ | org.springframework.boot.autoconfigure.jdbc.DataSourceInitializerInvoker └─────┘
END