我遇到的問題是這樣的。項目使用 Spring + Hibernate + proxool 實現資料庫連接管理和訪問。 需求是實現多資料來源的動態配置和載入使用。 思路是:
1. 用一個類 AdvancedDataSourceInitizer 實現ApplicationListener 介面,當 ContextRefreshEvent 事件被發布時, 自動從資料庫中讀取資料庫配置,轉化為 ProxoolDataSource 對象,並存入到一個 Map<dataSourceName,
ProxoolDataSource> 中;
package opstools.moonmm.support.listener;import java.util.List;import java.util.Map;import javax.sql.DataSource;import opstools.framework.datasource.MultiDataSource;import opstools.moonmm.clusterconfig.entity.ClusterConfig;import opstools.moonmm.clusterconfig.service.ClusterConfigService;import opstools.moonmm.support.utils.DBUtil;import org.springframework.beans.BeansException;import org.springframework.context.ApplicationContext;import org.springframework.context.ApplicationContextAware;import org.springframework.context.ApplicationEvent;import org.springframework.context.ApplicationListener;import org.springframework.jdbc.datasource.lookup.MapDataSourceLookup;public class AdvancedDataSourceInitializer implements ApplicationListener, ApplicationContextAware { private String desiredEventClassName; protected ApplicationContext applicationContext; public void onApplicationEvent(ApplicationEvent event) { if (shouldStart(event)) { Map<String, DataSource> cachedMap = (Map<String, DataSource>)applicationContext.getBean("dataSources"); ClusterConfigService clusterConfigService = (ClusterConfigService)applicationContext.getBean("clusterConfigService"); List<ClusterConfig> cclist = clusterConfigService.getAllClusterConfigInstances(); DBUtil.addCachedDatasources(cachedMap, cclist); MapDataSourceLookup dsLookup = (MapDataSourceLookup) applicationContext.getBean("dataSourceLookup"); dsLookup.setDataSources(cachedMap); MultiDataSource mds = (MultiDataSource) applicationContext.getBean("dataSource"); mds.setTargetDataSources(cachedMap); mds.afterPropertiesSet(); } } protected Class<?> getDesiredType() { try { return Class.forName(desiredEventClassName); } catch (ClassNotFoundException e) { throw new RuntimeException(e); } } public String getDesiredEventClassName() { return desiredEventClassName; } public void setDesiredEventClassName(String desiredEventClassName) { this.desiredEventClassName = desiredEventClassName; } public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.applicationContext = applicationContext; } protected boolean shouldStart(ApplicationEvent event){ Class<?> clazz = getDesiredType(); return clazz.isInstance(event); }}
DBUtil.java : 用於將資料庫配置轉化為 ProxoolDataSource 對象, 歸入串連池管理
package opstools.moonmm.support.utils;import java.util.List;import java.util.Map;import javax.sql.DataSource;import opstools.moonmm.clusterconfig.entity.ClusterConfig;import opstools.moonmm.monitorconfig.entity.MonitorConfig;import org.logicalcobwebs.proxool.ProxoolDataSource;public class DBUtil { private DBUtil() {} private static final String MYSQL_DRIVER = "com.mysql.jdbc.Driver"; public static DataSource cluconfig2DataSource(ClusterConfig cc) { ProxoolDataSource ds = new ProxoolDataSource(); String url = "jdbc:mysql://"+cc.getDbIp()+":"+cc.getDbPort()+"/"+cc.getDbName(); ds.setDriver(MYSQL_DRIVER); ds.setAlias(cc.getDataSource()); ds.setDriverUrl(url); ds.setUser(cc.getDbUser()); ds.setPassword(cc.getDbPassword()); ds.setPrototypeCount(5); ds.setMinimumConnectionCount(10); ds.setMaximumConnectionCount(50); return ds; } public static DataSource moniconfig2DataSource(MonitorConfig mc) { ProxoolDataSource ds = new ProxoolDataSource(); String url = "jdbc:mysql://"+ mc.getIp() +":"+ mc.getPort() + "/" + mc.getMonitordbName(); ds.setDriver(MYSQL_DRIVER); ds.setAlias(mc.getNickname()); ds.setDriverUrl(url); ds.setUser(mc.getUser()); ds.setPassword(mc.getPassword()); ds.setPrototypeCount(5); ds.setMinimumConnectionCount(10); ds.setMaximumConnectionCount(50); return ds; } public static void addCachedDatasources(Map<String, DataSource> cachedMap, List<ClusterConfig> cclist) { for (ClusterConfig cc: cclist) { cachedMap.put(cc.getDataSource(), cluconfig2DataSource(cc)); } } }
2. 用一個類 SpringEventPublisher 實現 ApplicationContextAware, 用於擷取 applicationContext 執行個體 ; 當應用啟動時,以及增刪更新資料庫配置時, 發布 ContextRefreshEvent 事件, 觸發動態載入資料來源的行為;
package opstools.moonmm.support.listener;import org.springframework.beans.BeansException;import org.springframework.context.ApplicationContext;import org.springframework.context.ApplicationContextAware;import org.springframework.context.event.ContextRefreshedEvent;public class SpringEventPublisher implements ApplicationContextAware { private ApplicationContext appContext; @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.appContext = applicationContext; } public void publishContextRefreshEvent() { appContext.publishEvent(new ContextRefreshedEvent(appContext)); }}
3. 用一個類MultiDataSource 繼承 AbstractRoutingDataSource 來定位和切換資料來源。
package opstools.framework.datasource;import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;public class MultiDataSource extends AbstractRoutingDataSource {@Overrideprotected Object determineCurrentLookupKey() {return DataSourceHolder.getCurrentDataSource();}}
package opstools.framework.datasource;public class DataSourceHolder {private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>();public static String getCurrentDataSource() {return (String) contextHolder.get();} public static void setDataSource(String dataSource){contextHolder.set(dataSource);}public static void setDefaultDataSource(){contextHolder.set(null);}public static void clearCustomerType() { contextHolder.remove(); } }
上述三個類的BEAN執行個體都可以直接配置在Spring 檔案中。
<util:map id="dataSources"><entry key="master" value-ref="masterDataSource" /></util:map><bean id="dataSourceLookup"class="org.springframework.jdbc.datasource.lookup.MapDataSourceLookup"></bean><bean id="dataSource" class="opstools.framework.datasource.MultiDataSource"><property name="targetDataSources" ref="dataSources"/><property name="defaultTargetDataSource" ref="masterDataSource" /><property name="dataSourceLookup" ref="dataSourceLookup" /></bean><bean id="sessionFactory"class="org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean"><property name="dataSource" ref="dataSource" /><property name="configLocation" value="classpath:hibernate.cfg.xml" /><property name="packagesToScan" value="opstools.*.*.entity" /><property name="configurationClass" value="org.hibernate.cfg.AnnotationConfiguration" /><property name="namingStrategy"><bean class="org.hibernate.cfg.ImprovedNamingStrategy"></bean></property></bean><bean id="transactionManager"class="org.springframework.orm.hibernate3.HibernateTransactionManager"><property name="sessionFactory" ref="sessionFactory" /></bean><bean id="dataSourceInitializer" class="opstools.moonmm.support.listener.AdvancedDataSourceInitializer"> <property name="desiredEventClassName" value="org.springframework.context.event.ContextRefreshedEvent"/> </bean> <bean id="eventPublisher" class="opstools.moonmm.support.listener.SpringEventPublisher"> </bean>
可是在實際使用中,卻無法正確切換資料來源,總是只能切換到第一個使用的資料來源。 後經查證, 發現必須設定 Proxool 別名,及串連數。
public static ProxoolDataSource cluconfig2DataSource(ClusterConfig cc) {
ProxoolDataSource pds = new ProxoolDataSource();
pds.setDriverUrl(...);
...
pds.setAlias(cc.getDataSource()); // 必須有這一行及下面幾行, 否則難以起作用。
pds.setMinimumConnectionCount(5);
pds.setMaximumConnectionCount(50);
pds.setPrototypeCount(10);
}
整個調試過程如下:
首先,前提是準備好源碼,可以使用 Eclipse 的 MAVEN 外掛程式下載。選中指定的JAR包,右鍵 Maven ---> Download sources ,放在指定 \.m2\repository 目錄下。 Windows 下一般放在 Documents and settings\使用者目錄\.m2\repository\
; Linux 下一般放在 ~/.m2/repository/ 。 當單步調試時,若缺乏相應類的源碼包, 會出現 Source Look up 介面及按鈕, 點擊添加源碼包之後,該介面就會變成相應類的源碼介面。建議使用項目構建工具 Maven 等,而不是手工從官網上搜尋下載。
由於架構互動的代碼很多地方都可能出問題,因此, 只能採用單步調試; 但一行行執行太慢, 因此,需要根據出錯特徵進行分析,設定一些關鍵斷點。比如,這裡的關鍵點有: 設定 dataSourceName 的地方(驗證確實傳入了正確的資料來源的 key ), 擷取 DataSource的地方(驗證確實定位得到了相對應的資料來源對象),擷取
Connection 的地方(驗證確實獲得了正確的資料庫連接)等。注意,使用 Debug 模式運行,就是有小蟲的那個表徵圖,而不是右箭頭表徵圖。 通過單步調試,可以知道擷取 proxool 資料庫連接的具體過程如下(畫成UML順序圖表更佳):
DataSourceHolder.setDataSource(dataSourceName) ---> AbstractRoutingDataSource.determineTargetDataSource(dataSourceName) ---> ProxoolDataSource ---> ProxoolDataSource.getConnection() --->
ConnectionPool.getConnection() ---> proxyConnections.getConnection(nextAvailableConnection)
發現在這裡拋出了 IndexOutOfBoundsException 異常。 proxyConnections 中並未含有剛剛切換的資料來源的串連,而我假定的是, 應該由 Proxool 自動預先建立若干個串連放在相應串連池裡面的。 在代碼裡設定了串連數後,成功了; 其後還出現一次類似錯誤, 是通過設定別名而解決的。
因為假定Proxool 會預先自動建立預設串連數的(靜態設定檔中沒有設定串連數是可用的,網上諸多文章也講到存在預設串連數的),並且以為別名是無關緊要的, 沒想到在這裡出了錯。 所以說,不能隨便作假設,但 Proxool 切換資料來源依賴於別名,這一點也挺讓人吃驚。
為什麼ProxoolDataSource 的別名如此重要呢? 因為 proxool 使用 alias 識別不同資料庫的串連池。 有代碼為證:
ProxoolDataSource.getConnection() 擷取資料庫連接的方法:
/** * @see javax.sql.DataSource#getConnection() */ public Connection getConnection() throws SQLException { ConnectionPool cp = null; try { if (!ConnectionPoolManager.getInstance().isPoolExists(alias)) { registerPool(); } cp = ConnectionPoolManager.getInstance().getConnectionPool(alias); return cp.getConnection(); } catch (ProxoolException e) { LOG.error("Problem getting connection", e); throw new SQLException(e.toString()); } }
串連池管理器用於擷取串連池的代碼 ConnectionPoolManager.getConnectionPool , 使用一個MAP 來存放串連池,其中 Key 是串連池的別名,Value 是串連池執行個體
class ConnectionPoolManager { private static final Object LOCK = new Object(); private Map connectionPoolMap = new HashMap(); private Set connectionPools = new HashSet(); private static ConnectionPoolManager connectionPoolManager = null; private static final Log LOG = LogFactory.getLog(ProxoolFacade.class); public static ConnectionPoolManager getInstance() { if (connectionPoolManager == null) { synchronized (LOCK) { if (connectionPoolManager == null) { connectionPoolManager = new ConnectionPoolManager(); } } } return connectionPoolManager; } private ConnectionPoolManager() { } /** * Get the pool by the alias * @param alias identifies the pool * @return the pool * @throws ProxoolException if it couldn't be found */ protected ConnectionPool getConnectionPool(String alias) throws ProxoolException { ConnectionPool cp = (ConnectionPool) connectionPoolMap.get(alias); if (cp == null) { throw new ProxoolException(getKnownPools(alias)); } return cp; } /** * Convenient method for outputing a message explaining that a pool couldn't * be found and listing the ones that could be found. * @param alias identifies the pool * @return a description of the wht the pool couldn't be found */ protected String getKnownPools(String alias) { StringBuffer message = new StringBuffer("Couldn't find a pool called '" + alias + "'. Known pools are: "); Iterator i = connectionPoolMap.keySet().iterator(); while (i.hasNext()) { message.append((String) i.next()); message.append(i.hasNext() ? ", " : "."); } return message.toString(); } /** * Whether the pool is already registered * @param alias how we identify the pool * @return true if it already exists, else false */ protected boolean isPoolExists(String alias) { return connectionPoolMap.containsKey(alias); } /** @return an array of the connection pools */ protected ConnectionPool[] getConnectionPools() { return (ConnectionPool[]) connectionPools.toArray(new ConnectionPool[connectionPools.size()]); } protected ConnectionPool createConnectionPool(ConnectionPoolDefinition connectionPoolDefinition) throws ProxoolException { ConnectionPool connectionPool = new ConnectionPool(connectionPoolDefinition); connectionPools.add(connectionPool); connectionPoolMap.put(connectionPoolDefinition.getAlias(), connectionPool); return connectionPool; } protected void removeConnectionPool(String name) { ConnectionPool cp = (ConnectionPool) connectionPoolMap.get(name); if (cp != null) { connectionPoolMap.remove(cp.getDefinition().getAlias()); connectionPools.remove(cp); } else { LOG.info("Ignored attempt to remove either non-existent or already removed connection pool " + name); } } public String[] getConnectionPoolNames() { return (String[]) connectionPoolMap.keySet().toArray(new String[connectionPoolMap.size()]); }}
這就解釋了,為什麼Proxool 與別名的關係如此緊密。
調試架構互動的問題還需要耐心。 因為出錯的具體地方可能分布在任何意料之外的位置,有可能在認為不相關的地方直接跳過了, 需要返回去再定位之前的位置,反覆如此,直到一步步接近出錯的位置。比如,開始在定位問題的時候, 並沒有做很詳細的分析,而是較隨意地單步加跳躍執行,從 Spring 源碼跳轉到 Proxool
的源碼 跳轉到 Hibernate 的源碼再跳回到 Spring , 不亦樂乎, 後來終於發現了一點小線索,逐步縮小範圍,最終定位到問題所在。 今天一整天的功夫就用來調試切換資料來源所出現的這兩個問題。這多少說明, 使用開發架構會增大調試的難度, 增加一些維護的成本。
主要收穫是: 終於成功調試了一個關於架構互動的問題 :-)