SpringBootg集成HTML转PDF

PDF文件生成是常见需求——从报表导出、合同签署到票据打印,都需要服务端高效生成标准格式的PDF文档。但传统实现往往面临中文乱码、样式错乱、分页异常等问题,调试过程耗时费力。那么,基于这种情况,htmltopdf来了……

技术选型:

目前主流的HTML转PDF框架对比:

框架/方案核心原理/特点适用场景关键优势潜在考量缺点
​wkhtmltopdf​基于 ​​Qt WebKit 引擎​​的命令行工具,通过进程调用。本质是​​无头浏览器​​。对现代 CSS 和复杂页面布局保真度要求高的场景。​渲染质量高​​,对现代 Web 标准(HTML5, CSS3)支持良好;原生支持中文,配置极简,API直观。需在服务器上​​单独安装​​该工具,部署稍复杂。高级特性较少
​Flying Saucer + OpenPDF​Flying Saucer负责将 HTML+CSS 转换为 PDF 文档模型,OpenPDF(基于 iText 5)负责 PDF 生成。需要开源免费、且对 CSS 样式支持较好的 Java 纯方案。纯 Java 实现,集成方便;对 CSS 支持较好;成熟稳定,社区活跃。对非常复杂或前沿的 CSS 特性支持可能不如无头浏览器方案。依赖较多,转换速度慢
​iText​功能强大的原生 PDF 操作库,可编程创建 PDF,也提供 HTML 转换模块。需要​​极致性能控制​​、处理复杂 PDF 逻辑(如加密、签名、动态表格)的场景。​功能全面​​,API 强大,性能优秀。学习曲线较陡峭;​​AGPL 开源协议​​,商业用途需购买许可证或开源代码。学习成本高,需要购买
​OpenHTMLtoPDF​另一个纯 Java 的 HTML 转 PDF 库,基于 Apache PDFBox。需要开源协议友好(Apache License)、且支持高质量排版(如字体、分页)的项目。开源免费,采用 Apache 许可证,对商业应用友好;支持复杂CSS,扩展性强。社区生态和资料相对 iText 或 wkhtmltopdf 可能少一些。需手动配置中文字体

选型结论:

htmltopdf凭借“零配置支持中文”和“5分钟集成”的优势,成为快速开发的首选。

技术实现:从依赖配置到完整接口

环境准备与依赖配置

1. Maven依赖(pom.xml)

在pom.xml中添加以下依赖,包含SpringBoot Web、Thymeleaf模板引擎和HTML转PDF核心框架:

xml

<!-- SpringBoot Web -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!-- Thymeleaf模板引擎(用于HTML模板渲染) -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

<!-- HTML转PDF核心框架 -->
<dependency>
    <groupId>io.woo</groupId>
    <artifactId>htmltopdf</artifactId>
    <version>1.0.4</version>
</dependency>

<!-- 工具类依赖(用于IO操作) -->
<dependency>
    <groupId>commons-io</groupId>
    <artifactId>commons-io</artifactId>
    <version>2.15.1</version>
</dependency>

2. 配置文件(application.yml)

无需额外配置,框架默认支持UTF-8编码和A4纸尺寸,如需自定义可添加:

yaml

# PDF转换配置(可选)
html-to-pdf:
  page-size: A4  # 页面尺寸,可选A3、Letter等
  margin: 10mm   # 页边距,默认10mm

核心工具类封装:PDF生成工具

将PDF转换逻辑封装为工具类,便于复用和维护。工具类需实现“HTML内容转PDF流”的核心功能,并处理异常:

java

import io.woo.htmltopdf.HtmlToPdf;
import io.woo.htmltopdf.HtmlToPdfObject;
import org.apache.commons.io.IOUtils;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

/**
 * PDF生成工具类
 * 封装HTML转PDF的核心逻辑,提供简洁API
 */
@Component
public class PdfGeneratorUtil {

    /**
     * 将HTML内容转换为PDF并写入响应流
     * @param htmlContent HTML字符串内容
     * @param response 响应对象,用于输出PDF文件
     * @param fileName 下载文件名(不含.pdf后缀)
     * @throws IOException 转换或IO异常
     */
    public void generatePdf(String htmlContent, HttpServletResponse response, String fileName) throws IOException {
        // 设置响应头,指定为PDF文件
        response.setContentType("application/pdf");
        response.setHeader("Content-Disposition", "attachment; filename=" + 
                          new String((fileName + ".pdf").getBytes("UTF-8"), "ISO-8859-1"));

        // 核心转换逻辑:使用io.woo.htmltopdf生成PDF
        try (InputStream pdfStream = HtmlToPdf.create()
                .object(HtmlToPdfObject.forHtml(htmlContent)  // 传入HTML内容
                        .defaultEncoding("UTF-8"))  // 指定编码,避免中文乱码
                .convert();  // 执行转换,返回PDF输入流
             OutputStream out = response.getOutputStream()) {

            // 将PDF流写入响应输出流
            IOUtils.copyLarge(pdfStream, out);
            out.flush();
        } catch (Exception e) {
            // 转换失败时抛出异常,由全局异常处理器捕获
            throw new IOException("PDF生成失败:" + e.getMessage(), e);
        }
    }
}

