十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

EasyExcel 2.2.10 指定行与指定列样式设置实战指南

EasyExcel 2.2.10 指定行与指定列样式设置实战指南 1. 先说结论EasyExcel 2.2.10 的样式机制为什么单独的行和列这么难搞用 EasyExcel 2.2.10 做过导出的人应该都有这个感受普通导出太简单了实体类加几个注解一行doWrite就把本地文件写出来了。但一旦涉及到给指定行、指定列添加样式你就发现官方文档能给的东西非常有限。它默认只给你一个水平方向上的整体样式策略也就是所有表头统一一套样式所有数据区统一一套样式。你没法直接说第 3 行给我标黄一下金额这一列给我染个浅蓝色。先说清楚EasyExcel 底层的样式机制其实并不复杂。整个写入过程本质上是把数据封装成 POI 的 Workbook再落盘到本地文件。而样式处理EasyExcel 留了一个处理器handler的口子。你在写出的时候不断registerWriteHandler框架就会按照注册顺序在创建单元格、写入单元格、处理完单元格等节点回调你的处理器。掌握了这套回调机制指定行指定列就只是你在回调里判断行号、列号的问题根本不需要去改 EasyExcel 源码也不用绕道去用 POI 重写文件。这篇博客的目标读者很明确已经在用 EasyExcel 2.2.x 做本地文件的读写、现在被某一行、某一列单独上色这类需求卡住的人。我会直接给出完整代码和参数说明顺带把我踩过的坑一起讲了。2. 环境准备依赖、实体类和一个最小可运行的本地文件写出2.1 Maven 依赖和 POI 版本问题项目里引入 EasyExcel 2.2.10只需要一个依赖dependency groupIdcom.alibaba/groupId artifactIdeasyexcel/artifactId version2.2.10/version /dependency这里有个隐藏信息容易忽略easyexcel 2.2.10 会传递依赖 POI 4.1.2。为什么要强调这个因为下面写自定义样式的时候我们用的是 POI 底层的CellStyle、Workbook、FillPatternType这些类不同 POI 大版本之间的 API 有差异。尤其注意 4.x 里CellStyle.cloneStyleFrom()已经标记为过时推荐用copyFrom()。你用 2.2.10 自带的 POI 4.1.2代码里就该写copyFrom而不是去网上抄一个基于 POI 3.x 的老写法。建议动手前先看一眼本地依赖树确认 POI 版本没被项目里其他依赖顶掉。那种样式明明写了却不生效的诡异问题大概率就是 POI 版本被覆盖后 API 行为变了。2.2 实体类设计样式的核心载体是单元格单元格由实体类字段映射生成。所以先定义好实体Data public class OrderExcel { ExcelProperty(订单号) private String orderNo; ExcelProperty(客户名称) private String customerName; ExcelProperty(商品名) private String product; ExcelProperty(数量) private Integer quantity; ExcelProperty(金额) private BigDecimal amount; ExcelProperty(状态) private String status; }这里我故意没加ColumnWidth、ContentStyle这些注解因为本次需求是动态的指定行、指定列注解方案是静态的、列级别的两者配合起来再看情况。有一点可以提前说ContentStyle、HeadStyle这类注解在 2.2.10 里已经存在如果你的样式是固定配好的用注解是最省事的但注解管不了行顶多对某一列生效所以千万别指望靠注解完成所有花活。2.3 最小的本地文件写出代码后面的所有样式方案都基于这段基准代码扩展String targetPath D:/reports/order_report.xlsx; ListOrderExcel orderList buildOrders(); EasyExcel.write(targetPath, OrderExcel.class) .sheet(订单明细) .doWrite(orderList);这个targetPath就是本地文件的落点。EasyExcel 会自己创建FileOutputStream去写这个路径目录必须存在且可写不然运行时会直接报FileNotFoundException。如果你要覆盖已有文件它会直接重建不会像 Word 那样弹警告这点在报表场景里往往是个坑后面讲常见问题再细说。3. 入门方案HorizontalCellStyleStrategy 全局设置表头和数据区样式3.1 为什么先做全局样式很多人的需求其实是两层先要一个整体上说得过去的基础样式再对个别行或列做重点突出。全局样式用HorizontalCellStyleStrategy就够了它也是 EasyExcel 官方文档里唯一写得比较详细的样式方案。它做的事情本质上是对表头区域和数据区域各套一个WriteCellStyle。这个策略本身就是通过registerWriteHandler注册进去的一个特殊 WriterHandler所以理解它也就理解了整个 handler 机制的入口。3.2 WriteCellStyle 和 WriteFont 的配置看代码WriteCellStyle headStyle new WriteCellStyle(); headStyle.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex()); headStyle.setFillPatternType(FillPatternType.SOLID_FOREGROUND); WriteFont headFont new WriteFont(); headFont.setBold(true); headFont.setFontHeightInPoints((short) 12); headStyle.setWriteFont(headFont); WriteCellStyle contentStyle new WriteCellStyle(); contentStyle.setHorizontalAlignment(HorizontalAlignment.CENTER); contentStyle.setVerticalAlignment(VerticalAlignment.CENTER); HorizontalCellStyleStrategy styleStrategy new HorizontalCellStyleStrategy(headStyle, contentStyle); EasyExcel.write(targetPath, OrderExcel.class) .registerWriteHandler(styleStrategy) .sheet(订单明细) .doWrite(orderList);几个容易记混的细节setFillForegroundColor接收的是IndexedColors枚举的索引值颜色深浅和打印效果要看实际情况报表场景我一般推荐浅色系比如LIGHT_YELLOW、LIGHT_BLUE、GREY_25_PERCENT深色背景一旦打印或者转 PDF很容易把字吃掉。setFillPatternType(FillPatternType.SOLID_FOREGROUND)必须配上前景色的设置否则颜色设置不生效。这是 POI 历史上最容易踩的空手坑。边框、数据格式、自动换行也都挂在WriteCellStyle上比如金额列要显示千分位可以配合实体字段的NumberFormat(#,##0.00)或者直接给整列数据区域加数据格式。3.3 全局方案的局限HorizontalCellStyleStrategy的定位是横向一致性。它让所有表头长得一样、所有数据行长得一样但无法做到指定行指定列更细的差异化。比如你想在数据区的第一行前面插一行合计或者把状态这一列里值为异常的单元格标红这个策略就帮不上忙了。从整个需求来看全局策略应该当成地基来用指定行、指定列是盖在上面的装修。正确的组合方式后面第 6 章会讲。4. 核心方案一自定义 CellWriteHandler 给指定行加样式4.1 CellWriteHandler 回调参数全解析在 2.2.10 里官方主推的新版写入处理器接口是com.alibaba.excel.write.handler.CellWriteHandler它一共有四个默认方法做样式最常用的是afterCellDispose也就是单元格内容处理完毕之后回调default void afterCellDispose(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Cell cell, Head head, Integer relativeRowIndex, Boolean isHead) {}这个方法里的参数是把样式写到指定行、指定列的关键参数含义典型用法cellPOI 的Cell对象能拿到行号、列号、值cell.getRowIndex()、cell.getColumnIndex()head当前列的Head元信息能拿到表头名head.getColumnName()relativeRowIndex数据区域内的相对行号从 0 开始判断第几条数据isHead当前单元格是否属于表头区域排除表头或专门处理表头这里必须分清两个行号cell.getRowIndex()是整个 Sheet 的绝对行号从第 0 行开始relativeRowIndex是数据区域里的相对行号。假如表头占了 1 行那么第一条数据的绝对行号是 1相对行号是 0。表头占了 2 行第一个数据行的绝对行号就变成 2相对行号还是 0。用哪个取决于你的需求描述。你想说Excel 里的第 3 行那就用绝对行号你想说数据区域里的第 1 条记录就用相对行号。4.2 指定行样式的完整实现直接上代码。这个处理器接收一个需要高亮的行号集合以及一个颜色索引会把匹配到的整行所有单元格统一设置背景色public class SpecifiedRowStyleHandler extends AbstractCellWriteHandler { private final SetInteger rowIndexSet; private final short fillColor; private CellStyle cacheStyle; public SpecifiedRowStyleHandler(SetInteger rowIndexSet, short fillColor) { this.rowIndexSet rowIndexSet null ? new HashSet() : rowIndexSet; this.fillColor fillColor; } Override public void afterCellDispose(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Cell cell, Head head, Integer relativeRowIndex, Boolean isHead) { if (Boolean.TRUE.equals(isHead)) { return; } if (!rowIndexSet.contains(cell.getRowIndex())) { return; } Workbook workbook cell.getSheet().getWorkbook(); if (cacheStyle null) { cacheStyle workbook.createCellStyle(); cacheStyle.copyFrom(cell.getCellStyle()); cacheStyle.setFillForegroundColor(fillColor); cacheStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND); } cell.setCellStyle(cacheStyle); } }然后在使用时注册进去SetInteger highlightRows new HashSet(); highlightRows.add(1); // 表头占1行时这是数据区第1行 highlightRows.add(12); // 第13行比如合计行 EasyExcel.write(targetPath, OrderExcel.class) .registerWriteHandler(styleStrategy) .registerWriteHandler(new SpecifiedRowStyleHandler(highlightRows, IndexedColors.LIGHT_YELLOW.getIndex())) .sheet(订单明细) .doWrite(orderList);这段代码里有几个地方要展开讲第一为什么用cacheStyle缓存而不是每个单元格都新建CellStyle因为 POI 的CellStyle对象和Workbook强关联创建太多会占用较多内存。如果数据量几万行你又高亮了几千行逐格创建是一笔不小的开销。缓存同一个CellStyle直接套上去性能上安全很多。第二copyFrom(cell.getCellStyle())的作用是把当前单元格的原有样式字体、边框、对齐方式等复制到新样式再叠加颜色。如果直接workbook.createCellStyle()然后只设前景色这个单元格的字体、边框全部会丢失。这个细节很容易被忽略我见过不少人样式一改整张表的边框全没了。第三cacheStyle的基准样式来自第一个被匹配到的单元格。如果几个被高亮的行之间原本样式就不同统一套同一个cacheStyle会导致它们之间的差异被抹平。报表场景里这通常无所谓但如果你要求每一行保持各自原有的边框、字体那就要改成逐格复制样式放弃缓存数据量大时需要用其他方式做性能补偿。4.3 行高、行内所有单元格都要照顾到指定行加样式除了背景色经常还涉及到行高。比如标题行要加高合计行要突出。行高可以用RowWriteHandler来做它专门在行处理完的时候回调public class SpecifiedRowHeightHandler extends AbstractRowWriteHandler { private final MapInteger, Float rowHeightMap; public SpecifiedRowHeightHandler(MapInteger, Float rowHeightMap) { this.rowHeightMap rowHeightMap; } Override public void afterRowDispose(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Row row, Integer relativeRowIndex, Boolean isHead) { Float height rowHeightMap.get(row.getRowNum()); if (height ! null) { row.setHeightInPoints(height); } } }行高和背景色分开做的好处是职责单一。特别是当你只想调行高、不想动颜色或者只想调颜色、不想动行高的时候不会互相干扰。还有一个容易忽略的点一个指定行如果数据本身只有 5 列但表格有 6 列第 6 列没有值时afterCellDispose未必会对不存在的空单元格回调。也就是说你可能只给前 5 个单元格上了色第 6 列没有背景色看起来像一块补丁。这种场景需要你在afterRowDispose里手动补建空单元格再赋样式或者干脆接受现状。我这边的建议是做报表时先确认每行数据的字段完整性要么把所有列都填值要么通过afterRowDispose统一补齐样式。5. 核心方案二自定义 CellWriteHandler 给指定列加样式5.1 按列下标直接染列给指定列加样式最粗暴的方式是判断cell.getColumnIndex()。列下标从 0 开始第 5 列就是下标 4。public class SpecifiedColumnStyleHandler extends AbstractCellWriteHandler { private final SetInteger columnIndexSet; private final short fillColor; private CellStyle cacheStyle; public SpecifiedColumnStyleHandler(SetInteger columnIndexSet, short fillColor) { this.columnIndexSet columnIndexSet null ? new HashSet() : columnIndexSet; this.fillColor fillColor; } Override public void afterCellDispose(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Cell cell, Head head, Integer relativeRowIndex, Boolean isHead) { if (Boolean.TRUE.equals(isHead)) { return; } if (!columnIndexSet.contains(cell.getColumnIndex())) { return; } Workbook workbook cell.getSheet().getWorkbook(); if (cacheStyle null) { cacheStyle workbook.createCellStyle(); cacheStyle.copyFrom(cell.getCellStyle()); cacheStyle.setFillForegroundColor(fillColor); cacheStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND); } cell.setCellStyle(cacheStyle); } }用的时候SetInteger highlightColumns new HashSet(); highlightColumns.add(4); // 金额列下标从0开始 EasyExcel.write(targetPath, OrderExcel.class) .registerWriteHandler(styleStrategy) .registerWriteHandler(new SpecifiedColumnStyleHandler(highlightColumns, IndexedColors.LIGHT_BLUE.getIndex())) .sheet(订单明细) .doWrite(orderList);按下标实现很简单但有个致命问题一旦实体类字段顺序调整或者表结构里私下多塞了一列高亮列就错位了。所以它只适合字段结构极其稳定的内部系统。5.2 按表头名动态定位列更稳妥的做法是根据head.getColumnName()来定位列。这样哪怕列的位置换了只要表头名还叫金额渲染就不会错。public class SpecifiedColumnByNameStyleHandler extends AbstractCellWriteHandler { private final ListString columnNameList; private final short fillColor; private CellStyle cacheStyle; public SpecifiedColumnByNameStyleHandler(ListString columnNameList, short fillColor) { this.columnNameList columnNameList null ? Collections.emptyList() : columnNameList; this.fillColor fillColor; } Override public void afterCellDispose(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Cell cell, Head head, Integer relativeRowIndex, Boolean isHead) { if (Boolean.TRUE.equals(isHead) || head null) { return; } if (!columnNameList.contains(head.getColumnName())) { return; } Workbook workbook cell.getSheet().getWorkbook(); if (cacheStyle null) { cacheStyle workbook.createCellStyle(); cacheStyle.copyFrom(cell.getCellStyle()); cacheStyle.setFillForegroundColor(fillColor); cacheStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND); } cell.setCellStyle(cacheStyle); } }注意head可能为 null尤其是当你表里某列根本没有映射到实体字段的时候不判空直接调head.getColumnName()会空指针。另外getColumnName()返回的是复杂表头里最底层那一个表头名这个行为在多层表头时很重要后面第 6 章会细说。建议日常都用按表头名的方式维护成本低逻辑也更贴近业务语义。5.3 按单元格值做条件样式指定列的另一个常见玩法是这个列里满足条件的单元格才变色。比如金额超过 1000 的标红状态为异常的标红。这个其实也是在列判断基础上再加一层值判断public class AmountConditionStyleHandler extends AbstractCellWriteHandler { Override public void afterCellDispose(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Cell cell, Head head, Integer relativeRowIndex, Boolean isHead) { if (Boolean.TRUE.equals(isHead) || head null) { return; } if (!金额.equals(head.getColumnName())) { return; } double value extractDoubleValue(cell); if (value 1000) { return; } Workbook workbook cell.getSheet().getWorkbook(); CellStyle cellStyle workbook.createCellStyle(); cellStyle.copyFrom(cell.getCellStyle()); cellStyle.setFillForegroundColor(IndexedColors.RED.getIndex()); cellStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND); cell.setCellStyle(cellStyle); } private double extractDoubleValue(Cell cell) { if (cell.getCellType() CellType.NUMERIC) { return cell.getNumericCellValue(); } if (cell.getCellType() CellType.STRING) { try { return Double.parseDouble(cell.getStringCellValue()); } catch (NumberFormatException e) { return 0.0; } } return 0.0; } }这里提醒一句afterCellDispose阶段单元格里已经是写入后的值EasyExcel 默认会把 BigDecimal 转成数字写入所以CellType.NUMERIC是最常见的。但如果你在实体字段上挂了NumberFormat写出来的单元格可能是字符串所以判断类型时要考虑两种情况别一上来就getNumericCellValue()。条件样式本质上是在定位之后再加一个判断从这往后你可以任意扩展逻辑根据业务字段判断、根据相邻单元格判断、甚至根据当前行的其他列数据联动判断。这已经是写报表样式时最灵活的一套玩法了。6. 组合玩法与常见坑注册顺序、合并单元格、大数据量6.1 多个 handler 注册的顺序决定了样式的覆盖关系registerWriteHandler注册的处理器会按注册顺序依次执行。所以如果你把HorizontalCellStyleStrategy放在自定义 handler 后面全局策略设定的样式就会覆盖掉你刚刚给指定行列设的颜色。反过来HorizontalCellStyleStrategy放在前面等于先打好地基再让自定义 handler 去做重点装修这才是正确顺序。我见过有人搞反了排查半天最后把注册顺序换一下就解决了。建议把全局策略固定放在最前面所有的精细化 handler 往后排EasyExcel.write(targetPath, OrderExcel.class) .registerWriteHandler(styleStrategy) // 先全局 .registerWriteHandler(new SpecifiedRowStyleHandler(...)) // 再行 .registerWriteHandler(new SpecifiedColumnByNameStyleHandler(...)) // 后列 .sheet(订单明细) .doWrite(orderList);行和列处理器如果都命中了同一个单元格谁后注册谁生效。比如某一行和第 5 列交叉的那个格子你既在行集合里又在列集合里最后显示的颜色由后一个 handler 决定。6.2 复杂表头场景下的注意点easyexcel 复杂的表头导入这件事本身就和样式有强关联。复杂表头通常用嵌套ExcelProperty实现ExcelProperty(value {基本信息, 订单号}) private String orderNo; ExcelProperty(value {基本信息, 客户名称}) private String customerName;这种写法下表头区域会产生横向或纵向的合并单元格。head.getColumnName()返回的是最底层的表头名比如订单号客户名称这一点和普通表头一致所以按表头名定位列的 handler 在复杂表头下仍然能正常工作。但要注意合并表头里的中间层单元格不一定会在afterCellDispose回调里按你的直觉逐一触发。EasyExcel 内部对合并区域的处理和普通单元格不同合并区域的样式经常只体现在区域的左上角单元格上。所以处理复杂表头时建议先用一个两条数据的样例文件跑一遍打开生成的 Excel 看看哪些单元格回调了、哪些没回调再决定你的样式策略。别一上来就在复杂表头上写几百行的样式逻辑踩了合并单元格的坑会很痛。6.3 数据量变大之后的性能问题本地文件动辄几万行时样式处理就成了导出性能的一个包袱。主要瓶颈有两个一个是CellStyle对象的创建这也是全文中反复强调缓存的原因。一个Workbook里CellStyle数量很多的话文件体积会明显膨胀而且 Excel 的样式上限是 65430 个左右超过会直接报错。所以高亮规则最好收敛到有限的几种组合把样式实例提前缓存好。另一个是我前面提到的情况如果要求每个单元格都保留自身原有边框、字体再叠加颜色你就必须逐格copyFrom这会放大成本。折中的办法是先判断这个格子原本的样式是不是已经在缓存里有对应版本有就直接复用没有再新建。实际项目里我们可以做一个以原始样式指纹为 key 的Map来做样式复用思路就是典型的空间换时间。6.4 本地文件相关的几个隐藏问题文件路径和文件类型要一起说。EasyExcel 写本地文件时文件扩展名和ExcelTypeEnum要匹配。默认EasyExcel.write(path)会根据扩展名推断.xlsx走 XLSX.xls走 XLS。但样式 API 有些在 XLS 老格式下支持不完整报表类需求我建议统一输出.xlsx省得给自己找事。还有一个容易被业务方投诉的点EasyExcel.write(targetPath)在文件已存在时会直接覆盖不会报错。有时候定时任务跑挂了残留了一个只有半截数据的文件下次任务启动直接覆盖是没事的但怕的是任务挂了之后文件没写完你拿半截文件去交付。我现在做这类需求习惯都是先写临时文件比如xxx_tmp.xlsx全部完成后用Files.move原子替换正式文件这样至少不会把坏文件留给下游。6.5 从本地文件先读后写的典型场景标题提到本地文件有一种很常见的完整链路是先读一个本地存量文件加工数据后写成另一个带样式的新文件。这在报表整理场景里太常用了给你一个可直接抄的骨架// Step1 读取本地文件 ListOrderExcel existList EasyExcel.read(D:/data/orders_old.xlsx) .head(OrderExcel.class) .sheet(0) .headRowNumber(1) .doReadSync(); // Step2 业务加工比如筛出待处理订单 ListOrderExcel needList existList.stream() .filter(o - o.getStatus() ! null o.getStatus().contains(待处理)) .collect(Collectors.toList()); // Step3 写到新本地文件并给指定行指定列上样式 EasyExcel.write(D:/output/orders_highlight.xlsx, OrderExcel.class) .registerWriteHandler(styleStrategy) .registerWriteHandler(new SpecifiedRowStyleHandler( new HashSet(Arrays.asList(1, needList.size())), IndexedColors.LIGHT_YELLOW.getIndex())) .registerWriteHandler(new SpecifiedColumnByNameStyleHandler( Collections.singletonList(金额), IndexedColors.LIGHT_BLUE.getIndex())) .sheet(处理结果) .doWrite(needList);这里有一个很容易被忽略的细节doReadSync()读取时如果原有文件里不仅有数据还带了格式、合并单元格之类的东西实体映射会按行数据读样式是读不进来的。所以保留原文件样式再加工这件事EasyExcel 本身做不到只能做读取数据重新生成再套新样式。这算是它的边界知道就好别在这里浪费太多时间。7. 一点个人经验总结最后分享几个我做完这个需求后最想说的感受。样式处理这件事最忌一上来就写一个巨型 handler把行、列、条件、字体、边框全揉在一起。我一开始就是图省事写了一个几百行的SuperStyleHandler结果改一个颜色的需求要动半天还容易影响其他逻辑。后来拆成SpecifiedRowStyleHandler、SpecifiedColumnStyleHandler、SpecifiedRowHeightHandler这种单一职责的小类每次需求变了只改一个类就够了排查问题也快得多。另外建议工作里任何时候都先跑一个最小样例数据只用两三条样式只覆盖你要验证的场景打开生成的xlsx肉眼确认一遍再往上堆复杂度。特别是涉及合并单元格、复杂表头的时候这个习惯能帮你节省大量排查时间。还有一点是关于颜色的选择。浅黄、浅蓝、浅绿这类浅色系打印和电脑上看都不容易刺眼红色我一般留给异常失败这种强警示场景用过一次之后你就会懂为什么深色那么坑。如果你后续还要做更复杂的报表导出可以在这个 handler 机制基础上继续扩展比如根据登录用户动态决定高亮规则、根据导出参数控制是否输出样式等。EasyExcel 2.2.10 虽然是个老版本但这次需求中表现出的稳定性确实没让我失望。
返回列表