採用 Google ZXing (Zebra Crossing) 3.x 核心庫,純 Java 實現,無原生依賴,支持 QR Code、Data Matrix、Aztec 等多種格式。
<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>
public final class QRCodeUtil {
private static final String CHARSET = "UTF-8";
private static final String FORMAT = "PNG";
private static final int QR_SIZE = 300; // 二維碼邊長
private static final int LOGO_SIZE = 60; // Logo 最大邊長
private QRCodeUtil() {}
/** 生成帶 Logo 二維碼到文件 */
public static void encode(String content, String logoPath, String destDir, boolean compressLogo)
throws Exception {
BufferedImage image = createImage(content, logoPath, compressLogo);
mkdirs(destDir);
String fileName = System.currentTimeMillis() + ".png";
ImageIO.write(image, FORMAT, new File(destDir, fileName));
}
/** 生成無 Logo 二維碼 */
public static void encode(String content, String destDir) throws Exception {
encode(content, null, destDir, false);
}
/** 生成到輸出流 */
public static void encode(String content, String logoPath, OutputStream out, boolean compressLogo)
throws Exception {
ImageIO.write(createImage(content, logoPath, compressLogo), FORMAT, out);
}
/** 核心繪製邏輯 */
private static BufferedImage createImage(String content, String logoPath, boolean compressLogo)
throws Exception {
// 1. 編碼配置
Map<EncodeHintType, Object> hints = new EnumMap<>(EncodeHintType.class);
hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.H); // 30% 容錯
hints.put(EncodeHintType.CHARACTER_SET, CHARSET);
hints.put(EncodeHintType.MARGIN, 1); // 邊距 1 模塊
// 2. 生成位矩陣
BitMatrix matrix = new MultiFormatWriter()
.encode(content, BarcodeFormat.QR_CODE, QR_SIZE, QR_SIZE, hints);
// 3. 繪製基礎二維碼
BufferedImage image = new BufferedImage(QR_SIZE, QR_SIZE, BufferedImage.TYPE_INT_RGB);
for (int x = 0; x < QR_SIZE; x++) {
for (int y = 0; y < QR_SIZE; y++) {
image.setRGB(x, y, matrix.get(x, y) ? 0xFF000000 : 0xFFFFFFFF);
}
}
// 4. 嵌入 Logo
if (StringUtils.isNotBlank(logoPath)) {
insertLogo(image, logoPath, compressLogo);
}
return image;
}
private static void insertLogo(BufferedImage qr, String logoPath, boolean compress) throws IOException {
File file = new File(logoPath);
if (!file.exists()) {
log.warn("Logo 文件不存在: {}", logoPath);
return;
}
Image logo = ImageIO.read(file);
int w = logo.getWidth(null), h = logo.getHeight(null);
if (compress) {
if (w > LOGO_SIZE) w = LOGO_SIZE;
if (h > LOGO_SIZE) h = LOGO_SIZE;
logo = logo.getScaledInstance(w, h, Image.SCALE_SMOOTH);
}
// 繪製到中心
Graphics2D g = qr.createGraphics();
int x = (QR_SIZE - w) / 2;
int y = (QR_SIZE - h) / 2;
g.drawImage(logo, x, y, w, h, null);
// 可選:白底圓角邊框
g.setStroke(new BasicStroke(3f));
g.draw(new RoundRectangle2D.Float(x, y, w, h, 6, 6));
g.dispose();
}
/** 解析二維碼 */
public static String decode(File file) throws Exception {
BufferedImage img = ImageIO.read(file);
if (img == null) return null;
LuminanceSource source = new BufferedImageLuminanceSource(img);
BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));
Map<DecodeHintType, Object> hints = new EnumMap<>(DecodeHintType.class);
hints.put(DecodeHintType.CHARACTER_SET, CHARSET);
Result result = new MultiFormatReader().decode(bitmap, hints);
return result.getText();
}
public static String decode(String path) throws Exception {
return decode(new File(path));
}
private static void mkdirs(String dir) {
File f = new File(dir);
if (!f.exists()) f.mkdirs();
}
}
ZXing 3.x 需要實現 LuminanceSource 接口讀取圖片像素,完整代碼見原文,核心是將 BufferedImage 轉灰度像素數組。
public static void main(String[] args) throws Exception {
String url = "https://www.example.com";
// 1. 純二維碼
QRCodeUtil.encode(url, "d:/qrcode/plain");
// 2. 帶 Logo 二維碼
QRCodeUtil.encode(url, "d:/logo.png", "d:/qrcode/with_logo", true);
// 3. 解析驗證
File testFile = new File("d:/qrcode/with_logo/最新生成的文件.png");
System.out.println("解析結果: " + QRCodeUtil.decode(testFile));
}
| 參數 | 推薦值 | 影響 |
|---|---|---|
| ErrorCorrectionLevel | H (30%) | 容錯率高,遮蓋 Logo 後仍可掃描;代價是存儲內容略減 |
| MARGIN | 1~2 | 邊距過大浪費空間,過小可能識別困難 |
| QR_SIZE | 300~500 | 打印場景建議 ≥ 400px,純屏幕展示 300 足夠 |
| LOGO_SIZE | QR_SIZE / 5 | 超過 1/3 會導致掃描失敗,圓角邊框可提升識別率 |
CHARSET = UTF-8。image.flush()、分批處理、避免同時持有大量 BufferedImage。@RestController
@RequestMapping("/api/qrcode")
public class QRCodeController {
@GetMapping("/generate")
public void generate(@RequestParam String content,
@RequestParam(required = false) String logo,
HttpServletResponse resp) throws Exception {
resp.setContentType("image/png");
QRCodeUtil.encode(content, logo, resp.getOutputStream(), true);
}
@PostMapping("/decode")
public String decode(@RequestParam("file") MultipartFile file) throws Exception {
return QRCodeUtil.decode(file.getInputStream()); // 需擴展支持 InputStream
}
}