记录一次使用easypoi时与源码博弈的过程

京东云开发者
• 阅读 377

一、背景介绍

最近刚刚接手了保险一线之声平台的开发和维护工作,第一个需要修复的问题是:平台的事件导出成excel功能在经过一次上线之后突然不可用了,于是就开始了几轮痛苦的排查以及与源码博弈的过程。




二、问题描述

一线之声在事件查询菜单下支持将结果导出为Excel,程序中使用easypoi+apache-poi实现,此功能一直正常使用,直到从2024-04-12 12.04.39之后的任务全部都导出失败



记录一次使用easypoi时与源码博弈的过程






三、问题定位

3.1 排查过程

看到这个问题后,第一反应是不是某次上线引起的?于是去查看了服务的上线记录,果然在当天出现问题前几分钟有过一次上线!!!



记录一次使用easypoi时与源码博弈的过程



看到这里,心中猜测这个问题十有八九是这次上线导致的。这还不简单,去看看上线修改了什么内容~😁

看完上线内容之后,一言难尽,改动的内容跟这里八竿子打不着!

不由想起了那个困扰大部分程序员的问题:我就加了一行日志打印,怎么程序就报错了呢? 🤔😨😠😡

没办法,于是又尝试去查看线上日志,找到一个报错信息如下:

cn.afterturn.easypoi.exception.excel.ExcelExportException: Excel导出错误
    at cn.afterturn.easypoi.excel.export.ExcelExportService.createSheet(ExcelExportService.java:118)
    at cn.afterturn.easypoi.excel.ExcelExportUtil.exportExcel(ExcelExportUtil.java:87)
    at com.jd.jxqe.ph.service.export.impl.ExportRequireServiceImpl.writeOfflineExcel(ExportRequireServiceImpl.java:346)
    at com.jd.jxqe.ph.service.export.impl.ExportRequireServiceImpl.export(ExportRequireServiceImpl.java:216)
    at sun.reflect.GeneratedMethodAccessor1931.invoke(Unknown Source)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:497)
    at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:343)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:197)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163)
    at org.springframework.aop.interceptor.AsyncExecutionInterceptor.lambda$invoke$0(AsyncExecutionInterceptor.java:115)
    at java.util.concurrent.FutureTask.run(FutureTask.java:266)
    at com.jd.jxqe.ph.common.MdcTaskDecorator.lambda$decorate$0(MdcTaskDecorator.java:26)
    at com.alibaba.ttl.TtlRunnable.run(TtlRunnable.java:59)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
    at java.lang.Thread.run(Thread.java:745)
Caused by: java.lang.NoSuchMethodError: org.apache.poi.ss.usermodel.CellStyle.setAlignment(S)V
    at cn.afterturn.easypoi.excel.export.styler.ExcelExportStylerDefaultImpl.stringNoneStyle(ExcelExportStylerDefaultImpl.java:69)
    at cn.afterturn.easypoi.excel.export.styler.AbstractExcelExportStyler.createStyles(AbstractExcelExportStyler.java:44)
    at cn.afterturn.easypoi.excel.export.styler.ExcelExportStylerDefaultImpl.<init>(ExcelExportStylerDefaultImpl.java:31)
    at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
    at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
    at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
    at java.lang.reflect.Constructor.newInstance(Constructor.java:422)
    at cn.afterturn.easypoi.excel.export.ExcelExportService.insertDataToSheet(ExcelExportService.java:159)
    at cn.afterturn.easypoi.excel.export.ExcelExportService.createSheetForMap(ExcelExportService.java:145)
    at cn.afterturn.easypoi.excel.export.ExcelExportService.createSheet(ExcelExportService.java:115)
    ... 16 common frames omitted

3.2 白夜追凶

可以看到是在CellStyle源码中出现了报错,而项目中使用的easypoi和apache-poi版本分别如下:

<dependency>
    <groupId>org.apache.poi</groupId>
    <artifactId>poi</artifactId>
    <version>4.0.1</version>
</dependency>
<dependency>
    <groupId>org.apache.poi</groupId>
    <artifactId>poi-ooxml</artifactId>
    <version>4.0.1</version>
</dependency>

