JasperReport 中踩过的坑

Mac Book Pro 10.13.6
Jaspersoft Studio community version 6.6.9
JDK 8

安装 Jaspersoft Studio

Jasper Report 分为专业版(收费)和社区版(免费),如果只是用来设计一些 基本的报表模板,社区版就足够了。从这里可以下载,选择 Jaspersoft Studio。

jaspersoft-studio.png

安装时可能遇到的问题

如果在安装时一切顺利,那么可以直接跳过本小节。

install-jasper-studio.png

如果安装过程中遇到如上错误,可以尝试一下 解决办法:

  • Step 1

System Preferences -> Security & Privacy -> General

security-privacy.png
  • Step 2

如果 Step 1 不能解决问题,打开 Terminal,执行如下命令:

# sudo bash
# xattr -cr "/Applications/TIBCO Jaspersoft Studio 6.6.0.app"

如果你对这个 xattr 感到好奇,可以执行 man xattr 读一读解释。

理解 Report Band

Jasper Studio 内置了很多种 Band,不同的 Band 有不同的用途,也有些不同默认行为。除了 Background Band,其他的 Band 的总高度要小于或等于一页的最大高度减去顶部和底部的边距。

Title

默认只会出现在第一页的最顶部。也可以像下面这样控制它显示在单独的一页上。

title-on-new-page.png

Page Header

页眉默认会出现在每一页的顶部。当 Title 和 Summary Band 设置为显示在单独的一页上时,页眉则不会显示出来。

Detail

Detail Band 比较神奇,它能够自动的迭代 DataSource 中的所有元素,如果 DataSource 为 null,则只有静态文本会显示出来。默认情况下,Main Dataset 的 Query 有多少条记录,Detail Band 就会重复多少遍。

更加详细一点的解释看这里

Page Footer

页脚默认会出现在每一页的底部。

Last Page Footer

如果想在最后一页的页脚上放一些不同的内容,就可以考虑使用这个 Band。

Summary

默认会显示在报表的最后,和 Title Band 一样,也可以设置为显示在单独的一页上。如果是基于统计的报表,一般会放一些计算出来的内容,比如:最大值,最小值,平均值,总值等。当然,根据实际需要,也可以放任意内容。

summary-on-new-page.png

Background

顾名思义,这个一般就是放报表的背景的,比如水印之类的。它的最大高度可以和报表一页的高度相同。

其他的 Band

Column Header, Group Header, Group Footer, Column Footer, No Data 这些 Band 根据名字也比较容易理解,具体的解释在这篇文章中讲解的比较详细。

全局设置

在页面的全局设置中可以设置页面的大小,边距,尺寸单位,页面方向等。

page-format.png

一个小建议是,如果设计稿是什么计量单位,这里就设置成什么计量单位,然后把尺寸设置的和设计稿一样,避免在设计模板的时候做繁琐的尺寸换算。

关于页面边距需要注意的一点是,即使将上下左右都设置为 0,最终导出 PDF 打印出来,边距也不见得是 0,因为打印机的设置中可能还会有默认的边距设置。

printer-settings.png

页码

在报表中,几乎都会有显示页码的需求,比如 Page x of y 这种。看似很简单,并且 Jasper Studio 中已经提供预设好的组件。

page-x-of-y.png

这个内置的组件是通过两个 Text Field 实现的,如果对对齐的精度要求不高,直接使用这个组件也是挺方便的。其原理就是使用了两次 $V{PAGE_NUMBER} 变量,只是设置不同的求值时机即可。对于每一页的页码设置 Evaluation TimeNow,对于总页码设置为 Report

但是如果对精度要求非常高的话,这个方式就不太合适了,我们需要用一个 Text Field 来实现,具体的做法参考这里

设置中英文为不同的字体(Font Set)

相信用 Word 软件进行过中英文排版的可能应对过这种需求,就是对一篇文章中的中英文需要设置不同 的字体,在 Word 中是有这个菜单项可以设置的,但是在 Jasper Report 中要实现这个需求就不那么简单了。

主要涉及到下面这些步骤:

创建 Font Set

创建 Font ExtensionFont Set 的步骤比较直观,参考官方文档即可。

在 Java 代码中引用字体文件

如果服务器所在的系统中没有相应的字体,在 Jasper Report 尝试生成 PDF 的时候就会报错。需要以下几步来解决:

  1. 首先需要从 Jasper Studio 中导出包含字体的 Jar 包,一次点击 Window > Preferences > Jaspersoft Studio > Fonts,选中创建的 Font Set 和其包含的字体并点击 Export

  2. 在导出 Font 到 Jar 包的对话框中写上名称并保存。比如 SampleFontSet.jar。这个 Jar 包和一般普通的 Jar 包不同,它包含了一些 Jaspersoft 需要的额外信息。

  3. 在代码依赖中引入字体 Jar 包。

