Springboot+mybaitsPlus动态数据源配置:
1. 动态数据源配置:
首先要将 spring boot 自带的 DataSourceAutoConfiguration
禁掉,因为它会读取 application.properties
文件的 spring.datasource.*
属性并自动配置单数据源。在 @SpringBootApplication
注解中添加 exclude
属性即可:
@SpringBootApplication (exclude = {
DataSourceAutoConfiguration.class
})
@EnableScheduling
public class Application extends SpringBootServletInitializer{
@Override
protected SpringApplicationBuilderconfigure(SpringApplicationBuilderapplication ) {
return application .sources(Application.class );
}
public static void main(String[] args ) {
SpringApplication.run (Application.class ,args );
}
}
Pom.xml 配置
< dependency >
< groupId > com.alibabagroupId >
< artifactId > druid artifactId >
< version > 1.0.11version >
dependency >
application.properties文件添加多源数据库配置
#从库数量(配置几个从库)
dataSourceNumber=1
druid.type: com.alibaba.druid.pool.DruidDataSource
druid.master:
druid.master.url= jdbc:mysql://127.0.0.1 :13306/baoliyun3?characterEncoding=utf8&useSSL=true
druid.master.driver-class-name= com.mysql.jdbc.Driver
druid.master.username= root
druid.master.password= 12345678
druid.master.initial-size= 5
druid.master.min-idle= 1
druid.master.max-active= 100
druid.master.test-on-borrow=true
druid.master.validationQuery=SELECT 1 FROM dual
druid.slave1:
druid.slave1.url= jdbc:mysql://127.0.0.1 :13306/baoliyunST?characterEncoding=utf8&useSSL=true
druid.slave1.driver-class-name= com.mysql.jdbc.Driver
druid.slave1.username= root
druid.slave1.password= 12345678
druid.slave1.initial-size= 5
druid.slave1.min-idle= 1
druid.slave1.max-active= 100
druid.slave1.test-on-borrow=true
druid.slave1.validationQuery=SELECT 1 FROM dual
# druid.slave2:
# druid.slave2.url=jdbc:mysql://127.0.0.1:13306/baoliyun2?characterEncoding=utf8&useSSL=true
# druid.slave2.driver-class-name=com.mysql.jdbc.Driver
# druid.slave2.username= root
# druid.slave2.password= 12345678
# druid.slave2.initial-size= 5
# druid.slave2.min-idle= 1
# druid.slave2.max-active= 100
# druid.slave2.test-on-borrow= true
# druid.slave2.validationQuery=SELECT 1FROM dual
目前配置两个数据库 主库master、从库slave1,可扩展
自动数据源已被禁掉,现在手动配置:
import javax.sql.DataSource;
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.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
@Configuration
@EnableTransactionManagement
public class DataSourceConfiguration {
@Value ("${druid.type}" )
private Classextends DataSource> dataSourceType ;
@Bean (name = "slaveDataSource1" )
@ConfigurationProperties (prefix ="druid.slave1" )
public DataSource slaveDataSource1(){
return DataSourceBuilder.create ().build();
}
// @Bean(name= "slaveDataSource2")
// @ConfigurationProperties(prefix = "druid.slave2")
// publicDataSource slaveDataSource2(){
// return DataSourceBuilder.create().build();
// }
@Bean (name = "masterDataSource" )
@ConfigurationProperties (prefix ="druid.master" )
public DataSource masterDataSource(){
return DataSourceBuilder.create ().build();
}
/**
* 配置事务管理器
*/
@Bean
public DataSourceTransactionManagertransactionManager(DynamicDataSourcedataSource ) throws Exception {
return new DataSourceTransactionManager(dataSource );
}
}
使用mybaitsplus的MybatisSqlSessionFactoryBean配置动态数据源
import java.util.HashMap;
import java.util.Map;
import javax.annotation.Resource;
import javax.sql.DataSource;
import org.apache.ibatis.mapping.DatabaseIdProvider;
import org.apache.ibatis.plugin.Interceptor;
import org.mybatis.spring.boot.autoconfigure.MybatisProperties;
import org.mybatis.spring.boot.autoconfigure.SpringBootVFS;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.io.ResourceLoader;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import com.baomidou.mybatisplus.MybatisConfiguration;
import com.baomidou.mybatisplus.MybatisXMLLanguageDriver;
import com.baomidou.mybatisplus.entity.GlobalConfiguration;
import com.baomidou.mybatisplus.enums.IdType;
import com.baomidou.mybatisplus.plugins.PaginationInterceptor;
import com.baomidou.mybatisplus.spring.MybatisSqlSessionFactoryBean;
import com.ibr.baoliyun.config.DataSourceContextHolder.DbType;
@Configuration
public class MybatisPlusConfig{
@Autowired
private MybatisProperties properties;
@Autowired
private ResourceLoader resourceLoader = new DefaultResourceLoader();
@Autowired(required = false)
private Interceptor[] interceptors;
@Autowired(required = false)
private DatabaseIdProvider databaseIdProvider;
/**
*
mybatis-plus分页插件
*/
@Bean
public PaginationInterceptor paginationInterceptor() {
PaginationInterceptor page = new PaginationInterceptor();
page.setDialectType("mysql");
return page;
}
/**
* 这里全部使用mybatis-autoconfigure 已经自动加载的资源。不手动指定
* 配置文件和mybatis-boot的配置文件同步
* @return
*/
@Bean
public MybatisSqlSessionFactoryBean mybatisSqlSessionFactoryBean() {
MybatisSqlSessionFactoryBean mybatisPlus = new MybatisSqlSessionFactoryBean();
mybatisPlus.setDataSource(dynamicDataSource);
mybatisPlus.setVfs(SpringBootVFS.class);
if (StringUtils.hasText(this.properties.getConfigLocation())) {
mybatisPlus.setConfigLocation(this.resourceLoader.getResource(this.properties.getConfigLocation()));
}
mybatisPlus.setGlobalConfig(new GlobalConfiguration(){{setIdType(IdType.AUTO.getKey());}});
mybatisPlus.setConfiguration(properties.getConfiguration());
if (!ObjectUtils.isEmpty(this.interceptors)) {
mybatisPlus.setPlugins(this.interceptors);
}
MybatisConfiguration mc = new MybatisConfiguration();
mc.setDefaultScriptingLanguage(MybatisXMLLanguageDriver.class);
mybatisPlus.setConfiguration(mc);
if (this.databaseIdProvider != null) {
mybatisPlus.setDatabaseIdProvider(this.databaseIdProvider);
}
if (StringUtils.hasLength(this.properties.getTypeAliasesPackage())) {
mybatisPlus.setTypeAliasesPackage(this.properties.getTypeAliasesPackage());
}
if (StringUtils.hasLength(this.properties.getTypeHandlersPackage())) {
mybatisPlus.setTypeHandlersPackage(this.properties.getTypeHandlersPackage());
}
if (!ObjectUtils.isEmpty(this.properties.resolveMapperLocations())) {
mybatisPlus.setMapperLocations(this.properties.resolveMapperLocations());
}
return mybatisPlus;
}
@Resource(name = "masterDataSource")
private DataSource masterDataSource;
@Resource(name = "slaveDataSource1")
private DataSource slaveDataSource1;
@Resource(name = "slaveDataSource2")
private DataSource slaveDataSource2;
@Resource(name = "dynamicDataSource")
private DataSource dynamicDataSource;
/**
* 动态数据源: 通过AOP在不同数据源之间动态切换
* @return
*/
@Bean(name = "dynamicDataSource")
public DataSource dataSource() {
DynamicDataSource dynamicDataSource = new DynamicDataSource();
// 默认数据源
dynamicDataSource.setDefaultTargetDataSource(masterDataSource);
// 配置多数据源
Map
dsMap = new HashMap(5);
dsMap.put(DbType.MASTER, masterDataSource);
dsMap.put(DbType.SLAVE+"1", slaveDataSource1);
// dsMap.put(DbType.SLAVE+"2", slaveDataSource2);
dynamicDataSource.setTargetDataSources(dsMap);
return dynamicDataSource;
}
}
我们通过自定义注解 + AOP的方式实现数据源动态切换。
首先定义一个ContextHolder, 用于保存当前线程使用的数据源名:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class DataSourceContextHolder {
public enum DbType{
MASTER,SLAVE
}
public static final Logger log =LoggerFactory.getLogger(DataSourceContextHolder.class);
/**
* 默认数据源
*/
public static final DbType DEFAULT_DB =DbType.MASTER;
private static finalThreadLocal contextHolder = new ThreadLocal<>();
// 设置数据源名
public static void setDB(String dbType) {
log.debug("切换到{}数据源", dbType);
contextHolder.set(dbType.toString());
}
// 获取数据源名
public static String getDB() {
returncontextHolder.get()==null?DbType.MASTER.toString():contextHolder.get();
}
// 清除数据源名
public static void clearDB() {
contextHolder.remove();
}
}
然后自定义一个javax.sql.DataSource接口的实现,这里只需要继承Spring为我们预先实现好的父类AbstractRoutingDataSource即可:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
importorg.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
public class DynamicDataSource extendsAbstractRoutingDataSource {
private static final Logger log =LoggerFactory.getLogger(DynamicDataSource.class);
@Override
protected ObjectdetermineCurrentLookupKey() {
log.debug("数据源为{}", DataSourceContextHolder.getDB());
returnDataSourceContextHolder.getDB();
}
}
自定义注释 @DB
用于在编码时指定方法使用哪个数据源:
import java.lang.annotation.*;
importcom.ibr.baoliyun.config.DataSourceContextHolder.DbType;
@Retention(RetentionPolicy.RUNTIME)
@Target({
ElementType.METHOD
})
public @interface DB {
DbType value() default DbType.MASTER;
}
编写AOP切面,实现切换逻辑:
import java.lang.reflect.Method;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.reflect.MethodSignature;
importorg.springframework.beans.factory.annotation.Value;
import org.springframework.core.Ordered;
import org.springframework.stereotype.Component;
importcom.ibr.baoliyun.config.DataSourceContextHolder.DbType;
@Aspect
@Component
public class DynamicDataSourceAspect implements Ordered {
@Value("${dataSourceNumber}")
private int dataSourceNumber;
@Before("@annotation(DB)")
public void beforeSwitchDB(JoinPointpoint){
//获得当前访问的class
Class> className =point.getTarget().getClass();
//获得访问的方法名
String methodName =point.getSignature().getName();
//得到方法的参数的类型
Class[] argClass = ((MethodSignature)point.getSignature()).getParameterTypes();
DbType dataSource =DataSourceContextHolder.DEFAULT_DB;
try {
// 得到访问的方法对象
Method method = className.getMethod(methodName,argClass);
// 判断是否存在@DB注解
if(method.isAnnotationPresent(DB.class)) {
DB annotation =method.getAnnotation(DB.class);
// 取出注解中的数据源名
dataSource =annotation.value();
}
} catch (Exception e) {
e.printStackTrace();
}
//多个从库可选时
if(dataSource.equals(DbType.SLAVE)){
intsize = (int)((Math.random()*10) % dataSourceNumber )+1;
//切换数据源
DataSourceContextHolder.setDB(dataSource.toString().concat(size+""));
}else{
DataSourceContextHolder.setDB(dataSource.toString());
}
}
@After("@annotation(DB)")
public void afterSwitchDB(JoinPoint point){
DataSourceContextHolder.clearDB();
}
@Override
public int getOrder() {
return 0;
}
}
Ordered排序接口,在这是用用于解决@Transactional问题,事务会在缓存中获取数据源,因为前面配置了默认数据源,所以首先读取默认数据源并且后面不能再次修改, 使用Ordered接口让动态数据源先配置
完成上述配置后就可以在Service中使用注解的方式切换数据源了
// @DB(DbType.MASTER)
// @DB(DbType.SLAVE)
@Override
public PagegetUserList(String name,String tel,int affiliatedFactoringEnterpriseId,ReqPagepage) {
return new Page(){{
setRecords(baseMapper.getUserList(name,tel,affiliatedFactoringEnterpriseId, page.getPageStart(), page.getPageSize()));
setTotal(selectCount(newEntityWrapper()
.where("affiliatedFactoringEnterpriseId={0}",affiliatedFactoringEnterpriseId)
.addFilterIfNeed(!StringUtils.isEmpty(name),"name={0}", name)
.addFilterIfNeed(!StringUtils.isEmpty(tel),"tel={0}", tel)));
}};
}
如果使用多个从库,可以放开配置中注释掉的部分,从库选择方式为随机选择,如要修改可在 AOP 切面逻辑中修改
2.在方法中切换数据源
在需要切换数据源的地方添加如下代码:
DataSourceContextHolder.setDB(DbType.MASTER.toString());
或
DataSourceContextHolder.setDB(DbType.SLAVE.toString()+"1");
注意:如果存在事务,则事务中无法使用以上切换数据源,需要用到分布式事务,待完成
3.如何在不修改现有代码的情况下,动态切换数据源
添加全局拦截器,所有以get开头的方法走从库,其他走主库,可以与前面的配置共存,全局拦截中只会对没有@DS注解的方法进行处理
@Aspect
@Component
public classGlobalDateSourceInterceptor extends HandlerInterceptorAdapter{
@Value("${dataSourceNumber}")
private int dataSourceNumber;
protected static final Logger mlog = LoggerFactory.getLogger(GlobalDateSourceInterceptor.class);
@Around("execution(*com.ibr.baoliyun.service.impl..*(..))")
public ObjectgetGlobalDateSource(ProceedingJoinPoint point){
//获得当前访问的class
Class> className =point.getTarget().getClass();
//获得访问的方法名
String methodName =point.getSignature().getName();
//得到方法的参数的类型
Class[] argClass = ((MethodSignature)point.getSignature()).getParameterTypes();
DbType dataSource =DataSourceContextHolder.DEFAULT_DB;
try {
// 得到访问的方法对象
Method method =className.getMethod(methodName, argClass);
// 判断是否存在@DB注解
if(!method.isAnnotationPresent(DB.class)) {
String str = methodName.substring(0, 3);
if(str.equals("get")){
dataSource =DbType.SLAVE;
int size =(int)((Math.random()*10) % dataSourceNumber )+1;
//切换数据源
DataSourceContextHolder.setDB(dataSource.toString().concat(size+""));
}else{
DataSourceContextHolder.setDB(DbType.MASTER.toString());
}
}
} catch (Exception e) {
e.printStackTrace();
}
try {
returnpoint.proceed();
} catch (Throwable e) {
return null;
}
}