<dependency>
    <groupId>cn.afterturn</groupId>
    <artifactId>easypoi-spring-boot-starter</artifactId>
    <version>4.0.0</version>
    <exclusions>
        <exclusion>
            <artifactId>guava</artifactId>
            <groupId>com.google.guava</groupId>
        </exclusion>
    </exclusions>
</dependency>

同时由于pom中引入的某个domain包中依赖了easypoi-base 3.2版本,所以最终使用的easypoi-base版本被强制修改为了3.2,在此版本中cn.afterturn.easypoi.excel.export.ExcelExportService类下看到一段代码:



记录一次使用easypoi时与源码博弈的过程



其中飘红报错的地方是因为在org.apache.poi.ss.usermodel.CellStyle这个接口中,没有定义相应的常量,为什么会出现这个问题呢?按理说并没有修改过依赖的apache-poi版本。

于是尝试往前追溯几个版本的org.apache.poi.ss.usermodel.CellStyle,终于在3.6这个版本中找到了确实曾经定义过这些常量,但是在后续的版本升级中已经弃用了。



记录一次使用easypoi时与源码博弈的过程



那问题来了,为什么没有修改过pom文件,但是在2024-04-12程序上线后出现了这个源码报错的问题?带着这个问题继续追查的时候我发现编译平台上配置的编译命令启用了-U参数:



记录一次使用easypoi时与源码博弈的过程



-U参数的作用是强制更新快照(snapshot)版本的依赖和插件。大致可以理解为,当你使用 mvn compile -U 或其他带有 -U 参数的命令时,Maven 会检查并下载最新的快照版本,而不是使用本地缓存的快照版本。我猜测可能是由于这个参数导致了之前的快照版本被覆盖了,而历史快照版本中存在过这些常量。




四、问题解决

4.1 初次升级版本

由于此服务历史代码较多,尝试降低apache-poi版本后出现了更多的兼容性问题,所以最终决定升级easypoi和apache-poi的版本,此时选择了easypoi 4.5.0和apache-poi 5.0.0版本的组合,至于为什么使用这个两个版本,可能当时看着比较顺眼吧😩(居然忘了看下版本是否兼容),这也为我后续陷入第二轮的源码追查埋下了伏笔😖。

4.2 二次入坑

使用上述的配置,在本地和预发验证后均没有问题,但是上线后发现偶现任务一直处于执行中的情况,此时已经意识到问题的不妙,果然在日志中发现了如下的报错:

2024-06-25 20:38:04.730 [/] [exportThreadPoolExecutor--6] ERROR org.springframework.aop.interceptor.SimpleAsyncUncaughtExceptionHandler - Unexpected error occurred invoking async method 'public com.jd.jxqe.ph.common.ResponseResult com.jd.jxqe.ph.service.export.impl.ExportRequireServiceImpl.export(com.jd.jxqe.ph.model.dto.EventQueryDTO) throws java.lang.InterruptedException'.
java.lang.NoSuchMethodError: org.openxmlformats.schemas.spreadsheetml.x2006.main.CTFont.addNewFamily()Lorg/openxmlformats/schemas/spreadsheetml/x2006/main/CTFontFamily;
    at org.apache.poi.xssf.usermodel.XSSFFont.setFamily(XSSFFont.java:635)
    at org.apache.poi.xssf.usermodel.XSSFFont.setFamily(XSSFFont.java:647)
    at org.apache.poi.xssf.model.StylesTable.createDefaultFont(StylesTable.java:765)
    at org.apache.poi.xssf.model.StylesTable.initialize(StylesTable.java:716)
    at org.apache.poi.xssf.model.StylesTable.<init>(StylesTable.java:130)
    at org.apache.poi.ooxml.POIXMLFactory.newDocumentPart(POIXMLFactory.java:94)
    at org.apache.poi.ooxml.POIXMLDocumentPart.createRelationship(POIXMLDocumentPart.java:591)
    at org.apache.poi.ooxml.POIXMLDocumentPart.createRelationship(POIXMLDocumentPart.java:500)
    at org.apache.poi.xssf.usermodel.XSSFWorkbook.onWorkbookCreate(XSSFWorkbook.java:465)
    at org.apache.poi.xssf.usermodel.XSSFWorkbook.<init>(XSSFWorkbook.java:255)
    at org.apache.poi.xssf.usermodel.XSSFWorkbook.<init>(XSSFWorkbook.java:249)
    at org.apache.poi.xssf.usermodel.XSSFWorkbook.<init>(XSSFWorkbook.java:237)
    at cn.afterturn.easypoi.excel.ExcelExportUtil.getWorkbook(ExcelExportUtil.java:124)
    at cn.afterturn.easypoi.excel.ExcelExportUtil.exportExcel(ExcelExportUtil.java:115)
    at com.jd.jxqe.ph.service.export.impl.ExportRequireServiceImpl.writeOfflineExcel(ExportRequireServiceImpl.java:348)
    at com.jd.jxqe.ph.service.export.impl.ExportRequireServiceImpl.export(ExportRequireServiceImpl.java:216)
    at sun.reflect.GeneratedMethodAccessor1333.invoke(Unknown Source)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:497)
    at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:343)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:197)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163)
    at org.springframework.aop.interceptor.AsyncExecutionInterceptor.lambda$invoke$0(AsyncExecutionInterceptor.java:115)
    at java.util.concurrent.FutureTask.run(FutureTask.java:266)
    at com.jd.jxqe.ph.common.MdcTaskDecorator.lambda$decorate$0(MdcTaskDecorator.java:26)
    at com.alibaba.ttl.TtlRunnable.run(TtlRunnable.java:59)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
    at java.lang.Thread.run(Thread.java:745)

