FTP Outbound Gateway

FTP 出站网关提供了一组有限的命令来与一个远程 FTP 或 FTPS 服务器进行交互。支持的命令包括:

  • ls (list files)

  • nlst (list file names)

  • get (retrieve file)

  • mget (retrieve file(s))

  • rm (remove file(s))

  • mv (move/rename file)

  • put (send file)

  • mput (send multiple files)

Using the ls Command

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

  • -1:检索文件名的列表。默认设置是检索 FileInfo 对象的列表。

  • -a:包括所有文件(包括以“.”开头的文件)

  • -f:不按清单排序

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

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

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

此外,还提供了文件名筛选,其方式与 `inbound-channel-adapter`相同。请参见 FTP Inbound Channel Adapter

ls 操作返回的消息有效负载是一个文件名列表或一个 FileInfo 对象列表。这些对象提供诸如修改时间、权限和其他详细信息之类的信息。

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

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

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

Using the nlst Command

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

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

  • -f:不按清单排序

nlst 操作返回的消息有效负载是一个文件名列表。

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

与使用 LIST 命令的 xref:ftp/outbound-gateway.adoc#ftp-using-ls[ls 命令的 -1 选项不同,nlst 命令会向目标 FTP 服务器发送 NLST 命令。当服务器不支持 LIST(例如由于安全限制)时,此命令非常有用。nlst 操作的结果是名称,不包含其他详细信息。因此,该框架无法判断一个实体是否是一个目录,例如是为了执行筛选或递归列出。

Using the get Command

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

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

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

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

file_remoteDirectory 标头提供远程目录名称,file_remoteFile 标头提供文件名。

get`操作产生的消息负载是表示检索到的文件或在您使用 `-stream`选项时的 `File`对象。-stream`选项允许将文件作为流检索。对于文本文件,一个常见的用例是将此操作与 file splitterstream transformer结合使用。当以流的形式消耗远程文件时,您负责在消耗流后关闭 Session。为了方便起见,`Session`在 `closeableResource`头中提供,您可以通过 `IntegrationMessageHeaderAccessor`上的便捷方法访问它。以下示例显示了如何使用便捷方法:

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

file splitterstream transformer等框架组件在数据传输后自动关闭会话。

以下示例显示了如何将文件作为流使用:

<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()" />

Using the mget Command

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

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

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

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

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

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

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

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

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

Example 1. Using recursion (-R)

将忽略模式,并假设为 *。默认检索整个远程树。但是,可以通过提供 FileListFilter 来过滤树中的文件。也可以通过这种方式过滤树中的目录。FileListFilter 可以通过引用、filename-patternfilename-regex 属性提供。例如,filename-regex="(subDir|.*1.txt) 检索远程目录中所有以 1.txt 结尾的文件和 subDir 子目录。然而,下一个示例显示了一个替代方法,版本 5.0 提供了该替代方法。 如果过滤了一个子目录,则不会对该子目录执行额外的遍历。 不允许使用 -dirs 选项(递归 mget 使用递归 ls 来获取目录树,因此无法将目录本身包含在列表中)。 通常,您会在 local-directory-expression 中使用 #remoteDirectory 变量,以便在本地保留远程目录结构。

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

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

<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 属性来使用一个过滤器。

另请参阅 xref:ftp/outbound-gateway.adoc#ftp-partial[Outbound Gateway Partial Success (mgetmput)。

Using the put Command

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

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

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

Using the mput Command

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

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

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

此命令支持与 put command相同的属性。此外,可以使用 mput-patternmput-regex、`mput-filter`或 `mput-filter-expression`之一过滤本地目录中的文件。只要子目录本身通过筛选器,筛选器就可以与递归一起使用。未通过筛选器的子目录不会递归。

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

另请参阅 xref:ftp/outbound-gateway.adoc#ftp-partial[Outbound Gateway Partial Success (mgetmput)。

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

Using the rm Command

rm 命令删除文件。

rm 命令没有选项。

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

Using the mv Command

mv 命令移动文件。

mv 命令没有选项。

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

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

Additional Information about FTP Outbound Gateway Commands

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

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

对于所有命令,网关的“expression”属性提供了命令作用的路径。对于 mget 命令,表达式可能会求值为“”,表示检索所有文件,或“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 开始,setWorkingDirExpression()(XML 中的 working-dir-expression)选项提供在 FtpOutboundGateway(XML 中的 int-ftp:outbound-gateway)上。它允许您在运行时更改客户端工作目录。表达式针对请求消息进行评估。每次网关操作后都将还原以前的工作目录。

Configuring with Java Configuration

以下 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;
    }

}

Configuring with the 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"));
    }

}

Outbound Gateway Partial Success (mget and mput)

当您对多个文件执行操作(通过使用 mgetmput)时,在传输了一个或多个文件后,一段时间后可能会出现异常。在这种情况下(从版本 4.2 开始),将引发 PartialSuccessException。除了通常的 MessagingException 属性(failedMessagecause)外,此异常还有两个其他属性:

  • partialResults: 成功传输的结果。

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

这些属性使您可以确定哪些文件已成功传输,哪些文件尚未传输。

在递归 mput 的情况下,PartialSuccessException 可能具有嵌套的 PartialSuccessException 发生。

考虑以下目录结构:

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

如果异常发生在 file3.txt 上,则网关引发的 PartialSuccessException 具有 file1.txtsubdirzoo.txtderivedInput,以及 file1.txtpartialResults。其 cause 是另一个具有 file2.txtfile3.txtderivedInputfile2.txtpartialResultsPartialSuccessException