從一個ConnectionPool的實現看Design Pattern的運用 (一)
作者:ajoo
什么是ConnectionPool? 我們知道,JDBC提供了java.sql.Connection interface, 供我們連接不同的數據源。但是,因為與數據庫建立連接是一個很大的開銷,所以,我們可以把已打開的數據庫連接緩存在一個連接池中,供后續的 Connection用戶使用。用戶使用完Connection后,再把它返還到連接池中。
對一個連接池,有許多功能上的考慮。
1. 如是否設置一個最大連接數,以保證數據庫不會因同時過多的連接請求而癱瘓;
2. 是否設置一個最小連接數,以保證任何時刻都至少有若干個連接可用;
3. 是否設置一個最多的空閑連接數,空閑連接超過這個數的就關閉過多的連接;
4. 當對一個連接的請求不能被滿足時,是否讓請求同步等待,還是直接返回一個錯誤。
5. 怎樣保證公平性,也就是說,一個對連接的同步請求會在一定的時間內得到x響應,而不是被餓死。
6. 連接池是用vector, list還是其他的什么Collection對象來實現。
等等等等。
下面,讓我們來看看一個ConnectionPool的實現:
public class ConnectionPool
{
private final Vector pool = new Vector();
private int clients;
private int maxClients;
其他的一些連接屬性如username, password, dsn等等;
public synchronized Connection getConnection()
{
如果pool里有Connection
從pool中去掉一個Connection conn;
clients++;
return conn;
否則,如果clients<maxClients
生成一個新的連接conn
clients++;
return conn;
否則,wait(),直到pool中有空閑Connection
}
public synchronized void closeConnection(Connection conn)
{
pool.add(conn);
clients--;
notify();
}
public synchronized void clear()
{
for each connection in pool
conn.close();
pool.removeAllElements();
}
}
好了,讓我們來看一看有沒有什么可以改善的。
首先,象我們剛開始所說的,對ConnectionPool的實現有許多不同的考慮,這個類明顯只是一個相當簡單的實現。那么,我們是不是應該根據我們前面列出的條條,逐條進行實現呢?
不,如果那樣,我們就犯了過度設計的錯誤。也許我們現在并不需要那么復雜的功能,為什么要自找麻煩呢?而且,有些功能的取舍,并不是簡單的好與壞,是要根據具體的需要變化的。有些人也許會說,好吧,我在類里放一些布爾變量,對每種功能是否支持都可以configure. 這樣也許可行,但,你還是要對每個功能寫代碼,最后這個ConnectionPool就變成一個spaghetti了。而且,伙計,讓我們謙虛一點吧!“不是我不明白,這世界變化快”,我們得承認,我們永遠也不可能預測所有的可能性。也不可能把所有的需求都實現到一個類中去。
那么,我還“首先”什么呢?反正也是先實現簡單的,“Simple and Stupid”。就這樣不是挺好嗎?問題是,我們要考慮使用我們ConnectionPool的用戶。雖然需求可能變化多端,但我們還是應該盡可能給用戶提供一個固定的接口。你不能這樣要求使用你的ConnectionPool的程序員:“喂,哥們兒,我昨天寫了一個ConnectionPool2, 比ConnectionPool酷多了,你改用這個吧。Replace All就行了”。
因此,我們應該把ConnectionPool設計成一個interface, 這樣,無論實現類如何變化,使用ConnectionPool interface的用戶可以不受影響。他們甚至可以根據需要使用不同的ConnectionPool的實現。簡單,是嗎?不就是一個interface嘛。相信我,你會看到它的作用有多大的。
其次,一個好的ConnectionPool應該是對Connection的使用者透明的。對一個使用Connection interface 的用戶,如:
void doQuery(Connection conn){……}
它應該對該Connection是否來自ConnectionPool不敏感。那么,當不使用ConnectionPool時,我們怎么釋放Connection呢?對了,是調用Connection.close(). 我們不應該要求用戶改調ConnectionPool.closeConnection來釋放Connection.
第三,假設我們有兩個ConnectionPool對象,一個連接Oracle數據庫,另一個連接SQL Server. 現在,用戶同時使用兩個ConnectionPool, 當用戶使用完時,他不小心調用了sqlPool.closeConnection(orclConn); orclPool.closeConnection(sqlConn);天啊!不定時炸彈!不要光埋怨程序員:“那種只知道依靠類庫的容錯性,沒有類庫的保護不知道怎么編程的程序員不適合使用Java”。其實這完全是你的錯。
好了,預知后事如何,請聽下回分解。家庭作業是,請大家回去想想怎么設計這個ConnectionPool。
從一個ConnectionPool的實現看design pattern的運用 (二)
好啦,現在讓我們看看我們是怎樣設計這個ConnectionPool的接口的。
Public interface ConnectionPool{
Connection getConnection();
Void clear();
}
當然,這只是一個簡化的接口。真正實現時也許我們還會有一些其他的方法。如同時支持同步getConnection()和異步getConnection().
同時,這個返回的Connection必須改寫close()方法。原來的close()方法是關閉物理連接,但現在,我們要把這個Connection返還到ConnectionPool. 因此,這個Connection對象必須知道它出身的ConnectionPool.
這樣,用戶仍然調用Connection.close(), 也不用擔心把Connection返還到錯誤的ConnectionPool.
再來看看我們的實現:
public class ConnectionPoolImpl: implements ConnectionPool{
class PooledConnection implements Connection{
private final Connection conn;
private Boolean closed;
public PooledConnection(Connection conn)
throws SQLException{
this.conn = conn;
closed = conn.isClosed();
}
public void close(){
if(!closed){
//保證重復調用close()不會把一個connection重復返還。
closeConnection(conn);
closed = true;
}
}
public Boolean isClosed(){return closed;}
public Statement createStatement()
throws SQLConnection{
if(isClosed()) throw new SQLException(“Connection closed”);
return conn.createStatement();
}
public void commit()
throws SQLConnection{
if(isClosed()) throw new SQLException(“Connection closed”);
conn.commit();
}
//其他所有的方法都這樣做委托。
}
public synchronized Connection getConnection(){
如果pool里有Connection
從pool中去掉一個Connection conn;
clients++;
return new PooledConnection(conn);
否則,如果clients<maxClients
生成一個新的連接conn
clients++;
return new PooledConnection(conn);
否則,wait(),直到pool中有空閑Connection
}
//其他的實現都和我們第一章里的代碼一樣。
}
好了,這樣,通過內部類PooledConnection這樣一個wrapper, 我們就可以實現這個能處理ConnectionPool的Connection.
對了,忘了說了,這個PooledConnection其實就是design pattern里的decorator模式。
現在讓我們再欣賞一下我們的代碼吧。ConnectonPoolImpl提供了一種基于簡單策略的ConnectionPool的實現。PooledConnection封裝了數據庫連接,使之能和ConnectionPool協同工作。
完美吧?
不過,慢著!PooledConnection只是和ConnectionPoolImpl協同工作。張三要寫ConnectionPool2還得自己重新實現一個decorator.
怎么樣能使不同的ConnectionPool的實現重用我們PooledConnection呢?
而且,在所有大約二十個委托函數里,我們都有if(isClosed())……, 是不是很眼熟啊?一個Connection一旦被close()之后,只有在涅磐之后(通過ConnectionPool.getConnection()再次返回),才能使用。而所有對關閉了的Connection的調用,返回結果都是一樣的.(不行,不行,不行!)
猜到點什么了嗎?對啦,state pattern!
從一個ConnectionPool的實現看design pattern的運用 (三)
根據上回對PooledConnection的分析,下面是對一個可重用PooledConnection的實現:
public class PooledConnection implements Connection{
public interface Pool{
//引入這個interface, 是因為我們的PooledConnection只需要知道如何返還Connection. 本著接口最小化原則,我們只定義我們需要的操作。
void closeConnection(Connection conn);
}
private interface ConnectionState{
//state pattern的interface.
ConnectionState close()
throws SQLException;
//close()方法是唯一引起狀態轉移的方法。
boolean isClosed();
Connection getOpenConnection()
throws SQLException;
}
private static class ClosedConnection implements ConnectionState{
public final ConnectionState close(){return this;}
//當一個Connection已經closed了的時候,它實際上已經死了。所有對它的操作,除了isClosed()和close(), 只產生異常。所以,一個closed的Connection, 它已經不需要保存那個物理數據庫連接和對出身ConnectionPool的連接。而且因為所有的 closed connection的狀態都一樣,所以可以用singleton來節省內存。
public final Connection getOpenConnection()
throws SQLException{
throw new SQLException("Connection closed");
}
public final boolean isClosed(){return true;}
private ClosedConnection(){}
private static final ConnectionState _instance = new ClosedConnection();
static ConnectionState instance(Connection conn, Pool pool){return _instance;}
}
private static class OpenConnection implements ConnectionState{
private final Pool pool;
private final Connection conn;
public final ConnectionState close(){
//對一個open connection的關閉,會把原始數據庫連接返還到connection pool. 同時,該連接死亡。
pool.closeConnection(conn);
return ClosedConnection.instance(conn, pool);
}
public final Connection getOpenConnection()
{return conn;}
public final boolean isClosed(){return false;}
OpenConnection(Connection conn, Pool pool){
this.conn = conn; this.pool = pool;
}
static ConnectionState instance(Connection conn, Pool pool){
return new OpenConnection(conn, pool);
}
}
private ConnectionState state;
//用靜態的工廠方法,可以隱藏我們的實現類,以后,根據需要,我們可以方便地修改實現類,比如用內部類取代。
//根據要修飾的Connection的狀態,初始化PooledConnection
public static Connection decorate(Connection conn, Pool pool)
throws SQLException{
if(conn.isClosed()){
return new PooledConnection(ClosedConnection.instance(conn, pool));
}
else{
return new PooledConnection(OpenConnection.instance(conn, pool));
}
}
private PooledConnection(ConnectionState state){
this.state = state;
}
public final boolean isClosed(){
return state.isClosed();
}
public final void close()
throws SQLException{
state = state.close();
}
private final Connection getOpenConnection()
throws SQLException
{return state.getOpenConnection();}
/*****然后,做委托****/
public final Statement createStatement()
throws SQLException{
return getOpenConnection().createStatement();
}
public final void rollback()throws SQLException{
getOpenConnection().rollback();
}
//等等等等
}
好,再來看看ConnectionPoolImpl怎樣使用PooledConnection.
public class ConnectionPoolImpl implements ConnectionPool{
public synchronized Connection getConnection(){
Connection ret;
如果pool里有Connection
從pool中去掉一個Connection conn;
clients++;
ret = conn;
否則,如果clients<maxClients
生成一個新的連接conn
clients++;
ret = conn;
否則,wait(),直到pool中有空閑Connection
//下面的這個匿名類實際上是個adapter pattern. J
return PooledConnection.decorate(ret,
new PooledConnection.Pool{
public void closeConnection(Connection conn){
ConnectionPoolImpl.this.closeConnection(conn);
}
}
}
//其他都和原來一樣
}
這樣,所有對ConnectionPool的實現,都可以在返回一個物理Connection之前,把它用PooledConnection封裝一下。如此,代碼得到了重用。ConnectionPool的實現者可以把主要精力放在怎樣處理池的各種功能。而不是怎樣包裝Connection.
世界真美好!
不過。。。。。
萬一,李四忘了用PooledConnection包裝他的Connection怎么辦?編譯器不會報錯,因為反正都是Connection類型。
“你也太杞人憂天了吧?他忘了揍他不就得了?”哎,保不齊呀!人不是機器,總有犯錯的時候,到時候揍他有啥用?還手疼呢。
同學們,今天的家庭作業是:想辦法讓李四的健忘癥不會影響我們的ConnectionPool大業。
相關文章
( ) |
|
( ) |
| |
從一個ConnectionPool的實現看design pattern的運用 (四)
好了,同學們,大家對上回的“李四猜想”有沒有結果呀?
我們的口號是?。。。。。。
“沒有蛀牙”!
No! 是“用戶至上”!
既然用戶有容易忘的可能,那就證明我們的工作做得不好。我們為什么非要用戶做他們做不好或容易弄錯的事呢?
好吧,讓我們知錯就改:
public interface ConnectionMan extends PooledConnection.Pool{
//在這個interface里,我們不再要求程序員必須封裝Connection, 他們只需要直接返回Connection對象。 實際上,程序員可以完全忘記封裝這碼事。
//我們將對返回的對象進行封裝。
Connection getConnection()throws SQLException;
void clear();
void closeConnection(Connection conn);
}
//然后,我們用一下的decorator類對返回值進行封裝
public class ConnectionMan2ConnectionPool implements ConnectionPool{
public final Connection getConnection()throws SQLException{
return PooledConnection.decorate(man.getConnection(), man);
}
public final void clear(){
man.clear();
}
private final ConnectionMan man;
private ConnectionMan2ConnectionPool(ConnectionMan man){
this.man = man;
}
public static ConnectionPool decorate(ConnectionMan man){
return new ConnectionMan2ConnectionPool(man);
}
}
這樣,程序員只需要實現一個輔助interface ConnectionMan. 完全不要考慮封裝Connection的事。然后再用我們的ConnectionMan2ConnectionPool類把它轉換成ConnectionPool, 交給ConnectionPool的用戶使用。耶!
“那萬一李四忘了用ConnectionMan2ConnectionPool轉換怎么辦?”
呵呵,別忘了,編譯器不是吃素的。用戶期待ConnectionPool, 而李四只有ConnectionMan, 他想不轉換也不行?。?/SPAN>
什么?今天的家庭作業?
啊,讓你們家長寫表揚信給ajoo老師。:)
玩笑。如果那位能發現進一步refactor的地方,歡迎指出!
從一個ConnectionPool的實現看design pattern的運用 (五)
OK, 現在我們已經把封裝Connection的任務從ConnectionPool的開發者身上去掉了。他們只要實現一個輔助的ConnectionMan 接口,余下的事由PooledConnection類和ConnectionMan2ConnectionPool類來完成。
下面,再讓我們仔細地看一下ConnectionManImpl類:
public class ConnectioManImpl implements ConnectionMan{
public synchronized Connection getConnection(){
Connection ret;
如果pool里有Connection
從pool中去掉一個Connection conn;
clients++;
ret = conn;
否則,如果clients<maxClients
conn = newConnection();
clients++;
ret = conn;
否則,wait(),直到pool中有空閑Connection
return conn;
}
public synchronized void closeConnection(Connection conn){
pool.add(conn);
clients--;
notify();
}
private Connection newConnection(){
//使用用戶名,密碼,數據庫url等等信息從DriverManager生成一個Connection
}
//必要的一些用戶名,密碼等建立connection的信息。
}
大家是否注意到了?ConnectionMan的實現者除了寫pooling的算法,還要關心如何創建connection. 而這個創建connection的過程并不是總是一樣的。我們可能從DriverManager生成Connection, 也可能從DataSource生成connection;可能用用戶名,密碼生成,也可能用connection string生成。
同樣的pooling邏輯,可能需要處理不同的生成Connection的方式, 同一種生成connection的方式又有可能需要不同的pooling邏輯。因此,把pooling邏輯和connection生成耦合在一起似乎不是一個好辦法。
那么如何解決這個問題呢?pooling算法中,確實需要在適當的時刻生成connection??!
“把ConnectionManImpl做成抽象類,然后要求每個子類覆蓋newConnection()方法”。 資深程序員張三不屑地說。
是啊,這確實是個直觀又有效的方法。對同一個pooling算法,你只要subclass自己的子類,制定自己的connection生成,就可以重用父類的邏輯。這叫template method pattern.
不過,說實話,個人很不喜歡這個pattern. 從此例來說,假如我們有五種pooling算法,三種connection生成方法,那我們就需要寫十五個子類。太不靈活了。而且,實現繼承造成的父子類的強耦合關系,也是我所向來討厭的。父類的某個不經心的改變,有可能就使子類不再工作。
那么。。。。
對啦!讓我們抽象一下connection的生成吧。用abstract factory.
先定義一個factory的接口。
public interface ConnectionFactory{
public Connection createConnection()throws SQLException;
}
然后改寫我們的ConnectionManImpl, 讓它把生成Connection的工作委托給一個ConnectionFactory.
Public class ConnectionManImpl implements ConnectionMan{
Private final ConnectionFactory factory;
Private final int maxConn;
private ConnectionManImpl(ConnectionFactory factory, int max){
this.factory = factory;
this.maxConn = max;
}
static public ConnectionMan instance(ConnectionFactory factory, int max){
return new ConnectionManImpl(factory, max);
}
public final synchronized Connection getConnection()
throws SQLException
{
如果pool里有Connection
從pool中去掉一個Connection conn;
clients++;
return conn;
否則,如果clients<maxClients
conn = factory.createConnection();
clients++;
return conn;
否則,wait(),直到pool中有空閑Connection
}
//其他和前面一樣。
}
再看一個示例ConnectionFactory的實現:
public class ConnectionFactoryImpl
{
private ConnectionFactoryImpl(){}
static public ConnectionFactory instance(final String user, final String pwd,
final String url, final String driver)
throws SQLException, ClassNotFoundException{
final Class driverClass = Class.forName(driver);
return new ConnectionFactory(){
private final Class keeper = driverClass;
public final Connection createConnection()
throws SQLException{
return DriverManager.getConnection(url,user,pwd);
}
};
}
}
最后,再看看我們是怎樣把一個ConnectionMan, 一個ConnectionFactory組合成一個ConnectionPool的。
public class TestConnectionPool{
public static void test(String user, String pwd, String url, String driver)
throws java.sql.SQLException, ClassNotFoundException{
final ConnectionPool pool = ConnectionMan2ConnectionPool.decorate(
ConnectionManImpl.instance(
ConnectionFactoryImpl.instance(user, pwd, url, driver),
1000)
);
}
}
好啦,這一章,我們顯示了怎樣把ConnectionManImpl中的pooling邏輯和Connection 生成的邏輯分開,從而實現更大程度上的代碼重用。
思考題:
pooling, 作為一種技術,并不只是應用于ConnectionPool, 其他如Thread pool以及任何一種需要一定開銷創建的資源都可以應用這種技術。
那么,我們怎樣能夠把一個pooling的算法重用給connection pool, thread pool等不同的pool呢?怎樣才能說:“給我李四寫的pooling算法,我要拿它來對我的線程進行緩沖。”?而不是說:“李四,你的connection pooling算法寫的不錯,能不能給我的thread pooling也寫一個一樣的?”
從一個ConnectionPool的實現看design pattern的運用 (六)
要對不同資源重用pooling的算法?先讓我們再從頭審視一下我們ConnectionPool的實現。
1。 Pooling算法由ConnectionMan來實現。它需要委托ConnectionFactory來具體創建Connection對象
2。 ConnectionFactory負責建立連接。它封裝了如何建立連接
3。 PooledConnection負責封裝Connection對象,修改close()等跟pooling有關的方法,并對其它方法進行委托。
4。 ConnectionMan2ConnectionPool負責使用PooledConnection來把一個不能對用戶容錯,對用戶不透明的ConnectionMan轉化成對用戶安全透明的ConnectionPool.
首先,PooledConnection是無法重用的。它僅僅實現了Connection接口,重寫了close()方法。而對于一個未知的資源,我們自然是無法事先寫一個實現,無法未卜先知地知道哪一個方法是負責釋放的。
其次,ConnectionMan2ConnectionPool, 因為它直接使用了PooledConnection, 也無法對不同的資源pool重用。換句話說,對不同的資源,我們必須對應地寫封裝類和轉換類。
ConnectionFactory是一個接口,我們是可以寫一個更generic的ResourceFactory, 來讓不同的ResourcePool重用它。
重用Pooling算法是關鍵。而能否重用依賴于我們把所有與具體Resource相關的細節抽取出來。在此算法中,什么是與具體資源相關呢?資源創建自然是一個,我們已經用一個abstract factory把它抽取出來了。另一個跟具體資源相關的是資源的釋放。我們用close()方法釋放Connection. 但對于其它的資源,也許那是一個release()方法,destroy()方法,甚至是沒有任何方法。
所以,為了重用pooling算法,我們還需要抽象資源釋放。一個ResourceCollector的接口應該能夠完成這樣的工作。(如果說,ConnectionFactory是abstract factory pattern, 為什么GOF的書里沒有一個針對于ResourceCollector的pattern呢?)
好了,在寫代碼之前,還有一點需要澄清:
許多C++程序員認為,C++的GP是一個比OO更有用的技術。其實,GP中最主要的function object的思想,和OO中的面向接口編程只是一對孿生兄弟罷了。Template比目前Java的OO的主要優點在于:1,效率高。2,減少了很多類型強制轉換(downcast).
而許多Java程序員則認為, Java不需要GP, 因為所有GP能做的事,OO都能做。如GP可以有vector<string>, Java則有Vector, 只要做些downcast就可以了。但他們忘記了,靜態類型檢查是Java這種強類型語言的原則,也是所有程序員應該盡力遵循的???/SPAN>downcast的程序是容易出錯的。所以,Generic Java (加入generics的Java) 已經是下一版Java的目標了。
說這些題外話的目的是,使用目前的Java語法,即使我們可以寫出可以重用的ResourcePool的框架來,它也是以犧牲程序的類型安全來保障的。用戶需要顯式地加入downcast來使用這個框架。
因此,既然我們反正也只是學術上的探討,讓我們在這里使用類似Generic Java的語法,來使我們的程序看起來更美。
首先是ResourceFactory接口:
public interface ResourceFactory<R>{
R createResource();
}
ResourceCollector接口:
public interface ResourceCollector<R>{
void closeResource(R r);
}
ResourceMan接口,仍然是一個對實現者友好,但需要封裝才能交給客戶的接口。
public interface ResourceMan<R>{
R getResource()
throws Throwable;
void clear();
void releaseResource(R r);
}
下面是一個使用我們的ConnectionManImpl的pooling邏輯的ResourceMan的實現。
public class ResourceManImpl<R> implements ResourceMan<R>{
typedef ResourceMan<R> Man;
typedef ResourceFactory<R> Factory;
typedef ResourceCollector<R> Collector;
private final Factory factory;
private final Collector collector;
private int client;
private final Vector<R> freeList = new Vector<R>();
private final int maxClients;
static public Man instance(Factory f, Collector c, int m){
return new ResourceManImpl(f, c, m);
}
private ResourceManImpl(Factory f, Collector c, int m)
{
this.maxClients = m;
this.factory = f;
this.collector = c;
}
public final synchronized void releaseResource(R resource)
{
freeList.addElement(resource);
client--;
notify();
}
public final synchronized R getResource()
throws Throwable
{
R ret;
如果pool里有R
從pool中去掉一個R r;
clients++;
ret = r;
否則,如果clients<maxClients
r = factory.createResource();
clients++;
ret = r;
否則,wait(),直到pool中有空閑R
return ret;
}
public synchronized void clear(){
對每一個再free list中的資源r:
collector. closeResource (r);
freeList.removeAllElements();
}
}
呵呵,這樣,你的公司可以雇傭幾個只鉆研各種pooling算法的人,讓他們什么都不想,只寫不同的pooling的算法,也就是不同的ResourceMan的實現。
對于一個具體的資源,我們只需要給出ResourceFactory和ResourceCollector的實現,就可以實例化一個ResourceMan, 再根據具體的資源,寫出封裝類和轉換類,(這其中,封裝類稍微麻煩一些,但也不過是一些委托和幾個跟pooling相關的特殊函數的處理),一個對那個具體資源的pool就可以出爐了。你甚至可以成立一個公司,專門賣各種pool給其他的開發公司。
于是,假設你手頭有五種pooling算法,三種需要pool的資源,每種資源有四種創建方法,兩種釋放方法,那么,你就可以任意組合,而形成5*3*4*2=90種不同的pool. 這就是面向接口編程的威力!你不再是僅開發一個順序執行的流程,不再是在已有的一些類上面做一些修修改改,擴展覆蓋。你是把一個一個插座和插頭插到一起去!只要兩者的接口一致,你就可以任意組合。
美妙吧?更美妙的是,不知道你注意到沒有,在我們的實現中,并沒有對某種特定情況的假設。你不會因為我們對某種你用不到的功能的照顧而犧牲到你想用的功能的效率。你不會拿到一個1000行的代碼,而其中900行都是為實現你根本用不到的功能。
當然,要說效率沒有因為照顧一般性而受到絲毫影響,卻也不是。請注意ResourceManImpl.clear()方法。這個方法的目的是釋放所有在池中的空閑資源,并清空資源池。
我們目前的實現是,遍歷池中所有資源,調用ResourceCollector.closeResource方法來釋放。然后再清空資源池。
這對于Connection Pool是很好的。我們只需要實現一個簡單的ConnectionCollector, 就象這樣:
public class ConnectionCollector implements ResourceCollector<Connection>{
public void closeResource(Connection conn){
conn.close();
}
private ConnectionCollector(){}
private static final test.res.ResourceCollector<Connection> singleton = new ConnectionCollector();
public static test.res.ResourceCollector<Connection> instance(){return singleton;}
}
然后把它交給一個我們選定的ResourceMan的實現類。
但是,實際上,并不是所有的資源都需要顯式釋放的。如thread, 我們只需要去掉對該thread的引用,它就會最終被垃圾收集器釋放。我們根本不需要遍歷空閑線程來釋放它們。
當然,你可以傳給這個ResourceMan一個ResourceCollector, 在它的closeResource方法里,什么也不干。這樣,功能還是被實現了。
不過,這種方法的不足之處在于,它把一個本來可以是O(1)的clear()方法變成了O(n)的。Java雖然并不對效率錙殊必較,比如,你不需要對一個虛函數的調用開銷過于擔心。但增大一個方法的復雜度總是一個非常非常不好的事。
而且,(僅僅是想象)也許對一些特殊的資源,它的回收一定要基于某種邏輯次序呢?
如何解決這種對所有空閑資源的釋放的策略問題呢?如果要求寫pooling算法的人,對不需要調用資源釋放,或者有特殊釋放順序要求的資源再重新寫一個pooling的實現,顯然是不好的。他怎么重用他那些聰明的pooling算法呢?copy paste嗎?
相信許多人的第一感一定還是:重載clear().
但,就象我們前面說的:
第一:這會產生過多的類
第二:占用了唯一的父類的位置。Java里一個類只能有一個父類,如果你的框架要求用戶繼承你的類,你也就剝奪了用戶繼承其它類的權利。
第三:父類子類之間的耦合是大型系統的大敵。父類一旦發布出去,再想更改(比如說,加個私有方法什么的),非常困難!(因為你不知道是否某個子類已經使用了這個函數signature.)
怎么辦呢?還是interface啊!
我們可以加入一個負責回收所有資源的接口。然后實現一個什么也不做得類給thread pool. (保證了clear()的O(1)的運算復雜度);再實現特殊順序回收給我們假想的資源;再實現普通的順序回收類給一般的資源如connection.
這個接口類似于這樣:
public interface ResourcesCollector<R>{
public void collect(Collection<R> rs);
}
修改后的ResourceManImpl的代碼是這樣:
public class ResourceManImpl<R> implements ResourceMan<R>{
typedef ResourceMan<R> Man;
typedef ResourceFactory<R> Factory;
typedef ResourcesCollector<R> Collector;
private final Factory factory;
private final Collector collector;
private int client;
private final Vector<R> freeList = new Vector<R>();
private final int maxClients;
static public Man instance(Factory f, Collector c, int m){
return new ResourceManImpl(f, c, m);
}
private ResourceManImpl(Factory f, Collector c, int m)
{
this.maxClients = m;
this.factory = f;
this.collector = c;
}
public final synchronized void releaseResource(R resource)
{
freeList.addElement(resource);
client--;
notify();
}
public final synchronized R getResource()
throws Throwable
{
R ret;
如果pool里有R
從pool中去掉一個R r;
clients++;
ret = r;
否則,如果clients<maxClients
r = factory.createResource();
clients++;
ret = r;
否則,wait(),直到pool中有空閑R
return ret;
}
public synchronized void clear(){
collector. collect(freeList);
freeList.removeAllElements();
}
}
給thread pool的什么也不干的ResourcesCollector的實現是這樣:
public class NopResourcesCollector implements ResourcesCollector<R>{
private static final NopResourcesCollector singleton = new NopResourcesCollector();
private NopResourcesCollector(){}
public void collect(Collection<R> rs){}
public static ResourcesCollector<R> instance(){
return singleton;
}
}
(題外話:這里的NopResourcesCollector有點特別,它不是一個帶類型參數的類,相反,它是一個普通的類,但它卻實現了任意ResourcesCollector<R>接口。
這種特殊的語法是C++ template和Generic Java所不支持的。它是實驗室里的一種“趣向”。它的作用是避免了多個singleton實例。對所有的資源R, 無論是Connection, Thread 還是其它什么,都共享一個NopResourcesCollector. 這是安全的,因為不管怎么說, NopResourcesCollector并不做任何事情。)
給Connection pool之類的普通得ResourcesCollector的實現是這樣:
public class LinearResourcesCollector<R> implements ResourcesCollector<R>{
private final ResourceCollector<R> c;
private LinearResourcesCollector(ResourceCollector<R> c){
this.c = c;
}
public void collect(Collection<R> rs){
對Collection中的每個r
c.collect(r);
}
public static ResourcesCollector<R> instance(ResourceCollector c){
return new LinearResourcesCollector<R>(c);
}
}
從一個ConnectionPool的實現看design pattern的運用 (續六) 選擇自 ajoo 的 Blog |
|
|
|
| |
|
這種ResourcesCollector的方法也有一點美中不足的地方,那就是,我們把我們在ResourceManImpl中使用java.util.Collection的實現細節暴露給了ResourcesCollection。如果一個ResourceMan的實現者不想用Collection,那就不太容易了。
你可以說,反正Collection是個interface, 我們可以讓那個ResourceMan的實現者寫一個adapter, 不就行了?
是啊。理論上是可以。但是,該死的Sun在Collection里面定義了太多的方法。而一些方法竟然是optional的。也就是說,如果一個ResourcesCollector的實現者使用了某個optional方法,編譯器不會報錯。而如果一個ResourceMan的實現者使用的容器并不支持這些optional的方法,編譯器也不會報錯。但是,當你把兩者組裝起來的時候,通!OperationNotSupportedException!
哎,要定義一個既要滿足所有可能的ResourcesCollecor的要求 (如順序存取,隨機存取等等),又要讓所有可能的ResourceMan都能實現的容器的接口是太困難了!
我想,這種要求應該是無解的。因為,ResourceMan和ResourcesCollector之間并不是足夠松的耦合。概念上,ResourceMan必須選擇一種能夠符合ResourcesCollector的要求的容器。這就是它們之間需求上自然定義的耦合。所以,不存在一個可解除它們之間的耦合的完美解也就不值得驚訝了。
好在,ResourcesCollector只是ResourceMan功能的一個小子模塊。它們之間如何組織并不影響我們整個pooling 框架系統。
下面,讓我總結一下我們的pooling的框架系統的結構:
1. 可重用的pooling邏輯:
ResourceMan: 實現純pooling算法邏輯,對具體的資源種類保持最大的透明度。
ResourceFactory: 用來封裝資源生成邏輯,需要組合進ResourceMan.
ResourcesCollector: 用來封裝整組資源回收,需要組合進ResourceMan。
ResourceCollector: 用來封裝個體資源回收,需要組合進ResourcesCollector.
2.
PooledConnection: 封裝Connection對象,使之與pool協調工作。
3.
ResourceMan2ConnectionPool: 類似于ConnectionMan2ConnectionPool, 負責使用PooledConnection來把一個不能對用戶容錯,對用戶不透明的ResourceMan<Connection>轉化成對用戶安全透明的ConnectionPool.
要實現一個ConnectionPool,
1。 我們先要找一個合適的pooling邏輯,也就是一個ResourceMan的實現類,
2。 然后, 根據資源的特性,決定使用哪一個ResourcesCollector. 比如,為ConnectionPool使用LinearResourcesCollector<Connection>; 為thread 使用NopResourcesCollector.
3。因為Connection資源需要對單個資源進行釋放,把ConnectionCollector交給LinearResourceCollector<Connection>
4。構造ResourceMan的對象實例。
5。 使用ResourceMan2ConnectionPool類把ResourceMan轉換為ConnectionPool. ResourceMan2ConnectionPool會使用PooledConnection進行封裝。
在以上的步驟中,還有一些共性可以提取。雖然用戶自己來選購ResourceMan 和ResourceConnection<Connection>, 但對ConnectionPool來說,ResourcesCollector, ResourceCollector的實現都是固定的, ResourceMan2ConnectionPool也是固定使用的。
如果我們抽象這個過程, 可不可以是我們的用戶接口更加簡單呢?理想中,接口應該是這樣:
public interface ResourceManFactory<R>{
public ResourceMan<R> getResourceMan(ResourceFactory<R> factory, ResourcesCollector<R> collector);
}
public class ConnectionPoolFactory{
public static ConnectionPool
getConnectionPool(ResourceManFactory<Connection> mf, ResourceFactory<Connection rf){
return mf.getResourceMan(rf,
LinearResourcesCollector<Connection>.instance(
ConnectionCollector.instance()
)
}
}
這樣,構造ConnectionPool的人,只需要選擇合適的ResourceManFactory 和ResourceFactory<Connection>, 其它什么都不用管了。不能再簡單了。
實現pooling 算法的人,只需要研究他的算法,其它什么都不用管了。不能再簡單了。
實現ResourceFactory<Connection>的人,只需要關注怎么連接數據庫,其它什么都不用管了。不能再簡單了。
一些橋接Connection和Resource的類都已經寫好了。不用管它們。
實現封裝Connection或其它Resource的人,只需要關注那個資源的接口和語義,做出適當對pool邏輯的調整,其它什么都不用管了。不能再簡單了。
從一個ConnectionPool的實現看design pattern的運用 (七)
這里是bonmot對這個Connection pool的一個意見:
pooled Connection可能由于一個client忘記關閉,而導致整個pool阻塞。所以,應該對pooled Connection進行監控,對于超時的或其他invaild狀態的pooled connection強制回收。
下面,讓我們來分析這個需求。
首先,這樣一個監視進程的邏輯可能是什么樣的呢? 如果我們對超時的定義是:該連接從被分配出去到現在已經太久了。那么,我們需要在該連接對象上記錄它被分配出去的時間。然后,在一個后臺運行的daemon線程中定期檢查這些正在使用的Connection. 而如果我們的超時又包括了對該connection使用的頻繁程度,比如說:該連接已經有兩個小時沒人動過了,(這里,“動過”又需要定義。是只要某個成員函數被調用了就算被“動過”了嗎?還是所有從該連接生成的對象,如Statement, ResultSet等等都算?)那我們就要重載我們該興趣的方法,紀錄該方法被調用的時間。
其次,一般來說,監視已分配連接和管理空閑連接之間到底有多大耦合呢?能否對它們解耦呢?經過分析,我感覺,答案是:不能。監視已分配連接的算法理論上有可能需要知道空閑連接的一些信息,而反之也是一樣。而且,更討厭的是,它們之間所需要的信息量無法估計,也就是說,對一些特定的算法,它們可能是完全的緊耦合。如果按這樣分析,這種ConnectionPool可能還得要求實現者直接實現ConnectionPool, 就象我們第三章里使用的方法,只能偶爾使用一些utility類,象PooledConnection之類。 不過,雖然我們不能完全把監視算法和分配算法分開。但事實上很多監視算法,分配算法確實是互不相關的。比如象我們剛才分析的需求。所以我們也許可以寫一個框架,簡化對這些互不相關的算法的實現。雖然對完全緊耦合的情況我們無能為力,但對多數普通的情況,我們還是可以有些作為的。而且,這樣一個框架并不影響對復雜的緊耦合情況的特殊實現。
好吧,現在就讓我們著手構建這個框架。我們的目標是:定義一個Monitor的接口,負責監視所有分配出去的連接。然后,把一個Monitor對象,一個ConnectionPooling對象組合成一個ConnectionPool.
算法決定數據結構,首先是需要紀錄的時間信息: public interface Momento{ java.util.Date getTimestamp(); } 其次,我們的監視類需要知道怎樣強行回收一個Connection: public interface ResourceProxy{ Momento getBirthMomento(); void release(); boolean isReleased(); } 注意,這里,我們的ResourceProxy并不與Connection直接相關。這樣,任何的資源,只要實現了這個接口,都可以被我們的監視類所監視。
然后是監視類的接口: public interface ResourceProxyMonitor{ public void addResourceProxy(ResourceProxy proxy); } 這個接口在connection被返回出ConnectionPool之前被調用,把分配的Connection注冊給監視類。
下面是監視類的實現: public class SimpleResourceProxyMonitor implements ResourceProxyMonitor{ public synchronized void addResourceProxy(ResourceProxy proxy){ list.add(proxy); } private final java.util.List list = new java.util.LinkedList(); private final HeartBeatEngine hb= HeartBeatEngine.instance(); private final void releaseProxy(ResourceProxy proxy){proxy.release();} public final Runnable getMonitor(final long interval, final long ttl){ return hb.getThreadProc(interval, new HeartBeat(){ public boolean beat(){ final java.util.Date now = new java.util.Date(); synchronized(SimpleResourceProxyMonitor.this){ for(java.util.Iterator it =list.iterator();it.hasNext();){ final ResourceProxy proxy = (ResourceProxy)it.next(); final java.util.Date then = proxy.getMomento().getTimestamp(); if(now.getTime()-then.getTime()>=ttl){ releaseProxy(proxy); } if(proxy.isReleased()){ it.remove(); } } } return true; } }); } public final synchronized void clear(){ turnoffMonitors(); for(java.util.Iterator it=list.iterator();it.hasNext();){ final ResourceProxy proxy = (ResourceProxy)it.next(); releaseProxy(proxy); } list.clear(); } public final synchronized void empty(){ turnoffMonitors(); list.clear(); } public final void turnonMonitors(){ hb.turnon(); } public final void turnoffMonitors(){ hb.turnoff(); } private SimpleResourceProxyMonitor(){} public static SimpleResourceProxyMonitor instance(){ return new SimpleResourceProxyMonitor(); } }
以及兩個輔助接口和類:HeartBeat和HeartBeatEngine, 負責daemon線程的睡與醒。 public interface HeartBeat{ public boolean beat(); file://return true if continue, false otherwise; }
public class HeartBeatEngine{ public Runnable getThreadProc(final long interval, final HeartBeat r){ return new Runnable(){ public void run(){ for(;isOn();){ try{ Thread.sleep(interval); }catch(InterruptedException e){} synchronized(HeartBeatEngine.this){ if(!isOn())return; } if(!r.beat())return; } } }; } private boolean down = false; private HeartBeatEngine(boolean d){ this.down = d; } public final synchronized void turnon(){ down = false; } public final synchronized void turnoff(){ down = true; } private final boolean isOn(){ return !down; } public static HeartBeatEngine instance(){ return new HeartBeatEngine(false); } }
這里,getMonitor()僅僅返回一個Runnable, 而不是直接啟動Thread。這樣做更加靈活。使用這個monitor類的客戶可以自由地使用這個Runnable, 比如說,使用一個線程池。
然后,我們需要一個proxy類來記錄連接被分配的時間:
public class PooledConnectionProxy implements ResourceProxy{ public final Momento getMomento(){return momento;} public void release(){ try{ conn.close(); }catch(SQLException e){ System.err.println(e.getMessage()); } } file://the conn.close() method has to be synchronized public boolean isReleased(){ try{ return conn.isClosed(); }catch(SQLException e){return false;} } private final Momento momento; private final Connection conn; private PooledConnectionProxy(Momento momento, Connection conn){ this.momento = momento; this.conn = conn; } public static ResourceProxy instance(Momento momento, Connection conn){ return new PooledConnectionProxy(momento, conn); } }
好了,現在我們可以把它們組裝在一起,做出一個ConnectionPool來。 還記得我們的ConnectionPooling2Pool嗎?它負責封裝ConnectionPooling并對每一個連接進行封裝。當時我們把封裝邏輯寫入了ConnectionPooling2Pool, 因為封裝邏輯只有一種。 但現在,我們有了另一種封裝邏輯。所以, refactor. 了解我的人應該知道,我是不會用template method pattern, 繼承ConnectionPooling2Pool然后重載wrapup函數的。用組合!
ConnectionPooler是一個代表封裝Connection的接口: public interface ConnectionPooler{ public Connection pool(Connection conn, ConnectionHome home) throws SQLException; } ConnectionPooling2Pool將使用ConnectionPooler進行封裝。 public class ConnectionPooling2Pool implements ConnectionPool{ public final Connection getConnection() throws test.res.ResourceNotAvailableException, SQLException{ return wrapup(pooling.getConnection()); } public final Connection getConnection(long timeout) throws test.res.ResourceTimeOutException, SQLException{ return wrapup(pooling.getConnection(timeout)); } private final Connection wrapup(Connection conn) throws SQLException{ return pl.pool(conn, pooling); } public final void clear(){ pooling.clear(); } private final ConnectionPooling pooling; private final ConnectionPooler pl; private ConnectionPooling2Pool(ConnectionPooling pooling, ConnectionPooler pl){ this.pooling = pooling; this.pl = pl; } public static ConnectionPool bridge(ConnectionPooling pooling, ConnectionPooler pl){ return new ConnectionPooling2Pool(pooling, pl); } }
原來的封裝邏輯被實現為: public class SimpleConnectionPooler implements ConnectionPooler{ public Connection pool(Connection conn, ConnectionHome home) throws SQLException{ return PooledConnection.decorate(conn, home); } private SimpleConnectionPooler(){} private static final ConnectionPooler singleton = new SimpleConnectionPooler(); public static ConnectionPooler instance(){return singleton;} }
我們新的封裝邏輯為: public class MonitoredConnectionPooler implements ConnectionPooler{ public Connection pool(Connection conn, ConnectionHome home) throws SQLException{ final Connection pooled = PooledConnection.decorate(conn, home); monitor.addResourceProxy( PooledConnectionProxy.instance(factory.newMomento(), pooled) ); return pooled; } private final MomentoFactory factory; private final ResourceProxyMonitor monitor; private MonitoredConnectionPooler(ResourceProxyMonitor mon, MomentoFactory factory){ this.monitor = mon; this.factory = factory; } public static ConnectionPooler instance(ResourceProxyMonitor mon, MomentoFactory factory){ return new MonitoredConnectionPooler(mon, factory); } } 最終的組合代碼為: public class TestConnectionPool{ public static void test(String driver, String url, String user, String pwd) throws java.sql.SQLException, test.res.ResourceNotAvailableException, test.res.ResourceTimeOutException, ClassNotFoundException{ final ConnectionPool pool = ConnectionPooling2Pool.bridge( ConnectionPoolingImpl.instance( ConnectionFactoryImpl.instance( driver, url, user, pwd), 1000), SimpleConnectionPooler.instance() ); final SimpleResourceProxyMonitor mon = SimpleResourceProxyMonitor .instance();
final ConnectionPool pool2 = ConnectionPooling2Pool.bridge( ConnectionPoolingImpl.instance( ConnectionFactoryImpl.instance( driver, url, user, pwd), 1000), MonitoredConnectionPooler.instance( mon, SimpleMomentoFactory.instance()) ); final Runnable monproc = mon.getMonitor(1000L, 1000000L); new Thread(monproc).start(); } }
對connection的使用頻繁程度的監視,因為算法所要求的數據結構會有所不同,所以會需要自己的一套ResourceProxy, ResourceProxyMonitor接口以及對Connection甚至其它Connection生成對象的進行同步處理和記錄存取時間的封裝。但實現的機理是相似的。
|