写入文件

要将消息写入文件系统,可以使用 FileWritingMessageHandler。此类可以处理以下负载类型:

  • File

  • String

  • 字节数组

  • InputStream(自 版本 4.2 起)

对于 String 负载,可以配置编码和字符集。

为了简化,您可以使用 XML 命名空间将 FileWritingMessageHandler 配置为出站通道适配器或出站网关的一部分。

从版本 4.3 开始,可以指定写入文件时使用的缓冲区大小。

从版本 5.1 开始,可以提供一个 BiConsumer<File, Message<?>> 类型的 newFileCallback,如果您使用 FileExistsMode.APPENDFileExistsMode.APPEND_NO_FLUSH 模式且需要创建新文件时,将触发此回调。此回调接收新创建的文件以及触发它的消息。例如,此回调可用于写入消息头中定义的 CSV 标题。

生成文件名

最简单的形式下,FileWritingMessageHandler 只需要一个目标目录来写入文件。待写入文件的名称由处理器的 FileNameGenerator 决定。默认实现查找键与常量 FileHeaders.FILENAME 匹配的消息头。

或者,可以指定一个表达式,针对消息进行评估以生成文件名——例如,headers['myCustomHeader'] + '.something'。该表达式必须评估为一个 String。为了方便起见,DefaultFileNameGenerator 还提供了 setHeaderName 方法,允许您明确指定要用作文件名的消息头的值。

设置好后,DefaultFileNameGenerator 采用以下解析步骤来确定给定消息负载的文件名:

  1. 针对消息评估表达式,如果结果是非空的 String,则将其用作文件名。

  2. 否则,如果负载是 java.io.File,则使用 File 对象的文件名。

  3. 否则,使用消息 ID 并附加 .msg 作为文件名。

使用 XML 命名空间支持时,文件出站通道适配器和文件出站网关都支持以下互斥的配置属性:

  • filename-generator(对 FileNameGenerator 实现的引用)

  • filename-generator-expression(评估为 String 的表达式)

写入文件时,会使用临时文件后缀(默认值为 .writing)。它会在文件写入期间附加到文件名。要自定义后缀,可以在文件出站通道适配器和文件出站网关上设置 temporary-file-suffix 属性。

使用 APPEND 文件 mode 时,temporary-file-suffix 属性将被忽略,因为数据直接附加到文件中。

从版本 4.2.5 开始,生成的文件名(由 filename-generatorfilename-generator-expression 评估产生)可以表示一个子路径以及目标文件名。它像以前一样用作 File(File parent, String child) 的第二个构造函数参数。但是,过去我们不会为子路径创建 (mkdirs()) 目录,仅假定只有文件名。这种方法对于需要恢复文件系统树以匹配源目录的情况非常有用——例如,解压存档并将所有文件按原始顺序保存在目标目录中时。

指定输出目录

文件出站通道适配器和文件出站网关都提供了两个互斥的配置属性来指定输出目录:

  • directory

  • directory-expression

Spring Integration 2.2 引入了 directory-expression 属性。

使用 directory 属性

使用 directory 属性时,输出目录被设置为固定值,该值在 FileWritingMessageHandler 初始化时设置。如果未指定此属性,则必须使用 directory-expression 属性。

使用 directory-expression 属性

如果希望获得完整的 SpEL 支持,可以使用 directory-expression 属性。该属性接受一个 SpEL 表达式,该表达式会针对处理的每条消息进行评估。因此,动态指定输出文件目录时,您可以完全访问消息的负载及其头。

SpEL 表达式必须解析为 Stringjava.io.Fileorg.springframework.core.io.Resource。(后者无论如何都会评估为 File。)此外,结果的 StringFile 必须指向一个目录。如果未指定 directory-expression 属性,则必须设置 directory 属性。

使用 auto-create-directory 属性

默认情况下,如果目标目录不存在,将自动创建相应的目标目录和所有不存在的父目录。为了阻止这种行为,可以将 auto-create-directory 属性设置为 false。此属性适用于 directorydirectory-expression 属性。

当使用 directory 属性且 auto-create-directoryfalse 时,自 Spring Integration 2.2 起进行了以下更改:

现在不是在适配器初始化时检查目标目录是否存在,而是针对处理的每条消息执行此检查。

此外,如果 auto-create-directorytrue,并且目录在处理消息之间被删除,则会为处理的每条消息重新创建该目录。

处理已存在的目录文件

