自定义动态数据源

一、思路

1)自定义一个注解 @DataSource,该注解可以加在service层方法或者类上面,表示该方法或者类中的所有方法都是用某一个数据源。
2)如果某个方法上或类上有 @DataSource 注解,将该方法或类需要使用的数据源名称存入到ThreadLocal。
3)自定义切面,在切面中解析 @DataSource 注解,当一个方法或者类上面有 @DataSource注解的时候,将 @DataSource注解所标记的数据源存入到 ThreadLocal中。
4)最后,当 Mapper执行的时候,需要 DataSource数据源,会自动去 AbstractRoutingDataSource类中查找需要的数据源,我们只需要在 AbstractRoutingDataSource中返回 ThreadLocal中的值即可。

二、项目结构

三、操作步骤

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>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.9</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.lcb</groupId>
    <artifactId>dynamicdatasource</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>dynamicdatasource</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--aop依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>3.0.0</version>
        </dependency>

        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <!-- https://mvnrepository.com/artifact/com.alibaba/druid-spring-boot-starter -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.2.16</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </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>

2.application.yaml

server:
  port: 8002
# 数据源配置
spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driverClassName: com.mysql.cj.jdbc.Driver
    ds:
      # 主库数据源
      master:
        url: jdbc:mysql://localhost:3306/test01?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
        username: root
        password: 123456
      # 从库数据源
      slave:
        url: jdbc:mysql://localhost:3306/test02?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
        username: root
        password: 123456
    # 初始连接数
    initialSize: 5
    # 最小连接池数量
    minIdle: 10
    # 最大连接池数量
    maxActive: 20
    # 配置获取连接等待超时的时间
    maxWait: 60000
    # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
    timeBetweenEvictionRunsMillis: 60000
    # 配置一个连接在池中最小生存的时间,单位是毫秒
    minEvictableIdleTimeMillis: 300000
    # 配置一个连接在池中最大生存的时间,单位是毫秒
    maxEvictableIdleTimeMillis: 900000
    # 配置检测连接是否有效
    validationQuery: SELECT 1 FROM DUAL
    testWhileIdle: true
    testOnBorrow: false
    testOnReturn: false
    webStatFilter:
      enabled: true
    statViewServlet:
      enabled: true
      # 设置白名单,不填则允许所有访问
      allow:
      url-pattern: /druid/*
      # 控制台管理用户名和密码
      login-username: tienchin
      login-password: 123456
    filter:
      stat:
        enabled: true
        # 慢SQL记录
        log-slow-sql: true
        slow-sql-millis: 1000
        merge-sql: true
      wall:
        config:
          multi-statement-allow: true

3.在datasource包下 DynamicDataSourceContextHolder.java

用来存储和获取当前线程所使用的数据源名称

package com.lcb.dynamicdatasource.datasource;

/**
 * @author lcb
 * @className: DynamicDataSourceContextHolder
 * @description: 用来存储当前线程所使用的数据源名称
 */

public class DynamicDataSourceContextHolder {
    private static ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();

    /**
     * 设置数据源类型
     * @param dsType
     */
    public static void setDataSourceType(String dsType){
        CONTEXT_HOLDER.set(dsType);
    }

    /**
     * 获取数据源类型
     * @return
     */
    public static String getDataSourceType(){
        return CONTEXT_HOLDER.get();
    }

    /**
     * 清空线程
     */
    public static void clearDataSourceType(){
        CONTEXT_HOLDER.remove();
    }

}

4.在annanation包下 DataSource注解类 DataSource.java

这个注解(DataSource)将来可以加在某一个service类上或者方法上,通过 value属性来指定类或方法应该使用哪个数据源。

package com.lcb.dynamicdatasource.annanation;


import com.lcb.dynamicdatasource.datasource.DataSourceType;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 这个注解(DataSource)将来可以加在某一个service类上或者方法上,通过 value属性来指定类或方法应该使用哪个数据源
 * @author lcb
 */
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE,ElementType.METHOD})
public @interface DataSource {
    /**
     * 如果一个方法上加了 @DataSource 注解,但是没有指定数据源名称,默认使用master数据源
     * @return
     */
    String value() default DataSourceType.DEFAULT_DS_NAME;
}

