ZXing(Zebra Crossing)是 Java 生態系最成熟的條碼/QR Code 處理庫。本文提供生產級三層架構實作:表單驗證層(參數綁定與校驗)、工具核心層(編碼、去白邊、圖片輸出)、控制器層(HTTP 回應處理、資源釋放),並解析關鍵技術細節與擴展建議。
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>core</artifactId>
<version>3.5.1</version> <!-- 請使用最新版 -->
</dependency>
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>javase</artifactId>
<version>3.5.1</version>
</dependency>
core 為純 Java 邏輯,javase 提供 BufferedImage 整合與 MatrixToImageWriter 便利類。
public class QRCodeRequestForm extends BaseForm {
private int width = 100;
private int height = 100;
private String imageType = "png";
@NotBlank // 或自訂 @VNotEmpty
private String content;
// Getter/Setter 省略...
}
設計要點:
@NotBlank 確保內容非空,避免生成空碼。BaseForm 統一驗證錯誤處理(如統一回傳 JSON 錯誤碼)。
@Component // 註冊為 Spring Bean,支援注入
public class QRCodeUtil {
private static final Logger log = LoggerFactory.getLogger(QRCodeUtil.class);
public static void encode(String contents, int width, int height, String format, OutputStream output) {
Map<EncodeHintType, Object> hints = new EnumMap<>(EncodeHintType.class);
hints.put(EncodeHintType.CHARACTER_SET, "UTF-8");
hints.put(EncodeHintType.MARGIN, 1); // 邊距最小化
try {
BitMatrix bitMatrix = new MultiFormatWriter()
.encode(contents, BarcodeFormat.QR_CODE, width, height, hints);
// 1. 去白邊:計算實際內容邊界
int[] rect = bitMatrix.getEnclosingRectangle();
if (rect != null) {
int resWidth = rect[2] + 1;
int resHeight = rect[3] + 1;
BitMatrix trimmed = new BitMatrix(resWidth, resHeight);
for (int x = 0; x < resWidth; x++) {
for (int y = 0; y < resHeight; y++) {
if (bitMatrix.get(x + rect[0], y + rect[1])) {
trimmed.set(x, y);
}
}
}
bitMatrix = trimmed;
}
// 2. 輸出圖片
writeToStream(bitMatrix, format, output);
} catch (Exception e) {
log.error("QRCode 生成失敗: {}", contents, e);
throw new RuntimeException("QRCode 生成異常", e);
}
}
private static BufferedImage toBufferedImage(BitMatrix matrix) {
int w = matrix.getWidth(), h = matrix.getHeight();
BufferedImage img = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
for (int x = 0; x < w; x++) {
for (int y = 0; y < h; y++) {
img.setRGB(x, y, matrix.get(x, y) ? 0x000000 : 0xFFFFFF);
}
}
return img;
}
private static void writeToStream(BitMatrix matrix, String format, OutputStream os) throws IOException {
BufferedImage img = toBufferedImage(matrix);
if (!ImageIO.write(img, format, os)) {
throw new IOException("不支援的圖片格式: " + format);
}
}
}
getEnclosingRectangle() 回傳 [left, top, width, height],僅複製有內容區域,顯著縮小圖片體積、提升掃描辨識率。CHARACTER_SET = UTF-8,支援中文、Emoji 等多語言內容。EncodeHintType.ERROR_CORRECTION 設定 L/M/Q/H 四級,預設 L(7% 修正),行銷場景建議 M(15%)。
@RestController
@RequestMapping("/api/qrcode")
public class QRCodeController {
private static final Logger log = LoggerFactory.getLogger(QRCodeController.class);
@GetMapping("/generate")
public void generate(HttpServletResponse response, @Valid QRCodeRequestForm form) {
// 禁用快取,確保即時生成
response.setHeader("Pragma", "No-cache");
response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
response.setDateHeader("Expires", 0);
response.setContentType("image/" + form.getImageType());
try (OutputStream os = response.getOutputStream()) {
QRCodeUtil.encode(form.getContent(), form.getWidth(), form.getHeight(), form.getImageType(), os);
os.flush();
} catch (IOException e) {
log.error("QRCode 輸出異常", e);
// 此處無法再寫入 JSON 錯誤,因回應已提交;建議前端處理圖片載入失敗
}
}
}
try-with-resources 自動關閉 OutputStream,避免洩漏。Content-Type: image/png 等正確 MIME,瀏覽器可直接 <img src="/api/qrcode/generate?content=..."> 嵌入。Graphics2D 繪製中央 Logo,需調高錯誤修正等級至 H(30%)。CompletableFuture 或訊息佇列異步生成,回傳任務 ID 輪詢結果。data:image/png;base64,... 字串。| 現象 | 原因 | 解決 |
|---|---|---|
| 中文內容掃描亂碼 | 未設定 CHARACTER_SET=UTF-8 | hints.put(EncodeHintType.CHARACTER_SET, "UTF-8") |
| 圖片四周留白過大 | 預設 MARGIN=4 | hints.put(EncodeHintType.MARGIN, 1) 或去白邊邏輯 |
| OutputStream 已關閉異常 | 重複 flush/close 或回應已提交 | try-with-resources 統一管理 |
| 生成圖片模糊 | 寬高過小或壓縮失真 | 最小建議 200x200,format=png 無損 |