EasyExcel的应用

发布于:2025-02-10 ⋅ 阅读:(26) ⋅ 点赞:(0)

一、简单使用

        引入依赖:
        这里我们可以使用最新的4.0.2版本,也可以选择之前的稳定版本,3.1.x以后的版本API大致相同,新的版本也会向前兼容(3.1.x之前的版本,部分API可能在高版本被废弃),关于POI、JDK版本适配问题,具体可参考官网-版本说明

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

        下载excel文件:

    @GetMapping("/download")
    public void excelDownload(HttpServletResponse response) throws IOException {
        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
        response.setCharacterEncoding("utf-8");
        EasyExcel.write(response.getOutputStream(), Data.class).sheet("模板").doWrite(datas);
        String fileName = URLEncoder.encode("测试", "UTF-8").replaceAll("\\+", "%20");
        response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
    }

        读取excel文件:

    @PostMapping("/read")
    public void read(MultipartFile file) throws IOException {
        
          1、这只是简单演示,一般不使用 doReadSync 方法,
             此方法同步执行的,即它会阻塞当前线程,直到读取完整个Excel文件并返回所有数据。
             读取大型文件时,可能会导致程序响应变慢或阻塞。
          2、使用head映射字段时,该实体类上不能加 @Accessors 注解,加上此注解
             会字段映射不成功。
          3、一般会使用监听器 + doRead 方法实现excel文件的读取
        List<Data> datas = EasyExcel.read(file.getInputStream()).sheet().head(Data.class).doReadSync();
        System.out.println(datas);
    }

二、常用注解

        1、@ExcelProperty注解

                这个注解应该是最常用的注解,通常用来映射字段跟excel的列名,有以下几个属性:

名称 默认值 描述
value 用于匹配excel中的头,必须全匹配,如果有多行头,会匹配最后一行头
order Integer.MAX_VALUE 优先级高于value,会根据order的顺序来匹配实体和excel中数据的顺序
index -1 优先级高于valueorder,会根据index直接指定到excel中具体的哪一列
converter 自动选择 指定当前字段用什么转换器,默认会自动选择。写的情况下只要实现com.alibaba.excel.converters.Converter#convertToExcelData(com.alibaba.excel.converters.WriteConverterContext<T>) 方法即可

         注意: 

         1、如果没有特殊的调整一般,使用value属性就够了,在读取或者导出时都能匹配或者映射为对应的列名。
         2、value 跟 index 可以在导出数据的时候配合使用,value指定列名,index指定该列的顺序,例如:

    @ExcelProperty(value = "性别",index = 3) 代表列名为 性别,导出到第三列的位置。
    但是在导入时,如果设置了order属性,表示会根据指定列来匹配字段,例如上面就会将第三列匹配为性别字段,如果该列字段为空,或者字段类型不匹配就会报错,一般在读取数据时不会这么使用这个属性。

        3、order 属性代表按顺序匹配,比如说导出数据时,会按照字段上该属性的顺序,依次为列设置对应字段的值,比如order最小的,就匹配第一列的值,依次往后,在导出时也是一样,order最小的值,导出到第一列依次往后。

        4、converter:自定义的类型转换器,该属性可以实现自定义处理类,这个功能通常用来在 Excel 数据与 Java 对象之间进行特定格式的转换,例如日期、布尔值、自定义对象等。

  • 实现 Converter 接口
    要自定义一个转换器,需要实现 EasyExcel 提供的 Converter 接口。

  • 重写必要的方法

    • supportJavaTypeKey(): 指定支持的 Java 数据类型。
    • convertToExcelData(): 将 Java 数据类型转换为 Excel 单元格数据。
    • convertToJavaData(): 将 Excel 单元格数据转换为 Java 数据类型

        EasyExcel 自带了一些常用的转换器(例如 LocalDateConverterIntegerConverter 等),可以直接使用而无需自定义。

         例如:在姓名上加上该属性:

@ExcelProperty(value = "姓名",converter = ExcelStringConverter.class)
private String name;

        实现自定义处理类:

public class ExcelStringConverter implements Converter<String> {


    @Override
    public Class<?> supportJavaTypeKey() {
        return String.class;
    }

    @Override
    public CellDataTypeEnum supportExcelTypeKey() {
        return CellDataTypeEnum.STRING;
    }

    @Override
    public String convertToJavaData(ReadCellData<?> cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) throws Exception {
        return cellData.getStringValue() + "导入数据进行处理!";
    }

    @Override
    public WriteCellData<?> convertToExcelData(String value, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) throws Exception {
        return new WriteCellData<>(value + "导出数据进行处理");
    }

}

        例如实现在 Excel 中用 "是""否" 表示布尔值,而不是默认的 true/false

public class BooleanStringConverter implements Converter<Boolean> {

    @Override
    public Class<?> supportJavaTypeKey() {
        return Boolean.class; // 支持的 Java 类型
    }

    @Override
    public WriteCellData<?> convertToExcelData(Boolean value, ExcelContentProperty contentProperty) {
        return new WriteCellData<>(value ? "是" : "否"); // 将布尔值转为字符串
    }

    @Override
    public Boolean convertToJavaData(ReadCellData<?> cellData, ExcelContentProperty contentProperty) {
        String stringValue = cellData.getStringValue();
        return "是".equals(stringValue); // 将字符串 "是"/"否" 转为布尔值
    }
}

         2、@ExcelIgnore注解
  • 作用范围:数据实体类的字段上;
  • 注解释义:当前字段不参与excel列的匹配,即处理时忽略该字段;

        在默认情况下,数据模型类中的所有字段都会参与匹配,可如果你定义的Java类中,有些字段在读写时并不需要参与进来,这时就可以给对应字段加上@ExcelIgnore注解,具备该注解的字段会被忽略。

        3、@ExcelIgnoreUnannotated注解 
  • 作用范围:数据模型类上;
  • 注解释义:匹配列时忽略所有未使用@ExcelProperty注解的字段;

        如果类中许多字段都不想参与excel读写,而你又嫌挨个加@ExcelIgnore注解麻烦,这时就可以直接在类上加一个@ExcelIgnoreUnannotated注解,以此来忽略所有未添加@ExcelProperty注解的字段。

        4、@DateTimeFormat注解
  • 作用范围:数据实体类的字段上;
  • 注解释义:用String接收日期数据时,会根据指定格式转换日期;
  • 可选参数:
    • value:日期数据转换为字符串的目标格式;
    • use1904windowing:excel日期数据默认从1900年开始,但有些会从1904开始;

        在解析excel文件时,如果使用String字段接收日期数据,就会根据指定的格式转换数据,格式可以参考java.text.SimpleDateFormat的写法,例如yyyy-MM-dd HH:mm:ss。而在往excel写数据时,如果Java中的字段类型为Date、LocalDate、LocalDateTime等日期类型,则会将日期数据转换为指定的格式写入对应列。

        例如:

    @DateTimeFormat(value = "yyyy年MM月dd日 HH时mm分ss秒")

        5、@NumberFormat注解     
  • 作用范围:数据实体类的字段上;
  • 注解释义:用String接收数值数据时,会根据指定格式转换数值;
  • 可选参数:
    • value:数值转换为字符串的目标格式;
    • roundingMode:数值格式化时的舍入模式,如四舍五入、向上取整等;

        这个注解和前一个注解类似,只不过是用于将非整数类型的数值数据转换成给定格式,格式可以参考java.text.DecimalFormat的写法,如#.##。除了可以指定格式外,还可以指定舍入模式,枚举可参考java.math.RoundingMode类。

        使用方法:

  • 指定数字格式
    使用 @NumberFormat 注解的 value 属性指定数字格式。例如:

    • #: 表示一个数字字符(整数部分)。
    • 0: 表示一个数字字符(小数部分,不足补零)。
    • ,: 表示千分位分隔符。
    • .: 表示小数点。
  • @ExcelProperty 搭配使用
    在数值类型字段上添加 @NumberFormat,并用 @ExcelProperty 指定列名。

    @ExcelProperty("销售金额")
    @NumberFormat("#,##0.00") // 指定数字格式,保留两位小数,带千分位
    private BigDecimal salesAmount;

       

