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,代表列的下标

单行表头

姓名学号生日
lgz31904211211999-01-01
cjw31904211272000-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 指定列

多行表头

信息
姓名学号生日
lgz31904211211999-01-01
cjw31904211272000-12-12

@ExcelPropertyvalue 属性是一个字符串数组

当我们传入多个字符串时,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
姓名学号生日
lgz31904211211999-01-01
cjw31904211272000-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): 
0:信息
[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): 
2:a
[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): 
2:s
[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:姓名
[INFO] 16:12:10 cn.cimoc.easyexcel.DemoHeadDataListener.invokeHead(DemoHeadDataListener.java:37): 
1:学号
[INFO] 16:12:10 cn.cimoc.easyexcel.DemoHeadDataListener.invokeHead(DemoHeadDataListener.java:37): 
2:生日
[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
HH24小时制的小时
hh12小时制的小时
a上午/下午
mm分钟
ss

@NumberFormat

该注解包含两个参数:

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

value

由 3 种字符构成:#.%

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

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

行号数据
122
223.6
324.1234
415%
5115.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,根据需要,我们可以使用不同参数的 readreadSheetwritewriteSheetwriteTable 方法来获取对应的 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 中的数据

姓名生日年龄
lgz1999/1/122
cjw2000/12/1223

简单的读取

前置知识: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()) {
    
}

ExcelReaderread 方法接收 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
姓名学号生日
lgz31904211211999-01-01
cjw31904211272000-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());
}

输出结果

text1text2text3
helloworld!!
今天星期一

指定包含哪些列

测试方法

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

输出结果

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

输出结果

text1text2text3
helloworld!!
今天星期一
helloworld!!
今天星期一
helloworld!!
今天星期一
helloworld!!
今天星期一
helloworld!!
今天星期一

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

text1text2text3
helloworld!!
今天星期一

第二张 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
参数类型含义可选值
fontNameString字体名参考Excel里的字体名
fontHeightInPointsshort字体大小参考Excel字体大小
italicBooleanEnum是否斜体TRUE / FALSE
strikeoutBooleanEnum是否加删除线TRUE / FALSE
boldBooleanEnum是否加粗TRUE / FALSE
colorshort字体颜色IndexedColors中枚举了65中颜色,参考对应的数字即可
typeOffsetshort字体位置,上标下标poi 的 Font 中枚举了 SS_开头的 3 个常量
underlinebyte下划线poi 的 Font 中枚举了 U_开头的 5 个常量
charsetint字符集poi 的 FontCharset

@ContentStyle
参数类型含义可选值
dataFormatshort数据格式化,例如数值、货币、日期、百分比等EasyExcel 的 BuiltinFormats
hiddenBooleanEnum是否隐藏,不过目前好像还没有效果TRUE / FALSE
lockedBooleanEnum是否锁定,需要配合拦截器TRUE / FALSE
quotePrefixBooleanEnum在单元格前面增加`符号,数字或公式将以字符串形式展示TRUE / FALSE
horizontalAlignmentHorizontalAlignmentEnum水平对齐方式EasyExcel 的 HorizontalAlignmentEnum
wrappedBooleanEnum设置文本是否应换行显示TRUE / FALSE
verticalAlignmentVerticalAlignmentEnum垂直对齐方式EasyExcel 的 VerticalAlignmentEnum
rotationshort设置单元格中文本旋转角度。03版本的Excel旋转角度区间为-90°90°,07版本的Excel旋转角度区间为0°180°
indentshort文本缩进的空格数量
borderLeftBorderStyleEnum左边框样式EasyExcel 的 BorderStyleEnum
borderRightBorderStyleEnum右边框样式EasyExcel 的 BorderStyleEnum
borderTopBorderStyleEnum上边框样式EasyExcel 的 BorderStyleEnum
borderBottomBorderStyleEnum下边框样式EasyExcel 的 BorderStyleEnum
leftBorderColorshort左边框颜色IndexedColors中枚举了65中颜色,参考对应的数字即可
rightBorderColorshort右边框颜色IndexedColors中枚举了65中颜色,参考对应的数字即可
topBorderColorshort上边框颜色IndexedColors中枚举了65中颜色,参考对应的数字即可
bottomBorderColorshort下边框颜色IndexedColors中枚举了65中颜色,参考对应的数字即可
fillPatternTypeFillPatternTypeEnum填充类型EasyExcel 的 FillPatternTypeEnum
fillBackgroundColorshort背景色IndexedColors中枚举了65中颜色,参考对应的数字即可
fillForegroundColorshort前景色IndexedColors中枚举了65中颜色,参考对应的数字即可
shrinkToFitBooleanEnum单元格是否自动适应大小TRUE / FALSE

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

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

@ContentRowHeight
参数类型含义
valueshort行高,默认自动
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
参数类型含义
eachRowint纵向合并的高度
columnExtendint横向合并的宽度
@OnceAbsoluteMerge
参数类型含义
firstRowIndexint需要合并的范围第一行下标
lastRowIndexint需要合并的范围最后一行下标
firstColumnIndexint需要合并的范围第一列下标
lastColumnIndexint需要合并的范围最后一列下标

实体类

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

测试方法

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

文章作者: ❤纱雾
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 ❤纱雾 !
评论
  目录