简介:这份资源面向使用JasperReports进行报表开发的IT人员与数据分析从业者,聚焦报表结构理解与制作效率提升,帮助解决字段显示、分组统计、自定义纸张等常见实操问题。资源包为1个docx文档,约599KB,内容以图文笔记形式整理,便于随时查阅与对照练习。文档系统梳理了Title、Page Header、Column Header、Detail、Column Footer、Page Footer、Summary等七个Band的职责与显示规则,并给出Json数据源接入、中文字段PDF打印配置、手工新增字段、明细分组、序号列添加、自定义纸张大小、控件显示隐藏及Crosstab交叉报表等实用技巧,还涉及报表上传与打印配置的衔接思路。目前已有242人学习,适合刚接触Jasper报表或希望补齐细节的开发者参考,可快速建立结构认知并积累排错经验。
1. Jasper报表制作小技巧:为什么你的第一张报表总在分页上翻车
刚接触 JasperReports 的人,八成都有过这样的经历:数据明明查出来了,预览时却要么挤成一团,要么每页只显示一条记录,要么页脚跑到了第二页开头。这不是玄学,而是 Jasper 的 Band(带区)模型和分页机制在作祟。JasperReports 是一套基于 Java 的开源报表引擎,核心思路是把报表拆成 Title、Page Header、Column Header、Detail、Column Footer、Page Footer、Summary 等若干带区,每个带区有自己的触发时机和重复规则。你写的每一条记录,最终落在哪个带区、怎么分页,全由这套模型决定。这篇笔记面向已经能跑通 JDBC 数据源、但被分页和布局反复折磨的开发者,把 Jasper 报表制作里最常踩的几个坑拆开讲清楚,从带区配置、参数传递到子报表和导出,每一步都给可复现的做法。
2. 带区模型与分页机制:先搞懂 Jasper 怎么切页面
2.1 七个带区的触发时机与重复规则
Jasper 的报表模板(.jrxml)本质是一棵带区树。理解每个带区什么时候打印、打印几次,是解决分页问题的前提。常见做法是先把下面这张表在脑子里过一遍,再动手拖控件。
| 带区 | 触发时机 | 是否每页重复 | 典型用途 |
|---|---|---|---|
| Title | 报表开头一次 | 否 | 报表大标题、Logo |
| Page Header | 每页顶部 | 是 | 列名、页码 |
| Column Header | 每列顶部 | 是(分列时) | 多列布局的列头 |
| Detail | 每条记录 | 否 | 数据行 |
| Column Footer | 每列底部 | 是 | 列小计 |
| Page Footer | 每页底部 | 是 | 页码、打印时间 |
| Summary | 报表结尾一次 | 否 | 总计、签字栏 |
很多人第一张报表翻车,是因为把「列名」放进了 Title,结果第二页开始就没有表头;或者把「总计」放进了 Page Footer,导致每页都打印一次合计。判断标准很简单:这个内容只出现一次,还是每页都要出现?只出现一次的放 Title 或 Summary,每页都要的放 Page Header 或 Page Footer。
2.2 分页的三种触发条件和优先级
Jasper 分页不是随机的,它由三个条件触发,优先级从高到低:
- 带区的
isStartNewPage属性为 true,强制换页; - 当前页剩余高度不足以容纳下一个带区,自动换页;
- 数据源分组变化且分组带区设置了
isStartNewPage。
其中第二条最容易出问题。Jasper 在渲染时会计算每个带区的实际高度,如果 Detail 带区高度加上 Page Footer 高度超过了页面可用高度,就会提前分页。这就是为什么你把 Detail 拉高一点,每页记录数就少一条。解决办法是精确控制 Detail 高度,并给 Page Footer 留出固定空间。
<!-- 页面设置:A4 纵向,上下左右边距各 20 --> <pageWidth>595</pageWidth> <pageHeight>842</pageHeight> <topMargin>20</topMargin> <bottomMargin>20</bottomMargin> <leftMargin>20</leftMargin> <rightMargin>20</rightMargin> <!-- Detail 带区:高度 20,不强制换页 --> <detail> <band height="20"> <textField> <reportElement x="0" y="0" width="200" height="20"/> <textFieldExpression><![CDATA[$F{orderNo}]]></textFieldExpression> </textField> </band> </detail>上面这段配置里,pageHeight减去上下边距后是 802,再减去 Page Header 和 Page Footer 的高度,剩下的才是 Detail 可用空间。如果 Detail 高度是 20,理论上每页能放 30 多条,但实际会因为边框、字体行高有出入。我一般会留 10% 余量,避免最后一条记录被挤到下一页。
2.3 用isSplitAllowed控制带区跨页行为
Detail 带区默认允许跨页拆分,也就是说一条记录如果太高,会被切成两半分别打印在两页上。这在打印发票明细时是灾难。把isSplitAllowed设为 false,Jasper 会把整条记录移到下一页,保证完整性。
<detail> <band height="60" isSplitAllowed="false"> <!-- 一条明细占 60 高度,不允许拆分 --> </band> </detail>注意,isSplitAllowed只对 Detail 和分组带区有效,对 Page Header 和 Page Footer 无效。如果你发现设置了还是被拆,检查是不是放在了 Column Header 里,或者外层有 Frame 容器在作怪。
3. 参数、字段与变量:数据绑定的三个层次
3.1 Parameters 和 Fields 的区别与传参方式
Parameters 是外部传入的值,Fields 是数据源每行提供的值。新手常犯的错是把该用 Parameter 的地方写成了 Field,结果报表一运行就报字段找不到。
// Java 端填充报表时传入参数 Map<String, Object> params = new HashMap<>(); params.put("reportTitle", "月度销售汇总"); params.put("startDate", java.sql.Date.valueOf("2024-01-01")); // 数据源用 JRBeanCollectionDataSource 包装 List<Order> orders = orderService.queryOrders(params); JasperPrint print = JasperFillManager.fillReport( compiledReport, // 编译后的 .jasper 文件 params, // 参数 Map new JRBeanCollectionDataSource(orders) // 数据源 );params里的 key 必须和 jrxml 里<parameter name="...">的 name 完全一致,大小写敏感。Fields 则对应数据源对象的属性名,比如$F{orderNo}对应 Order 类的getOrderNo()方法。如果数据源是 Map 列表,字段名就是 Map 的 key。
3.2 Variables 的计算时机与 resetType 设置
Variables 是 Jasper 内置的计算字段,能做 Sum、Count、Average 等聚合。它的核心属性是resetType,决定什么时候归零。
<!-- 按分组重置的销售额合计 --> <variable name="groupAmount" class="java.math.BigDecimal" resetType="Group" resetGroup="cityGroup" calculation="Sum"> <variableExpression><![CDATA[$F{amount}]]></variableExpression> </variable> <!-- 整表合计,只在报表结尾重置 --> <variable name="totalAmount" class="java.math.BigDecimal" resetType="Report" calculation="Sum"> <variableExpression><![CDATA[$F{amount}]]></variableExpression> </variable>resetType有 None、Report、Page、Column、Group 五种。设成 Page 就是每页归零,适合做页小计;设成 Group 要配合resetGroup指定分组名。我见过有人把页小计变量设成 Report,结果只有最后一页的数字是对的,前面几页全是累计值,这就是 resetType 没配对。
3.3 用$P{}和$F{}时的空值处理
数据库里的 null 传到报表里,如果直接参与计算会抛异常。常见做法是在 SQL 里用 COALESCE 兜底,或者在 jrxml 里用三元表达式。
<textFieldExpression> <![CDATA[$F{amount} == null ? BigDecimal.ZERO : $F{amount}]]> </textFieldExpression>对于字符串字段,空值和空串要区分对待。$F{name}为 null 时,Jasper 默认打印空白,但如果设了isBlankWhenNull="false",会打印 "null" 字样。这个属性在 textField 上,默认是 true,一般不用改。
4. 子报表与分组:复杂布局的拆解方法
4.1 子报表的两种传参模式
子报表适合把主报表拆成多个独立模块,比如主表显示订单头,子表显示订单明细。传参有两种模式:一种是传数据源,一种是传参数。
<!-- 主报表中引用子报表 --> <subreport> <reportElement x="0" y="0" width="555" height="100"/> <subreportParameter name="orderId"> <subreportParameterExpression> <![CDATA[$F{orderId}]]> </subreportParameterExpression> </subreportParameter> <connectionExpression><![CDATA[$P{REPORT_CONNECTION}]]></connectionExpression> <subreportExpression> <![CDATA[$P{SUBREPORT_DIR} + "orderDetail.jasper"]]> </subreportExpression> </subreport>这里REPORT_CONNECTION是 Jasper 内置参数,直接把主报表的数据库连接传给子报表,子报表用自己的 SQL 查数据。另一种做法是用dataSourceExpression传一个 JRDataSource 对象,适合子报表数据已经在内存里的场景。两种模式不要混用,否则会出现连接泄漏或数据重复。
4.2 分组带区的三层结构与排序
分组是 Jasper 最强大的功能之一,但三层结构(Group Header、Group Footer、Detail)容易配错。一个分组至少要有 Group Header 和 Group Footer,Detail 放在中间。
<group name="cityGroup"> <groupExpression><![CDATA[$F{city}]]></groupExpression> <groupHeader> <band height="25"> <textField> <reportElement x="0" y="0" width="200" height="25"/> <textFieldExpression><![CDATA[$F{city}]]></textFieldExpression> </textField> </band> </groupHeader> <groupFooter> <band height="25"> <textField> <reportElement x="400" y="0" width="155" height="25"/> <textFieldExpression><![CDATA[$V{groupAmount}]]></textFieldExpression> </textField> </band> </groupFooter> </group>groupExpression决定按什么字段分组,数据源必须按这个字段排序,否则分组会错乱。我一般会在 SQL 里加 ORDER BY,而不是依赖 Jasper 的isSortInDataSource属性,因为数据库排序更可控。
4.3 子报表返回值的接收与使用
子报表可以返回值给主报表,通过<returnValue>标签。这个功能适合做嵌套汇总,比如子报表算完明细合计,主报表拿来做总合计。
<subreport> <!-- 其他配置省略 --> <returnValue subreportVariable="detailAmount" toVariable="subTotalAmount"/> </subreport>subreportVariable是子报表里的变量名,toVariable是主报表里的变量名。注意主报表的变量 resetType 要设成 Report 或合适的范围,否则返回值会被覆盖。这个功能用的人不多,但在多层汇总场景下能省不少事。
5. 避坑与排查:五个让报表跑不起来的常见问题
5.1 中文不显示或显示成方框
现象:预览时中文全是方框,英文正常。原因:Jasper 默认用的字体不含中文字形。解决:在 jrxml 里指定一个支持中文的字体,比如宋体或黑体,并确保该字体在 classpath 或系统字体目录里。
<textField> <reportElement x="0" y="0" width="200" height="20"/> <textElement> <font fontName="SimSun" size="10" pdfFontName="STSong-Light" pdfEncoding="UniGB-UCS2-H" isPdfEmbedded="true"/> </textElement> <textFieldExpression><![CDATA[$F{name}]]></textFieldExpression> </textField>导出 PDF 时,pdfFontName和pdfEncoding必须配对,否则 PDF 里中文会丢失。如果服务器上没有中文字体,可以把字体文件打包进应用,用net.sf.jasperreports.extension配置加载。
5.2 数据量大了内存溢出
现象:导出几万条记录时抛 OutOfMemoryError。原因:Jasper 默认把整个 JasperPrint 对象放在内存里。解决:启用虚拟化器(Virtualizer),把中间结果写到磁盘。
// 使用文件虚拟化器,阈值 500 条 JRFileVirtualizer virtualizer = new JRFileVirtualizer(500, "/tmp/jasper"); params.put(JRParameter.REPORT_VIRTUALIZER, virtualizer); JasperPrint print = JasperFillManager.fillReport(compiledReport, params, dataSource); // 导出完成后清理临时文件 virtualizer.cleanup();阈值设太小会频繁读写磁盘,设太大起不到省内存的作用。我一般按单条记录大小估算,500 到 2000 之间比较稳。
5.3 分页位置不对,页脚被顶到下一页
现象:Page Footer 的内容跑到了下一页顶部。原因:Page Footer 高度加上 Detail 高度超过了页面可用高度,Jasper 把 Footer 挤下去了。解决:减小 Detail 高度,或者把 Page Footer 的isFloatColumnFooter设为 true,让它浮动在列底部。
<pageFooter> <band height="30" isFloatColumnFooter="true"> <!-- 页码等内容 --> </band> </pageFooter>另一个常见原因是 Page Footer 里放了太大的图片或嵌套了子报表,导致实际渲染高度超出声明高度。检查方法是把 Footer 内容暂时清空,看分页是否正常,逐步加回内容定位。
5.4 子报表路径找不到
现象:运行时报 "Resource not found" 或子报表空白。原因:SUBREPORT_DIR参数没传,或者路径拼接少了分隔符。解决:在 Java 端显式传入子报表所在目录,确保末尾带斜杠。
params.put("SUBREPORT_DIR", getClass().getResource("/reports/").getPath() + "/");如果报表打包在 jar 里,getPath()返回的路径可能不可用,这时要用getResourceAsStream方式加载,或者把子报表编译后放在文件系统的固定目录。
5.5 导出 Excel 时格式错乱
现象:PDF 正常,导出 Excel 后单元格合并、边框全乱。原因:Excel 导出器对带区高度的处理方式和 PDF 不同,它按行合并。解决:给需要合并的单元格设置net.sf.jasperreports.export.xls.merge属性,并避免在 Detail 里用嵌套 Frame。
<textField> <reportElement x="0" y="0" width="200" height="20"> <property name="net.sf.jasperreports.export.xls.merge" value="true"/> </reportElement> <!-- 表达式省略 --> </textField>Excel 导出还有一个坑:如果 Detail 带区高度不是标准行高的整数倍,导出的行高会异常。我一般把 Detail 高度设成 15 或 20 的倍数,导出效果最稳。
6. 进阶技巧:用样式模板和条件样式统一报表外观
6.1 把样式抽成模板文件复用
报表做多了会发现,每个 jrxml 都在重复定义字体、边框、背景色。Jasper 支持把样式抽成独立的.jrtx模板文件,多个报表引用同一份样式,改一处全生效。
<!-- styles.jrtx 模板文件 --> <jasperTemplate> <style name="tableHeader" fontName="SimSun" fontSize="10" bold="true" backcolor="#E0E0E0" border="Thin" borderColor="#999999"/> <style name="tableCell" fontName="SimSun" fontSize="10" border="Thin" borderColor="#CCCCCC"/> </jasperTemplate>在 jrxml 里用<template>标签引入:
<template><![CDATA["styles.jrtx"]]></template>然后在控件上写style="tableHeader"即可。模板文件要放在 classpath 能访问到的位置,和子报表路径的处理方式一样。
6.2 条件样式:根据数据值动态改颜色
条件样式(Conditional Style)是 Jasper 里被低估的功能。比如金额为负时显示红色,库存低于阈值时加粗。
<style name="amountStyle" fontName="SimSun" fontSize="10"> <conditionalStyle> <conditionExpression> <![CDATA[$F{amount} != null && $F{amount}.doubleValue() < 0]]> </conditionExpression> <style forecolor="#FF0000" bold="true"/> </conditionalStyle> </style>把amountStyle应用到金额字段上,正数黑色、负数红色加粗。条件样式的判断顺序是从上到下,第一个匹配的条件生效,所以要把最特殊的条件放在最前面。
6.3 用evaluationTime控制样式计算时机
条件样式默认在渲染时计算,但如果数据量大,每个单元格都判断一次会影响性能。evaluationTime可以设成 Report 或 Page,让样式只计算一次。
<conditionalStyle evaluationTime="Report"> <!-- 条件表达式 --> </conditionalStyle>设成 Report 时,样式在整个报表填充阶段只算一次,适合那些不随记录变化的全局条件。设成 Page 则每页算一次。默认是 Now,即每条记录都算。这个参数在数据量上万时能明显减少渲染时间。
6.4 导出前的最后检查清单
导出前我习惯过一遍这几项:字体是否嵌入、分页是否连续、空值是否兜底、子报表路径是否可访问、虚拟化器是否清理。尤其是虚拟化器,忘记 cleanup 会在临时目录堆积大量文件,跑几天就把磁盘占满。这个坑我踩过一次,后来在 finally 块里固定加 cleanup,再没出过问题。
报表制作这件事,细节多但规律性强。把带区模型吃透,参数和变量的边界划清,剩下的就是耐心调布局。希望帮到你。
本文还有配套的精品资源,点击获取