三、常用生成注解

        1、@ColumnWidth注解
  • 作用范围:数据模型类上、字段上;
  • 注解释义:设置列的宽度;

        这个注解如果加在类上,则会对所有字段生效;如果单独加在某个字段上,则只对特定的列有效,单位是px

        例如:

    @ExcelProperty("销售金额")
    @ColumnWidth(200)
    private BigDecimal salesAmount;
        2、@ContentFontStyle注解
  • 作用范围:数据模型类上、字段上;
  • 注解释义:用于设置单元格内容字体格式的注解;
  • 可选参数:
    • fontName:字体名称,如“黑体、宋体、Arial”等;
    • fontHeightInPoints:字体高度,以磅为单位;
    • italic:是否设置斜体(字体倾斜);
    • strikeout:是否设置删除线;
    • color:字体的颜色,通过RGB值来设置;
    • typeOffset:偏移量,用于调整字体的位置;
    • underline:是否添加下划线;
    • bold:是否对字体加粗;
    • charset:设置编码格式,只能对全局生效(字段上设置无效)。

        这个注解用于设置主体内容的字体样式(不包含表头),与上个注解同理,加在类上对整个excel文件生效,加在字段上只对单列有效,可以通过该注解来设置字体风格、高度、是否斜体等属性。

    @ExcelProperty(value ="日期")
    @DateTimeFormat(value = "yyyy年MM月dd日 HH时mm分ss秒")
    @ContentFontStyle(
            fontName = "黑体",/* 字体类型 */
            fontHeightInPoints = 50, /* 字体高度,以磅为单位; */
            italic = BooleanEnum.TRUE,/* 是否设置斜体(字体倾斜); */
            strikeout = BooleanEnum.TRUE,/* 是否设置删除线; */
            color = 14,/* 字体的颜色,通过RGB值来设置;  0	黑色 (默认)
                                                    9	红色
                                                    10	绿色
                                                    12	蓝色
                                                    13	黄色
                                                    14	粉色
                                                    15	青色
                                                    16	白色 */
            typeOffset = 1,/*偏移量,用于调整字体的位置; */
            underline = 1,/* 是否添加下划线; */
            bold = BooleanEnum.TRUE/* 是否对字体加粗; */
    )
    private LocalDateTime date;

       

        3、@ContentRowHeight注解
  • 作用范围:数据模型类上;
  • 注解释义:用于设置行高。

        这个注解只能加在类上面,作用就是设置单元格的高度,但这里不能像Excel那样精准设置不同行的高度,只能设置所有单元格统一的高度。

@ContentRowHeight(80)
        4、@ContentStyle注解
  • 作用范围:数据模型类上、字段上;
  • 注解释义:用于设置内容格式;
属性名 类型 功能描述
horizontalAlignment HorizontalAlignment 设置单元格内容的水平对齐方式(如左对齐、居中、右对齐)。
verticalAlignment VerticalAlignment 设置单元格内容的垂直对齐方式(如顶部对齐、中间对齐、底部对齐)。
wrapped boolean 是否自动换行(true 开启自动换行)。
dataFormat short 设置单元格的数据格式(例如日期格式、数字格式等)。
fillPatternType FillPatternType 设置单元格的填充模式(如纯色填充、斜线填充等)。
fillForegroundColor short 设置单元格的前景色(通过颜色索引表示)。
fillBackgroundColor short 设置单元格的背景色(通过颜色索引表示)。
borderLeft BorderStyle 设置单元格左边框样式(如实线、虚线等)。
borderRight BorderStyle 设置单元格右边框样式。
borderTop BorderStyle 设置单元格顶部边框样式。
borderBottom BorderStyle 设置单元格底部边框样式。
leftBorderColor short 设置单元格左边框颜色。
rightBorderColor short 设置单元格右边框颜色。
topBorderColor short 设置单元格顶部边框颜色。
bottomBorderColor short 设置单元格底部边框颜色。
    @ContentStyle(
            horizontalAlignment = HorizontalAlignmentEnum.CENTER,/* 水平对齐方式,如居中、左对齐等; */
            verticalAlignment = VerticalAlignmentEnum.CENTER,/*垂直对齐方式,如上对齐、下对齐等;*/
            wrapped = BooleanEnum.TRUE, /* 设置文本是否应该换行(自动根据内容长度换行); */
            dataFormat = 0, /*数据格式,对应excel的内置数据格式; */
            fillPatternType = FillPatternTypeEnum.SOLID_FOREGROUND,/*设置单元格的填充模式(如纯色填充、斜线填充等)*/
            fillForegroundColor = 9,/*设置单元格的前景色(通过颜色索引表示)。*/
            fillBackgroundColor = 12,/*设置单元格的背景色(通过颜色索引表示)。*/
            borderLeft = BorderStyleEnum.THICK,/*设置单元格左边框样式(如实线、虚线等)。*/
            borderRight = BorderStyleEnum.THICK,/*设置单元格右边框样式。*/
            borderTop = BorderStyleEnum.THICK,/*设置单元格顶部边框样式。*/
            borderBottom = BorderStyleEnum.THICK,/*设置单元格底部边框样式。*/
            leftBorderColor = 14,/*设置单元格左边框颜色。*/
            rightBorderColor = 14,/*设置单元格右边框颜色。*/
            topBorderColor = 14,/*设置单元格顶部边框颜色。*/
            bottomBorderColor = 14/*设置单元格底部边框颜色。*/
    )

        这个注解的属性还有很多,需要的话可以自行再查阅。

        5、@HeadFontStyle注解
  • 作用范围:数据模型类上、字段上;
  • 注解释义:用于定制标题字体格式。

        这个注解的作用和可选参数,与@ContentFontStyle注解类似,不过这个注解是针对列头(表头)有效罢了。

  • 可选参数:
    • fontName:字体名称,例如 Arial宋体 等。
    • fontHeightInPoints:字体大小,以磅为单位.
    • bold:是否加粗。
    • color:字体颜色,使用 Excel 的内置颜色索引值。

        6、@HeadRowHeight注解
  • 作用范围:数据模型类上;
  • 注解释义:用于设置标题行的行高。

        此注解的作用参考@ContentRowHeight注解,当前注解只对表头生效。        

@HeadRowHeight(30) // 设置表头行高为 30pt
        7、@HeadStyle注解 
  • 作用范围:数据模型类上
  • 注解释义:用于设置标题样式。

        该注解的作用和可选参数参考@ContentStyle注解,但是当前注解只对表头生效。

        8、@OnceAbsoluteMerge注解
  • 作用范围:数据模型类上;
  • 注解释义:用于合并指定的单元格;
  • 可选参数:
    • firstRowIndex:从哪行开始合并;
    • lastRowIndex:到哪行结束合并;
    • firstColumnIndex:从哪列开始合并;
    • lastColumnIndex:到哪列结束合并。

        从这个注解提供的可选参数就能看出具体作用,这是通过单元格行、列索引的方式,指定生成excel文件时要合并的区域。不过要注意,使用该注解只能合并一次(对应OnceAbsoluteMerge这个合并策略类)。

@OnceAbsoluteMerge(firstRowIndex = 0, lastRowIndex = 1, firstColumnIndex = 0, lastColumnIndex = 1)

        9、@ContentLoopMerge注解
  • 作用范围:数据模型类的字段上;
  • 注解释义:用于合并单元格;
  • 可选参数:
    • eachRow:指定每x行合并为一行;
    • columnExtend:指定每x列合并为一列。

        该注解也是用于合并单元格的,但是可以合并多次,不过只能实现每隔n个单元格合并,使用起来限制很大,通常也不会选择通过这种注解的形式来合并单元格,这里了解即可。

四、常用读取Excel方法:

        1、同步读取所有数据后返回

        同步的返回,不推荐使用,如果数据量大会把数据放到内存里面,会影响性能。这里只做简单得举例:

    @PostMapping("/import")
    public void importExcel(MultipartFile file) throws IOException {

        // head: 指定读用哪个class去读
        // headRowNumber: 指定行头,如果行头指定错误可能会读取不到数据。如果多行头,可以设置其他值。没有指定头,也就是默认是第1行。
        // sheet:指定读哪个sheet页,从0开始,第一个sheet是0,第二个是1,默认就是读第一个
        // doReadSync: 同步读,读取完所有数据返回
        List<ExcelDemo> list = EasyExcel.read(file.getInputStream()).headRowNumber(1).head(ExcelDemo.class).sheet().doReadSync();
        for (ExcelDemo data : list) {
            log.info("读取到数据:{}", JSON.toJSONString(data));
        }

    }
        2、使用监听器读取所有数据

        监听器是EasyExcel常用的一个方法,监听器的好处就是可以一行一行获取数据,不用全部读完在进行处理。

        监听器示例:

@Slf4j
public class ExcelDemoListener extends AnalysisEventListener<ExcelDemo> {

    private final List<ExcelDemo> data = new ArrayList<>();

    /**
     * 每解析一条数据都会触发一次invoke()方法
     */
    @Override
    public void invoke(ExcelDemo excelDemo, AnalysisContext analysisContext) {
        log.info("成功解析到一条数据:{}", excelDemo);
        data.add(excelDemo);
    }

    /**
     * 当一个excel文件所有数据解析完成后,会触发此方法
     */
    @Override
    public void doAfterAllAnalysed(AnalysisContext analysisContext) {
        log.info("所有数据都已解析完毕!");
    }

    public List<ExcelDemo> getData() {
        return data;
    }
}

        使用监听器读取excel文件:

    @PostMapping("/import")
    public void importExcel(MultipartFile file) throws IOException {

        // 监听器需要手动new出来
        ExcelDemoListener excelDemoListener = new ExcelDemoListener();
        // 将监听器放入read方法中,会走监听器内部的方法来读取数据                      EasyExcel.read(file.getInputStream(),ExcelDemo.class,excelDemoListener).sheet().doRead();
        List<ExcelDemo> data = excelDemoListener.getData();
        log.info("总共解析到到" + data.size() + "条数据!");

    }

        监听器内部逻辑可以自行定义,一般会设置数据上限,当数据读取到上限时,就自动批量存储到数据库中。

        3、读多个sheet

        一次读取全部sheet,需要使用  doReadAll 方法,这个方法一次读取全部sheet页,并传给监听器处理。这中适用于全部sheet页全部都是同一个实体类接收。

        ExcelDemoListener excelDemoListener = new ExcelDemoListener();
        EasyExcel.read(file.getInputStream(),ExcelDemo.class,excelDemoListener).doReadAll();
        List<ExcelDemo> data = excelDemoListener.getData();
        log.info("总共解析到到" + data.size() + "条数据!");

        读取指定的sheet页,并指定不同的实体类接收。

        try (ExcelReader excelReader = EasyExcel.read(file.getInputStream()).build()) {
            // 这里为了简单 所以注册了 同样的head 和Listener 自己使用功能必须不同的Listener
            ExcelDemoListener excelDemoListener = new ExcelDemoListener();
            ReadSheet readSheet1 =
                    EasyExcel.readSheet(0).head(ExcelDemo.class).registerReadListener(excelDemoListener).build();
            ReadSheet readSheet2 =
                    EasyExcel.readSheet(1).head(ExcelDemo.class).registerReadListener(excelDemoListener).build();
            // 这里注意 一定要把sheet1 sheet2 一起传进去,不然有个问题就是03版的excel 会读取多次,浪费性能
            excelReader.read(readSheet1, readSheet2);
        }

        4、日期、数字或者自定义格式转换

        需要用到上面的注解,以及自定义转换器实现类。

        5、多行头

        当读取excel时,如果行头并不是第一行,就需要配合 headRowNumber 方法指定行头是哪一行,但是如果指定的不对,会导致数据读取失败。

        ExcelDemoListener excelDemoListener = new ExcelDemoListener();
        EasyExcel.read(file.getInputStream(),ExcelDemo.class,excelDemoListener).sheet()
                // 默认读取第一行为表头,如果第一行不是,需要单独设置headRowNumber,从0开始
                .headRowNumber(1).doRead();
        List<ExcelDemo> data = excelDemoListener.getData();
        log.info("总共解析到到" + data.size() + "条数据!");
        6、读取表头数据

        只需要在监听器中实现一个方法,只要重写invokeHeadMap方法即可

    @Override
    public void invokeHead(Map<Integer, ReadCellData<?>> headMap, AnalysisContext context) {
        log.info("解析到一条头数据:{}", JSON.toJSONString(headMap));
    }

    @Override
    public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) {
        log.info("解析到一条头数据:{}", JSON.toJSONString(headMap));
        // 如果想转成成 Map<Integer,String>
        // 方案1: 不要implements ReadListener 而是 extends AnalysisEventListener
        // 方案2: 调用 ConverterUtils.convertToStringMap(headMap, context) 自动会转换
    }

 解析到一条头数据:{0:{"columnIndex":0,"dataFormatData":{"format":"General","index":0},"rowIndex":0,"stringValue":"姓名","type":"STRING"},1:{"columnIndex":1,"dataFormatData":{"$ref":"$[0].dataFormatData"},"rowIndex":0,"stringValue":"日期","type":"STRING"},2:{"columnIndex":2,"dataFormatData":{"$ref":"$[0].dataFormatData"},"rowIndex":0,"stringValue":"年龄","type":"STRING"},3:{"columnIndex":3,"dataFormatData":{"$ref":"$[0].dataFormatData"},"rowIndex":0,"stringValue":"薪水","type":"STRING"},4:{"columnIndex":4,"dataFormatData":{"$ref":"$[0].dataFormatData"},"rowIndex":0,"stringValue":"地址","type":"STRING"}} 

成功解析到一条数据:ExcelDemo(name=张三, age=18, salary=11111.11, address=北京, dateTime=2024-12-21T20:20:20) 

        7、额外信息(批注、超链接、合并单元格信息读取)

        一般很少用,需要可以再了解一下,这里只简单做示例:
        需要 在监听器里面多实现一个 extra 方法:

 @Override
    public void extra(CellExtra extra, AnalysisContext context) {
        log.info("读取到了一条额外信息:{}", JSON.toJSONString(extra));
        switch (extra.getType()) {
            case COMMENT:
                log.info("额外信息是批注,在rowIndex:{},columnIndex;{},内容是:{}", extra.getRowIndex(), extra.getColumnIndex(),
                    extra.getText());
                break;
            case HYPERLINK:
                if ("Sheet1!A1".equals(extra.getText())) {
                    log.info("额外信息是超链接,在rowIndex:{},columnIndex;{},内容是:{}", extra.getRowIndex(),
                        extra.getColumnIndex(), extra.getText());
                } else if ("Sheet2!A1".equals(extra.getText())) {
                    log.info(
                        "额外信息是超链接,而且覆盖了一个区间,在firstRowIndex:{},firstColumnIndex;{},lastRowIndex:{},lastColumnIndex:{},"
                            + "内容是:{}",
                        extra.getFirstRowIndex(), extra.getFirstColumnIndex(), extra.getLastRowIndex(),
                        extra.getLastColumnIndex(), extra.getText());
                } else {
                    Assert.fail("Unknown hyperlink!");
                }
                break;
            case MERGE:
                log.info(
                    "额外信息是超链接,而且覆盖了一个区间,在firstRowIndex:{},firstColumnIndex;{},lastRowIndex:{},lastColumnIndex:{}",
                    extra.getFirstRowIndex(), extra.getFirstColumnIndex(), extra.getLastRowIndex(),
                    extra.getLastColumnIndex());
                break;
            default:
        }
    }
 // 这里 需要指定读用哪个class去读,然后读取第一个sheet
        EasyExcel.read(fileName, DemoExtraData.class, new DemoExtraListener())
            // 需要读取批注 默认不读取
            .extraRead(CellExtraTypeEnum.COMMENT)
            // 需要读取超链接 默认不读取
            .extraRead(CellExtraTypeEnum.HYPERLINK)
            // 需要读取合并单元格信息 默认不读取
            .extraRead(CellExtraTypeEnum.MERGE).sheet().doRead();

        8、读取公式和单元格类型

        也比较少用,需要自行钻研,只简单举例:

@Getter
@Setter
@EqualsAndHashCode
public class CellDataReadDemoData {
    private CellData<String> string;
    // 这里注意 虽然是日期 但是 类型 存储的是number 因为excel 存储的就是number
    private CellData<Date> date;
    private CellData<Double> doubleData;
    // 这里并不一定能完美的获取 有些公式是依赖性的 可能会读不到 这个问题后续会修复
    private CellData<String> formulaValue;
}
   @Test
    public void cellDataRead() {
        String fileName = TestFileUtil.getPath() + "demo" + File.separator + "cellDataDemo.xlsx";
        // 这里 需要指定读用哪个class去读,然后读取第一个sheet
        EasyExcel.read(fileName, CellDataReadDemoData.class, new CellDataDemoHeadDataListener()).sheet().doRead();
    }
        9、数据转换等异常处理

        需要在监听器里面实现重写onException方法即可:

  @Override
    public void onException(Exception exception, AnalysisContext context) {
        log.error("解析失败,但是继续解析下一行:{}", exception.getMessage());
        // 如果是某一个单元格的转换异常 能获取到具体行号
        // 如果要获取头的信息 配合invokeHeadMap使用
        if (exception instanceof ExcelDataConvertException) {
            ExcelDataConvertException excelDataConvertException = (ExcelDataConvertException)exception;
            log.error("第{}行,第{}列解析异常,数据为:{}", excelDataConvertException.getRowIndex(),
                excelDataConvertException.getColumnIndex(), excelDataConvertException.getCellData());
        }
    }
        10、不创建对象的读

        不创建对象的读,可以接收一个map集合,然后在监听器中自行进行转换。

@Slf4j
public class DataListener extends AnalysisEventListener<Map<Integer, String>> {
    /**
     * 每隔5条存储数据库,实际使用中可以100条,然后清理list ,方便内存回收
     */
    private static final int BATCH_COUNT = 5;
    private List<Map<Integer, String>> dataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);

    @Override
    public void invoke(Map<Integer, String> data, AnalysisContext context) {
        log.info("解析到一条数据:{}", JSON.toJSONString(data));
        dataList.add(data);
        if (dataList.size() >= BATCH_COUNT) {
            saveData();
            dataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);
        }
    }

    @Override
    public void doAfterAllAnalysed(AnalysisContext context) {
        saveData();
        log.info("所有数据解析完成!");
    }

    /**
     * 加上存储数据库
     */
    private void saveData() {
        log.info("{}条数据,开始存储数据库!", dataList.size());
        log.info("存储数据库成功!");
    }
}
        // 这里 只要,然后读取第一个sheet 同步读取会自动finish
        EasyExcel.read(fileName, new DataListener()).sheet().doRead();

五、常用导出Excel方法

        1、简单导出

         注意:简单导出 在数据量不大的情况下可以使用(5000以内,具体也要看实际情况),数据量大参照 重复多次写入

    @PostMapping("/export")
    public void exportExcel(HttpServletResponse response) throws IOException {

        // write 指定输出流,跟模板对象
        // sheet 指定导出sheet页名称
        // dowrite 指定数据源
        EasyExcel.write(response.getOutputStream(), ExcelDemo.class)
                .sheet("excel模板")
                .doWrite(this::getDatas);

        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
        response.setCharacterEncoding("utf-8");
        String fileName = URLEncoder.encode("测试导出excel", "UTF-8").replaceAll("\\+", "%20");
        response.setHeader("Content-disposition", "attachment;filename*=utf-8" + fileName + ".xlsx");
    }
        2、根据参数只导出指定列

              根据参数名列表,导出指定字段,需要使用 includeColumnFieldNames 方法:

        //导出指定字段
        Set<String> exportFields = new HashSet<String>();
        exportFields.add("name");
        exportFields.add("age");

        // write 指定输出流,跟模板对象
        // sheet 指定导出sheet页名称
        // dowrite 指定数据源
        EasyExcel.write(response.getOutputStream(), ExcelDemo.class)
                .includeColumnFieldNames(exportFields)
                .sheet("excel模板")
                .doWrite(getDatas());

        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
        response.setCharacterEncoding("utf-8");
        String fileName = URLEncoder.encode("测试导出excel", "UTF-8").replaceAll("\\+", "%20");
        response.setHeader("Content-disposition", "attachment;filename*=utf-8" + fileName + ".xlsx");

        根据参数名列表,忽略指定字段,需要使用 excludeColumnFiledNames 方法:

        //导出指定字段
        Set<String> noExportFields = new HashSet<String>();
        noExportFields.add("name");
        noExportFields.add("age");

        // write 指定输出流,跟模板对象
        // sheet 指定导出sheet页名称
        // dowrite 指定数据源
        EasyExcel.write(response.getOutputStream(), ExcelDemo.class)
                .excludeColumnFieldNames(noExportFields)
                .sheet("excel模板")
                .doWrite(getDatas());

        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
        response.setCharacterEncoding("utf-8");
        String fileName = URLEncoder.encode("测试导出excel", "UTF-8").replaceAll("\\+", "%20");
        response.setHeader("Content-disposition", "attachment;filename*=utf-8" + fileName + ".xlsx");

         导出指定列的数据还可以结合 @ExcelIgnore注解 或 @ExcelIgnoreUnannotated注解 使用,来导出指定列,或者忽略某些列。

        3、复杂头写入

        使用@ExcelProperty注解即可设置:

@Data
@AllArgsConstructor
@NoArgsConstructor
public class ExcelDemo {

    @ExcelProperty({"表头1","姓名"})
    private String name;

    @ExcelProperty({"表头1","年龄"})
    private int age;

    @ExcelProperty({"薪水"})
    private BigDecimal salary;

    @ExcelProperty({"表头2","地址"})
    private String address;

    @ExcelProperty({"表头2","日期"})
    private LocalDateTime dateTime;

}

        

         相同表头会合并。

        4、重复多次写入(写到单个或者多个Sheet)

        重复写入同一个sheet页:

        // 方法1: 如果写到同一个sheet
        // 这里 需要指定写用哪个class去写
        try (ExcelWriter excelWriter = EasyExcel.write(response.getOutputStream(), ExcelDemo.class).build()) {
            // 这里注意 如果同一个sheet只要创建一次
            WriteSheet writeSheet = EasyExcel.writerSheet("模板").build();
            // 去调用写入,这里我调用了五次,实际使用时根据数据库分页的总的页数来
            for (int i = 0; i < 5; i++) {
                // 分页去数据库查询数据 这里可以去数据库查询每一页的数据
                List<ExcelDemo> data = getDatas();
                excelWriter.write(data, writeSheet);
            }
        }

        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
        response.setCharacterEncoding("utf-8");
        String fileName = URLEncoder.encode("测试导出excel", "UTF-8").replaceAll("\\+", "%20");
        response.setHeader("Content-disposition", "attachment;filename*=utf-8" + fileName + ".xlsx");

        写入不同得sheet页,同一个对象:

        // 方法2: 如果写到不同的sheet 同一个对象
        // 这里 指定文件
        try (ExcelWriter excelWriter = EasyExcel.write(response.getOutputStream(), ExcelDemo.class).build()) {
            // 去调用写入,这里我调用了五次,实际使用时根据数据库分页的总的页数来。这里最终会写到5个sheet里面
            for (int i = 0; i < 5; i++) {
                // 每次都要创建writeSheet 这里注意必须指定sheetNo 而且sheetName必须不一样
                WriteSheet writeSheet = EasyExcel.writerSheet(i, "模板" + i).build();
                // 分页去数据库查询数据 这里可以去数据库查询每一页的数据
                List<ExcelDemo> data = getDatas();
                excelWriter.write(data, writeSheet);
            }
        }

        写入不同的sheet页,不同的对象:

        // 方法3 如果写到不同的sheet 不同的对象
        // 这里 指定文件
        try (ExcelWriter excelWriter = EasyExcel.write(response.getOutputStream()).build()) {
            // 去调用写入,这里我调用了五次,实际使用时根据数据库分页的总的页数来。这里最终会写到5个sheet里面
            for (int i = 0; i < 5; i++) {
                // 每次都要创建writeSheet 这里注意必须指定sheetNo 而且sheetName必须不一样。这里注意DemoData.class 可以每次都变,我这里为了方便 所以用的同一个class
                // 实际上可以一直变
                WriteSheet writeSheet = EasyExcel.writerSheet(i, "模板" + i).head(ExcelDemo.class).build();
                // 分页去数据库查询数据 这里可以去数据库查询每一页的数据
                List<ExcelDemo> data = getDatas();
                excelWriter.write(data, writeSheet);
            }
        }

        5、日期、数字或者自定义格式转换

        日期、数字、自定义格式转行,可以结合 注解:
        @DateTimeFormat("yyyy年MM月dd日HH时mm分ss秒")
        @NumberFormat("#.##%")
        Converter 自定义转换器
        上面写了,不再举例

       

        6、图片导出

        7、超链接、备注、公式、指定单个单元格的样式、单个单元格多种样式

        8、根据模板写入

        我得理解就是,在已经提供了一份excel文件中,继续导入数据,例如我们现在有一份excel文件如下所示:

        
        现在我们要以这个文件为模板,在这个文件得基础上继续导出数据,会得到如下所示:
       

        导出代码:

       String templateFileName = "C:\\Users\\Administrator\\Desktop\\ces\\" + "response111.xlsx";
        // 这里 需要指定写用哪个class去写,然后写到第一个sheet,名字为模板 然后文件流会自动关闭
        // 这里要注意 withTemplate 的模板文件会全量存储在内存里面,所以尽量不要用于追加文件,如果文件模板文件过大会OOM
        // 如果要再文件中追加(无法在一个线程里面处理,可以在一个线程的建议参照多次写入的demo) 建议临时存储到数据库 或者 磁盘缓存(ehcache) 然后再一次性写入
        EasyExcel.write(response.getOutputStream(), ExcelDemo.class).withTemplate(templateFileName).sheet().doWrite(getDatas());
         9、列宽、行高

        需要结合注解: 
        @ContentRowHeight(10) :设置行高为10px
        @HeadRowHeight(20):设置标题行的行高为20px
        @ColumnWidth(25):列的宽度为25px

