SpringBoot+AOP构建多数据源的切换实践

简介: SpringBoot+AOP构建多数据源的切换实践

针对微服务架构中常用的设计模块,通常我们都会需要使用到druid作为我们的数据连接池,当架构发生扩展的时候 ,通常面对的数据存储服务器也会渐渐增加,从原本的单库架构逐渐扩展为复杂的多库架构。


当在业务层需要涉及到查询多种同数据库的场景下,我们通常需要在执行sql的时候动态指定对应的datasource。


而Spring的AbstractRoutingDataSource则正好为我们提供了这一功能点,下边我将通过一个简单的基于springboot+aop的案例来实现如何通过自定义注解切换不同的数据源进行读数据操作,同时也将结合部分源码的内容进行讲解。


首先我们需要自定义一个专门用于申明当前java应用程序所需要使用到哪些数据源信息:


package mutidatasource.annotation;
import mutidatasource.config.DataSourceConfigRegister;
import mutidatasource.enums.SupportDatasourceEnum;
import org.springframework.context.annotation.Import;
import org.springframework.stereotype.Component;
import java.lang.annotation.*;
/**
 * 注入数据源
 *
 * @author idea
 * @data 2020/3/7
 */
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(DataSourceConfigRegister.class)
public @interface AppDataSource {
    SupportDatasourceEnum[] datasourceType();
}


这里为了方便,我将测试中使用的数据源地址都配置来enum里面,如果后边需要灵活处理的话,可以将这些配置信息抽取出来放在一些配置中心上边。


package mutidatasource.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
/**
 * 目前支持的数据源信息
 *
 * @author idea
 * @data 2020/3/7
 */
@AllArgsConstructor
@Getter
public enum SupportDatasourceEnum {
    PROD_DB("jdbc:mysql://localhost:3306/db-prod?useUnicode=true&characterEncoding=utf8","root","root","db-prod"),
    DEV_DB("jdbc:mysql://localhost:3306/db-dev?useUnicode=true&characterEncoding=utf8","root","root","db-dev"),
    PRE_DB("jdbc:mysql://localhost:3306/db-pre?useUnicode=true&characterEncoding=utf8","root","root","db-pre");
    String url;
    String username;
    String password;
    String databaseName;
    @Override
    public String toString() {
        return super.toString().toLowerCase();
    }
}


之所以要创建这个@AppDataSource注解,是要在springboot的启动类上边进行标注:


package mutidatasource;
import mutidatasource.annotation.AppDataSource;
import mutidatasource.enums.SupportDatasourceEnum;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
 * @author idea
 * @data 2020/3/7
 */
@SpringBootApplication
@AppDataSource(datasourceType = {SupportDatasourceEnum.DEV_DB, SupportDatasourceEnum.PRE_DB, SupportDatasourceEnum.PROD_DB})
public class SpringApplicationDemo {
    public static void main(String[] args) {
        SpringApplication.run(SpringApplicationDemo.class);
    }
}


借助springboot的ImportSelector 自定义一个注册器来获取启动类头部的注解所指定的数据源类型:


package mutidatasource.config;
import lombok.extern.slf4j.Slf4j;
import mutidatasource.annotation.AppDataSource;
import mutidatasource.core.DataSourceContextHolder;
import mutidatasource.enums.SupportDatasourceEnum;
import org.springframework.context.annotation.ImportSelector;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.stereotype.Component;
/**
 * @author idea
 * @data 2020/3/7
 */
@Slf4j
@Component
public class DataSourceConfigRegister implements ImportSelector {
    @Override
    public String[] selectImports(AnnotationMetadata annotationMetadata) {
        AnnotationAttributes attributes = AnnotationAttributes.fromMap(annotationMetadata.getAnnotationAttributes(AppDataSource.class.getName()));
        System.out.println("#######  datasource import #######");
        if (null != attributes) {
            Object object = attributes.get("datasourceType");
            SupportDatasourceEnum[] supportDatasourceEnums = (SupportDatasourceEnum[]) object;
            for (SupportDatasourceEnum supportDatasourceEnum : supportDatasourceEnums) {
                DataSourceContextHolder.addDatasource(supportDatasourceEnum);
            }
        }
        return new String[0];
    }
}


好的,现在我们已经能够获取到对应的数据源类型信息了,这里你会看到一个叫做DataSourceContextHolder的角色。这个对象主要是用于对每个请求线程的数据源信息做统一的分配和管理。


