Wrayの知识库 Wrayの知识库
首页
  • Java 基础
  • Java 集合
  • Java 并发
  • Java IO
  • JVM
  • Spring Framework
  • Spring Boot
  • Spring Cloud
  • Spring Security
  • MySQL
  • Redis
  • 计算机基础
  • 操作系统原理
  • Linux
  • MacOS
  • Windows
  • 系统工程与研究专题
  • AI 基础
  • 大模型基础
  • Prompt 工程
  • RAG 检索增强生成
  • Agent 智能体
  • AI 应用开发
  • AI 工程化
  • AI 安全与治理
  • AI 面试与设计题
  • 纸质书
  • 电子书
  • 学习课程
疑难杂症
GitHub (opens new window)
首页
  • Java 基础
  • Java 集合
  • Java 并发
  • Java IO
  • JVM
  • Spring Framework
  • Spring Boot
  • Spring Cloud
  • Spring Security
  • MySQL
  • Redis
  • 计算机基础
  • 操作系统原理
  • Linux
  • MacOS
  • Windows
  • 系统工程与研究专题
  • AI 基础
  • 大模型基础
  • Prompt 工程
  • RAG 检索增强生成
  • Agent 智能体
  • AI 应用开发
  • AI 工程化
  • AI 安全与治理
  • AI 面试与设计题
  • 纸质书
  • 电子书
  • 学习课程
疑难杂症
GitHub (opens new window)
  • Java章节编写规范
  • Java基础

  • Java集合

  • Java并发

  • Java IO

  • JVM

    • JVM概述
    • JVM基础

    • 类加载机制

      • 类的生命周期
      • 类加载器
      • 双亲委派模型
      • 类初始化顺序
      • 类加载问题与SPI
        • 1. ClassNotFoundException
        • 2. NoClassDefFoundError
        • 3. ClassCastException: A cannot be cast to A
        • 4. SPI 是什么
        • 5. SPI 与线程上下文类加载器
        • 6. 排查类加载问题的方法
        • 7. SPI 加载流程图
        • 8. JDBC 驱动为什么能自动发现
        • 9. 类加载排查清单
        • 10. 开发中常见问题
          • 10.1 NoClassDefFoundError 不一定是类不存在
          • 10.2 SPI 文件写错很难发现
        • 11. Tips 快问快答
        • 12. 总结
    • 运行时内存

    • 垃圾回收

    • 执行引擎与优化

    • 工具与问题排查

    • 调优实践与面试

目录

类加载问题与SPI

类加载问题在业务开发中很常见,尤其是在大型项目、应用服务器、插件系统、微服务依赖复杂的场景中。SPI 则是理解“为什么有时要打破双亲委派”的经典例子。

# 1. ClassNotFoundException

ClassNotFoundException 是受检异常,通常发生在显式加载类时。

Class.forName("com.example.User");

如果 classpath 中没有这个类,就会抛出该异常。

常见原因:

  • 依赖没有引入。
  • 运行环境和编译环境不一致。
  • 打包遗漏。
  • 类名写错。
  • 使用了错误的类加载器。

# 2. NoClassDefFoundError

NoClassDefFoundError 是 Error,通常表示编译时存在,运行时找不到或初始化失败。

常见场景:

  • 编译时依赖存在,运行时依赖缺失。
  • 类静态初始化失败。
  • 依赖版本冲突。

它和 ClassNotFoundException 的区别是:前者常发生在 JVM 被动解析依赖时,后者常发生在主动按名称加载时。

# 3. ClassCastException: A cannot be cast to A

这种看似荒诞的错误通常来自类加载器隔离。

com.example.User cannot be cast to com.example.User

原因是两个 User 类由不同类加载器加载。对 JVM 来说,它们不是同一个类型。

常见于:

  • 插件系统。
  • Web 容器。
  • 热部署。
  • 多版本依赖隔离。

# 4. SPI 是什么

SPI(Service Provider Interface)是一种服务发现机制。

典型使用方式:

ServiceLoader<MyService> loader = ServiceLoader.load(MyService.class);
for (MyService service : loader) {
    service.execute();
}

服务提供方在资源目录中声明实现类:

META-INF/services/com.example.MyService

文件内容:

com.example.MyServiceImpl

# 5. SPI 与线程上下文类加载器

SPI 的接口可能由核心类库加载,但实现类在应用 classpath 中。

如果完全使用父加载器向下加载,核心类加载器无法看到应用类路径中的实现类。

因此 Java 引入线程上下文类加载器:

Thread.currentThread().getContextClassLoader();

它允许父层代码反向使用应用层类加载器加载服务实现。

JDBC 驱动加载就是典型例子。

# 6. 排查类加载问题的方法

常用方法:

  • 查看依赖树。
  • 使用 -verbose:class 或统一日志查看类加载。
  • 打印 clazz.getClassLoader()。
  • 检查 fat jar 或部署包内容。
  • 检查容器类加载隔离规则。
  • 排查同名不同版本依赖。

示例:

System.out.println(User.class.getClassLoader());
System.out.println(Thread.currentThread().getContextClassLoader());

# 7. SPI 加载流程图

调用方代码
  ServiceLoader.load(接口)
        │
        ▼
读取 META-INF/services/接口全限定名
        │
        ▼
得到实现类全限定名
        │
        ▼
使用线程上下文类加载器加载实现类
        │
        ▼
创建实现类实例并返回

SPI 的关键是:接口和实现可以来自不同层级。父层框架定义接口,应用层提供实现,线程上下文类加载器让父层代码能“向下”看到应用层实现。

# 8. JDBC 驱动为什么能自动发现

现代 JDBC 驱动 jar 中通常包含:

META-INF/services/java.sql.Driver

内容类似:

com.mysql.cj.jdbc.Driver

DriverManager 通过 SPI 发现并加载驱动。这里就涉及父层 Java SQL API 和应用 classpath 中具体驱动实现之间的关系。

# 9. 类加载排查清单

遇到类加载问题,可以按下面顺序排查:

步骤 要看什么
1 报错是 ClassNotFoundException、NoClassDefFoundError 还是 LinkageError
2 编译依赖和运行依赖是否一致
3 最终 jar/war 中是否真的包含目标类
4 是否有多个版本依赖
5 目标类由哪个 ClassLoader 加载
6 是否存在静态初始化失败
7 容器或插件系统是否做了类隔离

# 10. 开发中常见问题

# 10.1 NoClassDefFoundError 不一定是类不存在

类静态初始化失败后,再次使用也可能表现为 NoClassDefFoundError。

所以要回看第一次异常栈,找到最早的初始化失败原因。

# 10.2 SPI 文件写错很难发现

SPI 文件常见错误:

  • 文件路径写错。
  • 文件名不是接口全限定名。
  • 实现类名写错。
  • 实现类没有无参构造。
  • 实现类依赖缺失。

# 11. Tips 快问快答

Q:SPI 为什么要用线程上下文类加载器?

A:因为父层框架代码需要加载应用层实现,普通双亲委派方向做不到,线程上下文类加载器提供了反向加载入口。

Q:ClassNotFoundException 和 NoClassDefFoundError 最大区别是什么?

A:前者通常是主动按名称加载失败,后者通常是运行时解析或初始化依赖失败。

Q:怎么打印类由谁加载?

A:SomeClass.class.getClassLoader()。

Q:为什么插件系统容易出现类冲突?

A:插件和主应用可能持有不同版本依赖,需要合理设计父子加载器和依赖隔离边界。

# 12. 总结

类加载问题往往不是“类真的不存在”这么简单,而是依赖、版本、加载器、初始化和容器隔离共同作用的结果。SPI 是理解线程上下文类加载器和双亲委派调整的最佳入口。

上次更新: 2026/06/24, 16:03:01
类初始化顺序
Java堆

← 类初始化顺序 Java堆→

Copyright © 2023-2026 Wray | 鄂ICP备2024050235号-1
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式