查看过maven依赖树以及源码后,注意到一个细节:org.openxmlformats.schemas.spreadsheetml.x2006.main.CTFont这个类在以下:

org.apache.poi:poi-ooxml-lite:jar:5.0.0:compile org.apache.poi:poi-ooxml-schemas:jar:4.1.1:compile

两个包中都存在。但是两者的addNewFamily()方法实现方式不同。

调试过程中发现程序再本地使用的是org.apache.poi:poi-ooxml-lite:jar包中的CTFont,这时生成Excel并无异常。猜测可能是线上偶尔执行了poi-ooxml-schemas包下的CTFont导致的异常。

为了验证这个猜想,通过在本地强制修改org.apache.poi:poi-ooxml-lite:jar:5.0.0:compile包内容后,让程序固定使用poi-ooxml-schemas:jar下的CTFont类,稳定复现出了上述的问题。所以归根结底还是依赖的版本有冲突。

4.3 彻底修复

既然是依赖版本有冲突,那解决的方式就从版本匹配入手,这一次不再盲目的修改,参考了源码中的版本依赖关系,参考中央仓库中easypoi4.5.0版本的pom配置( https://repo1.maven.org/maven2/cn/afterturn/easypoi/4.5.0/easypoi-4.5.0.pom



记录一次使用easypoi时与源码博弈的过程



于是最终将项目中的pom依赖配置为easyapi4.5.0+apache-poi4.1.1版本,上线验证后问题彻底解决

4.4 遗留的困惑

上述问题中提到:org.openxmlformats.schemas.spreadsheetml.x2006.main.CTFont这个类在org.apache.poi:poi-ooxml-lite:jar:5.0.0:compile 和 org.apache.poi:poi-ooxml-schemas:jar:4.1.1:compile两个包都存在

按照mvn的依赖管理机制,不管是优先加载机制还是就近路径机制,应该都会只使用其中一个固定的包,本地调试是也一直使用poi-ooxml-lite.jar中的CTFont,但是线上偶现的失败场景下,似乎两个包下的类都会用到,此处不确定是否在存在嵌套复杂依赖的场景下,对于类加载的顺序是否会存在随机性,期待有了解的大佬希望能给解答一下🤝。




五、致谢

最后,特别感谢信总、磊哥在此问题定位过程中给予的支持和耐心的答疑!

点赞
收藏
评论区
推荐文章
Wesley13 Wesley13
3年前
java实现 Excel 导入导出
日常工作中,Excel是我们经常需要处理的文件,报表的生成,数据的导出,几乎每个项目都需要写对应的处理.作者也是编写这块代码大军的一员,能否有方法让我们不用重复编写代码呢,能否只要简单配置就可以完成我们的Excel生成呢,作者分析了Excel和对象的关系,发现Row就是我们的一个对象cell是我们的一个属性,从而开发了Easypoi,下面讲解下用
Stella981 Stella981
3年前
HuTool工具类使用之Excel文档的导入导出
HuTool工具类使用之Excel文档的导入导出前言在日常的工作开发中,Excel的导入和导出是必不可少的,如果自己写相应的导入导出方法,会显得十分繁琐,本文采用Hutool工具类实现的Excel导入导出功能,可以大幅度减少今后开发中Excel的导入导出的相关操作。
Stella981 Stella981
3年前
Flutter Dojo的设计之道
认识Flutter是在18年,移动端开发日趋成熟的情况下,很多开发者都在寻求跨平台开发的终极法门,在经过了webview、RN的痛苦之后,Flutter的出现,给跨平台开发带来了一线曙光。自此,便开始了Flutter的学习之路,布道师之路,修仙之路。筑基Flutter的学习曲线很奇怪,像坐过山车一样,初学很简单,上手几天,很快就能写一些基本的界
Wesley13 Wesley13
3年前
ueditor源代码重点难点分析
网上好像几乎没有研究ueditor源码的文章,原因可能是ueditor源码太复杂了,接近浏览器代码和word/excel源码。本文分析ueditor源码整体流程逻辑以及重点难点细节。首先,编辑器是如何实现输入的?本人开始始终不得其解,在源码找不到输入事件绑定的处理函数,后来在白云峰同学的提醒下才顿悟,整个iframe网页就相当于是一个<textarea
Stella981 Stella981
3年前
PHPWord导出word文档
最近接了个把数据导出到word文档的需求,之前一直都是使用PHPExcel库导出excel的,还是头次接到导出到word文档的需求,我想既然有PHPExcel,那么肯定也会有PHPWord库吧,在网上一搜,还真有!而且都是phpoffice家的。看了下文档,最终决定使用模板的方式来导出数据,感觉也是最简单的一种方式了。过程如下:使用composer下
Stella981 Stella981
3年前
EasyExcel引入
好久没更了,都在有道云上面记录,没时间搬过来。easyexcel是最近项目做优化涉及的一个改善点吧。简介        导出是后台管理系统的常用功能,当数据量特别大的时候会内存溢出和卡顿页面,曾经自己封装过一个导出,POI百万级大数据量EXCEL导出采用了分批查询数据来避免内存溢出和使用SXSSFWorkbook方式缓存数据到文件上以解决
Stella981 Stella981
3年前
React项目中应对开发、测试、生产环境下使用不同全局常量的问题
在开发过程中,避免不了后端在开发、测试、生产环境不一致的情况,这不最近就遇到这样的问题:在开发和测试环境,后端需要一个参数比如:zonehk1,在生产环境中需要的参数又是zonehka,那么解决这种问题的方法也不是唯一的,这里只介绍一种通过配置文件完成的过程。我们在构建react项目时,会执行npmruneject,导出react脚手架
LeeFJ LeeFJ
1年前
Foxnic-SQL (15) —— 使用记录集导入或导出Excel
很多时候,我们需要将外部Excel表中的数据导入到数据库,或是需要将某个查询结果导出到Excel文件中,对于这种简单的操作,FoxnicSQL已经内置了ExcelReader和ExcelWriter用于处理Excel数据。本文中的示例代码均可在https://gitee.com/LeeFJ/foxnicsamples项目中找到。读取Excel到RcdSetFoxnicSQL使用ExcelReader类读取Excel中某个sheet的数据,这些数据将被读取到RcdSet,通过RcdSet可以完成数据库保存等操作。在读取Excel前需要定义Excel结构,将Excel列映射到数据库字段,如下代码所示。一旦Excel数据转换成RcdSet,开发人员就可以去做其它更多额外的数据处理。
当小白遇到FullGC | 京东云技术团队
本文记录了一次排查FullGC导致的TP99过高过程,介绍了一些排查时思路,线索以及工具的使用,希望能够帮助一些新手在排查问题没有很好的思路时,提供一些思路,让小白也能轻松解决FullGC问题
京东云开发者 京东云开发者
9个月前
博弈论入门篇——「三个枪手」的心理博弈
博弈论是一门很有趣的学科,本文将以博弈问题《三个枪手》为脉络,从零基础开始介绍博弈论,和大家一起博弈论是如何解决实际问题的。希望通过本文,让大家都能听懂博弈论。题目:《三个枪手》三个小伙子同时爱上了一个姑娘,为了决定他们谁能娶这个姑娘,他们决定用枪进行一次