之前我一直使用阿里的 EasyExcel 实现 Excel 文件的导入和导出,但是它现在处于维护模式了,不会再有新特性加入了。
FastExcel 是 EasyExcel 的延续,后来由 Apache 进行孵化,名字更改为 Apache Fesod。
项目名称 fesod 是 “fast easy spreadsheet and other documents” 的缩写,体现了项目的起源、背景与愿景。
Apache Fesod 的使用跟 EasyExcel 基本上一样,只是更改了一些包名和类名。
本篇博客通过 Demo 代码介绍常用的读写 Excel 的方式,博客最后会提供源代码下载,更多内容请参考官网文档。
Apache Fesod 官网地址:https://fesod.apache.org/zh-cn/
EasyExcel 官网地址:https://easyexcel.opensource.alibaba.com/
一、搭建工程
新建一个名称为 springboot_fesod 的项目 工程,其结构如下图所示:

TestController 里面有两个接口,分别实现 Excel 文件的上传和下载。
EmpGenderConverter 是自定义的转换器,负责将 java 中的性别(1 男,0 女)和 Excel 或 Csv 文件中的性别(汉字)互相转换。
Employee 是 Java 实体类属性与 Excel 或 Csv 文件中的字段之间的映射关系。
EmployeeListener 是自定义的监听器,自定义实现对 Excel 文件的读取处理。
ReadService 里面只有一个公用方法,该方法实现对 resources 目录下 employee.csv 文件的读取,用于提供测试数据。
ExcelTest 里面编写了对 Excel 或 Csv 的读写测试方法,核心代码示例就在该类中。
二、代码细节
首先在 pom 文件中需要引入 fesod-sheet 依赖包,pom 文件内容如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><groupId>com.jobs</groupId><artifactId>springboot_fesod</artifactId><version>1.0-SNAPSHOT</version><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.4.5</version></parent><properties><maven.compiler.source>8</maven.compiler.source><maven.compiler.target>8</maven.compiler.target><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></dependency><dependency><groupId>org.apache.commons</groupId><artifactId>commons-lang3</artifactId></dependency><dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>2.0.53</version></dependency><!--引入 fesod-sheet 依赖包--><dependency><groupId>org.apache.fesod</groupId><artifactId>fesod-sheet</artifactId><version>2.0.1-incubating</version></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId><version>2.4.5</version></plugin></plugins></build>
</project>
为了防止一次性上传太大的文件,所以我在 application.yml 中进行了限制,具体内容如下:
server:port: 9000
spring:application:name: fesodDemoservlet:multipart:# 单个文件的上传大小限制max-file-size: 10MB# 如果同时上传多个文件时,总大小限制max-request-size: 100MB
为了能够提供测试数据,我在 resources 目录下准备了一个 employee.csv 文件和一个 emplist.xlsx 文件,其内容大致如下:


首先创建一个 Employee 类,其属性需要跟 Excel 或 Csv 中的字段进行对应,内容如下:
package com.jobs.entity;import com.jobs.converter.EmpGenderConverter;
import lombok.Data;
import org.apache.fesod.sheet.annotation.ExcelProperty;
import org.apache.fesod.sheet.annotation.write.style.ColumnWidth;
import org.apache.fesod.sheet.converters.longconverter.LongStringConverter;import java.math.BigDecimal;
import java.time.LocalDateTime;@Data
public class Employee {//Excel标题头名称或索引,只需要使用一个即可。大部分情况下使用标题头名称(不能重复)//@ExcelProperty(index = 0)//@ExcelProperty(value = "编号", converter = LongStringConverter.class)//指定列的宽度信息,否则内容显示不全@ColumnWidth(18)private Long empNo;@ExcelProperty("姓名")private String empName;@ExcelProperty("年龄")private Integer empAge;//标题头名称必须与 excel 或 csv 的标题头完全一致,否则读取不到数据//性别:1 男,0 女,-1 保密@ExcelProperty(value = "性别", converter = EmpGenderConverter.class)private Integer empGender;@ExcelProperty("工资")private BigDecimal empSalary;//如果不想读取或写入某项数据,可以使用 @ExcelIgnore 注解//@ExcelIgnore@ExcelProperty("创建时间")//指定列的宽度,否则内容显示不全,会出现 #### 隐藏了日期信息@ColumnWidth(18)private LocalDateTime createTime;
}
我们创建的 Employee 类中的性别是 Integer 类型,Excel 或 Csv 文件中的性别是汉字(男或女),因此可以编写一个转换器(EmpGenderConverter)
package com.jobs.converter;import org.apache.fesod.sheet.converters.Converter;
import org.apache.fesod.sheet.converters.ReadConverterContext;
import org.apache.fesod.sheet.converters.WriteConverterContext;
import org.apache.fesod.sheet.metadata.data.WriteCellData;/*** 性别数据转换(java 实体类中是 Integer 类型,Excel 或 Csv 文件中要写入字符串类型,比如男或女)*/
public class EmpGenderConverter implements Converter<Integer> {/*** 将从 excel 或 csv 中读取的数据,转换成 java 字段类型的数据*/@Overridepublic Integer convertToJavaData(ReadConverterContext<?> context) throws Exception {String cellData = context.getReadCellData().getStringValue();if ("男".equals(cellData)) {return 1;} else if ("女".equals(cellData)) {return 0;} else {return -1;}}/*** 将 java 字段类型的数据,转换成 Excel 或 csv 中要写入的数据*/@Overridepublic WriteCellData<?> convertToExcelData(WriteConverterContext<Integer> context) throws Exception {if (context.getValue().equals(1)) {return new WriteCellData<String>("男");} else if (context.getValue().equals(0)) {return new WriteCellData<String>("女");} else {return new WriteCellData<String>("保密");}}
}
然后在 ReadService 中编写了一个公共方法 getEmployeesFromCsv() 用于提供测试数据,内容如下:
package com.jobs.service;import com.jobs.entity.Employee;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.csv.QuoteMode;
import org.apache.fesod.sheet.FesodSheet;
import org.apache.fesod.sheet.metadata.csv.CsvConstant;
import org.apache.fesod.sheet.read.listener.PageReadListener;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Service;import java.util.ArrayList;
import java.util.List;@Slf4j
@Service
public class ReadService {//从当前项目中的 resources 目录下寻找 employee.csv 文件@Value("classpath:employee.csv")private Resource employeeCsv;/*** 从 csv 文件中读取数据*/public List<Employee> getEmployeesFromCsv() {try {List<Employee> emplist = new ArrayList<>();//使用 fesod 自带的 PageReadListener 读取文件数据FesodSheet.read(employeeCsv.getFile(), Employee.class, new PageReadListener<Employee>((datalist -> {emplist.addAll(datalist);})))//读取Csv文件.csv()//(可省略)用于指定 CSV 文件中的字段分隔符。默认值为英文逗号.delimiter(CsvConstant.COMMA)//(可省略)用于指定包裹字段的引用符号。默认值为双引号.quote(CsvConstant.DOUBLE_QUOTE, QuoteMode.MINIMAL).doRead();return emplist;} catch (Exception e) {log.error(e.getMessage());return null;}}
}
在读取 excel 或 csv 时,我们可以使用 fesod 中自带的 PageReadListener 监听器,也可以创建自己的监听器(EmployeeListener)内容如下:
package com.jobs.listener;import com.alibaba.fastjson.JSON;
import com.jobs.entity.Employee;
import lombok.extern.slf4j.Slf4j;
import org.apache.fesod.sheet.context.AnalysisContext;
import org.apache.fesod.sheet.exception.ExcelDataConvertException;
import org.apache.fesod.sheet.read.listener.ReadListener;@Slf4j
public class EmployeeListener implements ReadListener<Employee> {/*** 每读取一行数据,都会调用该方法*/@Overridepublic void invoke(Employee employee, AnalysisContext analysisContext) {//这里打印日志log.info("读取成功一条数据:{}", JSON.toJSONString(employee));}/*** 一个 sheet 中的数据读取完毕后,调用该方法*/@Overridepublic void doAfterAllAnalysed(AnalysisContext analysisContext) {//这里打印日志log.info("所有数据读取成功");}/*** 当发生异常时,调用该方法*/@Overridepublic void onException(Exception exception, AnalysisContext context) throws Exception {log.error("数据读取失败: {}", exception.getMessage());//如果是读取文件中的数据转换成java类的字段数据,转换失败时,打印出日志if (exception instanceof ExcelDataConvertException) {ExcelDataConvertException ex = (ExcelDataConvertException) exception;log.error("第 {} 行, 第 {} 列数据 {} 转换异常", ex.getRowIndex(), ex.getColumnIndex(), ex.getCellData());}}
}
对 Excel 或 Csv 读写的核心方法都在 ExcelTest 测试类中,具体内容如下:
package com.jobs;import com.jobs.entity.Employee;
import com.jobs.listener.EmployeeListener;
import com.jobs.service.ReadService;
import org.apache.commons.csv.QuoteMode;
import org.apache.fesod.sheet.ExcelReader;
import org.apache.fesod.sheet.ExcelWriter;
import org.apache.fesod.sheet.FesodSheet;
import org.apache.fesod.sheet.metadata.csv.CsvConstant;
import org.apache.fesod.sheet.read.listener.PageReadListener;
import org.apache.fesod.sheet.read.metadata.ReadSheet;
import org.apache.fesod.sheet.write.metadata.WriteSheet;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.util.ResourceUtils;import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;@SpringBootTest
public class ExcelTest {@Autowiredprivate ReadService readService;/*** 读取 resources 目录下的 csv 文件中的数据*/@Testpublic void readCsvTest() {List<Employee> emplist = readService.getEmployeesFromCsv();emplist.forEach(System.out::println);}/*** 读取 resources 目录下的 csv 文件的数据,然后生成 excel 文件*/@Testpublic void writeExcelTest1() {String fullFileName = "D:/emplist" + System.currentTimeMillis() + ".xlsx";List<Employee> emplist = readService.getEmployeesFromCsv();FesodSheet.write(fullFileName, Employee.class)//自定义 sheet 的名称.sheet("员工信息").doWrite(emplist);System.out.println("写入完成");}/*** 读取 resources 目录下的 csv 文件的数据,然后生成 excel 文件* 分别写到不同的 sheet 里面*/@Testpublic void writeExcelTest2() {String fullFileName = "D:/emplist" + System.currentTimeMillis() + ".xlsx";List<Employee> emplist = readService.getEmployeesFromCsv();//每个 sheet 最多写入 100 条数据,计算需要多少 sheet 页int pageNum = emplist.size() / 100;if (emplist.size() % 100 > 0) {pageNum++;}//写完之后自动关闭流try (ExcelWriter excelWriter = FesodSheet.write(fullFileName, Employee.class).build()) {for (int i = 0; i < pageNum; i++) {//获取每页的数据List<Employee> datalist = emplist.stream().skip(i * 100).limit(100).collect(Collectors.toList());//自定义要写入的 sheet 名称,sheet 索引是从 0 开始的WriteSheet writeSheet = FesodSheet.writerSheet(i, "员工信息" + (i + 1)).build();//向 sheet 中写入数据excelWriter.write(datalist, writeSheet);}}System.out.println("写入完成");}/*** 读取 resources 目录下的 emplist.xlsx 文件,打印到控制台上*/@Testpublic void readExcelTest1() throws Exception {//获取 resources 目录下的 emplist.xlsx 文件File excelFile = ResourceUtils.getFile("classpath:emplist.xlsx");//使用 fesod 自带的 PageReadListener 监听器FesodSheet.read(excelFile, Employee.class, new PageReadListener<Employee>(datalist -> {//打印读取到的数据datalist.forEach(System.out::println);}))//没有指定具体的 sheet,默认情况下只读取第一个 sheet.sheet().doRead();}/*** 使用我们自定义的 listener 监听器读取 excel 文件,打印到控制台上*/@Testpublic void readExcelTest2() throws Exception {//获取 resources 目录下的 emplist.xlsx 文件File excelFile = ResourceUtils.getFile("classpath:emplist.xlsx");//使用自定义的 EmployeeListener 监听器读取 excel 文件FesodSheet.read(excelFile, Employee.class, new EmployeeListener())//没有指定具体的 sheet,默认情况下只读取第一个 sheet.sheet()//(可忽略)跳过前多少行,一般情况下前多少行都是标题头,默认是 1.headRowNumber(1).doRead();}/*** 读取 resources 目录下的 emplist.xlsx 文件中所有的 sheet 数据,打印到控制台上*/@Testpublic void readExcelTest3() throws Exception {//获取 resources 目录下的 emplist.xlsx 文件File excelFile = ResourceUtils.getFile("classpath:emplist.xlsx");//使用自定义的 EmployeeListener 监听器读取 excel 文件中所有 sheet 的数据//在自定义的 EmployeeListener 中,每读取完一个 sheet 都会触发 doAfterAllAnalysed 事件FesodSheet.read(excelFile, Employee.class, new EmployeeListener()).doReadAll();}/*** 读取 resources 目录下的 emplist.xlsx 文件中指定 sheet 中的数据,打印到控制台上*/@Testpublic void readExcelTest4() throws Exception {//获取 resources 目录下的 emplist.xlsx 文件File excelFile = ResourceUtils.getFile("classpath:emplist.xlsx");//指定读取第 0 个和第 2 个 sheet 中的数据try (ExcelReader excelReader = FesodSheet.read(excelFile).build()) {//使用 Sheet 索引ReadSheet sheet1 = FesodSheet.readSheet(0).head(Employee.class)//使用 fesod 自带的 PageReadListener 监听器.registerReadListener(new PageReadListener<Employee>(datalist -> {//打印读取到的数据datalist.forEach(System.out::println);})).build();//使用 Sheet 名ReadSheet sheet2 = FesodSheet.readSheet("员工信息3").head(Employee.class)//使用自定义的 EmployeeListener 监听器.registerReadListener(new EmployeeListener()).build();//读取数据excelReader.read(sheet1, sheet2);}}/*** 读取 resources 目录下的 emplist.xlsx 文件中 sheet 名称为 “员工信息2” 的数据,写入到 csv 文件中*/@Testpublic void readExcelWriteCsvTest() throws Exception {//获取 resources 目录下的 emplist.xlsx 文件File excelFile = ResourceUtils.getFile("classpath:emplist.xlsx");List<Employee> emplist = new ArrayList<>();FesodSheet.read(excelFile, Employee.class, new PageReadListener<Employee>(emplist::addAll)).sheet("员工信息2").doRead();//要写入的 csv 文件全路径名String fullCsvName = "D:/sheet" + System.currentTimeMillis() + ".csv";//写入 csv 文件FesodSheet.write(fullCsvName,Employee.class).csv()//(可省略)用于指定 CSV 文件中的字段分隔符。默认值为英文逗号.delimiter(CsvConstant.COMMA)//(可省略)用于指定包裹字段的引用符号。默认值为双引号.quote(CsvConstant.DOUBLE_QUOTE, QuoteMode.MINIMAL)//用于写入文件中将 null 值置换成特定字符串。.nullString("N/A").doWrite(emplist);System.out.println("写入完成");}
}
为了实现 Excel 文件的上传下载,在 TestController 上提供了上传和下载接口,具体内容如下:
package com.jobs.controller;import com.jobs.entity.Employee;
import com.jobs.service.ReadService;
import org.apache.fesod.sheet.FesodSheet;
import org.apache.fesod.sheet.read.listener.PageReadListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;import javax.servlet.http.HttpServletResponse;
import java.io.InputStream;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.List;@RequestMapping("/test")
@RestController
public class TestController {@Autowiredprivate ReadService readService;/*** 上传 Excel 文件,返回读取到的数据* 可以把 resources 目录下的 emplist.xlsx 文件进行上传测试*/@PostMapping("/upload")public ResponseEntity uploadExcel(@RequestParam("file") MultipartFile file) {if (!file.isEmpty()) {try {InputStream inputStream = file.getInputStream();List<Employee> emplist = new ArrayList<>();//这里只读取第 0 个 sheet 的数据FesodSheet.read(inputStream, Employee.class,new PageReadListener<Employee>(emplist::addAll)).sheet().doRead();return new ResponseEntity(emplist, HttpStatus.OK);} catch (Exception e) {return new ResponseEntity(e.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR);}} else {//返回错误return new ResponseEntity("没有上传任何文件", HttpStatus.INTERNAL_SERVER_ERROR);}}/*** 使用 resources 目录下 employee.csv 中的数据生成 excel 文件(不产生临时文件),返回给前端下载*/@GetMapping("/download")public ResponseEntity downExcel(HttpServletResponse response) {try {List<Employee> emplist = readService.getEmployeesFromCsv();//设置下载的文件名称String excelFileName = URLEncoder.encode("表格文件" + System.currentTimeMillis(), "UTF-8").replaceAll("\\+", "%20");//设置下载文件类型,这里设置成二进制格式,让浏览器直接下载response.setContentType("application/octet-stream");//设置响应头信息response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + excelFileName + ".xlsx");FesodSheet.write(response.getOutputStream(), Employee.class)//自定义 sheet 的名称.sheet("员工信息").doWrite(emplist);return new ResponseEntity(HttpStatus.OK);} catch (Exception e) {return new ResponseEntity(e.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR);}}
}
三、测试验证
可以使用 postman 或 ApiPost 相关工具进行 Excel 文件的上传下载测试,我使用的是 ApiPost 工具。
使用 ApiPost 工具请求上传 Excel 文件的接口,返回内容截图如下:

使用 ApiPost 工具请求下载 Excel 文件的接口,然后点击 ApiPost 工具右侧的下载按钮即可下载文件,具体截图如下:

本篇博客的源代码下载地址为:https://files.cnblogs.com/files/blogs/699532/springboot_fesod.zip