jasper-font-set.png

将字体嵌入到 PDF 中

有人可能会怀疑将报表中用到的字体嵌入到导出的 PDF 中是否会带来性能问题,毕竟一般汉字的字体文件体积都比较大,大几十兆。在用 Japser 导出 PDF 的时候,只会嵌入用到的字体文件的子集,所以 几乎不用担心导出的 PDF 的体积。

测量 Text Field 的实际宽度(使用 Font Set )

如果在 Text Field 上设置的字体是自定义的 Font Set,那么需要注意了,不是那么容易能够测量出真实的宽度。目前的解决办法是先分离出文本中的汉字字符和非汉字字符,然后分别设置成相应的字体,再各自测量出宽度,最后相加即为文本所占的实际宽度。

  • 分离汉字字符和非汉字字符
    private static Pair<String, String> splitHanAndOthers(String str) {
        StringBuilder hanToken = new StringBuilder();
        StringBuilder othersToken = new StringBuilder();

        for (int i = 0; i < str.length(); ) {
            final int codePoint = str.codePointAt(i);

            if (UnicodeScript.of(codePoint) == UnicodeScript.HAN) {
                hanToken.append(str.charAt(i));
            } else {
                othersToken.append(str.charAt(i));
            }
            i += Character.charCount(codePoint);
        }

        return Pair.of(hanToken.toString(), othersToken.toString());
    }
  • 测量文本的实际宽度
    private static float getWidth(JRPrintText jrPrintText, String text, String fontName) {
        final DefaultJasperReportsContext instance = DefaultJasperReportsContext.getInstance();

        final JRDefaultStyleProvider defaultStyleProvider = jrPrintText.getDefaultStyleProvider();
        JRPrintText printText = new JRBasePrintText(defaultStyleProvider);
        printText.setText(text);
        printText.setFontName(fontName);
        printText.setFontSize(jrPrintText.getFontsize());

        final JRStyledText hanTextFullStyledText = printText.getFullStyledText(JRStyledTextAttributeSelector.getAllSelector(instance));
        final JRTextMeasurer measure = JRTextMeasurerUtil.getInstance(instance).createTextMeasurer(jrPrintText);
        final JRMeasuredText measuredText = measure.measure(hanTextFullStyledText, 0, 0, false);
        return measuredText.getTextWidth();
    }
  • 计算总宽度
    private float getTotalWidth(JRPrintText jrPrintText, String text) {
        final Pair<String, String> hanAndOthers = splitHanAndOthers(text);
        final String han = hanAndOthers.getLeft();
        final String others = hanAndOthers.getRight();

        final float hanWidth = getWidth(jrPrintText, han, "JianSong");
        final float othersWidth = getWidth(jrPrintText, others, "CorporateS");
        return hanWidth + othersWidth;
    }

Image

显示图片

  • 路径问题

在 Jasper Studio 中添加一个 Image 组件,正确的设置一个图片的路径,点击预览就可以看到显示出来的图片了。但是细心的同学会发现在 .jrxml 模板文件中图片的路径是一个绝对路径,这在实际生产过程中肯定是不行的。

因此,通过搜索找出了一个方法,可以解决静态图片的路径问题。在 Image 组件的表达式中写上这样的表达式:

this.getClass().getResourceAsStream("/images/logo.png")
image-expression.png

还有几种其他的场景可能直接写相对路径也能工作,我没有验证过。

  • 传参问题

其实,Image 组件的表达式中不仅支持写图片的路径,也可以在代码中将图片读入内存然后直接以 InputStream 的形式传递,不过要确保每个 Stream 只能被消费一次,不要复用同一个 InputStream,如果想要复用,考虑使用 Image 组件的 isUsingCache 属性。不过 Jasper 支持的类型除此之外,还有好几种:

* java.lang.String
* java.io.File
* java.net.URL
* java.io.InputStream
* java.awt.Image
* net.sf.jasperreports.engine.JRRenderable

例子可以参考这个

使用 InputStream 时可能遇到的问题

