背景 —— 提一个好的问题
开发过程中经常会遇到 Excel 导出的情况,尤其是在企业开发中,涉及到客户信息、财务报表、市场分析等,情景非常多。平常开发过程中大多都会针对每个导出单独写一套代码,随着导出越来越多,心里便想:有没有一个足够通用东西可以让我们不用写这么多代码来实现 Excel 导出?
带着这个问题便开始了自己的“ExcelUtil”之路,在这过程中主要接触过 easypoi,但还是不太满足。因为 easypoi 和大多数 Java 库一样:基于字段写配置。当然不是说这个不好,有很多库都这样,比如 fastjson、Jackson 等都是在字段上写注解,描述这个字段有些什么信息或作用等。但对于 Excel 导出,我总觉得还有更加通用的方式。
经过一段时间的摸索和发掘,在前端的 table 标签上找到了灵感,认为这个方式很好、非常好。table 标签本身包含了很多描述信息,像行、列、合并行、合并列这些与 excel 的 sheet 页“惊人的相似”,再加上近几年前端三大框架的大力发展,尤其是 angular 和 vue 这两个框架在标签上自定义属性的方式进一步让我在写 ExcelUtil 过程中得到了不少启发。
简介
ExcelUtil 和 RunnerUtil(GitHub) 一样,大概是在今年 5 到 6 月写的,最近又重新整理了一下,已上传 GitHub # ExcelUtil。
ExcelUtil 根据 excel 文件、sheet 页、row 行、cell 单元格这样的层次结构分别定义了自己的作用域,每个作用域内可以一定程度上自定义变量等,作用域之间互不影响,同名变量下层作用域等声明优先于上层作用域等这些与 java、JavaScript 等语言的作用域结构一致。
使用介绍
-
使用 ExcelUtil 的之前首先要准备的就是数据,数据并没有特殊的格式要求,可以是任意 Java 类型数据,如 Collection、Iterable、Iterator(迭代器模式可,这是在一次面试时得到的启发,可用于超大 Excel 导出,虽然后来没通过,但仍然很感谢那位面试官!)、Map、数组、POJO、Number等。
-
第二步是生成 Workbook 位置的方法上进行“注解编程” —— 对的,Java 的注解功能很强大,可以在 Java 内部又单独作为 Java 内的“编程语言”(其实就是写了个简单的解析器而言,捂脸一笑)。
// 在什么地方导出,就在那个方法上进行声明式“注解编程”
// 首先要声明这是一个 Excel,用 type 指定是 xls 或者 xlsx
@TableExcel(type = TableExcel.Type.XLS, value = {
/*
* value 包含的是左右 sheet 页的信息
* 自 sheet 向下,每个标签可以判断、循环等
* 用 sheetName 指定 sheet 名
* 为什么要用单引号再多包裹一层呢?详见 RunnerUtil
* 因为这里面的所有内容都是用 RunnerUtil 解析的,需要符合它的格式
*/
@TableSheet(sheetName = "'人员信息'", value = {
/*
* 在这儿声明了一个名为 names 的数组,用作标题
*/
@TableRow(var = "names = {'序号','姓名','性别','年龄','电话','家庭住址', '备注'}", value = {
/*
* 这儿用了迭代,迭代 row 上声明的 names
* 这个迭代将按 names 的内容生成对应数量和内容的 cell 单元格
*/
@TableCell(var = "name:names", value = name)
}),
/*
* 上面 cell 的迭代用的是冒号,这儿用了 in,二者意义完全一样
* 支持 in 完全是为了向灵感的来源(前端)致敬
* 但是 in 并不是关键字,仍可作为普通变量
* 不同的是 in 的两端至少各有一个空格
* 可迭代的数据类型一会儿详细介绍
*/
@TableRow(var = "($rowData, index) in collect", value = {
@TableCell("index + 1"), // 序号
@TableCell("$rowData.name"), // 姓名
@TableCell("$rowData.sex"), // 性别
@TableCell("$rowData.age"), // 年龄
@TableCell("$rowData.mobile"), // 电话
@TableCell("$rowData.address"), // 家庭住址
// 最后这个对于上面的备注,这儿有个 when,只有 index == 0 才创建这个单元格
// 同时这儿还用到了并合并行,另外 colspan 是合并列
@TableCell(when = "index == 0", rowspan = "data.size()")
})
})
})
public Workbook exportExcel(Object data){
/*
* 写好注解后只需要调用这个方法便可得到一个 Workbook
*/
return ExcelUtil.render(data);
}
复制代码
- ExcelUtil.render(data); 在渲染中 in (或冒号 :)可迭代的数据有:
- number(整数),如 var = "$item in 10",循环十次;
- 字符串,迭代出字符串中的每个字符,但由于 RunnerUtil 是不支持 char 类型数据的,所以实际上迭代出来的是单个字符的字符串
- Collection、Iterable、List、Set 等集合。
- Map,迭代出来的是每一个键值对的值;
- POJO,普通 Java 对象按字段名迭代
- when 后面的表达式返回值必须是 boolean 类型
- colspan、rowspan 表达式返回值必须是 int 类型
- 其他的还有 heigit、width 等也必须是 int 类型
使用效果:
- 生成的对应 Excel 效果图
性能测试
贴一个本工具导出的 10 列 Excel 的性能测试表(本机环境 i7-8700K 16G Win10)
行数(万行) | 生成数据耗时(ms) | write到文件耗时(ms) | 总耗时(ms) |
---|---|---|---|
100 | 6,182 | 5,565 | 11,747 |
300 | 14,800 | 16,693 | 31,493 |
500 | 25,876 | 27,317 | 53,193 |
700 | 36,121 | 42,171 | 78,292 |
999 | 53,532 | 54,745 | 108,277 |
4000 | 240,453 | 271,832 | 512,285 |
6000 | 366,987 | 423,351 | 790,338 |
8000 | 528,654 | 498,490 | 1,027,144 |
从这个数据可以看出,随着数据量增加,时间与数据的关系呈正相关性,比较接近线性关系,100 万行数据生成 Workbook 耗时 6s,总耗时 12s,在正常业务场景下能满足时间的要求。
其他说明
- 当 Excel 数据量超过 150 万行时,不建议用 xls 格式(这个数据在不同机器上应该有差异,本机 150 万行的 xls 能正常导出,180 万行就 OOM 了);
- 当数据量超过 500 万行时,TableExcel 的 type 值应为 SUPER(type = TableExcel.Type.SUPER),SUPER 对应的也是 xlsx 格式,但是 SUPER 是用来支持超大数据导出的;
- 150 万行和 500 万行基本是极限值了。