jquick-pdf 表格实战:动态数据 PDF 报表生成
引入
销售日报、库存清单、对账单这类报表结构高度规则,真正的工作量在于行数不固定:有多少条记录,就要输出多少行。用 iText 或 PDFBox 逐行创建 Cell、单独设置字体并处理分页,代码量大,且每次增删字段都要回归。jquick-pdf 使用类 HTML 模板,后端只需要把查询结果变成表格行,布局与分页交给引擎。本文说明"固定骨架 + 后端生成行"这种模板组织方式,以及它带来的边界与注意事项。
核心讲解
表格元素结构
<table>、<tr>、<th>、<td> 构成报表骨架:table 承载整体宽度与样式,tr 表示一行,th 用于表头,td 用于数据单元格。最常见的形式是首行为表头,之后每行对应一条业务记录。单元格样式可以直接写在元素上:为 <th> 设置 fontColor、backgroundColor、textAlignment,为 <td> 设置 padding 和 fontSize。
模板的组织方式
报表模板可以来自字符串、classpath 资源或外部文件,分别对应 executeContent、executeResource 与 executeFile 三个入口。实践中推荐把固定骨架(标题、表头、外层 table)放进资源文件,行片段由代码生成后拼入:骨架可以由业务与设计共同评审,代码只维护动态部分。变量绑定、DTO 转换与响应输出的完整做法见第 14 篇,本篇只讨论行的生成机制。
为什么由后端生成
后端生成行片段有三个好处:一是绕开未证实的循环语法,模板始终是静态可读的;二是数据格式化、转义与空值处理都留在 Java 侧,便于测试和审计;三是行片段与模板解耦,同一份表头模板可以复用于不同数据源。代价是模板不再完全自描述,行结构由代码维护,因此需要为行片段补充单元测试。
渲染与分页
流程仍然是"模板 → 绑定 → 渲染":把最终模板交给 executeContent,接口返回字节数组,再落盘或写入响应流。行数增加时由布局引擎自然分页,业务代码不需要计算行高。
依赖
核心只需文档层 jquick-pdfx;jquick-pdf-svg、jquick-pdf-data、jquick-pdf-font、jquick-pdf-css 为可选模块,按需追加。
关键细节
字面量与变量
单元格中的固定文本必须使用单引号,例如 '编号';动态值使用 ${name}。这里有一个容易忽略的细节:行片段是拼接出来的,若每行都想用 ${...} 绑定,就要为每一行注册不同的键。更简单的做法是把已格式化的值作为单引号字面量写进行片段,由后端负责格式化。
行内容安全转义
不要把未过滤的用户输入直接拼进模板:单引号、<、>、& 等字符可能破坏模板结构或改变语义。应先做白名单过滤与转义,或只绑定可信字段;同时限制字段长度,避免超长文本撑破列宽。这一点在商品名、客户名这类自由文本字段上尤其重要。
空集合与列数稳定
- 查询结果为空时仍应输出表头,并给出一行"暂无数据"提示,而不是生成一张没有表头的空表。
- 单行与多行要分别验证:单行最容易暴露列错位与表头样式问题,多行超过一页时要观察分页是否落在行中间。
- 每行的单元格数量必须与表头严格一致,缺列会让整张表的列宽错位。
- 每增加一个字段,都要同步补充空值、
null与超长文本测试数据。
数据规模与分页
数据量大时不要把所有行一次性拼进内存字符串,应分批构造模板并限制单次导出条数;字段列数必须稳定,否则分页位置和列宽都会漂移。金额等数值建议在查询结果转成字符串后再绑定,保证小数位与千分位格式统一。
数值与日期格式
金额、数量和日期不要在模板里格式化:查询结果应先在 Java 侧转成字符串,统一小数位、千分位与日期格式,再拼进行片段。同一列的数据形式不一致时,表格的列宽和视觉对齐会明显漂移;若某列既可能为空又可能出现超长文本,应为它设置 minWidth、maxWidth 与 textAlignment,让超长内容按预期折行。
与固定表头模板的配合
把表头写在模板骨架里、行片段只包含数据行,可以让表头样式集中维护,也便于统一调整列宽。若报表很长、需要在每页重复表头,应先在目标版本上确认表头重复能力,不要按未经验证的行为设计版式。
实战说明
依赖
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> 支持 fontColor、backgroundColor、textAlignment,<td> 支持 padding 与 fontSize。
版本基线:jquick-pdfx 4.0.0、JDK 8+,升级前请核对 README_zh.md 的版本对照表(含许可证与依赖边界);更多示例见 GitHub 仓库。