本文记录了我与DeepSeek的一次深度对话,通过不断追问,从零开始理解 Spring Boot
deduceFromClasspath()方法的完整学习历程。
一、起点:一个看似简单的方法
在一次阅读 Spring Boot 源码时,我遇到了这样一段代码:
java
this.webApplicationType = WebApplicationType.deduceFromClasspath();
这段代码的作用 :在 Spring Boot 应用启动时,通过检查 ClassPath 中的类依赖,自动推断当前应用是 Servlet Web 应用、Reactive Web 应用还是非 Web 应用,并将结果赋值给
webApplicationType属性,后续启动流程会根据这个类型决定启动哪种 Web 容器。
为了深入理解这行代码背后的一系列行为,我向DeepSeek发起了一连串的追问,带着这些问题,我开始了深度探索:
deduceFromClasspath()做了什么?是怎么推断应用类型的?它到底检查了什么?- ClassPath 是什么?没有参数是怎么获取到 ClassPath?
- ClassLoader 是什么?是否每个类都有自己的 ClassLoader?
- 类的加载流程是什么样的(类缓存和双亲委派机制)?
二、核心源码分析
1. 入口方法:WebApplicationType 枚举的 deduceFromClasspath() 方法
java
package org.springframework.boot;
import org.springframework.util.ClassUtils;
public enum WebApplicationType {
NONE, // 非 Web 应用
SERVLET, // 传统 Servlet Web 应用(Spring MVC)
REACTIVE; // 响应式 Web 应用(Spring WebFlux)
// Servlet 环境的两个必要指示类
private static final String[] SERVLET_INDICATOR_CLASSES = new String[]{"javax.servlet.Servlet", "org.springframework.web.context.ConfigurableWebApplicationContext"};
// Spring MVC 的核心入口类
private static final String WEBMVC_INDICATOR_CLASS = "org.springframework.web.servlet.DispatcherServlet";
// Spring WebFlux 的核心入口类
private static final String WEBFLUX_INDICATOR_CLASS = "org.springframework.web.reactive.DispatcherHandler";
// Jersey 框架的核心类
private static final String JERSEY_INDICATOR_CLASS = "org.glassfish.jersey.servlet.ServletContainer";
// 核心推断方法
static WebApplicationType deduceFromClasspath() {
// 第一步:检查是否为 Reactive 环境
if (ClassUtils.isPresent("org.springframework.web.reactive.DispatcherHandler", (ClassLoader)null) && !ClassUtils.isPresent("org.springframework.web.servlet.DispatcherServlet", (ClassLoader)null) && !ClassUtils.isPresent("org.glassfish.jersey.servlet.ServletContainer", (ClassLoader)null)) {
return REACTIVE;
} else {
// 第二步:检查是否为 Servlet 环境
for(String className : SERVLET_INDICATOR_CLASSES) {
if (!ClassUtils.isPresent(className, (ClassLoader)null)) {
return NONE;
}
}
return SERVLET;
}
}
}
这段代码的作用 :定义了一个枚举类型
WebApplicationType,包含三种应用类型。deduceFromClasspath()方法是核心推断逻辑------先检查是否满足 Reactive 条件(有 WebFlux 且没有 MVC 和 Jersey),如果满足则返回REACTIVE;否则检查两个 Servlet 指示类是否都存在,如果都存在则返回SERVLET,否则返回NONE。这种设计体现了"约定优于配置"的思想,开发者只需引入对应的 Starter 依赖,框架就能自动识别应用类型。
2. 推断逻辑流程图
java
开始
│
▼
类路径有 DispatcherHandler (WebFlux) ?
│
├── 是 ──▶ 类路径没有 DispatcherServlet 且 没有 Jersey?
│ │
│ ├── 是 ──▶ 返回 REACTIVE
│ │
│ └── 否 ──▶ 进入 Servlet 检查(被 MVC 或 Jersey 否决)
│
└── 否 ──▶ 进入 Servlet 检查
│
▼
检查两个 Servlet 指示类是否存在
(javax.servlet.Servlet 和 ConfigurableWebApplicationContext)
│
├── 都存在 ──▶ 返回 SERVLET
│
└── 至少一个不存在 ──▶ 返回 NONE
3. 关键辅助方法:ClassUtils.isPresent()
在上面的 deduceFromClasspath() 方法中,判断类是否存在都依赖于 ClassUtils.isPresent()。来看它的真实源码:
java
// ClassUtils.java - Spring 框架的工具类
public static boolean isPresent(String className, @Nullable ClassLoader classLoader) {
try {
// 尝试加载指定的类
forName(className, classLoader);
return true; // 加载成功,类存在
} catch (IllegalAccessError err) {
// 访问权限错误,视为严重错误抛出
throw new IllegalStateException("Readability mismatch in inheritance hierarchy of class [" + className + "]: " + err.getMessage(), err);
} catch (Throwable var4) {
// 其他任何异常(如 ClassNotFoundException)都视为类不存在
return false;
}
}
这段代码的作用 :
isPresent()是对外提供的"探测"方法,它通过调用内部的forName()尝试加载目标类,如果加载成功则返回true,如果失败则返回false。它捕获了ClassNotFoundException等异常,将其转换为布尔值返回,让调用者无需处理烦人的异常,只需要关心"这个类存在吗?"这个简单问题。
isPresent() 方法内部调用了一个重载的 forName() 方法,这是真正执行类加载逻辑的地方:
java
// ClassUtils.java - 类加载的核心实现
public static Class<?> forName(String name, @Nullable ClassLoader classLoader)
throws ClassNotFoundException, LinkageError {
Assert.notNull(name, "Name must not be null");
// 第一步:处理基本类型(如 int、long 等)
Class<?> clazz = resolvePrimitiveClassName(name);
if (clazz == null) {
// 第二步:从 Spring 缓存中查找
clazz = (Class) commonClassCache.get(name);
}
if (clazz != null) {
return clazz; // 缓存命中,直接返回
}
// 第三步:处理数组类型(如 String[]、[Ljava.lang.String; 等)
// (数组处理逻辑与核心流程关系不大,此处省略)
// 第四步:获取实际使用的 ClassLoader
ClassLoader clToUse = classLoader;
if (classLoader == null) {
// ⭐ 关键:如果传入 null,使用默认的 ClassLoader
clToUse = getDefaultClassLoader();
}
// 第五步:使用 JVM 的 Class.forName() 真正加载类
try {
// 参数说明:name=类全限定名, false=不执行初始化, clToUse=使用的类加载器
return Class.forName(name, false, clToUse);
} catch (ClassNotFoundException ex) {
// 第六步:处理内部类场景(如将 "Outer.Inner" 尝试为 "Outer$Inner")
int lastDotIndex = name.lastIndexOf('.');
if (lastDotIndex != -1) {
String nestedClassName = name.substring(0, lastDotIndex) + '$' + name.substring(lastDotIndex + 1);
try {
return Class.forName(nestedClassName, false, clToUse);
} catch (ClassNotFoundException var8) {
// 内部类格式也找不到,继续抛出原始异常
}
}
throw ex; // 真的找不到,抛出 ClassNotFoundException
}
}
这段代码的作用 :
forName()是真正干活的方法,它按照以下优先级尝试加载一个类:① 基本类型 → ② Spring 缓存 → ③ 数组类型 → ④ 通过 ClassLoader 加载。最关键的是第四步 :当传入的classLoader参数为null时,会自动获取默认的 ClassLoader(即 Application ClassLoader),而这个 ClassLoader 的搜索范围正是我们之后要详细讨论的 ClassPath 。此外,方法还做了两件贴心的事:一是缓存了加载过的类(commonClassCache)以提高性能;二是自动处理内部类的命名格式(将Outer.Inner尝试为Outer$Inner)。
关于 ClassLoader 传 null 的说明:
java
// 在 deduceFromClasspath() 中调用 isPresent() 时,第二个参数都传 null
ClassUtils.isPresent(WEBFLUX_INDICATOR_CLASS, null)
传 null 的实际效果是:forName() 方法内部会调用 getDefaultClassLoader() 获取默认的类加载器。这个默认类加载器在 Spring Boot 应用中就是 Application ClassLoader ,它的搜索范围正好是应用的 ClassPath 。因此,isPresent() 方法做到了:
- 表面上:只需传入类名,无需额外参数
- 实际上:通过默认 ClassLoader 自动获取 ClassPath,完成类的查找
4. 获取默认 ClassLoader 的逻辑
java
@Nullable
public static ClassLoader getDefaultClassLoader() {
ClassLoader cl = null;
try {
// 1. 优先获取当前线程的 ContextClassLoader
cl = Thread.currentThread().getContextClassLoader();
} catch (Throwable var3) {
// 忽略
}
if (cl == null) {
// 2. 使用当前类的 ClassLoader
cl = ClassUtils.class.getClassLoader();
if (cl == null) {
try {
// 3. 最后使用系统 ClassLoader
cl = ClassLoader.getSystemClassLoader();
} catch (Throwable var2) {
// 忽略
}
}
}
return cl;
}
这段代码的作用:按照优先级顺序获取一个可用的 ClassLoader。它首先尝试获取当前线程的上下文 ClassLoader(这是最灵活的方式,尤其在 Web 容器等复杂环境中),如果获取不到则回退到 Spring 工具类自身的 ClassLoader,最后才尝试获取系统 ClassLoader。这种多级回退机制确保了在各种运行环境下都能获得有效的 ClassLoader,体现了 Spring 框架设计的健壮性。
至此,我们完整追踪了从 deduceFromClasspath() → isPresent() → forName() → getDefaultClassLoader() 的整个调用链。但还有一个核心问题没解决:ClassPath 到底是什么?ClassLoader 又是如何通过它找到类的?
三、核心概念深入理解
1. ClassPath(类路径)
ClassPath 是什么? ClassPath 是一个集合 ,存放着所有可能找到 .class 文件的位置。
java
ClassPath(类路径集合)
├── 位置1: /my-project/target/classes/
│ └── 你写的代码编译后的 .class 文件
│ ├── com/example/MyApp.class
│ └── com/example/controller/UserController.class
│
├── 位置2: /my-project/lib/spring-webmvc-6.0.jar
│ └── jar 包里的 .class 文件
│ ├── org/springframework/web/servlet/DispatcherServlet.class
│ └── org/springframework/web/context/ConfigurableWebApplicationContext.class
│
└── 位置3: /my-project/lib/spring-webflux-6.0.jar
└── jar 包里的 .class 文件
└── org/springframework/web/reactive/DispatcherHandler.class
简单理解 :ClassPath 就像一张"地图",告诉 Java 程序去哪里找编译后的
.class文件。它包含了你项目自己的代码编译结果,以及所有通过 Maven/Gradle 引入的第三方 jar 包。
2. ClassLoader(类加载器)
ClassLoader 是什么? ClassLoader 是一个 Java 对象(java.lang.ClassLoader)。这里需要注意一点,不是否每个类都有自己的 ClassLoader,每个类只是记录了自己是被哪个 ClassLoader 加载的(可通过 xxx.class.getClassLoader() 查看),但一个 ClassLoader 可以加载成千上万个类。ClassLoader 的它的核心职责是:
- 查找 :根据 ClassPath 找到
.class二进制文件 - 加载 :将二进制文件转换为 JVM 内存中的
Class对象 - 缓存 :将加载过的
Class对象存入缓存,下次直接使用
简单理解:如果把 ClassPath 比作"地图",那 ClassLoader 就是拿着地图去取货的"快递员",它的工作就是根据地图找到文件,然后送到 JVM 内存中。
用一段代码验证缓存机制:
java
ClassLoader loader = ClassLoader.getSystemClassLoader();
// 第一次加载:去 ClassPath 找文件,读进来,放入缓存
Class<?> clazz1 = loader.loadClass("org.springframework.web.servlet.DispatcherServlet");
// 第二次加载:直接从缓存返回,不会重新读文件
Class<?> clazz2 = loader.loadClass("org.springframework.web.servlet.DispatcherServlet");
// 输出 true,证明是同一个对象
System.out.println(clazz1 == clazz2); // true
这段代码的作用 :验证 ClassLoader 的缓存机制。通过两次加载同一个类并比较它们是否相等,证明第二次加载时 JVM 没有重新去读取
.class文件,而是直接从缓存中返回了之前加载过的Class对象。这是 Java 类加载机制的一个重要性能优化。
3. ClassLoader 的层级结构
JVM 中有三个主要的 ClassLoader,形成父子层级关系:
java
Bootstrap ClassLoader (启动类加载器,C++ 实现)
↑ 父级
├── 搜索范围:rt.jar(JDK 核心类,如 java.lang.String)
│
Extension ClassLoader (扩展类加载器,Java 实现)
↑ 父级
├── 搜索范围:lib/ext 目录(Java 扩展库)
│
Application ClassLoader (应用类加载器,Java 实现)
├── 搜索范围:ClassPath(你的项目代码 + 所有第三方依赖)
└── 这就是 getSystemClassLoader() 返回的那个
简单理解:三个 ClassLoader 形成了上下级关系,每个都有自己的"管辖范围"------Bootstrap 管 JDK 核心类,Extension 管扩展库,Application 管项目代码和第三方依赖。
4. 完整的双亲委派流程
ClassUtils.isPresent("org.springframework.web.servlet.DispatcherServlet", null) 被执行时,结合我们前面分析的源码,完整的流程如下:
java
1. AppClassLoader 收到加载请求
│
▼
2. AppClassLoader 检查自己的缓存 (Map<String, Class>)
→ 如果有,直接返回 ✅
→ 如果没有,向上委托给 ExtClassLoader
│
▼
3. ExtClassLoader 检查自己的缓存
→ 如果有,直接返回 ✅
→ 如果没有,继续向上委托给 BootstrapClassLoader
│
▼
4. BootstrapClassLoader 检查自己的缓存
→ 如果有,直接返回 ✅
→ 如果没有,在自己的搜索范围(rt.jar)里查找
│
├── 找到了 → 加载 → 存入缓存 → 返回 ✅
│
└── 找不到 → 告诉 ExtClassLoader:"我没有"
│
▼
5. ExtClassLoader 在自己的搜索范围(lib/ext)里查找
│
├── 找到了 → 加载 → 存入缓存 → 返回 ✅
│
└── 找不到 → 告诉 AppClassLoader:"我也没有"
│
▼
6. AppClassLoader 在自己的搜索范围(ClassPath)里查找
│
├── 找到了(在 spring-webmvc.jar 里)→ 加载 → 存入缓存 → 返回 ✅
│
└── 找不到 → 抛出 ClassNotFoundException ❌
这段流程的关键点 :每一步在查找之前都会先检查缓存,避免重复加载同一个类。这种"向上委托查找,向下委托加载"的机制被称为双亲委派模型,它是 Java 类加载机制的核心设计,保证了核心类库的安全性和类加载的唯一性。
5. 完整调用链回顾
现在我们可以把整个调用链串起来了:
java
deduceFromClasspath()
│
▼ 调用
ClassUtils.isPresent(className, null)
│
▼ 调用
ClassUtils.forName(className, null)
│
▼ 当 classLoader == null 时
ClassUtils.getDefaultClassLoader()
│
▼ 返回
Application ClassLoader
│
▼ 其搜索范围为
ClassPath(包含所有依赖 jar 包和编译后的类)
│
▼ 在其中查找
目标类(如 DispatcherServlet)
这正是我们之前讨论的完整链条:传 null → 获取 Application ClassLoader → Application ClassLoader 的搜索范围是 ClassPath → 在 ClassPath 中查找目标类。这也就是为什么
deduceFromClasspath()不需要传参,却能知道 ClassPath 里有什么的完整答案。
四、为什么这样设计?(设计哲学)
1. 为什么用 ClassPath 探测而不是配置文件?
Spring Boot 的核心哲学是约定优于配置(Convention over Configuration):
- 开发者不需要在
application.properties中配置spring.web.application-type - 只需引入对应的 Starter 依赖,框架自动识别
设计意图:让开发者专注于业务代码,框架负责"猜"你想做什么,减少重复的配置工作。
2. 为什么 MVC 优先于 Reactive?
- 向下兼容:Spring MVC 生态更成熟,用户基数更大
- 避免意外:如果默认选择 WebFlux,用户可能无意识引入 MVC 依赖(通过传递依赖)导致行为异常
- 显式优于隐式:真正需要 WebFlux 的用户应该确保 ClassPath 中没有 MVC 干扰
设计意图:在不确定性面前选择更保守、更安全的方案,保证大多数已有项目的平稳运行。
3. 为什么 Jersey 会否决 Reactive?
Jersey 是 JAX-RS 的参考实现,虽然它运行在 Servlet 容器上,但 Spring Boot 不认为它是标准的 Spring Servlet 应用。为了避免误判,Jersey 的存在会否决 Reactive,但又不足以成为 Servlet(缺少 Spring MVC 的 Context 类),最终返回 NONE。
设计意图:保持判断逻辑的精确性,避免将 Jersey 用户错误地归类为 Reactive 应用,导致启动不兼容的 Web 容器。
五、结语
这次学习经历让我深刻体会到,技术学习不在于记住多少结论,而在于理解结论背后的"为什么"。每一个看似简单的方法调用,背后都可能隐藏着一整套精巧的设计思想。希望这篇文章能帮助更多人理解 Spring Boot 的设计之美。
如果本文有遗漏或不准确之处,欢迎指正和补充。 🙏