好的,这是一份详细、实用且权威的 Java Spring Boot 实现 DOCX 转 PDF(基于 docx4j)的(基于docx4j的轻量级开源方案)全面指南。
目录
- 引言
- 1.1 DOCX 与 PDF 格式简介
- 1.2 转换需求与应用场景
- 1.3 方案选型:为什么选择 docx4j?
- 环境准备
- 2.1 基础环境要求
- 2.2 创建 Spring Boot 项目
- 2.3 添加 docx4j 及相关依赖
- 核心转换实现
- 3.1 基础转换流程
- 3.2 加载 DOCX 文档 (
WordprocessingMLPackage) - 3.3 配置 PDF 转换选项 (
PDFSettings) - 3.4 执行转换 (
Docx4J.toPDF) - 3.5 完整代码示例 (Service 层)
- 高级配置与优化
- 4.1 处理中文字体与乱码问题
- 4.2 设置 PDF 输出属性 (权限、元数据)
- 4.3 处理转换异常与日志记录
- 4.4 性能考量与内存管理
- 集成到 Spring Boot 应用
- 5.1 创建 RESTful API 接口 (Controller)
- 5.2 文件上传与下载处理
- 5.3 接口测试 (使用 Postman 或 curl)
- 测试与验证
- 6.1 单元测试 (JUnit)
- 6.2 转换结果验证
- 常见问题与解决方案 (FAQ)
- 总结
1. 引言
1.1 DOCX 与 PDF 格式简介
- DOCX: 是 Microsoft Office Word 2007 及以后版本使用的基于 XML 的文档格式标准 (Office Open XML)。它包含了文本内容、样式、图像、表格、图表等多种元素,主要用于文档的编辑和修改。
- PDF: 是由 Adobe Systems 开发的一种用于可靠地呈现和交换文档的文件格式。其特点是跨平台、保真度高、不易被编辑,非常适合用于文档的发布、共享和存档。
1.2 转换需求与应用场景
将 DOCX 文档转换为 PDF 的需求非常普遍,常见场景包括:
- 文档发布与共享: 确保接收方看到的内容与原始文档一致,不受软件版本或字体差异影响。
- 合同与协议签署: PDF 格式更利于电子签名和长期保存。
- 报告生成系统: 后端生成 DOCX 格式的报告,转换为 PDF 后提供给用户下载。
- 内容管理系统 (CMS): 用户上传 DOCX,系统自动转换为 PDF 存储或分发。
- 归档与合规: 某些行业或法规要求文档必须以 PDF 格式存档。
1.3 方案选型:为什么选择 docx4j?
有多种技术可以实现 DOCX 转 PDF,例如:
- Microsoft Office 互操作性 (COM): 依赖安装 Office,不适用于服务器环境,性能差,稳定性低。
- Apache POI: 主要擅长读写 Office 文档,其 PDF 转换功能较弱(特别是复杂格式)。
- 商业库 (如 Aspose.Words): 功能强大稳定,但需要付费。
- docx4j : 一个专注于处理 Open XML 文档 (DOCX, PPTX, XLSX) 的开源 Java 库。其优势在于:
- 纯 Java 实现: 不依赖外部软件,可在任何支持 Java 的平台上运行,包括 Linux 服务器。
- 轻量级: 相较于一些商业库,体积和依赖相对较小。
- 开源免费 (LGPL 许可证): 可自由使用于商业项目。
- 功能专注: 对 DOCX 的结构和内容有深入的支持。
- PDF 输出 : 通过
docx4j-export-FO和docx4j-export-PDF模块,利用 Apache FOP (Formatting Objects Processor) 或其他渲染器将 DOCX 内容转换为 PDF。本指南使用其内置的 Plutext PDF 转换器(基于docx4j-export-PDF)。
因此,docx4j 提供了一个在 Spring Boot 应用中实现轻量级、开源、可移植的 DOCX 转 PDF 功能的优秀方案。
2. 环境准备
2.1 基础环境要求
- Java Development Kit (JDK): 推荐使用 JDK 8, 11 或 17 (LTS 版本)。
- 构建工具: Apache Maven (推荐) 或 Gradle。
- 集成开发环境 (IDE): IntelliJ IDEA, Eclipse, VS Code 等。
- Spring Boot: 推荐使用较新稳定版本 (如 2.7.x, 3.0.x+)。
2.2 创建 Spring Boot 项目
可以使用以下方式之一创建项目:
- Spring Initializr (https://start.spring.io/) : 在网页上选择 Maven Project、Java、Spring Boot 版本,添加
Spring Web依赖,生成项目并下载。 - IDE 创建向导: IntelliJ IDEA 或 Eclipse 通常内置了 Spring Initializr 支持,可以通过向导创建。
- 命令行 : (可选) 使用
curl或直接下载。
项目创建后,确保基本的 Spring Boot 应用结构正常 (src/main/java, src/main/resources, pom.xml)。
2.3 添加 docx4j 及相关依赖
在项目的 pom.xml 文件中,添加以下依赖:
XML
<dependencies>
<!-- Spring Boot Starter Web (提供 RESTful 支持) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- docx4j 核心库 -->
<dependency>
<groupId>org.docx4j</groupId>
<artifactId>docx4j</artifactId>
<version>11.4.4</version> <!-- 请检查并使用最新版本 -->
</dependency>
<!-- docx4j 导出 PDF 模块 (使用 Plutext PDF 转换器) -->
<dependency>
<groupId>org.docx4j</groupId>
<artifactId>docx4j-export-PDF</artifactId>
<version>11.4.4</version> <!-- 版本应与核心库一致 -->
</dependency>
<!-- Apache FOP (可选,但 docx4j-export-PDF 内部可能使用或需要其部分功能) -->
<!-- docx4j-export-PDF 的 Plutext 转换器通常不直接依赖 FOP,但添加以防兼容性问题 -->
<dependency>
<groupId>org.apache.xmlgraphics</groupId>
<artifactId>fop</artifactId>
<version>2.7</version> <!-- 请检查并使用最新版本 -->
</dependency>
<!-- FOP 需要 XML Graphics Commons -->
<dependency>
<groupId>org.apache.xmlgraphics</groupId>
<artifactId>xmlgraphics-commons</artifactId>
<version>2.7</version> <!-- 请检查并使用最新版本 -->
</dependency>
<!-- 日志依赖 (SLF4J + Logback, 通常由 Spring Boot starter 提供) -->
<!-- 确保项目中存在有效的日志实现 -->
</dependencies>
注意:
- 请务必访问 docx4j Maven Repository 和 FOP Maven Repository 查看并替换为最新的稳定版本号。
- 添加
fop和xmlgraphics-commons是为了避免docx4j-export-PDF在转换时可能因缺少某些类而报错(如org.apache.xmlgraphics.util.MimeConstants)。虽然 Plutext 转换器可能不完全依赖它们,但添加它们是常见做法。 - Spring Boot 的
spring-boot-starter-web通常已经包含了spring-boot-starter-logging,它提供了 SLF4J 接口和 Logback 实现。确保日志配置正确,以便记录转换过程中的信息或错误。
运行 mvn clean install (或使用 IDE 的 Maven 工具) 下载依赖。
3. 核心转换实现
DOCX 转 PDF 的核心逻辑封装在一个 Service 类中。
3.1 基础转换流程
- 加载 DOCX : 将输入的 DOCX 文件加载到 docx4j 的内存表示 (
WordprocessingMLPackage) 中。 - 配置 PDF 选项: (可选) 设置 PDF 输出的各种属性。
- 执行转换 : 调用 docx4j 的方法将
WordprocessingMLPackage转换为 PDF 字节流或文件。 - 输出 PDF: 将转换后的 PDF 字节流写入文件或 HTTP 响应。
3.2 加载 DOCX 文档 (WordprocessingMLPackage)
docx4j 使用 WordprocessingMLPackage 对象来表示一个 DOCX 文档。可以从多种来源加载:
File对象: 从本地文件系统加载。InputStream: 从输入流加载 (例如上传的文件流)。URL: 从网络资源加载。
java
import org.docx4j.openpackaging.exceptions.Docx4JException;
import org.docx4j.openpackaging.packages.WordprocessingMLPackage;
public WordprocessingMLPackage loadDocx(File docxFile) throws Docx4JException {
return WordprocessingMLPackage.load(docxFile);
}
public WordprocessingMLPackage loadDocx(InputStream inputStream) throws Docx4JException {
return WordprocessingMLPackage.load(inputStream);
}
3.3 配置 PDF 转换选项 (PDFSettings)
PDFSettings 对象允许你定制 PDF 输出。常用设置包括:
setFoProcessorName(String): 设置使用的 FO 处理器。对于docx4j-export-PDF,通常使用"Plutext"(这是默认值)。setObfuscateFonts(boolean): 是否混淆字体 (可能用于规避某些字体许可问题,慎用)。setFontMapping(MappedFonts): 字体映射 (解决缺失字体问题,见 4.1 节)。setAccessibility(boolean): (实验性) 是否添加 PDF 可访问性标签。setRunOnly(boolean): (高级) 仅运行转换,不进行其他处理。
java
import org.docx4j.convert.out.pdf.PdfConversion;
import org.docx4j.convert.out.pdf.PdfSettings;
public PdfSettings createPdfSettings() {
PdfSettings pdfSettings = new PdfSettings();
// 使用 Plutext 转换器 (通常是默认)
pdfSettings.setFoProcessorName("Plutext");
// 是否混淆字体 (一般保持 false)
pdfSettings.setObfuscateFonts(false);
// 其他设置...
return pdfSettings;
}
3.4 执行转换 (Docx4J.toPDF)
docx4j 提供了便捷的方法 Docx4J.toPDF 来执行转换。它需要 WordprocessingMLPackage 和 PdfSettings,并输出到指定的 OutputStream。
java
import org.docx4j.Docx4J;
import org.docx4j.openpackaging.exceptions.Docx4JException;
public void convertToPdf(WordprocessingMLPackage wordMLPackage, PdfSettings pdfSettings, OutputStream outputStream)
throws Docx4JException {
Docx4J.toPDF(wordMLPackage, outputStream, pdfSettings);
}
3.5 完整代码示例 (Service 层)
创建一个 Spring Service (DocxToPdfService) 来封装转换逻辑:
java
package com.example.docx2pdf.service;
import org.docx4j.Docx4J;
import org.docx4j.convert.out.pdf.PdfConversion;
import org.docx4j.convert.out.pdf.PdfSettings;
import org.docx4j.openpackaging.exceptions.Docx4JException;
import org.docx4j.openpackaging.packages.WordprocessingMLPackage;
import org.springframework.stereotype.Service;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
@Service
public class DocxToPdfService {
/**
* 将输入的 DOCX 文件流转换为 PDF,并写入输出流
*
* @param docxInputStream DOCX 文件输入流
* @param pdfOutputStream PDF 输出流
* @throws Docx4JException DOCX 处理或转换错误
* @throws IOException 流操作错误
*/
public void convertDocxToPdf(InputStream docxInputStream, OutputStream pdfOutputStream)
throws Docx4JException, IOException {
// 1. 加载 DOCX 文档
WordprocessingMLPackage wordMLPackage = WordprocessingMLPackage.load(docxInputStream);
// 2. 创建 PDF 设置 (使用默认或自定义设置)
PdfSettings pdfSettings = createPdfSettings(); // 调用下面的方法创建设置
// 3. 执行转换
Docx4J.toPDF(wordMLPackage, pdfOutputStream, pdfSettings);
// 4. 重要:确保刷新输出流 (通常在调用方处理关闭)
pdfOutputStream.flush();
}
/**
* (可选) 创建并配置 PDF 设置
*
* @return PdfSettings 对象
*/
private PdfSettings createPdfSettings() {
PdfSettings pdfSettings = new PdfSettings();
// 使用 Plutext 转换器 (通常是默认,显式设置也可)
pdfSettings.setFoProcessorName("Plutext");
// 设置其他选项,例如字体映射等 (见高级配置)
return pdfSettings;
}
/**
* 便捷方法:将 DOCX 文件转换为 PDF 文件
*
* @param inputDocxFile 输入 DOCX 文件
* @param outputPdfFile 输出 PDF 文件
* @throws Docx4JException
* @throws IOException
*/
public void convertDocxFileToPdfFile(File inputDocxFile, File outputPdfFile)
throws Docx4JException, IOException {
try (FileOutputStream fos = new FileOutputStream(outputPdfFile)) {
convertDocxToPdf(new java.io.FileInputStream(inputDocxFile), fos);
}
}
}
关键点说明:
convertDocxToPdf方法接受InputStream和OutputStream,使其非常灵活,可以处理来自网络上传、文件系统或内存的数据流,并输出到文件、HTTP 响应或内存。- 使用了
try-with-resources语句 (在convertDocxFileToPdfFile中) 确保FileOutputStream被正确关闭。在convertDocxToPdf方法中,流的关闭责任交给了调用者(例如,Controller 处理 HTTP 响应流)。 flush()确保所有缓冲的数据都写入输出流。createPdfSettings()方法提供了配置扩展点。目前使用默认 Plutext 设置。
4. 高级配置与优化
4.1 处理中文字体与乱码问题
问题描述: 如果 DOCX 文档中使用了服务器上未安装的字体(尤其是中文字体如宋体、黑体),转换后的 PDF 可能会出现字体替换(如显示为 Times New Roman)或方块乱码。
解决方案:
- 物理安装字体: 将需要的字体文件 (TTF 或 OTF) 安装到运行 Spring Boot 应用的服务器操作系统上。docx4j 的 Plutext 转换器会尝试使用系统已安装的字体进行渲染。这是最直接有效的方法,但可能受限于服务器环境和字体许可。
- 使用
FontMapper进行映射 : docx4j 提供了FontMapper接口,允许你将 DOCX 中使用的字体名称映射到 PDF 中应该使用的字体名称或物理字体文件路径。这对于中文字体尤为重要!
java
import org.docx4j.fonts.MappedFonts;
import org.docx4j.fonts.PhysicalFont;
import org.docx4j.fonts.PhysicalFonts;
import org.docx4j.fonts.FontMapper;
private PdfSettings createPdfSettings() {
PdfSettings pdfSettings = new PdfSettings();
pdfSettings.setFoProcessorName("Plutext");
// 创建字体映射器
FontMapper fontMapper = new BestMatchingMapper(); // 或者 PhysicalFontMapper
// 关键:注册中文字体映射
// 假设服务器安装了 SimSun (宋体) 和 SimHei (黑体)
// 将 DOCX 中的 "宋体" 映射到物理字体 "SimSun"
fontMapper.put("宋体", PhysicalFonts.get("SimSun"));
fontMapper.put("SimSun", PhysicalFonts.get("SimSun")); // 有时字体名是英文的
fontMapper.put("黑体", PhysicalFonts.get("SimHei"));
fontMapper.put("SimHei", PhysicalFonts.get("SimHei"));
// 如果需要,映射其他常用字体
fontMapper.put("Calibri", PhysicalFonts.get("Calibri")); // 假设服务器有
fontMapper.put("Arial", PhysicalFonts.get("Arial"));
// 将 FontMapper 设置到 PdfSettings 的 MappedFonts 中
MappedFonts mappedFonts = new MappedFonts();
mappedFonts.setMapper(fontMapper);
pdfSettings.setFontMapping(mappedFonts);
return pdfSettings;
}
说明:
PhysicalFonts.get(String fontName)尝试查找系统中安装的、与给定名称匹配的物理字体。BestMatchingMapper或PhysicalFontMapper是FontMapper的实现类,用于处理映射逻辑。- 你需要准确知道 DOCX 中使用的字体名称(可以通过 Word 查看或程序分析)以及服务器上可用的对应字体名称。
- 如果服务器没有安装所需字体,你需要将字体文件放在应用可以访问的位置(例如
src/main/resources/fonts),并使用PhysicalFonts.addPhysicalFont(String path)注册,然后在FontMapper中映射到这个注册的字体名。注意字体文件许可问题!
java
// 示例:加载资源目录下的字体文件 (打包在 JAR 内)
PhysicalFonts.addPhysicalFont("/fonts/simsun.ttf"); // 注意路径,可能需要使用 ClassLoader
PhysicalFonts.addPhysicalFont("/fonts/simhei.ttf");
// 然后在 FontMapper 中映射
fontMapper.put("宋体", PhysicalFonts.get("simsun")); // 注意这里用的是注册时的名字,可能不是"SimSun"
4.2 设置 PDF 输出属性 (权限、元数据)
PdfSettings 允许通过 org.docx4j.convert.out.pdf.PdfConversion 设置一些 PDF 属性。一种常见方式是创建 PdfConversion 实例进行配置:
java
private PdfSettings createPdfSettings() {
PdfSettings pdfSettings = new PdfSettings();
pdfSettings.setFoProcessorName("Plutext");
// 创建 PdfConversion 实例进行更详细的设置
PdfConversion pdfConversion = pdfSettings.getPdfConversion();
// 设置 PDF 标题、作者等元数据 (可选)
pdfConversion.setTitle("Converted Document");
pdfConversion.setAuthor("My Application");
// 设置 PDF 权限 (可选,需要了解 iText 的 PdfWriter 常量)
// 注意:docx4j 内部使用 iText 5.x (AGPL 许可) 或 Flying Saucer 等,权限设置可能受限或复杂。
// pdfConversion.setPdfPermissions(...); // 通常需要直接操作底层 iText PdfWriter
// 其他高级设置...
// pdfConversion.setPdfVersion(...);
// pdfConversion.setTagged(...); // 可访问性
return pdfSettings;
}
注意: 深入设置 PDF 权限 (PdfWriter 的 ALLOW_XXX 常量) 通常需要直接访问底层的 PDF 生成库 (如 iText)。docx4j 的抽象层可能无法方便地设置所有权限。如果对权限有严格要求,可能需要考虑其他更底层的 PDF 生成库,或者生成 PDF 后再使用其他库 (如 Apache PDFBox) 进行权限修改。使用 iText 5.x 需要注意其 AGPL 许可证对分发的要求。
4.3 处理转换异常与日志记录
转换过程可能遇到多种错误:
Docx4JException: DOCX 加载、解析或转换过程中的错误(例如文件损坏、格式不支持)。IOException: 流读写错误。- 字体缺失导致的渲染问题(可能表现为乱码,不一定抛异常)。
- 内存不足 (
OutOfMemoryError): 处理大型复杂文档时。
异常处理策略:
- 在 Service 方法中声明抛出 : 如示例所示,将
Docx4JException和IOException抛给调用者 (Controller)。 - Controller 层捕获并处理: 在 Controller 中捕获异常,转换为友好的 HTTP 错误响应 (如 500 错误,附带错误信息)。
- 记录日志 : 在 Service 或 Controller 中使用日志记录器 (
Logger) 详细记录错误信息、堆栈跟踪和可能相关的文档信息(注意隐私和安全),便于排查。
java
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Service
public class DocxToPdfService {
private static final Logger logger = LoggerFactory.getLogger(DocxToPdfService.class);
public void convertDocxToPdf(InputStream docxInputStream, OutputStream pdfOutputStream)
throws Docx4JException, IOException {
try {
WordprocessingMLPackage wordMLPackage = WordprocessingMLPackage.load(docxInputStream);
PdfSettings pdfSettings = createPdfSettings();
Docx4J.toPDF(wordMLPackage, pdfOutputStream, pdfSettings);
pdfOutputStream.flush();
} catch (Docx4JException e) {
logger.error("DOCX 处理或转换失败", e);
throw e; // 重新抛出,由调用方处理
} catch (IOException e) {
logger.error("IO 操作失败", e);
throw e;
}
}
}
在 Controller 中处理:
java
@RestController
@RequestMapping("/api/convert")
public class ConversionController {
@Autowired
private DocxToPdfService docxToPdfService;
@PostMapping("/docx-to-pdf")
public ResponseEntity<Resource> convertDocxToPdf(@RequestParam("file") MultipartFile file) {
try {
// ... 创建临时文件或直接使用流 ...
ByteArrayOutputStream pdfOutputStream = new ByteArrayOutputStream();
docxToPdfService.convertDocxToPdf(file.getInputStream(), pdfOutputStream);
ByteArrayResource resource = new ByteArrayResource(pdfOutputStream.toByteArray());
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=converted.pdf")
.contentType(MediaType.APPLICATION_PDF)
.body(resource);
} catch (Docx4JException | IOException e) {
// 记录日志 (Controller 也可以有自己的 Logger)
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body("转换失败: " + e.getMessage()); // 注意:简单返回字符串,生产环境应更友好
}
}
}
4.4 性能考量与内存管理
- 内存消耗 : docx4j 在加载 DOCX 时会将整个文档(包括嵌入的图片等资源)解析到内存中的
WordprocessingMLPackage对象。处理大型 或包含高分辨率图片 的 DOCX 文件时,可能导致显著的堆内存消耗,甚至OutOfMemoryError。 - 性能: 转换过程(特别是复杂排版和大量图片)可能是 CPU 密集型的。
优化建议:
- 增加 JVM 堆内存 : 在启动 Spring Boot 应用时,通过
-Xmx参数设置更大的最大堆空间 (如-Xmx1024m或-Xmx2048m)。 - 流式处理?: docx4j 主要基于内存模型,没有直接的流式处理接口来处理超大文档。对于超大文件,可能需要考虑分拆文档或使用其他方案。
- 图片处理 : 如果 DOCX 包含大量图片,考虑在转换前或转换过程中 (如果 docx4j 支持) 对图片进行压缩或缩放。这可能需要深入操作
WordprocessingMLPackage中的BinaryPart。 - 异步处理 : 对于耗时较长的转换任务,使用 Spring 的
@Async或消息队列 (如 RabbitMQ, Kafka) 进行异步处理,避免阻塞 HTTP 请求线程。将转换任务提交到线程池,完成后通过通知 (如 WebSocket, 邮件, 回调 URL) 或提供下载链接。 - 资源清理 : 确保及时关闭不再使用的
InputStream,OutputStream。WordprocessingMLPackage对象在转换完成后应解除引用,以便 GC 回收。 - 监控: 使用监控工具 (如 Spring Boot Actuator, Prometheus) 监控应用的内存使用情况和转换接口的性能指标 (耗时、成功率)。
5. 集成到 Spring Boot 应用
5.1 创建 RESTful API 接口 (Controller)
创建一个 Controller 来提供 DOCX 转 PDF 的 HTTP 接口。通常使用 POST 请求接收上传的 DOCX 文件。
java
package com.example.docx2pdf.controller;
import com.example.docx2pdf.service.DocxToPdfService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import org.docx4j.openpackaging.exceptions.Docx4JException;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
@RestController
@RequestMapping("/api/convert")
public class ConversionController {
private final DocxToPdfService docxToPdfService;
@Autowired
public ConversionController(DocxToPdfService docxToPdfService) {
this.docxToPdfService = docxToPdfService;
}
@PostMapping("/docx-to-pdf")
public ResponseEntity<Resource> convertDocxToPdf(@RequestParam("file") MultipartFile file) {
// 1. 检查文件是否为空
if (file.isEmpty()) {
return ResponseEntity.badRequest().body("请上传一个 DOCX 文件");
}
// 2. 检查文件类型 (可选,非绝对可靠)
String contentType = file.getContentType();
if (contentType == null || !contentType.equals("application/vnd.openxmlformats-officedocument.wordprocessingml.document")) {
return ResponseEntity.badRequest().body("仅支持 DOCX 格式 (.docx)");
}
// 3. 执行转换
try (ByteArrayOutputStream pdfOutputStream = new ByteArrayOutputStream()) {
docxToPdfService.convertDocxToPdf(file.getInputStream(), pdfOutputStream);
// 4. 准备 PDF 响应
byte[] pdfBytes = pdfOutputStream.toByteArray();
ByteArrayResource resource = new ByteArrayResource(pdfBytes);
// 5. 构建响应:PDF 文件下载
String filename = file.getOriginalFilename();
if (filename != null) {
filename = filename.replaceFirst("\\.docx$", "") + ".pdf"; // 替换扩展名
} else {
filename = "converted.pdf";
}
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + filename)
.contentType(MediaType.APPLICATION_PDF)
.contentLength(pdfBytes.length)
.body(resource);
} catch (IOException | Docx4JException e) {
// 6. 处理错误
return ResponseEntity.internalServerError()
.body("转换失败: " + e.getMessage()); // 生产环境应返回更友好的错误对象
}
}
}
5.2 文件上传与下载处理
- 上传 : 使用
@RequestParam("file") MultipartFile file接收上传的文件。Spring Boot 自动处理multipart/form-data请求。 - 下载 :
- 将转换后的 PDF 数据写入一个
ByteArrayOutputStream。 - 将
ByteArrayOutputStream的内容转换为ByteArrayResource。 - 设置 HTTP 响应头:
Content-Disposition: attachment; filename=...: 提示浏览器下载文件,并指定文件名。Content-Type: application/pdf: 声明响应体是 PDF 格式。Content-Length: 设置文件大小。
- 将
ByteArrayResource作为响应体返回。
- 将转换后的 PDF 数据写入一个
使用临时文件 (替代方案):
对于非常大的文件,将整个 PDF 先写入内存 (ByteArrayOutputStream) 可能不高效或导致 OOM。可以考虑:
- 将上传的 DOCX 文件先保存到服务器临时目录 (
File.createTempFile())。 - 使用
docxToPdfService.convertDocxFileToPdfFile(inputTempFile, outputTempFile)进行转换。 - 使用
FileSystemResource或InputStreamResource包装输出 PDF 临时文件。 - 在响应发送完成后或在
finally块中删除临时文件。
java
try {
File inputTempFile = File.createTempFile("upload-", ".docx");
file.transferTo(inputTempFile); // 保存上传文件到临时位置
File outputTempFile = File.createTempFile("converted-", ".pdf");
docxToPdfService.convertDocxFileToPdfFile(inputTempFile, outputTempFile);
Path pdfPath = outputTempFile.toPath();
InputStreamResource resource = new InputStreamResource(new FileInputStream(outputTempFile));
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=converted.pdf")
.contentType(MediaType.APPLICATION_PDF)
.contentLength(Files.size(pdfPath))
.body(resource);
} finally {
// 尝试删除临时文件
if (inputTempFile != null) inputTempFile.delete();
if (outputTempFile != null) outputTempFile.delete();
}
5.3 接口测试 (使用 Postman 或 curl)
使用 Postman:
- 启动 Spring Boot 应用。
- 打开 Postman。
- 创建一个
POST请求,URL 为http://localhost:8080/api/convert/docx-to-pdf(端口可能不同)。 - 在
Body选项卡中选择form-data。 - 添加一个 key 为
file(与@RequestParam("file")匹配) 的类型为File的参数。 - 选择一个本地的
.docx文件。 - 点击
Send。 - 期望:收到一个
200 OK响应,内容类型为application/pdf,浏览器或 PDF 阅读器会自动下载或打开转换后的 PDF 文件。
使用 curl:
bash
curl -X POST -F "file=@/path/to/your/document.docx" http://localhost:8080/api/convert/docx-to-pdf --output converted.pdf
6. 测试与验证
6.1 单元测试 (JUnit)
为 DocxToPdfService 编写单元测试,验证其核心转换功能。需要使用测试用的 DOCX 文件。
java
package com.example.docx2pdf.service;
import org.docx4j.openpackaging.exceptions.Docx4JException;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.core.io.ClassPathResource;
import org.springframework.util.StreamUtils;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest
public class DocxToPdfServiceTest {
@Autowired
private DocxToPdfService docxToPdfService;
@Test
public void testConvertSampleDocxToPdf() throws IOException, Docx4JException {
// 1. 从测试资源目录加载一个小的 DOCX 样本文件
ClassPathResource sampleDocxResource = new ClassPathResource("testfiles/sample.docx");
byte[] docxBytes = StreamUtils.copyToByteArray(sampleDocxResource.getInputStream());
// 2. 准备输入流和输出流
ByteArrayInputStream docxInputStream = new ByteArrayInputStream(docxBytes);
ByteArrayOutputStream pdfOutputStream = new ByteArrayOutputStream();
// 3. 执行转换
docxToPdfService.convertDocxToPdf(docxInputStream, pdfOutputStream);
// 4. 验证输出
byte[] pdfBytes = pdfOutputStream.toByteArray();
assertNotNull(pdfBytes);
assertTrue(pdfBytes.length > 0);
// 5. (可选) 简单验证 PDF 头
// PDF 文件通常以 "%PDF-" 开头
String pdfHeader = new String(pdfBytes, 0, 5);
assertEquals("%PDF-", pdfHeader);
// 6. (可选) 将 PDF 写入临时文件检查
File tempPdfFile = File.createTempFile("test-output", ".pdf");
Files.write(tempPdfFile.toPath(), pdfBytes);
System.out.println("Test PDF output: " + tempPdfFile.getAbsolutePath());
// 手动检查 tempPdfFile 是否正确
// tempPdfFile.deleteOnExit(); // 让 JVM 退出时删除
}
}
说明:
- 使用
@SpringBootTest加载 Spring 上下文并注入DocxToPdfService。 - 使用
ClassPathResource加载位于src/test/resources/testfiles/sample.docx的测试 DOCX 文件。 - 验证转换后:
- 输出流非空。
- 输出字节长度大于 0。
- (基本验证) 检查字节流是否以 PDF 文件头
%PDF-开头。
- 可以将生成的 PDF 写入临时文件,方便手动打开验证格式是否正确。
6.2 转换结果验证
- 内容完整性: 打开生成的 PDF,逐页核对文本、图片、表格、页眉页脚、页码等内容是否与原始 DOCX 一致。
- 格式保真度 :
- 检查字体是否正确(特别是中文字体)。
- 检查段落缩进、间距、对齐。
- 检查表格边框、单元格对齐。
- 检查图片位置、大小、清晰度。
- 检查超链接是否有效。
- 特殊元素 : 测试文档应包含 DOCX 的各种常见元素(文本框、形状、SmartArt、图表、公式等),验证它们在 PDF 中的呈现效果。注意: docx4j 对某些复杂元素(如 VBA 宏、ActiveX 控件)的支持可能有限。
- 边缘情况: 测试空文件、超大文件、损坏文件、包含特殊字符文件等。
7. 常见问题与解决方案 (FAQ)
- Q: 转换后中文显示为方块或乱码?
- A: 这是最常见的问题。请参考 4.1 处理中文字体与乱码问题 。确保正确使用
FontMapper映射中文字体到服务器上已安装或注册的物理字体文件。
- A: 这是最常见的问题。请参考 4.1 处理中文字体与乱码问题 。确保正确使用
- Q: 转换过程抛出
NoClassDefFoundError或ClassNotFoundException?- A: 通常是缺少依赖。请仔细检查
pom.xml中的依赖是否完整,特别是docx4j,docx4j-export-PDF,fop,xmlgraphics-commons的版本是否兼容且已下载。运行mvn dependency:tree检查依赖树。
- A: 通常是缺少依赖。请仔细检查
- Q: 转换大型文件时内存溢出 (
OutOfMemoryError)?- A: 参考 4.4 性能考量与内存管理 。增加 JVM 堆内存 (
-Xmx),考虑异步处理,优化文档图片。
- A: 参考 4.4 性能考量与内存管理 。增加 JVM 堆内存 (
- Q: 转换后的 PDF 格式错乱(文字重叠、布局混乱)?
- A: 这通常是因为 DOCX 文档使用了非常复杂的布局、样式或 docx4j 不完全支持的元素。尝试简化 DOCX 文档的样式。检查 docx4j 的 issue 列表或社区论坛看是否有类似问题报告。确保使用最新版本的 docx4j。
- Q: 如何设置 PDF 的密码保护?
- A: docx4j 本身不直接提供简单的 PDF 加密接口。转换完成后,你需要使用专门的 PDF 库(如 Apache PDFBox, iText)对生成的 PDF 文件进行二次加密操作。
- Q:
Docx4J.toPDF方法内部使用的是哪个 PDF 库?- A: 当
PdfSettings配置为使用"Plutext"时,docx4j-export-PDF模块使用 Plutext 的 PDF 转换器。其底层在旧版本可能基于 iText 5.x (AGPL),新版本可能使用其他渲染器(如 Flying Saucer + iText 或 PDFBox)。务必注意其依赖库的许可证要求(特别是 iText AGPL 对分发的影响)。
- A: 当
- Q: 是否支持 DOC (旧版 Word 格式) 转 PDF?
- A: docx4j 主要处理 Open XML 格式 (DOCX)。对于旧版
.doc文件,docx4j 本身不直接支持加载。你需要先将 DOC 转换为 DOCX(例如使用 Apache POI 的HWPF组件读取 DOC 并写入 DOCX),或者使用其他专门处理 DOC 的库。
- A: docx4j 主要处理 Open XML 格式 (DOCX)。对于旧版
8. 总结
本指南详细介绍了如何在 Spring Boot 应用中,使用开源的 docx4j 库实现 DOCX 文档到 PDF 的转换。内容包括:
- 方案选型: 解释了选择 docx4j 的原因。
- 环境搭建: 创建项目、添加依赖。
- 核心实现: 加载 DOCX、配置转换选项、执行转换的代码示例。
- 高级配置: 重点解决了中文字体问题,介绍了 PDF 属性设置、异常处理和性能优化。
- Web 集成: 创建 REST API 处理文件上传和 PDF 下载。
- 测试验证: 单元测试和结果检查方法。
- 常见问题: 提供了典型问题的解决方案。
docx4j 提供了一个相对轻量级、开源且不依赖外部 Office 软件的解决方案,非常适合集成到 Java 后端服务中。虽然处理极端复杂的文档或某些特殊元素时可能存在挑战,但对于大多数常见的业务文档转换需求,它是一个强大而实用的工具。通过本指南的步骤和注意事项,你应该能够成功地在 Spring Boot 应用中实现 DOCX 转 PDF 功能。