环境:
jdk 1.8
maven 3.6.0
springboot 2.6.6
druid 1.2.8
1.实现思路
DataSource是和线程绑定的,动态数据源的配置主要是通过继承AbstractRoutingDataSource类实现的,实现在AbstractRoutingDataSource类中的 protected Object determineCurrentLookupKey()方法来获取数据源,所以我们需要先创建一个多线程线程数据隔离的类来存放DataSource,然后在determineCurrentLookupKey()方法中通过这个类获取当前线程的DataSource,在=AbstractRoutingDataSource类中,DataSource是通过Key-value的方式保存的,我们可以通过ThreadLocal来保存Key,从而实现数据源的动态切换。
2.配置信息
pom.xml
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.6</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>demo</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.0</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.8</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.70</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
3.具体代码
public class DynamicDataSourceContextHolder {
private static ThreadLocal<String> threadLocal = new ThreadLocal<>();
public static List<String> dataSourceIds = new ArrayList<>();
public static void setDataSourceType(String dataSourceType){
System.out.println("DynamicDataSourceContextHolder.setDataSourceType");
threadLocal.set(dataSourceType);
System.out.println("数据源修改为"+dataSourceType);
}
public static String getDataSourceType(){
System.out.println("DynamicDataSourceContextHolder.getDataSourceType");
return threadLocal.get();
}
public static void clearDataSourceType() {
System.out.println("DynamicDataSourceContextHolder.clearDataSourceType");
threadLocal.remove();
}
public static boolean containsDataSource(String dataSourceId) {
System.out.println("DynamicDataSourceContextHolder.containsDataSource");
return dataSourceIds.contains(dataSourceId);
}
}
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
System.out.println("DynamicDataSource.determineCurrentLookupKey");
return DynamicDataSourceContextHolder.getDataSourceType();
}
}
/**
* 动态数据源注册
* 启动动态数据源请在启动类中 添加 @Import(DynamicDataSourceRegister.class)
*/
@Component
public class DynamicDataSourceRegister implements BeanDefinitionRegistryPostProcessor, EnvironmentAware {
// 如配置文件中未指定数据源类型,使用该默认值
private static final Object DATASOURCE_TYPE_DEFAULT = "org.apache.tomcat.jdbc.pool.DataSource";
// 数据源
private DataSource defaultDataSource;
private Map<String, DataSource> customDataSources = new HashMap();
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry beanDefinitionRegistry) throws BeansException {
System.out.println("DynamicDataSourceRegister.postProcessBeanDefinitionRegistry");
Map<Object, Object> targetDataSources = new HashMap();
targetDataSources.put("datasource", defaultDataSource);
targetDataSources.putAll(customDataSources);
DynamicDataSourceContextHolder.dataSourceIds.add("datasource");
for (Map.Entry<String, DataSource> stringDataSourceEntry : customDataSources.entrySet()) {
DynamicDataSourceContextHolder.dataSourceIds.add(stringDataSourceEntry.getKey());
}
GenericBeanDefinition beanDefinition = new GenericBeanDefinition();
beanDefinition.setBeanClass(DynamicDataSource.class);
beanDefinition.setSynthetic(true);
MutablePropertyValues mpv = beanDefinition.getPropertyValues();
mpv.add("defaultTargetDataSource", defaultDataSource);
mpv.add("targetDataSources", targetDataSources);
beanDefinitionRegistry.registerBeanDefinition("datasource", beanDefinition);
System.out.println("Dynamic DataSource Registry");
}
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory configurableListableBeanFactory) throws BeansException {
}
//加载多数据源配置
@Override
public void setEnvironment(Environment environment) {
System.out.println("DynamicDataSourceRegister.setEnvironment");
initDefaultDataSource(environment);
initCustomDataSources(environment);
}
private void initDefaultDataSource(Environment env) {
System.out.println("DynamicDataSourceRegister.initDefaultDataSource");
Binder binder = Binder.get(env);
Map properties = binder.bind("spring.datasource", Map.class).orElseThrow(() -> {
return new RuntimeException("raincloud-jdbc:主数据源未配置");
});
try {
defaultDataSource = buildDataSource(properties);
dataBinder(defaultDataSource, env,"");
} catch (Exception var5) {
throw new RuntimeException("raincloud-jdbc:主数据源配置错误");
}
}
//创建DataSource
public DataSource buildDataSource(Map<String, Object> dsMap) {
System.out.println("DynamicDataSourceRegister.buildDataSource");
try {
Object type = dsMap.get("type");
if (type == null) {
type = DATASOURCE_TYPE_DEFAULT;// 默认DataSource
}
Class<? extends DataSource> dataSourceType = (Class<? extends DataSource>)Class.forName((String)type);
String driverClassName = dsMap.get("driverClassName").toString();
String url = dsMap.get("url").toString();
String username = dsMap.get("username").toString();
String password = dsMap.get("password").toString();
DataSourceBuilder factory = DataSourceBuilder.create().driverClassName(driverClassName).url(url).username(username).password(password).type(dataSourceType);
return factory.build();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
return null;
}
private void dataBinder(DataSource dataSource, Environment env,String dsPrefix) {
System.out.println("DynamicDataSourceRegister.dataBinder");
Map properties = (Map)Binder.get(env).bind("spring.datasource", Map.class).get();
if (TypeUtils.castToString(properties.get("type")).contains("com.alibaba.druid.pool.DruidDataSource")) {
Binder.get(env).bind("spring.datasource.druid"+dsPrefix, Bindable.ofInstance(dataSource));
} else if (TypeUtils.castToString(properties.get("type")).contains("com.zaxxer.hikari.HikariDataSource")) {
Binder.get(env).bind("spring.datasource.hikari", Bindable.ofInstance(dataSource));
}
}
private void initCustomDataSources(Environment env) {
System.out.println("DynamicDataSourceRegister.initCustomDataSources");
Binder binder = Binder.get(env);
Map properties = null;
properties = (Map)binder.bind("slaver.datasource", Map.class).orElse(properties);
if (properties != null) {
String dsPrefixs = (String)properties.get("names");
if (dsPrefixs != null) {
String[] dsPrefixsSplit = dsPrefixs.split(",");
int prefixLength = dsPrefixsSplit.length;
for(int i = 0; i < prefixLength; ++i) {
String dsPrefix = dsPrefixsSplit[i];
try {
Map<String, Object> dsMap = (Map)properties.get(dsPrefix);
DataSource ds = buildDataSource(dsMap);
customDataSources.put(dsPrefix, ds);
dataBinder(ds, env,dsPrefix);
} catch (Exception var11) {
throw new RuntimeException("raincloud-jdbc:从数据源" + dsPrefix + "配置错误");
}
}
}
}
}
public void putDataSouce(String name, DataSource dataSource) {
System.out.println("DynamicDataSourceRegister.putDataSouce");
customDataSources.put(name, dataSource);
DynamicDataSourceContextHolder.dataSourceIds.add(name);
((DynamicDataSource) ApplicationContextHandle.getBean(DynamicDataSource.class)).afterPropertiesSet();
}
}
@Component
@Aspect
@Order(-1)
public class DynamicDataSourceAspect {
@Before("@annotation(ds)")
public void changeDataSource(JoinPoint point, TargetDataSource ds) {
System.out.println("DynamicDataSourceAspect.changeDataSource");
String dsId = ds.name();
if (!DynamicDataSourceContextHolder.containsDataSource(dsId)) {
System.out.println("数据源"+ds.name()+"不存在,使用默认数据源 > "+point.getSignature());
} else {
System.out.println("Use DataSource :"+ds.name()+">"+point.getSignature());
DynamicDataSourceContextHolder.setDataSourceType(ds.name());
}
}
@After("@annotation(ds)")
public void restoreDataSource(JoinPoint point, TargetDataSource ds) {
System.out.println("Revert DataSource :"+ds.name()+">"+point.getSignature());
DynamicDataSourceContextHolder.clearDataSourceType();
}
}
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface TargetDataSource {
String name();
}
4.调用
@Data
public class Users {
private Long id;
private Long supervisorId;
private Long paGoalprivateId;
private Long cBpartnerId;
private String processing;
private Long cBpartnerLocationId;
private Long cGreetingId;
private Integer lastcontact;
private Integer birthday;
private Long adOrgtrxId;
private String isldapauthorized;
private String passwordhash;
private Integer isenabled;
private Integer isemployee;
private Integer isadmin;
private Integer usertype;
private String truename;
private String name;
private String description;
private String password;
private String email;
private String emailuser;
private String emailuserpw;
private String title;
private String comments;
private String phone;
private String phone2;
private String fax;
private String lastresult;
private String emailverify;
private Long cDepartmentId;
private String language;
private String isOtp;
private Integer otpLength;
private Integer otpSeconds;
private String otpSecret;
private BigDecimal otpCounter;
private String isOtpOnly;
private Date otpCdate;
private String loginIpRule;
private String issms;
private String isOut;
private Long assigneeId;
private String issaler;
private Long cStoreId;
private BigDecimal discountlimit;
private String isopr;
private String saasvendor;
private String saasuser;
private Long hrEmployeeId;
private Long cCustomerId;
private Long cCustomerupId;
private Long areamngId;
private Integer sgrade;
private Long cPriceregionId;
private Long cSupplierId;
private String subsystems;
private Date passwordexpirationdate;
private String passwordreset;
private String isret;
private String webposPer;
private String issys;
private String isSysUser;
private String smslogin;
private Long cMbrId;
private String pwdrand;
private Date lastlogindate;
private String lastloginip;
private String loginip;
private Date logindate;
private Long failedloginattempts;
private Long cAreaId;
private String wechatAuth;
private String qqAuth;
private String sinaAuth;
private String headimg;
private String employeeno;
private Integer ismanager;
private Integer groupsgrade;
private Long cpCHrorgId;
private String ecode;
private String ename;
private Long cpCDistribId;
private Long cpCStoreId;
private Long cpCSupplierId;
private String userstype;
private String f4;
private String rf;
private String pb1;
private String pb2;
private String pb3;
private String pb4;
private String pb5;
private String pb6;
private String remark;
private String mobil;
private String openid;
private Long cpCEmpId;
private String sex;
private String modifierename;
private String ownerename;
private Integer isdev;
private String wechat;
private String adyu;
private Long cpCPhyWarehouseId;
private String mac;
}
@Mapper
public interface UsersMapper extends BaseMapper<Users> {
@Select("select email_title as processing from ip_b_email where id = #{id}")
String getUser(@Param("id")Long id);
}
@Service
public class UsersService {
@Autowired
private UsersMapper usersMapper;
@TargetDataSource(name = "rds")
public String getUserById(Long id){
Users users = usersMapper.selectById(id);
String name = "";
if (null != users) {
name = users.getName();
}
return name;
}
@TargetDataSource(name = "mysql")
public String getUser(Long id){
return usersMapper.getUser(id);
}
}
@RestController
public class UserController {
@Autowired
private UsersService usersService;
@RequestMapping("/c/getUserName/{id}")
public String getUsersById(@PathVariable Long id){
return usersService.getUserById(id);
}
@RequestMapping("/c/getUser/{id}")
public String getUsers(@PathVariable Long id){
return usersService.getUser(id);
}
}
5.存在问题
工程启动正常,数据源切换不了,还是使用的默认数据源
6.问题解决
在注册数据源的时候会与com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure的类冲突报
The bean 'dataSource',could not be registered.A bean with that name has already been defined
要覆盖掉
beanDefinitionRegistry.registerBeanDefinition("dataSource", beanDefinition);
解决方案
在application配置文件里加一行:
spring.main.allow-bean-definition-overriding: true