简介:JasperReports Library 7.0.0 是面向 Java 开发者和报表设计人员的完整项目源码压缩包,覆盖核心报表引擎、导出模块、示例模板与说明文档,适合需要学习新版 API、定制报表输出或排查 PDF/HTML 导出问题的中高级开发者。包内共 2000 个文件,以 1687 个 Java 源文件为主,辅以 121 个 XML 报表模板/配置、74 个 Markdown 文档、73 个 properties 配置,以及少量 JSON、JSP、HTML、CSS 和 PDF 资源,整体约 19.07MB,目录结构清晰,便于按模块查阅和编译调试。目前已有 1986 人学习下载。读者可获得 7.0.0 版本的工程骨架、报表填充与导出流程示例、资源配置说明和文档导读,既能作为二次开发的基础代码,也可作为对照参考排查常见构建或运行异常。
1. JasperReports Library 7.0.0 (2024-06-17):报表需求拖到最后一刻时,我靠它把交付时间从两天压到两小时
下午四点接到一个紧急报表需求,客户要求第二天一早看到 PDF 格式的对账单,模板还是三年前写的。我翻出这个 2024-06-17 发布的 JasperReports Library 7.0.0,在旧代码里试了一版,发现老模板在新版本下编译直接报错。花了一小时做迁移适配,又用一小时调好数据源和字体,当天把成品出了。JasperReports Library 是 Java 生态里最老牌的开源报表引擎之一,核心工作是把 JRXML 模板编译成可执行定义,再填充数据并导出成 PDF、HTML、XLSX 等格式。它解决的痛点是:不要在后端代码里一行行拼表格,而是把报表样式和数据结构固定成模板,数据变了,出图逻辑不变。适合谁?后端开发、内部系统维护者,凡是需要定期产出账单、统计表、对账明细的人,都值得把它纳入工具箱。
2. 升到 7.0 前必须搞清楚的执行环境与依赖基线
2.1 JDK 17 起步:javax 到 jakarta 的那一刀
我这边从 6.x 往 7.0.0 升的时候,第一眼看到的不是 API 变化,而是环境要求:JDK 基线抬到了 17。这不是随便抬的,7.0 把底层依赖全面切到了 Jakarta EE 9 的命名空间,原先代码里常见的javax.servlet、javax.xml这类引用,在 7.0 构建的产物里基本都换成了jakarta.*。如果你的报表服务还跑在 JDK 8 或者 JDK 11 上,升级之前得先确认应用服务器和容器是否支持 Jakarta 命名空间,否则就算依赖坐标改对了,运行时也会在类加载阶段直接抛NoClassDefFoundError。
这个决策其实是个选择题。新项目如果已经用了 Spring Boot 3 或者其他强制 JDK 17 的框架,那 7.0 是顺势而为,没有额外成本;老项目如果还在 JDK 8 上稳如老狗,我一般会建议不要为了升级而升级,先停在 6.20 那条线,等容器环境一起升级时再动报表库。7.0 虽然带来了一些性能优化和 API 清理,但对一个运行良好的存量报表服务来说,「不折腾」就是最大的收益。
先给项目做一次体检,命令很简单:
find . -name "*.java" -o -name "*.xml" | xargs grep -l "net.sf.jasperreports" | xargs grep -n "javax\." | head -n 50这个命令会把项目里所有引用了net.sf.jasperreports的 Java 和 XML 文件找出来,再看它们内部是否混着javax.的类引用。逻辑是:报表引擎包名换了,但你的业务代码里可能还在用老命名空间操作数据源或 servlet 上下文。先把这些位置标出来,升级时逐个改,比在编译报错里被牵着走要省事得多。跑完看输出,如果发现大量javax.servlet引用,意味着你的报表模块和 Web 容器耦合很深,得先解耦再升级。
2.2 Maven 依赖的最小闭环与按需引入
7.0.0 的核心坐标还是那个老地址:net.sf.jasperreports:jasperreports:7.0.0。但要注意,7.0 开始模块化意识变强了,很多数据源适配器从核心包拆了出去。只引核心包能编译、能出 PDF,但要用 JSON 数据源、CSV 数据源或者 JDBC 直连,还得看具体模块是否打进了依赖树。我这边习惯的最小闭环是这样的:
<dependency> <groupId>net.sf.jasperreports</groupId> <artifactId>jasperreports</artifactId> <version>7.0.0</version> </dependency> <!-- 用 JSON 数据源时追加 --> <dependency> <groupId>net.sf.jasperreports</groupId> <artifactId>jasperreports-json</artifactId> <version>7.0.0</version> </dependency> <!-- 用 CSV 数据源时追加 --> <dependency> <groupId>net.sf.jasperreports</groupId> <artifactId>jasperreports-csv</artifactId> <version>7.0.0</version> </dependency>这里要说明两点。第一,jasperreports核心模块的 scope 用默认的compile就行,别改成provided,因为报表引擎在运行时需要它完成模板编译和导出渲染,不是只在编译期用到。第二,日志后端用的是 SLF4J API,如果你的项目里没有绑定具体实现,报表引擎的日志会静默丢弃,排查问题时看不到关键信息。我一般会加一个slf4j-simple或者让项目已有的 logback 去接管,这样编译告警和导出异常都能落到日志里。
还有一种情况:你发现项目里同时引了老版的jasperreports,又通过传递依赖带进了 7.0 的某个模块,两个版本在 classpath 里打架。这个用 Maven 的依赖树排查:
mvn dependency:tree -Dincludes=net.sf.jasperreports看到重复的版本号,直接用exclusion排除传进来的老版本,只保留显式声明的 7.0.0。依赖冲突的表现往往很诡异——模板能编译,但导出时某类方法报NoSuchMethodError,就是这个原因。
2.3 老模板的兼容性自检:编译一遍全部探雷
升级完成后别急着跑业务代码,先做一个全量模板编译体检。JRXML 文件在项目里通常成百上千个,分布在各个模块的 resources 目录里,写个脚本批量编译一遍,比等运行时一个个炸快得多。我一般用这样的命令:
find . -name "*.jrxml" -print0 | while IFS= read -r -d '' file; do java -cp "$CLASSPATH" net.sf.jasperreports.engine.JasperCompileManager -xml compile -p "$file" -o /tmp/report_check.jasper if [ $? -ne 0 ]; then echo "FAIL: $file" else echo "PASS: $file" fi done这个脚本的逻辑很简单:对每个 JRXML 文件调用JasperCompileManager的 XML 编译模式,只编译不填充数据,看语法和属性是否符合 7.0 的规则。CLASSPATH需要包含核心包和依赖,最省事的办法是用 Maven 把运行期 classpath 打出来。脚本跑完,FAIL 的列表就是你这次升级要处理的工作项,PASS 的模板基本可以直接进运行期。
这里有一条血泪经验:不要顺手改模板内容。7.0 对 JRXML 的 schema 版本校验更严格,某些老的xsi:schemaLocation指向的旧命名空间已经不被识别,报错信息会误导你以为是标签语法错了。实际上你只需要把根标签上的xmlns和xsi:schemaLocation更新到 7.0 的命名空间,其他内容原样保留,编译就能过。
2.4 老接口仍在,但别把双版本并行走到底
7.0 虽然在 API 上做了清理,但核心接口如JasperCompileManager、JasperFillManager、JasperExportManager仍然沿用老名字,这让很多项目在升级后第一眼感觉「好像没变」。真正变化的是内部实现和一些边缘接口。如果你的报表服务被多个业务线调用,我建议把升级做成一个独立分支,先在一台测试机上用真实模板跑一版完整导出,比对 PDF 页数和关键字段值,确认无差异后再合入主干。
如果报表服务没法一次性停掉,可以考虑一个更稳的做法:新旧版本并行部署,流量按比例灰度。老版本继续跑存量任务,新版本接收新任务,两边同时产出报表,核对半个月确认无误后再把老版本下线。并行期间要注意数据源连接和临时文件目录必须隔离,否则两边同时写缓存目录,产出的 PDF 会出现互相覆盖的问题。从 6.x 到 7.0 的迁移难度不算高,但要给它留出足够的验证时间。
3. 在本地跑通最小报表:JRXML 从写死到填数的完整链路
3.1 最小 JRXML 模板拆解
先忘掉设计器,从手写一个最精简的 JRXML 开始。这个文件虽然短,但它是理解报表引擎工作方式的最佳入口:
<jasperReport xmlns="http://jasperreports.sourceforge.net/jasperreports" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://jasperreports.sourceforge.net/jasperreports http://jasperreports.sourceforge.net/xsd/jasperreport.xsd" name="MinimalReport" pageWidth="595" pageHeight="842" columnCount="1" isIgnorePagination="false"> <title> <band height="50"> <staticText> <reportElement x="0" y="0" width="200" height="20"/> <text><![CDATA[Hello JasperReports 7.0.0]]></text> </staticText> </band> </title> <detail> <band height="30"> <textField> <reportElement x="0" y="0" width="200" height="20"/> <textFieldExpression><![CDATA[$F{name}]]></textFieldExpression> </textField> </band> </detail> <field name="name" class="java.lang.String"/> </jasperReport>模板的逻辑结构是这样的:根元素jasperReport定义了整张报表的物理属性,pageWidth和pageHeight决定纸张尺寸,单位是像素,A4 纵向一般是 595 乘 842。title部分只在第一页顶部渲染一次,适合放报表标题和公司名这类信息。detail部分是整张报表的核心,它会对数据源里的每一行重复渲染,也就是说,数据源有 10 条记录,detail band 就会画 10 遍。field元素声明的字段名必须和后面textFieldExpression里的$F{name}一致,它代表数据源中每一列的取值。
这里有个关键参数isIgnorePagination。默认是 false,表示按纸张高度分页,detail band 累积超过页面剩余高度时自动换页;如果设为 true,所有 detail 内容会连续排在一整张长页上,适合导出无分页概念的 HTML 或 Excel。做测试时先保持 false,等确认内容稳定后,再根据导出目标调整。
3.2 Java 侧编译、填充、导出三步
模板写好后,Java 侧的代码出奇地短。报表引擎的核心调用路径就三个静态方法,对应整个工作流的三阶段:
import net.sf.jasperreports.engine.*; import net.sf.jasperreports.engine.data.JRBeanCollectionDataSource; import java.util.*; public class MinimalReportRunner { public static void main(String[] args) throws JRException { // 第一步:把 JRXML 编译成二进制模板 JasperReport jasperReport = JasperCompileManager.compileReport("src/main/resources/MinimalReport.jrxml"); // 第二步:准备参数和字段数据 Map<String, Object> params = new HashMap<>(); params.put("REPORT_LOCALE", Locale.CHINA); List<Map<String, Object>> rows = new ArrayList<>(); Map<String, Object> row = new HashMap<>(); row.put("name", "第一行数据"); rows.add(row); JRBeanCollectionDataSource dataSource = new JRBeanCollectionDataSource(rows); // 第三步:填充数据生成 JasperPrint 对象 JasperPrint jasperPrint = JasperFillManager.fillReport(jasperReport, params, dataSource); // 第四步:导出成 PDF JasperExportManager.exportReportToPdfFile(jasperPrint, "output/result.pdf"); System.out.println("PDF 导出完成"); } }这段代码的逻辑分四步:compileReport把 JRXML 编译成JasperReport对象,这个对象是线程安全的,可以在多线程间复用,所以生产环境里建议启动时就编译好并缓存起来,不要让每个请求都编译一遍。fillReport是真正的数据绑定环节,它接收三个参数:编译后的模板、参数 Map、数据源。参数 Map 里放的是模板中用$P{xxx}引用的变量,以及引擎内置参数如REPORT_LOCALE,这个参数用来控制数字、日期的本地化格式。数据源是JRDataSource类型,JRBeanCollectionDataSource是最简单的实现,它把 JavaBean 或 Map 的 List 包装成报表引擎可迭代的数据行。
exportReportToPdfFile则是把填充后的JasperPrint对象写成本地 PDF。注意JasperPrint是内存对象,所有数据行渲染后的完整内容都存在里面,数据量极大时内存占用会直线上升,这个问题放到后面章节细说。当前测试阶段,先确保它能跑通就行。
运行这段代码前要先把 classpath 准备好。用 Maven 构建的项目直接打 dependency 插件:
mvn dependency:build-classpath -Dmdep.outputFile=cp.txt javac -cp "target/classes:$(cat cp.txt)" -d target src/main/java/MinimalReportRunner.java java -cp "target/classes:$(cat cp.txt)" MinimalReportRunnerdependency:build-classpath会把所有依赖 jar 的绝对路径输出到cp.txt,编译和运行阶段都用它拼接 classpath。看到output/result.pdf生成后,用任意 PDF 阅读器打开,内容应该有一个标题加一行「第一行数据」。到这里,最小可运行的链路已经打通,接下来是把真实场景的数据源接进来。
3.3 报表设计器与手写 XML 的配合方式
新手最容易犯的错误是一上来就用报表设计器拖控件,生成了几百行 XML 后云里雾里,出了问题无从下手。我的建议是反过来:先能手写最小模板,再进入设计器做复杂版式。设计器是一个可视化编辑器,它左侧是组件面板,右侧是模板结构树,中间是可拖拽的画布。它减少了你计算坐标的负担,但生成的 JRXML 里会有大量冗余属性,比如每个元素的positionType、stretchType,这些属性在 7.0 里仍然是合法的,但会干扰你定位问题。
我一般在设计器里做两件事:一是快速排版,把静态文本和字段的位置放好,二是把报表参数和数据源预览跑起来看效果。真正涉及动态变化的部分,比如子报表、条件显示、行号计算,我会在设计器里写表达式,然后切回文本模式核对一遍生成的 XML。还有一个值得注意的地方:设计器导出的模板会带上报表属性uuid,同一条模板在不同版本里 UUID 不一致会影响版本对比,所以项目里应禁用 UUID 的自动刷新,让同一个模板的 UUID 保持稳定。
设计器和手写 XML 的最终产物都是同一个 JRXML 文件,引擎不关心你是拖出来的还是手打的。所以在本地做最小验证时,手写完全够用;复杂报表需要精细到每个元素的位置和大小时,设计器的 1 像素级微调才是效率工具。两者并不是替代关系,而是不同阶段的选择。
4. 把真实数据喂给报表:四种数据源接入与参数传递
4.1 JavaBean 集合:最省事但 getter 命名写错就白干
企业内部系统最常见的场景是报表数据来自一个服务层方法,它返回一个 JavaBean 的List。接入方式很简单,直接用JRBeanCollectionDataSource包一层就行:
List<OrderSummary> orders = orderService.queryTodayOrders(); JRBeanCollectionDataSource dataSource = new JRBeanCollectionDataSource(orders);JRXML 字段声明时,field的name属性必须对应 JavaBean 的属性名,这个属性名按 JavaBean 规范是 getter 方法去除get前缀后首字母小写。比如 JavaBean 里有getOrderId(),字段名就是orderId;有getTotalAmount(),字段名就是totalAmount。报表引擎在填充时通过反射调用 getter,如果模板里写的字段名和 getter 对不上,它不会报编译错误,而是在fillReport阶段抛异常,或者更隐蔽地输出 null 值。
这里有个容易翻车的点:JavaBean 内部类如果没有公开 getter,比如直接访问内部字段的私有成员变量,反射拿不到值。这在旧版本里可能输出 null 但不报错,7.0 对访问权限的校验更严,会直接抛JRException。我踩过之后形成习惯:所有要进报表的 JavaBean,统一走 public getter,不让模板字段直接摸内部状态。另外,JRBeanCollectionDataSource不要求 JavaBean 是非内部类,但要求类是 public 的。
4.2 JSON 数据源:路径表达式与数字精度陷阱
现在很多报表数据是从接口下发的 JSON 拿到的,7.0 里用 JSON 数据源的思路和 JavaBean 完全不同。它使用 JsonPath 语法定位数据行,并需要额外的jasperreports-json模块。典型用法:
import net.sf.jasperreports.engine.data.JsonDataSource; String json = "{ \"orders\": [ {\"id\":1,\"customer\":\"张三\",\"amount\":99.9}, " + "{\"id\":2,\"customer\":\"李四\",\"amount\":120.5} ] }"; JsonDataSource dataSource = new JsonDataSource(new ByteArrayInputStream(json.getBytes("UTF-8"))); dataSource.setSelectExpression("$.orders[*]");setSelectExpression指定从 JSON 的哪个节点开始遍历行。$.orders[*]表示取出orders数组里的每一个对象作为一行;如果 JSON 本身就是数组,可以写$.[*]或者直接不设置。填到字段时要注意,JSON 里的数字类型默认被解析成Double,如果你在 JRXML 里定义的字段类型是java.lang.Integer或者java.math.BigDecimal,填充时会报类型转换异常。解决方法是两个:一是把模板字段类型对应改成java.lang.Double,在表达式里做一次转换;二是给JsonDataSource设置setNumberPattern("#,##0.00"),让它按你定义的模式解析数字。
另一个容易忽略的是日期字符串的解析。JSON 里常见的"2024-06-17 10:30:00"默认不会被识别成日期,需要在数据源上设setDatePattern("yyyy-MM-dd HH:mm:ss")。设置好后,JRXML 里字段类型定义为java.util.Date就能直接格式化输出。如果模板里日期显示成了「2024-06-17 10:30:00.0」这样的带毫秒字符串,多半就是没设日期解析模式,把它当成纯文本输出了。
4.3 CSV 和 JDBC:老牌适配器在 7.0 下的使用侧重
CSV 数据源适合快速处理从业务系统导出的文本表格。CSVDataSource的构造逻辑是:第一行作为列头,后续行作为数据记录。列头的命名会直接映射到 JRXML 的字段名。编码问题在中文环境下很常见:很多业务系统导出的 CSV 是 GBK 编码,而报表引擎默认按 UTF-8 解析,结果第一行列头是中文时直接乱码。处理方式是在构造数据源时明确指定字符集:
import net.sf.jasperreports.engine.data.CSVDataSource; import java.io.FileInputStream; CSVDataSource dataSource = new CSVDataSource( new FileInputStream("data/report.csv"), "UTF-8", true);CSVDataSource第二个参数指定字符集,第三个参数true表示首行是列头。如果你的 CSV 没有列头,这里可以直接用索引访问列,需要在 JRXML 里通过field的property属性或者自定义数据来源表达式指定。对比来说,CSV 数据源是弱类型,所有列默认都是字符串,进入模板后按字段类型做转换,所以 Excel 里存的长数字超过 15 位会被抹精度,这种问题是源头数据问题,在报表层无法修正。
JDBC 数据源是这四种里唯一直接连数据库的。用法是传入一个java.sql.Connection:
Connection conn = DriverManager.getConnection(url, user, pass); JasperPrint jasperPrint = JasperFillManager.fillReport(jasperReport, params, conn); conn.close();注意fillReport完成后要自己关闭连接,引擎不会代劳。如果报表里使用 SQL 查询,JRXML 的<queryString>里写 SQL,字段名要和查询返回的列别名一致。另一个细节是 JDBC 驱动加载:7.0 在高版本 JDK 下不再自动扫描META-INF/services/java.sql.Driver外的驱动,建议使用Class.forName("com.mysql.cj.jdbc.Driver")显式加载,否则拿到 null 连接。
四种数据源的选型,我用一个表格总结适用场景:
| 数据源 | 适用场景 | 注意点 | 对应模块 |
|---|---|---|---|
| JavaBean 集合 | 数据已在内存中的 Spring 服务 | getter 命名与大小写 | 核心包 |
| JSON | 接口返回的嵌套数据结构 | 数字精度、日期模式 | jasperreports-json |
| CSV | Excel 导出的文本表格 | 编码、列头映射 | jasperreports-csv |
| JDBC | 报表直查数据库 | 连接管理、SQL 列别名 | 核心包 |
4.4 参数传递与子报表传参的隐蔽细节
报表参数通过Map<String, Object>传入,模板中用$P{paramName}引用。参数可以在三个场景发挥作用:作为 SQL 查询条件、作为文本字段输出、作为子报表输入。最常出问题的是参数类型不一致:模板中声明的类型是java.lang.String,你传入的却是 BigDecimal,填充时不报错但输出为 null。
子报表是把主报表的一部分拆到另一个 JRXML 文件里。主报表通过<subreport>元素引用子报表文件,并传递参数和数据源。这里有个非常重要的细节:子报表的数据源默认不是自动继承主报表数据源的,需要显式传入。常用做法:
<subreport> <reportElement x="0" y="0" width="200" height="30"/> <subreportExpression><![CDATA[$P{SUBREPORT_DIR} + "detail_sub.jasper"]]></subreportExpression> <dataSourceExpression><![CDATA[new net.sf.jasperreports.engine.data.JRBeanCollectionDataSource($F{items})]]></dataSourceExpression> </subreport>这个表达式的含义是:SUBREPORT_DIR是主报表传入的参数,值为子报表模板所在目录;dataSourceExpression中$F{items}是当前行中的一个字段,假设它是 List 类型,就用它现场构造一个子数据源。逻辑上是把主报表一行数据里的集合字段作为子报表的多行数据渲染。注意subreportExpression的结果必须是.jasper编译后的文件路径,不是.jrxml。
关于参数传递我说一个最常被忽略的:参数名大小写敏感。主报表传params.put("SUBREPORT_DIR", "/tmp"),模板引用$P{subreport_dir},两个名字不一致,引擎不会提醒,只在渲染时静默输出空。我处理这种问题的方法是固定命名规范:报表参数统一大写加下划线,并在接口文档里明确写出,不允许同义不同形。
5. 迁移到 7.0 的避坑清单:五个让我返工过的现场
5.1 编译期报 ClassNotFound 或方法被移除
现象:compileReport时抛出ClassNotFoundException: net.sf.jasperreports.engine.JRField,或者调用JasperFillManager.fillReport时报NoSuchMethodError。
原因:依赖树里混着不同版本的 jasperreports 模块。最常见的是项目直接引了 7.0.0 核心包,但某个传递依赖把 6.x 的 jasperreports 带进来了,classpath 里先加载了旧类。7.0 移除了部分旧接口,旧接口的方法签名自然对不上。
解决:用mvn dependency:tree找到重复引入的位置,在对应依赖上加<exclusion>排除老版本。排完之后在 IDE 里刷新依赖,再重新编译。如果项目里用的是 Gradle,则需要用./gradlew dependencyInsight --dependency jasperreports定位并处理同样的问题。
5.2 导出的 PDF 中文变方块
现象:PDF 能正常生成,但所有中文字符变成一个个空心方块或者乱码,英文和数字正常。
原因:PDF 导出基于字体驱动的绘制,默认字体不支持中文字形。报表引擎没有自动注册系统字体的机制,7.0 在字体缺失时不再静默降级,而是使用一个无中文字形的默认字体。
解决:配置字体扩展。在 classpath 下放一个jasperreports_extension.properties文件,注册字体族 XML 的位置,然后在 JRXML 的文本字段上指定fontName="SimSun"这类实际存在的中文字体名。我一般把字体文件打包进项目资源目录,通过字体扩展按名称引用,而不是直接依赖操作系统是否安装了该字体。配置完成后,先用第 2.3 节的编译脚本验证字体名能被解析,再导出 PDF 看效果。
5.3 JSON 里的数字变成 Double 后填不进 Integer 字段
现象:用 JSON 数据源跑报表,模板字段类型定义成java.lang.Integer,fillReport时抛net.sf.jasperreports.engine.JRException: Unable to get value for field,堆栈里能看到ClassCastException: Double cannot be cast to Integer。
原因:JSON 数据源对 JSON 数字的默认映射是 Double,JSON 里写99和写99.0解析出来都是 Double 对象。
解决:两个方向。一是改模板字段类型,把class属性改成java.lang.Double,并在输出表达式里按需转换成整数;二是在构造JsonDataSource时设置setNumberPattern("#"),让解析器按整数模式读取。我更推荐第一种,因为 numberPattern 影响的是格式化输出,并不影响底层类型转换。如果只是显示,直接把文本字段表达式写成new Integer($F{amount}.intValue())也可以,但注意空值判断。
5.4 Excel 导出行数被截断
现象:同一张报表导出 PDF 时 100 行显示正常,导出 XLSX 时只出了 50 行,后面内容消失,也没有报错。
原因:XLSX 导出走的是表格模型,严格按列宽和行高撑页,不会像 PDF 那样按 detail band 高度自动分页。当模板里detailband 高度设置过小,或者某些单元格开了stretchWithOverflow属性导致内容溢出时,导出器在计算可见行时出现偏差。
解决:在 JRXML 的根节点上检查是否设置了net.sf.jasperreports.export.xls.maximum.rows.per.sheet这个属性,7.0 里默认值受 Excel 单表 1048576 行上限约束。如果业务数据确实很大,设置分 sheet 输出的属性:<property name="net.sf.jasperreports.export.xls.break.after.write" value="true"/>,让超出部分写到下一个工作表。遇到截断先确认是不是这个属性没配,再看模板 band 高度是否合理。
5.5 子报表打成 jar 后找不到 jasper 文件
现象:本地开发时子报表渲染正常,部署成 jar 包后运行,日志报FileNotFoundException: detail_sub.jasper,但 jar 包内确实有这个文件。
原因:代码或模板中用了new File("detail_sub.jasper")这种相对路径解析方式,它依赖当前工作目录。jar 包内的资源不是文件系统里的普通文件,无法用 File 定位。
解决:改用 classpath 资源加载。在 Java 侧先通过getResourceAsStream读出子报表字节流,再交给引擎加载:
InputStream subReportStream = getClass().getResourceAsStream("/reports/detail_sub.jasper"); JasperReport subReport = (JasperReport) JRLoader.loadObject(subReportStream); params.put("detailSubReport", subReport);主报表的subreportExpression改为引用参数$P{detailSubReport},而不是拼文件路径。这里的关键是:编译后的.jasper对象是可序列化的,通过流加载相当于把子报表模板提前装入内存,彻底绕开文件系统路径问题。我后面做的项目里,所有子报表统一走这个方式,再没出现过打包后找不到模板的问题。
6. 收尾:从「能出图」到「稳定交付」的验证习惯
报表功能交付前,我最依赖的一个技巧是用JRPdfExporter配合导出监听器做结构校验,而不是只看 PDF 能不能打开。监听器可以在每个页面导出完成后捕获页面的元数据,比如页面大小、页面上的元素数量,用来发现内容溢出或空白页。实际代码是这样:
import net.sf.jasperreports.export.*; import net.sf.jasperreports.export.ExporterListener; SimplePdfReportConfiguration pdfConfig = new SimplePdfReportConfiguration(); pdfConfig.setForceLineBreakPolicy(false); JRPdfExporter exporter = new JRPdfExporter(); exporter.setExporterInput(new SimpleExporterInput(jasperPrint)); exporter.setExporterOutput(new SimpleOutputStreamExporterOutput("output/check.pdf")); exporter.setConfiguration(pdfConfig); exporter.addExporterListener(new ExporterListener() { @Override public void afterExportFinish(Exporter exporter) { System.out.println("导出完成"); } }); exporter.exportReport();逻辑说明:JRPdfExporter是底层导出器,比JasperExportManager给你更多控制点。pdfConfig.setForceLineBreakPolicy(false)是关闭强制换行策略,某些文本内容超宽时它默认会硬换行,关了之后能更真实地暴露排版溢出问题。监听器里的afterExportFinish是最终出口,可以在这里追加对导出文件的二次校验,比如判断文件大小是否过小、页数是否为零。
我现在的习惯是:每次改完模板,先跑一行命令导出 PDF,然后固定检查三样东西——中文是否有方块、小数位是否符合业务规则、分页是否把一条记录切成了两页。这个习惯源于一次教训:模板在本地导出完美,发版后客户说打印出来全是乱码,原因是客户机器上没装模板里指定的字体,而我只验证了屏幕显示,没验证字体嵌入。后来我把字体打包进项目资源,每次导出后都会用 pdfbox 这类工具检查字体是否嵌入,确认嵌入成功再走交付流程。报表引擎本身不难,难的是从「能出图」到「每一次出的图都没问题」的稳定感。希望这些经验能帮你在 7.0.0 这条路上少踩几个坑,把时间花在真正有价值的报表设计上。
本文还有配套的精品资源,点击获取