隨著業務變遷/需求變更,JavaEE 應用中會被迫連接多個數據源進行業務處理。 怎樣在不影響原有項目結構的情況下,已最優雅/最簡潔的方式動態切換數據源呢? 本文已一次添加數據源後動態切換實踐為例,描述整個思考和實踐過程,文中如有紕漏,還望指正。 1. 依賴 Spring 動態數據源實現 Spring ...
隨著業務變遷/需求變更,JavaEE 應用中會被迫連接多個數據源進行業務處理。
怎樣在不影響原有項目結構的情況下,已最優雅/最簡潔的方式動態切換數據源呢?
本文已一次添加數據源後動態切換實踐為例,描述整個思考和實踐過程,文中如有紕漏,還望指正。
1. 依賴 Spring 動態數據源實現
Spring 中提供了一個叫做 AbstractRoutingDataSource 抽象路由數據源對象繼承自 AbstractDataSource 並實現了 JDK 中 DataSource 介面。
也就意味著繼承 AbstractRoutingDataSource 並重寫它 determineCurrentLookupKey 方法的類可以作為數據源,並個性化多數據源動態路由切換。
(如果你平時夠仔細的話,現開源的資料庫連接池都實現 DataSource 介面併進行了自己的個性化封裝。)
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DbContextHolder.getDbType();
}
}
對於一次 web 請求來說可以理解為單獨的線程,將當前數據源暫存線上程當中是比較合理的做法。
public class DbContextHolder { private static final ThreadLocal contextHolder = new ThreadLocal<>(); /** * 設置數據源 * * @param dbSourceEnum 要設置的資料庫枚舉名稱 */ public static void setDbType(DBSourceEnum dbSourceEnum) { contextHolder.set(dbSourceEnum.getValue()); } /** * 取得當前數據源 */ public static String getDbType() { return String.valueOf(contextHolder.get()); } /** * 清除上下文數據 */ public static void clearDbType() { contextHolder.remove(); } }
當然為了後期的擴展和維護,以及使用的便捷性,這裡數據源對象我們引入枚舉類型。
這樣後續其他同事編程使用枚舉,改動起來也相當方便,還能進行二次數據源的一些自定義標示。
public enum DBSourceEnum {
one("dataSource1"),
two("dataSource2");
private String value;
DBSourceEnum(String value) {
this.value = value;
}
public String getValue() {
return value;
}
}
上述的 dataSource1/dataSource2 即為 spring-context 中已載入的數據源對象 Id。
<bean name="dataSource1" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
......
</bean>
<bean name="dataSource2" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
......
</bean>
接下來在 context 中配置繼承自 AbstractRoutingDataSource 的 DynamicDataSource。
<bean id="dataSource" class="com.rambo.spm.core.multidb.DynamicDataSource">
<property name="targetDataSources">
<map key-type="java.lang.String">
<entry key="dataSource1" value-ref="dataSource1"/>
<entry key="dataSource2" value-ref="dataSource2"/>
</map>
</property>
<property name="defaultTargetDataSource" ref="dataSource1"/>
</bean>
Ok,這樣在配置後續 dao 層時使用該 DynamicDataSource 即可。
2. 最優雅的切換數據源方式
完成上述工作之後,其實動態切換數據源已經實現,在業務層如下麵這樣編程。
DbContextHolder.setDbType(DBSourceEnum.one);
List<Menu> menuList = menuService.selectList(null);
DbContextHolder.setDbType(DBSourceEnum.two);
List<User> userList = userService.selectList(null);
缺點很明顯,連接數據源2時要進行切換/不利於擴展/切換不當時給後人埋雷的幾率很大。
和團隊進行交流時,討論出用強大 aop 來攔截 dao 層對象,動態切換數據源的方案。
對於 dao 層對象來說訪問資料庫的哪張表是確定的,編寫自定義註解與 dao 層對象進行綁定。
自定義數據源註解如下:
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface DataSource {
DBSourceEnum value() default DBSourceEnum.one;
}
編寫切麵處理對象,在 dao 層對象使用前進行攔截,順手切換數據源,如果沒有數據源註解,設置為預設。
所以對於項目中原配數據源 dao 層對象,不需要進行任何修改,切麵處理如下。
@Before("cut()")
public void doBefore(JoinPoint joinPoint) {
DataSource dataSource = joinPoint.getTarget().getClass().getAnnotation(DataSource.class);
DbContextHolder.setDbType(dataSource != null ? dataSource.value() : DBSourceEnum.one);
log.info("當前數據源為:" + DbContextHolder.getDbType());
}
多數項目中 dao 層錯綜複雜的抽象和繼承關係會給你 aop 切麵攔截造成一定的困難,多思考、多實踐總會有辦法的。
好了,就這樣吧,是不是感覺比 aop 攔截方式比在程式中硬編碼更容易擴展、更容易編程、更容易理解,當然也更優雅。
代碼已托管在:https://git.oschina.net/LanboEx/spmvc-mybatis.git 有興趣的朋友,可以檢到本地 run 一下。