SpringBoot+AOP構(gòu)建多數(shù)據(jù)源的切換實(shí)踐
針對(duì)微服務(wù)架構(gòu)中常用的設(shè)計(jì)模塊,通常我們都會(huì)需要使用到druid作為我們的數(shù)據(jù)連接池,當(dāng)架構(gòu)發(fā)生擴(kuò)展的時(shí)候 ,通常面對(duì)的數(shù)據(jù)存儲(chǔ)服務(wù)器也會(huì)漸漸增加,從原本的單庫(kù)架構(gòu)逐漸擴(kuò)展為復(fù)雜的多庫(kù)架構(gòu)。
當(dāng)在業(yè)務(wù)層需要涉及到查詢多種同數(shù)據(jù)庫(kù)的場(chǎng)景下,我們通常需要在執(zhí)行sql的時(shí)候動(dòng)態(tài)指定對(duì)應(yīng)的datasource。
而Spring的AbstractRoutingDataSource則正好為我們提供了這一功能點(diǎn),下邊我將通過一個(gè)簡(jiǎn)單的基于springboot+aop的案例來實(shí)現(xiàn)如何通過自定義注解切換不同的數(shù)據(jù)源進(jìn)行讀數(shù)據(jù)操作,同時(shí)也將結(jié)合部分源碼的內(nèi)容進(jìn)行講解。
首先我們需要自定義一個(gè)專門用于申明當(dāng)前java應(yīng)用程序所需要使用到哪些數(shù)據(jù)源信息:
- 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.*;
- /**
- * 注入數(shù)據(jù)源
- *
- * @author idea
- * @data 2020/3/7
- */
- @Target({ElementType.METHOD,ElementType.TYPE})
- @Retention(RetentionPolicy.RUNTIME)
- @Documented
- @Import(DataSourceConfigRegister.class)
- public @interface AppDataSource {
- SupportDatasourceEnum[] datasourceType();
- }
這里為了方便,我將測(cè)試中使用的數(shù)據(jù)源地址都配置在來enum里面,如果后邊需要靈活處理的話,可以將這些配置信息抽取出來放在一些配置中心上邊。
- package mutidatasource.enums;
- import lombok.AllArgsConstructor;
- import lombok.Getter;
- import lombok.NoArgsConstructor;
- /**
- * 目前支持的數(shù)據(jù)源信息
- *
- * @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();
- }
- }
之所以要?jiǎng)?chuàng)建這個(gè)@AppDataSource注解,是要在springboot的啟動(dòng)類上邊進(jìn)行標(biāo)注:
- 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 自定義一個(gè)注冊(cè)器來獲取啟動(dòng)類頭部的注解所指定的數(shù)據(jù)源類型:
- 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];
- }
- }
好的,現(xiàn)在我們已經(jīng)能夠獲取到對(duì)應(yīng)的數(shù)據(jù)源類型信息了,這里你會(huì)看到一個(gè)叫做DataSourceContextHolder的角色。這個(gè)對(duì)象主要是用于對(duì)每個(gè)請(qǐng)求線程的數(shù)據(jù)源信息做統(tǒng)一的分配和管理。
在多并發(fā)場(chǎng)景下,為了防止不同線程請(qǐng)求的數(shù)據(jù)源出現(xiàn)“互竄”情況,通常我們都會(huì)使用到threadlocal來做處理。為每一個(gè)線程都分配一個(gè)指定的,屬于其內(nèi)部的副本變量,當(dāng)當(dāng)前線程結(jié)束之前,記得將對(duì)應(yīng)的線程副本也進(jìn)行銷毀。
- 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());
- }
- /**
- * 取得當(dāng)前數(shù)據(jù)源
- *
- * @return
- */
- public static String getDatabaseHolder() {
- return databaseHolder.get();
- }
- /**
- * 添加數(shù)據(jù)源
- *
- * @param supportDatasourceEnum
- */
- public static void addDatasource(SupportDatasourceEnum supportDatasourceEnum) {
- dataSourceSet.add(supportDatasourceEnum);
- }
- /**
- * 獲取當(dāng)期應(yīng)用所支持的所有數(shù)據(jù)源
- *
- * @return
- */
- public static HashSet<SupportDatasourceEnum> getDataSourceSet() {
- return dataSourceSet;
- }
- /**
- * 清除上下文數(shù)據(jù)
- */
- public static void clear() {
- databaseHolder.remove();
- }
- }
spring內(nèi)部的AbstractRoutingDataSource動(dòng)態(tài)路由數(shù)據(jù)源里面有一個(gè)抽象方法叫做
determineCurrentLookupKey,這個(gè)方法適用于提供給開發(fā)者自定義對(duì)應(yīng)數(shù)據(jù)源的查詢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數(shù)據(jù)源,所以配置數(shù)據(jù)源的配置類如下:這里面我默認(rèn)該應(yīng)用配置類PROD數(shù)據(jù)源,用于測(cè)試使用。
- 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();
- //設(shè)置原始數(shù)據(jù)源
- 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);
- //間隔多久才進(jìn)行一次檢測(cè),檢測(cè)需要關(guān)閉的空閑連接,單位是毫秒
- druidDataSource.setTimeBetweenConnectErrorMillis(60000);
- return druidDataSource;
- }
- }
好了現(xiàn)在一個(gè)基礎(chǔ)的數(shù)據(jù)源注入已經(jīng)可以了,那么我們?cè)撊绾谓柚⒔鈦韺?shí)現(xiàn)動(dòng)態(tài)切換數(shù)據(jù)源的操作呢?
為此,我設(shè)計(jì)了一個(gè)叫做UsingDataSource的注解,通過利用該注解來識(shí)別當(dāng)前線程所需要使用的數(shù)據(jù)源操作:
- 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();
- }
- }
測(cè)試類如下所示:
- 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()));
- }
- }
最后 啟動(dòng)springboot服務(wù),通過使用注解即可測(cè)試對(duì)應(yīng)功能。
關(guān)于AbstractRoutingDataSource 動(dòng)態(tài)路由數(shù)據(jù)源的注入原理,
可以看到這個(gè)內(nèi)部類里面包含了多種用于做數(shù)據(jù)源映射的map數(shù)據(jù)結(jié)構(gòu)。
在該類的最底部,有一個(gè)determineCurrentLookupKey函數(shù),也就是上邊我們所提及的使用于查詢當(dāng)前數(shù)據(jù)源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");
- //這里面注入我們當(dāng)前線程使用的數(shù)據(jù)源
- Object lookupKey = determineCurrentLookupKey();
- //在初始化數(shù)據(jù)源的時(shí)候需要我們?nèi)ソoresolvedDataSources進(jìn)行注入
- 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里面,又有對(duì)于初始化數(shù)據(jù)源的注入操作,這里面的targetDataSources 正是上文中我們對(duì)在初始化數(shù)據(jù)源時(shí)候注入的信息。
- @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);
- }
- }