Java实现pdf导出

一.简介

1.1、Java 实现动态 PDF 导出:FreeMarker 模板 + OpenHTMLtoPDF 完整指南

在日常企业级开发中,将业务数据导出为 PDF 是一个常见需求------无论是报告、合同还是质检单据。本文将结合一个医疗设备质控报告 的实际场景,分享如何使用 FreeMarker 模板引擎 + OpenHTMLtoPDF 工具,动态生成美观的 PDF,并支持多文件打包成 ZIP 下载。同时会分享一些坑点与优化方案。

1.2 核心组件

组件 作用
FreeMarker 模板引擎,将 Java 对象渲染为 HTML 字符串
OpenHTMLtoPDFcom.openhtmltopdf:pdfbox 将 HTML+CSS 转换为 PDF,支持中文字体
Servlet / Spring MVC 接收导出请求,输出 ZIP 流

二、整体流程

  1. 客户端请求导出(可能含筛选条件)。

  2. 后端查询数据,根据不同类型(日检、周检、季度检)准备对应的数据模型。

  3. 加载 FreeMarker 模板(.ftl),渲染出完整 HTML。

  4. 调用 OpenHTMLtoPDF 将 HTML 转为 PDF 字节数组。

  5. 如果有多条记录,将所有 PDF 打包成一个 ZIP 文件返回。

  6. 响应流设置 Content-Type: application/zip,浏览器自动下载。

三、代码实现(简化版)

3.1 代码

java 复制代码
public class PdfUtil {
    private static final Configuration cfg;

    static {
        cfg = new Configuration(Configuration.VERSION_2_3_31);
        // 模板放在 classpath:/templates/ 下
        cfg.setClassLoaderForTemplateLoading(PdfUtil.class.getClassLoader(), "templates");
        cfg.setDefaultEncoding("UTF-8");
    }

    // 渲染 HTML
    public static String render(String templateName, Map<String, Object> data) {
        try (StringWriter writer = new StringWriter()) {
            Template template = cfg.getTemplate(templateName);
            template.process(data, writer);
            return writer.toString();
        } catch (Exception e) {
            throw new RuntimeException("模板渲染失败", e);
        }
    }

    // HTML 转 PDF
    public static byte[] htmlToPdf(String html) {
        try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
            PdfRendererBuilder builder = new PdfRendererBuilder();
            builder.withHtmlContent(html, null);
            builder.toStream(out);
            // 关键:加载中文字体,避免中文乱码或方块
            builder.useFont(new File("C:/Windows/Fonts/simsun.ttc"), "SimSun");
            builder.run();
            return out.toByteArray();
        } catch (Exception e) {
            throw new RuntimeException("PDF生成失败", e);
        }
    }
}

注意 :生产环境建议将字体文件放在项目资源目录中,通过 getResourceAsStream 加载

3.2 导出服务核心逻辑(批量 ZIP)

java 复制代码
@Override
public void exportMessages(List<BaseMessage> messages, HttpServletResponse response) {
    ZipOutputStream zipOut = null;
    try {
        if (messages == null || messages.isEmpty()) {
            response.setContentType("text/plain;charset=UTF-8");
            response.getWriter().write("无数据可导出");
            return;
        }
        response.setContentType("application/zip");
        response.setHeader("Content-Disposition", 
            "attachment;filename=reports_" + System.currentTimeMillis() + ".zip");

        zipOut = new ZipOutputStream(response.getOutputStream());

        for (BaseMessage msg : messages) {
            try {
                // 1. 构建数据模型
                Map<String, Object> dataModel = buildDataModel(msg);
                // 2. 选择模板(日检/周检等)
                String template = getTemplateByType(msg.getCheckType());
                // 3. 渲染 HTML
                String html = PdfUtil.render(template, dataModel);
                // 4. 转 PDF
                byte[] pdfBytes = PdfUtil.htmlToPdf(html);
                // 5. 生成文件名
                String fileName = msg.getEquipment() + "_" + msg.getCheckTime() + ".pdf";
                ZipEntry entry = new ZipEntry(fileName);
                zipOut.putNextEntry(entry);
                zipOut.write(pdfBytes);
                zipOut.closeEntry();
            } catch (Exception e) {
                log.error("导出单条失败:{}", msg.getId(), e);
                // 继续处理下一条
            }
        }
    } catch (Exception e) {
        log.error("ZIP打包失败", e);
    } finally {
        if (zipOut != null) { try { zipOut.close(); } catch (IOException ignored) {} }
    }
}

3.3 模板示例(日检报告片段)

