ARTICLE DETAIL

资讯详情

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

EasyExcel单元格样式不生效?从POI共享模型到拦截器全解析

EasyExcel单元格样式不生效?从POI共享模型到拦截器全解析 做Java导出功能这些年EasyExcel用得不算少群里最常被翻牌的问题除了“大数据量怎么优化”就是“EasyExcel自定义单元格样式不生效”。这问题看起来简单实际上牵涉到拦截器机制、POI底层样式对象模型、注解和模板的优先级关系只靠搜索引擎里的零散答案很难一次解决。今天我把这块彻底掰开先说清楚为什么你的样式“纹丝不动”再给可以直接抄走的解法最后附上一个高频问题速查表。适合正在被导出功能折磨的Java开发也适合刚接触EasyExcel但想搞懂样式原理的同学。1. 问题现场样式为什么“纹丝不动”1.1 这个经典问题的几种表现很多时候大家来问“样式不生效”描述的现象是完全不同的而不同现象的根因并不一样。我大概归纳一下常见的几类第一种你在代码里写了CellWriteHandler也注册了程序执行也不报错但导出的Excel打开一看该红的没红、该粗的没粗就跟没写过样式一样。这种最诡异明明逻辑都跑了怎么就没效果第二种用ContentStyle注解给某个字段设置了背景色导出后确实有变化但变化得莫名其妙——比如整张表都变成了同一个颜色或者颜色和注解里定义的完全对不上。第三种表头样式正常数据区样式失效或者第一行有效后面行全部失效。这种“半灵不灵”的现象最容易误导人让你觉得是代码写得不够“深”。第四种Excel打开正常WPS打开就丢样式这种一般不是代码问题但排查起来也费劲。如果你遇到的是其中任何一种别急着在拦截器里写一堆setFillForegroundColor试试看。先搞懂EasyExcel到底是怎么处理样式的后面所有问题都迎刃而解。1.2 先从EasyExcel的架构理解根因EasyExcel本质上是阿里开源的对Apache POI的二次封装核心卖点是“低内存写大数据量Excel”。怎么做到低内存答案是“复用”。它不会为每一行数据的每一个单元格创建一个全新的样式对象而是尽量让整张表的单元格引用同一个或少数几个CellStyle实例。但POI的CellStyle在同一个Workbook里是共享对象不是“每个单元格独享一份样式副本”。你可以理解成整张Excel表格的员工都住在集体宿舍所谓单元格样式就是宿舍的装修风格。你拿到一个CellStyle对象改了颜色等于把整间宿舍刷了漆所有住在这间宿舍的单元格全跟着变。这就导致了两个直接后果第一你以为你在改“当前单元格”的样式实际上可能把几十个单元格的样式一起改了最后呈现出来的效果就是“整个表都花了”。第二如果你在拦截器里修改了EasyExcel内部缓存复用的那个CellStyle那可能后面还没来得及写的行也会受影响。尤其是大数据量导出时新的行不断引用这个被修改的样式最后导出文件里到处是这个样式看起来就像“样式不生效”其实是“样式泛滥”。所以真正稳定的做法只有一种在拦截器里获取到单元格当前样式之后不要直接改它而是基于它复制出一个新的样式对象在新对象上设置你想要的属性最后再setCellStyle给当前单元格。这也是我后面所有示例代码的核心逻辑。2. 自定义样式不生效的根源排查2.1 核心机制WriteHandler到底什么时候能碰单元格要正确设置样式必须先搞明白WriteHandler的生命周期。EasyExcel提供了几个回调钩子beforeSheetCreate/afterSheetCreatesheet创建前后的钩子此时还没有行和单元格。beforeRowCreate/afterRowCreate行创建前后的钩子。beforeCellCreate单元格创建之前触发此时Cell对象还不存在只能通过row自己createCell。afterCellDispose单元格数据处理完之后触发这是最关键的一个钩子英文文档里的“dispose”指的是“处理完单元格生命周期”。afterRowDispose整行处理完之后触发。真正适合做自定义单元格样式的是afterCellDispose。原因很简单在这个节点你要的Cell对象已经存在了数据也已经写进去了你可以随意修改这个单元格的样式而不影响数据的正常写入。如果你在beforeCellCreate里强行自己createCell然后设置样式那EasyExcel后面写数据时可能会用内部逻辑再对单元格做覆盖导致你设的样式被冲掉。还有一个常见误区是在afterRowDispose里遍历整行去设置样式这样虽然也能弄但如果你需要根据单元格的值来判断样式在这个阶段数据已经被封装成了字符串拿到的东西就不太“原汁原味”了。所以面对“自定义单元格样式不生效”的问题第一步就是确认你的代码是不是写在afterCellDispose里不在这个回调链路上的写法不生效是大概率事件。2.2 最容易踩的坑直接修改共享CellStyle我见过最多、也是最隐蔽的错误代码长这样public class BugStyleHandler implements CellWriteHandler { Override public void afterCellDispose(CellWriteHandlerContext context) { Cell cell context.getCell(); CellStyle style cell.getCellStyle(); // 危险这是共享对象 style.setFillForegroundColor(IndexedColors.YELLOW.getIndex()); style.setFillPattern(FillPatternType.SOLID_FOREGROUND); // 没有 setCellStyle你可能会想我改的本来就是单元格的style啊 } }这段代码在某些版本上“碰巧”会有局部效果比如当前单元格好像变黄了但其他引用同一个CellStyle的单元格也会跟着变黄。更麻烦的是如果EasyExcel内部为了让内存低而复用了同一个CellStyle那么这次修改可能把后面还没创建的单元格样式也污染了最终你看到的可能就是“整张表全黄了”或者“最后一行把前几行样式顶掉了”。正确的姿势是拿到当前样式以后先新建一个样式对象再把当前样式的属性复制过来修改完之后重新setCellStyle给这个单元格。CellStyle newStyle workbook.createCellStyle(); newStyle.cloneStyleFrom(cell.getCellStyle()); newStyle.setFillForegroundColor(IndexedColors.YELLOW.getIndex()); newStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND); cell.setCellStyle(newStyle);这样改完后只有当前单元格用的是新样式其他单元格的样式完全不受影响。这个思路是解决EasyExcel样式不生效问题的总钥匙。2.3 三个维度判断你的代码为什么没生效代码不生效先从三个维度自查绝大多数问题都藏在这里。第一个维度handler到底注册了没有。registerWriteHandler这个环节太容易被忽略了。很多人把handler类写出来了但是写Excel的时候忘了在链路上注册那handler就像个没有接到电源的电风扇转都不转。注册代码很简单EasyExcel.write(outputStream, Data.class) .registerWriteHandler(new CustomStyleHandler()) .sheet(模板) .doWrite(dataList);建议在排查第一步就在handler里打个日志确认afterCellDispose到底有没有进去。如果日志都没打出来那谈样式修改都是空谈。第二个维度样式属性设置全了没有。POI里很多样式属性是有“前置条件”的。最典型的是背景色只设置fillForegroundColor是不够的必须同时设置fillPattern为SOLID_FOREGROUND背景色才能显示出来。边框也是只设置边框颜色不设置边框样式边框不会出现。第三个维度有没有被其他地方覆盖。如果你注册了多个WriteHandler后注册的执行顺序在后会把前面设置过的样式覆盖掉。另外注解样式、模板自带的样式也会和handler里的样式打架。下一步我们就围绕拦截器的实操来展开。3. 实操手写一个真正生效的样式拦截器3.1 从零实现CellWriteHandler直接给一个可以用的“骨架”public class CustomStyleHandler implements CellWriteHandler { private static final int HEAD_ROW 0; Override public void afterCellDispose(CellWriteHandlerContext context) { Cell cell context.getCell(); Workbook workbook cell.getSheet().getWorkbook(); boolean isHead context.getHead(); // 核心基于原样式复制避免污染共享样式 CellStyle newStyle workbook.createCellStyle(); newStyle.cloneStyleFrom(cell.getCellStyle()); if (isHead) { setHeadStyle(newStyle, workbook); } else { setContentStyle(cell, newStyle); } cell.setCellStyle(newStyle); } private void setHeadStyle(CellStyle style, Workbook workbook) { style.setFillForegroundColor(IndexedColors.DARK_BLUE.getIndex()); style.setFillPattern(FillPatternType.SOLID_FOREGROUND); style.setAlignment(HorizontalAlignment.CENTER); style.setVerticalAlignment(VerticalAlignment.CENTER); Font font workbook.createFont(); font.setFontName(微软雅黑); font.setFontHeightInPoints((short) 11); font.setBold(true); font.setColor(IndexedColors.WHITE.getIndex()); style.setFont(font); } private void setContentStyle(Cell cell, CellStyle style) { // 这里可以按列索引判断也可以按值判断 style.setVerticalAlignment(VerticalAlignment.CENTER); } }这里有两个关键细节一cloneStyleFrom是深拷贝样式属性它会把原样式的字体、边框、对齐、颜色全部复制过来然后你只改你想改的部分。这比从零创建样式要安全得多因为它保留了EasyExcel或模板里预设的默认格式。二字体对象用workbook.createFont()创建不要复用其他单元格的Font对象。POI的Font也是共享对象直接改一个已有Font同样会殃及池鱼。创建完Font之后通过style.setFont(font)关联到新样式上。注册方式和前面一样EasyExcel.write(response.getOutputStream(), OrderVO.class) .registerWriteHandler(new CustomStyleHandler()) .sheet(订单明细) .doWrite(orderList);3.2 表头与正文样式分开处理业务里最常见的需求是表头深色底、白字、加粗、居中正文只需要默认边框或者特定列对齐。我建议在handler里把表头和正文拆成两个私有方法而不是混在一起写。拿订单导出举个例子。表头统一用深蓝背景、白色加粗字数据区“订单状态”列根据值做不同背景色“金额”列右对齐并保留两位小数格式。afterCellDispose里通过context.getHead()可以直接判断当前是不是表头单元格。如果你用的是EasyExcel 3.x这个方法很稳定如果是2.x老项目可以用cell.getRowIndex() 0来判断表头行。在数据区里判断具体是哪一列时用cell.getColumnIndex()这个索引和实体类字段顺序有关不一定是你Excel里看到的第几列这点要注意。比如实体里status字段是第6个字段索引5那就在索引等于5时做特殊处理private void setContentStyle(Cell cell, CellStyle style) { int column cell.getColumnIndex(); if (column 5) { String value cell.getStringCellValue(); if (未支付.equals(value)) { style.setFillForegroundColor(IndexedColors.RED.getIndex()); style.setFillPattern(FillPatternType.SOLID_FOREGROUND); } else if (已支付.equals(value)) { style.setFillForegroundColor(IndexedColors.GREEN.getIndex()); style.setFillPattern(FillPatternType.SOLID_FOREGROUND); } } }这里有个排查点cell.getStringCellValue()在单元格类型不是字符串时会抛异常或者拿到空值。如果判断不生效先确认你拿到的实际值是“已支付”还是带空格或者换行的“已支付”。3.3 动态条件样式按行、列、值做差异化展示除了按列固定设置真实项目里更常见的是“按数据内容动态设置样式”。举几个我实际遇到过的需求对账明细里金额超过10万的整行标红。成绩单里低于60分的单元格标红、90以上标绿。库存预警表里库存量为0的单元格加粗并填充灰色。这些需求本质都是一样的提前知道哪些行、哪些列需要特殊样式或者根据单元格的值在运行期判断。如果数据量不大直接在afterCellDispose里读cell.getStringCellValue()或者cell.getNumericCellValue()来判断是最省事的。如果数据量大频繁调用getNumericCellValue来回转换可能有些性能损耗更好的做法是在调用doWrite之前把“需要特殊标红的行号集合”算出来传给handlerpublic class DynamicStyleHandler implements CellWriteHandler { private final SetInteger redRowSet; public DynamicStyleHandler(SetInteger redRowSet) { this.redRowSet redRowSet; } Override public void afterCellDispose(CellWriteHandlerContext context) { if (context.getHead()) { return; } int rowIndex context.getCell().getRowIndex(); if (redRowSet.contains(rowIndex)) { Cell cell context.getCell(); Workbook workbook cell.getSheet().getWorkbook(); CellStyle newStyle workbook.createCellStyle(); newStyle.cloneStyleFrom(cell.getCellStyle()); newStyle.setFillForegroundColor(IndexedColors.RED.getIndex()); newStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND); cell.setCellStyle(newStyle); } } }需要注意这里说的“行号”是指包括表头在内的绝对行号。如果你的数据是从第1行开始而表头占了第0行那第1行才是第一条数据。我建议在构建redRowSet的时候直接加1偏移或者根据context.getRowIndex()和表头占位做计算别搞错。4. 注解样式与拦截器样式的分工与冲突4.1 HeadStyle / ContentStyle 的正确打开方式EasyExcel提供了HeadStyle、ContentStyle这类注解用来快速给字段设置静态样式。比如给某个字段设置背景色public class OrderVO { ContentStyle(fillForegroundColor 10, fillPatternType FillPatternType.SOLID_FOREGROUND) private String status; }注意fillForegroundColor这个属性接收的是IndexedColors的索引值不是RGB颜色值。10对应的是IndexedColors.RED之类的枚举索引写起来比较抽象。很多人直接填了0导致显示黑色或者不显示就以为是样式不生效。注解适合做“静态样式”也就是这个字段不管值是什么样式都一样。一旦你需要根据数据动态变化注解就非常别扭。比如“已支付显示绿色未支付显示红色”注解做不到。这种就必须用拦截器。我的经验是能用注解解决的简单静态样式就用注解涉及动态判断的样式全部用拦截器。不要把两种方案混在一起用在同一个字段上会互相干扰。4.2 注解、拦截器、模板三者的优先级关系根据实际执行结果我把样式优先级从低到高整理成了这样优先级样式来源说明1EasyExcel默认样式所有单元格最初共享的样式属性固定2注解HeadStyle/ContentStyle在创建单元格时根据注解设置3后注册的WriteHandler在afterCellDispose里最终设置4模板预置样式如果用了模板填充模板里已有的样式会和代码叠加简单来说拦截器是后执行的它会覆盖注解设置的样式。所以如果同时写注解和拦截器最后以拦截器设置的为准。模板预置样式的情况比较复杂因为Excel原生格式的优先级比代码写得高尤其是合并单元格和设置了固定行高的情况下代码改样式不一定能顶掉模板的原始设定。这也就解释了一个常见现象你在模板里手工把表头背景色设成了黄色然后代码里又用拦截器把它改成蓝色最后导出发现还是黄色。这不是代码没执行而是模板的样式优先级更高覆盖了你代码里的设置。遇到这种需求最简单的办法是代码里不要依赖模板的这些样式直接用EasyExcel.write()生成sheet不走模板填充。4.3 为什么注解设置了背景色还是没变化注解看着简单坑也不少。我总结几个高频原因第一个fillPatternType没有设置。前面提过POI里只有fillForegroundColor而没有fillPattern背景色不会显示。注解里也要同时写fillPatternType FillPatternType.SOLID_FOREGROUND才行。第二个颜色索引值不对。fillForegroundColor填的是索引值不是颜色名称。你在IDE里看到IndexedColors.RED.getIndex()返回的是几十这个数字直接手写一个10可能根本不是你想要的颜色。第三个字段值是null。部分版本的EasyExcel在处理null字段时可能会跳过样式设置。你看到单元格是空的本以为应该显示底色结果完全没有。处理方式就是给null值赋空字符串保证单元格有真实的值。第四个被其他handler覆盖。如果你也注册了全局的样式handler它对每个单元格都设置了新的样式而新的样式是从默认样式复制的并没有保留注解设置的背景色那注解等于白写了。这也是很多人“加了注解还不行”的真正原因。5. 常见问题与排查技巧实录5.1 五类高频故障速查表为了方便你对照排查我整理了一个速查表故障现象可能原因解决方法样式完全没生效handler未注册或执行中异常被吞在handler里加日志确认afterCellDispose进入只有表头有效数据区没变只处理了head分支漏掉了数据区用context.getHead()区分两个分支都写设置背景色后还是没有颜色只设了fillForegroundColor没设fillPattern同时设置FillPatternType.SOLID_FOREGROUND整张表格变成同一种背景色直接修改了共享CellStyle对象用cloneStyleFrom复制后建新样式再setCellStyleWPS打开丢失Excel打开正常WPS兼容性问题或渲染差异用Excel验证为准颜色改用IndexedColors基础色这五类问题覆盖了日常90%以上的“样式不生效”现场。5.2 WPS打开丢样式Excel打开正常是怎么回事这个现象特别迷惑人。你辛辛苦苦调了半天代码里也确认执行了用Microsoft Excel打开文件一看样式好好的。但你用WPS打开颜色没了、边框不规范、字体对不齐。这时候千万别以为代码还有问题问题出在WPS对部分Excel样式属性的渲染和Excel不一样。我遇到过最典型的是自动换行和行高问题Excel里设置了wrapTexttrue但行高没有设置为“自动调整”在Excel里会自适应显示WPS里可能就出现文字被截断看起来像是样式“不生效”。还有一个是索引色差异某些IndexedColors的颜色在WPS主题色板里映射出来和Excel不一样会对不上。遇到这种问题第一原则以Excel显示为准确认代码正确性。第二原则如果客户环境只用WPS尽量选择通用的基础色比如红、黄、绿、蓝这几个最常用的索引色避免使用冷门索引色和特殊线条样式。第三原则WPS有缓存改代码以后重新导出文件建议清一下WPS的本地缓存或换个文件名否则你可能看到的是旧文件。5.3 性能陷阱百万行数据下的样式内存控制样式问题不只是“不生效”和“乱掉”还有个容易被忽略的坑是性能。POI的CellStyle对象数量直接影响内存和生成文件的大小。如果你在afterCellDispose里对每个单元格都执行workbook.createCellStyle()加cloneStyleFrom几万行数据下来就会创建好几万个样式对象轻则导出变慢、文件体积暴涨重则直接OOM。正确做法是样式池化把需要使用的样式提前创建好放进Map里按条件直接取用。例如只创建表头样式、默认数据样式、红色警告样式、绿色完成样式这四种然后在handler里根据条件取对应样式private final MapString, CellStyle styleCache new HashMap(); private CellStyle getStyle(Workbook workbook, String key) { return styleCache.computeIfAbsent(key, k - createStyle(workbook, k)); }这样无论数据量多大整个导出过程只创建固定数量的CellStyle对象内存占用可控。我做过一次10万行报表导出全部样式池化后只创建了5个样式对象导出速度非常快文件大小也从动不动几MB降到几百KB。对于大数据量导出这条经验特别重要。6. 我对这个问题的最终看法我的个人体会是EasyExcel样式问题的根源不在EasyExcel本身而在于POI的共享对象模型被框架的封装给“藏起来”了。很多人不了解底层拿到CellStyle就当成普通Java对象去修改结果踩进共享对象的大坑。所以遇到“自定义单元格样式不生效”这类问题先别急着搜“万能代码”你真正需要做的是想清楚三件事我在哪个回调里改样式我拿到的CellStyle是不是共享对象我设置的属性是否被其他优先级更高的东西覆盖了这三个问题搞明白90%的样式问题都能自己解决。最后再分享一个小技巧如果项目里Excel样式特别复杂比如几十个字段、各种颜色字体边框交错我会优先考虑“模板填充”方案——把一个已做好所有样式的xlsx模板放在resources下用EasyExcel的fill方法把数据填进去。这样样式全部由Excel原生模板掌控代码几乎不碰CellStyle既稳定又高效。这套方案绕开了拦截器、注解的各种边界情况是我现在最推荐的新项目做法。
返回列表