(安全)文件传输,最佳选择…额,最佳复制方式

工程 | Josh Long | 2010年8月23日 | ...

方法有很多。如今许多应用程序依赖于消息传递(AMQP、JMS)来弥合不同系统和数据之间的差距。其他应用程序依赖于 RPC(通常是 Web 服务或 REST)。但是,对于许多应用程序来说,文件传输仍然是一种非常普遍的方式!支持文件传输的常见方法有很多,但三种最常见的方法是使用共享挂载点或文件夹、使用 FTP 服务器以及——对于更安全的交换——使用 SSH(或 SFTP)。众所周知,Spring 一直以来都为消息传递(JMS、AMQP)和 RPC 提供一流的支持(远程选项太多无法列举!),但许多人可能会对Spring Integration 项目中众多强大的文件传输选项感到惊讶。在这篇文章中,我将基于即将推出的 Spring Integration 2.0 框架中的一些令人兴奋的支持进行构建,该框架允许您在到达新文件时连接到事件,并将文件发送到远程端点,例如 FTP 或 SFTP 服务器或共享挂载点。

我们将使用一对熟悉的 Java 类——一个用于生成出站数据,另一个用于接收入站数据,无论是用于 SFTP、FTP 还是普通文件系统都无关紧要。所有适配器都将java.io.File对象作为其入站有效负载传递,我们可以将 File、String 或byte[]发送到远程系统。首先,让我们看看我们的标准客户端。在 Spring Integration 中,根据入站消息执行逻辑的类称为“服务激活器”。您只需配置一个<service-activator>元素并告诉它要使用哪个 bean 来处理Message。它将遵循一些不同的启发式方法来帮助您确定要调度哪个方法来处理 Message。在这里,我们只是明确地对其进行注释。因此,以下是我们将在整篇文章中使用的客户端代码

import org.springframework.integration.annotation.*;
import org.springframework.stereotype.Component;
import java.io.File;
import java.util.Map;

@Component
public class InboundFileProcessor {

    @ServiceActivator
    public void onNewFileArrival(
            @Headers Map&lt;String, Object&gt; headers,
            @Payload File file) {

        System.out.printf("A new file has arrived deposited into " +
                          "the accounting folder at the absolute " +
                          "path %s \n", file.getAbsolutePath());

        System.out.println("The headers are:");
        for (String k : headers.keySet())
            System.out.println(String.format("%s=%s", k, headers.get(k)));

    }
}

此外,这是我们将用于合成最终存储在文件系统中的文件的代码

import org.springframework.integration.annotation.Header;
import org.springframework.integration.aop.Publisher;
import org.springframework.integration.file.FileHeaders;
import org.springframework.stereotype.Component;

@Component
public class OutboundFileProducer {

    @Publisher(channel = "outboundFiles")
    public String writeReportToDisk (
             @Header("customerId") long customerId,
             @Header(FileHeaders.FILENAME) String fileName    ) {
        return String.format("this is a message tailor made for customer # %s", customerId);
    }

}

最后一个例子是我在 Spring Integration 甚至 Spring 中最喜欢的功能之一:接口透明性。OutboundFileProducer类定义了一个用@Publisher注解注释的方法。@Publisher注解告诉 Spring Integration 将此方法调用的返回值转发到一个通道(这里我们通过注解命名它——outboundFiles)。这与直接注入org.springframework.integration.MessageChannel实例并在其上直接发送Message相同。不同的是,现在这一切都隐藏在一个简洁的 POJO 后面!任何人都可以根据自己的意愿注入此 bean——这将是我们的秘密,当他们调用该方法时,返回值将被写入某个地方的File :-) 要激活此功能,我们在 Spring 上下文中安装一个 SpringBeanPostProcessor。bean 后处理器机制允许您轻松扫描 Spring 上下文中的 bean,并在适当的情况下增强其定义。在这种情况下,我们正在增强用@Publisher注释的 bean。安装BeanPostProcessor就像实例化它一样简单

<beans:bean class="org.springframework.integration.aop.PublisherAnnotationBeanPostProcessor"/>

现在,我可以创建一个注入此 bean 的客户端(或简单地从上下文中访问它),并像使用任何其他服务一样使用它

@Autowired
private OutboundFileProducer outboundFileProducer ; 

 // ... 

