[db:标题]
摘要:SpringBoot的类加载和传统的双亲委派有什么区别? SpringBoot如何按顺序实例化Bean
前言
在SpringBoot中,类加载机制与Java的传统双亲委派类加载机制是有一定区别。主要体现在自定义类加载器与fat jar(可执行jar)的加载方式上。
Java的传统双亲委派模型
Java传统类加载机制,遵循双亲委派模型,核心规则:类加载请求优先由父类加载器处理,只有父加载器无法加载时才由子加载器尝试。
1、JDK 1.8及更早版本采用如下层级结构:
2、从 JDK 9 引入模块系统开始,是这样的层级结构
这样设计的主要目的是为了,避免重复加载核心类(如java.lang.String),确保安全性(防止用户篡改核心类)。
SpringBoot的类加载器改造
改造原因
SpringBoot通过自定义类加载器LaunchedURLClassLoader打破了传统双亲委派的严格层级,主要解决fat jar中嵌套jar的加载问题。
在SpringBoot中,使用打包构建工具时,无论是Maven还是Gradle,在lib/目录中的第三方依赖是以JAR形式打入项目主JAR内的,默认会生成一个包含所有依赖项的fat jar。
目录结构示例如下:
mySpringBootApp.jar
├── BOOT-INF
│ ├── classes(用户代码)
│ └── lib(依赖的第三方jar)
└── org.springframework.boot.loader
传统的Java类加载机制,Application ClassLoader只能从外部classpath加载类,无法直接加载JAR包内嵌的其他JAR(fat jar),因此SpringBoot加入了自定义的类加载器。
主要做了哪些改造
SpringBoot使用LaunchedURLClassLoader(继承自URLClassLoader)替代了ApplicationClassLoader,通过运行时动态生成jar路径的URL来加载嵌套jar。
LaunchedURLClassLoader会先加载BOOT-INF/classes目录下的应用类(优先于JDK类)。
再加载BOOT-INF/lib/目录下的依赖JAR,LaunchedURLClassLoader会解析BOOT-INF/lib/下的每个jar,将其URL添加到类路径中。
最后再交给父类加载器(即ApplicationClassLoader)。
总结一下:为了加载嵌套在主JAR内部的fat jar,SpringBoot在类加载流程上做了改造,增加了LaunchedURLClassLoader类加载器,并且会先尝试加载自身的类和依赖JAR,找不到要加载的类时,才交给父类加载器,从而对传统的双亲委派模型进行了改造。
注意,LaunchedURLClassLoader 仅对 BOOT-INF/classes 和 BOOT-INF/lib 下的类采用“子加载器优先”策略,核心类库仍严格遵循双亲委派,因此不会破坏 JDK 的安全模型。
扩展知识
在使用SpringBoot进行开发项目时,SpringBoot官方推荐我们使用热部署的方式是使用 spring-boot-devtools 模块。
其实SpringBoot的热部署并不是真正意义上的“热替换”,而是通过 双类加载器机制 实现的“快速重启”。
SpringBoot的“热部署”主要实现原理如下:
双 ClassLoader 架构:
Base ClassLoader:加载第三方 jar 包(不会频繁变动)
Restart ClassLoader:加载开发者自己写的类(会频繁变动)
文件变化监听机制
DevTools 启动一个后台线程,监听 classpath 下 .class 文件的变化
一旦检测到变化,丢弃旧的 Restart ClassLoader
重新创建一个新的 Restart ClassLoader,加载更新后的类
然后通过反射重新调用 main() 方法,实现应用重启
由于不需要重新加载第三方类(Base ClassLoader 不变),也不需要重新初始化整个 Spring 容器,重启过程只涉及开发者代码部分,节省大量时间。
虽然叫“热部署”,但本质上是“部分重启”,不是真正的 JVM 热替换(如 JRebel 那样)
SpringBoot如何指定在其他Bean之前实例化指定的Bean
Bean 实例化/初始化顺序其实就是指“哪个 Bean 先被 new、哪个 @PostConstruct 先跑”。
目前有6种方式可以实现按照一定顺序进行实例化Bean。
1、构造器依赖(最稳,无侵入)
Spring 保证一个 Bean 实例化之前,它依赖的 Bean 必须已实例化。
