(window.webpackJsonp=window.webpackJsonp||[]).push([[495],{937:function(e,t,a){"use strict";a.r(t);var r=a(56),o=Object(r.a)({},(function(){var e=this,t=e.$createElement,a=e._self._c||t;return a("ContentSlotsDistributor",{attrs:{"slot-key":e.$parent.slotKey}},[a("h1",{attrs:{id:"sftp-适配器"}},[a("a",{staticClass:"header-anchor",attrs:{href:"#sftp-适配器"}},[e._v("#")]),e._v(" SFTP 适配器")]),e._v(" "),a("h2",{attrs:{id:"sftp-适配器-2"}},[a("a",{staticClass:"header-anchor",attrs:{href:"#sftp-适配器-2"}},[e._v("#")]),e._v(" SFTP 适配器")]),e._v(" "),a("p",[e._v("Spring 集成为 SFTP 上的文件传输操作提供了支持。")]),e._v(" "),a("p",[e._v("安全文件传输协议(英语:Secure File Transfer Protocol,SFTP)是一种网络协议,允许你通过任何可靠的流在互联网上的两台计算机之间传输文件。")]),e._v(" "),a("p",[e._v("SFTP 协议需要一个安全的通道(如 SSH),并且在 SFTP会话中对客户端身份的可见性。")]),e._v(" "),a("p",[e._v("Spring 集成通过提供三个客户端端点来支持通过 SFTP 发送和接收文件:入站通道适配器、出站通道适配器和出站网关。它还提供了方便的名称空间配置来定义这些客户机组件。")]),e._v(" "),a("p",[e._v("你需要在项目中包含此依赖项:")]),e._v(" "),a("p",[e._v("Maven")]),e._v(" "),a("div",{staticClass:"language- extra-class"},[a("pre",{pre:!0,attrs:{class:"language-text"}},[a("code",[e._v("\n org.springframework.integration\n spring-integration-sftp\n 5.5.9\n\n")])])]),a("p",[e._v("Gradle")]),e._v(" "),a("div",{staticClass:"language- extra-class"},[a("pre",{pre:!0,attrs:{class:"language-text"}},[a("code",[e._v('compile "org.springframework.integration:spring-integration-sftp:5.5.9"\n')])])]),a("p",[e._v("要在 XML 配置中包含 SFTP 命名空间,请在根元素上包含以下属性:")]),e._v(" "),a("div",{staticClass:"language- extra-class"},[a("pre",{pre:!0,attrs:{class:"language-text"}},[a("code",[e._v('xmlns:int-sftp="http://www.springframework.org/schema/integration/sftp"\nxsi:schemaLocation="http://www.springframework.org/schema/integration/sftp\n https://www.springframework.org/schema/integration/sftp/spring-integration-sftp.xsd"\n')])])]),a("h3",{attrs:{id:"sftp会话工厂"}},[a("a",{staticClass:"header-anchor",attrs:{href:"#sftp会话工厂"}},[e._v("#")]),e._v(" sftp会话工厂")]),e._v(" "),a("table",[a("thead",[a("tr",[a("th"),e._v(" "),a("th",[e._v("从版本 3.0 开始,默认情况下会话不再缓存。"),a("br"),e._v("参见"),a("a",{attrs:{href:"#sftp-session-caching"}},[e._v("SFTP Session Caching")]),e._v("。")])])]),e._v(" "),a("tbody")]),e._v(" "),a("p",[e._v("在配置 SFTP 适配器之前,你必须配置一个 SFTP会话工厂。你可以使用常规的 Bean 定义来配置 SFTP会话工厂,如下例所示:")]),e._v(" "),a("div",{staticClass:"language- extra-class"},[a("pre",{pre:!0,attrs:{class:"language-text"}},[a("code",[e._v('\n \n \n \n \n \n\n')])])]),a("p",[e._v("每当适配器从其"),a("code",[e._v("SessionFactory")]),e._v("请求会话对象时,就会创建一个新的 SFTP会话。在这种情况下,SFTP会话工厂依赖于"),a("a",{attrs:{href:"http://www.jcraft.com/jsch",target:"_blank",rel:"noopener noreferrer"}},[e._v("JSch"),a("OutboundLink")],1),e._v("库来提供 SFTP 功能。")]),e._v(" "),a("p",[e._v("然而, Spring 集成还支持对 SFTP 会话的缓存。有关更多信息,请参见"),a("a",{attrs:{href:"#sftp-session-caching"}},[e._v("SFTP Session Caching")]),e._v("。")]),e._v(" "),a("table",[a("thead",[a("tr",[a("th"),e._v(" "),a("th",[e._v("JSCH 支持在与服务器的连接上进行多个通道(操作),"),a("br"),e._v("默认情况下, Spring 集成会话工厂为每个通道使用单独的物理连接,"),a("br"),e._v("自 Spring 集成 3.0 以来,你可以配置会话工厂(使用布尔构造函数 arg-default"),a("code",[e._v("false")]),e._v(")来使用到服务器的单个连接,并在该单个连接上创建多个"),a("code",[e._v("JSch")]),e._v("通道。,"),a("br"),a("br"),e._v("当使用此功能时,你必须将会话工厂包装在缓存会话工厂中,作为"),a("a",{attrs:{href:"#sftp-session-caching"}},[e._v("稍后描述")]),e._v(",以便当一个操作完成时连接不是物理关闭的。"),a("br"),a("br"),e._v("如果缓存被重置,会话仅在关闭最后一个通道时断开连接。"),a("br"),a("br"),e._v("如果在新操作获得会话时发现断开连接,则刷新该连接。")])])]),e._v(" "),a("tbody")]),e._v(" "),a("table",[a("thead",[a("tr",[a("th"),e._v(" "),a("th",[e._v("如果你遇到连接问题,并且希望跟踪会话创建并查看轮询的会话,则可以通过将记录器设置为"),a("code",[e._v("TRACE")]),e._v("级别(例如,"),a("code",[e._v("log4j.category.org.springframework.integration.sftp=TRACE")]),e._v(")来启用跟踪。"),a("br"),e._v("参见"),a("a",{attrs:{href:"#sftp-jsch-logging"}},[e._v("SFTP/JSCH 日志记录")]),e._v("。")])])]),e._v(" "),a("tbody")]),e._v(" "),a("p",[e._v("现在,你需要做的就是将这个 SFTP会话工厂注入到适配器中。")]),e._v(" "),a("table",[a("thead",[a("tr",[a("th"),e._v(" "),a("th",[e._v("为 SFTP会话工厂提供值的一种更实用的方法是使用 Spring 的"),a("a",{attrs:{href:"https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#beans-factory-placeholderconfigurer",target:"_blank",rel:"noopener noreferrer"}},[e._v("属性占位符支持"),a("OutboundLink")],1),e._v("。")])])]),e._v(" "),a("tbody")]),e._v(" "),a("h4",{attrs:{id:"配置属性"}},[a("a",{staticClass:"header-anchor",attrs:{href:"#配置属性"}},[e._v("#")]),e._v(" 配置属性")]),e._v(" "),a("p",[e._v("下面的列表描述了["),a("code",[e._v("DefaultSftpSessionFactory")]),e._v("](https://DOCS. Spring.io/ Spring-integration/api/org/springframework/integration/sftp/会话/defaultsftpsessionfactory.html)所公开的所有属性。")]),e._v(" "),a("p",[a("code",[e._v("isSharedSession")]),e._v("(构造函数参数)::当"),a("code",[e._v("true")]),e._v("时,使用单个连接,并且"),a("code",[e._v("JSch Channels")]),e._v("是多路复用的。它的默认值为"),a("code",[e._v("false")]),e._v("。")]),e._v(" "),a("p",[a("code",[e._v("clientVersion")]),e._v("::允许你设置客户机版本属性。它的默认值取决于底层的 JSCH 版本,但它看起来会是:"),a("em",[e._v("SSH-2.0-JSCH-0.1.45")])]),e._v(" "),a("p",[a("code",[e._v("enableDaemonThread")]),e._v("::如果"),a("code",[e._v("true")]),e._v(",所有线程都是守护进程线程。如果设置为"),a("code",[e._v("false")]),e._v(",则使用普通的非守护进程线程。此属性设置在底层"),a("a",{attrs:{href:"https://epaul.github.io/jsch-documentation/javadoc/com/jcraft/jsch/Session.html",target:"_blank",rel:"noopener noreferrer"}},[e._v("session"),a("OutboundLink")],1),e._v("上。在这里,此属性默认为"),a("code",[e._v("false")]),e._v("。")]),e._v(" "),a("p",[a("code",[e._v("host")]),e._v(":你想要连接的主机的 URL。必须的。")]),e._v(" "),a("p",[a("code",[e._v("hostKeyAlias")]),e._v(":设置主机键的别名,在将主机键与已知的主机列表进行比较时使用该别名。")]),e._v(" "),a("p",[a("code",[e._v("knownHostsResource")]),e._v("::指定用于主密钥存储库的文件资源。该文件的格式与 OpenSSH 的"),a("code",[e._v("known_hosts")]),e._v("文件相同,并且是必需的,如果"),a("code",[e._v("allowUnknownKeys")]),e._v("为 false,则必须预先填充该文件。")]),e._v(" "),a("p",[a("code",[e._v("password")]),e._v(":对远程主机进行身份验证的密码。如果没有提供密码,则需要"),a("code",[e._v("privateKey")]),e._v("属性。如果你设置"),a("code",[e._v("userInfo")]),e._v(",则不允许这样做。密码是从那个对象获得的。")]),e._v(" "),a("p",[a("code",[e._v("port")]),e._v(":应在其上建立 SFTP 连接的端口。如果没有指定,该值默认为"),a("code",[e._v("22")]),e._v("。如果指定,此属性必须是正数。")]),e._v(" "),a("p",[a("code",[e._v("privateKey")]),e._v("::允许你设置一个"),a("a",{attrs:{href:"https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/core/io/Resource.html",target:"_blank",rel:"noopener noreferrer"}},[e._v("resource"),a("OutboundLink")],1),e._v(",该位置表示用于对远程主机进行身份验证的私钥的位置。如果不提供"),a("code",[e._v("privateKey")]),e._v(",则需要"),a("code",[e._v("password")]),e._v("属性。")]),e._v(" "),a("p",[a("code",[e._v("privateKeyPassphrase")]),e._v(":私钥的密码。如果设置"),a("code",[e._v("userInfo")]),e._v(",则不允许设置"),a("code",[e._v("privateKeyPassphrase")]),e._v("。密码是从那个物体上取得的。可选的。")]),e._v(" "),a("p",[a("code",[e._v("proxy")]),e._v("::允许指定基于 JSCH 的"),a("a",{attrs:{href:"https://epaul.github.com/jsch-documentation/javadoc/com/jcraft/jsch/Proxy.html",target:"_blank",rel:"noopener noreferrer"}},[e._v("proxy"),a("OutboundLink")],1),e._v("。如果设置了,代理对象将用于通过代理创建到远程主机的连接。有关配置代理的方便方法,请参见"),a("a",{attrs:{href:"#sftp-proxy-factory-bean"}},[e._v("Proxy Factory Bean")]),e._v("。")]),e._v(" "),a("p",[a("code",[e._v("serverAliveCountMax")]),e._v("::指定服务器激活消息的数量,这些消息在断开连接之前不需要服务器的任何回复就可以发送。如果未设置,此属性默认为"),a("code",[e._v("1")]),e._v("。")]),e._v(" "),a("p",[a("code",[e._v("serverAliveInterval")]),e._v(":设置发送服务器激活消息之前的超时间隔(以毫秒为单位),以防服务器未接收到任何消息。")]),e._v(" "),a("p",[a("code",[e._v("sessionConfig")]),e._v("::通过使用"),a("code",[e._v("Properties")]),e._v(",你可以在底层 JSCH会话上设置额外的配置设置。")]),e._v(" "),a("p",[a("code",[e._v("socketFactory")]),e._v("::让你传入一个["),a("code",[e._v("SocketFactory")]),e._v("](https://epaul.github.com/jsch-documentation/javadoc/com/jcraft/jsch/socketfactory.html)。套接字工厂用于创建到目标主机的套接字。当使用代理时,套接字工厂将传递给代理。默认情况下,使用的是普通的 TCP 套接字。")]),e._v(" "),a("p",[a("code",[e._v("timeout")]),e._v("::超时属性用作套接字超时参数,以及默认的连接超时。默认值为"),a("code",[e._v("0")]),e._v(",这意味着不会发生超时。")]),e._v(" "),a("p",[a("code",[e._v("user")]),e._v(":远程用户使用。必须的。")]),e._v(" "),a("p",[a("code",[e._v("allowUnknownKeys")]),e._v("::设置为"),a("code",[e._v("true")]),e._v(",以允许使用未知(或更改)键连接到主机。它的默认值是“false”。只有在不提供"),a("code",[e._v("userInfo")]),e._v("的情况下,才应用它。如果"),a("code",[e._v("false")]),e._v(",则需要一个预先填充的"),a("code",[e._v("knownHosts")]),e._v("文件。")]),e._v(" "),a("p",[a("code",[e._v("userInfo")]),e._v(":设置一个在身份验证期间使用的自定义"),a("code",[e._v("UserInfo")]),e._v("。特别地,当接收到未知(或更改的)主机键时,将调用"),a("code",[e._v("promptYesNo()")]),e._v("。另见["),a("code",[e._v("allowUnknownKeys")]),e._v("]。当你提供一个"),a("code",[e._v("UserInfo")]),e._v("时,"),a("code",[e._v("password")]),e._v("和私钥"),a("code",[e._v("passphrase")]),e._v("是从它获得的,并且你不能设置离散的"),a("code",[e._v("password")]),e._v("和"),a("code",[e._v("privateKeyPassphrase")]),e._v("属性。")]),e._v(" "),a("h3",{attrs:{id:"代理工厂-bean"}},[a("a",{staticClass:"header-anchor",attrs:{href:"#代理工厂-bean"}},[e._v("#")]),e._v(" 代理工厂 Bean")]),e._v(" "),a("p",[a("code",[e._v("Jsch")]),e._v("提供了一种通过 HTTP 或 SOCKS 代理连接到服务器的机制。要使用此功能,请配置"),a("code",[e._v("Proxy")]),e._v(",并提供对"),a("code",[e._v("DefaultSftpSessionFactory")]),e._v("的引用,如前面讨论的那样。由"),a("code",[e._v("Jsch")]),e._v("提供三种实现方式:"),a("code",[e._v("HTTP")]),e._v("、"),a("code",[e._v("SOCKS4")]),e._v("和"),a("code",[e._v("SOCKS5")]),e._v("。 Spring Integration4.3 引入了"),a("code",[e._v("FactoryBean")]),e._v(",通过允许属性注入来简化这些代理的配置,如下例所示:")]),e._v(" "),a("div",{staticClass:"language- extra-class"},[a("pre",{pre:!0,attrs:{class:"language-text"}},[a("code",[e._v('\n \n \n \n \n \n\n\n\n ...\n \n ...\n\n')])])]),a("h3",{attrs:{id:"委托会话工厂"}},[a("a",{staticClass:"header-anchor",attrs:{href:"#委托会话工厂"}},[e._v("#")]),e._v(" 委托会话工厂")]),e._v(" "),a("p",[e._v("版本 4.2 引入了"),a("code",[e._v("DelegatingSessionFactory")]),e._v(",它允许在运行时选择实际的会话工厂。在调用 SFTP 端点之前,你可以在工厂上调用"),a("code",[e._v("setThreadKey()")]),e._v("将一个键与当前线程关联。然后使用该键查找要使用的实际会话工厂。使用后,你可以通过调用"),a("code",[e._v("clearThreadKey()")]),e._v("清除密钥。")]),e._v(" "),a("p",[e._v("我们添加了方便的方法,这样你就可以更容易地从消息流中实现这一点,如下例所示:")]),e._v(" "),a("div",{staticClass:"language- extra-class"},[a("pre",{pre:!0,attrs:{class:"language-text"}},[a("code",[e._v('\n \n \n \x3c!-- delegate factories here --\x3e\n \n \n\n\n\n\n\n\n\n')])])]),a("table",[a("thead",[a("tr",[a("th"),e._v(" "),a("th",[e._v("当使用会话缓存(参见"),a("a",{attrs:{href:"#sftp-session-caching"}},[e._v("SFTP Session Caching")]),e._v(")时,每个委托都应该被缓存。"),a("br"),e._v("你不能缓存"),a("code",[e._v("DelegatingSessionFactory")]),e._v("本身。")])])]),e._v(" "),a("tbody")]),e._v(" "),a("p",[e._v("从版本 5.0.7 开始,"),a("code",[e._v("DelegatingSessionFactory")]),e._v("可以与"),a("code",[e._v("RotatingServerAdvice")]),e._v("一起用于轮询多个服务器;请参见"),a("a",{attrs:{href:"#sftp-rotating-server-advice"}},[e._v("入站通道适配器:轮询多个服务器和目录")]),e._v("。")]),e._v(" "),a("h3",{attrs:{id:"sftp会话缓存"}},[a("a",{staticClass:"header-anchor",attrs:{href:"#sftp会话缓存"}},[e._v("#")]),e._v(" SFTP会话缓存")]),e._v(" "),a("table",[a("thead",[a("tr",[a("th"),e._v(" "),a("th",[e._v("从 Spring Integration Version3.0 开始,默认情况下不再缓存会话。"),a("br"),e._v("端点不再支持"),a("code",[e._v("cache-sessions")]),e._v("属性。"),a("br"),e._v("如果你希望缓存会话,则必须使用"),a("code",[e._v("CachingSessionFactory")]),e._v("(参见下一个示例)。")])])]),e._v(" "),a("tbody")]),e._v(" "),a("p",[e._v("在 3.0 之前的版本中,默认情况下会话是自动缓存的。有一个"),a("code",[e._v("cache-sessions")]),e._v("属性可用于禁用自动缓存,但该解决方案没有提供一种配置其他会话-缓存属性的方法。例如,你无法限制创建的会话的数量。为了支持该需求和其他配置选项,我们添加了"),a("code",[e._v("CachingSessionFactory")]),e._v("。它提供"),a("code",[e._v("sessionCacheSize")]),e._v("和"),a("code",[e._v("sessionWaitTimeout")]),e._v("属性。顾名思义,"),a("code",[e._v("sessionCacheSize")]),e._v("属性控制工厂在其缓存中维护的活动会话的数量(默认情况是无界的)。如果已经达到"),a("code",[e._v("sessionCacheSize")]),e._v("阈值,则试图获取另一个会话块,直到其中一个缓存的会话变得可用,或者直到一个会话的等待时间过期(默认的等待时间是"),a("code",[e._v("Integer.MAX_VALUE")]),e._v(")。"),a("code",[e._v("sessionWaitTimeout")]),e._v("属性允许配置等待时间。")]),e._v(" "),a("p",[e._v("如果你希望你的会话被缓存,那么配置你的默认会话工厂(作为"),a("a",{attrs:{href:"#sftp-session-factory"}},[e._v("前面描述的")]),e._v("),然后将其包装到"),a("code",[e._v("CachingSessionFactory")]),e._v("的实例中,你可以在其中提供这些附加属性。下面的示例展示了如何做到这一点:")]),e._v(" "),a("div",{staticClass:"language- extra-class"},[a("pre",{pre:!0,attrs:{class:"language-text"}},[a("code",[e._v('\n \n\n\n\n \n \n \n\n')])])]),a("p",[e._v("前面的示例创建一个"),a("code",[e._v("CachingSessionFactory")]),e._v(",其"),a("code",[e._v("sessionCacheSize")]),e._v("设置为"),a("code",[e._v("10")]),e._v(",其"),a("code",[e._v("sessionWaitTimeout")]),e._v("设置为一秒(1000 毫秒)。")]),e._v(" "),a("p",[e._v("从 Spring 集成版本 3.0 开始,"),a("code",[e._v("CachingConnectionFactory")]),e._v("提供了一个"),a("code",[e._v("resetCache()")]),e._v("方法。当调用时,所有空闲会话都会立即关闭,而当正在使用的会话被返回到缓存时,它们会被关闭。当使用"),a("code",[e._v("isSharedSession=true")]),e._v("时,只有当最后一个通道关闭时,通道才关闭,共享会话才关闭。新的届会请求在必要时设立新的届会。")]),e._v(" "),a("p",[e._v("从版本 5.1 开始,"),a("code",[e._v("CachingSessionFactory")]),e._v("有一个新的属性"),a("code",[e._v("testSession")]),e._v("。当为真时,会话将通过执行"),a("code",[e._v("stat(getHome())")]),e._v("命令来进行测试,以确保它仍然处于活动状态;如果不是,则将从缓存中删除它;如果缓存中没有活动会话,则将创建一个新的会话。")]),e._v(" "),a("h3",{attrs:{id:"使用remotefiletemplate"}},[a("a",{staticClass:"header-anchor",attrs:{href:"#使用remotefiletemplate"}},[e._v("#")]),e._v(" 使用"),a("code",[e._v("RemoteFileTemplate")])]),e._v(" "),a("p",[e._v("Spring 集成版本 3.0 在"),a("code",[e._v("SftpSession")]),e._v("对象上提供了一个新的抽象。该模板提供了发送、检索(作为"),a("code",[e._v("InputStream")]),e._v(")、删除和重命名文件的方法。此外,我们提供了一个"),a("code",[e._v("execute")]),e._v("方法来让调用方在会话上运行多个操作。在所有情况下,模板都会可靠地关闭会话。有关更多信息,请参见[Javadoc for"),a("code",[e._v("RemoteFileTemplate")]),e._v("](https://DOCS. Spring.io/ Spring-integration/api/org/springframework/integration/file/file/remotefiletemplate.html)SFTP 有一个子类:["),a("code",[e._v("SftpRemoteFileTemplate")]),e._v("](https://DOCS. Spring.template/ Spring-integration/api/org/springframework/integration/sftepramework/sftefilefilemplate/sp/会话/sf")]),e._v(" "),a("p",[e._v("我们在版本 4.1 中添加了其他方法,包括"),a("code",[e._v("getClientInstance()")]),e._v("。它提供了对底层"),a("code",[e._v("ChannelSftp")]),e._v("的访问,从而能够访问底层 API。")]),e._v(" "),a("p",[e._v("5.0 版引入了"),a("code",[e._v("RemoteFileOperations.invoke(OperationsCallback action)")]),e._v("方法。这个方法允许在同一个线程有界的"),a("code",[e._v("Session")]),e._v("的范围内调用几个"),a("code",[e._v("RemoteFileOperations")]),e._v("调用。当你需要将"),a("code",[e._v("RemoteFileTemplate")]),e._v("作为一个工作单元执行多个高级操作时,这是非常有用的。例如,"),a("code",[e._v("AbstractRemoteFileOutboundGateway")]),e._v("将其与"),a("code",[e._v("mput")]),e._v("命令实现一起使用,其中我们对提供的目录中的每个文件执行"),a("code",[e._v("put")]),e._v("操作,并递归地对其子目录执行该操作。有关更多信息,请参见"),a("a",{attrs:{href:"https://docs.spring.io/spring-integration/api/org/springframework/integration/file/remote/RemoteFileTemplate.html#invoke-org.springframework.integration.file.remote.OperationsCallback-",target:"_blank",rel:"noopener noreferrer"}},[e._v("Javadoc"),a("OutboundLink")],1),e._v("。")]),e._v(" "),a("h3",{attrs:{id:"sftp-入站通道适配器"}},[a("a",{staticClass:"header-anchor",attrs:{href:"#sftp-入站通道适配器"}},[e._v("#")]),e._v(" SFTP 入站通道适配器")]),e._v(" "),a("p",[e._v("SFTP 入站通道适配器是一种特殊的侦听器,它连接到服务器并侦听远程目录事件(例如正在创建的新文件),此时它将启动文件传输。下面的示例展示了如何配置 SFTP 入站通道适配器:")]),e._v(" "),a("div",{staticClass:"language- extra-class"},[a("pre",{pre:!0,attrs:{class:"language-text"}},[a("code",[e._v('\n \n\n')])])]),a("p",[e._v("前面的配置示例展示了如何为各种属性提供值,包括以下内容:")]),e._v(" "),a("ul",[a("li",[a("p",[a("code",[e._v("local-directory")]),e._v(":文件要传输到的位置")])]),e._v(" "),a("li",[a("p",[a("code",[e._v("remote-directory")]),e._v(":文件将从其中传输的远程源目录")])]),e._v(" "),a("li",[a("p",[a("code",[e._v("session-factory")]),e._v(":对我们之前配置的 Bean 的引用")])])]),e._v(" "),a("p",[e._v("默认情况下,传输的文件带有与原始文件相同的名称。如果要重写此行为,可以设置"),a("code",[e._v("local-filename-generator-expression")]),e._v("属性,该属性允许你提供一个 SPEL 表达式来生成本地文件的名称。与出站网关和适配器(SPEL 求值上下文的根对象是"),a("code",[e._v("Message")]),e._v(")不同,此入站适配器在求值时还没有消息,因为这是它最终以传输的文件作为有效负载生成的消息。因此,SPEL 求值上下文的根对象是远程文件的原始名称(a"),a("code",[e._v("String")]),e._v(")。")]),e._v(" "),a("p",[e._v("入站通道适配器首先将文件检索到本地目录,然后根据 Poller 配置发出每个文件。从版本 5.0 开始,当需要新的文件检索时,可以限制从 SFTP 服务器获取的文件的数量。当目标文件很大时,或者在具有持久文件列表过滤器的集群系统中运行时,这可能是有益的,本文将在后面讨论。为此,请使用"),a("code",[e._v("max-fetch-size")]),e._v("。负值(默认值)意味着没有限制,所有匹配的文件都会被检索到。有关更多信息,请参见"),a("a",{attrs:{href:"#sftp-max-fetch"}},[e._v("入站通道适配器:控制远程文件获取")]),e._v("。从版本 5.0 开始,你还可以通过设置"),a("code",[e._v("scanner")]),e._v("属性,为"),a("code",[e._v("inbound-channel-adapter")]),e._v("提供一个自定义的"),a("code",[e._v("DirectoryScanner")]),e._v("实现。")]),e._v(" "),a("p",[e._v("从 Spring Integration3.0 开始,你可以指定"),a("code",[e._v("preserve-timestamp")]),e._v("属性(默认值是"),a("code",[e._v("false")]),e._v(")。当"),a("code",[e._v("true")]),e._v("时,本地文件的修改时间戳被设置为从服务器检索到的值。否则,它被设置为当前时间。")]),e._v(" "),a("p",[e._v("从版本 4.2 开始,你可以指定"),a("code",[e._v("remote-directory-expression")]),e._v("而不是"),a("code",[e._v("remote-directory")]),e._v(",这使你可以动态地确定每个轮询上的目录——例如,"),a("code",[e._v('remote-directory-expression="@myBean.determineRemoteDir()"')]),e._v("。")]),e._v(" "),a("p",[e._v("有时,基于通过"),a("code",[e._v("filename-pattern")]),e._v("属性指定的简单模式的文件过滤可能还不够。如果是这种情况,可以使用"),a("code",[e._v("filename-regex")]),e._v("属性来指定正则表达式(例如,"),a("code",[e._v('filename-regex=".*\\.test$"')]),e._v(")。如果需要完整的控制,可以使用"),a("code",[e._v("filter")]),e._v("属性提供对"),a("code",[e._v("org.springframework.integration.file.filters.FileListFilter")]),e._v("的自定义实现的引用,这是用于过滤文件列表的策略接口。此筛选器确定要检索哪些远程文件。你还可以使用"),a("code",[e._v("CompositeFileListFilter")]),e._v("将基于模式的过滤器与其他过滤器(例如"),a("code",[e._v("AcceptOnceFileListFilter")]),e._v(",以避免同步先前已获取的文件)结合起来。")]),e._v(" "),a("p",[a("code",[e._v("AcceptOnceFileListFilter")]),e._v("将其状态存储在内存中。如果你希望该状态在系统重新启动后仍然有效,请考虑使用"),a("code",[e._v("SftpPersistentAcceptOnceFileListFilter")]),e._v("代替。这个过滤器将接受的文件名存储在"),a("code",[e._v("MetadataStore")]),e._v("策略的实例中(参见"),a("RouterLink",{attrs:{to:"/spring-integration/meta-data-store.html#metadata-store"}},[e._v("元数据存储")]),e._v(")。此筛选器匹配文件名和远程修改时间。")],1),e._v(" "),a("p",[e._v("从版本 4.0 开始,这个过滤器需要"),a("code",[e._v("ConcurrentMetadataStore")]),e._v("。当与共享数据存储一起使用时(例如"),a("code",[e._v("Redis")]),e._v("与"),a("code",[e._v("RedisMetadataStore")]),e._v("),这使得过滤器键可以在多个应用程序或服务器实例之间共享。")]),e._v(" "),a("p",[e._v("从版本 5.0 开始,对于"),a("code",[e._v("SftpInboundFileSynchronizer")]),e._v(",默认情况下会应用带有内存"),a("code",[e._v("SimpleMetadataStore")]),e._v("的"),a("code",[e._v("SftpPersistentAcceptOnceFileListFilter")]),e._v("。这个筛选器也将应用于 XML 配置中的"),a("code",[e._v("regex")]),e._v("或"),a("code",[e._v("pattern")]),e._v("选项,以及 Java DSL 中的"),a("code",[e._v("SftpInboundChannelAdapterSpec")]),e._v("选项。你可以通过使用"),a("code",[e._v("CompositeFileListFilter")]),e._v("(或"),a("code",[e._v("ChainFileListFilter")]),e._v(")来处理任何其他用例。")]),e._v(" "),a("p",[e._v("上面的讨论涉及在检索文件之前对文件进行过滤。检索完文件后,将对文件系统上的文件应用一个额外的过滤器。默认情况下,这是一个"),a("code",[e._v("AcceptOnceFileListFilter")]),e._v(",正如本节讨论的那样,它在内存中保留状态,并且不考虑文件的修改时间。除非你的应用程序在处理后删除文件,否则在应用程序重新启动后,适配器默认会重新处理磁盘上的文件。")]),e._v(" "),a("p",[e._v("另外,如果将"),a("code",[e._v("filter")]),e._v("配置为使用"),a("code",[e._v("SftpPersistentAcceptOnceFileListFilter")]),e._v("并更改远程文件的时间戳(导致重新获取它),则默认的本地过滤器不允许处理此新文件。")]),e._v(" "),a("p",[e._v("有关此过滤器的详细信息,以及如何使用它,请参见"),a("RouterLink",{attrs:{to:"/spring-integration/file.html#remote-persistent-flf"}},[e._v("远程持久文件列表过滤器")]),e._v("。")],1),e._v(" "),a("p",[e._v("你可以使用"),a("code",[e._v("local-filter")]),e._v("属性来配置本地文件系统过滤器的行为。从版本 4.3.8 开始,默认情况下配置了"),a("code",[e._v("FileSystemPersistentAcceptOnceFileListFilter")]),e._v("。该过滤器将接受的文件名和修改的时间戳存储在"),a("code",[e._v("MetadataStore")]),e._v("策略的实例中(参见"),a("RouterLink",{attrs:{to:"/spring-integration/meta-data-store.html#metadata-store"}},[e._v("元数据存储")]),e._v("),并检测本地文件修改时间的更改。默认的"),a("code",[e._v("MetadataStore")]),e._v("是在内存中存储状态的"),a("code",[e._v("SimpleMetadataStore")]),e._v("。")],1),e._v(" "),a("p",[e._v("从版本 4.1.5 开始,这些过滤器有一个新的属性"),a("code",[e._v("flushOnUpdate")]),e._v(",这会导致它们在每次更新时刷新元数据存储(如果存储实现"),a("code",[e._v("Flushable")]),e._v(")。")]),e._v(" "),a("table",[a("thead",[a("tr",[a("th"),e._v(" "),a("th",[e._v("此外,如果使用分布式"),a("code",[e._v("MetadataStore")]),e._v("(例如"),a("RouterLink",{attrs:{to:"/spring-integration/redis.html#redis-metadata-store"}},[e._v("Redis 元数据存储")]),e._v("或"),a("RouterLink",{attrs:{to:"/spring-integration/gemfire.html#gemfire-metadata-store"}},[e._v("Gemfire 元数据存储")]),e._v("),则可以使用同一个适配器或应用程序的多个实例,并确保只有一个实例处理一个文件。")],1)])]),e._v(" "),a("tbody")]),e._v(" "),a("p",[e._v("实际的本地过滤器是一个"),a("code",[e._v("CompositeFileListFilter")]),e._v(",其中包含提供的过滤器和一个模式过滤器,该过滤器防止正在下载过程中的文件被处理(基于"),a("code",[e._v("temporary-file-suffix")]),e._v(")。文件是用这个后缀下载的(默认值是"),a("code",[e._v(".writing")]),e._v("),当传输完成时,文件被重命名为它们的最终名称,使它们对过滤器“可见”。")]),e._v(" "),a("p",[e._v("有关这些属性的更多详细信息,请参见"),a("a",{attrs:{href:"https://github.com/spring-projects/spring-integration/tree/main/spring-integration-core/src/main/resources/org/springframework/integration/config",target:"_blank",rel:"noopener noreferrer"}},[e._v("schema"),a("OutboundLink")],1),e._v("。")]),e._v(" "),a("p",[e._v("SFTP 入站通道适配器是一个轮询消费者。因此,你必须配置一个 Poller(一个全局默认值或一个局部元素)。一旦文件被传输到本地目录,将生成一条以"),a("code",[e._v("java.io.File")]),e._v("为有效负载类型的消息,并将其发送到由"),a("code",[e._v("channel")]),e._v("属性标识的通道。")]),e._v(" "),a("h4",{attrs:{id:"更多关于文件过滤和大文件的信息"}},[a("a",{staticClass:"header-anchor",attrs:{href:"#更多关于文件过滤和大文件的信息"}},[e._v("#")]),e._v(" 更多关于文件过滤和大文件的信息")]),e._v(" "),a("p",[e._v("有时,刚刚出现在监视(远程)目录中的文件是不完整的。通常,这样的文件是以某种临时扩展名编写的(例如在名为"),a("code",[e._v("something.txt.writing")]),e._v("的文件上使用"),a("code",[e._v(".writing")]),e._v("),然后在编写过程完成后重新命名。在大多数情况下,开发人员只对完整的文件感兴趣,并且只想过滤那些文件。要处理这些场景,可以使用"),a("code",[e._v("filename-pattern")]),e._v("、"),a("code",[e._v("filename-regex")]),e._v("和"),a("code",[e._v("filter")]),e._v("属性提供的过滤支持。如果需要自定义过滤器实现,可以通过设置"),a("code",[e._v("filter")]),e._v("属性在适配器中包含引用。下面的示例展示了如何做到这一点:")]),e._v(" "),a("div",{staticClass:"language- extra-class"},[a("pre",{pre:!0,attrs:{class:"language-text"}},[a("code",[e._v('\n \n\n\n\n')])])]),a("h4",{attrs:{id:"从故障中恢复"}},[a("a",{staticClass:"header-anchor",attrs:{href:"#从故障中恢复"}},[e._v("#")]),e._v(" 从故障中恢复")]),e._v(" "),a("p",[e._v("你应该了解适配器的体系结构。文件同步器获取文件,"),a("code",[e._v("FileReadingMessageSource")]),e._v("为每个同步文件发出一条消息。由于"),a("a",{attrs:{href:"#sftp-inbound"}},[e._v("前面讨论过")]),e._v(",涉及两个过滤器。"),a("code",[e._v("filter")]),e._v("属性(和模式)引用远程文件列表,以避免获取已经被获取的文件。"),a("code",[e._v("FileReadingMessageSource")]),e._v("使用"),a("code",[e._v("local-filter")]),e._v("来确定哪些文件将作为消息发送。")]),e._v(" "),a("p",[e._v("同步器列出远程文件并查看其过滤器。然后文件被转移。如果在文件传输过程中发生 IO 错误,那么已经添加到过滤器中的所有文件都将被删除,以便在下一次投票时有资格重新获取它们。这仅在过滤器实现"),a("code",[e._v("ReversibleFileListFilter")]),e._v("(例如"),a("code",[e._v("AcceptOnceFileListFilter")]),e._v(")时才适用。")]),e._v(" "),a("p",[e._v("如果在同步文件后,处理文件的下游流出现错误,则不会自动回滚过滤器,因此默认情况下不会重新处理失败的文件。")]),e._v(" "),a("p",[e._v("如果你希望在失败后重新处理此类文件,则可以使用类似于以下的配置,以便于从筛选器中删除失败的文件:")]),e._v(" "),a("div",{staticClass:"language- extra-class"},[a("pre",{pre:!0,attrs:{class:"language-text"}},[a("code",[e._v('\n \n \n \n\n\n\n\n\n \n\n\n\n')])])]),a("p",[e._v("前面的配置适用于任何"),a("code",[e._v("ResettableFileListFilter")]),e._v("。")]),e._v(" "),a("p",[e._v("从版本 5.0 开始,入站通道适配器可以根据生成的本地文件名在本地构建子目录。这也可以是一个远程子路径。为了能够根据层次结构支持递归地读取本地目录以进行修改,你现在可以基于"),a("code",[e._v("Files.walk()")]),e._v("算法提供一个内部"),a("code",[e._v("FileReadingMessageSource")]),e._v("和一个新的"),a("code",[e._v("RecursiveDirectoryScanner")]),e._v("。参见["),a("code",[e._v("AbstractInboundFileSynchronizingMessageSource.setScanner()")]),e._v("](https://DOCS. Spring.io/ Spring-integration/api/org/springframework/integration/file/remote/synchronizer/abstractinboundfilesynchronizingmessagesource.html#setscanner-org.springframework.integration.file.directoryscanner)了解更多信息。此外,你现在可以通过使用"),a("code",[e._v("setUseWatchService()")]),e._v("选项将"),a("code",[e._v("AbstractInboundFileSynchronizingMessageSource")]),e._v("切换到基于"),a("code",[e._v("WatchService")]),e._v("的"),a("code",[e._v("DirectoryScanner")]),e._v("。它还被配置为所有"),a("code",[e._v("WatchEventType")]),e._v("实例,以便对本地目录中的任何修改做出反应。前面显示的再处理示例基于"),a("code",[e._v("FileReadingMessageSource.WatchServiceDirectoryScanner")]),e._v("的内置功能,当从本地目录中删除文件("),a("code",[e._v("StandardWatchEventKinds.ENTRY_DELETE")]),e._v(")时,它使用"),a("code",[e._v("ResettableFileListFilter.remove()")]),e._v("。有关更多信息,请参见["),a("code",[e._v("WatchServiceDirectoryScanner")]),e._v("](./file.html#watch-service-directory-scanner)。")]),e._v(" "),a("h4",{attrs:{id:"使用-java-配置进行配置"}},[a("a",{staticClass:"header-anchor",attrs:{href:"#使用-java-配置进行配置"}},[e._v("#")]),e._v(" 使用 Java 配置进行配置")]),e._v(" "),a("p",[e._v("Spring 以下引导应用程序展示了如何使用 Java 配置入站适配器的示例:")]),e._v(" "),a("div",{staticClass:"language- extra-class"},[a("pre",{pre:!0,attrs:{class:"language-text"}},[a("code",[e._v('@SpringBootApplication\npublic class SftpJavaApplication {\n\n public static void main(String[] args) {\n new SpringApplicationBuilder(SftpJavaApplication.class)\n .web(false)\n .run(args);\n }\n\n @Bean\n public SessionFactory sftpSessionFactory() {\n DefaultSftpSessionFactory factory = new DefaultSftpSessionFactory(true);\n factory.setHost("localhost");\n factory.setPort(port);\n factory.setUser("foo");\n factory.setPassword("foo");\n factory.setAllowUnknownKeys(true);\n factory.setTestSession(true);\n return new CachingSessionFactory(factory);\n }\n\n @Bean\n public SftpInboundFileSynchronizer sftpInboundFileSynchronizer() {\n SftpInboundFileSynchronizer fileSynchronizer = new SftpInboundFileSynchronizer(sftpSessionFactory());\n fileSynchronizer.setDeleteRemoteFiles(false);\n fileSynchronizer.setRemoteDirectory("foo");\n fileSynchronizer.setFilter(new SftpSimplePatternFileListFilter("*.xml"));\n return fileSynchronizer;\n }\n\n @Bean\n @InboundChannelAdapter(channel = "sftpChannel", poller = @Poller(fixedDelay = "5000"))\n public MessageSource sftpMessageSource() {\n SftpInboundFileSynchronizingMessageSource source =\n new SftpInboundFileSynchronizingMessageSource(sftpInboundFileSynchronizer());\n source.setLocalDirectory(new File("sftp-inbound"));\n source.setAutoCreateLocalDirectory(true);\n source.setLocalFilter(new AcceptOnceFileListFilter());\n source.setMaxFetchSize(1);\n return source;\n }\n\n @Bean\n @ServiceActivator(inputChannel = "sftpChannel")\n public MessageHandler handler() {\n return new MessageHandler() {\n\n @Override\n public void handleMessage(Message message) throws MessagingException {\n System.out.println(message.getPayload());\n }\n\n };\n }\n\n}\n')])])]),a("h4",{attrs:{id:"使用-java-dsl-进行配置"}},[a("a",{staticClass:"header-anchor",attrs:{href:"#使用-java-dsl-进行配置"}},[e._v("#")]),e._v(" 使用 Java DSL 进行配置")]),e._v(" "),a("p",[e._v("Spring 以下引导应用程序展示了如何使用 Java DSL 配置入站适配器的示例:")]),e._v(" "),a("div",{staticClass:"language- extra-class"},[a("pre",{pre:!0,attrs:{class:"language-text"}},[a("code",[e._v('@SpringBootApplication\npublic class SftpJavaApplication {\n\n public static void main(String[] args) {\n new SpringApplicationBuilder(SftpJavaApplication.class)\n .web(false)\n .run(args);\n }\n\n @Bean\n public IntegrationFlow sftpInboundFlow() {\n return IntegrationFlows\n .from(Sftp.inboundAdapter(this.sftpSessionFactory)\n .preserveTimestamp(true)\n .remoteDirectory("foo")\n .regexFilter(".*\\\\.txt$")\n .localFilenameExpression("#this.toUpperCase() + \'.a\'")\n .localDirectory(new File("sftp-inbound")),\n e -> e.id("sftpInboundAdapter")\n .autoStartup(true)\n .poller(Pollers.fixedDelay(5000)))\n .handle(m -> System.out.println(m.getPayload()))\n .get();\n }\n}\n')])])]),a("h4",{attrs:{id:"处理不完整数据"}},[a("a",{staticClass:"header-anchor",attrs:{href:"#处理不完整数据"}},[e._v("#")]),e._v(" 处理不完整数据")]),e._v(" "),a("p",[e._v("见"),a("RouterLink",{attrs:{to:"/spring-integration/file.html#file-incomplete"}},[e._v("处理不完整的数据")]),e._v("。")],1),e._v(" "),a("p",[a("code",[e._v("SftpSystemMarkerFilePresentFileListFilter")]),e._v("用于过滤远程系统上没有相应标记文件的远程文件。有关配置信息,请参见"),a("a",{attrs:{href:"https://docs.spring.io/spring-integration/api/org/springframework/integration/sftp/filters/SftpSystemMarkerFilePresentFileListFilter.html",target:"_blank",rel:"noopener noreferrer"}},[e._v("Javadoc"),a("OutboundLink")],1),e._v("。")]),e._v(" "),a("h3",{attrs:{id:"sftp-流媒体入站通道适配器"}},[a("a",{staticClass:"header-anchor",attrs:{href:"#sftp-流媒体入站通道适配器"}},[e._v("#")]),e._v(" SFTP 流媒体入站通道适配器")]),e._v(" "),a("p",[e._v("版本 4.3 引入了流入站通道适配器。这个适配器生成的消息的有效负载类型为"),a("code",[e._v("InputStream")]),e._v(",这样你就可以在不写到本地文件系统的情况下获取文件。由于会话保持打开,所以当文件已被消费时,消费应用程序负责关闭会话。会话在"),a("code",[e._v("closeableResource")]),e._v("报头("),a("code",[e._v("IntegrationMessageHeaderAccessor.CLOSEABLE_RESOURCE")]),e._v(")中提供。标准框架组件,例如"),a("code",[e._v("FileSplitter")]),e._v("和"),a("code",[e._v("StreamTransformer")]),e._v(",会自动关闭会话。有关这些组件的更多信息,请参见"),a("RouterLink",{attrs:{to:"/spring-integration/file.html#file-splitter"}},[e._v("文件拆分器")]),e._v("和"),a("RouterLink",{attrs:{to:"/spring-integration/transformer.html#stream-transformer"}},[e._v("流变压器")]),e._v("。下面的示例展示了如何配置 SFTP 流入站通道适配器:")],1),e._v(" "),a("div",{staticClass:"language- extra-class"},[a("pre",{pre:!0,attrs:{class:"language-text"}},[a("code",[e._v('\n \n\n')])])]),a("p",[e._v("你只能使用"),a("code",[e._v("filename-pattern")]),e._v("、"),a("code",[e._v("filename-regex")]),e._v("、"),a("code",[e._v("filter")]),e._v("或"),a("code",[e._v("filter-expression")]),e._v("中的一个。")]),e._v(" "),a("table",[a("thead",[a("tr",[a("th"),e._v(" "),a("th",[e._v("从版本 5.0 开始,默认情况下,"),a("code",[e._v("SftpStreamingMessageSource")]),e._v("适配器通过使用基于内存"),a("code",[e._v("SimpleMetadataStore")]),e._v("的"),a("code",[e._v("SftpPersistentAcceptOnceFileListFilter")]),e._v("来防止远程文件的重复。"),a("br"),e._v("默认情况下,该过滤器还与文件名模式(或正则表达式)一起应用。,如果你需要允许重复,"),a("br"),e._v(",你可以使用"),a("code",[e._v("AcceptAllFileListFilter")]),e._v("。"),a("br"),e._v("你可以通过使用"),a("code",[e._v("CompositeFileListFilter")]),e._v("(或"),a("code",[e._v("ChainFileListFilter")]),e._v(")来处理任何其他用例。"),a("br"),e._v("Java 配置"),a("a",{attrs:{href:"#sftp-streaming-java-config"}},[e._v("稍后显示")]),e._v("显示了一种在处理后删除远程文件的技术,从而避免重复。")])])]),e._v(" "),a("tbody")]),e._v(" "),a("p",[e._v("有关"),a("code",[e._v("SftpPersistentAcceptOnceFileListFilter")]),e._v("及其使用方式的更多信息,请参见"),a("RouterLink",{attrs:{to:"/spring-integration/file.html#remote-persistent-flf"}},[e._v("远程持久文件列表过滤器")]),e._v("。")],1),e._v(" "),a("p",[e._v("你可以使用"),a("code",[e._v("max-fetch-size")]),e._v("属性来限制在需要获取时在每个轮询中获取的文件数量。将其设置为"),a("code",[e._v("1")]),e._v(",并在集群环境中运行时使用持久过滤器。有关更多信息,请参见"),a("a",{attrs:{href:"#sftp-max-fetch"}},[e._v("入站通道适配器:控制远程文件获取")]),e._v("。")]),e._v(" "),a("p",[e._v("适配器将远程目录和文件名分别放入头文件中("),a("code",[e._v("FileHeaders.REMOTE_DIRECTORY")]),e._v("和"),a("code",[e._v("FileHeaders.REMOTE_FILE")]),e._v(")。从版本 5.0 开始,"),a("code",[e._v("FileHeaders.REMOTE_FILE_INFO")]),e._v("头提供了额外的远程文件信息(在 JSON 中)。如果将"),a("code",[e._v("SftpStreamingMessageSource")]),e._v("上的"),a("code",[e._v("fileInfoJson")]),e._v("属性设置为"),a("code",[e._v("false")]),e._v(",则头包含一个"),a("code",[e._v("SftpFileInfo")]),e._v("对象。你可以使用"),a("code",[e._v("SftpFileInfo.getFileInfo()")]),e._v("方法访问底层 JSCH 库提供的"),a("code",[e._v("LsEntry")]),e._v("对象。当你使用 XML 配置时,"),a("code",[e._v("fileInfoJson")]),e._v("属性是不可用的,但是你可以通过将"),a("code",[e._v("SftpStreamingMessageSource")]),e._v("注入到你的一个配置类中来设置它。另见"),a("a",{attrs:{href:"#sftp-remote-file-info"}},[e._v("远程文件信息")]),e._v("。")]),e._v(" "),a("p",[e._v("从版本 5.1 开始,"),a("code",[e._v("comparator")]),e._v("的通用类型是"),a("code",[e._v("LsEntry")]),e._v("。在此之前,它是"),a("code",[e._v("AbstractFileInfo")]),e._v("。这是因为排序现在是在处理的较早阶段执行的,在过滤和应用"),a("code",[e._v("maxFetch")]),e._v("之前。")]),e._v(" "),a("h4",{attrs:{id:"使用-java-配置进行配置-2"}},[a("a",{staticClass:"header-anchor",attrs:{href:"#使用-java-配置进行配置-2"}},[e._v("#")]),e._v(" 使用 Java 配置进行配置")]),e._v(" "),a("p",[e._v("Spring 以下引导应用程序展示了如何使用 Java 配置入站适配器的示例:")]),e._v(" "),a("div",{staticClass:"language- extra-class"},[a("pre",{pre:!0,attrs:{class:"language-text"}},[a("code",[e._v('@SpringBootApplication\npublic class SftpJavaApplication {\n\n public static void main(String[] args) {\n new SpringApplicationBuilder(SftpJavaApplication.class)\n .web(false)\n .run(args);\n }\n\n @Bean\n @InboundChannelAdapter(channel = "stream")\n public MessageSource ftpMessageSource() {\n SftpStreamingMessageSource messageSource = new SftpStreamingMessageSource(template());\n messageSource.setRemoteDirectory("sftpSource/");\n messageSource.setFilter(new AcceptAllFileListFilter<>());\n messageSource.setMaxFetchSize(1);\n return messageSource;\n }\n\n @Bean\n @Transformer(inputChannel = "stream", outputChannel = "data")\n public org.springframework.integration.transformer.Transformer transformer() {\n return new StreamTransformer("UTF-8");\n }\n\n @Bean\n public SftpRemoteFileTemplate template() {\n return new SftpRemoteFileTemplate(sftpSessionFactory());\n }\n\n @ServiceActivator(inputChannel = "data", adviceChain = "after")\n @Bean\n public MessageHandler handle() {\n return System.out::println;\n }\n\n @Bean\n public ExpressionEvaluatingRequestHandlerAdvice after() {\n ExpressionEvaluatingRequestHandlerAdvice advice = new ExpressionEvaluatingRequestHandlerAdvice();\n advice.setOnSuccessExpression(\n "@template.remove(headers[\'file_remoteDirectory\'] + headers[\'file_remoteFile\'])");\n advice.setPropagateEvaluationFailures(true);\n return advice;\n }\n\n}\n')])])]),a("p",[e._v("请注意,在本例中,Transformer 下游的消息处理程序有一个建议,该建议在处理后删除远程文件。")]),e._v(" "),a("h3",{attrs:{id:"入站通道适配器-轮询多个服务器和目录"}},[a("a",{staticClass:"header-anchor",attrs:{href:"#入站通道适配器-轮询多个服务器和目录"}},[e._v("#")]),e._v(" 入站通道适配器:轮询多个服务器和目录")]),e._v(" "),a("p",[e._v("从版本 5.0.7 开始,"),a("code",[e._v("RotatingServerAdvice")]),e._v("是可用的;当配置为 Poller 建议时,入站适配器可以轮询多个服务器和目录。配置建议,并将其正常添加到 Poller 的建议链中。a"),a("code",[e._v("DelegatingSessionFactory")]),e._v("用于选择服务器,有关更多信息,请参见"),a("RouterLink",{attrs:{to:"/spring-integration/ftp.html#ftp-dsf"}},[e._v("Delegating Session Factory")]),e._v("。通知配置由"),a("code",[e._v("RotationPolicy.KeyDirectory")]),e._v("对象列表组成。")],1),e._v(" "),a("p",[e._v("例子")]),e._v(" "),a("div",{staticClass:"language- extra-class"},[a("pre",{pre:!0,attrs:{class:"language-text"}},[a("code",[e._v('@Bean\npublic RotatingServerAdvice advice() {\n List keyDirectories = new ArrayList<>();\n keyDirectories.add(new RotationPolicy.KeyDirectory("one", "foo"));\n keyDirectories.add(new RotationPolicy.KeyDirectory("one", "bar"));\n keyDirectories.add(new RotationPolicy.KeyDirectory("two", "baz"));\n keyDirectories.add(new RotationPolicy.KeyDirectory("two", "qux"));\n keyDirectories.add(new RotationPolicy.KeyDirectory("three", "fiz"));\n keyDirectories.add(new RotationPolicy.KeyDirectory("three", "buz"));\n return new RotatingServerAdvice(delegatingSf(), keyDirectories);\n}\n')])])]),a("p",[e._v("此建议将轮询服务器"),a("code",[e._v("foo")]),e._v("上的目录"),a("code",[e._v("one")]),e._v(",直到没有新文件存在,然后移动到目录"),a("code",[e._v("bar")]),e._v(",然后在服务器"),a("code",[e._v("baz")]),e._v("上的目录"),a("code",[e._v("two")]),e._v(",等等。")]),e._v(" "),a("p",[e._v("可以使用"),a("code",[e._v("公平")]),e._v("构造函数 arg 修改此默认行为:")]),e._v(" "),a("p",[e._v("fair")]),e._v(" "),a("div",{staticClass:"language- extra-class"},[a("pre",{pre:!0,attrs:{class:"language-text"}},[a("code",[e._v("@Bean\npublic RotatingServerAdvice advice() {\n ...\n return new RotatingServerAdvice(delegatingSf(), keyDirectories, true);\n}\n")])])]),a("p",[e._v("在这种情况下,无论上一次投票是否返回了文件,通知都将移动到下一个服务器/目录。")]),e._v(" "),a("p",[e._v("或者,你可以提供自己的"),a("code",[e._v("RotationPolicy")]),e._v(",以便根据需要重新配置消息源:")]),e._v(" "),a("p",[e._v("政策")]),e._v(" "),a("div",{staticClass:"language- extra-class"},[a("pre",{pre:!0,attrs:{class:"language-text"}},[a("code",[e._v("public interface RotationPolicy {\n\n void beforeReceive(MessageSource source);\n\n void afterReceive(boolean messageReceived, MessageSource source);\n\n}\n")])])]),a("p",[e._v("and")]),e._v(" "),a("p",[e._v("习惯")]),e._v(" "),a("div",{staticClass:"language- extra-class"},[a("pre",{pre:!0,attrs:{class:"language-text"}},[a("code",[e._v("@Bean\npublic RotatingServerAdvice advice() {\n return new RotatingServerAdvice(myRotationPolicy());\n}\n")])])]),a("p",[a("code",[e._v("local-filename-generator-expression")]),e._v("属性(同步器上的"),a("code",[e._v("localFilenameGeneratorExpression")]),e._v(")现在可以包含"),a("code",[e._v("#remoteDirectory")]),e._v("变量。这允许将从不同目录检索到的文件下载到本地类似的目录中:")]),e._v(" "),a("div",{staticClass:"language- extra-class"},[a("pre",{pre:!0,attrs:{class:"language-text"}},[a("code",[e._v('@Bean\npublic IntegrationFlow flow() {\n return IntegrationFlows.from(Sftp.inboundAdapter(sf())\n .filter(new SftpPersistentAcceptOnceFileListFilter(new SimpleMetadataStore(), "rotate"))\n .localDirectory(new File(tmpDir))\n .localFilenameExpression("#remoteDirectory + T(java.io.File).separator + #root")\n .remoteDirectory("."),\n e -> e.poller(Pollers.fixedDelay(1).advice(advice())))\n .channel(MessageChannels.queue("files"))\n .get();\n}\n')])])]),a("table",[a("thead",[a("tr",[a("th"),e._v(" "),a("th",[e._v("使用此建议时,不要在 Poller 上配置"),a("code",[e._v("TaskExecutor")]),e._v(";有关更多信息,请参见"),a("RouterLink",{attrs:{to:"/spring-integration/polling-consumer.html#conditional-pollers"}},[e._v("消息源的条件 Poller")]),e._v("。")],1)])]),e._v(" "),a("tbody")]),e._v(" "),a("h3",{attrs:{id:"入站通道适配器-控制远程文件获取"}},[a("a",{staticClass:"header-anchor",attrs:{href:"#入站通道适配器-控制远程文件获取"}},[e._v("#")]),e._v(" 入站通道适配器:控制远程文件获取")]),e._v(" "),a("p",[e._v("在配置入站通道适配器时,应该考虑两个属性。"),a("code",[e._v("max-messages-per-poll")]),e._v(",与所有的 Poller 一样,可以用来限制在每个轮询上发出的消息的数量(如果超过配置的值已经准备好)。"),a("code",[e._v("max-fetch-size")]),e._v("(自版本 5.0 起)可以限制一次从远程服务器检索到的文件的数量。")]),e._v(" "),a("p",[e._v("以下场景假定起始状态是一个空的本地目录:")]),e._v(" "),a("ul",[a("li",[a("p",[a("code",[e._v("max-messages-per-poll=2")]),e._v("和"),a("code",[e._v("max-fetch-size=1")]),e._v(":适配器获取一个文件,发出它,获取下一个文件,并发出它。然后它会一直睡到下一次投票。")])]),e._v(" "),a("li",[a("p",[a("code",[e._v("max-messages-per-poll=2")]),e._v("和"),a("code",[e._v("max-fetch-size=2")]),e._v("):适配器获取这两个文件,然后发射每个文件。")])]),e._v(" "),a("li",[a("p",[a("code",[e._v("max-messages-per-poll=2")]),e._v("和"),a("code",[e._v("max-fetch-size=4")]),e._v(":适配器获取最多 4 个文件(如果可用)并发出前两个文件(如果至少有两个)。接下来的两个文件将在下一次投票时发出。")])]),e._v(" "),a("li",[a("p",[a("code",[e._v("max-messages-per-poll=2")]),e._v("和"),a("code",[e._v("max-fetch-size")]),e._v("未指定:适配器获取所有远程文件并发出前两个文件(如果至少有两个)。后续的文件将在后续的轮询中发出(一次两个)。当所有的文件都用完后,将再次尝试远程获取,以获取任何新的文件。")])])]),e._v(" "),a("table",[a("thead",[a("tr",[a("th"),e._v(" "),a("th",[e._v("当你部署一个应用程序的多个实例时,我们建议设置一个小的"),a("code",[e._v("max-fetch-size")]),e._v(",以避免一个实例“抓取”所有文件并使其他实例无法使用。")])])]),e._v(" "),a("tbody")]),e._v(" "),a("p",[a("code",[e._v("max-fetch-size")]),e._v("的另一种用法是,当你想要停止获取远程文件,但要继续处理已经获取的文件时。在"),a("code",[e._v("MessageSource")]),e._v("上设置"),a("code",[e._v("maxFetchSize")]),e._v("属性(通过编程方式,通过 JMX 或通过"),a("RouterLink",{attrs:{to:"/spring-integration/control-bus.html#control-bus"}},[e._v("控制总线")]),e._v(")可以有效地阻止适配器获取更多文件,但可以让 Poller 继续为以前已获取的文件发送消息。如果属性更改时 poller 处于活动状态,则更改将在下一次投票时生效。")],1),e._v(" "),a("p",[e._v("从版本 5.1 开始,同步器可以提供"),a("code",[e._v("Comparator")]),e._v("。这在限制使用"),a("code",[e._v("maxFetchSize")]),e._v("获取的文件数量时很有用。")]),e._v(" "),a("h3",{attrs:{id:"sftp-出站通道适配器"}},[a("a",{staticClass:"header-anchor",attrs:{href:"#sftp-出站通道适配器"}},[e._v("#")]),e._v(" SFTP 出站通道适配器")]),e._v(" "),a("p",[e._v("SFTP 出站通道适配器是一种特殊的"),a("code",[e._v("MessageHandler")]),e._v(",它连接到远程目录,并为它作为传入"),a("code",[e._v("Message")]),e._v("的有效负载接收的每个文件发起文件传输。它还支持文件的几种表示方式,因此不限于"),a("code",[e._v("File")]),e._v("对象。与 FTP 出站适配器类似,SFTP 出站通道适配器支持以下有效负载:")]),e._v(" "),a("ul",[a("li",[a("p",[a("code",[e._v("java.io.File")]),e._v(":实际的文件对象")])]),e._v(" "),a("li",[a("p",[a("code",[e._v("byte[]")]),e._v(":表示文件内容的字节数组")])]),e._v(" "),a("li",[a("p",[a("code",[e._v("java.lang.String")]),e._v(":表示文件内容的文本")])]),e._v(" "),a("li",[a("p",[a("code",[e._v("java.io.InputStream")]),e._v(":要传输到远程文件的数据流")])]),e._v(" "),a("li",[a("p",[a("code",[e._v("org.springframework.core.io.Resource")]),e._v(":用于将数据传输到远程文件的资源")])])]),e._v(" "),a("p",[e._v("下面的示例展示了如何配置 SFTP 出站通道适配器:")]),e._v(" "),a("div",{staticClass:"language- extra-class"},[a("pre",{pre:!0,attrs:{class:"language-text"}},[a("code",[e._v('\n')])])]),a("p",[e._v("有关这些属性的更多详细信息,请参见"),a("a",{attrs:{href:"https://github.com/spring-projects/spring-integration/tree/main/spring-integration-core/src/main/resources/org/springframework/integration/config",target:"_blank",rel:"noopener noreferrer"}},[e._v("schema"),a("OutboundLink")],1),e._v("。")]),e._v(" "),a("h4",{attrs:{id:"spel-和-sftp-出站适配器"}},[a("a",{staticClass:"header-anchor",attrs:{href:"#spel-和-sftp-出站适配器"}},[e._v("#")]),e._v(" spel 和 SFTP 出站适配器")]),e._v(" "),a("p",[e._v("与 Spring 集成中的许多其他组件一样,在配置 SFTP 出站通道适配器时,可以使用 Spring 表达式语言,方法是指定两个属性:"),a("code",[e._v("remote-directory-expression")]),e._v("和"),a("code",[e._v("remote-filename-generator-expression")]),e._v("("),a("a",{attrs:{href:"#sftp-inbound"}},[e._v("前面描述的")]),e._v(")。表达式求值上下文将消息作为其根对象,它允许你使用表达式,这些表达式可以基于消息中的数据(来自“payload”或“headers”)动态计算文件名或现有目录路径。在前面的示例中,我们使用一个表达式值来定义"),a("code",[e._v("remote-filename-generator-expression")]),e._v("属性,该表达式值根据原始名称计算文件名,同时还附加一个后缀:“-mysuffix”。")]),e._v(" "),a("p",[e._v("从版本 4.1 开始,你可以在传输文件时指定"),a("code",[e._v("mode")]),e._v("。默认情况下,现有文件将被覆盖。模式由"),a("code",[e._v("FileExistsMode")]),e._v("枚举定义,其中包括以下值:")]),e._v(" "),a("ul",[a("li",[a("p",[a("code",[e._v("REPLACE")]),e._v("(默认)")])]),e._v(" "),a("li",[a("p",[a("code",[e._v("REPLACE_IF_MODIFIED")])])]),e._v(" "),a("li",[a("p",[a("code",[e._v("APPEND")])])]),e._v(" "),a("li",[a("p",[a("code",[e._v("APPEND_NO_FLUSH")])])]),e._v(" "),a("li",[a("p",[a("code",[e._v("IGNORE")])])]),e._v(" "),a("li",[a("p",[a("code",[e._v("FAIL")])])])]),e._v(" "),a("p",[e._v("使用"),a("code",[e._v("IGNORE")]),e._v("和"),a("code",[e._v("FAIL")]),e._v("时,文件不会被传输。"),a("code",[e._v("FAIL")]),e._v("会导致抛出异常,而"),a("code",[e._v("IGNORE")]),e._v("会静默地忽略传输(尽管生成了"),a("code",[e._v("DEBUG")]),e._v("日志条目)。")]),e._v(" "),a("p",[e._v("4.3 版本引入了"),a("code",[e._v("chmod")]),e._v("属性,你可以使用该属性在上传后更改远程文件权限。你可以使用常规的 UNIX 八进制格式(例如,"),a("code",[e._v("600")]),e._v("仅允许文件所有者进行读写)。当使用 Java 配置适配器时,可以使用"),a("code",[e._v('setChmodOctal("600")')]),e._v("或"),a("code",[e._v("setChmod(0600)")]),e._v("。")]),e._v(" "),a("h4",{attrs:{id:"避免部分写入的文件"}},[a("a",{staticClass:"header-anchor",attrs:{href:"#避免部分写入的文件"}},[e._v("#")]),e._v(" 避免部分写入的文件")]),e._v(" "),a("p",[e._v("处理文件传输时的常见问题之一是处理部分文件的可能性。文件在实际传输完成之前可能会出现在文件系统中。")]),e._v(" "),a("p",[e._v("Spring 为了处理这个问题,集成 SFTP 适配器使用一种常见的算法,在这种算法中,文件以临时名称进行传输,并在文件完全传输后重新命名。")]),e._v(" "),a("p",[e._v("默认情况下,每个正在传输过程中的文件都会出现在文件系统中,并带有一个附加的后缀,默认情况下,后缀是"),a("code",[e._v(".writing")]),e._v("。可以通过设置"),a("code",[e._v("temporary-file-suffix")]),e._v("属性进行更改。")]),e._v(" "),a("p",[e._v("但是,在某些情况下,你可能不想使用这种技术(例如,如果服务器不允许重命名文件)。对于这样的情况,可以通过将"),a("code",[e._v("use-temporary-file-name")]),e._v("设置为"),a("code",[e._v("false")]),e._v("来禁用此功能(默认值为"),a("code",[e._v("true")]),e._v(")。当此属性"),a("code",[e._v("false")]),e._v("时,文件将以其最终名称写入,而使用该属性的应用程序需要其他一些机制来检测该文件是否已完全上传,然后才能访问它。")]),e._v(" "),a("h4",{attrs:{id:"使用-java-配置进行配置-3"}},[a("a",{staticClass:"header-anchor",attrs:{href:"#使用-java-配置进行配置-3"}},[e._v("#")]),e._v(" 使用 Java 配置进行配置")]),e._v(" "),a("p",[e._v("Spring 以下引导应用程序展示了如何使用 Java 配置出站适配器的示例:")]),e._v(" "),a("div",{staticClass:"language- extra-class"},[a("pre",{pre:!0,attrs:{class:"language-text"}},[a("code",[e._v('@SpringBootApplication\n@IntegrationComponentScan\npublic class SftpJavaApplication {\n\n public static void main(String[] args) {\n ConfigurableApplicationContext context =\n new SpringApplicationBuilder(SftpJavaApplication.class)\n .web(false)\n .run(args);\n MyGateway gateway = context.getBean(MyGateway.class);\n gateway.sendToSftp(new File("/foo/bar.txt"));\n }\n\n @Bean\n public SessionFactory sftpSessionFactory() {\n DefaultSftpSessionFactory factory = new DefaultSftpSessionFactory(true);\n factory.setHost("localhost");\n factory.setPort(port);\n factory.setUser("foo");\n factory.setPassword("foo");\n factory.setAllowUnknownKeys(true);\n factory.setTestSession(true);\n return new CachingSessionFactory(factory);\n }\n\n @Bean\n @ServiceActivator(inputChannel = "toSftpChannel")\n public MessageHandler handler() {\n SftpMessageHandler handler = new SftpMessageHandler(sftpSessionFactory());\n handler.setRemoteDirectoryExpressionString("headers[\'remote-target-dir\']");\n handler.setFileNameGenerator(new FileNameGenerator() {\n\n @Override\n public String generateFileName(Message message) {\n return "handlerContent.test";\n }\n\n });\n return handler;\n }\n\n @MessagingGateway\n public interface MyGateway {\n\n @Gateway(requestChannel = "toSftpChannel")\n void sendToSftp(File file);\n\n }\n}\n')])])]),a("h4",{attrs:{id:"使用-java-dsl-进行配置-2"}},[a("a",{staticClass:"header-anchor",attrs:{href:"#使用-java-dsl-进行配置-2"}},[e._v("#")]),e._v(" 使用 Java DSL 进行配置")]),e._v(" "),a("p",[e._v("Spring 以下引导应用程序展示了如何使用 Java DSL 配置出站适配器的示例:")]),e._v(" "),a("div",{staticClass:"language- extra-class"},[a("pre",{pre:!0,attrs:{class:"language-text"}},[a("code",[e._v('@SpringBootApplication\npublic class SftpJavaApplication {\n\n public static void main(String[] args) {\n new SpringApplicationBuilder(SftpJavaApplication.class)\n .web(false)\n .run(args);\n }\n\n @Bean\n public IntegrationFlow sftpOutboundFlow() {\n return IntegrationFlows.from("toSftpChannel")\n .handle(Sftp.outboundAdapter(this.sftpSessionFactory, FileExistsMode.FAIL)\n .useTemporaryFileName(false)\n .remoteDirectory("/foo")\n ).get();\n }\n\n}\n')])])]),a("h3",{attrs:{id:"sftp-出站网关"}},[a("a",{staticClass:"header-anchor",attrs:{href:"#sftp-出站网关"}},[e._v("#")]),e._v(" SFTP 出站网关")]),e._v(" "),a("p",[e._v("SFTP 出站网关提供了一组有限的命令,允许你与远程 SFTP 服务器进行交互:")]),e._v(" "),a("ul",[a("li",[a("p",[a("code",[e._v("ls")]),e._v("(列表文件)")])]),e._v(" "),a("li",[a("p",[a("code",[e._v("nlst")]),e._v("(列出文件名)")])]),e._v(" "),a("li",[a("p",[a("code",[e._v("get")]),e._v("(检索文件)")])]),e._v(" "),a("li",[a("p",[a("code",[e._v("mget")]),e._v("(检索多个文件)")])]),e._v(" "),a("li",[a("p",[a("code",[e._v("rm")]),e._v("(删除文件)")])]),e._v(" "),a("li",[a("p",[a("code",[e._v("mv")]),e._v("(移动和重命名文件)")])]),e._v(" "),a("li",[a("p",[a("code",[e._v("put")]),e._v("(发送文件)")])]),e._v(" "),a("li",[a("p",[a("code",[e._v("mput")]),e._v("(发送多个文件)")])])]),e._v(" "),a("h4",{attrs:{id:"使用ls命令"}},[a("a",{staticClass:"header-anchor",attrs:{href:"#使用ls命令"}},[e._v("#")]),e._v(" 使用"),a("code",[e._v("ls")]),e._v("命令")]),e._v(" "),a("p",[a("code",[e._v("ls")]),e._v("列出了远程文件并支持以下选项:")]),e._v(" "),a("ul",[a("li",[a("p",[a("code",[e._v("-1")]),e._v(":检索文件名列表。默认值是检索"),a("code",[e._v("FileInfo")]),e._v("对象的列表")])]),e._v(" "),a("li",[a("p",[a("code",[e._v("-a")]),e._v(":包含所有文件(包括以“.”开头的文件)")])]),e._v(" "),a("li",[a("p",[a("code",[e._v("-f")]),e._v(":不对列表进行排序")])]),e._v(" "),a("li",[a("p",[a("code",[e._v("-dirs")]),e._v(":包括目录(默认排除)")])]),e._v(" "),a("li",[a("p",[a("code",[e._v("-links")]),e._v(":包括符号链接(默认排除)")])]),e._v(" "),a("li",[a("p",[a("code",[e._v("-R")]),e._v(":递归列出远程目录")])])]),e._v(" "),a("p",[e._v("此外,文件名过滤是以与"),a("code",[e._v("inbound-channel-adapter")]),e._v("相同的方式提供的。")]),e._v(" "),a("p",[e._v("由"),a("code",[e._v("ls")]),e._v("操作产生的消息有效负载是文件名列表或"),a("code",[e._v("FileInfo")]),e._v("对象列表(取决于你是否使用"),a("code",[e._v("-1")]),e._v("开关)。这些对象提供诸如修改时间、权限等信息。")]),e._v(" "),a("p",[a("code",[e._v("ls")]),e._v("命令所作用的远程目录在"),a("code",[e._v("file_remoteDirectory")]),e._v("报头中提供。")]),e._v(" "),a("p",[e._v("当使用递归选项("),a("code",[e._v("-R")]),e._v(")时,"),a("code",[e._v("fileName")]),e._v("包括任意子目录元素,并表示文件的相对路径(相对于远程目录)。如果使用"),a("code",[e._v("-dirs")]),e._v("选项,那么每个递归目录也将作为列表中的一个元素返回。在这种情况下,我们建议你不要使用"),a("code",[e._v("-1")]),e._v("选项,因为你将无法区分文件和目录,这在使用"),a("code",[e._v("FileInfo")]),e._v("对象时是可以做到的。")]),e._v(" "),a("h4",{attrs:{id:"使用nlst命令"}},[a("a",{staticClass:"header-anchor",attrs:{href:"#使用nlst命令"}},[e._v("#")]),e._v(" 使用"),a("code",[e._v("nlst")]),e._v("命令")]),e._v(" "),a("p",[e._v("版本 5 引入了对"),a("code",[e._v("nlst")]),e._v("命令的支持。")]),e._v(" "),a("p",[a("code",[e._v("nlst")]),e._v("列出远程文件名,并且只支持一个选项:")]),e._v(" "),a("ul",[a("li",[a("code",[e._v("-f")]),e._v(":不要对列表进行排序")])]),e._v(" "),a("p",[e._v("由"),a("code",[e._v("nlst")]),e._v("操作产生的消息有效负载是一个文件名列表。")]),e._v(" "),a("p",[a("code",[e._v("file_remoteDirectory")]),e._v("头保存了执行"),a("code",[e._v("nlst")]),e._v("命令的远程目录。")]),e._v(" "),a("p",[e._v("SFTP 协议不提供列出名称的功能。此命令相当于带有"),a("code",[e._v("-1")]),e._v("选项的"),a("code",[e._v("ls")]),e._v("命令,在此添加此命令是为了方便。")]),e._v(" "),a("h4",{attrs:{id:"使用get命令"}},[a("a",{staticClass:"header-anchor",attrs:{href:"#使用get命令"}},[e._v("#")]),e._v(" 使用"),a("code",[e._v("get")]),e._v("命令")]),e._v(" "),a("p",[a("code",[e._v("get")]),e._v("检索远程文件并支持以下选项:")]),e._v(" "),a("ul",[a("li",[a("p",[a("code",[e._v("-P")]),e._v(":保留远程文件的时间戳。")])]),e._v(" "),a("li",[a("p",[a("code",[e._v("-stream")]),e._v(":以流的形式检索远程文件。")])]),e._v(" "),a("li",[a("p",[a("code",[e._v("-D")]),e._v(":成功传输后删除远程文件。如果忽略传输,则不会删除远程文件,因为"),a("code",[e._v("FileExistsMode")]),e._v("是"),a("code",[e._v("IGNORE")]),e._v(",并且本地文件已经存在。")])])]),e._v(" "),a("p",[a("code",[e._v("file_remoteDirectory")]),e._v("头保存远程目录,"),a("code",[e._v("file_remoteFile")]),e._v("头保存文件名。")]),e._v(" "),a("p",[e._v("由"),a("code",[e._v("get")]),e._v("操作产生的消息有效负载是表示检索到的文件的"),a("code",[e._v("File")]),e._v("对象。如果使用"),a("code",[e._v("-stream")]),e._v("选项,则有效负载是"),a("code",[e._v("InputStream")]),e._v(",而不是"),a("code",[e._v("File")]),e._v("。对于文本文件,一个常见的用例是将此操作与"),a("RouterLink",{attrs:{to:"/spring-integration/file.html#file-splitter"}},[e._v("文件拆分器")]),e._v("或"),a("RouterLink",{attrs:{to:"/spring-integration/transformer.html#stream-transformer"}},[e._v("流变压器")]),e._v("结合起来。当以流的形式使用远程文件时,你要负责在流被使用后关闭"),a("code",[e._v("Session")]),e._v("。为了方便起见,"),a("code",[e._v("Session")]),e._v("在"),a("code",[e._v("closeableResource")]),e._v("头中提供了"),a("code",[e._v("IntegrationMessageHeaderAccessor")]),e._v("提供了方便的方法:")],1),e._v(" "),a("div",{staticClass:"language- extra-class"},[a("pre",{pre:!0,attrs:{class:"language-text"}},[a("code",[e._v("Closeable closeable = new IntegrationMessageHeaderAccessor(message).getCloseableResource();\nif (closeable != null) {\n closeable.close();\n}\n")])])]),a("p",[e._v("框架组件,例如"),a("RouterLink",{attrs:{to:"/spring-integration/file.html#file-splitter"}},[e._v("文件拆分器")]),e._v("和"),a("RouterLink",{attrs:{to:"/spring-integration/transformer.html#stream-transformer"}},[e._v("流变压器")]),e._v(",在传输数据后自动关闭会话。")],1),e._v(" "),a("p",[e._v("下面的示例展示了如何将文件作为流使用:")]),e._v(" "),a("div",{staticClass:"language- extra-class"},[a("pre",{pre:!0,attrs:{class:"language-text"}},[a("code",[e._v('\n\n\n')])])]),a("table",[a("thead",[a("tr",[a("th"),e._v(" "),a("th",[e._v("如果在自定义组件中使用输入流,则必须关闭"),a("code",[e._v("Session")]),e._v("。"),a("br"),e._v("你可以在自定义代码中这样做,也可以将消息的副本路由到"),a("code",[e._v("service-activator")]),e._v("并使用 SPEL,如下例所示:")])])]),e._v(" "),a("tbody")]),e._v(" "),a("div",{staticClass:"language- extra-class"},[a("pre",{pre:!0,attrs:{class:"language-text"}},[a("code",[e._v('\n')])])]),a("h4",{attrs:{id:"使用mget命令"}},[a("a",{staticClass:"header-anchor",attrs:{href:"#使用mget命令"}},[e._v("#")]),e._v(" 使用"),a("code",[e._v("mget")]),e._v("命令")]),e._v(" "),a("p",[a("code",[e._v("mget")]),e._v("基于模式检索多个远程文件,并支持以下选项:")]),e._v(" "),a("ul",[a("li",[a("p",[a("code",[e._v("-P")]),e._v(":保留远程文件的时间戳。")])]),e._v(" "),a("li",[a("p",[a("code",[e._v("-R")]),e._v(":递归地检索整个目录树。")])]),e._v(" "),a("li",[a("p",[a("code",[e._v("-x")]),e._v(":如果没有与模式匹配的文件,则抛出异常(否则,将返回一个空列表)。")])]),e._v(" "),a("li",[a("p",[a("code",[e._v("-D")]),e._v(":成功传输后删除每个远程文件。如果忽略传输,则不会删除远程文件,因为"),a("code",[e._v("FileExistsMode")]),e._v("是"),a("code",[e._v("IGNORE")]),e._v(",并且本地文件已经存在。")])])]),e._v(" "),a("p",[e._v("由"),a("code",[e._v("mget")]),e._v("操作产生的消息有效负载是一个"),a("code",[e._v("List")]),e._v("对象(即"),a("code",[e._v("List")]),e._v("的"),a("code",[e._v("File")]),e._v("对象,每个对象代表一个检索到的文件)。")]),e._v(" "),a("table",[a("thead",[a("tr",[a("th"),e._v(" "),a("th",[e._v("从版本 5.0 开始,如果"),a("code",[e._v("FileExistsMode")]),e._v("是"),a("code",[e._v("IGNORE")]),e._v(",则输出消息的有效负载不再包含由于文件已经存在而未被获取的文件。"),a("br"),e._v("以前,该数组包含所有文件,包括已经存在的文件。")])])]),e._v(" "),a("tbody")]),e._v(" "),a("p",[e._v("你使用的表达式确定远程路径应该产生一个以``"),a("strong",[e._v("结尾的结果,例如"),a("code",[e._v("myfiles/")])]),e._v(" 在"),a("code",[e._v("myfiles")]),e._v("下获取完整的树。")]),e._v(" "),a("p",[e._v("从版本 5.0 开始,你可以使用递归的"),a("code",[e._v("MGET")]),e._v(",并结合"),a("code",[e._v("FileExistsMode.REPLACE_IF_MODIFIED")]),e._v("模式,在本地定期同步整个远程目录树。此模式将本地文件上一次修改的时间戳设置为远程文件的时间戳,而不考虑"),a("code",[e._v("-P")]),e._v("(保留时间戳)选项。")]),e._v(" "),a("table",[a("thead",[a("tr",[a("th"),e._v(" "),a("th",[e._v("Notes for when using recursion ("),a("code",[e._v("-R")]),e._v(")"),a("br"),a("br"),e._v("The pattern is ignored and "),a("code",[e._v("*")]),e._v(" is assumed."),a("br"),e._v("By default, the entire remote tree is retrieved."),a("br"),e._v("However, you can filter files in the tree by providing a "),a("code",[e._v("FileListFilter")]),e._v("."),a("br"),e._v("You can also filter directories in the tree this way."),a("br"),e._v("A "),a("code",[e._v("FileListFilter")]),e._v(" can be provided by reference or by "),a("code",[e._v("filename-pattern")]),e._v(" or "),a("code",[e._v("filename-regex")]),e._v(" attributes."),a("br"),e._v("For example, "),a("code",[e._v('filename-regex="(subDir|.*1.TXT)“')]),e._v(" retrieves all files ending with "),a("code",[e._v("1.TXT")]),e._v(" in the remote directory and the subdirectory "),a("code",[e._v("subdir")]),e._v("."),a("br"),e._v("However, we describe an alternative available after this note."),a("br"),a("br"),e._v("If you filter a subdirectory, no additional traversal of that subdirectory is performed."),a("br"),a("br"),e._v("The "),a("code",[e._v("-dirs")]),e._v(" option is not allowed (the recursive "),a("code",[e._v("mget")]),e._v(" uses the recursive "),a("code",[e._v("ls")]),e._v(" to obtain the directory tree and the directories themselves cannot be included in the list)."),a("br"),a("br"),e._v("Typically, you would use the "),a("code",[e._v("#remoteDirectory")]),e._v(" variable in the "),a("code",[e._v("local-directory-expression")]),e._v(" 使远程目录结构在本地保留。")])])]),e._v(" "),a("tbody")]),e._v(" "),a("p",[e._v("持久文件列表过滤器现在有一个布尔属性"),a("code",[e._v("forRecursion")]),e._v("。将此属性设置为"),a("code",[e._v("true")]),e._v(",还设置"),a("code",[e._v("alwaysAcceptDirectories")]),e._v(",这意味着出站网关上的递归操作("),a("code",[e._v("ls")]),e._v("和"),a("code",[e._v("mget")]),e._v(")现在每次都将遍历完整目录树。这是为了解决未检测到目录树中深层更改的问题。此外,"),a("code",[e._v("forRecursion=true")]),e._v("会导致文件的完整路径被用作元数据存储键;这解决了一个问题,即如果同名文件在不同的目录中多次出现,则过滤器无法正常工作。重要提示:这意味着,对于顶层目录下的文件,将找不到持久性元数据存储中的现有密钥。由于这个原因,默认情况下,该属性是"),a("code",[e._v("false")]),e._v(";这可能会在将来的版本中发生变化。")]),e._v(" "),a("p",[e._v("从版本 5.0 开始,你可以将"),a("code",[e._v("SftpSimplePatternFileListFilter")]),e._v("和"),a("code",[e._v("SftpRegexPatternFileListFilter")]),e._v("配置为始终通过将"),a("code",[e._v("alwaysAcceptDirectorties")]),e._v("设置为"),a("code",[e._v("true")]),e._v("的目录。这样做允许对简单模式进行递归,如下例所示:")]),e._v(" "),a("div",{staticClass:"language- extra-class"},[a("pre",{pre:!0,attrs:{class:"language-text"}},[a("code",[e._v('\n \n \n\n\n\n \n \n\n')])])]),a("p",[e._v("你可以通过使用网关上的"),a("code",[e._v("filter")]),e._v("属性来提供这些过滤器之一。")]),e._v(" "),a("p",[e._v("另请参见[出站网关部分成功("),a("code",[e._v("mget")]),e._v("和"),a("code",[e._v("mput")]),e._v(")]。")]),e._v(" "),a("h4",{attrs:{id:"使用put命令"}},[a("a",{staticClass:"header-anchor",attrs:{href:"#使用put命令"}},[e._v("#")]),e._v(" 使用"),a("code",[e._v("put")]),e._v("命令")]),e._v(" "),a("p",[a("code",[e._v("put")]),e._v("将文件发送到远程服务器。消息的有效负载可以是"),a("code",[e._v("java.io.File")]),e._v("、"),a("code",[e._v("byte[]")]),e._v("或"),a("code",[e._v("String")]),e._v("。使用"),a("code",[e._v("remote-filename-generator")]),e._v("(或表达式)为远程文件命名。其他可用的属性包括"),a("code",[e._v("remote-directory")]),e._v("、"),a("code",[e._v("temporary-remote-directory")]),e._v("以及它们的"),a("code",[e._v("*-expression")]),e._v("等价物:"),a("code",[e._v("use-temporary-file-name")]),e._v("和"),a("code",[e._v("auto-create-directory")]),e._v("。有关更多信息,请参见"),a("a",{attrs:{href:"https://github.com/spring-projects/spring-integration/tree/main/spring-integration-core/src/main/resources/org/springframework/integration/config",target:"_blank",rel:"noopener noreferrer"}},[e._v("模式文档"),a("OutboundLink")],1),e._v("。")]),e._v(" "),a("p",[e._v("由"),a("code",[e._v("put")]),e._v("操作产生的消息有效负载是"),a("code",[e._v("String")]),e._v(",其中包含了传输后服务器上文件的完整路径。")]),e._v(" "),a("p",[e._v("版本 4.3 引入了"),a("code",[e._v("chmod")]),e._v("属性,该属性在上传后更改远程文件权限。你可以使用常规的 UNIX 八进制格式(例如,"),a("code",[e._v("600")]),e._v("仅允许文件所有者进行读写)。当使用 Java 配置适配器时,可以使用"),a("code",[e._v("setChmod(0600)")]),e._v("。")]),e._v(" "),a("h4",{attrs:{id:"使用mput命令"}},[a("a",{staticClass:"header-anchor",attrs:{href:"#使用mput命令"}},[e._v("#")]),e._v(" 使用"),a("code",[e._v("mput")]),e._v("命令")]),e._v(" "),a("p",[a("code",[e._v("mput")]),e._v("向服务器发送多个文件,并支持以下选项:")]),e._v(" "),a("ul",[a("li",[a("code",[e._v("-R")]),e._v(":递归——发送目录和子目录中的所有文件(可能经过筛选)")])]),e._v(" "),a("p",[e._v("消息有效负载必须是表示本地目录的"),a("code",[e._v("java.io.File")]),e._v("(或"),a("code",[e._v("String")]),e._v(")。从版本 5.1 开始,还支持"),a("code",[e._v("File")]),e._v("或"),a("code",[e._v("String")]),e._v("的集合。")]),e._v(" "),a("p",[e._v("支持与["),a("code",[e._v("put")]),e._v("命令](#sftp-put-command)相同的属性。此外,你还可以使用"),a("code",[e._v("mput-pattern")]),e._v("、"),a("code",[e._v("mput-regex")]),e._v("、"),a("code",[e._v("mput-filter")]),e._v("或"),a("code",[e._v("mput-filter-expression")]),e._v("中的一个来过滤本地目录中的文件。只要子目录本身通过筛选器,筛选器就可以使用递归。不通过筛选器的子目录不会被递归。")]),e._v(" "),a("p",[e._v("由"),a("code",[e._v("mput")]),e._v("操作产生的消息有效负载是"),a("code",[e._v("List")]),e._v("对象(即由传输产生的远程文件路径的"),a("code",[e._v("List")]),e._v(")。")]),e._v(" "),a("p",[e._v("另请参见[出站网关部分成功("),a("code",[e._v("mget")]),e._v("和"),a("code",[e._v("mput")]),e._v(")]。")]),e._v(" "),a("p",[e._v("版本 4.3 引入了"),a("code",[e._v("chmod")]),e._v("属性,它允许你在上传后更改远程文件权限。你可以使用常规的 UNIX 八进制格式(例如,"),a("code",[e._v("600")]),e._v("仅允许文件所有者进行读写)。当使用 Java 配置适配器时,可以使用"),a("code",[e._v('setChmodOctal("600")')]),e._v("或"),a("code",[e._v("setChmod(0600)")]),e._v("。")]),e._v(" "),a("h4",{attrs:{id:"使用rm命令"}},[a("a",{staticClass:"header-anchor",attrs:{href:"#使用rm命令"}},[e._v("#")]),e._v(" 使用"),a("code",[e._v("rm")]),e._v("命令")]),e._v(" "),a("p",[a("code",[e._v("rm")]),e._v("命令没有选项。")]),e._v(" "),a("p",[e._v("如果删除操作成功,则得到的消息有效负载为"),a("code",[e._v("Boolean.TRUE")]),e._v("。否则,消息有效负载为"),a("code",[e._v("Boolean.FALSE")]),e._v("。"),a("code",[e._v("file_remoteDirectory")]),e._v("头保存远程目录,"),a("code",[e._v("file_remoteFile")]),e._v("头保存文件名。")]),e._v(" "),a("h4",{attrs:{id:"使用mv命令"}},[a("a",{staticClass:"header-anchor",attrs:{href:"#使用mv命令"}},[e._v("#")]),e._v(" 使用"),a("code",[e._v("mv")]),e._v("命令")]),e._v(" "),a("p",[a("code",[e._v("mv")]),e._v("命令没有选项。")]),e._v(" "),a("p",[a("code",[e._v("expression")]),e._v("属性定义了“from”路径,"),a("code",[e._v("rename-expression")]),e._v("属性定义了“to”路径。默认情况下,"),a("code",[e._v("rename-expression")]),e._v("是"),a("code",[e._v("headers['file_renameTo']")]),e._v("。这个表达式不能计算为空或空"),a("code",[e._v("String")]),e._v("。如有必要,将创建所需的任何远程目录。结果消息的有效负载是"),a("code",[e._v("Boolean.TRUE")]),e._v("。"),a("code",[e._v("file_remoteDirectory")]),e._v("报头保存原始远程目录,"),a("code",[e._v("file_remoteFile")]),e._v("报头保存文件名。"),a("code",[e._v("file_renameTo")]),e._v("头保存新路径。")]),e._v(" "),a("p",[e._v("从版本 5.5.6 开始,"),a("code",[e._v("remoteDirectoryExpression")]),e._v("可以在"),a("code",[e._v("mv")]),e._v("命令中使用,以方便使用。如果“from”文件不是完整的文件路径,则将"),a("code",[e._v("remoteDirectoryExpression")]),e._v("的结果用作远程目录。这同样适用于“to”文件,例如,如果任务只是重命名某个目录中的远程文件。")]),e._v(" "),a("h4",{attrs:{id:"附加命令信息"}},[a("a",{staticClass:"header-anchor",attrs:{href:"#附加命令信息"}},[e._v("#")]),e._v(" 附加命令信息")]),e._v(" "),a("p",[a("code",[e._v("get")]),e._v("和"),a("code",[e._v("mget")]),e._v("命令支持"),a("code",[e._v("local-filename-generator-expression")]),e._v("属性。它定义了一个 SPEL 表达式,在传输过程中生成本地文件的名称。求值上下文的根对象是请求消息。"),a("code",[e._v("remoteFileName")]),e._v("变量也是可用的。它对于"),a("code",[e._v("mget")]),e._v("特别有用(例如:"),a("code",[e._v('local-filename-generator-expression="#remoteFileName.toUpperCase() + headers.foo"')]),e._v(")。")]),e._v(" "),a("p",[a("code",[e._v("get")]),e._v("和"),a("code",[e._v("mget")]),e._v("命令支持"),a("code",[e._v("local-directory-expression")]),e._v("属性。它定义了一个 SPEL 表达式,在传输过程中生成本地目录的名称。求值上下文的根对象是请求消息。"),a("code",[e._v("remoteDirectory")]),e._v("变量也是可用的。它对 MGET 特别有用(例如:"),a("code",[e._v("local-directory-expression=\"'/tmp/local/' + #remoteDirectory.toUpperCase() + headers.myheader\"")]),e._v(")。这个属性与"),a("code",[e._v("local-directory")]),e._v("属性是互斥的。")]),e._v(" "),a("p",[e._v("对于所有命令,网关的“表达式”属性保存命令在其上执行的路径。对于"),a("code",[e._v("mget")]),e._v("命令,表达式可以计算为``"),a("strong",[e._v(",这意味着检索所有文件,"),a("code",[e._v("somedirectory/")])]),e._v(",以及以"),a("code",[e._v("*")]),e._v("结尾的其他值。")]),e._v(" "),a("p",[e._v("下面的示例显示了为"),a("code",[e._v("ls")]),e._v("命令配置的网关:")]),e._v(" "),a("div",{staticClass:"language- extra-class"},[a("pre",{pre:!0,attrs:{class:"language-text"}},[a("code",[e._v('\n')])])]),a("p",[e._v("发送到"),a("code",[e._v("toSplitter")]),e._v("通道的消息的有效负载是"),a("code",[e._v("String")]),e._v("对象的列表,每个对象都包含一个文件的名称。如果省略"),a("code",[e._v('command-options="-1"')]),e._v(",则有效负载将是"),a("code",[e._v("FileInfo")]),e._v("对象的列表。你可以以空格分隔的列表的形式提供选项(例如,"),a("code",[e._v('command-options="-1 -dirs -links"')]),e._v(")。")]),e._v(" "),a("p",[e._v("从版本 4.2 开始,"),a("code",[e._v("GET")]),e._v("、"),a("code",[e._v("MGET")]),e._v("、"),a("code",[e._v("PUT")]),e._v("和"),a("code",[e._v("MPUT")]),e._v("命令支持"),a("code",[e._v("FileExistsMode")]),e._v("属性(使用名称空间支持时"),a("code",[e._v("mode")]),e._v(")。这会影响当本地文件存在("),a("code",[e._v("GET")]),e._v("和"),a("code",[e._v("MGET")]),e._v(")或远程文件存在("),a("code",[e._v("PUT")]),e._v("和"),a("code",[e._v("MPUT")]),e._v(")时的行为。支持的模式有"),a("code",[e._v("REPLACE")]),e._v("、"),a("code",[e._v("APPEND")]),e._v("、"),a("code",[e._v("FAIL")]),e._v("和"),a("code",[e._v("IGNORE")]),e._v("。对于向后兼容性,"),a("code",[e._v("PUT")]),e._v("和"),a("code",[e._v("MPUT")]),e._v("操作的默认模式是"),a("code",[e._v("REPLACE")]),e._v("。对于"),a("code",[e._v("GET")]),e._v("和"),a("code",[e._v("MGET")]),e._v("操作,缺省值是"),a("code",[e._v("FAIL")]),e._v("。")]),e._v(" "),a("h4",{attrs:{id:"使用-java-配置进行配置-4"}},[a("a",{staticClass:"header-anchor",attrs:{href:"#使用-java-配置进行配置-4"}},[e._v("#")]),e._v(" 使用 Java 配置进行配置")]),e._v(" "),a("p",[e._v("Spring 以下引导应用程序展示了如何使用 Java 配置出站网关的示例:")]),e._v(" "),a("div",{staticClass:"language- extra-class"},[a("pre",{pre:!0,attrs:{class:"language-text"}},[a("code",[e._v('@SpringBootApplication\npublic class SftpJavaApplication {\n\n public static void main(String[] args) {\n new SpringApplicationBuilder(SftpJavaApplication.class)\n .web(false)\n .run(args);\n }\n\n @Bean\n @ServiceActivator(inputChannel = "sftpChannel")\n public MessageHandler handler() {\n return new SftpOutboundGateway(ftpSessionFactory(), "ls", "\'my_remote_dir/\'");\n }\n\n}\n')])])]),a("h4",{attrs:{id:"使用-java-dsl-进行配置-3"}},[a("a",{staticClass:"header-anchor",attrs:{href:"#使用-java-dsl-进行配置-3"}},[e._v("#")]),e._v(" 使用 Java DSL 进行配置")]),e._v(" "),a("p",[e._v("Spring 以下引导应用程序展示了如何使用 Java DSL 配置出站网关的示例:")]),e._v(" "),a("div",{staticClass:"language- extra-class"},[a("pre",{pre:!0,attrs:{class:"language-text"}},[a("code",[e._v('@SpringBootApplication\npublic class SftpJavaApplication {\n\n public static void main(String[] args) {\n new SpringApplicationBuilder(SftpJavaApplication.class)\n .web(false)\n .run(args);\n }\n\n @Bean\n public SessionFactory sftpSessionFactory() {\n DefaultSftpSessionFactory sf = new DefaultSftpSessionFactory();\n sf.setHost("localhost");\n sf.setPort(port);\n sf.setUsername("foo");\n sf.setPassword("foo");\n factory.setTestSession(true);\n return new CachingSessionFactory(sf);\n }\n\n @Bean\n public QueueChannelSpec remoteFileOutputChannel() {\n return MessageChannels.queue();\n }\n\n @Bean\n public IntegrationFlow sftpMGetFlow() {\n return IntegrationFlows.from("sftpMgetInputChannel")\n .handle(Sftp.outboundGateway(sftpSessionFactory(),\n AbstractRemoteFileOutboundGateway.Command.MGET, "payload")\n .options(AbstractRemoteFileOutboundGateway.Option.RECURSIVE)\n .regexFileNameFilter("(subSftpSource|.*1.txt)")\n .localDirectoryExpression("\'myDir/\' + #remoteDirectory")\n .localFilenameExpression("#remoteFileName.replaceFirst(\'sftpSource\', \'localTarget\')"))\n .channel("remoteFileOutputChannel")\n .get();\n }\n\n}\n')])])]),a("h4",{attrs:{id:"出站网关部分成功-mget和mput"}},[a("a",{staticClass:"header-anchor",attrs:{href:"#出站网关部分成功-mget和mput"}},[e._v("#")]),e._v(" 出站网关部分成功("),a("code",[e._v("mget")]),e._v("和"),a("code",[e._v("mput")]),e._v(")")]),e._v(" "),a("p",[e._v("在对多个文件执行操作时(通过使用"),a("code",[e._v("mget")]),e._v("和"),a("code",[e._v("mput")]),e._v("),在传输一个或多个文件后的一段时间内可能会发生异常。在这种情况下(从版本 4.2 开始),将抛出一个"),a("code",[e._v("PartialSuccessException")]),e._v("。除了通常的"),a("code",[e._v("MessagingException")]),e._v("属性("),a("code",[e._v("failedMessage")]),e._v("和"),a("code",[e._v("cause")]),e._v(")外,此异常还具有两个附加属性:")]),e._v(" "),a("ul",[a("li",[a("p",[a("code",[e._v("partialResults")]),e._v(":成功的转移结果。")])]),e._v(" "),a("li",[a("p",[a("code",[e._v("derivedInput")]),e._v(":从请求消息中生成的文件列表(例如要传输的本地文件为"),a("code",[e._v("mput")]),e._v(")。")])])]),e._v(" "),a("p",[e._v("这些属性允许你确定哪些文件已成功传输,哪些文件未成功传输。")]),e._v(" "),a("p",[e._v("在递归"),a("code",[e._v("mput")]),e._v("的情况下,"),a("code",[e._v("PartialSuccessException")]),e._v("可能嵌套了"),a("code",[e._v("PartialSuccessException")]),e._v("实例。")]),e._v(" "),a("p",[e._v("考虑以下目录结构:")]),e._v(" "),a("div",{staticClass:"language- extra-class"},[a("pre",{pre:!0,attrs:{class:"language-text"}},[a("code",[e._v("root/\n|- file1.txt\n|- subdir/\n | - file2.txt\n | - file3.txt\n|- zoo.txt\n")])])]),a("p",[e._v("如果异常发生在"),a("code",[e._v("file3.txt")]),e._v("上,则网关抛出的"),a("code",[e._v("PartialSuccessException")]),e._v("具有"),a("code",[e._v("derivedInput")]),e._v("of"),a("code",[e._v("file1.txt")]),e._v(","),a("code",[e._v("subdir")]),e._v(",以及"),a("code",[e._v("zoo.txt")]),e._v("和"),a("code",[e._v("partialResults")]),e._v("of"),a("code",[e._v("file1.txt")]),e._v("。它的"),a("code",[e._v("cause")]),e._v("是另一个"),a("code",[e._v("PartialSuccessException")]),e._v("与"),a("code",[e._v("derivedInput")]),e._v("之"),a("code",[e._v("file2.txt")]),e._v("和"),a("code",[e._v("file3.txt")]),e._v("之"),a("code",[e._v("partialResults")]),e._v("之"),a("code",[e._v("file2.txt")]),e._v('之")]),e._v("("),a("code",[e._v("SftpOutboundGateway")]),e._v(")的"),a("code",[e._v("MessageSessionCallback")]),e._v("实现来执行带有"),a("code",[e._v("requestMessage")]),e._v("上下文的"),a("code",[e._v("Session")]),e._v("上的任何操作。你可以将它用于任何非标准或低级别的 SFTP 操作(或多个),例如允许从集成流定义或功能接口实现注入进行访问。下面的示例使用 lambda:")]),e._v(" "),a("div",{staticClass:"language- extra-class"},[a("pre",{pre:!0,attrs:{class:"language-text"}},[a("code",[e._v('@Bean\n@ServiceActivator(inputChannel = "sftpChannel")\npublic MessageHandler sftpOutboundGateway(SessionFactory sessionFactory) {\n return new SftpOutboundGateway(sessionFactory,\n (session, requestMessage) -> session.list(requestMessage.getPayload()));\n}\n')])])]),a("p",[e._v("另一个示例可能是对要发送或检索的文件数据进行预处理或后处理。")]),e._v(" "),a("p",[e._v("在使用 XML 配置时,"),a("code",[e._v("")]),e._v("提供了一个"),a("code",[e._v("session-callback")]),e._v("属性,该属性允许你指定"),a("code",[e._v("MessageSessionCallback")]),e._v(" Bean 名称。")]),e._v(" "),a("table",[a("thead",[a("tr",[a("th"),e._v(" "),a("th",[a("code",[e._v("session-callback")]),e._v("与"),a("code",[e._v("command")]),e._v("和"),a("code",[e._v("expression")]),e._v("属性是互斥的。"),a("br"),e._v("当使用 Java 进行配置时,"),a("code",[e._v("SftpOutboundGateway")]),e._v("类提供了不同的构造函数。")])])]),e._v(" "),a("tbody")]),e._v(" "),a("h3",{attrs:{id:"apache-mina-sftp-服务器事件"}},[a("a",{staticClass:"header-anchor",attrs:{href:"#apache-mina-sftp-服务器事件"}},[e._v("#")]),e._v(" Apache Mina SFTP 服务器事件")]),e._v(" "),a("p",[e._v("在版本 5.2 中添加的"),a("code",[e._v("ApacheMinaSftpEventListener")]),e._v("监听某些 Apache Mina SFTP 服务器事件,并将其发布为"),a("code",[e._v("ApplicationEvent")]),e._v("s,该事件可以由任何"),a("code",[e._v("ApplicationListener")]),e._v(" Bean、"),a("code",[e._v("@EventListener")]),e._v(" Bean 方法或"),a("RouterLink",{attrs:{to:"/spring-integration/event.html#appevent-inbound"}},[e._v("事件入站通道适配器")]),e._v("方法接收。")],1),e._v(" "),a("p",[e._v("目前支持的活动有:")]),e._v(" "),a("ul",[a("li",[a("p",[a("code",[e._v("SessionOpenedEvent")]),e._v("-打开了一个客户端会话")])]),e._v(" "),a("li",[a("p",[a("code",[e._v("DirectoryCreatedEvent")]),e._v("-创建了一个目录")])]),e._v(" "),a("li",[a("p",[a("code",[e._v("FileWrittenEvent")]),e._v("-一个文件被写入到")])]),e._v(" "),a("li",[a("p",[a("code",[e._v("PathMovedEvent")]),e._v("-重命名了一个文件或目录")])]),e._v(" "),a("li",[a("p",[a("code",[e._v("PathRemovedEvent")]),e._v("-删除了一个文件或目录")])]),e._v(" "),a("li",[a("p",[a("code",[e._v("SessionClosedEvent")]),e._v("-客户端已断开连接")])])]),e._v(" "),a("p",[e._v("每一个都是"),a("code",[e._v("ApacheMinaSftpEvent")]),e._v("的子类;你可以配置一个侦听器来接收所有的事件类型。每个事件的"),a("code",[e._v("source")]),e._v("属性是一个"),a("code",[e._v("ServerSession")]),e._v(",你可以从它获得诸如客户机地址之类的信息;在抽象事件上提供了一个方便的"),a("code",[e._v("getSession()")]),e._v("方法。")]),e._v(" "),a("p",[e._v("要用侦听器(必须是 Spring Bean)配置服务器,只需将其添加到"),a("code",[e._v("SftpSubsystemFactory")]),e._v("中:")]),e._v(" "),a("div",{staticClass:"language- extra-class"},[a("pre",{pre:!0,attrs:{class:"language-text"}},[a("code",[e._v("server = SshServer.setUpDefaultServer();\n...\nSftpSubsystemFactory sftpFactory = new SftpSubsystemFactory();\nsftpFactory.addSftpEventListener(apacheMinaSftpEventListenerBean);\n...\n")])])]),a("p",[e._v("要使用 Spring 集成事件适配器来使用这些事件:")]),e._v(" "),a("div",{staticClass:"language- extra-class"},[a("pre",{pre:!0,attrs:{class:"language-text"}},[a("code",[e._v("@Bean\npublic ApplicationEventListeningMessageProducer eventsAdapter() {\n ApplicationEventListeningMessageProducer producer =\n new ApplicationEventListeningMessageProducer();\n producer.setEventTypes(ApacheMinaSftpEvent.class);\n producer.setOutputChannel(eventChannel());\n return producer;\n}\n")])])]),a("h3",{attrs:{id:"远程文件信息"}},[a("a",{staticClass:"header-anchor",attrs:{href:"#远程文件信息"}},[e._v("#")]),e._v(" 远程文件信息")]),e._v(" "),a("p",[e._v("从版本 5.2 开始,"),a("code",[e._v("SftpStreamingMessageSource")]),e._v("("),a("a",{attrs:{href:"#sftp-streaming"}},[e._v("SFTP 流入站通道适配器")]),e._v(")、"),a("code",[e._v("SftpInboundFileSynchronizingMessageSource")]),e._v("("),a("a",{attrs:{href:"#sftp-inbound"}},[e._v("SFTP 入站通道适配器")]),e._v(")和“read”---命令的"),a("code",[e._v("SftpOutboundGateway")]),e._v("("),a("a",{attrs:{href:"#sftp-outbound-gateway"}},[e._v("SFTP 出站网关")]),e._v(")在消息中提供额外的头,以生成有关远程文件的信息:")]),e._v(" "),a("ul",[a("li",[a("p",[a("code",[e._v("FileHeaders.REMOTE_HOST_PORT")]),e._v("-在文件传输操作期间,远程会话已连接到的 host:port pair;")])]),e._v(" "),a("li",[a("p",[a("code",[e._v("FileHeaders.REMOTE_DIRECTORY")]),e._v("-已执行操作的远程目录;")])]),e._v(" "),a("li",[a("p",[a("code",[e._v("FileHeaders.REMOTE_FILE")]),e._v("-远程文件名;仅适用于单个文件操作。")])])]),e._v(" "),a("p",[e._v("由于"),a("code",[e._v("SftpInboundFileSynchronizingMessageSource")]),e._v("不会针对远程文件生成消息,而是使用本地副本,因此"),a("code",[e._v("AbstractInboundFileSynchronizer")]),e._v("在同步操作期间以 URI 样式("),a("code",[e._v("protocol://host:port/remoteDirectory#remoteFileName")]),e._v(")在"),a("code",[e._v("MetadataStore")]),e._v("(可以在外部配置)中存储有关远程文件的信息。当对本地文件进行轮询时,将通过"),a("code",[e._v("SftpInboundFileSynchronizingMessageSource")]),e._v("检索此元数据。当本地文件被删除时,建议删除其元数据条目。"),a("code",[e._v("AbstractInboundFileSynchronizer")]),e._v("为此提供了一个"),a("code",[e._v("removeRemoteFileMetadata()")]),e._v("回调。此外还有一个"),a("code",[e._v("setMetadataStorePrefix()")]),e._v("要在元数据中使用的键。建议将该前缀与基于"),a("code",[e._v("MetadataStore")]),e._v("的"),a("code",[e._v("FileListFilter")]),e._v("实现中使用的前缀不同,当相同的"),a("code",[e._v("MetadataStore")]),e._v("实例在这些组件之间共享时,为了避免条目重写,因为 filter 和"),a("code",[e._v("AbstractInboundFileSynchronizer")]),e._v("都对元数据项使用相同的本地文件名。")])])}),[],!1,null,null,null);t.default=o.exports}}]);