Skip to content

1 导出Excel

guanquan.wang edited this page Jan 6, 2025 · 25 revisions

EEC目前支持SimpleSheet, ListSheetListMapSheetTemplateSheetStatementSheetResultSetSheetCSVSheetEmptySheet几种内置的Worksheet,如果不能满足需求你也可以继承已有的Worksheet来扩展,最常见的就是对于大数据量写入时的分片处理,这个在后面会讲到,目前还是从最简单的ListSheet出发。

将数据导出到excel

数据导出应该是开发过程中比较常见的功能,可就是这种简单功能如果使用Apache POI来开发可不是一件轻松的活,幸好EEC已经为我们做了大量的封装,使我们可以做到开箱即用,下面代码展示如何开发简单的对象数组导出功能

/**
 * 导出学生信息
 */
public void exportStudent(List<Student> students) throws IOException {
    new Workbook("二年级学生表") // 新增一个Workbook并指定名称,也就是Excel文件名
        .addSheet(new ListSheet<>(students)) // 添加一个Sheet页,并指定导出数据
        .writeTo(Paths.get("F:/excel")); // 指定导出位置
}

以上writeTo方法指定一个输出位置,不需要指定具体文件名称,名称在实例化Workbook时指定,如果未指定则默认使用“新建文件”做为文件名,如果指定到具体文件而不是文件夹则替换原有文件,没有权限则会抛异常。另外writeTo终止符,调用该方法将触发写操作,在其后设置的所有属性将不生效。

如果是做web开发则可以将writeTo直接输出到Response的Outputstream中,如下代码

/**
 * 直接将excel输出到流
 */
@GetMapping("/download")
public void download(HttpServletResponse response) throws IOException {
    String fileName = java.net.URLEncoder.encode("新建文件.xlsx", "UTF-8");
    response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + fileName + "\"; filename*=utf-8''" + fileName);
    // 查询数据
    List<Student> students = studentService.list();

    new Workbook().addSheet(new ListSheet<>(students)).writeTo(response.getOutputStream()); // <- 直接写到Response流
}

添加多个Worksheet

EEC是通过Workbook#addSheet方法添加Worksheet,添加的时候你可以指定Sheet的名称,如果不指定则默认使用Sheet {N}命名。对于导出多个Sheet页只需要多调用几次addSheet方法即可,非常方便。 另外,添加顺序决定导出时各Sheet顺序,如果想调整此顺序可以调用Workbook#insertSheet方法插入到指定下标(从0开始),与普通的Array操作一样。

下面代码演示生成多个Worksheet

new Workbook("multi-sheets")
    .addSheet(new ListSheet<>("帐单表").setData(checksTestData()))
    .addSheet(new ListSheet<>("客户表").setData(customersTestData()))
    .addSheet(new ListSheet<>("用户客户关系表").setData(c2CSTestData()))
    .writeTo(Paths.get("F:/excel"));

导出文件如下:

multi-sheet.png

隐藏Sheet

出于某些安全考虑需要隐藏某个或多个Sheet页该如何处理呢?答案是只需要在对应的Sheet上调用#hidden()方法。调用该方法后数据依然会正常写出,只是该页被隐藏。

下面代码演示隐藏某个Worksheet

new Workbook("multiSheet")
    .addSheet(new ListSheet<>("帐单表").setData(checksTestData()))
    .addSheet(new ListSheet<>("客户表").setData(customersTestData()).hidden()) // <- 隐藏该Sheet
    .addSheet(new ListSheet<>("用户客户关系表").setData(c2CSTestData()))
    .writeTo(Paths.get("F:/excel"));

导出文件如下,点击右键选择“取消隐藏”就可以还原了。

hidden-sheet.png

强制导出ForceExport

为了数据安全EEC默认只会导出标记有@ExcelColumn的属性,但某些情况不方便在实体中加入注解此时就可以调用forceExport方法全字段导出(标记了@IgnroeExport注解除外)。

Force Export会将实体中所有字段导出,这是非常危险的做法不建议使用。如果其它开发者新增了一些属性且未意识到forceExport则会出现数据泄漏风险,为防止数据泄露推荐手动指定Column,这样的话即使对象被添加了敏感字段也不会被自动导出,降低不可预期风险发生。

示例代码如下

