# 文件支持

# 文件支持

Spring Integration 的文件支持扩展了 Spring Integration 核心,提供了一个专用词汇表来处理读、写和转换文件。

你需要在项目中包含此依赖项:

Maven

<dependency>
    <groupId>org.springframework.integration</groupId>
    <artifactId>spring-integration-file</artifactId>
    <version>5.5.9</version>
</dependency>

Gradle

compile "org.springframework.integration:spring-integration-file:5.5.9"

它提供了一个命名空间,使元素能够定义专门用于文件的通道适配器,并支持可以将文件内容读入字符串或字节数组的转换器。

本节解释FileReadingMessageSourceFileWritingMessageHandler的工作原理,以及如何将它们配置为 bean。它还讨论了通过Transformer的特定于文件的实现来处理文件的支持。最后,它解释了特定于文件的命名空间。

# 读取文件

可以使用FileReadingMessageSource来使用文件系统中的文件。这是MessageSource的一个实现,它从文件系统目录创建消息。下面的示例展示了如何配置FileReadingMessageSource:

<bean id="pollableFileSource"
    class="org.springframework.integration.file.FileReadingMessageSource"
    p:directory="${input.directory}"/>

为了防止为某些文件创建消息,你可以提供FileListFilter。默认情况下,我们使用以下过滤器:

  • IgnoreHiddenFileListFilter

  • AcceptOnceFileListFilter

IgnoreHiddenFileListFilter确保不处理隐藏的文件。请注意,隐藏的确切定义是依赖于系统的。例如,在基于 UNIX 的系统中,以句号开头的文件被认为是隐藏的。另一方面,Microsoft Windows 有一个专用的文件属性来指示隐藏的文件。

版本 4.2 引入了IgnoreHiddenFileListFilter
在以前的版本中,包含了隐藏文件。
使用默认配置时,首先触发IgnoreHiddenFileListFilter,然后是AcceptOnceFileListFilter

AcceptOnceFileListFilter确保只从目录中拾取文件一次。

AcceptOnceFileListFilter将其状态存储在内存中。
如果你希望该状态在系统重新启动后继续存在,你可以使用FileSystemPersistentAcceptOnceFileListFilter
此过滤器将接受的文件名存储在MetadataStore实现中(参见元数据存储)。
此过滤器在文件名和修改时间上匹配。

自版本 4.0 起,这个过滤器需要ConcurrentMetadataStore
当与共享数据存储一起使用时(例如RedisRedisMetadataStore一起使用时),它允许过滤器键在多个应用程序实例之间或多个服务器使用的网络文件共享之间共享。,

自 4.1.5 版本以来,这个过滤器有一个新的属性(flushOnUpdate),这会导致它在每次更新时刷新元数据存储(如果存储实现Flushable)。

持久文件列表过滤器现在有一个布尔属性forRecursion。将此属性设置为true,还将设置alwaysAcceptDirectories,这意味着出站网关上的递归操作(lsmget)现在每次都将遍历完整目录树。这是为了解决未检测到目录树中深层更改的问题。此外,forRecursion=true会导致文件的完整路径被用作元数据存储键;这解决了一个问题,即如果同名文件在不同的目录中多次出现,则过滤器无法正常工作。重要提示:这意味着,对于顶层目录下的文件,将找不到持久性元数据存储中的现有密钥。由于这个原因,默认情况下,该属性是false;这可能会在将来的版本中发生变化。

下面的示例使用筛选器配置FileReadingMessageSource:

<bean id="pollableFileSource"
    class="org.springframework.integration.file.FileReadingMessageSource"
    p:inputDirectory="${input.directory}"
    p:filter-ref="customFilterBean"/>

读取文件的一个常见问题是,在文件准备就绪之前可能会检测到该文件(即,其他一些进程可能仍在写入该文件)。默认的AcceptOnceFileListFilter并不能防止这种情况发生。在大多数情况下,如果文件写入过程在每个文件准备好读取后立即重命名,就可以防止这种情况发生。一个filename-patternfilename-regex过滤器只接受已准备好的文件(可能基于已知的后缀),并使用默认的AcceptOnceFileListFilter组成,允许这种情况。CompositeFileListFilter启用了组合,如下例所示:

<bean id="pollableFileSource"
    class="org.springframework.integration.file.FileReadingMessageSource"
    p:inputDirectory="${input.directory}"
    p:filter-ref="compositeFilter"/>

<bean id="compositeFilter"
    class="org.springframework.integration.file.filters.CompositeFileListFilter">
    <constructor-arg>
        <list>
            <bean class="o.s.i.file.filters.AcceptOnceFileListFilter"/>
            <bean class="o.s.i.file.filters.RegexPatternFileListFilter">
                <constructor-arg value="^test.*$"/>
            </bean>
        </list>
    </constructor-arg>
</bean>

如果不可能创建带有临时名称的文件并将其重命名为最终名称, Spring Integration 提供了另一种选择。版本 4.2 增加了LastModifiedFileListFilter。这个过滤器可以配置为age属性,这样过滤器只会传递比这个值更早的文件。年龄默认为 60 秒,但你应该选择一个足够大的年龄,以避免过早地接收文件(例如,由于网络故障)。下面的示例展示了如何配置LastModifiedFileListFilter:

<bean id="filter" class="org.springframework.integration.file.filters.LastModifiedFileListFilter">
    <property name="age" value="120" />
</bean>