5.自定义切面 aspect包下DataSourceAspect.java

方法或类上有 @DataSource 注解就将方法或类拦截下来

package com.lcb.dynamicdatasource.aspect;

import com.lcb.dynamicdatasource.annanation.DataSource;
import com.lcb.dynamicdatasource.datasource.DynamicDataSourceContextHolder;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.stereotype.Component;

/**
 * @author lcb
 * @className: DataSourceAspect
 * @description: TODO
 * @date 2023/03/24
 */
@Aspect
@Component
public class DataSourceAspect {
    /**
     * @annotation(注解类所在包名) 表示方法上有 @DataSource 注解就将方法拦截下来
     * @within(注解类所在包名) 表示如果类上面有 @DataSource 注解就将类中的方法拦截下来
     */
    @Pointcut("@annotation(com.lcb.dynamicdatasource.annanation.DataSource) || @within(com.lcb.dynamicdatasource.annanation.DataSource)")
    public void pc(){

    }

    @Around("pc()")
    public Object around(ProceedingJoinPoint pjp){
        //获取方法上的有效注解
        DataSource dataSource = getDataSource(pjp);
        if (dataSource != null){
            //获取注解中数据源名称
            String value = dataSource.value();
            //将数据源名称存储到线程中
            DynamicDataSourceContextHolder.setDataSourceType(value);
        }
        try {
            //让目标方法继续执行
            return pjp.proceed();
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }finally {
            //清空线程
            DynamicDataSourceContextHolder.clearDataSourceType();
        }
        return null;
    }

    private DataSource getDataSource(ProceedingJoinPoint pjp) {
        MethodSignature signature = (MethodSignature) pjp.getSignature();
        //查找方法上的注解
        DataSource annotation = AnnotationUtils.findAnnotation(signature.getMethod(), DataSource.class);
        if(annotation != null){
            //方法上有 @DataSource 注解 直接返回
            return annotation;
        }
        // 若方法上没有 则返回类上 @DataSource 注解
        return AnnotationUtils.findAnnotation(signature.getDeclaringType(),DataSource.class);
    }
}

6.datasource包下 DruidProperties.java

用来读取数据源

package com.lcb.dynamicdatasource.datasource;

import com.alibaba.druid.pool.DruidDataSource;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;

import javax.sql.DataSource;
import java.util.Map;

/**
 * @author lcb
 * @className: DruidProperties
 * @description: 读取数据源
 */
@ConfigurationProperties(prefix = "spring.datasource")
@Data
public class DruidProperties {
    private String type;
    private String driverClassName;
    private Map<String, Map<String,String>>  ds;
    private Integer initialSize;
    private Integer minIdle;
    private Integer maxActive;
    private Integer maxWait;

    /**
     * 在外部构造一个 DruidDataSource 对象,这个对象包含三个属性 url,username,password
     * 在这个方法中,给这个对象设置公共属性
     * @param druidDataSource
     * @return
     */
    public DataSource dataSource(DruidDataSource druidDataSource){
        druidDataSource.setInitialSize(initialSize);
        druidDataSource.setMinIdle(minIdle);
        druidDataSource.setMaxActive(maxActive);
        druidDataSource.setMaxWait(maxWait);
        return druidDataSource;
    }

}

7.datasource包下 LoadDataSource.java

用来加载数据源

package com.lcb.dynamicdatasource.datasource;

import com.alibaba.druid.pool.DruidDataSource;
import com.alibaba.druid.pool.DruidDataSourceFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.stereotype.Component;

import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

/**
 * @author lcb
 * @className: loadDataSource
 * @description: 加载数据源
 */
@Component
@EnableConfigurationProperties(DruidProperties.class)
public class LoadDataSource {
    @Autowired
    DruidProperties druidProperties;

