背景 -- 提一个好的问题
开发过程中经常会遇到 Excel 导出的情况, 尤其是在企业开发中, 涉及到客户信息, 财务报表, 市场分析等, 情景非常多. 平常开发过程中大多都会针对每个导出单独写一套代码, 随着导出越来越多, 心里便想: 有没有一个足够通用东西可以让我们不用写这么多代码来实现 Excel 导出?
带着这个问题便开始了自己的 "ExcelUtil" 之路, 在这过程中主要接触过 easypoi, 但还是不太满足. 因为 easypoi 和大多数 Java 库一样: 基于字段写配置. 当然不是说这个不好, 有很多库都这样, 比如 fastjson,Jackson 等都是在字段上写注解, 描述这个字段有些什么信息或作用等. 但对于 Excel 导出, 我总觉得还有更加通用的方式.
经过一段时间的摸索和发掘, 在前端的 table 标签上找到了灵感, 认为这个方式很好, 非常好. table 标签本身包含了很多描述信息, 像行, 列, 合并行, 合并列这些与 Excel 的 sheet 页 "惊人的相似", 再加上近几年前端三大框架的大力发展, 尤其是 angular 和 vue 这两个框架在标签上自定义属性的方式进一步让我在写 ExcelUtil 过程中得到了不少启发.
简介
ExcelUtil 和 RunnerUtil(GitHub https://github.com/xua744531854/moon-util ) 一样, 大概是在今年 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);
- }
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 万行基本是极限值了.
目前只支持导出, 还不能导入.
ExcelUtil # GitHub
RunnerUtil 的用法介绍
来源: https://juejin.im/post/5bfdf1aa6fb9a049a62c460f