FTP 出站网关

FTP 出站网关提供了一组有限的命令,用于与远程 FTP 或 FTPS 服务器交互。支持的命令有:

  • ls (列出文件)

  • nlst (列出文件名)

  • get (检索文件)

  • mget (检索文件(s))

  • rm (删除文件(s))

  • mv (移动/重命名文件)

  • put (发送文件)

  • mput (发送多个文件)

使用 ls 命令

ls 用于列出远程文件并支持以下选项:

  • -1:获取文件名称列表。默认是获取 FileInfo 对象列表。

  • -a:包含所有文件(包括以 '.' 开头的文件)。

  • -f:不排序列表。

  • -dirs:包含目录(默认不包含)。

  • -links:包含符号链接(默认不包含)。

  • -R:递归列出远程目录。

此外,提供了文件名过滤功能,方式与 inbound-channel-adapter 相同。参见 FTP 入站通道适配器

ls 操作产生的消息负载是文件名称列表或 FileInfo 对象列表。这些对象提供了修改时间、权限等信息。

ls 命令操作的远程目录在 file_remoteDirectory 消息头中提供。

当使用递归选项 (-R) 时,fileName 会包含任何子目录元素,表示文件的相对路径(相对于远程目录)。如果包含 -dirs 选项,每个递归目录也会作为列表中的一个元素返回。在这种情况下,建议您不要使用 -1 选项,因为您无法区分文件和目录,而使用 FileInfo 对象可以做到。

从版本 4.3 开始,FtpSession 支持 list()listNames() 方法的 null 参数。因此,您可以省略 expression 属性。为了方便,Java 配置提供了两个不带 expression 参数的构造函数。对于 LSNLSTPUTMPUT 命令,根据 FTP 协议,null 被视为客户端工作目录。所有其他命令必须提供 expression 以根据请求消息评估远程路径。当您扩展 DefaultFtpSessionFactory 并实现 postProcessClientAfterConnect() 回调时,可以使用 FTPClient.changeWorkingDirectory() 函数设置工作目录。

使用 nlst 命令

版本 5 引入了对 nlst 命令的支持。

nlst 列出远程文件名,仅支持一个选项:

  • -f:不排序列表。

nlst 操作产生的消息负载是文件名称列表。

nlst 命令操作的远程目录在 file_remoteDirectory 消息头中提供。

与使用 LIST 命令的 ls 命令-1 选项不同,nlst 命令发送一个 NLST 命令到目标 FTP 服务器。当服务器不支持 LIST(例如,由于安全限制)时,此命令非常有用。nlst 操作的结果是只有名称,没有其他细节。因此,框架无法确定一个实体是否是目录,从而无法执行过滤或递归列表等操作。

使用 get 命令

get 用于检索远程文件。它支持以下选项:

  • -P:保留远程文件的时间戳。

  • -stream:将远程文件作为流检索。

  • -D:传输成功后删除远程文件。如果传输被忽略(因为 FileExistsModeIGNORE 且本地文件已存在),则不会删除远程文件。

file_remoteDirectory 消息头提供了远程目录名,file_remoteFile 消息头提供了文件名。

get 操作产生的消息负载是一个表示检索到的文件的 File 对象,或者当您使用 -stream 选项时,是一个 InputStream-stream 选项允许将文件作为流检索。对于文本文件,一个常见的用例是将此操作与文件分发器流转换器结合使用。当以流方式消费远程文件时,您负责在流消费后关闭 Session。为了方便,SessioncloseableResource 消息头中提供,您可以使用 IntegrationMessageHeaderAccessor 上的便捷方法访问它。以下示例展示了如何使用便捷方法:

Closeable closeable = new IntegrationMessageHeaderAccessor(message).getCloseableResource();
if (closeable != null) {
    closeable.close();
}

文件分发器流转换器等框架组件在数据传输后会自动关闭 session。

以下示例展示了如何将文件作为流消费:

<int-ftp:outbound-gateway session-factory="ftpSessionFactory"
                            request-channel="inboundGetStream"
                            command="get"
                            command-options="-stream"
                            expression="payload"
                            remote-directory="ftpTarget"
                            reply-channel="stream" />

