Java Servlet作为首选的
服务器端数据处理技术
,正在迅速取代CGI脚本
。Servlet超越CGI的优势之一在于
,不仅多个请求可以共享公用资源,而且还可以在不同用户请求之间保留持续数据
。本文介绍一种充分发挥该特色的实用技术,即数据库连接池。  
  
  
一、实现连接池的意义  
  
动态Web站点往往用数据库存储的信息生成Web页面,每一个页面请求导致一次数据库访问。连接数据库不仅要开销一定的通讯和内存资源,还必须完成用户验证、
安全上下文配置这类任务,因而往往成为最为耗时的操作。当然,实际的连接时间开销千变万化,但1到2秒延迟并非不常见。如果某个基于数据库的Web应用只需建立一次初始连
  
接,不同页面请求能够共享同一连接,就能获得显著的性能改善。
  
Servlet是一个Java类。Servlet引擎(它可能是Web服务软件的一部分,也可能是一个独立的附加模块)在系统启动或Servlet第一次被请求时将该类装入Java虚拟机并创建它的一个实例。不同用户请求由同一Servlet实例的多个独立线程处理。那些要
  
求在不同请求之间持续有效的数据既可以用Servlet的实例变量来保存,也可以保存在独立的辅助对象中。
  
用JDBC访问数据库首先要创建与数据库之间的连接,获得一个连接对象(Connection),由连接对象提供执行
SQL语句的方法。
  
本文介绍的数据库连接池包括一个管理类DBConnectionManager,负责提供与多个连接池对象(DBConnectionPool类)之间的接口。每一个连接池对象管理一组JDBC连接对象,每一个连接对象可以被任意数量的Servlet共享。
  
类DBConnectionPool提供以下功能:  
  
1) 从连接池获取(或创建)可用连接。
  
2) 把连接返回给连接池。
  
3) 在系统关闭时释放所有资源,关闭所有连接。  
  
此外, DBConnectionPool类还能够处理无效连接(原来登记为可用的连接,由于某种原因不再可用,如超时,通讯问题)
  
,并能够限制连接池中的连接总数不超过某个预定值。
  
管理类DBConnectionManager用于管理多个连接池对象,它提供以下功能:  
  
1) 装载和注册JDBC驱动程序。
  
2) 根据在属性文件中定义的属性创建连接池对象。
  
3) 实现连接池名字与其实例之间的映射。
  
4) 跟踪客户程序对连接池的引用,保证在最后一个客户程序结束时
安全地关闭所有连接池。  
  
本文余下部分将详细说明这两个类,最后给出一个示例演示Servlet使用连接池的一般过程。  
  
  
二、具体实现  
  
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 /**
  
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 }