开发过程中经常会遇到 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 使用频率比 RunnerUtil 频率高很多,写 RunnerUtil 的初衷也是为了这个 ExcelUtil 导出,最开始想到了 Java 内置脚本引擎(ScriptEngine),但内置脚本引擎的效率实在太低,数据量稍微大一点(不用太大)情况下直接卡死(不该这么吐槽的,但的确不适合这个场景)。但是 RunnerUtil 的功能独立且完善,完全可以单独使用。
使用 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); } 复制代码
贴一个本工具导出的 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,在正常业务场景下能满足时间的要求。