(window.webpackJsonp=window.webpackJsonp||[]).push([[464],{896:function(e,t,r){"use strict";r.r(t);var a=r(56),i=Object(a.a)({},(function(){var e=this,t=e.$createElement,r=e._self._c||t;return r("ContentSlotsDistributor",{attrs:{"slot-key":e.$parent.slotKey}},[r("h1",{attrs:{id:"文件支持"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#文件支持"}},[e._v("#")]),e._v(" 文件支持")]),e._v(" "),r("h2",{attrs:{id:"文件支持-2"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#文件支持-2"}},[e._v("#")]),e._v(" 文件支持")]),e._v(" "),r("p",[e._v("Spring Integration 的文件支持扩展了 Spring Integration 核心,提供了一个专用词汇表来处理读、写和转换文件。")]),e._v(" "),r("p",[e._v("你需要在项目中包含此依赖项:")]),e._v(" "),r("p",[e._v("Maven")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v("\n org.springframework.integration\n spring-integration-file\n 5.5.9\n\n")])])]),r("p",[e._v("Gradle")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('compile "org.springframework.integration:spring-integration-file:5.5.9"\n')])])]),r("p",[e._v("它提供了一个命名空间,使元素能够定义专门用于文件的通道适配器,并支持可以将文件内容读入字符串或字节数组的转换器。")]),e._v(" "),r("p",[e._v("本节解释"),r("code",[e._v("FileReadingMessageSource")]),e._v("和"),r("code",[e._v("FileWritingMessageHandler")]),e._v("的工作原理,以及如何将它们配置为 bean。它还讨论了通过"),r("code",[e._v("Transformer")]),e._v("的特定于文件的实现来处理文件的支持。最后,它解释了特定于文件的命名空间。")]),e._v(" "),r("h3",{attrs:{id:"读取文件"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#读取文件"}},[e._v("#")]),e._v(" 读取文件")]),e._v(" "),r("p",[e._v("可以使用"),r("code",[e._v("FileReadingMessageSource")]),e._v("来使用文件系统中的文件。这是"),r("code",[e._v("MessageSource")]),e._v("的一个实现,它从文件系统目录创建消息。下面的示例展示了如何配置"),r("code",[e._v("FileReadingMessageSource")]),e._v(":")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('\n')])])]),r("p",[e._v("为了防止为某些文件创建消息,你可以提供"),r("code",[e._v("FileListFilter")]),e._v("。默认情况下,我们使用以下过滤器:")]),e._v(" "),r("ul",[r("li",[r("p",[r("code",[e._v("IgnoreHiddenFileListFilter")])])]),e._v(" "),r("li",[r("p",[r("code",[e._v("AcceptOnceFileListFilter")])])])]),e._v(" "),r("p",[r("code",[e._v("IgnoreHiddenFileListFilter")]),e._v("确保不处理隐藏的文件。请注意,隐藏的确切定义是依赖于系统的。例如,在基于 UNIX 的系统中,以句号开头的文件被认为是隐藏的。另一方面,Microsoft Windows 有一个专用的文件属性来指示隐藏的文件。")]),e._v(" "),r("table",[r("thead",[r("tr",[r("th"),e._v(" "),r("th",[e._v("版本 4.2 引入了"),r("code",[e._v("IgnoreHiddenFileListFilter")]),e._v("。"),r("br"),e._v("在以前的版本中,包含了隐藏文件。"),r("br"),e._v("使用默认配置时,首先触发"),r("code",[e._v("IgnoreHiddenFileListFilter")]),e._v(",然后是"),r("code",[e._v("AcceptOnceFileListFilter")]),e._v("。")])])]),e._v(" "),r("tbody")]),e._v(" "),r("p",[r("code",[e._v("AcceptOnceFileListFilter")]),e._v("确保只从目录中拾取文件一次。")]),e._v(" "),r("table",[r("thead",[r("tr",[r("th"),e._v(" "),r("th",[r("code",[e._v("AcceptOnceFileListFilter")]),e._v("将其状态存储在内存中。"),r("br"),e._v("如果你希望该状态在系统重新启动后继续存在,你可以使用"),r("code",[e._v("FileSystemPersistentAcceptOnceFileListFilter")]),e._v("。"),r("br"),e._v("此过滤器将接受的文件名存储在"),r("code",[e._v("MetadataStore")]),e._v("实现中(参见"),r("RouterLink",{attrs:{to:"/spring-integration/meta-data-store.html#metadata-store"}},[e._v("元数据存储")]),e._v(")。"),r("br"),e._v("此过滤器在文件名和修改时间上匹配。"),r("br"),r("br"),e._v("自版本 4.0 起,这个过滤器需要"),r("code",[e._v("ConcurrentMetadataStore")]),e._v("。"),r("br"),e._v("当与共享数据存储一起使用时(例如"),r("code",[e._v("Redis")]),e._v("与"),r("code",[e._v("RedisMetadataStore")]),e._v("一起使用时),它允许过滤器键在多个应用程序实例之间或多个服务器使用的网络文件共享之间共享。,"),r("br"),r("br"),e._v("自 4.1.5 版本以来,这个过滤器有一个新的属性("),r("code",[e._v("flushOnUpdate")]),e._v("),这会导致它在每次更新时刷新元数据存储(如果存储实现"),r("code",[e._v("Flushable")]),e._v(")。")],1)])]),e._v(" "),r("tbody")]),e._v(" "),r("p",[e._v("持久文件列表过滤器现在有一个布尔属性"),r("code",[e._v("forRecursion")]),e._v("。将此属性设置为"),r("code",[e._v("true")]),e._v(",还将设置"),r("code",[e._v("alwaysAcceptDirectories")]),e._v(",这意味着出站网关上的递归操作("),r("code",[e._v("ls")]),e._v("和"),r("code",[e._v("mget")]),e._v(")现在每次都将遍历完整目录树。这是为了解决未检测到目录树中深层更改的问题。此外,"),r("code",[e._v("forRecursion=true")]),e._v("会导致文件的完整路径被用作元数据存储键;这解决了一个问题,即如果同名文件在不同的目录中多次出现,则过滤器无法正常工作。重要提示:这意味着,对于顶层目录下的文件,将找不到持久性元数据存储中的现有密钥。由于这个原因,默认情况下,该属性是"),r("code",[e._v("false")]),e._v(";这可能会在将来的版本中发生变化。")]),e._v(" "),r("p",[e._v("下面的示例使用筛选器配置"),r("code",[e._v("FileReadingMessageSource")]),e._v(":")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('\n')])])]),r("p",[e._v("读取文件的一个常见问题是,在文件准备就绪之前可能会检测到该文件(即,其他一些进程可能仍在写入该文件)。默认的"),r("code",[e._v("AcceptOnceFileListFilter")]),e._v("并不能防止这种情况发生。在大多数情况下,如果文件写入过程在每个文件准备好读取后立即重命名,就可以防止这种情况发生。一个"),r("code",[e._v("filename-pattern")]),e._v("或"),r("code",[e._v("filename-regex")]),e._v("过滤器只接受已准备好的文件(可能基于已知的后缀),并使用默认的"),r("code",[e._v("AcceptOnceFileListFilter")]),e._v("组成,允许这种情况。"),r("code",[e._v("CompositeFileListFilter")]),e._v("启用了组合,如下例所示:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('\n\n\n \n \n \n \n \n \n \n \n\n')])])]),r("p",[e._v("如果不可能创建带有临时名称的文件并将其重命名为最终名称, Spring Integration 提供了另一种选择。版本 4.2 增加了"),r("code",[e._v("LastModifiedFileListFilter")]),e._v("。这个过滤器可以配置为"),r("code",[e._v("age")]),e._v("属性,这样过滤器只会传递比这个值更早的文件。年龄默认为 60 秒,但你应该选择一个足够大的年龄,以避免过早地接收文件(例如,由于网络故障)。下面的示例展示了如何配置"),r("code",[e._v("LastModifiedFileListFilter")]),e._v(":")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('\n \n\n')])])]),r("p",[e._v("从版本 4.3.7 开始,引入了"),r("code",[e._v("ChainFileListFilter")]),e._v("("),r("code",[e._v("CompositeFileListFilter")]),e._v("的扩展),以允许在后续过滤器只看到上一个过滤器的结果的情况下使用。(使用"),r("code",[e._v("CompositeFileListFilter")]),e._v(",所有过滤器都会看到所有的文件,但它只会传递通过所有过滤器的文件)。需要新行为的一个例子是"),r("code",[e._v("LastModifiedFileListFilter")]),e._v("和"),r("code",[e._v("AcceptOnceFileListFilter")]),e._v("的组合,此时我们不希望在经过一定时间之后才接受该文件。对于"),r("code",[e._v("CompositeFileListFilter")]),e._v(",由于"),r("code",[e._v("AcceptOnceFileListFilter")]),e._v("在第一次传递时看到所有文件,因此当另一个过滤器这样做时,它不会在以后传递它。当模式筛选器与自定义筛选器相结合时,"),r("code",[e._v("CompositeFileListFilter")]),e._v("方法非常有用,自定义筛选器将查找辅助文件以指示文件传输已完成。模式过滤器可能只传递主文件(例如"),r("code",[e._v("something.txt")]),e._v("),但“done”过滤器需要查看是否(例如)存在"),r("code",[e._v("something.done")]),e._v("。")]),e._v(" "),r("p",[e._v("假设我们有文件"),r("code",[e._v("a.txt")]),e._v(","),r("code",[e._v("a.done")]),e._v(",和"),r("code",[e._v("b.txt")]),e._v("。")]),e._v(" "),r("p",[e._v("模式过滤器只通过"),r("code",[e._v("a.txt")]),e._v("和"),r("code",[e._v("b.txt")]),e._v(",而“完成”过滤器看到所有三个文件,只通过"),r("code",[e._v("a.txt")]),e._v("。复合过滤器的最终结果是只释放"),r("code",[e._v("a.txt")]),e._v("。")]),e._v(" "),r("table",[r("thead",[r("tr",[r("th"),e._v(" "),r("th",[e._v("使用"),r("code",[e._v("ChainFileListFilter")]),e._v(",如果链中的任何筛选器返回一个空列表,则不会调用其余的筛选器。")])])]),e._v(" "),r("tbody")]),e._v(" "),r("p",[e._v("版本 5.0 引入了一个"),r("code",[e._v("ExpressionFileListFilter")]),e._v("来对文件执行 spel 表达式,作为上下文计算根对象。为此,所有用于文件处理的 XML 组件(本地和远程),以及现有的"),r("code",[e._v("filter")]),e._v("属性,都提供了"),r("code",[e._v("filter-expression")]),e._v("选项,如下例所示:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('\n')])])]),r("p",[e._v("版本 5.0.5 引入了对拒绝文件感兴趣的"),r("code",[e._v("DiscardAwareFileListFilter")]),e._v("实现。为此,这样的过滤器实现应该通过"),r("code",[e._v("addDiscardCallback(Consumer)")]),e._v("提供一个回调。在框架中,这个功能是从"),r("code",[e._v("FileReadingMessageSource.WatchServiceDirectoryScanner")]),e._v("与"),r("code",[e._v("LastModifiedFileListFilter")]),e._v("组合使用的。与常规的"),r("code",[e._v("DirectoryScanner")]),e._v("不同,"),r("code",[e._v("WatchService")]),e._v("根据目标文件系统上的事件提供用于处理的文件。在用这些文件轮询内部队列时,"),r("code",[e._v("LastModifiedFileListFilter")]),e._v("可能会丢弃它们,因为它们相对于其配置的"),r("code",[e._v("age")]),e._v("太年轻了。因此,为了将来可能的考虑,我们丢失了该文件。Discard 回调钩让我们将文件保留在内部队列中,以便在随后的民意调查中根据"),r("code",[e._v("age")]),e._v("对文件进行检查。"),r("code",[e._v("CompositeFileListFilter")]),e._v("还实现了"),r("code",[e._v("DiscardAwareFileListFilter")]),e._v(",并在其所有"),r("code",[e._v("DiscardAwareFileListFilter")]),e._v("委托中填充一个 discard 回调。")]),e._v(" "),r("table",[r("thead",[r("tr",[r("th"),e._v(" "),r("th",[e._v("由于"),r("code",[e._v("CompositeFileListFilter")]),e._v("将文件与所有委托匹配,因此对于同一个文件,可以多次调用"),r("code",[e._v("discardCallback")]),e._v("。")])])]),e._v(" "),r("tbody")]),e._v(" "),r("p",[e._v("从版本 5.1 开始,"),r("code",[e._v("FileReadingMessageSource")]),e._v("不会检查一个目录是否存在,并且直到调用它的"),r("code",[e._v("start()")]),e._v("(通常通过包装"),r("code",[e._v("SourcePollingChannelAdapter")]),e._v(")才会创建它。在此之前,没有简单的方法可以防止在引用目录时出现操作系统权限错误,例如在测试中,或者在以后应用权限时。")]),e._v(" "),r("h4",{attrs:{id:"消息头"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#消息头"}},[e._v("#")]),e._v(" 消息头")]),e._v(" "),r("p",[e._v("从版本 5.0 开始,"),r("code",[e._v("FileReadingMessageSource")]),e._v("(除了"),r("code",[e._v("payload")]),e._v("作为 polled"),r("code",[e._v("File")]),e._v(")将以下头填充到出站"),r("code",[e._v("Message")]),e._v(":")]),e._v(" "),r("ul",[r("li",[r("p",[r("code",[e._v("FileHeaders.FILENAME")]),e._v(":要发送的文件的"),r("code",[e._v("File.getName()")]),e._v("。可用于后续的重命名或复制逻辑.")])]),e._v(" "),r("li",[r("p",[r("code",[e._v("FileHeaders.ORIGINAL_FILE")]),e._v(":"),r("code",[e._v("File")]),e._v("对象本身。通常,当我们丢失原始的"),r("code",[e._v("File")]),e._v("对象时,框架组件(例如"),r("a",{attrs:{href:"#file-splitter"}},[e._v("splitters")]),e._v("或"),r("a",{attrs:{href:"#file-transforming"}},[e._v("变形金刚")]),e._v(")会自动填充此标头。然而,为了与任何其他自定义用例保持一致和方便,这个头可以用于访问原始文件。")])]),e._v(" "),r("li",[r("p",[r("code",[e._v("FileHeaders.RELATIVE_PATH")]),e._v(":引入了一个新的头,用于表示相对于扫描的根目录的文件路径的一部分。当需要在其他地方恢复源目录层次结构时,这个头可能很有用。为此,可以将"),r("code",[e._v("DefaultFileNameGenerator")]),e._v("(参见“`"),r("a",{attrs:{href:"#file-writing-file-names"}},[e._v("生成文件名")]),e._v(")配置为使用此标头。")])])]),e._v(" "),r("h4",{attrs:{id:"目录扫描和轮询"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#目录扫描和轮询"}},[e._v("#")]),e._v(" 目录扫描和轮询")]),e._v(" "),r("p",[r("code",[e._v("FileReadingMessageSource")]),e._v("不会立即为目录中的文件生成消息。它对"),r("code",[e._v("scanner")]),e._v("返回的“合格文件”使用内部队列。"),r("code",[e._v("scanEachPoll")]),e._v("选项用于确保在每个轮询中使用最新的输入目录内容刷新内部队列。默认情况下("),r("code",[e._v("scanEachPoll = false")]),e._v("),"),r("code",[e._v("FileReadingMessageSource")]),e._v("在再次扫描目录之前清空其队列。这种默认行为对于减少对目录中大量文件的扫描特别有用。然而,在需要自定义排序的情况下,重要的是要考虑将此标志设置为"),r("code",[e._v("true")]),e._v("的效果。文件处理的顺序可能不像预期的那样。默认情况下,队列中的文件按其自然("),r("code",[e._v("path")]),e._v(")顺序进行处理。通过扫描添加的新文件(即使队列中已经有文件)被插入到适当的位置,以维护该 Natural Order。要定制顺序,"),r("code",[e._v("FileReadingMessageSource")]),e._v("可以接受"),r("code",[e._v("Comparator")]),e._v("作为构造函数参数。内部("),r("code",[e._v("PriorityBlockingQueue")]),e._v(")使用它来根据业务需求对其内容进行重新排序。因此,要以特定的顺序处理文件,你应该为"),r("code",[e._v("FileReadingMessageSource")]),e._v("提供一个比较器,而不是对由自定义"),r("code",[e._v("DirectoryScanner")]),e._v("生成的列表进行排序。")]),e._v(" "),r("p",[e._v("5.0 版本引入了"),r("code",[e._v("RecursiveDirectoryScanner")]),e._v("来执行文件树访问。该实现是基于"),r("code",[e._v("Files.walk(Path start, int maxDepth, FileVisitOption…​ options)")]),e._v("的功能。结果中排除了根目录("),r("code",[e._v("DirectoryScanner.listFiles(File)")]),e._v(")参数。所有其他子目录包含和排除都是基于目标"),r("code",[e._v("FileListFilter")]),e._v("实现的。例如,"),r("code",[e._v("SimplePatternFileListFilter")]),e._v("默认情况下会过滤掉目录。有关更多信息,请参见["),r("code",[e._v("AbstractDirectoryAwareFileListFilter")]),e._v("](https://DOCS. Spring.io/ Spring-integration/api/org/springframework/integration/file/filters/AbstractDirectoryAwarefilestFilter.html)及其实现。")]),e._v(" "),r("table",[r("thead",[r("tr",[r("th"),e._v(" "),r("th",[e._v("从版本 5.5 开始,爪哇 DSL 的"),r("code",[e._v("FileInboundChannelAdapterSpec")]),e._v("有一个方便的"),r("code",[e._v("recursive(boolean)")]),e._v("选项,可以在目标"),r("code",[e._v("RecursiveDirectoryScanner")]),e._v("中使用"),r("code",[e._v("RecursiveDirectoryScanner")]),e._v(",而不是默认的。")])])]),e._v(" "),r("tbody")]),e._v(" "),r("h4",{attrs:{id:"名称空间支持"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#名称空间支持"}},[e._v("#")]),e._v(" 名称空间支持")]),e._v(" "),r("p",[e._v("通过使用特定于文件的命名空间,可以简化文件读取的配置。要做到这一点,请使用以下模板:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('\n\n\n')])])]),r("p",[e._v("在此命名空间中,你可以减少"),r("code",[e._v("FileReadingMessageSource")]),e._v("并将其包装到入站通道适配器中,如下所示:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('\n\n\n\n\n\n\n')])])]),r("p",[e._v("第一个通道适配器示例依赖于默认的"),r("code",[e._v("FileListFilter")]),e._v("实现:")]),e._v(" "),r("ul",[r("li",[r("p",[r("code",[e._v("IgnoreHiddenFileListFilter")]),e._v("(不处理隐藏文件)")])]),e._v(" "),r("li",[r("p",[r("code",[e._v("AcceptOnceFileListFilter")]),e._v("(防止重复)")])])]),e._v(" "),r("p",[e._v("因此,还可以保留"),r("code",[e._v("prevent-duplicates")]),e._v("和"),r("code",[e._v("ignore-hidden")]),e._v("属性,因为默认情况下它们是"),r("code",[e._v("true")]),e._v("。")]),e._v(" "),r("table",[r("thead",[r("tr",[r("th"),e._v(" "),r("th",[e._v("Spring Integration4.2 引入了"),r("code",[e._v("ignore-hidden")]),e._v("属性。"),r("br"),e._v("在以前的版本中,隐藏的文件被包括在内。")])])]),e._v(" "),r("tbody")]),e._v(" "),r("p",[e._v("第二个通道适配器示例使用自定义过滤器,第三个使用"),r("code",[e._v("filename-pattern")]),e._v("属性添加基于"),r("code",[e._v("AntPathMatcher")]),e._v("的过滤器,第四个使用"),r("code",[e._v("filename-regex")]),e._v("属性向"),r("code",[e._v("FileReadingMessageSource")]),e._v("添加基于正则表达式模式的过滤器。"),r("code",[e._v("filename-pattern")]),e._v("和"),r("code",[e._v("filename-regex")]),e._v("属性都与常规的"),r("code",[e._v("filter")]),e._v("引用属性互斥。但是,你可以使用"),r("code",[e._v("filter")]),e._v("属性引用"),r("code",[e._v("CompositeFileListFilter")]),e._v("的实例,该实例结合了任意数量的过滤器,包括一个或多个基于模式的过滤器,以满足你的特定需求。")]),e._v(" "),r("p",[e._v("当多个进程从同一个目录中读取时,你可能想要锁定文件,以防止它们同时被拾取。要做到这一点,你可以使用"),r("code",[e._v("FileLocker")]),e._v("。有一个基于"),r("code",[e._v("java.nio")]),e._v("的实现可用,但也可以实现自己的锁定方案。"),r("code",[e._v("nio")]),e._v("储物柜可以按以下方式注入:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('\n \n\n')])])]),r("p",[e._v("你可以按以下方式配置自定义的储物柜:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('\n \n\n')])])]),r("table",[r("thead",[r("tr",[r("th"),e._v(" "),r("th",[e._v("当文件入站适配器配置了锁存器时,它将负责在允许接收文件之前获取锁。"),r("br"),e._v("它不承担解锁文件的责任。"),r("br"),e._v("如果你已经处理了文件并保持锁在周围,你有内存泄漏。"),r("br"),e._v("如果这是一个问题,你应该在适当的时候自己调用"),r("code",[e._v("FileLocker.unlock(File file)")]),e._v("。")])])]),e._v(" "),r("tbody")]),e._v(" "),r("p",[e._v("当过滤和锁定文件还不够时,你可能需要完全控制列出文件的方式。要实现这种类型的需求,可以使用"),r("code",[e._v("DirectoryScanner")]),e._v("的实现。此扫描仪可让你准确地确定每个投票中列出的文件。这也是 Spring 集成内部使用的接口,用于将"),r("code",[e._v("FileListFilter")]),e._v("实例和"),r("code",[e._v("FileLocker")]),e._v("连接到"),r("code",[e._v("FileReadingMessageSource")]),e._v("。可以在"),r("code",[e._v("scanner")]),e._v("属性上的"),r("code",[e._v("")]),e._v("中插入自定义"),r("code",[e._v("DirectoryScanner")]),e._v(",如下例所示:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('\n')])])]),r("p",[e._v("这样做可以让你完全自由地选择排序、列表和锁定策略。")]),e._v(" "),r("p",[e._v("同样重要的是要了解过滤器(包括"),r("code",[e._v("patterns")]),e._v(","),r("code",[e._v("regex")]),e._v(","),r("code",[e._v("prevent-duplicates")]),e._v(",以及其他)和"),r("code",[e._v("locker")]),e._v("实例实际上是由"),r("code",[e._v("scanner")]),e._v("使用的。适配器上设置的任何这些属性随后都会被注入到内部"),r("code",[e._v("scanner")]),e._v("中。对于外部"),r("code",[e._v("scanner")]),e._v("的情况,在"),r("code",[e._v("FileReadingMessageSource")]),e._v("上禁止所有筛选器和寄存器属性。它们必须在该自定义"),r("code",[e._v("DirectoryScanner")]),e._v("上指定(如果需要)。换句话说,如果将"),r("code",[e._v("scanner")]),e._v("注入"),r("code",[e._v("FileReadingMessageSource")]),e._v("中,则应该在"),r("code",[e._v("filter")]),e._v("和"),r("code",[e._v("locker")]),e._v("上提供"),r("code",[e._v("scanner")]),e._v(",而不是在"),r("code",[e._v("FileReadingMessageSource")]),e._v("上提供。")]),e._v(" "),r("table",[r("thead",[r("tr",[r("th"),e._v(" "),r("th",[e._v("默认情况下,"),r("code",[e._v("DefaultDirectoryScanner")]),e._v("使用"),r("code",[e._v("IgnoreHiddenFileListFilter")]),e._v("和"),r("code",[e._v("AcceptOnceFileListFilter")]),e._v("。"),r("br"),e._v("为了防止它们的使用,你可以配置自己的过滤器(例如"),r("code",[e._v("AcceptAllFileListFilter")]),e._v("),甚至将其设置为"),r("code",[e._v("null")]),e._v("。")])])]),e._v(" "),r("tbody")]),e._v(" "),r("h4",{attrs:{id:"watchservicedirectoryscanner"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#watchservicedirectoryscanner"}},[e._v("#")]),e._v(" "),r("code",[e._v("WatchServiceDirectoryScanner")])]),e._v(" "),r("p",[e._v("当新文件被添加到目录时,"),r("code",[e._v("FileReadingMessageSource.WatchServiceDirectoryScanner")]),e._v("依赖于文件系统事件。在初始化过程中,目录被注册以生成事件。初始文件列表也是在初始化期间生成的。在遍历目录树时,还会注册遇到的任意子目录以生成事件。在第一次轮询时,将返回从遍历目录中获得的初始文件列表。在随后的投票中,将返回来自新创建事件的文件。如果添加了一个新的子目录,它的创建事件将用于遍历新的子树,以查找现有的文件并注册找到的任何新的子目录。")]),e._v(" "),r("table",[r("thead",[r("tr",[r("th"),e._v(" "),r("th",[e._v("当它的内部事件"),r("code",[e._v("WatchKey")]),e._v("没有像发生目录修改事件那样迅速地被程序耗尽时,"),r("code",[e._v("queue")]),e._v("就存在一个问题。"),r("br"),e._v("如果超过了队列大小,a"),r("code",[e._v("StandardWatchEventKinds.OVERFLOW")]),e._v("被发出以指示一些文件系统事件可能丢失。"),r("br"),e._v("在这种情况下,根目录被完全重新扫描。"),r("br"),e._v("为了避免重复,考虑使用适当的"),r("code",[e._v("FileListFilter")]),e._v("(例如"),r("code",[e._v("AcceptOnceFileListFilter")]),e._v(")或在处理完成时删除文件。")])])]),e._v(" "),r("tbody")]),e._v(" "),r("p",[e._v("可以通过"),r("code",[e._v("WatchServiceDirectoryScanner")]),e._v("选项启用"),r("code",[e._v("FileReadingMessageSource.use-watch-service")]),e._v("选项,该选项与"),r("code",[e._v("scanner")]),e._v("选项互斥。为提供的"),r("code",[e._v("directory")]),e._v("填充内部"),r("code",[e._v("FileReadingMessageSource.WatchServiceDirectoryScanner")]),e._v("实例。")]),e._v(" "),r("p",[e._v("此外,现在"),r("code",[e._v("WatchService")]),e._v("轮询逻辑可以跟踪"),r("code",[e._v("StandardWatchEventKinds.ENTRY_MODIFY")]),e._v("和"),r("code",[e._v("StandardWatchEventKinds.ENTRY_DELETE")]),e._v("。")]),e._v(" "),r("p",[e._v("如果需要跟踪对现有文件和新文件的修改,则应在"),r("code",[e._v("FileListFilter")]),e._v("中实现"),r("code",[e._v("ENTRY_MODIFY")]),e._v("事件逻辑。否则,来自这些事件的文件将以相同的方式处理。")]),e._v(" "),r("p",[r("code",[e._v("ResettableFileListFilter")]),e._v("实现拾取"),r("code",[e._v("ENTRY_DELETE")]),e._v("事件。因此,它们的文件是为"),r("code",[e._v("remove()")]),e._v("操作提供的。启用此事件后,"),r("code",[e._v("AcceptOnceFileListFilter")]),e._v("之类的过滤器将删除该文件。因此,如果出现同名的文件,它就会通过筛选器并作为消息发送。")]),e._v(" "),r("p",[e._v("为此,引入了"),r("code",[e._v("watch-events")]),e._v("属性("),r("code",[e._v("FileReadingMessageSource.setWatchEvents(WatchEventType…​ watchEvents)")]),e._v(")。("),r("code",[e._v("WatchEventType")]),e._v("是"),r("code",[e._v("FileReadingMessageSource")]),e._v("中的一个公共内部枚举。)有了这样的选项,我们可以对新文件使用一个下游流逻辑,并对修改过的文件使用一些其他逻辑。下面的示例展示了如何为在同一目录中创建和修改事件配置不同的逻辑:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('\n\n \x3c!-- The default is CREATE. --\x3e\n')])])]),r("h4",{attrs:{id:"限制内存消耗"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#限制内存消耗"}},[e._v("#")]),e._v(" 限制内存消耗")]),e._v(" "),r("p",[e._v("你可以使用"),r("code",[e._v("HeadDirectoryScanner")]),e._v("来限制保留在内存中的文件数量。这在扫描大型目录时可能很有用。对于 XML 配置,可以通过在入站通道适配器上设置"),r("code",[e._v("queue-size")]),e._v("属性来实现这一点。")]),e._v(" "),r("p",[e._v("在版本 4.2 之前,此设置与任何其他过滤器的使用都不兼容。任何其他过滤器(包括"),r("code",[e._v('prevent-duplicates="true"')]),e._v(")都覆盖了用于限制大小的过滤器。")]),e._v(" "),r("table",[r("thead",[r("tr",[r("th"),e._v(" "),r("th",[e._v("使用"),r("code",[e._v("HeadDirectoryScanner")]),e._v("与"),r("code",[e._v("AcceptOnceFileListFilter")]),e._v("不兼容。"),r("br"),e._v("由于在投票决定期间会参考所有过滤器,"),r("code",[e._v("AcceptOnceFileListFilter")]),e._v("不知道其他过滤器可能正在临时过滤文件。"),r("br"),e._v("即使以前由"),r("code",[e._v("HeadDirectoryScanner.HeadFilter")]),e._v("过滤的文件现在可用,"),r("code",[e._v("AcceptOnceFileListFilter")]),e._v("对它们进行过滤。"),r("br"),r("br"),e._v("通常,在这种情况下,你应该删除处理过的文件,以便在未来的投票中可以使用以前过滤过的文件。")])])]),e._v(" "),r("tbody")]),e._v(" "),r("h4",{attrs:{id:"使用-爪哇-配置进行配置"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#使用-爪哇-配置进行配置"}},[e._v("#")]),e._v(" 使用 爪哇 配置进行配置")]),e._v(" "),r("p",[e._v("下面的 Spring 引导应用程序展示了如何使用 Java 配置配置出站适配器的示例:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@SpringBootApplication\npublic class FileReadingJavaApplication {\n\n public static void main(String[] args) {\n new SpringApplicationBuilder(FileReadingJavaApplication.class)\n .web(false)\n .run(args);\n }\n\n @Bean\n public MessageChannel fileInputChannel() {\n return new DirectChannel();\n }\n\n @Bean\n @InboundChannelAdapter(value = "fileInputChannel", poller = @Poller(fixedDelay = "1000"))\n public MessageSource fileReadingMessageSource() {\n FileReadingMessageSource source = new FileReadingMessageSource();\n source.setDirectory(new File(INBOUND_PATH));\n source.setFilter(new SimplePatternFileListFilter("*.txt"));\n return source;\n }\n\n @Bean\n @Transformer(inputChannel = "fileInputChannel", outputChannel = "processFileChannel")\n public FileToStringTransformer fileToStringTransformer() {\n return new FileToStringTransformer();\n }\n\n}\n')])])]),r("h4",{attrs:{id:"使用-java-dsl-进行配置"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#使用-java-dsl-进行配置"}},[e._v("#")]),e._v(" 使用 Java DSL 进行配置")]),e._v(" "),r("p",[e._v("Spring 以下引导应用程序展示了如何使用 Java DSL 配置出站适配器的示例:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@SpringBootApplication\npublic class FileReadingJavaApplication {\n\n public static void main(String[] args) {\n new SpringApplicationBuilder(FileReadingJavaApplication.class)\n .web(false)\n .run(args);\n }\n\n @Bean\n public IntegrationFlow fileReadingFlow() {\n return IntegrationFlows\n .from(Files.inboundAdapter(new File(INBOUND_PATH))\n .patternFilter("*.txt"),\n e -> e.poller(Pollers.fixedDelay(1000)))\n .transform(Files.toStringTransformer())\n .channel("processFileChannel")\n .get();\n }\n\n}\n')])])]),r("h4",{attrs:{id:"tail-ing-文件"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#tail-ing-文件"}},[e._v("#")]),e._v(" “tail”ing 文件")]),e._v(" "),r("p",[e._v("另一个流行的用例是从文件的末尾(或尾部)获取“行”,在添加新行时捕获新行。提供了两种实现方式。第一个是"),r("code",[e._v("OSDelegatingFileTailingMessageProducer")]),e._v(",它使用本机"),r("code",[e._v("tail")]),e._v("命令(在具有该命令的操作系统上)。这通常是这些平台上最有效的实现。对于没有"),r("code",[e._v("tail")]),e._v("命令的操作系统,第二个实现"),r("code",[e._v("ApacheCommonsFileTailingMessageProducer")]),e._v("使用 Apache"),r("code",[e._v("commons-io``Tailer")]),e._v("类。")]),e._v(" "),r("p",[e._v("在这两种情况下,通过使用正常的 Spring 事件发布机制,文件系统事件(例如文件不可用和其他事件)被发布为"),r("code",[e._v("ApplicationEvent")]),e._v("实例。这类事件的例子包括:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v("[message=tail: cannot open '/tmp/somefile' for reading:\n No such file or directory, file=/tmp/somefile]\n\n[message=tail: '/tmp/somefile' has become accessible, file=/tmp/somefile]\n\n[message=tail: '/tmp/somefile' has become inaccessible:\n No such file or directory, file=/tmp/somefile]\n\n[message=tail: '/tmp/somefile' has appeared;\n following end of new file, file=/tmp/somefile]\n")])])]),r("p",[e._v("前面示例中显示的事件序列可能会发生,例如,当文件旋转时。")]),e._v(" "),r("p",[e._v("从版本 5.0 开始,当"),r("code",[e._v("idleEventInterval")]),e._v("期间文件中没有数据时,会发出"),r("code",[e._v("FileTailingIdleEvent")]),e._v("。下面的示例显示了这样的事件的样子:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v("[message=Idle timeout, file=/tmp/somefile] [idle time=5438]\n")])])]),r("table",[r("thead",[r("tr",[r("th"),e._v(" "),r("th",[e._v("并非所有支持"),r("code",[e._v("tail")]),e._v("命令的平台都提供这些状态消息。")])])]),e._v(" "),r("tbody")]),e._v(" "),r("p",[e._v("从这些端点发出的消息具有以下标题:")]),e._v(" "),r("ul",[r("li",[r("p",[r("code",[e._v("FileHeaders.ORIGINAL_FILE")]),e._v(":"),r("code",[e._v("File")]),e._v("对象")])]),e._v(" "),r("li",[r("p",[r("code",[e._v("FileHeaders.FILENAME")]),e._v(":文件名("),r("code",[e._v("File.getName()")]),e._v(")")])])]),e._v(" "),r("table",[r("thead",[r("tr",[r("th"),e._v(" "),r("th",[e._v("在版本 5.0 之前的版本中,"),r("code",[e._v("FileHeaders.FILENAME")]),e._v("头包含文件绝对路径的字符串表示。"),r("br"),e._v("你现在可以通过在原始文件头上调用"),r("code",[e._v("getAbsolutePath()")]),e._v("来获得该字符串表示。")])])]),e._v(" "),r("tbody")]),e._v(" "),r("p",[e._v("下面的示例创建一个带有默认选项(“-f-n0”,意思是跟随当前结尾的文件名)的本机适配器。")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('\n')])])]),r("p",[e._v("下面的示例创建一个带有“-f-n+0”选项的本机适配器(意思是跟随文件名,发出所有已存在的行)。")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('\n')])])]),r("p",[e._v("如果"),r("code",[e._v("tail")]),e._v("命令失败(在某些平台上,丢失的文件会导致"),r("code",[e._v("tail")]),e._v("失败,即使指定了"),r("code",[e._v("-F")]),e._v("),该命令将每 10 秒重试一次。")]),e._v(" "),r("p",[e._v("默认情况下,本机适配器从标准输出捕获内容并将其作为消息发送。他们还从标准错误中捕捉,以提出事件。从版本 4.3.6 开始,你可以通过将"),r("code",[e._v("enable-status-reader")]),e._v("设置为"),r("code",[e._v("false")]),e._v("来丢弃标准错误事件,如下例所示:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('\n')])])]),r("p",[e._v("在下面的示例中,"),r("code",[e._v("IdleEventInterval")]),e._v("被设置为"),r("code",[e._v("5000")]),e._v(",这意味着,如果五秒内不写行,则每隔五秒就会触发"),r("code",[e._v("FileTailingIdleEvent")]),e._v(":")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('\n')])])]),r("p",[e._v("当你需要停止适配器时,这可能会很有用。")]),e._v(" "),r("p",[e._v("下面的示例创建了一个 Apache"),r("code",[e._v("commons-io``Tailer")]),e._v("适配器,该适配器每两秒检查文件的新行,并每十秒检查是否存在丢失的文件:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('\n')])])]),r("table",[r("thead",[r("tr",[r("th",[r("strong",[e._v("1")])]),e._v(" "),r("th",[e._v("该文件从开始("),r("code",[e._v('end="false"')]),e._v(")而不是从结束(这是默认的)跟踪。")])])]),e._v(" "),r("tbody",[r("tr",[r("td",[r("strong",[e._v("2")])]),e._v(" "),r("td",[e._v("为每个块重新打开文件(默认情况是保持文件打开)。")])])])]),e._v(" "),r("table",[r("thead",[r("tr",[r("th"),e._v(" "),r("th",[e._v("指定"),r("code",[e._v("delay")]),e._v("、"),r("code",[e._v("end")]),e._v("或"),r("code",[e._v("reopen")]),e._v("属性将强制使用 Apache"),r("code",[e._v("commons-io")]),e._v("适配器,并使"),r("code",[e._v("native-options")]),e._v("属性不可用。")])])]),e._v(" "),r("tbody")]),e._v(" "),r("h4",{attrs:{id:"处理不完整数据"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#处理不完整数据"}},[e._v("#")]),e._v(" 处理不完整数据")]),e._v(" "),r("p",[e._v("在文件传输场景中,一个常见的问题是如何确定传输已经完成,这样你就不会开始读取不完整的文件。解决此问题的一种常见技术是使用一个临时名称编写文件,然后将其自动重命名为最终名称。这种技术,再加上屏蔽临时文件使其不被用户获取的过滤器,提供了一种健壮的解决方案。 Spring 编写文件(本地或远程)的集成组件使用这种技术。默认情况下,它们将"),r("code",[e._v(".writing")]),e._v("附加到文件名,并在传输完成后将其删除。")]),e._v(" "),r("p",[e._v("另一种常见的技术是编写第二个“标记”文件,以表明文件传输已经完成。在这种情况下,你不应该认为"),r("code",[e._v("somefile.txt")]),e._v("(例如)是可用的,直到"),r("code",[e._v("somefile.txt.complete")]),e._v("也存在。 Spring 集成版本 5.0 引入了新的过滤器以支持该机制。实现方式是为文件系统("),r("code",[e._v("FileSystemMarkerFilePresentFileListFilter")]),e._v(")、"),r("RouterLink",{attrs:{to:"/spring-integration/ftp.html#ftp-incomplete"}},[e._v("FTP")]),e._v("和"),r("RouterLink",{attrs:{to:"/spring-integration/sftp.html#sftp-incomplete"}},[e._v("SFTP")]),e._v("提供的。它们是可配置的,使得标记文件可以有任何名称,尽管它通常与正在传输的文件相关。有关更多信息,请参见"),r("a",{attrs:{href:"https://docs.spring.io/spring-integration/api/org/springframework/integration/file/filters/FileSystemMarkerFilePresentFileListFilter.html",target:"_blank",rel:"noopener noreferrer"}},[e._v("Javadoc"),r("OutboundLink")],1),e._v("。")],1),e._v(" "),r("h3",{attrs:{id:"写入文件"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#写入文件"}},[e._v("#")]),e._v(" 写入文件")]),e._v(" "),r("p",[e._v("要将消息写入文件系统,可以使用["),r("code",[e._v("FileWritingMessageHandler")]),e._v("](https://DOCS. Spring.io/ Spring-integration/api/org/springframework/integration/file/FileWritingMessageHandler.html)。这个类可以处理以下有效负载类型:")]),e._v(" "),r("ul",[r("li",[r("p",[r("code",[e._v("File")])])]),e._v(" "),r("li",[r("p",[r("code",[e._v("String")])])]),e._v(" "),r("li",[r("p",[e._v("字节数组")])]),e._v(" "),r("li",[r("p",[r("code",[e._v("InputStream")]),e._v("(自"),r("em",[e._v("版本 4.2")]),e._v("起)")])])]),e._v(" "),r("p",[e._v("对于字符串有效负载,你可以配置编码和字符集。")]),e._v(" "),r("p",[e._v("为了使事情变得更简单,你可以使用 XML 命名空间将"),r("code",[e._v("FileWritingMessageHandler")]),e._v("配置为出站通道适配器或出站网关的一部分。")]),e._v(" "),r("p",[e._v("从版本 4.3 开始,你可以指定写入文件时使用的缓冲区大小。")]),e._v(" "),r("p",[e._v("从版本 5.1 开始,你可以提供一个"),r("code",[e._v("BiConsumer>``newFileCallback")]),e._v(",如果你使用"),r("code",[e._v("FileExistsMode.APPEND")]),e._v("或"),r("code",[e._v("FileExistsMode.APPEND_NO_FLUSH")]),e._v(",并且必须创建一个新文件,则会触发该命令。此回调接收一个新创建的文件和触发该文件的消息。例如,这个回调可以用来编写在消息头中定义的 CSV 头。")]),e._v(" "),r("h4",{attrs:{id:"生成文件名"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#生成文件名"}},[e._v("#")]),e._v(" 生成文件名")]),e._v(" "),r("p",[e._v("在最简单的形式中,"),r("code",[e._v("FileWritingMessageHandler")]),e._v("只需要一个目标目录来编写文件。要写入的文件的名称由处理程序的["),r("code",[e._v("FileNameGenerator")]),e._v("](https://DOCS. Spring.io/ Spring-integration/api/org/springframework/integration/file/filenamegenerator.html)决定。"),r("a",{attrs:{href:"https://docs.spring.io/spring-integration/api/org/springframework/integration/file/DefaultFileNameGenerator.html",target:"_blank",rel:"noopener noreferrer"}},[e._v("默认实现"),r("OutboundLink")],1),e._v("查找一个消息头,其键与定义为["),r("code",[e._v("FileHeaders.FILENAME")]),e._v("]的常量匹配(https://DOCS. Spring.io/ Spring-integration/api/constant-values.html#org.springframework.integration.file.fileheaders.filename)。")]),e._v(" "),r("p",[e._v("或者,你可以指定一个表达式,该表达式将根据消息进行求值,以生成一个文件名——例如,"),r("code",[e._v("headers['myCustomHeader'] + '.something'")]),e._v("。表达式必须求值为"),r("code",[e._v("String")]),e._v("。为了方便起见,"),r("code",[e._v("DefaultFileNameGenerator")]),e._v("还提供了"),r("code",[e._v("setHeaderName")]),e._v("方法,让你显式地指定要用作文件名的值的消息头。")]),e._v(" "),r("p",[e._v("一旦设置好,"),r("code",[e._v("DefaultFileNameGenerator")]),e._v("将采用以下解析步骤来确定给定消息有效负载的文件名:")]),e._v(" "),r("ol",[r("li",[r("p",[e._v("根据消息计算表达式,如果结果是非空的"),r("code",[e._v("String")]),e._v(",则将其用作文件名。")])]),e._v(" "),r("li",[r("p",[e._v("否则,如果有效负载是"),r("code",[e._v("java.io.File")]),e._v(",则使用"),r("code",[e._v("File")]),e._v("对象的文件名。")])]),e._v(" "),r("li",[r("p",[e._v("否则,使用附加了."),r("code",[e._v("msg")]),e._v("的消息 ID 作为文件名。")])])]),e._v(" "),r("p",[e._v("使用 XML 名称空间支持时,文件出站通道适配器和文件出站网关都支持以下互斥配置属性:")]),e._v(" "),r("ul",[r("li",[r("p",[r("code",[e._v("filename-generator")]),e._v("(对"),r("code",[e._v("FileNameGenerator")]),e._v("实现的引用)")])]),e._v(" "),r("li",[r("p",[r("code",[e._v("filename-generator-expression")]),e._v("(计算为"),r("code",[e._v("String")]),e._v("的表达式)")])])]),e._v(" "),r("p",[e._v("在编写文件时,会使用一个临时文件后缀(其默认值是"),r("code",[e._v(".writing")]),e._v(")。当文件被写入时,它被追加到文件名中。要自定义后缀,你可以在文件出站通道适配器和文件出站网关上设置"),r("code",[e._v("temporary-file-suffix")]),e._v("属性。")]),e._v(" "),r("table",[r("thead",[r("tr",[r("th"),e._v(" "),r("th",[e._v("当使用"),r("code",[e._v("APPEND")]),e._v("文件"),r("code",[e._v("mode")]),e._v("时,"),r("code",[e._v("temporary-file-suffix")]),e._v("属性将被忽略,因为数据将直接附加到文件中。")])])]),e._v(" "),r("tbody")]),e._v(" "),r("p",[e._v("从版本 4.2.5 开始,生成的文件名(作为"),r("code",[e._v("filename-generator")]),e._v("或"),r("code",[e._v("filename-generator-expression")]),e._v("求值的结果)可以与目标文件名一起表示子路径。和前面一样,它被用作"),r("code",[e._v("File(File parent, String child)")]),e._v("的第二个构造函数参数。然而,在过去,我们并没有为子路径创建("),r("code",[e._v("mkdirs()")]),e._v(")目录,只假设了文件名。当我们需要恢复文件系统树以匹配源目录时,这种方法非常有用——例如,当解压缩归档并以原始顺序保存目标目录中的所有文件时。")]),e._v(" "),r("h4",{attrs:{id:"指定输出目录"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#指定输出目录"}},[e._v("#")]),e._v(" 指定输出目录")]),e._v(" "),r("p",[e._v("文件出站通道适配器和文件出站网关都为指定输出目录提供了两个相互排斥的配置属性:")]),e._v(" "),r("ul",[r("li",[r("p",[r("code",[e._v("directory")])])]),e._v(" "),r("li",[r("p",[r("code",[e._v("directory-expression")])])])]),e._v(" "),r("table",[r("thead",[r("tr",[r("th"),e._v(" "),r("th",[e._v("Spring Integration2.2 引入了"),r("code",[e._v("directory-expression")]),e._v("属性。")])])]),e._v(" "),r("tbody")]),e._v(" "),r("h5",{attrs:{id:"使用directory属性"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#使用directory属性"}},[e._v("#")]),e._v(" 使用"),r("code",[e._v("directory")]),e._v("属性")]),e._v(" "),r("p",[e._v("当使用"),r("code",[e._v("directory")]),e._v("属性时,输出目录被设置为固定值,这是在初始化"),r("code",[e._v("FileWritingMessageHandler")]),e._v("时设置的。如果未指定此属性,则必须使用"),r("code",[e._v("directory-expression")]),e._v("属性。")]),e._v(" "),r("h5",{attrs:{id:"使用directory-expression属性"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#使用directory-expression属性"}},[e._v("#")]),e._v(" 使用"),r("code",[e._v("directory-expression")]),e._v("属性")]),e._v(" "),r("p",[e._v("如果你希望获得完整的 SPEL 支持,可以使用"),r("code",[e._v("directory-expression")]),e._v("属性。此属性接受一个 SPEL 表达式,该表达式是为正在处理的每条消息计算的。因此,在动态指定输出文件目录时,你可以完全访问消息的有效负载及其标题。")]),e._v(" "),r("p",[e._v("SPEL 表达式必须解析为"),r("code",[e._v("String")]),e._v("、"),r("code",[e._v("java.io.File")]),e._v("或"),r("code",[e._v("org.springframework.core.io.Resource")]),e._v("。(后面的值被计算为"),r("code",[e._v("File")]),e._v(")此外,得到的"),r("code",[e._v("String")]),e._v("或"),r("code",[e._v("File")]),e._v("必须指向一个目录。如果没有指定"),r("code",[e._v("directory-expression")]),e._v("属性,则必须设置"),r("code",[e._v("directory")]),e._v("属性。")]),e._v(" "),r("h5",{attrs:{id:"使用auto-create-directory属性"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#使用auto-create-directory属性"}},[e._v("#")]),e._v(" 使用"),r("code",[e._v("auto-create-directory")]),e._v("属性")]),e._v(" "),r("p",[e._v("默认情况下,如果目标目录不存在,则会自动创建相应的目标目录和任何不存在的父目录。为了防止这种行为,你可以将"),r("code",[e._v("auto-create-directory")]),e._v("属性设置为"),r("code",[e._v("false")]),e._v("。此属性同时适用于"),r("code",[e._v("directory")]),e._v("和"),r("code",[e._v("directory-expression")]),e._v("属性。")]),e._v(" "),r("table",[r("thead",[r("tr",[r("th"),e._v(" "),r("th",[e._v("当使用"),r("code",[e._v("directory")]),e._v("属性并且"),r("code",[e._v("auto-create-directory")]),e._v("是"),r("code",[e._v("false")]),e._v("时,从 Spring Integration2.2 开始进行以下更改:"),r("br"),r("br"),e._v("而不是在初始化适配器时检查目标目录的存在,现在将对正在处理的每条消息执行此检查。"),r("br"),r("br"),e._v("此外,如果"),r("code",[e._v("auto-create-directory")]),e._v("是"),r("code",[e._v("true")]),e._v("并且在处理消息之间删除了目录,则将为正在处理的每条消息重新创建目录。")])])]),e._v(" "),r("tbody")]),e._v(" "),r("h4",{attrs:{id:"处理现有的目标文件"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#处理现有的目标文件"}},[e._v("#")]),e._v(" 处理现有的目标文件")]),e._v(" "),r("p",[e._v("当你写入文件并且目标文件已经存在时,默认的行为是覆盖该目标文件。可以通过在相关的文件出站组件上设置"),r("code",[e._v("mode")]),e._v("属性来更改此行为。存在以下备选方案:")]),e._v(" "),r("ul",[r("li",[r("p",[r("code",[e._v("REPLACE")]),e._v("(默认)")])]),e._v(" "),r("li",[r("p",[r("code",[e._v("REPLACE_IF_MODIFIED")])])]),e._v(" "),r("li",[r("p",[r("code",[e._v("APPEND")])])]),e._v(" "),r("li",[r("p",[r("code",[e._v("APPEND_NO_FLUSH")])])]),e._v(" "),r("li",[r("p",[r("code",[e._v("FAIL")])])]),e._v(" "),r("li",[r("p",[r("code",[e._v("IGNORE")])])])]),e._v(" "),r("table",[r("thead",[r("tr",[r("th"),e._v(" "),r("th",[e._v("Spring Integration2.2 引入了"),r("code",[e._v("mode")]),e._v("属性和"),r("code",[e._v("APPEND")]),e._v("、"),r("code",[e._v("FAIL")]),e._v("和"),r("code",[e._v("IGNORE")]),e._v("选项。")])])]),e._v(" "),r("tbody")]),e._v(" "),r("p",[r("code",[e._v("REPLACE")])]),e._v(" "),r("p",[e._v("如果目标文件已经存在,则该文件将被覆盖。如果没有指定"),r("code",[e._v("mode")]),e._v("属性,这是写文件时的默认行为。")]),e._v(" "),r("p",[r("code",[e._v("REPLACE_IF_MODIFIED")])]),e._v(" "),r("p",[e._v("如果目标文件已经存在,则只有在最后修改的时间戳与源文件的时间戳不同时才会覆盖该文件。对于"),r("code",[e._v("File")]),e._v("有效载荷,将有效载荷"),r("code",[e._v("lastModified")]),e._v("的时间与现有文件进行比较。对于其他有效负载,将"),r("code",[e._v("FileHeaders.SET_MODIFIED")]),e._v("("),r("code",[e._v("file_setModified")]),e._v(")头与现有文件进行比较。如果缺少头文件或其值不是"),r("code",[e._v("Number")]),e._v(",则始终替换该文件。")]),e._v(" "),r("p",[r("code",[e._v("APPEND")])]),e._v(" "),r("p",[e._v("这种模式允许你将消息内容追加到现有的文件中,而不是每次都创建一个新的文件。请注意,此属性与"),r("code",[e._v("temporary-file-suffix")]),e._v("属性是互斥的,因为当它将内容追加到现有文件时,适配器不再使用临时文件。在每条消息之后关闭该文件。")]),e._v(" "),r("p",[r("code",[e._v("APPEND_NO_FLUSH")])]),e._v(" "),r("p",[e._v("此选项具有与"),r("code",[e._v("APPEND")]),e._v("相同的语义,但不会刷新数据,也不会在每条消息之后关闭文件。这可以在发生故障时的数据丢失风险下提供重要的性能。有关更多信息,请参见[使用"),r("code",[e._v("APPEND_NO_FLUSH")]),e._v("时刷新文件]。")]),e._v(" "),r("p",[r("code",[e._v("FAIL")])]),e._v(" "),r("p",[e._v("如果目标文件存在,则抛出["),r("code",[e._v("MessageHandlingException")]),e._v("](https://DOCS. Spring.io/ Spring/DOCS/current/javadoc-api/org/springframework/messageHandlingException.html)。")]),e._v(" "),r("p",[r("code",[e._v("IGNORE")])]),e._v(" "),r("p",[e._v("如果目标文件存在,则消息有效负载将被静默忽略。")]),e._v(" "),r("table",[r("thead",[r("tr",[r("th"),e._v(" "),r("th",[e._v("当使用临时文件后缀(默认值是"),r("code",[e._v(".writing")]),e._v(")时,如果存在最终文件名或临时文件名,则"),r("code",[e._v("IGNORE")]),e._v("选项将应用。")])])]),e._v(" "),r("tbody")]),e._v(" "),r("h4",{attrs:{id:"使用append-no-flush时刷新文件"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#使用append-no-flush时刷新文件"}},[e._v("#")]),e._v(" 使用"),r("code",[e._v("APPEND_NO_FLUSH")]),e._v("时刷新文件")]),e._v(" "),r("p",[e._v("在版本 4.3 中添加了"),r("code",[e._v("APPEND_NO_FLUSH")]),e._v("模式。使用它可以提高性能,因为文件不会在每条消息之后关闭。然而,如果发生故障,这可能会导致数据丢失。")]),e._v(" "),r("p",[e._v("Spring 集成提供了几种刷新策略来减轻这种数据损失:")]),e._v(" "),r("ul",[r("li",[r("p",[e._v("使用"),r("code",[e._v("flushInterval")]),e._v("。如果文件在这段时间内没有被写入,它将被自动刷新。这是近似的,这一次可能达到"),r("code",[e._v("1.33x")]),e._v("(平均值为"),r("code",[e._v("1.167x")]),e._v(")。")])]),e._v(" "),r("li",[r("p",[e._v("将包含正则表达式的消息发送到消息处理程序的"),r("code",[e._v("trigger")]),e._v("方法。具有与模式匹配的绝对路径名的文件将被刷新。")])]),e._v(" "),r("li",[r("p",[e._v("向处理程序提供一个自定义的"),r("code",[e._v("MessageFlushPredicate")]),e._v("实现,以修改当消息发送到"),r("code",[e._v("trigger")]),e._v("方法时所采取的操作。")])]),e._v(" "),r("li",[r("p",[e._v("通过传入自定义"),r("code",[e._v("FileWritingMessageHandler.FlushPredicate")]),e._v("或"),r("code",[e._v("FileWritingMessageHandler.MessageFlushPredicate")]),e._v("实现,调用处理程序的"),r("code",[e._v("flushIfNeeded")]),e._v("方法之一。")])])]),e._v(" "),r("p",[e._v("为每个打开的文件调用谓词。有关更多信息,请参见"),r("a",{attrs:{href:"https://docs.spring.io/spring-integration/api/index.html",target:"_blank",rel:"noopener noreferrer"}},[e._v("Javadoc"),r("OutboundLink")],1),e._v("中的这些接口。请注意,自版本 5.0 以来,谓词方法提供了另一个参数:当前文件首次写入(如果是新建的或先前关闭的)的时间。")]),e._v(" "),r("p",[e._v("当使用"),r("code",[e._v("flushInterval")]),e._v("时,间隔从最后一次写入开始。只有当文件空闲了一段时间时,才会刷新该文件。从版本 4.3.7 开始,可以将一个附加属性("),r("code",[e._v("flushWhenIdle")]),e._v(")设置为"),r("code",[e._v("false")]),e._v(",这意味着该间隔从对先前刷新的(或新建的)文件的第一次写入开始。")]),e._v(" "),r("h4",{attrs:{id:"文件时间戳"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#文件时间戳"}},[e._v("#")]),e._v(" 文件时间戳")]),e._v(" "),r("p",[e._v("默认情况下,目标文件的"),r("code",[e._v("lastModified")]),e._v("时间戳是文件创建的时间(除了就地重命名保留当前时间戳)。从版本 4.3 开始,你现在可以配置"),r("code",[e._v("preserve-timestamp")]),e._v("(或者在使用 Java 配置时"),r("code",[e._v("setPreserveTimestamp(true)")]),e._v(")。对于"),r("code",[e._v("File")]),e._v("有效负载,这将时间戳从入站文件传输到出站文件(无论是否需要副本)。对于其他有效负载,如果存在"),r("code",[e._v("FileHeaders.SET_MODIFIED")]),e._v("报头("),r("code",[e._v("file_setModified")]),e._v("),则用于设置目标文件的"),r("code",[e._v("lastModified")]),e._v("时间戳,只要报头是"),r("code",[e._v("Number")]),e._v("。")]),e._v(" "),r("h4",{attrs:{id:"文件权限"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#文件权限"}},[e._v("#")]),e._v(" 文件权限")]),e._v(" "),r("p",[e._v("从版本 5.0 开始,当将文件写入支持 POSIX 权限的文件系统时,你可以在出站通道适配器或网关上指定这些权限。该属性是一个整数,通常以常见的八进制格式提供——例如,"),r("code",[e._v("0640")]),e._v(",这意味着所有者具有读/写权限,组具有只读权限,而其他组没有访问权限。")]),e._v(" "),r("h4",{attrs:{id:"文件出站通道适配器"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#文件出站通道适配器"}},[e._v("#")]),e._v(" 文件出站通道适配器")]),e._v(" "),r("p",[e._v("以下示例配置文件出站通道适配器:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('\n')])])]),r("p",[e._v("基于名称空间的配置还支持"),r("code",[e._v("delete-source-files")]),e._v("属性。如果设置为"),r("code",[e._v("true")]),e._v(",则会在写入目标文件后触发原始源文件的删除。该标志的默认值是"),r("code",[e._v("false")]),e._v("。下面的示例展示了如何将其设置为"),r("code",[e._v("true")]),e._v(":")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('\n')])])]),r("table",[r("thead",[r("tr",[r("th"),e._v(" "),r("th",[e._v("只有当入站消息具有"),r("code",[e._v("File")]),e._v("有效负载,或者"),r("code",[e._v("FileHeaders.ORIGINAL_FILE")]),e._v("头值包含源"),r("code",[e._v("File")]),e._v("实例或表示原始文件路径的"),r("code",[e._v("String")]),e._v("实例时,"),r("code",[e._v("delete-source-files")]),e._v("属性才具有效果。")])])]),e._v(" "),r("tbody")]),e._v(" "),r("p",[e._v("从版本 4.2 开始,"),r("code",[e._v("FileWritingMessageHandler")]),e._v("支持"),r("code",[e._v("append-new-line")]),e._v("选项。如果设置为"),r("code",[e._v("true")]),e._v(",则在写入消息后会在文件中添加新的行。默认属性值为"),r("code",[e._v("false")]),e._v("。下面的示例展示了如何使用"),r("code",[e._v("append-new-line")]),e._v("选项:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('\n')])])]),r("h4",{attrs:{id:"出站网关"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#出站网关"}},[e._v("#")]),e._v(" 出站网关")]),e._v(" "),r("p",[e._v("如果你希望继续基于写好的文件处理消息,则可以使用"),r("code",[e._v("outbound-gateway")]),e._v("代替。它的作用类似于"),r("code",[e._v("outbound-channel-adapter")]),e._v("。然而,在写入文件之后,它还将其作为消息的有效负载发送到应答通道。")]),e._v(" "),r("p",[e._v("以下示例配置出站网关:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('\n')])])]),r("p",[e._v("如前所述,你还可以指定"),r("code",[e._v("mode")]),e._v("属性,该属性定义了如何处理目标文件已经存在的情况的行为。详情见"),r("a",{attrs:{href:"#file-writing-destination-exists"}},[e._v("处理现有的目标文件")]),e._v("。通常,当使用文件出站网关时,结果文件将作为回复通道上的消息有效负载返回。")]),e._v(" "),r("p",[e._v("这也适用于指定"),r("code",[e._v("IGNORE")]),e._v("模式时。在这种情况下,将返回预先存在的目标文件。如果请求消息的有效负载是一个文件,那么你仍然可以通过消息头访问该原始文件。见"),r("a",{attrs:{href:"https://docs.spring.io/spring-integration/api/org/springframework/integration/file/FileHeaders.html",target:"_blank",rel:"noopener noreferrer"}},[e._v("fileheaders.original_file "),r("OutboundLink")],1),e._v("。")]),e._v(" "),r("table",[r("thead",[r("tr",[r("th"),e._v(" "),r("th",[e._v("“出站网关”在需要先移动文件,然后通过处理管道发送文件的情况下工作得很好,"),r("br"),e._v("在这种情况下,你可以将文件命名空间的"),r("code",[e._v("inbound-channel-adapter")]),e._v("元素连接到"),r("code",[e._v("outbound-gateway")]),e._v(",然后将该网关的"),r("code",[e._v("reply-channel")]),e._v("连接到管道的开头。")])])]),e._v(" "),r("tbody")]),e._v(" "),r("p",[e._v("如果你有更详细的需求,或者需要支持额外的有效负载类型作为要转换为文件内容的输入,则可以扩展"),r("code",[e._v("FileWritingMessageHandler")]),e._v(",但更好的选择是依赖["),r("code",[e._v("Transformer")]),e._v("](#file-transforming)。")]),e._v(" "),r("h4",{attrs:{id:"使用-java-配置进行配置"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#使用-java-配置进行配置"}},[e._v("#")]),e._v(" 使用 Java 配置进行配置")]),e._v(" "),r("p",[e._v("Spring 以下引导应用程序展示了如何使用 Java 配置配置入站适配器的示例:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@SpringBootApplication\n@IntegrationComponentScan\npublic class FileWritingJavaApplication {\n\n public static void main(String[] args) {\n ConfigurableApplicationContext context =\n new SpringApplicationBuilder(FileWritingJavaApplication.class)\n .web(false)\n .run(args);\n MyGateway gateway = context.getBean(MyGateway.class);\n gateway.writeToFile("foo.txt", new File(tmpDir.getRoot(), "fileWritingFlow"), "foo");\n }\n\n @Bean\n @ServiceActivator(inputChannel = "writeToFileChannel")\n public MessageHandler fileWritingMessageHandler() {\n Expression directoryExpression = new SpelExpressionParser().parseExpression("headers.directory");\n FileWritingMessageHandler handler = new FileWritingMessageHandler(directoryExpression);\n handler.setFileExistsMode(FileExistsMode.APPEND);\n return handler;\n }\n\n @MessagingGateway(defaultRequestChannel = "writeToFileChannel")\n public interface MyGateway {\n\n void writeToFile(@Header(FileHeaders.FILENAME) String fileName,\n @Header(FileHeaders.FILENAME) File directory, String data);\n\n }\n}\n')])])]),r("h4",{attrs:{id:"使用-java-dsl-进行配置-2"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#使用-java-dsl-进行配置-2"}},[e._v("#")]),e._v(" 使用 Java DSL 进行配置")]),e._v(" "),r("p",[e._v("Spring 以下引导应用程序展示了如何使用 Java DSL 配置入站适配器的示例:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@SpringBootApplication\npublic class FileWritingJavaApplication {\n\n public static void main(String[] args) {\n ConfigurableApplicationContext context =\n new SpringApplicationBuilder(FileWritingJavaApplication.class)\n .web(false)\n .run(args);\n MessageChannel fileWritingInput = context.getBean("fileWritingInput", MessageChannel.class);\n fileWritingInput.send(new GenericMessage<>("foo"));\n }\n\n @Bean\n \tpublic IntegrationFlow fileWritingFlow() {\n \t return IntegrationFlows.from("fileWritingInput")\n \t\t .enrichHeaders(h -> h.header(FileHeaders.FILENAME, "foo.txt")\n \t\t .header("directory", new File(tmpDir.getRoot(), "fileWritingFlow")))\n \t .handle(Files.outboundGateway(m -> m.getHeaders().get("directory")))\n \t .channel(MessageChannels.queue("fileWritingResultChannel"))\n \t .get();\n }\n\n}\n')])])]),r("h3",{attrs:{id:"文件变形金刚"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#文件变形金刚"}},[e._v("#")]),e._v(" 文件变形金刚")]),e._v(" "),r("p",[e._v("要将从文件系统读取的数据转换为对象,或者反过来,你需要做一些工作。与"),r("code",[e._v("FileReadingMessageSource")]),e._v("和在较小程度上"),r("code",[e._v("FileWritingMessageHandler")]),e._v("不同,你可能需要自己的机制来完成工作。为此,你可以实现"),r("code",[e._v("Transformer")]),e._v("接口。或者,你可以为入站消息扩展"),r("code",[e._v("AbstractFilePayloadTransformer")]),e._v("。 Spring 集成提供了一些明显的实现方式。")]),e._v(" "),r("p",[e._v("参见[Javadoc for"),r("code",[e._v("Transformer")]),e._v("接口](https://DOCS. Spring.io/ Spring-integration/api/org/springframework/integration/Transformer/Transformer.html)以查看哪个 Spring 集成类实现了它。类似地,你可以检查["),r("code",[e._v("AbstractFilePayloadTransformer")]),e._v("类的 Javadoc](https://DOCS. Spring.io/ Spring-integration/api/org/springframework/integration/file/Transformer/AbstractFilePayloadTransformer.html),以查看哪个 Spring 集成类扩展了它。")]),e._v(" "),r("p",[r("code",[e._v("FileToByteArrayTransformer")]),e._v("扩展"),r("code",[e._v("AbstractFilePayloadTransformer")]),e._v(",并通过使用 Spring 的"),r("code",[e._v("FileCopyUtils")]),e._v("将"),r("code",[e._v("File")]),e._v("对象转换为"),r("code",[e._v("byte[]")]),e._v("。使用一系列的转换通常比将所有的转换放在一个类中更好。在这种情况下,"),r("code",[e._v("File")]),e._v("到"),r("code",[e._v("byte[]")]),e._v("的转换可能是合乎逻辑的第一步。")]),e._v(" "),r("p",[r("code",[e._v("FileToStringTransformer")]),e._v("扩展"),r("code",[e._v("AbstractFilePayloadTransformer")]),e._v("将"),r("code",[e._v("File")]),e._v("对象转换为"),r("code",[e._v("String")]),e._v("对象。如果没有其他方法,这对于调试是有用的(考虑将其与"),r("RouterLink",{attrs:{to:"/spring-integration/channel.html#channel-wiretap"}},[e._v("wire tap")]),e._v("一起使用)。")],1),e._v(" "),r("p",[e._v("要配置特定于文件的转换器,你可以使用文件命名空间中的适当元素,如下例所示:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('\n\n\n')])])]),r("p",[r("code",[e._v("delete-files")]),e._v("选项向 Transformer 发出信号,表示它应该在转换完成后删除入站文件。这绝不是在多线程环境中使用"),r("code",[e._v("FileReadingMessageSource")]),e._v("时使用"),r("code",[e._v("AcceptOnceFileListFilter")]),e._v("的替代方法(例如,通常使用 Spring 集成时)。")]),e._v(" "),r("h3",{attrs:{id:"文件拆分器"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#文件拆分器"}},[e._v("#")]),e._v(" 文件拆分器")]),e._v(" "),r("p",[e._v("在版本 4.1.2 中添加了"),r("code",[e._v("FileSplitter")]),e._v(",并在版本 4.2 中添加了对名称空间的支持。基于"),r("code",[e._v("BufferedReader.readLine()")]),e._v(","),r("code",[e._v("FileSplitter")]),e._v("将文本文件分割成单独的行。默认情况下,拆分器使用"),r("code",[e._v("Iterator")]),e._v("从文件中读取一行时,每次发出一行。将"),r("code",[e._v("iterator")]),e._v("属性设置为"),r("code",[e._v("false")]),e._v("会使它在将所有行作为消息发送之前将它们读入内存。这样做的一个用例可能是,如果你想在发送包含行的任何消息之前检测文件上的 I/O 错误。然而,它只适用于相对较短的文件。")]),e._v(" "),r("p",[e._v("入站有效载荷可以是"),r("code",[e._v("File")]),e._v(","),r("code",[e._v("String")]),e._v("(a"),r("code",[e._v("File")]),e._v("路径),"),r("code",[e._v("InputStream")]),e._v(",或"),r("code",[e._v("Reader")]),e._v("。其他有效载荷类型的发射不变。")]),e._v(" "),r("p",[e._v("下面的清单显示了配置"),r("code",[e._v("FileSplitter")]),e._v("的可能方法:")]),e._v(" "),r("p",[e._v("Java DSL")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@SpringBootApplication\npublic class FileSplitterApplication {\n\n public static void main(String[] args) {\n new SpringApplicationBuilder(FileSplitterApplication.class)\n .web(false)\n .run(args);\n }\n\n @Bean\n public IntegrationFlow fileSplitterFlow() {\n return IntegrationFlows\n .from(Files.inboundAdapter(tmpDir.getRoot())\n .filter(new ChainFileListFilter()\n .addFilter(new AcceptOnceFileListFilter<>())\n .addFilter(new ExpressionFileListFilter<>(\n new FunctionExpression(f -> "foo.tmp".equals(f.getName()))))))\n .split(Files.splitter()\n .markers()\n .charset(StandardCharsets.US_ASCII)\n .firstLineAsHeader("fileHeader")\n .applySequence(true))\n .channel(c -> c.queue("fileSplittingResultChannel"))\n .get();\n }\n\n}\n')])])]),r("p",[e._v("Kotlin DSL")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@Bean\nfun fileSplitterFlow() =\n integrationFlow(\n Files.inboundAdapter(tmpDir.getRoot())\n .filter(\n ChainFileListFilter()\n .addFilter(AcceptOnceFileListFilter())\n .addFilter(ExpressionFileListFilter(FunctionExpression { f: File? -> "foo.tmp" == f!!.name }))\n )\n ) {\n split(\n Files.splitter()\n .markers()\n .charset(StandardCharsets.US_ASCII)\n .firstLineAsHeader("fileHeader")\n .applySequence(true)\n )\n channel { queue("fileSplittingResultChannel") }\n }\n')])])]),r("p",[e._v("Java")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@Splitter(inputChannel="toSplitter")\n@Bean\npublic MessageHandler fileSplitter() {\n FileSplitter splitter = new FileSplitter(true, true);\n splitter.setApplySequence(true);\n splitter.setOutputChannel(outputChannel);\n return splitter;\n}\n')])])]),r("p",[e._v("XML")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v(' (14)\n')])])]),r("table",[r("thead",[r("tr",[r("th",[r("strong",[e._v("1")])]),e._v(" "),r("th",[e._v("Bean 分离器的名称。")])])]),e._v(" "),r("tbody",[r("tr",[r("td",[r("strong",[e._v("2")])]),e._v(" "),r("td",[e._v("设置为"),r("code",[e._v("true")]),e._v("(默认值)以使用迭代器或"),r("code",[e._v("false")]),e._v("在发送行之前将文件加载到内存中。")])]),e._v(" "),r("tr",[r("td",[r("strong",[e._v("3")])]),e._v(" "),r("td",[e._v("设置为"),r("code",[e._v("true")]),e._v("以在文件数据前后发出文件开始和文件结束标记消息。"),r("br"),e._v("标记是具有"),r("code",[e._v("FileSplitter.FileMarker")]),e._v("有效负载的消息(带有"),r("code",[e._v("START")]),e._v(")并且"),r("code",[e._v("END``mark")]),e._v("属性中的值)。"),r("br"),e._v("当在下游流中按顺序处理文件时,你可能会使用标记,其中一些行被过滤。"),r("br"),e._v("它们使下游处理能够知道文件何时已被完全处理。此外,一个"),r("code",[e._v("file_marker")]),e._v("包含"),r("code",[e._v("START")]),e._v("或"),r("code",[e._v("END")]),e._v("的头被添加到这些消息中。"),r("br"),r("code",[e._v("END")]),e._v("标记包括一个行数。"),r("br"),e._v("如果文件为空,只有"),r("code",[e._v("START")]),e._v("和"),r("code",[e._v("END")]),e._v("标记与"),r("code",[e._v("0")]),e._v("一起作为"),r("code",[e._v("lineCount")]),e._v("发出。"),r("br"),e._v("默认为"),r("code",[e._v("false")]),e._v("。"),r("br"),e._v("当"),r("code",[e._v("apply-sequence")]),e._v("时,"),r("code",[e._v("false")]),e._v("默认为"),r("code",[e._v("false")]),e._v("。(下一个属性)。")])]),e._v(" "),r("tr",[r("td",[r("strong",[e._v("4")])]),e._v(" "),r("td",[e._v("当"),r("code",[e._v("markers")]),e._v("为真时,将其设置为"),r("code",[e._v("true")]),e._v(",以便将"),r("code",[e._v("FileMarker")]),e._v("对象转换为 JSON 字符串。"),r("br"),e._v("(下面使用"),r("code",[e._v("SimpleJsonSerializer")]),e._v(")。")])]),e._v(" "),r("tr",[r("td",[r("strong",[e._v("5")])]),e._v(" "),r("td",[e._v("设置为"),r("code",[e._v("false")]),e._v("以禁用消息中的"),r("code",[e._v("sequenceSize")]),e._v("和"),r("code",[e._v("sequenceNumber")]),e._v("标题的包含。"),r("br"),e._v("默认值为"),r("code",[e._v("true")]),e._v(",除非"),r("code",[e._v("markers")]),e._v("是"),r("code",[e._v("true")]),e._v("。"),r("br"),e._v("和"),r("code",[e._v("markers")]),e._v("是"),r("code",[e._v("true")]),e._v(",该标记被包括在测序中。"),r("br"),e._v("当"),r("code",[e._v("true")]),e._v("和"),r("code",[e._v("iterator")]),e._v("是"),r("code",[e._v("true")]),e._v("时,"),r("code",[e._v("sequenceSize")]),e._v("标头被设置为"),r("code",[e._v("0")]),e._v(",因为大小未知。")])]),e._v(" "),r("tr",[r("td",[r("strong",[e._v("6")])]),e._v(" "),r("td",[e._v("设置为"),r("code",[e._v("true")]),e._v(",以便在文件中没有行的情况下引发"),r("code",[e._v("RequiresReplyException")]),e._v("。"),r("br"),e._v("默认值为"),r("code",[e._v("false")]),e._v("。")])]),e._v(" "),r("tr",[r("td",[r("strong",[e._v("7")])]),e._v(" "),r("td",[e._v("设置将文本数据读入"),r("code",[e._v("String")]),e._v("有效负载时使用的字符集名称。"),r("br"),e._v("默认为平台字符集。")])]),e._v(" "),r("tr",[r("td",[r("strong",[e._v("8")])]),e._v(" "),r("td",[e._v("第一行的头名称将作为其余各行发出的消息中的头名称。"),r("br"),e._v("自版本 5.0 起。")])]),e._v(" "),r("tr",[r("td",[r("strong",[e._v("9")])]),e._v(" "),r("td",[e._v("设置用于向拆分器发送消息的输入通道。")])]),e._v(" "),r("tr",[r("td",[r("strong",[e._v("10")])]),e._v(" "),r("td",[e._v("设置发送消息的输出通道。")])]),e._v(" "),r("tr",[r("td",[r("strong",[e._v("11")])]),e._v(" "),r("td",[e._v("设置发送超时。"),r("br"),e._v("仅在"),r("code",[e._v("output-channel")]),e._v("可以阻塞的情况下才适用,例如完整的"),r("code",[e._v("QueueChannel")]),e._v("。")])]),e._v(" "),r("tr",[r("td",[r("strong",[e._v("12")])]),e._v(" "),r("td",[e._v("设置为"),r("code",[e._v("false")]),e._v(",以便在刷新上下文时禁用自动启动拆分器。"),r("br"),e._v("默认值为"),r("code",[e._v("true")]),e._v("。")])]),e._v(" "),r("tr",[r("td",[r("strong",[e._v("13")])]),e._v(" "),r("td",[e._v("如果"),r("code",[e._v("input-channel")]),e._v("是"),r("code",[e._v("")]),e._v(",则设置该端点的顺序。")])]),e._v(" "),r("tr",[r("td",[r("strong",[e._v("14")])]),e._v(" "),r("td",[e._v("设置分割器的启动阶段(当"),r("code",[e._v("auto-startup")]),e._v("为"),r("code",[e._v("true")]),e._v("时使用)。")])])])]),e._v(" "),r("p",[r("code",[e._v("FileSplitter")]),e._v("还将任何基于文本的"),r("code",[e._v("InputStream")]),e._v("拆分成行。从版本 4.3 开始,当与 FTP 或 SFTP 流入站通道适配器或使用"),r("code",[e._v("stream")]),e._v("选项检索文件的 FTP 或 SFTP 出站网关一起使用时,当文件被完全消耗时,分割器会自动关闭支持流的会话。有关这些工具的更多信息,请参见"),r("RouterLink",{attrs:{to:"/spring-integration/ftp.html#ftp-streaming"}},[e._v("FTP 流入站通道适配器")]),e._v("和"),r("RouterLink",{attrs:{to:"/spring-integration/sftp.html#sftp-streaming"}},[e._v("SFTP 流入站通道适配器")]),e._v("以及"),r("RouterLink",{attrs:{to:"/spring-integration/ftp.html#ftp-outbound-gateway"}},[e._v("FTP 出站网关")]),e._v("和"),r("RouterLink",{attrs:{to:"/spring-integration/sftp.html#sftp-outbound-gateway"}},[e._v("SFTP 出站网关")]),e._v("。")],1),e._v(" "),r("p",[e._v("当使用 Java 配置时,可以使用一个额外的构造函数,如下例所示:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v("public FileSplitter(boolean iterator, boolean markers, boolean markersJson)\n")])])]),r("p",[e._v("当"),r("code",[e._v("markersJson")]),e._v("为真时,标记被表示为一个 JSON 字符串(使用"),r("code",[e._v("SimpleJsonSerializer")]),e._v(")。")]),e._v(" "),r("p",[e._v("版本 5.0 引入了"),r("code",[e._v("firstLineAsHeader")]),e._v("选项,以指定第一行内容是一个标题(例如 CSV 文件中的列名)。传递给此属性的参数是头名称,在其余各行发出的消息中,第一行作为头进行。这一行不包含在序列头中(如果"),r("code",[e._v("applySequence")]),e._v("为真),也不包含在与"),r("code",[e._v("lineCount")]),e._v("关联的"),r("code",[e._v("FileMarker.END")]),e._v("中。注意:从版本 5.5 开始,linecount"),r("code",[e._v("is also included as a")]),e._v("fileheaders.line_count"),r("code",[e._v("into headers of the")]),e._v("filemarker.end"),r("code",[e._v("message, since the")]),e._v("filemarker"),r("code",[e._v("could be serialized into JSON. If a file contains only the header line, the file is treated as empty and, therefore, only")]),e._v("filemarker` 实例在拆分过程中被发出(如果启用了标记,则不会发出消息)。默认情况下(如果没有设置头名称),第一行被认为是数据,并成为第一个发出的消息的有效负载。")]),e._v(" "),r("p",[e._v("如果需要从文件内容中提取有关头的更复杂的逻辑(不是第一行,不是整行的内容,也不是一个特定的头,等等),请考虑在"),r("RouterLink",{attrs:{to:"/spring-integration/content-enrichment.html#header-enricher"}},[e._v("页眉 Enricher ")]),e._v("之前使用"),r("code",[e._v("FileSplitter")]),e._v("。请注意,已移动到标题的行可能会从正常的内容处理过程中向下游过滤。")],1),e._v(" "),r("h4",{attrs:{id:"幂等下游处理拆分文件"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#幂等下游处理拆分文件"}},[e._v("#")]),e._v(" 幂等下游处理拆分文件")]),e._v(" "),r("p",[e._v("当"),r("code",[e._v("apply-sequence")]),e._v("为真时,拆分器在"),r("code",[e._v("SEQUENCE_NUMBER")]),e._v("标头中添加行号(当"),r("code",[e._v("markers")]),e._v("为真时,标记被计为行)。行号可以与"),r("RouterLink",{attrs:{to:"/spring-integration/handler-advice.html#idempotent-receiver"}},[e._v("幂等接收机")]),e._v("一起使用,以避免重新启动后重新处理行。")],1),e._v(" "),r("p",[e._v("例如:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@Bean\npublic ConcurrentMetadataStore store() {\n return new ZookeeperMetadataStore();\n}\n\n@Bean\npublic MetadataStoreSelector selector() {\n return new MetadataStoreSelector(\n message -> message.getHeaders().get(FileHeaders.ORIGINAL_FILE, File.class)\n .getAbsolutePath(),\n message -> message.getHeaders().get(IntegrationMessageHeaderAccessor.SEQUENCE_NUMBER)\n .toString(),\n store())\n .compareValues(\n (oldVal, newVal) -> Integer.parseInt(oldVal) < Integer.parseInt(newVal));\n}\n\n@Bean\npublic IdempotentReceiverInterceptor idempotentReceiverInterceptor() {\n return new IdempotentReceiverInterceptor(selector());\n}\n\n@Bean\npublic IntegrationFlow flow() {\n ...\n .split(new FileSplitter())\n ...\n .handle("lineHandler", e -> e.advice(idempotentReceiverInterceptor()))\n ...\n}\n')])])]),r("h3",{attrs:{id:"文件聚合器"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#文件聚合器"}},[e._v("#")]),e._v(" 文件聚合器")]),e._v(" "),r("p",[e._v("从版本 5.5 开始,在启用开始/结束标记时,引入了"),r("code",[e._v("FileAggregator")]),e._v("来覆盖"),r("code",[e._v("FileSplitter")]),e._v("用例的另一面。为了方便起见,"),r("code",[e._v("FileAggregator")]),e._v("实现了所有三种序列细节策略:")]),e._v(" "),r("ul",[r("li",[r("p",[e._v("带有"),r("code",[e._v("FileHeaders.FILENAME")]),e._v("属性的"),r("code",[e._v("HeaderAttributeCorrelationStrategy")]),e._v("用于相关键的计算。当在"),r("code",[e._v("FileSplitter")]),e._v("上启用标记时,它不会填充序列详细信息标题,因为开始/结束标记消息也包含在序列大小中。对于发出的每一行,"),r("code",[e._v("FileHeaders.FILENAME")]),e._v("仍然填充,包括开始/结束标记消息。")])]),e._v(" "),r("li",[r("p",[r("code",[e._v("FileMarkerReleaseStrategy")]),e._v("-检查组中的"),r("code",[e._v("FileSplitter.FileMarker.Mark.END")]),e._v("消息,然后将"),r("code",[e._v("FileHeaders.LINE_COUNT")]),e._v("标头值与组大小减去"),r("code",[e._v("2")]),e._v("-"),r("code",[e._v("FileSplitter.FileMarker")]),e._v("实例进行比较。它还实现了一个方便的"),r("code",[e._v("GroupConditionProvider")]),e._v("用于"),r("code",[e._v("conditionSupplier")]),e._v("函数的"),r("code",[e._v("AbstractCorrelatingMessageHandler")]),e._v("联系人。有关更多信息,请参见"),r("RouterLink",{attrs:{to:"/spring-integration/message-store.html#message-group-condition"}},[e._v("消息组条件")]),e._v("。")],1)]),e._v(" "),r("li",[r("p",[r("code",[e._v("FileAggregatingMessageGroupProcessor")]),e._v("只需从组中删除"),r("code",[e._v("FileSplitter.FileMarker")]),e._v("消息,并将其余消息收集到一个列表有效负载中以生成。")])])]),e._v(" "),r("p",[e._v("下面的清单显示了配置"),r("code",[e._v("FileAggregator")]),e._v("的可能方法:")]),e._v(" "),r("p",[e._v("Java DSL")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@Bean\npublic IntegrationFlow fileSplitterAggregatorFlow(TaskExecutor taskExecutor) {\n return f -> f\n .split(Files.splitter()\n .markers()\n .firstLineAsHeader("firstLine"))\n .channel(c -> c.executor(taskExecutor))\n .filter(payload -> !(payload instanceof FileSplitter.FileMarker),\n e -> e.discardChannel("aggregatorChannel"))\n .transform(String::toUpperCase)\n .channel("aggregatorChannel")\n .aggregate(new FileAggregator())\n .channel(c -> c.queue("resultChannel"));\n}\n')])])]),r("p",[e._v("Kotlin DSL")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@Bean\nfun fileSplitterAggregatorFlow(taskExecutor: TaskExecutor?) =\n integrationFlow {\n split(Files.splitter().markers().firstLineAsHeader("firstLine"))\n channel { executor(taskExecutor) }\n filter({ it !is FileMarker }) { discardChannel("aggregatorChannel") }\n transform(String::toUpperCase)\n channel("aggregatorChannel")\n aggregate(FileAggregator())\n channel { queue("resultChannel") }\n }\n')])])]),r("p",[e._v("Java")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@serviceActivator(inputChannel="toAggregateFile")\n@Bean\npublic AggregatorFactoryBean fileAggregator() {\n AggregatorFactoryBean aggregator = new AggregatorFactoryBean();\n aggregator.setProcessorBean(new FileAggregator());\n aggregator.setOutputChannel(outputChannel);\n return aggregator;\n}\n')])])]),r("p",[e._v("XML")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('\n \n \n \n \n\n')])])]),r("p",[e._v("如果"),r("code",[e._v("FileAggregator")]),e._v("的默认行为不满足目标逻辑,则建议使用单独的策略配置聚合器端点。有关更多信息,请参见"),r("code",[e._v("FileAggregator")]),e._v("Javadocs。")]),e._v(" "),r("h3",{attrs:{id:"远程持久文件列表过滤器"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#远程持久文件列表过滤器"}},[e._v("#")]),e._v(" 远程持久文件列表过滤器")]),e._v(" "),r("p",[e._v("入站和流入站远程文件通道适配器("),r("code",[e._v("FTP")]),e._v(","),r("code",[e._v("SFTP")]),e._v(",以及其他技术)在默认情况下配置有相应的"),r("code",[e._v("AbstractPersistentFileListFilter")]),e._v("实现方式,在内存中配置有"),r("code",[e._v("MetadataStore")]),e._v("。要在集群中运行,可以使用共享的"),r("code",[e._v("MetadataStore")]),e._v("替换这些过滤器(有关更多信息,请参见"),r("RouterLink",{attrs:{to:"/spring-integration/meta-data-store.html#metadata-store"}},[e._v("元数据存储")]),e._v(")。这些过滤器用于防止多次抓取同一个文件(除非修改了时间)。从版本 5.2 开始,在获取文件之前立即将文件添加到筛选器中(如果获取失败,则进行反向操作)。")],1),e._v(" "),r("table",[r("thead",[r("tr",[r("th"),e._v(" "),r("th",[e._v("如果发生灾难性故障(例如断电),当前正在获取的文件可能会保留在过滤器中,并且在重新启动应用程序时不会重新获取。"),r("br"),e._v("在这种情况下,你需要从"),r("code",[e._v("MetadataStore")]),e._v("中手动删除此文件。")])])]),e._v(" "),r("tbody")]),e._v(" "),r("p",[e._v("在以前的版本中,文件在被获取之前会被过滤,这意味着在灾难性的失败之后,有几个文件可能处于这种状态。")]),e._v(" "),r("p",[e._v("为了促进这种新行为,向"),r("code",[e._v("FileListFilter")]),e._v("添加了两个新方法。")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v("boolean accept(F file);\n\nboolean supportsSingleFileFiltering();\n")])])]),r("p",[e._v("如果一个过滤器在"),r("code",[e._v("supportsSingleFileFiltering")]),e._v("中返回"),r("code",[e._v("true")]),e._v(",则它"),r("strong",[e._v("必须")]),e._v("实现"),r("code",[e._v("accept()")]),e._v("。")]),e._v(" "),r("p",[e._v("如果远程过滤器不支持单个文件过滤(例如"),r("code",[e._v("AbstractMarkerFilePresentFileListFilter")]),e._v("),则适配器将恢复到以前的行为。")]),e._v(" "),r("p",[e._v("如果使用多个过滤器(使用"),r("code",[e._v("CompositeFileListFilter")]),e._v("或"),r("code",[e._v("ChainFileListFilter")]),e._v("),则委托过滤器的"),r("strong",[e._v("全部")]),e._v("必须支持单个文件过滤,以便复合过滤器支持它。")]),e._v(" "),r("p",[e._v("持久文件列表过滤器现在有一个布尔属性"),r("code",[e._v("forRecursion")]),e._v("。将此属性设置为"),r("code",[e._v("true")]),e._v(",还将设置"),r("code",[e._v("alwaysAcceptDirectories")]),e._v(",这意味着出站网关上的递归操作("),r("code",[e._v("ls")]),e._v("和"),r("code",[e._v("mget")]),e._v(")现在每次都将遍历完整目录树。这是为了解决未检测到目录树中深层更改的问题。此外,"),r("code",[e._v("forRecursion=true")]),e._v("会导致文件的完整路径被用作元数据存储键;这解决了一个问题,即如果同名文件在不同的目录中多次出现,则过滤器无法正常工作。重要提示:这意味着,对于顶层目录下的文件,将找不到持久性元数据存储中的现有密钥。由于这个原因,默认情况下,该属性是"),r("code",[e._v("false")]),e._v(";这可能会在将来的版本中发生变化。")])])}),[],!1,null,null,null);t.default=i.exports}}]);