做 Java 开发这些年,二维码生成这个需求基本上每个项目都躲不掉——分享链接、扫码登录、活动页面导流、固定资产生成、连接 WiFi、券码导入,后端动不动就要给前端一个二维码图片。虽然前端用 qrcode.js 也能画,但遇到服务端导出 PDF、批量生成券码、接口直接吐图片、App 消息里内嵌二维码这类场景,后端必须自己会生成。网上搜 "java 二维码生成",资料很杂,很多帖子还是十年前复制粘贴的老代码,跑起来一堆问题。这篇文章就围绕 qrgen 这个主题,也就是 Java 里最常用的二维码生成方案(底层依赖 ZXing),把最简单的实现方式、参数调优和踩坑经验一次讲清楚。
1. 需求背景与方案选型
1.1 什么时候你需要后端生成二维码
很多人第一次接触到二维码生成需求时,第一个想法是"这玩意不都是前端干的吗?"——确实,纯网页场景下,前端引入一个 qrcode.js 就解决了。但你往后做就会发现,后端生成二维码的场景其实非常硬核:
- 批量生成:给一批商品生成追溯码、给会员批量生成电子券、给线下门店生成桌贴码,数量可能是几百上千个,前端一个一个画不现实,必须后端循环生成。
- 服务端对接:需要把二维码图片塞进 PDF 导出、作为邮件附件发送、放到对象存储里返回 URL,这时候前端生成没有任何意义。
- 接口速率与权限控制:二维码内容可能包含服务端临时生成的 token 或签名,这些内容不适合暴露给前端去生成,必须在后端拼好字符串、直接出图。
- 多端复用:同一个链接要生成供 PC 网页、小程序、App 内嵌使用的二维码,后端统一出一个接口,所有端使用同一份资源,维护成本最低。
类比我自己的经历,上个月刚给一个线下活动系统做了签到码功能:管理员导入一份手机号名单,系统为每个手机号生成一个带签名的二维码,按人数批量导出成 ZIP 包,线下按手机尾号领取并扫码签到。如果靠前端生成,整个链路根本走不通。
1.2 为什么选择 ZXing / qrgen 方案
Java 生态里做二维码生成,库的选择其实很集中:ZXing(Zebra Crossing)是谷歌开源的条码/二维码解析生成库,几乎可以说是事实标准。另外有 QRGen 这类封装库,它把你经常要写的编码、纠错等级、尺寸这些配置封装成链式调用,代码更短但底层还是 ZXing。
我选择直接用 ZXing(或者用类 QRGen 的封装思路)的原因很朴素:
- 维护活跃、资料多:ZXing 从 2007 年到现在持续维护,Maven 中央仓库的坐标稳定,遇到问题随便一搜就有答案。
- 功能完整:不只是 QR 码,Code128、EAN-13、DataMatrix 等条码格式也支持,以后碰到别的需求不用换库。
- 可定制空间大:源码也不复杂,真遇到特殊需求可以读源码改逻辑,不会像某些封装库那样遇到问题只能"骂骂咧咧绕过"。
- 部署简单:两个 jar 包,没有本地依赖,不需要额外安装外部程序,纯 JDK 就能跑。
至于为什么不建议自己写二维码生成算法,一句话回答:二维码编码涉及 Reed-Solomon 纠错、掩码选择、版本切换等一整套规范,自己写大概率会踩进"手机怎么都扫不出来"的深坑里,完全没有必要。
1.3 方案落地的整体思路
这篇博文的实现思路是:用 Maven 引入 ZXing 依赖,核心代码分三步——先通过QRCodeWriter把字符串内容编码成BitMatrix(像素矩阵),再把矩阵转成BufferedImage图片对象,最后输出成文件、Base64 字符串或 HTTP 响应流。
听起来很简单,但实际写的时候有几个细节非常影响体验:字符编码设置不对会导致中文变成乱码;纠错等级选错会导致内容一多就生成失败;边距没配好会让二维码"糊成一团"扫码困难。这些细节我会在后面的章节逐个展开。
2. 开发环境与依赖引入
2.1 基础环境要求
先确认一下环境,我的实践环境是:
- JDK 8 及以上(8、11、17 都试过,没有兼容性问题)
- Maven 3.6+
- 不需要额外安装任何本地工具
JDK 8 是大多数老项目的基线,ZXing 也完全兼容,不用担心版本强制要求;如果你用的是 JDK 17,只要注意模块化相关的--add-opens参数(一般用不到)就行。
如果你还没装 Maven,也可以直接把依赖 jar 包下载后放进WEB-INF/lib,原理完全一样。但强烈建议用 Maven,毕竟后面还要加别的依赖,互相之间的关系靠 Maven 管理最省心。
2.2 引入 ZXing 依赖
ZXing 在 Maven 中央仓库里的坐标是:
<dependency> <groupId>com.google.zxing</groupId> <artifactId>core</artifactId> <version>3.5.3</version> </dependency> <dependency> <groupId>com.google.zxing</groupId> <artifactId>javase</artifactId> <version>3.5.3</version> </dependency>这里有个重要的点:必须两个依赖都引,而且版本要一致。core里是编码和解码的核心类(QRCodeWriter、BitMatrix、BarcodeFormat),而javase里才有MatrixToImageWriter这种把矩阵转成BufferedImage的工具类。如果你只引了core,编译到MatrixToImageWriter就会报错找不到类。
2.3 版本选择与兼容性说明
截至我写这篇内容,ZXing 的最新稳定版本是 3.5.3。如果你的项目里有老版本,比如 3.3.x、3.4.x,其实都能用,API 上没什么大变化。
但有几个兼容性坑需要提一下:
- 引用了多个版本的 ZXing,或者项目里其他依赖(比如某些报表库)间接带了旧版 ZXing,会导致类冲突,表现是编译正常但运行时方法签名对不上。建议在 pom 里用
dependencyManagement锁定版本。 - 如果你用的 JDK 9+ 模块化项目,
javase这个模块会自动依赖java.desktop,一般没问题。但如果你的项目是纯命令行工具,连 AWT 都没有初始化,某些环境下生成 PNG 会抛HeadlessException,需要在启动参数加-Djava.awt.headless=true。 - 如果你看网上老帖子用了
com.google.zxing:zxing-parent这种坐标,那是老写法,新版本已经拆分了,直接按我上面的坐标引就行。
3. 核心代码实现
3.1 最简版:几行代码生成第一张二维码
先把最简单的实现跑通。新建一个 Java 类,核心逻辑如下:
import com.google.zxing.BarcodeFormat; import com.google.zxing.BinaryBitmap; import com.google.zxing.MultiFormatReader; import com.google.zxing.MultiFormatWriter; import com.google.zxing.client.j2se.MatrixToImageWriter; import com.google.zxing.common.BitMatrix; import java.io.File; import java.io.OutputStream; import java.nio.file.FileSystems; import java.nio.file.Path; public class SimpleQrCode { public static void main(String[] args) throws Exception { String content = "https://example.com"; int width = 300; int height = 300; MultiFormatWriter writer = new MultiFormatWriter(); BitMatrix matrix = writer.encode(content, BarcodeFormat.QR_CODE, width, height); Path path = FileSystems.getDefault().getPath("qrcode.png"); MatrixToImageWriter.writeToPath(matrix, "PNG", path); System.out.println("二维码已生成: " + path.toAbsolutePath()); } }跑完这段代码,项目根目录下就会多出一个qrcode.png,用手机扫一下,打开的就是https://example.com。
这里用MultiFormatWriter而不是直接new QRCodeWriter(),原因是MultiFormatWriter会根据BarcodeFormat自动寻找对应的 Writer,以后如果要做条形码、Code128 等格式,同一套代码就能复用。最简版虽然能跑,但它有个隐患:没指定字符编码,如果 content 里含有中文,默认会采用 ISO-8859-1 去编字节,轻则抛异常,重则生成一个扫出来全是乱码的二维码。这个坑下一节细说。
3.2 参数配置:编码、纠错等级与边距
想在生产环境用,三个参数必须配齐,缺一个都可能出问题:
import com.google.zxing.BarcodeFormat; import com.google.zxing.EncodeHintType; import com.google.zxing.MultiFormatWriter; import com.google.zxing.common.BitMatrix; import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel; import java.util.HashMap; import java.util.Map; public class QrCodeWithHint { public static void main(String[] args) throws Exception { String content = "你好,这是一个包含中文的二维码内容"; int width = 300; int height = 300; Map<EncodeHintType, Object> hints = new HashMap<>(); // 1. 字符编码,中文内容必须指定 UTF-8 hints.put(EncodeHintType.CHARACTER_SET, "UTF-8"); // 2. 纠错等级,H 表示最高纠错级别 hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.H); // 3. 边距,单位是"模块",二维码四周留白 hints.put(EncodeHintType.MARGIN, 1); MultiFormatWriter writer = new MultiFormatWriter(); BitMatrix matrix = writer.encode(content, BarcodeFormat.QR_CODE, width, height, hints); MatrixToImageWriter.writeToPath(matrix, "PNG", new java.io.File("qrcode_hint.png").toPath()); } }逐个解释这三个参数的意义:
CHARACTER_SET(字符编码):编码器要把字符串变成字节流,再进一步变成二维码中的 0/1 模块。如果 content 里是英文和 URL,用什么编码无所谓;一旦出现中文、日文、表情符号,不指定 UTF-8 就可能乱码或直接报错IllegalArgumentException: Can't find a codec for ISO-8859-1。所以无论内容里有没有中文,我都建议无条件加上这个参数,成本为零。
ERROR_CORRECTION(纠错等级):ZXing 支持四个等级,L、M、Q、H,纠错能力分别大约能恢复 7%、15%、25%、30% 的损坏数据。等级越高,二维码越抗污损、抗遮挡,但同样内容占用的模块数也越多,生成出的二维码图案越密集。我的建议是:常规业务用 H,因为扫码场景里存在遮挡、折痕、屏幕反光的概率很大;如果二维码要印刷在很小的地方且内容很短,改用 M 可以更清晰。
MARGIN(边距):这是最容易忽略的参数。二维码规范要求四周保留至少 4 个模块的空白区域(静区),否则扫码器很难定位。ZXing 默认边距是 4,但实际生成时因为内部实现原因,边距常常比预期少,我一般直接手动设成 1 或者 2。这里要注意,这个参数的单位是"模块",不是像素,300x300 的图片、边距设成 4 时空白会占掉相当大的面积,所以设成 1 通常已经是安全的底线。
3.3 工具类封装:纯 JDK 图像处理,不依赖 javase
网上最常见的代码都用了MatrixToImageWriter,但它把像素处理"包"得太好了,导致很多人不知道 BitMatrix 到底是什么。事实上,你自己手动把 BitMatrix 画到BufferedImage上也就几行代码,而且能完全摆脱对javase模块的依赖。对于需要精简依赖的项目(比如 Android 或纯服务端),这个方案更干净。
import com.google.zxing.BarcodeFormat; import com.google.zxing.EncodeHintType; import com.google.zxing.MultiFormatWriter; import com.google.zxing.common.BitMatrix; import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel; import javax.imageio.ImageIO; import java.awt.image.BufferedImage; import java.io.ByteArrayOutputStream; import java.io.File; import java.util.HashMap; import java.util.Map; public class QrCodeUtils { public static BufferedImage createQrImage(String content, int width, int height) throws Exception { return createQrImage(content, width, height, ErrorCorrectionLevel.H); } public static BufferedImage createQrImage(String content, int width, int height, ErrorCorrectionLevel level) throws Exception { Map<EncodeHintType, Object> hints = new HashMap<>(); hints.put(EncodeHintType.CHARACTER_SET, "UTF-8"); hints.put(EncodeHintType.ERROR_CORRECTION, level); hints.put(EncodeHintType.MARGIN, 1); MultiFormatWriter writer = new MultiFormatWriter(); BitMatrix matrix = writer.encode(content, BarcodeFormat.QR_CODE, width, height, hints); // 关键点:把 BitMatrix 逐像素画到 BufferedImage 上 int[] pixels = new int[width * height]; for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { pixels[y * width + x] = matrix.get(x, y) ? 0x000000 : 0xFFFFFF; } } BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); image.setRGB(0, 0, width, height, pixels, 0, width); return image; } public static void saveToFile(BufferedImage image, String fileName) throws Exception { ImageIO.write(image, "png", new File(fileName)); } public static byte[] toPngBytes(BufferedImage image) throws Exception { ByteArrayOutputStream out = new ByteArrayOutputStream(); ImageIO.write(image, "png", out); return out.toByteArray(); } }这段代码里有一个非常容易踩的坐标坑:BitMatrix.get(x, y)的第一个参数是横坐标,第二个是纵坐标,而BufferedImage.setRGB(x, y, color)也是横纵坐标。但很多网上的老代码会把两层循环写成外层 x、内层 y,然后把像素下标写成x * width + y,这实际上是行列转置,生成的二维码手机根本扫不出来,因为二维码图案被"旋转+翻转"了。
为什么转置了扫不出来?QR 码里有三个角上的"回"字形定位图案,如果整个矩阵被转置,定位图案虽然在,但版本信息和格式信息的校验会失败,解码器直接报错。所以写循环时务必统一:外层循环用 y,内层用 x,像素数组下标用y * width + x。
这个工具类把三种输出方式都封装好了:生成BufferedImage(给接口用)、输出成文件、转成字节数组(给 Base64 用)。后面所有需求都围绕这三个方法来转。
4. 进阶美化与实际应用场景
4.1 自定义颜色与加 Logo
产品经理几乎一定会提的需求:二维码黑乎乎一片太丑了,能不能换成品牌色?能不能中间加个 Logo?
先说自定义颜色。原理很简单——之前黑色像素写的是0x000000,白色像素写的是0xFFFFFF,只要替换成你想要的数值就行。但这里有两个原则要注意:
- 前景色(二维码图案)必须深色,背景色必须浅色。扫码器靠的是明暗反差识别模块,如果前景色太浅,比如灰色、浅蓝色,反射对比度不够,手机经常扫半天扫不出来。
- 不要用纯红色作为背景色或前景色。有些扫码器在红光下对红色不敏感,实测容易失败。
常见的做法是前景用深蓝(0x1B4F72)或黑色,背景用白色或米白色。若想做得更好,可以把整张图渲染成带渐变或圆角的 PNG,但那已经不是 ZXing 的职责了,需要结合 Java 2D 的Graphics2D来做。
加 Logo 的核心思路更简单:先生成完整的二维码,再把 Logo 图片缩放到合适大小,画在二维码正中央。注意 Logo 遮挡了中心区域的数据,所以生成二维码时纠错等级至少要选 H,否则被 Logo 挡住的部分无法恢复,扫码会失败。
import java.awt.*; import java.awt.geom.RoundRectangle2D; import java.awt.image.BufferedImage; public class QrCodeWithLogo { public static BufferedImage addLogo(BufferedImage qrImage, BufferedImage logoImage) { int qrWidth = qrImage.getWidth(); int qrHeight = qrImage.getHeight(); // 计算 Logo 大小,不能超过二维码宽度的 1/5,否则遮挡数据过多 int logoSize = qrWidth / 5; int logoX = (qrWidth - logoSize) / 2; int logoY = (qrHeight - logoSize) / 2; BufferedImage combined = new BufferedImage(qrWidth, qrHeight, BufferedImage.TYPE_INT_RGB); Graphics2D g = combined.createGraphics(); g.drawImage(qrImage, 0, 0, null); g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); // 画一个白色圆角矩形垫底,再画 Logo,视觉上更干净 Shape clip = new RoundRectangle2D.Float(logoX, logoY, logoSize, logoSize, 20, 20); g.setClip(clip); g.drawImage(logoImage, logoX, logoY, logoSize, logoSize, null); g.dispose(); return combined; } public static void main(String[] args) throws Exception { String content = "https://example.com"; BufferedImage qrImage = QrCodeUtils.createQrImage(content, 400, 400); BufferedImage logo = ImageIO.read(new File("logo.png")); BufferedImage finalImage = addLogo(qrImage, logo); QrCodeUtils.saveToFile(finalImage, "qrcode_with_logo.png"); } }这里 Logo 面积我严格控制在二维码边长的五分之一以内,换算成面积占比是 4%,属于安全范围。有些人为了好看把 Logo 放得很大,结果扫码器识别不出来了——如果产品一定要大 Logo,那只能把二维码整体尺寸放大,再用 H 级纠错硬扛。
4.2 输出 Base64 与 Spring Boot 接口
实际项目里,"生成图片文件保存到硬盘"这个需求其实很少见,更多是把图片作为 HTTP 响应返回,或者转成 Base64 字符串内嵌到 JSON 里。
Base64 的好处是前端img标签直接能用,不需要二次请求:
public static String toBase64(BufferedImage image) throws Exception { byte[] bytes = toPngBytes(image); return "data:image/png;base64," + java.util.Base64.getEncoder().encodeToString(bytes); }配上 Spring Boot 接口就是一套完整的二维码服务:
@RestController @RequestMapping("/api/qrcode") public class QrCodeApi { @GetMapping(value = "/image", produces = MediaType.IMAGE_PNG_VALUE) public void qrImage(@RequestParam String content, @RequestParam(defaultValue = "300") int size, HttpServletResponse response) throws Exception { response.setContentType("image/png"); BufferedImage image = QrCodeUtils.createQrImage(content, size, size); ImageIO.write(image, "png", response.getOutputStream()); } @GetMapping("/base64") public Map<String, String> qrBase64(@RequestParam String content, @RequestParam(defaultValue = "300") int size) throws Exception { BufferedImage image = QrCodeUtils.createQrImage(content, size, size); Map<String, String> result = new HashMap<>(); result.put("qrCode", QrCodeUtils.toBase64(image)); return result; } }在写接口时有三个细节:
size参数要限制范围。如果外部调用者传入size=10,生成的图片可能小到根本没法扫;如果传入size=10000,服务器内存直接被打满。我通常会校验size在 100 到 1000 之间。content要限制长度。二维码存储容量有限,内容越长,图案越密集,扫起来越费劲。服务端最好限制一下 content 的长度,比如 500 字符以内,超出直接返回 400。- 接口要做访问速率控制。二维码接口非常容易被刷,一张大图可能消耗几 MB 内存,高并发下几百个请求就能把年轻代打爆。轻量方案是在网关或过滤器里加个简单的令牌桶,或者至少用本地缓存把相同 content 生成的二维码缓存起来。
4.3 批量生成与文件命名技巧
批量生成比单张生成多两个问题:性能和文件组织。
性能方面,ZXing 本身是纯计算,不涉及 IO,生成一张 300x300 的二维码大概在几毫秒到十几毫秒,批量生成 1 万张也没问题,瓶颈全在写文件。建议的做法是先生成BufferedImage,攒一批再用ImageIO.write落盘,不要生成一张写一张;如果数量巨大,可以考虑用固定大小线程池并行生成,比如 8 个线程,吞吐量能翻几倍。
文件组织方面,业务上经常要求"文件名可溯源",我一般把业务主键拼进文件名,比如activity_签到码_123456.png,如果平台对中文文件名敏感,就统一用拼音或纯数字:
public static void batchGenerate(List<String> contents, String outputDir) throws Exception { Files.createDirectories(Paths.get(outputDir)); for (int i = 0; i < contents.size(); i++) { BufferedImage image = QrCodeUtils.createQrImage(contents.get(i), 300, 300); String fileName = String.format("%s/%06d.png", outputDir, i + 1); ImageIO.write(image, "png", new File(fileName)); // 每 500 条打印一次进度,方便处理海量数据时掌握进度 if ((i + 1) % 500 == 0) { System.out.println("已生成: " + (i + 1)); } } }另外一个细节是:批量生成的二维码内容如果有重复,建议在内存里做一层Map<String, BufferedImage>缓存,避免重复计算同样的内容。实测在 10 万条数据、去重率 30% 的场景下,缓存能节省将近 30% 的生成时间。
5. 常见问题与排查记录
5.1 高频问题速查表
我把这几年在二维码生成上碰到的典型问题整理成了一张表,基本覆盖了日常开发的 90% 场景:
| 问题现象 | 根本原因 | 解决方案 |
|---|---|---|
| 中文内容生成时报错 "Can't find a codec" | 编码没设成 UTF-8 | hints 里加CHARACTER_SET=UTF-8 |
| 二维码生成成功但手机扫出来是乱码 | 编码方式与解码不一致 | 重新生成,指定 UTF-8,并且 content 不要提前做 URLEncoder 编码 |
| 图片越大的二维码越难扫 | 内容过长,模块过密 | 降低纠错等级到 M,或增大图片尺寸 |
| 四周留白太大,视觉上很小 | MARGIN 默认值太高 | 手动设MARGIN=1 |
| 加 Logo 后扫不出来 | Logo 面积占比过大/纠错等级太低 | Logo 控制在 1/5 边长内,纠错等级用 H |
| 自定义颜色后扫码失败 | 前景背景对比度不足 | 前景深色、背景浅色,不要用红色系 |
MatrixToImageWriter编译找不到 | 只引了 core 依赖 | 补上javase依赖 |
| 生成的二维码底色是黑色 | BitMatrix 默认 get 返回 true 表示黑色 | 用matrix.get(x, y) ? 前景色 : 背景色自行控制 |
| 扫描时识别慢,要多扫几次 | 图片尺寸过小 | 至少 200x200,建议 300x300 起步 |
服务器环境生成报HeadlessException | 无图形环境 | JVM 参数加-Djava.awt.headless=true |
5.2 几个印象深刻的排查案例
有一个案例我记得很清楚。上线前测试二维码图片,我让测试同事扫一下,结果十个二维码里有两三个扫不出来。排查了很久,最后发现是内容里带的 URL 参数中包含「?」和「&」这类特殊字符,URL 本身又被 URLEncoder 编码了一遍,导致内容里出现大量%3F、%26这样的转义字符,内容长度暴增,二维码图案变得特别密,扫码器对焦困难。
这个案例的教训是:二维码内容尽量放短链,不要直接放带长参数的长 URL。如果必须放长 URL,至少保证图片尺寸在 500x500 以上,并且使用 M 或 H 纠错。另外,内容里不要先 URLEncode——二维码图片被扫出来后,微信/支付宝这类扫码器会自动识别 URL 并跳转,不需要你自己额外编码。
还有一个案例是给客户做了个带渐变背景的二维码,设计稿上非常漂亮,但客户用手机怎么都扫不出来。后来逐项排查发现,渐变的浅色部分反射率太高,导致模块边界模糊。解决办法是保持模块区域纯深色,只在边缘区域做视觉装饰,或者干脆用"透明背景 + 深色模块"的方案,由前端把二维码贴在渐变底图上。
5.3 关于容量与尺寸的底层逻辑
很多人不理解为什么"内容一多二维码就变密"。本质上,QR 码是按「版本」区分的,版本越高,模块越多。ZXing 在encode时并不是简单地把内容缩放填充到你给的宽高,而是:先根据内容和纠错等级选定 QR 版本,确定需要多少行×多少列的模块,再把这个模块矩阵整体放大到指定的宽高。所以同样 300x300 的图片,内容短时模块稀疏、干净,内容长时模块密集、混乱。
做个小实验你就明白了:生成内容为"A"和内容为"这是一段很长很长很长……"的 300x300 图,前者图案稀疏、看起来清爽,后者几乎全是密密麻麻的小方块——所以如果内容真的长,提高图片尺寸才是关键,而不是硬扛纠错等级。
另一个实用技巧是,默认生成的二维码内容是文本,如果你希望用户扫描后直接执行"添加到联系人"这类操作,可以使用 vCard 或 WiFi 配置的固定格式文本。比如网盘分享码、WiFi 连接码,本质上都是把一段约定格式的字符串编码进二维码,ZXing 不限制内容格式,格式化工作完全是业务自己拼字符串,这也是二维码应用很灵活的地方。
6. 长期维护:从"能出图"到"好用"
6.1 我总结的四条使用经验
二维码生成表面上是个小功能,但它在生产环境里坑不少。这几年经验下来,我把使用经验总结成几条硬规矩:
- 内存和线程安全要提前规划。
MultiFormatWriter每次encode其实是新建内部对象,不会保存状态,线程安全没问题。但BufferedImage不是线程安全的,多个线程同时写同一个 Image 会导致花屏,所以批量并发生成时每个线程必须各建各的图,不要共享。 - 图片格式统一用 PNG。不要用 JPG,JPG 是有损压缩,二维码边缘会产生伪影,细小的模块可能被压缩糊掉,扫码成功率明显下降。
- 输出文件名别用 UUID 裸奔。UUID 文件名完全无法追溯业务来源,除了浪费存储,出问题还找不到对应记录。至少要拼上业务主键,比如
order_20250601_100001.png。 - 二维码内容不要放敏感信息。二维码图片被拍照、截图传播的概率很高,如果你把加密 token、内部编号直接放进去,等于把钥匙挂在门上。正确做法是内容只放一个短码或短链接,真正的业务数据放在服务端,通过短码关联查询。
6.2 扩展思路:识别与解码
生成二维码做到头以后,解码(识别)往往也躲不开。ZXing 的解码 API 同样成熟,核心代码很短:
import com.google.zxing.*; import com.google.zxing.client.j2se.BufferedImageLuminanceSource; import com.google.zxing.common.HybridBinarizer; public class QrDecoder { public static String decode(File qrFile) throws Exception { BufferedImage image = ImageIO.read(qrFile); LuminanceSource source = new BufferedImageLuminanceSource(image); BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source)); MultiFormatReader reader = new MultiFormatReader(); Result result = reader.decode(bitmap); return result.getText(); } }识别和解码的场景主要是做"扫码入库"、"防伪校验"、"自动化测试里验证生成的二维码是否正确"。在你开发完生成功能后,我强烈建议随手写一个解码工具类,专门用来做回归测试——生成一批二维码,再解码一遍,比对内容是否一致,这比肉眼盯着图片看靠谱得多。
6.3 关于未来需求升级的预判
如果你的项目可能会碰到彩色二维码、动态二维码、大量自定义样式需求,建议在架构上把生成逻辑和业务逻辑分离。核心工具类只负责"字符串 -> 图片",业务层负责拼内容、选样式、定尺寸。这样将来无论换成任何二维码生成库,改动的只是工具类内部实现,不会牵连业务代码。
另外二维码生成现在有个趋势是配合"短链 + 统计",二维码只记录一个短码,扫描后服务端重定向并做埋点统计,这样比起在二维码里直接塞长链接,既减少了码的密度,又能拿到扫描量的数据。这个方向在活动运营场景里几乎必做,建议设计接口时预留一个"内容由短码映射"的模式,而不是直接把长链接作为入参写死。
我自己最常用的一套组合就是:短链 + H 级纠错 + 300~400px 尺寸 + PNG 输出 + 白边 1 模块。这套参数在桌面显示器、手机屏幕、A4 打印纸、喷绘布这几种介质上都测试过,实测识别率最高。最后再分享一个小技巧:生成完成后,最好用手机原相机和微信各扫一次,比任何代码检查都管用——毕竟你的用户,用的就是这两个东西。