1. 程式人生 > >SSM動態切換資料來源

SSM動態切換資料來源

****
> 有需求就要想辦法解決,最近參與的專案其涉及的三個資料表分別在三臺不同的伺服器上,這就有點突兀了,第一次遇到這種情況,可這難不倒筆者,資料一查,程式碼一打,回頭看看原始碼,萬事大吉
## 1. 預備知識 這裡預設大家都會SSM框架了,使用時我們要往sqlSessionFactory裡注入資料來源。那麼猜測:1、可以往sqlSessionFactory裡注入多資料來源來實現切換;2、將多個數據源封裝成一個`總源`,再把這個`總源`注入到sqlSessionFactory裡實現切換。答案是使用後者,即封裝成`總源`的形式。Spring提供了動態切換資料來源的功能,那麼我們來看看其實現原理



## 2. 實現原理 筆者是根據原始碼講解的,這些步驟講完會貼出原始碼內容
### 一、 Spring提供了AbstractRoutingDataSource抽象類,其繼承了AbstractDataSource。而AbstractDataSource又實現了DataSource。因此我們可以將AbstractRoutingDataSource的實現類注入到sqlSessionFactory中來實現切換資料來源
### 二、 剛才我們將多個數據源封裝成`總源`的想法在AbstractRoutingDataSource中有體現,其內部用一個Map集合封裝多個數據源,即 `private Map resolvedDataSources;` ,那麼要使用時從該Map集合中獲取即可
### 三、 AbstractRoutingDataSource中有個determineTargetDataSource()方法,其作用是決定使用哪個資料來源。我們通過determineTargetDataSource()方法從Map集合中獲取資料來源,那麼必須有個key值指定才行。所以determineTargetDataSource()方法內部通過呼叫determineCurrentLookupKey()方法來獲取key值,Spring將determineCurrentLookupKey()方法抽象出來給使用者實現,從而讓使用者決定使用哪個資料來源
### 四、 既然知道我們需要重寫determineCurrentLookupKey()方法,那麼就開始把。實現時發現該方法沒有引數,我們無法傳參來決定返回的key值,又不能改動方法(因為是重寫),所以方法內部呼叫我們自定義類的靜態方法即可解決問題 ```java public class DynamicDataSource extends AbstractRoutingDataSource { @Override protected Object determineCurrentLookupKey() { return DynamicDataSourceHolder.getDataSourceKey(); } } ```
### 五、 自定義類,作用是讓我們傳入key值來決定使用哪個key ```java public class DynamicDataSourceHolder { // ThreadLocal沒什麼好說的,綁定當前執行緒 private static final ThreadLocal dataSourceKey = new ThreadLocal(); public static String getDataSourceKey(){ return dataSourceKey.get(); } public static void setDataSourceKey(String key){ dataSourceKey.set(key); } public static void clearDataSourceKey(){ dataSourceKey.remove(); } } ```
### 六、 AbstractRoutingDataSource抽象類原始碼(**不喜可跳**) ```java public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean { @Nullable private Map targetDataSources; @Nullable private Object defaultTargetDataSource; private boolean lenientFallback = true; private DataSourceLookup dataSourceLookup = new JndiDataSourceLookup(); @Nullable private Map resolvedDataSources; @Nullable private DataSource resolvedDefaultDataSource; public AbstractRoutingDataSource() { } public void setTargetDataSources(Map targetDataSources) { this.targetDataSources = targetDataSources; } public void setDefaultTargetDataSource(Object defaultTargetDataSource) { this.defaultTargetDataSource = defaultTargetDataSource; } public void setLenientFallback(boolean lenientFallback) { this.lenientFallback = lenientFallback; } public void setDataSourceLookup(@Nullable DataSourceLookup dataSourceLookup) { this.dataSourceLookup = (DataSourceLookup)(dataSourceLookup != null ? dataSourceLookup : new JndiDataSourceLookup()); } public void afterPropertiesSet() { if (this.targetDataSources == null) { throw new IllegalArgumentException("Property 'targetDataSources' is required"); } else { this.resolvedDataSources = new HashMap(this.targetDataSources.size()); this.targetDataSources.forEach((key, value) -> { Object lookupKey = this.resolveSpecifiedLookupKey(key); DataSource dataSource = this.resolveSpecifiedDataSource(value); this.resolvedDataSources.put(lookupKey, dataSource); }); if (this.defaultTargetDataSource != null) { this.resolvedDefaultDataSource = this.resolveSpecifiedDataSource(this.defaultTargetDataSource); } } } protected Object resolveSpecifiedLookupKey(Object lookupKey) { return lookupKey; } protected DataSource resolveSpecifiedDataSource(Object dataSource) throws IllegalArgumentException { if (dataSource instanceof DataSource) { return (DataSource)dataSource; } else if (dataSource instanceof String) { return this.dataSourceLookup.getDataSource((String)dataSource); } else { throw new IllegalArgumentException("Illegal data source value - only [javax.sql.DataSource] and String supported: " + dataSource); } } public Connection getConnection() throws SQLException { return this.determineTargetDataSource().getConnection(); } public Connection getConnection(String username, String password) throws SQLException { return this.determineTargetDataSource().getConnection(username, password); } public T unwrap(Class iface) throws SQLException { return iface.isInstance(this) ? this : this.determineTargetDataSource().unwrap(iface); } public boolean isWrapperFor(Class iface) throws SQLException { return iface.isInstance(this) || this.determineTargetDataSource().isWrapperFor(iface); } protected DataSource determineTargetDataSource() { Assert.notNull(this.resolvedDataSources, "DataSource router not initialized"); Object lookupKey = this.determineCurrentLookupKey(); DataSource 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 + "]"); } else { return dataSource; } } @Nullable protected abstract Object determineCurrentLookupKey(); } ```



