Writing files

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

To write messages to the file system, you can use a FileWritingMessageHandler. This class can deal with the following payload types:

  • File

  • String

  • byte array

  • InputStream (since version 4.2)

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

For a String payload, you can configure the encoding and the charset.

为了更轻松,您可以使用 XML 命名空间在出站通道适配器或出站网关中配置 FileWritingMessageHandler

To make things easier, you can configure the FileWritingMessageHandler as part of an outbound channel adapter or outbound gateway by using the XML namespace.

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

Starting with version 4.3, you can specify the buffer size to use when writing files.

从 5.1 版开始,您可以提供一个 BiConsumer<File, Message<?>> newFileCallback,如果你使用 FileExistsMode.APPENDFileExistsMode.APPEND_NO_FLUSH 并且必须创建一个新文件,该回调器就会被触发。此回调接收到新创建的文件和触发它的消息。例如,此回调可用于编写消息头中定义的 CSV 标头。

Starting with version 5.1, you can provide a BiConsumer<File, Message<?>> newFileCallback which is triggered if you use FileExistsMode.APPEND or FileExistsMode.APPEND_NO_FLUSH and a new file has to be created. This callback receives a newly created file and the message which triggered it. This callback could be used to write a CSV header defined in the message header, for an example.

Generating File Names

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

In its simplest form, the FileWritingMessageHandler requires only a destination directory for writing the files. The name of the file to be written is determined by the handler’s FileNameGenerator. The default implementation looks for a message header whose key matches the constant defined as FileHeaders.FILENAME.

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

Alternatively, you can specify an expression to be evaluated against the message to generate a file name — for example, headers['myCustomHeader'] + '.something'. The expression must evaluate to a String. For convenience, the DefaultFileNameGenerator also provides the setHeaderName method, letting you explicitly specify the message header whose value is to be used as the filename.

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

Once set up, the DefaultFileNameGenerator employs the following resolution steps to determine the filename for a given message payload:

  1. Evaluate the expression against the message and, if the result is a non-empty String, use it as the filename.

  2. Otherwise, if the payload is a java.io.File, use the File object’s filename.

  3. Otherwise, use the message ID appended with .msg as the filename.

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

When you use the XML namespace support, both the file outbound channel adapter and the file outbound gateway support the following mutually exclusive configuration attributes:

  • filename-generator (a reference to a FileNameGenerator implementation)

  • filename-generator-expression (an expression that evaluates to a String)

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

While writing files, a temporary file suffix is used (its default is .writing). It is appended to the filename while the file is being written. To customize the suffix, you can set the temporary-file-suffix attribute on both the file outbound channel adapter and the file outbound gateway.

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

When using the APPEND file mode, the temporary-file-suffix attribute is ignored, since the data is appended to the file directly.

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

Starting with ,version 4.2.5, the generated file name (as a result of filename-generator or filename-generator-expression evaluation) can represent a child path together with the target file name. It is used as a second constructor argument for File(File parent, String child) as before. However, in the past we did not create (mkdirs()) directories for the child path, assuming only the file name. This approach is useful for cases when we need to restore the file system tree to match the source directory — for example, when unzipping the archive and saving all the files in the target directory in the original order.

Specifying the Output Directory

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

Both, the file outbound channel adapter and the file outbound gateway provide two mutually exclusive configuration attributes for specifying the output directory:

  • directory

  • directory-expression

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

Spring Integration 2.2 introduced the directory-expression attribute.

Using the directory Attribute

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

When you use the directory attribute, the output directory is set to a fixed value, which is set when the FileWritingMessageHandler is initialized. If you do not specify this attribute, you must use the directory-expression attribute.

Using the directory-expression Attribute

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

If you want to have full SpEL support, you can use the directory-expression attribute. This attribute accepts a SpEL expression that is evaluated for each message being processed. Thus, you have full access to a message’s payload and its headers when you dynamically specify the output file directory.

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

The SpEL expression must resolve to either a String, java.io.File or org.springframework.core.io.Resource. (The latter is evaluated into a File anyway.) Furthermore, the resulting String or File must point to a directory. If you do not specify the directory-expression attribute, then you must set the directory attribute.

Using the auto-create-directory Attribute

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

By default, if the destination directory does not exist, the respective destination directory and any non-existing parent directories are automatically created. To prevent that behavior, you can set the auto-create-directory attribute to false. This attribute applies to both the directory and the directory-expression attributes.

当使用`directory`属性且`auto-create-directory`为`false`时,从 Spring Integration 2.2 开始做出的更改如下:

When using the directory attribute and auto-create-directory is false, the following change was made starting with Spring Integration 2.2:

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

Instead of checking for the existence of the destination directory when the adapter is initialized, this check is now performed for each message being processed.

此外,如果`auto-create-directory`为`true`并且在处理消息之间删除了该目录,则将为每个正在处理的消息重新创建该目录。

Furthermore, if auto-create-directory is true and the directory was deleted between the processing of messages, the directory is re-created for each message being processed.

Dealing with Existing Destination Files

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

