简介:《Jasper报表制作小技巧》是一份面向使用Jaspersoft进行报表开发与维护的IT人员的实操型文档,适合已具备基础报表概念、希望系统梳理模板结构与常见问题处理方式的读者。内容围绕报表七个Band(Title、Page Header、Column Header、Detail、Column Footer、Page Footer、Summary)展开,并延伸到Json数据源接入、中文字段在PDF中正常显示、手工补充字段、明细分组、序号列变量、自定义纸张尺寸以及交叉报表配置等典型场景,对快递面单和WMS打印模板也有针对性说明。资源包共1个docx文件,约599KB,轻量便于查阅和检索;已有242人学习。读者可从中获得按结构划分的知识框架、可直接对照的操作要点和排错思路,用于快速定位字段、分组、分页与打印显示问题,提升报表模板设计和交付效率。
1. 从一张快递面单说起:Jasper 报表到底解决了什么
一张快递面单要在 100mm×150mm 的纸上放下收件人、电话、地址、条码、路由信息,换一家承运商就得改一版;一张入库商品明细表要按供应商分组、带序号、跨页出合计、导出 PDF 还不能中文乱码。这两类需求摆在面前,用 Word 或 Excel 手工排版基本等于给自己挖坑。Jasper 报表制作的核心价值就在这儿:把版面切成职责固定的区段(Band),数据源一换、分组一改、导出格式一端自适应。这篇是把一套仓储场景里的报表模板拆完之后的整理,覆盖 JSON 数据源接入、PDF 中文字体、分组与交叉表、条件打印、上传配置这几件事。适合正在用 Jasper 出作业单据和管理报表的人,也适合照着步骤一步步复现的新手。
2. Band 分工与 JSON 数据源:九个区段怎么排成一条产线
2.1 九个 Band 的职责边界与常见错位
刚上手的人最容易犯的错,是把报表当成一张画布,控件想放哪就放哪。Jasper 的版面不是画布,是一条从上到下依次执行的产线,每个 Band 有明确的出现时机和出现次数,放错位置的结果不是「丑一点」,而是数据重复或压根不出现。
| Band | 出现位置 | 出现次数 | 通常放什么 |
|---|---|---|---|
| Title | 第一页最上方 | 仅第一页 | 报表大标题、单位名、制表周期 |
| pageHeader | 每页上部 | 每页 | 页码、打印时间、制表人 |
| columnHeader | 明细之上 | 每页 | 列的标签,不放数据 |
| Detail | 内容区 | 每条记录一次 | 明细数据行 |
| columnFooter | 明细之后 | 每页 | 列级小计或列说明 |
| pageFooter | 页面最下端 | 每页 | 页级小计、页码 |
| Summary | 最后一页 Detail 之后 | 仅一次 | 全表合计 |
| groupHeader | 每组开头 | 每组一次 | 组标题、组字段值 |
| groupFooter | 每组结尾 | 每组一次 | 组小计 |
| lastPageFooter | 最后一页最下 | 仅最后一页 | 签收栏、末页说明 |
Title 和 pageHeader 的关系值得单独说一句:第一页时 pageHeader 的内容排在 Title 下面,第二页开始 Title 消失,pageHeader 顶到页面最上端。这意味着一份带单位抬头的报表,抬头写在 Title,页码和打印时间写在 pageHeader,两者不会打架。但如果你把单位抬头也复制了一份到 pageHeader 里,第二页就会重复出现一行抬头,跨页打印时第一眼就很别扭。
合计放哪儿是第二个高频错位点。Summary 只出现一次,是放全表合计的地方;pageFooter 每页出现,放进去的合计会在每一页各算一遍,翻到第二页看到「总计 120」你会以为算错了,其实是本页小计。老手区分得很清楚:需要跨页累加的总数走 Summary,需要每页收口的走 pageFooter 或 columnFooter。
2.2 从 JSON 到可预览报表的九步流水线
把 JSON 当数据源是这套模板里最常用的一条路,顺序基本固定:添加数据源、新建报表页、关联数据源、添加数据字段、添加并修改字段标签、属性设置、分组、预览、编译。顺序颠倒的代价是返工,比如先拖了一堆控件再关联数据源,字段绑定就得一个个重来。
先准备一份结构稳定的 JSON 样本,字段名建议直接用业务字段名,别用 f1、f2 这种占位名,后面 SQL 对不上会很难受:
{ "receiptCode": "RK20240115001", "shipFromName": "某供应商", "closedAt": "2024-01-15 10:22:31", "itemCode": "SKU-0001", "itemName": "某物料", "totalQty": 120, "openQty": 0, "storeName": "某仓位" }这几个字段的用途分别是:receiptCode做单号列,shipFromName做分组字段,closedAt做日期列并按date类型处理,totalQty与openQty做数量列并按Integer处理,差值totalQty - openQty在表达式里现算。字段类型这一步别偷懒,JSON 读进来默认全是字符串,数字字段不改成整型,后面做 Sum 合计、做交叉表都会失败。
数据源配好后新建报表页,选空白模板,关联刚建的数据源。添加字段有两种方式:一种是在查询对话框里粘贴 JSON 样本自动解析出字段列表,另一种是手工在报表大纲里补。前者快,后者准,我一般是先用样本自动生成,再逐个核对类型和显示名。字段标签不是字段本身,它是 columnHeader 里那个文本控件,改的是显示文字,不影响绑定关系。
属性设置这一步有几个必调项:页面尺寸和边距、字体、是否允许溢出。Detail 的行高要卡死,不能让内容撑开,否则一条长地址能把整页挤变形。做完这些再进分组、预览、编译,预览阶段看的不是好不好看,而是三条:行数对不对、合计对不对、跨页时表头有没有重复。
提示:没有数据源也能生成报表,只是出来的是纯静态版式,预览看不到真实数据。做面单这类结构固定的单据时,先无数据源排好版再挂数据源,反而更快。
2.3 手工补字段:name 对齐是硬约束
样本 JSON 只反映某一次请求的字段,实际业务里经常漏字段——接口加了个新列,或者某条记录里正好没这个 key,自动解析就抓不到。这时候在左下角报表大纲里右键fields,选新增字段,在属性框里填 Name、描述、类型三样。
<!-- 手工补字段:name 必须与 JSON key 或 SQL 列名逐字符一致 --> <field name="storeName" class="java.lang.String"> <property name="com.jaspersoft.studio.field.label" value="仓位名称"/> </field>name是绑定用的内部标识,大小写、下划线、前后空格都算数,storeName和storename在报表引擎眼里是两个字段。class决定这个字段参与运算时的类型,java.lang.String只能拼字符串,java.lang.Integer才能进 Sum。field.label那个 property 只是设计器里显示的中文名,改它不影响数据。
血泪经验是:补完字段一定要回预览里确认那一列有值。字段名写错时报表不会报错,$F{storename}取不到值就渲染成空白,再叠加上 Blank When Null,整列看起来就像「这个字段本来就没数据」,排查起来非常费时间。
3. PDF 中文与自定义纸张:三个字体参数、一条毫米量法
3.1 PDF 字体三件套:pdfFontName、pdfEncoding、isPdfEmbedded
最典型的翻车现场是这个:设计器里预览,中文显示得好好的;导出 PDF 或者上传到系统里再打印,中文全变成空白或者方块。原因不复杂——预览用的是本机字体,PDF 导出走的是另一套字体映射,没配映射就只能画个空。
补救办法有两种,逐字段改或者全局改。逐字段改是在控件的属性面板里进Advanced页,把 PDF 相关的三项填上;全局改是切到source源码界面,在样式区插一条默认样式:
<!-- 全局 PDF 样式:放在 style 段,isDefault 让它对未单独指定的元素生效 --> <style name="pdfStyle" isDefault="true" pdfFontName="STSong-Light" pdfEncoding="UniGB-UCS2-H" isPdfEmbedded="true"/>三个参数各管一件事。pdfFontName指定 PDF 里实际使用的字体名,STSong-Light是常见的中日韩字体映射名。pdfEncoding指定编码,UniGB-UCS2-H对应简体中文的 Unicode 编码,填错了字符会解析成乱码而不是空白。isPdfEmbedded控制是否嵌入字体,设为 true 表示把字体信息带进 PDF;如果目标阅读环境缺少对应的字体包,就得换成可以真正嵌入的字体文件,否则打开还是空白,这时候报错信息往往很含糊,只能靠换字体试出来。
要注意全局样式只对「没有单独指定字体」的元素生效。如果某个控件之前被手工设过字体,它会覆盖全局设置,于是出现「大部分中文正常、就那一个标签是方块」的玄学现象。我一般的做法是全局样式加一遍,再对着每个中文标签把 Advanced 页过一遍,编译前在 source 里搜一次pdfFontName,确认没有漏网的。
3.2 自定义纸张与面单裁剪:把非 Detail 区压到零
标准 A4 之外的纸张都得手工定义。新建完报表后,点页面最边框外侧的空白区域选中整个报表,在 Report 属性里找Edit Page Format,Format选Custom,然后在Units里把单位切成 mm,再填宽高。
Jasper 内部长度单位是 point(1/72 英寸),100mm 换算下来大约是 283pt,150mm 约 425pt。所以源码里看到的是这样一串数字,别以为是随便写的:
<!-- 100mm x 150mm 的面单,换算成 point 后约 283 x 425 --> <pageFormat width="283" height="425" orientation="Portrait"/>面单这类单据还有个关键动作:只保留 Detail 区,其余 Band 的高度全部设成 0。Title、pageHeader、columnHeader、columnFooter、pageFooter、Summary 一个不留,全压到 0,页面空间就整个让给明细行。这里有个顺序讲究——先把 Band 里的控件删干净,再把高度改成 0;反过来操作的话,控件还挂在高度为 0 的 Band 里,预览时会诡异地挤到相邻区域,看着像版面错乱。
| 区域 | 面单场景高度 | 说明 |
|---|---|---|
| Title | 0 | 面单不需要大标题 |
| pageHeader | 0 | 不打页码和制表人 |
| columnHeader | 0 | 面单没有列名行 |
| Detail | 剩余全部 | 收件信息、条码、路由 |
| columnFooter | 0 | 无列小计 |
| pageFooter | 0 | 无页脚 |
| Summary | 0 | 单条记录,无需合计 |
3.3 条件打印:用 EQUALS 区分陆运与航空
面单上常有一类信息只在特定条件下出现,比如承运方式标识,陆运件印一个标记、航空件印另一个,其余情况整块留白。硬用两个控件叠着放不行,得让控件自己判断该不该打印。
<!-- 仅当 billType 等于 "E" 时才打印这个文本控件 --> <textField> <reportElement x="10" y="4" width="24" height="12"> <printWhenExpression><![CDATA[EQUALS($F{billType}, "E")]]></printWhenExpression> </reportElement> <textFieldExpression><![CDATA["陆运"]]></textFieldExpression> </textField>printWhenExpression返回布尔值,true 就打印,false 就整块跳过,不占位。EQUALS是报表引擎内置的比较函数,两个参数分别是字段和字面量,字段为 null 时返回 false,不会抛空指针。如果条件更复杂,比如要同时判断承运方式和目的区域,写成EQUALS($F{billType},"E") && $F{areaCode} != null这种组合表达式也能跑。
用这个机制要注意一点:条件为 false 时控件彻底不渲染,不是渲染成空白。所以如果版面是「左边标签、右边值」的成对结构,标签和值两个控件必须挂同一个条件表达式,漏挂一个就会只剩半截。
4. 分组、序号与交叉表:三种版式需求的落地做法
4.1 分组两种做法:Create Group 与 columnHeader 归零
按供应商分组、同供应商的明细归档到同一页,是入库明细表最常见的诉求。实现路径有两条,效果接近但维护成本不一样。
第一条是在报表大纲里右键报表名,选Create Group,选中分组字段,确定后在属性面板里把band properties的Height设成 0,同时勾上Start New Column和Start New Page。Height 设 0 是为了让组头不额外占一行,Start New Page 让每个组从新页开始。这条路的坑在于 Height 归零之前得先把组头里的控件清掉,留着控件会以零高度渲染,看起来像页面上多了一道细缝。
第二条是针对单个分组字段的简化做法:直接把 columnHeader 的 Height 设成 0,再把原本在 columnHeader 里的列名控件全部拖进 groupHeader,分组字段本身也放进去,设置隐藏不显示。这样列名就只在每个组的开头出现一次,视觉上等同于按组分页。
<!-- 分组定义:groupExpression 决定按哪个字段切组 --> <group name="storeGroup"> <groupExpression><![CDATA[$F{storeName}]]></groupExpression> <groupHeader> <band height="20"> <!-- 组头里放组名,替代原来的列名行 --> <textField> <reportElement x="0" y="0" width="120" height="18"/> <textFieldExpression><![CDATA[$F{storeName}]]></textFieldExpression> </textField> </band> </groupHeader> </group>groupExpression是切组依据,每次这个值变化就开一个新组,所以字段最好先排过序,否则同一家供应商会被切成好几段。groupHeader的height决定组头占多高,设 0 就完全隐藏。组的统计值放在groupFooter里,引擎会自动按组累加,不用自己写循环。
4.2 序号列:Create Variable 的四个参数
明细行前面加一列 1、2、3,看起来简单,但用字段拼是拼不出来的,得用变量。在报表大纲里右键fx选Create Variable,重点填四个参数:
| 参数 | 建议取值 | 作用 |
|---|---|---|
| Name | SEQ_NO | 变量名,表达式里用$V{}引用 |
| Value Class Name | java.lang.Integer | 必须是数值型,字符串没法自增 |
| Initial Value Expression | 0 | 首次求值前的初值 |
| Variable Expression | $V{SEQ_NO} + 1 | 每行求值一次,实现自增 |
<!-- 序号变量:每次求值加 1,resetType=Report 表示全表连续编号 --> <variable name="SEQ_NO" class="java.lang.Integer" resetType="Report" calculation="Nothing"> <initialValueExpression><![CDATA[0]]></initialValueExpression> <variableExpression><![CDATA[$V{SEQ_NO} + 1]]></variableExpression> </variable>初值填 0 而不是 1,是因为变量表达式在明细行渲染之前先执行一次,0 加 1 得 1,第一行正好显示 1。初值填 1 的话第一行会变成 2,这个偏移量很多人第一次都会踩。resetType决定什么时候清零重来:填Report是全表连续编号,填Page是每页从 1 开始,填Group则每个分组内重新计数——按供应商分组又要求组内序号时,这个参数必须改成Group。
序号变量建好后直接从大纲拖到 Detail 区第一列位置,控件会自动绑上$V{SEQ_NO}。
4.3 Crosstab 交叉表:类型、位置与字号
交叉表用来做二维汇总,比如按商品类别和仓库两个维度统计收发货数量。它和普通表格的实现思路完全不同,坑也集中。
第一道坎是字段类型。明细里常用的ReceiptCount、TotalQty从 JSON 读进来默认是 String,直接丢进 Crosstab 做 Sum 会报类型错误。必须先在字段定义里把class改成java.lang.Integer,或者用Integer.parseInt()在表达式里转一次。类型不改,向导走到度量那一步就卡住。
第二道坎是位置。Crosstab 只能放在 Summary 区,拖到 Detail 区会直接拒绝。所以做交叉表之前,先把用不上的 Band 删掉,只留 Title 和 Summary,版面才干净。
第三道坎是字段太多显示不下。向导里依次选行维度、列维度、度量字段,度量字段的calculation统一改成Sum;完成后把 Crosstab 拉伸到覆盖整个页面,双击进去还能改静态文本的行列标题。默认字号对多列场景偏大,全选明细字段改成 6 号并居中,才装得下。
| 向导步骤 | 选什么 | 注意 |
|---|---|---|
| 数据源 | 引用已存在的数据源 | 别新建空数据源 |
| 行分组 | 商品类别等低基数字段 | 基数太高会撑爆页面 |
| 列分组 | 仓库、月份等分类字段 | 列数控制在 12 以内 |
| 度量 | 数量、金额等 Integer 字段 | calculation 全部改 Sum |
| 布局 | 明细行、合计行按需勾选 | 先勾全再删减 |
5. 避坑清单:编译上传路上翻过的五辆车
5.1 现象:预览中文正常,进系统后中文全消失
原因:PDF 字体属性只设在了部分控件上,或者设了全局样式但被个别控件的本地字体覆盖。预览走系统字体,系统渲染不走这套映射,差异就在这一步暴露。
解决:先加全局pdfStyle,再逐个中文标签检查 Advanced 页的三项是否齐全;在 source 界面搜一遍pdfFontName,把没有该属性的文本控件补上。编译后重新导出 PDF 抽查第一页和最后一页,末页最容易漏。
5.2 现象:报表跑出来全是空行,行数对但每列都没值
原因:字段name与 JSON key 或 SQL 列名大小写不一致,比如 JSON 里是itemCode,字段定义写成了itemcode。引擎取不到值不报错,配合 Blank When Null 就渲染成空白。
解决:在 source 界面把字段定义和样本 JSON 并排比对,逐字符核一遍;预览时先不加 Blank When Null,让缺失值显式暴露出来,确认字段通了再开这个选项。
5.3 现象:分组后每页末尾多出一行空白
原因:groupHeader 高度设成了 0,但里面还留着控件;或者 Detail 行高加组头高度超过了页面可用高度(页高减去上下边距),引擎被迫多分一页。
解决:先把 groupHeader 里的控件全部删除或挪走,再把高度改 0;然后算一遍pageHeight - topMargin - bottomMargin,确认 Detail 行高乘以每页行数不超过这个值。
5.4 现象:Crosstab 向导走不完,或者出来一张空表
原因:度量字段类型是 String,Sum 无法计算;或者 Crosstab 放错了 Band。
解决:回字段定义改class为java.lang.Integer,已有控件里的绑定表达式同步改;确认 Crosstab 在 Summary 区,删掉多余 Band 再重新拖一次。
5.5 现象:管理报表传了查询条件,结果还是全量数据
原因:查询条件 JSON 里的type与 SQL 里命名参数的实际类型对不上,日期传成字符串、整型传成文本,条件被当成恒真。
解决:对着 SQL 里的每个:param逐个核对条件 JSON 的type,date、datetime、string、integer不能混用;field和id保持一致,改完重新上传模板再试。
6. 打印配置与 SQL 条件模板:让模板真正接进业务流程
模板编译完只是半成品,还得在系统里挂上才能被业务用到。作业报表走「配置 → 打印配置 → 报表」这条路,新增一条记录,填代码标识(尽量和模板名一致)、描述、报表类型,然后上传模板文件。管理报表则是在「管理 → 报表查询」里直接双击预览,选好期间和其他条件点 PDF 下载。
真正决定报表能不能用的,是那段查询条件 JSON。它由系统渲染成查询表单,再由后端替换成 SQL 参数:
[ {"field": "", "id": "h1_basic", "name": "基本信息", "type": "h1"}, {"field": "beg", "id": "beg", "name": "起始日期", "type": "date"}, {"field": "end", "id": "end", "name": "结束日期", "type": "date"}, {"field": "itemCode", "id": "itemCode", "name": "商品编码", "type": "string"}, {"field": "shipFromName", "id": "shipFromName", "name": "供应商名", "type": "string"} ]type为h1的那一行是分组标题,不参与查询,剩下的四项才是真正的条件。field、id、name、type四个都是必填,field和id尽量写成一个词,name是表单上显示的中文,type必须和 SQL 参数类型对齐。作业报表通常只需要改name,管理报表要加期间、单号、供应商这类自定义条件时,就得按上面这样逐条补。
对应的 SQL 用命名参数占位,每个条件都套一层「参数为空则忽略」:
select rh.closedAt, rd.receiptCode, rh.shipFromName, it.code, it.name, rd.totalQty, rd.openQty, (rd.totalQty - rd.openQty) as redQty from RECEIPT_HEADER rh inner join RECEIPT_DETAIL rd on rh.id = rd.receiptId inner join ITEM it on rd.itemCode = it.code where rh.leadingSts = 900 and (rh.code = :code or :code is null) and (rd.itemCode = :itemCode or :itemCode is null) and (rh.shipFromName = :shipFromName or :shipFromName is null) and (rh.closedAt >= :beg or :beg is null) and (rh.closedAt <= :end or :end is null) order by rh.closedAt descor :param is null这个写法是让条件可选的关键:用户不填,参数为 null,整条判断恒真,等价于不过滤;用户填了,就按等值或区间过滤。少了这一层,表单留空时查出来是零条。日期区间要成对写,只写>=不写<=会漏掉上界校验。
从那以后我每次配完一份报表,都会强制走一遍三步自检:先在设计器里用最大数据量预览一次,看跨页和合计;再导出 PDF 抽查首末两页的中文;最后在系统里用空条件、单条件、全条件各查一次,确认行数随条件收敛。这套动作重复起来枯燥,但能挡掉绝大多数上线后才发现的问题。希望帮到你。
本文还有配套的精品资源,点击获取