Spring Boot 实现DOCX转PDF(基于docx4j的轻量级开源方案)

好的,这是一份详细、实用且权威的 Java Spring Boot 实现 DOCX 转 PDF(基于 docx4j)的(基于docx4j的轻量级开源方案)全面指南。


目录

  1. 引言
    • 1.1 DOCX 与 PDF 格式简介
    • 1.2 转换需求与应用场景
    • 1.3 方案选型:为什么选择 docx4j?
  2. 环境准备
    • 2.1 基础环境要求
    • 2.2 创建 Spring Boot 项目
    • 2.3 添加 docx4j 及相关依赖
  3. 核心转换实现
    • 3.1 基础转换流程
    • 3.2 加载 DOCX 文档 (WordprocessingMLPackage)
    • 3.3 配置 PDF 转换选项 (PDFSettings)
    • 3.4 执行转换 (Docx4J.toPDF)
    • 3.5 完整代码示例 (Service 层)
  4. 高级配置与优化
    • 4.1 处理中文字体与乱码问题
    • 4.2 设置 PDF 输出属性 (权限、元数据)
    • 4.3 处理转换异常与日志记录
    • 4.4 性能考量与内存管理
  5. 集成到 Spring Boot 应用
    • 5.1 创建 RESTful API 接口 (Controller)
    • 5.2 文件上传与下载处理
    • 5.3 接口测试 (使用 Postman 或 curl)
  6. 测试与验证
    • 6.1 单元测试 (JUnit)
    • 6.2 转换结果验证
  7. 常见问题与解决方案 (FAQ)
  8. 总结

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-FOdocx4j-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 项目

可以使用以下方式之一创建项目:

  1. Spring Initializr (https://start.spring.io/) : 在网页上选择 Maven Project、Java、Spring Boot 版本,添加 Spring Web 依赖,生成项目并下载。
  2. IDE 创建向导: IntelliJ IDEA 或 Eclipse 通常内置了 Spring Initializr 支持,可以通过向导创建。
  3. 命令行 : (可选) 使用 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 RepositoryFOP Maven Repository 查看并替换为最新的稳定版本号。
  • 添加 fopxmlgraphics-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 基础转换流程

  1. 加载 DOCX : 将输入的 DOCX 文件加载到 docx4j 的内存表示 (WordprocessingMLPackage) 中。
  2. 配置 PDF 选项: (可选) 设置 PDF 输出的各种属性。
  3. 执行转换 : 调用 docx4j 的方法将 WordprocessingMLPackage 转换为 PDF 字节流或文件。
  4. 输出 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 来执行转换。它需要 WordprocessingMLPackagePdfSettings,并输出到指定的 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 方法接受 InputStreamOutputStream,使其非常灵活,可以处理来自网络上传、文件系统或内存的数据流,并输出到文件、HTTP 响应或内存。
  • 使用了 try-with-resources 语句 (在 convertDocxFileToPdfFile 中) 确保 FileOutputStream 被正确关闭。在 convertDocxToPdf 方法中,流的关闭责任交给了调用者(例如,Controller 处理 HTTP 响应流)。
  • flush() 确保所有缓冲的数据都写入输出流。
  • createPdfSettings() 方法提供了配置扩展点。目前使用默认 Plutext 设置。

4. 高级配置与优化

4.1 处理中文字体与乱码问题

问题描述: 如果 DOCX 文档中使用了服务器上未安装的字体(尤其是中文字体如宋体、黑体),转换后的 PDF 可能会出现字体替换(如显示为 Times New Roman)或方块乱码。

解决方案:

  1. 物理安装字体: 将需要的字体文件 (TTF 或 OTF) 安装到运行 Spring Boot 应用的服务器操作系统上。docx4j 的 Plutext 转换器会尝试使用系统已安装的字体进行渲染。这是最直接有效的方法,但可能受限于服务器环境和字体许可。
  2. 使用 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) 尝试查找系统中安装的、与给定名称匹配的物理字体。
  • BestMatchingMapperPhysicalFontMapperFontMapper 的实现类,用于处理映射逻辑。
  • 你需要准确知道 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 权限 (PdfWriterALLOW_XXX 常量) 通常需要直接访问底层的 PDF 生成库 (如 iText)。docx4j 的抽象层可能无法方便地设置所有权限。如果对权限有严格要求,可能需要考虑其他更底层的 PDF 生成库,或者生成 PDF 后再使用其他库 (如 Apache PDFBox) 进行权限修改。使用 iText 5.x 需要注意其 AGPL 许可证对分发的要求。

4.3 处理转换异常与日志记录

转换过程可能遇到多种错误:

  • Docx4JException: DOCX 加载、解析或转换过程中的错误(例如文件损坏、格式不支持)。
  • IOException: 流读写错误。
  • 字体缺失导致的渲染问题(可能表现为乱码,不一定抛异常)。
  • 内存不足 (OutOfMemoryError): 处理大型复杂文档时。

异常处理策略:

  1. 在 Service 方法中声明抛出 : 如示例所示,将 Docx4JExceptionIOException 抛给调用者 (Controller)。
  2. Controller 层捕获并处理: 在 Controller 中捕获异常,转换为友好的 HTTP 错误响应 (如 500 错误,附带错误信息)。
  3. 记录日志 : 在 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 密集型的。