// 手动指定Column
new Workbook().addSheet(new ListSheet<>(
        new Column("学号", "id") // "学号"为别名即Excel呈现的表头,"id"为Student实体中的字段
        , new Column("姓名", "name")
        , new Column("成绩", "score")
    ).setData(Student.randomTestData()))
    .writeTo(Paths.get("F:/excel"));

关于自动分页

单个worksheet页有行数上限,xls上限为65,536,xlsx上限为1,048,576,如果数据超过该如何处理呢,需要手动进行截取么,还是抛异常?

EEC是为大数据量而生,所以自然考虑到了这种情况,当数据量超过单sheet上限时会自动进行分页处理,无须用户额外处理,而大多数同类工具均是直接抛异常。

以下示例代码展示如何导出200w行随机测试数据

new Workbook()
    // 通过onProgress观察导出进度
    .onProgress((sheet, rows) -> System.out.println(sheet.getName() + " 已写入: " + rows))
    // i表示已导出数据行数
    // lastOne表示最后一个导出数据,示例表示最后一个Item
    .addSheet(new ListSheet<Item>().setData((i, lastOne) -> i < 2000000 ? Item.randomData() : null))
    .writeTo(Paths.get("F:/excel/200w.xlsx"));

大数据量导出请参考 大数据量导出

多行表头

多行表头请参考 如何设置多行表头

忽略表头

EEC提供Sheet#ignoreHeader方法来忽略表头输出,当然你在表头上设置的任何信息依然有效,只在输出的时候跳过表头,注意这里是忽略表头不是隐藏

new Workbook("Ignore header")
    .addSheet(new ListSheet<>(randomTestData()).ignoreHeader()) // <- 使用#ignoreHeader忽略表头输出
    .writeTo(Paths.get("F:/excel"));

简单数据类型导出

有时候仅仅想导出最简单的数据类型,比如Integer,String,如果定义实体就显得过度设计,此时可以像下面示例一样导入简单类型

List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 0);
new Workbook("Integer array")
    .addSheet(new SimpleSheet<>(list))
    .writeTo(Paths.get("F:/excel"));

未知的数据类型

EEC内置处理如下类型,并按照文本居左,数字居右,日期/bool/char居中输出

String CharSequence int Integer short Short
byte Byte long Long float Float
double Double BigDecimal boolean Boolean char
Character java.util.Date java.sql.Date java.sql.Timestamp java.sql.Time java.time.LocalDate
java.time.LocalDateTime java.time.LocalTime

其余类型均默认调用toString方法输入,如果需要特殊处理则可以使用自定义ICellValueAndStyle类并覆写unknownType方法,示例如下

public class MyXMLCellValueAndStyle extends XMLCellValueAndStyle {

    @Override
    public void unknownType(Row row, Cell cell, Object e, Column hc, Class<?> clazz) {
        // 如果认别到自定义枚举则输出枚举desc字段
        if (clazz == PlatformEnum.class) {
            cell.setSv(((PlatformEnum) e).getDesc());
        }
        // 其它情况默认处理
        else {
            super.unknownType(row, cell, e, hc, clazz);
        }
    }
}

// 添加Worksheet时指定自定认MyXMLCellValueAndStyle即可
new Workbook()
    .addSheet(new ListSheet<>(data).setCellValueAndStyle(new MyXMLCellValueAndStyle()))
    .writeTo(Paths.get("F:/excel"));

以上代码展示了自定义枚举类型的特殊处理,对于没有权限修改(如对象放在公共的jar包中或者多个团队共同使用不能修改)的情况下自定义ICellValueAndStyle就显得特别重要了,当然如果有权限的话你也可以直接在PlatformEnum枚举内添加toString并返回desc属性,但并不建议此类做法

unknownType的优先级最低,所以无法在unknownType方法中处理String等内置类型,如果需要则可以覆写reset方法,像下面示例一样

public class MyXMLCellValueAndStyle extends XMLCellValueAndStyle {
    @Override
    public void reset(Row row, Cell cell, Object e, Column hc) {
        // 调用预处理方法
        preCellValue(row, cell, e, hc, hc.getClazz(), hc.processor != null);
        if (hc.processor == null) {
            cell.xf = getStyleIndex(row, hc, e);
        }
    }

    void preCellValue(Row row, Cell cell, Object e, Column hc, Class<?> clazz, boolean hasProcessor) {
        // TODO 前置处理内置类型
        if (isString(clazz)) {
            cell.setSv("##" + e + "##");
            return;
        }
        // 其它类型走原方法
        setCellValue(row, cell, e, hc, clazz, hasProcessor);
    }
}

