Poi-3.17前后版本api使用差异
1. 升级缘由
最近公司prod
环境出现因为Excel文件下载数据量过大导致应用out of memory
, 然后就需要找到内存溢出的原因及优化方案. 经分析, 得出以下结论:
1.1 事故原因
(1) 应用场景发生在页面列表查询功能, 可以批量勾选列表项选择下载, 后台又需要根据勾选的列表项字段关联查询出
N+
条大量明细数据, 系统程序又没有对大数据量做评估校验, 一路绿灯,最终形成社会性死亡现场 . 导致宕机的主要原因倒不是因为单个请求线程的数据量过大导致, 当前请求最多不过是下载失败, 真正导致应用宕机是因为前端界面一直展示下载加载效果, 用户一直重复点击, 多个请求线程访问应用, 熟悉POI的老铁们应该都知道, POI生成DOM节点需要消耗大量的内存, 结果就是开头的out of memory
了.(2) 系统监控平台发现大量接口请求超时, 为了不影响后续其他业务的正常作业, 立即展开抢救, 过程就是根据导致宕机的请求数据找到操作用户(因为是内网用户, 可以查到用户联系方式), 然后打电话给那位用户说明情况并让其不要再重复点击, 最后就是重启
Server
了, 很多问题都是重启可以搞定的哈, 当然我们是主从2台机器, 所以停机重启不会大面积影响业务, 请求会经F5负载均衡
转发到另外一台正常运行的机器上去 .(3) 经过在测试环境复现事故场景, 发现超过65536条(含标题行)记录就会报错, 相信用过
Poi
的老铁们都知道了, 我们程序使用的是POI
的HSSF
创建的Workbook
工作簿, 也就是创建的2003版Excel文件(xls), 最多仅支持65536
行记录写入(0-65535), 超过这个量的数据下载肯定会报错了. 下面是我自己测试复现的报错截图:
1.2 优化方案
分析出上面的问题原因后, 我也查阅了操作Excel相关的技术, 尝试了下面几种解决方案.
1.2.1 改用XSSF生成
针对上面HSSF
只能写入65536行记录的局限, POI
也是给出了解决方案的, XSSF
创建的Workbook
工作簿, 生成的是2007版Excel(xlsx), 支持上限1048576行记录的写入 , 基本满足我们日常的应用场景了. 另外在XSSF基础上, POI-3.8
版本开始提供的支持低内存占用的操作方式SXSSFWorkbook
, 支持2007以上版本的Excel操作.
关键代码体现 :
/**
* 根据要生成的文件类型创建HSSF或者XSSF工作簿
* @param fileType .xls .xlsx
* @return
*/
public static Workbook getWorkbook(String fileType) {
Workbook wb = null;
switch (fileType) {
case CSISCONSTANT.EXCEL03_EXTENSION:
wb = new HSSFWorkbook(); // 创建工作簿 2003版excel
break;
case CSISCONSTANT.EXCEL07_EXTENSION:
default:
wb = new XSSFWorkbook(); // 创建工作簿 2007版excel
break;
}
return wb;
}
具体实现代码请移步博客Poi实现Excel导出
1.2.2 数据拆分文件压缩
XSSF
方案只是解决了大数据量写入的问题, 从系统安全性和性能方面考虑, 还有更多的优化空间. 程序现状没有对数据做分流处理, 依然是全部数据一次性生成并写入Excel文件, 对内存的消耗仍旧很糟糕. 其实可以对查询出来的大量数据根据自己设置的阈值做分流处理, 阈值设置在1万-2万之间, 因为打开一个Excel文件当前Sheet页方便快速翻阅查看最好了, 数据写入太多翻页查看数据会很卡, 用户体验不好. 根据阈值分流后的数据在性能允许范围内, 循环调用提前封装好的生成Excel文件的方法, 每次循环生成后Excel文件后, 会释放掉POI
消耗的内存, 相比较一次性生成并写入Excel文件内存的占用时间和消耗小很多. 待所有Excel文件生成完成, 最后将这些文件打包压缩成zip
文件流返回给界面.
另外, SpringMVC
中的Web端文件下载是在当前请求线程内完成的, 在生成并写入数据文件的过程中, 界面只能等着后端系统的响应, 且当前请求的线程会一直被占用着, 数据量不大还好, 如果数据量很大会影响用户体验. 我工作中用到的是前后端分离的项目, 文件下载
是异步请求实现的, 设计思想就是界面发起文件下载的异步请求, 后台程序将生成的数据文件存放到服务器临时下载目录中 , 将临时下载目录的数据文件路径以流的形式返回给前端界面, 前端使用封装好的api直接去服务器临时下载目录中下载文件即可, 而不需要将整个数据文件以流的形式返回给前端界面. 文件上传
也可以采用这种思想, 前端将文件直接上传到服务器临时上传目录中, 将文件路径提交到后台系统, 后台程序直接去服务器临时上传目录中读取解析.
数据分流生成Excel的实现代码请移步博客 数据分流写入Excel
文件压缩的实现代码请移步博客 Poi实现Excel导出
1.2.3 使用开源技术EasyExcel
EasyExcel
是一个基于Java的简单、省内存的读写Excel的开源项目。在尽可能节约内存的情况下支持读写百MB的Excel文件. 阿里的大牛们对POI
进行了各种封装和优化, 不管是性能还是内存消耗方面都是质的飞跃, 在api使用方面也极具人性化, 简单易用.
说明:
EasyExcel
底层是依赖POI
的, 最低版本的EasyExcel
要求POI-3.17
版本, 如果我用这个方案, 必然面临POI
版本升级问题, 因为POI-3.17
版本相比POI3.17
以下的版本, 在样式设置和单元格数据类型设置等方面改动比较大, 很多API进行了抽取. 项目组的系统有很多文件下载功能, 不能因为这个场景的问题全面改造, 没有测试人力的全面支持, 无法形成有效的升级. 后面我采用了数据分流写入并压缩
的方案, 避免了POI
版本升级的麻烦. 但是作为一名技术宅男, 肯定是不会放过这个优化方案的, 自己业余时间搭建了demo进行了冲突aip的升级改造, 具体冲突api的使用请见 Poi升级 .
EasyExcel
请移步博客 EasyExcel学习笔记
1.2.4 其他
除了原生POI
, EasyExcel
外, 还有其他相关技术可以实现Excel的操作, 比如EasyPoi
, Hutool-poi
, jxl
等.
EasyPoi
请移步博客 EasyPoi学习笔记
Hutool-poi
请参考Hutool官方文档中的office文档操作
部分.
2. Poi升级
POI3.17与POI旧版本对比, 有很多API的使用改变了, 下面将依次列出它们的不同及新API的使用.
2.1 颜色定义
旧版本
HSSFColor.GREEN.index
HSSFColor.BLACK.index
新版本
IndexedColors.GREEN.getIndex()
IndexedColors.GREEN.index
IndexedColors.BLACK.getIndex()
IndexedColors.BLACK.index
2.2 获取单元格格式
旧版本
// 获取单元格格式
int cellType = cell.getCellType();
// 与之对应的单元格格式int值
HSSFCell.CELL_TYPE_BLANK // 空
HSSFCell.CELL_TYPE_STRING // 字符串
HSSFCell.CELL_TYPE_NUMERIC // 数字类型
HSSFCell.CELL_TYPE_BOOLEAN // 布尔
HSSFCell.CELL_TYPE_FORMULA // 公式
HSSFCell.CELL_TYPE_ERROR // 错误
新版本
CellType cellTypeEnum = cell.getCellTypeEnum(); // 获取单元格格式
// 与之对应的单元格格式枚举值
CellType.BLANK // 空
CellType.STRING // 字符串
CellType.NUMERIC // 数字类型
CellType.BOOLEAN // 布尔
CellType.FORMULA // 公式
CellType.ERROR // 错误
2.3 设置单元格数据类型
旧版本
Cell cell = row.getCell(0); // 获取单元格对象
cell.setCellType(Cell.CELL_TYPE_STRING); // 设置单元格为字符串类型
新版本
Cell cell = row.getCell(0); // 获取单元格对象
cell.setCellType(CellType.STRING); // 设置单元格为字符串类型
2.4 设置单元格样式
单元格样式包含垂直居中样式
, 边框样式
, 背景填充颜色
, 边框线条
等…
旧版本
HSSFCellStyle cellStyle = wb.createCellStyle();
// XSSFCellStyle cellStyle = wb.createCellStyle();
cellStyle.setFillForegroundColor(HSSFColor.LIME.index); // 标题行背景色为绿色
cellStyle.setFillPattern(HSSFCellStyle.SOLID_FOREGROUND); // 填充背景色
cellStyle.setAlignment(XSSFCellStyle.ALIGN_CENTER); // 文字水平居中
cellStyle.setVerticalAlignment(XSSFCellStyle.VERTICAL_CENTER); // 文字垂直居中
cellStyle.setBorderBottom(XSSFCellStyle.BORDER_THIN); // 底部边框实体线条
cellStyle.setBorderTop(XSSFCellStyle.BORDER_THIN); // 顶部边框实体线条
cellStyle.setBorderLeft(XSSFCellStyle.BORDER_THIN); // 左部边框实体线条
cellStyle.setBorderRight(XSSFCellStyle.BORDER_THIN); // 右部边框实体线条
新版本
CellStyle cellStyle = wb.createCellStyle();
// XSSFCellStyle cellStyle = wb.createCellStyle();
cellStyle.setFillForegroundColor(IndexedColors.LIME.index); // 标题行背景色为绿色
cellStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND); // 填充背景色
cellStyle.setAlignment(HorizontalAlignment.CENTER); // 文字水平居中
cellStyle.setVerticalAlignment(VerticalAlignment.CENTER); // 文字垂直居中
cellStyle.setBorderBottom(BorderStyle.THIN); // 底部边框实体线条
cellStyle.setBorderTop(BorderStyle.THIN); // 顶部边框实体线条
cellStyle.setBorderLeft(BorderStyle.THIN); // 左部边框实体线条
cellStyle.setBorderRight(BorderStyle.THIN); // 右部边框实体线条
2.5 合并单元格
旧版本
Sheet sheet = workbook.createSheet("sheet1");
// 起始行,结束行,起始列,结束列
sheet.addMergedRegion(new CellRangeAddress(1, 1,(short) 0, (short) 0));
新版本
Sheet sheet = workbook.createSheet("sheet1");
// 起始行,起始列,结束行,结束列
sheet.addMergedRegion(new Region(1, (short) 0, 1,(short) 0));
2.6 设置字体加粗
旧版本
Font font = workbook.createFont();
// font.setBoldweight((short) 400);
font.setBoldweight(HSSFFont.BOLDWEIGHT_NORMAL);
font.setBoldweight(HSSFFont.BOLDWEIGHT_BOLD);
font.setBoldweight(XSSFFont.BOLDWEIGHT_NORMAL);
font.setBoldweight(HSSFFont.BOLDWEIGHT_BOLD);
新版本
Font font = workbook.createFont();
font.setBold(true);
2.7 设置图片属性
旧版本
// anchor主要用于设置图片的属性
XSSFClientAnchor anchor = new XSSFClientAnchor(0, 0, 255, 255, (short) i, rowNum + 1,
(short) i + 1, rowNum + 2);
anchor.setAnchorType(3);
// 插入图片
patriarch.createPicture(anchor,workbook.addPicture(byteArrayOut.toByteArray(),
ClientAnchor.MOVE_DONT_RESIZE));
新版本
// anchor主要用于设置图片的属性
XSSFClientAnchor anchor = new XSSFClientAnchor(0, 0, 255, 255, (short) i, rowNum + 1,
(short) i + 1, rowNum + 2);
anchor.setAnchorType(ClientAnchor.AnchorType.DONT_MOVE_AND_RESIZE));
// 插入图片
patriarch.createPicture(anchor,workbook.addPicture(byteArrayOut.toByteArray(),
ClientAnchor.AnchorType.MOVE_DONT_RESIZE.value));
2.8 小结
poi版本升级问题产生报错汇总(后续待补充)
poi3.17之前版本 | poi3.17+版本 | 用途 |
---|---|---|
Cell.CELL_TYPE_STRING | CellType.STRING | 单元格数据格式判断 |
HSSFCell.CELL_TYPE_NUMERIC | CellType.NUMERIC | 单元格数据格式判断 |
CellStyle.ALIGN_CENTER | HorizontalAlignment.CENTER | 单元格水平居中 |
CellStyle.VERTICAL_CENTER | VerticalAlignment.CENTER | 单元格垂直居中 |
HSSFColor.GREY_25_PERCENT.index | IndexedColors.GREY_25_PERCENT.index | 设置图案颜色 |
CellStyle.SOLID_FOREGROUND | FillPatternType.SOLID_FOREGROUND | 设置图案样式 |
CellStyle.BORDER_THIN | BorderStyle.THIN | 边框 |
ClientAnchor.MOVE_DONT_RESIZE | AnchorType.MOVE_DONT_RESIZE.value | 单元格插入图片 |
相关推荐
个人博客
欢迎各位访问我的个人博客: https://www.crystalblog.xyz/