添加链接
link管理
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

EasyExcel教程


EasyExcel教程

github: alibaba/easyexcel: 快速、简洁、解决大文件内存溢出的java处理Excel工具 (github.com)

官方demo: easyexcel/easyexcel-test/src/test/java/com/alibaba/easyexcel/test/demo at master · alibaba/easyexcel (github.com)

最新版本:

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>easyexcel</artifactId>
    <version>3.1.2</version>
</dependency>

Excel 实体对象

正如数据库中的表需要对应一个 Java 实体类,Excel 中的表也有这样的规则。

对于这一块内容可以先稍作了解,建议先去看 读 Excel ,途中遇到疑惑的地方再回上来看

实体类指定列名

@ExcelProperty

先介绍这个注解的两个重要属性:

  1. value,是个字符串数组
  2. index,代表列的下标

单行表头

姓名 学号 生日
lgz 3190421121 1999-01-01
cjw 3190421127 2000-12-12

使用 @ExcelProperty 注解,通过 value 指定列名,或者 index 指定列号

示例:

@Getter
@Setter
@EqualsAndHashCode
public class DemoData {
    @ExcelProperty(index = 0)
   	private String name;
    @ExcelProperty("学号")
    private String number;
    @ExcelProperty("生日")
    private Date birthday;
}

同一个实体类中,不建议同时使用 index 和 value 指定列

多行表头

信息
姓名 学号 生日
lgz 3190421121 1999-01-01
cjw 3190421127 2000-12-12

@ExcelProperty value 属性是一个字符串数组

当我们传入多个字符串时, EasyExcel 首先找到所有使用注解的字段中, value 数组最长的一个,然后根据数组的长度锁定行号。

随后,所有注解的 value 只取最后一个字符串的值,来匹配这一行的表头

示例:

@Getter
@Setter
@EqualsAndHashCode
public class DemoData {
    // 2. 匹配第二行中的“姓名”表头
    @ExcelProperty("姓名")
   	private String name;
    // 1. 这个注解的value最长,匹配第二行,并取最后的字符串“学号”作为表头匹配
    @ExcelProperty({"信息", "学号"})
    private String number;
    // 3. 没有使用注解,那么按照定义的顺序匹配到第二行中的第三列
    private Date birthday;
}

下面是特殊示例,有助于理解多行表头的匹配规则:

@Getter
@Setter
@EqualsAndHashCode
// 表格还是上头那一个
public class DemoData {
    // 2. 前面的字符串都被忽略,即便aa不存在,这里只取lgz进行第三行的匹配,匹配成功,所以最后获取到的值是 cjw
    @ExcelProperty("aa", "lgz")
   	private String text1;
    // 1. 根据长度匹配到第三行,但是没有gg这个表头,所以最终获取的输入值为null
    @ExcelProperty({"信息", "love", "gg"})
    private String text2;
    // 3. 虽然上面一个字段匹配失败,但是text3还是顺位第三,最终获取到的值是 24,也就是第三列的位置
    private String text3;
}

当然还有更特殊的

信息 a
s
姓名 学号 生日
lgz 3190421121 1999-01-01
cjw 3190421127 2000-12-12

这种情况下,姓名、学号、生日,到底算第二行还是算第三行?

这根据实体类指定了哪些列来判断。

如果实体类只包括左边两列,那么 信息 单元格只看做一行。这时,姓名和学号算作第二行。

如果实体类还包括了第三列,那么 信息 单元格看做两行。这时,姓名、学号、生日看做第三行。

关于 EasyExcel 如何解析表头,可以自己写一个 ReadListener 并重写 invokeHead 方法来查看

这里不再给出代码,只贴上一段日志

[INFO] 16:12:10 cn.cimoc.easyexcel.DemoHeadDataListener.invokeHead(DemoHeadDataListener.java:35): 
解析到一条头数据
[INFO] 16:12:10 cn.cimoc.easyexcel.DemoHeadDataListener.invokeHead(DemoHeadDataListener.java:37): 
[INFO] 16:12:10 cn.cimoc.easyexcel.DemoHeadDataListener.invokeHead(DemoHeadDataListener.java:37): 
1:null
[INFO] 16:12:10 cn.cimoc.easyexcel.DemoHeadDataListener.invokeHead(DemoHeadDataListener.java:37): 
[INFO] 16:12:10 cn.cimoc.easyexcel.DemoHeadDataListener.invokeHead(DemoHeadDataListener.java:35): 
解析到一条头数据
[INFO] 16:12:10 cn.cimoc.easyexcel.DemoHeadDataListener.invokeHead(DemoHeadDataListener.java:37): 
0:null
[INFO] 16:12:10 cn.cimoc.easyexcel.DemoHeadDataListener.invokeHead(DemoHeadDataListener.java:37): 
1:null
[INFO] 16:12:10 cn.cimoc.easyexcel.DemoHeadDataListener.invokeHead(DemoHeadDataListener.java:37): 
[INFO] 16:12:10 cn.cimoc.easyexcel.DemoHeadDataListener.invokeHead(DemoHeadDataListener.java:35): 
解析到一条头数据
[INFO] 16:12:10 cn.cimoc.easyexcel.DemoHeadDataListener.invokeHead(DemoHeadDataListener.java:37): 
[INFO] 16:12:10 cn.cimoc.easyexcel.DemoHeadDataListener.invokeHead(DemoHeadDataListener.java:37): 
[INFO] 16:12:10 cn.cimoc.easyexcel.DemoHeadDataListener.invokeHead(DemoHeadDataListener.java:37): 
[INFO] 16:12:11 cn.cimoc.easyexcel.DemoHeadDataListener.invoke(DemoHeadDataListener.java:30): 
读取1条数据:[text1:lgz, text2:3190421121, text3:1999/1/1]
[INFO] 16:12:11 cn.cimoc.easyexcel.DemoHeadDataListener.invoke(DemoHeadDataListener.java:30): 
读取1条数据:[text1:cjw, text2:3190421127, text3:2000/12/12]

字段格式化注解

@DateTimeFormat

该注解包含两个参数:

  1. value,格式化字符串
  2. use1904windowing,是否使用1904作为起始时间,因为有的Excel是1900,而有的是1904

value

与 JDK中的 DateTimeFormatter 模式串规则相同,这里提供常用格式,更多格式请参考 JDK

格式 含义
yyyy
MM
dd
HH 24小时制的小时
hh 12小时制的小时
a 上午/下午
mm 分钟
ss

@NumberFormat

该注解包含两个参数:

  1. value,格式化字符串
  2. roundingMode,保留小数的规则, 默认 是四舍五入

value

由 3 种字符构成: # . %

  1. # 号代表数字的位数(只能控制小数位数)
  2. . 号代表小数点
  3. % 号代表百分比,若输入数据中不带有百分号,那么最终呈现效果将乘以100

示例:我们将读取如下表格中的“数据”一栏