导出图片

默认情况下EEC总是以"值"的形式导出,即使是byte[]也将进行toString后导出,所以EEC是安全的。

从v0.5.10开始支持导出图片,EEC使用以下双检查以保证安全,避免可执行文件、木马病毒写入Excel

  1. 检查列是否以Media格式导出,用户必须显示的调用writeAsMedia方法指定
  2. 检查FILE SIGNATURES是否为白名单中的图片格式,只有识别到白名单的Signatures才会写出到excel

这里的file signatures检查并非简单的扩展名匹配,而是尝试解析文件头的少量字节进行signatures匹配,更安全的除了文件头还需要匹配文件尾,但图片一般都几百Kb甚至几Mb为了性能EEC牺牲了绝对的安全

默认FILE SIGNATURES白名单格式如下

扩展名 Content-type
.png image/png
.jpg image/jpg
.gif image/gif
.tiff image/tiff
.bmp image/bmp
.ico image/x-ico
.emf image/x-emf
.wmf image/x-wmf
.webp image/webp

EEC支持Path, File, URL, byte[], ByteBuffer, InputStream, base64 image string7种类型的Media,你可以使用Column#writeAsMedia()或者@MediaColumn注解来指定当前列类型为Media,此注解还附加了一个属性presetEffect用于预设图片样式

简单示例代码如下

public static class Pic {
    @ExcelColumn("地址")
    private String addr;
    @MediaColumn // 指定以Media形式导出
    private String url;
}

new Workbook()
    .addSheet(new ListSheet<>(randomTestData()).setRowHeight(100))
    .writeTo(Paths.get("F:/excel"));

write media

添加图片样式

EEC支持Excel内置的28种预设图片样式,内置样式使用PresetPictureEffect枚举类型获取,枚举的顺序与Excel中的顺序完全一致,预设样式如下:

excel preset effects

使用presetEffect指定预设样式

public static class Pic {
    @ExcelColumn("地址")
    private String addr;
    @MediaColumn(presetEffect = PresetPictureEffect.Rotated_White) // 指定以Media形式导出并添加图片效果
    private String url;
}

Rotated white

以上是为整列设置样同的图片样式,我们也可以自定义XMLWorksheetWriter为每列设置不同样式

new Workbook()
    .addSheet(new ListSheet<>(randomTestData())
    .setRowHeight(217.5).autoSize().setSheetWriter(new XMLWorksheetWriter() {
        @Override
        protected Picture createPicture(int column, int row) {
            Picture picture = super.createPicture(column, row);
            // 某些效果会加边框、倒影或者旋转所以这里增加padding的大小以显示完整的效果
            picture.padding = 15 << 24 | 15 << 16 | 35 << 8 | 15;
            PresetPictureEffect[] effects = PresetPictureEffect.values();
            // 添加效果
            picture.effect = effects[row - 2].getEffect();
            return picture;
        }
})).writeTo(Paths.get("F:/excel"));

如下图展示 A列是枚举值,B列是对应的效果

preset effect

PresetPictureEffect枚举中英文对照表

中文 枚举值
None
简单框架,白色 SimpleFrame_White
棱台亚光,白色 BeveledMatte
金属框架 MetalFrame
矩形投影 DropShadowRectangle
映像圆角矩形 ReflectedRoundedRectangle
柔化边缘矩形 SoftEdgeRectangle
双框架,黑色 DoubleFrame
厚重亚光,黑色 ThickMatte
简单框架,黑色 SimpleFrame_Black
棱台形椭圆,黑色 BeveledOval
复杂框架,黑色 CompoundFrame
中等复杂框架,黑色 ModerateFrame
居中矩形阴影 CenterShadowRectangle
圆形对角,白色 RoundedDiagonalCorner
剪去对角,白色 SnipDiagonalCorner
中等复杂框架,白色 ModerateFrame
旋转,白色 Rotated
透视阴影,白色 PerspectiveShadow
松散透视,白色 RelaxedPerspective
柔化边缘椭圆 SoftEdgeOval
棱台矩形 BevelRectangle
棱台透视 BevelPerspective
映像右透视 ReflectedPerpsectiveRight
棱台左透视,白色 BevelPerspectiveLeft
映像棱台,黑色 ReflectedBevel
映像棱台,白色 ReflectedBevel_White
金属圆角矩形 MetalRoundedRectangle
金属椭圆 MetalOval

