之前在项目中使用了com.alibaba.excel.ExcelWriter 的导出API,功能已经上线了使用了一段时间,一直没有问题,突然 PM 就反馈生产环境点击导出功能提示成功,但是下载中心没有生成下载任务;
紧着接着研发同学就开始排查问题,罗列出几个疑问点:
1.控制台接口响应显示 “操作成功”,但是异步下载中心并没有生成对应的下载任务;
2.ELK查看应用是否有报错日志,结果查询ERROR级别日志输出也没有发现异常;
3.导出功能代码明明做了 try…catch,为什么没有输出 ERROR 日志;
文章目录
💗💗💗您的 点赞、 收藏、 评论 是博 主输 出优 质文 章的 的动 力!!!💗💗💗
欢迎在评论区与博主沟通交流!!!大佬们关注我!种个草不亏!👇🏻 👇🏻 👇🏻
为什么报错NoClassDefFoundError?
带着以上疑问,在本地环境开始MOCK,果然程序报错了,Exception in thread “task-1” java.lang.NoClassDefFoundError: org/apache/poi/util/DefaultTempFileCreationStrategy。
等等,为什么报错抛出了个 Error?catch(Exception) 没作用呀…
通过本地 Debug 找到了报错原因:
mock业务代码
public class ExportW3ServiceImpl implements ExportW3Service {
/**
* 导出业务方法
*/
public void export(Supplier<Collection<?>> supplier, Class<?> cls, ExportW3TaskDto taskDto) {
try (ExcelWriter excelWriter = EasyExcel.write(fileName, cls).excelType(ExcelTypeEnum.XLSX).registerConverter(new LongStringConverter()).charset(Charset.forName("GBK")).build()) {
// ...省略
} catch (Exception e) {
log.error("--->exportDatabaseToFile export failure taskId:{} error:{}", taskId, e.getMessage());
return;
}
}
}
业务方法会调用 com.alibaba.excel.write.builder.ExcelWriterBuilder#build
方法:
public class ExcelWriterBuilder extends AbstractExcelWriterParameterBuilder<ExcelWriterBuilder, WriteWorkbook> {
public ExcelWriter build() {
return new ExcelWriter(writeWorkbook);
}
}
紧接着调用实例化 com.alibaba.excel.ExcelWriter#ExcelWriter
对象:
public class ExcelWriter implements Closeable {
public ExcelWriter(WriteWorkbook writeWorkbook) {
excelBuilder = new ExcelBuilderImpl(writeWorkbook);
}
}
com.alibaba.excel.ExcelWriter#ExcelWriter
的构造方法中 实例化 com.alibaba.excel.write.ExcelBuilderImpl#ExcelBuilderImpl
对象:
在 com.alibaba.excel.write.ExcelBuilderImpl
中可以看到使用了静态代码块,静态代码块中的内容会在类被加载(类初始化阶段)的时候运行
public class ExcelBuilderImpl implements ExcelBuilder {
static {
// Create temporary cache directory at initialization time to avoid POI concurrent write bugs
FileUtils.createPoiFilesDirectory();
}
public ExcelBuilderImpl(WriteWorkbook writeWorkbook) {
try {
context = new WriteContextImpl(writeWorkbook);
} catch (RuntimeException e) {
finishOnException();
throw e;
} catch (Throwable e) {
finishOnException();
throw new ExcelGenerateException(e);
}
}
}
继续 Debug 就可以看到报错的地方的,从下述图片中可以看到,底层抛出的异常其实是 java.lang.ClassNotFoundException: org.apache.poi.util.DefaultTempFileCreationStrategy,但实际控制台中打印的错误是:java.lang.NoClassDefFoundError: org/apache/poi/util/DefaultTempFileCreationStrategy。
在静态初始化时,某个类的静态初始化块中抛出异常,导致该类的加载中断,再次引用该类时会抛出NoClassDefFoundError
。
DefaultTempFileCreationStrategy 类去哪了?
功能上线快一年,一直没有问题,怎么突然后duang了?
控制台输出 java.lang.NoClassDefFoundError: org/apache/poi/util/DefaultTempFileCreationStrategy 时,第一反应就是去查看依赖,如下图所示,依赖中命名是存在该类的,但是为什么会提示找不到该类的呢?
接着又怀疑是不是有依赖冲突,真实运行时调用的并不是这个该依赖,
果然,目录存在两个不同版本的 poi 依赖,而 Maven 中依赖传递的版本由最近路径优先解析策略(Nearest-First Resolution)决定。也就是说,在依赖树中距离根项目最近的依赖版本优先。
经过核实,该项目应用中按照依赖层级,确实会先解析 poi:3.5 的这个版本。
问题解决
在对应的外部依赖中,把不需要的依赖全部排出
<dependency>
<groupId>com.xx.xx.config</groupId>
<artifactId>config-facade</artifactId>
<version>1.0.0-SNAPSHOT</version>
<exclusions>
<exclusion>
<groupId>*</groupId>
<artifactId>*</artifactId>
</exclusion>
</exclusions>
</dependency>
总结
- 引入外部依赖时,一定要检查一下是否会携带冗余依赖,冗余依赖可能会对当前应用造成影响;
- 设计项目层级时,要考虑把外部依赖做应用归类(收口),尽可能把外部依赖优先级调整至最低;
Maven 依赖传递原则(扩展知识,感兴趣可以继续阅读)
上面说到项目中依赖传递,这里就顺便介绍一下:
Maven 解析依赖项(dependencies)的顺序基于以下几个规则:
-
直接依赖的优先级(Direct Dependencies First)
- Maven会首先解析在项目的
pom.xml
文件中的直接依赖。直接依赖是指那些在<dependencies>
标签内直接声明的依赖。它们具有最高优先级。
- Maven会首先解析在项目的
-
依赖传递(Transitive Dependencies)
- 当直接依赖项本身也声明了依赖项时,这些被称为传递依赖。Maven会递归地解析这些传递依赖,确保所有必须的依赖都可以被加载。
-
最短路径优先(Nearest First Resolution)
- 如果有多个版本的同一个依赖项出现在依赖树中,Maven会选择版本最近的依赖项。依赖的“近”定义为从项目到依赖项路径上的最短路径。
-
首次声明优先(First Declared First)
- 在同一级别的依赖中,如果同一依赖项的多个版本被引入,Maven会优先选择第一次声明的版本。这通常出现在直接依赖和传递依赖之间。
-
依赖范围(Dependency Scope)
- 不同的依赖范围(如
compile
、provided
、runtime
、test
、system
)会影响依赖的解析顺序和可用性。例如:compile
:在编译、测试和运行阶段都可用(默认范围)。provided
:在编译和测试阶段可用,但运行时需要由JDK或容器提供。runtime
:在运行和测试阶段可用,但不用于编译。test
:仅在测试阶段可用。system
:需要本地存在的依赖,不从远程仓库下载。
- 不同的依赖范围(如
-
依赖管理(Dependency Management)
- 在父POM或其他继承的POM中可以使用
<dependencyManagement>
来集中管理依赖版本。子模块中实际的依赖声明会依据这些版本号,尽管这些声明本身不会引入新的依赖项。
- 在父POM或其他继承的POM中可以使用
-
插件依赖优先级
- 如果构建过程使用了Maven插件,这些插件也可能有自己的依赖项。Maven插件的依赖解析方式与项目依赖解析类似,但已经解析的依赖项优先级会高于尚未解析的。
示例及解析
以下是一个示例 pom.xml
,简要说明依赖解析的顺序:
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>example-project</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>5.2.0.RELEASE</version>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- 直接依赖 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>5.3.0.RELEASE</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>2.13.3</version>
</dependency>
<!-- 传递依赖 -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.10.0</version>
</dependency>
</dependencies>
</project>
在这个例子中,依赖解析顺序如下:
- 直接依赖:
spring-core
版本5.3.0.RELEASE
:因为其在<dependencies>
部分直接声明,因此具有最高优先级。log4j-api
版本2.13.3
:同样是直接依赖,优先级很高。jackson-databind
版本2.10.0
:直接依赖。
- 传递依赖:
- 如果
spring-core
、log4j-api
或jackson-databind
有自己的依赖项,Maven 会递归解析这些传递依赖。
- 如果
- 依赖管理:
- 如果在直接依赖中未声明
spring-core
的版本,Maven 会使用dependencyManagement
中声明的5.2.0.RELEASE
版本。
- 如果在直接依赖中未声明
总结
Maven在处理依赖时,会按照以下规则来读取和解析依赖:
- 直接依赖的优先级(Direct Dependencies First):
- 在
<dependencies>
部分直接声明的依赖具有最高优先级,会首先被解析。
- 在
- 传递依赖(Transitive Dependencies):
- 当直接依赖项自身有依赖时,这些传递依赖会被递归解析。版本号由最近的路径优先规则(Nearest First Resolution)决定。
- 最短路径优先(Nearest First Resolution):
- 在依赖树中,如果有多个版本的同一个依赖项,最近路径(从该项目到依赖项路径上的最短路径)会被优先选择。
- 首次声明优先(First Declared First):
- 在相同级别依赖中的多个版本冲突时,Maven会选择第一个声明的版本。这通常在直接依赖和传递依赖之间发生。
- 依赖范围(Dependency Scope):
- 依赖项的不同范围(如
compile
、provided
、runtime
、test
、system
)决定了它们在构建生命周期中的可用性和优先级。
- 依赖项的不同范围(如
- 依赖管理(Dependency Management):
- 使用
<dependencyManagement>
集中管理依赖版本。子模块中的依赖会使用父模块或继承层次中的依赖管理声明的版本号。
- 使用
- 插件依赖优先级:
- 构建过程中使用的Maven插件的依赖也会按照类似于项目依赖解析的规则进行处理,但已解析的依赖优先级会高于尚未解析的。
通过上述规则,Maven确保在构建时正确处理和解析依赖项,避免版本冲突和其他依赖问题。了解这些规则,可以更好地管理和优化项目的依赖,确保构建过程的顺利进行。