實(shí)現(xiàn)效果
先說(shuō)效果,要實(shí)現(xiàn)方法級(jí)別注解切換當(dāng)前數(shù)據(jù)源,不設(shè)置注解時(shí)走默認(rèn)數(shù)據(jù)源,同時(shí)支持JNDI源。
總體思路
Spring框架中存在一個(gè)抽象類(lèi)AbstractRoutingDataSource
,他是一個(gè)可以動(dòng)態(tài)選擇當(dāng)前DataSource的路由類(lèi),我們就是要從這里入手,重新實(shí)現(xiàn)數(shù)據(jù)源的切換選擇邏輯。然后借助注解和切面,將當(dāng)前需要的數(shù)據(jù)源名稱(chēng)放在ThreadLocal中,需要時(shí)從當(dāng)前線程取得即可完成數(shù)據(jù)源的切換。
注解部分比較簡(jiǎn)單不再詳說(shuō),看AbstractRoutingDataSource
。該類(lèi)文檔寫(xiě)的非常全面,自行翻譯一下就可以看懂。主要看其中的幾個(gè)關(guān)鍵方法。
setTargetDataSources
類(lèi)中存在一個(gè)成員變量targetDataSources
,結(jié)合之后的setTargetDataSources
方法可知,這里用來(lái)保存目標(biāo)數(shù)據(jù)源。
根據(jù)注釋我們可以知道,targetDataSources
的key可以是數(shù)據(jù)源的名字,value是相應(yīng)數(shù)據(jù)源的實(shí)例。
當(dāng)然這里也可是使用其他的保存方式,然后自行改寫(xiě)用來(lái)查找數(shù)據(jù)源的determineCurrentLookupKey
方法,默認(rèn)場(chǎng)景就足夠我們使用了。所以我們要構(gòu)建一個(gè)Map出來(lái),其中key用來(lái)區(qū)分?jǐn)?shù)據(jù)源的名字,value放入對(duì)應(yīng)數(shù)據(jù)源的實(shí)例,有幾個(gè)數(shù)據(jù)源就放幾個(gè)進(jìn)去。
@Nullable
private Map<Object, Object> targetDataSources;
/**
* Specify the map of target DataSources, with the lookup key as key.
* The mapped value can either be a corresponding {@link javax.sql.DataSource}
* instance or a data source name String (to be resolved via a
* {@link #setDataSourceLookup DataSourceLookup}).
* <p>The key can be of arbitrary type; this class implements the
* generic lookup process only. The concrete key representation will
* be handled by {@link #resolveSpecifiedLookupKey(Object)} and
* {@link #determineCurrentLookupKey()}.
*/
public void setTargetDataSources(Map<Object, Object> targetDataSources) {
this.targetDataSources = targetDataSources;
}
setDefaultTargetDataSource
上面說(shuō)了如何設(shè)置當(dāng)前數(shù)據(jù)源,那如果在開(kāi)發(fā)的時(shí)候每一個(gè)方法都要聲明一下使用哪個(gè)源就太麻煩了,所以Spring提供了一個(gè)方法用來(lái)設(shè)置默認(rèn)的數(shù)據(jù)源,沒(méi)啥可說(shuō)的,傳入DataSource
實(shí)例就好了。
@Nullable
private Object defaultTargetDataSource;
/**
* Specify the default target DataSource, if any.
* <p>The mapped value can either be a corresponding {@link javax.sql.DataSource}
* instance or a data source name String (to be resolved via a
* {@link #setDataSourceLookup DataSourceLookup}).
* <p>This DataSource will be used as target if none of the keyed
* {@link #setTargetDataSources targetDataSources} match the
* {@link #determineCurrentLookupKey()} current lookup key.
*/
public void setDefaultTargetDataSource(Object defaultTargetDataSource) {
this.defaultTargetDataSource = defaultTargetDataSource;
}
determineCurrentLookupKey
在設(shè)置好數(shù)據(jù)源之后,接下來(lái)這幾個(gè)尋路方法則是能實(shí)現(xiàn)動(dòng)態(tài)數(shù)據(jù)源切換的重點(diǎn)。afterPropertiesSet
方法對(duì)我們以配置的數(shù)據(jù)源進(jìn)行校驗(yàn);如果我們?cè)诘谝徊脚渲脭?shù)據(jù)源map的時(shí)候?qū)ey有特殊處理則要自己實(shí)現(xiàn)抽象方法resolveSpecifiedLookupKey
,告訴Spring應(yīng)該怎么解析這個(gè)key值;determineTargetDataSource
則最終確定要使用哪一個(gè)數(shù)據(jù)源,其中有一個(gè)方法determineCurrentLookupKey
需要關(guān)注,這個(gè)方法會(huì)返回當(dāng)前要使用的數(shù)據(jù)源名字,但他是個(gè)抽象方法,所以我們需要給他重寫(xiě)一下,改為從當(dāng)前線程獲取數(shù)據(jù)源名稱(chēng)。
@Override
public void afterPropertiesSet() {
if (this.targetDataSources == null) {
throw new IllegalArgumentException("Property 'targetDataSources' is required");
}
this.resolvedDataSources = CollectionUtils.newHashMap(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);
}
}
@Override
public Connection getConnection(String username, String password) throws SQLException {
return determineTargetDataSource().getConnection(username, password);
}
/**
* 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();
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();
代碼實(shí)現(xiàn)
思路理順了,代碼寫(xiě)起來(lái)就比較快,直接貼最后代碼,部分地方保留了注釋。
數(shù)據(jù)源切換注解
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 數(shù)據(jù)源切換注解,默認(rèn)為primary
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface TargetDataSource {
DataSourceEnum value() default DataSourceEnum.PRIMARY;
}
數(shù)據(jù)源切換切面
這里需要特別提醒一下,事務(wù)注解@Transactional
默認(rèn)處于切面代理的最后一個(gè),所以我們需要保證數(shù)據(jù)源切換注解優(yōu)先級(jí)要高于事務(wù)注解。文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-481399.html
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
/**
* 數(shù)據(jù)源切換切面
*/
@Aspect
@Component
@Order(1)
public class DynamicDataSourceAspect {
private final static Logger log = LoggerFactory.getLogger(DynamicDataSourceAspect.class);
@Before(value = "@annotation(targetDataSource)")
public void beforePointCut(TargetDataSource targetDataSource) {
log.debug("數(shù)據(jù)源切換為 " + targetDataSource.value().getDataSourceName());
DynamicDataSourceContextHolder.setDataSource(targetDataSource.value().getDataSourceName());
}
@After(value = "@annotation(targetDataSource)")
public void afterPointCut(TargetDataSource targetDataSource) {
log.debug("數(shù)據(jù)源恢復(fù)為 " + DataSourceEnum.PRIMARY.getDataSourceName());
DynamicDataSourceContextHolder.clearDataSource();
}
}
數(shù)據(jù)源枚舉類(lèi)
/**
* 數(shù)據(jù)源枚舉類(lèi)
*/
public enum DataSourceEnum {
PRIMARY("primary"), SECONDARY("secondary");
private final String dataSourceName;
public String getDataSourceName() {
return dataSourceName;
}
DataSourceEnum(String dataSourceName) {
this.dataSourceName = dataSourceName;
}
}
數(shù)據(jù)源上下文保持類(lèi)
/**
* 數(shù)據(jù)源上下文線程持有類(lèi)
*/
public class DynamicDataSourceContextHolder {
/**
* 存放當(dāng)前線程使用的數(shù)據(jù)源類(lèi)型信息
*/
private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();
public static void setDataSource(String dataSourceType) {
CONTEXT_HOLDER.set(dataSourceType);
}
public static String getDataSource() {
return CONTEXT_HOLDER.get();
}
public static void clearDataSource() {
CONTEXT_HOLDER.remove();
}
}
AbstractRoutingDataSource自定義實(shí)現(xiàn)
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import javax.sql.DataSource;
import java.util.Map;
/**
* 動(dòng)態(tài)數(shù)據(jù)源切換類(lèi)
*
* @author liuenqi
*/
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DynamicDataSourceContextHolder.getDataSource();
}
public DynamicDataSource(DataSource defaultTargetDataSource, Map<Object, Object> targetDataSources) {
// 默認(rèn)數(shù)據(jù)源
super.setDefaultTargetDataSource(defaultTargetDataSource);
// 所有目標(biāo)數(shù)據(jù)源
super.setTargetDataSources(targetDataSources);
// 后處理
super.afterPropertiesSet();
}
}
數(shù)據(jù)源注冊(cè)
注意使用jndi源的時(shí)候需要加一個(gè)特定前綴。文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-481399.html
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.config.ConstructorArgumentValues;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.GenericBeanDefinition;
import org.springframework.context.EnvironmentAware;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.env.Environment;
import org.springframework.core.type.AnnotationMetadata;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
/**
* 多數(shù)據(jù)源注冊(cè)類(lèi)
*/
public class DynamicDataSourceRegister implements ImportBeanDefinitionRegistrar, EnvironmentAware {
private DataSource primaryDataSource;
private DataSource secondaryDataSource;
@Override
public void setEnvironment(Environment environment) {
initPrimaryDataSource(environment);
initSecondaryDataSource(environment);
}
/**
* 組裝主數(shù)據(jù)源參數(shù),兼容jdbc-url與jndi
*
* @param env Environment
*/
private void initPrimaryDataSource(Environment env) {
Map<String, String> paramMap = new HashMap<>(4);
if (StringUtils.isNotBlank(env.getProperty("spring.datasource.primary.url"))) {
paramMap.put("url", env.getProperty("spring.datasource.primary.url"));
paramMap.put("userName", env.getProperty("spring.datasource.primary.username"));
paramMap.put("password", env.getProperty("spring.datasource.primary.password"));
paramMap.put("driverClassName", env.getProperty("spring.datasource.primary.driver-class-name"));
} else {
paramMap.put("jndi", env.getProperty("spring.datasource.primary.jndi-name"));
}
primaryDataSource = buildDataSource(paramMap);
}
/**
* 組裝輔數(shù)據(jù)源參數(shù),兼容jdbc-url與jndi
*
* @param env Environment
*/
private void initSecondaryDataSource(Environment env) {
if (StringUtils.isNotBlank(env.getProperty("spring.datasource.secondary.url"))) {
Map<String, String> paramMap = new HashMap<>(4);
paramMap.put("url", env.getProperty("spring.datasource.secondary.url"));
paramMap.put("userName", env.getProperty("spring.datasource.secondary.username"));
paramMap.put("password", env.getProperty("spring.datasource.secondary.password"));
paramMap.put("driverClassName", env.getProperty("spring.datasource.secondary.driver-class-name"));
secondaryDataSource = buildDataSource(paramMap);
} else if (StringUtils.isNotBlank(env.getProperty("spring.datasource.secondary.jndi-name"))) {
Map<String, String> paramMap = new HashMap<>(2);
paramMap.put("jndi", env.getProperty("spring.datasource.secondary.jndi-name"));
secondaryDataSource = buildDataSource(paramMap);
}
}
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
Map<Object, Object> targetDataSource = new HashMap<>(2);
targetDataSource.put("primary", primaryDataSource);
if (Objects.nonNull(secondaryDataSource)) {
targetDataSource.put("secondary", secondaryDataSource);
}
// 為DynamicDataSource構(gòu)造參數(shù),注意參數(shù)順序
ConstructorArgumentValues constructorArgumentValues = new ConstructorArgumentValues();
constructorArgumentValues.addGenericArgumentValue(primaryDataSource);
constructorArgumentValues.addGenericArgumentValue(targetDataSource);
// 構(gòu)造bean放入IOC
GenericBeanDefinition beanDefinition = new GenericBeanDefinition();
beanDefinition.setBeanClass(DynamicDataSource.class);
beanDefinition.setConstructorArgumentValues(constructorArgumentValues);
beanDefinition.setSynthetic(true);
registry.registerBeanDefinition("dataSource", beanDefinition);
}
/**
* 使用HikariDataSource
*
* @param paramMap {"url":"JDBC-URL","userName":"數(shù)據(jù)庫(kù)用戶(hù)名","password":"密碼","driverClassName":"驅(qū)動(dòng)名","jndi":"jndi源"}
* @return HikariDataSource
*/
private DataSource buildDataSource(Map<String, String> paramMap) {
HikariConfig hikariConfig = new HikariConfig();
if (paramMap.containsKey("url")) {
hikariConfig.setJdbcUrl(paramMap.get("url"));
hikariConfig.setUsername(paramMap.get("userName"));
hikariConfig.setPassword(paramMap.get("password"));
hikariConfig.setDriverClassName(paramMap.get("driverClassName"));
} else {
hikariConfig.setDataSourceJNDI("java:comp/env/" + paramMap.get("jndi"));
}
return new HikariDataSource(hikariConfig);
}
}
啟動(dòng)類(lèi)配置
@Import({DynamicDataSourceRegister.class})
application.yml配置
spring:
datasource:
primary:
url: jdbc:mysql://xxxxx
username: xxxx
password: xxxxx
driver-class-name: com.mysql.cj.jdbc.Driver
secondary:
jndi-name: jdbc/db
到了這里,關(guān)于基于注解切換、Hikari實(shí)現(xiàn)的SpringBoot動(dòng)態(tài)數(shù)據(jù)源(支持JNDI)的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!