java 类加载基础
类加载器层次结构
java 虚拟机使用双亲委派模型(parent delegation model)来加载类:
bootstrap classloader (启动类加载器)
↓
extension/platform classloader (扩展/平台类加载器)
↓
application/system classloader (应用/系统类加载器)
双亲委派机制
双亲委派模型的工作原理:
- 当一个类加载器收到类加载请求时,它首先不会自己去尝试加载这个类
- 而是把这个请求委派给父类加载器去完成
- 每一个层次的类加载器都是如此
- 只有当父加载器反馈自己无法完成这个加载请求(找不到所需的类)时,子加载器才会尝试自己去加载
protected class<?> loadclass(string name, boolean resolve) {
synchronized (getclassloadinglock(name)) {
// 1. 检查是否已经加载
class<?> c = findloadedclass(name);
if (c == null) {
try {
if (parent != null) {
// 2. 委派给父类加载器
c = parent.loadclass(name, false);
} else {
c = findbootstrapclassornull(name);
}
} catch (classnotfoundexception e) {
// 父类加载器无法加载
}
if (c == null) {
// 3. 自己尝试加载
c = findclass(name);
}
}
if (resolve) {
resolveclass(c);
}
return c;
}
}spring boot 类加载架构
为什么需要特殊的类加载器?
spring boot 应用通常打包为可执行的 jar(fat jar 或 uber jar),这种 jar 包含了:
- 应用代码
- 所有依赖的第三方库
- spring boot 自身的类
传统的 jar 加载机制无法正确处理这种嵌套的 jar 结构,因此 spring boot 实现了自定义的类加载器。
spring boot 启动流程
java -jar app.jar
↓
jarlauncher.main()
↓
创建 launchedurlclassloader
↓
设置 contextclassloader
↓
调用应用程序的 main 方法
核心类
spring boot 类加载相关的核心类位于 org.springframework.boot.loader 包:
| 类名 | 作用 |
|---|---|
jarlauncher | 可执行 jar 的启动器 |
warlauncher | 可执行 war 的启动器 |
launchedurlclassloader | spring boot 自定义的类加载器 |
jarfile | 封装 jar 文件访问 |
jarfileregister | jar 文件注册表 |
fat jar 结构
目录结构
spring boot 可执行 jar 的内部结构:
app.jar ├── boot-inf/ │ ├── classes/ # 应用类文件 │ │ └── com/example/ │ │ └── application.class │ └── lib/ # 依赖库 │ ├── spring-boot-2.7.0.jar │ ├── spring-core-5.3.20.jar │ └── ... ├── meta-inf/ │ ├── manifest.mf # 清单文件 │ └── spring.factories # spring 配置 ├── org/springframework/boot/loader/ # spring boot loader 类 │ ├── jarlauncher.class │ ├── launchedurlclassloader.class │ └── ... └── [其他资源]
manifest.mf 关键配置
manifest-version: 1.0 start-class: com.example.application main-class: org.springframework.boot.loader.jarlauncher spring-boot-classes: boot-inf/classes/ spring-boot-lib: boot-inf/lib/ spring-boot-version: 2.7.0
关键属性说明:
main-class: 指向 spring boot 的启动器类start-class: 实际的应用程序主类spring-boot-classes: 应用类所在目录spring-boot-lib: 依赖库所在目录
launchedurlclassloader 详解
继承关系
java.lang.classloader
↓
java.net.urlclassloader
↓
org.springframework.boot.loader.launchedurlclassloader
核心特性
1. 打破双亲委派
launchedurlclassloader 在某些情况下会打破传统的双亲委派模型:
@override
protected class<?> loadclass(string name, boolean resolve) throws classnotfoundexception {
// 1. 检查是否已经加载
class<?> loadedclass = findloadedclass(name);
if (loadedclass == null) {
// 2. 对于某些包,优先从 boot-inf 加载
if (iseligibleforoverride(name)) {
try {
loadedclass = findclass(name);
if (resolve) {
resolveclass(loadedclass);
}
return loadedclass;
} catch (classnotfoundexception e) {
// 继续尝试父加载器
}
}
// 3. 使用标准的双亲委派
return super.loadclass(name, resolve);
}
return loadedclass;
}2. url 处理
launchedurlclassloader 能够处理嵌套 jar 的 url:
jar:file:/path/to/app.jar!/boot-inf/lib/spring-core-5.3.20.jar!/
这种 url 格式表示:
- 外层 jar:
file:/path/to/app.jar - 内层 jar:
boot-inf/lib/spring-core-5.3.20.jar
3. 资源加载
@override
public url findresource(string name) {
// 优先从 boot-inf/classes 查找
url url = findresourceinbootinf(name);
if (url != null) {
return url;
}
// 然后从 boot-inf/lib 查找
return findresourceindependencies(name);
}类加载优先级
spring boot 类加载器的加载优先级:
bootstrap classloader (jdk 核心类) launchedurlclassloader (boot-inf/classes) launchedurlclassloader (boot-inf/lib) extension/platform classloader application classloader (其他)
类加载委托模型
spring boot 的委托策略
spring boot 并非完全打破双亲委派,而是在特定场景下进行调整:
正常情况(遵循双亲委派): application classloader → launchedurlclassloader → bootstrap classloader 特殊情况(打破双亲委派): 对于 spring boot 应用类,launchedurlclassloader 优先加载
加载决策流程
收到类加载请求
↓
是否是 jdk 核心类?
↓ 是 → bootstrap classloader 加载
↓ 否
是否是 spring boot loader 类?
↓ 是 → bootstrap classloader 加载
↓ 否
是否在 boot-inf/classes 中?
↓ 是 → launchedurlclassloader 加载
↓ 否
是否在 boot-inf/lib 中?
↓ 是 → launchedurlclassloader 加载
↓ 否
委托给父类加载器
类隔离
spring boot 的类加载机制实现了类隔离:
- 应用类:
boot-inf/classes/ - 依赖类:
boot-inf/lib/ - 系统类:
rt.jar,jce.jar等
这种隔离确保了:
- 应用类不会被系统类覆盖
- 不同版本的依赖可以共存
- 类加载冲突最小化
常见问题与解决方案
1. classnotfoundexception
问题现象:
java.lang.classnotfoundexception: com.example.myclass
可能原因:
- 类文件不在
boot-inf/classes/中 - 依赖 jar 不在
boot-inf/lib/中 - manifest.mf 配置错误
解决方案:
<!-- maven 确保正确打包 -->
<plugin>
<groupid>org.springframework.boot</groupid>
<artifactid>spring-boot-maven-plugin</artifactid>
</plugin>2. noclassdeffounderror
问题现象:
java.lang.noclassdeffounderror: org/springframework/core/resolvabletypeprovider
可能原因:
- 依赖版本冲突
- 类加载顺序问题
- 传递依赖未正确包含
解决方案:
<!-- 排除冲突依赖 -->
<dependency>
<groupid>com.example</groupid>
<artifactid>some-lib</artifactid>
<exclusions>
<exclusion>
<groupid>org.springframework</groupid>
<artifactid>spring-core</artifactid>
</exclusion>
</exclusions>
</dependency>3. classcastexception
问题现象:
java.lang.classcastexception: com.example.myclass cannot be cast to com.example.myclass
可能原因:
- 同一个类被不同的类加载器加载
- 类加载器隔离导致类型不匹配
解决方案:
// 确保使用正确的类加载器
classloader classloader = thread.currentthread().getcontextclassloader();
class<?> clazz = classloader.loadclass("com.example.myclass");
4. nosuchmethoderror
问题现象:
java.lang.nosuchmethoderror: org.springframework.util.stringutils.isempty(ljava/lang/string;)z
可能原因:
- 依赖版本不匹配
- 方法签名在不同版本中发生变化
解决方案:
<!-- 使用 dependencymanagement 统一版本 -->
<dependencymanagement>
<dependencies>
<dependency>
<groupid>org.springframework</groupid>
<artifactid>spring-framework-bom</artifactid>
<version>5.3.20</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencymanagement>5. 资源文件找不到
问题现象:
inputstream is = getclass().getresourceasstream("/config.properties"); // 返回 null
可能原因:
- 资源文件打包位置不正确
- 使用了错误的类加载器
解决方案:
// 使用正确的加载方式
inputstream is = thread.currentthread()
.getcontextclassloader()
.getresourceasstream("config.properties");
// 或者使用 classpathresource
resource resource = new classpathresource("config.properties");
inputstream is = resource.getinputstream();最佳实践
1. 依赖管理
<!-- 使用 spring boot bom 统一版本 -->
<dependencymanagement>
<dependencies>
<dependency>
<groupid>org.springframework.boot</groupid>
<artifactid>spring-boot-dependencies</artifactid>
<version>2.7.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencymanagement>2. 避免类冲突
<!-- 排除不需要的传递依赖 -->
<dependency>
<groupid>org.springframework.boot</groupid>
<artifactid>spring-boot-starter-web</artifactid>
<exclusions>
<exclusion>
<groupid>org.springframework.boot</groupid>
<artifactid>spring-boot-starter-tomcat</artifactid>
</exclusion>
</exclusions>
</dependency>3. 自定义类加载器
public class customclassloader extends launchedurlclassloader {
@override
protected class<?> loadclass(string name, boolean resolve)
throws classnotfoundexception {
// 自定义加载逻辑
if (name.startswith("com.custom.")) {
class<?> clazz = findloadedclass(name);
if (clazz == null) {
clazz = findclass(name);
}
if (resolve) {
resolveclass(clazz);
}
return clazz;
}
return super.loadclass(name, resolve);
}
}4. 资源加载最佳实践
@service
public class resourceservice {
// 推荐:使用 resource 抽象
@value("classpath:config.properties")
private resource configresource;
public void loadconfig() throws ioexception {
properties props = new properties();
try (inputstream is = configresource.getinputstream()) {
props.load(is);
}
}
// 推荐:使用 classpathresource
public resource loadresource(string path) {
return new classpathresource(path);
}
}5. 调试类加载问题
@component
public class classloaderdebugger implements applicationlistener<applicationreadyevent> {
@override
public void onapplicationevent(applicationreadyevent event) {
classloader classloader = thread.currentthread().getcontextclassloader();
system.out.println("context classloader: " + classloader);
system.out.println("parent classloader: " + classloader.getparent());
// 打印类路径
if (classloader instanceof urlclassloader) {
url[] urls = ((urlclassloader) classloader).geturls();
system.out.println("classpath urls:");
for (url url : urls) {
system.out.println(" " + url);
}
}
}
}6. jvm 参数配置
# 启用类加载详细日志 java -verbose:class -jar app.jar # 设置类加载器调试 java -djava.security.debug=classloader -jar app.jar # 指定自定义类加载器 java -djava.system.class.loader=com.example.customclassloader -jar app.jar
总结
spring boot 的类加载机制是其可执行 jar 功能的核心:
- launchedurlclassloader 是核心类加载器,能够处理嵌套 jar 结构
- fat jar 结构 将应用类和依赖类组织在
boot-inf目录下 - 类加载策略 在保持双亲委派的同时,针对 spring boot 应用进行了优化
- 类隔离 确保了应用类和依赖类的正确加载顺序
理解 spring boot 的类加载机制有助于:
- 解决类加载相关的各种问题
- 优化应用启动性能
- 实现更复杂的应用架构
- 进行有效的故障排查
到此这篇关于spring boot 类加载流程分析的文章就介绍到这了,更多相关springboot类加载内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论