Java导出Excel实战:用MyExcel替代POI和EasyExcel,模板填充让报表开发更高效

📅 2026/8/27 1:20:21
Java导出Excel实战:用MyExcel替代POI和EasyExcel,模板填充让报表开发更高效
简介在Java后端开发中Excel导入导出是高频需求如何从繁琐的Apache POI编码中解脱出来成为不少开发者的痛点。EasyExcel虽解决了大文件读取问题但注解依赖和模板表达能力有限。MyExcel作为基于POI的轻量封装工具将Excel视为页面模板通过Freemarker/Beetl引擎渲染实现复杂报表的快速生成。本文从核心API出发拆解ExcelUtils与Writers门面的用法深入模板填充原理结合多Sheet、CSV处理、性能实测及生产环境踩坑经验提供一套完整的Java报表导出选型思路与工程实践方法帮助开发者在数据导出、模板报表、批量处理等场景中少走弯路。 讲一个真事。前阵子我们系统要上线一批报表需求方一口气提了三十多个导出场景有的要按模板出对账单有的要做多Sheet汇总还有的干脆要求CSV直接扔给下游。组里有个同事一开始撸起袖子直接用POI写一个模板一个类写到第五个的时候他已经开始怀疑人生。后来我把这个叫 MyExcel 的工具包丢给他他用了不到一个下午就把剩下二十多个导出场景全清了。那会儿他跟我说了一句话我印象很深这玩意儿比EasyExcel还顺手。今天就把这个工具包掰开揉碎讲一讲结合v4.5.0这个版本的实际表现聊聊它的核心API、模板填充原理、多Sheet与公式应用、性能表现以及我在生产环境里踩过的坑。如果你是做Java后端、经常和Excel导入导出打交道这篇内容应该能帮你省下不少头发。1. 为什么我放弃了手写POI甚至一度想用EasyExcel先说背景。MyExcel是一个基于Apache POI的Java Excel操作工具包GitHub上作者是liaochong坐标是com.github.liaochong:myexcel。它最大的特点是把生成Excel这件事抽象得特别顺手核心入口就一个ExcelUtils再配Writers和Readers两个门面类基本能覆盖95%以上的日常导入导出需求。1.1 一个导出需求引发的崩溃在没有这类工具之前我们写导出通常是这样Workbook workbook new XSSFWorkbook()然后建Sheet建Row建Cell设置样式合并单元格遇到日期还要手动格式化。这套代码单独看一次不复杂但架不住业务场景多。比如订单列表导出字段可能有三十个你要写三十次createCell循环里还要根据字段类型判断是字符串还是数字。等写到对账单导出又要套模板又要合并单元格又要对特定列做求和。代码量直接翻倍。更要命的是每次导出的需求都要重新走一遍这个流程同一个项目里能攒出好几个风格迥异的导出工具类。当时我的状态是看到POI的HSSFWorkbook和XSSFWorkbook就条件反射地头疼。后来也想过用EasyExcel但EasyExcel对模板填充的支持虽然不错API设计的思路还是偏读写分离、注解驱动在根据模板生成复杂报表这个场景下我还是觉得它的表达力不够直接。1.2 EasyExcel的爽与不爽这里说句公道话EasyExcel的SAX模式读取大文件确实是强项官方也宣称解决了OOM问题。但在实际使用时我有几个不太舒服的地方一是它强依赖注解模型。你要导出一个POJO列表得先在类上写一堆ExcelProperty字段一多类上密密麻麻全是注解看着累。二是模板填充能力相对固定。EasyExcel的模板填充基于预置占位符的思路支持list、map的填充但遇到复杂的单元格公式联动、条件格式化、动态合并这种需求写起来比较绕。三是它把写Excel和读Excel两套API分得很开学习成本相对高一些。你要不是天天跟Excel打交道过俩月再来写又得翻文档。1.3 MyExcel解决的是什么MyExcel的定位不太一样。它默认你手头已经有一个人话版的模板你要做的事情是把数据填进去而不是从零开始用代码画格子。它底层还是POI但在上面做了一层非常舒服的封装。核心思路写操作走ExcelUtils.write或Writers门面不需要手动创建Workbook和Cell。读操作走ExcelUtils.read或Readers直接把Excel读成ListMap、ListBean或者ListListObject。模板填充走Freemarker/Beetl模板引擎把Excel模板当成一种页面模板来处理变量替换、循环、条件判断全都能做。这个设计思路我特别喜欢它让Excel导出从写死代码变成了写模板填数据本质上跟做Web页面渲染是同一个套路。把报表格式交给模板把数据逻辑留在Java里各司其职维护起来非常舒服。2. 从依赖到第一张表MyExcel的核心API速通先不说模板咱们先用最原始的代码路径搞定从零创建Excel这个操作。毕竟有些导出场景根本不套模板就是把一堆数据一次性导出来。2.1 Maven依赖怎么加v4.5.0版本在Maven中央仓库可以直接拉到核心依赖加这一个dependency groupIdcom.github.liaochong/groupId artifactIdmyexcel/artifactId version4.5.0/version /dependency注意MyExcel的底层是POI所以它会把POI的依赖一并带进来。如果项目里已经用了POI要留意版本冲突。我遇到过的情况是项目里POI版本比较老比如3.17而MyExcel要求的POI版本更高启动时直接报NoSuchMethodError。解决思路很简单把项目里的POI版本统一提升到MyExcel要求的版本区间或者用exclusions把传递依赖排除掉自己显式声明POI版本。MyExcel本身是按模块拆的但核心功能xls/xlsx读写、模板填充都在主包里。如果你还要用CSV、XML等格式可以按需加对应的扩展模块。不过绝大多数场景下主包就够了。2.2 最简单的写入三行代码出Excel假设我要导出一个用户列表字段有姓名、年龄、邮箱。用MyExcel代码长这样ListUser users userService.listAll(); ExcelUtils.write(FileUtil.createFile(用户列表.xlsx), users);就这对就这。ExcelUtils.write有两个参数第一个是输出文件第二个是数据集合。MyExcel会自动获取User类的字段名作为表头然后把每条数据写成一列自动生成一个Sheet默认Sheet名是sheet0。这里有个细节MyExcel默认把字段名的英文原样作为表头如果你想显示中文表头可以用注解ExcelModel配合字段上的ExcelColumn(title 姓名)来做。但如果你不想在POJO上写注解也可以用Map方式比如ListMapString, Object dataList new ArrayList(); MapString, Object row1 new HashMap(); row1.put(姓名, 张三); row1.put(年龄, 28); dataList.add(row1); ExcelUtils.write(file, dataList);表头就是你Map里的key数据就是value。这种方式的优点是灵活适合动态字段的导出场景。2.3 读取数据反向也不难读Excel同样简单。比如我有一个Excel想读成ListMapString, ObjectListMapString, Object rows ExcelUtils.read(file, new ReadConfig());ReadConfig可以配置是否读取表头、从第几行开始读、是否忽略空行等。如果想把每一行读成一个Bean可以用ListUser users ExcelUtils.read(file, User.class);MyExcel会按字段名自动匹配表头表头是中文的话需要在字段上用ExcelColumn(title 姓名)来映射。如果只是想要原始数据比如做数据清洗、转换可以用ListListString rawRows ExcelUtils.read(file);这个方法返回的每一行是一个List适合那种表头不固定、需要自己处理逻辑的场景。2.4 为什么API要这样设计很多人第一次用MyExcel会觉得步子迈得太大ExcelUtils.write(file, list)一行就把活干完了参数太少了反而心里没底。实际上这正是它有意为之默认情况覆盖常见需求把可配置项拆成Writers和Readers两个门面需要用高级功能时再往深处走。比如Writers的用法Writers.of(file, dataList) .sheetName(用户数据) .build() .write();如果要设置列宽、行高、单元格样式就在Writers.of(...)后面链式调用。这种默认简单、深层可配的设计我觉得才是工具类该有的样子——80%的场景一行代码搞定20%的复杂场景再提供精细控制而不是一上来就甩给你十个参数。3. 模板填充才是杀手锏用Freemarker思路做报表如果说ExcelUtils.write是开胃菜那模板填充就是MyExcel的主菜。我之所以从POI转过来极大程度上就是因为这个功能。3.1 模板文件怎么准备模板填充的思路是你先用WPS或Office手工做一份报表模板在需要填数据的地方写上Freemarker语法然后Java代码只负责提供数据剩下的都交给MyExcel去渲染。比如有个对账单模板需要在第二行写客户名称第三行写订单日期后面几行循环输出订单明细。模板大概长这样A1单元格对账单A2单元格客户名称${customerName}A3单元格日期${date}A4单元格订单编号表头A5单元格#list orderList as order${order.id}/#list然后Java代码// 准备数据 MapString, Object data new HashMap(); data.put(customerName, 某某科技有限公司); data.put(date, 2024-06-01); ListMapString, Object orderList new ArrayList(); orderList.add(...); data.put(orderList, orderList); // 填充 ExcelUtils.write(FileUtil.createFile(对账单.xlsx), templateFile, data);ExcelUtils.write的重载版本里第二个参数可以直接传模板文件第三个参数传数据Map。MyExcel会加载模板把${customerName}、#list这些标签渲染成真实数据然后输出成一份新Excel模板本身不会被修改。3.2 Freemarker模板引擎的工作原理这里稍微解释一下为什么能做到把Excel当页面模板渲染。MyExcel内置了对Freemarker和Beetl两个模板引擎的支持。它读取Excel模板时不是直接把单元格文本原样输出而是先解析每个单元格里的文本内容把它当作Freemarker模板片段来处理。流程大概是读取模板Excel文件拿到Workbook对象。遍历所有Sheet、所有Row、所有Cell。对每个Cell的字符串内容执行模板引擎渲染数据源就是你传入的Map。把渲染后的文本写回Cell再输出Excel。这意味着什么呢意味着你不仅能在单元格里写${customerName}还能写#if order.status 1已支付#else未支付/#if甚至可以在单元格里写SUM(#list ...)这样的公式。模板引擎的完整能力都能在Excel里用表达力非常强。但是注意一个性能点因为要遍历整个模板的所有单元格所以模板文件不宜太大。几千行的模板没问题但如果你要渲染一个十万行的大报表模板填充方式就有点吃力了更适合用Writers直接写。3.3 模板里的公式怎么处理Excel模板里经常会写公式比如某一行算合计SUM(D5:D8)。用模板填充时这个公式单元格有两种处理方式方式是保留原公式渲染后Excel打开时自动计算。方式是需要用Java端算好结果直接当作文本填入单元格模板里不写公式。实际项目里我推荐后者。原因有两个一是Excel打开大文件时公式重算会卡二是有些下游系统解析Excel时根本不计算公式只读缓存值如果你没填静态值下游解析到公式单元格得到的可能就是null。所以我的习惯是模板里需要合计的地方要么用Freemarker的#assign或者Java端预先算好直接把数值渲染进单元格要么在Java端用POI自带的公式求值器算好再写进去。总之别把公式关键逻辑赌在Excel打开时会自动算上。3.4 复杂模板的坑合并单元格与循环模板填充有一个非常容易踩的坑合并单元格 循环列表。举个例子模板里A5到A8合并了用来显示一个订单的备注B5到B8是订单明细的多行数据。如果你用#list在B列循环输出多条数据A列的合并单元格不会自动跟着扩展。为什么会这样因为合并单元格是静态的在模板里就已经定义好了。模板引擎只负责把文本渲染到对应的Cell里它不会去动态调整合并区域。解决办法一般有两种第一种模板里不用合并单元格改用数据计算好位置后Java端在渲染完成后手动合并。MyExcel提供了一些单元格操作的辅助API可以在渲染后调整Workbook。第二种把需要合并的数据放到同一行里用单元格内的换行来模拟多行效果。比如A5单元格里用#list输出多条备注每条之间加\n然后设置A5单元格的wrap text属性。这样表格视觉上是多行实际结构是单行。这种方式简单粗暴但视觉上完全够用。我个人更倾向于第二种因为省事。除非下游系统对合并单元格有强校验否则用换行符模拟多行是最经济的做法。4. 多Sheet、函数与CSV日常高频场景的实战讲完模板再聊几个我在项目里经常用到的进阶场景。这些场景不是MyExcel独有的但它处理起来确实比原生POI省事而且很容易被忽略。4.1 多Sheet导出的正确姿势单Sheet用ExcelUtils.write(file, dataList)就够了。多Sheet就得用Writers。比如我要导一个季度报表里面有1月、2月、3月三个Sheet每个Sheet的数据结构不同。代码Writers.of(FileUtil.createFile(季度报表.xlsx)) .sheet(1月, janDataList) .sheet(2月, febDataList) .sheet(3月, marDataList) .build() .write();这个方法签名很直观交替传Sheet名和数据集合。每个Sheet的表头、数据格式如果不同MyExcel会自动根据每个List元素的类型来生成。如果某个Sheet要套模板可以用sheet(String sheetName, File templateFile, MapString, Object dataMap)这个重载。实际项目里经常是固定几个Sheet用模板渲染再加一个Sheet是纯数据这种需求用Writers串起来就很顺手。4.2 Excel函数与单元格格式很多Excel导出场景不只是把数据摆上去还需要对单元格做格式化。最常见的几个日期格式化POI里设置日期格式要写cellStyle.setDataFormat(...)MyExcel里可以在字段注解上直接配ExcelColumn(title 下单时间, dateFormat yyyy-MM-dd HH:mm:ss) private LocalDateTime orderTime;数字千分位比如金额字段导出时显示成1,234,567.89。在MyExcel里可以通过模板填充时用Freemarker的?string(,##0.00)来实现或者字段用注解配置。这个场景跟热词里ABAP上传Excel数字去除千分符其实是同一个问题的反面——导出加千分位导入去千分位。导入时你拿到的可能是字符串需要自己做清洗String rawValue rowMap.get(金额).toString().replaceAll(,, ); BigDecimal amount new BigDecimal(rawValue);单元格背景色、字体高级自定义可以用Writers里的background、font相关配置或者干脆在模板里手工设置好样式MyExcel填充时尽量保留原样式只替换文本内容这是模板填充方式的一大优势。4.3 CSV批量处理和Excel互补热词里有一个excel一列用逗号隔开为一行这本质上就是CSV格式处理。MyExcel对CSV的处理也做了封装但说实话CSV本身格式太简单我一般不会用MyExcel去操作CSV除非是导出的时候要直接输出CSV给下游系统。MyExcel里CSV导出走Writers.of(file, dataList).sheet(csv).build().csv()之类的调用但我实际项目里更习惯用OpenCSV或者干脆自己拼字符串。原因很简单CSV没有排版、没有样式Java代码自己拼字符串反而更好控制编码和特殊字符转义。那什么时候用MyExcel的CSV能力呢场景是这样的数仓那边要求每天推送一个CSV文件数据量大概二十万行字段几十个。用POI导xlsx光创建Workbook就占内存写出来的文件几十MB。改用CSV输出后文件只有几MB速度也快得多。你会感觉MyExcel在这类导出上也能统一入口挺省心的。4.4 和Spring框架整合后端项目十有八九是Spring Boot。MyExcel本身没有专门的Spring Boot Starter但用起来反而更自由直接在Controller里返回ResponseEntity下载即可。我常用的写法PostMapping(/export) public ResponseEntitybyte[] export() throws IOException { ListMapString, Object dataList ...; ByteArrayOutputStream baos new ByteArrayOutputStream(); ExcelUtils.writeByOutputStream(baos, dataList); HttpHeaders headers new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_OCTET_STREAM); headers.setContentDispositionFormData(attachment, URLEncoder.encode(报表.xlsx, UTF-8)); return new ResponseEntity(baos.toByteArray(), headers, HttpStatus.OK); }ExcelUtils提供了writeByOutputStream这类重载方法可以把Excel内容直接写到输出流不需要先落盘再读文件。这样导出的压力直接走内存配合Spring的下载响应体验很好。要提醒一句如果导出的数据量特别大比如超过十万行一次性把byte[]放内存里可能会有压力。我遇到过的解决方案是直接用HttpServletResponse.getOutputStream()流式写入写完后flush避免一次性把整个文件byte数组堆在内存里。5. 性能、内存与坑v4.5.0版本实测记录工具好用归好用但我向来对一行代码导出这种封装保持警惕。你把它当黑盒用它早晚会在某个数据量临界点给你颜色看。所以我特意用v4.5.0版本做了几组针对性的测试重点看数据量增大后的表现。5.1 十万行数据导出实测测试环境普通办公笔记本OpenJDK 8内存默认分配512MB。数据是十万条用户记录每条记录九个字段目标是生成一个xlsx文件。第一组测试用注解方式ExcelUtils.write(file, list)耗时大概6秒左右文件大小约15MB。说实话这个表现中规中矩毕竟底层是POI的XSSF模式XSSF本身是DOM模型十万行数据要在内存里维护所有的Cell对象对GC压力不小。第二组测试用模板填充方式模板文件只有几十行包含表头、标题行数据通过#list渲染出十万行。这组就慢了不少耗时接近20秒。原因上面提过模板填充要遍历所有Sheet的所有单元格而且要多次与模板引擎交互性能天然不如直接构建。第三组测试改用SAX模式的读取接口读取一个十万行的xlsx文件耗时大概是3秒左右内存表现平稳。结论很清楚如果你要导出的数据量级以上十万别用模板填充老老实实用ExcelUtils.write(file, list)直接写数据。模板填充适合模板复杂但数据量小比如对账单、报表头的场景。5.2 大文件读取会不会OOM这是个老生常谈的问题。POI读取xlsx默认用XSSFWorkbook会把整个文件读进内存。MyExcel的ExcelUtils.read在底层也是基于XSSF的所以我实测读取一个20MB、包含几万行的xlsx内存上涨明显但还没到OOM的程度。如果你要处理的是几百MB级别的大Excel还是用EasyExcel的SAX模式更稳。MyExcel也提供了一些读取配置比如ReadConfig支持将cell的类型结果强制转为字符串减少自动类型推断的开销但内存模型的本质没有变。所以在选型上我的建议是普通导入导出数据量小于五万行MyExcel完全够用。大文件读取几十MB以上EasyExcel或POI的SAX/EventModel更合适。复杂报表导出模板、格式、多SheetMyExcel的模板填充体验最好。5.3 v4.5.0版本我踩到的坑坑一Excel模板里的中文表头偶尔会出现乱码。排查下来发现是模板文件本身编码问题跟MyExcel无关但出现的场景很隐蔽用WPS保存的xlsx跟用Office保存的xlsx底层XML结构有些微妙区别MyExcel解析模板时对某些字符处理不当就会产生乱码。解决办法是统一使用Office或只使用WPS别混着来。坑二日期时间字段的格式。用ExcelColumn注解配置dateFormat时如果字段类型是LocalDateTime版本差异可能导致格式化不生效。v4.5.0对java.time包的支持比旧版完善了很多但如果遇到格式化不生效可以检查一下是不是在字段上忘了加ExcelColumn或者是字段类型混用了Date和LocalDateTime建议统一用LocalDateTime。坑三Writers链式调用的sheet()方法如果把同一个数据List传给两个Sheet可能会出现两个Sheet共享同一个迭代器导致第二个Sheet空白。这个问题不算MyExcel的bug更像是使用陷阱——你以为是复用了数据实际内部可能对List做了迭代处理。解决办法是传给每个Sheet时用new ArrayList(dataList)拷贝一份。坑四模板填充数据Map里如果缺少某个模板变量Freemarker的默认行为可能是直接报错或者输出空白。建议启动时设置Freemarker的classic_compatible为true或者使用模板变量前先在Java端做好默认值处理免得用户反馈Excel里有个空白格。6. 读与写之外MyExcel不适合做什么最后一个部分聊聊边界。任何工具都有它的射程范围知道它不擅长什么比知道它擅长什么更重要。6.1 超大Excel文件的读取与写入前面已经说了MyExcel底层是POI的XSSF模式是DOM模型。它的内存模型决定了它不适合处理超大Excel文件。如果你要处理的是几十万行甚至百万行的数据建议写入端用POI的SXSSFWorkbook流式写入或者直接输出CSV。读取端用EasyExcel的SAX模式或者POI的XSSFReader SAX解析。这个我不建议硬扛我见过一个项目用XSSFWorkbook读取一个50MB的xlsxJVM堆直接飙到1.5GB然后再加个并发服务就假死了。后来换了SAX模式内存降到几百MB问题彻底解决。6.2 复杂样式与图表支持MyExcel对样式的控制主要靠模板 Freemarker渲染以及基础API提供的列宽、行高、背景色、字体配置。如果你需要的是像前端报表工具那样做数据透视图、Excel图表、复杂的条件格式、跨Sheet引用公式MyExcel并不擅长。这类需求目前的常规解法是在模板里预先画好图表MyExcel填充数据后图表引用的数据区域能自适应扩展这个能实现一部分但如果是代码动态生成图表MyExcel做不到最终还是得用POI原生的图表API去操作或者用ECharts生成图片再插入Excel。6.3 和其他Java Excel库的选择建议最后给一个不严谨但很实用的选型经验需求场景推荐方案快速导出数据列表同时要求代码简洁MyExcel的ExcelUtils.write按Excel模板生成报表模板复杂、数据量适中MyExcel的模板填充读取超大Excel文件EasyExcel/POI SAX模式动态生成Excel图表、透视表直接学POI的底层API大批量数据导出且不要求格式CSV输出别折腾Excel表格之外补充一句我的个人体会MyExcel最适合的场景就是业务报表系统尤其是那种模板固定、每周/每月出一次、格式由业务人员维护的场景。它把改格式这件事从开发手里交给了业务人员——他们改模板你只改数据逻辑这种协作模式真的能省掉很多扯皮。最后分享一个小技巧如果你在项目里用了MyExcel的模板填充我强烈建议你给模板文件建一个单独的resources目录比如excel-templates然后写一个工具方法读取模板时统一走classpathpublic static File getTemplateFile(String templateName) { try { URL url MyExcelUtils.class.getClassLoader().getResource(excel-templates/ templateName); if (url null) { throw new IllegalArgumentException(模板文件不存在: templateName); } return FileUtil.createFile(url.getPath()); } catch (Exception e) { throw new RuntimeException(e); } }这样项目打jar包的时候模板也能一起打包服务器部署后不需要额外维护模板文件目录。模板一旦改动直接替换jar包里的文件即可逻辑代码完全不用动。另外凡是导出的Excel需要给下游系统解析的我建议导出后都加一道校验用ExcelUtils.read重新读一遍确认行数、字段数和预期一致。不是对MyExcel不放心而是对模板和数据组合这件事不放心——模板引擎渲染出错是运行时才暴露的提前校验能避免生产事故。我在实际项目里被这种导出成功但内容不对的问题坑过好几次多一步校验省很多麻烦。本文还有配套的精品资源点击获取