针对微服务架构中常用的设计模块,通常我们都会需要使用到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;@Overridepublic 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 {@Overridepublic 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 {@Overrideprotected 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@ConditionalOnMissingBeanpublic 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 {@Autowiredprivate 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数据结构。

在该类的最底部,有一个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.*/@Nullableprotected abstract Object determineCurrentLookupKey();

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

    @Overridepublic 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

Java面试题专栏

【41期】盘点那些必问的数据结构算法题之链表

【42期】盘点那些必问的数据结构算法题之二叉堆

【43期】盘点那些必问的数据结构算法题之二叉树基础

【44期】盘点那些必问的数据结构算法题之二分查找算法

【45期】盘点那些必问的数据结构算法题之基础排序

【46期】盘点那些必问的数据结构算法题之快速排序

【47期】六大类二叉树面试题汇总解答

【48期】盘点Netty面试常问考点:什么是 Netty 的零拷贝?

【49期】面试官:SpringMVC的控制器是单例的吗?

【50期】基础考察:ClassNotFoundException 和 NoClassDefFoundError 有什么区别

我知道你 “在看”

SpringBoot+AOP构建多数据源的切换实践相关推荐

  1. SpringBoot+AOP实现多数据源动态切换

    SpringBoot+AOP实现多数据源动态切换 背景 设计总体思路 步骤 背景 系统后端需要访问多个数据库,现有的数据库连接配置写入配置文件中.后端需要从一个数据库的配置表里动态的读取其它mysql ...

  2. Spring-Boot + AOP实现多数据源动态切换

    2019独角兽企业重金招聘Python工程师标准>>> 最近在做保证金余额查询优化,在项目启动时候需要把余额全量加载到本地缓存,因为需要全量查询所有骑手的保证金余额,为了不影响主数据 ...

  3. springboot使用mybatis多数据源动态切换的实现

    需求:项目使用了读写分离,或者数据进行了分库处理,我们希望在操作不同的数据库的时候,我们的程序能够动态的切换到相应的数据库,执行相关的操作. 首先,你需要一个能够正常运行的springboot项目,配 ...

  4. SpringBoot 项目构建 Docker 镜像调优实践

    点击上方"Java知音",选择"置顶公众号" 技术文章第一时间送达! 作者:超级小豆丁 http://www.mydlq.club/article/16/ PS ...

  5. 多数据源解决方案——AOP实现多数据源动态切换

    文章目录 1. 场景描述 2. 项目依赖 3. 场景实现 4. 项目源代码 GitHub 1. 场景描述 在springboot开发中,可能遇见操作多个数据库的情形.一种解决方案就是:配置动态数据源, ...

  6. SpringBoot多数据源切换,AOP实现动态数据源切换

    SpringBoot多数据源切换,AOP实现动态数据源切换 操作数据一般都是在DAO层进行处理,可以选择直接使用JDBC进行编程 或者是使用多个DataSource 然后创建多个SessionFact ...

  7. SpringBoot+AOP实现动态切换数据源

    首先描述下笔者的基本步骤: 1.yml配置文件中定义多数据源: 2.自定义一个注解类(@DataSource 可标注在类或方法上),定义String类型的变量value,value中存储数据源名称: ...

  8. SpringBoot 优雅实现动态数据源切换配置

    点击关注公众号,利用碎片时间学习 前言 随着应用用户数量的增加,相应的并发请求的数量也会跟着不断增加,慢慢地,单个数据库已经没有办法满足我们频繁的数据库操作请求了,在某些场景下,我们可能会需要配置多个 ...

  9. SpringBoot+MybatisPlus多数据源动态切换

    公司某项目做大屏展示,但数据来源自7个不同的数据库,需要涉及跨库查询,要求. 本项目采用SpringBoot+MybatisPlus做服务端提供RESTful接口,前后端分离开发,总结一下项目中实现的 ...

最新文章

  1. spring boot基础配置
  2. php json csv,比JSON更简单,随便记数据的CSV介绍,以及PHP解析方法-csv文件怎么打开...
  3. 大连工业大学艺术学院计算机考试,大连工业大学艺术与信息工程学院应用科技学院...
  4. netty mysql 中间件_Cobar_基于MySQL的分布式数据库服务中间件
  5. “曲屏版iPhone 13 Pro” 2799元!荣耀60 SE新版上架:12GB+256GB超大存储
  6. HDU1863 畅通工程【Kruskal算法+并查集】
  7. Android 意图(Intent) 理论详解
  8. 第4个HttpClient 例子,下载指定图片并保存到请定目录
  9. 虚拟机-Debian服务器配置
  10. 微信公众号开发获取code
  11. Servlet九大内置对象
  12. 小新pro13 archlinux 显卡 声卡 驱动安装
  13. flutter无法抓包
  14. 带你从头到尾梳理大图片加载OOM处理问题
  15. microsoft的罗马帝国——浪潮之巅
  16. 龙芯3a5000部署nacos 1.4低版本方法
  17. python:小鱼的航程
  18. centos7—DNS域名系统
  19. 【项目】实现一个mini的tcmalloc(高并发内存池)
  20. C#版谷歌地图下载器设计与实现

热门文章

  1. 蚂蚁森林上线三周年,5亿人“手机种树”1.22亿棵...
  2. 百度回应2015年的“复旦视频”:不要让当年唱歌的学生面对舆论压力
  3. Apple Watch再立功!67岁男子意外摔倒后得救
  4. 请查收~微信春节聊天彩蛋 微信群的卖萌小神器
  5. 腾讯的一道链表笔试题【总结】
  6. 内存管理(C语言中malloc和free的用法)
  7. php开发神器 -- phpStudy
  8. mysql技术简介_MySQL数据类型介绍
  9. 《转》ReentrantLock实现原理深入探究
  10. ORA-12505,TNS:listener does not currently know of SID given in connect descriptor(不知道的SID)