mvnforum之数据源管理(Decorator)
写这篇文章的目的是结合实际应用说明一下Decorator模式的使用,前提是要对Decorator模式的基本概念比较清晰,具体的Decorator介绍就不在这里叙述了。 前两天花了点时间装了个开源的project:mvnforum论坛,想学习一下这个项目里面的一些设计思想和实现,上午又花了点时间看了一下,先从它的底层封装数据库的一些代码看起的,看着看着,发现mvnforum的底层数据源和Connection管理就是一个典型的Decorator模式,所以就上来说一下,当然我知道,用Decorator模式来设计数据源的管理是一个十分常见的设计思路,这里主要是讲一下它的具体实现。 我还是列举了一些相对比较容易看懂的代码来说明,毕竟这个项目里有的地方还是比较复杂的,我旨在说明问题,所以一切从简。 我们知道,关于底层数据库的操作是每个项目都需要设计的,而关于数据资源的管理也是设计的重中之重,会有数据库连接池,当然,各大应用服务器都支持POOL的配置,来统一管理和调度这些资源,因为我们知道,创建应用与数据库之间的连接并不是我们看上去那么简单的,过程是非常繁琐的,是非常消耗资源的,所以这方面的设计是体现性能的关键,我们建议使用应用服务器中提供的这种数据库连接池配置,专业的服务器厂商对于这方面的设计和实现是有保障的,当然,也不是说自己不可以去设计,mvnforum就提供了两重设计,第一种是使用服务器配置的数据库连接池,如果你不使用也可以,mvnforum提供了一个自我实现的connection pool,并且进行了封装。大概的一个架构是这样的,如图:(祥见附件) 好了,我来解释一下这个图吧,既然我们要用Decorator模式,那么究竟为什么要用这个模式呢,我的理解是这样的,当应用程序和数据库建立连接后,当我们使用完这个连接后,理所当然的需要关闭,然而这样就带来的性能的损失,也不符合我们用数据库连接池的宗旨,你一旦con.close()那么完了,下次我要用又得去连接,比较理想的解决方案是我们开辟连接,并保持这种状态在连接池里,外程序要使用就可以来调用,池分配一个没有被使用的但是已经连接上的Connection给你,用完后,你没有权力去关闭它,你还给我(池),然后我来决定是不是要关闭,但是在我们使用时,得到一个Connection很容易,但是关闭一个Connection同样很容易,作为池来说,我不能保证外面的程序不会不经过我就把Connection给close了,这样我就很不爽了。那么怎么杜绝这样的一种情况呢,呵呵,Decorator模式就派上用场了,好来看看吧。 ConnectionWrapper这个class就是一个Decorator类,我贴下代码给大家看看,它实现了Connection接口。
/* * $Header: /cvsroot/mvnforum/myvietnam/src/net/myvietnam/mvncore/db/ConnectionWrapper.java,v 1.6 2007/09/26 04:11:07 minhnn Exp $ * $Author: minhnn $ * $Revision: 1.6 $ * $Date: 2007/09/26 04:11:07 $ * * ==================================================================== * * Copyright (C) 2002-2007 by MyVietnam.net * * All copyright notices regarding MyVietnam and MyVietnam CoreLib * MUST remain intact in the scripts and source code. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * * Correspondence and Marketing Questions can be sent to: * info at MyVietnam net * * @author: Minh Nguyen */package net.myvietnam.mvncore.db;import java.sql.*;import java.util.Map;import java.util.Properties;import org.apache.commons.lang.NotImplementedException;public class ConnectionWrapper implements Connection { private DBConnectionManager connectionManager = null; private static int outsideConnection = 0; Connection delegate = null; ConnectionWrapper(Connection original, DBConnectionManager conManager) { if (original == null) { throw new IllegalArgumentException("Cannot accept the connection is null."); } if (conManager == null) { throw new IllegalArgumentException("Cannot accept the DBConnectionManager is null."); } delegate = original; connectionManager = conManager; outsideConnection++; } private void makeSureNotClose() { if (delegate == null) { throw new IllegalStateException("Connection has been closed (delegate == null)."); } } public void close() throws SQLException { //delegate.close(); if (delegate != null) { connectionManager.freeConnection(delegate); delegate = null; outsideConnection--; } } public void clearWarnings() throws SQLException { makeSureNotClose(); delegate.clearWarnings(); } public void commit() throws SQLException { makeSureNotClose(); delegate.commit(); } public Statement createStatement() throws SQLException { makeSureNotClose(); return delegate.createStatement(); } public Statement createStatement(int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException { makeSureNotClose(); return delegate.createStatement(resultSetType, resultSetConcurrency, resultSetHoldability); } public Statement createStatement(int resultSetType, int resultSetConcurrency) throws SQLException { makeSureNotClose(); return delegate.createStatement(resultSetType, resultSetConcurrency); } public boolean getAutoCommit() throws SQLException { makeSureNotClose(); return delegate.getAutoCommit(); } public String getCatalog() throws SQLException { makeSureNotClose(); return delegate.getCatalog(); } public int getHoldability() throws SQLException { makeSureNotClose(); return delegate.getHoldability(); } public DatabaseMetaData getMetaData() throws SQLException { makeSureNotClose(); return delegate.getMetaData(); } public int getTransactionIsolation() throws SQLException { makeSureNotClose(); return delegate.getTransactionIsolation(); } public Map getTypeMap() throws SQLException { makeSureNotClose(); return delegate.getTypeMap(); } public SQLWarning getWarnings() throws SQLException { makeSureNotClose(); return delegate.getWarnings(); } public boolean isClosed() throws SQLException { makeSureNotClose(); return delegate.isClosed(); } public boolean isReadOnly() throws SQLException { makeSureNotClose(); return delegate.isReadOnly(); } public String nativeSQL(String sql) throws SQLException { makeSureNotClose(); return delegate.nativeSQL(sql); } public CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException { makeSureNotClose(); return delegate.prepareCall(sql, resultSetType, resultSetConcurrency, resultSetHoldability); } public CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency) throws SQLException { makeSureNotClose(); return delegate.prepareCall(sql, resultSetType, resultSetConcurrency); } public CallableStatement prepareCall(String sql) throws SQLException { makeSureNotClose(); return delegate.prepareCall(sql); } public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException { makeSureNotClose(); return delegate.prepareStatement(sql, resultSetType, resultSetConcurrency, resultSetHoldability); } public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency) throws SQLException { makeSureNotClose(); return delegate.prepareStatement(sql, resultSetType, resultSetConcurrency); } public PreparedStatement prepareStatement(String sql, int autoGeneratedKeys) throws SQLException { makeSureNotClose(); return delegate.prepareStatement(sql, autoGeneratedKeys); } public PreparedStatement prepareStatement(String sql, int[] columnIndexes) throws SQLException { makeSureNotClose(); return delegate.prepareStatement(sql, columnIndexes); } public PreparedStatement prepareStatement(String sql, String[] columnNames) throws SQLException { makeSureNotClose(); return delegate.prepareStatement(sql, columnNames); } public PreparedStatement prepareStatement(String sql) throws SQLException { makeSureNotClose(); return delegate.prepareStatement(sql); } public void releaseSavepoint(Savepoint savepoint) throws SQLException { makeSureNotClose(); delegate.releaseSavepoint(savepoint); } public void rollback() throws SQLException { makeSureNotClose(); delegate.rollback(); } public void rollback(Savepoint savepoint) throws SQLException { makeSureNotClose(); delegate.rollback(savepoint); } public void setAutoCommit(boolean autoCommit) throws SQLException { makeSureNotClose(); delegate.setAutoCommit(autoCommit); } public void setCatalog(String catalog) throws SQLException { makeSureNotClose(); delegate.setCatalog(catalog); } public void setHoldability(int holdability) throws SQLException { makeSureNotClose(); delegate.setHoldability(holdability); } public void setReadOnly(boolean readOnly) throws SQLException { makeSureNotClose(); delegate.setReadOnly(readOnly); } public Savepoint setSavepoint() throws SQLException { makeSureNotClose(); return delegate.setSavepoint(); } public Savepoint setSavepoint(String name) throws SQLException { makeSureNotClose(); return delegate.setSavepoint(name); } public void setTransactionIsolation(int level) throws SQLException { makeSureNotClose(); delegate.setTransactionIsolation(level); } public void setTypeMap(Map map) throws SQLException { makeSureNotClose(); delegate.setTypeMap(map); } public Array createArrayOf(String typeName, Object[] elements) throws SQLException { throw new NotImplementedException("createArrayOf"); } public Blob createBlob() throws SQLException { throw new NotImplementedException("createBlob"); } public Clob createClob() throws SQLException { throw new NotImplementedException("createClob"); } public NClob createNClob() throws SQLException { throw new NotImplementedException("createNClob"); } public SQLXML createSQLXML() throws SQLException { throw new NotImplementedException("createSQLXML"); } public Struct createStruct(String typeName, Object[] attributes) throws SQLException { throw new NotImplementedException("createStruct"); } public Properties getClientInfo() throws SQLException { throw new NotImplementedException("getClientInfo"); } public String getClientInfo(String name) throws SQLException { throw new NotImplementedException("getClientInfo"); } public boolean isValid(int timeout) throws SQLException { throw new NotImplementedException("isValid"); } public void setClientInfo(Properties properties) throws SQLClientInfoException { throw new NotImplementedException("setClientInfo"); } public void setClientInfo(String name, String value) throws SQLClientInfoException { throw new NotImplementedException("setClientInfo"); } public boolean isWrapperFor(Class iface) throws SQLException { throw new NotImplementedException("isWrapperFor"); } public Object unwrap(Class iface) throws SQLException { throw new NotImplementedException("unwrap"); }} /* * $Header: /cvsroot/mvnforum/myvietnam/src/net/myvietnam/mvncore/db/DBConnectionManager.java,v 1.22 2007/11/22 04:40:29 minhnn Exp $ * $Author: minhnn $ * $Revision: 1.22 $ * $Date: 2007/11/22 04:40:29 $ * * ==================================================================== * * Copyright (C) 2002-2007 by MyVietnam.net * * All copyright notices regarding MyVietnam and MyVietnam CoreLib * MUST remain intact in the scripts and source code. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * * Correspondence and Marketing Questions can be sent to: * info at MyVietnam net * * @author: Minh Nguyen * @author: Mai Nguyen */package net.myvietnam.mvncore.db;import java.sql.*;import java.util.*;import net.myvietnam.mvncore.MVNCoreConfig;import org.apache.commons.logging.Log;import org.apache.commons.logging.LogFactory;/** * This class is a Singleton that provides access to the * connection pool. A client gets access to the single * instance through the static getInstance() method * and can then check-out and check-in connections from a pool. * When the client shuts down it should call the release() method * to close all opened connections and do other clean up. */class DBConnectionManager { private static Log log = LogFactory.getLog(DBConnectionManager.class); private static final int TIME_BETWEEN_RETRIES = 500; // O.5 second // static variable static private DBConnectionManager instance = null; // The single instance // instance variable private DBConnectionPool pool = null;// please be careful if u want to make this variable static private static Map dbManagers = new HashMap(); private static final int MANAGER_MAX = 5; /** * A private constructor since this is a Singleton * Note: This constructor is lightweight since DBConnectionPool is lightweight, * so no connection is created until the first time getConnection() is called */ private DBConnectionManager() { String driverClassName = MVNCoreConfig.getDriverClassName(); try { Class.forName(driverClassName).newInstance(); } catch (Exception e) { log.fatal("DBConnectionManager: Unable to load driver = " + driverClassName, e); } String url = MVNCoreConfig.getDatabaseURL(); String user = MVNCoreConfig.getDatabaseUser(); String password = MVNCoreConfig.getDatabasePassword(); int maxConnection = MVNCoreConfig.getMaxConnection(); //always new the pool because pool is an instance variable pool = new DBConnectionPool(url, user, password, maxConnection); } private DBConnectionManager(DBOptions dbOptions) { String driverClassName = dbOptions.getDriverClass(); try { Class.forName(driverClassName).newInstance(); } catch (Exception e) { log.fatal("DBConnectionManager: Unable to load driver = " + driverClassName, e); } String url = dbOptions.getDbUrl(); String user = dbOptions.getUsername(); String password = dbOptions.getPassword(); int maxConnection = dbOptions.getConMax(); //always new the pool because pool is an instance variable pool = new DBConnectionPool(url, user, password, maxConnection); } /** * Returns the single instance, creating one if it's the * first time this method is called. * * @return DBConnectionManager The single instance. */ /* public static synchronized DBConnectionManager getInstance() { if (instance == null) { DBOptions option = new DBOptions(); instance = new DBConnectionManager(option); } return instance; }*/ /** * Returns the single instance, creating one if it's the * first time this method is called. * * @return DBConnectionManager The single instance. */ /* private static synchronized DBConnectionManager getInstance(DBOptions option) { if (instance == null) { if (option == null) { option = new DBOptions(); } instance = new DBConnectionManager(option); } return instance; }*/ /** * DBUtil use this method */ public static synchronized DBConnectionManager getInstance(boolean useConfig) { if (instance == null) { instance = new DBConnectionManager(); } return instance; } /** * DBUtil2 use this method */ public static synchronized DBConnectionManager getDBConnectionManager(DBOptions dbOptions) { if (dbOptions == null) { throw new IllegalArgumentException("Cannot get DBConnectionManager. Missing DBOptions."); } if ( (dbOptions.getDbManagerName() == null) || (dbOptions.getDbManagerName().length() == 0) ) { throw new IllegalArgumentException("Cannot get DBConnectionManager. Missing [Database Connection Manager Name]."); } DBConnectionManager dbManager = (DBConnectionManager)dbManagers.get(dbOptions.getDbManagerName()); if (dbManager == null) { dbManager = createDbConnectionManager(dbOptions); } return dbManager; } private static DBConnectionManager createDbConnectionManager(DBOptions dbOptions) { if (dbManagers.size() >= MANAGER_MAX) { throw new IllegalStateException("System only support max " + MANAGER_MAX + " DBConnectionManager(s)"); } DBConnectionManager instance = new DBConnectionManager(dbOptions); dbManagers.put(dbOptions.getDbManagerName(), instance); return instance; } /** * Returns a connection to the pool. * * @param con The Connection */ void freeConnection(Connection con) { pool.freeConnection(con); } /** * Returns an open connection. If no one is available, and the max * number of connections has not been reached, a new connection is * created. * * @return Connection The connection or null */ Connection getConnection() { return getConnection(0); } /** * Returns an open connection. If no one is available, and the max * number of connections has not been reached, a new connection is * created. If the max number has been reached, waits until one * is available or the specified time has elapsed. * * @param time The number of milliseconds to wait * @return Connection The connection or null */ Connection getConnection(long time) { Connection connection = pool.getConnection(time); if (connection == null) { return null; } try { // we always setAutoCommit(true) for backward compatible with mvnForum connection.setAutoCommit(true); } catch (SQLException e) { log.error("Cannot setAutoCommit", e); } ConnectionWrapper wrapper = new ConnectionWrapper(connection, this); return wrapper; } /** * Closes all open connections. * @return true if the pool is empty and balance * false if the pool has returned some connection to outside */ boolean release() { return pool.release(); } /** * This inner class represents a connection pool. It creates new * connections on demand, up to a max number if specified. * It also checks to make sure that the connection is still open * before it is returned to a client. */ class DBConnectionPool { private int checkedOut = 0;//NOTE: this variable should be changed in synchronized method only private Vector freeConnections = new Vector(); private int maxConn = 0; private String password = null; private String URL = null; private String user = null; /** * Creates new connection pool. * NOTE: new an instance of this class is lightweight since it does not create any connections * * @param URL The JDBC URL for the database * @param user The database user, or null * @param password The database user password, or null * @param maxConn The maximal number of connections, or 0 for no limit */ public DBConnectionPool(String URL, String user, String password, int maxConn) { this.URL = URL; this.user = user; this.password = password; this.maxConn = maxConn; } /** * Checks in a connection to the pool. Notify other Threads that * may be waiting for a connection. * * @todo: Maybe we dont need notifyAll(); ??? * * @param con The connection to check in */ synchronized void freeConnection(Connection con) { // Put the connection at the end of the Vector if (con != null) {//make sure that the connection is not null if (checkedOut <= 0) { // this means that connection is open too much // There are 2 cases: // 1. Not get from this connection pool (maybe get directly) // 2. this connection is gotten and then the whole pool is released // In these case, just close the connection try { log.debug("DBConnectionManager: about to close the orphan connection."); con.close(); } catch (SQLException ex) { } } else { // Return this connection to the pool // note that we dont have to check if the connection is not connected // this will be check in the getConnection method freeConnections.addElement(con); // FIXME: posible negative value // NOTE: checkOut should never be negative here checkedOut--; // NOTE: this number can be negative (in case connection does not come from the pool) notifyAll(); // can I remove it ??? } } } /** * Checks out a connection from the pool. If no free connection * is available, a new connection is created unless the max * number of connections has been reached. If a free connection * has been closed by the database, it's removed from the pool * and this method is called again recursively. */ synchronized Connection getConnection() { Connection con = null; while ( (freeConnections.size() > 0) && (con == null) ) { // Pick the first Connection in the Vector // to get round-robin usage con = (Connection) freeConnections.firstElement(); freeConnections.removeElementAt(0); try { if (con.isClosed()) { log.info("Removed bad connection in DBConnectionPool."); con = null; // to make the while loop to continue } } catch (SQLException e) { con = null; // to make the while loop to continue } } // while if (con == null) {// cannot get any connection from the pool if (maxConn == 0 || checkedOut < maxConn) {// maxConn = 0 means unlimited connections con = newConnection(); } } if (con != null) { checkedOut++; } return con; } /** * Checks out a connection from the pool. If no free connection * is available, a new connection is created unless the max * number of connections has been reached. If a free connection * has been closed by the database, it's removed from the pool * and this method is called again recursively. * <P> * If no connection is available and the max number has been * reached, this method waits the specified time for one to be * checked in. * * @param timeout The timeout value in milliseconds */ /** * Note that this method is not synchronized since it relies on the getConnection(void) method * I also believe that this method SHOULD NOT synchronized because I use #sleep() method * @todo: check if we should synchronize this method and use wait instead of sleep ??? */ Connection getConnection(long timeout) { long startTime = System.currentTimeMillis(); Connection con; while ((con = getConnection()) == null) { long elapsedTime = System.currentTimeMillis() - startTime; if (elapsedTime >= timeout) { // Timeout has expired return null; } long timeToWait = timeout - elapsedTime; if (timeToWait > TIME_BETWEEN_RETRIES) timeToWait = TIME_BETWEEN_RETRIES;// we dont want to wait for more than TIME_BETWEEN_RETRIES second each time try { Thread.sleep(timeToWait); } catch (InterruptedException e) {} } return con; } /** * Closes all available connections. * @return true if the pool is empty and balance * false if the pool has returned some connection to outside */ synchronized boolean release() { boolean retValue = true; Enumeration allConnections = freeConnections.elements(); while (allConnections.hasMoreElements()) { Connection con = (Connection) allConnections.nextElement(); try { con.close(); } catch (SQLException e) { log.error("Cannot close connection in DBConnectionPool."); } } freeConnections.removeAllElements(); if (checkedOut != 0) { retValue = false; log.warn("DBConnectionManager: the built-in connection pool is not balanced."); } checkedOut = 0; return retValue; } /** * Creates a new connection, using a userid and password * if specified. * @todo: check if this method need synchronized */ private Connection newConnection() { Connection con = null; try { if (user == null) { con = DriverManager.getConnection(URL); } else { con = DriverManager.getConnection(URL, user, password); } // Note that we dont need to call setAutoCommit here because we // will call it at DBConnectionManager.getConnection() //con.setAutoCommit(true);//thread 804 by trulore } catch (SQLException e) { log.error("Cannot create a new connection in DBConnectionPool. URL = " + URL, e); return null; } return con; } } }