PAYATHON 2026

Apache Camel SFTP Consumer 如何按最后修改时间预排序?

技术老齐

可以将 SftpOperations.listFiles() 返回的条目按最后修改时间升序排列,再配合 maxMessagesPerPoll=10 和 eagerMaxMessagesPerPoll=true,让 Consumer 优先选取最旧的候选文件。这样能减少后续为排序创建的对象,但仍需获取完整目录列表,内存占用不会固定在 10 个文件的水平。

现有材料没有提供 Camel 支持 file:modified 内置预排序选项的官方计划或发布承诺,因此无法确认未来哪个版本会支持。

现有配置的取舍

按题述处理流程,这三个参数作用于不同阶段:

配置作用
maxMessagesPerPoll=10限制单次轮询选取的文件数量
sortBy=file:modified按最后修改时间对候选文件排序
eagerMaxMessagesPerPoll决定是否在完成候选文件收集前提前应用数量限制

当 eagerMaxMessagesPerPoll=false 时,Camel 可以先收集候选文件并排序,再选取前 10 个。在题述执行路径中,排序涉及的 Exchange、GenericFileMessage 等对象会随候选文件数量增加。这个范围是进入排序的候选文件,未必包含服务器上所有被过滤掉的文件。

设置为 eagerMaxMessagesPerPoll=true 后,数量限制会提前生效。如果目录列表没有预先按修改时间排序,后面的 sortBy 只能调整已选文件的顺序,无法补回修改时间更早却未被选中的文件。要优先选取最旧文件,就需要在数量限制生效前排好目录条目的顺序。

在目录列表返回后排序

可以在 super.listFiles(path) 返回后排序:

public class CustomSortedSftpOperations extends SftpOperations {

    public CustomSortedSftpOperations(Proxy proxy) {
        super(proxy);
    }

    @Override
    public SftpRemoteFile[] listFiles(String path)
            throws GenericFileOperationFailedException {

        SftpRemoteFile[] files = super.listFiles(path);

        if (files != null && files.length > 1) {
            Arrays.sort(
                files,
                Comparator.comparingLong(SftpRemoteFile::getLastModified)
            );
        }

        return files;
    }
}

代码沿用题述 API 签名,省略了 import,能否直接编译需以实际使用的 Camel 依赖为准。

执行到 Arrays.sort() 时,SftpRemoteFile[] 和数组中的条目已经存在,因此不能称为“在创建任何文件对象之前排序”。排序发生在目录条目返回之后、Consumer 后续包装和创建 Exchange 之前。

假设一次目录列表包含 N 个条目:

  • 完整目录列表占用的内存仍随 N 增长。
  • 排序时间复杂度为 O(N log N),对象数组排序还可能使用临时空间。
  • 提前限制候选数量,可以减少后续处理大量候选文件时的对象分配。

这项优化不能保证“没有额外内存开销”,也不会让目录列表“只保留 10 个目录条目”。

题述的 preSortComparator 位于 GenericFile 层,与直接排序 SftpRemoteFile[] 的位置不同。Java setter 的存在也不足以证明某种 URI 写法受支持。能否通过参数绑定或注册表引用配置,需要检查目标版本的端点选项和实现。

接入自定义 Operations

按照题述扩展方式,由自定义端点创建 Operations:

public class CustomSortedSftpEndpoint extends SftpEndpoint {

    public CustomSortedSftpEndpoint(
            String uri,
            SftpComponent component,
            SftpConfiguration configuration) {
        super(uri, component, configuration);
    }

    @Override
    public RemoteFileOperations<SftpRemoteFile>
            createRemoteFileOperations() {

        CustomSortedSftpOperations operations =
            new CustomSortedSftpOperations(getProxy());

        operations.setEndpoint(this);
        return operations;
    }
}

再通过组件返回自定义端点:

@Component("sftp")
public class CustomSortedSftpComponent extends SftpComponent {

    @Override
    protected GenericFileEndpoint<SftpRemoteFile> buildFileEndpoint(
            String uri,
            String remaining,
            Map<String, Object> parameters) throws Exception {

        GenericFileEndpoint<SftpRemoteFile> stockEndpoint =
            super.buildFileEndpoint(uri, remaining, parameters);

        SftpEndpoint sftp = (SftpEndpoint) stockEndpoint;

        return new CustomSortedSftpEndpoint(
            uri,
            this,
            (SftpConfiguration) sftp.getConfiguration()
        );
    }
}

接入后,轮询配置可以使用:

maxMessagesPerPoll=10&eagerMaxMessagesPerPoll=true

如果 sortBy=file:modified 仅用于相同的升序排序,可以在验证行为后移除,避免重复排序。若还有其他排序规则,则需要确认它们与目录预排序如何配合。

部署前还要确认 Camel 实际解析到的是自定义组件。仅有 @Component("sftp"),不足以确定当前 Spring Boot 自动配置和 Camel 组件注册流程会采用这个实例。

检查端点和 Operations 的初始化

排序覆写本身只调整数组顺序,完整接入代码还替换了端点和 Operations 的创建过程,因此需要检查默认初始化行为是否保留。

组件先创建默认端点,再用它的 SftpConfiguration 创建新端点。复用配置对象并不会复制默认端点的全部状态。应对照 Camel 4.14.5 的实现,确认默认构建过程中是否还设置了其他端点属性,以及 URI 参数最终绑定到了哪个实例。

自定义 createRemoteFileOperations() 只执行了构造和 setEndpoint(),也需要与目标版本的默认方法逐项比较,检查是否遗漏其他初始化步骤。这几行代码不足以证明认证、代理及其他连接配置全部保留。

如果初始化路径一致,过滤、读取和消费后的处理仍可交给 Camel。不过,文件遍历顺序已经改变,提前生效的数量限制也会选出不同的文件。

文件顺序与稳定性

按最后修改时间排序只能决定候选顺序,无法判断文件是否写入完成。仍在上传的文件也可能排在前面。

文件稳定性需要通过目标版本支持的读取锁、文件年龄条件,或上传完成后再重命名等约定处理。还需验证以下情况:

  • 前 10 个候选中有未稳定文件时,实际消费数量可能不足 10,不能假定 Camel 会继续补足。
  • 递归扫描子目录时,每次 listFiles(path) 的局部排序无法保证整个目录树的全局顺序。
  • 修改时间相同的文件没有明确的业务先后关系。如果需要确定顺序,应增加文件名或路径等次级比较条件。
  • 即使候选选取顺序正确,并发或异步处理仍可能改变完成顺序。

这一方案能保证的范围是:在指定目录列表、过滤条件和稳定性检查规则下,优先选取修改时间较早的候选文件。

升级时需要验证什么

继承实现类会依赖具体实现路径,因此存在升级风险。兼容性需要结合这些路径判断,不能只看是否标有 SPI 注解。

方法签名、构造函数或泛型发生变化,通常会导致编译失败。更隐蔽的情况是代码仍能编译,但 Camel 调整了端点初始化、目录遍历或数量限制的位置,导致覆写失效。这类变化未必会提前给出弃用警告。

升级时至少应验证:目录返回顺序打乱后,是否仍优先选取最旧文件;过滤和稳定性检查是否正常;连接配置是否完整保留;大量文件场景下的对象分配是否确实减少。验证内存时,应分别观察目录条目和 Exchange 的数量。每轮最多消费 10 个文件,并不能说明内存占用已经受控。

备注:内容仅供参考。