写入文件时,如果目标文件已存在,默认行为是覆盖该目标文件。可以通过在相关的文件出站组件上设置 mode 属性来更改此行为。存在以下选项:

  • REPLACE(默认)

  • REPLACE_IF_MODIFIED

  • APPEND

  • APPEND_NO_FLUSH

  • FAIL

  • IGNORE

Spring Integration 2.2 引入了 mode 属性和 APPENDFAILIGNORE 选项。
REPLACE

如果目标文件已存在,则会被覆盖。如果未指定 mode 属性,这是写入文件时的默认行为。

REPLACE_IF_MODIFIED

如果目标文件已存在,则仅在最后修改时间戳与源文件不同时才会被覆盖。对于 File 负载,会将负载的 lastModified 时间与现有文件进行比较。对于其他负载,会将 FileHeaders.SET_MODIFIED (file_setModified) 头与现有文件进行比较。如果头缺失或其值不是 Number 类型,则总是替换文件。

APPEND

此模式允许您将消息内容追加到现有文件,而不是每次都创建一个新文件。请注意,此属性与 temporary-file-suffix 属性互斥,因为它将内容追加到现有文件时,适配器不再使用临时文件。文件在每条消息处理后关闭。

APPEND_NO_FLUSH

此选项与 APPEND 具有相同的语义,但数据不会被刷新,并且文件在每条消息处理后不会关闭。这可以显著提升性能,但也存在失败时数据丢失的风险。有关更多信息,请参阅 使用 APPEND_NO_FLUSH 时刷新文件

FAIL

如果目标文件存在,则会抛出 MessageHandlingException

IGNORE

如果目标文件存在,则静默忽略消息负载。

使用临时文件后缀(默认为 .writing)时,如果最终文件名或临时文件名已存在,则应用 IGNORE 选项。

使用 APPEND_NO_FLUSH 时刷新文件

APPEND_NO_FLUSH 模式在版本 4.3 中添加。使用它可以提高性能,因为文件在每条消息处理后不会关闭。但是,这可能会导致在发生故障时丢失数据。

Spring Integration 提供了几种刷新策略来减轻此数据丢失的风险:

  • 使用 flushInterval。如果文件在此期间没有被写入,则会自动刷新。这是近似值,可能达到此时间的 1.33 倍(平均 1.167 倍)。

  • 向消息处理器的 trigger 方法发送包含正则表达式的消息。与模式匹配的绝对路径文件将被刷新。

  • 为处理器提供自定义的 MessageFlushPredicate 实现,以修改将消息发送到 trigger 方法时采取的操作。

  • 通过传入自定义的 FileWritingMessageHandler.FlushPredicateFileWritingMessageHandler.MessageFlushPredicate 实现,调用处理器的其中一个 flushIfNeeded 方法。

谓词会为每个打开的文件调用。有关这些接口的更多信息,请参阅 Javadoc。请注意,自版本 5.0 起,谓词方法提供另一个参数:如果是新文件或之前已关闭的文件,则表示当前文件首次写入的时间。

使用 flushInterval 时,间隔时间从最后一次写入开始计算。文件只有在空闲达到指定间隔时间后才会被刷新。从版本 4.3.7 开始,可以设置一个额外的属性 (flushWhenIdle) 为 false,这意味着间隔时间从首次写入之前已刷新(或新)的文件开始计算。

文件时间戳

默认情况下,目标文件的 lastModified 时间戳是文件创建的时间(但原地重命名会保留当前时间戳)。从版本 4.3 开始,现在可以配置 preserve-timestamp(或使用 Java 配置时设置 setPreserveTimestamp(true))。对于 File 负载,这会将时间戳从入站文件传输到出站文件(无论是否需要复制)。对于其他负载,如果存在 FileHeaders.SET_MODIFIED 头 (file_setModified),只要该头是 Number 类型,就用它来设置目标文件的 lastModified 时间戳。

文件权限

从版本 5.0 开始,在支持 Posix 权限的文件系统上写入文件时,可以在出站通道适配器或网关上指定这些权限。该属性是一个整数,通常以熟悉的八进制格式提供——例如,0640,意味着所有者具有读/写权限,组具有只读权限,而其他人没有访问权限。

文件出站通道适配器

以下示例配置了一个文件出站通道适配器:

<int-file:outbound-channel-adapter id="filesOut" directory="${input.directory.property}"/>

基于命名空间的配置还支持一个 delete-source-files 属性。如果设置为 true,则在写入目标后触发删除原始源文件。该标志的默认值为 false。以下示例展示了如何将其设置为 true

