(安全) 檔案傳輸,唯一的選擇……呃,複製

工程 | 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 社群所有即將舉行的活動。

檢視所有