java 复制代码
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>质控报告</title>
    <style>
        @page { size: A4; margin: 20mm; }
        body { font-family: SimSun; font-size: 14px; }
        .title { color: #016fa0; text-align: center; font-size: 24px; }
        table { width: 100%; border-collapse: collapse; margin-top: 20px; }
        th, td { border: 1px solid #aaa; padding: 8px; text-align: center; }
        .table-header { background-color: #016fa0; color: white; }
    </style>
</head>
<body>
    <div class="title">医用电子直线加速器质量控制检查报告</div>
    <div class="info">
        <span>检查日期:${(checkDate?string("yyyy-MM-dd"))!}</span>
        <span>设备:${equipment!}</span>
        <span>检查人:${inspector!}</span>
    </div>
    <h3>等中心指示(激光灯)技术要求:≤2mm</h3>
    <table>
        <tr class="table-header">
            <th>检查位置</th><th>机架角度</th><th>偏差(mm)</th><th>判定</th>
        </tr>
        <#list dayWaitList as item>
        <tr>
            <td>${item.position!}</td>
            <td>${item.angle!}</td>
            <td>${item.deviation!}</td>
            <td>${item.result!}</td>
        </tr>
        </#list>
    </table>
    <div class="sign">检查人签字:________  复核人签字:________</div>
</body>
</html>

四、常见问题与解决方案

4.1 表格超出页面被截断(重点)

你很可能遇到过:生成的 PDF 中表格右侧被裁切,尤其列数较多时。

原因分析

  • 纸张 A4 可用宽度约为 170mm(210mm - 左右边距20mm×2)。

  • 模板中 body 或外层容器设置了固定宽度(如 180mm)又加上 padding,实际占用宽度超过 170mm。

  • 浏览器/PDF 引擎不会横向滚动,直接裁剪超出部分。

解决方案

  1. 使用相对宽度 :外层容器 width: 100%; box-sizing: border-box;

  2. 去掉固定宽度的表格table { width: 100%; table-layout: auto; }

  3. 控制列宽比例 :给 <th> 设置百分比宽度,如 width: 15%

  4. 强制文字换行td { word-break: break-word; }

  5. 减小页边距@page { margin: 15mm; } 增加可用宽度。

示例修正:

css

复制代码
* { box-sizing: border-box; }
@page { size: A4; margin: 15mm; }
body { width: 100%; margin: 0; }
table { width: 100%; table-layout: auto; }
td, th { word-break: break-word; }

4.2 中文乱码 / 方块

  • 必须在 PdfRendererBuilder 中指定中文字体,如 builder.useFont(simsunFont, "SimSun")

  • HTML 中的 font-family 需与该字体名称匹配。

4.3 分页时表格行被截断

使用 CSS 分页控制:

css

复制代码
tr { page-break-inside: avoid; }
thead { display: table-header-group; }  /* 每页重复表头 */

4.4 大并发下性能问题

  • FreeMarker 模板实例是线程安全的,可缓存。

  • 每次生成 PDF 比较耗 CPU,建议对简单报告增加缓存或异步生成。

  • 批量导出时控制单次打包数量(如 ≤50),避免内存溢出。

通过 FreeMarker + OpenHTMLtoPDF,我们可以像写普通网页一样设计 PDF 模板,极大降低了报表开发门槛。关键点在于:

  • 模板与数据分离,易于维护

  • 正确处理中文与页面截断问题

  • 批量导出时使用 ZIP 流,提升用户体验

本文提供的代码片段可直接应用于 Spring Boot 项目中。如果你也遇到了表格超出、中文乱码等困扰,希望这篇文章能帮你快速避坑。

最终结果

相关推荐
无巧不成书02183 小时前
Java变量初始化全攻略:2026最新规范+新手避坑实战
java·开发语言·java基础·java变量初始化·java语法规范·var关键字
kobe_OKOK_3 小时前
vue3+Ant-design-vue3+i18n多语种切换
前端·javascript·vue.js
爱分享的阿Q3 小时前
技术饱和度视角下的编程语言选择:一场关于供需博弈的深度思考
java·python·go
E_ICEBLUE3 小时前
Python 办公自动化:快速将 HTML 转换为 PDF 格式
python·pdf·html
IT大师兄吖4 小时前
paddleocr PP-StructureV3 pdf转md 懒人整合包 gpu可用
pdf
一口甜西瓜4 小时前
《Vue3 + TS 语言包:i18n Ally 不显示翻译?这份配置我踩完坑了》
vue.js·visual studio code
拆房老料4 小时前
开源预览引擎 BaseMetas Fileview v1.4.0 发布:PDF 渲染升级 + RAR5 修复 + 压缩包优化,企业级文档预览更强了
3d·pdf·开源·开源软件
有来技术4 小时前
Vite 8 全面 Rust 化!vue3-element-admin 升级实战,构建提速 65%
前端·vue.js·前端框架·vue
Zafkiel8624 小时前
求助:macOS 运行 JavaFX 工具报错
java