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


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



  • 相关阅读:
    Java实现 蓝桥杯 算法提高 特等奖学金(暴力)
    Java实现 蓝桥杯 算法提高 特等奖学金(暴力)
    Java实现 蓝桥杯 算法提高 GPA(暴力)
    Java实现 蓝桥杯 算法提高 GPA(暴力)
    Java实现 蓝桥杯 算法提高 GPA(暴力)
    Java实现 蓝桥杯 算法提高 套正方形(暴力)
    Java实现 蓝桥杯 算法提高 套正方形(暴力)
    第一届云原生应用大赛火热报名中! helm install “一键安装”应用触手可及!
    云原生时代,2个方案轻松加速百万级镜像
    Knative 基本功能深入剖析:Knative Serving 自动扩缩容 Autoscaler
  • 原文地址:https://www.cnblogs.com/lovesqcc/p/4037780.html
Copyright © 2011-2022 走看看