封裝一個FTP工具類

封裝一個FTP操做工具類

概述

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

FTP鏈接對象池

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

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了,因此咱們的工廠類是這樣的,apache

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

相應的,咱們提供一個對象池的實現,其實很簡單多線程

public class FtpClientPool extends GenericKeyedObjectPool<FtpClientConfig, FTPClient>  {

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

}

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

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分支

相關文章
相關標籤/搜索