jquick-pdf 表格实战:动态数据 PDF 报表生成

jquick-pdf 表格实战:动态数据 PDF 报表生成

引入

销售日报、库存清单、对账单这类报表结构高度规则,真正的工作量在于行数不固定:有多少条记录,就要输出多少行。用 iText 或 PDFBox 逐行创建 Cell、单独设置字体并处理分页,代码量大,且每次增删字段都要回归。jquick-pdf 使用类 HTML 模板,后端只需要把查询结果变成表格行,布局与分页交给引擎。本文说明"固定骨架 + 后端生成行"这种模板组织方式,以及它带来的边界与注意事项。

核心讲解

表格元素结构

<table><tr><th><td> 构成报表骨架:table 承载整体宽度与样式,tr 表示一行,th 用于表头,td 用于数据单元格。最常见的形式是首行为表头,之后每行对应一条业务记录。单元格样式可以直接写在元素上:为 <th> 设置 fontColorbackgroundColortextAlignment,为 <td> 设置 paddingfontSize

模板的组织方式

报表模板可以来自字符串、classpath 资源或外部文件,分别对应 executeContentexecuteResourceexecuteFile 三个入口。实践中推荐把固定骨架(标题、表头、外层 table)放进资源文件,行片段由代码生成后拼入:骨架可以由业务与设计共同评审,代码只维护动态部分。变量绑定、DTO 转换与响应输出的完整做法见第 14 篇,本篇只讨论行的生成机制。

为什么由后端生成

后端生成行片段有三个好处:一是绕开未证实的循环语法,模板始终是静态可读的;二是数据格式化、转义与空值处理都留在 Java 侧,便于测试和审计;三是行片段与模板解耦,同一份表头模板可以复用于不同数据源。代价是模板不再完全自描述,行结构由代码维护,因此需要为行片段补充单元测试。

渲染与分页

流程仍然是"模板 → 绑定 → 渲染":把最终模板交给 executeContent,接口返回字节数组,再落盘或写入响应流。行数增加时由布局引擎自然分页,业务代码不需要计算行高。

依赖

核心只需文档层 jquick-pdfxjquick-pdf-svgjquick-pdf-datajquick-pdf-fontjquick-pdf-css 为可选模块,按需追加。

关键细节

字面量与变量

单元格中的固定文本必须使用单引号,例如 '编号';动态值使用 ${name}。这里有一个容易忽略的细节:行片段是拼接出来的,若每行都想用 ${...} 绑定,就要为每一行注册不同的键。更简单的做法是把已格式化的值作为单引号字面量写进行片段,由后端负责格式化。

行内容安全转义

不要把未过滤的用户输入直接拼进模板:单引号、<>& 等字符可能破坏模板结构或改变语义。应先做白名单过滤与转义,或只绑定可信字段;同时限制字段长度,避免超长文本撑破列宽。这一点在商品名、客户名这类自由文本字段上尤其重要。

空集合与列数稳定

  • 查询结果为空时仍应输出表头,并给出一行"暂无数据"提示,而不是生成一张没有表头的空表。
  • 单行与多行要分别验证:单行最容易暴露列错位与表头样式问题,多行超过一页时要观察分页是否落在行中间。
  • 每行的单元格数量必须与表头严格一致,缺列会让整张表的列宽错位。
  • 每增加一个字段,都要同步补充空值、null 与超长文本测试数据。

数据规模与分页

数据量大时不要把所有行一次性拼进内存字符串,应分批构造模板并限制单次导出条数;字段列数必须稳定,否则分页位置和列宽都会漂移。金额等数值建议在查询结果转成字符串后再绑定,保证小数位与千分位格式统一。

数值与日期格式

金额、数量和日期不要在模板里格式化:查询结果应先在 Java 侧转成字符串,统一小数位、千分位与日期格式,再拼进行片段。同一列的数据形式不一致时,表格的列宽和视觉对齐会明显漂移;若某列既可能为空又可能出现超长文本,应为它设置 minWidthmaxWidthtextAlignment,让超长内容按预期折行。

与固定表头模板的配合

把表头写在模板骨架里、行片段只包含数据行,可以让表头样式集中维护,也便于统一调整列宽。若报表很长、需要在每页重复表头,应先在目标版本上确认表头重复能力,不要按未经验证的行为设计版式。

实战说明

依赖

xml 复制代码
<dependency>
    <!-- 文档层核心:模板解析与 PDF 渲染 -->
    <groupId>io.github.paohaijiao</groupId>
    <artifactId>jquick-pdfx</artifactId>
    <version>4.0.0</version>
