Spring多資料來源管理實現原理
[TOC]
應用場景:
大部分單一架構專案連線一臺資料庫伺服器,但隨著業務的增加資料庫資料量不斷飆升,資料庫達到效能瓶頸,大部分技術人員都會對資料庫主從配置;既然讀寫分離那就需要連線兩個不同的資料庫,這時候Spring多資料來源管理類AbstractRoutingDataSource就要派上用場了(排除使用資料庫叢集管理工具統一管理的應用場景)
原始碼分析:
public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {
private Map<Object, Object> targetDataSources;
private Object defaultTargetDataSource;
private boolean lenientFallback = true;
private DataSourceLookup dataSourceLookup = new JndiDataSourceLookup();
private Map<Object, DataSource> resolvedDataSources;
private DataSource resolvedDefaultDataSource;
...
}
複製程式碼
通過原始碼可以看出該類是一個抽象類,定義了6個屬性。 targetDataSources:是一個map型別該屬性正是用來維護專案中多個資料來源 defaultTargetDataSource:通過屬性名很直觀的可以理解它的作用(預設資料來源) lenientFallback:預設為true,無需改動 dataSourceLookup:查詢資料來源介面的名稱 resolvedDataSources:如果該欄位沒有賦值,就是targetDataSources resolvedDefaultDataSource:改變後的資料來源
public interface DataSourceLookup {
/**
* Retrieve the DataSource identified by the given name.
* @param dataSourceName the name of the DataSource
* @return the DataSource (never {@code null})
* @throws DataSourceLookupFailureException if the lookup failed
*/
DataSource getDataSource(String dataSourceName) throws DataSourceLookupFailureException;
}
複製程式碼
該類是一個interface並且只有一個方法getDataSource,通過方法的引數名稱應該清楚傳入一個字元型別的資料來源名稱獲取DataSource
深入理解:
使用資料來源的目的就是要獲取Connection,接下來就從AbstractRoutingDataSource的getConnection方法一探究竟。
@Override
public Connection getConnection() throws SQLException {
return determineTargetDataSource().getConnection();
}
@Override
public Connection getConnection(String username, String password) throws SQLException {
return determineTargetDataSource().getConnection(username, password);
}
複製程式碼
直接進入determineTargetDataSource方法
/**
* 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");
//該方法是一個抽象方法,返回要從resolvedDataSources查詢key,該方法還會實現檢查執行緒繫結事務上下文。
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;
}
複製程式碼
###程式碼實現
實現AbstractRoutingDataSource重寫determineCurrentLookupKey
/**
* @author yangzhao
* Created by 17/2/7.
*/
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
String dataSourceName = DataSourceContextHolder.getDataSourceName();
return dataSourceName;
}
}
複製程式碼
定義DataSourceContextHolder
/**
* 該類內部維護了{@link ThreadLocal}
* @author yangzhao
* Created by 17/2/7.
*/
public class DataSourceContextHolder {
private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>();
/**
* @Description: 設定資料來源型別
* @param dataSourceName 資料來源名稱
* @return void
* @throws
*/
public static void setDataSourceName(String dataSourceName) {contextHolder.set(dataSourceName);}
/**
* @Description: 獲取資料來源名稱
* @param
* @return String
* @throws
*/
public static String getDataSourceName() {
return contextHolder.get();
}
/**
* @Description: 清除資料來源名稱
* @param
* @return void
* @throws
*/
public static void clearDataSource() {
contextHolder.remove();
}
}
複製程式碼
通過ThreadLocal類使每個執行緒獲取獨立的資料來源,防止併發訪問時獲取錯誤的資料來源
基於SpringAop實現資料來源動態切換
註解類DataSource
/**
* 資料來源
* Created by yangzhao on 17/2/7.
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSource {
String value() default "defaultSource";
}
複製程式碼
增強類(DataSouceAdvisor)
/**
* 增強類
* 實現MethodInterceptor介面,通過反射動態解析方法是否標註@DataSource {@link DataSource}註解。
* 如果已標註@DataSource註解取值,set到{@link DataSourceContextHolder}
* @author yangzhao
* create by 17/10/20
*/
@Component("dataSourceAdvisor")
public class DataSouceAdvisor implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation methodInvocation) throws Throwable {
Method method = methodInvocation.getMethod();
Object aThis = methodInvocation.getThis();
//設定預設資料庫
DataSourceContextHolder.setDataSourceName("defaultSource");
DataSource dataSource = aThis.getClass().getAnnotation(DataSource.class);
if (dataSource!=null){
DataSourceContextHolder.setDataSourceName(dataSource.value());
}
dataSource = method.getAnnotation(DataSource.class);
if (dataSource!=null){
DataSourceContextHolder.setDataSourceName(dataSource.value());
}
Object proceed = null;
try {
proceed = methodInvocation.proceed();
}catch (Exception e){
throw e;
}finally {
DataSourceContextHolder.clearDataSource();
}
return proceed;
}
}
複製程式碼
核心管理類(DataSourceManager真正實現切換)
/**
* 資料來源切換管理類
*
* @author yangzhao
* Created by 17/2/7.
*/
@Component
public class DataSourceManager implements BeanFactoryPostProcessor {
private final Logger logger = LogManager.getLogger(DataSourceManager.class);
/**
* 掃描包
* 一般專案都是以com開頭所以這裡預設為com
*/
private String pacakgePath = "com";
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
//getconfigs
List<String> configs = getconfigs().stream().collect(Collectors.toList());
//列印所有生成的expression配置資訊
configs.forEach(s -> logger.info(s));
//設定aop資訊
setAopInfo(configs,beanFactory);
}
/**
* 設定註冊bean動態AOP資訊
* @param configs
* @param beanFactory
*/
private void setAopInfo(List<String> configs, ConfigurableListableBeanFactory beanFactory) {
if (beanFactory instanceof BeanDefinitionRegistry){
BeanDefinitionRegistry beanDefinitionRegistry = (BeanDefinitionRegistry) beanFactory;
for (String config :configs) {
//增強器
RootBeanDefinition advisor = new RootBeanDefinition(DefaultBeanFactoryPointcutAdvisor.class);
advisor.getPropertyValues().addPropertyValue("adviceBeanName",new RuntimeBeanReference("dataSourceAdvisor").getBeanName());
//切點類
RootBeanDefinition pointCut = new RootBeanDefinition(AspectJExpressionPointcut.class);
pointCut.setScope(BeanDefinition.SCOPE_PROTOTYPE);
pointCut.setSynthetic(true);
pointCut.getPropertyValues().addPropertyValue("expression",config);
advisor.getPropertyValues().addPropertyValue("pointcut",pointCut);
//註冊到spring容器
String beanName = BeanDefinitionReaderUtils.generateBeanName(advisor, beanDefinitionRegistry,false);
beanDefinitionRegistry.registerBeanDefinition(beanName,advisor);
}
}
}
public Set<String> getconfigs() {
Set<String> configs = new HashSet<>();
Reflections reflections = new Reflections(new ConfigurationBuilder().addUrls(ClasspathHelper.forPackage(pacakgePath)));
//獲取所有標記@DataSource的類
Set<Class<?>> typesAnnotatedWith = reflections.getTypesAnnotatedWith(DataSource.class);
Iterator<Class<?>> iterator = typesAnnotatedWith.iterator();
while (iterator.hasNext()){
Class<?> next = iterator.next();
//獲取該類所有方法
Method[] declaredMethods = next.getDeclaredMethods();
for (Method method:declaredMethods){
String classAndMethod = method.getDeclaringClass().getCanonicalName()+"."+method.getName();
//生成expression配置
String expression = "execution (* "+classAndMethod+"(..))";
configs.add(expression);
}
}
reflections = new Reflections(new ConfigurationBuilder().setUrls(ClasspathHelper.forPackage(pacakgePath)).setScanners(new MethodAnnotationsScanner()));
//獲取所有類中標記@DataSource的方法
Set<Method> methodsAnnotatedWith = reflections.getMethodsAnnotatedWith(DataSource.class);
Iterator<Method> it = methodsAnnotatedWith.iterator();
while (it.hasNext()){
Method method = it.next();
String classAndMethod = method.getDeclaringClass().getCanonicalName()+"."+method.getName();
//生成expression配置
String expression = "execution (* "+classAndMethod+"(..))";
configs.add(expression);
}
return configs;
}
}
複製程式碼
以上屬於原創文章,轉載請註明作者@怪咖 QQ:208275451