报表能正常显示一页筛选结果,导出相同范围却明显更慢。流程已经使用流式工作簿写入器,批次大小和写入器配置自然成为排查方向。
沿数据提供层继续检查,发现每个输出批次都会重新进入列表分页流程:计算总数、执行分页查询、转换这一批数据,再追加到工作簿。文件在逐批写入,数据库执行却不断重新开始。
本次本地修复让导出拥有独立的执行生命周期。下文依据本地测试和受控只读比对说明已完成的部分;修复包尚未部署,生产 HTTP 提速也尚未验收。
先统计语句执行次数
列表页需要一段结果,有时还需要总数来计算页码。全量下载需要读完本次选中的结果,再生成完整文件。
旧实现把每个写入批次绑定到新的分页操作,反复支付计数和分页执行的成本。原本用于控制处理内存的小批次,同时决定了数据库重复工作的次数。
检查时分别记录:
- 数据 SELECT 执行次数。
- 结果总数 SELECT 执行次数。
修复后,两者分别为一次和零次。这里统计的是导出结果的取数与计数语句,权限、报表定义和列配置仍可能有自己的查询。
驱动从正在执行的结果中继续取行,也与重新执行 SELECT 有区别。JDBC 把 fetch size 定义为给驱动的取行数量提示;仅修改这个提示,无法消除代码中重新执行查询的循环。
消费一个游标,保留外围契约
新增入口限定到目标报表,继续使用既有权限检查、当前用户范围、业务筛选、导出列和值转换。只在导出结果选择中去除分页参数。
数据提供层使用 MyBatis 游标,即可以关闭、逐步读取查询结果的惰性迭代器。这里的游标与列表接口传给客户端的翻页令牌不同。MyBatis 文档说明了这份契约。消费期间,数据库会话保持有效。
处理顺序为:
- 校验权限,确定报表和列配置。
- 用本次业务筛选打开一个结果游标。
- 读取有界批次,调用原转换器并写入单元格。
- 持续消费同一个游标。
- 完成并检查工作簿,再发送文件。
转换和写入仍然分批,批次不再触发新的分页查询或总数查询。
分别检查读取与写入的资源占用
修复继续使用 Apache POI 的 SXSSF 流式 XLSX 写入器。POI 文档说明,它只保留一个滑动行窗口,较早的行会写入磁盘;临时文件需要显式清理,共享字符串等功能仍可能消耗较多内存。
链路两端都需要检查。若数据提供层先加载所有行,流式写入器无法消除此前的内存占用;若消费方把游标结果重新收集成完整列表,也会失去增量读取的收益。
数据库会话、游标、工作簿、输出流及临时文件都有明确的关闭责任。测试检查了成功、查询失败和模拟客户端断线后的清理,范围同时包括写入器临时文件与最终 XLSX。
生成有效文件后再开始交付
实现先生成并检查临时 XLSX,再发送下载字节。暂存增加了 I/O,也推迟了传输开始时间,同时让生成阶段的失败可以在成功下载响应提交前处理。
Servlet 响应契约规定,响应提交后,状态和响应头已经发出,不能再重置。文件暂存无法阻止后续网络失败,它分开了有效文件生成与文件交付。
实现还限制了工作表容量。空结果仍输出原配置的完整表头。
核验等价性,说明计时范围
检查覆盖单条结果、空结果,以及较短和较长的时间范围。重复的本地比较使用相同筛选和受控只读快照,新路径的中位耗时更低。
验收同时检查:
- 业务字段的多重集合,即保留每条记录出现次数的集合,以及相关汇总值。
- 表头、配置列顺序、单元格类型和值。
- 原有权限和参数处理。
- 一次数据查询、零次结果总数查询。
- 异常后的资源关闭及临时文件删除。
- 实际本地 HTTP 下载与重复请求。
独立生成的展示序号,也就是行编号,被明确排除在等价性比较之外,业务字段仍必须一致。
本地计时包含数据库读取、转换及文件生成,没有覆盖生产路由、真实服务并发或用户浏览器的完整下载。部署、请求级日志和生产耗时仍需分别验收。
全量导出适合拥有独立的执行生命周期:沿用已验证的权限与转换规则,一次消费本次结果,并把有效文件生成、交付和清理作为明确的检查点。