封装一个FTP操作工具类
概述
前人的代码中把FTP操作和业务逻辑实现耦合在一起,据说经过多次的修改,在性能表现方面已经非常靠谱。
在原来的代码中可以看到使用了commons-net进行FTP操作,使用commons-pool对象池方式管理FTP连接,完成了多线程下载和上传的功能,本次的修改只是把耦合的地方剥离开来。FTP连接对象池
使用apache commons pool对象池管理方式需要提供一个工厂类,管理对象的生成销毁等。
需要实现如下方法,PooledObjectmakeObject(K key) throws Exception;void destroyObject(K key, PooledObject p) throws Exception;boolean validateObject(K key, PooledObject p);
apache commons pool提供了一个带泛型的接口KeyedPooledObjectFactory<K,V>
,
public class FtpClientConfig { private String host; private int port; private String username; private String password;...}
这里要生产的对象类型当然是FTPClient了,所以我们的工厂类是这样的,
public class FtpClientFactory implements KeyedPooledObjectFactory{...}
相应的,我们提供一个对象池的实现,其实很简单
public class FtpClientPool extends GenericKeyedObjectPool{ public FtpClientPool(FtpClientFactory factory, FtpPoolConfig config) { super(factory, config); }}
构造方法的参数就是我们提供的对象工厂FtpClientFactory
和FtpPoolConfig
,FtpPoolConfig
是
public class FtpPoolConfig extends GenericKeyedObjectPoolConfig{ public FtpPoolConfig() { setTestWhileIdle(true); setTimeBetweenEvictionRunsMillis(60000); setMinEvictableIdleTimeMillis(1800000L); setTestOnBorrow(true); }}
针对FTP连接设置了一些参数。
外部只要拿到FtpClientPool
对象就可以获取FTPClient
对象、返回FTPClien
对象了,
FTPClient
的生成销毁就交给了FtpClientFactory
管理。 使用FTP连接对象池
FTP连接池比方数据库连接池来看,使用连接池似乎可以模仿Spring
的JdbcTemplate
,这个模板封装了
public class FtpTemplate implements FtpOperations{ @Autowired private FtpClientPool ftpClientPool; }
继承自己定义的FTP操作接口,并且注入上一步封装好的对象池,当然在实践过程中可能做不到像JdbcTemplate
InterfaceConfig
类我们才能真正实现FtpTemplate
类。 public class FtpTemplate implements FtpOperations{ ... @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 ConcurrentHashMappoolMap = new ConcurrentHashMap<>(); //存储线程池 public void downloadDirectory(InterfaceConfig config, final FtpCallback callback) { logger.info("正在下载FTP目录" + toFtpInfo(config)); ThreadPoolExecutor workPool = poolMap.get(config.getInterfaceCode()); if(workPool == null) { BlockingQueue workQueue = new LinkedBlockingQueue (100); workPool = new ThreadPoolExecutor(MAX_CORE_NUM, MAX_THREAD_NUM, 1, TimeUnit.MINUTES, workQueue); poolMap.put(config.getInterfaceCode(), workPool); } try { List fileNames = ftpTemplate.listFiles(config,config.getOrdersCount()); BlockingQueue fileQueue = new LinkedBlockingQueue (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
,加入了多线程线程池管理,下载方法也需要外部传入回调方法。
回调操作
略
程序调用图
关于单元测试
从上往下可以看出来三处封装,分别是FtpUtils
、FtpTemplate
和FtpClientPool
,我们可以分别
- 注入
FtpClientPool
,测试FTP连接问题等 - 注入
FtpTemplate
,测试FTP操作问题等 - 注入
FtpUtils
,传入回调函数,测试业务问题
由于JUnit
对多线程单元测试并没有提供支持,所以第3点实现起来有困难。
代码地址
见master分支