寫入檔案

要將訊息寫入檔案系統,可以使用 FileWritingMessageHandler。此類可以處理以下負載型別:

  • File

  • String

  • 位元組陣列

  • InputStream(自 版本 4.2 起)

對於 String 負載,可以配置編碼和字元集。

為了簡化,您可以使用 XML 名稱空間將 FileWritingMessageHandler 配置為出站通道介面卡或出站閘道器的一部分。

從版本 4.3 開始,可以指定寫入檔案時使用的緩衝區大小。

從版本 5.1 開始,可以提供一個 BiConsumer<File, Message<?>> 型別的 newFileCallback,如果您使用 FileExistsMode.APPENDFileExistsMode.APPEND_NO_FLUSH 模式且需要建立新檔案時,將觸發此回撥。此回撥接收新建立的檔案以及觸發它的訊息。例如,此回撥可用於寫入訊息頭中定義的 CSV 標題。

生成檔名

最簡單的形式下,FileWritingMessageHandler 只需要一個目標目錄來寫入檔案。待寫入檔案的名稱由處理器的 FileNameGenerator 決定。預設實現查詢鍵與常量 FileHeaders.FILENAME 匹配的訊息頭。

或者,可以指定一個表示式,針對訊息進行評估以生成檔名——例如,headers['myCustomHeader'] + '.something'。該表示式必須評估為一個 String。為了方便起見,DefaultFileNameGenerator 還提供了 setHeaderName 方法,允許您明確指定要用作檔名的訊息頭的值。

設定好後,DefaultFileNameGenerator 採用以下解析步驟來確定給定訊息負載的檔名:

  1. 針對訊息評估表示式,如果結果是非空的 String,則將其用作檔名。

  2. 否則,如果負載是 java.io.File,則使用 File 物件的檔名。

  3. 否則,使用訊息 ID 並附加 .msg 作為檔名。

使用 XML 名稱空間支援時,檔案出站通道介面卡和檔案出站閘道器都支援以下互斥的配置屬性:

  • filename-generator(對 FileNameGenerator 實現的引用)

  • filename-generator-expression(評估為 String 的表示式)

寫入檔案時,會使用臨時檔案字尾(預設值為 .writing)。它會在檔案寫入期間附加到檔名。要自定義字尾,可以在檔案出站通道介面卡和檔案出站閘道器上設定 temporary-file-suffix 屬性。

使用 APPEND 檔案 mode 時,temporary-file-suffix 屬性將被忽略,因為資料直接附加到檔案中。

從版本 4.2.5 開始,生成的檔名(由 filename-generatorfilename-generator-expression 評估產生)可以表示一個子路徑以及目標檔名。它像以前一樣用作 File(File parent, String child) 的第二個建構函式引數。但是,過去我們不會為子路徑建立 (mkdirs()) 目錄,僅假定只有檔名。這種方法對於需要恢復檔案系統樹以匹配源目錄的情況非常有用——例如,解壓存檔並將所有檔案按原始順序儲存在目標目錄中時。

指定輸出目錄

檔案出站通道介面卡和檔案出站閘道器都提供了兩個互斥的配置屬性來指定輸出目錄:

  • directory

  • directory-expression

Spring Integration 2.2 引入了 directory-expression 屬性。

使用 directory 屬性

使用 directory 屬性時,輸出目錄被設定為固定值,該值在 FileWritingMessageHandler 初始化時設定。如果未指定此屬性,則必須使用 directory-expression 屬性。

使用 directory-expression 屬性

如果希望獲得完整的 SpEL 支援,可以使用 directory-expression 屬性。該屬性接受一個 SpEL 表示式,該表示式會針對處理的每條訊息進行評估。因此,動態指定輸出檔案目錄時,您可以完全訪問訊息的負載及其頭。

SpEL 表示式必須解析為 Stringjava.io.Fileorg.springframework.core.io.Resource。(後者無論如何都會評估為 File。)此外,結果的 StringFile 必須指向一個目錄。如果未指定 directory-expression 屬性,則必須設定 directory 屬性。

使用 auto-create-directory 屬性

預設情況下,如果目標目錄不存在,將自動建立相應的目標目錄和所有不存在的父目錄。為了阻止這種行為,可以將 auto-create-directory 屬性設定為 false。此屬性適用於 directorydirectory-expression 屬性。

當使用 directory 屬性且 auto-create-directoryfalse 時,自 Spring Integration 2.2 起進行了以下更改:

現在不是在介面卡初始化時檢查目標目錄是否存在,而是針對處理的每條訊息執行此檢查。

此外,如果 auto-create-directorytrue,並且目錄在處理訊息之間被刪除,則會為處理的每條訊息重新建立該目錄。

處理已存在的目錄檔案