    public Map<String, DataSource> loadAllDataSource(){
        Map<String, DataSource> map = new HashMap<>();
        //获取所有数据源
        Map<String, Map<String, String>> ds = druidProperties.getDs();
        try {
            //遍历数据源
            Set<String> keySet = ds.keySet();
            for (String key : keySet) {
                //创建数据源
                map.put(key, druidProperties.dataSource((DruidDataSource) DruidDataSourceFactory.createDataSource(ds.get(key))));
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return map;
    }
}

8.datasource包下 DynamicDataSource.java

继承AbstractRoutingDataSource类 ,重写 determineCurrentLookupKey方法,用来处理数据源。

package com.lcb.dynamicdatasource.datasource;

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import org.springframework.stereotype.Component;

import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;

/**
 * @author lcb
 * @className: DynamicDataSource
 * @description: 处理数据源
 */
@Component
public class DynamicDataSource extends AbstractRoutingDataSource {

    public DynamicDataSource(LoadDataSource loadDataSource){
        //1.设置所有的数据源
        Map<String, DataSource> allDs = loadDataSource.loadAllDataSource();
        super.setTargetDataSources(new HashMap<>(allDs));
        //2.设置默认的数据源
        //并不是所有的方法上都有 @DataSource注解,对于没有 @DataSource注解的方法,使用默认的数据源
        super.setDefaultTargetDataSource(allDs.get(DataSourceType.DEFAULT_DS_NAME));
        //3.作用:初始化bean的时候执行,可以针对某个具体的bean进行配置
        super.afterPropertiesSet();
    }
    /**
     * 用来返回数据源名称,当系统需要获取数据源名称的时候,会自动调用该方法获取数据源的名称
     * @return
     */
    @Override
    protected Object determineCurrentLookupKey() {
        return DynamicDataSourceContextHolder.getDataSourceType();
    }
}

9.datasource包下 DataSourceType.java

定义默认数据源名称

package com.lcb.dynamicdatasource.datasource;

/**
 * @author lcb
 * @className: DataSourceType
 * @description: 定义默认数据源
 */

public interface DataSourceType {
    String DEFAULT_DS_NAME = "master";
}

10.model包下 User.java

创建User实体类,实体类根据自己需求创建。

package com.lcb.dynamicdatasource.model;

import lombok.Data;

/**
 * @author lcb
 * @className: User
 * @description: TODO
 */
@Data
public class User {
    private Integer id;
    private String username;
    private String age;

}

11.mapper包下 UserMapper.java

用来查询用户信息,方便测试用注解形式,推荐用xml,

package com.lcb.dynamicdatasource.mapper;

import com.lcb.dynamicdatasource.model.User;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;

import java.util.List;

/**
 * @author lcb
 * @className: UserMapper
 * @description: 查询用户信息
 */
@Mapper
public interface UserMapper {
    /**
     * 获取所有信息
     * @return
     */
    @Select("select * from user")
    List<User> getAllUsers();
}

12.service包下 UserService.java

@DataSource 注解可加在service层

package com.lcb.dynamicdatasource.service;

import com.lcb.dynamicdatasource.mapper.UserMapper;
import com.lcb.dynamicdatasource.model.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

/**
 * @author lcb
 * @className: UserService
 * @description: 在service层添加注解
 */
@Service
public class UserService {
    @Autowired
    UserMapper userMapper;

    public List<User> getAllUsers(){
        return userMapper.getAllUsers();
    }
}

13.在test测试类中添加测试方法

测试自定义动态数据源,也可写接口进行测试

    @Autowired
    UserService userService;

    @Test
    void contextLoads() {
        List<User> list = userService.getAllUsers();
        for (User user : list) {
            System.out.println(user);
        }

    }

四、测试

1.创建test01和test02两个数据库

2.分别创建user表
test01 中 user表数据为

test02 中 user表数据为

3.启动test测试类中测试方法

不加@DataSource注解,数据源默认使用master,数据库为test01,控制台打印结果为

在service包下UserService类中方法上添加 @DataSource注解,指定数据源名称为slave。

控制台打印结果为

在service包下UserService类上面添加 @DataSource注解,指定数据源名称为slave,数据库为test02。

控制台打印结果为

posted @   ___sunsets  阅读(94)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
历史上的今天:
2022-03-23 打印水仙花数
2022-03-23 打印金字塔
2022-03-23 九九乘法表
点击右上角即可分享
微信分享提示