如果您使用Java进行编程的时间足够长,则有可能需要为业务用户生成报告。 就我而言,我已经看到几个项目使用JasperReports®库来生成PDF和其他文件格式的报告。 最近,我荣幸地观察了Mike和他的团队使用上述报告库及其面临的挑战。
简而言之JasperReports
简而言之,使用JasperReports(JR)生成报告涉及三个步骤:
- 加载已编译的报告(即,加载
JasperReport
对象) - 通过用数据填充报告来运行报告(结果是
JasperPrint
对象) - 将填充的报告导出到文件(例如,使用
JRPdfExporter
导出到PDF)
在Java代码中,看起来像这样。
JasperReport compiledReport = JasperCompileManager.compileReport("sample.jrxml");
Map<String, Object> parameters = ...;
java.sql.Connection connection = dataSource.getConnection();
try {JasperPrint filledReport = JasperFillManager.fillReport(compiledReport, parameters, connection);JasperExportManager.exportReportToPdf(filledReport, "report.pdf");
} finally {connection.close();
}
多亏了facade类,这看起来很简单。 但是外表可能是骗人的!
鉴于以上代码段(以及概述的三个步骤),您认为哪些部分需要最多的时间和内存? (听起来像面试问题)。
如果您回答(#2)填写数据,那是对的! 如果您回答了#3,那也是正确的,因为#3与#2成正比。
恕我直言 ,大多数在线教程仅显示简单的部分。 就JR而言 ,似乎缺少对较困难和棘手的部分的讨论。 在这里,与Mike的团队一起,我们遇到了两个困难:内存不足错误和长期运行的报告。 使这些困难特别令人难忘的是,它们仅在生产过程中出现(而不在开发过程中)。 我希望通过共享它们,将来可以避免它们。
内存不足错误
第一个挑战是报告内存不足。 在开发过程中,与实际操作数据相比,我们用于运行报告的测试数据将太小。 因此, 为此设计 。
在我们的例子中,所有报告都使用JRVirtualizer
运行。 这样,当达到内存中的页面/对象最大数量时,它将刷新到磁盘/文件。
在此过程中,我们还了解到虚拟器需要清理。 否则,周围会有几个临时文件。 而且,只有在报告导出到文件后 ,我们才能清理这些临时文件。
Map<String, Object> parameters = ...;
JRVirtualizer virtualizer = new JRFileVirtualizer(100);
try {parameters.put(JRParameter.REPORT_VIRTUALIZER, virtualizer);...... filledReport = JasperFillManager.fillReport(compiledReport, parameters, ...);// cannot cleanup virtualizer at this pointJasperExportManager.exportReportToPdf(filledReport, ...);
} finally {virtualizer.cleanup();
}
有关更多信息,请参见Virtualizer Sample – JasperReports 。
请注意,当我们在运行报表时遇到内存不足的错误时,JR 并不总是罪魁祸首。 有时,即使在使用JR之前,我们也会遇到内存不足错误。 我们看到了如何滥用JPA来加载报告的整个数据集( Query.getResultList()
和TypedQuery.getResultList()
)。 同样,由于数据集仍然很小,因此在开发期间不会显示该错误。 但是,当数据集太大而无法容纳在内存中时,我们会遇到内存不足错误。 我们选择避免使用JPA生成报告。 我猜我们只需要等待JPA 2.2的Query.getResultStream()
可用即可。 我希望JPA的Query.getResultList()
返回Iterable
。 这样,就有可能一次映射一个实体,而不是整个结果集。
现在,避免加载整个数据集。 一次加载一个记录。 在此过程中,我们返回了良好的JDBC。 不错,JR很好地使用了ResultSet
。
长期运行的报告
第二个挑战是长期运行报告。 同样,在开发过程中可能不会发生这种情况。 充其量,将运行10秒钟左右的报告视为冗长。 但是,有了实际的运行数据,它可以运行大约5-10分钟。 当根据HTTP请求生成报告时,这尤其麻烦。 如果报告可以在超时时间段内(通常为60秒或最多5分钟)开始写入响应输出流,那么它很有可能被请求用户(通常是通过浏览器)接收。 但是,如果填写报告需要5分钟以上的时间,而导出到文件又需要8分钟,那么用户将只会看到超时的HTTP请求,并将其记录为错误。 听起来有点熟?
请记住,报告可能会运行几分钟。 因此, 为此设计 。
就我们而言,我们在单独的线程上启动报告。 对于通过HTTP请求触发的报告,我们将以一个页面进行响应,该页面包含指向所生成报告的链接。 这样可以避免超时问题。 当用户单击此链接而报告尚未完成时,他/她将看到仍在生成报告。 但完成的报告时,他/她就可以看到生成的报告文件。
ExecutorService executorService = ...;
... = executorService.submit(() -> {Map<String, Object> parameters = ...;try {...... filledReport = JasperFillManager.fillReport(compiledReport, parameters, ...);JasperExportManager.exportReportToPdf(filledReport, ...);} finally {...}
});
我们还必须添加停止/取消运行报告的功能。 好东西,JR有检查Thread.interrupted()
代码。 因此,仅中断线程将使其停止。 当然,您需要编写一些测试来进行验证(期望JRFillInterruptedException
和ExportInterruptedException
)。
在讨论过程中,我们重新发现了将“监听器”添加到报告生成中的方法(例如FillListener
和JRExportProgressMonitor
)并为用户提供一些进度信息。
我们还创建了实用程序测试类,以通过反复重复给定的数据来生成大量数据。 这对于帮助团队的其他成员开发专为处理长期运行和内存不足错误而设计的JR应用程序很有用。
进一步的设计考虑
要考虑的另一件事是填写报告时需要打开和关闭所需的资源。 这可以是JDBC连接,Hibernate会话,JPA EntityManager
或文件输入流(例如CSV,XML)。 下图是我的设计注意事项的粗略草图。
1. Compiling- - - - - - - - - - - - - -\- - - -\ \
2. Filling > open-close \- - - -/ resource > swap to file/
3. Exporting /- - - - - - - - - - - - - -/
我们要隔离#2并定义装饰器,这些装饰器将打开资源,填充报告并在finally
块中关闭打开的资源。 打开的资源可能取决于报表中的<queryString>
元素(如果存在)。 在某些情况下,如果没有<queryString>
元素,则可能无需打开资源。
<queryString language="hql"><![CDATA[ ... ]]>
</queryString>
...
<queryString language="csv"><![CDATA[ ... ]]>
</queryString>
此外,我们还希望将#2和#3组合为一种抽象。 这种单一的抽象使您可以更轻松地进行增强装饰,例如将创建的页面对象刷新为文件,并在导出过程中将其加载回。 如前所述,这就是JRVirtualizer
所做的。 但是我们希望使用结合#2和#3的抽象对对象透明的设计。
致谢
目前为止就这样了。 再次感谢Mike和他的团队分享了他们的经验。 是的,他是将自己应用的收入捐赠给慈善机构的那个人 。 另外,还要感谢克莱尔(Claire)通过一次又一次重复给定数据进行测试的想法。 相关代码段可以在GitHub上找到 。
翻译自: https://www.javacodegeeks.com/2018/01/jasperreports-tricky-parts.html