net.sf.jasperreports.engine.JRException: java.io.IOException: The byte array is not a recognized imageformat.
      at net.sf.jasperreports.engine.export.JRPdfExporter$InternalImageProcessor.processImageRetainShape(JRPdfExporter.java:1747)
      at net.sf.jasperreports.engine.export.JRPdfExporter$InternalImageProcessor.process(JRPdfExporter.java:1604)
      at net.sf.jasperreports.engine.export.JRPdfExporter$InternalImageProcessor.access$300(JRPdfExporter.java:1532)
      at net.sf.jasperreports.engine.export.JRPdfExporter.exportImage(JRPdfExporter.java:1472)
      at net.sf.jasperreports.engine.export.JRPdfExporter.exportElements(JRPdfExporter.java:1090)
      at net.sf.jasperreports.engine.export.JRPdfExporter.exportFrame(JRPdfExporter.java:3117)
      at net.sf.jasperreports.engine.export.JRPdfExporter.exportElements(JRPdfExporter.java:1098)
      at net.sf.jasperreports.engine.export.JRPdfExporter.exportPage(JRPdfExporter.java:1053)
      at net.sf.jasperreports.engine.export.JRPdfExporter.exportReportToStream(JRPdfExporter.java:917)
      at net.sf.jasperreports.engine.export.JRPdfExporter.exportReport(JRPdfExporter.java:537)
      at net.sf.jasperreports.engine.JasperExportManager.exportToPdfFile(JasperExportManager.java:155)
      at net.sf.jasperreports.engine.JasperExportManager.exportReportToPdfFile(JasperExportManager.java:503)
      at com.finger.hr.controllers.HRCreatePDFController$2.run(HRCreatePDFController.java:244)
Caused by: java.io.IOException: The byte array is not a recognized imageformat.
      at com.lowagie.text.Image.getInstance(Unknown Source)
      at net.sf.jasperreports.engine.export.JRPdfExporter$InternalImageProcessor.processImageRetainShape(JRPdfExporter.java:1742)
      ... 12 more

解决办法很奇特,到现在我也不知道为什么。

如果传递的参数是一个独立的 InputStream 类型的字段,就会有上述错误。 解决办法就是把这个单独的 字段包到一个 Java 对象中(:D)。

@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class PhotoDto {
    private InputStream photo;
}

List

List 显示行号

不管是 List 还是 Table 如果只是想显示简单的行号,最简单的办法就是使用 $V{REPORT_COUNT}。 如果对行号还有一些比较复杂的计算逻辑,则最好在代码中处理好,然后作为数据的一个字段传递给 Jasper 直接渲染。

_THIS 大法

如果传递给 List 的(或者是 Table)数据是一个基本类型(这里姑且把 String 也看做基本类型,其实只要是不包含属性的对象都满足条件)的数据的集合,那么在引用单个数据的时候就得注意了,因为基本类型的数据不存在字段一说,所以需要在 Field 的命名上做点文章。

比如说 dataSource 是 List<String>,那么在定义 Field 的时候就没办引用对象的字段名了,这时候我们只需要将字段名定义为 _THIS 即可,在引用的地方写 $F{_THIS}。更多解释参考这里

Table

Dataset

Table 嵌套中的坑

Table 中嵌套 Table 可以实现类似合并单元格的效果,但是如果最外层的 Table 中一行的高度超过了页面的最大高度就会直接报错,而不是分页。

Table 中实现分组效果

  • Merge Cell?impossible!

在 Table 组件中不能直接做合并单元格的操作,只能通过嵌套 Table 来实现类似的效果。

  • Table Cell 中的负数缩进

在使用 Jasper Studio 6.6.0 版的设计器的时候,在界面上设置坐标值的时候,会出现保存不成功的情况。只能通过直接修改模板源文件的方式设置负数坐标。感觉是 IDE 的 Bug,在后续版本应该修复了。

