近日我接到了一个关于FTP监听处理的工单任务,在查阅代码的时候发现原有FTP监听逻辑是需要对FTP和SFTP进行支持的,但是同样的处理流程对FTP和SFTP写了两遍,追究其原因是因为缺少对对FTP和SFTP操作接口的抽象层来屏蔽两者差异。自然而然的会想到使用适配器模式来完成这件工作。
适配器模式
适配器模式(Adapter Pattern) :将一个接口转换成客户希望的另一个接口,适配器模式使接口不兼容的那些类可以一起工作,其别名为包装器(Wrapper)。 适配器模式既可以作为类结构型模式,也可以作为对象结构型模式。
模式结构
适配器模式包含如下角色:
Target:目标抽象类
Adapter:适配器类
Adaptee:适配者类
Client:客户类
其UML结构如下图所示:
适配器模式有类适配器和对象适配器两种,上面展示的是对象适配器,类适配器不在此展开。
适配器如何解决FTP兼容性问题
我接到的工单任务中对FTP的接口使用有限的,不需要进行过多的FTP操作,主要用到了连接、关闭连接、扫描目录、下载、移动、重命名这几个主要的接口。 因此我的主要工作是完成这些接口的适配。根据上述的描述我抽象了FTP操作接口. 注意: 本文中的代码只是暂时适配器的使用,实际上并不可用,缺失了部分依赖
import java.util.Collections;
import java.util.List;
/**
-
FileToolClientAdaper
-
文件客户端的适配器,用来屏蔽sftp,ftp等对文件的差异
-
@author alvinkwok
*/
public interface FileToolClientAdapter {
/**
- 获取原始客户端对象
- @return
*/
Object getClient();
/**
- 打开文件客户端
- @param url 连接地址
- @param port 连接端口
- @param username 连接用户名
- @param password 连接密码
- @throws FileToolClientAdapterException 连接失败时抛出该异常
*/
boolean connect(String url, int port, String username, String password) throws FileToolClientAdapterException;
/**
- 关闭文件客户端
- @throws FileToolClientAdapterException 关闭失败时抛出该异常
*/
void close() throws FileToolClientAdapterException;
/**
- 切换远程模块
- @param path 待切换路径
- @throws FileToolClientAdapterException 切换失败时抛出异常
*/
void cd(String path) throws FileToolClientAdapterException;
/**
- 上传文件
- @param path 待上传文件路径
- @param remotePath 远程上传文件路径
- @throws FileToolClientAdapterException 切换失败时抛出异常
*/
boolean upload(String path, String remotePath) throws FileToolClientAdapterException;
/**
- 下载文件
- @param remotePath 本地保存文件路径
- @param fileName 远程下载文件路径
- @param localPath 本地保存路口
- @return 下载成功返回true,失败返回false,内部自己处理异常
*/
boolean download(String remotePath, String fileName, String localPath) throws FileToolClientAdapterException;
/**
- 重命名文件
*重命名文件,仅限制于同层目录
- @param remotePath 远程路径
- @param oldFileName 待更新文件名
- @param newFileName 更新后文件名
- @return 更新成功返回true,失败返回false
*/
boolean rename(String remotePath, String oldFileName, String newFileName) throws FileToolClientAdapterException;
/**
- 移动文件
- @param remotePath 远程路径
- @param backupPath 备份路径
- @param fileName 文件名
- @return 移动成功返回true,移动失败返回false
*/
boolean move(String remotePath, String backupPath, String fileName) throws FileToolClientAdapterException;
/**
- 检查文件是否存在
- @param workPath 工作路径
- @param fileName 待检查文件
- @return 文件存在返回true,否则返回false
*/
boolean exist(String workPath, String fileName) throws FileToolClientAdapterException;
/**
- 获取数据文件列表
- @param remotePath 监听目录
- @return 返回一个非null的数据列表
- 内部消化掉异常
*/
List<FileAdapter> listFiles(String remotePath,IFileFilter fileFilter) throws FileToolClientAdapterException;
/**
- 获取指定文件
- @param remotePath 监听目录
- @return
*/
FileAdapter getFile(String remotePath,String fileName) throws FileToolClientAdapterException;
}
FTP和SFTP操作的文件对象也是不一致的,所以也需要进行适配处理,也可以选择是将原有FTP/SFTP信息转换后作为一个新的结构来供调用, 在此我选择的是直接适配。需要先建立文件对象的适配。
import java.util.Date;
/**
-
FileAdapter 文件适配器
-
用于屏蔽FTP和SFTP或者是其他的文件对象表达
-
@author alvinkwok
/
public interface FileAdapter {
/*
- 文件是否是文件
- @return 是文件返回true,不是文件返回false
*/
boolean isFile();
/**
- 返回文件名称
- @return 如果能够返回文件名称返回指定文件名称,否则返回null
- 内部将会屏蔽获取文件名的异常
/
String getFileName();
/*
- 获取没有文件后缀的文件名
- @return 无后缀文件名
*/
String getFileNameWithoutSuffix();
/**
- 获取文件的最新修改时间
- @return 文件最新修改时间
*/
Date getUpdateTime();
/**
- 获取文件名后缀
- @return 如果文件名为空 或者是无后缀将会返回null
*/
String getFileSuffix();
}
接下来是对FTP的适配,由于篇幅关系SFTP的适配方式也大致相同,所以此处只展示FTP。
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.net.ftp.FTPClient;
import org.apache.commons.net.ftp.FTPFile;
import org.apache.commons.net.ftp.FTPFileFilter;
import org.omg.CORBA.PRIVATE_MEMBER;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
-
FTPToolClientAdapter
-
@author alvinkwok
*/
@Slf4j
public class FTPToolClientAdapter implements FileToolClientAdapter {
private FTPClient ftpClient;
/**
- 获取原始客户端对象
- @return
*/
@Override
public Object getClient() {
return ftpClient;
}
/**
- 打开文件客户端
- @param url 连接地址
- @param port 连接端口
- @param username 连接用户名
- @param password 连接密码
- @throws FileToolClientAdapterException 连接失败时抛出该异常
*/
@Override
public boolean connect(String url, int port, String username, String password) {
ftpClient = new FTPClient();
return FtpUtil.connect(ftpClient, url, port, username, password);
}
/**
- 关闭文件客户端
- @throws FileToolClientAdapterException 关闭失败时抛出该异常
*/
@Override
public void close() throws FileToolClientAdapterException {
try {
if (ftpClient != null) {
ftpClient.disconnect();
}
} catch (IOException e) {
throw new FileToolClientAdapterException("关闭FTP连接异常", e);
}
}
/**
- 切换远程模块
- @param path 待切换路径
- @throws FileToolClientAdapterException 切换失败时抛出异常
*/
@Override
public void cd(String path) throws FileToolClientAdapterException {
}
/**
- 上传文件
- @param path 待上传文件路径
- @param remotePath 远程上传文件路径
- @throws FileToolClientAdapterException 切换失败时抛出异常
*/
@Override
public boolean upload(String path, String remotePath) throws FileToolClientAdapterException {
throw new FileToolClientAdapterException("方法未实现");
}
/**
- 下载文件
- @param remotePath 本地保存文件路径
- @param fileName 远程下载文件路径
- @param localPath 本地保存路口
- @throws FileToolClientAdapterException 切换失败时抛出异常
*/
@Override
public boolean download(String remotePath, String fileName, String localPath) throws FileToolClientAdapterException {
try {
return FtpUtil.download(ftpClient, remotePath, fileName, localPath);
} catch (Exception e) {
throw new FileToolClientAdapterException(String.format("路径[%s]下载文件[%s]出现异常", remotePath, fileName), e);
}
}
/**
- 重命名文件
*重命名文件,仅限制于同层目录
- @param remotePath 远程路径
- @param oldFileName 待更新文件名
- @param newFileName 更新后文件名
- @return 更新成功返回true,失败返回false
*/
@Override
public boolean rename(String remotePath, String oldFileName, String newFileName) throws FileToolClientAdapterException {
try {
return FtpUtil.rename(ftpClient, remotePath, oldFileName, newFileName);
} catch (Exception e) {
throw new FileToolClientAdapterException(String.format("路径[%s]重命名文件[%s]出现异常", remotePath, oldFileName), e);
}
}
/**
-
移动文件
-
@param remotePath 远程路径
-
@param backupPath 备份路径
-
@param fileName 文件名
-
@return 移动成功返回true,移动失败返回false
*/
@Override
public boolean move(String remotePath, String backupPath, String fileName) throws FileToolClientAdapterException {
try {
return FtpUtil.moveFile(ftpClient, remotePath, backupPath, fileName);
} catch (Exception e) {
throw new FileToolClientAdapterException(
String.format("路径[%s]文件[%s]移动到[%s]出现异常", remotePath, fileName, backupPath), e);
}
}
/**
- 检查文件是否存在
- @param workPath 工作路径
- @param fileName 待检查文件
- @return 文件存在返回true,否则返回false
- @throws FileToolClientAdapterException 出现异常的时候直接抛出
*/
@Override
public boolean exist(String workPath, String fileName) throws FileToolClientAdapterException {
try {
return FtpUtil.isExist(ftpClient, workPath, fileName);
} catch (Exception e) {
throw new FileToolClientAdapterException(e);
}
}
/**
- 获取数据文件列表
- @param remotePath 监听目录
- @return 返回一个非null的数据列表
- 内部消化掉异常
*/
@Override
public List<FileAdapter> listFiles(String remotePath, IFileFilter filter) {
// 进行类型强制转换
try {
FTPFile[] files = null;
if (filter == null) {
files = ftpClient.listFiles(remotePath);
} else {
files = ftpClient.listFiles(remotePath, file -> filter.accept(new FTPFileAdapter(file)));
}
return convertFiles(files);
} catch (IOException e) {
log.info("获取文件列表失败", e);
return Collections.emptyList();
}
}
/**
- 获取指定文件
- @param remotePath 监听目录
- @param fileName
- @return 返回一个非null的数据列表
- 内部消化掉异常
*/
@Override
public FileAdapter getFile(String remotePath, String fileName) throws FileToolClientAdapterException {
try {
if (FtpUtil.changeWorkingDirectory(ftpClient, remotePath)) {
//utf-8文件中中文,如此编码new String(fileName.getBytes("UTF-8"),"iso-8859-1")
FTPFile ftpFile = ftpClient.mlistFile(fileName);
if (ftpFile == null) {
return null;
}
// 使用mlist拿到的信息是详细信息
// 这玩意应该是这个ftp的缺陷,没有进行文件名的解析,getName方法直接返回了没有解析的内容。
if (StrUtil.isBlank(ftpFile.getName())) {
return null;
}
String[] splitStrs = ftpFile.getName().split(";");
// 4的情况是上述的例子,1的情况是返回正确的文件名
if (splitStrs.length >= 4) {
// 是文件信息中的第4部分
ftpFile.setName(splitStrs[3].trim());
} else if (splitStrs.length == 1) {
// 是文件信息中的第4部分
ftpFile.setName(splitStrs[0].trim());
}
return new FTPFileAdapter(ftpFile);
}
} catch (Exception e) {
throw new FileToolClientAdapterException("获取文件" + fileName + "出现异常", e);
}
return null;
}
private List<FileAdapter> convertFiles(FTPFile[] files) {
if (files == null || files.length == 0) {
return Collections.emptyList();
}
List<FileAdapter> adapters = new ArrayList<>();
for (FTPFile file : files) {
adapters.add(convertFile(file));
}
return adapters;
}
private FileAdapter convertFile(FTPFile ftpFile) {
return new FTPFileAdapter(ftpFile);
}
}
有一个特殊的点是扫描过程中是有对文件过滤的需求的,所以在对接口抽象的过程中添加了一个IFileFilter
接口。
实际在对FTP和SFTP都是有过滤接口的,在FTP和SFTP的扫描适配的过程中是将ftp文件对象转换为FileAdapter
然后委托到原始的FTP过滤器中完成文件过滤。
public interface IFileFilter{
/**
* @param file 待过滤文件
* @return 接受文件返回true,否则返回false
*/
boolean accept(IFileAdapter file);
}
将FTP和SFTP进行封装适配后将会统一操作接口,避免了重复代码,后续对文件监听操作的处理流程也将会轻松很多。