數據庫連接池在編寫應用服務是經常需要用到的模塊,太過頻繁的連接數據庫對服務性能來講是一個瓶頸,使用緩沖池技術可以來消除這個瓶頸。我們可以在互聯網上找到很多關於數據庫連接池的源程序,但是都發現這樣一個共同的問題:這些連接池的實現方法都不同程度地增加了與使用者之間的耦合度。很多的連接池都要求用戶通過其規定的方法獲取數據庫的連接,這一點我們可以理解,畢竟目前所有的應用服務器取數據庫連接的方式都是這種方式實現的。但是另外一個共同的問題是,它們同時不允許使用者顯式的調用Connection.close()方法,而需要用其規定的一個方法來關閉連接。這種做法有兩個缺點:
第一:改變了用戶使用習慣,增加了用戶的使用難度。
首先我們來看看一個正常的數據庫操作過程:
int executeSQL(String sql) throws SQLException
{
Connection conn = getConnection(); //通過某種方式獲取數據庫連接
PreparedStatement ps = null;
int res = 0;
try{
ps = conn.prepareStatement(sql);
res = ps.executeUpdate();
}finally{
try{
ps.close();
}catch(Exception e){}
try{
conn.close();//
}catch(Exception e){}
}
return res;
}
使用者在用完數據庫連接後通常是直接調用連接的方法close來釋放數據庫資源,如果用我們前面提到的連接池的實現方法,那語句conn.close()將被某些特定的語句所替代。
第二:使連接池無法對之中的所有連接進行獨占控制。由於連接池不允許用戶直接調用連接的close方法,一旦使用者在使用的過程中由於習慣問題直接關閉了數據庫連接,那麼連接池將無法正常維護所有連接的狀態,考慮連接池和應用由不同開發人員實現時這種問題更容易出現。
綜合上面提到的兩個問題,我們來討論一下如何解決這兩個要命的問題。
首先我們先設身處地的考慮一下用戶是想怎麼樣來使用這個數據庫連接池的。用戶可以通過特定的方法來獲取數據庫的連接,同時這個連接的類型應該是標准的java.sql.Connection。用戶在獲取到這個數據庫連接後可以對這個連接進行任意的操作,包括關閉連接等。
通過對用戶使用的描述,怎樣可以接管Connection.close方法就成了我們這篇文章的主題。
為了接管數據庫連接的close方法,我們應該有一種類似於鉤子的機制。例如在Windows編程中我們可以利用Hook API來實現對某個Windows API的接管。在JAVA中同樣也有這樣一個機制。JAVA提供了一個Proxy類和一個InvocationHandler,這兩個類都在java.lang.reflect包中。我們先來看看SUN公司提供的文檔是怎麼描述這兩個類的。
public interface InvocationHandler
InvocationHandler is the interface implemented by the invocation handler of a proxy instance.
Each proxy instance has an associated invocation handler.
When a method is invoked on a proxy instance,
the method invocation is encoded and dispatched to the invoke method of its invocation handler.
SUN的API文檔中關於Proxy的描述很多,這裡就不羅列出來。通過文檔對接口InvocationHandler的描述我們可以看到當調用一個Proxy實例的方法時會觸發Invocationhanlder的invoke方法。從JAVA的文檔中我們也同時了解到這種動態代理機制只能接管接口的方法,而對一般的類無效,考慮到java.sql.Connection本身也是一個接口由此就找到了解決如何接管close方法的出路。
首先,我們先定義一個數據庫連接池參數的類,定義了數據庫的JDBC驅動程序類名,連接的URL以及用戶名口令等等一些信息,該類是用於初始化連接池的參數,具體定義如下:
public class ConnectionParam implements Serializable
{
private String driver; //數據庫驅動程序
private String url; //數據連接的URL
private String user; //數據庫用戶名
private String password; //數據庫密碼
private int minConnection = 0; //初始化連接數
private int maxConnection = 50; //最大連接數
private long timeoutValue = 600000;//連接的最大空閒時間
private long waitTime = 30000; //取連接的時候如果沒有可用連接最大的等待時間
其次是連接池的工廠類ConnectionFactory,通過該類來將一個連接池對象與一個名稱對應起來,使用者通過該名稱就可以獲取指定的連接池對象,具體代碼如下:
/**
* 連接池類廠,該類常用來保存多個數據源名稱合數據庫連接池對應的哈希
* @author liusoft
*/
public class ConnectionFactory
{
//該哈希表用來保存數據源名和連接池對象的關系表
static Hashtable connectionPools = null;
static{
connectionPools = new Hashtable(2,0.75F);
}
/**
* 從連接池工廠中獲取指定名稱對應的連接池對象
* @param dataSource 連接池對象對應的名稱
* @return DataSource 返回名稱對應的連接池對象
* @throws NameNotFoundException 無法找到指定的連接池
*/
public static DataSource lookup(String dataSource)
throws NameNotFoundException
{
Object ds = null;
ds = connectionPools.get(dataSource);
if(ds == null || !(ds instanceof DataSource))
throw new NameNotFoundException(dataSource);
return (DataSource)ds;
}
/**
* 將指定的名字和數據庫連接配置綁定在一起並初始化數據庫連接池
* @param name 對應連接池的名稱
* @param param 連接池的配置參數,具體請見類ConnectionParam
* @return DataSource 如果綁定成功後返回連接池對象
* @throws NameAlreadyBoundException 一定名字name已經綁定則拋出該異常
* @throws ClassNotFoundException 無法找到連接池的配置中的驅動程序類
* @throws IllegalAccessException 連接池配置中的驅動程序類有誤
* @throws InstantiationException 無法實例化驅動程序類
* @throws SQLException 無法正常連接指定的數據庫
*/
public static DataSource bind(String name, ConnectionParam param)
throws NameAlreadyBoundException,ClassNotFoundException,
IllegalAccessException,InstantiationException,SQLException
{
DataSourceImpl source = null;
try{
lookup(name);
throw new NameAlreadyBoundException(name);
}catch(NameNotFoundException e){
source = new DataSourceImpl(param);
source.initConnection();
connectionPools.put(name, source);
}
return source;
}
/**
* 重新綁定數據庫連接池
* @param name 對應連接池的名稱
* @param param 連接池的配置參數,具體請見類ConnectionParam
* @return DataSource 如果綁定成功後返回連接池對象
* @throws NameAlreadyBoundException 一定名字name已經綁定則拋出該異常
* @throws ClassNotFoundException 無法找到連接池的配置中的驅動程序類
* @throws IllegalAccessException 連接池配置中的驅動程序類有誤
* @throws InstantiationException 無法實例化驅動程序類
* @throws SQLException 無法正常連接指定的數據庫
*/
public static DataSource rebind(String name, ConnectionParam param)
throws NameAlreadyBoundException,ClassNotFoundException,
IllegalAccessException,InstantiationException,SQLException
{
try{
unbind(name);
}catch(Exception e){}
return bind(name, param);
}
/**
* 刪除一個數據庫連接池對象
* @param name
* @throws NameNotFoundException
*/
public static void unbind(String name) throws NameNotFoundException
{
DataSource dataSource = lookup(name);
if(dataSource instanceof DataSourceImpl){
DataSourceImpl dsi = (DataSourceImpl)dataSource;
try{
dsi.stop();
dsi.close();
}catch(Exception e){
}finally{
dsi = null;
}
}
connectionPools.remove(name);
}
}
ConnectionFactory主要提供了用戶將將連接池綁定到一個具體的名稱上以及取消綁定的操作。使用者只需要關心這兩個類即可使用數據庫連接池的功能。下面我們給出一段如何使用連接池的代碼:
String name = "pool";
String driver = " sun.jdbc.odbc.JdbcOdbcDriver ";
String url = "jdbc:odbc:datasource";
ConnectionParam param = new ConnectionParam(driver,url,null,null);
param.setMinConnection(1);
param.setMaxConnection(5);
param.setTimeoutValue(20000);
ConnectionFactory.bind(name, param);
System.out.println("bind datasource ok.");
//以上代碼是用來登記一個連接池對象,該操作可以在程序初始化只做一次即可
//以下開始就是使用者真正需要寫的代碼
DataSource ds = ConnectionFactory.lookup(name);
try{
for(int i=0;i<10;i++){
Connection conn = ds.getConnection();
try{
testSQL(conn, sql);
}finally{
try{
conn.close();
}catch(Exception e){}
}
}
}catch(Exception e){
e.printStackTrace();
}finally{
ConnectionFactory.unbind(name);
System.out.println("unbind datasource ok.");
System.exit(0);
}
從使用者的示例代碼就可以看出,我們已經解決了常規連接池產生的兩個問題。但是我們最最關心的是如何解決接管close方法的辦法。接管工作主要在ConnectionFactory中的兩句代碼:
source = new DataSourceImpl(param);
source.initConnection();
DataSourceImpl是一個實現了接口javax.sql.DataSource的類,該類維護著一個連接池的對象。由於該類是一個受保護的類,因此它暴露給使用者的方法只有接口DataSource中定義的方法,其他的所有方法對使用者來說都是不可視的。我們先來關心用戶可訪問的一個方法getConnection
/**
* @see javax.sql.DataSource#getConnection(String,String)
*/
public Connection getConnection(String user, String password) throws SQLException
{
//首先從連接池中找出空閒的對象
Connection conn = getFreeConnection(0);
if(conn == null){
//判斷是否超過最大連接數,如果超過最大連接數
//則等待一定時間查看是否有空閒連接,否則拋出異常告訴用戶無可用連接
if(getConnectionCount() >= connParam.getMaxConnection())
conn = getFreeConnection(connParam.getWaitTime());
else{//沒有超過連接數,重新獲取一個數據庫的連接
connParam.setUser(user);
connParam.setPassword(password);
Connection conn2 = DriverManager.getConnection(connParam.getUrl(),
user, password);
//代理將要返回的連接對象
_Connection _conn = new _Connection(conn2,true);
synchronized(conns){
conns.add(_conn);
}
conn = _conn.getConnection();
}
}
return conn;
}
/**
* 從連接池中取一個空閒的連接
* @param nTimeout 如果該參數值為0則沒有連接時只是返回一個null
* 否則的話等待nTimeout毫秒看是否還有空閒連接,如果沒有拋出異常
* @return Connection
* @throws SQLException
*/
protected synchronized Connection getFreeConnection(long nTimeout)
throws SQLException
{
Connection conn = null;
Iterator iter = conns.iterator();
while(iter.hasNext()){
_Connection _conn = (_Connection)iter.next();
if(!_conn.isInUse()){
conn = _conn.getConnection();
_conn.setInUse(true);
break;
}
}
if(conn == null && nTimeout > 0){
//等待nTimeout毫秒以便看是否有空閒連接
try{
Thread.sleep(nTimeout);
}catch(Exception e){}
conn = getFreeConnection(0);
if(conn == null)
throw new SQLException("沒有可用的數據庫連接");
}
return conn;
}
DataSourceImpl類中實現getConnection方法的跟正常的數據庫連接池的邏輯是一致的,首先判斷是否有空閒的連接,如果沒有的話判斷連接數是否已經超過最大連接數等等的一些邏輯。但是有一點不同的是通過DriverManager得到的數據庫連接並不是及時返回的,而是通過一個叫_Connection的類中介一下,然後調用_Connection.getConnection返回的。如果我們沒有通過一個中介也就是JAVA中的Proxy來接管要返回的接口對象,那麼我們就沒有辦法截住Connection.close方法。
終於到了核心所在,我們先來看看_Connection是如何實現的,然後再介紹是客戶端調用Connection.close方法時走的是怎樣一個流程,為什麼並沒有真正的關閉連接。
/**
* 數據連接的自封裝,屏蔽了close方法
* @author Liudong
*/
class _Connection implements InvocationHandler
{
private final static String CLOSE_METHOD_NAME = "close";
private Connection conn = null;
//數據庫的忙狀態
private boolean inUse = false;
//用戶最後一次訪問該連接方法的時間
private long lastAccessTime = System.currentTimeMillis();
_Connection(Connection conn, boolean inUse){
this.conn = conn;
this.inUse = inUse;
}
/**
* Returns the conn.
* @return Connection
*/
public Connection getConnection() {
//返回數據庫連接conn的接管類,以便截住close方法
Connection conn2 = (Connection)Proxy.newProxyInstance(
conn.getClass().getClassLoader(),
conn.getClass().getInterfaces(),this);
return conn2;
}
/**
* 該方法真正的關閉了數據庫的連接
* @throws SQLException
*/
void close() throws SQLException{
//由於類屬性conn是沒有被接管的連接,因此一旦調用close方法後就直接關閉連接
conn.close();
}
/**
* Returns the inUse.
* @return boolean
*/
public boolean isInUse() {
return inUse;
}
/**
* @see java.lang.reflect.InvocationHandler#invoke(java.lang.Object, java.lang.reflect.Method, java.lang.Object)
*/
public Object invoke(Object proxy, Method m, Object[] args)
throws Throwable
{
Object obj = null;
//判斷是否調用了close的方法,如果調用close方法則把連接置為無用狀態
if(CLOSE_METHOD_NAME.equals(m.getName()))
setInUse(false);
else
obj = m.invoke(conn, args);
//設置最後一次訪問時間,以便及時清除超時的連接
lastAccessTime = System.currentTimeMillis();
return obj;
}
/**
* Returns the lastAccessTime.
* @return long
*/
public long getLastAccessTime() {
return lastAccessTime;
}
/**
* Sets the inUse.
* @param inUse The inUse to set
*/
public void setInUse(boolean inUse) {
this.inUse = inUse;
}
}
一旦使用者調用所得到連接的close方法,由於用戶的連接對象是經過接管後的對象,因此JAVA虛擬機會首先調用_Connection.invoke方法,在該方法中首先判斷是否為close方法,如果不是則將代碼轉給真正的沒有被接管的連接對象conn。否則的話只是簡單的將該連接的狀態設置為可用。到此您可能就明白了整個接管的過程,但是同時也有一個疑問:這樣的話是不是這些已建立的連接就始終沒有辦法真正關閉?答案是可以的。我們來看看ConnectionFactory.unbind方法,該方法首先找到名字對應的連接池對象,然後關閉該連接池中的所有連接並刪除掉連接池。在DataSourceImpl類中定義了一個close方法用來關閉所有的連接,詳細代碼如下:
/**
* 關閉該連接池中的所有數據庫連接
* @return int 返回被關閉連接的個數
* @throws SQLException
*/
public int close() throws SQLException
{
int cc = 0;
SQLException excp = null;
Iterator iter = conns.iterator();
while(iter.hasNext()){
try{
((_Connection)iter.next()).close();
cc ++;
}catch(Exception e){
if(e instanceof SQLException)
excp = (SQLException)e;
}
}
if(excp != null)
throw excp;
return cc;
}
該方法一一調用連接池中每個對象的close方法,這個close方法對應的是_Connection中對close的實現,在_Connection定義中關閉數據庫連接的時候是直接調用沒有經過接管的對象的關閉方法,因此該close方法真正的釋放了數據庫資源。