背景
在实际开发中,我们一个项目可能会用到多个数据库,通常一个数据库对应一个数据源。本示例,通过两种方式实现多数据源切换
1)手动切换
2)使用注解进行切换
本文使用的是第二种方式,使用注解的方式进行切换
一. 准备
1 maven依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example.springboot</groupId>
<artifactId>multi-datasource</artifactId>
<version>1.0-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.3.RELEASE</version> <relativePath/>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.0.0</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>runtime</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.alibaba/druid -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.0.29</version>
</dependency> <!-- 使用aspectj时需要 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
2 配置
application.yml
spring:
datasource:
# 测试环境
type: com.alibaba.druid.pool.DruidDataSource
shike:
url: jdbc:mysql://***.**.**.*:3306/shike?zeroDateTimeBehavior=round&useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2b8
username: root
password: ******
#springboot 1.*使用com.mysql.jdbc.Driver;2.*的使用com.mysql.cj.jdbc.Driver
driver-class-name: com.mysql.cj.jdbc.Driver
initial-size: 5
min-idle: 1
max-active: 100
test-on-borrow: true
connection-test-query: "SELECT 1"
gift:
url: jdbc:mysql://***.**.**.*:3306/gift_db?zeroDateTimeBehavior=round&useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2b8
username: root
password: ******
driver-class-name: com.mysql.cj.jdbc.Driver
initial-size: 5
min-idle: 1
max-active: 100
test-on-borrow: true
connection-test-query: "SELECT 1"
mybatis:
mapperLocations: classpath:mapper/*Mapper.xml
typeAliasesPackage: com.yancheng.app.domain
工具类
DataSourceContextHolder
作用:构建一个DataSourceEnum容器,并提供了向其中设置和获取DataSorceEnum的方法
package com.yanchengtech.app.dataSource;
/**
* @author 文心雕龙 Email:xuwenlong@shike8888.com
* @version 创建时间:2017年10月18日 下午2:47:32 类说明
*/
public class DbContextHolder {
//列举数据源的key
public enum DbType {
WRITE, READ1,READ2
}
// private static final ThreadLocal<DbType> contextHolder = new ThreadLocal<>();
private static final ThreadLocal<DbType> contextHolder = new ThreadLocal<DbType>() {
@Override
protected DbType initialValue() {
return DbType.WRITE;
}
};
public static void setDbType(DbType dbType) {
if (dbType == null)
throw new NullPointerException();
contextHolder.set(dbType);
}
public static DbType getDbType() {
return contextHolder.get() == null ? DbType.WRITE : contextHolder.get();
}
public static void resetDbType(){
contextHolder.set(DbType.WRITE);
}
public static void clearDbType() {
contextHolder.remove();
}
}
DynamicDataSource
作用:使用DatabaseContextHolder获取当前线程的DataSoureEnum
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
public class DynamicDataSource extends AbstractRoutingDataSource {
protected Object determineCurrentLookupKey() {
return DbContextHolder.getDbType();
}
MyBatisConfig
通过读取application.properties文件生成两个数据源(masterDataSource,slaverDataSource)
使用以上生成的两个数据源构造动态数据源dataSource
@Primary:指定在同一个接口有多个实现类可以注入的时候,默认选择哪一个,而不是让@Autowire注解报错(一般用于多数据源的情况下)
@Qualifier:指定名称的注入,当一个接口有多个实现类的时候使用(在本例中,有三个DataSource类型的实例,DynamicDataSource也是一种DataSource,需要指定名称注入)
@Bean:生成的bean实例的名称是方法名(例如上边的@Qualifier注解中使用的名称是前边两个数据源的方法名,而这两个数据源也是使用@Bean注解进行注入的)
通过动态数据源构造SqlSessionFactory和事务管理器(如果不需要事务,后者可以去掉)
import java.util.HashMap;
import java.util.Map;
import javax.sql.DataSource;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.log4j.Logger;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder;
import org.springframework.boot.context.properties.ConfigurationProperties;
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 com.yanchengtech.app.dataSource.DbContextHolder.DbType;
import com.yanchengtech.app.dataSource.DynamicDataSource;
/**
* @author 文心雕龙 Email:xuwenlong@shike8888.com
* @version 创建时间:2017年10月18日 下午2:42:53
* 类说明
*/
@Configuration
@MapperScan("com.yanchengtech.app.mapper")
public class MybatisConfig {
// private static Logger logger = Logger.getLogger(MybatisConfig.class);
@Value("${druid.type}")
private Class<? extends DataSource> dataSourceType;
@Primary
@Bean(name = "writeDataSource")
@ConfigurationProperties(prefix = "druid.write")
public DataSource writeDataSource(){
return DataSourceBuilder.create().type(dataSourceType).build();
}
@Bean(name = "readDataSource1")
@ConfigurationProperties(prefix = "druid.read1")
public DataSource readDataSource(){
return DataSourceBuilder.create().type(dataSourceType).build();
}
@Bean(name = "readDataSource2")
@ConfigurationProperties(prefix = "druid.read2")
public DataSource readDataSource(){
return DataSourceBuilder.create().type(dataSourceType).build();
}
/**
* @Qualifier 根据名称进行注入,通常是在具有相同的多个类型的实例的一个注入(例如有多个DataSource类型的实例)
*/
@Bean("dynamicDataSource")
public DynamicDataSource dynamicDataSource(@Qualifier("writeDataSource") DataSource writeDataSource,
@Qualifier("readDataSource") DataSource readDataSource) {
Map<Object, Object> targetDataSources = new HashMap<Object, Object>();
// 将配置对应的数据源信息映射到枚举对象中
targetDataSources.put(DbType.WRITE, writeDataSource);
targetDataSources.put(DbType.READ1, readDataSource1);
targetDataSources.put(DbType.READ2, readDataSource2);
DynamicDataSource dataSource = new DynamicDataSource();
dataSource.setTargetDataSources(targetDataSources);// 该方法是AbstractRoutingDataSource的方法
dataSource.setDefaultTargetDataSource(writeDataSource);// 默认的datasource设置为myTestDbDataSource
return dataSource;
}
/**
* 根据数据源创建SqlSessionFactory
*/
@Bean
public SqlSessionFactory sqlSessionFactory(@Qualifier("dynamicDataSource") DynamicDataSource dynamicDataSource,
@Value("${mybatis.typeAliasesPackage}") String typeAliasesPackage,
@Value("${mybatis.mapperLocations}") String mapperLocations) throws Exception {
SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
factoryBean.setDataSource(dynamicDataSource);// 指定数据源(这个必须有,否则报错)
// 下边两句仅仅用于*.xml文件,如果整个持久层操作不需要使用到xml文件的话(只用注解就可以搞定),则不加
factoryBean.setTypeAliasesPackage(typeAliasesPackage);// 指定基包
factoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(mapperLocations));//
return factoryBean.getObject();
}
/**
* 配置事务管理器
*/
@Bean
public DataSourceTransactionManager transactionManager(DynamicDataSource dataSource) throws Exception {
return new DataSourceTransactionManager(dataSource);
}
}
DataSourceTypeAnno数据源类型注解
@Retention(RetentionPolicy.RUNTIME) 说是此注解在运行时可见
@Target(ElementType.METHOD) // 注解可以用在方法上
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import com.yanchengtech.app.dataSource.DbContextHolder.DbType;
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ReadOnlyConnection {
DbType value() default DbContextHolder.DbType.READ;
}
DataSourceAspect 数据源切面
用于捕获使用数据源注解的方法,并且根据注解上的数据源类型进行切换
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import com.yanchengtech.app.dataSource.DbContextHolder.DbType;
import java.lang.reflect.Method;
@Component
@Aspect
public class DataSourceAspect {
@Pointcut("@annotation(com.yanchengtech.app.dataSource.ReadOnlyConnection)")
public void dataSourcePointcut() {
}
@Around("dataSourcePointcut()")
public Object doAround(ProceedingJoinPoint pjp) {
MethodSignature methodSignature = (MethodSignature) pjp.getSignature();
Method method = methodSignature.getMethod();
ReadOnlyConnection typeAnno = method.getAnnotation(ReadOnlyConnection.class);
DbType sourceEnum = typeAnno.value();
if (sourceEnum == DbType.WRITE) {
DbContextHolder.setDbType(DbType.WRITE);
} else if (sourceEnum == DbType.READ) {
DbContextHolder.setDbType(DbType.READ);
}
Object result = null;
try {
result = pjp.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
} finally {
DbContextHolder.resetDbType();
}
return result;
}
}
测试:
代码:
Controller:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.yanchengtech.app.service.PosterService;
@RestController
@RequestMapping("poster")
public class PosterCotroller {
@Autowired
PosterService posterService;
@RequestMapping("getAllPoster")
private String getAllPoster() {
return posterService.findAllPoster();
}
@RequestMapping("getAllPoster2")
private String getAllPoster2() {
return posterService.findAllPoster2();
}
@RequestMapping("getAllPoster1")
private String getAllPoster1() {
return posterService.findAllPoster1();
}
}
Service:
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.alibaba.fastjson.JSONObject;
import com.yanchengtech.app.dataSource.DbContextHolder.DbType;
import com.yanchengtech.app.dataSource.ReadOnlyConnection;
import com.yanchengtech.app.domain.Poster;
import com.yanchengtech.app.mapper.PosterMapper;
import com.yanchengtech.app.service.PosterService;
@Service
public class PosterServiceImpl implements PosterService{
@Autowired
PosterMapper posterMapper;
@Override
// 默认使用的是主数据库
public String findAllPoster() {
JSONObject object = new JSONObject();
List<Poster> list = posterMapper.findAllPoster();
if (null != list && list.size()>0) {
object.put("code", 1);
object.put("msg", "success");
object.put("data", list);
}else{
object.put("code", 0);
object.put("msg", "fail");
object.put("data", null);
}
return object.toString();
}
@Override
@ReadOnlyConnection(DbType.READ1)
public String findAllPoster1() {
JSONObject object = new JSONObject();
List<Poster> list = posterMapper.findAllPoster();
if (null != list && list.size()>0) {
object.put("code", 1);
object.put("msg", "success");
object.put("data", list);
}else{
object.put("code", 0);
object.put("msg", "fail");
object.put("data", null);
}
return object.toString();
}
@Override
@ReadOnlyConnection(DbType.READ2)
public String findAllPoster2() {
JSONObject object = new JSONObject();
List<Poster> list = posterMapper.findAllPoster();
if (null != list && list.size()>0) {
object.put("code", 1);
object.put("msg", "success");
object.put("data", list);
}else{
object.put("code", 0);
object.put("msg", "fail");
object.put("data", null);
}
return object.toString();
}
}
Mapper
import java.util.List;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import com.yanchengtech.app.domain.Poster;
@Mapper
public interface PosterMapper {
@Select("select * from poster where delFlag = 1")
List<Poster> findAllPoster();
}
springboot2.0 注解不仅可以在service层使用,也可以使用在dao/mapper中的方法上.
springboot1.* 只能注解在service层才能生效,且必须加上:
<dependency>
<groupId>com.mchange</groupId>
<artifactId>c3p0</artifactId>
<version>0.9.5</version> <!-- 自行使用适用版本 -->
</dependency>
觉得本文好的可以点个赞,觉得不好的可以提提建议或意见,一起进步,一起成长!!!
欢迎邮件来信讨论 490514142@qq.com