1. 程式人生 > >使用JAVA中的動態代理實現資料庫連線池

使用JAVA中的動態代理實現資料庫連線池

資料庫連線池在編寫應用服務是經常需要用到的模組,太過頻繁的連線資料庫對服務效能來講是一個瓶頸,使用緩衝池技術可以來消除這個瓶頸。我們可以在網際網路上找到很多關於資料庫連線池的源程式,但是都發現這樣一個共同的問題:這些連線池的實現方法都不同程度地增加了與使用者之間的耦合度。很多的連線池都要求使用者通過其規定的方法獲取資料庫的連線,這一點我們可以理解,畢竟目前所有的應用伺服器取資料庫連線的方式都是這種方式實現的。但是另外一個共同的問題是,它們同時不允許使用者顯式的呼叫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方法真正的釋放了資料庫資源。

以上文字只是描述了介面方法的接管,具體一個實用的連線池模組還需要對空閒連線的監控並及時釋放連線,詳細的程式碼請參照附件。