EasyExcel 的使用与应用函数式编程

概述

自上次项目完成之后,又要写下一个项目,其中需要用到「导出数据为Excel」的需求,因此学习了 EasyExcel 的技术栈。之后在对应的代码编写之中运用到了之前所学的「函数式编程」。

使用

pom

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

应用

实际上如同其名,使用方法很是简单。比如我们要生成一个账单的表格。那么你可以使用「Navicat」打开你对应的表,其实它显示的是不是就是一张表格~

所以,其实生成表格只是将对应的数据转换一下而已.. 对吧! 只是要声明对应参数的列名是什么而已!

因此我们需要创建一个对应的实体类,如下。我们使用了「 @ExcelProperty」注解来声明这个参数的列名为「单元名称」。你会发现,我们还在 id 参数之上加入了「 @ExcelIgnore」注解,很显然我们不需要在表格中出现 id 这一列,所以加入此注解的参数不会渲染出来~

@ExcelIgnore
private String id;
/**
 * 单元名称
 */
@ExcelProperty("单元名称")
private String unitName;

以上编写实际上不够严谨,我们最好还是要声明参数对应的索引~ 这样我们之后就可以根据索引去判断是那一列了!

/**
 * 是否出售
 */
@ExcelProperty(value = "是否出售",index = 8)
private String isSale;

当然,我们还可以去声明这个列的宽度,只需要在参数之上加入「@ColumnWidth(20)」注解。也可以在实体类上面加入,这样代表每个参数多占用这个长度。这个「20」对应的是什么单位呢?这个不得而知了,文档似乎也没有说明,但「20」实际生成的列宽是「17.93字符」。不过我们不需要在意这个,只要整体协调即可。

而接下来如何使用呢,就更简单了。我们只需要声明具体的路径和文件名称,如何设置对应的实体类与数据即可。下列代码可以看出,我们创建了一个表名为「sss」的表格,并且是从「sysRoomService」的「list()」方法获取的数据,它是以「SysRoom.class」实体类为主体。

    String fileName = "/Volumes/data/project/estate_management/simpleWrite" + System.currentTimeMillis() + ".xlsx";
    EasyExcel.write(fileName, SysRoom.class)
            .sheet("sss")
            .doWrite(sysRoomService.list());

这样我们就直接生成了一个表格。

样式

通过上述我们已经能够通过数据去生成一张表格了,但是这并不够,我们有时候需要对其样式进行修改。

样式一共有两种方式修改,一种是通过渲染的拦截器去设置,一种是使用注解形式去设置。

拦截器

下放为官方文档中提供的代码,其实很好理解,我们分别设置好对应的样式,然后使用「registerWriteHandler」方法注册其中。你需要理解其中的重点——它是一个拦截器。也就是说,我们这些设置其实是告诉「HorizontalCellStyleStrategy」这个类该如何处理我们的数据样式,之后它注册进我们的任务之后会对应这些设置做出拦截修改。


    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(fileName, DemoData.class).registerWriteHandler(horizontalCellStyleStrategy).sheet("模板")
        .doWrite(data());

注解形式

第二种方式就是通过注解去设置样式,如下面代码之中,我们通过「@ContentFontStyle」注解分别设置了字体颜色和字体大小。值得注意的有两点。其一,我们的颜色值来自于「IndexedColors」枚举类中记录的颜色数值,其次如果我们不设置字体大小,它的默认大小是有问题的!

 /**
 * 是否出售
 */
@ContentFontStyle(color = 10,fontHeightInPoints = 11)
@ExcelProperty(value = "是否出售",index = 8)
private String isSale;

而在设置字体样式之外还有:

  • @ContentStyle 设置内容样式
  • @HeadFontStyle 设置头部字体样式
  • @HeadStyle 设置头部样式

其中可以设置:背景颜色、字体大小、字体颜色、对齐方式等..。

需求

学完以上内容之后,我们的需求是在「是否出售」一列之中,如果内容是「已有住户」那么字体颜色就改为红色,反之为蓝色。如何实现呢?
很显然使用拦截器即可。

我们创建了一个名为「CustomCellWriteHandler」的类,并将其实现「CellWriteHandler」接口,我们在「afterCellDispose」方法中编写了以下代码。逻辑很简单只有当前不为头部以及当前索引为「8」的时候才去执行下列代码。

