Skip to content

1 导出Excel

guanquan.wang edited this page Aug 18, 2023 · 25 revisions

EEC目前支持ListSheetListMapSheetStatementSheetResultSetSheetCSVSheetEmptySheet几种内置的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("e:/excel")); // 指定导出位置
}

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

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

/**
 * 直接将excell输出到流
 */
@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());
}

强制导出forceExport

为了数据安全,EEC默认只会导出标记有@ExcelColumn的属性,如果Student对象里未标记@ExcelColumn那上面代码与EmptySheet等效,如果不方便加入注解你也可以调用forceExport方法全字段导出(标记了@IgnroeExport注解除外)或者手动添加Column。

// 手动指定Column
new Workbook().addSheet(new ListSheet<>(Student.randomTestData()
        , new Column("学号", "id")
        , new Column("姓名", "name")
        , new Column("成绩", "score")
    ))
    .writeTo(Paths.get("e:/excel"));

如果其它开发者新增了一些属性且未意识到forceExport标识则会出现数据泄漏风险,为防止数据泄露最好是指定Column导出,这样的话即使对象被人添加了新的敏感字段也不会被自动导出,降低不可预期风险发生。

添加多个Worksheet

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

下面代码演示生成多个Worksheet

new Workbook("multiSheet")
    .addSheet(new ListSheet<>("帐单表", checksTestData()))
    .addSheet(new ListSheet<>("客户表", customersTestData()))
    .addSheet(new ListSheet<>("用户客户关系表", c2CSTestData()))
    .writeTo(Paths.get("e:/excel"));

导出文件如下:

multi-sheet.png

隐藏某个Sheet

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

下面代码演示隐藏某个Worksheet

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

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

hidden-sheet.png

关于自动分页

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

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

自动分页部分代码解析

/**
 * Split worksheet data
 */
@Override
protected void paging() {
    // dataSize()是当前一组数据块的大小,limit是获取单个worksheet的行上限
    int len = dataSize(), limit = getRowLimit();
    // paging
    if (len + rows > limit) {
        // Reset current index
        end = limit - rows + start; // end是标记dataSize的最后位置,因为已经超限了所以当前页只会取未超限的数据
        shouldClose = false;
        eof = true;
        size = limit;

        int n = id;
        for (int i = end; i < len; ) {
            @SuppressWarnings("unchecked")
            ListSheet<T> copy = getClass().cast(clone()); // 复制一个新的worksheet
            copy.start = i;
            copy.end = (i = Math.min(i + limit, len));
            copy.size = copy.end - copy.start;
            copy.eof = copy.size == limit;
            workbook.insertSheet(n++, copy); // 插入到当前worksheet后面
        }
        // Close on the last copy worksheet
        workbook.getSheetAt(n - 1).shouldClose = true; // 如果是最后一个分页则关闭
    } else {
        end = len;
        size += len;
    }
}

多行表头

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

忽略表头

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

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

简单数据类型导出

有时候仅仅想导出最简单的数据类型,比如Integer,String,如果定义实体就显得过度设计,

List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 0);
new Workbook("Integer array")
    .addSheet(new ListSheet<>(list))
    .writeTo(defaultTestPath);

未知的数据类型

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

String CharSequence int Integer short Short
byte Byte long Long float Float
double Double boolean Boolean char Character
BigDecimal 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(int 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("d:/");

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

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

public class MyXMLCellValueAndStyle extends XMLCellValueAndStyle {
    @Override
    public void reset(int 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(int 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白名单格式如下

扩展名 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
    private String url;
}

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

write media

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

excel preset effects

使用presetEffect指定预设样式

public static class Pic {
    @ExcelColumn("地址")
    private String addr;
    @MediaColumn(presetEffect = PresetPictureEffect.Rotated_White)
    private String url;
}

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

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://"));

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

注意:自适应列宽对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 down load from ftp server
            }
        }
    })).writeTo(Paths.get("F://"));

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

上面代码可以看到只需要在完成下载后调用downloadCompleted方法通知EEC,那是否可以替换为异步下载呢?答案是"YES"。以上代码只需要将#execute()改为#enqueue(new Callback() {}并在onResponse和onFailure方法中调用downloadCompleted即可。

注意:目前来说异步下载并未通过充分测试和优化,至少v0.5.10版本不要在生产环境使用

查看更多 高级特性

Clone this wiki locally