跳到主要内容
版本:7.0.2

写入文件

DeepSeek V3 中英对照 Writing files

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

  • File

  • String

  • 字节数组

  • InputStream(自 4.2 版本起)

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

为了简化操作,你可以通过使用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();
}

}