编程语言SpringBoot+AOP构建多数据源的切换实践

    作者:idea更新于: 2020-03-25 20:14:49

    大神带你学编程,欢迎选课

    SpringBoot+AOP构建多数据源的切换实践.高级语言的出现使得计算机程序设计语言不再过度地依赖某种特定的机器或环境。这是因为高级语言在不同的平台上会被编译成不同的机器语言,而不是直接被机器执行。最早出现的编程语言之一FORTRAN的一个主要目标,就是实现平台独立。

    下边我将通过一个简单的基于springboot+aop的案例来实现如何通过自定义注解切换不同的数据源进行读数据操作,同时也将结合部分源码的内容进行讲解。

    【金融特辑】光大银行科技部DBA女神带你从0到1揭秘MGR

    编程语言SpringBoot+AOP构建多数据源的切换实践_编程语言_ java_开发_课课家

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

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

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

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

    1. package mutidatasource.annotation;  
    2. import mutidatasource.config.DataSourceConfigRegister;  
    3. import mutidatasource.enums.SupportDatasourceEnum;  
    4. import org.springframework.context.annotation.Import;  
    5. import org.springframework.stereotype.Component;  
    6. import java.lang.annotation.*;  
    7. /**  
    8.  * 注入数据源  
    9.  *  
    10.  * @author idea  
    11.  * @data 2020/3/7  
    12.  */  
    13. @Target({ElementType.METHOD,ElementType.TYPE})  
    14. @Retention(RetentionPolicy.RUNTIME)  
    15. @Documented  
    16. @Import(DataSourceConfigRegister.class)  
    17. public @interface AppDataSource {  
    18.     SupportDatasourceEnum[] datasourceType();  

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

    1. package mutidatasource.enums;  
    2. import lombok.AllArgsConstructor;  
    3. import lombok.Getter;  
    4. import lombok.NoArgsConstructor;  
    5. /**  
    6.  * 目前支持的数据源信息  
    7.  *  
    8.  * @author idea  
    9.  * @data 2020/3/7  
    10.  */  
    11. @AllArgsConstructor  
    12. @Getter  
    13. public enum SupportDatasourceEnum {  
    14.     PROD_DB("jdbc:MySQL://localhost:3306/db-prod?useUnicode=true&characterEncoding=utf8","root","root","db-prod"), 
    15.     DEV_DB("jdbc:mysql://localhost:3306/db-dev?useUnicode=true&characterEncoding=utf8","root","root","db-dev"),  
    16.     PRE_DB("jdbc:mysql://localhost:3306/db-pre?useUnicode=true&characterEncoding=utf8","root","root","db-pre");  
    17.     String url;  
    18.     String username;  
    19.     String password;  
    20.     String databaseName;  
    21.     @Override  
    22.     public String toString() {  
    23.         return super.toString().toLowerCase();  
    24.     }  

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

    1. package mutidatasource;  
    2. import mutidatasource.annotation.AppDataSource;  
    3. import mutidatasource.enums.SupportDatasourceEnum;  
    4. import org.springframework.boot.SpringApplication;  
    5. import org.springframework.boot.autoconfigure.SpringBootApplication;  
    6. /**  
    7.  * @author idea  
    8.  * @data 2020/3/7  
    9.  */  
    10. @SpringBootApplication  
    11. @AppDataSource(datasourceType = {SupportDatasourceEnum.DEV_DB, SupportDatasourceEnum.PRE_DB, SupportDatasourceEnum.PROD_DB})  
    12. public class SpringApplicationDemo {  
    13.     public static void main(String[] args) {  
    14.         SpringApplication.run(SpringApplicationDemo.class);  
    15.     }  

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

    1. package mutidatasource.config;  
    2. import lombok.extern.slf4j.Slf4j;  
    3. import mutidatasource.annotation.AppDataSource;  
    4. import mutidatasource.core.DataSourceContextHolder;  
    5. import mutidatasource.enums.SupportDatasourceEnum;  
    6. import org.springframework.context.annotation.ImportSelector;  
    7. import org.springframework.core.annotation.AnnotationAttributes;  
    8. import org.springframework.core.type.AnnotationMetadata;  
    9. import org.springframework.stereotype.Component;  
    10. /**  
    11.  * @author idea  
    12.  * @data 2020/3/7  
    13.  */  
    14. @Slf4j  
    15. @Component  
    16. public class DataSourceConfigRegister implements ImportSelector {  
    17.     @Override  
    18.     public String[] selectImports(AnnotationMetadata annotationMetadata) {  
    19.         AnnotationAttributes attributes = AnnotationAttributes.fromMap(annotationMetadata.getAnnotationAttributes(AppDataSource.class.getName())); 
    20.          System.out.println("#######  datasource import #######");  
    21.         if (null != attributes) {  
    22.             Object object = attributes.get("datasourceType");  
    23.             SupportDatasourceEnum[] supportDatasourceEnums = (SupportDatasourceEnum[]) object;  
    24.             for (SupportDatasourceEnum supportDatasourceEnum : supportDatasourceEnums) {  
    25.                 DataSourceContextHolder.addDatasource(supportDatasourceEnum);  
    26.             }  
    27.         }  
    28.         return new String[0];  
    29.     }  

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

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

    1. package mutidatasource.core;  
    2. import mutidatasource.enums.SupportDatasourceEnum;  
    3. import java.util.HashSet;  
    4. /**  
    5.  * @author idea  
    6.  * @data 2020/3/7  
    7.  */  
    8. public class DataSourceContextHolder {  
    9.     private static final HashSet<SupportDatasourceEnum> dataSourceSet = new HashSet<>();  
    10.     private static final ThreadLocal<String> databaseHolder = new ThreadLocal<>();  
    11.     public static void setDatabaseHolder(SupportDatasourceEnum supportDatasourceEnum) {  
    12.         databaseHolder.set(supportDatasourceEnum.toString()); 
    13.     }  
    14.     /**  
    15.      * 取得当前数据源  
    16.      *  
    17.      * @return  
    18.      */  
    19.     public static String getDatabaseHolder() {  
    20.         return databaseHolder.get();  
    21.     }  
    22.     /**  
    23.      * 添加数据源  
    24.      *  
    25.      * @param supportDatasourceEnum  
    26.      */  
    27.     public static void addDatasource(SupportDatasourceEnum supportDatasourceEnum) {  
    28.         dataSourceSet.add(supportDatasourceEnum);  
    29.     }  
    30.     /**  
    31.      * 获取当期应用所支持的所有数据源  
    32.      *  
    33.      * @return  
    34.      */  
    35.     public static HashSet<SupportDatasourceEnum> getDataSourceSet() {  
    36.         return dataSourceSet;  
    37.     }  
    38.     /**  
    39.      * 清除上下文数据  
    40.      */  
    41.     public static void clear() {  
    42.         databaseHolder.remove();  
    43.     }  

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

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

    1. package mutidatasource.core;  
    2. import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;  
    3. /**  
    4.  * @author idea  
    5.  * @data 2020/3/7  
    6.  */  
    7. public class DynamicDataSource extends AbstractRoutingDataSource {  
    8.     @Override  
    9.     protected Object determineCurrentLookupKey() {  
    10.         String dataSource = DataSourceContextHolder.getDatabaseHolder();  
    11.         return dataSource;  
    12.     }  

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

    1. package mutidatasource.core;  
    2. import com.alibaba.druid.pool.DruidDataSource;  
    3. import lombok.extern.slf4j.Slf4j;  
    4. import mutidatasource.enums.SupportDatasourceEnum;  
    5. import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;  
    6. import org.springframework.context.annotation.Bean;  
    7. import org.springframework.context.annotation.Primary;  
    8. import org.springframework.stereotype.Component;  
    9. import javax.sql.DataSource;  
    10. import java.util.HashMap;  
    11. import java.util.HashSet;  
    12. /**  
    13.  * @author idea  
    14.  * @data 2020/3/7  
    15.  */  
    16. @Slf4j  
    17. @Component  
    18. public class DynamicDataSourceConfiguration {  
    19.     @Bean  
    20.     @Primary  
    21.     @ConditionalOnMissingBean  
    22.     public DataSource dataSource() {  
    23.         System.out.println("init datasource");  
    24.         DynamicDataSource dynamicDataSource = new DynamicDataSource();  
    25.         //设置原始数据源  
    26.         HashMap<Object, Object> dataSourcesMap = new HashMap<>();  
    27.         HashSet<SupportDatasourceEnum> dataSet = DataSourceContextHolder.getDataSourceSet();  
    28.         for (SupportDatasourceEnum supportDatasourceEnum : dataSet) {  
    29.             DataSource dataSource = this.createDataSourceProperties(supportDatasourceEnum);  
    30.             dataSourcesMap.put(supportDatasourceEnum.toString(), dataSource); 
    31.         }  
    32.         dynamicDataSource.setTargetDataSources(dataSourcesMap);  
    33.         dynamicDataSource.setDefaultTargetDataSource(createDataSourceProperties(SupportDatasourceEnum.PRE_DB));  
    34.         return dynamicDataSource;  
    35.     }  
    36.     private synchronized DataSource createDataSourceProperties(SupportDatasourceEnum supportDatasourceEnum) {  
    37.         DruidDataSource druidDataSource = new DruidDataSource();  
    38.         druidDataSource.setUrl(supportDatasourceEnum.getUrl());  
    39.         druidDataSource.setUsername(supportDatasourceEnum.getUsername());  
    40.         druidDataSource.setPassword(supportDatasourceEnum.getPassword());  
    41.         //具体配置  
    42.         druidDataSource.setMaxActive(100);  
    43.         druidDataSource.setInitialSize(5);  
    44.         druidDataSource.setMinIdle(1);  
    45.         druidDataSource.setMaxWait(30000);  
    46.         //间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒  
    47.         druidDataSource.setTimeBetweenConnectErrorMillis(60000);  
    48.         return druidDataSource;  
    49.     }  

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

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

    1. package mutidatasource.annotation;  
    2. import mutidatasource.enums.SupportDatasourceEnum;  
    3. import java.lang.annotation.*;  
    4. /**  
    5.  * @author idea  
    6.  * @data 2020/3/7  
    7.  */  
    8. @Target({ElementType.METHOD,ElementType.TYPE})  
    9. @Retention(RetentionPolicy.RUNTIME)  
    10. @Documented  
    11. public @interface UsingDataSource {  
    12.     SupportDatasourceEnum type()  ;  

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

    1. package mutidatasource.core;  
    2. import lombok.extern.slf4j.Slf4j;  
    3. import mutidatasource.annotation.UsingDataSource;  
    4. import org.aspectj.lang.JoinPoint;  
    5. import org.aspectj.lang.ProceedingJoinPoint;  
    6. import org.aspectj.lang.Signature;  
    7. import org.aspectj.lang.annotation.*;  
    8. import org.aspectj.lang.reflect.MethodSignature;  
    9. import org.springframework.context.annotation.Configuration;  
    10. import org.springframework.core.annotation.AnnotationUtils;  
    11. import org.springframework.core.annotation.Order;  
    12. import org.springframework.stereotype.Component;  
    13. import java.lang.reflect.Method;  
    14. import java.util.Arrays;  
    15. /**  
    16.  * @author idea  
    17.  * @data 2020/3/7  
    18.  */  
    19. @Slf4j  
    20. @Aspect  
    21. @Configuration  
    22. public class DataSourceAspect {  
    23.     public DataSourceAspect(){  
    24.         System.out.println("this is init");  
    25.     }  
    26.     @Pointcut("@within(mutidatasource.annotation.UsingDataSource) || " +  
    27.             "@annotation(mutidatasource.annotation.UsingDataSource)")  
    28.     public void pointCut(){  
    29.     }  
    30.     @Before("pointCut() && @annotation(usingDataSource)")  
    31.     public void doBefore(UsingDataSource usingDataSource){  
    32.         log.debug("select dataSource---"+usingDataSource.type());  
    33.         DataSourceContextHolder.setDatabaseHolder(usingDataSource.type()); 
    34.     }  
    35.     @After("pointCut()")  
    36.     public void doAfter(){  
    37.         DataSourceContextHolder.clear();  
    38.     }  

    测试类如下所示:

    1. package mutidatasource.controller;  
    2. import lombok.extern.slf4j.Slf4j;  
    3. import mutidatasource.annotation.UsingDataSource;  
    4. import mutidatasource.enums.SupportDatasourceEnum;  
    5. import org.springframework.beans.factory.annotation.Autowired;  
    6. import org.springframework.jdbc.core.JdbcTemplate;  
    7. import org.springframework.web.bind.annotation.GetMapping;  
    8. import org.springframework.web.bind.annotation.RequestMapping;  
    9. import org.springframework.web.bind.annotation.RestController;  
    10. /**  
    11.  * @author idea  
    12.  * @data 2020/3/8  
    13.  */  
    14. @RestController  
    15. @RequestMapping(value = "/test")  
    16. @Slf4j  
    17. public class TestController {  
    18.     @Autowired  
    19.     private JdbcTemplate jdbcTemplate;  
    20.     @GetMapping(value = "/testDev")  
    21.     @UsingDataSource(type=SupportDatasourceEnum.DEV_DB)  
    22.     public void testDev() {  
    23.         showData();  
    24.     }  
    25.     @GetMapping(value = "/testPre")  
    26.     @UsingDataSource(type=SupportDatasourceEnum.PRE_DB)  
    27.     public void testPre() {  
    28.         showData();  
    29.     }  
    30.     private void showData() {  
    31.         jdbcTemplate.queryForList("select * from test1").forEach(row -> log.info(row.toString()));  
    32.     }  

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

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

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

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

    具体代码如下:

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

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

    1. @Override  
    2.     public void afterPropertiesSet() {  
    3.         if (this.targetDataSources == null) {  
    4.             throw new IllegalArgumentException("Property 'targetDataSources' is required");  
    5.         }  
    6.         this.resolvedDataSources = new HashMap<>(this.targetDataSources.size());  
    7.         this.targetDataSources.forEach((key, value) -> {  
    8.             Object lookupKey = resolveSpecifiedLookupKey(key);  
    9.             DataSource dataSource = resolveSpecifiedDataSource(value);  
    10.             this.resolvedDataSources.put(lookupKey, dataSource);  
    11.         });  
    12.         if (this.defaultTargetDataSource != null) {  
    13.             this.resolvedDefaultDataSource = resolveSpecifiedDataSource(this.defaultTargetDataSource);  
    14.         }  
    15.     }  
      编程语言是人类控制电脑的手段,所以绝大多数编程语言都试图使这个过程更加强大和简单。也正因此这个领域诞生了数百种编程语言,不过其中许多编程语言目前已经不再使用,还有一些可能在未来几年里会过时,然而还有很多语言将不断发展,在目前以及未来都占有重要的位置。

课课家教育

未登录