连结池详解 
2002-10-30 
到目前为目,JDBC2的连结池只是一个接口,没有真正的实现,JDBC3正在开发中, 
据报已经支持连结池,但.......... 
JDBC3用了JNDI技术,连结池的配置可以让一个高手都烦死. 目前第三方已经实现的连结池当然是poolman,1.0版对一般用户来说已经足够用了.配置也简单,2.0版虽然增加了一些功能,但配置也是采用JNDI,对RMI和EJB不懂的朋友可能很烦.建议用1.0的了. 如果有兴趣,自己也可以实现连结池,最关键的技术也就是把连结作为参数传给一个BEAN,用完后返回这个参数连结而不是关闭. 
下面是一个简单的实现: 
DBConnectionManager.java程序清单如下: 
   
  001 import java.io.*; 
  002 import java.sql.*; 
  003 import java.util.*; 
  004 import java.util.Date; 
  005 
  006 /** 
  007 * 管理类DBConnectionManager支持对一个或多个由属性文件定义的数据库连接 
  008 * 池的访问.客户程序可以调用getInstance()方法访问本类的唯一实例. 
  009 */ 
  010 public class DBConnectionManager { 
  011 static private DBConnectionManager instance; // 唯一实例 
  012 static private int clients; 
  013 
  014 private Vector drivers = new Vector(); 
  015 private PrintWriter log; 
  016 private Hashtable pools = new Hashtable(); 
  017 
  018 /** 
  019 * 返回唯一实例.如果是第一次调用此方法,则创建实例 
  020 * 
  021 * @return DBConnectionManager 唯一实例 
  022 */ 
  023 static synchronized public DBConnectionManager getInstance() { 
  024 if (instance == null) { 
  025 instance = new DBConnectionManager(); 
  026 } 
  027 clients++; 
  028 return instance; 
  029 } 
  030 
  031 /** 
  032 * 建构函数私有以防止其它对象创建本类实例 
  033 */ 
  034 private DBConnectionManager() { 
  035 init(); 
  036 } 
  037 
  038 /** 
  039 * 将连接对象返回给由名字指定的连接池 
  040 * 
  041 * @param name 在属性文件中定义的连接池名字 
  042 * @param con 连接对象 
  043 */ 
  044 public void freeConnection(String name, Connection con) { 
  045 DBConnectionPool pool = (DBConnectionPool) pools.get(name); 
  046 if (pool != null) { 
  047 pool.freeConnection(con); 
  048 } 
  049 } 
  050 
  051 /** 
  052 * 获得一个可用的(空闲的)连接.如果没有可用连接,且已有连接数小于最大连接数 
  053 * 限制,则创建并返回新连接 
  054 * 
  055 * @param name 在属性文件中定义的连接池名字 
  056 * @return Connection 可用连接或null 
  057 */ 
  058 public Connection getConnection(String name) { 
  059 DBConnectionPool pool = (DBConnectionPool) pools.get(name); 
  060 if (pool != null) { 
  061 return pool.getConnection(); 
  062 } 
  063 return null; 
  064 } 
  065 
  066 /** 
  067 * 获得一个可用连接.若没有可用连接,且已有连接数小于最大连接数限制, 
  068 * 则创建并返回新连接.否则,在指定的时间内等待其它线程释放连接. 
  069 * 
  070 * @param name 连接池名字 
  071 * @param time 以毫秒计的等待时间 
  072 * @return Connection 可用连接或null 
  073 */ 
  074 public Connection getConnection(String name, long time) { 
  075 DBConnectionPool pool = (DBConnectionPool) pools.get(name); 
  076 if (pool != null) { 
  077 return pool.getConnection(time); 
  078 } 
  079 return null; 
  080 } 
  081 
  082 /** 
  083 * 关闭所有连接,撤销驱动程序的注册 
  084 */ 
  085 public synchronized void release() { 
  086 // 等待直到最后一个客户程序调用 
  087 if (--clients != 0) { 
  088 return; 
  089 } 
  090 
  091 Enumeration allPools = pools.elements(); 
  092 while (allPools.hasMoreElements()) { 
  093 DBConnectionPool pool = (DBConnectionPool) allPools.nextElement(); 
  094 pool.release(); 
  095 } 
  096 Enumeration allDrivers = drivers.elements(); 
  097 while (allDrivers.hasMoreElements()) { 
  098 Driver driver = (Driver) allDrivers.nextElement(); 
  099 try { 
  100 DriverManager.deregisterDriver(driver); 
  101 log("撤销JDBC驱动程序 " + driver.getClass().getName()+"的注册"); 
  102 } 
  103 catch (SQLException e) { 
  104 log(e, "无法撤销下列JDBC驱动程序的注册: " + driver.getClass().getName()); 
  105 } 
  106 } 
  107 } 
  108 
  109 /** 
  110 * 根据指定属性创建连接池实例. 
  111 * 
  112 * @param props 连接池属性 
  113 */ 
  114 private void createPools(Properties props) { 
  115 Enumeration propNames = props.propertyNames(); 
  116 while (propNames.hasMoreElements()) { 
  117 String name = (String) propNames.nextElement(); 
  118 if (name.endsWith(".url")) { 
  119 String poolName = name.substring(0, name.lastIndexOf(".")); 
  120 String url = props.getProperty(poolName + ".url"); 
  121 if (url == null) { 
  122 log("没有为连接池" + poolName + "指定URL"); 
  123 continue; 
  124 } 
  125 String user = props.getProperty(poolName + ".user"); 
  126 String password = props.getProperty(poolName + ".password"); 
  127 String maxconn = props.getProperty(poolName + ".maxconn", "0"); 
  128 int max; 
  129 try { 
  130 max = Integer.valueOf(maxconn).intValue(); 
  131 } 
  132 catch (NumberFormatException e) { 
  133 log("错误的最大连接数限制: " + maxconn + " .连接池: " + poolName); 
  134 max = 0; 
  135 } 
  136 DBConnectionPool pool = 
  137 new DBConnectionPool(poolName, url, user, password, max); 
  138 pools.put(poolName, pool); 
  139 log("成功创建连接池" + poolName); 
  140 } 
  141 } 
  142 } 
  143 
  144 /** 
  145 * 读取属性完成初始化 
  146 */ 
  147 private void init() { 
  148 InputStream is = getClass().getResourceAsStream("/db.properties"); 
  149 Properties dbProps = new Properties(); 
  150 try { 
  151 dbProps.load(is); 
  152 } 
  153 catch (Exception e) { 
  154 System.err.println("不能读取属性文件. " + 
  155 "请确保db.properties在CLASSPATH指定的路径中"); 
  156 return; 
  157 } 
  158 String logFile = dbProps.getProperty("logfile", "DBConnectionManager.log"); 
  159 try { 
  160 log = new PrintWriter(new FileWriter(logFile, true), true); 
  161 } 
  162 catch (IOException e) { 
  163 System.err.println("无法打开日志文件: " + logFile); 
  164 log = new PrintWriter(System.err); 
  165 } 
  166 loadDrivers(dbProps); 
  167 createPools(dbProps); 
  168 } 
  169 
  170 /** 
  171 * 装载和注册所有JDBC驱动程序 
  172 * 
  173 * @param props 属性 
  174 */ 
  175 private void loadDrivers(Properties props) { 
  176 String driverClasses = props.getProperty("drivers"); 
  177 StringTokenizer st = new StringTokenizer(driverClasses); 
  178 while (st.hasMoreElements()) { 
  179 String driverClassName = st.nextToken().trim(); 
  180 try { 
  181 Driver driver = (Driver) 
  182 Class.forName(driverClassName).newInstance(); 
  183 DriverManager.registerDriver(driver); 
  184 drivers.addElement(driver); 
  185 log("成功注册JDBC驱动程序" + driverClassName); 
  186 } 
  187 catch (Exception e) { 
  188 log("无法注册JDBC驱动程序: " + 
  189 driverClassName + ", 错误: " + e); 
  190 } 
  191 } 
  192 } 
  193 
  194 /** 

解决方案 »

  1.   

    195 * 将文本信息写入日志文件 
      196 */ 
      197 private void log(String msg) { 
      198 log.println(new Date() + ": " + msg); 
      199 } 
      200 
      201 /** 
      202 * 将文本信息与异常写入日志文件 
      203 */ 
      204 private void log(Throwable e, String msg) { 
      205 log.println(new Date() + ": " + msg); 
      206 e.printStackTrace(log); 
      207 } 
      208 
      209 /** 
      210 * 此内部类定义了一个连接池.它能够根据要求创建新连接,直到预定的最 
      211 * 大连接数为止.在返回连接给客户程序之前,它能够验证连接的有效性. 
    212 */ 
      213 class DBConnectionPool { 
      214 private int checkedOut; 
      215 private Vector freeConnections = new Vector(); 
      216 private int maxConn; 
      217 private String name; 
      218 private String password; 
      219 private String URL; 
      220 private String user; 
      221 
      222 /** 
      223 * 创建新的连接池 
      224 * 
      225 * @param name 连接池名字 
      226 * @param URL 数据库的JDBC URL 
      227 * @param user 数据库帐号,或 null 
      228 * @param password 密码,或 null 
      229 * @param maxConn 此连接池允许建立的最大连接数 
      230 */ 
      231 public DBConnectionPool(String name, String URL, String user, String password, 
      232 int maxConn) { 
      233 this.name = name; 
      234 this.URL = URL; 
      235 this.user = user; 
      236 this.password = password; 
      237 this.maxConn = maxConn; 
      238 } 
      239 
      240 /** 
      241 * 将不再使用的连接返回给连接池 
      242 * 
      243 * @param con 客户程序释放的连接 
      244 */ 
      245 public synchronized void freeConnection(Connection con) { 
      246 // 将指定连接加入到向量末尾 
      247 freeConnections.addElement(con); 
      248 checkedOut--; 
      249 notifyAll(); 
      250 } 
      251 
      252 /** 
      253 * 从连接池获得一个可用连接.如没有空闲的连接且当前连接数小于最大连接 
      254 * 数限制,则创建新连接.如原来登记为可用的连接不再有效,则从向量删除之, 
      255 * 然后递归调用自己以尝试新的可用连接. 
      256 */ 
      257 public synchronized Connection getConnection() { 
      258 Connection con = null; 
      259 if (freeConnections.size() > 0) { 
      260 // 获取向量中第一个可用连接 
      261 con = (Connection) freeConnections.firstElement(); 
      262 freeConnections.removeElementAt(0); 
      263 try { 
      264 if (con.isClosed()) { 
      265 log("从连接池" + name+"删除一个无效连接"); 
      266 // 递归调用自己,尝试再次获取可用连接 
      267 con = getConnection(); 
      268 } 
      269 } 
      270 catch (SQLException e) { 
      271 log("从连接池" + name+"删除一个无效连接"); 
      272 // 递归调用自己,尝试再次获取可用连接 
      273 con = getConnection(); 
      274 } 
      275 } 
      276 else if (maxConn == 0 || checkedOut < maxConn) { 
      277 con = newConnection(); 
      278 } 
      279 if (con != null) { 
      280 checkedOut++; 
      281 } 
      282 return con; 
      283 } 
      284 
      285 /** 
      286 * 从连接池获取可用连接.可以指定客户程序能够等待的最长时间 
      287 * 参见前一个getConnection()方法. 
      288 * 
      289 * @param timeout 以毫秒计的等待时间限制 
      290 */ 
      291 public synchronized Connection getConnection(long timeout) { 
      292 long startTime = new Date().getTime(); 
      293 Connection con; 
      294 while ((con = getConnection()) == null) { 
      295 try { 
      296 wait(timeout); 
      297 } 
      298 catch (InterruptedException e) {} 
      299 if ((new Date().getTime() - startTime) >= timeout) { 
      300 // wait()返回的原因是超时 
      301 return null; 
      302 } 
      303 } 
      304 return con; 
      305 } 
      306 
      307 /** 
      308 * 关闭所有连接 
      309 */ 
      310 public synchronized void release() { 
      311 Enumeration allConnections = freeConnections.elements(); 
      312 while (allConnections.hasMoreElements()) { 
      313 Connection con = (Connection) allConnections.nextElement(); 
      314 try { 
      315 con.close(); 
      316 log("关闭连接池" + name+"中的一个连接"); 
      317 } 
      318 catch (SQLException e) { 
      319 log(e, "无法关闭连接池" + name+"中的连接"); 
      320 } 
      321 } 
      322 freeConnections.removeAllElements(); 
      323 } 
      324 
      325 /** 
      326 * 创建新的连接 
      327 */ 
      328 private Connection newConnection() { 
      329 Connection con = null; 
      330 try { 
      331 if (user == null) { 
      332 con = DriverManager.getConnection(URL); 
      333 } 
      334 else { 
      335 con = DriverManager.getConnection(URL, user, password); 
      336 } 
      337 log("连接池" + name+"创建一个新的连接"); 
      338 } 
      339 catch (SQLException e) { 
      340 log(e, "无法创建下列URL的连接: " + URL); 
      341 return null; 
      342 } 
      343 return con; 
      344 } 
      345 } 
      346 } 
       
       
      

  2.   

    三、类DBConnectionPool说明 
       
      该类在209至345行实现,它表示指向某个数据库的连接池。数据库由JDBC URL标识。一个JDBC URL由三部分组成:协议标识(总是jdbc),驱动程序标识(如 odbc、idb、oracle等),数据库标识(其格式依赖于驱动程序)。例如,jdbc:odbc:demo,即是一个指向demo数据库的JDBC URL,而且访问该数据库要使用JDBC-ODBC驱动程序。每个连接池都有一个供客户程序使用的名字以及可选的用户帐号、密码、最大连接数限制。如果Web应用程序所支持的某些数据库操作可以被所有用户执行,而其它一些操作应由特别许可的用户执行,则可以为两类操作分别定义连接池,两个连接池使用相同的JDBC URL,但使用不同的帐号和密码。 
      类DBConnectionPool的建构函数需要上述所有数据作为其参数。如222至238行所示,这些数据被保存为它的实例变量: 
      如252至283行、285至305行所示, 客户程序可以使用DBConnectionPool类提供的两个方法获取可用连接。两者的共同之处在于:如连接池中存在可用连接,则直接返回,否则创建新的连接并返回。如果没有可用连接且已有连接总数等于最大限制数,第一个方法将直接返回null,而第二个方法将等待直到有可用连接为止。 
      所有的可用连接对象均登记在名为freeConnections的向量(Vector)中。如果向量中有多于一个的连接,getConnection()总是选取第一个。同时,由于新的可用连接总是从尾部加入向量,从而使得数据库连接由于长时间闲置而被关闭的风险减低到最小程度。 
      第一个getConnection()在返回可用连接给客户程序之前,调用了isClosed()方法验证连接仍旧有效。如果该连接被关闭或触发异常,getConnection()递归地调用自己以尝试获取另外的可用连接。如果在向量freeConnections中不存在任何可用连接,getConnection()方法检查是否已经指定最大连接数限制。如已经指定,则检查当前连接数是否已经到达极限。此处maxConn为0表示没有限制。如果没有指定最大连接数限制或当前连接数小于该值,该方法尝试创建新的连接。如创建成功,则增加已使用连接的计数并返回,否则返回空值。 
      如325至345行所示,创建新连接由newConnection()方法实现。创建过程与是否已经指定数据库帐号、密码有关。 
      JDBC的DriverManager类提供多个getConnection()方法,这些方法要用到JDBC URL与其它一些参数,如用户帐号和密码等。DriverManager将使用指定的JDBC URL确定适合于目标数据库的驱动程序及建立连接。 
      在285至305行实现的第二个getConnection()方法需要一个以毫秒为单位的时间参数,该参数表示客户程序能够等待的最长时间。建立连接的具体操作仍旧由第一个getConnection()方法实现。 
      该方法执行时先将startTime初始化为当前时间。在while循环中尝试获得一个连接。如果失败,则以给定的时间值为参数调用wait()。wait()的返回可能是由于其它线程调用notify()或notifyAll(),也可能是由于预定时间已到。为找出wait()返回的真正原因,程序用当前时间减开始时间(startTime),如差值大于预定时间则返回空值,否则再次调用getConnection()。 
      把空闲的连接登记到连接池由240至250行的freeConnection()方法实现,它的参数为返回给连接池的连接对象。该对象被加入到freeConnections向量的末尾,然后减少已使用连接计数。调用notifyAll()是为了通知其它正在等待可用连接的线程。 
      许多Servlet引擎为实现安全关闭提供多种方法。数据库连接池需要知道该事件以保证所有连接能够正常关闭。DBConnectionManager类负协调整个关闭过程,但关闭连接池中所有连接的任务则由DBConnectionPool类负责。在307至323行实现的release()方法供DBConnectionManager调用。该方法遍历freeConnections向量并关闭所有连接,然后从向量中删除这些连接。 
    四、类DBConnectionManager 说明 
       
      该类只能创建一个实例,其它对象能够调用其静态方法(也称为类方法)获得该唯一实例的引用。如031至036行所示,DBConnectionManager类的建构函数是私有的,这是为了避免其它对象创建该类的实例。 
      DBConnectionManager类的客户程序可以调用getInstance()方法获得对该类唯一实例的引用。如018至029行所示,类的唯一实例在getInstance()方法第一次被调用期间创建,此后其引用就一直保存在静态变量instance中。每次调用getInstance()都增加一个DBConnectionManager的客户程序计数。即,该计数代表引用DBConnectionManager唯一实例的客户程序总数,它将被用于控制连接池的关闭操作。 
      该类实例的初始化工作由146至168行之间的私有方法init()完成。其中 getResourceAsStream()方法用于定位并打开外部文件。外部文件的定位方法依赖于类装载器的实现。标准的本地类装载器查找操作总是开始于类文件所在路径,也能够搜索CLASSPATH中声明的路径。db.properties是一个属性文件,它包含定义连接池的键-值对。可供定义的公用属性如下: 
       
      drivers 以空格分隔的JDBC驱动程序类列表 
      logfile 日志文件的绝对路径 
       
      其它的属性和特定连接池相关,其属性名字前应加上连接池名字: 
       
      < poolname>.url 数据库的 JDBC URL  
      < poolname>.maxconn 允许建立的最大连接数,0表示没有限制  
      < poolname>.user 用于该连接池的数据库帐号 
      < poolname>.password 相应的密码 
       
      其中url属性是必需的,而其它属性则是可选的。数据库帐号和密码必须合法。用于Windows平台的db.properties文件示例如下: 
       
      drivers=sun.jdbc.odbc.JdbcOdbcDriver jdbc.idbDriver 
      logfile=D:\\user\\src\\java\\DBConnectionManager\\log.txt 
       
      idb.url=jdbc:idb:c:\\local\\javawebserver1.1\\db\\db.prp 
      idb.maxconn=2 
       
      access.url=jdbc:odbc:demo 
      access.user=demo 
      access.password=demopw 
       
      注意在Windows路径中的反斜杠必须输入2个,这是由于属性文件中的反斜杠同时也是一个转义字符。 
      init()方法在创建属性对象并读取db.properties文件之后,就开始检查logfile属性。如果属性文件中没有指定日志文件,则默认为当前目录下的DBConnectionManager.log文件。如日志文件无法使用,则向System.err输出日志记录。 
      装载和注册所有在drivers属性中指定的JDBC驱动程序由170至192行之间的loadDrivers()方法实现。该方法先用StringTokenizer将drivers属性值分割为对应于驱动程序名称的字符串,然后依次装载这些类并创建其实例,最后在 DriverManager中注册该实例并把它加入到一个私有的向量drivers。向量drivers将用于关闭服务时从DriverManager取消所有JDBC 驱动程序的注册。 
      init()方法的最后一个任务是调用私有方法createPools()创建连接池对象。如109至142行所示,createPools()方法先创建所有属性名字的枚举对象(即Enumeration对象,该对象可以想象为一个元素系列,逐次调用其nextElement()方法将顺序返回各元素),然后在其中搜索名字以“.url”结尾的属性。对于每一个符合条件的属性,先提取其连接池名字部分,进而读取所有属于该连接池的属性,最后创建连接池对象并把它保存在实例变量pools中。散列表(Hashtable类 )pools实现连接池名字到连接池对象之间的映射,此处以连接池名字为键,连接池对象为值。 
      为便于客户程序从指定连接池获得可用连接或将连接返回给连接池,类DBConnectionManager提供了方法getConnection()和freeConnection()。所有这些方法都要求在参数中指定连接池名字,具体的连接获取或返回操作则调用对应的连接池对象完成。它们的实现分别在051至064行、066至080行、038至049行。 
      如082至107行所示,为实现连接池的安全关闭,DBConnectionManager提供了方法release()。在上面我们已经提到,所有DBConnectionManager的客户程序都应该调用静态方法getInstance()以获得该管理器的引用,此调用将增加客户程序计数。客户程序在关闭时调用release()可以递减该计数。当最后一个客户程序调用release(),递减后的引用计数为0,就可以调用各个连接池的release()方法关闭所有连接了。管理类release()方法最后的任务是撤销所有JDBC驱动程序的注册。 
       
       
      五、Servlet使用连接池示例 
       
      Servlet API所定义的Servlet生命周期类如: 
       
      1) 创建并初始化Servlet(init()方法)。 
      2) 响应客户程序的服务请求(service()方法)。 
      3) Servlet终止运行,释放所有资源(destroy()方法)。 
       
      本例演示连接池应用,上述关键步骤中的相关操作为: 
       
      1) 在init(),用实例变量connMgr 保存调用DBConnectionManager.getInstance()所返回的引用。 
      2) 在service(),调用getConnection(),执行数据库操作,用freeConnection()将连接返回给连接池。 
      3) 在destroy(),调用release()关闭所有连接,释放所有资源。