一、背景介绍
最近刚刚接手了保险一线之声平台的开发和维护工作,第一个需要修复的问题是:平台的事件导出成excel功能在经过一次上线之后突然不可用了,于是就开始了几轮痛苦的排查以及与源码博弈的过程。
二、问题描述
一线之声在事件查询菜单下支持将结果导出为Excel,程序中使用easypoi+apache-poi实现,此功能一直正常使用,直到从2024-04-12 12.04.39之后的任务全部都导出失败
三、问题定位
3.1 排查过程
看到这个问题后,第一反应是不是某次上线引起的?于是去查看了服务的上线记录,果然在当天出现问题前几分钟有过一次上线!!!
看到这里,心中猜测这个问题十有八九是这次上线导致的。这还不简单,去看看上线修改了什么内容~😁
看完上线内容之后,一言难尽,改动的内容跟这里八竿子打不着!
不由想起了那个困扰大部分程序员的问题:我就加了一行日志打印,怎么程序就报错了呢?🤔😨😠😡
没办法,于是又尝试去查看线上日志,找到一个报错信息如下:
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类下看到一段代码:
其中飘红报错的地方是因为在org.apache.poi.ss.usermodel.CellStyle这个接口中,没有定义相应的常量,为什么会出现这个问题呢?按理说并没有修改过依赖的apache-poi版本。
于是尝试往前追溯几个版本的org.apache.poi.ss.usermodel.CellStyle,终于在3.6这个版本中找到了确实曾经定义过这些常量,但是在后续的版本升级中已经弃用了。
那问题来了,为什么没有修改过pom文件,但是在2024-04-12程序上线后出现了这个源码报错的问题?带着这个问题继续追查的时候我发现编译平台上配置的编译命令启用了-U参数:
-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 )
于是最终将项目中的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,但是线上偶现的失败场景下,似乎两个包下的类都会用到,此处不确定是否在存在嵌套复杂依赖的场景下,对于类加载的顺序是否会存在随机性,期待有了解的大佬希望能给解答一下🤝。
五、致谢
最后,特别感谢信总、磊哥在此问题定位过程中给予的支持和耐心的答疑!