outboundFileProducer.writeReportToDisk(1L, "1.txt") ;

最后,在我的所有 Spring 上下文中,我将打开<context:component-scan ... />以让 Java 代码执行大部分工作并处理业务逻辑。我使用 XML 的地方仅在于描述全局集成解决方案的流程和配置。

文件系统

第一个选择——共享挂载点——非常常见。构建此类解决方案的方法越来越多。大多数操作系统都有一种机制,允许您在文件到达时接收通知。Win32/.NET 为 Windows 提供挂钩,在 Linux 上,内核级别有许多机制,如 inotify。在 Java 平台上,Java 7 计划在 NIO.2 包中包含一个 WatchService。但是,在此之前,您需要编写执行目录轮询、保持状态然后调度事件的代码。听起来不太令人兴奋,不是吗?请注意,我们将讨论的所有适配器都需要某种轮询。轮询工作得足够好,但需要您进行一定程度的校准。首先,除非您适当地屏蔽文件,否则完全有可能扫描目录会拾取仍在写入的文件。通常,系统会将文件存放在某个挂载点上,写入它,然后将其重命名为与适配器上的正则表达式掩码匹配的方式:这保证了适配器在文件完成之前不会“看到”该文件。

在这里,Spring Integration 提供了很大的帮助——使您免于所有目录轮询代码,并使您可以编写对您重要的逻辑。如果您以前使用过 Spring Integration,那么您就会知道,从外部系统接收事件就像插入适配器然后让适配器告诉您何时值得做出反应一样简单。设置很简单:监控一个文件的文件夹以查找新文件,当一个新文件到达并(可选)匹配某些条件时,Spring Integration 将转发一个有效负载为java.io.File文件的引用的Message

您可以为此目的使用file:inbound-channel-adapter。适配器以固定的间隔(由poller元素配置)监控目录,然后在检测到新文件时发布Message。让我们看看如何在 Spring Integration 中配置它

<?xml version="1.0" encoding="UTF-8"?>

<beans:beans ... xmlns:file="http://www.springframework.org/schema/integration/file" >
    <context:component-scan base-package="org.springframework.integration.examples.filetransfer.core"/>

    <file:inbound-channel-adapter channel="inboundFiles"
                                  auto-create-directory="true"
                                  filename-pattern=".*?csv"
                                  directory="#{systemProperties['user.home']}/accounting">
        <poller fixed-rate="10000"/>
    </file:inbound-channel-adapter>

    <channel id="inboundFiles"/>

    <service-activator input-channel="inboundFiles" ref="inboundFileProcessor"/>

</beans:beans>

我认为这些选项非常不言自明。filename-pattern是一个正则表达式,它将针对目录中的每个文件名进行评估。如果文件名与正则表达式匹配,则对其进行处理。适配器标签内的 poller 元素告诉适配器每 10,000 毫秒或 10 秒重新检查一次目录。directory 属性允许您指定要监控的目录,当然,channel 描述了当适配器找到某些内容时要转发消息的命名通道。在此示例中,与所有后续示例一样,我们将使其将消息转发到连接到<service-activator>元素的命名通道。服务激活器只是您提供的 Java 代码,Spring Integration 将在新消息到达时调用它。在那里,您可以执行任何您想做的事情。

写入文件系统挂载点是另一回事;它更容易!

<?xml version="1.0" encoding="UTF-8"?>

<beans:beans ... xmlns:file="http://www.springframework.org/schema/integration/file" >

    <context:component-scan base-package="org.springframework.integration.examples.filetransfer.core"/>
    <beans:bean class="org.springframework.integration.aop.PublisherAnnotationBeanPostProcessor"/>

    <channel id="outboundFiles"/>

    <file:outbound-channel-adapter
            channel="outboundFiles"
            auto-create-directory="true"
            directory="#{systemProperties['user.home']}/Desktop/sales"/>

</beans:beans>

在此示例中,我们描述了一个命名通道和一个出站适配器。回想一下,出站通道是从我们之前创建的 Publisher 类中引用的。在所有示例中,当您调用writeReportToDisk方法时,它将把一个 Message 放到通道(outboundFiles)上,这些消息将一直传播到它们到达出站适配器为止。当您调用writeReportToDisk方法时,返回值(一个 String)将用作Message的有效负载,并且用@Header元素注释的两个方法参数将作为标头添加到Message中。键为FileHeaders.FILENAME@Header用于告诉出站适配器在配置的目录中写入时要使用什么文件名。如果我们没有指定它,它会为我们基于UUID合成一个。很巧妙,对吧?

