使用commons-pool管理FTP連線

weixin_33766168發表於2018-01-31

使用commons-pool管理FTP連線

背景

封裝一個FTP工具類文章,已經完成一版對FTP連線的管理,設計了模板方法,為工具類上傳和下載檔案方法的提供獲取物件和釋放物件支援。

此番重新造輪子更多地考慮功能複用的角度,支援更多可配置引數,不止是連線池相關的屬性;只考慮維護同一個連線請求多個連線物件的情況,將多個不同請求的情況交給外部管理,由外部定製,類似多資料來源資料庫連線的方式;重新審視模板方法的使用,在不引入模板的方法,設計封裝物件池管理功能,以更自然的方式獲取物件和釋放物件。

思路

整體的思路來自BasicDataSource,它是javax.sql.DataSource的具體實現,實現的是資料庫連線池,使用上完全感覺不到物件池的存在,通過dataSource獲取物件connection,釋放物件則使用connection.close()即可。然而,與javax.sql.DataSourcejava.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屬性的協議分別建立不同的物件工廠,如FTPClientFactoryFTPSClientFactory等。

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);//反向引用
}

此外,在執行PoolableConnectionFactorymakeObject方法,對生成的物件做一次封裝,傳遞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

相關文章