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 |
单元格插入图片 |
相关推荐
数据分流写入Excel
Poi版本升级优化
StringTemplate实现Excel导出
Poi模板技术
SAX方式实现Excel导入
DOM方式实现Excel导入
Poi实现Excel导出
EasyExcel实现Excel文件导入导出
EasyPoi实现excel文件导入导出
个人博客
欢迎各位访问我的个人博客: https://www.crystalblog.xyz/
备用地址: https://wang-qz.gitee.io/crystal-blog/