注意:自适应列宽对Media列无效,它总是以固定宽度显示

关于图片下载

虽然EEC支持远程图片下载但能力较弱,内置下载工具仅使用java.net.HttpURLConnection类,它不会使用连接池也没有支持ftp以及身份鉴权, 所以在集成的过程中如果有图片下载的话最好使用已有下载器,然后使用InputStream,byte[]或者ByteBuffer传入EEC,这样的话你可能需要修改实体对代码有一定的破坏性,当然你也可以自定义XMLWorksheetWriter 将下载器集成进EEC,这样就不需要修改已有Java对象了。

下面展示一段使用OkHttp做为下载器替换java.net.HttpURLConnection的示例,你可以使用已有任何工具替换

new Workbook().addSheet(new ListSheet<>(getRemoteUrls())
    .setColumns(new Column().setWidth(20).writeAsMedia()).setRowHeight(100)
    .setSheetWriter(new XMLWorksheetWriter() {
        @Override public void downloadRemoteResource(Picture picture, String uri) throws IOException {
            // http or https
            if (uri.startsWith("http")) {
                try (Response response = OkHttpClientUtil.client().newCall(new Request.Builder().url(uri).get().build()).execute()) {
                    ResponseBody body;
                    if (response.isSuccessful() && (body = response.body()) != null) {
                        downloadCompleted(picture, body.bytes());
                    }
                } catch (IOException ex) {
                    downloadCompleted(picture, null);
                }
            }
            // ftp or ftps
            else if (uri.startsWith("ftp")) {
                // TODO download from ftp server
            }
        }
    })).writeTo(Paths.get("F:/excel"));

通过downloadCompleted方法告诉EEC图片数据已准备好,此方法会进行文件签名检查通过后才会将图片数据写到excel中去,在捕获异常代码块中传入null告诉EEC下载失败。

性能模式

v0.5.22开始导出功能开放性能模式,性能模式下导出性能大幅提升,经测试性能模式比普通模式导出性能提升60%~100%,但文件大小略增加5%~10%

# 1k~1000k 表示写入行数1~100万耗时多少毫秒 数字越小性能越好
# 最后一列 Cells/s 表示每秒导出单元格数量  数字越大性能越好
+--------+----+------+------+------+------+--------+--------+----------+
| TOOLS  | 1k |  5k  | 10k  | 50k  | 100k |  500k  | 1000k  | Cells/s  |
+--------+----+------+------+------+------+--------+--------+----------+
| Eec(w) | 59 |  186 |  343 | 1600 | 3183 |  15814 |  31945 |   595788 | <- 普通模式
+--------+----+------+------+------+------+--------+--------+----------+
| Eec(w) | 43 |  107 |  190 |  871 | 1716 |   8406 |  16681 |  1130056 | <- 性能模式89.67%↑
+--------+----+------+------+------+------+--------+--------+----------+
|                           ↓Shared String↓                            |
+--------+----+------+------+------+------+--------+--------+----------+
| Eec(w) | 91 |  326 |  816 | 2493 | 4568 |  21575 |  44497 |   425652 | <- 普通模式
+--------+----+------+------+------+------+--------+--------+----------+
| Eec(w) | 67 |  243 |  668 | 1619 | 2861 |  12911 |  25428 |   722785 | <- 性能模式69.8%↑
+--------+----+------+------+------+------+--------+--------+----------+

启用性能模式

在原有导出方法上调用#bestSpeed方法即可启用性能模式

new Workbook()
    .bestSpeed() <- 启用性能模式
    .addSheet(new EmptySheet())
    .writeTo(Paths.get("F:/excel"));

性能模式的本质

或许你已经知道了xlsx本质是zip包,所以本次性能模式即调整zip压缩等级,Java的Deflater算法压缩等级从0-9共10个等级,数字越大压缩率越高,0表示无压缩仅归档。 普通模式下压缩等级为5,性能模式下压缩等级为1,虽然数字看上去相差很大但实际对于纯文本来说它们的压缩比相差很少,正如上面的测试结果一样成品文件大小仅增加5%~10%平均增幅约7.8%, 与性能增幅相比压缩比的影响甚微尤其对于几百,几千行的Excel文件来说这点增幅可以忽略

内存是否增加

不增加,性能模式仅调整压缩等级并不会影响内存使用,性能模式下10MB内存依然可以跑1~100万行数据导出测试

查看更多 高级特性