从一个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, 他想不转换也不行啊!
什么?今天的家庭作业?
啊,让你们家长写表扬信给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这种强类型语言的原则,也是所有程序员应该尽力遵循的。靠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生成对象的进行同步处理和记录存取时间的封装。但实现的机理是相似的。
|