EasyExcel使用

发布于:2025-07-21 ⋅ 阅读:(15) ⋅ 点赞:(0)

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

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

在这里插入图片描述

简单使用

(1)创建项目

创建一个Maven项目,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>

    <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 的依赖

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

(2)创建pojo对象

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

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文件中的列名,也可以填列的序号,如下,填列名比较好些,一眼就能知道对应关系。

    @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 接口,如下:

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文件中的顺序一致。

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 文件也能使用这个类。

如下

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);
    }
}

错误信息对象如下

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

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

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

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

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

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

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));
        }
    }
}

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

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

    @Getter(value = AccessLevel.NONE)
    private final Set<Integer> rowErrorSet = new HashSet<>();

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

@Getter
public class ParseResult<T> {

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

在这里插入图片描述

再进一步

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

在这里插入图片描述

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

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

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;
}

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

(布尔类型转换器)

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);
    }
}

(成绩枚举类型转换器)

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查对应的枚举项

    /**
     * 根据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 属性里,指定对应的转换器,如下:

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

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

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

        // 该行没有错误才加入到返回数据集合中
        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 的使用,以及可能遇到的问题


网站公告

今日签到

点亮在社区的每一天
去签到