zoukankan      html  css  js  c++  java
  • 多数据源的动态配置与加载使用兼框架交互的问题调试


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


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



  • 相关阅读:
    考拉兹猜想,100万以内的数
    给任意a、b、c三个数作为边盘都是否能构成三角形
    计数算法-对200万考生的成绩就行排序
    冒泡排序
    插入排序
    Echarts
    vue+tinymce
    java RestTemplate.postForObject请求传参
    java计算今天起后7天时间 +昨天八点+今天前7天的日期
    layui+echarts+动态数据
  • 原文地址:https://www.cnblogs.com/lovesqcc/p/4037780.html
Copyright © 2011-2022 走看看