前言
当前时代的业务繁杂,经常会使用到多个数据库,并且来回切换。为简化使用,我们会用到动态数据源的技术。其原理是使用了 Spring 框架提供的 AbstractRoutingDataSource 中的 determineCurrentLookupKey() 。
通过该方法切换 key 值,可以理解为各个数据库的名字或多租户场景下租户的名字。
本文对其做代码实现,并且增加使用 AOP + 注解的方式去切换数据源。最终还会将数据库连接的配置移到nacos中。
文中,懒的加日志了,直接 控制台打印了。不喜勿喷!!
1 项目文件结构
主要的代码在 org.feng.datasource 中,org.feng.service 和 org.feng.controller 是为了测试功能而增加的。
还有 bootstrap.yml 配置文件,其名称也是不能更改的(这是Nacos 的一个坑)。
2 配置文件和pom.xml文件
本文的 Java环境是 Java11
2.1 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>
<groupId>org.feng</groupId>
<artifactId>dynamic-datasource</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>dynamic-datasource</name>
<description>dynamic-datasource</description>
<properties>
<java.version>11</java.version>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<spring-boot.version>2.2.5.RELEASE</spring-boot.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
<version>2.2.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-context</artifactId>
<version>2.2.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>11</source>
<target>11</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
</plugins>
</build>
</project>
2.2 bootstrap.yml
spring:
profiles:
active: ${profiles.active:dev}
application:
name: dynamic-datasource
cloud:
nacos:
config:
enabled: true
file-extension: yml
group: fjs
extension-configs:
- data-id: datasource.yml
group: fjs
refresh: true
debug: false
---
spring:
profiles: dev
cloud:
nacos:
config:
username: nacos
password: nacos
server-addr: localhost:80
namespace: gnefjs
2.3 Nacos 中的配置
命名空间:
配置:
spring:
datasource:
config:
master:
- driverClassName=com.mysql.cj.jdbc.Driver
- jdbcUrl=jdbc:mysql://localhost:13321/dynamic_master1?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=UTF-8
- username=root
- password=123456
slave1:
- driverClassName=com.mysql.cj.jdbc.Driver
- jdbcUrl=jdbc:mysql://localhost:13321/dynamic_slave1?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=UTF-8
- username=root
- password=123456
3 Java文件内容
3.1 org.feng.datasource 中
3.1.1 DynamicDataSource
通过继承 AbstractRoutingDataSource 并重写其方法,获取到不同的数据源的 key。
package org.feng.datasource;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DynamicDataSourceContextHolder.getDataSourceKey();
}
}
3.1.2 DynamicDataSourceContextHolder
使用 ThreadLocal 维护数据源 key。
package org.feng.datasource;
import java.util.Optional;
public class DynamicDataSourceContextHolder {
private static final ThreadLocal<String> DATASOURCE_CONTEXT_MERCHANT_HOLDER = new InheritableThreadLocal<>();
public static void setDataSourceKey(String merchant) {
System.out.println("切换数据源:" + merchant);
DATASOURCE_CONTEXT_MERCHANT_HOLDER.set(merchant);
}
public static String getDataSourceKey() {
return Optional.ofNullable(DATASOURCE_CONTEXT_MERCHANT_HOLDER.get())
.orElse(DataSourceConstant.MASTER);
}
public static void removeDataSourceKey() {
DATASOURCE_CONTEXT_MERCHANT_HOLDER.remove();
}
}
3.1.3 DynamicDataSourceConfig
package org.feng.datasource.config;
import org.feng.datasource.DataSourceConstant;
import org.feng.datasource.DataSourceProperties;
import org.feng.datasource.DynamicDataSource;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.core.JdbcTemplate;
import javax.annotation.Resource;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
@EnableAutoConfiguration(exclude = {DataSourceAutoConfiguration.class})
@Configuration
public class DynamicDataSourceConfig {
@Resource
private DataSourcePropertiesConfig dataSourcePropertiesConfig;
private DataSource dataSource(DataSourceProperties dataSourceProperties){
return DataSourceBuilder.create()
.driverClassName(dataSourceProperties.getDriverClassName())
.url(dataSourceProperties.getJdbcUrl())
.username(dataSourceProperties.getUsername())
.password(dataSourceProperties.getPassword())
.build();
}
@Primary
@Bean
public DataSource dynamicDataSource() {
Map<Object, Object> dataSourceMap = new HashMap<>(16);
Map<String, DataSourceProperties> dataSourcePropertiesMap = dataSourcePropertiesConfig.getDataSourceConfig();
dataSourcePropertiesMap.forEach((merchant, properties) -> dataSourceMap.put(merchant, dataSource(properties)));
DynamicDataSource dynamicDataSource = new DynamicDataSource();
dynamicDataSource.setTargetDataSources(dataSourceMap);
dynamicDataSource.setDefaultTargetDataSource(dataSourceMap.get(DataSourceConstant.MASTER));
return dynamicDataSource;
}
@Primary
@Bean
public JdbcTemplate jdbcTemplate() {
return new JdbcTemplate(dynamicDataSource());
}
}
3.1.4 DataSourceConstant
package org.feng.datasource;
public interface DataSourceConstant {
String MASTER = "master";
String SLAVE1 = "slave1";
}
3.1.5 DataSourceProperties
package org.feng.datasource;
import lombok.Data;
@Data
public class DataSourceProperties {
private String driverClassName;
private String jdbcUrl;
private String username;
private String password;
}
3.1.6 DataSourcePropertiesConfig
package org.feng.datasource.config;
import lombok.Setter;
import org.feng.datasource.DataSourceProperties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
@RefreshScope
@ConfigurationProperties(prefix = DataSourcePropertiesConfig.PREFIX)
public class DataSourcePropertiesConfig {
public static final String PREFIX = "spring.datasource";
private static final String DRIVER_CLASS_PREFIX = "driverClassName=";
private static final String JDBC_URL_PREFIX = "jdbcUrl=";
private static final String USERNAME_PREFIX = "username=";
private static final String PASSWORD_PREFIX = "password=";
@Setter
private Map<String, Set<String>> config;
public Map<String, DataSourceProperties> getDataSourceConfig() {
Map<String, DataSourceProperties> resultMap = new HashMap<>(16);
config.forEach((merchant, setConfig) -> {
DataSourceProperties properties = setToProperties(setConfig);
resultMap.put(merchant, properties);
});
return resultMap;
}
private static DataSourceProperties setToProperties(Set<String> setConfig) {
DataSourceProperties dataSourceProperties = new DataSourceProperties();
for (String param : setConfig) {
if (param.startsWith(DRIVER_CLASS_PREFIX)) {
dataSourceProperties.setDriverClassName(param.replaceFirst(DRIVER_CLASS_PREFIX, ""));
continue;
}
if (param.startsWith(JDBC_URL_PREFIX)) {
dataSourceProperties.setJdbcUrl(param.replaceFirst(JDBC_URL_PREFIX, ""));
continue;
}
if (param.startsWith(USERNAME_PREFIX)) {
dataSourceProperties.setUsername(param.replaceFirst(USERNAME_PREFIX, ""));
continue;
}
if (param.startsWith(PASSWORD_PREFIX)) {
dataSourceProperties.setPassword(param.replaceFirst(PASSWORD_PREFIX, ""));
}
}
return dataSourceProperties;
}
}
3.1.7 ExchangeDataSource
package org.feng.datasource;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ExchangeDataSource {
String value() default DataSourceConstant.MASTER;
}
3.1.8 DynamicDataSourceAspect
package org.feng.datasource.aop;
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;
import org.feng.datasource.DynamicDataSourceContextHolder;
import org.feng.datasource.ExchangeDataSource;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.util.Objects;
@Aspect
@Order(1)
@Component
public class DynamicDataSourceAspect {
@Before("execution(public * org.feng.service.*.*(..))")
public void before(JoinPoint joinPoint) {
System.out.println("开始切入切换数据源...");
ExchangeDataSource exchangeDataSource = parseMethodWithExchangeDataSource(joinPoint);
if (Objects.isNull(exchangeDataSource)) {
return;
}
String merchantKey = exchangeDataSource.value();
System.out.println("merchantKey = " + merchantKey);
DynamicDataSourceContextHolder.setDataSourceKey(merchantKey);
}
@After("execution(public * org.feng.service.*.*(..))")
public void after() {
System.out.println("切入数据源之后...");
DynamicDataSourceContextHolder.removeDataSourceKey();
}
private ExchangeDataSource parseMethodWithExchangeDataSource(JoinPoint joinPoint) {
Class<?> targetClass = joinPoint.getTarget().getClass();
if (targetClass.isAnnotationPresent(ExchangeDataSource.class)) {
return targetClass.getAnnotation(ExchangeDataSource.class);
}
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
if (method.isAnnotationPresent(ExchangeDataSource.class)) {
return method.getAnnotation(ExchangeDataSource.class);
}
return null;
}
}
4 启动类&测试使用
4.1 启动类
package org.feng;
import org.feng.datasource.config.DataSourcePropertiesConfig;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
@EnableAspectJAutoProxy(exposeProxy = true)
@EnableConfigurationProperties(value = {DataSourcePropertiesConfig.class})
@SpringBootApplication
public class DynamicDatasourceApplication {
public static void main(String[] args) {
SpringApplication.run(DynamicDatasourceApplication.class, args);
}
}
4.2 org.feng.service 中
4.2.1 Test
package org.feng.service;
import java.util.Map;
public interface Test {
Map<String, Object> dbTestMaster();
Map<String, Object> dbTestSlave1();
}
4.2.1 TestServiceImpl
在这里做切换数据源切面操作。 如果想使用手动切换数据,请参考本文中的 3.1.8 小节。
package org.feng.service;
import org.feng.datasource.DataSourceConstant;
import org.feng.datasource.ExchangeDataSource;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.Map;
@EnableAspectJAutoProxy(proxyTargetClass = true)
@Service
public class TestServiceImpl implements Test {
@Resource
private JdbcTemplate jdbcTemplate;
@Override
@ExchangeDataSource
public Map<String, Object> dbTestMaster() {
return jdbcTemplate.queryForMap("select * from test");
}
@Override
@ExchangeDataSource(DataSourceConstant.SLAVE1)
public Map<String, Object> dbTestSlave1() {
return jdbcTemplate.queryForMap("select * from test");
}
}
4.3 org.feng.controller 中
只有一个文件, TestController.java
4.3.1 TestController
package org.feng.controller;
import org.feng.service.Test;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.util.List;
import java.util.Map;
@RestController
public class TestController {
@Resource
private Test test;
@GetMapping("/testMaster")
public Map<String, Object> testMaster() {
return test.dbTestMaster();
}
@GetMapping("/testSlave1")
public Map<String, Object> testSlave1() {
return test.dbTestSlave1();
}
@GetMapping("/test")
public List<Map<String, Object>> test() {
Map<String, Object> map1 = test.dbTestMaster();
Map<String, Object> map2 = test.dbTestSlave1();
return List.of(map1, map2);
}
}
5 测试结果
启动项目后,调用:
GET http://localhost:8080/test
返回的结果是:
[
{
"id": 2,
"name": "测试22222"
},
{
"id": 3,
"name": "测试33333"
}
]
6 数据库文件
创建两个库: 并且都创建一个叫 test 的表。
6.1 数据库1
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
DROP TABLE IF EXISTS `test`;
CREATE TABLE `test` (
`id` int(0) NOT NULL,
`name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
INSERT INTO `test` VALUES (2, '测试22222');
SET FOREIGN_KEY_CHECKS = 1;
6.2 数据库2
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
DROP TABLE IF EXISTS `test`;
CREATE TABLE `test` (
`id` int(0) NOT NULL,
`name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
INSERT INTO `test` VALUES (3, '测试33333');
SET FOREIGN_KEY_CHECKS = 1;
|