封装一个FTP操作工具类
概述
前人的代码中把FTP操作和业务逻辑实现耦合在一起,据说经过多次的修改,在性能表现方面已经非常靠谱。
在原来的代码中可以看到使用了commons-net进行FTP操作,使用commons-pool对象池方式管理FTP连接,
完成了多线程下载和上传的功能,本次的修改只是把耦合的地方剥离开来。
FTP连接对象池
使用apache commons pool对象池管理方式需要提供一个工厂类,管理对象的生成销毁等。
需要实现如下方法,
PooledObject makeObject(K key) throws Exception;
void destroyObject(K key, PooledObject p) throws Exception;
boolean validateObject(K key, PooledObject p);
apache commons pool提供了一个带泛型的接口KeyedPooledObjectFactory
,
需要继承实现类提供对象工厂的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{
...
}
相应的,我们提供一个对象池的实现,其实很简单
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 ConcurrentHashMap poolMap = 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
,加入了多线程线程池管理,下载方法也需要外部传入回调方法。
回调方法中就可以完成保存下载的FTP文件,删除远端对应的文件等逻辑。即使了多了一层多线程
下载功能的封装,我们也没有把业务处理逻辑耦合进来。当然,不满意的地方还是引入了业务的Model类。
回调操作
略
程序调用图
关于单元测试
从上往下可以看出来三处封装,分别是FtpUtils
、FtpTemplate
和FtpClientPool
,我们可以分别
对他们进行单元测试,
- 注入
FtpClientPool
,测试FTP连接问题等 - 注入
FtpTemplate
,测试FTP操作问题等 - 注入
FtpUtils
,传入回调函数,测试业务问题
由于JUnit
对多线程单元测试并没有提供支持,所以第3点实现起来有困难。
代码地址
https://github.com/Honwhy/com... 见master分支