SpringBoot+AOP構建多數據源的切換實踐

  • 2020 年 3 月 16 日
  • 筆記

針對微服務架構中常用的設計模組,通常我們都會需要使用到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數據結構。

SpringBoot+AOP構建多數據源的切換實踐

在該類的最底部,有一個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);          }      }