设置 right indent 为负数的时候,left indent 不能为 0,如果为 0 right indent 不会生效 :( (至今不知道为什么 :boom:)

  • 利用 Table 组件的特性

如果有分组的需求,可以考虑使用 Table 的 Group Header 和 Group Footer 来显示组名和小计之类的数据。

Text Field & Static Text

  • Strech

关于 Strech,在设计器里有一个属性叫做 Strech With Overflow,当这个属性被勾选后,表示当数据的长度超过组件的长度时会自动折行,相当于垂直方向上的 Overflow,而 Jasper 也不支持水平方向的 Overflow。

  • Pattern

在 Jasper 中可以很方便的设置一些特殊文本的显示格式,比如: 日期,时间,货币,数字,百分比等。

pattern.png

SubReport

  • Compile 问题

在 Jasper Studio 的设计器里可以通过直接引用 *.jasper 文件的方式,在主报表中引用子报表。但是如果在实际的产品代码中这样做,非常不利于对源码的版本控制,因为这个 *.jasper 文件是一个经过编译之后的临时文件。

subreport-expression.png

我们在生产代码中可以先将子报表的模板预编译好,然后直接以参数的形式传递给主报表(例如如下代码将编译好的 “checklist.jrxml” 子报表作为参数传给主报表,然后在主报表中的引用表达式写成$P{checklist})。

    String subreport = "checklist";
    JasperReport jasperReport = null;
    try (InputStream inputStream = getResourceAsStream(subreport + ".jrxml")) {
        jasperReport = JasperCompileManager.compileReport(inputStream);
    } catch (IOException | JRException e) {
        e.printStackTrace();
    }
    params.put(subreport, jasperReport);

Detail Band 重复多次的问题

Detail Band 被渲染的次数是由 Main Dataset 中数据条数决定的,如果发现 Detail Band 被重复渲染了多次,那么请检查是否添加了 Main Dataset 的 Query,并且查询出来的数据不止一条。如果没有要设置 Main Dataset 的场景,可以选择 One Empty Record

页面的多列排版

在页面设置中的多列设置,只对 Detail Band,Column Header,Column Footer 有影响。

HTML Markup 的限制

对于 HTML Markup 的使用,其实没有想象中的那么美好,使用起来功能比较有限。只能支持文本格式相关的标签和属性,不支持布局相关的标签。所以想通过 CSS 实现 一些效果,基本上都行不通。

支持的 HTML 标签如下:

  • b
  • u
  • i
  • font
  • sup
  • sub
  • li
  • br

这个列表是从源码中 找出来的,并没有什么文档(也有可能是我没搜到)明确的列出来。 源码参考这里

Position Type 设置为 Float 中的误区

一开始我不是特别理解这个 Float Position Type 的原理,走了很多弯路,经过很长时间的摸索才理解 Float Position Type 的用法。其实主要是因为 Jaser Studio 设计器中显示出来的样子容易误导使用的人。

正常情况下,在设计模板的时候我们会把一些组件摆放的规规矩矩,整整齐齐的,看着都觉得舒服。Float 是相对于设计器里离它最近的元素漂移的,其相对位置在设计模板的阶段就决定好了。而有的组件在设计器里展示的形状比较大,如果要摆放整齐,需要 Float 的元素就会有比较大的相对距离出现。其实,这个时候我们不能相信我们眼睛看到的,也不能有强迫症,在设计器里。因为很多时候需要一些元素看上去是叠在一起的,渲染出来的效果反而是我们需要的。

position-type-float.png

我们在实际项目中有一个模板设计出来是这样的,是不是看着感觉很乱,但是要使用 Float 达到我们想要的效果,还必须要设计成这样。

时间的时区问题

解决报表中时区问题很简单,只需要给报表内置的一个变量传入相应的时区就行了。

Map<String, Object> params = new HashMap<>();
params.put("REPORT_TIME_ZONE", TimeZone.getTimeZone(ZoneId.of("Asia/Shanghai")));

通过代码定位元素坐标(终极大法)

如果在 Jasper Studio 中无法实现组件的动态定位,基本上也就只有这么一条路可以走了。如果这条路也实现不了,要么改需求,要么换掉 Jasper Report 解决方案。

参考链接

  1. https://community.jaspersoft.com/documentation/tibco-jaspersoft-studio-user-guide/v71/getting-started-jaspersoft-studio-0
  2. https://community.jaspersoft.com/documentation/tibco-jaspersoft-studio-user-guide/v630/understanding-bands
  3. https://community.jaspersoft.com/wiki/report-structure-jaspersoft-studio
  4. https://communities.ca.com/blogs/rob.ensinger/2015/12/28/how-to-single-textbox-page-x-of-y-page-counts-in-jaspersoft-reports
  5. https://stackoverflow.com/questions/10673263/show-page-x-of-y-using-a-single-text-field
  6. https://community.jaspersoft.com/documentation/tibco-jaspersoft-studio-user-guide/v630/working-font-extensions
  7. https://stackoverflow.com/questions/3623420/image-expression-url-in-jasper-reports
  8. https://stackoverflow.com/questions/11949333/passing-the-list-of-primitive-type-objects-as-datasource-for-subreport
  9. https://stackoverflow.com/questions/27903072/print-liststring-each-element-in-new-field
  10. https://stackoverflow.com/questions/11949333/passing-the-list-of-primitive-type-objects-as-datasource-for-subreport
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,607评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,047评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,496评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,405评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,400评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,479评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,883评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,535评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,743评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,544评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,612评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,309评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,881评论 3 306
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,891评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,136评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,783评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,316评论 2 342

推荐阅读更多精彩内容

  • 问答题47 /72 常见浏览器兼容性问题与解决方案? 参考答案 (1)浏览器兼容问题一:不同浏览器的标签默认的外补...
    _Yfling阅读 13,722评论 1 92
  • 1、通过CocoaPods安装项目名称项目信息 AFNetworking网络请求组件 FMDB本地数据库组件 SD...
    X先生_未知数的X阅读 15,967评论 3 119
  • 我来到你的城市,走过你来时的路。 好喜欢陈奕迅的好久不见啊!虽然已经算是老歌了!但还是百听不厌。 那天看到你微笑,...
    子陌晨曦阅读 363评论 2 2
  • skfdsjfsfkds;fjsdkldfjdsfs
    林奇思妙想阅读 124评论 0 0
  • Is it now?(现在是现在吗?)这个听起来有点病句的问句,你是不是偶尔也会问起。也许是在某个午夜梦回,也许在...
    卢小五阅读 1,120评论 0 1