@Override
public void afterCellDispose(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, List<CellData> cellDataList, Cell cell, Head head, Integer relativeRowIndex, Boolean isHead) {
   if(!isHead && cell.getColumnIndex() == 8){
       Workbook workbook = writeSheetHolder.getSheet().getWorkbook();
       CellStyle cellStyle = workbook.createCellStyle();
       Font font = workbook.createFont();
       System.out.println(cell.getStringCellValue());
       short color = cell.getStringCellValue().equals("已有住户")
               ? IndexedColors.RED.getIndex() : IndexedColors.BLUE.getIndex();
       font.setColor(color);
       cellStyle.setFont(font);
       cell.setCellStyle(cellStyle);
   }
}

然后我们只需要注册这个拦截器即可。

    EasyExcel.write(fileName, SysRoom.class)
            .sheet("sss")
            .registerWriteHandler(new CustomCellWriteHandler())
            .doWrite(sysRoomService.list());

但是写到这里,你应该会去思考,如果我们在生成其他表的时候能不能去复用一下这个拦截器呢?我们总不能有一个需求就重新实现一个拦截器吧?

因此我们想到了之前学习到的「函数式编程」。

函数式编程

我们创建了一个名为「CustomCellWrite」的接口。为了声明它是一个「函数式接口」我们在之上加入了「 @FunctionalInterface」注解,并且创建了一个「operation」方法。

@FunctionalInterface
public interface CustomCellWrite {
    void operation(WriteSheetHolder writeSheetHolder, Cell cell, Boolean isHead);
}

其后我们在我们刚才创建的「CustomCellWriteHandler」类加入「CustomCellWrite」类型的参数,并创建对应的构造方法。

private CustomCellWrite customCellWrite;

public CustomCellWriteHandler(CustomCellWrite customCellWrite) {
    this.customCellWrite = customCellWrite;
}

而在之后我们就可以将「afterCellDispose」方法直接改成下面这样。也就是说,我们创建这个拦截器类之时为其传递一个「CustomCellWrite」的实现类。很明显,看到这里应该明白了吧~ 我们在参数之中直接使用 lambda表达式即可。

@Override
public void afterCellDispose(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, List<CellData> cellDataList, Cell cell, Head head, Integer relativeRowIndex, Boolean isHead) {
    if (customCellWrite != null) {
        customCellWrite.operation(writeSheetHolder, cell, isHead);
    }
}

其后,我们的代码就可以改成下面这样。

    EasyExcel.write(fileName, SysRoom.class)
            .sheet("sss")
            .registerWriteHandler(new CustomCellWriteHandler((writeSheetHolder, cell, isHead) -> {
                if (!isHead && cell.getColumnIndex() == 8) {
                    Workbook workbook = writeSheetHolder.getSheet().getWorkbook();
                    CellStyle cellStyle = workbook.createCellStyle();
                    Font font = workbook.createFont();
                    System.out.println(cell.getStringCellValue());
                    short color = cell.getStringCellValue().equals("已有住户")
                            ? IndexedColors.RED.getIndex() : IndexedColors.BLUE.getIndex();
                    font.setColor(color);
                    cellStyle.setFont(font);
                    cell.setCellStyle(cellStyle);
                }
            }))
            .doWrite(sysRoomService.list());

下载接口(传递给前端)

实际上 EasyExcel 已经封装好了对应的方法,所以我们只需要把对应的数据传递一下就可以了。

/**
 * 创建对应数据的表格并传递给前端
 *
 * @param response        响应
 * @param fileName        文件名称
 * @param clazz           目标数据实体类
 * @param data            数据集合
 * @param customCellWrite 自定义的单元格处理类
 */
private void download(HttpServletResponse response, String fileName, Class clazz, List data, CustomCellWrite customCellWrite) {
    response.setContentType("application/vnd.ms-excel");
    response.setCharacterEncoding("utf-8");
    try {
        fileName = URLEncoder.encode(fileName, "UTF-8").replaceAll("\\+", "%20");
        response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");

        ExcelWriterSheetBuilder excelWriterSheetBuilder = EasyExcel.write(response.getOutputStream(), clazz)
                .sheet("sheet1");
        // 自定义单元格处理器如果不为空就加入拦截器
        if (customCellWrite != null) {
            excelWriterSheetBuilder = excelWriterSheetBuilder.
                    registerWriteHandler(new CustomCellWriteHandler(customCellWrite));
        }
        excelWriterSheetBuilder.doWrite(data);
    } catch (IOException e) {
        e.printStackTrace();
    }

}