在 Java 中將 InputStream 轉換為 DataHandler
1. 引言
在 Java 中處理大型檔案時,開發人員在從輸入流讀取資料時可能會遇到記憶體限制。特別是,如何在保持記憶體效率的前提下將InputStream轉換為DataHandler是一個常見問題。
本教學將介紹如何將InputStream轉換為DataHandler 。首先,我們將了解為什麼使用InputStream從大型文件中提取內容時會出現記憶體效率低下的問題。接下來,我們將介紹getBinaryStream函數及其限制。之後,我們將詳細闡述DataHandler的工作原理。最後,我們將實作DataSource及其相關實作。
2. 理解InputStream問題
在處理文件時,常見的技術是,從資料庫檢索資料時,使用ResultSet物件的getBytes()方法將整個文件內容提取為位元組數組。此外,此位元組數組會透過接受位元組數組和 MIME 類型規範的建構子轉換為DataHandler 。
我們來看看範例程式碼:
byte[] bytes = resultSet.getBytes(1);
DataHandler dh = new DataHandler(bytes, "application/octet-stream");
雖然這種方法適用於小文件,但處理大文件時就會出現問題。具體來說,整個文件內容必須同時駐留在記憶體中。因此,隨著堆疊空間耗盡,應用程式可能會遇到OutOfMemoryError異常或嚴重的效能下降。
3. 使用getBinaryStream
解決我們所述問題的第一個方案是使用getBinaryStream將資料檢索為 ` InputStream 。透過這種方法,**我們可以存取內容而無需一次性將所有內容載入到記憶體中**。然而, DataHandler類別並沒有提供直接接受InputStream的建構子。這為實現帶來了障礙。
此外,嘗試建立可存取OutputStream的空DataHandler也失敗了,因為除非底層資料來源明確支援輸出操作,否則DataHandler方法getOutputStream通常傳回null 。
4. DataHandler的工作原理
Java Activation Framework 在建立DataHandler時,並不會提供直接接受InputStream建構子。這時就需要用到DataSource了。 DataHandler 與DataHandler DataSource配合使用,它作為一個抽象層,封裝資料並提供存取輸入和輸出流的方法。
需要注意的是,自 Java 11 起,Java 啟動框架javax.activation已不再包含在 JDK 中。因此,我們必須明確地將所需的依賴項新增至專案。
4.1. 實現DataSource
在這個例子中,我們建立一個實作DataSource介面的自訂類別。該類別提供了四個基本方法:
-
getInputStream() -
getOutputStream() -
getContentType() -
getName()
讓我們來看看InputStreamDataSource.java的程式碼:
public class InputStreamDataSource implements DataSource {
private InputStream inputStream;
public InputStreamDataSource(InputStream inputStream) {
this.inputStream = inputStream;
}
@Override
public InputStream getInputStream() throws IOException {
return inputStream;
}
@Override
public OutputStream getOutputStream() throws IOException {
throw new UnsupportedOperationException("Not implemented");
}
@Override
public String getContentType() {
return "*/*";
}
@Override
public String getName() {
return "InputStreamDataSource";
}
}
在這個例子中,我們只實作了getInputStream來示範該功能。 `getOutputStream` 方法會拋出UnsupportedOperationException異常getOutputStream因為我們展示的用例只需要讀取數據,不需要寫入數據。
4.2 DataSource使用情況
現在我們使用可重複使用的InputStreamDataSource來建立一個DataHandler 。以下範例模擬從資料庫檢索資料。我們在DataSourceDemo.java中模擬從資料庫檢索資料並建立一個DataHandler :
public class DataSourceDemo {
public static void main(String[] args) {
try {
String sampleData = "Hello from the database! This could be a large file.";
InputStream inputStream = new ByteArrayInputStream(sampleData.getBytes());
System.out.println("Step 1: Retrieved InputStream from database");
System.out.println("Data size: " + sampleData.length() + " bytes\n");
DataHandler dataHandler = new DataHandler(
new InputStreamDataSource(inputStream)
);
System.out.println("Step 2: Created DataHandler successfully!");
System.out.println("Content type: " + dataHandler.getContentType());
System.out.println("Data source name: " + dataHandler.getName() + "\n");
InputStream resultStream = dataHandler.getInputStream();
String retrievedData = new String(resultStream.readAllBytes());
System.out.println("Step 3: Retrieved data from DataHandler:");
System.out.println("\"" + retrievedData + "\"");
System.out.println("\n✓ Success! Data streamed without loading entirely into memory first.");
} catch (Exception e) {
System.err.println("Error: " + e.getMessage());
e.printStackTrace();
}
}
}
在上面的範例中,我們將InputStream封裝在InputStreamDataSource中,然後建立了一個DataHandler 。雖然readAllBytes()簡化了範例,但它會將所有內容載入到記憶體中。這違背了流式處理大型文件的初衷。在實際應用中,我們使用緩衝讀取來增量處理InputStream ,以提高記憶體效率。
接下來,我們編譯該檔案:
javac DataSourceDemo.java
java DataSourceDemo
現在輸出結果顯示串流的資料:
Step 1: Retrieved InputStream from database
Data size: 52 bytes
Step 2: Created DataHandler successfully!
Content type: */*
Data source name: InputStreamDataSource
Step 3: Retrieved data from DataHandler:
"Hello from the database! This could be a large file."
✓ Success! Data streamed without loading entirely into memory first.
關鍵在於,我們在實際環境中用真實的資料庫程式碼取代模擬程式碼。
這種方法的關鍵優勢在於資料始終保存在資料庫中,並根據需要流經應用程式。記憶體佔用量保持不變,不受檔案大小的影響,這使得應用程式能夠處理以前會導致記憶體耗盡的檔案。
4.3. 使用內容類型
雖然基本實作方式適用於唯讀場景,但某些情況下可能需要額外的功能。例如,如果需要動態指定內容類型而不是使用通配符,則可以增強InputStreamDataSource建構函數,使其接受 MIME 類型參數:
public class EnhancedDataSourceDemo {
public static void main(String[] args) {
try {
...
InputStream pdfStream = new ByteArrayInputStream("PDF content here".getBytes());
DataHandler pdfHandler = new DataHandler(
new InputStreamDataSource(pdfStream, "application/pdf")
);
System.out.println("Content type: " + pdfHandler.getContentType());
System.out.println();
}
...
}
}
透過此項修改,我們能夠精確地修改內容類型規格。這在 MIME 類型會影響行為的情況下尤其有用,例如電子郵件附件或 HTTP 回應。
5. 結論
本文介紹如何將InputStream轉換為DataHandler 。首先,我們解釋了 Java 中的記憶體效率問題。然後,我們討論了單獨使用getBytes的低效之處。最後,我們探討如何使用DataSource抽象將InputStream轉換為DataHandler 。
原始碼可在 GitHub 上找到。