來(lái)源:Java知音 針對(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ù)層需要涉及到查詢(xún)多種同數(shù)據(jù)庫(kù)的場(chǎng)景下,我們通常需要在執(zhí)行sql的時(shí)候動(dòng)態(tài)指定對(duì)應(yīng)的datasource。 而Spring的AbstractRoutingDataSource則正好為我們提供了這一功能點(diǎn),下邊我將通過(guò)一個(gè)簡(jiǎn)單的基于springboot+aop的案例來(lái)實(shí)現(xiàn)如何通過(guò)自定義注解切換不同的數(shù)據(jù)源進(jìn)行讀數(shù)據(jù)操作,同時(shí)也將結(jié)合部分源碼的內(nèi)容進(jìn)行講解。 首先我們需要自定義一個(gè)專(zhuān)門(mén)用于申明當(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ù)源地址都配置在來(lái)enum里面,如果后邊需要靈活處理的話(huà),可以將這些配置信息抽取出來(lái)放在一些配置中心上邊。 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)類(lèi)上邊進(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è)器來(lái)獲取啟動(dòng)類(lèi)頭部的注解所指定的數(shù)據(jù)源類(lèi)型: 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ù)源類(lèi)型信息了,這里你會(huì)看到一個(gè)叫做DataSourceContextHolder的角色。這個(gè)對(duì)象主要是用于對(duì)每個(gè)請(qǐng)求線(xiàn)程的數(shù)據(jù)源信息做統(tǒng)一的分配和管理。 在多并發(fā)場(chǎng)景下,為了防止不同線(xiàn)程請(qǐng)求的數(shù)據(jù)源出現(xiàn)“互竄”情況,通常我們都會(huì)使用到threadlocal來(lái)做處理。為每一個(gè)線(xiàn)程都分配一個(gè)指定的,屬于其內(nèi)部的副本變量,當(dāng)當(dāng)前線(xiàn)程結(jié)束之前,記得將對(duì)應(yīng)的線(xiàn)程副本也進(jìn)行銷(xiāo)毀。 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è)方法適用于提供給開(kāi)發(fā)者自定義對(duì)應(yīng)數(shù)據(jù)源的查詢(xún)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ù)源的配置類(lèi)如下:這里面我默認(rèn)該應(yīng)用配置類(lèi)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è)撊绾谓柚⒔鈦?lái)實(shí)現(xiàn)動(dòng)態(tài)切換數(shù)據(jù)源的操作呢? 為此,我設(shè)計(jì)了一個(gè)叫做UsingDataSource的注解,通過(guò)利用該注解來(lái)識(shí)別當(dāng)前線(xiàn)程所需要使用的數(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來(lái)做切面攔截: 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è)試類(lèi)如下所示: 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ù),通過(guò)使用注解即可測(cè)試對(duì)應(yīng)功能。 關(guān)于AbstractRoutingDataSource 動(dòng)態(tài)路由數(shù)據(jù)源的注入原理, 可以看到這個(gè)內(nèi)部類(lèi)里面包含了多種用于做數(shù)據(jù)源映射的map數(shù)據(jù)結(jié)構(gòu)。  在該類(lèi)的最底部,有一個(gè)determineCurrentLookupKey函數(shù),也就是上邊我們所提及的使用于查詢(xún)當(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)前線(xiàn)程使用的數(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();
而在該類(lèi)的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); } }
END
|