EasyExcel使用

说明:EasyExcel是一个基于Java的、快速、简洁、解决大文件内存溢出的Excel处理工具。他能让你在不用考虑性能、内存的等因素的情况下,快速完成Excel的读、写等功能。(官方语,官网:https://easyexcel.opensource.alibaba.com/

本文介绍EasyExcel使用,读取下面这个excel文件

简单使用

(1)创建项目

创建一个Maven项目,pom文件如下:

xml 复制代码
<?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>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.12</version>
        <relativePath/>
    </parent>

    <groupId>com.hezy</groupId>
    <artifactId>excel_parse_demo</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</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.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.8.1</version>
        </dependency>

        <!-- easyexcel依赖 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>easyexcel</artifactId>
            <version>3.3.3</version>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
    </dependencies>
</project>

其中,下面这个是 easyexcel 的依赖

xml 复制代码
        <!-- easyexcel依赖 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>easyexcel</artifactId>
            <version>3.3.3</version>
        </dependency>

(2)创建pojo对象

定义一个 pojo 对象,与读取的数据对应

java 复制代码
import com.alibaba.excel.annotation.ExcelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;

/**
 * 学生对象
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Student implements Serializable {

    @ExcelProperty("学号")
    public String no;

    @ExcelProperty("姓名")
    public String name;

    @ExcelProperty("性别")
    public String sex;

    @ExcelProperty("班级")
    public String room;
}

这里的@ExcelProperty注解内可以填excel文件中的列名,也可以填列的序号,如下,填列名比较好些,一眼就能知道对应关系。

java 复制代码
    @ExcelProperty(index = 1)
    public String no;

    @ExcelProperty(index = 2)
    public String name;

    @ExcelProperty(index = 3)
    public String sex;

    @ExcelProperty(index = 4)
    public String room;

另外,个人经验,项目中凡涉及解析、序列化操作的对象,最好实现其全参构造、无参构造方法,并实现序列化接口。

(3)创建读取监听器

创建一个读取监听器,实现 EasyExcel 接口,如下:

java 复制代码
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.read.listener.ReadListener;
import com.hezy.pojo.Student;

import java.util.List;

/**
 * 读取学生数据监听器
 */
public class StudentReadListener implements ReadListener<Student> {

    /**
     * 返回对象
     */
    private final List<Student> studentData;

    public StudentReadListener(List<Student>  studentList) {
        this.studentData = studentList;
    }

    /**
     * 这里每次读取一行都会进行回调
     *
     * @param student 逐行解析封装完成的学生对象
     * @param analysisContext 读取内容上下文,可以用来获取当前行号
     */
    @Override
    public void invoke(Student student, AnalysisContext analysisContext) {
        studentData.add(student);
    }

    /**
     * 解析完成后执行的方法
     *
     * @param analysisContext 读取内容上下文,可以用来获取当前行号
     */
    @Override
    public void doAfterAllAnalysed(AnalysisContext analysisContext) {
        System.out.println("读取完成");
    }
}

(4)使用

接下来就能使用了,创建一个上传文件的接口,传入一个 List 集合,用于接收读取的数据。这里用 linkedList 是保证读取的数据顺序与excel文件中的顺序一致。

java 复制代码
import com.alibaba.excel.EasyExcel;
import com.hezy.listener.StudentReadListener;
import com.hezy.pojo.Student;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

import java.util.LinkedList;
import java.util.List;

@RestController
public class FileController {

    @PostMapping("/import")
    public List<Student> parseDeviceSummaryExcel(MultipartFile file) throws Exception {
        // 定义一个结果
        List<Student> result = new LinkedList<>();

        // 注意这里定义表头占一行,默认取sheet1中的数据
        EasyExcel.read(file.getInputStream(), Student.class,
                new StudentReadListener(result)).headRowNumber(1).sheet(0).doRead();
        return result;
    }
}

调用,发送,一把过

控制台可见执行了解析完成的代码

更近一步

一般来说,开放 excel 模板给用户,填写的数据百分百是有不符合校验的,所以说我们最好能设计一个返回对象,返回能通过校验的有用数据,和不能通过的校验的错误信息。另外,考虑到复用性,这个对象要设计成通用的,读取其他 excel 文件也能使用这个类。

如下

java 复制代码
import java.util.*;

/**
 * 解析结果
 * @param <T>
 */
public class ParseResult<T> {

    /**
     * 错误信息
     */
    private final List<ErrorInfo> errors = new LinkedList<>();

    /**
     * 联系人信息
     */
    private final List<T> parseData = new LinkedList<>();

    /**
     * 数据校验不通过的数据的行号
     */
    private final Set<Integer> rowErrorSet = new HashSet<>();

    /**
     * 添加错误信息
     *
     * @param rowIndex 行索引
     * @param columnIndex 列索引
     * @param message 错误信息
     */
    public void addError(int rowIndex, int columnIndex, String message) {
        // 添加错误信息
        errors.add(new ErrorInfo(rowIndex + 1, columnIndex + 1, message));
        // 行号加入到集合中
        rowErrorSet.add(rowIndex);
    }

    /**
     * 添加数据到返回结果中
     *
     * @param data 数据对象
     */
    public void addData(T data) {
        parseData.add(data);
    }

    /**
     * 判断该行是否有错误,有错误则不添加到返回结果中
     *
     * @param row 行号
     * @return true 表示有错误,false 表示没有错误
     */
    public boolean hasError(int row) {
        return rowErrorSet.contains(row);
    }
}

错误信息对象如下

java 复制代码
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * 错误信息
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ErrorInfo {

    /**
     * 行索引
     */
    private int rowIndex;

    /**
     * 列索引
     */
    private int columnIndex;

    /**
     * 错误信息
     */
    private String message;
}

接着,改造读取学生数据监听器,如下,解析后进行相关的校验,没问题再加入到返回结果中

java 复制代码
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.read.listener.ReadListener;
import com.hezy.pojo.ParseResult;
import com.hezy.pojo.Student;
import org.apache.commons.lang3.StringUtils;

/**
 * 读取学生数据监听器
 */
public class StudentReadListener implements ReadListener<Student> {

    /**
     * 返回对象
     */
    private final ParseResult parseResult;

    public StudentReadListener(ParseResult parseResult) {
        this.parseResult = parseResult;
    }

    /**
     * 这里每次读取一行都会进行回调
     *
     * @param student         逐行解析封装完成的学生对象
     * @param analysisContext 读取内容上下文,可以用来获取当前行号
     */
    @Override
    public void invoke(Student student, AnalysisContext analysisContext) {
        // 获取读取的行号
        Integer rowIdx = analysisContext.readRowHolder().getRowIndex();
        // 检查数据
        checkDate(student, rowIdx, 0);
    }

    /**
     * 解析完成后执行的方法
     *
     * @param analysisContext 读取内容上下文,可以用来获取当前行号
     */
    @Override
    public void doAfterAllAnalysed(AnalysisContext analysisContext) {
        System.out.println("读取完成");
    }

    /**
     * 这里进行行数据校验
     *
     * @param student 行数据
     * @param rowIdx  行号
     * @param offset  列号,应与你所判断的字段所处列一致
     */
    public void checkDate(Student student, int rowIdx, int offset) {
        String no = student.getNo();
        if (StringUtils.isBlank(no)) {
            parseResult.addError(rowIdx, offset, "学号不能为空");
        }

        String name = student.getName();
        if (StringUtils.isBlank(name) || name.length() > 20) {
            parseResult.addError(rowIdx, 1 + offset, "姓名不能为空,并且不能超过20个字");
        }

        String sex = student.getSex();
        if (StringUtils.isBlank(sex) || sex.length() > 10) {
            parseResult.addError(rowIdx, 2 + offset, "性别不能为空,并且不能超过10个字");
        }

        String room = student.getRoom();
        if (StringUtils.isBlank(room) || room.length() > 20) {
            parseResult.addError(rowIdx, 3 + offset, "班级不能为空,并且不能超过20个字");
        }

        // 该行没有错误才加入到返回数据集合中
        if (!parseResult.hasError(rowIdx)) {
            parseResult.addData(new Student(no, name, sex, room));
        }
    }
}

接口使用这里,就传入一个返回结果对象

java 复制代码
    @PostMapping("/import")
    public ParseResult<Student> parseDeviceSummaryExcel(MultipartFile file) throws Exception {
        // 定义一个结果
        ParseResult<Student> result = new ParseResult<>();

        // 注意这里定义表头占一行,默认取sheet1中的数据
        EasyExcel.read(file.getInputStream(), Student.class,
                new StudentReadListener(result)).headRowNumber(1).sheet(0).doRead();
        return result;
    }

调用,测试,把 excel 文件中的数据,随便删掉几个,使校验不通过

返回结果里有校验通过,能用的数据,也有校验不通过的错误信息,还提供了错误的单元格位置,就很nice

其中rowErrorSet是我们对象内的属性,用于存储校验不通过的数据行索引,属于我们代码内部数据,没有必要返回给前端,可以在对象属性上加上这行注解,避免被序列化返回给前端。

java 复制代码
    @Getter(value = AccessLevel.NONE)
    private final Set<Integer> rowErrorSet = new HashSet<>();

属性上加了这个注解,类上也要加 @Getter 注解,表示该类都生成 Getter 方法,但 rowErrorSet 不生成

java 复制代码
@Getter
public class ParseResult<T> {

这样 rowErrorSet 就不会被返回给前端了

再进一步

我们再来考虑一个问题,excel 文件中的数据有的是布尔类型,或者是枚举类型,数据值是备选列表中的一个,这种情况我们要怎么把文件中的布尔类型、枚举类型,转为我们代码中的true、false或者是枚举型的code值?

首先,先在对象中增加这两个字段

java 复制代码
import com.alibaba.excel.annotation.ExcelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;

/**
 * 学生对象
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Student implements Serializable {

    @ExcelProperty("学号")
    public String no;

    @ExcelProperty("姓名")
    public String name;

    @ExcelProperty("性别")
    public String sex;

    @ExcelProperty("班级")
    public String room;

    @ExcelProperty("是否成年")
    private Boolean adultOrNot;

    @ExcelProperty("成绩")
    private String score;
}

其中成绩,对应的是枚举,如下,excel 文件中填的是枚举的 desc,但是我们代码中需要的是枚举的 code

java 复制代码
import lombok.AllArgsConstructor;
import lombok.Getter;

/**
 * 成绩枚举
 *
 * @author hezy
 * @version 1.0.0
 * @create 2025/7/19 18:01
 */
@AllArgsConstructor
@Getter
public enum ScoreEnum {

    A("A", "优秀"),

    B("B", "良好"),

    C("C", "及格");

    private final String code;

    private final String desc;
}

这时,需要创建两个类型转换器,如下:

(布尔类型转换器)

java 复制代码
import com.alibaba.excel.converters.Converter;
import com.alibaba.excel.metadata.GlobalConfiguration;
import com.alibaba.excel.metadata.data.ReadCellData;
import com.alibaba.excel.metadata.data.WriteCellData;
import com.alibaba.excel.metadata.property.ExcelContentProperty;

/**
 * 布尔类型转换器
 */
public class BooleanConverter implements Converter<Boolean> {

    /**
     * excel 转 javaBean
     */
    @Override
    public Boolean convertToJavaData(ReadCellData<?> cellData, ExcelContentProperty contentProperty,
                                     GlobalConfiguration globalConfiguration) {
        return "是".equals(cellData.getStringValue());
    }

    /**
     * javaBean 转 excel
     */
    @Override
    public WriteCellData<?> convertToExcelData(Boolean value, ExcelContentProperty contentProperty,
                                               GlobalConfiguration globalConfiguration) {
        String strValue = value ? "是" : "否";
        return new WriteCellData<>(strValue);
    }
}

(成绩枚举类型转换器)

java 复制代码
import com.alibaba.excel.converters.Converter;
import com.alibaba.excel.metadata.GlobalConfiguration;
import com.alibaba.excel.metadata.data.ReadCellData;
import com.alibaba.excel.metadata.data.WriteCellData;
import com.alibaba.excel.metadata.property.ExcelContentProperty;
import com.hezy.enums.ScoreEnum;

/**
 * 成绩枚举转换器
 *
 * @author hezy
 * @version 1.0.0
 * @create 2025/7/19 18:12
 */
public class ScoreEnumConverter implements Converter<String> {

    /**
     * excel 转 javaBean
     */
    @Override
    public String convertToJavaData(ReadCellData<?> cellData, ExcelContentProperty contentProperty,
                                    GlobalConfiguration globalConfiguration) {
        return ScoreEnum.getEnumByDesc(cellData.getStringValue()).getCode();
    }

    /**
     * javaBean 转 excel
     */
    @Override
    public WriteCellData<?> convertToExcelData(String value, ExcelContentProperty contentProperty,
                                               GlobalConfiguration globalConfiguration) {
        return new WriteCellData<>(ScoreEnum.getEnumByDesc(value).getDesc());
    }
}

成绩枚举里要增加两个静态方法,用于根据code、desc查对应的枚举项

java 复制代码
    /**
     * 根据desc获取枚举
     */
    public static ScoreEnum getEnumByDesc(String desc) {
        return Arrays.stream(ScoreEnum.values())
                .filter(scoreEnum -> scoreEnum.getDesc().equals(desc))
                .findFirst()
                .orElse(null);
    }

    /**
     * 根据desc获取枚举
     */
    public static ScoreEnum getEnumByCode(String code) {
        return Arrays.stream(ScoreEnum.values())
                .filter(scoreEnum -> scoreEnum.getDesc().equals(code))
                .findFirst()
                .orElse(null);
    }

回到对象上,在学生对象属性上,@ExcelProperty 属性里,指定对应的转换器,如下:

java 复制代码
    @ExcelProperty(value = "是否成年", converter = BooleanConverter.class)
    private Boolean adultOrNot;

    @ExcelProperty(value = "成绩", converter = ScoreEnumConverter.class)
    private String score;

OK,读取监听器这里,写入数据时,加上这两个字段

java 复制代码
        // 该行没有错误才加入到返回数据集合中
        if (!parseResult.hasError(rowIdx)) {
            parseResult.addData(new Student(no, name, sex, room, student.getAdultOrNot(), student.getScore()));
        }

调用接口,查看返回值,可见对应属性的值被转换成了布尔类型、枚举类型对应枚举项的code值

可能遇到的问题

使用 EasyExcel 时,如果你没有遇到问题,那么万事大吉,如果遇到了问题,数据解析不出来,或者解析出来的数据都是默认值,0、null 这些,需要关注以下两个地方:

  • 使用了lombok注解给对象生成 Setter/Getter 方法,可能会导致数据无法写入到对象,可手动生成 Setter/Getter 方法;

  • 对象属性名有以"is"开头的,导致数据无法写入,这个在阿里巴巴开发手册中亦有记载,不要以"is"开头给属性命名;

总结

本文介绍了 EasyExcel 的使用,以及可能遇到的问题

相关推荐
David爱编程8 分钟前
Java 三目运算符完全指南:写法、坑点与最佳实践
java·后端
遇见尚硅谷16 分钟前
C语言:单链表学习
java·c语言·学习
学习编程的小羊42 分钟前
Spring Boot 全局异常处理与日志监控实战
java·spring boot·后端
YA3332 小时前
java基础(六)jvm
java·开发语言
Moonbit2 小时前
MoonBit 作者寄语 2025 级清华深圳新生
前端·后端·程序员
前端的阶梯2 小时前
开发一个支持支付功能的微信小程序的注意事项,含泪送上
前端·后端·全栈
咕噜分发企业签名APP加固彭于晏2 小时前
腾讯元器的优点是什么
前端·后端
JavaArchJourney2 小时前
Java 集合框架
java
尘民10243 小时前
面试官笑了:线程start() 为什么不能再来一次?
java
AAA修煤气灶刘哥3 小时前
Swagger 用着糟心?试试 Knife4j,后端开发狂喜
后端·面试