多数据源的动态配置与加载使用兼框架交互的问题调试
我遇到的问题是这样的。项目使用 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 {
@Override
protected 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 , 不亦乐乎, 后来终于发现了一点小线索,逐步缩小范围,最终定位到问题所在。 今天一整天的功夫就用来调试切换数据源所出现的这两个问题。这多少说明, 使用开发框架会增大调试的难度, 增加一些维护的成本。
主要收获是: 终于成功调试了一个关于框架交互的问题 :-)