记录一次spring boot动态数据源配置
maven引入
<!--连接池-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.8</version>
</dependency>
<!--数据库链接自定义依赖-->
<!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.16</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
application.yml配置
具体属性根据项目需要增删
spring:
datasource:
druid:
appid1:
url: jdbc:mysql://localhost:3306/appid1?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=UTF-8
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
max-idle: 10
max-wait: 10000
max-active: 30
min-idle: 1
initial-size: 1
validation-query: SELECT '1'
test-on-borrow: false
test-on-return: false
test-while-idle: true
time-between-eviction-runs-millis: 60000 # 注意控制连接数据库的时间,不然会断开连接
min-evictable-idle-time-millis: 300000
pool-prepared-statements: true
filters: stat
appid2:
url: jdbc:mysql://localhost:3306/appid2?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=UTF-8
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
max-idle: 10
max-wait: 10000
max-active: 30
min-idle: 1
initial-size: 1
validation-query: SELECT '1'
test-on-borrow: false
test-on-return: false
test-while-idle: true
time-between-eviction-runs-millis: 60000 # 注意控制连接数据库的时间,不然会断开连接
min-evictable-idle-time-millis: 300000
pool-prepared-statements: true
filters: stat
创建配置文件类 BaseDataSourceProperties
此类中的属性要同步application.yml,否则会注入失败
package cn.yueworld.ray.data.datasource;
import lombok.Data;
/**
* @Author yanzhiwei
* @Date 2021/3/14 14:30
*/
@Data
public class BaseDataSourceProperties {
protected String url;
protected String username;
protected String password;
protected String driverClassName;
protected int initialSize;
protected int minIdle;
protected int maxActive;
protected int maxWait;
protected int timeBetweenEvictionRunsMillis;
protected int minEvictableIdleTimeMillis;
protected String validationQuery;
protected boolean testWhileIdle;
protected boolean testOnBorrow;
protected boolean testOnReturn;
protected boolean poolPreparedStatements;
protected String filters;
}
配置通用设置属性的DataSource:BaseMyBatisDataSource
根据配置文件中需要自定义DataSource属性进行设置
package cn.yueworld.ray.data.datasource;
import com.alibaba.druid.pool.DruidDataSource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.sql.DataSource;
import java.sql.SQLException;
/**
* //mybaits mapper 搜索路径
* @Author yanzhiwei
* @Date 2021/3/14 14:38
*/
@Component
@Slf4j
public class BaseMyBatisDataSource {
public DataSource druidDataSource(BaseDataSourceProperties config) {
DruidDataSource datasource = new DruidDataSource();
datasource.setDriverClassName(config.getDriverClassName());
datasource.setUrl(config.getUrl());
datasource.setUsername(config.getUsername());
datasource.setPassword(config.getPassword());
datasource.setInitialSize(config.getInitialSize());
datasource.setMinIdle(config.getMinIdle());
datasource.setMaxActive(config.getMaxActive());
datasource.setMaxWait(config.getMaxWait());
datasource.setTimeBetweenEvictionRunsMillis(config.getTimeBetweenEvictionRunsMillis());
datasource.setMinEvictableIdleTimeMillis(config.getMinEvictableIdleTimeMillis());
datasource.setValidationQuery(config.getValidationQuery());
datasource.setTestWhileIdle(config.isTestWhileIdle());
datasource.setTestOnBorrow(config.isTestOnBorrow());
datasource.setTestOnReturn(config.isTestOnReturn());
datasource.setPoolPreparedStatements(config.isPoolPreparedStatements());
try {
datasource.setFilters(config.getFilters());
} catch (SQLException e) {
log.error("druid configuration initialization filter", e);
}
return datasource;
}
}
使用 ThreadLocal 保存 DataSources 数据源的key
package cn.yueworld.ray.data.datasource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* 使用 ThreadLocal 保存 DataSources 数据源的key
* @Author yanzhiwei
* @Date 2021/3/14 14:41
*/
public class DataSourceContextHolder {
public static final Logger log = LoggerFactory.getLogger(DataSourceContextHolder.class);
/**
* 默认数据源appid1
*/
public static final String DS_APPID1 = "appid1";
/**
* appid2数据源
*/
public static final String DS_APPID2 = "appid2";
/**
* 本地线程
*/
private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();
static {
contextHolder.set(DS_APPID1);
}
// 设置数据源名
public static void setDB(String dbType) {
log.debug("切换到{}数据源", dbType);
contextHolder.set(dbType);
}
// 获取数据源名
public static String getDB() {
return (contextHolder.get());
}
// 清除数据源名
public static void clearDB() {
contextHolder.remove();
}
/**
* 判断当前数据源key是否存在
* @param key
* @return
*/
public static boolean isDataSourceKey (String key) {
String [] keys = {DS_APPID1, DS_APPID2};
for (int i = 0 ; i< keys.length;i ++) {
if (keys[i].equals(key)) {
return true;
}
}
return false;
}
}
注册多数据源
package cn.yueworld.ray.data.datasource;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 注册多数据源
* @Author yanzhiwei
* @Date 2021/3/14 14:31
*/
@Configuration
public class DataSourcePropertiesFactory {
private static final String dataSourceAppid1 = "spring.datasource.druid.appid1";
private static final String dataSourceAppid2 = "spring.datasource.druid.appid2";
@Bean("appid1")
@ConfigurationProperties(prefix = dataSourceAppid1, ignoreUnknownFields = false, ignoreInvalidFields = true)
public BaseDataSourceProperties getAppid1DataSourceInstance(){
BaseDataSourceProperties mp = new BaseDataSourceProperties();
return mp;
}
@Bean("appid2")
@ConfigurationProperties(prefix = dataSourceAppid2, ignoreUnknownFields = false, ignoreInvalidFields = true)
public BaseDataSourceProperties getAppid2SourceInstance(){
BaseDataSourceProperties mp = new BaseDataSourceProperties();
return mp;
}
}
继承AbstractRoutingDataSource
package cn.yueworld.ray.data.datasource;
/**
* AbstractRoutingDataSource 是由spring提供的类 该类充当了DataSource的路由中介, 能有在运行时, 根据某种key值来动态切换到真正的DataSource上,
* 不用每个数据源都去创建一次sqlSessionFactory。
* sqlSessionFactory 顾名思义 ,就是用来创建session会话的工厂。如果存在多个Sessionfactory 那么Session是不是就乱套了,因此这种架构不可取。
*
* determineCurrentLookupKey()方法,这是AbstractRoutingDataSource类中的一个抽象方法,而它的返回值是你所要用的数据源dataSource的key值,
* 有了这个key值,resolvedDataSource(这是个map,由配置文件中设置好后存入的)就从中取出对应的DataSource,如果找不到,就用配置默认的数据源。
*
*/
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
/**
* @Author yanzhiwei
* @Date 2021/3/14 14:39
*/
public class DynamicDataSource extends AbstractRoutingDataSource {
private static final Logger log = LoggerFactory.getLogger(DynamicDataSource.class);
@Override
protected Object determineCurrentLookupKey() {
log.debug("数据源为{}", DataSourceContextHolder.getDB());
return DataSourceContextHolder.getDB();
}
}
由于业务需要,配置通用创建人,创建时间,更新人,更新时间的设置
package cn.yueworld.ray.data.datasource;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
/**
* @Author yanzhiwei
* @Date 2021/3/15 9:07
*/
@Component
public class MybatisObjectHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
LocalDateTime now = LocalDateTime.now();
setFieldValByName("createdDate", now, metaObject);
setFieldValByName("createdName", now + "createdName", metaObject);
setFieldValByName("updatedDate", now, metaObject);
setFieldValByName("updatedName", now + "updatedName", metaObject);
}
@Override
public void updateFill(MetaObject metaObject) {
LocalDateTime now = LocalDateTime.now();
setFieldValByName("updatedDate", now, metaObject);
setFieldValByName("updatedName", now + "updatedName", metaObject);
}
}
配置类 CustomMybatisDataSource
package cn.yueworld.ray.data.datasource;
import com.baomidou.mybatisplus.core.config.GlobalConfig;
import com.baomidou.mybatisplus.extension.plugins.PaginationInterceptor;
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import javax.sql.DataSource;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* MapperScan ---> mybaits mapper 搜索路径
* @Author yanzhiwei
* @Date 2021/3/14 14:44
*/
@Configuration
@MapperScan(basePackages = {"cn.yueworld.ray.data.dao"}, sqlSessionFactoryRef = "sqlSessionFactory1")
@EnableTransactionManagement
public class CustomMybatisDataSource extends BaseMyBatisDataSource {
@Autowired
@Qualifier("appid1")
private BaseDataSourceProperties appid1;
@Autowired
@Qualifier("appid2")
private BaseDataSourceProperties appid2;
/**
* 动态数据源配置
* @return
*/
@Bean
public DataSource getDataSource() {
DataSource appid1DataSource = super.druidDataSource(appid1);
DataSource appid2DataSource = super.druidDataSource(appid2);
// 此类继承了spring的 AbstractRoutingDataSource 类 代替原有的
DynamicDataSource dynamicDataSource = new DynamicDataSource();
// 默认数据源
dynamicDataSource.setDefaultTargetDataSource(appid1DataSource);
// 配置多数据源
Map<Object, Object> dsMap = new ConcurrentHashMap<>(5);
dsMap.put(DataSourceContextHolder.DS_APPID1, appid1DataSource);
dsMap.put(DataSourceContextHolder.DS_APPID2, appid2DataSource);
dynamicDataSource.setTargetDataSources(dsMap);
return dynamicDataSource;
}
@Bean
public PlatformTransactionManager transactionManager() {
return new DataSourceTransactionManager(getDataSource());
}
/**
* 分页插件
*/
@Bean
public PaginationInterceptor paginationInterceptor() {
PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
return paginationInterceptor;
}
@Bean
public SqlSessionFactory sqlSessionFactory1() throws Exception {
// 使用MybatisSqlSessionFactoryBean
MybatisSqlSessionFactoryBean factoryBean = new MybatisSqlSessionFactoryBean();
// 使用数据源, 连接库
factoryBean.setDataSource(getDataSource());
// 下面一步很重要,配置mapper。xml的扫描位置
factoryBean.setMapperLocations(
new PathMatchingResourcePatternResolver().
getResources("classpath*:mybatis/mappers/*.xml"));
// 配置扫描包的位置
factoryBean.setTypeAliasesPackage("cn.yueworld.ray.data.dao");
// mybatis-plus注解实现:@TableField(fill = FieldFill.INSERT)
GlobalConfig globalConfig = new GlobalConfig();
globalConfig.setMetaObjectHandler(new MybatisObjectHandler());
factoryBean.setGlobalConfig(globalConfig);
// 重新注入分页插件
factoryBean.setPlugins(new Interceptor[]{paginationInterceptor()});
return factoryBean.getObject();
}
@Bean
public SqlSessionTemplate sqlSessionTemplate1() throws Exception {
// 使用上面配置的Factory
SqlSessionTemplate template = new SqlSessionTemplate(sqlSessionFactory1());
return template;
}
}
AOP切面进行动态切换数据源
package cn.yueworld.ray.data.datasource;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.CodeSignature;
import org.springframework.stereotype.Component;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
/**
* @Author yanzhiwei
* @Date 2021/3/14 14:52
*/
@Aspect
@Component
@Slf4j
public class DynamicDataSourceAop {
@Pointcut("execution(* cn.yueworld.ray.data.dao.*.*(..))")
public void aPointcut() {
}
@Around("aPointcut()")
public Object doBefore(ProceedingJoinPoint joinPoint) throws Throwable {
Map<String, Object> map = new HashMap();
// map = getFieldsName(joinPoint);
//参数名称
String[] names = ((CodeSignature) joinPoint.getSignature()).getParameterNames();
Object[] args = joinPoint.getArgs();
String appid = DataSourceContextHolder.DS_APPID1;
assert names != null;
for (int i = 0; i < names.length; i++) {
if (StringUtils.equals(names[i], "entity")) {
Class<?> type = args[i].getClass();
Field field = null;
Object res = null;
field = type.getField("appId");
res = field.get(args[i]);
if (res != null) {
appid = (String) res;
}
} else {
if (StringUtils.equals(names[i], "appId") && StringUtils.isNotBlank((String) args[i])) {
appid = (String) args[i];
}
}
}
log.info("切换数据源至={}", appid);
if (DataSourceContextHolder.isDataSourceKey(appid)) {
// 切换数据源
DataSourceContextHolder.setDB(appid);
}else {
throw new RuntimeException("数据源key书写错误");
}
return joinPoint.proceed(args);
}
@AfterReturning(returning = "object", pointcut = "aPointcut()")
public void doAfterReturning(Object object) {
log.info("切换数据源至={}",DataSourceContextHolder.DS_APPID1);
DataSourceContextHolder.setDB(DataSourceContextHolder.DS_APPID1);
}
private String attNameHandle(String method, String attName) {
StringBuilder res = new StringBuilder(method);
// 属性只有一个字母
if (attName.length() == 1) {
res.append(attName.toUpperCase());
} else {
// 属性包含两个字母及以上
char[] charArray = attName.toCharArray();
if (Character.isLowerCase(charArray[0]) && Character.isLowerCase(charArray[1])) {
res.append(Character.toUpperCase(charArray[0]));
res.append(attName.substring(1));
} else {
res.append(attName);
}
}
return res.toString();
}
}
测试类
实体类
package cn.yueworld.ray.data.client.entity;
import cn.yueworld.pd.tools.mybatis.vo.BaseEntity;
import com.baomidou.mybatisplus.annotation.TableField;
import lombok.Data;
/**
* @Author yanzhiwei
* @Date 2021/3/14 20:40
*/
@Data
public class CustomBaseEntity {
@TableField(exist = false)
public String appId;
}
package cn.yueworld.ray.data.client.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.time.LocalDateTime;
/**
* @Author yanzhiwei
* @Date 2021/3/14 16:41
*/
@Data
@TableName("test1")
public class Test1 extends CustomBaseEntity {
@TableId(type = IdType.AUTO)
private Long id;
private String name;
/**
* 创建时间
*/
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createdDate;
/**
* 创建人
*/
@TableField(fill = FieldFill.INSERT)
private String createdName;
/**
* 最后修改时间
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updatedDate;
/**
* 最后修改人
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
private String updatedName;
}
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="cn.yueworld.ray.data.dao.Test1Mapper">
<resultMap id="BaseResultMap" type="cn.yueworld.ray.data.client.entity.Test1">
<id column="id" jdbcType="BIGINT" property="id" />
<result column="name" jdbcType="VARCHAR" property="name" />
<result column="created_date" jdbcType="DATE" property="createdDate" />
<result column="created_name" jdbcType="VARCHAR" property="createdName" />
<result column="updated_date" jdbcType="DATE" property="updatedDate" />
<result column="updated_name" jdbcType="VARCHAR" property="updatedName" />
</resultMap>
<sql id="Base_Column_List">
id, `name`, created_date, created_name, updated_date, updated_name
</sql>
</mapper>
mapper接口
package cn.yueworld.ray.data.dao;
import cn.yueworld.ray.data.client.entity.Test1;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
/**
* @Author yanzhiwei
* @Date 2021/3/14 16:41
*/
public interface Test1Mapper extends BaseMapper<Test1> {
}
service接口
package cn.yueworld.ray.data.service;
import cn.yueworld.ray.data.client.entity.Test1;
/**
* @Author yanzhiwei
* @Date 2021/3/14 16:45
*/
public interface Test1Service {
int save(Test1 test1);
int update(Test1 test1);
}
service实现类
package cn.yueworld.ray.data.service.impl;
import cn.yueworld.ray.data.client.entity.Test1;
import cn.yueworld.ray.data.dao.Test1Mapper;
import cn.yueworld.ray.data.service.Test1Service;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* @Author yanzhiwei
* @Date 2021/3/14 16:46
*/
@Slf4j
@Service
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class Test1ServiceImpl implements Test1Service {
private final Test1Mapper test1Mapper;
@Override
public int save(Test1 test1) {
return test1Mapper.insert(test1);
}
@Override
public int update(Test1 test1) {
return test1Mapper.updateById(test1);
}
}
接口入口controller
package cn.yueworld.ray.data.api;
import cn.yueworld.ray.data.client.entity.Test1;
import cn.yueworld.ray.data.service.Test1Service;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
/**
* 应用表
* @author Wang TianWen
* @email wangtianwen90@163.com
* @date 2021-02-24 10:23:45
*/
@Slf4j
@RestController
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class Test1Api {
private final Test1Service test1Service;
@RequestMapping(value = "save", method = RequestMethod.GET)
public boolean save(Test1 test1) {
int count = test1Service.save(test1);
return count > 0;
}
@RequestMapping(value = "update", method = RequestMethod.GET)
public boolean update(Test1 test1) {
int count = test1Service.update(test1);
return count > 0;
}
}
以上内容也可以通过自动生成代码框架去生成,具体方法请自行根据需要添加
参考博客:https://blog.csdn.net/HELLO_WROLD_/article/details/90713897