尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Spring Boot集成x-easypdf:5分钟搞定中文PDF导出与排版

Spring Boot集成x-easypdf:5分钟搞定中文PDF导出与排版 在 Spring Boot 项目里做过 PDF 导出的人应该都体会过那种“想砸电脑”的感觉一个简单的报表用 iText 要注册字体、算坐标、调表格宽度动不动写几百行用 PDFBox 更夸张连中文字符换行都要自己算。我这次重构项目的导出模块时换成了 x-easypdf核心目标只有一个——5 分钟搞定 PDF 生成与中文排版。这篇文章是我从依赖引入、中文排版、表格图片到生产环境踩坑的完整笔记适合在后端 Java 项目里处理报表、订单、凭证导出的同学直接抄作业。x-easypdf 的定位是 PDFBox 的增强封装内置了黑体、宋体、楷体等常用中文字体API 走链式调用用起来比 iText 顺滑不少。1. 选择 x-easypdf 之前我对比过的那些 PDF 方案1.1 iText、PDFBox、OpenPDF 的普遍痛点先说 iText。这应该是 Java 生态里知名度最高的 PDF 库功能确实全从文本到表格、签名、表单、水印样样都有。但它有两个天然的坎第一是授权iText 7 用的是 AGPL 协议如果你的项目不是开源的或者不想把代码开源就得考虑商业授权这在很多公司里会直接触发法务审查第二是中文处理iText 要正常显示中文必须自己注册字体文件而且不同字体对应的编码规则还不一样一个宋体一个黑体写出来的注册代码能绕一大圈。再看 Apache PDFBox。它是 Apache 基金会底下的项目功能集中在 PDF 的底层读写没有授权风险API 也算干净。但它的问题在于“太底层”了。你要画一段文字得自己指定字体对象、计算字符串宽度、判断要不要换行、换行之后 y 坐标减多少。表格更不用说了本质上是画一堆矩形、画若干条线然后往格子里塞文字位置还得手工对齐。做一页简单的 A4 报表光坐标换算就能写到怀疑人生。OpenPDF 是 iText 4 的社区分支比 PDFBox 友好一些但也只是“友好一点”中文字体照样要自己去搞。Apache FOP 适合 XML 到 PDF 的文档生成走的是 XSL-FO 那套规范学习成本偏高用在业务报表上杀鸡用牛刀。我并不是说这些方案不行而是当业务场景是“尽快把数据库里的数据变成一页页能看的中文 PDF”它们给到的都是零件不是工具。你需要的是开箱即用的文字、表格、图片、自动换行而不是每次都重新拼装一遍。1.2 x-easypdf 的两个核心杀手锏内置字体与链式 APIx-easypdf 吸引我的点是它把 PDFBox 那些底层细节封装成了“看起来像写配置”的 API。两个杀手锏最关键。第一是内置中文字体。它自带了一组FontFamily枚举包括黑体、宋体、楷体、仿宋等常用字体不需要你准备任何字体文件也不需要在服务器上安装中文字体。这一点解决了最大的痛点——很多项目在 Windows 上跑得好好的部署到 Linux 就变乱码或方框就是因为服务器缺字体。用 x-easypdf 的内置字体这套问题直接绕过去了。第二是链式 API。一个文本对象从创建到确定位置、字体、大小、颜色、对齐方式全都在一行链式调用里完成代码整洁程度比 iText 高一个档次。后面生成表格、插入图片也都是类似风格。对于团队协作来说这种代码 review 起来也轻松因为你几乎一眼就能看出这段 PDF 在干嘛。2. 项目落地依赖、首份 PDF 与 Spring Boot 接口2.1 依赖坐标与版本选择Maven 依赖很简单。以我目前使用的 2.x 版本为例dependency groupIdcn.idev.excel/groupId artifactIdeasypdf-pdfbox/artifactId version2.3.1/version /dependency提醒版本号以 Maven 中央仓库当前最新稳定版为准建议打开仓库页面确认一下再粘。如果你的需求不止文本和表格还想直接把 HTML 转成 PDF那么再引入一个模块dependency groupIdcn.idev.excel/groupId artifactIdeasypdf-openhtmltopdf/artifactId version2.3.1/version /dependency这里要特别说明的是x-easypdf 1.x 和 2.x 的坐标、API 差异非常大。网上搜到的老文章大部分是 1.x 的用法类名和 2.x 对不上。我在项目中直接用 2.x后面贴的代码也都是 2.x 的写法。如果你之前看过 1.x 教程请直接忘掉它。2.2 十行代码生成第一份带中文的 PDF先跑一个最小例子验证整个环境通不通。生成一个 A4 页面上面写一句中文import cn.idev.excel.pdfbox.core.PdfDocument; import cn.idev.excel.pdfbox.core.PdfPage; import cn.idev.excel.pdfbox.layout.text.PdfText; import cn.idev.excel.enums.FontFamily; public class QuickStart { public static void main(String[] args) throws Exception { PdfDocument document new PdfDocument(); PdfPage page document.createNewPage(); PdfText text new PdfText(page); text.setText(你好Spring Boot x-easypdf) .setFontFamily(FontFamily.HEI_TI) .setFontSize(24) .setPosition(50, 700); document.save(hello.pdf); document.close(); } }这段代码里最关键的一行是setFontFamily(FontFamily.HEI_TI)。如果你把它去掉中文大概率会变成一个一个的方框。原因后面单独讲字体时细说这里先记住我们要做中文 PDF字体族一定要显式设置。setPosition(x, y)的坐标系要留心。PDF 页面的坐标原点在左下角x 向右递增y 向上递增这和我们在网页里习惯的“左上角为原点”完全不同。A4 页面高度约 842 点所以 y 设成 700 表示“页面底部往上 700 点”的位置也就是靠近页面顶部。2.3 用 Controller 直接输出 PDF 下载流项目里不能只在 main 方法里跑要接到 Spring Boot 接口上。方式也很直接生成 PDF 后把输出流交给HttpServletResponse。RestController public class PdfController { GetMapping(/report) public void generateReport(HttpServletResponse response) throws IOException { response.setContentType(application/pdf); response.setHeader(Content-Disposition, attachment; filenamereport.pdf); PdfDocument document new PdfDocument(); try { PdfPage page document.createNewPage(); PdfText text new PdfText(page); text.setText(月度销售报表) .setFontFamily(FontFamily.HEI_TI) .setFontSize(20) .setPosition(50, 700); // 这里继续填充业务数据... document.save(response.getOutputStream()); } finally { document.close(); } } }注意两点Content-Disposition里的filename如果是中文需要做 URL 编码否则部分浏览器下载文件名会乱码。不要自己response.getOutputStream().close()。Servlet 容器会管理 response 的流你 close 之后容器再写东西就可能报错。x-easypdf 的save(OutputStream)会完成需要的数据写入之后交给容器处理即可。这个接口其实就是很多公司给第三方对接的那个“PDF 导出接口”调用方拿到流直接保存成文件或者嵌入浏览器预览。后端不需要在服务器上生成临时文件天然适合无状态部署。3. 中文排版实操从“能显示”到“排得漂亮”3.1 为什么中文字体是 PDF 生成的第一道坎PDF 本身是一套复杂的文档规范。文字显示依赖字体字体里包含字形数据。PDFBox 默认用的是 Helvetica 这类西方字体它们一共就几百个字形根本没有汉字。当你把一个中文字符丢进去渲染器找不到对应字形只能画一个空框或占位符。这个问题和你用什么 IDE、什么操作系统没关系纯粹是字体数据不支持。iText 的解决方法是让你把系统的中文字体文件比如微软雅黑、思源黑体加载进来注册成 PDF 文档的字体资源。这个方案可行但带来了部署问题Windows 上字体文件和 Linux 上不一样一旦换了运行环境字体路径可能失效。有些项目图省事把字体文件塞进resources这倒是解决了路径问题但字体文件体积动不动 10MB 起步还不一定能过公司资产审查。x-easypdf 的做法是直接把常用中文字体打包进库里用FontFamily.HEI_TI、FontFamily.SONG_TI、FontFamily.KAI_TI这类枚举来引用。库在初始化时会把这些内置字体文件注册到 PDF 文档中应用代码完全不用关心字体文件在哪。这属于典型的“用封装换省心”。3.2 文本自动换行、对齐与行距控制生成 PDF 最容易写崩的地方之一就是长文本。数据库里存了一个客户备注长度 200 字你如果直接setText画到页面上它会往一个方向一直延伸出去最后被页面边缘裁掉。x-easypdf 提供了自动换行开关text.setText(客户备注内容可能很长请注意使用自动换行……) .setFontFamily(FontFamily.SONG_TI) .setFontSize(12) .setAutoWrap(true) .setWidth(400) .setPosition(50, 600);开了setAutoWrap(true)后文本会按setWidth(400)给定的宽度自动折行。这里宽度单位是 PDF 的点point和字体大小同一个度量体系。一般来说 1pt 约等于 1/72 英寸A4 的可用宽度大约 500 多点你设置 400 表示占据中间大部分区域。行距用setLeading控制text.setLeading(20f);默认行距跟字体大小有关但中文阅读习惯里行距通常要略大一些否则两行字会显得贴在一起。我个人的经验是字号 12 的话行距 20 比较舒服字号 10 的话行距 16 左右。对齐方式也是一行代码的事text.setHorizontalStyle(HorizontalStyle.CENTER); text.setVerticalStyle(VerticalStyle.MIDDLE);这个在表格单元格里特别有用因为单元格内容垂直居中、水平居中基本是报表刚需。注意这里的“垂直”对齐是相对于文本所在区域的上下方向不是字与字之间的竖直排列别理解反了。3.3 颜色、边框、背景这些细节怎么设报表讲究可读性颜色能快速区分表头和数据行。x-easypdf 的文本对象支持常见的链式样式配置text.setColor(Color.RED) .setBackgroundColor(Color.LIGHT_GRAY) .setBorderWidth(1f) .setBorderColor(Color.GRAY);setColor是文字颜色setBackgroundColor是文本块背景色这两个是最常用的。边框相关的方法多用在表格场景里。如果你要在一个页面里摆多个文本块比如标题一个区域、正文一个区域、落款一个区域可以创建多个PdfText对象分别设置位置。位置坐标需要自己算但这种“手摆”的方式其实可控性更强。我习惯在代码里定义一个常量数组维护所有区块的 y 坐标这样页面布局一目了然private static final float TITLE_Y 740; private static final float SECTION_HEADER_Y 680; private static final float CONTENT_Y 620;改版式的时候只需要动这几个常量不需要逐行找魔法数字。页面本身也可以定制。PdfPage默认是 A4 纵向你可以调整宽高PdfPage page document.createNewPage(); page.setWidth(595f); page.setHeight(420f);这种半页大小的 PDF 在一些单据打印场景里很实用比如快递面单、小票。4. 真实业务场景报表里的表格、图片与批量生成4.1 PdfTable 快速铺一个结构化报表业务报表里大量使用表格x-easypdf 的表格 API 设计得比较顺手。先建一个表格设置总宽度和边框样式PdfTable table new PdfTable(page); table.setWidth(480f) .setBorderWidth(1f) .setHorizontalStyle(HorizontalStyle.CENTER) .setVerticalStyle(VerticalStyle.MIDDLE);然后逐行创建单元格PdfTable.Row headerRow table.createRow(); headerRow.createCell().setText(订单编号); headerRow.createCell().setText(客户名称); headerRow.createCell().setText(金额); PdfTable.Row dataRow table.createRow(); dataRow.createCell().setText(SO20250101); dataRow.createCell().setText(示例客户); dataRow.createCell().setText(1288.00);表格创建完成之后还需要把表格渲染到某个位置。x-easypdf 一般采用table.write()或者设置表格的位置后自动渲染。具体调用方式在 2.x 小版本之间略有差异但整体思路是一致的先定义行和单元格再渲染到页面。这里给两个建议表头和数据的字体大小、颜色可以分开设置让表格层次更清楚。表头字体加粗或用深色背景数据行默认即可。如果表格行数很多、会超过一页x-easypdf 支持分页渲染但需要你在页面循环里处理断页逻辑。我的做法是预估每行高度在代码里算出“这一页还能放几行”超过就createNewPage()再建一张表。虽然土但可控。4.2 插入 Logo 图片位置与尺寸控制报告类 PDF 基本都要带公司 Logo 或产品图。x-easypdf 的图片对象用法和文本类似PdfImage image new PdfImage(page); image.setImage(logos/company-logo.png) .setPosition(50, 750) .setWidth(100) .setHeight(40) .write();图片路径支持文件路径、URL也支持直接传字节数组。如果你的图片放在 classpath 下用getResourceAsStream读出字节数组再传进去更保险避免路径分隔符在不同操作系统上的差异byte[] logoBytes getClass().getClassLoader() .getResourceAsStream(logos/company-logo.png) .readAllBytes(); PdfImage image new PdfImage(page); image.setImage(logoBytes) .setPosition(50, 750) .setWidth(100) .setHeight(40) .write();缩放图片时有个细节如果只设置setWidth不设置setHeight库会自动按原图比例计算高度这样不会变形。两个都设置反而可能拉伸。我建议宽高只定一个除非你明确知道图片的原始宽高比。4.3 批量导出的线程安全与性能建议真实项目里很少有“导一份 PDF”的需求更多的是“把这个月的 3 万条订单导成 PDF 报表”。批量场景要注意两个问题线程安全和内存。PdfDocument不是线程安全的至少我没有在文档里看到它可以跨线程共享的说明。正确做法是每个请求线程创建自己的PdfDocument用完即关。Spring Boot 默认的 Controller 就是每请求一线程天然满足这个要求。内存方面如果一次循环生成几百页中间不释放任何对象JVM 堆会很快飙升。我的做法是每生成一个PdfDocument就立即save和close不要在内存里积攒。如果是异步导出大文件先生成到临时文件再让用户下载比同步占着 HTTP 连接等半小时合理得多Async public CompletableFutureString generateAsyncReport(Long reportId) { String filePath tempDir /report_ reportId .pdf; PdfDocument document new PdfDocument(); try { // 生成内容... document.save(filePath); return CompletableFuture.completedFuture(filePath); } finally { document.close(); } }异步接口在做后台管理系统时几乎是标配用户点击导出后端先返回一个任务 ID前端轮询任务状态完成后再展示下载链接。这样既不阻塞线程也不会让浏览器等着超时。Spring Boot 里启用Async只需要在主类加一个EnableAsync配一个线程池即可。5. 生产环境踩坑记录字体、编码与资源关闭5.1 Linux 服务器上字体变了样的根因我在第一个项目里吃过这个亏症状是这样的本地 Windows 开发用 x-easypdf 生成 PDF 一切正常发布到 Linux 测试环境后同样代码生成的 PDF中文全部变成了方框。排查过程不复杂。先怀疑是不是库版本问题后来逐步确认是“自己注册字体”导致的。我当时贪心没有用内置FontFamily而是像 iText 一样痴迷于“用系统微软雅黑字体”在代码里加载了 Windows 的字体文件。到了 Linux那个字体文件根本不存在字体加载失败后回退到默认字体中文自然全部丢失。改用FontFamily.HEI_TI后问题立刻消失。所以我的建议很直接优先用内置字体别自己注册系统字体。内置字体的字重和样式可能没有你老板指定的“思源黑体 Semibold”那么精确但报表场景下干净、稳定比字体重更重要。5.2 中文乱码与文件编码问题的排查思路有时候不是字体问题而是编码问题。典型的场景是模板里写死的中文正常从数据库查出来的中文却乱码。这通常不会发生在 PDF 库层面而是在数据读取或文件保存环节。我遇到过一次诡异的事同一个字段在日志里打印出来是正常中文写到 PDF 里却变成乱码。后来发现日志打印时经过了 IDE 的编码处理反而是 PDF 保存时的文本来源没走 UTF-8。排查时不要动不动怀疑 PDF 库先在数据链路每一段打印字段的字节长度确认数据到 PDF 的入口时已经是解码后的正确字符串。另外生成 PDF 后如果又转成了其他格式比如再次读取 PDF 文本可能因为 PDF 内置字体的 ToUnicode 映射不完整导致提取乱码。这种是次要问题一般业务不会关心 PDF 里能不能被文本提取器搜索到。5.3 资源关闭与并发控制的经验PdfDocument.close()这句代码新手特别容易漏掉。它不只是释放一个 Java 对象背后还有 PDFBox 对文档资源的引用、字体缓存、图片数据流。长期不关在并发量上来之后一定会出问题。我在压测环境见过一次事故导出接口平均响应时间持续上升GC 频率翻了几倍最后 Java 进程直接 OOM。当时就是因为在try里生成了大量PdfDocumentsave之后没有close导致 PDFBox 的资源没有及时释放。查代码后发现异常路径上甚至没有 finally 块。正确写法是 try-finally 包住PdfDocument document new PdfDocument(); try { // 所有生成逻辑 document.save(outputStream); } finally { document.close(); }还有一次踩了重复关闭的坑。某个版本里我在 finally 里 close 之后后面的业务代码调用了 document 的一些查询方法结果抛了异常。后来统一了原则close 之后绝对不再碰 document 的任何方法。如果你需要先验证内容再提交可以在 close 前完成所有操作。并发控制方面除了每个线程独立创建 document还要注意输出流不能共享。如果同一个OutputStream被多个线程同时写PDF 文件会被写花。当你用异步批量导出时每条任务用独立的文件路径或独立的ByteArrayOutputStream最后再统一汇总。进阶HTML 转 PDF 的兜底方案说到最后我想提一个经常被忽略的模块。如果你发现用链式 API 拼复杂排版实在太痛苦——比如要做一个居中标题、两栏布局、带圆角边框的封面——别死磕代码直接上 x-easypdf 的 HTML 转 PDF 能力。引入easypdf-openhtmltopdf模块后你可以把 HTML 模板和 CSS 样式交给前端同事或自己用模板引擎渲染后端只做一个转换动作HtmlToPdfConverter converter HtmlToPdfConverter.getHtmlToPdfConverter(); converter.convert(htmlbodyh1你好HTML 转 PDF/h1/body/html, out.pdf);HTML/CSS 本身就是为排版设计的处理文字换行、对齐、间距、页边距这些事远比手写 API 直观。碰上那种业务方反复改样式的场景HTML 方案的最大优势是“改样式不用动 Java 代码”。我现在的项目里纯数据类报表用链式 API带复杂视觉设计的合同、封面、宣传页一律走 HTML 模板两者配合能把各种需求都接住。这套整合方案用下来我最大的感受是以前写 PDF 导出的节奏是“先跟字体纠缠一小时再跟坐标纠缠一小时”现在多数时间花在写业务字段上。如果你也遇到类似的场景记住两句话——能用内置字体就别自己注册字体能让库自动换行就别手写换行逻辑。x-easypdf 不是万能的但至少它把那些最烦人的基础问题挡在了门外。
返回列表