近日我接到了一个关于FTP监听处理的工单任务,在查阅代码的时候发现原有FTP监听逻辑是需要对FTP和SFTP进行支持的,但是同样的处理流程对FTP和SFTP写了两遍,追究其原因是因为缺少对对FTP和SFTP操作接口的抽象层来屏蔽两者差异。自然而然的会想到使用适配器模式来完成这件工作。

适配器模式

适配器模式(Adapter Pattern) :将一个接口转换成客户希望的另一个接口,适配器模式使接口不兼容的那些类可以一起工作,其别名为包装器(Wrapper)。 适配器模式既可以作为类结构型模式,也可以作为对象结构型模式。

模式结构

适配器模式包含如下角色: Target:目标抽象类 Adapter:适配器类 Adaptee:适配者类 Client:客户类 其UML结构如下图所示:Adapter.jpg

适配器模式有类适配器和对象适配器两种,上面展示的是对象适配器,类适配器不在此展开。

适配器如何解决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进行封装适配后将会统一操作接口,避免了重复代码,后续对文件监听操作的处理流程也将会轻松很多。