在多并发场景下,为了防止不同线程请求的数据源出现“互窜”情况,通常我们都会使用到threadlocal来做处理。为每一个线程都分配一个指定的,属于其内部的副本变量,当当前线程结束之前,记得将对应的线程副本也进行销毁。


package mutidatasource.core;
import mutidatasource.enums.SupportDatasourceEnum;
import java.util.HashSet;
/**
 * @author idea
 * @data 2020/3/7
 */
public class DataSourceContextHolder {
    private static final HashSet<SupportDatasourceEnum> dataSourceSet = new HashSet<>();
    private static final ThreadLocal<String> databaseHolder = new ThreadLocal<>();
    public static void setDatabaseHolder(SupportDatasourceEnum supportDatasourceEnum) {
        databaseHolder.set(supportDatasourceEnum.toString());
    }
    /**
     * 取得当前数据源
     *
     * @return
     */
    public static String getDatabaseHolder() {
        return databaseHolder.get();
    }
    /**
     * 添加数据源
     *
     * @param supportDatasourceEnum
     */
    public static void addDatasource(SupportDatasourceEnum supportDatasourceEnum) {
        dataSourceSet.add(supportDatasourceEnum);
    }
    /**
     * 获取当期应用所支持的所有数据源
     *
     * @return
     */
    public static HashSet<SupportDatasourceEnum> getDataSourceSet() {
        return dataSourceSet;
    }
    /**
     * 清除上下文数据
     */
    public static void clear() {
        databaseHolder.remove();
    }
}


spring内部的AbstractRoutingDataSource动态路由数据源里面有一个抽象方法叫做

determineCurrentLookupKey,这个方法适用于提供给开发者自定义对应数据源的查询key。


package mutidatasource.core;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
/**
 * @author idea
 * @data 2020/3/7
 */
public class DynamicDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        String dataSource = DataSourceContextHolder.getDatabaseHolder();
        return dataSource;
    }
}


这里我使用的druid数据源,所以配置数据源的配置类如下:这里面我默认该应用配置类PROD数据源,用于测试使用。


package mutidatasource.core;
import com.alibaba.druid.pool.DruidDataSource;
import lombok.extern.slf4j.Slf4j;
import mutidatasource.enums.SupportDatasourceEnum;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Component;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.HashSet;
/**
 * @author idea
 * @data 2020/3/7
 */
@Slf4j
@Component
public class DynamicDataSourceConfiguration {
    @Bean
    @Primary
    @ConditionalOnMissingBean
    public DataSource dataSource() {
        System.out.println("init datasource");
        DynamicDataSource dynamicDataSource = new DynamicDataSource();
        //设置原始数据源
        HashMap<Object, Object> dataSourcesMap = new HashMap<>();
        HashSet<SupportDatasourceEnum> dataSet = DataSourceContextHolder.getDataSourceSet();
        for (SupportDatasourceEnum supportDatasourceEnum : dataSet) {
            DataSource dataSource = this.createDataSourceProperties(supportDatasourceEnum);
            dataSourcesMap.put(supportDatasourceEnum.toString(), dataSource);
        }
        dynamicDataSource.setTargetDataSources(dataSourcesMap);
        dynamicDataSource.setDefaultTargetDataSource(createDataSourceProperties(SupportDatasourceEnum.PRE_DB));
        return dynamicDataSource;
    }
    private synchronized DataSource createDataSourceProperties(SupportDatasourceEnum supportDatasourceEnum) {
        DruidDataSource druidDataSource = new DruidDataSource();
        druidDataSource.setUrl(supportDatasourceEnum.getUrl());
        druidDataSource.setUsername(supportDatasourceEnum.getUsername());
        druidDataSource.setPassword(supportDatasourceEnum.getPassword());
        //具体配置
        druidDataSource.setMaxActive(100);
        druidDataSource.setInitialSize(5);
        druidDataSource.setMinIdle(1);
        druidDataSource.setMaxWait(30000);
        //间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
        druidDataSource.setTimeBetweenConnectErrorMillis(60000);
        return druidDataSource;
    }
}


好了现在一个基础的数据源注入已经可以了,那么我们该如何借助注解来实现动态切换数据源的操作呢?


为此,我设计了一个叫做UsingDataSource的注解,通过利用该注解来识别当前线程所需要使用的数据源操作:



package mutidatasource.annotation;
import mutidatasource.enums.SupportDatasourceEnum;
import java.lang.annotation.*;
/**
 * @author idea
 * @data 2020/3/7
 */
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface UsingDataSource {
    SupportDatasourceEnum type()  ;
}


然后,借助了spring的aop来做切面拦截:


package mutidatasource.core;
import lombok.extern.slf4j.Slf4j;
import mutidatasource.annotation.UsingDataSource;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.util.Arrays;
/**
 * @author idea
 * @data 2020/3/7
 */
@Slf4j
@Aspect
@Configuration
public class DataSourceAspect {
    public DataSourceAspect(){
        System.out.println("this is init");
    }
    @Pointcut("@within(mutidatasource.annotation.UsingDataSource) || " +
            "@annotation(mutidatasource.annotation.UsingDataSource)")
    public void pointCut(){
    }
    @Before("pointCut() && @annotation(usingDataSource)")
    public void doBefore(UsingDataSource usingDataSource){
        log.debug("select dataSource---"+usingDataSource.type());
        DataSourceContextHolder.setDatabaseHolder(usingDataSource.type());
    }
    @After("pointCut()")
    public void doAfter(){
        DataSourceContextHolder.clear();
    }
}


测试类如下所示:


package mutidatasource.controller;
import lombok.extern.slf4j.Slf4j;
import mutidatasource.annotation.UsingDataSource;
import mutidatasource.enums.SupportDatasourceEnum;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
 * @author idea
 * @data 2020/3/8
 */
@RestController
@RequestMapping(value = "/test")
@Slf4j
public class TestController {
    @Autowired
    private JdbcTemplate jdbcTemplate;
    @GetMapping(value = "/testDev")
    @UsingDataSource(type=SupportDatasourceEnum.DEV_DB)
    public void testDev() {
        showData();
    }
    @GetMapping(value = "/testPre")
    @UsingDataSource(type=SupportDatasourceEnum.PRE_DB)
    public void testPre() {
        showData();
    }
    private void showData() {
        jdbcTemplate.queryForList("select * from test1").forEach(row -> log.info(row.toString()));
    }
}


最后 启动springboot服务,通过使用注解即可测试对应功能。


关于AbstractRoutingDataSource 动态路由数据源的注入原理,


可以看到这个内部类里面包含了多种用于做数据源映射的map数据结构。


image.png


在该类的最底部,有一个determineCurrentLookupKey函数,也就是上边我们所提及的使用于查询当前数据源key的方法。

具体代码如下:


/**
     * Retrieve the current target DataSource. Determines the
     * {@link #determineCurrentLookupKey() current lookup key}, performs
     * a lookup in the {@link #setTargetDataSources targetDataSources} map,
     * falls back to the specified
     * {@link #setDefaultTargetDataSource default target DataSource} if necessary.
     * @see #determineCurrentLookupKey()
     */
    protected DataSource determineTargetDataSource() {
        Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
        //这里面注入我们当前线程使用的数据源
        Object lookupKey = determineCurrentLookupKey();
        //在初始化数据源的时候需要我们去给resolvedDataSources进行注入
        DataSource dataSource = this.resolvedDataSources.get(lookupKey);
        if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
            dataSource = this.resolvedDefaultDataSource;
        }
        if (dataSource == null) {
            throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
        }
        return dataSource;
    }
    /**
     * Determine the current lookup key. This will typically be
     * implemented to check a thread-bound transaction context.
     * <p>Allows for arbitrary keys. The returned key needs
     * to match the stored lookup key type, as resolved by the
     * {@link #resolveSpecifiedLookupKey} method.
     */
    @Nullable
    protected abstract Object determineCurrentLookupKey();


而在该类的afterPropertiesSet里面,又有对于初始化数据源的注入操作,这里面的targetDataSources 正是上文中我们对在初始化数据源时候注入的信息。


@Override
    public void afterPropertiesSet() {
        if (this.targetDataSources == null) {
            throw new IllegalArgumentException("Property 'targetDataSources' is required");
        }
        this.resolvedDataSources = new HashMap<>(this.targetDataSources.size());
        this.targetDataSources.forEach((key, value) -> {
            Object lookupKey = resolveSpecifiedLookupKey(key);
            DataSource dataSource = resolveSpecifiedDataSource(value);
            this.resolvedDataSources.put(lookupKey, dataSource);
        });
        if (this.defaultTargetDataSource != null) {
            this.resolvedDefaultDataSource = resolveSpecifiedDataSource(this.defaultTargetDataSource);
        }
    }


END

