多数据源的动态配置与加载使用兼框架交互的问题调试

来源:互联网 发布:烈焰龙城轮回数据 编辑:程序博客网 时间: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 , 不亦乐乎, 后来终于发现了一点小线索,逐步缩小范围,最终定位到问题所在。 今天一整天的功夫就用来调试切换数据源所出现的这两个问题。这多少说明, 使用开发框架会增大调试的难度, 增加一些维护的成本。


       主要收获是: 终于成功调试了一个关于框架交互的问题 :-) 



原创粉丝点击