寫入檔案時,如果目標檔案已存在,預設行為是覆蓋該目標檔案。可以透過在相關的檔案出站元件上設定 mode 屬性來更改此行為。存在以下選項:

  • REPLACE(預設)

  • REPLACE_IF_MODIFIED

  • APPEND

  • APPEND_NO_FLUSH

  • FAIL

  • IGNORE

Spring Integration 2.2 引入了 mode 屬性和 APPENDFAILIGNORE 選項。
REPLACE

如果目標檔案已存在,則會被覆蓋。如果未指定 mode 屬性,這是寫入檔案時的預設行為。

REPLACE_IF_MODIFIED

如果目標檔案已存在,則僅在最後修改時間戳與原始檔不同時才會被覆蓋。對於 File 負載,會將負載的 lastModified 時間與現有檔案進行比較。對於其他負載,會將 FileHeaders.SET_MODIFIED (file_setModified) 頭與現有檔案進行比較。如果頭缺失或其值不是 Number 型別,則總是替換檔案。

APPEND

此模式允許您將訊息內容追加到現有檔案,而不是每次都建立一個新檔案。請注意,此屬性與 temporary-file-suffix 屬性互斥,因為它將內容追加到現有檔案時,介面卡不再使用臨時檔案。檔案在每條訊息處理後關閉。

APPEND_NO_FLUSH

此選項與 APPEND 具有相同的語義,但資料不會被重新整理,並且檔案在每條訊息處理後不會關閉。這可以顯著提升效能,但也存在失敗時資料丟失的風險。有關更多資訊,請參閱 使用 APPEND_NO_FLUSH 時重新整理檔案

FAIL

如果目標檔案存在,則會丟擲 MessageHandlingException

IGNORE

如果目標檔案存在,則靜默忽略訊息負載。

使用臨時檔案字尾(預設為 .writing)時,如果最終檔名或臨時檔名已存在,則應用 IGNORE 選項。

使用 APPEND_NO_FLUSH 時重新整理檔案

APPEND_NO_FLUSH 模式在版本 4.3 中新增。使用它可以提高效能,因為檔案在每條訊息處理後不會關閉。但是,這可能會導致在發生故障時丟失資料。

Spring Integration 提供了幾種重新整理策略來減輕此資料丟失的風險:

  • 使用 flushInterval。如果檔案在此期間沒有被寫入,則會自動重新整理。這是近似值,可能達到此時間的 1.33 倍(平均 1.167 倍)。

  • 向訊息處理器的 trigger 方法傳送包含正則表示式的訊息。與模式匹配的絕對路徑檔案將被重新整理。

  • 為處理器提供自定義的 MessageFlushPredicate 實現,以修改將訊息傳送到 trigger 方法時採取的操作。

  • 透過傳入自定義的 FileWritingMessageHandler.FlushPredicateFileWritingMessageHandler.MessageFlushPredicate 實現,呼叫處理器的其中一個 flushIfNeeded 方法。

謂詞會為每個開啟的檔案呼叫。有關這些介面的更多資訊,請參閱 Javadoc。請注意,自版本 5.0 起,謂詞方法提供另一個引數:如果是新檔案或之前已關閉的檔案,則表示當前檔案首次寫入的時間。

使用 flushInterval 時,間隔時間從最後一次寫入開始計算。檔案只有在空閒達到指定間隔時間後才會被重新整理。從版本 4.3.7 開始,可以設定一個額外的屬性 (flushWhenIdle) 為 false,這意味著間隔時間從首次寫入之前已重新整理(或新)的檔案開始計算。

檔案時間戳

預設情況下,目標檔案的 lastModified 時間戳是檔案建立的時間(但原地重新命名會保留當前時間戳)。從版本 4.3 開始,現在可以配置 preserve-timestamp(或使用 Java 配置時設定 setPreserveTimestamp(true))。對於 File 負載,這會將時間戳從入站檔案傳輸到出站檔案(無論是否需要複製)。對於其他負載,如果存在 FileHeaders.SET_MODIFIED 頭 (file_setModified),只要該頭是 Number 型別,就用它來設定目標檔案的 lastModified 時間戳。

檔案許可權

從版本 5.0 開始,在支援 Posix 許可權的檔案系統上寫入檔案時,可以在出站通道介面卡或閘道器上指定這些許可權。該屬性是一個整數,通常以熟悉的八進位制格式提供——例如,0640,意味著所有者具有讀/寫許可權,組具有隻讀許可權,而其他人沒有訪問許可權。

檔案出站通道介面卡

以下示例配置了一個檔案出站通道介面卡:

<int-file:outbound-channel-adapter id="filesOut" directory="${input.directory.property}"/>

