1. 概述
本文将介绍如何借助 Apache Commons Net 库与外部 FTP 服务器进行交互。该库提供了完整的 FTP 协议支持,适合在实际项目中处理文件上传、下载和目录操作等场景。
我们还会使用 MockFtpServer 来编写可重复执行的集成测试,避免依赖真实 FTP 环境带来的不稳定因素。
2. 环境搭建
与外部服务交互时,编写集成测试是标配操作。虽然现在通常用 Docker 起一个真实 FTP 容器,但 FTP 协议(尤其是被动模式)对端口映射要求较高,在 CI 环境下容易踩坑 ❌。
因此我们选择 MockFtpServer —— 一个纯 Java 实现的轻量级 FTP 模拟服务器,专为单元/集成测试设计,支持动态端口绑定,完美适配自动化测试流程 ✅。
Maven 依赖如下:
<dependency>
<groupId>commons-net</groupId>
<artifactId>commons-net</artifactId>
<version>3.6</version>
</dependency>
<dependency>
<groupId>org.mockftpserver</groupId>
<artifactId>MockFtpServer</artifactId>
<version>2.7.1</version>
<scope>test</scope>
</dependency>
⚠️ 建议始终使用最新稳定版本,可在 Maven Central 和 MockFtpServer 查看。
3. JDK 内置 FTP 支持分析
你可能不知道,JDK 某些版本中其实自带了基础的 FTP 支持,位于 sun.net.www.protocol.ftp.FtpURLConnection
。不过这类内部 API 属于非公开接口,不推荐直接调用。
更规范的做法是通过标准 java.net.URL
接口间接使用:
@Test
public void givenRemoteFile_whenDownloading_thenItIsOnTheLocalFilesystem() throws IOException {
String ftpUrl = String.format(
"ftp://user:password@localhost:%d/foobar.txt", fakeFtpServer.getServerControlPort());
URLConnection urlConnection = new URL(ftpUrl).openConnection();
InputStream inputStream = urlConnection.getInputStream();
Files.copy(inputStream, new File("downloaded_buz.txt").toPath());
inputStream.close();
assertThat(new File("downloaded_buz.txt")).exists();
new File("downloaded_buz.txt").delete(); // cleanup
}
看起来挺简洁?但问题来了:这个原生方案 ❌ 不支持列出文件 ❌ 无法设置超时 ❌ 不支持主动/被动模式切换 —— 功能太弱鸡,仅适用于最简单的下载场景。
所以我们还是老老实实用 Apache Commons Net 吧,功能全、文档多、社区稳。
4. 连接 FTP 服务器
首先封装一个 FtpClient
类,作为对 Apache Commons Net 的高层抽象:
class FtpClient {
private String server;
private int port;
private String user;
private String password;
private FTPClient ftp;
// 构造函数略
void open() throws IOException {
ftp = new FTPClient();
ftp.addProtocolCommandListener(new PrintCommandListener(new PrintWriter(System.out)));
ftp.connect(server, port);
int reply = ftp.getReplyCode();
if (!FTPReply.isPositiveCompletion(reply)) {
ftp.disconnect();
throw new IOException("Exception in connecting to FTP Server");
}
ftp.login(user, password);
}
void close() throws IOException {
ftp.disconnect();
}
}
关键点说明:
- ✅
PrintCommandListener
:打印底层通信日志,调试时非常有用(类似 telnet 连接 FTP 的输出) - ✅ 必须检查
getReplyCode()
返回值,判断连接是否真正成功 - ✅ 登录后建议设置文件传输模式(如二进制),本文示例省略,生产环境务必加上
集成测试初始化
使用 JUnit 的 @Before
和 @After
统一管理资源生命周期:
public class FtpClientIntegrationTest {
private FakeFtpServer fakeFtpServer;
private FtpClient ftpClient;
@Before
public void setup() throws IOException {
fakeFtpServer = new FakeFtpServer();
fakeFtpServer.addUserAccount(new UserAccount("user", "password", "/data"));
FileSystem fileSystem = new UnixFakeFileSystem();
fileSystem.add(new DirectoryEntry("/data"));
fileSystem.add(new FileEntry("/data/foobar.txt", "abcdef 1234567890"));
fakeFtpServer.setFileSystem(fileSystem);
fakeFtpServer.setServerControlPort(0); // 自动分配空闲端口
fakeFtpServer.start();
ftpClient = new FtpClient("localhost", fakeFtpServer.getServerControlPort(), "user", "password");
ftpClient.open();
}
@After
public void teardown() throws IOException {
ftpClient.close();
fakeFtpServer.stop();
}
}
⚠️ 注意:setServerControlPort(0)
表示由系统自动分配端口,因此创建 FtpClient
时必须通过 getServerControlPort()
获取实际端口号,否则会连不上。
5. 列出远程文件
先写测试,走 TDD 流程:
@Test
public void givenRemoteFile_whenListingRemoteFiles_thenItIsContainedInList() throws IOException {
Collection<String> files = ftpClient.listFiles("");
assertThat(files).contains("foobar.txt");
}
实现也很简单,把 FTPFile[]
转成 List<String>
提高可用性:
Collection<String> listFiles(String path) throws IOException {
FTPFile[] files = ftp.listFiles(path);
return Arrays.stream(files)
.map(FTPFile::getName)
.collect(Collectors.toList());
}
💡 提示:如果需要文件大小、修改时间等元信息,直接返回 FTPFile[]
或封装成 DTO 更合适。
6. 下载文件
定义接口:指定远程路径和本地目标文件。
测试用例:
@Test
public void givenRemoteFile_whenDownloading_thenItIsOnTheLocalFilesystem() throws IOException {
ftpClient.downloadFile("/foobar.txt", "downloaded_buz.txt");
assertThat(new File("downloaded_buz.txt")).exists();
new File("downloaded_buz.txt").delete(); // cleanup
}
实现利用 retrieveFile(String, OutputStream)
直接写入文件流:
void downloadFile(String source, String destination) throws IOException {
FileOutputStream out = new FileOutputStream(destination);
ftp.retrieveFile(source, out);
out.close(); // 实际项目建议 try-with-resources
}
✅ 小技巧:可以用 Files.newOutputStream()
替代 FileOutputStream
,代码更现代。
7. 上传文件
MockFtpServer 提供了 getFileSystem().exists()
方法,可用于验证文件是否上传成功:
@Test
public void givenLocalFile_whenUploadingIt_thenItExistsOnRemoteLocation()
throws URISyntaxException, IOException {
File file = new File(getClass().getClassLoader().getResource("baz.txt").toURI());
ftpClient.putFileToPath(file, "/buz.txt");
assertThat(fakeFtpServer.getFileSystem().exists("/buz.txt")).isTrue();
}
上传使用 storeFile(String, InputStream)
接口:
void putFileToPath(File file, String path) throws IOException {
ftp.storeFile(path, new FileInputStream(file));
}
⚠️ 注意事项:
- 上传大文件时建议设置缓冲区或分块传输
- 生产环境务必用
try-with-resources
管理流,防止资源泄露 - 建议上传前先检查目标路径权限和空间
8. 总结
通过 Apache Commons Net,Java 程序可以轻松实现对 FTP 服务器的完整操作,包括连接管理、文件列表、上传下载等核心功能。
搭配 MockFtpServer 后,还能写出稳定可靠的集成测试,无需依赖外部环境,CI/CD 友好 ✅。
示例代码已托管至 GitHub:https://github.com/eugenp/tutorials/tree/master/libraries-apache-commons-2