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


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


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



 

posted @ 2013-02-25 21:12  琴水玉  阅读(646)  评论(0编辑  收藏  举报