ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

SpringBoot集成docx4j实现DOCX转PDF实战与踩坑指南

SpringBoot集成docx4j实现DOCX转PDF实战与踩坑指南 SpringBoot 项目里做 DOCX 转 PDF我前阵子刚在线上环境踩完坑。当时甲方丢过来一个需求管理后台要能把合同模板转成 PDF 存档而且 Word 文档里还带表格、图片、页眉页脚必须原样渲染。第一反应是让前端装 WPS 插件转但转头一想后端文件流才是正经路子于是开始调研 Java 生态里能用的方案。最后定下来的方案就是docx4j。它的定位是 Java 平台上的 OOXML 操作库能读能写 DOCX也能基于旧版 Office 格式做转换最关键的是纯 Java 实现不依赖本机 Office/Word 环境部署到 Linux 服务器上照样能跑。这篇文章把整套实现脉络和踩过的坑完整写一下内容包括环境选型、Maven 依赖配置、核心转换代码、中文乱码处理、并发性能优化这几个维度适合正在做文档在线预览、合同归档、电子签章前置转换的朋友直接参考。1. 方案选型为什么是 docx4j而不是 LibreOffice 或 Aspose1.1 需求对比轻量级、可控、免费我先把主流的几种 Java 后端转换方案过了一遍大概分成三派调用本机 Office 或 WPS COM 接口质量最稳但需要部署 Windows 环境Linux 上没法用而且并发高了以后 Word 进程会挂稳定性很差。LibreOffice headless 模式转换跨平台、支持格式多但需要在服务器额外安装 LibreOffice 软件进程占用内存大首次启动慢运维复杂度上来了。Aspose.Words 等商业库功能最全、转换保真度很高但价格不便宜一套 license 动辄几万人民币小项目扛不住。docx4j 纯 Java 解析转换既能操作 DOCX 的 XML 结构又能在文档里做内容填充、段落处理送 PDF 也不掉链子。没有本机依赖Maven 一把梭适合嵌入 SpringBoot 服务。这里我尤其想夸 docx4j 的一个点它不只是个转换器它是一个完整的 OOXML 处理框架。比如你需要在生成的 PDF 里保留 Word 中的书签或者要给文档里某个特定表格填充数据后再转换docx4j 可以在内存里直接改文档对象再输出 PDF。这对业务场景复杂的应用来说非常有用。1.2 版本选型与依赖引入我用的是docx4j 11.4.9配合JDK 1.8SpringBoot 版本用的2.7.x。如果你用的是 SpringBoot 3.x需要确认 docx4j 的 Jakarta 命名空间兼容性。之前看到 8.x 老版本和 11.x 的 API 差距不小推荐直接用新版本。Maven 依赖很简单核心加上一个dependency groupIdorg.docx4j/groupId artifactIddocx4j-core/artifactId version11.4.9/version /dependency转 PDF 需要额外的 export 模块再加一个dependency groupIdorg.docx4j/groupId artifactIddocx4j-export-fo/artifactId version11.4.9/version /dependencydocx4j 底层会把 WordprocessingML 映射成 XSL-FO然后用 Apache FOP 渲染成 PDF所以docx4j-export-fo是必需依赖。如果你转换的文档里包含复杂图表可能还需要引入 PL4J 相关的依赖来增强渲染但绝大多数文本加表格场景上面两个依赖就够了。注意docx4j 对「样式」的处理依赖 XSLT 转换某些环境下受 Java 模块化影响会报java.lang.NoClassDefFoundError: javax/xml/bind/JAXBException这时候需要单独补充javax.xml.bind:jaxb-api依赖。后面常见问题部分会说。2. 核心实现SpringBoot 集成 DOCX 转 PDF 的完整流程2.1 先搞清楚 docx4j 转换 PDF 的底层逻辑在写代码之前有必要先理解 docx4j 是怎么把 DOCX 变 PDF 的。DOCX 本质是一个 ZIP 压缩包里面包含word/document.xml、word/styles.xml、word/media/等一系列 XML 和资源文件。docx4j 读取这个 ZIP把 XML 解析成 Java 对象模型也就是 OOXML 的 Java 映射然后调用org.docx4j.convert.out.pdf.PdfConversion的转换器借助 XSL-FO 中间格式最后通过 Apache FOP 输出为 PDF。理解这条链路以后你就能预判几个常见的坑因为 XML 结构、样式定义和字体信息在转换中都要被映射所以字体的可用性直接影响 PDF 中文显示。XSL-FO 中间格式对复杂表格、浮动框的处理并不完美极端复杂的 Word 文档可能会轻微错位。docx4j 转换是在内存里构建模型再渲染所以大文档吃内存生产环境务必要设 JVM 堆内存上限。2.2 封装转换工具类我把转换逻辑封装成一个 Spring 组件方便在 Controller 层和 Service 层复用。核心代码如下import org.docx4j.Docx4J; import org.docx4j.convert.out.pdf.PdfConversion; import org.docx4j.openpackaging.packages.WordprocessingMLPackage; import org.springframework.stereotype.Component; import java.io.File; import java.io.FileOutputStream; import java.io.InputStream; import java.io.OutputStream; Component public class DocxToPdfConverter { /** * 将 DOCX 输入流转为 PDF 输出流 * * param docxInputStream Word 文档输入流 * return PDF 字节数组 */ public byte[] convertToPdf(InputStream docxInputStream) throws Exception { // 1. 加载 WordprocessingMLPackage这一步会完整解析 DOCX WordprocessingMLPackage wordMLPackage WordprocessingMLPackage.load(docxInputStream); // 2. 创建 PDF 转换器 PdfConversion pdfConversion Docx4J.createPdfConversion(wordMLPackage); // 3. 转换并写入字节数组 java.io.ByteArrayOutputStream out new java.io.ByteArrayOutputStream(); pdfConversion.output(out); return out.toByteArray(); } /** * 重载方法直接输出到文件 */ public void convertToPdf(File docxFile, File pdfFile) throws Exception { WordprocessingMLPackage wordMLPackage WordprocessingMLPackage.load(docxFile); Docx4J.toPDF(wordMLPackage, new FileOutputStream(pdfFile)); } }2.3 Controller 层文件上传下载Controller 层把接口暴露给前端实现「上传 DOCX - 转换 - 下载 PDF」的闭环。这里我用 Spring MultipartFile 接收文件import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; RestController RequestMapping(/api/docx) public class DocxConvertController { private final DocxToPdfConverter converter; public DocxConvertController(DocxToPdfConverter converter) { this.converter converter; } PostMapping(/convert) public ResponseEntitybyte[] convert(RequestParam(file) MultipartFile file) throws Exception { byte[] pdfBytes converter.convertToPdf(file.getInputStream()); String fileName URLEncoder.encode( file.getOriginalFilename().replace(.docx, .pdf), StandardCharsets.UTF_8 ); return ResponseEntity.ok() .header(HttpHeaders.CONTENT_DISPOSITION, attachment; filename*UTF-8 fileName) .contentType(MediaType.APPLICATION_PDF) .body(pdfBytes); } }这段代码有几个细节值得说Content-Disposition里用filename*UTF-8这种 RFC 5987 编码方式避免中文文件名在浏览器里乱码。ResponseEntitybyte[]适合文件小于几十 MB 的场景如果文件很大建议直接用StreamingResponseBody流式输出避免大数组占内存。记得对上传文件做格式校验只允许.docx扩展名同时可以检查文件的 MIME 类型防止恶意上传。2.4 批量转换与文件归档扩展实际业务里往往是批量转换比如一批合同模板统一生成 PDF。我一般会把待转换任务丢进线程池并行处理核心代码是在循环里调用转换方法然后记录每个文件的转换状态import java.util.concurrent.*; public class BatchConvertService { private final ExecutorService executor new ThreadPoolExecutor( 4, 8, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue(1000), new ThreadPoolExecutor.CallerRunsPolicy() ); public void batchConvert(ListFile docxFiles, String outputDir) { ListFutureFile futures new ArrayList(); for (File docx : docxFiles) { futures.add(executor.submit(() - { File pdf new File(outputDir, docx.getName().replace(.docx, .pdf)); new DocxToPdfConverter().convertToPdf(docx, pdf); return pdf; })); } // 等待所有任务完成并处理异常 for (FutureFile future : futures) { try { future.get(); } catch (Exception e) { // 记录失败的文档便于重试 log.error(转换失败, e); } } } }这里有个容易被忽略的细节线程池的拒绝策略要用CallerRunsPolicy。因为文档转换是 CPU 密集型任务如果队列满了以后直接丢弃任务会造成文件静默丢失。用CallerRunsPolicy可以保证多余任务由提交线程自己执行宁可阻塞入口也不能丢掉业务数据。3. 实操陷阱中文乱码、字体缺失与样式丢失问题3.1 中文乱码的根源不在代码而在字体docx4j 转到 PDF 时样式会用 XSL-FO 描述PDF 渲染引擎要根据字体描述去系统里找实体字体文件。如果服务器上没有对应中文字体PDF 里就会出现方块字或者乱码。我在 CentOS 服务器上验证过默认最小化安装后系统里只有很少的字库。解决方式有两种方式一安装中文字体到系统推荐# CentOS / RHEL yum install -y fontconfig yum install -y fontconfig-devel # 下载思源黑体或文泉驿字体 # 将 .ttf 或 .ttc 文件放到 /usr/share/fonts/chinese/ fc-cache -fv方式二在 Dockerfile 里把字体打进去如果你的应用是容器化部署建议在构建阶段就把字体复制进去FROM eclipse-temurin:8-jre RUN mkdir -p /usr/share/fonts/chinese COPY fonts/source-han-sans.ttc /usr/share/fonts/chinese/ RUN fc-cache -fv除了系统层面docx4j 还允许你通过配置自定义字体映射。在 WordprocessingMLPackage 加载后可以遍历文档中的RPr字体内存将「宋体」「黑体」等中文名映射到系统里的实际字体路径// 伪代码示意遍历所有 run 的字体设置动态替换字体名称 ListObject allElements wordMLPackage.getMainDocumentPart().getContent(); for (Object element : allElements) { // 判断是否为段落遍历 run找到 rPr/rFonts设置 eastAsia 字体 }说实话这种动态换字体的方式比较繁琐大多数场景直接装字库就解决了。但如果你做的是 SaaS 服务不能控制客户服务器环境那就必须在代码里做字体兜底方案保证最少有一个中文字体在 classpath 里。3.2 样式丢失表格边框消失、段落缩进不对docx4j 转 PDF 的忠实度比 Aspose 差一些尤其是 Word 里那些「依赖于主题的样式」和「嵌套表格」偶尔会出现渲染不一致。我遇到最多的是表格边框打印不出来原因是 Word 的表格边框定义在tblPr的tblBorders里而 docx4j 的 FO 转换对tblBorders某些组合支持不好。解决方案有两个方向尽量让 Word 文档里用「内置表格样式」和「默认段落样式」不要用自定义主题。 docx4j 对内建样式支持更好。在转换前手动给文档注入基础 CSS 补丁通过Docx4J.toFO前的FOSettings设置自定义 XSLT把缺的样式补上。如果是极少数文档有样式问题我更推荐换一个思路用 docx4j 的WordprocessingMLPackage重新构建表格、边框样式把不可控的样式渲染变成可控模型。虽然麻烦些但能保证输出一致性。3.3 大文档转换内存溢出OOMWord 文档如果超过 50MB 或几千页docx4j 的转换过程可能会压垮 JVM。处理这类问题需要从两个方向入手第一合理限制上传文件大小。 SpringBoot 里可以配置spring: servlet: multipart: max-file-size: 20MB max-request-size: 50MB超过阈值的文档直接拒绝避免服务被拖垮。第二调整 JVM 堆内存。如果是 Docker 容器建议-Xmx2g起步如果是物理机可以根据上传并发量适当调整。docx4j 的解析过程会把整个文档的 XML 模型加载到内存这个内存占用没有固定公式我实测一份 10MB 的 DOCX含大量图片转 PDF峰值内存约 500MB大家可以按十倍余量预估。3.4 排除干扰避免把敏感信息写进 PDF 元数据还有一个容易被忽略的合规问题。DOCX 文件本身可能包含作者、公司、修订记录等元数据转成 PDF 时这些信息会默认带过去。我在处理一些外部客户的合同时会先把元数据清零再转wordMLPackage.getPackageProperties().setCreator(null); wordMLPackage.getPackageProperties().setDescription(null); wordMLPackage.getPackageProperties().setTitle(null);这属于细节层面但一旦涉及隐私合规往往就是关键一环。4. 常见问题与排查技巧让你的转换服务更稳4.1 转换过程抛javax.xml.bind或NoClassDefFoundError这个在高版本 JDK 上经常出现。JDK 9 以后JAXB 被从标准库中移除docx4j 底层需要解析 XML如果在 classpath 里找不到 JAXB 类就会报错。解决方式是补依赖dependency groupIdjavax.xml.bind/groupId artifactIdjaxb-api/artifactId version2.3.1/version /dependency dependency groupIdorg.glassfish.jaxb/groupId artifactIdjaxb-runtime/artifactId version2.3.1/version /dependency4.2 生成 PDF 后中文变成「□□□」或空白按优先级排查确认服务器是否有中文字体执行fc-list :langzh如果输出为空说明没装字库。按上文方式安装后重启应用。确认文档编辑器用的字体名称如果文档里用的字体既不是系统自带也没有安装docx4j 无法映射就会出现空白。这种情况要提供自定义字体别名将「微软雅黑」映射到「思源黑体」。检查 docx4j 的日志开启 debug 日志会输出 FO 内容查看里面嵌入的字体 URL 是否指向了系统不存在的路径。4.3 转换时间长、CPU 100%docx4j 是纯 CPU 解析加渲染性能损耗远大于调用本地 Office COM。如果是大文件并发转换机器 CPU 会被瞬间打满。我建议做三件事在线程池层面对并发数做限制单机建议corePoolSize不超过 CPU 核心数减一我们生产环境是 4C8G 的容器并发设置为 3实测吞吐度和稳定性最平衡。在 Controller 层做等待队列如果同时来了 20 个转换任务不要让它们全部挤进线程池用有界队列挡住。因为docx4j-export-fo依赖 FOP 渲染FOP 对字体缓存有自己的缓存机制可以提前预热的初始化一次转换把字体缓存加载好后续转换速度会明显提升。4.4 转换出的 PDF 比原始 DOCX 大得多这是因为 docx4j 默认会把文档中的图片原样嵌入而有些 Word 文档里嵌入了多张高清大图所以 PDF 文件也会变大。如果想压缩可以加一层 FOP 配置调整图片质量或者提前用 Java 的 ImageIO 对图片做缩放。但必须提醒一句过度压缩会让 PDF 变得模糊合同扫描类图片不建议压。5. 生产环境经验稳定性、日志与监控5.1 日志规范转换服务做好日志非常重要。我在项目里用了 SLF4J 记录以下关键信息收到转换请求文件名、文件大小。转换开始时间、结束时间、耗时。转换异常堆栈、失败原因。结果文件大小、输出路径。出了线上问题日志就是第一手排查依据。我用Slf4j加上 AOP 切面统一打印这些数据避免散落在业务代码里。5.2 优雅停机与任务队列持久化如果服务端可能在半夜被自动扩容销毁正在执行的转换任务会中断。要保证可靠性建议把待转换任务列表丢进 MQ比如 RabbitMQ 或 Kafka消费者从 MQ 拿任务来执行。MQ 中的消息如果消费失败会进行重试极大降低文件丢失概率。对于一般项目也可以先存数据库表状态字段标记待转换 / 转换中 / 成功 / 失败通过 Job 定期扫描补偿。5.3 预热机制我提到过 FOP 字体缓存的问题。为了不浪费第一次请求的时间我通常会在应用启动后异步跑一个 1 页空文档的转换把字体资源、FOP 配置全部加载到内存里。这种做法在线上验证过第一次转换耗时能从 3 秒降到 500 毫秒左右效果很明显。Component public class Docx4jPreloader implements ApplicationRunner { Override public void run(ApplicationArguments args) throws Exception { try { // 用临时空文档触发字体缓存加载 WordprocessingMLPackage pkg WordprocessingMLPackage.createPackage(); pkg.getMainDocumentPart().addParagraphOfText(预热字体缓存); File tmpPdf File.createTempFile(preload, .pdf); Docx4J.toPDF(pkg, new FileOutputStream(tmpPdf)); tmpPdf.delete(); } catch (Exception e) { log.warn(docx4j预热失败, e); } } }5.4 一种常见错误文件名大小写与扩展名判断文件转换前做扩展名校验时千万别只判断.docx而忽略了.DOCX。我习惯用StringUtils.endsWithIgnoreCase或直接转为小写再判断。还有用户可能上传的是.doc老格式docx4j 对于.doc的支持有限需要先转换为.docx但这个转换能力不在本文范围内实际工程中如果遇到.doc文件建议直接提示用户转成.docx再上传。6. 扩展思路从「转换」到「内容处理」的能力升级6.1 表格数据填充后转 PDFdocx4j 最有价值的使用场景之一是先对 DOCX 模板做数据填充再转成 PDF。比如合同、报价单、录取通知书这类固定版式的文档我们可以提前在 Word 里用占位符做好模板后端读取模板后用 docx4j 定位占位符并替换文字然后直接转 PDF。核心逻辑可以用变量替换来实现// 简化示范寻找所有 w:t 文本节点替换 {{name}} 占位符 String regex \\{\\{\\s*name\\s*\\}\\}; ListObject texts wordMLPackage.getMainDocumentPart().getJAXBNodesViaXPath(//w:t, true); for (Object obj : texts) { Text text (Text) obj; String value text.getValue(); if (value ! null value.contains({{name}})) { text.setValue(value.replaceAll(regex, 张三)); } }这种方案比直接在 PDF 上用 PDF 编辑器改文字要可靠得多因为 PowerPoint 生成的 PDF 文本块位置固定直接改会造成格式错乱。docx4j 先改 XML 再渲染版式完全可控。6.2 添加水印后转 PDF业务上经常需要给 PDF 加水印尤其是合同归档和内部审批。docx4j 可以直接在 WordprocessingMLPackage 里添加水印文本或图片再输出 PDF。最简单的是在页脚位置插入一个居中的透明文字块// 添加一个段落并设置旋转角度和透明度 Paragraph watermarkPara new Paragraph(); // 配置 run 属性设置字体大小、颜色、透明度 // 最后添加到 footer 里这里要补充说明docx4j 对水印的支持基于 Word 的「衬于文字下方」特性如果 Word 版本太老可能不支持需要检查水印元素是否被正确加载。6.3 参数化配置与公共转换服务封装如果你所在公司有多个系统都需要 DOCX 转 PDF最好封装成一个公共微服务对外提供 REST API其他系统通过 HTTP 调用。这样做的好处是字体、JDK、docx4j 版本统一维护。并发控制、日志监控只做一套。后续想换转 PDF 引擎也只需要改服务内部实现不用动各个业务系统。我看很多团队喜欢在每一个业务工程里都写一份转换代码这其实是重复建设。虽然部署一个附加系统也需要成本但长期来看可维护性高得多。提示服务化改造时建议用 OpenAPI 定义接口规范输入base64或二进制流输出 PDF 流。注意请求体大小限制最好支持分片上传避免大文档被网关拦截。7. 最终落地的一些心得再聊点实在的。docx4j 这套方案我在生产环境跑了半年多日均转换几百份文档稳定性方面给了我不小的信心。但如果你问我能不能做到 100% 还原 Word 版式我的答案是做不到尤其是那些图文混排复杂、文本框层叠的文档多多少少会有偏差。所以我的建议是业务上把「转换」当做一个尽力而为的兜底方案而不是官方格式转换器。如果是对外正式交付的公文最好用 Adobe 插件直接导出或者是先转 PDF 后人工抽检。如果只是内部预览、归档、加个水印docx4j 完全够用。还遇到过一个细节转换出来的 PDF 一般是不带目录书签的。docx4j 在转换时会根据文档里的w:bookmarkStart生成 PDF 书签如果你的 Word 文档里没有设置书签那 PDF 左侧导航也不会显示目录。因此若客户有 PDF 书签需求最好要求他们先在 Word 模板里插入书签或者在转换前用 docx4j 自动根据标题样式生成书签。整个项目做下来最值钱的经验并不是「加个 Maven 依赖调用一行 API」而是搞清楚 docx4j 的转换链路、字体机制和并发控制。把这三点吃透无论后续换版本还是扩展功能心里都有底。希望能帮正在做这个功能的你少踩几个坑。
返回列表