(安全) 檔案傳輸,唯一的“飛行”... 或者說是“複製”方式

工程 | 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 activators)。你只需配置一個 <service-activator> 元素,並告訴它你要使用哪個 bean 來處理 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 元素告訴介面卡每隔 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 的檔案,然後將其作為 payload 為 java.io.FileMessage 傳遞,就像之前一樣。這就是為什麼我們可以簡單地重用之前的 inboundFileProcessor bean。如果你想進一步控制哪些檔案被下載,哪些不被下載,可以考慮使用 filename-pattern 指定一個掩碼。請注意,這裡暴露了相當多的控制選項,包括連線模式的控制以及檔案交付後是否刪除原始檔。

出站介面卡看起來與我們為檔案支援配置的出站介面卡非常相似。執行時,它將整理傳入 payload 的內容,然後將這些內容儲存在 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 類生成要儲存的內容,因此無需回顧這部分。剩下的就是通道和介面卡本身的配置,其中規定了你期望看到的所有內容:伺服器配置以及用於存放 payload 的遠端目錄。

繼續....

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,酌情更改相關的配置值(埠、主機等),刪除 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 社群所有即將發生的活動。

檢視全部