EasyExcel工具使用教程


Java 中有两个较为常用的 Excel 类库,分别是 ApachePOI 以及 AlibabaEasyExcel,其中 EasyExcel 引入了事件驱动模型在内存占用方面有着优秀的表现。

在这篇文章中将介绍如何通过 EasyExcelExcel 文件进行读写操作。

一、基础读取

1. 模拟数据

首先先准备一个 Excel 数据文件,文件内容如下:

id    name    gender    birthday
1    User-1    Male    2024-01-29
2    User-2    Male    2024-01-29
3    User-3    Male    2024-01-29
4    User-4    Male    2024-01-29

完成后,为上述的数据创建对应的实体类对象,这里省去了 getset 方法部分内容。

public class User {

    private String id;

    private String name;

    private String gender;

    private Date birthday;
}

2. 数据读取

读取数据的方式十分简单,通过 EasyExcel 对象即可创建读取对象,其可配置的内容参考下表。

方法 作用
read() 读取的对象,传入类型可为 File, IO 或 文件路径。
head() 指定 Excel 列,当为类时则与类属性名称一一对应。
sheet() 指定 Sheet 页,默认为第一页,可通过名称或下标指定。
headRowNumber() 指定文件表头所在的行数,默认为 1。

需要注意的是在使用 head(xxx.class) 时默认数据字段映射关系是与类属性定义顺序一致的。

以刚才创建的 User 类为例,Excel 文件中列的定义顺序为 id, name, gender, birthday,则在赋值时其默认实体类中第一个属性即为 id 并以此类推。如果此时 User 的属性定义顺序为 name, id 那么最终读取的数据 id 值将会被赋值给 name 字段。

下述示例代码即为通过 EasyExcel 读取文件数据。

public class ExcelReadTest {

