去年双十一前一周,我在公司后台搞一个“导出一百万行订单”的功能,原来那套老 POI 模板一跑就把测试机内存打到 90% 往上蹦,同事直接在工位喊我:“你这个导出一开像 DOS 攻击一样。”那次之后,我正式把 Excel 处理这件事从「POI 体力活」升级到了「开源项目接力赛」:第一棒是 EasyExcel,现在第二棒,轮到 Apache Fesod 了。
老 Javaer 都知道,EasyExcel 当年出来,就是为了解决两个痛点:
XSSFWorkbook 一次性把整个文件拉进内存,几十 MB 还行,上百 MB 就开始抖。EasyExcel 做了两件特别“接地气”的事情:
@ExcelProperty("列名") 就能读写,和 ORM 那套思路差不多。像以前我导出订单列表,大概是这样写的:
// 订单导出模型@DatapublicclassOrderExportRow{@ExcelProperty("订单号")private String orderNo;@ExcelProperty("用户ID")private Long userId;@ExcelProperty("下单时间")@DateTimeFormat("yyyy-MM-dd HH:mm:ss")private LocalDateTime createdAt;@ExcelProperty("实付金额")private BigDecimal paidAmount;}// EasyExcel 导出publicvoidexportWithEasyExcel(HttpServletResponse response, List<OrderExportRow> rows)throws IOException { String fileName = URLEncoder.encode("订单列表.xlsx", StandardCharsets.UTF_8.name()); response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); response.setCharacterEncoding("utf-8"); response.setHeader("Content-disposition", "attachment;filename=" + fileName); EasyExcel.write(response.getOutputStream(), OrderExportRow.class) .sheet("订单") .doWrite(rows);}这套东西在业务早期很好用,而且生态成熟、资料多,是很多 Java 项目的默认选型。
但是,用得越久,你越会感受到一些现实问题:比如大文件边界性能、历史包袱、兼容性问题,以及大家经常吐槽的“某些 bug 修了又复活”,这也是后来 FastExcel / Fesod 接棒的背景之一。
你在 GitHub 上打开 Apache Fesod 的仓库,会看到一句话:
Fast. Easy. Done. Processing spreadsheets without worrying about large files causing OOM.
这句话基本把它的目标写死了:还是做 Excel,但要比以前更快、更省内存、更省心。
几个关键点简单说下:
而且,有一篇中文博客写得很直白:FastExcel(现在的 Fesod)支持无缝从 EasyExcel 迁移,兼容 EasyExcel 的使用方式,还在性能和功能上做了增强,比如“读取指定行数”“Excel 转 PDF”等特性。
你可以把这件事理解成:
EasyExcel 把「Excel 处理这件事」在国内 Java 圈做火了,FastExcel 在原基础上做性能优化和补坑,最后 Fesod 把这条路线捐给了 Apache,变成一个真正社区化、开放治理的项目。
这就是标题里那句“开源项目的接力赛”。
不讲虚的,就从实际开发者视角,看三个维度:性能、易用性、工程化。
1)性能 & 内存
官方文档的定位写得很清楚:高性能、内存友好,专攻大规模表格数据。
结合实际体验,大致是这几类提升:
在 GitHub issue 和 milestone 里也能看到,很多任务就是“修 EasyExcel 里遗留的 bug”“进一步优化性能”等。
2)API 易用性
Fesod 的 API 设计,明显是延续 EasyExcel + FastExcel 那一脉的:
read / write,上手成本很低。看个读取示例,感受一下它的风格:
// 定义数据模型publicclassUserImportRow{@ExcelProperty("用户ID")private Long userId;@ExcelProperty("昵称")private String nickname;@ExcelProperty("创建时间")private LocalDateTime createdAt;}// 实现监听器publicclassUserRowListenerimplementsReadListener<UserImportRow> {privatefinal UserService userService;publicUserRowListener(UserService userService){this.userService = userService; }@Overridepublicvoidinvoke(UserImportRow data, AnalysisContext context){// 每读到一行就落库 / 校验 userService.saveOrUpdate(data); }@OverridepublicvoiddoAfterAllAnalysed(AnalysisContext context){ System.out.println("用户导入完成"); }}// 使用 Fesod 读取publicvoidimportUsers(MultipartFile file, UserService userService)throws IOException {try (InputStream in = file.getInputStream()) { FesodSheet.read(in, UserImportRow.class, newUserRowListener(userService)) .sheet() // 默认第一个 sheet .doRead(); }}是不是很眼熟?如果你用过 EasyExcel,这种写法几乎不用学习成本。([GitHub][2])
3)工程化 & 合规
这块是 Apache 项目独有的优势:
对普通开发来说,这意味着一件事:这个库不是某家公司的“私产”,而是整个社区一起养,稳定性、延续性、可预期性都会好不少。
讲点干货,假设你现在项目里已经在用 EasyExcel,我们来做一个“最小改动”的迁移示例。
1)Maven 依赖调整
原来可能是这样的:
<!-- 原 EasyExcel 依赖 --><dependency><groupId>com.alibaba</groupId><artifactId>easyexcel</artifactId><version>3.3.2</version></dependency>改成 Fesod:
<!-- Apache Fesod 依赖,版本号示例,实际以官网 / Maven Central 为准 --><dependency><groupId>org.apache.fesod</groupId><artifactId>fesod</artifactId><version>1.3.0</version></dependency>官方文档有提醒:Fesod 目前内部还是用 POI,如果你自己项目里已经显式引入了 POI 相关 jar,需要注意排除重复依赖,避免版本冲突。
2)导出代码改造示例
假设你原来的导出代码长这样(EasyExcel):
publicvoidexportWithEasyExcel(OutputStream out, List<OrderExportRow> rows){ EasyExcel.write(out, OrderExportRow.class) .autoCloseStream(false) .sheet("订单") .doWrite(rows);}迁移到 Fesod,一般只需要把入口类和包名换掉(实际项目里可能有少量细节差异,这里演示整体思路):
publicvoidexportWithFesod(OutputStream out, List<OrderExportRow> rows){ FesodSheet.write(out, OrderExportRow.class) .autoCloseStream(false) .sheet("订单") .doWrite(rows);}大部分业务代码(模型类、注解、字段定义)都可以原样复用,这也是很多文章说“可以无缝从 EasyExcel 迁移”的原因。
3)一个综合小工具示例:统一导入导出工具类
很多项目里都会封装一个 ExcelHelper,下面是用 Fesod 写的一个简单版本,方便你直接抄回去改:
publicclassExcelHelper{/** * 读取 Excel 并用指定监听器处理 */publicstatic <T> voidread(InputStream in, Class<T> head, ReadListener<T> listener){ FesodSheet.read(in, head, listener) .sheet() .doRead(); }/** * 读取 Excel 第一行作为表头,返回 List */publicstatic <T> List<T> readToList(InputStream in, Class<T> head){ List<T> result = new ArrayList<>(); FesodSheet.read(in, head, new ReadListener<T>() {@Overridepublicvoidinvoke(T data, AnalysisContext context){ result.add(data); }@OverridepublicvoiddoAfterAllAnalysed(AnalysisContext context){// ignore } }) .sheet() .doRead();return result; }/** * 写 List 到 Excel */publicstatic <T> voidwrite(OutputStream out, Class<T> head, List<T> data, String sheetName){ FesodSheet.write(out, head) .sheet(sheetName) .doWrite(data); }}后面你在 Service 里就可以很自然地用:
publicvoidexportUsers(HttpServletResponse response)throws IOException { List<UserExportRow> rows = userService.queryExportRows(); response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); response.setCharacterEncoding("utf-8"); response.setHeader("Content-disposition","attachment;filename=" + URLEncoder.encode("用户列表.xlsx", "UTF-8"));try (OutputStream out = response.getOutputStream()) { ExcelHelper.write(out, UserExportRow.class, rows, "用户"); }}迁移完一两个功能,你基本就对 Fesod 的使用心里有数了。
实话实说,不是所有项目都必须“立刻迁移”,我自己的判断标准大概是:
如果你现在还停留在“原生 POI + 自己造轮子”的阶段,那就别纠结了,无论是 EasyExcel 还是 Fesod,随便选一个都会比你自己写强很多。
-END-
我为大家打造了一份RPA教程,完全免费:songshuhezi.com/rpa.html