基於名稱空間的配置還支援一個 delete-source-files 屬性。如果設定為 true,則在寫入目標後觸發刪除原始原始檔。該標誌的預設值為 false。以下示例展示瞭如何將其設定為 true

<int-file:outbound-channel-adapter id="filesOut"
    directory="${output.directory}"
    delete-source-files="true"/>
delete-source-files 屬性僅在入站訊息具有 File 負載,或者 FileHeaders.ORIGINAL_FILE 頭的值包含源 File 例項或表示原始檔案路徑的 String 時生效。

從版本 4.2 開始,FileWritingMessageHandler 支援一個 append-new-line 選項。如果設定為 true,則在寫入訊息後向檔案追加新行。該屬性的預設值為 false。以下示例展示瞭如何使用 append-new-line 選項:

<int-file:outbound-channel-adapter id="newlineAdapter"
	append-new-line="true"
    directory="${output.directory}"/>

出站閘道器

在需要根據寫入的檔案繼續處理訊息的情況下,可以使用 outbound-gateway 代替。它的作用類似於 outbound-channel-adapter。然而,在寫入檔案後,它還會將檔案作為訊息負載傳送到回覆通道。

以下示例配置了一個出站閘道器:

<int-file:outbound-gateway id="mover" request-channel="moveInput"
    reply-channel="output"
    directory="${output.directory}"
    mode="REPLACE" delete-source-files="true"/>

如前所述,您還可以指定 mode 屬性,該屬性定義瞭如何處理目標檔案已存在的情況。有關更多詳細資訊,請參閱 處理已存在的目錄檔案。通常,使用檔案出站閘道器時,結果檔案將作為訊息負載在回覆通道上返回。

指定 IGNORE 模式時也適用此情況。在這種情況下,返回預先存在的目標檔案。如果請求訊息的負載是檔案,您仍然可以透過訊息頭訪問原始檔案。請參閱 FileHeaders.ORIGINAL_FILE

在需要先移動檔案然後再將其傳送到處理管道的情況下,'outbound-gateway' 工作良好。在這種情況下,您可以將檔案名稱空間的 inbound-channel-adapter 元素連線到 outbound-gateway,然後將該閘道器的 reply-channel 連線到管道的開頭。

如果您有更復雜的需求或需要支援其他負載型別作為輸入以轉換為檔案內容,可以擴充套件 FileWritingMessageHandler,但更好的選擇是依賴於 Transformer

使用 Java 配置進行配置

以下 Spring Boot 應用展示瞭如何使用 Java 配置配置入站介面卡的示例:

@SpringBootApplication
@IntegrationComponentScan
public class FileWritingJavaApplication {

    public static void main(String[] args) {
        ConfigurableApplicationContext context =
                      new SpringApplicationBuilder(FileWritingJavaApplication.class)
                              .web(false)
                              .run(args);
             MyGateway gateway = context.getBean(MyGateway.class);
             gateway.writeToFile("foo.txt", new File(tmpDir.getRoot(), "fileWritingFlow"), "foo");
    }

    @Bean
    @ServiceActivator(inputChannel = "writeToFileChannel")
    public MessageHandler fileWritingMessageHandler() {
         Expression directoryExpression = new SpelExpressionParser().parseExpression("headers.directory");
         FileWritingMessageHandler handler = new FileWritingMessageHandler(directoryExpression);
         handler.setFileExistsMode(FileExistsMode.APPEND);
         return handler;
    }

    @MessagingGateway(defaultRequestChannel = "writeToFileChannel")
    public interface MyGateway {

        void writeToFile(@Header(FileHeaders.FILENAME) String fileName,
                       @Header(FileHeaders.FILENAME) File directory, String data);

    }
}

使用 Java DSL 進行配置

以下 Spring Boot 應用展示瞭如何使用 Java DSL 配置入站介面卡的示例:

@SpringBootApplication
public class FileWritingJavaApplication {

    public static void main(String[] args) {
        ConfigurableApplicationContext context =
                 new SpringApplicationBuilder(FileWritingJavaApplication.class)
                         .web(false)
                         .run(args);
        MessageChannel fileWritingInput = context.getBean("fileWritingInput", MessageChannel.class);
        fileWritingInput.send(new GenericMessage<>("foo"));
    }

    @Bean
   	public IntegrationFlow fileWritingFlow() {
   	    return IntegrationFlow.from("fileWritingInput")
   		        .enrichHeaders(h -> h.header(FileHeaders.FILENAME, "foo.txt")
   		                  .header("directory", new File(tmpDir.getRoot(), "fileWritingFlow")))
   	            .handle(Files.outboundGateway(m -> m.getHeaders().get("directory")))
   	            .channel(MessageChannels.queue("fileWritingResultChannel"))
   	            .get();
    }

}