HTML模板设计:最佳实践与避坑指南

使用Thymeleaf模板引擎渲染动态HTML,设计时需注意PDF转换的特殊性:

1. 模板文件位置

在src/main/resources/templates目录下创建模板文件(如report.html)。

2. 设计要点与示例

html

<!DOCTYPE html>
<html lang="zh-CN" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>车辆评估报告</title>
    <style>
        /* 关键:设置PDF页面宽度适配A4纸(21cm-左右边距) */
        .container { width: 19cm; margin: 0 auto; padding: 1cm; }
        /* 标题样式 */
        .title { text-align: center; font-size: 20px; margin-bottom: 20px; }
        /* 表格样式 */
        .info-table { width: 100%; border-collapse: collapse; margin: 15px 0; }
        .info-table th, .info-table td { 
            border: 1px solid #333; 
            padding: 8px; 
            text-align: left; 
        }
        /* 分页控制:强制在指定元素后分页 */
        .page-break { page-break-after: always; }
    </style>
</head>
<body>
    <div class="container">
        <h1 class="title" th:text="${title}">车辆评估报告</h1>
        
        <table class="info-table">
            <tr><th>品牌</th><td th:text="${car.brand}">路虎</td></tr>
            <tr><th>车系</th><td th:text="${car.series}">Defender</td></tr>
            <tr><th>年款</th><td th:text="${car.year}">2023款</td></tr>
            <tr><th>成交价</th><td th:text="${car.price}">1000000元</td></tr>
        </table>

        <!-- 分页示例:下一页从这里开始 -->
        <div class="page-break"></div>
        <h2>第二页内容</h2>
    </div>
</body>
</html>

3. 设计最佳实践

  • 尺寸适配:PDF默认A4纸宽度21cm,建议内容容器宽度设为19cm(预留左右边距各1cm);
  • 样式简化:避免使用复杂CSS(如flex/grid布局),优先用table和inline样式;
  • 分页控制:通过page-break-after: always强制分页,避免内容跨页断裂;
  • 图片处理:使用绝对路径(如https://xxx.com/img.jpg)或Base64编码,避免相对路径导致图片丢失。

Controller层实现:完整接口示例

创建Controller接收请求,渲染模板为HTML,调用工具类生成PDF:

java

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.context.Context;

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

/**
 * PDF生成控制器
 * 提供HTTP接口,接收参数并生成PDF文件
 */
@Controller
public class PdfController {

    @Autowired
    private PdfGeneratorUtil pdfGeneratorUtil;  // 注入PDF工具类

    @Autowired
    private TemplateEngine templateEngine;  // Thymeleaf模板引擎

    /**
     * 生成车辆评估报告PDF
     * @param id 车辆ID,用于查询评估数据
     * @param response 响应对象,输出PDF
     * @throws IOException 异常
     */
    @GetMapping("/pdf/car-report")
    public void generateCarReport(Long id, HttpServletResponse response) throws IOException {
        // 1. 模拟查询车辆数据(实际项目中从数据库获取)
        Map<String, Object> carData = new HashMap<>();
        carData.put("title", "车辆评估报告");
        Map<String, String> car = new HashMap<>();
        car.put("brand", "路虎");
        car.put("series", "Defender 130");
        car.put("year", "2023款");
        car.put("price", "1000000元");
        carData.put("car", car);

        // 2. 渲染Thymeleaf模板为HTML字符串
        Context context = new Context();
        context.setVariables(carData);
        String htmlContent = templateEngine.process("report", context);  // "report"对应templates/report.html

        // 3. 调用工具类生成PDF并输出
        pdfGeneratorUtil.generatePdf(htmlContent, response, "车辆评估报告_" + id);
    }
}

异常处理与性能优化

1. 全局异常处理

创建全局异常处理器,捕获转换过程中的异常并返回友好提示:

java

import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(IOException.class)
    public void handlePdfException(IOException e, HttpServletResponse response) throws IOException {
        response.setContentType("text/plain;charset=UTF-8");
        response.getWriter().write("PDF生成失败:" + e.getMessage());
        response.setStatus(500);
    }
}

2. 性能优化建议

  • 模板缓存:Thymeleaf默认缓存模板,生产环境需开启(spring.thymeleaf.cache=true);
  • 异步处理:耗时的PDF生成(如多页报表)通过@Async异步执行,避免阻塞请求;
  • 资源复用:复用HtmlToPdf实例,减少对象创建开销;
  • 并发控制:通过线程池限制PDF转换并发数,避免服务器资源耗尽。

场景应用:3个可直接复用的实战案例

案例1:车辆评估报告生成

业务场景:二手车交易平台,用户查看车辆评估后可下载PDF报告。关键代码:已在Controller层示例中实现,通过/pdf/car-report?id=1调用,返回车辆评估PDF。

案例2:电子合同导出

