(安全) 文件传输,唯一的选择……呃,复制

工程 | 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 上下文中安装了一个 Spring `BeanPostProcessor`。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 会转发一个 `Message`,其有效负载是添加到文件的 `java.io.File` 引用。

您可以使用 `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` 元素告诉适配器每 10000 毫秒(即 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` 匹配的文件,然后像以前一样将它们作为 `Message` 提供,其有效负载为 `java.io.File`。这就是为什么我们能够简单地重用之前的 `inboundFileProcessor` bean。如果您想更精细地控制哪些文件被下载,可以考虑使用 `filename-pattern` 来指定一个掩码。请注意,这里暴露了相当多的控制选项,包括对连接模式的控制,以及在文件交付时是否应删除源文件。

出站适配器看起来与我们为文件支持配置的出站适配器非常相似。当执行此操作时,它将对进入它的有效负载的内容进行封送处理,然后将这些内容存储在 FTP 服务器上。目前,它为封送处理 `String`、`byte[]` 和 `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 适配器。这可以说是这 3 个适配器中最复杂的配置,但也是最容易测试的之一。SFTP 通常在任何有 SSH 访问的地方都能工作,尽管它严格来说并不局限于此。SFTP 不是通过 SSH 进行 FTP,而是一种完全不同的协议。它通常比 SCP 更普及和一致,它规定了 SCP 留给解释的许多内容。SFTP 本身是一个相对精简的协议,因为它对通信的连接做了很多假设:它假设——除其他外——客户端用户的身份是已知的,通信是在安全通道上进行的,并且已经完成了身份验证。它由设计 SSH2 的同一个工作组设计,并作为 SSH2 子系统运行良好;可以想象您可以在 SSH1 服务器上运行 SFTP。因为 SFTP 在 SSH 之上运行,而 SSH 提供了身份验证机制,所以它支持相同的身份验证选项,包括用户名、密码,以及/或公钥(它们本身可能可选地带有密码)。如果您运行的是相对较新版本的 OpenSSH(它本身运行在 AIX、HP-UX、Iris、Linux、Cygwin、Mac OSX、Solaris、SNI、Digital Unix/Tru64/OSF、NeXT (!)、SCO 等系统上),那么您很可能已经安装了它,并且可以继续进行。换句话说,找到一个支持某种形式 SFTP 的计算机比找到一个支持您可以挂载的文件系统的计算机更容易。看,我告诉过您测试起来会很简单!

要开始使用入站适配器,只需复制并粘贴 FTP 适配器,将所有出现的 FTP 替换为 SFTP,更改相关的配置值(如端口、主机...),删除 `client-mode` 选项,然后您就完成了!当然还有其他选项——大量其他选项可以让你限定你的身份验证机制;例如,公钥或用户名。这是一个熟悉的例子。

<?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 社区所有即将举行的活动。

查看所有