Java批量生成资产标签PDF:itextpdf配ZXing二维码实战指南

发布时间:2026/10/15 5:38:22
Java批量生成资产标签PDF:itextpdf配ZXing二维码实战指南
上周有位朋友微信找我说单位三百多台仪器每年盘点都要两个人蹲在地上逐个抄标签标签还是买设备时经销商送的掉了三年没人补。他问我网上说的“用itextpdf 配合 google.zxing 批量生成资产标签PDF”到底能不能直接用能而且我认为这是资产管理里最该先落地的小工具。我先后在两个项目里用这套组合批量输出过带二维码的资产标签PDF台账一份Excel导出来程序跑一遍直接生成可打印的A4标签页打印在不干胶标签纸上一贴就能用。这篇文章不是泛泛讲概念而是完整的选型思路、最小可运行方案、批量实现代码以及我在字体、二维码扫描率、打印对齐上踩过的坑。适合有Java基础、想快速给自家资产做标签打印的人也适合刚开始接触PDF生成、不知道从哪下手的同事。先说一句结论itextpdf负责排版和输出PDFZXing负责把资产编号变成二维码两个库配合正好覆盖“看得见、扫得出、打得准”三个核心诉求。1. 为什么非要程序生成资产标签从“账实不符”说起1.1 手工贴标和Excel台账的尴尬大部分中小型单位做资产管理起步都是同一个套路用Excel登记台账资产编号一列、名称一列、购买日期一列、责任人一列然后没了。问题出在物理资产和电子台账脱节。你Excel里写着“编号SB-2024-0001”对应某台服务器但服务器机柜上没贴任何标识或者贴的是厂商出厂贴纸编号压根对不上。盘点时全靠人肉记忆“这台大概是财务部的”“那台好像是研发的”最后盘点单上全是不确定项审计抽几台立刻暴露。我还见过更极端的操作为了应付检查临时从网上买现成的“资产编号贴纸”拿马克笔手写编号贴上去。结果一年后贴纸掉色重新盘点又变成了“猜编号”游戏。手工贴标不是不能用是它在资产数量过百之后就很难维持准确率。标签容易掉、容易写错、格式不统一一旦跟台账编号对不上后面每一次盘点都在为前面的随意买单。1.2 二维码的价值在于“一物一码”如果把资产标签从普通的“文字编号贴纸”升级成“二维码文字编号”等于给每台设备发了一张身份证。平时找货、盘点、维修登记拿手机或扫码枪一扫直接得到这台设备的唯一编号再关联到后台台账整个过程不需要抄写、不需要手工录入。这里有个关键设计二维码内容不建议放资产名称、部门、购买日期这些可变信息只放一个全局唯一的资产编号。为什么因为编号是稳定的名称和责任人可能变更。贴标签贴的是“身份”不是“快照”。后续任何信息变化改后台台账就行不需要重新贴标签。这也是程序批量生成和二维码在线生成器的本质区别在线生成器适合临时生成一二十个批量场景下效率和规则一致性才是第一位。1.3 什么时候值得上“程序标签纸”不是所有场景都需要代码生成。我给自己定的判断标准很简单资产数量超过50个后续还要定期补打、重打标签值得上程序。资产分散在多个房间、楼层盘点依赖扫码必须上程序。标签需要统一品牌风格、包含公司名称或部门信息值得用模板固定排版。如果只有三五台设备拿不出手直接找个在线工具生成二维码再截图反而更快。换句话说程序生成的核心不是“生成一个二维码”而是“批量、稳定、可重复地生成一整批格式统一的PDF”。资产越多这套做法的收益越明显。2. 选型思路为什么是iTextPDF配ZXing而不是PDFBox、JasperReports或前端打印2.1 我曾对比过的“另一批方案”做标签PDF之前我先把市面上常见的几条路都过了一遍结论比较明确。这里直接给一张对比表省得你再踩一遍方案优点缺点我的使用建议iTextPDF 5.x ZXing轻量、API直观、中文字体方案成熟、批量控制力强AGPL许可需注意赶不上PDFBox的底层自由度首选大部分Java标签场景都够用Apache PDFBox ZXing纯Apache项目底层渲染自由模块划分清晰文字定位、排版要自己算坐标中文字体嵌入要手动折腾适合要精细控制PDF内部结构的场景标签这种重复排版用起来偏累JasperReports 二维码报表设计器可视化套打能力强依赖重、学习成本高做卡片式标签有点大材小用适合规章制度、复杂报表和标签混合的项目前端HTML打印浏览器渲染排版容易不同浏览器打印缩放不同PDF边距和坐标不可控适合内部处理不适合直接交给不干胶打印机坦白说这些方案没有绝对优劣只有适不适合。资产标签的特点是页面结构高度重复、标签卡片尺寸固定、需要精确到毫米级的坐标控制。这类需求恰好是iText这种“把PDF当画布来操作”的库最擅长的事情。2.2 iTextPDF和ZXing组合的真正理由我选iTextPDF主要有三个原因。第一控制粒度细。标签卡片的位置、边框、文字全部可以用绝对坐标写死。PDF页面坐标以左下角为原点、以pt为单位我可以根据A4标签纸的物理尺寸反推每个卡片的位置打印出来基本不会偏。第二中文字体处理成熟。Java生成PDF绕不开中文iText 5.x配合itext-asian包用STSong-Light标准字体不需要额外嵌入字体文件PDF文件体积小跨机器运行结果一致。这一点PDFBox用起来没这么省心。第三批量逻辑简单。PDF的本质是一页一页的内容流iText的Document对象天然支持循环newPage()我可以把“每页10个标签、数据自动换页”写成一个很直白的for循环代码量不大出问题也好排查。ZXing这边没什么悬念。二维码生成库有很多但ZXing社区成熟、API稳定QRCodeWriter几行代码就能出一个BitMatrix再配合javase包里的MatrixToImageWriter转成图片二十行内搞定。它不需要联网、不需要付费接口本地批量生成几千个标签也没有额外成本。2.3 版本选择与许可证提醒我用的是iText 5.5.13.3不是iText 7。原因很实际5.x的API资料多网上随手能搜到合适的写法7.x架构更新、性能更好但很多老帖子里的写法不通用项目里如果只是做标签打印没必要为了新版而升级。需要提一句iText 5.x采用AGPL许可。内部使用、不对外分发软件通常没问题如果是闭源商业软件要打包分发需要评估商业授权。ZXing是Apache 2.0许可宽松很多。这个细节很多人忽略等产品上线再做合规评估就晚了。3. 先把一张标签跑通环境配置与最小可运行示例3.1 POM依赖里三个包的各自任务我建议新建一个干净的Maven项目Java 8及以上都可以。pom.xml里加下面四个依赖dependency groupIdcom.itextpdf/groupId artifactIditextpdf/artifactId version5.5.13.3/version /dependency dependency groupIdcom.itextpdf/groupId artifactIditext-asian/artifactId version5.2.0/version /dependency dependency groupIdcom.google.zxing/groupId artifactIdcore/artifactId version3.5.2/version /dependency dependency groupIdcom.google.zxing/groupId artifactIdjavase/artifactId version3.5.2/version /dependency这四个依赖各自的角色itextpdf负责PDF排版itext-asian提供中文字体名称到字体文件的映射没有它“STSong-Light”这个字体名认不出来zxing core负责把字符串编码成二维码矩阵zxing javase里的MatrixToImageWriter负责把矩阵转成BufferedImage图片对象。不要图省事只加core不加javase因为矩阵转图片的工具类在javase包里漏了会在运行时报ClassNotFoundException。3.2 最小程序一张小卡片一个可扫描二维码先别急着做批量目标只有一个生成一张带二维码的小PDF。代码尽量精简public class SingleLabelDemo { public static void main(String[] args) throws Exception { // 1. 用ZXing生成二维码BufferedImage String content ASSET:SB-2024-0001; QRCodeWriter qrWriter new QRCodeWriter(); MapEncodeHintType, Object hints new HashMap(); hints.put(EncodeHintType.CHARACTER_SET, UTF-8); hints.put(EncodeHintType.MARGIN, 2); BitMatrix matrix qrWriter.encode(content, BarcodeFormat.QR_CODE, 220, 220, hints); BufferedImage qrImage MatrixToImageWriter.toBufferedImage(matrix); // 2. 用iText生成PDF Document document new Document(PageSize.A4, 36, 36, 36, 36); PdfWriter writer PdfWriter.getInstance(document, new FileOutputStream(single-label.pdf)); document.open(); // 中文字体 BaseFont bf BaseFont.createFont(STSong-Light, UniGB-UCS2-H, BaseFont.NOT_EMBEDDED); Font titleFont new Font(bf, 14, Font.BOLD); Font infoFont new Font(bf, 10, Font.NORMAL); PdfContentByte canvas writer.getDirectContent(); // 画一个圆角卡片 canvas.setLineWidth(1f); canvas.roundRectangle(60, 680, 220, 90, 6); canvas.stroke(); // 卡片标题 ColumnText.showTextAligned(canvas, Element.ALIGN_CENTER, new Phrase(资产编号, titleFont), 170, 750, 0); // 二维码放在左下角 Image qr Image.getInstance(qrImage, null); qr.scaleToFit(70, 70); qr.setAbsolutePosition(75, 682); document.add(qr); // 编号文字放在二维码右侧 ColumnText.showTextAligned(canvas, Element.ALIGN_LEFT, new Phrase(SB-2024-0001, infoFont), 160, 710, 0); document.close(); System.out.println(生成完成请打开 single-label.pdf 检查); } }代码不复杂但要提醒几个点二维码内容我写的是“ASSET:SB-2024-0001”这个前缀是给盘点系统识别的不是随便写的后面第4章会细说。hints.put(EncodeHintType.MARGIN, 2)控制了二维码四周的白边宽度这是个关键参数直接关系到扫描率后面第5章展开讲。ColumnText.showTextAligned的第一参数是canvas第二个参数是对齐方式后面是坐标和旋转角。iText坐标原点在页面左下角不是左上角初学容易把文字画到页面外面。3.3 运行后你应该看到什么以及常见启动报错正常跑完会生成一个几十KB大小的single-label.pdf打开以后能看到页面上方一张带圆角边框的小卡片卡片顶部写着“资产编号”左下角一个二维码右边跟着编号文字。如果直接编译运行最容易遇到两个问题一是找不到“STSong-Light”字体报错内容类似Font STSong-Light with UniGB-UCS2-H is not recognized。这个基本就是itext-asian依赖没加或者加错版本。加上com.itextpdf:itext-asian:5.2.0就能解决。二是MatrixToImageWriter找不到类报NoClassDefFoundError。因为zxing javase包没加到classpath里。zxing core和javase是两个独立的jar包别只依赖core。4. 批量资产标签的核心实现数据模型、坐标计算与完整代码4.1 资产数据模型该有哪些字段第一张标签跑通之后批量就只是“循环”的问题。先定义一个最小的资产数据类字段根据实际台账定public class AssetRecord { private String assetNo; // 唯一资产编号二维码内容的核心 private String assetName; // 资产名称用于标签展示 private String category; // 类别比如“办公设备/仪器仪表” private String owner; // 责任人 private String purchaseDate; // 购置日期 public AssetRecord() {} public AssetRecord(String assetNo, String assetName, String category, String owner, String purchaseDate) { this.assetNo assetNo; this.assetName assetName; this.category category; this.owner owner; this.purchaseDate purchaseDate; } // getter / setter 省略IDE自动生成即可 }字段不是越多越好。标签卡片只有这么大放太多信息反而扫描时一坨文字看不清。我最后只保留了“编号、名称、类别、责任人、购置日期”五个字段必要信息都在又不臃肿。数据来源可以是Excel、CSV、数据库甚至可以直接在Java代码里new一个List测试。我建议先用List 跑通流程再替换成实际数据源。4.2 二维码内容编码规则先定规则再写代码这是整个项目最容易被低估的一步。很多人上来就问“二维码里放啥”我的答案永远是先想清楚你的扫码端是谁。如果资产盘点用的是自研小程序建议二维码内容用统一前缀加编号比如ASSET:SB-2024-0001如果将来要对互联网用户公开查询可以放一个链接https://asset.example.com/view/SB-2024-0001如果盘点系统用的是第三方扫码工具对方通常只识别纯数字编号那就直接用SB-2024-0001我自己更推荐“前缀编号”的格式尤其是当现场可能出现多个二维码混贴的情况。盘点程序拿到内容后先判断前缀是不是ASSET再取编号这样不会把别人的二维码误认成自己的资产。编码规则确定后要固化到文档里不要今天一种格式明天一种格式。二维码内容一旦变了已经贴出去的标签全部作废重打成本很高。4.3 理解iText的坐标系统左下角是原点开始写批量代码前先花两分钟搞懂iText坐标系否则调试标签位置会让你怀疑人生。iText中A4页面的宽度是595pt高度是842pt坐标原点在页面左下角x向右增长y向上增长。而我们在视觉上习惯从上往下数格子两者之间需要做一次转换。假设我用的是常见的A4不干胶标签纸每页2列5行也就是10个标签卡片纸张四周留10mm边距卡片之间留3mm间隙。换算成pt1mm约等于2.8346ptprivate static final float MM 2.8346f; private static final float MARGIN 10 * MM; // 页面边距 private static final float GAP 3 * MM; // 卡片间隙 private static final int COLS 2; private static final int ROWS 5;每个卡片宽高根据纸张大小反推float labelW (PageSize.A4.getWidth() - 2 * MARGIN - (COLS - 1) * GAP) / COLS; float labelH (PageSize.A4.getHeight() - 2 * MARGIN - (ROWS - 1) * GAP) / ROWS;计算出来大概是90mm×47mm正好落进市面上常见的“二五格”标签纸尺寸。关键转换在两处。列号从左往右编号x坐标直接算float x MARGIN col * (labelW GAP);行号从视觉上是从上往下而坐标原点在左下角所以要先反转行号int rowFromTop indexInPage / COLS; int rowFromBottom ROWS - 1 - rowFromTop; float y MARGIN rowFromBottom * (labelH GAP);这个反转就是iText初学最绕的地方。我自己第一次做的时候忘了反转结果第一张标签跑到了页面底部第二张跑到倒数第二行跟预期正好上下颠倒。4.4 页面分格与翻页循环批量逻辑本身不复杂拿到资产列表按每页10个切片最后一页不足10个就只画有的部分。注意一个细节——最后一页不足10个时要不要补画空边框我建议不补。标签纸打印出来没画的区域就是空白补一堆空框既浪费碳带贴的时候还容易混淆。空框只在实际少数据时能帮人对齐位置但实际贴标的人根本不会在意空框反而会觉得浪费。翻页用document.newPage()在循环里每处理完一页就开新页。最后一页不翻所以判断条件要写在每页处理之前for (int pageIndex 0; pageIndex totalPages; pageIndex) { if (pageIndex 0) { document.newPage(); } // 当前页的数据切片 }4.5 完整批量代码与逐段说明下面是一个可以直接使用的批量生成器我把它整理成一个类方便复制修改import com.google.zxing.BarcodeFormat; import com.google.zxing.EncodeHintType; import com.google.zxing.common.BitMatrix; import com.google.zxing.qrcode.QRCodeWriter; import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel; import com.google.zxing.client.j2se.MatrixToImageWriter; import com.itextpdf.text.*; import com.itextpdf.text.pdf.*; import java.awt.image.BufferedImage; import java.io.FileOutputStream; import java.util.HashMap; import java.util.List; import java.util.Map; public class AssetPdfBatchGenerator { private static final float MM 2.8346f; private static final float MARGIN 10 * MM; private static final float GAP 3 * MM; private static final int COLS 2; private static final int ROWS 5; private static final float LABEL_W (PageSize.A4.getWidth() - 2 * MARGIN - (COLS - 1) * GAP) / COLS; private static final float LABEL_H (PageSize.A4.getHeight() - 2 * MARGIN - (ROWS - 1) * GAP) / ROWS; public void generate(ListAssetRecord assets, String outputPath) throws Exception { Document document new Document(PageSize.A4, MARGIN, MARGIN, MARGIN, MARGIN); PdfWriter writer PdfWriter.getInstance(document, new FileOutputStream(outputPath)); document.open(); BaseFont bf BaseFont.createFont(STSong-Light, UniGB-UCS2-H, BaseFont.NOT_EMBEDDED); Font titleFont new Font(bf, 9, Font.BOLD); Font labelFont new Font(bf, 8, Font.NORMAL); Font tipFont new Font(bf, 7, Font.NORMAL); int perPage COLS * ROWS; int totalPages (int) Math.ceil(assets.size() / (double) perPage); for (int pageIndex 0; pageIndex totalPages; pageIndex) { if (pageIndex 0) { document.newPage(); } PdfContentByte canvas writer.getDirectContent(); int startIndex pageIndex * perPage; int endIndex Math.min(startIndex perPage, assets.size()); for (int i startIndex; i endIndex; i) { int posInPage i - startIndex; int rowFromTop posInPage / COLS; int col posInPage % COLS; int rowFromBottom ROWS - 1 - rowFromTop; float x MARGIN col * (LABEL_W GAP); float y MARGIN rowFromBottom * (LABEL_H GAP); drawSingleTag(canvas, document, x, y, LABEL_W, LABEL_H, assets.get(i), titleFont, labelFont, tipFont); } // 页码放在页面底部右下角 ColumnText.showTextAligned(canvas, Element.ALIGN_RIGHT, new Phrase(第 (pageIndex 1) / totalPages 页, tipFont), PageSize.A4.getWidth() - 15, 12, 0); } document.close(); } private void drawSingleTag(PdfContentByte canvas, Document document, float x, float y, float w, float h, AssetRecord asset, Font titleFont, Font labelFont, Font tipFont) throws Exception { // 外边框 canvas.setLineWidth(0.8f); canvas.setColorStroke(BaseColor.GRAY); canvas.roundRectangle(x, y, w, h, 4); canvas.stroke(); // 左上角类别 ColumnText.showTextAligned(canvas, Element.ALIGN_LEFT, new Phrase(asset.getCategory(), titleFont), x 6, y h - 12, 0); // 顶部第二行资产名称超出长度截断 String name asset.getAssetName(); if (name.length() 12) { name name.substring(0, 12) ...; } ColumnText.showTextAligned(canvas, Element.ALIGN_LEFT, new Phrase(名称 name, titleFont), x 6, y h - 24, 0); // 第三行资产编号 ColumnText.showTextAligned(canvas, Element.ALIGN_LEFT, new Phrase(编号 asset.getAssetNo(), labelFont), x 6, y h - 36, 0); // 底部责任人和购置日期 String owner 责任人 asset.getOwner(); if (owner.length() 12) { owner owner.substring(0, 11) ...; } ColumnText.showTextAligned(canvas, Element.ALIGN_LEFT, new Phrase(owner, labelFont), x 6, y 10, 0); ColumnText.showTextAligned(canvas, Element.ALIGN_RIGHT, new Phrase(asset.getPurchaseDate(), labelFont), x w - 6, y 10, 0); // 二维码放在标签右侧中间 String qrContent ASSET: asset.getAssetNo(); BufferedImage qrImage createQrImage(qrContent, 220, 220); Image qr Image.getInstance(qrImage, null); float qrSize Math.min(70, w - 100); qr.scaleToFit(qrSize, qrSize); float qrX x w - qrSize - 6; float qrY y (h - qrSize) / 2; qr.setAbsolutePosition(qrX, qrY); document.add(qr); // 二维码正下方放编号文字扫码失败时还能人工录入 ColumnText.showTextAligned(canvas, Element.ALIGN_CENTER, new Phrase(asset.getAssetNo(), tipFont), qrX qrSize / 2, qrY - 10, 0); } private BufferedImage createQrImage(String content, int width, int height) throws Exception { QRCodeWriter writer new QRCodeWriter(); MapEncodeHintType, Object hints new HashMap(); hints.put(EncodeHintType.CHARACTER_SET, UTF-8); hints.put(EncodeHintType.MARGIN, 2); hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.M); BitMatrix matrix writer.encode(content, BarcodeFormat.QR_CODE, width, height, hints); return MatrixToImageWriter.toBufferedImage(matrix); } }代码有几个可以按需改动的地方第一二维码尺寸。标签宽90mm左右二维码留出70pt约25mm大小是足够扫码的。如果资产编号长到十几个字符二维码模块会变密可以把qrSize适当放大一点但不要超过纸质标签的2/3否则会挤压文字区域。第二二维码生成尺寸和显示尺寸的关系。我在createQrImage里生成220×220像素的矩阵图在PDF里显示成70pt。两个尺寸可以不一致ZXing会按显示尺寸缩放。生成像素高一些是为了缩放后边缘不糊。第三文字字号。9pt、8pt、7pt是专门为90×47mm小卡片调的。字体太大会溢出卡片边界字体太小在普通喷墨打印机上又看不清。这个组合我在A4不干胶纸上实测过属于比较稳妥的参数。跑完这段代码几百条资产的标签PDF就生成了。打印页数和资产数量自动匹配最后一页不足10个也不会报错。5. 实测中绕不开的四个坑字体方块、二维码扫不出、打印错位、内存爆表以下四个坑每一个我都真实遇到过。贴出来是为了让后面的朋友少走弯路。5.1 中文全部变成“口口口”Java生成PDF最大的新手杀手就是中文变方块。症状很典型程序没报错PDF也生成了但所有中文文字在阅读器里显示成一排小方框。本质原因是iText默认字体只支持拉丁字符中文字符没有对应字形。解决方式就两招第一招用itext-asian包里的内置中文字体。就是我在pom里加的依赖然后在代码里声明BaseFont bf BaseFont.createFont(STSong-Light, UniGB-UCS2-H, BaseFont.NOT_EMBEDDED);注意第二个参数必须是UniGB-UCS2-H这是中文的编码规则填错照样出方块或乱码。第二招如果公司有指定的品牌字体比如黑体、宋体可以读取系统字体文件BaseFont bf BaseFont.createFont(C:/Windows/Fonts/simhei.ttf, BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);这种方式的缺点是字体文件路径依赖机器换个电脑路径不存在就报错。如果只是内部打印STSong-Light足够PDF体积还小。5.2 二维码调试白边、纠错级别与打印尺寸二维码扫不出的原因我排查下来通常集中在三个点。第一白边不够。二维码矩阵周围必须有一圈静区ZXing默认白边参数是4我代码里手动改成了2。有人为了省空间改成0或者1这在屏幕上看没问题打印出来就各种识别失败。二是打印的墨迹扩散code区域边缘会糊静区太窄直接压到边框。如果你用的是喷墨打印机建议保持2-3用激光或热转印可以适当压缩。第二纠错级别太低。ZXing默认的纠错级别是L只能容忍约7%的损伤。标签贴到设备上难免有污渍、折角、磨损建议至少用M级别15%我给的是M。如果标签会暴露在户外或者环境比较脏直接上Q25%更稳但二维码模块会稍微密一点。第三打印尺寸太小。二维码打印出来单个模块至少要0.5mm才容易被手机识别。简单换算一下如果二维码是37×37个模块整体尺寸20mm每个模块约0.54mm比较保险如果压缩到15mm每个模块0.4mm普通手机就可能识别慢了。所以二维码在标签上的边长不要小于20mm我代码里70pt约25mm是留了余量的。调试时最有效的办法生成一页PDF用普通纸先打印出来拿手机和扫码枪分别扫一遍再贴到实际设备上扫一遍。纸上能扫不算数设备上能扫才算数。5.3 打印机“自适应缩放”导致标签错位这个问题经常被忽略因为它出现在打印环节不是代码环节。生成好的PDF看起来无比标准A4上每个卡片位置都对。但发给打印机一打所有卡片往右上角偏了5mm或者整体被缩小了。原因基本是打印驱动开启了“适应页面”或“缩放以适合”选项。同一份PDF在“实际大小”和“适合页面”两种模式下内容占比完全不同。不干胶标签纸是预先切好卡片的任何缩放都会导致二维码和卡片边界错开。解决办法有两个一是在打印设置里选“实际大小”或“100%大小”不要选“适应页面”。这个操作要看打印机驱动普通的喷墨一体机在打印预览界面能找到。二是给PDF页面设置成和标签纸完全一致的物理尺寸比如标签纸本身不是标准A4而是某种特殊规格就用Rectangle指定页面尺寸Rectangle tagPage new Rectangle(PageSize.A4.getWidth(), PageSize.A4.getHeight()); Document document new Document(tagPage, MARGIN, MARGIN, MARGIN, MARGIN);特殊尺寸纸张用Rectangle自定义这是iText支持标准A4与自定义尺寸的关键。另外提一个容易被忽略的点有些打印机不支持边到边打印会在四边各切掉几毫米。如果你的标签纸本身就把内容做到边缘打印后标签会缺角。所以标签页四周至少要留5mm安全边距我代码里留了10mm比较稳。5.4 批量生成时内存和PDF体积失控几百张标签完全没压力但到几千张时如果不注意写法内存和文件体积会一起失控。最常见的错误是把全部二维码图片一次性生成好放到内存里再开始写PDF。几千张BufferedImage放在堆里很容易把内存占满。正确做法是循环里生成一个、写入一个用完的Image对象自然脱离作用域让GC回收。我上面的完整代码就是这么写的。输出端也建议直接写文件流PdfWriter.getInstance(document, new FileOutputStream(outputPath));不要先写到一个ByteArrayOutputStream再一次性刷盘。几千页PDF全部堆在内存里几十MB文件加上对象开销很容易触发OutOfMemoryError。如果资产量非常大比如几万条建议每5000张生成一个独立PDF文件文件名带上批次号。这样即使某批生成一半失败重跑那一批就行不会前功尽弃。二维码内容长度也在影响文件体积。内容越长二维码版本越高模块越多图片越大。代码里二维码内容只放“ASSET:编号”就是刻意控制长度。有人图省事把资产名称全部塞进二维码一个标签的二维码文件多出好几KB成百上千个累积起来文件瞬间变大。6. 从“能出图”到“能上岗”撕下来就能贴的几点实操经验6.1 标签排版字段分区排版看似小事实际直接影响使用效率。我最后确定的标签版式是“左右分区”左侧从上到下放类别、名称、编号、责任人右侧放二维码二维码正下方再放一遍编号文字。之所以二维码下方还要放一遍编号是因为现场总有扫码设备没电、网络断了、或者二维码被污染的情况。这时人工看文字编号也能完成登记。很多标签设计只放二维码不放文字编号扫码工具一坏全盘抓瞎。类别放在左上角起到快速识别作用盘点时一眼就能判断这层楼哪几台是仪器哪几台是电脑不用逐台扫。6.2 用CSV驱动批量任务非技术人员也能独立操作程序写好后不要让业务人员碰Java代码。提供一个CSV文件作为入口他们改Excel另存为CSV跑一下脚本PDF就出来了。CSV加载我建议自己写个简单方法不额外引POI减少部署成本public ListAssetRecord loadFromCsv(String path) throws IOException { ListAssetRecord list new ArrayList(); try (BufferedReader reader Files.newBufferedReader(Paths.get(path))) { String line reader.readLine(); // 表头跳过 while ((line reader.readLine()) ! null) { if (line.trim().isEmpty()) { continue; } line line.replace(\uFEFF, ); // 去掉CSV的BOM头防止第一个字段多字符 String[] cols line.split(,, -1); if (cols.length 4) { continue; } AssetRecord asset new AssetRecord(); asset.setCategory(cols[0].trim()); asset.setAssetNo(cols[1].trim()); asset.setAssetName(cols[2].trim()); asset.setOwner(cols[3].trim()); asset.setPurchaseDate(cols.length 4 ? cols[4].trim() : ); list.add(asset); } } return list; }两个细节第一Excel另存的CSV默认带UTF-8 BOMJava按UTF-8读的时候第一行第一列会多出几个隐藏字符BOM导致“类别”字段开头看起来正常但实际不对。replace(\uFEFF, )就是干这个的。第二split(,, -1)最后的参数-1很关键。它保证即使某行末尾有空字段数组长度也保持稳定不会丢掉最后一个字段。很多资产管理Excel里“购置日期”偶尔是空的没有-1参数数组长度会不对取数据就错位。6.3 先测一页再测一卷打印机永远等得起这一分钟完整流程我建议分三步验证第一步把资产列表里前5条数据单独导出一页PDF在普通A4纸上打印。确认卡片位置、字号、二维码大小、中文字体全部正确。第二步从资产列表里挑一批编码相对复杂的比如编号特别长的、名称特别长的导出几页再到不干胶纸上打一卷试打印。因为普通纸和不干胶纸的打印精度有差异二维码在普通纸上能扫不代表在不干胶上也能扫。第三步把试打印的标签贴到真实的仪器上放半个月再扫一次。这样能验出油墨、环境对二维码的损耗程度。这三步看着慢其实也就是多花半小时但能避免“一口气打了两千张发现二维码扫不出”的灾难。6.4 标签的补打与重打策略资产标签迟早要面对补打和重打的问题。标签掉了、贴错位置、设备退库重新分配都需要重新生成单张标签。我一般会在批量工具里额外留一个“单号补打”入口命令行传入一个或多个资产编号程序只生成一两页。不需要为了补一张标签重新跑全量数据。实现也简单把批量方法里的List切片逻辑去掉直接处理传入的资产列表即可。补打时有个细节不要在旧标签上直接叠新标。不干胶标签揭下来常有残胶二维码区域有残留胶层扫描容易花。现场条件允许的话用除胶剂清理后再贴新标如果贴双层标签一定要确保旧标签完全平整否则新标签悬空扫码时会轻微反光影响识别。如果让我给这个项目排优先级第一是编码规则先定清楚第二才是PDF排版。因为编码规则决定了二维码里放什么而扫码系统对接不上时再漂亮的标签都是麻烦。这也是我用这套组合做了第三个标签项目之后才真正想明白的事情。希望这篇能让后来的你少走一段弯路直接打印出能扫、能贴、能盘点的资产标签。