-
Notifications
You must be signed in to change notification settings - Fork 59
1 导出Excel
EEC目前支持SimpleSheet
, ListSheet
,ListMapSheet
,TemplateSheet
,StatementSheet
,ResultSetSheet
,CSVSheet
和EmptySheet
几种内置的Worksheet,如果不能满足需求你也可以继承已有的Worksheet来扩展,最常见的就是对于大数据量写入时的分片处理,这个在后面会讲到,目前还是从最简单的ListSheet出发。
数据导出应该是开发过程中比较常见的功能,可就是这种简单功能如果使用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流
}
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"));
导出文件如下:
出于某些安全考虑需要隐藏某个或多个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"));
导出文件如下,点击右键选择“取消隐藏”就可以还原了。
为了数据安全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
- 检查列是否以
Media
格式导出,用户必须显示的调用writeAsMedia
方法指定 - 检查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 string
7种类型的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"));
EEC支持Excel内置的28种预设图片样式,内置样式使用PresetPictureEffect
枚举类型获取,枚举的顺序与Excel中的顺序完全一致,预设样式如下:
使用presetEffect
指定预设样式
public static class Pic {
@ExcelColumn("地址")
private String addr;
@MediaColumn(presetEffect = PresetPictureEffect.Rotated_White) // 指定以Media形式导出并添加图片效果
private String url;
}
以上是为整列设置样同的图片样式,我们也可以自定义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列是对应的效果
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万行数据导出测试
查看更多 高级特性
让JAVA操作excel更简单