写入文件
要将消息写入文件系统,可以使用 FileWritingMessageHandler。这个类可以处理以下有效负载类型:
- 
文件
- 
字符串
- 
字节数组 
- 
输入流(自 版本 4.2 起)
对于 String 类型的有效负载,您可以配置编码和字符集。
为了简化操作,你可以通过使用 XML 命名空间,将 FileWritingMessageHandler 配置为出站通道适配器或出站网关的一部分。
从 4.3 版本开始,您可以指定在写入文件时使用的缓冲区大小。
从 5.1 版本开始,你可以提供一个 BiConsumer<File, Message<?>> newFileCallback,当使用 FileExistsMode.APPEND 或 FileExistsMode.APPEND_NO_FLUSH 并且需要创建一个新文件时,会触发这个回调。此回调接收一个新创建的文件和触发它的消息。例如,此回调可以用于写入在消息头中定义的 CSV 标题。
生成文件名
在其最简单的形式中,FileWritingMessageHandler 仅需要一个用于写入文件的目标目录。要写入的文件的名称由处理器的 FileNameGenerator 确定。默认实现 会查找消息头中的一个键与作为 FileHeaders.FILENAME 定义的常量相匹配的值。
或者,你可以指定一个表达式,根据消息评估生成文件名——例如,headers['myCustomHeader'] + '.something'。该表达式必须评估为一个 String。为了方便起见,DefaultFileNameGenerator 还提供了 setHeaderName 方法,让你可以明确指定要将其值用作文件名的消息头。
一旦设置好,DefaultFileNameGenerator 会采用以下解析步骤来确定给定消息负载的文件名:
- 
对表达式进行求值,如果结果是非空的 String,则将其用作文件名。
- 
否则,如果有效负载是 java.io.File,则使用File对象的文件名。
- 
否则,使用消息 ID 并附加 . msg作为文件名。
当你使用 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()) 目录,而只是假设它是文件名。当需要还原文件系统树以匹配源目录时,此方法非常有用——例如,在解压缩存档并将所有文件按原始顺序保存到目标目录时。
指定输出目录
文件 outbound 通道适配器和文件 outbound 网关都提供了两个互斥的配置属性来指定输出目录:
- 
目录
- 
目录表达式
Spring Integration 2.2 引入了 directory-expression 属性。
使用 directory 属性
当你使用 directory 属性时,输出目录被设置为一个固定值,该值在 FileWritingMessageHandler 初始化时设置。如果你不指定此属性,则必须使用 directory-expression 属性。
使用 directory-expression 属性
如果你想拥有完整的 SpEL 支持,你可以使用 directory-expression 属性。此属性接受一个 SpEL 表达式,在处理每条消息时进行评估。因此,在动态指定输出文件目录时,你可以完全访问消息的有效负载及其标题。
SpEL 表达式必须解析为 String、java.io.File 或 org.springframework.core.io.Resource。(后者无论如何都会被评估为 File。)此外,生成的 String 或 File 必须指向一个目录。如果你不指定 directory-expression 属性,则必须设置 directory 属性。
使用 auto-create-directory 属性
默认情况下,如果目标目录不存在,则 respective 目标目录和任何不存在的父目录会自动创建。要阻止此行为,您可以将 auto-create-directory 属性设置为 false。此属性适用于 directory 和 directory-expression 属性。
当使用 directory 属性且 auto-create-directory 为 false 时,从 Spring Integration 2.2 开始,做出了以下更改:
不再是在适配器初始化时检查目标目录是否存在,而是现在对每个正在处理的消息执行此检查。
此外,如果 auto-create-directory 为 true 并且目录在消息处理之间被删除,则会为每个正在处理的消息重新创建该目录。
处理已存在的目标文件
当你写入文件且目标文件已经存在时,默认行为是覆盖该目标文件。你可以通过设置相关文件 outbound 组件的 mode 属性来更改此行为。以下选项存在:
- overwrite:覆盖现有文件(默认行为)。
- append:在现有文件末尾追加内容。
- skip:如果文件已存在,则跳过写入。
请根据你的需求选择合适的模式。
- 
REPLACE(默认)
- 
REPLACE_IF_MODIFIED
- 
APPEND
- 
APPEND_NO_FLUSH
- 
FAIL
- 
IGNORE
Spring Integration 2.2 引入了 mode 属性和 APPEND、FAIL、IGNORE 选项。
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.33x倍的时间(平均为1.167x倍)。
- 
向消息处理程序的 trigger方法发送包含正则表达式的消息。与模式匹配的具有绝对路径名称的文件将被刷新。
- 
为处理程序提供自定义的 MessageFlushPredicate实现以修改向trigger方法发送消息时采取的操作。
- 
通过传递自定义的 FileWritingMessageHandler.FlushPredicate或FileWritingMessageHandler.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 权限的文件系统时,您可以在 outbound 通道适配器或网关上指定这些权限。该属性是一个整数,通常以熟悉的八进制格式提供——例如,0640,表示所有者具有读/写权限,组具有只读权限,其他人没有访问权限。
文件 outbound 通道适配器
以下示例配置了一个文件 outbound 通道适配器:
<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 属性,它定义了如何处理目标文件已经存在的情况的行为。详情请参阅处理已存在的目标文件。通常,当使用文件 outbound 网关时,结果文件会作为消息的有效载荷在回复通道上返回。
这也适用于指定 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();
    }
}