資料庫 BLOB 型別存儲圖片二進位資料,Java 讀取後需高效轉為前端可用格式。本文示範三種主流方案:JDBC InputStream 直輸 Servlet 輸出流、Base64 編碼嵌入 HTML/JSON、Spring MVC ResponseEntity/StreamingResponseBody 響應式輸出,並解決大圖記憶體溢出、連接洩漏、快取頭部設置等生產問題。
CREATE TABLE product_image (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
product_id BIGINT NOT NULL,
image_data LONGBLOB NOT NULL, -- MySQL 大圖用 LONGBLOB (4GB)
mime_type VARCHAR(50) NOT NULL, -- image/jpeg, image/png, image/webp
file_name VARCHAR(200),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
@WebServlet("/image/raw/*")
public class RawImageServlet extends HttpServlet {
private final DataSource dataSource;
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
String pathInfo = req.getPathInfo(); // /123
if (pathInfo == null || pathInfo.length() <= 1) {
resp.sendError(HttpServletResponse.SC_NOT_FOUND);
return;
}
Long id = Long.valueOf(pathInfo.substring(1));
String sql = "SELECT image_data, mime_type FROM product_image WHERE id = ?";
try (Connection conn = dataSource.getConnection();
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setLong(1, id);
try (ResultSet rs = ps.executeQuery()) {
if (!rs.next()) {
resp.sendError(HttpServletResponse.SC_NOT_FOUND);
return;
}
// 關鍵:getBinaryStream() 返回 InputStream,避免 getBytes() 全量載入記憶體
try (InputStream is = rs.getBinaryStream("image_data")) {
resp.setContentType(rs.getString("mime_type"));
resp.setHeader("Cache-Control", "public, max-age=31536000"); // 1 年快取
resp.setContentLengthLong(rs.getLong("content_length")); // 若表有長度欄位
// 零拷貝輸出 (Servlet 3.1+ 支援)
is.transferTo(resp.getOutputStream());
}
}
} catch (SQLException e) {
throw new UncheckedSQLException(e);
}
}
}
@RestController
@RequestMapping("/api/images")
public class ImageApiController {
private final JdbcTemplate jdbcTemplate;
@GetMapping("/{id}/base64")
public Map getBase64(@PathVariable Long id) {
String sql = "SELECT image_data, mime_type, file_name FROM product_image WHERE id = ?";
return jdbcTemplate.queryForObject(sql, (rs, rowNum) -> {
byte[] bytes = rs.getBytes("image_data"); // 小圖才用 getBytes
String base64 = Base64.getEncoder().encodeToString(bytes);
String dataUri = "data:" + rs.getString("mime_type") + ";base64," + base64;
return Map.of(
"id", id,
"dataUri", dataUri, // 直接用於
"fileName", rs.getString("file_name"),
"size", bytes.length
);
}, id);
}
}
⚠️ **限制**:Base64 體積膨脹 33%,大圖會導致 JSON 過大、記憶體飆升、傳輸緩慢。**僅建議 < 50KB 圖標/縮圖使用**。
@RestController
@RequestMapping("/images")
public class ReactiveImageController {
private final DataSource dataSource;
@GetMapping("/{id}")
public ResponseEntity streamImage(@PathVariable Long id) {
String sql = "SELECT image_data, mime_type FROM product_image WHERE id = ?";
return jdbcTemplate.query(sql, ps -> ps.setLong(1, id), rs -> {
if (!rs.next()) {
return ResponseEntity.notFound().build();
}
final InputStream is = rs.getBinaryStream("image_data");
final String mimeType = rs.getString("mime_type");
StreamingResponseBody body = out -> {
try (is; out) {
is.transferTo(out); // 非阻塞、背壓感知
}
};
return ResponseEntity.ok()
.contentType(MediaType.parseMediaType(mimeType))
.header("Cache-Control", "public, max-age=31536000")
.body(body);
});
}
}
優勢:StreamingResponseBody 整合 Spring MVC 異步 Servlet 支援,配合 Tomcat NIO / Undertow 非阻塞 IO,單執行緒可支撐數萬併發圖片下載。
spark-md5 計算分片哈希 → 並發上傳分片 → 合併通知Files.copy(part, merged, StandardCopyOption.REPLACE_EXISTING) → 寫入 BLOB 或對象存儲 (S3/MinIO)| 坑點 | 後果 | 解決 |
|---|---|---|
用 getBytes() 讀大圖 | Heap OOM | 必須用 getBinaryStream() + transferTo() |
未關閉 ResultSet/Connection | 連接池洩漏、資料庫連接耗盡 | try-with-resources 強制關閉 |
未設 Content-Type | 瀏覽器無法正確渲染/下載 | 從 DB 讀 mime_type 動態設置 |
| 無快取頭部 | 重複請求壓垮 DB/頻寬 | Cache-Control: public, max-age=31536000 + ETag/Last-Modified |
| BLOB 存超大圖 (>100MB) | 備份慢、遷移難、GC 壓力大 | 遷移至對象存儲 (S3/MinIO/OSS),DB 存預簽名 URL |