前言

当前时代的业务繁杂,经常会使用到多个数据库,并且来回切换。为简化使用,我们会用到动态数据源的技术。其原理是使用了 Spring 框架提供的 AbstractRoutingDataSource 中的 determineCurrentLookupKey() 。

通过该方法切换 key 值,可以理解为各个数据库的名字或多租户场景下租户的名字。

本文对其做代码实现,并且增加使用 AOP + 注解的方式去切换数据源。最终还会将数据库连接的配置移到nacos中。

文中,懒的加日志了,直接 控制台打印了。不喜勿喷!!

1 项目文件结构

在这里插入图片描述
主要的代码在 org.feng.datasource 中,org.feng.serviceorg.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>


        <!-- Nacos读取配置-->
        <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:
        	# nacos 中组为 fjs,data-id 是 datasource.yml
          - 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;

/**
 * 动态数据源
 *
 * @version v1.0
 * @author: fengjinsong
 * @date: 2022年05月05日 15时09分
 */
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;

/**
 * 动态数据源上下文保持类
 *
 * @version v1.0
 * @author: fengjinsong
 * @date: 2022年05月05日 15时19分
 */
public class DynamicDataSourceContextHolder {

    /**
     * 动态数据源的上下文
     */
    private static final ThreadLocal<String> DATASOURCE_CONTEXT_MERCHANT_HOLDER = new InheritableThreadLocal<>();

    /**
     * 切换数据源
     *
     * @param merchant 租户Key
     */
    public static void setDataSourceKey(String merchant) {
        System.out.println("切换数据源:" + merchant);
        DATASOURCE_CONTEXT_MERCHANT_HOLDER.set(merchant);
    }

    /**
     * 获取当前数据源名称
     *
     * @return 当前数据源名称
     */
    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;

/**
 * 动态数据源配置
 *
 * @version v1.0
 * @author: fengjinsong
 * @date: 2022年05月05日 14时56分
 */
@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;

/**
 * 数据源常量
 *
 * @version v1.0
 * @author: fengjinsong
 * @date: 2022年05月05日 15时00分
 */
public interface DataSourceConstant {

    String MASTER = "master";

    String SLAVE1 = "slave1";
}

3.1.5 DataSourceProperties
package org.feng.datasource;

import lombok.Data;

/**
 * 数据库连接属性
 *
 * @version v1.0
 * @author: fengjinsong
 * @date: 2022年05月06日 12时58分
 */
@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;

/**
 * 数据源属性配置
 *
 * @version v1.0
 * @author: fengjinsong
 * @date: 2022年05月06日 13时01分
 */
@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=";

    /**
     * key=租户,value=连接数据库的参数
     */
    @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;

/**
 * 注解切换数据源
 *
 * @version v1.0
 * @author: fengjinsong
 * @date: 2022年05月05日 16时33分
 */
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ExchangeDataSource {
    /**
     * 数据源名称:租户key
     *
     * @return 数据源名称
     */
    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;

/**
 * 切面实现动态数据源切换
 *
 * @version v1.0
 * @author: fengjinsong
 * @date: 2022年05月05日 16时35分
 */
@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;

/**
 * @author Administrator
 */
@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;

/**
 * 测试
 *
 * @version v1.0
 * @author: fengjinsong
 * @date: 2022年05月06日 10时33分
 */
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;

/**
 * 测试切换数据源
 *
 * @version v1.0
 * @author: fengjinsong
 * @date: 2022年05月06日 10时33分
 */
@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;

/**
 * 测试
 *
 * @version v1.0
 * @author: fengjinsong
 * @date: 2022年05月05日 15时43分
 */
@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;

-- ----------------------------
-- Table structure for test
-- ----------------------------
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;

-- ----------------------------
-- Records of test
-- ----------------------------
INSERT INTO `test` VALUES (2, '测试22222');

SET FOREIGN_KEY_CHECKS = 1;

6.2 数据库2

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for test
-- ----------------------------
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;

-- ----------------------------
-- Records of test
-- ----------------------------
INSERT INTO `test` VALUES (3, '测试33333');

SET FOREIGN_KEY_CHECKS = 1;

7 配置优化(简化配置

这里提出一个优化点,简化配置的方式。
参考:springboot中复杂的配置

简化本文中的数据源对应的配置,可以直接进行解析,这里写一个示例:

@Data
@Slf4j
@ConfigurationProperties(prefix = "spring.datasource")
public class DataSourceConfiguration {

    private Map<String, DataSourceProperties> configMap;

    @PostConstruct
    private void init() {
        configMap.forEach((k, v) -> log.info("租户:{} 的数据库信息配置为:{}", k, v));
    }
}

这个配置类对应的配置文件如下:

spring:
  datasource:
    config-map:
      master:
        driver-class-name: "com.mysql.cj.jdbc.Driver"
        jdbc-url: "jdbc:mysql://localhost:13320/dynamic_master1?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=UTF-8"
        username: "root"
        password: "123456"
      slave1:
        driver-class-name: "com.mysql.cj.jdbc.Driver"
        jdbc-url: "jdbc:mysql://localhost:13321/dynamic_master1?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=UTF-8"
        username: "root1"
        password: "123456"
      slave2:
        driver-class-name: "com.mysql.cj.jdbc.Driver"
        jdbc-url: "jdbc:mysql://localhost:13322/dynamic_master1?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=UTF-8"
        username: "root2"
        password: "123456"

当启动项目时,控制台会输出对应的数据源配置信息:
在这里插入图片描述

GitHub 加速计划 / na / nacos
29.81 K
12.75 K
下载
Nacos是由阿里巴巴开源的服务治理中间件,集成了动态服务发现、配置管理和服务元数据管理功能,广泛应用于微服务架构中,简化服务治理过程。
最近提交(Master分支:1 个月前 )
3a9003bc 15 天前
963b221f 15 天前
Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