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 消息头中提供。

ls 命令-1 选项(它使用 LIST 命令)不同,nlst 命令向目标 FTP 服务器发送 NLST 命令。 当服务器不支持 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();
}

框架组件(如 文件拆分器流转换器)在数据传输后会自动关闭会话。

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

<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(保留时间戳)选项如何。

Example 1. 使用递归 (-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 开始,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 属性来使用其中一个。

使用 put 命令

put 命令将文件发送到远程服务器。 消息的负载可以是 java.io.Filebyte[]Stringremote-filename-generator(或表达式)用于命名远程文件。 其他可用属性包括 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.FALSE(否则)。 file_remoteDirectory 消息头提供远程目录,file_remoteFile 消息头提供文件名。

使用 mv 命令

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 命令中用于方便。 如果“from”文件不是完整的文件路径,则 remoteDirectoryExpression 的结果用作远程目录。 对于“to”文件也适用,例如,如果任务只是重命名某个目录中的远程文件。

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

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

使用 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