@Data
@AllArgsConstructor
@NoArgsConstructor
@ContentRowHeight(10)
@HeadRowHeight(20)
@ColumnWidth(25)
public class ExcelDemo {

    @ExcelProperty({"表头1","姓名"})
    private String name;

    @ExcelProperty({"表头1","年龄"})
    private int age;

    @ExcelProperty({"薪水"})
    private BigDecimal salary;

    @ExcelProperty({"表头2","地址"})
    private String address;

    @ExcelProperty({"表头2","日期"})
    private LocalDateTime dateTime;
}
        10、注解形式自定义样式

        需要结合上面讲述得注解:

@Data
@AllArgsConstructor
@NoArgsConstructor
// 头背景设置成红色 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 ExcelDemo {

    // 字符串的头背景设置成粉红 IndexedColors.PINK.getIndex()
    @HeadStyle(fillPatternType = FillPatternTypeEnum.SOLID_FOREGROUND, fillForegroundColor = 14)
    // 字符串的头字体设置成20
    @HeadFontStyle(fontHeightInPoints = 30)
    // 字符串的内容的背景设置成天蓝 IndexedColors.SKY_BLUE.getIndex()
    @ContentStyle(fillPatternType = FillPatternTypeEnum.SOLID_FOREGROUND, fillForegroundColor = 40)
    // 字符串的内容字体设置成20
    @ContentFontStyle(fontHeightInPoints = 30)
    @ExcelProperty({"表头1","姓名"})
    private String name;

    @ExcelProperty({"表头1","年龄"})
    private int age;

    @ExcelProperty({"薪水"})
    private BigDecimal salary;

    @ExcelProperty({"表头2","地址"})
    private String address;

    @ExcelProperty({"表头2","日期"})
    private LocalDateTime dateTime;

}

        一般只会设置一些简单样式,具体需要可以自行查询相应注解。

        11、使用内置策略模式设置自定义样式

         EasyExcel中提供了两个样式策略,用来是设置导出文件得样式,只需要简单配置即可使用:
        HorizontalCellStyleStrategy:允许设置每一行的样式一致,或者隔行样式一致。
        AbstractVerticalCellStyleStrategy:使用这个策略时,可以为每一列单独设置样式,需要通过回调函数来定义不同列的样式。

         设置每一行得样式:

        HorizontalCellStyleStrategy 方法,一般接收两个 WriteCellStyle 类型得参数,第一个是设置表头格式得,第二个是设置内容格式的。
        WriteCellStyle 是 EasyExcel 中用于设置 Excel 单元格样式的类。每个属性用于定义单元格的不同样式,包括字体、边框、填充颜色、对齐方式等。下面是对每个参数详细得简单的讲解:

        WriteCellStyle cellStyle = new WriteCellStyle();

        // 1. DataFormatData 设置数据格式
        // 这里假设你设置为数字格式, 示例代码中未使用具体的 DataFormatData
        // cellStyle.setDataFormatData(new DataFormatData("0.00"));

        // 2. WriteFont 设置字体
        WriteFont font = new WriteFont();
        font.setFontHeightInPoints((short) 12);  // 字体大小为 12
        font.setBold(true);  // 设置加粗
        font.setFontName("Arial");  // 字体设置为 Arial
        cellStyle.setWriteFont(font);

        // 3. hidden 设置隐藏
        cellStyle.setHidden(false);  // 设置为可见

        // 4. locked 设置是否锁定
        cellStyle.setLocked(true);  // 设置单元格为锁定,不能编辑

        // 5. quotePrefix 设置前缀引号
        cellStyle.setQuotePrefix(true);  // 设置文本前加引号

        // 6. HorizontalAlignment 设置水平对齐
        cellStyle.setHorizontalAlignment(HorizontalAlignment.CENTER);  // 设置为居中对齐

        // 7. wrapped 设置是否换行
        cellStyle.setWrapped(true);  // 设置文本自动换行

        // 8. VerticalAlignment 设置垂直对齐
        cellStyle.setVerticalAlignment(VerticalAlignment.CENTER);  // 设置为垂直居中

        // 9. rotation 设置文本旋转
        cellStyle.setRotation((short) 90);  // 设置文本旋转 90 度

        // 10. indent 设置缩进
        cellStyle.setIndent((short) 2);  // 设置缩进为 2 个字符

        // 11-14. BorderStyle 设置边框样式
        cellStyle.setBorderLeft(BorderStyle.THIN);  // 设置左边框为细边框
        cellStyle.setBorderRight(BorderStyle.MEDIUM);  // 设置右边框为中等边框
        cellStyle.setBorderTop(BorderStyle.THICK);  // 设置顶部边框为粗边框
        cellStyle.setBorderBottom(BorderStyle.DOTTED);  // 设置底部边框为虚线

        // 15-18. 边框颜色设置
        cellStyle.setLeftBorderColor(IndexedColors.BLACK.getIndex());  // 左边框颜色为黑色
        cellStyle.setRightBorderColor(IndexedColors.BLUE.getIndex());  // 右边框颜色为蓝色
        cellStyle.setTopBorderColor(IndexedColors.GREEN.getIndex());  // 顶部边框颜色为绿色
        cellStyle.setBottomBorderColor(IndexedColors.RED.getIndex());  // 底部边框颜色为红色

        // 19. FillPatternType 设置填充模式
        cellStyle.setFillPatternType(FillPatternType.SOLID_FOREGROUND);  // 设置为实心填充模式

        // 20-21. 背景颜色和前景颜色设置
        cellStyle.setFillBackgroundColor(IndexedColors.YELLOW.getIndex());  // 设置背景色为黄色
        cellStyle.setFillForegroundColor(IndexedColors.ORANGE.getIndex());  // 设置前景色为橙色

        // 22. shrinkToFit 设置是否自适应缩小
        cellStyle.setShrinkToFit(true);  // 设置文本自适应缩小

        简单使用:

        //行头字体
        WriteFont headFont = new WriteFont();
        headFont.setFontName("华文楷体");
        headFont.setFontHeightInPoints((short) 18);
        headFont.setBold(true);
        //内容字体
        WriteCellStyle cellStyle = createCellStyle();
        WriteFont contentFont = new WriteFont();
        contentFont.setFontName("宋体");
        contentFont.setFontHeightInPoints((short) 10);
        contentFont.setBold(false);
        //行头设置
        WriteCellStyle headCellStyle = new WriteCellStyle();
        headCellStyle.setWriteFont(headFont);
        headCellStyle.setFillForegroundColor(IndexedColors.WHITE1.getIndex());
        headCellStyle.setBorderTop(BorderStyle.THIN);
        headCellStyle.setBorderBottom(BorderStyle.THIN);
        headCellStyle.setBorderLeft(BorderStyle.THIN);
        headCellStyle.setBorderRight(BorderStyle.THIN);
        headCellStyle.setHorizontalAlignment(HorizontalAlignment.CENTER);
        //内容设置
        WriteCellStyle contentCellStyle = new WriteCellStyle();
        contentCellStyle.setWriteFont(contentFont);
        contentCellStyle.setBorderTop(BorderStyle.THIN);
        contentCellStyle.setBorderBottom(BorderStyle.THIN);
        contentCellStyle.setBorderLeft(BorderStyle.THIN);
        contentCellStyle.setBorderRight(BorderStyle.THIN);
        contentCellStyle.setHorizontalAlignment(HorizontalAlignment.CENTER);

        HorizontalCellStyleStrategy horizontalCellStyleStrategy = new HorizontalCellStyleStrategy(headCellStyle, cellStyle);

        EasyExcel.write(response.getOutputStream(), ExcelDemo.class)
                .registerWriteHandler(horizontalCellStyleStrategy)
                .sheet("模板")
                .doWrite(getDatas());

        

        设置列的样式:

  AbstractVerticalCellStyleStrategy 是一个抽象类,必须通过继承并重写方法来实现自定义的列样式。主要是重写下面两个方法:

       headCellStyle(Head head)          ----设置标题栏的列样式
       contentCellStyle(Head head)     ----设置表格内容的列样式

       具体实现代码:
       先实现重写AbstractVerticalCellStyleStrategy