业务场景:在线签约系统,用户签署合同后导出PDF合同文件。实现步骤

  1. 创建合同模板contract.html,包含甲方、乙方、条款等动态字段;
  2. 编写Controller接口,接收合同ID,查询签约信息;
  3. 渲染模板并生成PDF,关键代码如下:

java

@GetMapping("/pdf/contract")
public void generateContract(Long contractId, HttpServletResponse response) throws IOException {
    // 模拟查询合同数据
    Map<String, Object> contractData = new HashMap<>();
    contractData.put("partyA", "张三");
    contractData.put("partyB", "李四");
    contractData.put("amount", "50000元");
    contractData.put("signDate", "2025-10-14");

    // 渲染模板并生成PDF
    Context context = new Context();
    context.setVariables(contractData);
    String html = templateEngine.process("contract", context);
    pdfGeneratorUtil.generatePdf(html, response, "电子合同_" + contractId);
}

案例3:票据打印(如发票、收据)

业务场景:电商平台订单支付后,用户下载PDF发票用于报销。实现要点

  • 票据尺寸通常为“票据专用纸”(如21cm×14cm),需自定义PDF页面大小;
  • 包含二维码/条形码,便于扫码验证;
  • 关键代码(自定义页面大小):

java

// 在工具类中扩展自定义页面大小的方法
public void generatePdfWithCustomSize(String htmlContent, HttpServletResponse response, 
                                     String fileName, float width, float height) throws IOException {
    response.setContentType("application/pdf");
    response.setHeader("Content-Disposition", "attachment; filename=" + 
                      new String((fileName + ".pdf").getBytes("UTF-8"), "ISO-8859-1"));

    try (InputStream pdfStream = HtmlToPdf.create()
            .object(HtmlToPdfObject.forHtml(htmlContent).defaultEncoding("UTF-8"))
            .size(width, height)  // 自定义页面宽高(单位:毫米)
            .convert();
         OutputStream out = response.getOutputStream()) {
        IOUtils.copyLarge(pdfStream, out);
        out.flush();
    }
}

// 调用时指定票据尺寸(210mm×140mm)
pdfGeneratorUtil.generatePdfWithCustomSize(html, response, "发票_12345", 210, 140);

问题解决方案:5个典型问题与避坑指南

问题1:中文乱码或不显示

现象:PDF中中文显示为方框或空白。原因:io.woo.htmltopdf默认支持中文,但部分环境可能缺少字体。解决方案:在HTML模板中显式指定中文字体:

css

body { font-family: SimHei, "Microsoft YaHei", sans-serif; }

问题2:样式丢失或错乱

现象:HTML页面样式正常,转PDF后样式丢失(如颜色、边框)。原因:PDF转换器对CSS支持有限,复杂样式无法解析。解决方案

  • 使用内联样式(style=”color: red;”)替代外部CSS;
  • 避免CSS3特性,用基础样式(如background-color而非linear-gradient)。

问题3:分页异常(内容跨页断裂)

现象:表格或段落被分页符分割,显示不完整。解决方案:使用CSS控制分页:

css

/* 避免元素跨页 */
.no-break { page-break-inside: avoid; }
/* 强制分页 */
.break-after { page-break-after: always; }

问题4:图片不显示

现象:HTML中图片正常,PDF中图片空白或显示“broken image”。原因:图片路径为相对路径,转换器无法访问本地资源。解决方案

  • 使用绝对URL(如https://xxx.com/logo.png);
  • 将图片转为Base64编码嵌入HTML:
  • html
  • <img src=”data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA…” />

问题5:PDF文件过大

现象:生成的PDF超过10MB,下载缓慢。原因:图片未压缩,或内容包含大量高分辨率图片。解决方案

  • 压缩图片(如将PNG转为JPG,分辨率控制在72dpi);
  • 移除PDF中不必要的图片或降低质量。

扩展进阶:2个高级特性实现

特性1:动态数据填充与条件渲染

结合Thymeleaf的表达式,实现复杂动态内容(如列表循环、条件显示):

html

<!-- 循环展示多辆车信息 -->
<div th:each="car : ${cars}">
    <h3 th:text="${car.brand + ' ' + car.series}">车辆名称</h3>
    <p th:if="${car.price > 500000}">
        <span style="color: red;">高价车</span>
    </p>
    <p th:unless="${car.price > 500000}">普通车</p>
</div>

Controller传参

java

Map<String, Object> data = new HashMap<>();
data.put("cars", Arrays.asList(
    new Car("路虎", "Defender", 1000000),
    new Car("丰田", "卡罗拉", 150000)
));

特性2:自定义水印与页眉页脚

通过CSS的@page规则添加水印和页眉页脚:

css

@page {
    /* 页眉 */
    @top-center {
        content: "车辆评估报告 - 第" counter(page) "页";
        font-size: 10px;
        color: #666;
    }
    /* 水印 */
    background-image: url("data:image/png;base64,...");  /* Base64水印图片 */
    background-position: center;
    background-repeat: no-repeat;
    background-size: 50%;
}

Leave a Comment

Comments

No comments yet. Why don’t you start the discussion?

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注