行号 数据
1 22
2 23.6
3 24.1234
4 15%
5 115.345%
  1. @NumberFormat("#")

    读取到的数据分别为 22、24、24、0、1

  2. @NumberFormat("#.")

    读取到的数据分别为 22.、24.、24.、0.、1.

  3. @NumberFormat("#.##")

    读取到的数据分别为 22、23.6、24.12、0.15、1.15

  4. @NumberFormat("#%")

    读取到的数据分别为 2200%、2360%、2412%、15%、115%

  5. @NumberFormat("#.#%")

    读取到的数据分别为 2200%、2360%、2412.3%、15%、115.3%

roundingMode

使用 java.math 包下的枚举类 RoundingMode ,JDK中有详细的注释,这里不在赘述

自定义格式化

只需要写一个类,实现 Converter 接口 ,使用的时候在实体属性的 @ExcelProperty 注解中指定 converter 即可

@ExcelProperty(converter = CustomStringStringConverter.class)
private String string;

这里贴上官方的一个示例

public




    
 class CustomStringStringConverter implements Converter<String> {
    @Override
    public Class<?> supportJavaTypeKey() {
        return String.class;
    @Override
    public CellDataTypeEnum supportExcelTypeKey() {
        return CellDataTypeEnum.STRING;
     * 这里读的时候会调用
     * @param context
     * @return
    @Override
    public String convertToJavaData(ReadConverterContext<?> context) {
        return "自定义:" + context.getReadCellData().getStringValue();
     * 这里是写的时候会调用 不用管
     * @return
    @Override
    public WriteCellData<?> convertToExcelData(WriteConverterContext<String> context) {
        return new WriteCellData<>(context.getValue());
}

回调监听器

介绍

EasyExcel 不会自动帮我们将数据读写到 Java 对象,而是提供了回调监听器,类似于每一段数据的生命周期,在不同的时期,我们可以做不同的操作,这也包括了将数据写入 Java 对象或存入数据库等操作

接口

这是 EasyExcel 提供的监听器接口

public interface ReadListener<T> extends Listener {
     * 当读取出现异常时调用的方法
    default void onException(Exception exception, AnalysisContext context) throws Exception {
        throw exception;
     * 读取表头(按行)时调用的方法,参数中的headMap一次对应一行的表头
     * @param headMap key是列号,value是表头的值
    default void invokeHead(Map<Integer, ReadCellData<?>> headMap, AnalysisContext context) {}
     * 读取数据(按行)时调用的方法,参数中的data对应的是一行数据
     * @param data 泛型是自定义的实体类,数据是一行的数据
    void invoke(T data, AnalysisContext context);
     * 读取额外信息时调用
    default void extra




    
(CellExtra extra, AnalysisContext context) {}
     * 所有数据读取完毕后调用
    void doAfterAllAnalysed(AnalysisContext context);
     * 判断是否有下一行数据
    default boolean hasNext(AnalysisContext context) {
        return true;
}

PageReadListener

EasyExcel 帮我们封装了一个简单的读取监听器 PageReadListener

其内部包含一个缓存 list 与数量上限 BATCH_COUNT ,以及一个函数式接口 Consumer (我们作为使用者唯一需要传入的)。

内部的逻辑是:

源码如下

public class PageReadListener<T>




    
 implements ReadListener<T> {
     * Single handle the amount of data
    public static int BATCH_COUNT = 100;
     * Temporary storage of data
    private List<T> cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);
     * consumer
    private final Consumer<List<T>> consumer;
    public PageReadListener(Consumer<List<T>> consumer) {
        this.consumer = consumer;
    @Override
    public void invoke(T data, AnalysisContext context) {
        cachedDataList.add(data);
        if (cachedDataList.size() >= BATCH_COUNT) {
            consumer.accept(cachedDataList);
            cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);
    @Override
    public void doAfterAllAnalysed(AnalysisContext context) {
        if (CollectionUtils.isNotEmpty(cachedDataList)) {
            consumer.accept(cachedDataList);
}

简单工厂 EasyExcel

EasyExcel 是一个空的类,继承自 EasyExcelFactory ,是一个简单工厂,也是我们使用 EasyExcel 的核心入口。

EasyExcelFactory 内部集成了多种读写方式的 builder,根据需要,我们可以使用不同参数的 read readSheet write writeSheet writeTable 方法来获取对应的 builder

读 Excel

开始之前

详细代码请参考 官方教程

我们先导入 junit、lombok 和 log4j ,方便测试

<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.13.1</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.24</version>
</dependency>
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>1.7.32</version>
</dependency>
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-log4j12</artifactId>
    <version>1.7.32</version>
</dependency>

测试类

@Slf4j
public class ReadTest {
    String filePath = "C:\\Users\\11047\\Desktop\\demo.xlsx";
}

实体类

@Getter
@Setter
@EqualsAndHashCode
public class DemoData {
    String text1;
    String text2;
    String text3;
    @Override
    public String toString() {
        return String.format("[text1:%s, text2:%s, text3:%s]", text1, text2, text3);
}

Excel 中的数据

姓名 生日 年龄
lgz 1999/1/1 22
cjw 2000/12/12 23

简单的读取

前置知识: PageReadListener

@Test
public void simpleRead() {
    // 存放读取的数据
    List<MyData> list = new ArrayList<>();
    // 指定文件路径、实体类、回调监听器
    // 这里使用EasyExcel自带的读取监听器,并且将其内部的缓存添加到我们自己定义的 list,达到一个取出数据的作用
    EasyExcel.read(filePath, MyData.class, new PageReadListener<MyData>(dataList -> {
        list.addAll(dataList);
    })).sheet().doRead();
    log.info("读取到{}条数据", list.size());
}

这是最简单的读取逻辑,作用也只有一个,那就是读取数据然后存放到我们自己定义的 list 中

如果我们想要更复杂的操作,例如读取的同时存入数据库,那么 PageReadListener 就无法满足了,我们需要自己写一个监听器

自定义监听器

1. 方法一:实现 ReadListener 接口

ReadListener 只有两个方法需要我们重写,分别是读取数据时执行的 invoke 方法,和读取完毕后执行的 doAfterAllAnalysed 方法。

下面的代码模拟存入数据库的过程:

@Slf4j
public class DemoDataListener implements ReadListener<DemoData> {
    public static final int BATCH_COUNT = 100;
    private List<DemoData> cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);
    @Override
    public void invoke(DemoData data, AnalysisContext context) {
        log.info("读取1条数据:{}", data);
        cachedDataList.add(data);
        if (cachedDataList.size() >= BATCH_COUNT) {
            saveData();
            cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);
    @Override
    public void doAfterAllAnalysed(AnalysisContext context) {
        log.info("数据读取完毕");
        saveData();
    private void saveData() {
        log.info("正在存入数据库...");
        log.info("存入数据库成功");
}

测试:

@Test
public void simpleRead1() {
    EasyExcel.read(filePath, DemoData.class, new DemoDataListener()).sheet().doRead();
}

2. 方法二:匿名类

对于逻辑简单,代码量少,而且不需要重复使用的监听器,我们可以直接用匿名类的方式定义

@Test
public void simpleRead2() {
    EasyExcel.read(filePath, DemoData.class, new ReadListener<MyData>() {
        @Override
        public void invoke(Demodata data, AnalysisContext context) {
            log.info("读取一条数据:{}", data);
        @Override
        public void doAfterAllAnalysed(AnalysisContext context) {
            log.info("数据读取完毕");
    }).sheet().doRead();
}

指定列号或列名

上面我们用的实体类是这样的

@Getter
@Setter
@EqualsAndHashCode
public class DemoData {
    String text1;
    String text2;
    String text3;
    @Override
    public String toString() {
        return String.format("[text1:%s, text2:%s, text3:%s]", text1, text2, text3);
}

属性名是随便取的,也没有添加任何注解,这个时候 EasyExcel 会按照顺序读取,第一列的给 text1,第二列给 text2,…

如果想要指定列号或者列名,那么需要使用到 @ExcelProperty 注解

为了更高的可读性,我也会用上更合适的属性名

@Getter
@Setter
@EqualsAndHashCode
public class IndexOrNameData {
    // 不建议一个类中index和name混用
    @ExcelProperty(index = 0)
    private String name;
    @ExcelProperty("学号")
    private String number;
    @ExcelProperty("年龄")
    private Integer age;
    @Override
    public String toString() {
        return String.format("[name:%s, number:%s, age:%d]", name, number, age);
}

关于注解的更多详细说明,可以参考上面的 @ExcelProperty

@Test
public void indexOrNameRead() {
    EasyExcel.read(filePath, IndexOrNameData.class, new IndexOrNameDataListener()).sheet().doRead();
}

指定 sheet 表

上面的例子我们都只读取了一张 sheet 表,由于没有指定下标, 所以默认是从第 0 张表开始(这个 0 当然是计算机中下标的起始,应该不用我多说了,实际上对应的就是 Excel 文件中的第一张 sheet)

想要指定很简单,只需要在调用 sheet 方法时传入一个下标参数即可

多次读取

1. 读多张 sheet 表

如果你已经熟练掌握上面的例子,你应该会发现我们只能进行一张 sheet 表的读取,使用的方法是 doRead

点进去看源码,你会发现:读的动作实际使用的是 ExcelReader 对象的 read 方法,随后调用 finish 关闭了文件流

public void doRead() {
    if (excelReader == null) {
        throw new ExcelGenerateException("Must use 'EasyExcelFactory.read().sheet()' to call this method");
    excelReader.read(build());
    excelReader.finish();
}

那么我们想要读取多张 sheet 表也很简单,只要能拿到 ExcelReader 就可以了,当然,不要忘记在最后关闭文件流

ExcelReader 实现了 Closeable 接口,在 JDK 8 中,我们可以直接用 try-catch 来自动关闭流

// EasyExcel 中的 build 方法可以帮助我们获取到 ExcelReader 实例
try (ExcelReader excelReader = EasyExcel.read(filePath, DemoData.class, new DemoDataListener()).build()) {
}

ExcelReader read 方法接收 ReadSheet 参数,工厂中同样提供了构造方式

ReadSheet sheet = EasyExcel.readSheet(下标).build();

表结构相同

那么如果我们想要读取前两张表,可以这么写:

@Test
public void simpleRead3() {
    try (ExcelReader excelReader = EasyExcel.read(filePath, DemoData.class, new DemoDataListener()).build()) {
        // 读取第一张
        ReadSheet sheet = EasyExcel.readSheet(0).build();
        excelReader.read(sheet);
        // 读取第二张
        ReadSheet sheet = EasyExcel.readSheet(1).build();
        excelReader.read(sheet);
}

表结构不同

那么我们构造 ExcelReader 时就不需要指定实体类和监听器,而是由后续传入

@Test
public void simpleRead4() {
    try (ExcelReader excelReader = EasyExcel.read(filePath).build()) {
        // 指定第一张 sheet 表
        ReadSheet sheet1 = EasyExcel.readSheet(0)
            // 设置实体类
            .head(IndexOrNameData.class)
            // 设置监听器
            .registerReadListener(new IndexOrNameDataListener())
            // 构造对象
            .build();
        // 指定第二张 sheet 表
        ReadSheet sheet2 = EasyExcel.readSheet(1)
            .head(DemoData.class)
            .registerReadListener(new DemoDataListener())
            .build();
        // 进行读取
        excelReader.read(sheet1, sheet2);
}

2. 读全部的 sheet 表

直接使用 doReadAll 方法即可

@Test
public void repeatRead() {
    EasyExcel.read(filePath, MyData.class, new MyDataListener()).doReadAll();
}

数据类型转换

常用的转换有:

日期格式化示例

@DateTimeFormat("yyyy年MM月dd日 HH时mm分ss秒")
private String date;

数字格式化示例

@NumberFormat(value = "#.##%")
private String doubleData;

自定义格式化

第一步,写一个自定义的格式转换器,需要实现 Converter 接口,泛型是最终返回数据的类型

public class CustomStringStringConverter implements Converter<String> {
    // 这里返回支持转换的Java数据类型
    @Override
    public Class<?> supportJavaTypeKey() {
        return String.class;
    // 这里返回支持转换的Excel数据类型
    @Override
    public CellDataTypeEnum supportExcelTypeKey() {
        return CellDataTypeEnum.STRING;
    // 这里就是我们自定义的格式化逻辑
    @Override
    public String convertToJavaData(ReadConverterContext<?> context) throws Exception {
        // 将需要格式化的字符串前面加上“自定义”三个字
        return "自定义" + context.getReadCellData().getStringValue();
    // 这里是写入Excel的时候的转换,保持原样就可以
    @Override
    public WriteCellData<?> convertToExcelData(WriteConverterContext<String> context) throws Exception {
        return new WriteCellData<>(context.getValue());
}

随后,我们使用 @ExcelProperty 注解指定 converter

复杂表头

信息 a
s
姓名 学号 生日
lgz 3190421121 1999-01-01
cjw 3190421127 2000-12-12

如上所示的表格,我们在实体类中该怎么指定呢。

请仔细阅读 @ExcelProperty 部分

表头监听器 & 异常处理

ReadListener 中还有几个不是一定要重写的方法

这一节就要介绍其中的两个

@Slf4j
public class DemoHeadDataListener implements ReadListener<DemoData> {
    // onException 在出现异常时执行,并且不会中断程序
    @Override
    public void onException(Exception exception, AnalysisContext context) throws Exception {
        log.error("解析失败,但是继续解析下一行:{}", exception.getMessage());
        if (exception instanceof ExcelDataConvertException) {
            ExcelDataConvertException excelDataConvertException = (ExcelDataConvertException)exception;
            log.error("第{}行,第{}列解析异常,数据为:{}", excelDataConvertException.getRowIndex(), excelDataConvertException.getColumnIndex(), excelDataConvertException.getCellData());
    @Override
    public void invoke(DemoData data, AnalysisContext context) {
    // invokeHead 在解析头数据是执行,也是按行解析
    @Override
    public void invokeHead(Map<Integer, ReadCellData<?>> headMap, AnalysisContext context) {
        log.info("解析到一条头数据");
        for (Map.Entry<Integer, ReadCellData<?>> entry : headMap.entrySet()) {
            log.info("{}:{}", entry.getKey(), entry.getValue().getStringValue());
    @Override
    public




    
 void doAfterAllAnalysed(AnalysisContext context) {
}

CellData

CellData 是Excel 的原始数据类型,实体类中的属性可以用 CellData 来包裹

写 Excel

开始之前

准备好一个测试类

@Slf4j
public class WriteTest {
    String filePath = "C:\\Users\\11047\\Desktop\\writeDemo.xlsx";
    // 准备好一个方法,模拟从数据库获取数据
    private List<DemoData> data() {
        List<DemoData> list = new ArrayList<>();
        DemoData data1 = new DemoData();
        DemoData data2 = new DemoData();
        list.add(data1);
        list.add(data2);
        data1.setText1("hello");
        data1.setText2("world");
        data1.setText3("!!");
        data2.setText1("今天");
        data2.setText2("是");
        data2.setText3("星期一");
        return list;
}

简单的写

实体类

@Getter
@Setter
@EqualsAndHashCode
public class DemoData {
    String text1;
    String text2;
    String text3;
}

测试方法

@Test
public void simpleWrite() {
    // 方法1
    EasyExcel.write(filePath, DemoData.class).sheet().doWrite(this::data);
    // 方法2
    EasyExcel.write(filePath, DemoData.class).sheet().doWrite(data());
}

输出结果

text1 text2 text3
hello world !!
今天 星期一

指定包含哪些列

测试方法

@Test
public void includeWrite() {
    Set<String> includeColumnNames = new HashSet<>(




    
);
    includeColumnNames.add("text2");
    EasyExcel.write(filePath, DemoData.class).includeColumnFieldNames(columnNames).sheet().doWrite(data());
}

输出结果

text2
world

指定排除哪些列

测试方法

@Test
public void excludeOrIncludeWrite() {
    Set<String> excludeColumnNames = new HashSet<>();
    excludeColumnNames.add("text2");
    EasyExcel.write(filePath, DemoData.class).excludeColumnFieldNames(excludeColumnNames).sheet().doWrite(data());
}

输出结果

text1 text3
hello !!
今天 星期一

多次写入

1. 写入同一张 sheet

多次读取 一样,多次写入也是先创建一个 ExcelWriter 对象,每次写入就创建一个 WriteSheet

测试方法

@Test
public void repeatWrite() {
    try (ExcelWriter writer = EasyExcel.write(filePath, DemoData.class).build()) {
        WriteSheet sheet = EasyExcel.writerSheet().build();
        // 这里用循环来模拟多次写入,实际业务中应该从数据库中分页读取
        for (int i = 0; i < 5; i++) {
            writer.write(data(), sheet);
}

输出结果

text1 text2 text3
hello world !!
今天 星期一
hello world !!
今天 星期一
hello world !!
今天 星期一
hello world !!
今天 星期一
hello world !!
今天 星期一

2. 写入不同 sheet

同样可以参考 多次读取 助于理解

新用到的实体类

@Getter
@Setter
@EqualsAndHashCode
public class IndexOrNameData {
    // 不建议一个类中index和name混用
    @ExcelProperty(index = 0)
    private String name;
    @ExcelProperty("学号")
    private String number;
    @ExcelProperty("年龄")
    private Integer age;
}

测试方法

@Test
public void repeatWrite2() {
    try (ExcelWriter writer = EasyExcel.write(filePath, DemoData.class).build()) {
        // 第一张表用DemoData
        WriteSheet sheet = EasyExcel.writerSheet(0).head(DemoData.class).build();
        writer.write(data(), sheet);
        // 第二张表用IndexOrNameData
        WriteSheet sheet1 = EasyExcel.writerSheet(1).head(IndexOrNameData.class).build();
        writer.write(Collections.emptyList(), sheet1);
}

输出结果

第一张 sheet

text1 text2 text3
hello world !!
今天 星期一

第二张 sheet(只有表头,数据为空,因为参数传入的是空集合)

name 学号 年龄

特殊数据

图片

实体类

@Getter
@Setter
@EqualsAndHashCode
public class ImageDemoData {
     * 这些类型都是EasyExcel支持的图片类型,我们只要传入即可,不用关心实际实现
    // 方法1:直接以File文件的形式
    private File file;
    // 方法2:以输入流的形式
    private InputStream inputStream;
    // 方法3:以图片路径的形式,由于String类型默认是写入文字,所以要指定一个converter
    @ExcelProperty(converter = StringImageConverter.class)
    private String string;
    // 方法4:字节数组,一般都是File或者InputStream转换而来
    private byte[] byteArray;
    // 方法5:网络图片,只需要url地址
    private URL url;
    // 方法6:复杂形式
    private WriteCellData<Void> writeCellData;
}
1. 基本方法

测试方法

@Test
public void imageWrite() {
    String imagePath = "E:\\img\\5.jpg";
    try (InputStream inputStream = FileUtils.openInputStream(new File(imagePath))) {
        // 创建一行数据
        List<ImageDemoData> list = ListUtils.newArrayList();
        ImageDemoData imageDemoData = new ImageDemoData();
        list.add(imageDemoData);
        // 下面五种方法都是图片的导入,实际使用只要选一种方式即可
        imageDemoData.setByteArray(FileUtils.readFileToByteArray(new File(imagePath)));
        imageDemoData.setFile(new File(imagePath));
        imageDemoData.setString(imagePath);
        imageDemoData.setInputStream(inputStream);
        imageDemoData.setUrl(new URL("https://foruda.gitee.com/avatar/1666774722876138257/8677752_sagiri-kawaii01_1666774722.png!avatar60"




    
));
        // 写入
        EasyExcel.write(filePath, ImageDemoData.class).sheet().doWrite(list);
    } catch (Exception e) {
        log.error("", e);
}
2. 复杂操作

上面介绍的基本方法,是一个单元格对应一张图片

而实际需求可能是,一个单元格有两个图片,或者既有文字又有图片。那么这时候就需要用 WriteCellData 来详细构造单元格了

测试方法

@Test
public void complexImageWrite() {
    String imagePath = "E:\\img\\5.jpg";
    try {
        // 创建一行数据
        List<ImageDemoData> list = ListUtils.newArrayList();
        ImageDemoData imageDemoData = new ImageDemoData();
        list.add(imageDemoData);
        // 创建一个单元格对象
        WriteCellData<Void> writeCellData = new WriteCellData<>();
        imageDemoData.setWriteCellData(writeCellData);
        // 先写入文字
        writeCellData.setType(CellDataTypeEnum.STRING);
        writeCellData.setStringValue("额外放入文字");
        // 准备放入两张图片
        List<ImageData> imageDataList = new ArrayList<>();
        // ImageData是EasyExcel提供的图片类
        ImageData imageData = new ImageData();
        // 放入第一张图片(这里只是放入这个对象,图片数据还没读取呢)
        imageDataList.add(imageData);
        // 单元格设置图片集合
        writeCellData.setImageDataList(imageDataList);
        // 读取图片
        imageData.setImage(FileUtils.readFileToByteArray(new File(imagePath)));
        // 设置类型
        imageData.setImageType(ImageData.ImageType.PICTURE_TYPE_PNG);
        // 上 右 下 左 需要留空
        // 这个类似于 css 的 margin
        // 这里实测 不能设置太大 超过单元格原始大小后 打开会提示修复。暂时未找到很好的解法。
        imageData.setTop(5);
        imageData.setRight(40);
        imageData.setBottom(5);
        imageData.setLeft(5);
        // 第二张图片
        imageData =




    
 new ImageData();
        imageDataList.add(imageData);
        writeCellData.setImageDataList(imageDataList);
        imageData.setImage(FileUtils.readFileToByteArray(new File(imagePath)));
        imageData.setImageType(ImageData.ImageType.PICTURE_TYPE_PNG);
        imageData.setTop(5);
        imageData.setRight(5);
        imageData.setBottom(5);
        imageData.setLeft(50);
        // 设置图片的位置 假设 现在目标 是 覆盖 当前单元格 和当前单元格右边的单元格
        // 起点相对于当前单元格为0 当然可以不写
        imageData.setRelativeFirstRowIndex(0);
        imageData.setRelativeFirstColumnIndex(0);
        imageData.setRelativeLastRowIndex(0);
        // 前面3个可以不写  下面这个需要写 也就是 结尾 需要相对当前单元格 往右移动一格
        // 也就是说 这个图片会覆盖当前单元格和 后面的那一格
        imageData.setRelativeLastColumnIndex(1);
        // 写入数据
        EasyExcel.write(filePath, ImageDemoData.class).sheet().doWrite(list);
    } catch (IOException e) {
        log.error("", e);
}

超链接

实体类

@Getter
@Setter
@EqualsAndHashCode
public class HyperLinkDemoData {
    private WriteCellData<String> hyperLink;
}

测试方法

@Test
public void hyperlinkWrite() {
    // 创建一行数据
    ArrayList<HyperLinkDemoData> data = new ArrayList<>();
    HyperLinkDemoData hyperLinkDemoData = new HyperLinkDemoData();
    data.add(hyperLinkDemoData);
    // 创建单元格,显示的内容是 github
    WriteCellData<String> hyperLink = new WriteCellData<>("github");
    hyperLinkDemoData.setHyperLink(hyperLink);
    // HyperLinkData是EasyExcel提供的超链接类
    HyperlinkData hyperlinkData = new HyperlinkData();
    // 为单元格设置超链接
    hyperLink.setHyperlinkData(hyperlinkData)




    
;
    // 设置超链接的类型以及地址
    hyperlinkData.setHyperlinkType(HyperlinkData.HyperlinkType.URL);
    hyperlinkData.setAddress("https://github.com");
    // 写入数据
    EasyExcel.write(filePath, HyperLinkDemoData.class).sheet().doWrite(data);
}

输出数据

hyperLink
github

鼠标点击 github 单元格后,会打开浏览器进入 github 的网站

备注

实体类

@Getter
@Setter
@EqualsAndHashCode
public class CommentDemoData {
    private WriteCellData<String> comment;
}

测试方法

@Test
public void commentWrite() {
    // 创建一行数据
    ArrayList<CommentDemoData> data = new ArrayList<>();
    CommentDemoData commentDemoData = new CommentDemoData();
    data.add(commentDemoData);
    // 创建单元格
    WriteCellData<String> comment = new WriteCellData<>("备注的单元格信息");
    commentDemoData.setComment(comment);
    // CommentData是EasyExcel提供的备注类
    CommentData commentData = new CommentData();
    comment.setCommentData




    
(commentData);
    // 设置备注信息
    commentData.setAuthor("Sagiri_kawaii");
    commentData.setRichTextStringData(new RichTextStringData("这是一个备注"));
    // 备注的默认大小是按照单元格的大小 这里想调整到4个单元格那么大 所以向后 向下 各额外占用了一个单元格
    commentData.setRelativeLastColumnIndex(1);
    commentData.setRelativeLastRowIndex(1);
    // 写入数据,需要打开 inMemory,评论和富文本数据需要在内存中渲染。
    EasyExcel.write(filePath, CommentDemoData.class).inMemory(true).sheet().doWrite(data);
}

公式

实体类

@Getter
@Setter
@EqualsAndHashCode
public class FormulaDemoData {
    private WriteCellData<String> formula;
}

测试方法

@Test
public void formulaWrite() {
    // 创建一行数据
    ArrayList<FormulaDemoData> data = new ArrayList<>();
    FormulaDemoData formulaDemoData = new FormulaDemoData();
    data.add(formulaDemoData);
    // 创建单元格
    WriteCellData<String> formula = new WriteCellData<>();
    formulaDemoData.setFormula(formula);
    // FormulaData是EasyExcel提供的公式类
    FormulaData formulaData = new FormulaData();
    formula.setFormulaData(formulaData);
    // 设置公式
    // 将 123456789 中的第一个数字替换成 2
    // 这里只是例子 如果真的涉及到公式 能内存算好尽量内存算好 公式能不用尽量不用
    formulaData.setFormulaValue("REPLACE(123456789, 1, 1, 2)");
    // 写入数据
    EasyExcel.write(filePath, FormulaDemoData.class).sheet().doWrite(data);
}

样式

实体类

@Getter
@Setter
@EqualsAndHashCode
public class StyleDemoData {
    private WriteCellData<String> text1;
    private WriteCellData<String> text2;
    private WriteCellData<String> text3;
}

单个单元格

1. 单种样式

测试方法

@Test
public void styleWrite() {
    // 创建一行数据
    ArrayList<StyleDemoData> data = new ArrayList<>();
    StyleDemoData styleDemoData = new StyleDemoData();
    data.add(styleDemoData);
    // 设置单个单元格的样式
    WriteCellData<String> cell = new WriteCellData<>("单元格样式");
    cell.setType(CellDataTypeEnum.STRING);
    styleDemoData.setText1(cell);
    // WriteCellStyle是EasyExcel提供的样式类
    WriteCellStyle writeCellStyleData = new WriteCellStyle();
    cell.setWriteCellStyle(writeCellStyleData);
    // 这里需要指定 FillPatternType 为FillPatternType.SOLID_FOREGROUND 不然无法显示背景颜色.
    writeCellStyleData.setFillPatternType(FillPatternType.SOLID_FOREGROUND);
    // 背景绿色
    writeCellStyleData.setFillForegroundColor(IndexedColors.GREEN.getIndex());
    // 写入数据
    EasyExcel.write(filePath, StyleDemoData.class).sheet().doWrite(data);
}
2. 多种样式

测试方法

@Test
public void styleWrite2() {
    // 创建一行数据
    ArrayList<StyleDemoData> data = new ArrayList<>();
    StyleDemoData styleDemoData = new StyleDemoData();
    data.add(styleDemoData);
    // 创建单元格
    WriteCellData<String> cell = new WriteCellData<>();
    cell.setType(CellDataTypeEnum.RICH_TEXT_STRING);
    styleDemoData.setText1(cell);
    // RichTextStringData是EasyExcel提供的富文本类
    RichTextStringData richTextStringData = new RichTextStringData();
    cell.setRichTextStringDataValue(richTextStringData);
    richTextStringData.setTextString("红色绿色默认");
    // 设置前两字为红色
    WriteFont writeFont = new WriteFont();
    writeFont.setColor(IndexedColors.RED.getIndex());
    richTextStringData.applyFont(0, 2, writeFont);
    // 设置后两字为绿色
    writeFont = new WriteFont();
    writeFont.setColor(IndexedColors.GREEN.getIndex());
    richTextStringData.applyFont(2, 4, writeFont);
    // 写入数据,需要打开 inMemory,富文本数据需要在内存中渲染
    EasyExcel.write(filePath, StyleDemoData.class).inMemory(true).sheet().doWrite(data);
}

注解

1. 介绍

样式注解

注解 含义 使用范围
@HeadStyle 表头 单元格 样式 类、属性
@ContentStyle 内容 单元格 样式 类、属性
@HeadFontStyle 表头 字体 样式 类、属性
@ContentFontStyle 内容 字体 样式 类、属性
@ColumnWidth 列宽 类、属性
@HeadRowHeight 表头行高
@ContentRowHeight 内容行高
@Target({ElementType.FIELD, ElementType.TYPE})

写在类(Type)上相当于写在所有属性(All Field)上

而优先级 Type < Field ,即属性上的样式会覆盖类的样式

注解参数

@HeadStyle @ContentStyle 参数相同

@HeadFontStyle @ContentFontStyle 参数相同

@ContentFontStyle
参数 类型 含义 可选值
fontName String 字体名 参考Excel里的字体名
fontHeightInPoints short 字体大小 参考Excel字体大小
italic BooleanEnum 是否斜体 TRUE / FALSE
strikeout BooleanEnum 是否加删除线 TRUE / FALSE
bold BooleanEnum 是否加粗 TRUE / FALSE
color short 字体颜色 IndexedColors中枚举了65中颜色,参考对应的数字即可
typeOffset short 字体位置, 上标 下标 poi 的 Font 中枚举了 SS_开头的 3 个常量
underline byte 下划线 poi 的 Font 中枚举了 U_开头的 5 个常量
charset int 字符集 poi 的 FontCharset

@ContentStyle
参数 类型 含义 可选值
dataFormat short 数据格式化,例如数值、货币、日期、百分比等 EasyExcel 的 BuiltinFormats
hidden BooleanEnum 是否隐藏,不过目前好像还没有效果 TRUE / FALSE
locked BooleanEnum 是否锁定,需要配合拦截器 TRUE / FALSE
quotePrefix BooleanEnum 在单元格前面增加`符号,数字或公式将以字符串形式展示 TRUE / FALSE
horizontalAlignment HorizontalAlignmentEnum 水平对齐方式 EasyExcel 的 HorizontalAlignmentEnum
wrapped BooleanEnum 设置文本是否应换行显示 TRUE / FALSE
verticalAlignment VerticalAlignmentEnum 垂直对齐方式 EasyExcel 的 VerticalAlignmentEnum
rotation short 设置单元格中文本旋转角度。03版本的Excel旋转角度区间为-90°90°,07版本的Excel旋转角度区间为0°180°
indent short 文本缩进的空格数量
borderLeft BorderStyleEnum 左边框样式 EasyExcel 的 BorderStyleEnum
borderRight BorderStyleEnum 右边框样式 EasyExcel 的 BorderStyleEnum
borderTop BorderStyleEnum 上边框样式 EasyExcel 的 BorderStyleEnum
borderBottom BorderStyleEnum 下边框样式 EasyExcel 的 BorderStyleEnum
leftBorderColor short 左边框颜色 IndexedColors中枚举了65中颜色,参考对应的数字即可
rightBorderColor short 右边框颜色 IndexedColors中枚举了65中颜色,参考对应的数字即可
topBorderColor short 上边框颜色 IndexedColors中枚举了65中颜色,参考对应的数字即可
bottomBorderColor short 下边框颜色 IndexedColors中枚举了65中颜色,参考对应的数字即可
fillPatternType FillPatternTypeEnum 填充类型 EasyExcel 的 FillPatternTypeEnum
fillBackgroundColor short 背景色 IndexedColors中枚举了65中颜色,参考对应的数字即可
fillForegroundColor short 前景色 IndexedColors中枚举了65中颜色,参考对应的数字即可
shrinkToFit BooleanEnum 单元格是否自动适应大小 TRUE / FALSE

@ColumnWidth
参数 类型 含义
value int 列宽,默认自动,单位(字数)

@HeadRowHeight
参数 类型 含义
value short 行高,默认自动

@ContentRowHeight
参数 类型 含义
value short 行高,默认自动
2. 测试

实体类

@Getter
@Setter
@EqualsAndHashCode
// 头背景设置成红色 IndexedColors.RED.getIndex()
@HeadStyle(fillPatternType = FillPatternTypeEnum.SOLID_FOREGROUND, fillForegroundColor = 10)
// 头字体设置成20
@HeadFontStyle(fontHeightInPoints = 20)
// 内容的背景设置成绿色 IndexedColors.GREEN.getIndex()
@ContentStyle(fillPatternType = FillPatternTypeEnum.SOLID_FOREGROUND, fillForegroundColor = 17)
// 内容字体设置成20
@ContentFontStyle(fontHeightInPoints = 20)
public class DemoStyleData {
    // 字符串的头背景设置成粉红 IndexedColors.PINK.getIndex()
    @HeadStyle(fillPatternType = FillPatternTypeEnum.SOLID_FOREGROUND, fillForegroundColor = 14)
    // 字符串的头字体设置成30
    @HeadFontStyle(fontHeightInPoints = 30)
    // 字符串的内容的背景设置成天蓝 IndexedColors.SKY_BLUE.getIndex()
    @ContentStyle(fillPatternType = FillPatternTypeEnum.SOLID_FOREGROUND, fillForegroundColor = 40)
    // 字符串的内容字体设置成30
    @ContentFontStyle(fontHeightInPoints = 30)
    @ExcelProperty("字符串标题")
    private String string;
    @ExcelProperty("日期标题")
    private Date date;
    @ExcelProperty("数字标题")
    private Double doubleData;
}

测试方法

@Test
public void annotationStyleWrite() {
    // 创建一行数据
    ArrayList<DemoStyleData> data = new ArrayList<>();
    DemoStyleData demoStyleData = new DemoStyleData();
    data.add(demoStyleData);
    demoStyleData.setDoubleData(23.9);
    demoStyleData.setString("hhh");
    // 写入数据
    EasyExcel.write(filePath, DemoStyleData.class).sheet().doWrite(data);
}

拦截器

1. 使用已有的策略

测试方法

@Test
public void handlerStyleWrite1() {
    // 方法1 使用已有的策略 推荐
    // HorizontalCellStyleStrategy 每一行的样式都一样 或者隔行一样
    // AbstractVerticalCellStyleStrategy 每一列的样式都一样 需要自己回调每一页
    // 头的策略
    WriteCellStyle headWriteCellStyle = new WriteCellStyle();
    // 背景设置为红色
    headWriteCellStyle.setFillForegroundColor(IndexedColors.RED.getIndex());
    WriteFont headWriteFont = new WriteFont();
    headWriteFont.setFontHeightInPoints((short)20);
    headWriteCellStyle.setWriteFont(headWriteFont);
    // 内容的策略
    WriteCellStyle contentWriteCellStyle = new WriteCellStyle();
    // 这里需要指定 FillPatternType 为FillPatternType.SOLID_FOREGROUND 不然无法显示背景颜色.头默认了 FillPatternType所以可以不指定
    contentWriteCellStyle.setFillPatternType(FillPatternType.SOLID_FOREGROUND);
    // 背景绿色
    contentWriteCellStyle.setFillForegroundColor(IndexedColors.GREEN.getIndex());
    WriteFont contentWriteFont = new WriteFont();
    // 字体大小
    contentWriteFont.setFontHeightInPoints((short)20);
    contentWriteCellStyle.setWriteFont(contentWriteFont);
    // 这个策略是 头是头的样式 内容是内容的样式 其他的策略可以自己实现
    HorizontalCellStyleStrategy horizontalCellStyleStrategy =
            new HorizontalCellStyleStrategy(headWriteCellStyle, contentWriteCellStyle);
    // 这里 需要指定写用哪个class去写,然后写到第一个sheet,名字为模板 然后文件流会自动关闭
    EasyExcel.write(filePath, DemoData.class)
            .registerWriteHandler(horizontalCellStyleStrategy)
            .sheet("模板")
            .doWrite(data());
}
2. 使用easyexcel的方式完全自己写

测试方法

@Test
public void handlerStyleWrite2() {
    // 方法2: 使用easyexcel的方式完全自己写 不太推荐 尽量使用已有策略
    EasyExcel.write(filePath, DemoData.class)
            .registerWriteHandler(new CellWriteHandler() {
                @Override
                public void afterCellDispose(CellWriteHandlerContext context) {
                    // 当前事件会在 数据设置到poi的cell里面才会回调




    

                    // 判断不是头的情况 如果是fill 的情况 这里会==null 所以用not true
                    if (BooleanUtils.isNotTrue(context.getHead())) {
                        // 第一个单元格
                        // 只要不是头 一定会有数据 当然fill的情况 可能要context.getCellDataList() ,这个需要看模板,因为一个单元格会有多个 WriteCellData
                        WriteCellData<?> cellData = context.getFirstCellData();
                        // 这里需要去cellData 获取样式
                        // 很重要的一个原因是 WriteCellStyle 和 dataFormatData绑定的 简单的说 比如你加了 DateTimeFormat
                        // ,已经将writeCellStyle里面的dataFormatData 改了 如果你自己new了一个WriteCellStyle,可能注解的样式就失效了
                        // 然后 getOrCreateStyle 用于返回一个样式,如果为空,则创建一个后返回
                        WriteCellStyle writeCellStyle = cellData.getOrCreateStyle();
                        writeCellStyle.setFillForegroundColor(IndexedColors.RED.getIndex());
                        // 这里需要指定 FillPatternType 为FillPatternType.SOLID_FOREGROUND
                        writeCellStyle.setFillPatternType(FillPatternType.SOLID_FOREGROUND);
                        // 这样样式就设置好了 后面有个FillStyleCellWriteHandler 默认会将 WriteCellStyle 设置到 cell里面去 所以可以不用管了
            }).sheet("模板")
            .doWrite(data());
}
3. 使用poi的样式完全自己写

测试方法

@Test
public void handlerStyleWrite3() {
    // 方法3: 使用poi的样式完全自己写 不推荐
    // 坑1:style里面有dataformat 用来格式化数据的 所以自己设置可能导致格式化注解不生效
        // 坑2:不要一直去创建style 记得缓存起来 最多创建6W个就挂了
    EasyExcel.write(filePath, DemoData.class)
            .registerWriteHandler(new CellWriteHandler() {
                @Override
                public void afterCellDispose(CellWriteHandlerContext context) {
                    // 当前事件会在 数据设置到poi的cell里面才会回调
                    // 判断不是头的情况 如果是fill 的情况 这里会==null 所以用not true
                    if (BooleanUtils.isNotTrue(context.getHead())) {
                        Cell cell = context.getCell();
                        // 拿到poi的workbook
                        Workbook workbook = context.getWriteWorkbookHolder().getWorkbook();
                        // 这里千万记住 想办法能复用的地方把他缓存起来 一个表格最多创建6W个样式
                        // 不同单元格尽量传同一个 cellStyle
                        CellStyle cellStyle = workbook.createCellStyle();
                        cellStyle.setFillForegroundColor(IndexedColors.RED.getIndex());
                        // 这里需要指定 FillPatternType 为FillPatternType.SOLID_FOREGROUND
                        cellStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
                        cell.setCellStyle(cellStyle);
                        // 由于这里没有指定dataformat 最后展示的数据 格式可能会不太正确
                        // 这里要把 WriteCellData的样式清空, 不然后面还有一个拦截器 FillStyleCellWriteHandler 默认会将 WriteCellStyle 设置到
                        // cell里面去 会导致自己设置的不一样
                        context.getFirstCellData().setWriteCellStyle(null);
            }).sheet("模板")
            .doWrite(data());
}

合并单元格

注解

注解 含义 使用范围
@ContentLoopMerge 内容合并 属性
@OnceAbsoluteMerge 一次性合并
@ContentLoopMerge
参数 类型 含义
eachRow int 纵向合并的高度
columnExtend int 横向合并的宽度
@OnceAbsoluteMerge
参数 类型 含义
firstRowIndex int 需要合并的范围第一行下标
lastRowIndex int 需要合并的范围最后一行下标
firstColumnIndex int 需要合并的范围第一列下标
lastColumnIndex int 需要合并的范围最后一列下标

实体类

@Getter
@Setter
@EqualsAndHashCode
// 将第6-7行的2-3列合并成一个单元格
// @OnceAbsoluteMerge(firstRowIndex = 5, lastRowIndex = 6, firstColumnIndex = 1, lastColumnIndex = 2)
public class DemoMergeData {
    // 这一列 每隔2行 合并单元格
    @ContentLoopMerge(eachRow = 2)
    @ExcelProperty("字符串标题")
    private String string;
    @ExcelProperty("日期标题")
    private Date date;
    @ExcelProperty("数字标题")
    private Double doubleData;
}

测试方法

@Test
public void mergeWrite() {
    ArrayList<DemoMergeData> data = new ArrayList<>();
    DemoMergeData demoMergeData =




    
 new DemoMergeData();
    demoMergeData.setString("aaa");
    demoMergeData.setDate(new Date());
    demoMergeData.setDoubleData(12.3);
    data.add(demoMergeData);
    data.add(demoMergeData);
    data.add(demoMergeData);
    data.add(demoMergeData);
    EasyExcel.write(filePath, DemoMergeData.class).sheet().doWrite(data);
}

拦截器

测试方法

@Test
public void mergeWrite2() {
    // 每隔2行会合并 把eachColumn 设置成 3 也就是我们数据的长度,所以就第一列会合并。当然其他合并策略也可以自己写
    LoopMergeStrategy loopMergeStrategy = new LoopMergeStrategy(2, 0);
    // 这里 需要指定写用哪个class去写,然后写到第一个sheet,名字为模板 然后文件流会自动关闭
    EasyExcel.write(filePath, DemoData.class).registerWriteHandler(loopMergeStrategy).sheet("模板").doWrite(data());
}

一张 sheet 多个表

测试方法

@Test
public void tableWrite() {
    // 方法1 这里直接写多个table的案例了,如果只有一个 也可以直一行代码搞定,参照其他案
    // 这里 需要指定写用哪个class去写
    try (ExcelWriter excelWriter = EasyExcel.write(filePath, DemoData.class).build()) {
        // 把sheet设置为不需要头 不然会输出sheet的头 这样看起来第一个table 就有2个头了
        WriteSheet writeSheet = EasyExcel.writerSheet("模板").needHead(Boolean.FALSE).build();
        // 这里必须指定需要头,table 会继承sheet的配置,sheet配置了不需要,table 默认也是不需要
        WriteTable writeTable0 = EasyExcel.writerTable(0).needHead(Boolean.TRUE).build




    
();
        WriteTable writeTable1 = EasyExcel.writerTable(1).needHead(Boolean.TRUE).build();
        // 第一次写入会创建头
        excelWriter.write(data(), writeSheet, writeTable0);
        // 第二次写如也会创建头,然后在第一次的后面写入数据
        excelWriter.write(data(), writeSheet, writeTable1);
}

自定义拦截器

这些复杂操作就需要用到 apache 的 poi 了

头部超链接

拦截器

@Slf4j
public class CustomCellWriteHandler implements CellWriteHandler {
    @Override
    public void afterCellDispose(CellWriteHandlerContext context) {
        Cell cell = context.getCell();
        // 这里可以对cell进行任何操作
        log.info("第{}行,第{}列写入完成。", cell.getRowIndex(), cell.getColumnIndex());
        if (BooleanUtils.isTrue(context.getHead()) && cell.getColumnIndex() == 0) {
            CreationHelper createHelper = context.getWriteSheetHolder().getSheet().getWorkbook().getCreationHelper();
            Hyperlink hyperlink = createHelper.createHyperlink(HyperlinkType.URL);
            hyperlink.setAddress("https://github.com/alibaba/easyexcel");
            cell.setHyperlink(hyperlink);
}

测试方法

@Test




    

public void hyperHandlerTest() {
    EasyExcel.write(filePath, DemoData.class).registerWriteHandler(new CustomCellWriteHandler()).sheet().doWrite(data());
}

下拉框

拦截器

@Slf4j
public class CustomSheetWriteHandler implements SheetWriteHandler {
    @Override
    public void afterSheetCreate(SheetWriteHandlerContext context) {
        log.info("第{}个Sheet写入成功。", context.getWriteSheetHolder().getSheetNo());
        // 区间设置 第一列第一行和第二行的数据。由于第一行是头,所以第一、二行的数据实际上是第二三行
        CellRangeAddressList cellRangeAddressList = new CellRangeAddressList(1, 2, 0, 0);
        DataValidationHelper helper = context.getWriteSheetHolder().getSheet().getDataValidationHelper();
        DataValidationConstraint constraint = helper.createExplicitListConstraint(new String[] {"测试1", "测试2"});
        DataValidation dataValidation = helper.createValidation(constraint, cellRangeAddressList);
        context.getWriteSheetHolder().getSheet().addValidationData(dataValidation);
}

测试方法

@Test
public void sheetHandlerTest() {
    EasyExcel.write(filePath, DemoData.class).registerWriteHandler(new CustomSheetWriteHandler()).sheet().doWrite(data());
}

批注

拦截器

@Slf4j
public class CommentWriteHandler implements RowWriteHandler {
    @Override
    public void afterRowDispose(RowWriteHandlerContext context) {
        if (BooleanUtils.isTrue(context.getHead())) {
            Sheet sheet = context.getWriteSheetHolder().getSheet();
            Drawing<?> drawingPatriarch = sheet.createDrawingPatriarch();
            // 在第一行 第二列创建一个批注
            Comment comment =
                drawingPatriarch.createCellComment(new XSSFClientAnchor(0, 0, 0, 0, (short)1, 0, (short)2, 1));
            // 输入批注信息
            comment.setString(new XSSFRichTextString("创建批注!"));
            // 将批注添加到单元格对象中
            sheet.getRow(0).getCell(1).setCellComment(comment);
}

测试方法

@Test
public void commentHandlerTest() {
    EasyExcel.write(filePath, DemoData.class).registerWriteHandler(new CommentWriteHandler()).sheet().doWrite(data());
}

筛选

拦截器

public class CellFilterHandler implements CellWriteHandler {
    @Override
    public void afterCellDispose(CellWriteHandlerContext context) {
        Cell cell = context.getCell();
        if (BooleanUtils.isTrue(context.getHead()) && cell.getColumnIndex() == 0) {
            cell.getSheet().setAutoFilter(CellRangeAddress.valueOf(cell.getAddress().toString()));