<int-file:splitter input-channel="stream" output-channel="lines" />
如果您在自定义组件中消费输入流,必须关闭 Session。您可以在自定义代码中完成,或通过将消息副本路由到 service-activator 并使用 SpEL 来完成,如下例所示:
<int:service-activator input-channel="closeSession"
    expression="headers['closeableResource'].close()" />

使用 mget 命令

mget 根据模式检索多个远程文件,并支持以下选项:

  • -P:保留远程文件的时间戳。

  • -R:递归检索整个目录树。

  • -x:如果没有文件与模式匹配,则抛出异常(否则返回空列表)。

  • -D:传输成功后删除每个远程文件。如果传输被忽略(因为 FileExistsModeIGNORE 且本地文件已存在),则不会删除远程文件。

mget 操作产生的消息负载是一个 List<File> 对象(即一个 File 对象列表,每个对象表示一个检索到的文件)。

从版本 5.0 开始,如果 FileExistsModeIGNORE,输出消息的负载将不再包含由于文件已存在而未获取的文件。此前,列表包含所有文件,包括已存在的文件。

用于确定远程路径的表达式应产生一个以 * 结尾的结果,例如 somedir/* 将获取 somedir 下的完整树。

从版本 5.0 开始,递归 mget 与新的 FileExistsMode.REPLACE_IF_MODIFIED 模式相结合,可用于定期在本地同步整个远程目录树。无论 -P(保留时间戳)选项如何,此模式都会用远程时间戳替换本地文件的最后修改时间戳。

使用递归 (-R)

模式被忽略,并假定为 *。默认情况下,检索整个远程树。但是,可以通过提供 FileListFilter 来过滤树中的文件。也可以以这种方式过滤树中的目录。FileListFilter 可以通过引用、filename-patternfilename-regex 属性提供。例如,filename-regex="(subDir|.*1.txt)" 会检索远程目录及其子目录 subDir 中所有以 1.txt 结尾的文件。然而,下一个示例展示了版本 5.0 提供的一种替代方法。

如果过滤了子目录,则不会对该子目录进行额外遍历。

不允许使用 -dirs 选项(递归 mget 使用递归 ls 获取目录树,因此列表本身不能包含目录)。

通常,您会在 local-directory-expression 中使用 #remoteDirectory 变量,以便在本地保留远程目录结构。

持久化文件列表过滤器现在有一个布尔属性 forRecursion。将此属性设置为 true 也会设置 alwaysAcceptDirectories,这意味着对出站网关(lsmget)的递归操作现在每次都会遍历完整的目录树。这是为了解决未检测到目录树深层更改的问题。此外,forRecursion=true 会导致使用文件的完整路径作为元数据存储键;这解决了如果同一名称的文件出现在不同目录中多次时过滤器无法正常工作的问题。重要提示:这意味着对于顶层目录以下的文件,将不会在持久化元数据存储中找到现有键。因此,此属性默认为 false;这可能会在将来的版本中更改。

从版本 5.0 开始,通过将 alwaysAcceptDirectories 属性设置为 true,可以配置 FtpSimplePatternFileListFilterFtpRegexPatternFileListFilter 以始终允许目录通过。这样做允许对简单模式进行递归,如下例所示:

<bean id="starDotTxtFilter"
        class="org.springframework.integration.ftp.filters.FtpSimplePatternFileListFilter">
    <constructor-arg value="*.txt" />
    <property name="alwaysAcceptDirectories" value="true" />
</bean>

<bean id="dotStarDotTxtFilter"
            class="org.springframework.integration.ftp.filters.FtpRegexPatternFileListFilter">
    <constructor-arg value="^.*\.txt$" />
    <property name="alwaysAcceptDirectories" value="true" />
</bean>

定义了如上例所示的过滤器后,您可以通过在网关上设置 filter 属性来使用它们。

使用 put 命令

put 命令将文件发送到远程服务器。消息的负载可以是 java.io.Filebyte[]Stringremote-filename-generator(或 expression)用于命名远程文件。其他可用属性包括 remote-directorytemporary-remote-directory 及其 *-expression 等效属性:use-temporary-file-nameauto-create-directory。有关更多信息,请参见schema 文档。

put 操作产生的消息负载是一个 String,表示传输完成后文件在服务器上的完整路径。

版本 5.2 引入了 chmod 属性,用于在上传后更改远程文件权限。您可以使用传统的 Unix 八进制格式(例如,600 允许文件所有者进行读写)。使用 Java 配置适配器时,可以使用 setChmod(0600)。仅当您的 FTP 服务器支持 SITE CHMOD 子命令时才适用。

使用 mput 命令

mput 将多个文件发送到服务器,仅支持一个选项:

  • -R:递归。发送目录及其子目录中的所有文件(可能已过滤)。

消息负载必须是表示本地目录的 java.io.File(或 String)。从版本 5.1 开始,也支持 FileString 的集合。

此命令支持与put 命令相同的属性。此外,可以使用 mput-patternmput-regexmput-filtermput-filter-expression 之一来过滤本地目录中的文件。只要子目录本身通过过滤器,过滤器就适用于递归。未通过过滤器的子目录不会被递归处理。

mput 操作产生的消息负载是一个 List<String> 对象(即一个远程文件路径列表,是传输结果)。

版本 5.2 引入了 chmod 属性,允许您在上传后更改远程文件权限。您可以使用传统的 Unix 八进制格式(例如,600 允许文件所有者进行读写)。使用 Java 配置适配器时,可以使用 setChmodOctal("600")setChmod(0600)。仅当您的 FTP 服务器支持 SITE CHMOD 子命令时才适用。

使用 rm 命令

rm 命令用于删除文件。

rm 命令没有选项。

rm 操作产生的消息负载是一个 Boolean.TRUE,如果删除成功,否则为 Boolean.FALSEfile_remoteDirectory 消息头提供了远程目录,file_remoteFile 消息头提供了文件名。

使用 mv 命令

mv 命令用于移动文件。

mv 命令没有选项。

expression 属性定义了“源”路径,rename-expression 属性定义了“目标”路径。默认情况下,rename-expressionheaders['file_renameTo']。此表达式不能计算为 null 或空 String。如有必要,将创建任何必需的远程目录。结果消息的负载为 Boolean.TRUEfile_remoteDirectory 消息头提供了原始远程目录,file_remoteFile 消息头提供了文件名。新路径在 file_renameTo 消息头中。

从版本 5.5.6 开始,为了方便,remoteDirectoryExpression 可用于 mv 命令。如果“源”文件不是完整的文件路径,则 remoteDirectoryExpression 的结果将用作远程目录。对于“目标”文件也是如此,例如,如果任务只是重命名某个目录中的远程文件。

关于 FTP 出站网关命令的附加信息

getmget 命令支持 local-filename-generator-expression 属性。它定义了一个 SpEL 表达式,用于在传输期间生成本地文件的名称。评估上下文的根对象是请求消息。对于 mget 特别有用的 remoteFileName 变量也可用,例如 local-filename-generator-expression="#remoteFileName.toUpperCase() + headers.something"

getmget 命令支持 local-directory-expression 属性。它定义了一个 SpEL 表达式,用于在传输期间生成本地目录的名称。评估上下文的根对象是请求消息,但对于 mget 特别有用的 remoteDirectory 变量也可用,例如:local-directory-expression="'/tmp/local/' + #remoteDirectory.toUpperCase() + headers.something"。此属性与 local-directory 属性互斥。

对于所有命令,网关的 'expression' 属性提供了命令作用的路径。对于 mget 命令,expression 可能计算为 '*',表示检索所有文件,或 'somedirectory/*' 等。

以下示例展示了为 ls 命令配置的网关:

<int-ftp:outbound-gateway id="gateway1"
    session-factory="ftpSessionFactory"
    request-channel="inbound1"
    command="ls"
    command-options="-1"
    expression="payload"
    reply-channel="toSplitter"/>

发送到 toSplitter 通道的消息负载是一个 String 对象列表,每个对象包含一个文件名。如果省略了 command-options 属性,它将持有 FileInfo 对象。它使用空格分隔选项,例如 command-options="-1 -dirs -links"

从版本 4.2 开始,GETMGETPUTMPUT 命令支持 FileExistsMode 属性(使用命名空间支持时为 mode)。这影响了当本地文件存在(GETMGET)或远程文件存在(PUTMPUT)时的行为。支持的模式包括 REPLACEAPPENDFAILIGNORE。为了向后兼容,PUTMPUT 操作的默认模式是 REPLACE。对于 GETMGET 操作,默认是 FAIL

从版本 5.0 开始,FtpOutboundGateway 上提供了 setWorkingDirExpression()(XML 中为 working-dir-expression)选项(XML 中为 <int-ftp:outbound-gateway>)。它允许您在运行时更改客户端工作目录。表达式根据请求消息进行评估。在每次网关操作后,会恢复之前的工作目录。

使用 Java 配置进行配置

以下 Spring Boot 应用示例展示了如何使用 Java 配置来配置出站网关:

@SpringBootApplication
public class FtpJavaApplication {

    public static void main(String[] args) {
        new SpringApplicationBuilder(FtpJavaApplication.class)
            .web(false)
            .run(args);
    }

    @Bean
    public SessionFactory<FTPFile> ftpSessionFactory() {
        DefaultFtpSessionFactory sf = new DefaultFtpSessionFactory();
        sf.setHost("localhost");
        sf.setPort(port);
        sf.setUsername("foo");
        sf.setPassword("foo");
        sf.setTestSession(true);
        return new CachingSessionFactory<FTPFile>(sf);
    }

    @Bean
    @ServiceActivator(inputChannel = "ftpChannel")
    public MessageHandler handler() {
        FtpOutboundGateway ftpOutboundGateway =
                          new FtpOutboundGateway(ftpSessionFactory(), "ls", "'my_remote_dir/'");
        ftpOutboundGateway.setOutputChannelName("lsReplyChannel");
        return ftpOutboundGateway;
    }

}

使用 Java DSL 进行配置

以下 Spring Boot 应用示例展示了如何使用 Java DSL 来配置出站网关:

@SpringBootApplication
public class FtpJavaApplication {

    public static void main(String[] args) {
        new SpringApplicationBuilder(FtpJavaApplication.class)
            .web(false)
            .run(args);
    }

    @Bean
    public SessionFactory<FTPFile> ftpSessionFactory() {
        DefaultFtpSessionFactory sf = new DefaultFtpSessionFactory();
        sf.setHost("localhost");
        sf.setPort(port);
        sf.setUsername("foo");
        sf.setPassword("foo");
        sf.setTestSession(true);
        return new CachingSessionFactory<FTPFile>(sf);
    }

    @Bean
    public FtpOutboundGatewaySpec ftpOutboundGateway() {
        return Ftp.outboundGateway(ftpSessionFactory(),
            AbstractRemoteFileOutboundGateway.Command.MGET, "payload")
            .options(AbstractRemoteFileOutboundGateway.Option.RECURSIVE)
            .regexFileNameFilter("(subFtpSource|.*1.txt)")
            .localDirectoryExpression("'localDirectory/' + #remoteDirectory")
            .localFilenameExpression("#remoteFileName.replaceFirst('ftpSource', 'localTarget')");
    }

    @Bean
    public IntegrationFlow ftpMGetFlow(AbstractRemoteFileOutboundGateway<FTPFile> ftpOutboundGateway) {
        return f -> f
            .handle(ftpOutboundGateway)
            .channel(c -> c.queue("remoteFileOutputChannel"));
    }

}

出站网关部分成功 (mgetmput)

当您对多个文件执行操作时(使用 mgetmput),可能在传输一个或多个文件后发生异常。在这种情况下(从版本 4.2 开始),会抛出 PartialSuccessException。除了常见的 MessagingException 属性(failedMessagecause)之外,此异常还有两个附加属性:

  • partialResults:成功传输的结果。

  • derivedInput:根据请求消息生成的文件列表(例如,mput 要传输的本地文件)。

这些属性允许您确定哪些文件已成功传输,哪些未成功。

对于递归 mput 的情况,PartialSuccessException 可能包含嵌套的 PartialSuccessException

考虑以下目录结构:

root/
|- file1.txt
|- subdir/
   | - file2.txt
   | - file3.txt
|- zoo.txt

如果在 file3.txt 上发生异常,网关抛出的 PartialSuccessExceptionderivedInputfile1.txtsubdirzoo.txtpartialResultsfile1.txt。其 cause 是另一个 PartialSuccessException,其 derivedInputfile2.txtfile3.txtpartialResultsfile2.txt