## 3. 配置
### 3.1 配置db.properties 這裡配置兩個資料庫,一個評論庫,一個使用者庫 ```properties # 問題庫 howl.comments.driverClassName = com.mysql.jdbc.Driver howl.comments.url = jdbc:mysql://127.0.0.1:3306/comment howl.comments.username = root howl.comments.password = # 使用者庫 howl.users.driverClassName = com.mysql.jdbc.Driver howl.users.url = jdbc:mysql://127.0.0.1:3306/user howl.users.username = root howl.users.password = ```
### 3.2 配置applicationContext.xml ```xml
``` 因為dynamicDataSource是繼承AbstractRoutingDataSource,所以setter注入方法得去父類裡面去找,開始筆者也是懵了一下
### 3.3 切換資料來源 資料來源是在Service層切換的
**UserService** ```java @Service public class UserService { @Autowired private UserDao userDao; public User selectUserById(int id) { // 表明使用usersDataSource庫 DynamicDataSourceHolder.setDataSourceKey("uds"); return userDao.selectUserById(id); } } ```
**CommentService** ```java @Service public class CommentService { @Autowired CommentDao commentDao; public List selectCommentById(int blogId) { // 表明使用評論庫 DynamicDataSourceHolder.setDataSourceKey("cds"); return commentDao.selectCommentById(blogId, -1); } } ```
### 3.4 自動切換 手動切換容易忘記,我們學了AOP可以使用AOP來切換,這裡使用註解實現
```xml ```
**切面類** ```java @Component @Aspect public class DataSourceAspect { @Pointcut("execution(* com.howl.service.impl.*(..))") private void pt1() { } @Around("pt1()") public Object around(ProceedingJoinPoint pjp) { Object rtValue = null; try { String name = pjp.getTarget().getClass().getName(); if (name.equals("com.howl.service.UserService")) { DynamicDataSourceHolder.setDataSourceKey("uds"); } if (name.equals("com.howl.service.CommentService")){ DynamicDataSourceHolder.setDataSourceKey("cds"); } // 呼叫業務層方法 rtValue = pjp.proceed(); System.out.println("後置通知"); } catch (Throwable t) { System.out.println("異常通知"); t.printStackTrace(); } finally { System.out.println("最終通知"); } return rtValue; } } ``` 使用環繞通知實現切入com.howl.service.impl裡的所有方法,在遇到UserService、CommentService時,前置通知動態切換對應的資料來源



## 4. 總結 1. 以前筆者認為Service層多了impl包和介面是多餘的,現在要用到AOP的時候後悔莫及,所以預設結構如此肯定有道理的 2. 出bug的時候,才知道分步測試哪裡出問題了,如果TDD推動那麼能快速定位報錯地方,日誌也很重要
------ 參考
<