Writing files

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

  • File

  • String

  • byte array

  • InputStream (since version 4.2)

对于 String 有效负载,您可以配置编码和字符集。 为了更轻松,您可以使用 XML 命名空间在出站通道适配器或出站网关中配置 FileWritingMessageHandler。 从 4.3 版开始,您可以指定在写入文件时要使用的缓冲区大小。 从 5.1 版开始,您可以提供一个 BiConsumer<File, Message<?>> newFileCallback,如果你使用 FileExistsMode.APPENDFileExistsMode.APPEND_NO_FLUSH 并且必须创建一个新文件,该回调器就会被触发。此回调接收到新创建的文件和触发它的消息。例如,此回调可用于编写消息头中定义的 CSV 标头。

Generating File Names

最简单的形式,FileWritingMessageHandler`只要求一个目标路径来写文件。要写文件的文件名由处理器的 `FileNameGenerator确定。 default implementation搜索密钥与已定义为 FileHeaders.FILENAME常量相匹配的消息标头。

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

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

  1. 根据消息评估表达式,并且,如果结果为非空 String,则将其用作文件名称。

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

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

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

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

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

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

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

从 4.2.5 版开始,生成的文件名(作为 filename-generator`或`filename-generator-expression`评估的结果)可以表示子路径以及目标文件名。它用作 `File(File parent, String child)`的第二个构造函数参数,与之前相同。但是,在过去,我们没有为子路径(仅假设文件名)创建(`mkdirs())目录。当我们需要还原文件系统树以匹配源目录(例如,在解压缩存档并将所有文件按原始顺序保存在目标目录中)时,此方法非常有用。

Specifying the Output Directory

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

  • directory

  • directory-expression

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

Using the directory Attribute

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

Using the directory-expression Attribute

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

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

Using the auto-create-directory Attribute

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

当使用`directory`属性且`auto-create-directory`为`false`时,从 Spring Integration 2.2 开始做出的更改如下: 现在针对每个正在处理的消息执行此检查,而不是在初始化适配器时检查目标目录是否存在。 此外,如果`auto-create-directory`为`true`并且在处理消息之间删除了该目录,则将为每个正在处理的消息重新创建该目录。

Dealing with Existing Destination Files

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

  • REPLACE (Default)

  • REPLACE_IF_MODIFIED

  • APPEND

  • APPEND_NO_FLUSH

  • FAIL

  • IGNORE

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

REPLACE

If the target file already exists, it is overwritten. If the mode attribute is not specified, this is the default behavior when writing files.

REPLACE_IF_MODIFIED

If the target file already exists, it is overwritten only if the last modified timestamp differs from that of the source file. For File payloads, the payload lastModified time is compared to the existing file. For other payloads, the FileHeaders.SET_MODIFIED (file_setModified) header is compared to the existing file. If the header is missing or has a value that is not a Number, the file is always replaced.

APPEND

This mode lets you append message content to the existing file instead of creating a new file each time. Note that this attribute is mutually exclusive with the temporary-file-suffix attribute because, when it appends content to the existing file, the adapter no longer uses a temporary file. The file is closed after each message.

APPEND_NO_FLUSH

This option has the same semantics as APPEND, but the data is not flushed and the file is not closed after each message. This can provide a significant performance at the risk of data loss in the event of a failure. See Flushing Files When Using APPEND_NO_FLUSH for more information.

FAIL

If the target file exists, a MessageHandlingException is thrown.

IGNORE

If the target file exists, the message payload is silently ignored.

当使用临时文件后缀(默认值为 .writing)时,IGNORE 选项适用于最终文件名或临时文件名存在的情况。

Flushing Files When Using APPEND_NO_FLUSH

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

Spring Integration 提供了多种刷新策略来减轻这种数据丢失:

  • 使用 flushInterval。如果一段指定的时间没有写文件,则该文件被自动清空。这是一种近似值,并且可能长达此时间的 1.33x(平均值为 1.167x)。

  • 向消息处理程序的 `trigger`方法发送一个包含正则表达式的消息。具有与模式匹配的绝对路径名称的文件被清空。

  • 向处理程序提供一个自定义 `MessageFlushPredicate`实现,以修改在向 `trigger`方法发送消息时采取的操作。

  • 通过传入一个自定义 `FileWritingMessageHandler.FlushPredicate`或 `FileWritingMessageHandler.MessageFlushPredicate`实现来调用处理程序的 `flushIfNeeded`方法之一。

谓词用于每个打开的文件。更多信息,请参见这些接口的 Javadoc。注意,从 5.0 版起,谓词方法提供另一个参数:如果文件为新文件或先前的已关闭文件,则首次写入当前文件的时间。操作方法如下:

使用`flushInterval`时,间隔从上次写入开始。只有当文件空闲了间隔时间,文件才会被刷新。从 4.3.7 版开始,可以将附加属性(flushWhenIdle)设置为`false`,这意味着该间隔从首次写入先前的刷新(或新建)文件开始。

File Timestamps

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

File Permissions

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

File Outbound Channel Adapter

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

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

只有当入站消息具有 File 有效负载或 FileHeaders.ORIGINAL_FILE 头值包含源 File 实例或表示原始文件路径的 String 时,delete-source-files 属性才有效。

从版本 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-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`特性,它定义了文件已经存在时的情况如何处理。有关详细信息,请参见 Dealing with Existing Destination Files。总的来说,使用文件出口网关时,结果文件会在答复通道上返回为消息有效载荷。

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

当您想先移动文件然后通过处理管道发送它时,“outbound-gateway” 效果很好。在这种情况下,您可以将文件命名空间的 inbound-channel-adapter 元素连接到 outbound-gateway,然后将该网关的 reply-channel 连接到该管道的开头。

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

Configuring with Java Configuration

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

    }
}

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

}