军临世家 發表於 2025-11-4 16:32:00

技术面:SpringBoot(springboot的类加载和传统的双亲委派有什么区别、如何按顺序实例化Bean)

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

    @Bean
    public A a(B b) {      // Spring 保证 b() 先跑
      return new A(b);
    }
}
</code></pre>
<p><strong>使用这种方式,理解起来简单,并且可靠性高,与具体的应用框架无关,但是也有一定的短板,就是按指定顺序实例化的Bean,必须存在真实的依赖关系。</strong></p>
<h3 id="2dependson显式声明无真正依赖也适用">2、@DependsOn(显式声明,无真正依赖也适用)</h3>
<p>虽然两个 Bean 之间没有构造器/字段依赖,但你仍想让 BeanB 先实例化于BeanA。<br>
这个时候就可以使用<code>@DependsOn</code>注解了,但是需要注意一点:<code>@DependsOn</code> 只能保证先实例化,不能保证先销毁(销毁顺序用 <code>DependentBean.destroyMethod</code> 或 <code>DisposableBeanAdapter</code>)。</p>
<blockquote>
<p>@DependsOn 仅控制 初始化顺序;销毁时 Spring 会按依赖关系的反向顺序执行,因此若 B 依赖 A,则 B 先销毁,A 后销毁。</p>
</blockquote>
<p>如下代码:</p>
<pre><code class="language-java">@Configuration
public class Config {
    @Bean
    public B b() { return new B(); }

    @Bean
    @DependsOn("b")   // 容器会先实例化 b,再实例化 a
    public A a() { return new A(); }
}
</code></pre>
<p>直接将注解写在类上也可以</p>
<pre><code class="language-java">@Component
@DependsOn("b")
public class A { }
</code></pre>
<h3 id="3order-或-ordered只影响收集型顺序">3、@Order 或 Ordered(只影响“收集型”顺序)</h3>
<p>适用范围:</p>
<ul>
<li><code>@Bean</code> 方法返回的是 <strong>Collection</strong> 注入点(如 <code>List&lt;X&gt;、Map&lt;String,X&gt;</code>)。</li>
<li><code>CommandLineRunner / ApplicationRunner / Filter / Interceptor</code> 等“链式”扩展点。对普通的单例 <strong>Bean</strong> 实例化顺序无效。</li>
</ul>
<blockquote>
<p>即使两个单例 Bean 实现了 Ordered 接口,只要它们之间不存在“收集型注入”或“链式扩展点”,Spring 仍然不保证谁先实例化。</p>
</blockquote>
<p>使用场景如下代码示例,使用此注解的Bean都是实现了同一个接口的同类型。</p>
<pre><code class="language-java">@Component
@Order(1)
public class FirstRunner implements CommandLineRunner { ... }

@Component
@Order(2)
public class SecondRunner implements CommandLineRunner { ... }
</code></pre>
<h3 id="4beandefinitionregistrypostprocessorbeanfactorypostprocessor-的扩展">4、BeanDefinitionRegistryPostProcessor(BeanFactoryPostProcessor 的扩展)</h3>
<p>在容器刷新早期(所有 <strong><code>BeanDefinition</code></strong> 加载完、实例化之前)把定义顺序调到自己想要的顺序。</p>
<pre><code class="language-java">@Component
public class OrderBeanProcessor implements BeanFactoryPostProcessor {

                // 让某个 Bean 在普通 Bean 实例化之前提前实例化
    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
      PrimaryOrderBean bean = beanFactory.getBean(PrimaryOrderBean.class);
      System.out.println(bean);
    }
}
@Component
public class PrimaryOrderBean {

    public PrimaryOrderBean() {
      System.out.println("init primary order bean");
    }

    @Override
    public String toString() {
      return "PrimaryOrderBean{toString}";
    }
}
</code></pre>
<p><strong>此方式的风险:可读性差,容易踩坑,除非写框架,否则不建议</strong>。</p>
<h3 id="5-autoconfigurebefore--autoconfigureafter仅对-springfactories-里的自动配置生效">5、 @AutoConfigureBefore / @AutoConfigureAfter(仅对 spring.factories 里的自动配置生效)</h3>
<p>当在写自己的 <strong><code>starter</code></strong>时,想让 <code>MyAutoConfiguration</code> 在 <code>DataSourceAutoConfiguration</code> 之前/之后运行。<br>
对普通 <code>@Configuration</code> 无效,也不会影响 <strong>Bean</strong> 实例化顺序,只影响配置类解析顺序。</p>
<pre><code class="language-java">@Configuration
@AutoConfigureBefore(DataSourceAutoConfiguration.class)
public class MyAutoConfiguration { }
</code></pre>
<blockquote>
<p>这两个注解只对 META-INF/spring.factories 中注册的 EnableAutoConfiguration 类生效;写在普通 @Configuration 类上会被忽略。</p>
</blockquote>
<h3 id="6实现-priorityordered--ordered只影响后处理顺序">6、实现 PriorityOrdered / Ordered(只影响“后处理”顺序)</h3>
<p>对普通 <strong>Bean</strong> 的“<strong>实例化顺序</strong>”没有任何影响,仅当 <strong>Spring</strong> 内部收集 <code>BeanPostProcessor</code>、<code>FactoryPostProcessor</code> 等扩展点时使用。</p>


</div>
<div id="MySignature" role="contentinfo">
    <div style="float:left;letter-spacing: 1px;font-size: 13px;"><p><strong>作者:</strong>纪莫
<br>欢迎任何形式的转载,但请务必注明出处。<br>
限于本人水平,如果文章和代码有表述不当之处,还请不吝赐教。</p>
<!-- 分享图标开始 -->
<p>欢迎扫描二维码关注公众号:<strong>Jimoer</strong></p>
<p>文章会同步到公众号上面,大家一起成长,共同提升技术能力。</p>
<p><strong>声援博主:</strong>如果您觉得文章对您有帮助,可以点击文章右下角【推荐】一下。</p>
<p>您的鼓励是博主的最大动力!</p>
</div>
<div style="float:right;">
<img src="https://img2018.cnblogs.com/blog/772743/201909/772743-20190904004009398-659676330.png" alt="微信公众号" ></div>
<!-- 分享图标结束 --><br><br>
来源:https://www.cnblogs.com/jimoer/p/19190686
頁: [1]
查看完整版本: 技术面:SpringBoot(springboot的类加载和传统的双亲委派有什么区别、如何按顺序实例化Bean)