<int-file:outbound-channel-adapter id="filesOut"
    directory="${output.directory}"
    delete-source-files="true"/>
delete-source-files 属性仅在入站消息具有 File 负载,或者 FileHeaders.ORIGINAL_FILE 头的值包含源 File 实例或表示原始文件路径的 String 时生效。

从版本 4.2 开始,FileWritingMessageHandler 支持一个 append-new-line 选项。如果设置为 true,则在写入消息后向文件追加新行。该属性的默认值为 false。以下示例展示了如何使用 append-new-line 选项:

<int-file:outbound-channel-adapter id="newlineAdapter"
	append-new-line="true"
    directory="${output.directory}"/>

出站网关

在需要根据写入的文件继续处理消息的情况下,可以使用 outbound-gateway 代替。它的作用类似于 outbound-channel-adapter。然而,在写入文件后,它还会将文件作为消息负载发送到回复通道。

以下示例配置了一个出站网关:

<int-file:outbound-gateway id="mover" request-channel="moveInput"
    reply-channel="output"
    directory="${output.directory}"
    mode="REPLACE" delete-source-files="true"/>

如前所述,您还可以指定 mode 属性,该属性定义了如何处理目标文件已存在的情况。有关更多详细信息,请参阅 处理已存在的目录文件。通常,使用文件出站网关时,结果文件将作为消息负载在回复通道上返回。

指定 IGNORE 模式时也适用此情况。在这种情况下,返回预先存在的目标文件。如果请求消息的负载是文件,您仍然可以通过消息头访问原始文件。请参阅 FileHeaders.ORIGINAL_FILE

在需要先移动文件然后再将其发送到处理管道的情况下,'outbound-gateway' 工作良好。在这种情况下,您可以将文件命名空间的 inbound-channel-adapter 元素连接到 outbound-gateway,然后将该网关的 reply-channel 连接到管道的开头。

如果您有更复杂的需求或需要支持其他负载类型作为输入以转换为文件内容,可以扩展 FileWritingMessageHandler,但更好的选择是依赖于 Transformer

使用 Java 配置进行配置

以下 Spring Boot 应用展示了如何使用 Java 配置配置入站适配器的示例:

@SpringBootApplication
@IntegrationComponentScan
public class FileWritingJavaApplication {

    public static void main(String[] args) {
        ConfigurableApplicationContext context =
                      new SpringApplicationBuilder(FileWritingJavaApplication.class)
                              .web(false)
                              .run(args);
             MyGateway gateway = context.getBean(MyGateway.class);
             gateway.writeToFile("foo.txt", new File(tmpDir.getRoot(), "fileWritingFlow"), "foo");
    }

    @Bean
    @ServiceActivator(inputChannel = "writeToFileChannel")
    public MessageHandler fileWritingMessageHandler() {
         Expression directoryExpression = new SpelExpressionParser().parseExpression("headers.directory");
         FileWritingMessageHandler handler = new FileWritingMessageHandler(directoryExpression);
         handler.setFileExistsMode(FileExistsMode.APPEND);
         return handler;
    }

    @MessagingGateway(defaultRequestChannel = "writeToFileChannel")
    public interface MyGateway {

        void writeToFile(@Header(FileHeaders.FILENAME) String fileName,
                       @Header(FileHeaders.FILENAME) File directory, String data);

    }
}

使用 Java DSL 进行配置

以下 Spring Boot 应用展示了如何使用 Java DSL 配置入站适配器的示例:

@SpringBootApplication
public class FileWritingJavaApplication {

    public static void main(String[] args) {
        ConfigurableApplicationContext context =
                 new SpringApplicationBuilder(FileWritingJavaApplication.class)
                         .web(false)
                         .run(args);
        MessageChannel fileWritingInput = context.getBean("fileWritingInput", MessageChannel.class);
        fileWritingInput.send(new GenericMessage<>("foo"));
    }

    @Bean
   	public IntegrationFlow fileWritingFlow() {
   	    return IntegrationFlow.from("fileWritingInput")
   		        .enrichHeaders(h -> h.header(FileHeaders.FILENAME, "foo.txt")
   		                  .header("directory", new File(tmpDir.getRoot(), "fileWritingFlow")))
   	            .handle(Files.outboundGateway(m -> m.getHeaders().get("directory")))
   	            .channel(MessageChannels.queue("fileWritingResultChannel"))
   	            .get();
    }

}