使用 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 的项目 工程,其结构如下图所示:

01

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 文件,其内容大致如下:

02

03

首先创建一个 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 文件的接口,返回内容截图如下:

04

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

05


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

posted @ 2026-04-15 22:56  乔京飞  阅读(526)  评论(0)    收藏  举报