1. 项目概述:为什么要在JasperReport报表中处理图片?
在开发企业级报表时,我们经常遇到一个看似简单却暗藏玄机的需求:在生成的PDF报表中插入并正确显示图片。无论是公司的品牌Logo、产品的实物照片、用户的签名图像,还是动态生成的二维码、条形码,图片都是丰富报表内容、传递关键信息不可或缺的元素。JasperReport作为一款成熟的开源Java报表引擎,虽然功能强大,但在处理图片资源时,如果配置不当,很容易出现图片不显示、位置错乱、内存溢出等问题,让开发者头疼不已。
我接手过不少从其他报表工具迁移到JasperReport的项目,发现“图片显示”是高频踩坑区。很多开发者习惯在Jaspersoft Studio设计器中拖入一个图片组件,设置好路径,预览时一切正常,但一旦部署到生产环境,集成到Web应用或后台服务中,生成的PDF里就只剩下一个难看的红色叉叉或者一片空白。这背后的原因,往往是对JasperReport加载图片资源的机制理解不够深入。图片的存储位置(是嵌入报表文件、存放在类路径、服务器本地磁盘还是远程网络)、图片的编码格式(Base64嵌入还是文件引用)、以及运行时环境的资源查找策略,共同决定了最终PDF中图片的命运。
因此,掌握在JasperReport中可靠地插入和显示图片的技术,不仅是完成一个功能点,更是保障报表服务稳定性和可维护性的关键。接下来,我将从设计思路、核心配置、动态加载到生产环境排查,为你完整拆解这个过程中的每一个技术细节和避坑指南。
2. 核心思路与方案选型:静态嵌入 vs. 动态加载
在JasperReport中插入图片,从技术实现上可以分为两大流派:静态嵌入和动态加载。选择哪种方案,直接取决于你的图片是否在报表设计时就已经确定,以及运行时是否需要动态变化。
2.1 静态嵌入方案:适用于固定不变的图片
静态嵌入,顾名思义,就是将图片数据直接“烧录”进报表模板文件(.jrxml或编译后的.jasper文件)中。这是最简单直接的方法。
实现原理与操作:在Jaspersoft Studio中,你从工具栏拖拽一个<image>组件到设计区域,在它的属性面板中,Expression通常设置为new javax.swing.ImageIcon("你的图片绝对或相对路径").getImage()。当你保存.jrxml文件时,Studio会读取该路径下的图片文件,将其编码(通常是PNG或JPEG格式的二进制流)并作为<imageExpression>的一个子元素<image>的<data>标签内容,以Base64或类似方式直接存储在XML中。编译后,这些数据会成为.jasper文件的一部分。
优点:
- 部署简单:报表模板是自包含的,无需担心运行时找不到图片文件。模板文件传到哪,图片就跟到哪。
- 显示可靠:只要模板能加载,图片就一定能显示,不受外部文件系统或网络环境影响。
缺点与注意事项:
- 模板臃肿:图片数据会使模板文件体积显著增大,尤其是高清图片。这可能会影响模板的加载和传输速度。
- 无法动态更新:如果想更换Logo,必须重新设计、编译并部署报表模板,灵活性差。
- 路径陷阱:在Studio中使用的绝对路径(如
C:\logo.png)在服务器上显然不存在。因此,务必使用相对于报表项目或模块的路径,并确保该路径在Studio和运行时环境(如你的Java项目)中都能被正确解析。更稳妥的做法是,将图片文件放在项目的src/main/resources目录下,然后使用类似“classpath:images/logo.png”的表达式(具体写法后续详解)。
实操心得:对于公司Logo、固定的水印、报表边框装饰等极少变动的图片,采用静态嵌入是明智的。但在操作时,我强烈建议在Jaspersoft Studio中,通过“Image → Choose from File”选择图片后,检查生成的
<imageExpression>代码。最佳实践是将其改为使用getClass().getResourceAsStream("/images/logo.png")这样的类路径引用方式,这样能最大程度保证设计期和运行期的一致性。
2.2 动态加载方案:应对灵活多变的图片需求
动态加载是更强大、更常用的方案。图片的路径或二进制数据作为参数(java.lang.String或java.io.InputStream)或字段(java.awt.Image或byte[])传递给报表填充引擎,由引擎在运行时动态设置。
实现原理:报表模板中的<image>组件,其Expression不再指向一个具体文件,而是指向一个参数(如$P{LOGO_IMAGE})或一个字段(如$F{PRODUCT_PIC})。在Java代码中,你通过JasperFillManager.fillReport()方法传入的parametersMap或数据源中的每条记录,来提供对应的图片数据。
根据图片来源,动态加载又细分为几种常见场景:
类路径(Classpath)图片:图片打包在JAR/WAR文件的资源目录中。这是Web应用中最常见的方式。
- Java代码示例:
Map<String, Object> parameters = new HashMap<>(); InputStream logoStream = getClass().getResourceAsStream("/static/images/company_logo.png"); parameters.put("LOGO_IMAGE", logoStream); // 参数类型为 java.io.InputStream // 注意:需要确保报表中Image Expression的Evaluation Time设置正确,通常为“Now”
- Java代码示例:
文件系统图片:图片存储在服务器的某个磁盘目录下。
- Java代码示例:
File logoFile = new File("/opt/app/uploads/logo.png"); parameters.put("LOGO_IMAGE", new FileInputStream(logoFile));
- Java代码示例:
数据库存储的图片(BLOB字段):图片以二进制形式存储在数据库。
- 操作要点:你的查询SQL需要返回包含图片二进制数据的字段。在报表中,将该字段(例如
$F{IMAGE_DATA})的类型设置为java.io.InputStream或byte[],并直接作为<imageExpression>的值。 - 报表字段配置:在Jaspersoft Studio的Dataset and Query对话框中,为该BLOB字段创建一个字段(Field),并将其
Class类型设置为java.io.InputStream。
- 操作要点:你的查询SQL需要返回包含图片二进制数据的字段。在报表中,将该字段(例如
网络图片(URL):图片来自远程服务器。
- Java代码示例:
String imageUrl = "https://example.com/product/123.jpg"; URL url = new URL(imageUrl); BufferedImage bufferedImage = ImageIO.read(url); parameters.put("PRODUCT_IMAGE", bufferedImage); // 参数类型为 java.awt.Image - 注意事项:网络请求存在超时、失败的风险,必须考虑异常处理和超时设置,否则可能导致报表生成线程阻塞。生产环境中,建议增加本地缓存或备用图片机制。
- Java代码示例:
优点:
- 高度灵活:图片内容可以随时更换,无需修改报表模板。
- 模板精简:模板文件很小,便于管理和版本控制。
- 资源集中管理:图片可以统一存放在数据库、文件服务器或CDN,便于维护和更新。
缺点与挑战:
- 运行时依赖:必须保证报表引擎在运行时能够成功获取到图片资源,这引入了外部依赖和潜在的失败点。
- 性能考量:动态加载,特别是网络加载,可能影响报表生成速度,需要合理设计缓存策略。
- 内存管理:处理大量或大尺寸图片时,需要注意
InputStream或byte[]的及时关闭,防止内存泄漏。
方案选型总结:
- 选择静态嵌入:当图片是报表固有部分、永不改变、且数量少体积小时。
- 选择动态加载:当图片需要根据业务数据变化(如用户头像、产品图)、需要集中管理、或图片体积较大时。绝大多数企业级应用场景都推荐使用动态加载。
3. 核心配置与设计器实操详解
理解了方案,我们进入实战环节。Jaspersoft Studio是设计报表的利器,图片组件的属性配置是成败的关键。很多问题都源于这里配置不当。
3.1 图片组件关键属性深度解析
在Studio中选中一个图片组件,查看其属性面板,以下几个属性必须了然于胸:
Evaluation Time(评估时间):这是最容易出错的属性之一。它决定了图片表达式何时被计算。
- Now:在填充报表的当前时刻立即计算。这是最常用的设置,适用于通过参数(
$P{})传入的图片,或者与当前数据带无关的固定图片。 - Band:在整个Band(如Detail Band)渲染完成后计算。很少用于图片。
- Page/Column/Report:在整页、整列或整个报表渲染完成后计算。绝对不要将动态图片(如来自数据库字段
$F{}的图片)设置为Report,否则所有记录都会显示最后一张图片。 - Group:在指定分组发生变化时计算。适用于按组显示不同图片的场景。
- Auto:引擎自动判断。不推荐,明确设置更可靠。
- 黄金法则:参数(Parameter)传图用
Now,字段(Field)传图用Now或Band(对于Detail Band中的字段,Now即可)。
- Now:在填充报表的当前时刻立即计算。这是最常用的设置,适用于通过参数(
Expression(表达式):图片数据的来源。这里是核心代码区。
- 对于静态嵌入(不推荐在表达式里写死路径),Studio会自动生成包含
<data>的XML。 - 对于动态加载,这里填写的是参数或字段名,例如
$P{LOGO_STREAM}或$F{USER_AVATAR}。表达式的结果必须是java.awt.Image,java.awt.image.BufferedImage,java.io.InputStream,byte[],javax.swing.ImageIcon, 或java.net.URL类型。
- 对于静态嵌入(不推荐在表达式里写死路径),Studio会自动生成包含
Image Type(图片类型):告诉引擎如何解释二进制数据。
- Jpeg/Png/Gif/Tiff:对应常见的图片格式。如果你传递的是
InputStream或byte[],且知道确切的格式,就选这个。 - Unknow:让引擎自动检测。这是最省事的选择,绝大多数情况下都能正确识别。对于从数据库BLOB字段或网络获取的、格式明确的图片流,我通常直接选
Unknow,让引擎去处理。
- Jpeg/Png/Gif/Tiff:对应常见的图片格式。如果你传递的是
On Error Type(错误处理类型):当图片加载失败时怎么办。
- Error:抛出异常,报表生成失败。适用于Logo等必须显示的图片。
- Blank:留空,什么都不显示。
- Icon:显示一个默认的错误图标(红色叉叉)。在调试阶段可以用这个,快速定位哪些图片出问题了。
- 生产环境建议:对于非关键性图片(如用户可选头像),可以设为
Blank,避免因个别图片缺失导致整个报表生成失败。对于关键图片,设为Error,并在Java代码层做好异常捕获和降级处理(例如,用一张默认图片替换)。
3.2 在Jaspersoft Studio中一步步配置动态图片
假设我们要在报表标题栏显示一个来自类路径的Logo,在明细行显示来自数据库BLOB字段的产品图片。
步骤一:准备报表模板和数据集
- 在Studio中创建新报表。
- 配置数据源(例如,一个返回
ID, NAME, IMAGE_DATA的数据库查询)。确保IMAGE_DATA字段的Class类型设置为java.io.InputStream。
步骤二:添加并配置标题Logo(参数传入)
- 从面板拖一个
Image组件到TitleBand。 - 在属性面板中,点击
Expression旁的“...”按钮。 - 在表达式编辑器中,输入
$P{REPORT_LOGO}。这意味着Logo将由一个名为REPORT_LOGO的参数提供。 - 设置
Evaluation Time为Now。 - 设置
On Error Type为Error(因为Logo很重要)。 - 调整图片位置和大小。
步骤三:添加并配置产品图片(字段传入)
- 从面板拖一个
Image组件到DetailBand。 - 在属性面板的
Expression中,输入$F{IMAGE_DATA}(即你查询中的BLOB字段名)。 - 设置
Evaluation Time为Now。 - 设置
Image Type为Unknow。 - 设置
On Error Type为Blank(避免因某张产品图损坏影响整个列表)。 - 调整图片位置和大小,你可能需要设置
Stretch Type为Clip或FillFrame来控制图片在框内的适应方式。
步骤四:定义参数(可选但推荐)虽然不定义参数也能运行,但定义参数可以让模板更清晰。在Outline视图的Parameters节点上右键,创建REPORT_LOGO参数,将其Class类型设置为java.io.InputStream。
步骤五:预览测试在Studio中预览前,需要设置参数。点击预览按钮,在参数输入对话框中,为REPORT_LOGO参数提供一个本地的测试图片文件。预览成功,说明模板配置正确。
避坑技巧:在Studio中预览使用字段(
$F{})的图片时,如果数据源是空的或者字段值为null,图片区域可能不显示任何内容,这是正常的。你可以先给字段一个测试值,或者确保你的测试数据源包含有效的图片数据。
4. 后端Java代码集成实战
设计好的模板需要在Java应用中运行起来。这里提供几种典型场景的完整代码示例和深度解析。
4.1 场景一:从类路径加载Logo并填充报表
这是Web应用中最标准的做法。假设你的Logo图片放在src/main/resources/report/logo.png。
import net.sf.jasperreports.engine.*; import java.io.InputStream; import java.util.HashMap; import java.util.Map; public class ReportService { public byte[] generateReport() throws JRException { // 1. 加载编译好的报表模板文件 (.jasper) // 通常也将模板文件放在类路径下,如 /reports/invoice.jasper InputStream reportTemplateStream = getClass().getResourceAsStream("/reports/invoice.jasper"); // 2. 准备报表参数Map Map<String, Object> parameters = new HashMap<>(); // 3. 关键步骤:加载类路径下的Logo图片,作为InputStream传入 InputStream logoStream = getClass().getResourceAsStream("/report/logo.png"); if (logoStream != null) { parameters.put("REPORT_LOGO", logoStream); // 参数名与模板中定义的$P{REPORT_LOGO}一致 } else { // 处理图片缺失的情况,可以记录日志,或传入一个默认的InputStream // 例如:parameters.put("REPORT_LOGO", getDefaultLogoStream()); throw new RuntimeException("Logo image not found in classpath."); } // 4. 准备数据源(这里用空数据源示例,实际会连接数据库) JRDataSource dataSource = new JREmptyDataSource(); // 5. 填充报表 JasperPrint jasperPrint = JasperFillManager.fillReport(reportTemplateStream, parameters, dataSource); // 6. 导出为PDF byte[] pdfBytes = JasperExportManager.exportReportToPdf(jasperPrint); // 7. 重要!关闭由我们打开的InputStream(模板流由JasperReport内部管理,通常不需手动关) if (logoStream != null) { try { logoStream.close(); } catch (IOException e) { // 记录日志 } } return pdfBytes; } }代码解析与注意事项:
- 资源管理:我们通过
getClass().getResourceAsStream()获取资源流。路径以/开头,表示从类路径根目录开始查找。这是与文件系统路径解耦的关键。 - 参数传递:我们将
InputStream对象直接放入参数Map。JasperReport引擎会在需要时读取这个流。 - 异常处理:必须考虑图片资源找不到的情况。在生产代码中,不应直接抛出
RuntimeException,而应记录错误日志,并可能返回一个包含错误信息的PDF或使用默认图片。 - 流关闭:我们创建的
logoStream需要手动关闭。虽然JasperReport在填充完成后可能会尝试关闭它,但依赖框架行为是不安全的,显式关闭是好习惯。注意,从fillReport传入的reportTemplateStream,JasperReport会负责关闭。
4.2 场景二:结合数据库查询,动态填充产品图片
假设我们有一个products表,其中image_data是BLOB类型字段。
import java.sql.*; import javax.sql.DataSource; import net.sf.jasperreports.engine.*; public class ProductReportService { private DataSource dataSource; // 通过Spring等注入 public byte[] generateProductCatalog() throws JRException, SQLException { // 1. 加载模板 InputStream templateStream = getClass().getResourceAsStream("/reports/product_catalog.jasper"); // 2. 建立数据库连接,执行查询 // 注意:这里为了清晰,使用try-with-resources管理连接。实际项目可能使用连接池,由框架管理。 try (Connection conn = dataSource.getConnection()) { String sql = "SELECT id, name, description, image_data FROM products WHERE category = ?"; PreparedStatement stmt = conn.prepareStatement(sql); stmt.setString(1, "electronics"); ResultSet rs = stmt.executeQuery(); // 3. 创建JRDataSource // JRResultSetDataSource可以直接包装ResultSet,非常方便! JRDataSource jrDataSource = new JRResultSetDataSource(rs); // 4. 准备参数(可能包含一些全局参数,如报表标题、公司信息等) Map<String, Object> parameters = new HashMap<>(); parameters.put("REPORT_TITLE", "电子产品目录"); // 注意:产品图片是通过数据源字段$F{image_data}传递的,不需要放在参数里 // 5. 填充报表 JasperPrint jasperPrint = JasperFillManager.fillReport(templateStream, parameters, jrDataSource); // 6. 导出PDF return JasperExportManager.exportReportToPdf(jasperPrint); // try-with-resources会自动关闭ResultSet, Statement, Connection } // Connection 在这里自动关闭 // 注意:模板流(templateStream)的关闭由JasperReport处理。 } }关键点:
- 字段映射:
JRResultSetDataSource会自动将ResultSet中的列映射为报表中的字段($F{column_name})。因此,查询中的image_data列会自动成为报表中的$F{image_data}字段。确保报表模板中字段的名称和类型(java.io.InputStream)与查询结果匹配。 - 性能:如果产品图片很大很多,一次性查询所有BLOB数据可能导致内存溢出。在这种情况下,可以考虑分页查询,或者使用自定义的
JRDataSource实现懒加载,每次只读取当前记录的图片数据。
4.3 场景三:处理网络图片与图片缓存策略
直接传递URL给JasperReport并不总是可靠,因为报表引擎可能在导出时同步下载图片,容易受网络波动影响。更稳健的做法是在应用层控制下载和缓存。
import javax.imageio.ImageIO; import java.awt.image.BufferedImage; import java.net.URL; import java.util.concurrent.TimeUnit; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; public class NetworkImageService { // 使用Guava Cache做简单的内存缓存 private static final Cache<String, BufferedImage> IMAGE_CACHE = CacheBuilder.newBuilder() .maximumSize(1000) .expireAfterWrite(10, TimeUnit.MINUTES) .build(); public BufferedImage getImageFromUrl(String imageUrl) throws IOException { // 1. 检查缓存 BufferedImage cachedImage = IMAGE_CACHE.getIfPresent(imageUrl); if (cachedImage != null) { return cachedImage; } // 2. 缓存未命中,从网络下载 BufferedImage downloadedImage; try { URL url = new URL(imageUrl); downloadedImage = ImageIO.read(url); if (downloadedImage == null) { throw new IOException("Failed to decode image from URL: " + imageUrl); } } catch (Exception e) { // 3. 网络下载失败,返回一个本地默认图片(降级策略) // 记录日志 System.err.println("Failed to download image: " + imageUrl + ", using default."); return getDefaultImage(); // 实现一个获取默认图片的方法 } // 4. 放入缓存 IMAGE_CACHE.put(imageUrl, downloadedImage); return downloadedImage; } // 在报表生成代码中 public void generateReportWithNetworkImage() throws JRException { Map<String, Object> parameters = new HashMap<>(); NetworkImageService imageService = new NetworkImageService(); String dynamicImageUrl = "https://cdn.example.com/user/avatar_123.jpg"; try { BufferedImage avatar = imageService.getImageFromUrl(dynamicImageUrl); parameters.put("USER_AVATAR", avatar); // 参数类型为 java.awt.Image } catch (IOException e) { parameters.put("USER_AVATAR", getDefaultAvatarImage()); } // ... 填充和导出报表的代码 } }策略优势:
- 提升性能:避免相同图片重复下载,极大加快报表生成速度。
- 增加稳定性:网络不可用时,使用缓存图片或默认图片,保证报表能正常生成。
- 控制超时:可以在
ImageIO.read之前配置URLConnection的超时时间,避免报表线程长时间阻塞。
5. 生产环境常见问题与深度排查指南
即使本地测试通过,部署到生产环境后,图片显示问题仍可能发生。以下是多年踩坑后总结的排查清单。
5.1 问题一:图片显示为红叉或空白
这是最常见的问题。
排查步骤:
检查图片资源路径与权限:
- 类路径:确认你的图片/模板JAR/WAR包中确实存在。可以用
jar tf your-app.jar | grep logo.png检查。注意大小写敏感(Linux环境)。 - 文件系统:确认应用进程(如Tomcat用户)对图片所在目录有读取(
rx)权限。使用ls -la /path/to/image检查。 - 网络URL:直接在服务器上用
curl或wget测试URL是否能通,并检查返回的内容类型(Content-Type)是否是图片。
- 类路径:确认你的图片/模板JAR/WAR包中确实存在。可以用
检查JasperReport日志: JasperReport在填充报表时,如果
On Error Type设为Error,会抛出异常。确保你的应用日志级别包含了net.sf.jasperreports的DEBUG或WARN。异常信息通常会明确指出找不到哪个资源。验证参数/字段传递:
- 在Java代码中,在调用
fillReport之前,打印或日志输出parametersMap中图片参数的值,确认它不是null。 - 对于字段传递,检查你的SQL查询是否真的返回了非空的BLOB数据。可以在代码中遍历
ResultSet,检查getBinaryStream()是否返回null。
- 在Java代码中,在调用
确认图片组件属性:
- Evaluation Time:动态图片是否错误地设置为
Report? - Expression:表达式拼写是否正确?
$P{LOGO}和$P{logo}是不同的。 - Image Type:对于已知的JPEG图片,尝试显式设置为
Jpeg,看是否解决问题。有时自动检测会失败。
- Evaluation Time:动态图片是否错误地设置为
5.2 问题二:图片位置错乱或拉伸变形
这通常与图片组件和其父容器的尺寸、拉伸模式有关。
Stretch Type属性:
No Stretch:图片保持原始尺寸,可能被裁剪。Fill Frame:图片拉伸以填满整个Image框,可能变形。Retain Shape(最常用):按比例缩放图片,以完全放入框内,保持原形状,可能留白。Clip:按比例缩放图片,但可能裁剪掉超出框的部分以填满。- 建议:对于产品图等需要保持比例的,使用
Retain Shape。同时,在设计时,合理设置Image框的尺寸。
Position Type:控制图片在框内的对齐方式(
Float,FixRelativeToTop等),结合Stretch Type使用。Band的高度:如果图片放在Detail Band,而Band的
Height是固定的,但图片实际很高,可能导致图片被截断。可以设置Band的Height为能容纳图片的高度,或者设置图片的Position Type为Float并允许Band拉伸。
5.3 问题三:生成PDF性能慢或内存溢出(OOM)
处理大量高分辨率图片是性能杀手。
优化图片本身:
- 压缩:在插入报表前,使用工具(如TinyPNG、ImageMagick)对图片进行压缩,在可接受的清晰度下减小文件体积。
- 缩放:报表中显示的图片尺寸通常很小(如缩略图),没有必要传递原始4000x3000像素的图片。可以在后端先进行缩放,生成一个适合报表尺寸的版本(如200x150像素)再传递给JasperReport。
优化数据加载:
- 分页:不要一次性查询所有带图片的数据。实现分页报表,每次只填充和导出一页的数据。
- 懒加载:实现自定义的
JRDataSource,仅在需要当前记录时(即next()方法被调用时)才从数据库或文件中加载图片数据,而不是一次性加载所有图片到内存。
JVM调优:
- 增加JVM堆内存(
-Xmx)。 - 关注GC日志,如果频繁Full GC,可能是存在内存泄漏(如未关闭的
InputStream)。
- 增加JVM堆内存(
5.4 问题四:中文路径或文件名导致问题
虽然JasperReport和Java本身支持Unicode,但在某些特定环境(如旧版本Windows服务器、特定编码的Linux Shell下启动的服务)中,资源路径包含中文可能无法被正确加载。
- 最佳实践:永远避免在资源路径、文件名、报表参数名中使用中文或特殊字符。使用英文、数字和下划线。例如,用
user_avatar.png代替用户头像.png。
5.5 高级调试技巧:使用JasperReport内置的虚拟化器(Virtualizer)
当处理超大型报表(成千上万行,每行都有图片)时,即使优化了图片,所有JasperPrint对象仍可能占用巨大内存。JasperReport提供了JRVirtualizer,可以将部分页面数据临时交换到磁盘。
import net.sf.jasperreports.engine.JRVirtualizer; import net.sf.jasperreports.engine.fill.JRFileVirtualizer; public byte[] generateLargeReport() throws JRException { // 1. 创建虚拟化器,指定一个临时目录和最大内存中保留的页数 JRVirtualizer virtualizer = new JRFileVirtualizer(100, "/tmp/jasper_virtual"); // 内存中最多保留100页 Map<String, Object> parameters = new HashMap<>(); parameters.put(JRParameter.REPORT_VIRTUALIZER, virtualizer); // 关键:将虚拟化器作为参数传入 // 2. 正常填充报表 JasperPrint jasperPrint = JasperFillManager.fillReport(templateStream, parameters, dataSource); // 3. 导出PDF byte[] pdfBytes = JasperExportManager.exportReportToPdf(jasperPrint); // 4. 非常重要:清理虚拟化器创建的临时文件 virtualizer.cleanup(); return pdfBytes; }使用虚拟化器会牺牲一部分性能(因为涉及磁盘I/O),但可以防止生成超大报表时的OOM崩溃,是一种用空间换时间的权衡策略。