优化建议:

  1. 增加 JVM 堆内存 : 在启动 Spring Boot 应用时,通过 -Xmx 参数设置更大的最大堆空间 (如 -Xmx1024m-Xmx2048m)。
  2. 流式处理?: docx4j 主要基于内存模型,没有直接的流式处理接口来处理超大文档。对于超大文件,可能需要考虑分拆文档或使用其他方案。
  3. 图片处理 : 如果 DOCX 包含大量图片,考虑在转换前或转换过程中 (如果 docx4j 支持) 对图片进行压缩或缩放。这可能需要深入操作 WordprocessingMLPackage 中的 BinaryPart
  4. 异步处理 : 对于耗时较长的转换任务,使用 Spring 的 @Async 或消息队列 (如 RabbitMQ, Kafka) 进行异步处理,避免阻塞 HTTP 请求线程。将转换任务提交到线程池,完成后通过通知 (如 WebSocket, 邮件, 回调 URL) 或提供下载链接。
  5. 资源清理 : 确保及时关闭不再使用的 InputStream, OutputStreamWordprocessingMLPackage 对象在转换完成后应解除引用,以便 GC 回收。
  6. 监控: 使用监控工具 (如 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 请求。
  • 下载
    1. 将转换后的 PDF 数据写入一个 ByteArrayOutputStream
    2. ByteArrayOutputStream 的内容转换为 ByteArrayResource
    3. 设置 HTTP 响应头:
      • Content-Disposition: attachment; filename=...: 提示浏览器下载文件,并指定文件名。
      • Content-Type: application/pdf: 声明响应体是 PDF 格式。
      • Content-Length: 设置文件大小。
    4. ByteArrayResource 作为响应体返回。

使用临时文件 (替代方案):

对于非常大的文件,将整个 PDF 先写入内存 (ByteArrayOutputStream) 可能不高效或导致 OOM。可以考虑:

  1. 将上传的 DOCX 文件先保存到服务器临时目录 (File.createTempFile())。
  2. 使用 docxToPdfService.convertDocxFileToPdfFile(inputTempFile, outputTempFile) 进行转换。
  3. 使用 FileSystemResourceInputStreamResource 包装输出 PDF 临时文件。
  4. 在响应发送完成后或在 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:

  1. 启动 Spring Boot 应用。
  2. 打开 Postman。
  3. 创建一个 POST 请求,URL 为 http://localhost:8080/api/convert/docx-to-pdf (端口可能不同)。
  4. Body 选项卡中选择 form-data
  5. 添加一个 key 为 file (与 @RequestParam("file") 匹配) 的类型为 File 的参数。
  6. 选择一个本地的 .docx 文件。
  7. 点击 Send
  8. 期望:收到一个 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 映射中文字体到服务器上已安装或注册的物理字体文件。
  • Q: 转换过程抛出 NoClassDefFoundErrorClassNotFoundException
    • A: 通常是缺少依赖。请仔细检查 pom.xml 中的依赖是否完整,特别是 docx4j, docx4j-export-PDF, fop, xmlgraphics-commons 的版本是否兼容且已下载。运行 mvn dependency:tree 检查依赖树。
  • Q: 转换大型文件时内存溢出 (OutOfMemoryError)?
    • A: 参考 4.4 性能考量与内存管理 。增加 JVM 堆内存 (-Xmx),考虑异步处理,优化文档图片。
  • 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 对分发的影响)。
  • Q: 是否支持 DOC (旧版 Word 格式) 转 PDF?
    • A: docx4j 主要处理 Open XML 格式 (DOCX)。对于旧版 .doc 文件,docx4j 本身不直接支持加载。你需要先将 DOC 转换为 DOCX(例如使用 Apache POI 的 HWPF 组件读取 DOC 并写入 DOCX),或者使用其他专门处理 DOC 的库。

8. 总结

本指南详细介绍了如何在 Spring Boot 应用中,使用开源的 docx4j 库实现 DOCX 文档到 PDF 的转换。内容包括:

  1. 方案选型: 解释了选择 docx4j 的原因。
  2. 环境搭建: 创建项目、添加依赖。
  3. 核心实现: 加载 DOCX、配置转换选项、执行转换的代码示例。
  4. 高级配置: 重点解决了中文字体问题,介绍了 PDF 属性设置、异常处理和性能优化。
  5. Web 集成: 创建 REST API 处理文件上传和 PDF 下载。
  6. 测试验证: 单元测试和结果检查方法。
  7. 常见问题: 提供了典型问题的解决方案。

docx4j 提供了一个相对轻量级、开源且不依赖外部 Office 软件的解决方案,非常适合集成到 Java 后端服务中。虽然处理极端复杂的文档或某些特殊元素时可能存在挑战,但对于大多数常见的业务文档转换需求,它是一个强大而实用的工具。通过本指南的步骤和注意事项,你应该能够成功地在 Spring Boot 应用中实现 DOCX 转 PDF 功能。

相关推荐
mit6.8242 小时前
[todo]10个常见的后端框架
后端
没有bug.的程序员2 小时前
Spring Boot 与 Sleuth:分布式链路追踪的集成、原理与线上故障排查实战
java·spring boot·分布式·后端·分布式链路追踪·sleuth·线上故障排查
裴嘉靖2 小时前
uni-app 打包后 PDF 无法生成问题完整解决方案
pdf·uni-app
wujian83112 小时前
AI导出pdf方法
人工智能·pdf
爱敲代码的憨仔2 小时前
Spring-AOP
java·后端·spring
短剑重铸之日2 小时前
《设计模式》第四篇:观察者模式
java·后端·观察者模式·设计模式
Hx_Ma162 小时前
SpringBoot注册格式化器
java·spring boot·后端
乔江seven2 小时前
【python轻量级Web框架 Flask 】1 Flask 初识
开发语言·后端·python·flask
知识即是力量ol3 小时前
一次完整的 Spring Security JWT 鉴权链路解析
java·后端·spring·鉴权·springsecurity