FTP(文件传输协议)

FTP 是一种非常常见的文件存储方式。FTP 支持基本身份验证,因此它不是最安全的协议。它无处不在:所有操作系统都有免费客户端,事实上,许多并非技术人员的人都知道如何使用它,这使其成为集成和启用系统与客户之间文件共享的一种好方法。要在 Spring Integration 中使用 FTP 适配器,您需要告诉它如何连接到您的 FTP 服务器,并且您需要告诉它在入站情况下希望将文件下载到本地系统的哪个位置。

让我们看看如何配置 Spring Integration 以从远程 FTP 服务器接收新文件。

<?xml version="1.0" encoding="UTF-8"?>
<beans  ... xmlns:ftp="http://www.springframework.org/schema/integration/ftp">

    <context:component-scan base-package="org.springframework.integration.examples.filetransfer.core"/>
    <context:property-placeholder location="file://${user.home}/Desktop/ftp.properties" ignore-unresolvable="true"/>

    <ftp:inbound-channel-adapter
            remote-directory="${ftp.remotedir}"
            channel="ftpIn"
            auto-create-directories="true"
            host="${ftp.host}"
            auto-delete-remote-files-on-sync="false"
            username="${ftp.username}" password="${ftp.password}"
            port="2222"
            client-mode="passive-local-data-connection-mode"
            filename-pattern=".*?jpg"
            local-working-directory="#{systemProperties['user.home']}/received_ftp_files"
            >
        <int:poller fixed-rate="10000"/>
    </ftp:inbound-channel-adapter>

    <int:channel id="ftpIn"/>

    <int:service-activator input-channel="ftpIn" ref="inboundFileProcessor"/>

</beans>

您可以看到有很多选项!它们中的大多数只是可选的——但这很好,因为知道它们的存在。此适配器将下载与指定的filename-pattern匹配的文件,然后将其作为带有java.io.File作为有效负载的Message传递,就像之前一样。这就是为什么我们能够简单地重用之前的inboundFileProcessor bean。如果您想更好地控制下载的内容,请考虑使用filename-pattern指定掩码。请注意,这里有相当多的控制表面,包括对连接模式的控制以及在交付文件时是否应删除源文件。

出站适配器看起来与我们为文件支持配置的出站适配器非常相似。执行此操作时,它将编组进入它的有效负载的内容,然后将这些内容存储在 FTP 服务器上。目前,它预先支持编组Stringbyte[]java.io.File对象。

<?xml version="1.0" encoding="UTF-8"?>
<beans ... xmlns:ftp="http://www.springframework.org/schema/integration/ftp">

    <context:component-scan base-package="org.springframework.integration.examples.filetransfer.core"/>
    <context:property-placeholder location="file://${user.home}/Desktop/ftp.properties" ignore-unresolvable="true"/>

    <int:channel id="outboundFiles"/>

    <ftp:outbound-channel-adapter
            remote-directory="${ftp.remotedir}"
            channel="outboundFiles"
            host="${ftp.host}"
            username="${ftp.username}" password="${ftp.password}" port="2222"
            client-mode="passive-local-data-connection-mode"
            />
</beans>

与出站文件适配器一样,我们使用OutboundFileProducer类生成要存储的内容,因此无需对此进行审查。剩下的只是通道和适配器本身的配置,它规定了您期望看到的所有内容:服务器配置和有效负载存放的远程目录。

继续……

SSH文件传输协议(或安全文件传输协议)