public class CustomVerticalCellStyleStrategy extends AbstractVerticalCellStyleStrategy {


    // 重写定义表头样式的方法
    @Override
    protected WriteCellStyle headCellStyle(Head head) {

        WriteCellStyle writeCellStyle = new WriteCellStyle();

        if(head.getColumnIndex() == 0) {
            //设置行头的第一列  单元格水平居中
            writeCellStyle.setHorizontalAlignment(HorizontalAlignment.CENTER);
        }else if(head.getColumnIndex() == 1){
            //设置行头的第二列  单元格水平居左
            writeCellStyle.setHorizontalAlignment(HorizontalAlignment.LEFT);
        }else if(head.getColumnIndex() == 2){
            //设置行头的第三列  单元格水平居右
            writeCellStyle.setHorizontalAlignment(HorizontalAlignment.RIGHT);
        }else if(head.getColumnIndex() == 3){
            //设置行头大于第三列  单元格水平居右
            writeCellStyle.setHorizontalAlignment(HorizontalAlignment.CENTER);
        }
        return writeCellStyle;
    }

    // 重写定义内容部分样式的方法
    @Override
    protected WriteCellStyle contentCellStyle(Head head) {
        WriteCellStyle writeCellStyle = new WriteCellStyle();
        writeCellStyle.setFillPatternType(FillPatternType.SOLID_FOREGROUND);
        writeCellStyle.setFillBackgroundColor(IndexedColors.GREEN.getIndex());
        writeCellStyle.setHorizontalAlignment(HorizontalAlignment.CENTER);
        return writeCellStyle;
    }

}

        再将自定义样式策略注册到Excel中:

        //数据表格的自定义列样式
        CustomVerticalCellStyleStrategy customVerticalCellStyleStrategy
                = new CustomVerticalCellStyleStrategy();
        // 这里 需要指定写用哪个class去写,然后写到第一个sheet,名字为模板 然后文件流会自动关闭
        EasyExcel.write(response.getOutputStream(), ExcelDemo.class)
                .registerWriteHandler(customVerticalCellStyleStrategy)
                .sheet("模板")
                .doWrite(getDatas());

        上面是简单的示例,具体每一列的样式,可以根据自己的需要定义。

        12、自定义单元格

        通过实现 CellWriteHandler 接口,可以完全自定义单元格的写入行为和样式。

        CellWriteHandler 接口,继承自 WriteHandler,用于在 Excel 写入过程中处理单元格的不同生命周期阶段。每个方法都处理特定的操作,通常用于 Excel 文件的写入时,针对单元格的创建、数据转换、处理和销毁等环节进行自定义处理。

        这个接口的作用是提供钩子方法(默认方法)来让用户在写入数据到 Excel 时,能够在每个关键阶段插入自己的逻辑。以下是对每个方法的详细解释:
 

  • 1. beforeCellCreate(CellWriteHandlerContext context)
    • 作用:在单元格创建之前调用,用于在写入单元格之前进行一些准备工作。
    • 参数:方法内部通过 context 参数提取了多个信息,包括 WriteSheetHolder(写入的工作表)、WriteTableHolder(写入的表格)、Row(当前行)、Head(表头信息)、columnIndex(列索引)、relativeRowIndex(相对行索引)、isHead(是否是表头)等。
    • 默认实现:这个方法默认调用了另一个重载版本 beforeCellCreate(WriteSheetHolder, WriteTableHolder, Row, Head, Integer, Integer, Boolean),该重载方法的默认实现为空,表示没有默认操作,用户可以在实现接口时进行自定义。
  • 2. beforeCellCreate(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Row row, Head head, Integer columnIndex, Integer relativeRowIndex, Boolean isHead)
    • 作用:实际执行创建单元格之前的处理逻辑的方法。可以根据需要对 Excel 单元格的创建过程进行自定义操作,如设置格式、填充数据等。
    • 参数:接受了各个具体的参数,允许用户在不同的上下文中访问这些信息。
  • 3. afterCellCreate(CellWriteHandlerContext context)
    • 作用:在单元格创建之后调用。这个方法在单元格已经创建后执行,可以用来处理与单元格创建相关的操作,例如设置样式或处理其他后续步骤。
    • 参数:同样是通过 context 提供相关的信息,包括工作表、表格、当前单元格、表头信息、相对行索引等。
    • 默认实现:默认调用了另一个重载版本 afterCellCreate(WriteSheetHolder, WriteTableHolder, Cell, Head, Integer, Boolean),该重载方法默认没有实现任何逻辑,允许用户进行自定义。
  • 4. afterCellCreate(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Cell cell, Head head, Integer relativeRowIndex, Boolean isHead)
    • 作用:执行单元格创建后的一些自定义处理逻辑。与 beforeCellCreate 方法类似,但在单元格已创建之后进行操作。
    • 参数:该方法接受详细的参数,允许用户访问到当前工作表、单元格、表头等信息。
  • 5. afterCellDataConverted(CellWriteHandlerContext context)
    • 作用:在单元格的数据被转换(例如格式化、数据类型转换)之后调用。这个方法允许用户在数据转换后进行一些额外的处理,比如修改转换后的数据或进一步调整单元格的内容。
    • 参数:context.getCellDataList() 提供了当前单元格的数据,方法会根据需要选择第一个数据进行处理(假设该列表不为空)。其他参数和前面的钩子方法类似。
  • 6. afterCellDataConverted(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, WriteCellData<?> cellData, Cell cell, Head head, Integer relativeRowIndex, Boolean isHead)
    • 作用:执行数据转换后的处理逻辑。用户可以在此时进一步调整数据内容、修改样式或执行其他操作。
    • 参数:cellData 是转换后的数据,允许用户访问和操作。
  • 7. afterCellDispose(CellWriteHandlerContext context)
    • 作用:在单元格被销毁之前调用。这个方法是在单元格生命周期的最后阶段,用于清理或执行一些最终的操作。通常会在单元格数据处理和格式化完成后进行一些额外的操作,如记录日志或执行清理工作。
    • 参数:context.getCellDataList() 提供了当前单元格的所有数据列表,用户可以在此进行处理。
  • 8. afterCellDispose(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, List<WriteCellData<?>> cellDataList, Cell cell, Head head, Integer relativeRowIndex, Boolean isHead)
    • 作用:处理单元格销毁之后的操作。此方法接收了与前面方法相同的详细参数,允许用户在最后一步进行操作。
    • 参数:cellDataList 包含了所有需要处理的单元格数据,cell 是当前单元格,head 是表头信息,isHead 是一个布尔值,指示是否为表头行        

        总结:
        这个接口主要用于对 Excel 写入过程中的每个关键步骤提供自定义的处理机制。通过实现CellWriteHandler 接口,用户可以在:

  • 单元格创建之前 (beforeCellCreate)
  • 单元格创建之后 (afterCellCreate)
  • 数据转换之后 (afterCellDataConverted)
  • 单元格销毁之前 (afterCellDispose)

        等多个阶段进行干预和扩展,实现自定义的单元格处理逻辑。例如,可以用于设置单元格样式、数据格式化、数据验证、日志记录等。

        实际上上述的方法都可以进行单元格的样式设置,但是每个方法代表不同的时期,不同的时期能获取到的信息不同,所以可以选择合适的方法来进行自定义格式,但是其实如果不是有复杂的需求,其实不推荐使用该方法进行自定义单元格样式。

        简单示例:

        EasyExcel.write(response.getOutputStream(), ExcelDemo.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(getDatas());

        使用poi的样式设置,用的很少也不推荐使用,只做简单使用:

// 方法3: 使用poi的样式完全自己写 不推荐
        // @since 3.0.0-beta2
        // 坑1:style里面有dataformat 用来格式化数据的 所以自己设置可能导致格式化注解不生效
        // 坑2:不要一直去创建style 记得缓存起来 最多创建6W个就挂了
        fileName = TestFileUtil.getPath() + "handlerStyleWrite" + System.currentTimeMillis() + ".xlsx";
        EasyExcel.write(fileName, 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());

        13、合并单元格

        可以配合注解:
        @ContentLoopMerge
        @OnceAbsoluteMerge
        来进行单元格的合并。

// 将第6-7行的2-3列合并成一个单元格
 @OnceAbsoluteMerge(firstRowIndex = 5, lastRowIndex = 6, firstColumnIndex = 1, lastColumnIndex = 2)
public class ExcelDemo {

    // 这一列 每隔2行 合并单元格
    @ContentLoopMerge(eachRow = 2)
    @ExcelProperty({"姓名"})
    private String name;

    @ExcelProperty({"年龄"})
    private int age;

    @ExcelProperty({"薪水"})
    private BigDecimal salary;

    @ExcelProperty({"地址"})
    private String address;

    @ExcelProperty({"日期"})
    private LocalDateTime dateTime;

}

        14、使用table去写入

        我的理解就是在一个excel中重复的导入多个包含表头的表格,比如下方所示:          
      

       // 方法1 这里直接写多个table的案例了,如果只有一个 也可以直一行代码搞定,参照其他案
        // 这里 需要指定写用哪个class去写
        try (ExcelWriter excelWriter = EasyExcel.write(response.getOutputStream(), ExcelDemo.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(getDatas(), writeSheet, writeTable0);
            // 第二次写如也会创建头,然后在第一次的后面写入数据
            excelWriter.write(getDatas(), writeSheet, writeTable1);
        }

        感觉不是特别常用,参考上面代码示例即可。

        

        15、动态头,实时生成头写入

        就是我们的表头可以根据自定义生成的列表自动生成,而不是写死,需要配合head()方法使用:

    @Test
    public void dynamicHeadWrite() {
        String fileName = TestFileUtil.getPath() + "dynamicHeadWrite" + System.currentTimeMillis() + ".xlsx";
        EasyExcel.write(fileName)
            // 这里放入动态头
            .head(head()).sheet("模板")
            // 当然这里数据也可以用 List<List<String>> 去传入
            .doWrite(data());
    }

    private List<List<String>> head() {
        List<List<String>> list = new ArrayList<List<String>>();
        List<String> head0 = new ArrayList<String>();
        head0.add("字符串" + System.currentTimeMillis());
        List<String> head1 = new ArrayList<String>();
        head1.add("数字" + System.currentTimeMillis());
        List<String> head2 = new ArrayList<String>();
        head2.add("日期" + System.currentTimeMillis());
        list.add(head0);
        list.add(head1);
        list.add(head2);
        return list;
    }

        这个列头是根据,实体类字段的顺序来匹配的,所以要注意列头设置的列表顺序。

        16、自动列宽(不太精确)

                确实不太精确,需要使用内置的 LongestMatchColumnWidthStyleStrategy 类:

        EasyExcel.write(response.getOutputStream(), ExcelDemo.class)
                .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())
                .sheet("excel模板")
                .doWrite(getDatas());

        17、自定义拦截器

                当我们需要对单元格进行特定操作时,可以使用拦截器。可以实现CellWriteHandler接口或者 SheetWriteHandler 接口。

                CellWriteHandler:处理具体 Cell(单元格) 的写入过程,用于在单元格级别执行操作。

                SheetWriteHandler:处理整个 Sheet(工作表) 的写入过程,适合需要对整个 Sheet 级别进行操作的场景

                CellWriteHandler在上面自定义样式中 已经简单说过了,再简单说一下SheetWriteHandler:
                主要是两个方法:

  • beforeSheetCreate:在工作表创建之前执行。
  • afterSheetCreate:在工作表创建之后执行。

                每个方法都有两个形式:

  • SheetWriteHandlerContext 参数的默认方法,便于获取上下文信息
  • 直接使用核心参数(WriteWorkbookHolderWriteSheetHolder)的方法,提供直接操作能力。

        在工作表创建之前调用:

  • default void beforeSheetCreate(SheetWriteHandlerContext context) 
  • default void beforeSheetCreate(WriteWorkbookHolder writeWorkbookHolder,WriteSheetHolder writeSheetHolder)

        在工作表创建之后调用:

  • default void afterSheetCreate(SheetWriteHandlerContext context) 
  • default void afterSheetCreate(WriteWorkbookHolder writeWorkbookHolder,WriteSheetHolder writeSheetHolder) 

        举例,例如:对第一列第一行和第二行的数据新增下拉框,显示 测试1 测试2

         定义整个sheet拦截器:

@Slf4j
public class ExcelSheetWriteHandler 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);
    }
}

        定义Cell 单元格拦截器:

@Slf4j
public class ExcelCellWriteHandler implements CellWriteHandler {

    @Override
    public void afterCellDispose(CellWriteHandlerContext context) {
        Cell cell = context.getCell();
        // 这里可以对cell进行任何操作
        log.info("第{}行,第{}列写入完成。", cell.getRowIndex(), cell.getColumnIndex());
        if (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);
        }
    }

}

        写入拦截器:

        EasyExcel.write(response.getOutputStream(), ExcelDemo.class)
                .registerWriteHandler(new ExcelSheetWriteHandler())
                .registerWriteHandler(new ExcelCellWriteHandler()).sheet("模板").doWrite(getDatas());

        上面是简单示例,具体使用示例可以根据需求而定。

        18、插入批注

        一般也很少用,例如可以借助 实现 RowWriteHandler拦截器,给第一行行头加上批注:

        创建拦截器:

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

}

        注册拦截器:

        // 这里要注意inMemory 要设置为true,才能支持批注。目前没有好的办法解决 不在内存处理批注。这个需要自己选择。
        EasyExcel.write(response.getOutputStream(), ExcelDemo.class).inMemory(Boolean.TRUE).registerWriteHandler(new CommentWriteHandler())
                .sheet("模板").doWrite(getDatas());

        

        18、不创建对象的写

        可以直接传入List<List<Object>>列表,来传入数据,不需要生成具体生成类,但是不推荐这么做:

@Test
    public void noModelWrite() {
        // 写法1
        String fileName = TestFileUtil.getPath() + "noModelWrite" + System.currentTimeMillis() + ".xlsx";
        // 这里 需要指定写用哪个class去写,然后写到第一个sheet,名字为模板 然后文件流会自动关闭
        EasyExcel.write(fileName).head(head()).sheet("模板").doWrite(dataList());
    }

    private List<List<String>> head() {
        List<List<String>> list = ListUtils.newArrayList();
        List<String> head0 = ListUtils.newArrayList();
        head0.add("字符串" + System.currentTimeMillis());
        List<String> head1 = ListUtils.newArrayList();
        head1.add("数字" + System.currentTimeMillis());
        List<String> head2 = ListUtils.newArrayList();
        head2.add("日期" + System.currentTimeMillis());
        list.add(head0);
        list.add(head1);
        list.add(head2);
        return list;
    }

    private List<List<Object>> dataList() {
        List<List<Object>> list = ListUtils.newArrayList();
        for (int i = 0; i < 10; i++) {
            List<Object> data = ListUtils.newArrayList();
            data.add("字符串" + i);
            data.add(0.56);
            data.add(new Date());
            list.add(data);
        }
        return list;
    }

        总结:

        最要的其实就是 registerWriteHandler() ,这个方法可以在生成excel时,添加拦截器针对每一行、每一个单元格进行操作。这个方法只要是实现了  WriteHandler 接口的实现类都可以传入。excel默认实现了很多接口,有针对每一行操作的,每一个单元格的、每一列的。根据具体需要自行应用即可。