相关文章
|
1月前
|
Java API 数据库
构建RESTful API已经成为现代Web开发的标准做法之一。Spring Boot框架因其简洁的配置、快速的启动特性及丰富的功能集而备受开发者青睐。
【10月更文挑战第11天】本文介绍如何使用Spring Boot构建在线图书管理系统的RESTful API。通过创建Spring Boot项目,定义`Book`实体类、`BookRepository`接口和`BookService`服务类,最后实现`BookController`控制器来处理HTTP请求,展示了从基础环境搭建到API测试的完整过程。
48 4
|
1月前
|
Java API 数据库
如何使用Spring Boot构建RESTful API,以在线图书管理系统为例
【10月更文挑战第9天】本文介绍了如何使用Spring Boot构建RESTful API,以在线图书管理系统为例,从项目搭建、实体类定义、数据访问层创建、业务逻辑处理到RESTful API的实现,详细展示了每个步骤。通过Spring Boot的简洁配置和强大功能,开发者可以高效地开发出功能完备、易于维护的Web应用。
63 3
|
25天前
|
JavaScript 安全 Java
如何使用 Spring Boot 和 Ant Design Pro Vue 构建一个具有动态路由和菜单功能的前后端分离应用。
本文介绍了如何使用 Spring Boot 和 Ant Design Pro Vue 构建一个具有动态路由和菜单功能的前后端分离应用。首先,创建并配置 Spring Boot 项目,实现后端 API;然后,使用 Ant Design Pro Vue 创建前端项目,配置动态路由和菜单。通过具体案例,展示了如何快速搭建高效、易维护的项目框架。
97 62
|
14天前
|
Java 数据库连接
SpringBoot配置多数据源实战
第四届光学与机器视觉国际学术会议(ICOMV 2025) 2025 4th International Conference on Optics and Machine Vision
44 8
|
17天前
|
Java
SpringBoot构建Bean(RedisConfig + RestTemplateConfig)
SpringBoot构建Bean(RedisConfig + RestTemplateConfig)
37 2
|
23天前
|
JavaScript 安全 Java
如何使用 Spring Boot 和 Ant Design Pro Vue 构建一个前后端分离的应用框架,实现动态路由和菜单功能
本文介绍了如何使用 Spring Boot 和 Ant Design Pro Vue 构建一个前后端分离的应用框架,实现动态路由和菜单功能。首先,确保开发环境已安装必要的工具,然后创建并配置 Spring Boot 项目,包括添加依赖和配置 Spring Security。接着,创建后端 API 和前端项目,配置动态路由和菜单。最后,运行项目并分享实践心得,帮助开发者提高开发效率和应用的可维护性。
41 2
|
23天前
|
JSON Java 数据库
SpringBoot项目使用AOP及自定义注解保存操作日志
SpringBoot项目使用AOP及自定义注解保存操作日志
34 1
|
28天前
|
数据采集 Java 数据安全/隐私保护
Spring Boot 3.3中的优雅实践:全局数据绑定与预处理
【10月更文挑战第22天】 在Spring Boot应用中,`@ControllerAdvice`是一个强大的工具,它允许我们在单个位置处理多个控制器的跨切面关注点,如全局数据绑定和预处理。这种方式可以大大减少重复代码,提高开发效率。本文将探讨如何在Spring Boot 3.3中使用`@ControllerAdvice`来实现全局数据绑定与预处理。
61 2
|
1月前
|
自然语言处理 Java API
Spring Boot 接入大模型实战:通义千问赋能智能应用快速构建
【10月更文挑战第23天】在人工智能(AI)技术飞速发展的今天,大模型如通义千问(阿里云推出的生成式对话引擎)等已成为推动智能应用创新的重要力量。然而,对于许多开发者而言,如何高效、便捷地接入这些大模型并构建出功能丰富的智能应用仍是一个挑战。
130 6
|
1月前
|
文字识别 安全 Java
SpringBoot3.x和OCR构建车牌识别系统
本文介绍了一个基于Java SpringBoot3.x框架的车牌识别系统,详细阐述了系统的设计目标、需求分析及其实现过程。利用Tesseract OCR库和OpenCV库,实现了车牌图片的识别与处理,确保系统的高准确性和稳定性。文中还提供了具体的代码示例,展示了如何构建和优化车牌识别服务,以及如何处理特殊和异常车牌。通过实际应用案例,帮助读者理解和应用这一解决方案。
下一篇
无影云桌面