寫入檔案

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

  • File

  • String

  • byte array

  • 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. 否則,使用附加 .msg 的訊息 ID 作為檔案名稱。

當您使用 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 運算式必須解析為 String、java.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

REPLACE_IF_MODIFIED

APPEND

如果目標檔案已存在,則僅當上次修改的時間戳記與來源檔案的時間戳記不同時,才會覆寫它。對於 File 酬載,酬載 lastModified 時間會與現有檔案進行比較。對於其他酬載,FileHeaders.SET_MODIFIED (file_setModified) 標頭會與現有檔案進行比較。如果標頭遺失或值不是 Number,則始終會替換檔案。

APPEND_NO_FLUSH

APPEND

FAIL

此模式可讓您將訊息內容附加到現有檔案,而不是每次都建立新檔案。請注意,此屬性與 temporary-file-suffix 屬性互斥,因為當它將內容附加到現有檔案時,配接器不再使用臨時檔案。檔案會在每個訊息後關閉。

IGNORE

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,這表示間隔從首次寫入先前刷新的(或新的)檔案開始。

檔案時間戳記

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

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

<int-file:outbound-channel-adapter id="filesOut"
    directory="${output.directory}"
    delete-source-files="true"/>
檔案權限

從版本 5.0 開始,當將檔案寫入支援 Posix 權限的檔案系統時,您可以在輸出通道配接器或閘道器上指定這些權限。該屬性是整數,通常以熟悉的八進位格式提供,例如 0640,表示擁有者具有讀/寫權限,群組具有唯讀權限,而其他人則沒有存取權。

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

檔案輸出通道配接器

以下範例設定檔案輸出通道配接器

基於命名空間的設定也支援 delete-source-files 屬性。如果設定為 true,則會在寫入目的地後觸發刪除原始來源檔案。該標誌的預設值為 false。以下範例示範如何將其設定為 true

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

delete-source-files 屬性僅在輸入訊息具有 File 酬載,或 FileHeaders.ORIGINAL_FILE 標頭值包含來源 File 實例或表示原始檔案路徑的 String 時才生效。

從版本 4.2 開始,FileWritingMessageHandler 支援 append-new-line 選項。如果設定為 true,則在寫入訊息後,會在檔案末尾附加新行。預設屬性值為 false。以下範例示範如何使用 append-new-line 選項

輸出閘道器

在您想要根據寫入的檔案繼續處理訊息的情況下,可以使用 outbound-gateway 來代替。它扮演的角色與 outbound-channel-adapter 類似。但是,在寫入檔案後,它也會將其作為訊息的酬載傳送到回覆通道。

以下範例設定輸出閘道器

如前所述,您也可以指定 mode 屬性,該屬性定義如何處理目的地檔案已存在的情況的行為。如需更多詳細資訊,請參閱處理現有的目的地檔案。一般而言,當使用檔案輸出閘道器時,結果檔案會作為回覆通道上的訊息酬載傳回。

@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);

    }
}

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

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

@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();
    }

}