主從資料庫架構下,寫操作走主庫,讀操作走從庫,需透過 AOP 在方法執行前自動切換資料源。
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DBContextHolder.getDataSource(); // 從 ThreadLocal 取得當前資料源 Key
}
}
public abstract class DBContextHolder {
private static final ThreadLocal contextHolder = new ThreadLocal<>();
public enum DbType {
DB_TYPE_RW("dataSourceKeyRW"), // 讀寫/主庫
DB_TYPE_R("dataSourceKeyR"); // 唯讀/從庫
private final String dataSourceKey;
DbType(String key) { this.dataSourceKey = key; }
public String getDataSourceKey() { return dataSourceKey; }
}
public static String getDataSource() {
String ds = contextHolder.get();
return (ds == null) ? DbType.DB_TYPE_RW.getDataSourceKey() : ds; // 預設主庫
}
public static void setDataSource(String key) { contextHolder.set(key); }
public static void clear() { contextHolder.remove(); }
}
public class DataSourceSwitchingAop {
public void switchDataSource(JoinPoint joinPoint) {
Class> targetClass = joinPoint.getTarget().getClass();
String methodName = joinPoint.getSignature().getName();
Object[] args = joinPoint.getArgs();
// 優先級 1:方法級 @DataSourceKey
DataSourceKey dsKey = getAnnotation(targetClass, methodName, DataSourceKey.class, args);
if (dsKey != null) { setDataSource(joinPoint, dsKey.dataSourceKey()); return; }
// 優先級 2:方法級 @ReadOnlyKey
if (getAnnotation(targetClass, methodName, ReadOnlyKey.class, args) != null) {
setDataSource(joinPoint, DbType.DB_TYPE_R.getDataSourceKey()); return;
}
// 優先級 3:類級 @DataSourceKey
DataSourceKey classKey = targetClass.getAnnotation(DataSourceKey.class);
if (classKey != null) { setDataSource(joinPoint, classKey.dataSourceKey()); return; }
// 預設:讀寫主庫
setDataSource(joinPoint, DbType.DB_TYPE_RW.getDataSourceKey());
}
private void setDataSource(JoinPoint jp, String key) {
DBContextHolder.setDataSource(key);
log.debug("{} 配置資料源:{}", jp.getSignature(), key);
}
// 通用獲取方法級註解(支援參數解析)邏輯省略...
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ReadOnlyKey {} // 僅標記唯讀
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface DataSourceKey {
String dataSourceKey() default "dataSource"; // 可指定具體資料源 Key
}
<!-- 事務管理器綁定動態資料源 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dynamicDataSource"/>
</bean>
<!-- AOP 切面:order=0 確保在事務開啟前切換資料源 -->
<bean id="dataSourceSwitchingAop" class="com.xxx.common.framework2x.dao.DataSourceSwitchingAop"/>
<aop:config>
<aop:aspect id="dataSourceSwitching" ref="dataSourceSwitchingAop" order="0">
<aop:pointcut id="dataSourceSwitchingService"
expression="execution(* com.xxx.manager..*.*(..))"/>
<aop:before method="switchDataSource" pointcut-ref="dataSourceSwitchingService"/>
</aop:aspect>
</aop:config>
order="0" 確保資料源切換在 @Transactional 開啟事務之前執行,否則事務綁定錯誤連線@DataSourceKey > 方法 @ReadOnlyKey > 類 @DataSourceKey > 預設主庫@ReadOnlyKey 僅能用於方法;@DataSourceKey 可用於方法或類@DataSourceKey("dataSourceKeyX") 即可@Service
public class OrderService {
@ReadOnlyKey // 走從庫
public Order queryOrder(Long id) { ... }
@DataSourceKey("dataSourceKeyRW") // 顯式指定主庫
public void createOrder(Order order) { ... }
@DataSourceKey("dataSourceKeyReport") // 走報表庫
public Report generateReport() { ... }
}
方案核心:AbstractRoutingDataSource + ThreadLocal 上下文 + AOP 前置切換 + 註解聲明式配置。優點:非侵入式、優先級清晰、支援多資料源、與 Spring 事務無縫整合。