行业资讯
📅 2026/9/6 22:10:27
Java操作Excel全指南:POI与EasyExcel选型、读写与性能优化
简介Java开发中直接操作Excel并非标准库能力通常需要引入第三方解决方案。这份doc文档围绕Apache POI和JExcelApi两条技术路线系统讲解在B/S架构下通过JSP生成与导出Excel报表的具体方法适合正在处理Web报表导出需求的Java初学者和中级开发者。文档先对比两类工具的特点POI功能全面但合并单元格边框处理较繁琐JExcelApi体积轻量且对中文支持更友好随后从下载配置jxl.jar开始演示生成简单表格再到包含合并单元格、图片嵌入与复杂数据类型等场景的报表并配有JavaBean示例代码便于动手验证。资源为1个doc文件压缩包约100KB便于随时查阅。目前已有671人浏览学习是快速上手Java操作Excel的实用参考资料。1. 项目背景与整体设计思路看见Java对Excel表格的操作这个标题我第一反应是这又是一个被问烂了但依然挡不住大家踩坑的话题。Excel操作在Java后端开发里出现的频率有多高报表导出、数据导入、批量数据处理、模板填充、定时任务生成统计表……可以说只要你在做企业级应用基本绕不开它。但说实话很多人在真正动手之前对Java操作Excel的理解是混乱的。有人以为需要用JDBC去连Excel当数据库用有人以为只能手动拼CSV糊弄过去还有人一股脑引入POI之后发现几万行数据直接内存溢出。这篇文章我就结合自己这些年做过的项目把Java操作Excel这件事从选型、环境、读写实操、大数据量优化到问题排查完整拆一遍。先说清楚这篇内容能解决什么问题如果你正在做后台管理系统需要把数据导出成Excel报表或者需要解析用户上传的Excel文件入库又或者要基于模板批量生成合同、单据——那么这篇文章就是给你写的。如果你是刚接触Java的初学者也能照着步骤跑通第一个Excel读写程序并且理解每一步在做什么。1.1 核心需求解析日常开发里遇到的Excel操作需求本质上可以归为四类写入导出、读取解析、模板填充、格式美化。写入导出是最常见的比如把数据库查询结果导出成员工花名册、销售统计表读取解析则是把用户上传的Excel文件里的数据提取出来经过校验后写入数据库模板填充通常用于合同、报价单这类固定格式文档的大批量生成格式美化则是给生成的表格设置列宽、背景色、边框、合并单元格、公式等。这四类需求的技术侧重点完全不同。导出关注的是效率和内存占用读取关注的是数据准确性和类型转换模板填充关注的是定位单元格和样式保留格式美化关注的是API的调用细节。如果你只用一套方案打天下要么性能顶不住要么功能做不全。1.2 为什么这事不能随便搞搞有人会问我直接拼一个CSV文件后缀改成.xlsx用户不也一样能打开吗这个问题我在项目评审里听过不止一次。拼CSV在纯数据导出的场景下确实能用但一旦涉及合并单元格、公式、多Sheet、数据验证下拉框、图片插入CSV就彻底无能为力了。更关键的是CSV用逗号和换行分隔数据里只要出现换行、逗号、引号处理起来就是一场灾难。还有人说我用Java的Desktop类调用本地的Excel程序来操作行不行行但前提是你的服务器上装了Office并且在Windows环境运行。生产环境大多数是Linux服务器这条路直接堵死。所以结论只有一个使用纯Java的类库来读写Excel文件不依赖任何本地Office软件。这也是Apache POI和EasyExcel这类库存在的根本原因。2. 核心工具选型深度解析Java操作Excel的类库市面上有几种我试用过的就包括Apache POI、EasyExcel、JXL还有比较小众的fastexcel。选型这事不能只看名气要结合你的实际场景来决定。2.1 Apache POI功能全面但需要驾驭Apache POI是Apache基金会的开源项目也是历史最悠久、功能最全的Java Excel操作库。它支持xls和xlsx两种格式xls用HSSF实现xlsx用XSSF实现大数据量下还有SXSSF基于流式写入来降低内存消耗。POI的强大之处在于它几乎是所见即所得——你能在Excel界面里做的大部分操作POI API都能做合并单元格、设置样式、插入图表、添加数据验证、操作公式、读取图片等等。但也正因为功能太全API的复杂度相当高代码写起来很长。一个简单的导出POI要写几十行甚至上百行代码。另一个需要注意的点是XSSF的读模式是DOM模型会把整个文件加载进内存。一个几十MB的Excel文件解析时JVM内存可能飙到几百MB这在大文件场景下是不可接受的。所以POI适合对格式要求复杂、数据量可控万行以内的场景。2.2 EasyExcel大数据量的低内存解法EasyExcel是阿里巴巴开源的Excel处理库它的核心卖点是低内存。它采用SAX模式逐行解析文件写入时使用文件缓存而不是全量内存所以在处理几十万行数据时内存占用依旧平稳。这一点对后端服务来说非常关键。EasyExcel的API设计也很贴合业务——可以用注解直接把Java Bean映射到表格列也可以用动态头的方式在运行时决定列名读的时候支持同步和异步两种模式。我做导出功能时最喜欢用的是动态头加List数据模型因为它不需要为每个导出场景单独建实体类灵活性非常高。EasyExcel也有它的局限性对复杂格式的支持不如POI全面比如跨Sheet的公式、复杂的条件格式、图表等EasyExcel支持得不够好。另外早期版本对xls旧版Excel格式不支持现在虽然加入了支持但性能表现一般还是建议老项目迁移到xlsx。2.3 JXL与其它轻量选择的两面性JXL是一个很老牌的Excel操作库API简单体积小只支持xls格式。在十年前它是主流选择但现在Excel 2007之后的xlsx文件已经是绝对主流JXL无法处理这种格式所以新项目我不会推荐它。它唯一的存活场景是遗留系统维护——如果老代码已经在用JXL且业务没变没必要为了技术先进去重写。我给你的选型建议很直接如果只是简单的数据导入导出用EasyExcel省心省力如果要做复杂的表格格式、公式、模板操作用POI如果你需要大规模并发导出报表可以EasyExcel为主、POI为辅混合使用。3. 实操读写Excel的核心环节工具选完接下来就是动手写代码。这一节我会用Maven项目为例把环境搭建、写入、读取、样式设置和模板填充完整走一遍。你可以直接照着敲遇到问题再对照第五部分的排查表。3.1 环境准备与依赖引入首先确保本机装了JDK 8或更高版本并配置好了Maven。然后创建一个普通的Maven项目在pom.xml里加入依赖。我这里先用EasyExcel的版本演示因为它从入口到API都更贴近业务开发者的思维后面再补充POI的代码对比。dependency groupIdcom.alibaba/groupId artifactIdeasyexcel/artifactId version3.3.4/version /dependency !-- 如果要用POI可以额外引入 -- dependency groupIdorg.apache.poi/groupId artifactIdpoi-ooxml/artifactId version5.2.5/version /dependency3.2 基础写入用EasyExcel生成第一个Excel假设我们要导出一份用户列表包含姓名、年龄、邮箱三列。传统的做法是创建一个User类用ExcelProperty注解标注每一列对应的表头名称。代码如下public class User { ExcelProperty(姓名) private String name; ExcelProperty(年龄) private Integer age; ExcelProperty(邮箱) private String email; // getter/setter 省略 }导出逻辑非常简单ListUser users new ArrayList(); users.add(new User(张三, 28, zhangsanexample.com)); users.add(new User(李四, 32, lisiexample.com)); String fileName /tmp/users.xlsx; EasyExcel.write(fileName, User.class).sheet(用户表).doWrite(users);这段代码跑了之后在/tmp目录下就会生成一个名为users.xlsx的文件双击打开表头是姓名、年龄、邮箱下面是数据行。这段代码其实已经包含了EasyExcel的两个关键设计模型类上的注解决定表头和数据字段的映射关系write方法链式调用先指定文件路径和类型再指定Sheet名称最后doWrite触发写入。如果你不想为每个导出都建一个实体类可以用动态头的方式ListListString head new ArrayList(); head.add(Collections.singletonList(姓名)); head.add(Collections.singletonList(年龄)); head.add(Collections.singletonList(邮箱)); ListListObject data new ArrayList(); data.add(Arrays.asList(张三, 28, zhangsanexample.com)); EasyExcel.write(fileName).head(head).sheet(用户表).doWrite(data);这种方式在运行时决定列名适合列不固定、多类型导出的后台系统我最常用的是这一种。3.3 基础读取解析Excel数据读取同样有两种方式用注解映射到实体类或者直接用List接收。用注解映射的写法是ListUser list EasyExcel.read(fileName).head(User.class).sheet().doReadSync();这段代码会同步读取Excel中的第一个Sheet并按User类的注解映射字段。注意doReadSync这个方法是同步返回适合文件不大的场景。如果遇到大文件建议用异步监听模式后面第四部分会详细说。用动态接收的方式则是ListMapInteger, String data new ArrayList(); EasyExcel.read(fileName).sheet() .registerReadListener(new AnalysisEventListenerMapInteger, String() { Override public void invoke(MapInteger, String row, AnalysisContext context) { data.add(row); } Override public void doAfterAllAnalysed(AnalysisContext context) { // 全部解析完成 } }).doRead();这里Map的Key是列索引从0开始Value是单元格内容。这种方式不需要预先定义表头结构适合做通用解析工具。实际项目中我经常把两种方式结合——先动态读一遍判断格式是否符合预期再用注解映射的方式做正式解析。3.4 让表格更好看样式、数据格式与公式导出报表给领导看没有样式是绝对不行的——光秃秃的表格既不专业也不方便阅读。POI在样式控制上做得非常细我用POI写一个带表头背景色、加粗、列宽自适应和冻结首行的例子try (Workbook workbook new XSSFWorkbook()) { Sheet sheet workbook.createSheet(用户表); // 创建表头样式 CellStyle headerStyle workbook.createCellStyle(); headerStyle.setFillForegroundColor(IndexedColors.LIGHT_BLUE.getIndex()); headerStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND); Font headerFont workbook.createFont(); headerFont.setBold(true); headerStyle.setFont(headerFont); // 创建表头行 Row headerRow sheet.createRow(0); String[] columns {姓名, 年龄, 邮箱}; for (int i 0; i columns.length; i) { Cell cell headerRow.createCell(i); cell.setCellValue(columns[i]); cell.setCellStyle(headerStyle); } // 数据行 Row row sheet.createRow(1); row.createCell(0).setCellValue(张三); row.createCell(1).setCellValue(28); row.createCell(2).setCellValue(zhangsanexample.com); // 列宽自适应 for (int i 0; i columns.length; i) { sheet.autoSizeColumn(i); } // 冻结首行 sheet.createFreezePane(0, 1); try (FileOutputStream fos new FileOutputStream(/tmp/style_demo.xlsx)) { workbook.write(fos); } }这段代码里值得注意的有几个点cellStyle是workbook级的对象不能在一个Sheet创建后直接给另一个Sheet用autoSizeColumn对中文支持不算好如果表头是中文列宽可能计算不准所以生产环境我一般手工指定列宽例如sheet.setColumnWidth(i, 20 * 256)单位是1/256个字符宽。另外冻结首行用的是createFreezePane(0, 1)第一个参数是冻结右边多少列第二个参数是冻结下面多少行。还需要补充一个实用细节单元格的公式。POI里设置公式很简单cell.setCellFormula(SUM(A1:A10))在读取时用evaluator去计算结果。但尽量少在程序生成的Excel里用跨Sheet引用、间接函数这类复杂的公式因为Excel在打开时会重新计算公式如果公式写错了表格打开会直接报错排查起来比较痛苦。3.5 模板填充把Excel当报表引擎用最后说模板填充。这个场景在合同、审批单、请假单这类固定格式文档的批量生成中特别常见。思路是先手工做好一个Excel模板在需要填充的位置用特殊标记占位然后程序读取模板定位占位符并替换为真实数据。POI实现模板填充的核心是遍历单元格、找到被##包裹的占位符然后替换内容。我自己常用的占位符规范是##字段名##比如##name##、##date##这样在Java代码里用Map就能轻松对应。try (Workbook workbook WorkbookFactory.create(new FileInputStream(/tmp/template.xlsx))) { Sheet sheet workbook.getSheetAt(0); MapString, String data new HashMap(); data.put(name, 张三); data.put(date, 2024-08-15); for (Row row : sheet) { for (Cell cell : row) { if (cell.getCellType() CellType.STRING) { String value cell.getStringCellValue(); if (value.contains(##)) { for (Map.EntryString, String entry : data.entrySet()) { value value.replace(## entry.getKey() ##, entry.getValue()); } cell.setCellValue(value); } } } } try (FileOutputStream fos new FileOutputStream(/tmp/result.xlsx)) { workbook.write(fos); } }注意这里用WorkbookFactory.create而不是new XSSFWorkbook是因为WorkbookFactory能根据文件实际格式自动识别是xls还是xlsx兼容性更好。模板文件建议使用xlsx格式因为xls格式的行数上限只有65536样式功能也弱很多。4. 大数据量场景与性能优化小文件怎么玩都行一旦生产环境出现几十万行数据的导出或解析事情就会变得复杂。这一部分我说说大数据量下的典型问题和解决办法。4.1 为什么大数据量会OOMPOI的XSSF写入和读取模式都是DOM模型写入时在内存里构建完整的Excel对象树读取时把整个XML内容解析成对象放到内存。这种方式在小文件下没问题但假设你有一张50万行、20列的表每个单元格至少对应一个Java对象加上字符串内部化、样式对象、Sheet对象内存占用轻松超过1GB。所以很多人第一次用POI导出大文件时就遇到了java.lang.OutOfMemoryError: Java heap space。另外我在实际项目里还遇到过一种隐蔽的内存泄漏——循环里反复创建Workbook对象但没有关闭。POI的Workbook实现了Closeable接口底层有Zip相关的临时文件资源不关闭会导致文件句柄泄漏最终看起来也是内存溢出。所以务必使用try-with-resources或者finally里调用workbook.close()。4.2 SAX模式的正确打开方式EasyExcel之所以能处理大文件是因为它的底层是SAX模式读取时逐行解析XML事件流每解析一行就回调一次监听器行数据处理完就可以丢弃不会堆积在内存里。所以使用EasyExcel读大文件时不要再doReadSync一把梭了必须使用监听器模式。public class UserDataListener extends AnalysisEventListenerUser { private static final int BATCH_COUNT 1000; private ListUser cachedList new ArrayList(); Override public void invoke(User data, AnalysisContext context) { cachedList.add(data); if (cachedList.size() BATCH_COUNT) { saveToDatabase(cachedList); cachedList.clear(); } } Override public void doAfterAllAnalysed(AnalysisContext context) { if (!cachedList.isEmpty()) { saveToDatabase(cachedList); } } private void saveToDatabase(ListUser list) { // 批量插入数据库例如 MyBatis-Plus 的 saveBatch } }核心思想是批量累积、批次消费。每攒够1000条就批量入库然后清空列表。这样做有两个好处一是内存里始终只保留1000条数据无论源文件多大都不会撑爆内存二是批量INSERT远比逐条INSERT快得多数据库交互次数从50万次降到了500次。4.3 实测对比与调优建议我拿一份50万行、12列的数据做过对比测试POI XSSF导出内存最大用到1.8GB2GB堆仍然差点OOM同一台机器用EasyExcel导出内存峰值约300MB。读取测试类似EasyExcel的解析速度也略快于POI因为POI在解析时要构建完整的对象模型而EasyExcel只做流式回调。如果你短期内不打算引入EasyExcelPOI也提供了SXSSFWorkbook来应对大数据量写入——它内部维护一个滑动窗口窗口外的数据会被刷到磁盘上的临时文件从而控制内存。使用方式很简单new SXSSFWorkbook(100)其中100表示窗口大小也就是内存里最多保留100行其余写入磁盘。try (SXSSFWorkbook workbook new SXSSFWorkbook(100)) { Sheet sheet workbook.createSheet(大数据表); // 写入大量数据 workbook.setCompressTempFiles(true); // 压缩临时文件 // 处理完务必调用 dispose 清理临时文件 workbook.dispose(); }不过SXSSF有代价它不支持读取只能写并且部分样式类和公式评估在流式模式下会失效。所以我的建议是日常优先用EasyExcelPOI只保留给复杂模板和格式需求。5. 常见问题与排查技巧实录操作Excel的报错信息往往很抽象我把自己实际工作中遇到的高频问题整理成了一张表方便你按图索骥。5.1 常见错误速查表错误现象根因解决方案java.lang.OutOfMemoryError: Java heap space文件过大POI DOM模式内存耗尽换EasyExcel用SXSSFWorkbook调大-Xmx治标不治本java.io.IOException: The process cannot access the file文件被Excel或WPS占用关闭打开该文件的Office程序导出时输出到临时文件再移动org.apache.poi.openxml4j.exceptions.InvalidFormatException文件不是合法的xlsx格式确认是否真的用Excel另存为xlsx而不是直接把txt改成xlsxjava.lang.IllegalArgumentException: Invalid column index列索引超出Excel上限xls最大255列xlsx最大16384列检查数据列数确认是否用了旧版xls中文表头乱码字符编码问题xls格式下常见尽量改用xlsxxls写入时确保JVM默认编码UTF-8NoClassDefFoundError / AbstractMethodErrorPOI版本冲突多个依赖传递了不同版本POI用Maven dependencyManagement统一版本排除旧版本5.2 一个容易被忽略的坑日期格式Excel里日期存储的本质是数字从1900年1月1日起算的天数所以你直接用getStringCellValue去读一个日期单元格得到的不一定是2024-08-15而可能是一个数字或者一串序列值。正确做法是先判断cell.getCellType()再根据不同的类型取值。日期类型的判断在POI里有专门的工具if (DateUtil.isCellDateFormatted(cell)) { Date date cell.getDateCellValue(); SimpleDateFormat sdf new SimpleDateFormat(yyyy-MM-dd); return sdf.format(date); }如果你用EasyExcel在字段上加DateTimeFormat(yyyy-MM-dd)注解就能搞定同一问题读取时自动转成对应的字符串格式。5.3 排查问题的独门思路Excel相关的问题我的排查顺序永远是文件先行、代码其次。先拿一个最小复现的Excel文件手工在WPS或Office里打开确认文件本身正常再判断是不是代码问题。很多时候用户上传的Excel文件表面正常但里面混入了不可见字符、合并单元格、图片这些都需要在解析前做防御性处理。另外一个经验是生产环境一定要对解析的错误做行级隔离例如某一行数据格式错了不要把整个文件解析全部回滚而是记录错误行号继续解析最后汇总错误清单反馈给用户。这比一次性全失败友好得多。5.4 上传解析时的安全红线上传Excel文件给后端解析有一个很多新手忽略的问题Excel文件可能携带宏xlsm或者通过公式触发外部链接。虽然Java解析类库不会执行宏但上传文件本身有被恶意构造的可能——比如一个超大的xlsx文件压缩比极高解压后可能撑爆磁盘或内存。我在生产环境里对上传文件做了三件事限制上传大小常见做法是Spring MultipartFile的max-file-size配置检查文件扩展名和Content-Type但不要只信扩展名最好用库读一下文件头判断真实格式解析前判断Sheet数量、行数超过阈值直接拒绝解析并提示用户。这些不是多余的安全洁癖而是真实的线上故障教训堆出来的经验。6. 用POI还是EasyExcel我的最终建议说了这么多最后聊点实在的。我会怎么选如果是新项目、标准化的后端数据导入导出我建议直接上EasyExcel。原因很简单API简洁、内存友好、社区活跃而且和Spring Boot继承非常顺滑。你不需要为90%的常规需求去写大段POI样板代码。如果是合同模板、复杂报表这类对格式要求极高的场景保留POI能力是必要的——我见过很多实施项目要求Excel输出必须和某个审批系统的模板完全一致包括页眉页脚、特定字体、跨Sheet汇总公式这种只有POI能精确控制。也可以两者混合用EasyExcel负责日常数据流POI负责难啃的骨头。我自己维护的代码库里就是这么做的用一个统一的ExcelService接口封装了底层实现切换上层业务代码完全不感知。另外还有一个容易被忽略的注意点版本兼容性。POI的5.x版本包名和4.x不兼容EasyExcel的3.x版本底层依赖的POI版本也可能和其他依赖冲突。所以项目里如果同时有POI和EasyExcel一定要统一版本——我建议用EasyExcel 3.3.x配套POI 5.2.x实测稳定。引入依赖后用mvn dependency:tree排查一下冲突该排除的排除别等上线了再被ClassNotFound打脸。7. 最后再分享几个实战心得写了不少最后补充三个我个人的习惯对刚接触Excel操作的人可能有点帮助。第一处理Excel之前先明确数据量级和格式复杂度再决定用哪个库。一次性花10分钟想清楚省得以后反复重构。第二所有Excel导出接口都建议异步化。一个大文件的生成可能耗时几十秒如果同步返回给前端HTTP连接大概率超时前端体验也很糟糕。常见的做法是发起任务后立即返回任务ID后台线程生成文件生成完毕后上传到文件服务器或本地存储前端轮询任务状态再下载。这个模式我用过很多次非常稳。第三写完Excel相关代码务必写单元测试覆盖空文件、空Sheet、纯英文数据、中文数据、超长字符串、特殊字符比如换行符、公式前缀这些边界情况。Excel操作不是那种跑通一次就万事大吉的功能用户上传的数据永远比你想象的更脏测试多一分线上就少一分救火。我个人踩过最惨的一次坑是生产环境导出一个20万行的报表POI直接把应用的内存干到3GB最后整台机器服务全部假死。后来改成EasyExcel流式导出加异步下载内存降到400MB以内用户再也感觉不到卡顿。这大概就是工欲善其事必先利其器最真实的写照——工具选对后面能省下一大堆麻烦。本文还有配套的精品资源点击获取