写入文件
要将消息写入文件系统,可以使用 FileWritingMessageHandler。此类可以处理以下有效载荷类型:
-
文件 -
字符串 -
字节数组
-
InputStream(自版本 4.2起)
对于 String 有效载荷,可以配置编码和字符集。
为了简化操作,可以将 FileWritingMessageHandler 配置为出站通道适配器或出站网关的一部分,通过使用 XML 命名空间。
从版本 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对象的 文件名。 -
否则,使用附加了 .
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())目录,只假定文件名。这种方法对于需要恢复文件系统树以匹配源目录的情况很有用——例如,解压缩存档并将所有文件按原始顺序保存在目标目录中。
指定输出目录
文件出站通道适配器和文件出站网关都提供了两个互斥的配置属性来指定输出目录:
-
目录 -
目录表达式
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 属性
默认情况下,如果目标目录不存在,则会自动创建相应的目标目录和任何不存在的父目录。要防止此行为,可以将 auto-create-directory 属性设置为 false。此属性适用于 directory 和 directory-expression 属性。
|
当使用 不再在适配器初始化时检查目标目录是否存在,现在在处理每个消息时执行此检查。 此外,如果 |
处理现有目标文件
当您写入文件并且目标文件已存在时,默认行为是覆盖该目标文件。您可以通过设置相关文件出站组件上的 mode 属性来更改此行为。存在以下选项:
-
REPLACE(默认) -
REPLACE_IF_MODIFIED -
APPEND -
APPEND_NO_FLUSH -
FAIL -
IGNORE
Spring Integration 2.2 引入了 mode 属性以及 APPEND、FAIL 和 IGNORE 选项。 |
替换-
如果目标文件已存在,则覆盖它。如果未指定
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.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 权限的文件系统时,您可以在出站通道适配器或网关上指定这些权限。该属性是一个整数,通常以熟悉的八进制格式提供——例如,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。
“出站网关”在您希望首先移动文件,然后通过处理管道发送文件的情况下非常有效。在这种情况下,您可以将文件命名空间的 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();
}
}