多数据源的动态配置与加载使用兼框架交互的问题调试
来源:互联网 发布:烈焰龙城轮回数据 编辑:程序博客网 时间:2024/06/05 23:45
我遇到的问题是这样的。项目使用 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 , 不亦乐乎, 后来终于发现了一点小线索,逐步缩小范围,最终定位到问题所在。 今天一整天的功夫就用来调试切换数据源所出现的这两个问题。这多少说明, 使用开发框架会增大调试的难度, 增加一些维护的成本。
主要收获是: 终于成功调试了一个关于框架交互的问题 :-)
- 多数据源的动态配置与加载使用兼框架交互的问题调试
- spring框架中多数据源创建加载并且实现动态切换的配置实例代码
- spring框架中多数据源创建加载并且实现动态切换的配置实例代码
- spring框架中多数据源创建加载并且实现动态切换的配置实例代码
- SpringMVC框架中多数据源的配置问题、datasource
- 使用spring 实现真正多数据源的动态加载及动态切换
- 基于标注的spring多数据源配置与使用
- SSM框架多数据源配置以及一直调用默认的数据源的问题
- Spring 的动态多数据源的配置
- Spring+iBatis多数据源的动态配置方案
- Spring+iBatis多数据源的动态配置方案
- Spring+iBatis多数据源的动态配置方案
- Spring+iBatis多数据源的动态配置方案
- ssm框架实现多数据源的配置,本人亲测,项目正在使用。
- java基于ssm框架整合的多数据源配置
- SSM框架配置多数据源连接不同的数据库
- spring框架中解决多数据源的问题
- spring框架中切换多数据源的问题
- com.sun.awt.AWTUtilities.setWindowOpacity相关说明
- Spring --- Validation
- IPA转APP的方法和APP转IPA的方法
- Linux服务器(redhat)的安全加固
- Axis WebService 一看就懂
- 多数据源的动态配置与加载使用兼框架交互的问题调试
- Java 按行读文件操作代码
- 设置TextView中文字的超链接
- Codeforces Beta Round #46 (Div. 2), problem: (D) Game 贪心YY
- XML文件生成
- 递归法实现全排列
- 有一个数组,每次从中间随机取一个,然后放回去,当所有的元素都被取过,返回总共的取的次数。写一个函数实现。复杂度是什么。
- log不知道打到哪里去了?
- 解决Spring TestContext下运行JUnit4抛错(java.lang.NoClassDefFoundError)的问题