When you write files and the destination file already exists, the default behavior is to overwrite that target file. You can change this behavior by setting the mode attribute on the relevant file outbound components. The following options exist:

  • REPLACE (Default)

  • REPLACE_IF_MODIFIED

  • APPEND

  • APPEND_NO_FLUSH

  • FAIL

  • IGNORE

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

Spring Integration 2.2 introduced the mode attribute and the APPEND, FAIL, and IGNORE options.

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 选项适用于最终文件名或临时文件名存在的情况。

When using a temporary file suffix (the default is .writing), the IGNORE option applies if either the final file name or the temporary file name exists.

Flushing Files When Using APPEND_NO_FLUSH

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

The APPEND_NO_FLUSH mode was added in version 4.3. Using it can improve performance because the file is not closed after each message. However, this can cause data loss in the event of a failure.

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

Spring Integration provides several flushing strategies to mitigate this data loss:

  • Use flushInterval. If a file is not written to for this period of time, it is automatically flushed. This is approximate and may be up to 1.33x this time (with an average of 1.167x).

  • Send a message containing a regular expression to the message handler’s trigger method. Files with absolute path names matching the pattern are flushed.

  • Provide the handler with a custom MessageFlushPredicate implementation to modify the action taken when a message is sent to the trigger method.

  • Invoke one of the handler’s flushIfNeeded methods by passing in a custom FileWritingMessageHandler.FlushPredicate or FileWritingMessageHandler.MessageFlushPredicate implementation.

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

The predicates are called for each open file. See the Javadoc for these interfaces for more information. Note that, since version 5.0, the predicate methods provide another parameter: the time that the current file was first written to if new or previously closed.

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

When using flushInterval, the interval starts at the last write. The file is flushed only if it is idle for the interval. Starting with version 4.3.7, an additional property (flushWhenIdle) can be set to false, meaning that the interval starts with the first write to a previously flushed (or new) file.

File Timestamps

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

By default, the destination file’s lastModified timestamp is the time when the file was created (except that an in-place rename retains the current timestamp). Starting with version 4.3, you can now configure preserve-timestamp (or setPreserveTimestamp(true) when using Java configuration). For File payloads, this transfers the timestamp from the inbound file to the outbound (regardless of whether a copy was required). For other payloads, if the FileHeaders.SET_MODIFIED header (file_setModified) is present, it is used to set the destination file’s lastModified timestamp, as long as the header is a Number.

File Permissions

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

Starting with version 5.0, when writing files to a file system that supports Posix permissions, you can specify those permissions on the outbound channel adapter or gateway. The property is an integer and is usually supplied in the familiar octal format — for example, 0640, meaning that the owner has read/write permissions, the group has read-only permission, and others have no access.

File Outbound Channel Adapter

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

The following example configures a file outbound channel adapter:

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

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

The namespace-based configuration also supports a delete-source-files attribute. If set to true, it triggers the deletion of the original source files after writing to a destination. The default value for that flag is false. The following example shows how to set it to true:

<int-file:outbound-channel-adapter id="filesOut"
    directory="${output.directory}"
    delete-source-files="true"/>

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

The delete-source-files attribute has an effect only if the inbound message has a File payload or if the FileHeaders.ORIGINAL_FILE header value contains either the source File instance or a String representing the original file path.

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

Starting with version 4.2, the FileWritingMessageHandler supports an append-new-line option. If set to true, a new line is appended to the file after a message is written. The default attribute value is false. The following example shows how to use the append-new-line option:

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

Outbound Gateway

在要根据已写入的文件继续处理消息的情况下,你可以使用 “outbound-gateway”。它的作用类似于 “outbound-channel-adapter”。但是,它在写入文件后,也会将文件作为消息的负载发送到回复通道。

In cases where you want to continue processing messages based on the written file, you can use the outbound-gateway instead. It plays a role similar to that of the outbound-channel-adapter. However, after writing the file, it also sends it to the reply channel as the payload of a message.

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

The following example configures an outbound gateway:

<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。总的来说,使用文件出口网关时,结果文件会在答复通道上返回为消息有效载荷。

As mentioned earlier, you can also specify the mode attribute, which defines the behavior of how to deal with situations where the destination file already exists. See Dealing with Existing Destination Files for further details. Generally, when using the file outbound gateway, the result file is returned as the message payload on the reply channel.

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

This also applies when specifying the IGNORE mode. In that case the pre-existing destination file is returned. If the payload of the request message was a file, you still have access to that original file through the message header. See FileHeaders.ORIGINAL_FILE.

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

The 'outbound-gateway' works well in cases where you want to first move a file and then send it through a processing pipeline. In such cases, you may connect the file namespace’s inbound-channel-adapter element to the outbound-gateway and then connect that gateway’s reply-channel to the beginning of the pipeline.

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

If you have more elaborate requirements or need to support additional payload types as input to be converted to file content, you can extend the FileWritingMessageHandler, but a much better option is to rely on a Transformer.

Configuring with Java Configuration

以下 Spring Boot 应用展示了如何通过 Java 配置来配置入站适配器的一个示例:

The following Spring Boot application shows an example of how to configure the inbound adapter with Java configuration:

@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 配置入站适配器的示例:

The following Spring Boot application shows an example of how to configure the inbound adapter with the 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();
    }

}