You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

不使用递归实现FTP服务器目录文件遍历列出的方法

FTP目录遍历递归改非递归实现方案及优化建议

核心实现原理

递归遍历目录的本质是JVM自动维护方法调用栈,每个栈帧存储当前待处理的目录路径。非递归实现只需要手动维护一个存储待处理目录的容器,模拟这个调用栈即可,完全可以实现和递归一致的遍历效果,还能避免目录层级过深时JVM栈溢出的问题。
常用的两种遍历模式:

  • 深度优先遍历:和原递归逻辑顺序完全一致,用栈结构存储待处理目录,后进先出,优先处理最新发现的子目录
  • 广度优先(层序)遍历:用队列存储待处理目录,先进先出,先处理完当前层级所有目录和文件,再往下处理子目录

非递归实现代码(深度优先,匹配原递归遍历顺序)

以下代码直接修复了原递归代码里的显性bug:

import org.apache.commons.net.ftp.FTPClient;
import org.apache.commons.net.ftp.FTPFile;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayDeque;
import java.util.Deque;

private static void listFolderNonRecursive(FTPClient ftpClient, String remoteRootPath, String localSaveRoot) throws IOException {
    // 用ArrayDeque模拟栈,比传统Stack类性能更高,存储所有待遍历的远端目录
    Deque<String> pendingDirs = new ArrayDeque<>();
    // 初始化时把根目录压入栈
    pendingDirs.push(remoteRootPath);

    while (!pendingDirs.isEmpty()) {
        String currentDir = pendingDirs.pop();
        System.out.println("Listing folder " + currentDir);
        FTPFile[] remoteFiles = ftpClient.listFiles(currentDir);

        // 空值防御:权限不足、目录不存在、网络波动时listFiles可能返回null
        if (remoteFiles == null || remoteFiles.length == 0) {
            continue;
        }

        for (FTPFile remoteFile : remoteFiles) {
            String fileName = remoteFile.getName();
            // 跳过.和..特殊目录
            if (".".equals(fileName) || "..".equals(fileName)) {
                continue;
            }
            String remoteFullPath = currentDir + "/" + fileName;

            if (remoteFile.isDirectory()) {
                // 子目录压入栈等待后续处理
                pendingDirs.push(remoteFullPath);
                // 提前创建本地对应的目录,避免下载文件时父目录不存在报错
                String relativePath = remoteFullPath.substring(remoteRootPath.length());
                File localDir = new File(localSaveRoot, relativePath);
                if (!localDir.exists()) {
                    localDir.mkdirs();
                }
            } else {
                System.out.println("Found file " + remoteFullPath);
                String relativePath = remoteFullPath.substring(remoteRootPath.length());
                File localFile = new File(localSaveRoot, relativePath);
                // try-with-resources自动关闭流,避免资源泄漏
                try (OutputStream os = new BufferedOutputStream(new FileOutputStream(localFile))) {
                    ftpClient.retrieveFile(remoteFullPath, os);
                }
            }
        }
    }
}

代码优化建议

  • 修复原代码的显性bug
    • 原代码大括号不匹配,特殊目录的判断块没有正确闭合,导致子目录也会走到下载逻辑,直接调用retrieveFile下载目录会抛出IO异常
    • 原代码中本地存储根路径directory是未定义的魔法变量,应该作为方法入参显式传入,提升代码可读性
    • 原代码的文件输出流没有关闭,会导致文件句柄泄漏,长时间运行会耗尽系统资源
  • 性能优化
    • 不要使用继承自Vector的Stack类做栈结构,它的所有方法都加了synchronized锁,单线程场景下性能差,推荐用ArrayDeque替代
    • 如果遍历的目录文件量极大,可以把目录遍历和文件下载拆成两个逻辑,用生产者-消费者模式分线程处理,避免遍历耗时太长导致FTP控制连接超时断开
  • 鲁棒性优化
    • 增加FTP返回码校验,调用listFiles、retrieveFile后检查返回状态,遇到权限不足、连接断开等错误时做重试或明确抛出异常,不要静默失败
    • 路径拼接可以增加冗余斜杠处理,避免手动拼接时出现//这类无效路径;大文件下载可以调整缓冲区大小、增加断点续传逻辑,降低网络波动的影响
    • 如果需要调整为层序遍历,只需要把栈的push/pop操作替换为队列的offer/poll即可,核心遍历逻辑不需要改动

内容的提问来源于stack exchange,提问作者Ajm Kir

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.27 09:24:19