使用 Apache Fesod 读写 Excel
之前我一直使用阿里的 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: fesodDemo
servlet:
multipart:
# 单个文件的上传大小限制
max-file-size: 10MB
# 如果同时上传多个文件时,总大小限制
max-request-size: 100MB
我使用 AI 生成了测试数据,放在 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 字段类型的数据
*/
@Override
public 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 中要写入的数据
*/
@Override
public 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> {
/**
* 每读取一行数据,都会调用该方法
*/
@Override
public void invoke(Employee employee, AnalysisContext analysisContext) {
//这里打印日志
log.info("读取成功一条数据:{}", JSON.toJSONString(employee));
}
/**
* 一个 sheet 中的数据读取完毕后,调用该方法
*/
@Override
public void doAfterAllAnalysed(AnalysisContext analysisContext) {
//这里打印日志
log.info("所有数据读取成功");
}
/**
* 当发生异常时,调用该方法
*/
@Override
public 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 {
@Autowired
private ReadService readService;
/**
* 读取 resources 目录下的 csv 文件中的数据
*/
@Test
public void readCsvTest() {
List<Employee> emplist = readService.getEmployeesFromCsv();
emplist.forEach(System.out::println);
}
/**
* 读取 resources 目录下的 csv 文件的数据,然后生成 excel 文件
*/
@Test
public 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 里面
*/
@Test
public 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 文件,打印到控制台上
*/
@Test
public 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 文件,打印到控制台上
*/
@Test
public 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 数据,打印到控制台上
*/
@Test
public 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 中的数据,打印到控制台上
*/
@Test
public 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 文件中
*/
@Test
public 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 {
@Autowired
private 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
浙公网安备 33010602011771号