數據庫連接池在編寫(xiě)應用服務(wù)是經(jīng)常需要用到的模塊,太過(guò)頻繁的連接數據庫對服務(wù)性能來(lái)講是一個(gè)瓶頸,使用緩沖池技術(shù)可以來(lái)消除這個(gè)瓶頸。我們可以在互聯(lián)網(wǎng)上找到很多關(guān)于數據庫連接池的源程序,但是都發(fā)現這樣一個(gè)共同的問(wèn)題:這些連接池的實(shí)現方法都不同程度地增加了與使用者之間的耦合度。很多的連接池都要求用戶(hù)通過(guò)其規定的方法獲取數據庫的連接,這一點(diǎn)我們可以理解,畢竟目前所有的應用服務(wù)器取數據庫連接的方式都是這種方式實(shí)現的。但是另外一個(gè)共同的問(wèn)題是,它們同時(shí)不允許使用者顯式的調用Connection.close()方法,而需要用其規定的一個(gè)方法來(lái)關(guān)閉連接。這種做法有兩個(gè)缺點(diǎn):
第一:改變了用戶(hù)使用習慣,增加了用戶(hù)的使用難度。
首先我們來(lái)看看一個(gè)正常的數據庫操作過(guò)程:
int executeSQL(String sql) throws SQLException{ Connection conn = getConnection(); //通過(guò)某種方式獲取數據庫連接 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來(lái)釋放數據庫資源,如果用我們前面提到的連接池的實(shí)現方法,那語(yǔ)句conn.close()將被某些特定的語(yǔ)句所替代。
第二:使連接池無(wú)法對之中的所有連接進(jìn)行獨占控制。由于連接池不允許用戶(hù)直接調用連接的close方法,一旦使用者在使用的過(guò)程中由于習慣問(wèn)題直接關(guān)閉了數據庫連接,那么連接池將無(wú)法正常維護所有連接的狀態(tài),考慮連接池和應用由不同開(kāi)發(fā)人員實(shí)現時(shí)這種問(wèn)題更容易出現。
綜合上面提到的兩個(gè)問(wèn)題,我們來(lái)討論一下如何解決這兩個(gè)要命的問(wèn)題。
首先我們先設身處地的考慮一下用戶(hù)是想怎么樣來(lái)使用這個(gè)數據庫連接池的。用戶(hù)可以通過(guò)特定的方法來(lái)獲取數據庫的連接,同時(shí)這個(gè)連接的類(lèi)型應該是標準的java.sql.Connection。用戶(hù)在獲取到這個(gè)數據庫連接后可以對這個(gè)連接進(jìn)行任意的操作,包括關(guān)閉連接等。
通過(guò)對用戶(hù)使用的描述,怎樣可以接管Connection.close方法就成了我們這篇文章的主題。
為了接管數據庫連接的close方法,我們應該有一種類(lèi)似于鉤子的機制。例如在Windows編程中我們可以利用Hook API來(lái)實(shí)現對某個(gè)Windows API的接管。在JAVA中同樣也有這樣一個(gè)機制。JAVA提供了一個(gè)Proxy類(lèi)和一個(gè)InvocationHandler,這兩個(gè)類(lèi)都在java.lang.reflect包中。我們先來(lái)看看SUN公司提供的文檔是怎么描述這兩個(gè)類(lèi)的。
public interface InvocationHandlerInvocationHandler 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文檔中關(guān)于Proxy的描述很多,這里就不羅列出來(lái)。通過(guò)文檔對接口InvocationHandler的描述我們可以看到當調用一個(gè)Proxy實(shí)例的方法時(shí)會(huì )觸發(fā)Invocationhanlder的invoke方法。從JAVA的文檔中我們也同時(shí)了解到這種動(dòng)態(tài)代理機制只能接管接口的方法,而對一般的類(lèi)無(wú)效,考慮到j(luò )ava.sql.Connection本身也是一個(gè)接口由此就找到了解決如何接管close方法的出路。
首先,我們先定義一個(gè)數據庫連接池參數的類(lèi),定義了數據庫的JDBC驅動(dòng)程序類(lèi)名,連接的URL以及用戶(hù)名口令等等一些信息,該類(lèi)是用于初始化連接池的參數,具體定義如下:
public class ConnectionParam implements Serializable{ private String driver; //數據庫驅動(dòng)程序 private String url; //數據連接的URL private String user; //數據庫用戶(hù)名 private String password; //數據庫密碼 private int minConnection = 0; //初始化連接數 private int maxConnection = 50; //最大連接數 private long timeoutValue = 600000;//連接的最大空閑時(shí)間 private long waitTime = 30000; //取連接的時(shí)候如果沒(méi)有可用連接最大的等待時(shí)間
|
其次是連接池的工廠(chǎng)類(lèi)ConnectionFactory,通過(guò)該類(lèi)來(lái)將一個(gè)連接池對象與一個(gè)名稱(chēng)對應起來(lái),使用者通過(guò)該名稱(chēng)就可以獲取指定的連接池對象,具體代碼如下:
/** * 連接池類(lèi)廠(chǎng),該類(lèi)常用來(lái)保存多個(gè)數據源名稱(chēng)合數據庫連接池對應的哈希 * @author liusoft */public class ConnectionFactory{ //該哈希表用來(lái)保存數據源名和連接池對象的關(guān)系表 static Hashtable connectionPools = null; static{ connectionPools = new Hashtable(2,0.75F); } /** * 從連接池工廠(chǎng)中獲取指定名稱(chēng)對應的連接池對象 * @param dataSource 連接池對象對應的名稱(chēng) * @return DataSource 返回名稱(chēng)對應的連接池對象 * @throws NameNotFoundException 無(wú)法找到指定的連接池 */ 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 對應連接池的名稱(chēng) * @param param 連接池的配置參數,具體請見(jiàn)類(lèi)ConnectionParam * @return DataSource 如果綁定成功后返回連接池對象 * @throws NameAlreadyBoundException 一定名字name已經(jīng)綁定則拋出該異常 * @throws ClassNotFoundException 無(wú)法找到連接池的配置中的驅動(dòng)程序類(lèi) * @throws IllegalAccessException 連接池配置中的驅動(dòng)程序類(lèi)有誤 * @throws InstantiationException 無(wú)法實(shí)例化驅動(dòng)程序類(lèi) * @throws SQLException 無(wú)法正常連接指定的數據庫 */ 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 對應連接池的名稱(chēng) * @param param 連接池的配置參數,具體請見(jiàn)類(lèi)ConnectionParam * @return DataSource 如果綁定成功后返回連接池對象 * @throws NameAlreadyBoundException 一定名字name已經(jīng)綁定則拋出該異常 * @throws ClassNotFoundException 無(wú)法找到連接池的配置中的驅動(dòng)程序類(lèi) * @throws IllegalAccessException 連接池配置中的驅動(dòng)程序類(lèi)有誤 * @throws InstantiationException 無(wú)法實(shí)例化驅動(dòng)程序類(lèi) * @throws SQLException 無(wú)法正常連接指定的數據庫 */ public static DataSource rebind(String name, ConnectionParam param) throws NameAlreadyBoundException,ClassNotFoundException, IllegalAccessException,InstantiationException,SQLException { try{ unbind(name); }catch(Exception e){} return bind(name, param); } /** * 刪除一個(gè)數據庫連接池對象 * @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主要提供了用戶(hù)將將連接池綁定到一個(gè)具體的名稱(chēng)上以及取消綁定的操作。使用者只需要關(guān)心這兩個(gè)類(lèi)即可使用數據庫連接池的功能。下面我們給出一段如何使用連接池的代碼:
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."); //以上代碼是用來(lái)登記一個(gè)連接池對象,該操作可以在程序初始化只做一次即可 //以下開(kāi)始就是使用者真正需要寫(xiě)的代碼 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); }
|
從使用者的示例代碼就可以看出,我們已經(jīng)解決了常規連接池產(chǎn)生的兩個(gè)問(wèn)題。但是我們最最關(guān)心的是如何解決接管close方法的辦法。接管工作主要在ConnectionFactory中的兩句代碼:
source = new DataSourceImpl(param);source.initConnection();
|
DataSourceImpl是一個(gè)實(shí)現了接口javax.sql.DataSource的類(lèi),該類(lèi)維護著(zhù)一個(gè)連接池的對象。由于該類(lèi)是一個(gè)受保護的類(lèi),因此它暴露給使用者的方法只有接口DataSource中定義的方法,其他的所有方法對使用者來(lái)說(shuō)都是不可視的。我們先來(lái)關(guān)心用戶(hù)可訪(fǎng)問(wèn)的一個(gè)方法getConnection
/** * @see javax.sql.DataSource#getConnection(String,String) */ public Connection getConnection(String user, String password) throws SQLException { //首先從連接池中找出空閑的對象 Connection conn = getFreeConnection(0); if(conn == null){ //判斷是否超過(guò)最大連接數,如果超過(guò)最大連接數 //則等待一定時(shí)間查看是否有空閑連接,否則拋出異常告訴用戶(hù)無(wú)可用連接 if(getConnectionCount() >= connParam.getMaxConnection()) conn = getFreeConnection(connParam.getWaitTime()); else{//沒(méi)有超過(guò)連接數,重新獲取一個(gè)數據庫的連接 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; } /** * 從連接池中取一個(gè)空閑的連接 * @param nTimeout 如果該參數值為0則沒(méi)有連接時(shí)只是返回一個(gè)null * 否則的話(huà)等待nTimeout毫秒看是否還有空閑連接,如果沒(méi)有拋出異常 * @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("沒(méi)有可用的數據庫連接"); } return conn; }
|
DataSourceImpl類(lèi)中實(shí)現getConnection方法的跟正常的數據庫連接池的邏輯是一致的,首先判斷是否有空閑的連接,如果沒(méi)有的話(huà)判斷連接數是否已經(jīng)超過(guò)最大連接數等等的一些邏輯。但是有一點(diǎn)不同的是通過(guò)DriverManager得到的數據庫連接并不是及時(shí)返回的,而是通過(guò)一個(gè)叫_Connection的類(lèi)中介一下,然后調用_Connection.getConnection返回的。如果我們沒(méi)有通過(guò)一個(gè)中介也就是JAVA中的Proxy來(lái)接管要返回的接口對象,那么我們就沒(méi)有辦法截住Connection.close方法。
終于到了核心所在,我們先來(lái)看看_Connection是如何實(shí)現的,然后再介紹是客戶(hù)端調用Connection.close方法時(shí)走的是怎樣一個(gè)流程,為什么并沒(méi)有真正的關(guān)閉連接。
/** * 數據連接的自封裝,屏蔽了close方法 * @author Liudong */class _Connection implements InvocationHandler{ private final static String CLOSE_METHOD_NAME = "close"; private Connection conn = null; //數據庫的忙狀態(tài) private boolean inUse = false; //用戶(hù)最后一次訪(fǎng)問(wèn)該連接方法的時(shí)間 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的接管類(lèi),以便截住close方法 Connection conn2 = (Connection)Proxy.newProxyInstance( conn.getClass().getClassLoader(), conn.getClass().getInterfaces(),this); return conn2; } /** * 該方法真正的關(guān)閉了數據庫的連接 * @throws SQLException */ void close() throws SQLException{ //由于類(lèi)屬性conn是沒(méi)有被接管的連接,因此一旦調用close方法后就直接關(guān)閉連接 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方法則把連接置為無(wú)用狀態(tài) if(CLOSE_METHOD_NAME.equals(m.getName())) setInUse(false); else obj = m.invoke(conn, args); //設置最后一次訪(fǎng)問(wèn)時(shí)間,以便及時(shí)清除超時(shí)的連接 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方法,由于用戶(hù)的連接對象是經(jīng)過(guò)接管后的對象,因此JAVA虛擬機會(huì )首先調用_Connection.invoke方法,在該方法中首先判斷是否為close方法,如果不是則將代碼轉給真正的沒(méi)有被接管的連接對象conn。否則的話(huà)只是簡(jiǎn)單的將該連接的狀態(tài)設置為可用。到此您可能就明白了整個(gè)接管的過(guò)程,但是同時(shí)也有一個(gè)疑問(wèn):這樣的話(huà)是不是這些已建立的連接就始終沒(méi)有辦法真正關(guān)閉?答案是可以的。我們來(lái)看看ConnectionFactory.unbind方法,該方法首先找到名字對應的連接池對象,然后關(guān)閉該連接池中的所有連接并刪除掉連接池。在DataSourceImpl類(lèi)中定義了一個(gè)close方法用來(lái)關(guān)閉所有的連接,詳細代碼如下:
/** * 關(guān)閉該連接池中的所有數據庫連接 * @return int 返回被關(guān)閉連接的個(gè)數 * @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; }
|
該方法一一調用連接池中每個(gè)對象的close方法,這個(gè)close方法對應的是_Connection中對close的實(shí)現,在_Connection定義中關(guān)閉數據庫連接的時(shí)候是直接調用沒(méi)有經(jīng)過(guò)接管的對象的關(guān)閉方法,因此該close方法真正的釋放了數據庫資源。
以上文字只是描述了接口方法的接管,具體一個(gè)實(shí)用的連接池模塊還需要對空閑連接的監控并及時(shí)釋放連接,詳細的代碼請參照相關(guān)資料。