# FTP/FTPS
# FTP/FTPS 适配器
Spring 集成为使用 FTP 和 FTPS 的文件传输操作提供了支持。
文件传输协议是一种简单的网络协议,它允许你在互联网上的两台计算机之间传输文件。FTPS 代表“FTP over SSL”。
你需要在项目中包含此依赖项:
Maven
<dependency>
<groupId>org.springframework.integration</groupId>
<artifactId>spring-integration-ftp</artifactId>
<version>5.5.9</version>
</dependency>
Gradle
compile "org.springframework.integration:spring-integration-ftp:5.5.9"
当涉及到 FTP 通信时,有两个参与者:客户机和服务器。要使用 FTP 或 FTPS 传输文件,你可以使用一个客户端,该客户端启动到运行 FTP 服务器的远程计算机的连接。在建立连接之后,客户端可以选择发送或接收文件的副本。
Spring 集成通过提供三个客户端端点来支持通过 FTP 或 FTPS 发送和接收文件:入站通道适配器、出站通道适配器和出站网关。它还为定义这些客户机组件提供了方便的基于名称空间的配置选项。
要使用 FTP 名称空间,请将以下内容添加到 XML 文件的头中:
xmlns:int-ftp="http://www.springframework.org/schema/integration/ftp"
xsi:schemaLocation="http://www.springframework.org/schema/integration/ftp
https://www.springframework.org/schema/integration/ftp/spring-integration-ftp.xsd"
# ftp会话工厂
Spring 集成提供了你可以用来创建 FTP(或 FTPS)会话的工厂。
# 默认工厂
从版本 3.0 开始,默认情况下会话不再缓存。 参见FTP Session Caching。 |
---|
在配置 FTP 适配器之前,你必须配置一个 FTP会话工厂。你可以使用常规的 Bean 定义来配置 FTP会话工厂,其中实现类是o.s.i.ftp.session.DefaultFtpSessionFactory
。下面的示例展示了一个基本配置:
<bean id="ftpClientFactory"
class="org.springframework.integration.ftp.session.DefaultFtpSessionFactory">
<property name="host" value="localhost"/>
<property name="port" value="22"/>
<property name="username" value="kermit"/>
<property name="password" value="frog"/>
<property name="clientMode" value="0"/>
<property name="fileType" value="2"/>
<property name="bufferSize" value="100000"/>
</bean>
对于 FTPS 连接,你可以使用o.s.i.ftp.session.DefaultFtpsSessionFactory
代替。
下面的示例展示了一个完整的配置:
<bean id="ftpClientFactory"
class="org.springframework.integration.ftp.session.DefaultFtpsSessionFactory">
<property name="host" value="localhost"/>
<property name="port" value="22"/>
<property name="username" value="oleg"/>
<property name="password" value="password"/>
<property name="clientMode" value="1"/>
<property name="fileType" value="2"/>
<property name="useClientMode" value="true"/>
<property name="cipherSuites" value="a,b.c"/>
<property name="keyManager" ref="keyManager"/>
<property name="protocol" value="SSL"/>
<property name="trustManager" ref="trustManager"/>
<property name="prot" value="P"/>
<property name="needClientAuth" value="true"/>
<property name="authValue" value="oleg"/>
<property name="sessionCreation" value="true"/>
<property name="protocols" value="SSL, TLS"/>
<property name="implicit" value="true"/>
</bean>
如果你遇到连接问题,并且希望跟踪会话创建以及查看轮询的会话,则可以通过将记录器设置为TRACE 级别(例如,log4j.category.org.springframework.integration.file=TRACE )来启用会话跟踪。 |
---|
现在你只需要将这些工厂注入到适配器中即可。适配器使用的协议(FTP 或 FTPS)取决于已注入适配器的会话工厂的类型。
为 FTP 或 FTPS会话工厂提供值的一种更实用的方法是使用 Spring 的属性占位符支持(参见https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#beans-factory-placeholderconfigurer (opens new window))。 |
---|
# 高级配置
DefaultFtpSessionFactory
提供了对底层客户机 API 的抽象,它(自 Spring Integration2.0 以来)是Apache Commons 网 (opens new window)。这将使你免于了解org.apache.commons.net.ftp.FTPClient
的低级配置细节。在会话工厂上公开了几个常见的属性(自版本 4.0 以来,现在包括connectTimeout
、defaultTimeout
和dataTimeout
)。然而,有时需要访问较低级别的FTPClient
配置以实现更高级的配置(例如设置活动模式的端口范围)。为此,AbstractFtpSessionFactory
(所有 FTP会话工厂的基类)以以下清单中所示的两种后处理方法的形式公开钩子:
/**
* Will handle additional initialization after client.connect() method was invoked,
* but before any action on the client has been taken
*/
protected void postProcessClientAfterConnect(T t) throws IOException {
// NOOP
}
/**
* Will handle additional initialization before client.connect() method was invoked.
*/
protected void postProcessClientBeforeConnect(T client) throws IOException {
// NOOP
}
正如你所看到的,这两种方法都没有默认的实现。然而,通过扩展DefaultFtpSessionFactory
,你可以覆盖这些方法,以提供更高级的FTPClient
配置,如下例所示:
public class AdvancedFtpSessionFactory extends DefaultFtpSessionFactory {
protected void postProcessClientBeforeConnect(FTPClient ftpClient) throws IOException {
ftpClient.setActivePortRange(4000, 5000);
}
}
# FTPS 和共享 SSLSession
当通过 SSL 或 TLS 使用 FTP 时,一些服务器需要在控制和数据连接上使用相同的SSLSession
。这是为了防止“窃取”数据连接。有关更多信息,请参见https://scarybeastsecurity.blogspot.cz/2009/02/vsftpd-210-released.html (opens new window)。
目前,Apache FTPSclient 不支持此功能。见NET-408 (opens new window)。
下面的解决方案由堆栈溢出 (opens new window)提供,它在sun.security.ssl.SSLSessionContextImpl
上使用反射,因此它可能无法在其他 JVM 上工作。堆栈溢出答案是在 2015 年提交的, Spring 集成团队最近在 JDK1.8.0_112 上测试了该解决方案。
下面的示例展示了如何创建 FTPS会话:
@Bean
public DefaultFtpsSessionFactory sf() {
DefaultFtpsSessionFactory sf = new DefaultFtpsSessionFactory() {
@Override
protected FTPSClient createClientInstance() {
return new SharedSSLFTPSClient();
}
};
sf.setHost("...");
sf.setPort(21);
sf.setUsername("...");
sf.setPassword("...");
sf.setNeedClientAuth(true);
return sf;
}
private static final class SharedSSLFTPSClient extends FTPSClient {
@Override
protected void _prepareDataSocket_(final Socket socket) throws IOException {
if (socket instanceof SSLSocket) {
// Control socket is SSL
final SSLSession session = ((SSLSocket) _socket_).getSession();
final SSLSessionContext context = session.getSessionContext();
context.setSessionCacheSize(0); // you might want to limit the cache
try {
final Field sessionHostPortCache = context.getClass()
.getDeclaredField("sessionHostPortCache");
sessionHostPortCache.setAccessible(true);
final Object cache = sessionHostPortCache.get(context);
final Method method = cache.getClass().getDeclaredMethod("put", Object.class,
Object.class);
method.setAccessible(true);
String key = String.format("%s:%s", socket.getInetAddress().getHostName(),
String.valueOf(socket.getPort())).toLowerCase(Locale.ROOT);
method.invoke(cache, key, session);
key = String.format("%s:%s", socket.getInetAddress().getHostAddress(),
String.valueOf(socket.getPort())).toLowerCase(Locale.ROOT);
method.invoke(cache, key, session);
}
catch (NoSuchFieldException e) {
// Not running in expected JRE
logger.warn("No field sessionHostPortCache in SSLSessionContext", e);
}
catch (Exception e) {
// Not running in expected JRE
logger.warn(e.getMessage());
}
}
}
}
# 委托会话工厂
版本 4.2 引入了DelegatingSessionFactory
,它允许在运行时选择实际的会话工厂。在调用 FTP 端点之前,在工厂上调用setThreadKey()
将一个键与当前线程关联。然后使用该键查找要使用的实际会话工厂。使用后,你可以通过调用clearThreadKey()
清除密钥。
我们添加了方便的方法,这样你就可以从消息流中轻松地使用委托工厂。
下面的示例展示了如何声明一个委托会话工厂:
<bean id="dsf" class="org.springframework.integration.file.remote.session.DelegatingSessionFactory">
<constructor-arg>
<bean class="o.s.i.file.remote.session.DefaultSessionFactoryLocator">
<!-- delegate factories here -->
</bean>
</constructor-arg>
</bean>
<int:service-activator input-channel="in" output-channel="c1"
expression="@dsf.setThreadKey(#root, headers['factoryToUse'])" />
<int-ftp:outbound-gateway request-channel="c1" reply-channel="c2" ... />
<int:service-activator input-channel="c2" output-channel="out"
expression="@dsf.clearThreadKey(#root)" />
当你使用会话缓存(参见FTP Session Caching)时,每个委托都应该被缓存。 你不能缓存 DelegatingSessionFactory 本身。 |
---|
从版本 5.0.7 开始,DelegatingSessionFactory
可以与RotatingServerAdvice
一起用于轮询多个服务器;请参见入站通道适配器:轮询多个服务器和目录。
# FTP 入站通道适配器
FTP 入站通道适配器是一种特殊的侦听器,它连接到 FTP 服务器并侦听远程目录事件(例如,创建的新文件),此时它将启动文件传输。下面的示例展示了如何配置inbound-channel-adapter
:
<int-ftp:inbound-channel-adapter id="ftpInbound"
channel="ftpChannel"
session-factory="ftpSessionFactory"
auto-create-local-directory="true"
delete-remote-files="true"
filename-pattern="*.txt"
remote-directory="some/remote/path"
remote-file-separator="/"
preserve-timestamp="true"
local-filename-generator-expression="#this.toUpperCase() + '.a'"
scanner="myDirScanner"
local-filter="myFilter"
temporary-file-suffix=".writing"
max-fetch-size="-1"
local-directory=".">
<int:poller fixed-rate="1000"/>
</int-ftp:inbound-channel-adapter>
如前面的配置所示,你可以通过使用inbound-channel-adapter
元素来配置 FTP 入站通道适配器,同时还提供各种属性的值,例如local-directory
,filename-pattern
(这是基于简单的模式匹配,而不是正则表达式),以及对session-factory
的引用。
默认情况下,传输的文件带有与原始文件相同的名称。如果要重写此行为,可以设置local-filename-generator-expression
属性,该属性允许你提供一个 SPEL 表达式来生成本地文件的名称。与出站网关和适配器(SPEL 求值上下文的根对象是Message
)不同,此入站适配器在求值时还没有消息,因为这是它最终以传输的文件作为有效负载生成的消息。因此,SPEL 求值上下文的根对象是远程文件的原始名称(aString
)。
入站通道适配器首先检索本地目录的File
对象,然后根据 Poller 配置发出每个文件。从版本 5.0 开始,你现在可以在需要新的文件检索时限制从 FTP 服务器获取的文件的数量。当目标文件非常大时,或者当你在具有持久性文件列表过滤器的集群系统中运行时,这可能是有益的,将在后面讨论。为此,请使用max-fetch-size
。负值(默认值)表示没有限制,并检索所有匹配的文件。有关更多信息,请参见入站通道适配器:控制远程文件获取。从版本 5.0 开始,你还可以通过设置scanner
属性,为inbound-channel-adapter
提供一个自定义的DirectoryScanner
实现。
从 Spring Integration3.0 开始,你可以指定preserve-timestamp
属性(它的默认值是false
)。当true
时,本地文件的修改时间戳被设置为从服务器检索到的值。否则,它被设置为当前时间。
从版本 4.2 开始,你可以指定remote-directory-expression
而不是remote-directory
,让你动态地确定每个轮询上的目录——例如,remote-directory-expression="@myBean.determineRemoteDir()"
。
从版本 4.3 开始,你可以省略remote-directory
和remote-directory-expression
属性。它们默认为null
。在这种情况下,根据 FTP 协议,客户机工作目录被用作默认的远程目录。
有时,基于filename-pattern
属性指定的简单模式的文件过滤可能还不够。如果是这种情况,可以使用filename-regex
属性来指定正则表达式(例如filename-regex=".*\.test$"
)。此外,如果需要完全控制,则可以使用filter
属性并提供对o.s.i.file.filters.FileListFilter
的任何自定义实现的引用,这是用于过滤文件列表的策略接口。此筛选器确定要检索哪些远程文件。还可以通过使用CompositeFileListFilter
将基于模式的过滤器与其他过滤器(例如AcceptOnceFileListFilter
,以避免同步先前已获取的文件)结合在一起。
AcceptOnceFileListFilter
将其状态存储在内存中。如果你希望该状态在系统重新启动后仍然有效,请考虑使用FtpPersistentAcceptOnceFileListFilter
代替。这个过滤器将接受的文件名存储在MetadataStore
策略的实例中(参见元数据存储)。此筛选器匹配文件名和远程修改时间。
从版本 4.0 开始,这个过滤器需要ConcurrentMetadataStore
。当与共享数据存储一起使用时(例如Redis
与RedisMetadataStore
),它允许跨多个应用程序或服务器实例共享筛选键。
从版本 5.0 开始,在内存中FtpPersistentAcceptOnceFileListFilter
的SimpleMetadataStore
默认应用于FtpInboundFileSynchronizer
。在 XML 配置中,regex
或pattern
选项以及在 Java DSL 中,FtpInboundChannelAdapterSpec
选项也应用了该过滤器。任何其他用例都可以用CompositeFileListFilter
(或ChainFileListFilter
)来管理。
前面的讨论涉及在检索文件之前对文件进行过滤。检索完文件后,将对文件系统上的文件应用一个额外的过滤器。默认情况下,这是一个AcceptOnceFileListFilter
,正如前面讨论的,它在内存中保留状态,并且不考虑文件的修改时间。除非你的应用程序在处理后删除文件,否则在应用程序重新启动后,适配器将在默认情况下重新处理磁盘上的文件。
另外,如果将filter
配置为使用FtpPersistentAcceptOnceFileListFilter
并更改远程文件的时间戳(导致重新获取它),则默认的本地筛选器不允许处理此新文件。
有关此过滤器的详细信息,以及如何使用它,请参见远程持久文件列表过滤器。
你可以使用local-filter
属性来配置本地文件系统过滤器的行为。从版本 4.3.8 开始,默认情况下配置了FileSystemPersistentAcceptOnceFileListFilter
。该过滤器将接受的文件名和修改的时间戳存储在MetadataStore
策略的实例中(参见元数据存储),并检测本地文件修改时间的更改。默认的MetadataStore
是SimpleMetadataStore
,它在内存中存储状态。
从版本 4.1.5 开始,这些过滤器有一个新的属性(flushOnUpdate
),这会导致它们在每次更新时刷新元数据存储(如果存储实现Flushable
)。
此外,如果使用分布式MetadataStore (例如Redis或GemFire),则可以使用同一个适配器或应用程序的多个实例,并确保每个文件只处理一次。 |
---|
实际的本地筛选器是CompositeFileListFilter
,其中包含提供的筛选器和模式筛选器,该筛选器防止正在下载过程中的处理文件(基于temporary-file-suffix
)。文件是用这个后缀下载的(默认值是.writing
),当传输完成时,文件被重命名为它的最终名称,使它对过滤器“可见”。
如果缺省的“/”不适用于你的特定环境,remote-file-separator
属性允许你配置一个文件分隔符来使用。
有关这些属性的更多详细信息,请参见schema (opens new window)。
你还应该理解 FTP 入站通道适配器是一个轮询消费者。因此,你必须配置一个 Poller(通过使用全局默认值或局部子元素)。一旦文件被传输,将生成一条以java.io.File
作为有效负载的消息,并将其发送到由channel
属性标识的通道。
# 更多关于文件过滤和不完整文件的信息
有时,刚刚出现在监视(远程)目录中的文件是不完整的。通常,这样的文件是使用临时扩展名编写的(例如somefile.txt.writing
),然后在编写过程完成后重新命名。在大多数情况下,你只对完整的文件感兴趣,并且只想筛选完整的文件。要处理这些场景,可以使用filename-pattern
、filename-regex
和filter
属性提供的过滤支持。下面的示例使用了自定义过滤器实现:
<int-ftp:inbound-channel-adapter
channel="ftpChannel"
session-factory="ftpSessionFactory"
filter="customFilter"
local-directory="file:/my_transfers">
remote-directory="some/remote/path"
<int:poller fixed-rate="1000"/>
</int-ftp:inbound-channel-adapter>
<bean id="customFilter" class="org.example.CustomFilter"/>
# 入站 FTP 适配器的 Poller 配置注意事项
入站 FTP 适配器的工作包括两个任务:
与远程服务器通信,以便将文件从远程目录传输到本地目录。
对于每个传输的文件,生成一条以该文件为有效负载的消息,并将其发送到由“通道”属性标识的通道。这就是为什么他们被称为“通道适配器”,而不仅仅是“适配器”。这种适配器的主要工作是生成要发送到消息通道的消息。从本质上讲,第二个任务以这样一种方式获得优先权,即如果你的本地目录已经有一个或多个文件,它首先从这些文件生成消息。只有当所有的本地文件都被处理后,它才会启动远程通信来检索更多的文件。
此外,在 Poller 上配置触发器时,你应该密切关注max-messages-per-poll
属性。对于所有SourcePollingChannelAdapter
实例(包括 FTP),它的默认值是1
。这意味着,一旦一个文件被处理,它就会等待由触发器配置决定的下一个执行时间。如果你碰巧在local-directory
中有一个或多个文件,那么它将在启动与远程 FTP 服务器的通信之前处理这些文件。此外,如果max-messages-per-poll
被设置为1
(默认值),则它一次只处理一个文件,其间隔由触发器定义,本质上是“One-poll===one-file”。
对于典型的文件传输用例,你很可能想要相反的行为:为每个投票处理所有可以处理的文件,然后等待下一个投票。如果是这种情况,则将max-messages-per-poll
设置为-1。然后,在每个轮询中,适配器尝试尽可能多地生成消息。换句话说,它处理本地目录中的所有内容,然后连接到远程目录,将那里可用的所有内容转移到本地处理。只有这样,轮询操作才算完成,Poller 才会等待下一次执行时间。
你也可以将“max-messages-per-poll”值设置为正值,该值指示从每个 poll 文件创建的消息的向上限制。例如,值10
意味着,在每个轮询中,它尝试处理的文件不超过 10 个。
# 从故障中恢复
理解适配器的体系结构是很重要的。有一个文件同步器来获取文件,还有一个FileReadingMessageSource
为每个同步文件发出一条消息。正如前面所讨论的,涉及两个过滤器。filter
属性(和模式)引用远程文件列表,以避免获取已经获取的文件。local-filter
由FileReadingMessageSource
用来确定哪些文件将作为消息发送。
同步器列出远程文件并查看其过滤器。然后文件被转移。如果在文件传输过程中发生 IO 错误,那么已经添加到过滤器中的所有文件都将被删除,以便在下一次投票时有资格重新获取它们。这仅在过滤器实现ReversibleFileListFilter
(例如AcceptOnceFileListFilter
)时才适用。
如果在同步文件后,处理文件的下游流出现错误,则不会自动回滚过滤器,因此默认情况下不会重新处理失败的文件。
如果你希望在失败后重新处理此类文件,则可以使用类似于以下的配置,以便于从过滤器中删除失败的文件:
<int-ftp:inbound-channel-adapter id="ftpAdapter"
session-factory="ftpSessionFactory"
channel="requestChannel"
remote-directory-expression="'/ftpSource'"
local-directory="file:myLocalDir"
auto-create-local-directory="true"
filename-pattern="*.txt">
<int:poller fixed-rate="1000">
<int:transactional synchronization-factory="syncFactory" />
</int:poller>
</int-ftp:inbound-channel-adapter>
<bean id="acceptOnceFilter"
class="org.springframework.integration.file.filters.AcceptOnceFileListFilter" />
<int:transaction-synchronization-factory id="syncFactory">
<int:after-rollback expression="payload.delete()" />
</int:transaction-synchronization-factory>
<bean id="transactionManager"
class="org.springframework.integration.transaction.PseudoTransactionManager" />
前面的配置适用于任何ResettableFileListFilter
。
从版本 5.0 开始,入站通道适配器可以在本地构建与生成的本地文件名对应的子目录。这也可以是一个远程子路径。为了能够根据层次结构支持递归地读取本地目录以进行修改,你现在可以使用基于Files.walk()
算法的新的RecursiveDirectoryScanner
提供一个内部FileReadingMessageSource
。参见[AbstractInboundFileSynchronizingMessageSource.setScanner()
](https://DOCS. Spring.io/ Spring-integration/api/org/springframework/integration/file/remote/synchronizer/abstractinboundfilesynchronizingmessagesource.html#setscanner)了解更多信息。此外,你现在可以通过使用setUseWatchService()
选项将AbstractInboundFileSynchronizingMessageSource
切换到基于WatchService
的DirectoryScanner
。它还被配置为所有WatchEventType
实例,以对本地目录中的任何修改做出反应。前面显示的再处理示例基于FileReadingMessageSource.WatchServiceDirectoryScanner
的内置功能,当从本地目录中删除文件(StandardWatchEventKinds.ENTRY_DELETE
)时执行ResettableFileListFilter.remove()
。有关更多信息,请参见[WatchServiceDirectoryScanner
](./file.html#watch-service-directory-scanner)。
# 使用 Java 配置进行配置
Spring 以下引导应用程序展示了如何使用 Java 配置配置入站适配器的示例:
@SpringBootApplication
public class FtpJavaApplication {
public static void main(String[] args) {
new SpringApplicationBuilder(FtpJavaApplication.class)
.web(false)
.run(args);
}
@Bean
public SessionFactory<FTPFile> ftpSessionFactory() {
DefaultFtpSessionFactory sf = new DefaultFtpSessionFactory();
sf.setHost("localhost");
sf.setPort(port);
sf.setUsername("foo");
sf.setPassword("foo");
sf.setTestSession(true);
return new CachingSessionFactory<FTPFile>(sf);
}
@Bean
public FtpInboundFileSynchronizer ftpInboundFileSynchronizer() {
FtpInboundFileSynchronizer fileSynchronizer = new FtpInboundFileSynchronizer(ftpSessionFactory());
fileSynchronizer.setDeleteRemoteFiles(false);
fileSynchronizer.setRemoteDirectory("foo");
fileSynchronizer.setFilter(new FtpSimplePatternFileListFilter("*.xml"));
return fileSynchronizer;
}
@Bean
@InboundChannelAdapter(channel = "ftpChannel", poller = @Poller(fixedDelay = "5000"))
public MessageSource<File> ftpMessageSource() {
FtpInboundFileSynchronizingMessageSource source =
new FtpInboundFileSynchronizingMessageSource(ftpInboundFileSynchronizer());
source.setLocalDirectory(new File("ftp-inbound"));
source.setAutoCreateLocalDirectory(true);
source.setLocalFilter(new AcceptOnceFileListFilter<File>());
source.setMaxFetchSize(1);
return source;
}
@Bean
@ServiceActivator(inputChannel = "ftpChannel")
public MessageHandler handler() {
return new MessageHandler() {
@Override
public void handleMessage(Message<?> message) throws MessagingException {
System.out.println(message.getPayload());
}
};
}
}
# 使用 Java DSL 进行配置
Spring 以下引导应用程序展示了如何使用 Java DSL 配置入站适配器的示例:
@SpringBootApplication
public class FtpJavaApplication {
public static void main(String[] args) {
new SpringApplicationBuilder(FtpJavaApplication.class)
.web(false)
.run(args);
}
@Bean
public IntegrationFlow ftpInboundFlow() {
return IntegrationFlows
.from(Ftp.inboundAdapter(this.ftpSessionFactory)
.preserveTimestamp(true)
.remoteDirectory("foo")
.regexFilter(".*\\.txt$")
.localFilename(f -> f.toUpperCase() + ".a")
.localDirectory(new File("d:\\ftp_files")),
e -> e.id("ftpInboundAdapter")
.autoStartup(true)
.poller(Pollers.fixedDelay(5000)))
.handle(m -> System.out.println(m.getPayload()))
.get();
}
}
# 处理不完整数据
见处理不完整的数据。
提供FtpSystemMarkerFilePresentFileListFilter
以过滤远程系统上没有相应标记文件的远程文件。有关配置信息,请参见Javadoc (opens new window)(并浏览到父类)。
# FTP 流媒体入站通道适配器
版本 4.3 引入了流入站通道适配器。此适配器生成的消息的有效负载类型为InputStream
,这样就可以在不将文件写入本地文件系统的情况下获取文件。由于会话保持打开,所以当文件已被消费时,消费应用程序负责关闭会话。会话在closeableResource
报头(IntegrationMessageHeaderAccessor.CLOSEABLE_RESOURCE
)中提供。标准框架组件,例如FileSplitter
和StreamTransformer
,会自动关闭会话。有关这些组件的更多信息,请参见文件拆分器和流变压器。下面的示例展示了如何配置inbound-streaming-channel-adapter
:
<int-ftp:inbound-streaming-channel-adapter id="ftpInbound"
channel="ftpChannel"
session-factory="sessionFactory"
filename-pattern="*.txt"
filename-regex=".*\.txt"
filter="filter"
filter-expression="@myFilterBean.check(#root)"
remote-file-separator="/"
comparator="comparator"
max-fetch-size="1"
remote-directory-expression="'foo/bar'">
<int:poller fixed-rate="1000" />
</int-ftp:inbound-streaming-channel-adapter>
只有filename-pattern
、filename-regex
、filter
或filter-expression
中的一个是允许的。
从版本 5.0 开始,默认情况下,FtpStreamingMessageSource 适配器将防止基于内存FtpPersistentAcceptOnceFileListFilter 的SimpleMetadataStore 的远程文件的重复。默认情况下,该过滤器还将与文件名模式(或 regex)一起应用。 如果你需要允许重复,你可以使用 AcceptAllFileListFilter 。任何其他用例都可以由 CompositeFileListFilter (或ChainFileListFilter )处理。Java 配置(稍后在文档中)显示了一种在处理后删除远程文件以避免重复的技术。 |
---|
有关FtpPersistentAcceptOnceFileListFilter
及其使用方式的更多信息,请参见远程持久文件列表过滤器。
使用max-fetch-size
属性来限制在需要进行获取时在每个轮询中获取的文件数量。将其设置为1
,并在集群环境中运行时使用持久过滤器。有关更多信息,请参见入站通道适配器:控制远程文件获取。
适配器将远程目录和文件名分别放入FileHeaders.REMOTE_DIRECTORY
和FileHeaders.REMOTE_FILE
标题中。从版本 5.0 开始,FileHeaders.REMOTE_FILE_INFO
头提供了额外的远程文件信息(默认情况下用 JSON 表示)。如果将FtpStreamingMessageSource
上的fileInfoJson
属性设置为false
,则头包含一个FtpFileInfo
对象。可以使用FtpFileInfo.getFileInfo()
方法访问底层 Apache 网库提供的FTPFile
对象。当你使用 XML 配置时,fileInfoJson
属性是不可用的,但是你可以通过将FtpStreamingMessageSource
注入到你的一个配置类中来设置它。另见远程文件信息。
从版本 5.1 开始,comparator
的通用类型是FTPFile
。以前,它是AbstractFileInfo<FTPFile>
。这是因为排序现在是在处理的较早阶段执行的,在过滤和应用maxFetch
之前。
# 使用 Java 配置进行配置
Spring 以下引导应用程序展示了如何使用 Java 配置配置入站适配器的示例:
@SpringBootApplication
public class FtpJavaApplication {
public static void main(String[] args) {
new SpringApplicationBuilder(FtpJavaApplication.class)
.web(false)
.run(args);
}
@Bean
@InboundChannelAdapter(channel = "stream")
public MessageSource<InputStream> ftpMessageSource() {
FtpStreamingMessageSource messageSource = new FtpStreamingMessageSource(template());
messageSource.setRemoteDirectory("ftpSource/");
messageSource.setFilter(new AcceptAllFileListFilter<>());
messageSource.setMaxFetchSize(1);
return messageSource;
}
@Bean
@Transformer(inputChannel = "stream", outputChannel = "data")
public org.springframework.integration.transformer.Transformer transformer() {
return new StreamTransformer("UTF-8");
}
@Bean
public FtpRemoteFileTemplate template() {
return new FtpRemoteFileTemplate(ftpSessionFactory());
}
@ServiceActivator(inputChannel = "data", adviceChain = "after")
@Bean
public MessageHandler handle() {
return System.out::println;
}
@Bean
public ExpressionEvaluatingRequestHandlerAdvice after() {
ExpressionEvaluatingRequestHandlerAdvice advice = new ExpressionEvaluatingRequestHandlerAdvice();
advice.setOnSuccessExpression(
"@template.remove(headers['file_remoteDirectory'] + headers['file_remoteFile'])");
advice.setPropagateEvaluationFailures(true);
return advice;
}
}
请注意,在本例中,Transformer 下游的消息处理程序有一个建议,该建议在处理后删除远程文件。
# 入站通道适配器:轮询多个服务器和目录
从版本 5.0.7 开始,RotatingServerAdvice
是可用的;当配置为 Poller 建议时,入站适配器可以轮询多个服务器和目录。配置建议,并将其正常添加到 Poller 的建议链中。aDelegatingSessionFactory
用于选择服务器,有关更多信息,请参见Delegating Session Factory。通知配置由RotationPolicy.KeyDirectory
对象列表组成。
例子
@Bean
public RotatingServerAdvice advice() {
List<RotationPolicy.KeyDirectory> keyDirectories = new ArrayList<>();
keyDirectories.add(new RotationPolicy.KeyDirectory("one", "foo"));
keyDirectories.add(new RotationPolicy.KeyDirectory("one", "bar"));
keyDirectories.add(new RotationPolicy.KeyDirectory("two", "baz"));
keyDirectories.add(new RotationPolicy.KeyDirectory("two", "qux"));
keyDirectories.add(new RotationPolicy.KeyDirectory("three", "fiz"));
keyDirectories.add(new RotationPolicy.KeyDirectory("three", "buz"));
return new RotatingServerAdvice(delegatingSf(), keyDirectories);
}
此建议将轮询服务器foo
上的目录one
,直到没有新文件存在,然后移动到目录bar
,然后是服务器baz
上的目录two
,等等。
可以使用公平
构造函数 arg 修改此默认行为:
fair
@Bean
public RotatingServerAdvice advice() {
...
return new RotatingServerAdvice(delegatingSf(), keyDirectories, true);
}
在这种情况下,无论上一次投票是否返回了文件,通知都将移动到下一个服务器/目录。
或者,你可以提供自己的RotationPolicy
,以便根据需要重新配置消息源:
政策
public interface RotationPolicy {
void beforeReceive(MessageSource<?> source);
void afterReceive(boolean messageReceived, MessageSource<?> source);
}
and
习惯
@Bean
public RotatingServerAdvice advice() {
return new RotatingServerAdvice(myRotationPolicy());
}
local-filename-generator-expression
属性(同步器上的localFilenameGeneratorExpression
)现在可以包含#remoteDirectory
变量。这允许将从不同目录检索到的文件下载到本地类似的目录中:
@Bean
public IntegrationFlow flow() {
return IntegrationFlows.from(Ftp.inboundAdapter(sf())
.filter(new FtpPersistentAcceptOnceFileListFilter(new SimpleMetadataStore(), "rotate"))
.localDirectory(new File(tmpDir))
.localFilenameExpression("#remoteDirectory + T(java.io.File).separator + #root")
.remoteDirectory("."),
e -> e.poller(Pollers.fixedDelay(1).advice(advice())))
.channel(MessageChannels.queue("files"))
.get();
}
使用此建议时,不要在 poller 上配置TaskExecutor ;有关更多信息,请参见消息源的条件 Poller。 |
---|
# 入站通道适配器:控制远程文件获取
在配置入站通道适配器时,应该考虑两个属性。max-messages-per-poll
,与所有 Poller 一样,可以用来限制在每个轮询上发出的消息的数量(如果超过配置的值已经准备好)。max-fetch-size
(自版本 5.0 起)可以限制一次从远程服务器检索到的文件的数量。
以下场景假定起始状态是一个空的本地目录:
max-messages-per-poll=2
和max-fetch-size=1
:适配器获取一个文件,发出它,获取下一个文件,发出它,然后睡眠,直到下一个投票。max-messages-per-poll=2
和max-fetch-size=2
):适配器获取这两个文件,然后发射每个文件。max-messages-per-poll=2
和max-fetch-size=4
:适配器获取最多四个文件(如果可用)并发出前两个文件(如果至少有两个)。接下来的两个文件将在下一次投票时发出。max-messages-per-poll=2
和max-fetch-size
未指定:适配器获取所有远程文件并发出前两个文件(如果至少有两个)。后续的文件将在后续的轮询中发出(一次两个)。当所有文件都被消耗时,将再次尝试远程获取,以获取任何新的文件。
当部署一个应用程序的多个实例时,我们建议使用一个小的max-fetch-size ,以避免一个实例“抓取”所有文件并使其他实例无法使用。 |
---|
max-fetch-size
的另一种用法是,如果你想停止获取远程文件,而是继续处理已经获取的文件。在MessageSource
上设置maxFetchSize
属性(以编程方式使用 JMX,或使用控制总线)可以有效地阻止适配器获取更多文件,但可以让 Poller 继续为先前已获取的文件发送消息。如果属性更改时 poller 处于活动状态,则更改将在下一次投票时生效。
从版本 5.1 开始,同步器可以提供Comparator<FTPFile>
。这在限制使用maxFetchSize
获取的文件数量时很有用。
# FTP 出站通道适配器
FTP 出站通道适配器依赖于一个MessageHandler
实现,该实现连接到 FTP 服务器,并为它在传入消息的有效负载中接收的每个文件发起 FTP 传输。它还支持一个文件的几种表示,因此你不仅限于java.io.File
类型的有效负载。FTP 出站通道适配器支持以下有效负载:
java.io.File
:实际的文件对象byte[]
:表示文件内容的字节数组java.lang.String
:表示文件内容的文本java.io.InputStream
:要传输到远程文件的数据流org.springframework.core.io.Resource
:用于将数据传输到远程文件的资源
下面的示例展示了如何配置outbound-channel-adapter
:
<int-ftp:outbound-channel-adapter id="ftpOutbound"
channel="ftpChannel"
session-factory="ftpSessionFactory"
charset="UTF-8"
remote-file-separator="/"
auto-create-directory="true"
remote-directory-expression="headers['remote_dir']"
temporary-remote-directory-expression="headers['temp_remote_dir']"
filename-generator="fileNameGenerator"
use-temporary-filename="true"
chmod="600"
mode="REPLACE"/>
前面的配置显示了如何通过使用outbound-channel-adapter
元素来配置 FTP 出站通道适配器,同时还提供各种属性的值,例如filename-generator
(o.s.i.file.FileNameGenerator
策略接口的实现)、对session-factory
的引用以及其他属性。你还可以看到一些*expression
属性的示例,这些属性允许你使用 SPEL 来配置设置,例如remote-directory-expression
、temporary-remote-directory-expression
和remote-filename-generator-expression
(上一个示例中显示的filename-generator
的 SPEL 替代)。与允许使用 SPEL 的任何组件一样,可以通过“有效负载”和“头”变量访问有效负载和消息头。有关可用属性的更多详细信息,请参见schema (opens new window)。
默认情况下,如果没有指定文件名生成器, Spring 集成使用o.s.i.file.DefaultFileNameGenerator ,DefaultFileNameGenerator 根据file_name 报头的值(如果存在的话)确定文件名,或者,如果消息的有效负载已经是java.io.File ,它使用了该文件的原始名称。 |
---|
定义某些值(例如remote-directory )可能与平台或 FTP 服务器有关。例如,正如https://forum.spring.io/showthread.php?p=333478&posted=1#post333478 (opens new window)上报告的那样,在某些平台上,必须在目录定义的末尾添加斜杠(例如, remote-directory="/thing1/thing2/" 而不是remote-directory="/thing1/thing2" )。 |
---|
从版本 4.1 开始,你可以在传输文件时指定mode
。默认情况下,现有文件将被覆盖。模式由FileExistsMode
枚举定义,其中包括以下值:
REPLACE
(默认)REPLACE_IF_MODIFIED
APPEND
APPEND_NO_FLUSH
IGNORE
FAIL
IGNORE
和FAIL
不传输文件。FAIL
会导致抛出异常,而IGNORE
会默默地忽略传输(尽管会产生DEBUG
日志条目)。
版本 5.2 引入了chmod
属性,你可以使用该属性在上传后更改远程文件权限。你可以使用常规的 UNIX 八进制格式(例如,600
仅允许文件所有者进行读写)。当使用 Java 配置适配器时,可以使用setChmodOctal("600")
或setChmod(0600)
。仅当你的 FTP 服务器支持SITE CHMOD
子命令时才适用。
# 避免部分写入的文件
处理文件传输时出现的一个常见问题是处理部分文件的可能性。也就是说,一个文件可能会在其传输真正完成之前出现在文件系统中。
Spring 为了处理这个问题,集成 FTP 适配器使用一种常见的算法:文件以临时名称传输,然后在文件完全传输后重新命名。
默认情况下,正在传输过程中的每个文件都会在文件系统中出现一个附加的后缀,默认情况下,后缀是.writing
。你可以通过设置temporary-file-suffix
属性来更改此后缀。
但是,在某些情况下,你可能不想使用这种技术(例如,如果服务器不允许重命名文件)。对于这样的情况,你可以通过将use-temporary-file-name
设置为false
来禁用此功能(默认值为true
)。当此属性false
时,文件将以其最终名称写入,并且使用该应用程序的应用程序需要一些其他机制来检测该文件是否已完全上传,然后才能访问它。
# 使用 Java 配置进行配置
Spring 以下引导应用程序展示了如何使用 Java 配置配置出站适配器的示例:
@SpringBootApplication
@IntegrationComponentScan
public class FtpJavaApplication {
public static void main(String[] args) {
ConfigurableApplicationContext context =
new SpringApplicationBuilder(FtpJavaApplication.class)
.web(false)
.run(args);
MyGateway gateway = context.getBean(MyGateway.class);
gateway.sendToFtp(new File("/foo/bar.txt"));
}
@Bean
public SessionFactory<FTPFile> ftpSessionFactory() {
DefaultFtpSessionFactory sf = new DefaultFtpSessionFactory();
sf.setHost("localhost");
sf.setPort(port);
sf.setUsername("foo");
sf.setPassword("foo");
sf.setTestSession(true);
return new CachingSessionFactory<FTPFile>(sf);
}
@Bean
@ServiceActivator(inputChannel = "ftpChannel")
public MessageHandler handler() {
FtpMessageHandler handler = new FtpMessageHandler(ftpSessionFactory());
handler.setRemoteDirectoryExpressionString("headers['remote-target-dir']");
handler.setFileNameGenerator(new FileNameGenerator() {
@Override
public String generateFileName(Message<?> message) {
return "handlerContent.test";
}
});
return handler;
}
@MessagingGateway
public interface MyGateway {
@Gateway(requestChannel = "toFtpChannel")
void sendToFtp(File file);
}
}
# 使用 Java DSL 进行配置
Spring 以下引导应用程序展示了如何使用 Java DSL 配置出站适配器的示例:
@SpringBootApplication
@IntegrationComponentScan
public class FtpJavaApplication {
public static void main(String[] args) {
ConfigurableApplicationContext context =
new SpringApplicationBuilder(FtpJavaApplication.class)
.web(false)
.run(args);
MyGateway gateway = context.getBean(MyGateway.class);
gateway.sendToFtp(new File("/foo/bar.txt"));
}
@Bean
public SessionFactory<FTPFile> ftpSessionFactory() {
DefaultFtpSessionFactory sf = new DefaultFtpSessionFactory();
sf.setHost("localhost");
sf.setPort(port);
sf.setUsername("foo");
sf.setPassword("foo");
sf.setTestSession(true);
return new CachingSessionFactory<FTPFile>(sf);
}
@Bean
public IntegrationFlow ftpOutboundFlow() {
return IntegrationFlows.from("toFtpChannel")
.handle(Ftp.outboundAdapter(ftpSessionFactory(), FileExistsMode.FAIL)
.useTemporaryFileName(false)
.fileNameExpression("headers['" + FileHeaders.FILENAME + "']")
.remoteDirectory(this.ftpServer.getTargetFtpDirectory().getName())
).get();
}
@MessagingGateway
public interface MyGateway {
@Gateway(requestChannel = "toFtpChannel")
void sendToFtp(File file);
}
}
# FTP 出站网关
FTP 出站网关提供了一组有限的命令来与远程 FTP 或 FTPS 服务器进行交互。支持的命令是:
ls
(列表文件)nlst
(列出文件名)get
(检索文件)mget
(检索文件)rm
(删除文件)mv
(移动/重命名文件)put
(发送文件)mput
(发送多个文件)
# 使用ls
命令
ls
列出了远程文件并支持以下选项:
-1
:检索文件名列表。默认值是检索FileInfo
对象的列表。-a
:包含所有文件(包括以“.”开头的文件)-f
:不要对列表进行排序-dirs
:包括目录(默认情况下不包括目录)-links
:包括符号链接(默认情况下不包括这些链接)-R
:递归列出远程目录
此外,以与inbound-channel-adapter
相同的方式提供了文件名过滤。见FTP 入站通道适配器。
由ls
操作产生的消息有效负载是文件名列表或FileInfo
对象列表。这些对象提供诸如修改时间、权限和其他详细信息等信息。
ls
命令所作用的远程目录在file_remoteDirectory
报头中提供。
当使用递归选项(-R
)时,fileName
包括任意子目录元素,表示文件的相对路径(相对于远程目录)。如果包含-dirs
选项,那么每个递归目录也将作为列表中的一个元素返回。在这种情况下,建议你不要使用-1
选项,因为你将无法区分文件和目录,这可以通过FileInfo
对象来实现。
从版本 4.3 开始,FtpSession
对list()
和listNames()
方法支持null
。因此,你可以省略expression
属性。为了方便起见,Java 配置有两个不具有expression
参数的构造函数。或LS
、NLST
、PUT
和MPUT
命令,根据 FTP 协议,null
被视为客户机工作目录。所有其他命令都必须提供expression
,以根据请求消息计算远程路径。在扩展DefaultFtpSessionFactory
并实现postProcessClientAfterConnect()
回调时,可以使用FTPClient.changeWorkingDirectory()
函数设置工作目录。
# 使用nlst
命令
版本 5 引入了对nlst
命令的支持。
nlst
列出了远程文件名,并且只支持一个选项:
-f
:不对列表进行排序
由nlst
操作产生的消息有效负载是一个文件名列表。
nlst
命令所作用的远程目录在file_remoteDirectory
报头中提供。
与[ls
命令](#ftp-using-ls)的-1
选项不同,该选项使用LIST
命令,nlst
命令向目标 FTP 服务器发送NLST
命令。当服务器不支持LIST
(例如由于安全限制)时,此命令很有用。nlst
操作的结果是没有其他详细信息的名称。因此,框架不能确定一个实体是否是一个目录,例如执行筛选或递归列表。
# 使用get
命令
get
检索远程文件。它支持以下选项:
-P
:保留远程文件的时间戳。-stream
:以流的形式检索远程文件。-D
:成功传输后删除远程文件。如果忽略传输,则不会删除远程文件,因为FileExistsMode
是IGNORE
,并且本地文件已经存在。
file_remoteDirectory
头提供远程目录名,file_remoteFile
头提供文件名。
由get
操作产生的消息有效负载是一个File
对象,该对象表示检索到的文件,或者当你使用-stream
选项时是一个InputStream
对象。-stream
选项允许将文件作为流检索。对于文本文件,一个常见的用例是将此操作与文件拆分器或流变压器结合起来。当以流的形式使用远程文件时,你要负责在流被使用后关闭Session
。为了方便起见,Session
在closeableResource
标头中提供了closeableResource
标头,你可以在IntegrationMessageHeaderAccessor
上使用方便方法访问它。下面的示例展示了如何使用方便方法:
Closeable closeable = new IntegrationMessageHeaderAccessor(message).getCloseableResource();
if (closeable != null) {
closeable.close();
}
下面的示例展示了如何将文件作为流使用:
<int-ftp:outbound-gateway session-factory="ftpSessionFactory"
request-channel="inboundGetStream"
command="get"
command-options="-stream"
expression="payload"
remote-directory="ftpTarget"
reply-channel="stream" />
<int-file:splitter input-channel="stream" output-channel="lines" />
如果在自定义组件中使用输入流,则必须关闭Session 。可以在自定义代码中关闭,也可以通过将消息的副本路由到 service-activator 并使用 SPEL 来关闭,如下例所示: |
---|
<int:service-activator input-channel="closeSession"
expression="headers['closeableResource'].close()" />
# 使用mget
命令
mget
基于模式检索多个远程文件,并支持以下选项:
-P
:保留远程文件的时间戳。-R
:递归地检索整个目录树。-x
:如果没有与模式匹配的文件,则抛出异常(否则将返回空列表)。-D
:成功传输后删除每个远程文件。如果忽略传输,则不会删除远程文件,因为FileExistsMode
是IGNORE
,并且本地文件已经存在。
由mget
操作产生的消息负载是List<File>
对象(即List
的List
对象,每个对象代表一个检索到的文件)。
从版本 5.0 开始,如果FileExistsMode 是IGNORE ,则输出消息的有效负载不再包含由于文件已经存在而未被获取的文件。以前,该列表包含所有文件,包括已经存在的文件。 |
---|
用于确定远程路径的表达式应该产生一个以``结尾的结果-例如,somedir/
将在somedir
下获取完整的树。
从版本 5.0 开始,递归的mget
与新的FileExistsMode.REPLACE_IF_MODIFIED
模式相结合,可以用于在本地定期同步整个远程目录树。无论-P
(保留时间戳)选项如何,此模式都会用远程时间戳替换本地文件上一次修改的时间戳。
Using recursion (-R )The pattern is ignored, and * is assumed.By default, the entire remote tree is retrieved. However, files in the tree can be filtered, by providing a FileListFilter .Directories in the tree can also be filtered this way. A FileListFilter can be provided by reference, by filename-pattern , or by filename-regex attributes.For example, filename-regex="(subDir|.*1.TXT)“ retrieves all files ending with 1.TXT in the remote directory and the subdir child directory.However, the next example shows an alternative, which version 5.0 made available. If a subdirectory is filtered, no additional traversal of that subdirectory is performed. The -dirs option is not allowed (the recursive mget uses the recursive ls to obtain the directory tree, so the directories themselves cannot be included in the list).Typically, you would use the #remoteDirectory variable in the local-directory-expression 使远程目录结构在本地保留。 |
---|
持久文件列表过滤器现在有一个布尔属性forRecursion
。将此属性设置为true
,还将设置alwaysAcceptDirectories
,这意味着出站网关上的递归操作(ls
和mget
)现在每次都将遍历完整目录树。这是为了解决未检测到目录树中深层更改的问题。此外,forRecursion=true
会导致文件的完整路径被用作元数据存储键;这解决了一个问题,即如果同名文件在不同的目录中多次出现,则过滤器无法正常工作。重要提示:这意味着,对于顶层目录下的文件,将找不到持久性元数据存储中的现有密钥。由于这个原因,默认情况下,该属性是false
;这可能会在将来的版本中发生变化。
从版本 5.0 开始,FtpSimplePatternFileListFilter
和FtpRegexPatternFileListFilter
可以通过将alwaysAcceptDirectories
属性设置为true
来配置为始终传递目录。这样做允许对简单模式进行递归,如下例所示:
<bean id="starDotTxtFilter"
class="org.springframework.integration.ftp.filters.FtpSimplePatternFileListFilter">
<constructor-arg value="*.txt" />
<property name="alwaysAcceptDirectories" value="true" />
</bean>
<bean id="dotStarDotTxtFilter"
class="org.springframework.integration.ftp.filters.FtpRegexPatternFileListFilter">
<constructor-arg value="^.*\.txt$" />
<property name="alwaysAcceptDirectories" value="true" />
</bean>
一旦定义了前面示例中的过滤器,就可以通过在网关上设置filter
属性来使用过滤器。
另请参见[出站网关部分成功(mget
和mput
)]。
# 使用put
命令
put
commad 将文件发送到远程服务器。消息的有效负载可以是java.io.File
、byte[]
或String
。使用remote-filename-generator
(或表达式)为远程文件命名。其他可用的属性包括remote-directory
,temporary-remote-directory
,以及它们的*-expression
等价物:use-temporary-file-name
和auto-create-directory
。有关更多信息,请参见schema (opens new window)文档。
由put
操作产生的消息有效负载是一个String
,它表示服务器上传输后文件的完整路径。
5.2 版本引入了chmod
属性,该属性在上传后更改远程文件权限。你可以使用常规的 UNIX 八进制格式(例如,600
仅允许文件所有者进行读写)。当使用 Java 配置适配器时,可以使用setChmod(0600)
。仅当你的 FTP 服务器支持SITE CHMOD
子命令时才适用。
# 使用mput
命令
mput
将多个文件发送到服务器,并且只支持一个选项:
-R
:递归。发送目录及其子目录中的所有文件(可能已过滤)。
消息有效负载必须是表示本地目录的java.io.File
(或String
)。从版本 5.1 开始,还支持File
或String
的集合。
此命令支持与[put
命令](#ftp-put-command)相同的属性。此外,可以使用mput-pattern
、mput-regex
、mput-filter
或mput-filter-expression
中的一个对本地目录中的文件进行过滤。只要子目录本身通过筛选器,筛选器就可以使用递归。不通过筛选器的子目录不会被递归。
由mput
操作产生的消息有效负载是List<String>
对象(即由传输产生的远程文件路径的List
)。
另请参见[出站网关部分成功(mget
和mput
)]。
版本 5.2 引入了chmod
属性,它允许你在上传后更改远程文件权限。你可以使用常规的 UNIX 八进制格式(例如,600
仅允许文件所有者进行读写)。当使用 Java 配置适配器时,可以使用setChmodOctal("600")
或setChmod(0600)
。仅当你的 FTP 服务器支持SITE CHMOD
子命令时才适用。
# 使用rm
命令
rm
命令删除文件。
rm
命令没有选项。
如果删除成功,则rm
操作产生的消息有效负载为Boolean.TRUE
,否则为Boolean.FALSE
。file_remoteDirectory
头提供远程目录,file_remoteFile
头提供文件名。
# 使用mv
命令
mv
命令移动文件。
mv
命令没有选项。
expression
属性定义了“from”路径,rename-expression
属性定义了“to”路径。默认情况下,rename-expression
是headers['file_renameTo']
。这个表达式不能计算为空或空String
。如果需要,将创建任何必要的远程目录。结果消息的有效负载是Boolean.TRUE
。file_remoteDirectory
头提供原始远程目录,file_remoteFile
头提供文件名。新路径位于file_renameTo
标头中。
从版本 5.5.6 开始,remoteDirectoryExpression
可以在mv
命令中使用,以方便使用。如果“from”文件不是完整的文件路径,则将remoteDirectoryExpression
的结果用作远程目录。这同样适用于“to”文件,例如,如果任务只是重命名某个目录中的远程文件。
# 有关 FTP 出站网关命令的其他信息
get
和mget
命令支持local-filename-generator-expression
属性。它定义了一个 SPEL 表达式,以便在传输过程中生成本地文件的名称。求值上下文的根对象是请求消息。对于mget
特别有用的remoteFileName
变量也是可用的——例如,local-filename-generator-expression="#remoteFileName.toUpperCase() + headers.something"
。
get
和mget
命令支持local-directory-expression
属性。它定义了一个 SPEL 表达式,以便在传输过程中生成本地目录的名称。求值上下文的根对象是请求消息 but。对于mget
特别有用的remoteDirectory
变量也是可用的——例如:local-directory-expression="'/tmp/local/' + #remoteDirectory.toUpperCase() + headers.something"
。这个属性与local-directory
属性是互斥的。
对于所有命令,网关的“表达式”属性提供了命令执行的路径。对于mget
命令,表达式可以计算为’’,意思是检索所有文件,或’SomeDirectory/’,依此类推。
下面的示例显示了为ls
命令配置的网关:
<int-ftp:outbound-gateway id="gateway1"
session-factory="ftpSessionFactory"
request-channel="inbound1"
command="ls"
command-options="-1"
expression="payload"
reply-channel="toSplitter"/>
发送到toSplitter
通道的消息的有效负载是String
对象的列表,每个对象都包含一个文件的名称。如果省略了command-options
属性,则它持有FileInfo
对象。它使用空格分隔的选项——例如,command-options="-1 -dirs -links"
。
从版本 4.2 开始,GET
、MGET
、PUT
和MPUT
命令支持FileExistsMode
属性(使用名称空间支持时mode
)。这会影响当本地文件存在(GET
和MGET
)或远程文件存在(PUT
和MPUT
)时的行为。支持的模式有REPLACE
、APPEND
、FAIL
和IGNORE
。对于向后兼容性,PUT
和MPUT
操作的默认模式是REPLACE
。对于GET
和MGET
操作,缺省值是FAIL
。
从版本 5.0 开始,setWorkingDirExpression()
(在 XML 中为working-dir-expression
)选项在FtpOutboundGateway
(在 XML 中为<int-ftp:outbound-gateway>
)上提供。它允许你在运行时更改客户机工作目录。表达式根据请求消息进行求值。在每次网关操作之后,都会恢复以前的工作目录。
# 使用 Java 配置进行配置
Spring 以下引导应用程序展示了如何使用 Java 配置出站网关的示例:
@SpringBootApplication
public class FtpJavaApplication {
public static void main(String[] args) {
new SpringApplicationBuilder(FtpJavaApplication.class)
.web(false)
.run(args);
}
@Bean
public SessionFactory<FTPFile> ftpSessionFactory() {
DefaultFtpSessionFactory sf = new DefaultFtpSessionFactory();
sf.setHost("localhost");
sf.setPort(port);
sf.setUsername("foo");
sf.setPassword("foo");
sf.setTestSession(true);
return new CachingSessionFactory<FTPFile>(sf);
}
@Bean
@ServiceActivator(inputChannel = "ftpChannel")
public MessageHandler handler() {
FtpOutboundGateway ftpOutboundGateway =
new FtpOutboundGateway(ftpSessionFactory(), "ls", "'my_remote_dir/'");
ftpOutboundGateway.setOutputChannelName("lsReplyChannel");
return ftpOutboundGateway;
}
}
# 使用 Java DSL 进行配置
Spring 以下引导应用程序展示了如何使用 Java DSL 配置出站网关的示例:
@SpringBootApplication
public class FtpJavaApplication {
public static void main(String[] args) {
new SpringApplicationBuilder(FtpJavaApplication.class)
.web(false)
.run(args);
}
@Bean
public SessionFactory<FTPFile> ftpSessionFactory() {
DefaultFtpSessionFactory sf = new DefaultFtpSessionFactory();
sf.setHost("localhost");
sf.setPort(port);
sf.setUsername("foo");
sf.setPassword("foo");
sf.setTestSession(true);
return new CachingSessionFactory<FTPFile>(sf);
}
@Bean
public FtpOutboundGatewaySpec ftpOutboundGateway() {
return Ftp.outboundGateway(ftpSessionFactory(),
AbstractRemoteFileOutboundGateway.Command.MGET, "payload")
.options(AbstractRemoteFileOutboundGateway.Option.RECURSIVE)
.regexFileNameFilter("(subFtpSource|.*1.txt)")
.localDirectoryExpression("'localDirectory/' + #remoteDirectory")
.localFilenameExpression("#remoteFileName.replaceFirst('ftpSource', 'localTarget')");
}
@Bean
public IntegrationFlow ftpMGetFlow(AbstractRemoteFileOutboundGateway<FTPFile> ftpOutboundGateway) {
return f -> f
.handle(ftpOutboundGateway)
.channel(c -> c.queue("remoteFileOutputChannel"));
}
}
# 出站网关部分成功(mget
和mput
)
在对多个文件执行操作时(通过使用mget
和mput
),在传输了一个或多个文件后的一段时间内可能会发生异常。在这种情况下(从版本 4.2 开始),将抛出一个PartialSuccessException
。除了通常的MessagingException
属性(failedMessage
和cause
)外,该异常还具有两个附加属性:
partialResults
:成功的转移结果。derivedInput
:从请求消息中生成的文件列表(例如,要传输的本地文件为mput
)。
这些属性允许你确定哪些文件已成功传输,哪些文件未成功传输。
在递归mput
的情况下,PartialSuccessException
可能嵌套了PartialSuccessException
出现。
考虑以下目录结构:
root/
|- file1.txt
|- subdir/
| - file2.txt
| - file3.txt
|- zoo.txt
如果异常发生在file3.txt
上,则网关抛出的PartialSuccessException
具有derivedInput
offile1.txt
,subdir
,以及zoo.txt
和partialResults
offile1.txt
。其cause
是另一个PartialSuccessException
与derivedInput
之file2.txt
和file3.txt
之partialResults
之file2.txt
之。
# ftp会话缓存
从 Spring Integration3.0 开始,默认情况下不再缓存会话。 端点不再支持 cache-sessions 属性。如果你希望缓存会话,则必须使用 CachingSessionFactory (如下一个示例所示)。 |
---|
在 3.0 之前的版本中,默认情况下会话是自动缓存的。有一个cache-sessions
属性可用于禁用自动缓存,但该解决方案没有提供一种配置其他会话缓存属性的方法。例如,你无法限制创建的会话的数量。为了支持该需求和其他配置选项,添加了CachingSessionFactory
。它提供sessionCacheSize
和sessionWaitTimeout
属性。sessionCacheSize
属性控制工厂在其缓存中维护的活动会话的数量(缺省情况是无界的)。如果已经达到sessionCacheSize
阈值,则试图获取另一个会话的任何尝试都会阻塞,直到其中一个缓存的会话变得可用,或者直到一个会话的等待时间过期(默认的等待时间是Integer.MAX_VALUE
)。sessionWaitTimeout
属性配置该值。
如果你希望你的会话被缓存,那么按照前面的描述配置你的默认会话工厂,然后将其包装在CachingSessionFactory
的实例中,在该实例中你可以提供这些附加属性。下面的示例展示了如何做到这一点:
<bean id="ftpSessionFactory" class="o.s.i.ftp.session.DefaultFtpSessionFactory">
<property name="host" value="localhost"/>
</bean>
<bean id="cachingSessionFactory" class="o.s.i.file.remote.session.CachingSessionFactory">
<constructor-arg ref="ftpSessionFactory"/>
<constructor-arg value="10"/>
<property name="sessionWaitTimeout" value="1000"/>
</bean>
前面的示例显示了一个CachingSessionFactory
,该sessionCacheSize
设置为10
,而sessionWaitTimeout
设置为一秒(其值以毫秒为单位)。
从 Spring Integration3.0 开始,CachingConnectionFactory
提供了一个resetCache()
方法。当调用时,所有空闲会话都会立即关闭,而当正在使用的会话被返回到缓存时,它们会被关闭。新的届会请求在必要时设立新的届会。
从版本 5.1 开始,CachingSessionFactory
有一个新的属性testSession
。当为真时,会话将通过发送一个 Noop 命令来进行测试,以确保它仍然处于活动状态;如果不是,它将从缓存中删除;如果缓存中没有活动会话,则创建一个新的会话。
# 使用RemoteFileTemplate
从 Spring Integration3.0 开始,在FtpSession
对象上提供了一个新的抽象。该模板提供了发送、检索(作为InputStream
)、删除和重命名文件的方法。此外还提供了一种方法,允许调用者在会话上执行多个操作。在所有情况下,模板都能可靠地关闭会话。有关更多信息,请参见[Javadoc forRemoteFileTemplate
](https://DOCS. Spring.io/ Spring-integration/api/org/springframework/integration/file/remote/remotefiletemplate.html)。FTP 有一个子类:FtpRemoteFileTemplate
。
版本 4.1 增加了额外的方法,包括getClientInstance()
,它提供对底层FTPClient
的访问,从而使你能够访问低级 API。
并不是所有的 FTP 服务器都正确地实现了STAT <path>
命令。对于不存在的路径,有些返回正结果。当路径是一个文件并且它存在时,NLST
命令可靠地返回该名称。但是,这不支持检查空目录是否存在,因为当路径是目录时,NLST
总是返回一个空列表。由于模板不知道路径是否表示目录,因此当路径似乎不存在时(当使用NLST
时),它必须执行额外的检查。这增加了开销,需要向服务器发出多个请求。从版本 4.1.9 开始,FtpRemoteFileTemplate
提供了FtpRemoteFileTemplate.ExistsMode
属性,该属性具有以下选项:
STAT
:执行STAT
FTP 命令(FTPClient.getStatus(path)
)检查路径是否存在。这是默认的,要求你的 FTP 服务器正确地支持STAT
命令(带有路径)。NLST
:执行NLST
FTP 命令—FTPClient.listName(path)
。如果你正在测试的路径是文件的完整路径,请使用此方法。它不适用于空目录。NLST_AND_DIRS
:首先执行NLST
命令,如果不返回文件,则返回使用FTPClient.changeWorkingDirectory(path)
临时切换工作目录的技术。有关更多信息,请参见[FtpSession.exists()
](https://DOCS. Spring.io/ Spring-integration/api/org/springframework/integration/ftp/会话/ftpsession.html#exists)。
由于我们知道FileExistsMode.FAIL
情况总是只查找文件(而不是目录),因此我们可以安全地使用NLST
模式来处理FtpMessageHandler
和FtpOutboundGateway
组件。
对于任何其他情况,FtpRemoteFileTemplate
可以扩展为在重写的exist()
方法中实现自定义逻辑。
从版本 5.0 开始,新的RemoteFileOperations.invoke(OperationsCallback<F, T> action)
方法可用。这个方法允许在相同的、以线程为界的Session
的范围内调用几个RemoteFileOperations
调用。当你需要将RemoteFileTemplate
作为一个工作单元执行多个高级操作时,这是非常有用的。例如,AbstractRemoteFileOutboundGateway
将其与mput
命令实现一起使用,其中我们对提供的目录中的每个文件执行put
操作,并递归地对其子目录执行该操作。有关更多信息,请参见Javadoc (opens new window)。
# 使用MessageSessionCallback
从 Spring Integration4.2 开始,你可以使用带有<int-ftp:outbound-gateway/>
(在 Java 中是FtpOutboundGateway
)的MessageSessionCallback<F, T>
实现来执行带有requestMessage
上下文的Session<FTPFile>
上的任何操作。它可以用于任何非标准或低级别的 FTP 操作,并允许从集成流定义和功能接口实现注入进行访问,如以下示例所示:
@Bean
@ServiceActivator(inputChannel = "ftpChannel")
public MessageHandler ftpOutboundGateway(SessionFactory<FTPFile> sessionFactory) {
return new FtpOutboundGateway(sessionFactory,
(session, requestMessage) -> session.list(requestMessage.getPayload()));
}
另一个示例可能是对要发送或检索的文件数据进行预处理或后处理。
当使用 XML 配置时,<int-ftp:outbound-gateway/>
提供一个session-callback
属性,让你指定MessageSessionCallback
Bean 名称。
session-callback 与command 和expression 属性是互斥的。当使用 Java 进行配置时,在[ FtpOutboundGateway ](https://DOCS. Spring.io/ Spring-integration/api/org/boundspringframework/integration/ftp/gateway/ftpoutgateway.html)类中可以使用不同的构造函数。 |
---|
# Apache Mina FTP 服务器事件
在版本 5.2 中添加的ApacheMinaFtplet
监听某些 Apache Mina FTP 服务器事件,并将其发布为ApplicationEvent
s,该事件可以由任何ApplicationListener
Bean、@EventListener
Bean 方法或事件入站通道适配器方法接收。
目前支持的活动有:
SessionOpenedEvent
-打开了一个客户端会话DirectoryCreatedEvent
-创建了一个目录FileWrittenEvent
-一个文件被写入到PathMovedEvent
-重命名了一个文件或目录PathRemovedEvent
-删除了一个文件或目录SessionClosedEvent
-客户端已断开连接
每一个都是ApacheMinaFtpEvent
的子类;你可以配置一个侦听器来接收所有的事件类型。每个事件的source
属性是FtpSession
,你可以从它获得诸如客户机地址之类的信息;在抽象事件上提供了一个方便的getSession()
方法。
会话open/close 以外的事件具有另一个属性FtpRequest
,该属性具有命令和参数等属性。
要用侦听器(必须是 Spring Bean)配置服务器,请将其添加到服务器工厂:
FtpServerFactory serverFactory = new FtpServerFactory();
...
ListenerFactory factory = new ListenerFactory();
...
serverFactory.addListener("default", factory.createListener());
serverFactory.setFtplets(new HashMap<>(Collections.singletonMap("springFtplet", apacheMinaFtpletBean)));
server = serverFactory.createServer();
server.start();
要使用 Spring 集成事件适配器来使用这些事件:
@Bean
public ApplicationEventListeningMessageProducer eventsAdapter() {
ApplicationEventListeningMessageProducer producer =
new ApplicationEventListeningMessageProducer();
producer.setEventTypes(ApacheMinaFtpEvent.class);
producer.setOutputChannel(eventChannel());
return producer;
}
# 远程文件信息
从版本 5.2 开始,FtpStreamingMessageSource
(FTP 流入站通道适配器)、FtpInboundFileSynchronizingMessageSource
(FTP 入站通道适配器)和“read”---命令的FtpOutboundGateway
(FTP 出站网关)在消息中提供额外的头,以生成有关远程文件的信息:
FileHeaders.REMOTE_HOST_PORT
-在文件传输操作期间,远程会话已连接到的 host:port pair;FileHeaders.REMOTE_DIRECTORY
-已执行操作的远程目录;FileHeaders.REMOTE_FILE
-远程文件名;仅适用于单个文件操作。
由于FtpInboundFileSynchronizingMessageSource
不会针对远程文件生成消息,而是使用本地副本,因此AbstractInboundFileSynchronizer
在同步操作期间以 URI 样式(protocol://host:port/remoteDirectory#remoteFileName
)在MetadataStore
(可以在外部配置)中存储有关远程文件的信息。在对本地文件进行轮询时,将通过FtpInboundFileSynchronizingMessageSource
检索此元数据。当本地文件被删除时,建议删除其元数据条目。AbstractInboundFileSynchronizer
为此提供了一个removeRemoteFileMetadata()
回调。此外还有一个setMetadataStorePrefix()
要在元数据中使用的键。建议将该前缀与基于MetadataStore
的FileListFilter
实现中使用的前缀不同,当相同的MetadataStore
实例在这些组件之间共享时,为了避免条目重写,因为 Filter 和AbstractInboundFileSynchronizer
都对元数据项使用相同的本地文件名。