搞过后端开发的兄弟应该都躲不过excel导出这个需求。一开始我以为就是查询数据、遍历写入、返回下载,直到接了几个带动态表头的需求——同样的导出接口,不同角色看到的列不一样;同一张报表,换个语言表头要跟着变;还有那种用户自定义列,今天勾选abcd,明天勾选abef。固定写死表头的工具类根本撑不住。所以我把导出工具类专门重构了一版,核心就干一件事:允许在运行时动态修改表头,同时保留字段映射、样式、大数据量导出的能力。这篇把设计思路和代码实现完整记录下来,适合java后端同学参考,尤其是用easyexcel做报表导出、又不想把代码写死的场景。
1. 为什么需要一个支持动态表头的导出工具类
1.1 三种常见的动态表头业务场景
先说场景,因为没场景就谈设计容易飘。
第一种是权限控制列。内部管理系统里,管理员导出用户数据时要看到“最后登录ip”“登录设备”这种敏感字段,普通运营导出时这些列必须隐藏。如果用固定实体类,同一套数据得定义两个dto,一个带敏感字段,一个不带,接口也要复制一份。等哪天 权限又加了一档,第三个dto就来了。这种需求本质上是“同一张表,不同人看到的表头不一样”,只能在导出时动态决定。
第二种是国际化。外贸项目或者跨国公司的内部系统,用户可能切中英文甚至日文。表头“姓名/name/お名前”这种变化,不可能靠枚举类写死。固定用 @excelproperty("姓名") 的注解,换语言环境就得再写一个导出类,非常离谱。最合理的做法是表头从国际化配置里动态取。
第三种是用户自定义列。后台管理系统经常允许用户自己勾选列表字段,比如订单导出,有人想导出“订单号、金额、支付时间”,有人想导出“订单号、商品名、数量、收货地址”。前端把列配置存下来,后端导出时按配置生成表头。这种场景下,表头在接口调用前是未知的,必须支持运行时修改。
所以动态表头不是炫技,是这些业务需求倒逼出来的。
1.2 固定表头方案卡在哪
最常见的固定表头写法是定义一个实体类,字段上标 @excelproperty 注解,然后用easyexcel或者poi直接写。这个方案在小项目里确实够用,但一旦遇到动态列需求,就处处掣肘。
第一,注解是编译期写死的。 @excelproperty("姓名") 里的字符串在类加载时就确定了,运行时改不了。你想根据用户传入的列名动态调整,只能放弃注解,另找路子。第二,dto类数量会失控。每张导出表对应一个dto,每个权限角色又一个dto,每个语言版本再一个dto,时间一长,项目里全是 userexportdto 、 userexportadmindto 、 userexportendto 这种类,维护成本非常高。第三,业务代码没法复用。固定dto的导出逻辑是绑定在类结构上的,列顺序一变,数据组装逻辑就要跟着改,很容易改漏。
还有一个隐蔽的问题:前端已经做了列配置功能,后端却只能全部导出,再由前端隐藏。这个做法在白名单项目里能糊弄,但数据量一上来,全量导出的网络开销和内存开销都很大。
1.3 工具类的设计目标
我重构的时候给自己定了几个目标,不是随便写的。
第一,列配置必须运行时传入。表头列表由调用方组装,工具类本身不关心列的业务含义,只负责把列头和数据写进excel。第二,要支持多级表头。比如“产品信息”下挂“名称”“价格”,“下单信息”下挂“订单号”“时间”,这种集团式表头在统计报表里太常见了。第三,数据流要能分批写入,不能一上来就把全量数据塞在内存里。第四,样式要能自定义,至少表头字体、背景色、对齐方式、列宽都能控制。第五,调用方式要足够简单,最好一行代码搞定,不让业务方感知到底层是poi还是easyexcel。
把这些目标列完,工具类的基本轮廓就有了:一个表头列模型,一个导出接口,一个基于easyexcel的实现类。
2. 工具类整体设计与表头模型
2.1 表头列模型怎么定义
动态表头的第一步是定义列模型。我这里叫 dynamicheadcolumn ,核心字段如下:
public class dynamicheadcolumn {
/** 表头显示名称,例如“用户姓名” */
private string headname;
/** 字段名,对应数据行里的key,例如 username */
private string fieldname;
/** 列宽,单位是excel的字符宽度 */
private integer width;
/** 排序权重,数值小的排前面 */
private integer order;
/** 多级表头的父级名称,例如“基本信息” */
private string groupname;
/** 水平对齐方式:left、center、right */
private excelcolumnalign align;
/** 表头字体颜色,默认黑色 */
private color headfontcolor;
/** 表头背景色,默认浅灰 */
private color headbackgroundcolor;
}
字段说明看起来多,实际用起来很轻。绝大多数时候只需要设置 headname 和 fieldname ,其他都有默认值。 groupname 是用来支持多级表头的,如果为null,就按单层表头处理。 order 字段很关键,调用方传入的列顺序不一定是最终显示顺序,工具类内部会先按order排序再构建表头,方便前端配置和后端校验逻辑各管各的。
2.2 导出接口与调用入口设计
接口设计我建议收敛成两个核心方法,不要一上来就搞一堆重载:
public interface dynamicexcelexporter {
/**
* 导出,数据行按表头列顺序传入
*/
void export(outputstream outputstream,
string sheetname,
list<dynamicheadcolumn> columns,
list<list<object>> datarows) throws ioexception;
/**
* 导出,数据行按map传入,内部按fieldname取值转换
*/
void export(outputstream outputstream,
string sheetname,
list<dynamicheadcolumn> columns,
list<map<string, object>> datamaps) throws ioexception;
}
第一个方法接收 list<list<object>> ,每一行数据都已经按列顺序排好了,工具类什么都不用做,直接写。第二个方法接收 list<map<string, object>> ,工具类会根据每个dynamicheadcolumn的fieldname从map里取值,再转成顺序行。两个方法各有适用场景:从sql或者业务聚合层拿到的数据天然是map,用第二种;如果数据已经是前端传过来的二维数组,用第一种更省事。
接口里故意不依赖 httpservletresponse ,只依赖 outputstream 。这样做的目的是让工具类可以脱离web层单测。controller里只需要加一行 response.getoutputstream() 传入即可,社区里很多工具类把 httpservletresponse 写在方法签名里,导致单元测试要mock一堆servlet对象,完全没必要。
2.3 兼容原生easyexcel的扩展点
easyexcel的动态表头能力是通过 head(list<list<string>>) 方法暴露的,这个 list<list<string>> 的语义和普通二维表头不太一样。简单说,外层list代表多少列,内层list代表这一列表头在垂直方向上的单元格。
如果只有一个表头行,每个内层list就只有一个元素: "用户姓名" 。如果有两级表头,比如“基本信息”下面还要挂“姓名”和“电话”,那这两列的内层list分别是 ["基本信息", "姓名"] 和 ["基本信息", "电话"] 。第一行是父级表头,第二行是子级表头。如果并列的列不在同一个父级下,比如还有一列“订单号”没有父级,它的内层list就是 ["订单号"] ,长度比前面短。easyexcel会自动把缺失的位置合并成空单元格。
工具类要做的事就是把 list<dynamicheadcolumn> 按这个规则转成 list<list<string>> 。我把它单独拆了一个方法,方便以后扩展更多表头层级。
3. 核心代码实现:动态表头如何落地
3.1 引入依赖与版本选择
我用的是easyexcel 3.3.2,poi版本由easyexcel自己管理,不需要额外引入。maven依赖如下:
<dependency>
<groupid>com.alibaba</groupid>
<artifactid>easyexcel</artifactid>
<version>3.3.2</version>
</dependency>版本选择上有一个经验:easyexcel 2.x和3.x的api差异比较大,网上很多教程还是2.x的写法,导入导出工具类复制过来经常编译不过。如果项目是从零开始,建议直接用3.x,api更干净,写起来也更顺手。如果有老项目已经用了2.x,不要只升级一个包,因为两个版本的核心逻辑改动很多,很容易出现莫名其妙的问题,最稳妥的方式是拿一个过渡版本在测试环境完整跑一遍。
3.2 单层表头与多级表头的动态构建
核心方法就是把 list<dynamicheadcolumn> 转成easyexcel需要的 list<list<string>> :
private list<list<string>> buildhead(list<dynamicheadcolumn> columns) {
// 先按order排序,保证显示顺序正确
list<dynamicheadcolumn> sortedcolumns = new arraylist<>(columns);
sortedcolumns.sort(comparator.comparingint(c -> c.getorder() == null ? 0 : c.getorder()));
list<list<string>> head = new arraylist<>();
for (dynamicheadcolumn column : sortedcolumns) {
if (stringutils.isnotblank(column.getgroupname())) {
// 多级表头:父级名称 + 子级名称
head.add(arrays.aslist(column.getgroupname(), column.getheadname()));
} else {
// 单层表头
head.add(collections.singletonlist(column.getheadname()));
}
}
return head;
}
这段代码很短,但有几个细节值得注意。
第一个细节: head 里的每个内层list长度可以不同,但easyexcel构建表头时会以最长的list为准,其余list缺的位置会自动补空合并。这个行为对我们是有利的,但如果你希望父级表头跨多列合并,比如“时间”这个大组下面有“下单时间”“支付时间”“发货时间”三列,光靠这个 list<list<string>> 是表达不了“时间”跨三列的。这种情况需要额外的合并策略,后面踩坑部分我会专门讲。
第二个细节:order排序必须放在构建表头之前。因为前端传来的列顺序可能和后端数据组装顺序不一致,很容易出现表头顺序对、数据顺序错的问题。统一排序后,后续数据行的字段取值顺序也按排序后的columns来,两边就对齐了。
3.3 数据填充与字段映射
如果是 list<list<object>> 这种数据,直接写就行:
private void writerows(excelwriter writer, writesheet sheet, list<list<object>> datarows) {
writer.write(datarows, sheet);
}
但map类型的数据需要做一次转换。因为map是无序的,必须按照dynamicheadcolumn的顺序一个一个取值,不能直接丢给easyexcel让它自己找:
private list<list<object>> convertmapstorows(list<map<string, object>> datamaps,
list<dynamicheadcolumn> columns) {
list<list<object>> rows = new arraylist<>();
if (collectionutils.isempty(datamaps)) {
return rows;
}
// 这里columns已经是排序后的列表,由buildhead的调用方保证
for (map<string, object> datamap : datamaps) {
list<object> row = new arraylist<>(columns.size());
for (dynamicheadcolumn column : columns) {
row.add(datamap.get(column.getfieldname()));
}
rows.add(row);
}
return rows;
}
用map取值的性能其实还可以,单次导出一万行、每行20列,这种转换就是毫秒级,不用过度优化。但如果数据量大到十万行以上,建议在sql层面就按列顺序返回 list<object> ,省掉map这一跳。
真正要注意的是fieldname的命名统一。我见过项目里数据库字段叫 user_name ,java代码里叫 username ,前端配置里叫 name ,三套命名随意切换,到导出这层就抓瞎。我这个工具类不负责猜字段,fieldname必须严格和数据map的key一致,否则导出去就是空单元格。实际对接时建议前端传的配置先经过后端的一个字段映射表转换,不要直接拿前端字段名查数据库表。
3.4 表头样式、列宽与对齐控制
动态表头最大的问题之一就是样式难控制。固定注解方式里可以写 @contentstyle 、 @headstyle ,但动态表头没有实体类,只能通过注册writehandler来实现。
easyexcel里最常用的是 horizontalcellstylestrategy ,可以分别设置表头样式和内容样式:
private writehandler buildcellstylestrategy(dynamicexportoptions options) {
writecellstyle headcellstyle = new writecellstyle();
headcellstyle.setfillforegroundcolor(indexedcolors.grey_25_percent.getindex());
headcellstyle.setfillpatterntype(fillpatterntype.solid_foreground);
headcellstyle.sethorizontalalignment(horizontalalignment.center);
headcellstyle.setverticalalignment(verticalalignment.center);
font headfont = new font();
headfont.setbold(true);
headcellstyle.setwrapped(true);
headcellstyle.setfont(headfont);
writecellstyle contentcellstyle = new writecellstyle();
contentcellstyle.setwrapped(true);
contentcellstyle.setverticalalignment(verticalalignment.center);
return new horizontalcellstylestrategy(headcellstyle, contentcellstyle);
}
列宽和单列对齐需要在另一个handler里处理。easyexcel里有现成的 simplecolumnwidthstylestrategy ,但它只能统一设置所有列的宽度,不支持按列配置。我自己写了一个简版列宽handler:
public class dynamiccolumnwidthstylehandler implements writehandler {
private final map<integer, integer> columnwidthmap;
public dynamiccolumnwidthstylehandler(list<dynamicheadcolumn> columns) {
this.columnwidthmap = new hashmap<>();
int index = 0;
for (dynamicheadcolumn column : columns) {
if (column.getwidth() != null) {
columnwidthmap.put(index, column.getwidth());
}
index++;
}
}
@override
public void aftercelldispose(writesheetholder writesheetholder,
writetableholder writetableholder,
list<writecelldata<?>> celldatalist,
cell cell,
head head,
integer relativerowindex,
boolean ishead) {
integer width = columnwidthmap.get(cell.getcolumnindex());
if (width != null) {
writesheetholder.getsheet().setcolumnwidth(cell.getcolumnindex(), width);
}
}
}
这个handler在表头样例存在的时候会逐格访问,因此在校验阶段不要依赖它,只做简单的宽度覆盖。对齐方式也可以类似方式动态设置,但如果你不想写太多handler,建议在业务里只对表头统一居中,内容列保持默认左对齐,这样代码量最小。
最终导出方法组装如下:
public void export(outputstream outputstream,
string sheetname,
list<dynamicheadcolumn> columns,
list<list<object>> datarows,
dynamicexportoptions options) throws ioexception {
list<dynamicheadcolumn> sortedcolumns = sortcolumns(columns);
list<list<string>> head = buildhead(sortedcolumns);
writehandler stylestrategy = buildcellstylestrategy(options);
try (excelwriter writer = easyexcel.write(outputstream).build()) {
writesheet writesheet = easyexcel.writersheet(sheetname)
.head(head)
.registerwritehandler(stylestrategy)
.registerwritehandler(new dynamiccolumnwidthstylehandler(sortedcolumns))
.build();
writer.write(datarows == null ? collections.emptylist() : datarows, writesheet);
}
}
4. 三个实战场景改造过程
4.1 场景一:用户自定义导出列
需求是用户列表导出时,前端让用户勾选需要的列,存到userconfig表里。后端拿到用户配置后,动态拼表头。
改造前代码长这样:userexportdto写死全部字段,导出时全部输出,前端再隐藏不需要的列。改造后:
@getmapping("/export")
public void export(httpservletresponse response,
@requestparam long userid) throws ioexception {
// 1. 从配置中心查当前用户定义要导出的列
list<usercolumnconfig> configs = usercolumnconfigservice.listbyuserid(userid);
// 2. 转成dynamicheadcolumn列表
list<dynamicheadcolumn> columns = new arraylist<>();
for (usercolumnconfig config : configs) {
dynamicheadcolumn column = new dynamicheadcolumn();
column.setfieldname(config.getfieldname());
column.setheadname(config.getheadname());
column.setwidth(config.getwidth());
column.setorder(config.getsort());
if (stringutils.isnotblank(config.getgroupname())) {
column.setgroupname(config.getgroupname());
}
columns.add(column);
}
// 3. 查询数据
list<map<string, object>> datamaps = userservice.queryexportdatabyuserid(userid);
// 4. 调用工具类
response.setcontenttype("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setcharacterencoding("utf-8");
string filename = urlencoder.encode("用户列表.xlsx", "utf-8").replace("+", "%20");
response.setheader("content-disposition", "attachment;filename*=utf-8''" + filename);
dynamicexcelexporter.export(response.getoutputstream(), "用户列表", columns, datamaps);
}
这里最核心的收益是:用户配置里允许勾选哪些字段、列顺序怎么样,完全由产品前端控制,后端只需要把配置翻译成dynamicheadcolumn列表。以后要加一个新的可导出的字段,只需要在usercolumnconfig里增加一行记录,后端不用改任何代码。
有一个细节必须注意:用户配置的字段名一定不要直接作为sql查询列名使用,否则有注入风险。正确做法是先用白名单映射,比如配置里存的是 username ,sql模板里对应 u.user_name ,后端维护一个允许字段集合,不在集合里的直接忽略。
4.2 场景二:国际化表头切换
国际化的动态表头更简单,只需要把headname从写死的中文改成从i18n资源文件取。流程拆成两步:
第一步,dynamicheadcolumn里存表头资源的key,比如 user.name ,而不是直接存“姓名”。第二步,导出前根据当前locale批量翻译:
public list<dynamicheadcolumn> translatecolumns(list<dynamicheadcolumn> columns,
locale locale) {
for (dynamicheadcolumn column : columns) {
string i18nkey = column.getheadname();
column.setheadname(messagesource.getmessage(i18nkey, null, i18nkey, locale));
}
return columns;
}
这一步看着简单,但特别容易踩一个坑:如果某个列的headname不是i18n key而是业务上直接传的“订单号”, messagesource.getmessage 查不到会抛异常。我在工具类里统一改成查不到就返回key本身,这样业务侧传普通字符串也不会炸。
实战里还有一个更隐蔽的问题。多语言环境里,中文“姓名”通常比英文“name”长,同一个列宽下英文可能显示得很空,中文可能显示不全。所以我建议在翻译完表头后再做一次列宽调整,简单策略是根据headname字符串长度算出一个推荐列宽。当然这只能作为默认值,用户手动配置的列宽优先级要更高。
4.3 场景三:统计报表动态追加汇总列
第三个场景是统计报表。比如订单统计导出,原本只有“日期、订单数、销售额”三列,用户选择“需要同环比”后,导出结果要变成“日期、订单数、销售额、订单数环比、销售额环比、订单数同比、销售额同比”七列。这些汇总列在sql层面可能压根不存在,是java内存里算出来的。
实现方式很直接。先查原始统计数据,再遍历每行计算环比、同比,计算结果放回map里,同时往columns里追加对应列:
list<map<string, object>> orders = orderstatisticsservice.querybydaterange(startdate, enddate);
for (map<string, object> order : orders) {
bigdecimal todayamount = (bigdecimal) order.get("amount");
order.put("amountdayonday", calcdayonday(order, "amount"));
}
list<dynamicheadcolumn> columns = defaultcolumns();
if (enablecompare) {
columns.add(new dynamicheadcolumn("订单数环比", "ordercountdayonday", 16, 10));
columns.add(new dynamicheadcolumn("销售额环比", "amountdayonday", 16, 11));
}
这里不用动态表头工具类也能做,但有了工具类之后,追加列的逻辑就非常干净,只需要动columns列表。没有工具类时,要么重新定义一个带汇总字段的dto,要么在导出的业务方法里手动处理列顺序,代码很啰嗦。
4.4 导出结果的验证方法
工具类改动之后不能只靠肉眼打开excel看,我建议在测试环境做三层验证。
第一层是验证表头结构。写单测时把head列表打印出来,逐一校验层级和名称是否符合预期。第二层是验证数据行。拿一条已知数据,转成list后和表头逐列对照,重点看map转出来之后字段有没有错位或者空值。第三层是验证真实文件。用easyexcel的excelreader重新把导出的文件读回来,断言文件能正常解析,且第一行表头和指定列名完全一致。
很多“文件损坏”问题都可以在第三层被发现。如果文件是好的,但第一行表头有合并导致的行高度异常,二次读取时也能暴露出来。所以这个验证步骤不建议省。
5. 踩坑记录与问题排查速查表
5.1 表头错位与文件损坏
用动态表头最常见的报错就是导出的excel打开时提示“文件已损坏”,或者表头错位。
大部分情况下,问题出在 list<list<string>> 的子列表长度不一致。easyexcel后端确实允许长度不一致,但某些老版本的poi在生成xml时,遇到差异长度会渲染出奇怪的合并,导致excel打开时修复报错。我的建议是无论是否多级表头,都统一把所有子list补齐到最长层级数。比如有四列,前三列都有父级,第四列没有,那就把第四列的内层list改成 ["", "第四列"] 而不是 ["第四列"] 。这样所有子list长度一致,表头结构最稳。
另一个坑是空字符串。如果父级名称传的是空字符串 "" ,easyexcel在渲染时可能会认为这是“需要合并的空单元格”,导致合并范围错误。所以我在工具类里把groupname为null才作为单层表头判断,空字符串也强制走多级表头逻辑,并在写入前用null替换空字符串。
5.2 空数据导出没有表头
有一个非常典型的坑:业务方查询结果为空时,代码一提前返回,文件里连表头都没有了。
很多初版实现的逻辑是:
list<list<object>> datarows = querydata();
if (collectionutils.isempty(datarows)) {
return; // 这里直接return,导出就没有表头了
}
export(outputstream, columns, datarows);
这个思路是错误的。业务需求通常是“没数据也要让用户看到表头,下面空着就行”。改法很简单,把空判断从“提前return”改成“传空list继续导出”。easyexcel对空数据列表写head,正常情况是能写出表头的。为了保险,我在工具类内部做了兜底:
list<list<object>> saferows = datarows == null ? collections.emptylist() : datarows; writer.write(saferows, writesheet);
这样即使调用方传了null,表头也不会丢。另外,如果数据量特别大, datarows 内存中已经塞不下了,要改用分批写:
// 每查5000条就flush一次,不能全部堆积在list里
list<list<object>> batch = fetchbatch();
while (!batch.isempty()) {
writer.write(batch, writesheet);
batch.clear();
batch = fetchbatch();
}
最后别忘了 writer.finish() ,否则文件流不完整,excel也会提示损坏。
5.3 大数据量导出内存溢出
easyexcel相较于poi的一个优势就是可以边读边写,不把所有数据放内存。但如果你使用 list<map<string, object>> 一次性装载几十万行,照样会oom。
优化思路分三层。第一层,sql层面分页,每页5000或10000行,查一批写一批,写完把引用置空,让gc回收。第二层,字段类型上注意,所有列值尽量是基本类型、string、bigdecimal、localdate,避免在导出阶段做复杂对象嵌套,嵌套对象意味着map里存一堆没用的属性。第三层,outputstream要包一层 bufferedoutputstream :
bufferedoutputstream bufferedout = new bufferedoutputstream(outputstream, 8192);
这里看起来只是缓冲区,但在批量写场景下能显著减少io次数。如果导出的行数特别大,不建议边算边写,而是先把数据落到临时文件再导出,避免数据库连接长时间占用。
5.4 常见问题速查表
| 现象 | 常见原因 | 解决方案 |
|---|---|---|
| excel打开提示文件损坏 | head子list长度不一致或空字符串导致表头合并异常 | 所有子list补齐到相同长度,null替代空串 |
| 导出的表头错位 | columns顺序和数据顺序不一致 | 先排序columns,再按排序后顺序构建head和数据行 |
| 数据为空时表头丢失 | 提前return或传了null数据 | 数据为空时传入空list,工具类统一兜底 |
| 列宽对不上 | 没有针对单列设置宽度 | 自定义writehandler按dynamicheadcolumn属性设置列宽 |
| 大量导出oom | 用list一次性装载所有数据 | 分页查询+分批write+clear,使用缓冲流 |
| 中文表头乱码 | 响应头content-disposition编码不对 | 使用urlencoder.encode后替换+,设置filename*=utf-8'' |
| 多级表头父级不合并 | head子list长度不一致 | 长度一致后easyexcel自动合并;跨多列合并需要额外handler |
| map数据导出售空 | fieldname与map key不一致 | 统一字段命名并在工具类内按fieldname精确取值 |
6. 从工具类到通用能力:一点经验补充
6.1 动态表头与前端配置联动
用上动态表头之后,我最大的体会是它把后端从“每次都要改导出代码”里解放了出来。但前面的解放是有前提的:前端要能提供靠谱的列配置。
我项目里的做法是让前端先拉取一个“可导出列配置”的接口,这个接口返回当前用户权限范围内所有可导出的列,包括字段名、显示名、默认勾选状态、是否必选。前端渲染成checkbox列表,用户勾选后把列配置列表传回后端。后端拿到后,第一步是校验字段名是否在可导出白名单里,第二步是补上权限不允许的列直接丢弃,第三步才是构建dynamicheadcolumn。
这一步如果做反了——比如后端信任前端传来的字段名直接查库——风险非常大。轻则导出的数据列错位,重则通过导出接口把敏感字段带出去。所以一定要在后端做一次列白名单过滤,别贪图省事。
6.2 安全性与默认列兜底
动态能力越灵活,越需要一个兜底策略。我遇到过前端浏览器的localstorage被清理,用户之前勾选的列配置丢失,这时候再进导出页面,前端拿到的还是旧的配置列表,传给后端的列配置里可能包含已经下线、停用的字段。
保险做法是后端维护一份默认列配置。当前端传来的配置为空或者非法字段过多时,自动回退到默认列。这样用户的导出行为不会因为配置异常直接失败,最多就是列少一点。默认列配置放在后端代码里或者数据库配置表里都可以,我建议放配置中心或数据库,方便运营同学直接改,不用发版。
6.3 后续可扩展方向
这套工具类后续可以扩展的方向有不少。比如支持导出下拉框、日期选择器这类单元格约束,用easyexcel的 sheetwriterhandler 可以动态生成数据有效性。再比如复杂表头里需要跨列合并的场景,比如“时间”下面挂三列,这是自定义合并策略的事,我已经在计划里排上了。
我个人在实际操作中的体会是,动态表头工具类最难的不是写代码,而是想清楚“列从哪里来、数据怎么对应、空数据怎么办、用户瞎传配置怎么办”这几个问题。把边角问题处理干净,再套到业务场景里,基本不会出大问题。如果你也在重构excel导出,建议不要一开始就追求多级表头和复杂样式,先把单层动态表头跑通,再一步步往上叠能力,这样踩坑最少,维护起来也最轻松。
以上就是java基于easyexcel实现动态表头excel导出工具类的完整示例代码的详细内容,更多关于java easyexcel导出excel动态表头的资料请关注代码网其它相关文章!
发表评论