動態

詳情 返回 返回

SpringBoot(springboot的类加载和传统的双亲委派有什么区别、如何按顺序实例化Bean) - 動態 詳情

前言

SpringBoot中,類加載機制與Java的傳統雙親委派類加載機制是有一定區別。主要體現在自定義類加載器fat jar(可執行jar)的加載方式上。

Java的傳統雙親委派模型

Java傳統類加載機制,遵循雙親委派模型,核心規則:類加載請求優先由父類加載器處理,只有父加載器無法加載時才由子加載器嘗試。

1、JDK 1.8及更早版本採用如下層級結構:

SpringBoot(springboot的類加載和傳統的雙親委派有什麼區別、如何按順序實例化Bean)_加載

2、從 JDK 9 引入模塊系統開始,是這樣的層級結構

SpringBoot(springboot的類加載和傳統的雙親委派有什麼區別、如何按順序實例化Bean)_加載_02


這樣設計的主要目的是為了,避免重複加載核心類(如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的“熱部署”主要實現原理如下:

  1. 雙 ClassLoader 架構
  • Base ClassLoader:加載第三方 jar 包(不會頻繁變動)
  • Restart ClassLoader:加載開發者自己寫的類(會頻繁變動)
  1. 文件變化監聽機制
  • 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 必須已實例化。
直接讓 BeanA 的構造器裏需要 BeanB,或者 BeanA 裏有一個非延遲的 BeanB 字段 + @Autowired

如下代碼

@Configuration
public class Config {
    @Bean
    public B b() { return new B(); }

    @Bean
    public A a(B b) {      // Spring 保證 b() 先跑
        return new A(b);
    }
}

使用這種方式,理解起來簡單,並且可靠性高,與具體的應用框架無關,但是也有一定的短板,就是按指定順序實例化的Bean,必須存在真實的依賴關係。

2、@DependsOn(顯式聲明,無真正依賴也適用)

雖然兩個 Bean 之間沒有構造器/字段依賴,但你仍想讓 BeanB 先實例化於BeanA。
這個時候就可以使用@DependsOn註解了,但是需要注意一點:@DependsOn 只能保證先實例化,不能保證先銷燬(銷燬順序用 DependentBean.destroyMethod 或 DisposableBeanAdapter)。

@DependsOn 僅控制 初始化順序;銷燬時 Spring 會按依賴關係的反向順序執行,因此若 B 依賴 A,則 B 先銷燬,A 後銷燬。

如下代碼:

@Configuration
public class Config {
    @Bean
    public B b() { return new B(); }

    @Bean
    @DependsOn("b")   // 容器會先實例化 b,再實例化 a
    public A a() { return new A(); }
}

直接將註解寫在類上也可以

@Component
@DependsOn("b")
public class A { }

3、@Order 或 Ordered(隻影響“收集型”順序)

適用範圍:

  • @Bean 方法返回的是 Collection 注入點(如 List<X>、Map<String,X>)。
  • CommandLineRunner / ApplicationRunner / Filter / Interceptor 等“鏈式”擴展點。對普通的單例 Bean 實例化順序無效。

即使兩個單例 Bean 實現了 Ordered 接口,只要它們之間不存在“收集型注入”或“鏈式擴展點”,Spring 仍然不保證誰先實例化。

使用場景如下代碼示例,使用此註解的Bean都是實現了同一個接口的同類型。

@Component
@Order(1)
public class FirstRunner implements CommandLineRunner { ... }

@Component
@Order(2)
public class SecondRunner implements CommandLineRunner { ... }

4、BeanDefinitionRegistryPostProcessor(BeanFactoryPostProcessor 的擴展)

在容器刷新早期(所有 BeanDefinition 加載完、實例化之前)把定義順序調到自己想要的順序。

@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}";
    }
}

此方式的風險:可讀性差,容易踩坑,除非寫框架,否則不建議

5、 @AutoConfigureBefore / @AutoConfigureAfter(僅對 spring.factories 裏的自動配置生效)

當在寫自己的 starter時,想讓 MyAutoConfiguration 在 DataSourceAutoConfiguration 之前/之後運行。
對普通 @Configuration 無效,也不會影響 Bean 實例化順序,隻影響配置類解析順序。

@Configuration
@AutoConfigureBefore(DataSourceAutoConfiguration.class)
public class MyAutoConfiguration { }

這兩個註解只對 META-INF/spring.factories 中註冊的 EnableAutoConfiguration 類生效;寫在普通 @Configuration 類上會被忽略。

6、實現 PriorityOrdered / Ordered(隻影響“後處理”順序)

對普通 Bean 的“實例化順序”沒有任何影響,僅當 Spring 內部收集 BeanPostProcessorFactoryPostProcessor 等擴展點時使用。

Add a new 評論

Some HTML is okay.