从版本 4.3.7 开始,引入了ChainFileListFilterCompositeFileListFilter的扩展),以允许在后续过滤器只看到上一个过滤器的结果的情况下使用。(使用CompositeFileListFilter,所有过滤器都会看到所有的文件,但它只会传递通过所有过滤器的文件)。需要新行为的一个例子是LastModifiedFileListFilterAcceptOnceFileListFilter的组合,此时我们不希望在经过一定时间之后才接受该文件。对于CompositeFileListFilter,由于AcceptOnceFileListFilter在第一次传递时看到所有文件,因此当另一个过滤器这样做时,它不会在以后传递它。当模式筛选器与自定义筛选器相结合时,CompositeFileListFilter方法非常有用,自定义筛选器将查找辅助文件以指示文件传输已完成。模式过滤器可能只传递主文件(例如something.txt),但“done”过滤器需要查看是否(例如)存在something.done

假设我们有文件a.txta.done,和b.txt

模式过滤器只通过a.txtb.txt,而“完成”过滤器看到所有三个文件,只通过a.txt。复合过滤器的最终结果是只释放a.txt

使用ChainFileListFilter,如果链中的任何筛选器返回一个空列表,则不会调用其余的筛选器。

版本 5.0 引入了一个ExpressionFileListFilter来对文件执行 spel 表达式,作为上下文计算根对象。为此,所有用于文件处理的 XML 组件(本地和远程),以及现有的filter属性,都提供了filter-expression选项,如下例所示:

<int-file:inbound-channel-adapter
        directory="${inputdir}"
        filter-expression="name matches '.text'"
        auto-startup="false"/>

版本 5.0.5 引入了对拒绝文件感兴趣的DiscardAwareFileListFilter实现。为此,这样的过滤器实现应该通过addDiscardCallback(Consumer<File>)提供一个回调。在框架中,这个功能是从FileReadingMessageSource.WatchServiceDirectoryScannerLastModifiedFileListFilter组合使用的。与常规的DirectoryScanner不同,WatchService根据目标文件系统上的事件提供用于处理的文件。在用这些文件轮询内部队列时,LastModifiedFileListFilter可能会丢弃它们,因为它们相对于其配置的age太年轻了。因此,为了将来可能的考虑,我们丢失了该文件。Discard 回调钩让我们将文件保留在内部队列中,以便在随后的民意调查中根据age对文件进行检查。CompositeFileListFilter还实现了DiscardAwareFileListFilter,并在其所有DiscardAwareFileListFilter委托中填充一个 discard 回调。

由于CompositeFileListFilter将文件与所有委托匹配,因此对于同一个文件,可以多次调用discardCallback

从版本 5.1 开始,FileReadingMessageSource不会检查一个目录是否存在,并且直到调用它的start()(通常通过包装SourcePollingChannelAdapter)才会创建它。在此之前,没有简单的方法可以防止在引用目录时出现操作系统权限错误,例如在测试中,或者在以后应用权限时。

# 消息头