六、填充Excel

       
        1、最简单的填充

        填充模板:
        

        填充的对象:

@Data
@AllArgsConstructor
@NoArgsConstructor
public class ExcelDemo {

    private String name;

    private int age;

    private BigDecimal salary;

    private String address;

    private LocalDateTime dateTime;

}

        填充效果:

     

        代码举例:

        // 模板注意 用{} 来表示你要用的变量 如果本来就有"{","}" 特殊字符 用"\{","\}"代替
        String templateFileName =
                "C:\\Users\\Administrator\\Desktop\\ces\\" + "excel.xlsx";

        // 需要调用doFill方法,记住如果不是填充列表,数据只能传一个对象,传列表会导致不生效
        EasyExcel.write(response.getOutputStream(), ExcelDemo.class)
                .withTemplate(templateFileName)
                .sheet()
                .doFill(getDatas().get(0));

        2、填充列表

        如果要填充列表,填充模板需要在字段前加个 . ,例如下面:

      

        填充对象与上面一样。
        填充效果:

        代码举例,与填充一个对象不同的是,需要在doFill 中传入数据列表:

        // 填充list 的时候还要注意 模板中{.} 多了个点 表示list
        String templateFileName =
                "C:\\Users\\Administrator\\Desktop\\ces\\" + "excel.xlsx";

        // 方案1 一下子全部放到内存里面 并填充
        // 这里 会填充到第一个sheet, 然后文件流会自动关闭
        EasyExcel.write(response.getOutputStream(), ExcelDemo.class)
                .withTemplate(templateFileName)
                .sheet()
                .doFill(getDatas());


        // 方案2 分多次 填充 会使用文件缓存(省内存)
        try (ExcelWriter excelWriter = EasyExcel.write(response.getOutputStream()).withTemplate(templateFileName).build()) {
            WriteSheet writeSheet = EasyExcel.writerSheet().build();
            excelWriter.fill(getDatas(), writeSheet);
            excelWriter.fill(getDatas(), writeSheet);
        }

         

        3、复杂的填充

        例如既有单个又有列表的数据填充:      
       

        填充效果:
       
        代码举例:需要借助map的方式填充复杂数据:

        // 填充list 的时候还要注意 模板中{.} 多了个点 表示list
        String templateFileName =
                "C:\\Users\\Administrator\\Desktop\\ces\\" + "excel1.xlsx";

        // 方案1
        try (ExcelWriter excelWriter = EasyExcel.write(response.getOutputStream()).withTemplate(templateFileName).build()) {
            WriteSheet writeSheet = EasyExcel.writerSheet().build();
            // 这里注意 入参用了forceNewRow 代表在写入list的时候不管list下面有没有空行 都会创建一行,然后下面的数据往后移动。默认 是false,会直接使用下一行,如果没有则创建。
            // forceNewRow 如果设置了true,有个缺点 就是他会把所有的数据都放到内存了,所以慎用
            // 简单的说 如果你的模板有list,且list不是最后一行,下面还有数据需要填充 就必须设置 forceNewRow=true 但是这个就会把所有数据放到内存 会很耗内存
            // 如果数据量大 list不是最后一行 参照下一个
            FillConfig fillConfig = FillConfig.builder().forceNewRow(Boolean.TRUE).build();
            excelWriter.fill(getDatas(), fillConfig, writeSheet);
            excelWriter.fill(getDatas(), fillConfig, writeSheet);
            Map<String, Object> map = MapUtils.newHashMap();
            map.put("dateTime", "2019年10月9日13:28:28");
            map.put("total", 1000);
            excelWriter.fill(map, writeSheet);
        }

         

        4、数据量大的复杂填充

        模板,把列表后面的先不写在模板上,最后用代码生成合计: 
       

        代码示例:

        // 填充list 的时候还要注意 模板中{.} 多了个点 表示list
        String templateFileName =
                "C:\\Users\\Administrator\\Desktop\\ces\\" + "excel1.xlsx";

        // 方案1
        try (ExcelWriter excelWriter = EasyExcel.write(response.getOutputStream()).withTemplate(templateFileName).build()) {
            WriteSheet writeSheet = EasyExcel.writerSheet().build();
            // 直接写入数据
            excelWriter.fill(getDatas(), writeSheet);
            excelWriter.fill(getDatas(), writeSheet);

            // 写入list之前的数据
            Map<String, Object> map = new HashMap<String, Object>();
            map.put("date", "2019年10月9日13:28:28");
            excelWriter.fill(map, writeSheet);

            // list 后面还有个统计 想办法手动写入
            // 这里偷懒直接用list 也可以用对象
            List<List<String>> totalListList = ListUtils.newArrayList();
            List<String> totalList = ListUtils.newArrayList();
            totalListList.add(totalList);
            totalList.add(null);
            totalList.add(null);
            totalList.add(null);
            // 第四列
            totalList.add("统计:1000");
            // 这里是write 别和fill 搞错了
            excelWriter.write(totalListList, writeSheet);
            // 总体上写法比较复杂 但是也没有想到好的版本 异步的去写入excel 不支持行的删除和移动,也不支持备注这种的写入,所以也排除了可以
            // 新建一个 然后一点点复制过来的方案,最后导致list需要新增行的时候,后面的列的数据没法后移,后续会继续想想解决方案
        }

        实际上意思就是说,如果使用异步的方式插入列表,那么很难控制合计始终在最后一行,所以最好的方式就是,当所有列表的数据都导出完成,就用代码生成最后一行合计,不写在模板中。

        5、横向的填充

        模板:

      

        最终效果:

         代码示例:

        // 方案1
        try (ExcelWriter excelWriter = EasyExcel.write(response.getOutputStream()).withTemplate(templateFileName).build()) {
            WriteSheet writeSheet = EasyExcel.writerSheet().build();
            //设置填充方向为水平方向。即填充的数据将按行(水平)填充到 Excel 中。
            FillConfig fillConfig = FillConfig.builder().direction(WriteDirectionEnum.HORIZONTAL).build();
            excelWriter.fill(getDatas(), fillConfig, writeSheet);
            excelWriter.fill(getDatas(), fillConfig, writeSheet);

            Map<String, Object> map = new HashMap<>();
            map.put("date", "2019年10月9日13:28:28");
            excelWriter.fill(map, writeSheet);
        }

        6、多列表组合填充

        模板:

        最终效果:

        代码示例:

        // 方案1
        try (ExcelWriter excelWriter = EasyExcel.write(response.getOutputStream()).withTemplate(templateFileName).build()) {
            WriteSheet writeSheet = EasyExcel.writerSheet().build();
            FillConfig fillConfig = FillConfig.builder().direction(WriteDirectionEnum.HORIZONTAL).build();
            // 如果有多个list 模板上必须有{前缀.} 这里的前缀就是 data1,然后多个list必须用 FillWrapper包裹
            excelWriter.fill(new FillWrapper("data1", getDatas()), fillConfig, writeSheet);
            excelWriter.fill(new FillWrapper("data1", getDatas()), fillConfig, writeSheet);
            excelWriter.fill(new FillWrapper("data2", getDatas()), writeSheet);
            excelWriter.fill(new FillWrapper("data2", getDatas()), writeSheet);
            excelWriter.fill(new FillWrapper("data3", getDatas()), writeSheet);
            excelWriter.fill(new FillWrapper("data3", getDatas()), writeSheet);

            Map<String, Object> map = new HashMap<String, Object>();
            //map.put("date", "2019年10月9日13:28:28");
            map.put("date", new Date());

            excelWriter.fill(map, writeSheet);
        }


网站公告

今日签到

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