Java 使用EasyExcel导出报错,NoClassDefFoundError: org/apache/poi/util/DefaultTempFileCreationStrategy

之前在项目中使用了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>

总结

  1. 引入外部依赖时,一定要检查一下是否会携带冗余依赖,冗余依赖可能会对当前应用造成影响;
  2. 设计项目层级时,要考虑把外部依赖做应用归类(收口),尽可能把外部依赖优先级调整至最低;

Maven 依赖传递原则(扩展知识,感兴趣可以继续阅读)

上面说到项目中依赖传递,这里就顺便介绍一下:

Maven 解析依赖项(dependencies)的顺序基于以下几个规则:

  1. 直接依赖的优先级(Direct Dependencies First)

    • Maven会首先解析在项目的pom.xml文件中的直接依赖。直接依赖是指那些在<dependencies>标签内直接声明的依赖。它们具有最高优先级。
  2. 依赖传递(Transitive Dependencies)

    • 当直接依赖项本身也声明了依赖项时,这些被称为传递依赖。Maven会递归地解析这些传递依赖,确保所有必须的依赖都可以被加载。
  3. 最短路径优先(Nearest First Resolution)

    • 如果有多个版本的同一个依赖项出现在依赖树中,Maven会选择版本最近的依赖项。依赖的“近”定义为从项目到依赖项路径上的最短路径。
  4. 首次声明优先(First Declared First)

    • 在同一级别的依赖中,如果同一依赖项的多个版本被引入,Maven会优先选择第一次声明的版本。这通常出现在直接依赖和传递依赖之间。
  5. 依赖范围(Dependency Scope)

    • 不同的依赖范围(如compileprovidedruntimetestsystem)会影响依赖的解析顺序和可用性。例如:
      • compile:在编译、测试和运行阶段都可用(默认范围)。
      • provided:在编译和测试阶段可用,但运行时需要由JDK或容器提供。
      • runtime:在运行和测试阶段可用,但不用于编译。
      • test:仅在测试阶段可用。
      • system:需要本地存在的依赖,不从远程仓库下载。
  6. 依赖管理(Dependency Management)

    • 在父POM或其他继承的POM中可以使用<dependencyManagement>来集中管理依赖版本。子模块中实际的依赖声明会依据这些版本号,尽管这些声明本身不会引入新的依赖项。
  7. 插件依赖优先级

    • 如果构建过程使用了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>

在这个例子中,依赖解析顺序如下:

  1. 直接依赖
    • spring-core 版本 5.3.0.RELEASE:因为其在<dependencies>部分直接声明,因此具有最高优先级。
    • log4j-api 版本 2.13.3:同样是直接依赖,优先级很高。
    • jackson-databind 版本 2.10.0:直接依赖。
  2. 传递依赖
    • 如果 spring-corelog4j-apijackson-databind 有自己的依赖项,Maven 会递归解析这些传递依赖。
  3. 依赖管理
    • 如果在直接依赖中未声明 spring-core 的版本,Maven 会使用 dependencyManagement 中声明的 5.2.0.RELEASE 版本。

总结

Maven在处理依赖时,会按照以下规则来读取和解析依赖:

  1. 直接依赖的优先级(Direct Dependencies First)
    • <dependencies> 部分直接声明的依赖具有最高优先级,会首先被解析。
  2. 传递依赖(Transitive Dependencies)
    • 当直接依赖项自身有依赖时,这些传递依赖会被递归解析。版本号由最近的路径优先规则(Nearest First Resolution)决定。
  3. 最短路径优先(Nearest First Resolution)
    • 在依赖树中,如果有多个版本的同一个依赖项,最近路径(从该项目到依赖项路径上的最短路径)会被优先选择。
  4. 首次声明优先(First Declared First)
    • 在相同级别依赖中的多个版本冲突时,Maven会选择第一个声明的版本。这通常在直接依赖和传递依赖之间发生。
  5. 依赖范围(Dependency Scope)
    • 依赖项的不同范围(如compileprovidedruntimetestsystem)决定了它们在构建生命周期中的可用性和优先级。
  6. 依赖管理(Dependency Management)
    • 使用<dependencyManagement>集中管理依赖版本。子模块中的依赖会使用父模块或继承层次中的依赖管理声明的版本号。
  7. 插件依赖优先级
    • 构建过程中使用的Maven插件的依赖也会按照类似于项目依赖解析的规则进行处理,但已解析的依赖优先级会高于尚未解析的。

通过上述规则,Maven确保在构建时正确处理和解析依赖项,避免版本冲突和其他依赖问题。了解这些规则,可以更好地管理和优化项目的依赖,确保构建过程的顺利进行。


感 谢 各 位 大 佬 的 阅 读,随 手 点 赞,日 薪 过 万~! !!
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

zhuzicc

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值