从版本 5.0 开始,FileReadingMessageSource(除了payload作为 polledFile)将以下头填充到出站Message:

  • FileHeaders.FILENAME:要发送的文件的File.getName()。可用于后续的重命名或复制逻辑.

  • FileHeaders.ORIGINAL_FILE:File对象本身。通常,当我们丢失原始的File对象时,框架组件(例如splitters变形金刚)会自动填充此标头。然而,为了与任何其他自定义用例保持一致和方便,这个头可以用于访问原始文件。

  • FileHeaders.RELATIVE_PATH:引入了一个新的头,用于表示相对于扫描的根目录的文件路径的一部分。当需要在其他地方恢复源目录层次结构时,这个头可能很有用。为此,可以将DefaultFileNameGenerator(参见“`生成文件名)配置为使用此标头。

# 目录扫描和轮询

FileReadingMessageSource不会立即为目录中的文件生成消息。它对scanner返回的“合格文件”使用内部队列。scanEachPoll选项用于确保在每个轮询中使用最新的输入目录内容刷新内部队列。默认情况下(scanEachPoll = false),FileReadingMessageSource在再次扫描目录之前清空其队列。这种默认行为对于减少对目录中大量文件的扫描特别有用。然而,在需要自定义排序的情况下,重要的是要考虑将此标志设置为true的效果。文件处理的顺序可能不像预期的那样。默认情况下,队列中的文件按其自然(path)顺序进行处理。通过扫描添加的新文件(即使队列中已经有文件)被插入到适当的位置,以维护该 Natural Order。要定制顺序,FileReadingMessageSource可以接受Comparator<File>作为构造函数参数。内部(PriorityBlockingQueue)使用它来根据业务需求对其内容进行重新排序。因此,要以特定的顺序处理文件,你应该为FileReadingMessageSource提供一个比较器,而不是对由自定义DirectoryScanner生成的列表进行排序。

5.0 版本引入了RecursiveDirectoryScanner来执行文件树访问。该实现是基于Files.walk(Path start, int maxDepth, FileVisitOption…​ options)的功能。结果中排除了根目录(DirectoryScanner.listFiles(File))参数。所有其他子目录包含和排除都是基于目标FileListFilter实现的。例如,SimplePatternFileListFilter默认情况下会过滤掉目录。有关更多信息,请参见[AbstractDirectoryAwareFileListFilter](https://DOCS. Spring.io/ Spring-integration/api/org/springframework/integration/file/filters/AbstractDirectoryAwarefilestFilter.html)及其实现。

从版本 5.5 开始,爪哇 DSL 的FileInboundChannelAdapterSpec有一个方便的recursive(boolean)选项,可以在目标RecursiveDirectoryScanner中使用RecursiveDirectoryScanner,而不是默认的。

# 名称空间支持

通过使用特定于文件的命名空间,可以简化文件读取的配置。要做到这一点,请使用以下模板:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns:int="http://www.springframework.org/schema/integration"
  xmlns:int-file="http://www.springframework.org/schema/integration/file"
  xsi:schemaLocation="http://www.springframework.org/schema/beans
    https://www.springframework.org/schema/beans/spring-beans.xsd
    http://www.springframework.org/schema/integration
    https://www.springframework.org/schema/integration/spring-integration.xsd
    http://www.springframework.org/schema/integration/file
    https://www.springframework.org/schema/integration/file/spring-integration-file.xsd">
</beans>

在此命名空间中,你可以减少FileReadingMessageSource并将其包装到入站通道适配器中,如下所示:

<int-file:inbound-channel-adapter id="filesIn1"
    directory="file:${input.directory}" prevent-duplicates="true" ignore-hidden="true"/>

<int-file:inbound-channel-adapter id="filesIn2"
    directory="file:${input.directory}"
    filter="customFilterBean" />

<int-file:inbound-channel-adapter id="filesIn3"
    directory="file:${input.directory}"
    filename-pattern="test*" />

<int-file:inbound-channel-adapter id="filesIn4"
    directory="file:${input.directory}"
    filename-regex="test[0-9]+\.txt" />

第一个通道适配器示例依赖于默认的FileListFilter实现:

  • IgnoreHiddenFileListFilter(不处理隐藏文件)

  • AcceptOnceFileListFilter(防止重复)

因此,还可以保留prevent-duplicatesignore-hidden属性,因为默认情况下它们是true

Spring Integration4.2 引入了ignore-hidden属性。
在以前的版本中,隐藏的文件被包括在内。

第二个通道适配器示例使用自定义过滤器,第三个使用filename-pattern属性添加基于AntPathMatcher的过滤器,第四个使用filename-regex属性向FileReadingMessageSource添加基于正则表达式模式的过滤器。filename-patternfilename-regex属性都与常规的filter引用属性互斥。但是,你可以使用filter属性引用CompositeFileListFilter的实例,该实例结合了任意数量的过滤器,包括一个或多个基于模式的过滤器,以满足你的特定需求。

当多个进程从同一个目录中读取时,你可能想要锁定文件,以防止它们同时被拾取。要做到这一点,你可以使用FileLocker。有一个基于java.nio的实现可用,但也可以实现自己的锁定方案。nio储物柜可以按以下方式注入:

<int-file:inbound-channel-adapter id="filesIn"
    directory="file:${input.directory}" prevent-duplicates="true">
    <int-file:nio-locker/>
</int-file:inbound-channel-adapter>

你可以按以下方式配置自定义的储物柜:

<int-file:inbound-channel-adapter id="filesIn"
    directory="file:${input.directory}" prevent-duplicates="true">
    <int-file:locker ref="customLocker"/>
</int-file:inbound-channel-adapter>
当文件入站适配器配置了锁存器时,它将负责在允许接收文件之前获取锁。
它不承担解锁文件的责任。
如果你已经处理了文件并保持锁在周围,你有内存泄漏。
如果这是一个问题,你应该在适当的时候自己调用FileLocker.unlock(File file)

当过滤和锁定文件还不够时,你可能需要完全控制列出文件的方式。要实现这种类型的需求,可以使用DirectoryScanner的实现。此扫描仪可让你准确地确定每个投票中列出的文件。这也是 Spring 集成内部使用的接口,用于将FileListFilter实例和FileLocker连接到FileReadingMessageSource。可以在scanner属性上的<int-file:inbound-channel-adapter/>中插入自定义DirectoryScanner,如下例所示:

<int-file:inbound-channel-adapter id="filesIn" directory="file:${input.directory}"
     scanner="customDirectoryScanner"/>

这样做可以让你完全自由地选择排序、列表和锁定策略。

同样重要的是要了解过滤器(包括patternsregexprevent-duplicates,以及其他)和locker实例实际上是由scanner使用的。适配器上设置的任何这些属性随后都会被注入到内部scanner中。对于外部scanner的情况,在FileReadingMessageSource上禁止所有筛选器和寄存器属性。它们必须在该自定义DirectoryScanner上指定(如果需要)。换句话说,如果将scanner注入FileReadingMessageSource中,则应该在filterlocker上提供scanner,而不是在FileReadingMessageSource上提供。

默认情况下,DefaultDirectoryScanner使用IgnoreHiddenFileListFilterAcceptOnceFileListFilter
为了防止它们的使用,你可以配置自己的过滤器(例如AcceptAllFileListFilter),甚至将其设置为null

# WatchServiceDirectoryScanner

当新文件被添加到目录时,FileReadingMessageSource.WatchServiceDirectoryScanner依赖于文件系统事件。在初始化过程中,目录被注册以生成事件。初始文件列表也是在初始化期间生成的。在遍历目录树时,还会注册遇到的任意子目录以生成事件。在第一次轮询时,将返回从遍历目录中获得的初始文件列表。在随后的投票中,将返回来自新创建事件的文件。如果添加了一个新的子目录,它的创建事件将用于遍历新的子树,以查找现有的文件并注册找到的任何新的子目录。

当它的内部事件WatchKey没有像发生目录修改事件那样迅速地被程序耗尽时,queue就存在一个问题。
如果超过了队列大小,aStandardWatchEventKinds.OVERFLOW被发出以指示一些文件系统事件可能丢失。
在这种情况下,根目录被完全重新扫描。
为了避免重复,考虑使用适当的FileListFilter(例如AcceptOnceFileListFilter)或在处理完成时删除文件。

可以通过WatchServiceDirectoryScanner选项启用FileReadingMessageSource.use-watch-service选项,该选项与scanner选项互斥。为提供的directory填充内部FileReadingMessageSource.WatchServiceDirectoryScanner实例。

此外,现在WatchService轮询逻辑可以跟踪StandardWatchEventKinds.ENTRY_MODIFYStandardWatchEventKinds.ENTRY_DELETE

如果需要跟踪对现有文件和新文件的修改,则应在FileListFilter中实现ENTRY_MODIFY事件逻辑。否则,来自这些事件的文件将以相同的方式处理。

ResettableFileListFilter实现拾取ENTRY_DELETE事件。因此,它们的文件是为remove()操作提供的。启用此事件后,AcceptOnceFileListFilter之类的过滤器将删除该文件。因此,如果出现同名的文件,它就会通过筛选器并作为消息发送。

为此,引入了watch-events属性(FileReadingMessageSource.setWatchEvents(WatchEventType…​ watchEvents))。(WatchEventTypeFileReadingMessageSource中的一个公共内部枚举。)有了这样的选项,我们可以对新文件使用一个下游流逻辑,并对修改过的文件使用一些其他逻辑。下面的示例展示了如何为在同一目录中创建和修改事件配置不同的逻辑:

<int-file:inbound-channel-adapter id="newFiles"
     directory="${input.directory}"
     use-watch-service="true"/>

<int-file:inbound-channel-adapter id="modifiedFiles"
     directory="${input.directory}"
     use-watch-service="true"
     filter="acceptAllFilter"
     watch-events="MODIFY"/> <!-- The default is CREATE. -->

# 限制内存消耗

你可以使用HeadDirectoryScanner来限制保留在内存中的文件数量。这在扫描大型目录时可能很有用。对于 XML 配置,可以通过在入站通道适配器上设置queue-size属性来实现这一点。

在版本 4.2 之前,此设置与任何其他过滤器的使用都不兼容。任何其他过滤器(包括prevent-duplicates="true")都覆盖了用于限制大小的过滤器。

使用HeadDirectoryScannerAcceptOnceFileListFilter不兼容。
由于在投票决定期间会参考所有过滤器,AcceptOnceFileListFilter不知道其他过滤器可能正在临时过滤文件。
即使以前由HeadDirectoryScanner.HeadFilter过滤的文件现在可用,AcceptOnceFileListFilter对它们进行过滤。

通常,在这种情况下,你应该删除处理过的文件,以便在未来的投票中可以使用以前过滤过的文件。

# 使用 爪哇 配置进行配置

下面的 Spring 引导应用程序展示了如何使用 Java 配置配置出站适配器的示例:

@SpringBootApplication
public class FileReadingJavaApplication {

    public static void main(String[] args) {
        new SpringApplicationBuilder(FileReadingJavaApplication.class)
            .web(false)
            .run(args);
    }

    @Bean
    public MessageChannel fileInputChannel() {
        return new DirectChannel();
    }

    @Bean
    @InboundChannelAdapter(value = "fileInputChannel", poller = @Poller(fixedDelay = "1000"))
    public MessageSource<File> fileReadingMessageSource() {
         FileReadingMessageSource source = new FileReadingMessageSource();
         source.setDirectory(new File(INBOUND_PATH));
         source.setFilter(new SimplePatternFileListFilter("*.txt"));
         return source;
    }

    @Bean
    @Transformer(inputChannel = "fileInputChannel", outputChannel = "processFileChannel")
    public FileToStringTransformer fileToStringTransformer() {
        return new FileToStringTransformer();
    }

}

# 使用 Java DSL 进行配置

Spring 以下引导应用程序展示了如何使用 Java DSL 配置出站适配器的示例:

@SpringBootApplication
public class FileReadingJavaApplication {

    public static void main(String[] args) {
        new SpringApplicationBuilder(FileReadingJavaApplication.class)
            .web(false)
            .run(args);
    }

    @Bean
    public IntegrationFlow fileReadingFlow() {
         return IntegrationFlows
                  .from(Files.inboundAdapter(new File(INBOUND_PATH))
                              .patternFilter("*.txt"),
                          e -> e.poller(Pollers.fixedDelay(1000)))
                  .transform(Files.toStringTransformer())
                  .channel("processFileChannel")
                  .get();
    }

}

# “tail”ing 文件

另一个流行的用例是从文件的末尾(或尾部)获取“行”,在添加新行时捕获新行。提供了两种实现方式。第一个是OSDelegatingFileTailingMessageProducer,它使用本机tail命令(在具有该命令的操作系统上)。这通常是这些平台上最有效的实现。对于没有tail命令的操作系统,第二个实现ApacheCommonsFileTailingMessageProducer使用 Apachecommons-io``Tailer类。

在这两种情况下,通过使用正常的 Spring 事件发布机制,文件系统事件(例如文件不可用和其他事件)被发布为ApplicationEvent实例。这类事件的例子包括:

[message=tail: cannot open '/tmp/somefile' for reading:
               No such file or directory, file=/tmp/somefile]

[message=tail: '/tmp/somefile' has become accessible, file=/tmp/somefile]

[message=tail: '/tmp/somefile' has become inaccessible:
               No such file or directory, file=/tmp/somefile]

[message=tail: '/tmp/somefile' has appeared;
               following end of new file, file=/tmp/somefile]

前面示例中显示的事件序列可能会发生,例如,当文件旋转时。

从版本 5.0 开始,当idleEventInterval期间文件中没有数据时,会发出FileTailingIdleEvent。下面的示例显示了这样的事件的样子:

[message=Idle timeout, file=/tmp/somefile] [idle time=5438]
并非所有支持tail命令的平台都提供这些状态消息。

从这些端点发出的消息具有以下标题:

  • FileHeaders.ORIGINAL_FILE:File对象

  • FileHeaders.FILENAME:文件名(File.getName()

在版本 5.0 之前的版本中,FileHeaders.FILENAME头包含文件绝对路径的字符串表示。
你现在可以通过在原始文件头上调用getAbsolutePath()来获得该字符串表示。

下面的示例创建一个带有默认选项(“-f-n0”,意思是跟随当前结尾的文件名)的本机适配器。

<int-file:tail-inbound-channel-adapter id="native"
	channel="input"
	task-executor="exec"
	file="/tmp/foo"/>

下面的示例创建一个带有“-f-n+0”选项的本机适配器(意思是跟随文件名,发出所有已存在的行)。

<int-file:tail-inbound-channel-adapter id="native"
	channel="input"
	native-options="-F -n +0"
	task-executor="exec"
	file-delay=10000
	file="/tmp/foo"/>

如果tail命令失败(在某些平台上,丢失的文件会导致tail失败,即使指定了-F),该命令将每 10 秒重试一次。

默认情况下,本机适配器从标准输出捕获内容并将其作为消息发送。他们还从标准错误中捕捉,以提出事件。从版本 4.3.6 开始,你可以通过将enable-status-reader设置为false来丢弃标准错误事件,如下例所示:

<int-file:tail-inbound-channel-adapter id="native"
	channel="input"
	enable-status-reader="false"
	task-executor="exec"
	file="/tmp/foo"/>

在下面的示例中,IdleEventInterval被设置为5000,这意味着,如果五秒内不写行,则每隔五秒就会触发FileTailingIdleEvent:

<int-file:tail-inbound-channel-adapter id="native"
	channel="input"
	idle-event-interval="5000"
	task-executor="exec"
	file="/tmp/somefile"/>

当你需要停止适配器时,这可能会很有用。

下面的示例创建了一个 Apachecommons-io``Tailer适配器,该适配器每两秒检查文件的新行,并每十秒检查是否存在丢失的文件:

<int-file:tail-inbound-channel-adapter id="apache"
	channel="input"
	task-executor="exec"
	file="/tmp/bar"
	delay="2000"
	end="false"             (1)
	reopen="true"           (2)
	file-delay="10000"/>
1 该文件从开始(end="false")而不是从结束(这是默认的)跟踪。
2 为每个块重新打开文件(默认情况是保持文件打开)。
指定delayendreopen属性将强制使用 Apachecommons-io适配器,并使native-options属性不可用。

# 处理不完整数据

在文件传输场景中,一个常见的问题是如何确定传输已经完成,这样你就不会开始读取不完整的文件。解决此问题的一种常见技术是使用一个临时名称编写文件,然后将其自动重命名为最终名称。这种技术,再加上屏蔽临时文件使其不被用户获取的过滤器,提供了一种健壮的解决方案。 Spring 编写文件(本地或远程)的集成组件使用这种技术。默认情况下,它们将.writing附加到文件名,并在传输完成后将其删除。

另一种常见的技术是编写第二个“标记”文件,以表明文件传输已经完成。在这种情况下,你不应该认为somefile.txt(例如)是可用的,直到somefile.txt.complete也存在。 Spring 集成版本 5.0 引入了新的过滤器以支持该机制。实现方式是为文件系统(FileSystemMarkerFilePresentFileListFilter)、FTPSFTP提供的。它们是可配置的,使得标记文件可以有任何名称,尽管它通常与正在传输的文件相关。有关更多信息,请参见Javadoc (opens new window)

# 写入文件

要将消息写入文件系统,可以使用[FileWritingMessageHandler](https://DOCS. Spring.io/ Spring-integration/api/org/springframework/integration/file/FileWritingMessageHandler.html)。这个类可以处理以下有效负载类型:

  • File

  • String

  • 字节数组

  • InputStream(自版本 4.2起)

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

为了使事情变得更简单,你可以使用 XML 命名空间将FileWritingMessageHandler配置为出站通道适配器或出站网关的一部分。

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

从版本 5.1 开始,你可以提供一个BiConsumer<File, Message<?>>``newFileCallback,如果你使用FileExistsMode.APPENDFileExistsMode.APPEND_NO_FLUSH,并且必须创建一个新文件,则会触发该命令。此回调接收一个新创建的文件和触发该文件的消息。例如,这个回调可以用来编写在消息头中定义的 CSV 头。

# 生成文件名

在最简单的形式中,FileWritingMessageHandler只需要一个目标目录来编写文件。要写入的文件的名称由处理程序的[FileNameGenerator](https://DOCS. Spring.io/ Spring-integration/api/org/springframework/integration/file/filenamegenerator.html)决定。默认实现 (opens new window)查找一个消息头,其键与定义为[FileHeaders.FILENAME]的常量匹配(https://DOCS. Spring.io/ Spring-integration/api/constant-values.html#org.springframework.integration.file.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-generatorfilename-generator-expression求值的结果)可以与目标文件名一起表示子路径。和前面一样,它被用作File(File parent, String child)的第二个构造函数参数。然而,在过去,我们并没有为子路径创建(mkdirs())目录,只假设了文件名。当我们需要恢复文件系统树以匹配源目录时,这种方法非常有用——例如,当解压缩归档并以原始顺序保存目标目录中的所有文件时。

# 指定输出目录

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

  • directory

  • directory-expression

Spring Integration2.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 Integration2.2 开始进行以下更改:

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

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

# 处理现有的目标文件

当你写入文件并且目标文件已经存在时,默认的行为是覆盖该目标文件。可以通过在相关的文件出站组件上设置mode属性来更改此行为。存在以下备选方案:

  • REPLACE(默认)

  • REPLACE_IF_MODIFIED

  • APPEND

  • APPEND_NO_FLUSH

  • FAIL

  • IGNORE

Spring Integration2.2 引入了mode属性和APPENDFAILIGNORE选项。

REPLACE

如果目标文件已经存在,则该文件将被覆盖。如果没有指定mode属性,这是写文件时的默认行为。

REPLACE_IF_MODIFIED

如果目标文件已经存在,则只有在最后修改的时间戳与源文件的时间戳不同时才会覆盖该文件。对于File有效载荷,将有效载荷lastModified的时间与现有文件进行比较。对于其他有效负载,将FileHeaders.SET_MODIFIEDfile_setModified)头与现有文件进行比较。如果缺少头文件或其值不是Number,则始终替换该文件。

APPEND

这种模式允许你将消息内容追加到现有的文件中,而不是每次都创建一个新的文件。请注意,此属性与temporary-file-suffix属性是互斥的,因为当它将内容追加到现有文件时,适配器不再使用临时文件。在每条消息之后关闭该文件。

APPEND_NO_FLUSH

此选项具有与APPEND相同的语义,但不会刷新数据,也不会在每条消息之后关闭文件。这可以在发生故障时的数据丢失风险下提供重要的性能。有关更多信息,请参见[使用APPEND_NO_FLUSH时刷新文件]。

FAIL

如果目标文件存在,则抛出[MessageHandlingException](https://DOCS. Spring.io/ Spring/DOCS/current/javadoc-api/org/springframework/messageHandlingException.html)。

IGNORE

如果目标文件存在,则消息有效负载将被静默忽略。

当使用临时文件后缀(默认值是.writing)时,如果存在最终文件名或临时文件名,则IGNORE选项将应用。

# 使用APPEND_NO_FLUSH时刷新文件

在版本 4.3 中添加了APPEND_NO_FLUSH模式。使用它可以提高性能,因为文件不会在每条消息之后关闭。然而,如果发生故障,这可能会导致数据丢失。

Spring 集成提供了几种刷新策略来减轻这种数据损失:

  • 使用flushInterval。如果文件在这段时间内没有被写入,它将被自动刷新。这是近似的,这一次可能达到1.33x(平均值为1.167x)。

  • 将包含正则表达式的消息发送到消息处理程序的trigger方法。具有与模式匹配的绝对路径名的文件将被刷新。

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

  • 通过传入自定义FileWritingMessageHandler.FlushPredicateFileWritingMessageHandler.MessageFlushPredicate实现,调用处理程序的flushIfNeeded方法之一。

为每个打开的文件调用谓词。有关更多信息,请参见Javadoc (opens new window)中的这些接口。请注意,自版本 5.0 以来,谓词方法提供了另一个参数:当前文件首次写入(如果是新建的或先前关闭的)的时间。

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

# 文件时间戳

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

# 文件权限

从版本 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"/>
只有当入站消息具有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-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 (opens new window)

“出站网关”在需要先移动文件,然后通过处理管道发送文件的情况下工作得很好,
在这种情况下,你可以将文件命名空间的inbound-channel-adapter元素连接到outbound-gateway,然后将该网关的reply-channel连接到管道的开头。

如果你有更详细的需求,或者需要支持额外的有效负载类型作为要转换为文件内容的输入,则可以扩展FileWritingMessageHandler,但更好的选择是依赖[Transformer](#file-transforming)。

# 使用 Java 配置进行配置

Spring 以下引导应用程序展示了如何使用 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 以下引导应用程序展示了如何使用 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 IntegrationFlows.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();
    }

}

# 文件变形金刚

要将从文件系统读取的数据转换为对象,或者反过来,你需要做一些工作。与FileReadingMessageSource和在较小程度上FileWritingMessageHandler不同,你可能需要自己的机制来完成工作。为此,你可以实现Transformer接口。或者,你可以为入站消息扩展AbstractFilePayloadTransformer。 Spring 集成提供了一些明显的实现方式。

参见[Javadoc forTransformer接口](https://DOCS. Spring.io/ Spring-integration/api/org/springframework/integration/Transformer/Transformer.html)以查看哪个 Spring 集成类实现了它。类似地,你可以检查[AbstractFilePayloadTransformer类的 Javadoc](https://DOCS. Spring.io/ Spring-integration/api/org/springframework/integration/file/Transformer/AbstractFilePayloadTransformer.html),以查看哪个 Spring 集成类扩展了它。

FileToByteArrayTransformer扩展AbstractFilePayloadTransformer,并通过使用 Spring 的FileCopyUtilsFile对象转换为byte[]。使用一系列的转换通常比将所有的转换放在一个类中更好。在这种情况下,Filebyte[]的转换可能是合乎逻辑的第一步。

FileToStringTransformer扩展AbstractFilePayloadTransformerFile对象转换为String对象。如果没有其他方法,这对于调试是有用的(考虑将其与wire tap一起使用)。

要配置特定于文件的转换器,你可以使用文件命名空间中的适当元素,如下例所示:

<int-file:file-to-bytes-transformer  input-channel="input" output-channel="output"
    delete-files="true"/>

<int-file:file-to-string-transformer input-channel="input" output-channel="output"
    delete-files="true" charset="UTF-8"/>

delete-files选项向 Transformer 发出信号,表示它应该在转换完成后删除入站文件。这绝不是在多线程环境中使用FileReadingMessageSource时使用AcceptOnceFileListFilter的替代方法(例如,通常使用 Spring 集成时)。

# 文件拆分器

在版本 4.1.2 中添加了FileSplitter,并在版本 4.2 中添加了对名称空间的支持。基于BufferedReader.readLine()FileSplitter将文本文件分割成单独的行。默认情况下,拆分器使用Iterator从文件中读取一行时,每次发出一行。将iterator属性设置为false会使它在将所有行作为消息发送之前将它们读入内存。这样做的一个用例可能是,如果你想在发送包含行的任何消息之前检测文件上的 I/O 错误。然而,它只适用于相对较短的文件。

入站有效载荷可以是FileString(aFile路径),InputStream,或Reader。其他有效载荷类型的发射不变。

下面的清单显示了配置FileSplitter的可能方法:

Java DSL

@SpringBootApplication
public class FileSplitterApplication {

    public static void main(String[] args) {
        new SpringApplicationBuilder(FileSplitterApplication.class)
            .web(false)
            .run(args);
    }

    @Bean
    public IntegrationFlow fileSplitterFlow() {
        return IntegrationFlows
            .from(Files.inboundAdapter(tmpDir.getRoot())
                 .filter(new ChainFileListFilter<File>()
                        .addFilter(new AcceptOnceFileListFilter<>())
                        .addFilter(new ExpressionFileListFilter<>(
                             new FunctionExpression<File>(f -> "foo.tmp".equals(f.getName()))))))
            .split(Files.splitter()
                     .markers()
                     .charset(StandardCharsets.US_ASCII)
                     .firstLineAsHeader("fileHeader")
                     .applySequence(true))
            .channel(c -> c.queue("fileSplittingResultChannel"))
            .get();
    }

}

Kotlin DSL

@Bean
fun fileSplitterFlow() =
    integrationFlow(
        Files.inboundAdapter(tmpDir.getRoot())
            .filter(
                ChainFileListFilter<File?>()
                    .addFilter(AcceptOnceFileListFilter())
                    .addFilter(ExpressionFileListFilter(FunctionExpression { f: File? -> "foo.tmp" == f!!.name }))
            )
    ) {
        split(
            Files.splitter()
                .markers()
                .charset(StandardCharsets.US_ASCII)
                .firstLineAsHeader("fileHeader")
                .applySequence(true)
        )
        channel { queue("fileSplittingResultChannel") }
    }

Java

@Splitter(inputChannel="toSplitter")
@Bean
public MessageHandler fileSplitter() {
    FileSplitter splitter = new FileSplitter(true, true);
    splitter.setApplySequence(true);
    splitter.setOutputChannel(outputChannel);
    return splitter;
}

XML

<int-file:splitter id="splitter" (1)
    iterator=""                  (2)
    markers=""                   (3)
    markers-json=""              (4)
    apply-sequence=""            (5)
    requires-reply=""            (6)
    charset=""                   (7)
    first-line-as-header=""      (8)
    input-channel=""             (9)
    output-channel=""            (10)
    send-timeout=""              (11)
    auto-startup=""              (12)
    order=""                     (13)
    phase="" />                  (14)
1 Bean 分离器的名称。
2 设置为true(默认值)以使用迭代器或false在发送行之前将文件加载到内存中。
3 设置为true以在文件数据前后发出文件开始和文件结束标记消息。
标记是具有FileSplitter.FileMarker有效负载的消息(带有START)并且END``mark属性中的值)。
当在下游流中按顺序处理文件时,你可能会使用标记,其中一些行被过滤。
它们使下游处理能够知道文件何时已被完全处理。此外,一个file_marker包含STARTEND的头被添加到这些消息中。
END标记包括一个行数。
如果文件为空,只有STARTEND标记与0一起作为lineCount发出。
默认为false
apply-sequence时,false默认为false。(下一个属性)。
4 markers为真时,将其设置为true,以便将FileMarker对象转换为 JSON 字符串。
(下面使用SimpleJsonSerializer)。
5 设置为false以禁用消息中的sequenceSizesequenceNumber标题的包含。
默认值为true,除非markerstrue
markerstrue,该标记被包括在测序中。
trueiteratortrue时,sequenceSize标头被设置为0,因为大小未知。
6 设置为true,以便在文件中没有行的情况下引发RequiresReplyException
默认值为false
7 设置将文本数据读入String有效负载时使用的字符集名称。
默认为平台字符集。
8 第一行的头名称将作为其余各行发出的消息中的头名称。
自版本 5.0 起。
9 设置用于向拆分器发送消息的输入通道。
10 设置发送消息的输出通道。
11 设置发送超时。
仅在output-channel可以阻塞的情况下才适用,例如完整的QueueChannel
12 设置为false,以便在刷新上下文时禁用自动启动拆分器。
默认值为true
13 如果input-channel<publish-subscribe-channel/>,则设置该端点的顺序。
14 设置分割器的启动阶段(当auto-startuptrue时使用)。

FileSplitter还将任何基于文本的InputStream拆分成行。从版本 4.3 开始,当与 FTP 或 SFTP 流入站通道适配器或使用stream选项检索文件的 FTP 或 SFTP 出站网关一起使用时,当文件被完全消耗时,分割器会自动关闭支持流的会话。有关这些工具的更多信息,请参见FTP 流入站通道适配器SFTP 流入站通道适配器以及FTP 出站网关SFTP 出站网关

当使用 Java 配置时,可以使用一个额外的构造函数,如下例所示:

public FileSplitter(boolean iterator, boolean markers, boolean markersJson)

markersJson为真时,标记被表示为一个 JSON 字符串(使用SimpleJsonSerializer)。

版本 5.0 引入了firstLineAsHeader选项,以指定第一行内容是一个标题(例如 CSV 文件中的列名)。传递给此属性的参数是头名称,在其余各行发出的消息中,第一行作为头进行。这一行不包含在序列头中(如果applySequence为真),也不包含在与lineCount关联的FileMarker.END中。注意:从版本 5.5 开始,linecountis also included as afileheaders.line_countinto headers of thefilemarker.endmessage, since thefilemarkercould be serialized into JSON. If a file contains only the header line, the file is treated as empty and, therefore, onlyfilemarker` 实例在拆分过程中被发出(如果启用了标记,则不会发出消息)。默认情况下(如果没有设置头名称),第一行被认为是数据,并成为第一个发出的消息的有效负载。

如果需要从文件内容中提取有关头的更复杂的逻辑(不是第一行,不是整行的内容,也不是一个特定的头,等等),请考虑在页眉 Enricher 之前使用FileSplitter。请注意,已移动到标题的行可能会从正常的内容处理过程中向下游过滤。

# 幂等下游处理拆分文件

apply-sequence为真时,拆分器在SEQUENCE_NUMBER标头中添加行号(当markers为真时,标记被计为行)。行号可以与幂等接收机一起使用,以避免重新启动后重新处理行。

例如:

@Bean
public ConcurrentMetadataStore store() {
    return new ZookeeperMetadataStore();
}

@Bean
public MetadataStoreSelector selector() {
    return new MetadataStoreSelector(
            message -> message.getHeaders().get(FileHeaders.ORIGINAL_FILE, File.class)
                    .getAbsolutePath(),
            message -> message.getHeaders().get(IntegrationMessageHeaderAccessor.SEQUENCE_NUMBER)
                    .toString(),
            store())
                    .compareValues(
                            (oldVal, newVal) -> Integer.parseInt(oldVal) < Integer.parseInt(newVal));
}

@Bean
public IdempotentReceiverInterceptor idempotentReceiverInterceptor() {
    return new IdempotentReceiverInterceptor(selector());
}

@Bean
public IntegrationFlow flow() {
    ...
    .split(new FileSplitter())
    ...
    .handle("lineHandler", e -> e.advice(idempotentReceiverInterceptor()))
    ...
}

# 文件聚合器

从版本 5.5 开始,在启用开始/结束标记时,引入了FileAggregator来覆盖FileSplitter用例的另一面。为了方便起见,FileAggregator实现了所有三种序列细节策略:

  • 带有FileHeaders.FILENAME属性的HeaderAttributeCorrelationStrategy用于相关键的计算。当在FileSplitter上启用标记时,它不会填充序列详细信息标题,因为开始/结束标记消息也包含在序列大小中。对于发出的每一行,FileHeaders.FILENAME仍然填充,包括开始/结束标记消息。

  • FileMarkerReleaseStrategy-检查组中的FileSplitter.FileMarker.Mark.END消息,然后将FileHeaders.LINE_COUNT标头值与组大小减去2-FileSplitter.FileMarker实例进行比较。它还实现了一个方便的GroupConditionProvider用于conditionSupplier函数的AbstractCorrelatingMessageHandler联系人。有关更多信息,请参见消息组条件

  • FileAggregatingMessageGroupProcessor只需从组中删除FileSplitter.FileMarker消息,并将其余消息收集到一个列表有效负载中以生成。

下面的清单显示了配置FileAggregator的可能方法:

Java DSL

@Bean
public IntegrationFlow fileSplitterAggregatorFlow(TaskExecutor taskExecutor) {
    return f -> f
            .split(Files.splitter()
                    .markers()
                    .firstLineAsHeader("firstLine"))
            .channel(c -> c.executor(taskExecutor))
            .filter(payload -> !(payload instanceof FileSplitter.FileMarker),
                    e -> e.discardChannel("aggregatorChannel"))
            .<String, String>transform(String::toUpperCase)
            .channel("aggregatorChannel")
            .aggregate(new FileAggregator())
            .channel(c -> c.queue("resultChannel"));
}

Kotlin DSL

@Bean
fun fileSplitterAggregatorFlow(taskExecutor: TaskExecutor?) =
    integrationFlow {
        split(Files.splitter().markers().firstLineAsHeader("firstLine"))
        channel { executor(taskExecutor) }
        filter<Any>({ it !is FileMarker }) { discardChannel("aggregatorChannel") }
        transform(String::toUpperCase)
        channel("aggregatorChannel")
        aggregate(FileAggregator())
        channel { queue("resultChannel") }
    }

Java

@serviceActivator(inputChannel="toAggregateFile")
@Bean
public AggregatorFactoryBean fileAggregator() {
    AggregatorFactoryBean aggregator = new AggregatorFactoryBean();
    aggregator.setProcessorBean(new FileAggregator());
    aggregator.setOutputChannel(outputChannel);
    return aggregator;
}

XML

<int:chain input-channel="input" output-channel="output">
    <int-file:splitter markers="true"/>
    <int:aggregator>
        <bean class="org.springframework.integration.file.aggregator.FileAggregator"/>
    </int:aggregator>
</int:chain>

如果FileAggregator的默认行为不满足目标逻辑,则建议使用单独的策略配置聚合器端点。有关更多信息,请参见FileAggregatorJavadocs。

# 远程持久文件列表过滤器

入站和流入站远程文件通道适配器(FTPSFTP,以及其他技术)在默认情况下配置有相应的AbstractPersistentFileListFilter实现方式,在内存中配置有MetadataStore。要在集群中运行,可以使用共享的MetadataStore替换这些过滤器(有关更多信息,请参见元数据存储)。这些过滤器用于防止多次抓取同一个文件(除非修改了时间)。从版本 5.2 开始,在获取文件之前立即将文件添加到筛选器中(如果获取失败,则进行反向操作)。

如果发生灾难性故障(例如断电),当前正在获取的文件可能会保留在过滤器中,并且在重新启动应用程序时不会重新获取。
在这种情况下,你需要从MetadataStore中手动删除此文件。

在以前的版本中,文件在被获取之前会被过滤,这意味着在灾难性的失败之后,有几个文件可能处于这种状态。

为了促进这种新行为,向FileListFilter添加了两个新方法。

boolean accept(F file);

boolean supportsSingleFileFiltering();

如果一个过滤器在supportsSingleFileFiltering中返回true,则它必须实现accept()

如果远程过滤器不支持单个文件过滤(例如AbstractMarkerFilePresentFileListFilter),则适配器将恢复到以前的行为。

如果使用多个过滤器(使用CompositeFileListFilterChainFileListFilter),则委托过滤器的全部必须支持单个文件过滤,以便复合过滤器支持它。

持久文件列表过滤器现在有一个布尔属性forRecursion。将此属性设置为true,还将设置alwaysAcceptDirectories,这意味着出站网关上的递归操作(lsmget)现在每次都将遍历完整目录树。这是为了解决未检测到目录树中深层更改的问题。此外,forRecursion=true会导致文件的完整路径被用作元数据存储键;这解决了一个问题,即如果同名文件在不同的目录中多次出现,则过滤器无法正常工作。重要提示:这意味着,对于顶层目录下的文件,将找不到持久性元数据存储中的现有密钥。由于这个原因,默认情况下,该属性是false;这可能会在将来的版本中发生变化。