最后,我们到达了SFTP适配器。这可以说是三个适配器中最复杂的配置,但也是最容易测试的一个。SFTP通常在您可以访问SSH的地方都能工作,但这并非严格限制于此。SFTP并非SSH上的FTP,而是一种完全不同的协议。它通常比SCP更普遍和一致,指定了SCP留待解释的许多内容。SFTP本身是一个相对简单的协议,因为它对它通信的连接做了很多假设:它假设——除其他事项外——客户端用户的身份已知,它是在安全通道上进行的,并且已发生身份验证。它是由设计SSH2的同一个工作组设计的,并且可以很好地用作SSH2子系统;可以想象您可以在SSH1服务器上运行SFTP。由于SFTP在提供身份验证机制的SSH之上运行,因此它支持相同的身份验证选项,包括用户名、密码和/或公钥(这些公钥本身可能可选地具有密码)。如果您运行的是相对较新的OpenSSH版本(它本身运行在AIX、HP-UX、Iris、Linux、Cygwin、Mac OSX、Solaris、SNI、Digital Unix/Tru64/OSF、NeXT(!)、SCO等等上),那么您可能已经安装了它,并且可以继续进行。换句话说,找到可以支持某种形式的SFTP的计算机比找到可以支持您可以挂载的文件系统的计算机更容易。看,我告诉过你它很容易测试!

要开始使用入站适配器,只需复制和粘贴FTP适配器,将所有FTP出现的地方重命名为SFTP,根据需要更改相关的配置值(端口、主机……),删除客户端模式选项,然后就完成了!当然还有其他选项——许多其他选项允许您限定身份验证机制;例如公钥或用户名。这是一个熟悉的示例

<?xml version="1.0" encoding="UTF-8"?>
<beans ... xmlns:sftp="http://www.springframework.org/schema/integration/sftp">

    <context:component-scan base-package="org.springframework.integration.examples.filetransfer.core"/>
    <context:property-placeholder location="file://${user.home}/Desktop/sftp.properties" ignore-unresolvable="true"/>

    <sftp:inbound-channel-adapter
            remote-directory="${sftp.remotedir}"
            channel="sftpIn"
            auto-create-directories="true"
            host="${sftp.host}"
            auto-delete-remote-files-on-sync="false"
            username="${sftp.username}"
            password="${sftp.password}"
            filename-pattern=".*?jpg"
            local-working-directory="#{systemProperties['user.home']}/received_sftp_files"
            >
        <int:poller fixed-rate="10000"/>
    </sftp:inbound-channel-adapter>

    <int:channel id="sftpIn"/>

    <int:service-activator input-channel="sftpIn" ref="inboundFileProcessor"/>

</beans>

很方便,对吧?规则与之前的示例相同:您的客户端代码将交付一个java.io.File实例,您可以根据需要对其进行处理。SFTP出站适配器完善了该集合。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns:sftp="http://www.springframework.org/schema/integration/sftp">

    <context:component-scan base-package="org.springframework.integration.examples.filetransfer.core"/>
    <context:property-placeholder location="file://${user.home}/Desktop/sftp.properties" ignore-unresolvable="true"/>

    <int:channel id="outboundFiles"/>

    <sftp:outbound-channel-adapter
            remote-directory="${sftp.remotedir}"
            channel="outboundFiles"
            host="${sftp.host}"
            username="${sftp.username}"
            password="${sftp.password}"
    />
</beans>

下一步去哪里?

思考哪些问题通常是面向文件的或批处理性质的,这是很有用的。Spring Integration在通知您世界中的有趣事件(“文件夹中放置了新文件!”)和集成数据方面做得非常出色;Spring Integration是实现事件驱动架构的好方法。但是,包含一百万行的文件不是一个事件。Spring Integration没有内置的功能来处理框架中大型批处理文件有效负载——这是Spring Batch的工作。因此,考虑一种利用Spring Integration检测文件可用性以生成作业,然后启动Spring Batch作业的方法。Spring Batch没有无法处理的巨大作业。Spring Batch可以帮助您将包含一百万条记录的文件分解成Spring Integration更乐于处理的事件大小的记录。我喜欢将这两个框架视为在事件驱动、数据处理狂热中交织起舞的舞者!

总结

在这篇文章中,我们讨论了Spring Integration中文件传输适配器的广阔世界,这些适配器使用直接文件系统挂载、FTP和SFTP使基于文件的集成变得非常轻松。

获取Spring通讯

通过Spring通讯保持联系

订阅

领先一步

VMware提供培训和认证,以加快您的进度。

了解更多

获取支持

Tanzu Spring在一个简单的订阅中提供对OpenJDK™、Spring和Apache Tomcat®的支持和二进制文件。

了解更多

即将举行的活动

查看Spring社区中所有即将举行的活动。

查看全部