Poi版本升级优化

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的老铁们都知道了, 我们程序使用的是POIHSSF创建的Workbook工作簿, 也就是创建的2003版Excel文件(xls), 最多仅支持65536行记录写入(0-65535), 超过这个量的数据下载肯定会报错了. 下面是我自己测试复现的报错截图:

image-20210919110849540

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_STRINGCellType.STRING单元格数据格式判断
HSSFCell.CELL_TYPE_NUMERICCellType.NUMERIC单元格数据格式判断
CellStyle.ALIGN_CENTERHorizontalAlignment.CENTER单元格水平居中
CellStyle.VERTICAL_CENTERVerticalAlignment.CENTER单元格垂直居中
HSSFColor.GREY_25_PERCENT.indexIndexedColors.GREY_25_PERCENT.index设置图案颜色
CellStyle.SOLID_FOREGROUNDFillPatternType.SOLID_FOREGROUND设置图案样式
CellStyle.BORDER_THINBorderStyle.THIN边框
ClientAnchor.MOVE_DONT_RESIZEAnchorType.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/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值