</dependency>

环境为 JDK 8+,入口类是 com.github.paohaijiao.executor.JQuickPdfFactory

DynamicTableDemo

java 复制代码
import com.github.paohaijiao.executor.JQuickPdfFactory;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.List;

public class DynamicTableDemo {
    public static void main(String[] args) throws Exception {
        // 模拟数据库查询结果:编号、商品、金额
        List<String[]> rows = Arrays.asList(
                new String[]{"1001", "键盘", "199"},
                new String[]{"1002", "显示器", "1299"});
        // 由后端拼接数据行,模板中不含循环指令
        StringBuilder body = new StringBuilder();
        for (String[] row : rows) {
            body.append("<tr><td>'").append(row[0]).append("'</td><td>'")
                    .append(row[1]).append("'</td><td>'")
                    .append(row[2]).append("'</td></tr>");
        }
        // 固定表头 + 动态行,拼成完整模板
        String template = "<pdf><body><h1>'销售报表'</h1><table>"
                + "<tr><th>'编号'</th><th>'商品'</th><th>'金额'</th></tr>"
                + body + "</table></body></pdf>";
        // 执行渲染并保存结果
        byte[] pdf = new JQuickPdfFactory().executeContent(template);
        Files.write(Paths.get("dynamic-report.pdf"), pdf);
    }
}

运行效果

输出与检查

每个数组生成一行,首行为表头,生成后可按需分页输出 PDF。检查重点有三处:列数与表头是否一致、金额等数值格式是否统一、空集合时是否仍保留表头。生成后用阅读器抽查第一页与最后一页,确认列宽、行高与分页位置一致。

常见问题排查

表格出现错位时依次检查三件事:行片段的单引号是否成对闭合,避免模板结构被破坏;某一行是否少写或多写了 <td>;业务文本中是否含有单引号或尖括号而未转义。分页异常时对比行数与列数是否稳定,并确认表头没有被误包进动态行片段。

场景与选型

这套结构适合销售日报、库存清单、对账单等规则报表。同一份表头骨架可以复用于不同数据源,但列数与列顺序必须保持一致,否则表头与数据会错位。如果需求包含跨行跨列、模板内循环或复杂分组汇总,应先确认目标版本的能力边界,必要时在 Java 侧预先组织好行结构,或改用其他方案。

总结

动态报表的核心结论是:模板提供固定骨架,后端提供已验证的 <tr> 行片段。没有循环语法并不影响动态报表的可行性,反而让模板保持静态可读,API 也保持真实可运行。

边界与误区有三点:模板循环、表达式与 rowSpan/colSpan 均未证实,不能写成伪 API;行内容必须转义或只绑定可信字段,否则可能破坏模板结构;数据量大时要分批构造并限制单次导出条数,字段列数必须稳定。样式方面可按需扩展:<th> 支持 fontColorbackgroundColortextAlignment<td> 支持 paddingfontSize

版本基线:jquick-pdfx 4.0.0、JDK 8+,升级前请核对 README_zh.md 的版本对照表(含许可证与依赖边界);更多示例见 GitHub 仓库

相关推荐
aramae3 小时前
模拟实现memset()(C语言)
c语言·开发语言·后端
OxYGC3 小时前
[AI工程] Spring AI 第十四篇:Agent 五种模式在 2.0 里怎么写
java·spring·ai·ai编程
code2cat3 小时前
【随笔】从聊天到调用工具:理解MCP在AI应用中的位置
java·人工智能
2501_933923253 小时前
Spring Bean作用域揭秘:单例、原型、请求与会话的区别
java·后端·spring·java-ee
wuyk5554 小时前
【Socket 进阶之路】第 7 章 IO 多路复用 select & poll 完整实战|多 fd 监听、超时等待、优缺点横向对比
服务器·开发语言·网络·数据库·物联网
学编程就要猛4 小时前
基于Spring AI 的智能聊天机器人
java·spring ai·chat robot
修炼室4 小时前
Java开发工程师笔试经验贴【高频知识】
java
船厂电气自动化ai大模型4 小时前
AI大模型与数学|第81天 课程:正交向量、正交基、格拉姆‑施密特(Gram‑Schmidt)正交化
开发语言·数据结构·人工智能·线性代数·机器学习
木井巳4 小时前
【记忆化搜索】最长递增子序列
java·算法·leetcode·深度优先·剪枝·推荐算法