使用commons-pool管理FTP連線
背景
在封裝一個FTP工具類文章,已經完成一版對FTP連線的管理,設計了模板方法,為工具類上傳和下載檔案方法的提供獲取物件和釋放物件支援。
此番重新造輪子更多地考慮功能複用的角度,支援更多可配置引數,不止是連線池相關的屬性;只考慮維護同一個連線請求多個連線物件的情況,將多個不同請求的情況交給外部管理,由外部定製,類似多資料來源資料庫連線的方式;重新審視模板方法的使用,在不引入模板的方法,設計封裝物件池管理功能,以更自然的方式獲取物件和釋放物件。
思路
整體的思路來自BasicDataSource
,它是javax.sql.DataSource
的具體實現,實現的是資料庫連線池,使用上完全感覺不到物件池的存在,通過dataSource
獲取物件connection
,釋放物件則使用connection.close()
即可。然而,與javax.sql.DataSource
和java.sql.Connection
不同的是,JDK中並沒有支援FTP協議的類似的框架;另一個問題則是,專案中已經使用commons-net來建立FTP連線,使用FTPClient等API了,如何將具體實現整合到要新定義的介面中,似乎是本末倒置的。
實現
整體框架
首先定義整體框架,類似DataSource
public interface FTPManager extends AutoCloseable {
FTPConnection getFTPConnection() throws FTPException ;
}
定義連線物件,
public interface FTPConnection extends Wrapper, AutoCloseable {
void close() throws FTPException;
boolean isClosed() throws FTPException;
//ftp|ftps|ftp:http -- subprotocol
//String getSchema() throws FTPException;
}
從這個框架出發,獲取連線物件使用ftpManager.getFTPConnection
,釋放物件使用ftpConnection.close
。
整理配置屬性
引入主角FTPCPManager
,在FTPCPManager
定義和連線相關的屬性,抽取一個父類PoolProperties
專門用於配置物件池相關的配置。
public class FTPCPManager extends PoolProperties implements FTPManager {
protected String url;
protected String username;
protected String password;
protected String proxyHost = null;
protected int proxyPort = 80;
protected String proxyUser = null;
protected String proxyPassword = null;
protected String encoding = StandardCharsets.UTF_8.name();
protected long keepAliveTimeout = -1;
protected int controlKeepAliveReplyTimeout = -1;
protected String serverTimeZoneId = null;
protected int bufferSize = -1;
protected int connectTimeout = -1;
protected String localActive = "false";
}
類似地,若使用Spring的xml配置,配置FTPCPManager
或許是這樣的,
<bean id="ftpCPManager" class="com.honey.ftpcp.FTPCPManager" destroy-method="close">
<property name="url" value="ftp://127.0.0.1"/>
<property name="username" value="sa"/>
<property name="password" value="sa"/>
<property name="maxTotal" value="100"/>
<property name="maxIdle" value="8"/>
<property name="minIdle" value="0"/>
<property name="maxWait" value="1000"/>
<property name="initialSize" value="2"/>
<property name="testOnBorrow" value="true"/>、
<property name="testOnReturn" value="false"/>
<property name="testWhileIdle" value="false"/>
</bean>
關於物件池的屬性的說明請參考更多網路文章,或者官方文件。
獲取物件
這是FTPCPManager
最核心的部分了,入口是getFTPConnection
方法,
public FTPConnection getFTPConnection() throws FTPException {
return createFTPManager().getFTPConnection();
}
protected synchronized FTPManager createFTPManager() {
if(ftpManager != null) {
return ftpManager;
}
//create connection factory
IFTPClientFactory ftpClientFactory = createFTPClientFactory();
PoolingFTPManager newManager = new PoolingFTPManager(ftpClientFactory, this);
connectionPool = newManager.getPool();
this.ftpManager = newManager;
return newManager;
}
FTPCPManager
做了一個特殊處理,在內部維護了新的FTPManager
型別變數,不同的是它帶有物件池管理的功能,它存在的意義就是將物件池和物件工廠組合起來,這樣的處理方式減輕了FTPCPManager
的負擔,職責更少,只提供重要介面,重要的實現還是交給被代理的成員。(當然,這裡也可以有不同的看法)。createFTPClientFactory
會根據url屬性的協議分別建立不同的物件工廠,如FTPClientFactory
,FTPSClientFactory
等。
PoolingFTPManager
的構造方法,需要物件工廠及連線池配置屬性兩個引數,FTPCPManager
正好繼承擴充套件了PoolProperties
類,作為連線池配置引數很合適。所以構造被代理的成員,即newManager = new PoolingFTPManager(ftpClientFactory, this)
。
構造好PoolingFTPManager
的例項後,就可以獲取FTPConnection
連線物件了,接下來就是物件池的功能了。整體時序圖如下,
物件的獲取最終還是物件池與物件工廠的事情。
釋放物件
為了讓FTPConnection
執行close
方法的時候能夠釋放自己,將自己return到物件池,必須對FTPConnection
做一些封裝,連線物件需要記住最初的物件池物件,而物件池需要通過物件工廠來構造,通過這些條件程式碼的實現思路如下,
在構造PoolingFTPManager
的同時也針對FTP物件工廠進行了封裝,把原來的IFTPClientFactory
封裝成PoolableConnectionFactory
型別,並且PoolableConnectionFactory
持有GenericObjectPool
型別的的物件池變數。在構造完GenericObjectPool
物件池後,將物件池引用設定到PoolableConnectionFactory
中。
PoolingFTPManager(IFTPClientFactory clientFactory, PoolProperties poolProperties) {
//create object factory
_connectionFactory = new PoolableConnectionFactory(clientFactory);
GenericObjectPoolConfig config = new GenericObjectPoolConfig();
// set config
_pool = new GenericObjectPool<FTPConnection>(_connectionFactory, config);
_connectionFactory.setPool(_pool);//反向引用
}
此外,在執行PoolableConnectionFactory
的makeObject
方法,對生成的物件做一次封裝,傳遞PoolableConnectionFactory
持有的物件池給新生成的的物件。
public PooledObject<FTPConnection> makeObject() throws Exception {
FTPClient ftpClient = factory.getFTPClient();
FTPClientWrapperConnection wrapperConnection = new FTPClientWrapperConnection(ftpClient,pool);
return new DefaultPooledObject<FTPConnection>(wrapperConnection);
}
這個FTPClientWrapperConnection
類就是關鍵了。FTPConnection
執行close
方法能將自己釋放,return到物件池,就是由FTPClientWrapperConnection
具體實現的。
public void close() throws FTPException {
try {
if(pool != null && !pool.isClosed()) {
pool.returnObject(this);
} else {
if(ftpClient!=null) {
ftpClient.logout();
ftpClient.disconnect();
}
}
} catch (Exception e) {
//swallow everything
} finally {
_closed = true;
}
}
簡單測試
用一個測試來表現這個獲取和釋放物件的功能,
public class FTPCPManagerTest {
@Test
public void test1() throws Exception {
FTPCPManager manager = new FTPCPManager();
manager.setUrl("ftp://127.0.0.1");
manager.setUsername("sa");
manager.setPassword("sa");
manager.setInitialSize(2);
manager.setKeepAliveTimeout(1 * 60);
FTPConnection conn = manager.getFTPConnection();
assertTrue(manager.getNumActive() == 1);
assertTrue(manager.getNumIdle() == 1);
conn.close();
assertTrue(manager.getNumActive() == 0);
assertTrue(manager.getNumIdle() == 2);
manager.close();
}
}
首先initialSize
設定了物件池初始大小,在構造物件池的時候就呼叫了兩次物件工廠的makeObject
方法生成兩個物件。然後是通過manager
獲取一次物件,此時檢測物件池的被借出的物件manager.getNumActive() == 1
是否成立,檢測物件池保留的物件manager.getNumIdle() == 1
是否成立。接下里是呼叫連線物件的close
方法,再次檢測比較物件池保留的物件是否manager.getNumIdle() == 2
。如果以上斷言都成立,證明物件的獲取和釋放使用到了物件池管理而且能夠正常執行。
總結
至此,使用commons-pool管理FTP連線的功能算基本完成了。與封裝一個FTP工具類文章中的FTP工具相比還缺少上傳下載等功能的封裝,而這些功能將會交給另外的工程來完成。
專案地址:https://github.com/Honwhy/ftpcp