    @Test
    public void read() {
        String fileLocate = "src\\main\\resources\\data.xls";
        try (InputStream in = Files.newInputStream(Paths.get(fileLocate))) {
            List<User> list = EasyExcel.read(in)
                    .head(User.class)
                    .sheet()
                    .doReadSync();

            System.out.println(list);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

3. 动态读取

EasyExcel 除了支持实体类映射外还可以动态指定列名实现读取。

head() 中通过集合动态指定文件中的列名,其返回的结果为 List<LinkedMap<Integer, Object>>,其中每一个 LinkMap 对应文件内的一行数据,且 Key 为对应列的下表,Value 为对应单元格的数据。

public void read() {
    String fileLocate = "src\\main\\resources\\data.xls";
    try (InputStream in = Files.newInputStream(Paths.get(fileLocate))) {
        // 定义列名
        List<List<String>> heads = Stream.of("id", "name", "gender", "birthday")
                .map(Arrays::asList)
                .collect(Collectors.toList());
        List<LinkedMap<Integer, Object>> list = EasyExcel.read(in)
                .head(heads)
                .sheet()
                .doReadSync();

        System.out.println(list);
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
}

二、数据写入

1. 写入示例

EasyExcel 写入操作与读取类似,其中的 excelType() 用于指定文件类型,可选值有下述三类,对应三种不同的数据文件格式。

ExcelTypeEnum.CSV
ExcelTypeEnum.XLS
ExcelTypeEnum.XLSX

数据写入的方式与之前的读取示例类似,不同之处仅为之前的 read()doReadSync() 替换为 write()doWrite(),其余的属性配置不变,示例代码如下:

public void demo1() {
    List<User> dataList = new ArrayList<>();
    for (int i = 1; i < 5; i++) {
        dataList.add(new User(i + "", "User-" + i, "Male", new Date()));
    }

    String fileLocate = "src\\main\\resources\\data.xls";
    EasyExcel.write(fileLocate)
            .excelType(ExcelTypeEnum.XLS)
            .head(User.class)
            .sheet("Test-data")
            .doWrite(data);
}

2. Sheet管理

当需要写入多个 Sheet 到同一个 Excel 文件时,即可使用 ExcelWriterSheetBuilder

如下述示例中即创建两个 Sheet 页数据,Sheet 页名称分别为 Sheet-1Sheet-2

 public void demo2() throws IOException {
    List<User> dataList = new ArrayList<>();
    for (int i = 1; i < 5; i++) {
        dataList.add(new User(i + "", "User-" + i, "Male", new Date()));
    }

    String fileLocate = "src\\main\\resources\\info.xls";
    // 创建 ExcelWriter 对象
    ExcelWriter excelWriter = EasyExcel.write(fileLocate)
            .excelType(ExcelTypeEnum.XLS)
            .build();
    for (int i = 1; i <= 2; i++) {
        // 创建 Sheet 对象并写入数据
        ExcelWriterSheetBuilder sheetBuilder = new ExcelWriterSheetBuilder(excelWriter);
        sheetBuilder.sheetName("Sheet-" + i);
        sheetBuilder.head(User.class);
        sheetBuilder.registerWriteHandler(new LongestMatchColumnWidthStyleStrategy());
        // 将 sheet 页写入 excel 中
        excelWriter.write(dataList, sheetBuilder.build());
    }
    // 写入完成
    excelWriter.finish();
}

3. 动态写入

在上述的写入示例中,文件的数据的写入和列名的定义都是通过 Java 实体对象映射实现。而 EasyExcel 也提供了动态的写入方式,通过 List 集合传入可变对象,由此即可为系统设计一套通用的数据写入模块。

在动态写入中,Excel 文件的列名通过 List<List<String>> 格式数据指定,而数据的载体是 List<List<Object>> 而非动态读取示例中的 List<Map>

// 动态读取返回的数据格式
// List<Map<String, Object>>
[
    { 
        "id": "1", 
        "name": "user-1", 
        "gender": "Male", 
        "birthday": "2023-01-01 00:00:01"
    }, { 
        "id": "2", 
        "name": "user-2", 
        "gender": "Male", 
        "birthday": "2023-01-01 00:00:02"
    }
]

// 动态写入要求的数据格式
// List<List<Object>>
[
    ["1", "user-1", "Male", "2023-01-01 00:00:01"],
    ["2", "user-2", "Male", "2023-01-01 00:00:02"]
]

根据上述逻辑,其相对应的程序代码如下:

public void dynamicWrite() throws IOException {
    // 构建测试数据
    List<Map<String, Object>> dataList = new ArrayList<>();
    for (int i = 1; i < 5; i++) {
        Map<String, Object> map = Map.of(
                "id", i + "",
                "name", "user-" + i,
                "gender", "Male",
                "birthday", new Date()
        );
        dataList.add(map)
    }

    List<String> headList = List.of("id", "name", "gender", "birthday");
    // 转化 "List<Map>" 为 "List<List>"
    List<List<Object>> rowDatas = new ArrayList<>();
    for (Map<String, Object> map : dataList) {
        List<Object> row = new ArrayList<>();
        for (String name : headList) {
            row.add(map.get(name));
        }
        rowDatas.add(row);
    }
    // 声明 Excel 的表头
    List<List<String>> heads = headList.stream()
            .map(Arrays::asList)
            .collect(Collectors.toList());
    // 写入数据
    String fileLocate = "src\\main\\resources\\info.xls";
    EasyExcel.write(fileLocate)
            .excelType(ExcelTypeEnum.XLS)
            .head(heads)
            .sheet("Test-data")
            .doWrite(rowDatas);
}

三、注解使用

上面的读写示例中都是采用默认的实体类属性映射方式,但 EasyExcel 提供了更灵活的注解用于字段映射,下面将分别进行介绍。

1. ExcelProperty

@ExcelProperty 注解存在 valueorder 两个属性,前者用于设置别名而后者用于设置列顺序。

通过此属性即可指定列与实体字段的映射关系,同时默认的字段的映射顺序是与属性的定义顺序相关,因此通过 order 属性可灵活的修改映射顺序。

如下示例中则最后生成 Excel 文件中列名分别为 编号姓名,其中第一列为 编号,第二列为 姓名

public class ExcelUser {

    @ExcelProperty(value = "编号", order = 1)
    private String id;

    @ExcelProperty(value = "姓名", order = 2)
    private String name;
}

2. ExcelIgnore

@ExcelIgnore 用于指定需要忽略的属性,由于 EasyExcel 是与类的属性进行映射,如若实体的属性字段数量与文件表头数量不一致导致映射失败。而在实际开发场景中,在数据类中常会涉及到某些不相关的业务字段,此时即通过 @ExcelIgnore 标注为忽略。

如下示例在读取或写入文件时将忽略 address 字段。

public class ExcelUser {

    private String id;

    @ExcelIgnore
    private Date address;
}

3. DateTimeFormat

@DateTimeFormat 用于指定读取数据的时间格式化形式。

如下示例在读取或写入文件时将会格式化字段 birthday 字段 值为 yyy-MM-dd 格式。

public class ExcelUser {

    private String id;

    @DateTimeFormat("yyy-MM-dd")
    private Date birthday;
}

4. 应用示例

下面通过示例演示注解的具体应用方式。

同样的创建的 User 实体类,通过 @ExcelProperty 注解此时则无需担心属性的定义顺序,而是可以通过 order 指定顺序。

public class User {

    @ExcelProperty(value = "生日", order = 4)
    @DateTimeFormat("yyy-MM-dd")
    private Date birthday;

    @ExcelProperty(value = "性别", order = 3)
    private String gender;

    @ExcelProperty(value = "姓名", order = 2)
    private String name;

    @ExcelProperty(value = "编号", order = 1)
    private String id;
}

类似的,准备一份 Excel 文件,数据内容如下:

编号    姓名     性别       生日
1     User-1    Male    2024-01-29
2     User-2    Male    2024-01-29
3     User-3    Male    2024-01-29
4     User-4    Male    2024-01-29

对应的读取实现如下,可以发现与之前的读取代码完全一致。类似的,在写入时通过上述的注解属性映射的方式,可以数据文件的表头由默认的实体类属性名转为我们配置的 value 值。

@Test
public void read() {
    String fileLocate = "src\\main\\resources\\data.xls";
    try (InputStream in = Files.newInputStream(Paths.get(fileLocate))) {
        List<User> list = EasyExcel.read(in)
                .head(User.class)
                .sheet()
                .doReadSync();

        System.out.println(list);
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
}

四、事件监听

1. 基本介绍

EasyExcel 中执行数据的读取写入时,其也提供了监听器用于在读写过程中的操作。

监听器的声明方式十分简单,通过继承 AnalysisEventListener<> 泛型接口即可,如下述示例中即定义了一个针对 User 类数据读取的监听器。

public class ReadListener extends AnalysisEventListener<AnnoUser> {

}

2. 事件监听

在继承上述接口后,需要重写下表中的事件方法,详细的事件描述参考下表:

方法 作用
invokeHeadMap() 读取 headRowNumber() 指定的表头信息。
invoke() 读取动作的触发器,每读取一行数据都将触发。
onException() 读取数据异常时的事件方法。
doAfterAllAnalysed() 文件内所有数据读取完成后的触发事件。

下述为一个完成的事件监听器定义示例,代码如下:

public class ReadListener extends AnalysisEventListener<AnnoUser> {

    private final List<AnnoUser> dataList = new ArrayList<>();

    /**
     * 文件列名表头读取,可执行内容格式校验
     *
     * @param headMap 文件表头
     * @param context 上下文对象
     */
    @Override
    public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) {
        super.invokeHeadMap(headMap, context);
    }

    /**
     * 读取动作的触发器,可对读取的对应数据执行操作
     *
     * @param row     当前行数据
     * @param context 上下文对象
     */
    @Override
    public void invoke(AnnoUser row, AnalysisContext context) {
        // 可执行数据合法性校验或格式转化
        dataList.add(row);
    }

    /**
     * 读取数据异常时的触发器
     *
     * @param exception 异常对象
     * @param context   上下文对象
     */
    @Override
    public void onException(Exception exception, AnalysisContext context) throws Exception {
        // 异常处理
    }

    /**
     * 文件内所有数据读取完成后的触发事件
     *
     * @param context 上下文对象
     */
    @Override
    public void doAfterAllAnalysed(AnalysisContext context) {
        // 对完整的数据执行操作
        System.out.println("Listener after all: " + dataList);
    }
}

3. 应用示例

监听器的使用方式同样十分简单,以数据读取为例,在声明 read() 属性时传入即可。

如下述为监听器的使用示例,与之前的不同仅在于在 read() 中多了 listener 参数。

@Test
public void read() {
    String fileLocate = "src\\main\\resources\\data.xls";
    try (InputStream in = Files.newInputStream(Paths.get(fileLocate))) {
        // 创建监听器
        ReadListener listener = new ReadListener();
        // 传入监听器
        List<User> list = EasyExcel.read(in, listener)
                .head(User.class)
                .sheet()
                .doReadSync();

        System.out.println(list);
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
}

文章作者: 烽火戏诸诸诸侯
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 烽火戏诸诸诸侯 !
  目录