封裝一個FTP工具類

封裝一個FTP操作工具類

概述

前人的代碼中把FTP操作和業務邏輯實現耦合在一起,據說經過多次的修改,在性能表現方面已經非常靠譜。 在原來的代碼中可以看到使用了commons-net進行FTP操作,使用commons-pool對象池方式管理FTP連接, 完成了多線程下載和上傳的功能,本次的修改只是把耦合的地方剝離開來。

FTP連接對象池

使用apache commons pool對象池管理方式需要提供一個工廠類,管理對象的生成銷燬等。 需要實現如下方法,

PooledObject<V> makeObject(K key) throws Exception;
void destroyObject(K key, PooledObject<V> p) throws Exception;
boolean validateObject(K key, PooledObject<V> p);

apache commons pool提供了一個帶泛型的接口KeyedPooledObjectFactory<K,V>, 需要繼承實現類提供對象工廠的key類型,及要生產的對象類型,key可以是一個類,包含FTP的IP ,端口,用戶名密碼等屬性組成,目的是區分不同的FTP連接,

public class FtpClientConfig {
    
    private String host;
    private int port;
    private String username;
    private String password;
...
}

這裏要生產的對象類型當然是FTPClient了,所以我們的工廠類是這樣的,

public class FtpClientFactory implements KeyedPooledObjectFactory<FtpClientConfig, FTPClient>{
...
}

相應的,我們提供一個對象池的實現,其實很簡單

public class FtpClientPool extends GenericKeyedObjectPool<FtpClientConfig, FTPClient>  {

    public FtpClientPool(FtpClientFactory factory, FtpPoolConfig config) {
        super(factory, config);
    }

}

構造方法的參數就是我們提供的對象工廠FtpClientFactoryFtpPoolConfigFtpPoolConfig

public class FtpPoolConfig extends GenericKeyedObjectPoolConfig{
    public FtpPoolConfig() {
        setTestWhileIdle(true);
        setTimeBetweenEvictionRunsMillis(60000);
        setMinEvictableIdleTimeMillis(1800000L);
        setTestOnBorrow(true);
    }
}

針對FTP連接設置了一些參數。

外部只要拿到FtpClientPool對象就可以獲取FTPClient對象、返回FTPClien對象了, FTPClient的生成銷燬就交給了FtpClientFactory管理。

使用FTP連接對象池

FTP連接池比方數據庫連接池來看,使用連接池似乎可以模仿SpringJdbcTemplate,這個模板封裝了 獲取連接,執行數據庫操作,返還連接給連接池的過程,在這裏同樣也適合。這裏引入一個自己實現的"模板類",

public class FtpTemplate implements FtpOperations<K> {
    @Autowired
    private FtpClientPool ftpClientPool;    
}

繼承自己定義的FTP操作接口,並且注入上一步封裝好的對象池,當然在實踐過程中可能做不到像JdbcTemplate 那樣完全的泛型化。 比如爲了泛型實例化,引入InterfaceConfig類我們才能真正實現FtpTemplate類。

public class FtpTemplate implements FtpOperations<InterfaceConfig> {
    ...
    @Override
    public String getFile(InterfaceConfig k, String fileName) throws Exception {
        if (logger.isDebugEnabled()) {
            logger.debug("正在下載" + toFtpInfo(k) + "/" + fileName + "文件");
        }
        final FTPClient client = getFtpClient(getFtpClientPool(), k);
        boolean ret = changeDirectory(client,k);
        
        try {
            if(ret) {
                return performPerFile(client, fileName);
            }
            return null;
        } catch(Exception e) {
            logger.error("下載" + toFtpInfo(k) + "/" + fileName + "文件異常",e);
            throw e;
        } finally {
            //return to object pool
            if(client != null) {
                returnFtpClient(getFtpClientPool(), k, client);
            }
        }
    }
...
}

通過InterfaceConfig提供對象池識別的key獲得我們需要的對象池裏的對象FTPClient

    private FTPClient getFtpClient(FtpClientPool ftpClientPool, InterfaceConfig k) throws Exception {
        FtpClientConfig config = buildFtpClientConfig(k);
        FTPClient client = null;
        try {
            client = ftpClientPool.borrowObject(config);
        } catch (Exception e) {
            logger.error("獲取FTPClient對象異常 " + toFtpInfo(k),e);
            throw e;
        }
        return client;
    }
    private FtpClientConfig buildFtpClientConfig(InterfaceConfig k) {
        FtpClientConfig config = new FtpClientConfig();
        config.setHost(k.getFtpUrl());
        config.setPort(Integer.valueOf(k.getFtpPort()));
        config.setUsername(k.getUserName());
        config.setPassword(k.getPwd());
        return config;
    }    

這個InterfaceConfig是業務代碼中的對象類型,可能是Model類。目前爲止我引入了一點點關於 業務的代碼,但還沒有耦合進來業務實現邏輯。

FTP工具類

其實FtpTemplate已經是一個適合業務邏輯實現的工具類的,但是它的功能單純一些,爲了完成特殊的業務功能, 如多線程下載,下載文件業務處理成功後才刪除遠端服務的文件等,這裏再對FtpTemplate做一次封裝。

public class FtpUtils {
    @Autowired
    private FtpTemplate ftpTemplate;
    private ConcurrentHashMap<String, ThreadPoolExecutor> poolMap = new ConcurrentHashMap<>(); //存儲線程池
    
    public void downloadDirectory(InterfaceConfig config,
             final FtpCallback<FtpFile,Boolean> callback) {
        logger.info("正在下載FTP目錄" + toFtpInfo(config));
        ThreadPoolExecutor workPool = poolMap.get(config.getInterfaceCode());
        if(workPool == null) {
            BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<Runnable>(100);
            workPool = new ThreadPoolExecutor(MAX_CORE_NUM, MAX_THREAD_NUM, 1, TimeUnit.MINUTES, workQueue);
            poolMap.put(config.getInterfaceCode(), workPool);
        }
        try {
            List<String> fileNames = ftpTemplate.listFiles(config,config.getOrdersCount());
            BlockingQueue<String> fileQueue = new LinkedBlockingQueue<String>(fileNames); //生產者資料
            for(int i = 0; i < config.getThreadNum(); i++) {
                try {
                    workPool.execute(new GetFileConsumer(config, fileQueue, callback));
                } catch (Exception e) {
                    logger.error("提交線程出現異常",e);
                }
            }
        } catch (Exception e) {
            logger.error("FTP操作出現異常"+toFtpInfo(config),e);
        }
        logger.info("下載FTP目錄完成" + toFtpInfo(config));
    }        
}

注入了FtpTemplate,加入了多線程線程池管理,下載方法也需要外部傳入回調方法。 回調方法中就可以完成保存下載的FTP文件,刪除遠端對應的文件等邏輯。即使了多了一層多線程 下載功能的封裝,我們也沒有把業務處理邏輯耦合進來。當然,不滿意的地方還是引入了業務的Model類。

回調操作

程序調用圖

關於單元測試

從上往下可以看出來三處封裝,分別是FtpUtilsFtpTemplateFtpClientPool,我們可以分別 對他們進行單元測試,

  1. 注入FtpClientPool,測試FTP連接問題等
  2. 注入FtpTemplate,測試FTP操作問題等
  3. 注入FtpUtils,傳入回調函數,測試業務問題

由於JUnit對多線程單元測試並沒有提供支持,所以第3點實現起來有困難。

代碼地址

https://github.com/Honwhy/com... 見master分支

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章