JQuickExcel 单元格样式缓存:控制 CellStyle 基数而非只看堆大小
tags: #JQuickExcel #JavaExcel #开源 #POI #Excel工具
更多示例: JQuick-Excel 官网示例 https://www.jquick.org/jquick-excel/#demo-formulas0005
简介
大批量导出中,我见过最隐蔽的问题之一是:数据行并不复杂,堆也暂时没有明显告警,但工作簿中的样式对象持续增长,最终触及 Apache POI 的样式上限或让文件生成异常缓慢。单元格值不同不表示样式必须不同。一百万个订单号可以使用同一种普通文本样式,一百万个日期值也可以共享一种日期格式;真正危险的是把订单号、行号、动态颜色或动态格式拼进样式定义。
JQuickExcel 的JQuickExcelConfig提供setCellStyleCacheEnabled(boolean)。配置类注释明确该开关用于 CellStyle 缓存,以避免 POI 的 64,000 样式上限风险。本文只围绕这一已确认的开关、README-CN 中的STYLE与FORMATDSL 说明样式基数控制的方法,并说明它与流式导出和批量导入的边界。性能结论仅能来自读者本机复测的示例实验,不能杜撰为官方数据。
前言
我不把样式缓存理解为“打开后样式问题自然消失”。缓存能复用等价样式,却无法把一百万种真正不同的字体、填充色、边框和数据格式压缩为几个对象。若业务代码或 DSL 每行都产生一份新样式描述,缓存 key 本身也会呈高基数,最终仍然是高基数样式。调优的第一步是收敛视觉语义:表头、普通文本、日期、金额、百分比、状态警告等应该是有限集合。
样式问题与行内存问题也要分开判断。流式导出主要限制已写出行的内存驻留;样式缓存主要减少同一工作簿内等价CellStyle的重复创建。开启流式但每行生成独有颜色,仍可能遇到样式上限;开启样式缓存但仍在内存工作簿中保留几十万行,也无法解决行对象累积。我在压测记录中会分别观察行数、样式种类、堆、临时盘和导出正确性,避免把不同根因混为同一个“Excel 太大”。
我还会先定义样式的业务字典。比如订单状态只允许NORMAL、PAID、OVERDUE三种显示规则,金额列统一使用#,##0.00,日期列统一使用yyyy-MM-dd。这样不仅有利于缓存,也让模板审查更容易。若产品提出按每笔金额生成渐变色,我会先确认它是否真需要单元格级唯一格式;很多场景用有限档位、单独指标列或条件说明即可满足需求。
环境与依赖
本文基于项目当前 Maven 坐标与 Java 8+。pom.xml中已经包含 Apache POI 相关依赖,业务项目只需要引用 JQuickExcel。为了避免运行期不一致,我会让样式 DSL 和库版本一起回归测试,不会只因为生成的 xlsx 能打开就忽略样式数量的长期风险。
<dependency><groupId>io.github.paohaijiao</groupId><artifactId>jquick-excel</artifactId><version>3.6.0</version></dependency>README-CN 将STYLE定义为导出 DSL 的一部分,目标可以是行、列、单元格或矩形范围。下例只使用已展示的属性:表头粗体,数据区域固定对齐、换行和边框,金额列通过FORMAT使用稳定显示格式。关键不在于样式多,而在于同类单元格复用相同的规则组合。
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPEexcelsPUBLIC"-//PAOHAIJIAO//DTD API EXCEL 1.0//EN""classpath:paohaijiao/dtd/Jquick-excel.dtd"><excelsnamespace="com.github.paohaijiao.xml.service.JQuickExcelExportService"><excelname="exportExcel"returnClass="void"><![CDATA[ EXPORT WITH SHEET="Invoices", HEADER=true, MAPPING={"invoiceNo":"发票号","customer":"客户","invoiceDate":"开票日期","amount":"含税金额"}, FORMAT={"invoiceDate":"yyyy-MM-dd","amount":"#,##0.00"}, STYLE={ ROW 1:{bold:true,alignment:center,borderBottom:thin}, A2:D100:{alignment:center,wrapText:true,borderBottom:thin} } ]]></excel></excels>这个 XML 只是展示稳定样式规则的组织方式。范围的实际大小和业务数据量应由模板决定;我不会为了追求缓存而将不同语义的列强行设为同一种格式。日期、金额和普通文本仍然可以有不同的固定样式,只要组合数量保持受控即可。
代码示例
缓存开关是全局配置,适合在应用启动阶段统一设定。下面同时设置了流式相关参数,目的是说明两类策略可并存,不表示样式缓存依赖流式导出。若服务只导出小报表,也可以只设置样式缓存。
importcom.github.paohaijiao.config.JQuickExcelConfig;publicfinalclassExcelStyleConfig{privateExcelStyleConfig(){}publicstaticvoidconfigure(){JQuickExcelConfig.getInstance().setCellStyleCacheEnabled(true).setStreamingExportEnabled(true).setStreamingExportThreshold(5_000).setStreamingRowAccessWindowSize(100).setStreamingCompressTempFiles(true);}}导出时仍按 README-CN 的 XML 服务链路传入JQuickRow。缓存是否复用取决于导出过程中实际构建出的样式是否等价,因此我不会在每一行中拼接格式字符串或创建随机颜色。数据值可以变化,样式规则保持固定。
List<JQuickRow>rows=loadInvoiceRows();ExcelStyleConfig.configure();try(OutputStreamoutput=newFileOutputStream("invoices.xlsx")){JQuickParseHandlerparser=newJQuickExcelExportXmlParseFactory(rows,output);JQuickFactoryfactory=newJQuickXmlFactory(parser,"jquick-excel.xml");JQuickExcelExportServiceservice=factory.createApi(JQuickExcelExportService.class);service.exportExcel("field","value");}当业务需要状态样式时,我先把状态压缩为有限类别,再由模板或已有导出策略按类别处理。下面的 Java 代码不调用未确认的样式 API,只表达在业务数据中维护有限状态的原则。真正的视觉映射仍应使用当前项目已支持的 XML DSL 或既有扩展点。
privateStringtoStyleClass(Stringstatus){if("OVERDUE".equals(status)){return"OVERDUE";}if("PAID".equals(status)){return"PAID";}return"NORMAL";}我会把styleClass当作受控业务字段,而不是把#RRGGBB之类的行级随机值直接写入样式配置。假如只有三种状态,理论上的样式组合也应接近表头、日期、金额与三种状态的有限乘积,而不应随导出行数线性增加。具体缓存命中和对象数量必须以实际工作簿、主题和 DSL 组合验证。
一个最小的示例实验是固定 10 万行、10 列,其中日期与金额格式恒定、状态仅有三种,分别打开和关闭缓存并生成文件。每组至少重复多次,记录完成状态、文件可打开性、总耗时、堆、GC 以及可观察到的样式数量。结果表应明确写“示例实验,需本机复测”;我不会给出未经运行的毫秒、MB 或倍数,也不会称为项目官方基准。
原理说明
Apache POI 的CellStyle是工作簿级资源。一个样式对象不能脱离所属Workbook直接复用到另一个工作簿,因此缓存的有效范围应理解为单次工作簿创建过程,而不是跨导出文件永久保存的样式实体。JQuickExcel 配置中的cellStyleCacheEnabled正是为了控制这一类重复创建;配置类的说明明确指出其目标是避免 POI 样式数量超过 64,000。
从工程角度看,样式等价通常由多个维度共同构成:字体名称和字号、粗体斜体、字体颜色、水平与垂直对齐、换行、边框、填充、数据格式等。只要其中一个维度随行变化,就可能产生新的组合。典型反例包括:把行号拼进格式字符串;根据每笔金额算出一个独立 RGB 色;每行生成不同的字体名称;把订单编号用作自定义样式标识。它们看似只是“展示细节”,但会让缓存复用率急剧下降。
FORMAT与STYLE的职责也应分清。FORMAT用于 Excel 的显示格式,例如yyyy-MM-dd和#,##0.00;STYLE用于字体、边框、对齐、填充等样式属性。我的模板会把同一语义列的格式文本写成完全一致的规范形式,避免无意义空格或不同大小写造成表面相同、实际不同的配置。对业务人员维护的 DSL,我会加入模板评审,确保没有把动态变量放入样式描述。
流式导出启用时,处理器仍可能创建并使用样式。SXSSF 解决的是行刷新与临时文件,不会让样式对象自动随行释放;因此它与样式缓存形成互补。批量导入通常不涉及导出样式创建,setCellStyleCacheEnabled也不应被误解为大文件导入的主要优化按钮。这样划分资源所有权后,排查路径会更短:导出堆随行增长检查流式和源数据;样式对象增长检查 DSL 与动态视觉规则;导入堆增长检查全量List与业务队列。
我会将“样式基数”作为测试输入的一部分。例如同样是 10 万行,实验 A 使用一套表头、日期、金额和三种状态样式;实验 B 则人为让每行颜色不同。两者不能放在同一结论下,因为数据行数相同,样式问题却完全不同。这样设计可以验证缓存对重复样式的作用,也能及时暴露业务规则本身不适合高行数导出的事实。任何测试数值都只属于该机器和该输入,必须本机复测。
注意事项
- 我会始终开启
setCellStyleCacheEnabled(true)作为大导出的基础配置,但不会把它当成高基数样式的替代方案。 - 样式实例属于单个
Workbook。不要把一个文件的 POICellStyle保存后直接应用到另一个文件。 - 不要把订单号、行号、用户输入、时间戳或随机颜色拼进样式描述;这些值会让样式组合随行数增长。
- 日期、金额和百分比格式应规范化为有限字符串,例如统一使用
yyyy-MM-dd与#,##0.00。 - 状态色、告警色和等级色应先收敛为有限枚举。高基数视觉编码应重新评估产品必要性。
- 流式导出与样式缓存解决不同资源问题;需要同时评估行对象、临时盘和样式对象。
- 不要绕过既有导出路径在每个单元格循环中任意创建 POI 样式和字体对象,否则会破坏复用前提。
- 导出验证不止检查文件能打开,还要检查表头、日期、金额、状态样式与样式数量增长趋势。
- 配置是全局单例。不要在并发请求中临时关闭缓存,以免影响其他正在导出的任务。
- 示例实验的所有性能数值都必须在本机复测,并明确不是 JQuickExcel 官方数据。
总结
样式缓存能复用等价的CellStyle,却无法抵消业务规则制造的无限组合。日期、金额、表头和状态色若收敛为有限的FORMAT与STYLE组合,缓存才有稳定命中空间;将行号、订单号、随机色或动态文本带入样式,会使样式基数随数据增长。流式导出控制的是行驻留,样式缓存控制的是样式对象,两项需要分别观察。
验证时应同时准备有限样式基线和高基数风险样本,检查文件能否打开、表头与数据格式是否正确,以及样式数量和堆使用是否随行数异常增长。所有比较都应在本机以相同模板、JDK、堆参数和数据集复测,并保留人工查看的工作簿结果;性能下降或样式数量减少都不能成为破坏展示语义的理由。