前面介紹了獲取容器可以讓spring bean實現ApplicationContextAware,實際也是初始化執行了setApplicationContext接口,
初始化接口還可以借助一些注解或者spring bean的初始化方法,那么他們的執行順序是什么樣的呢?
一、驗證(沒有依賴關系時)是無序的
1、demo
下面新建三個class文件
分別使用ApplicationContextAware、@PostConstruct和InitializingBean
package com.bit.demo.test.bean;import com.bit.demo.dto.UserDTO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;@Slf4j
@Component
public class InitUserInitUtil implements ApplicationContextAware {private static UserDTO userDTO;@Overridepublic void setApplicationContext(ApplicationContext applicationContext) throws BeansException {log.info("初始化方法,ApplicationContextInitializer");UserDTO user = new UserDTO();user.setUserName("zs");user.setPassword("123456");this.userDTO = user;}public static UserDTO getUserDTO() {return userDTO;}
}
package com.bit.demo.test.bean;import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;@Component
@Slf4j
public class TestClass1 {@PostConstructpublic void init(){//項目啟動執行方法log.info("初始化方法, 使用PostConstruct");}
}
package com.bit.demo.test.bean;import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.stereotype.Component;@Component
@Slf4j
public class TestClass2 implements InitializingBean {@PostConstructpublic void initAgain() {log.info("初始化方法, 同時使用PostConstruct和InitializingBean");}@Overridepublic void afterPropertiesSet() throws Exception {log.info("初始化方法, 使用InitializingBean");}
}
? 啟動項目輸出
. ____ _ __ _ _/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \\\/ ___)| |_)| | | | | || (_| | ) ) ) )' |____| .__|_| |_|_| |_\__, | / / / /=========|_|==============|___/=/_/_/_/:: Spring Boot :: (v3.2.4)2025-03-28T10:56:01.574+08:00 INFO 13716 --- [ main] com.bit.demo.BitApplication : Starting BitApplication using Java 17.0.11 with PID 13716 (C:\mydemo\bit-demo\target\classes started by Tina.Zhang in C:\code\cci-voice)
2025-03-28T10:56:01.576+08:00 INFO 13716 --- [ main] com.bit.demo.BitApplication : No active profile set, falling back to 1 default profile: "default"
2025-03-28T10:56:02.209+08:00 INFO 13716 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port 7777 (http)
2025-03-28T10:56:02.216+08:00 INFO 13716 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat]
2025-03-28T10:56:02.217+08:00 INFO 13716 --- [ main] o.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/10.1.19]
2025-03-28T10:56:02.258+08:00 INFO 13716 --- [ main] o.a.c.c.C.[.[localhost].[/bitDemo] : Initializing Spring embedded WebApplicationContext
2025-03-28T10:56:02.258+08:00 INFO 13716 --- [ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 648 ms
Logging initialized using 'class org.apache.ibatis.logging.stdout.StdOutImpl' adapter.
Initialization Sequence datacenterId:6 workerId:4_ _ |_ _ _|_. ___ _ | _
| | |\/|_)(_| | |_\ |_)||_|_\ / | 3.5.5
2025-03-28T10:56:02.495+08:00 INFO 13716 --- [ main] c.bit.demo.test.bean.InitUserInitUtil : 初始化方法,ApplicationContextInitializer
2025-03-28T10:56:02.496+08:00 INFO 13716 --- [ main] com.bit.demo.test.bean.TestClass1 : 初始化方法, 使用PostConstruct
2025-03-28T10:56:02.497+08:00 INFO 13716 --- [ main] com.bit.demo.test.bean.TestClass2 : 初始化方法, 同時使用PostConstruct和InitializingBean
2025-03-28T10:56:02.497+08:00 INFO 13716 --- [ main] com.bit.demo.test.bean.TestClass2 : 初始化方法, 使用InitializingBean
2025-03-28T10:56:02.718+08:00 INFO 13716 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port 7777 (http) with context path '/bitDemo'
2025-03-28T10:56:02.723+08:00 INFO 13716 --- [ main] com.bit.demo.BitApplication : Started BitApplication in 1.385 seconds (process running for 1.765)
2、考慮到路徑影響類的加載
前面的輸出順序是InitUserInitUtil > TestClass1 > TestClass2,正好和類路徑順序一致,考慮到類的路徑影響類的加載,現在TestClass1、TestClass2各copy出來一個,加個前綴Aa,排在InitUserInitUtil 的前面。
新增文件
再次啟動
可以看到Aa開頭的類日志先輸出了。
3、結論
ApplicationContextAware的setApplicationContext、@PostConstruct、
InitializingBean的afterPropertiesSet 他們的執行順序是隨機的。
4、錯誤引用
基于上面,AaTestClass1 >?AaTestClass2 >?InitUserInitUtil >?TestClass1 >?TestClass2
現在如果在AaTestClass2中利用InitUserInitUtil 獲取UserDTO:
package com.bit.demo.test.bean;import com.bit.demo.dto.UserDTO;
import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.stereotype.Component;@Component
@Slf4j
public class AaTestClass2 implements InitializingBean {@PostConstructpublic void initAgain() {log.info("初始化方法, 同時使用PostConstruct和InitializingBean");}@Overridepublic void afterPropertiesSet() throws Exception {log.info("初始化方法, 使用InitializingBean");UserDTO innitUser = InitUserInitUtil.getUserDTO();if(innitUser != null) {log.info("獲取user成功",innitUser);}else{log.info("獲取user失敗",innitUser);}}
}
因為此時InitUserInitUtil 還沒有初始化,獲取到的是null:
注意:
這也間接說明了,不要濫用ApplicationContextAware來獲取Bean,能自動獲取Bean的都通過Autowired等注解獲取,因為使用了注解spring自身會優化加載順序,讓被依賴的Bean先執行。在必須手動獲取如非spring bean中使用則不用考慮加載問題(非spring bean根本不會自動加載)。
5、使用自動裝配引入依賴關系來解決引用問題?
我們知道,使用@Autowired等自動裝配,可以讓被依賴的bean先執行。針對上面的問題,改寫下:
package com.bit.demo.test.bean;import com.bit.demo.dto.UserDTO;
import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;@Component
@Slf4j
public class AaTestClass2 implements InitializingBean {@Autowiredprivate InitUserInitUtil initUserInitUtil;@PostConstructpublic void initAgain() {log.info("初始化方法, 同時使用PostConstruct和InitializingBean");}@Overridepublic void afterPropertiesSet() throws Exception {log.info("初始化方法, 使用InitializingBean");UserDTO innitUser = InitUserInitUtil.getUserDTO();if(innitUser != null) {log.info("獲取user成功",innitUser.toString());}else{log.info("獲取user失敗",innitUser);}}
}
這時啟動打印:AaTestClass1??>?InitUserInitUtil > AaTestClass2? >?TestClass1 >?TestClass2,
對比上面的AaTestClass1 >?AaTestClass2 >?InitUserInitUtil >?TestClass1 >?TestClass2,可見InitUserInitUtil 提前加載了?
6、引入ApplicationContextInitializer
上面增加依賴關系可以解決bean的引用問題。
那如果就希望InitUserInitUtil做為一個最底層的bean,能夠在其他業務bean之前加載,還可以使用
ApplicationContextInitializer。
package com.bit.demo.test.bean;import com.bit.demo.dto.UserDTO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.stereotype.Component;@Slf4j
@Component
public class InitUserInitUtil implements ApplicationContextInitializer {private static UserDTO userDTO;public static UserDTO getUserDTO() {return userDTO;}@Overridepublic void initialize(ConfigurableApplicationContext applicationContext) {log.info("初始化方法,ApplicationContextInitializer");UserDTO user = new UserDTO();user.setUserName("zs");user.setPassword("123456");this.userDTO = user;}
}
注冊:
@SpringBootApplication
public class BitApplication {public static void main(String[] args) {//SpringApplication.run(BitApplication.class, args);SpringApplication application = new SpringApplication(BitApplication.class);application.addInitializers(new InitUserInitUtil()); // 注冊自定義 ApplicationContextInitializerapplication.run(args);}
}
package com.bit.demo.test.bean;import com.bit.demo.dto.UserDTO;
import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.stereotype.Component;@Component
@Slf4j
public class AaTestClass2 implements InitializingBean {@PostConstructpublic void initAgain() {log.info("初始化方法, 同時使用PostConstruct和InitializingBean");}@Overridepublic void afterPropertiesSet() throws Exception {log.info("初始化方法, 使用InitializingBean");UserDTO innitUser = InitUserInitUtil.getUserDTO();if(innitUser != null) {log.info("獲取user成功",innitUser);}else{log.info("獲取user失敗",innitUser);}}
}
? ?
再次啟動輸出
=========|_|==============|___/=/_/_/_/:: Spring Boot :: (v3.2.4)2025-03-28T11:18:53.520+08:00 INFO 15408 --- [ main] c.bit.demo.test.bean.InitUserInitUtil : 初始化方法,ApplicationContextInitializer
2025-03-28T11:18:53.525+08:00 INFO 15408 --- [ main] com.bit.demo.BitApplication : Starting BitApplication using Java 17.0.11 with PID 15408 (C:\mydemo\bit-demo\target\classes started by Tina.Zhang in C:\code\cci-voice)
2025-03-28T11:18:53.526+08:00 INFO 15408 --- [ main] com.bit.demo.BitApplication : No active profile set, falling back to 1 default profile: "default"
2025-03-28T11:18:54.182+08:00 INFO 15408 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port 7777 (http)
2025-03-28T11:18:54.190+08:00 INFO 15408 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat]
2025-03-28T11:18:54.190+08:00 INFO 15408 --- [ main] o.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/10.1.19]
2025-03-28T11:18:54.233+08:00 INFO 15408 --- [ main] o.a.c.c.C.[.[localhost].[/bitDemo] : Initializing Spring embedded WebApplicationContext
2025-03-28T11:18:54.233+08:00 INFO 15408 --- [ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 672 ms
Logging initialized using 'class org.apache.ibatis.logging.stdout.StdOutImpl' adapter.
Initialization Sequence datacenterId:6 workerId:2_ _ |_ _ _|_. ___ _ | _
| | |\/|_)(_| | |_\ |_)||_|_\ / | 3.5.5
2025-03-28T11:18:54.467+08:00 INFO 15408 --- [ main] com.bit.demo.test.bean.AaTestClass1 : 初始化方法, 使用PostConstruct
2025-03-28T11:18:54.467+08:00 INFO 15408 --- [ main] com.bit.demo.test.bean.AaTestClass2 : 初始化方法, 同時使用PostConstruct和InitializingBean
2025-03-28T11:18:54.468+08:00 INFO 15408 --- [ main] com.bit.demo.test.bean.AaTestClass2 : 初始化方法, 使用InitializingBean
2025-03-28T11:18:54.468+08:00 INFO 15408 --- [ main] com.bit.demo.test.bean.AaTestClass2 : 獲取user成功
2025-03-28T11:18:54.469+08:00 INFO 15408 --- [ main] com.bit.demo.test.bean.TestClass1 : 初始化方法, 使用PostConstruct
2025-03-28T11:18:54.469+08:00 INFO 15408 --- [ main] com.bit.demo.test.bean.TestClass2 : 初始化方法, 同時使用PostConstruct和InitializingBean
2025-03-28T11:18:54.469+08:00 INFO 15408 --- [ main] com.bit.demo.test.bean.TestClass2 : 初始化方法, 使用InitializingBean
2025-03-28T11:18:54.688+08:00 INFO 15408 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port 7777 (http) with context path '/bitDemo'
2025-03-28T11:18:54.694+08:00 INFO 15408 --- [ main] com.bit.demo.BitApplication : Started BitApplication in 1.376 seconds (process running for 1.72)
二、ApplicationContextInitializer
1、介紹
ApplicationContextInitializer
是 Spring 框架中的一個接口,主要用于在 Spring 容器刷新(refresh()
)之前 對 ApplicationContext
進行自定義的初始化操作。它允許在 ApplicationContext
完全初始化之前進行配置,例如:添加屬性、激活配置文件(Profiles)等。
這種機制讓開發者能夠在 Spring 啟動過程中更早地進行干預,適用于有高級配置需求的應用程序。
該接口位于 org.springframework.context
包下:
public interface ApplicationContextInitializer<C extends ConfigurableApplicationContext> {void initialize(C applicationContext);
}
2、方法說明
initialize(C applicationContext)
:
-
該方法在
ApplicationContext
刷新(refresh()
)之前調用,可以對applicationContext
進行初始化。 -
C
是ConfigurableApplicationContext
的子類,如AnnotationConfigApplicationContext
或GenericApplicationContext
。
3、典型使用場景
(1)動態修改 Environment
變量
在 initialize()
方法中修改 Environment
,實現動態配置:
@Override
public void initialize(ConfigurableApplicationContext applicationContext) {ConfigurableEnvironment environment = applicationContext.getEnvironment();environment.getSystemProperties().put("my.custom.property", "CustomValue");System.out.println("? 設置環境變量 my.custom.property = " + environment.getProperty("my.custom.property"));
}
(2)激活特定的 Spring Profile
@Override
public void initialize(ConfigurableApplicationContext applicationContext) {ConfigurableEnvironment environment = applicationContext.getEnvironment();environment.setActiveProfiles("dev"); // 激活 dev ProfileSystem.out.println("? 激活 Profile: " + String.join(", ", environment.getActiveProfiles()));
}
(3)在 Spring 容器啟動前添加 PropertySource
如果需要在 Spring 啟動前添加額外的配置源(如數據庫、遠程配置中心等),可以這樣做:
import org.springframework.core.env.MutablePropertySources;
import org.springframework.core.env.PropertiesPropertySource;
import java.util.Properties;@Override
public void initialize(ConfigurableApplicationContext applicationContext) {MutablePropertySources propertySources = applicationContext.getEnvironment().getPropertySources();Properties properties = new Properties();properties.put("extra.config", "Loaded from ApplicationContextInitializer");propertySources.addFirst(new PropertiesPropertySource("extraProperties", properties));System.out.println("? 額外配置: " + applicationContext.getEnvironment().getProperty("extra.config"));
}
4、與其他 Spring 組件的對比
組件 | 作用范圍 | 作用時間點 | 主要用途 |
---|
ApplicationContextInitializer | Spring 容器 | ApplicationContext.refresh() 之前 | 初始化 ApplicationContext ,添加 PropertySource 、修改配置等 |
BeanFactoryPostProcessor | BeanFactory | ApplicationContext.refresh() 期間 | 修改 Bean 定義(如動態修改 @Bean 配置) |
BeanPostProcessor | 單個 Bean | Bean 初始化前后 | 處理 Bean 實例,如 AOP、代理等 |
ApplicationRunner / CommandLineRunner | 應用啟動完成后 | ApplicationContext 初始化后 | 運行啟動任務(如初始化數據、執行業務邏輯) |
5、執行時期和注意事項
ApplicationContextInitializer
的執行時機 早于 Spring 容器刷新(refresh()
),如下所示:
(1)創建 SpringApplication
(2)調用 ApplicationContextInitializer.initialize()
(3)加載 Environment
(4)創建 ApplicationContext
并調用 refresh()
(5)掃描 BeanFactory
并注冊 Bean
(5)調用 BeanFactoryPostProcessor
(6)調用 InitializingBean
或 @PostConstruct
(7)啟動 Spring 容器
注意:由于 ApplicationContextInitializer
在 refresh()
之前執行,此時 Bean 還未初始化,不能調用 applicationContext.getBean()
。
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;public class MyApplicationContextInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {@Overridepublic void initialize(ConfigurableApplicationContext applicationContext) {System.out.println("? 獲取 ApplicationContext:" + applicationContext);System.out.println("? 獲取 Environment:" + applicationContext.getEnvironment());// ? 不能使用 getBean(),因為此時 Bean 還未初始化try {Object myBean = applicationContext.getBean("myBean");System.out.println("獲取 Bean: " + myBean);} catch (Exception e) {System.out.println("?? 此時無法獲取 Bean,因為 ApplicationContext 還未刷新!");}}
}
6、使用方法
(1)實現ApplicationContextInitializer
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.env.ConfigurableEnvironment;public class MyApplicationContextInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {@Overridepublic void initialize(ConfigurableApplicationContext applicationContext) {System.out.println("🚀 ApplicationContextInitializer 執行...");// 獲取 EnvironmentConfigurableEnvironment environment = applicationContext.getEnvironment();// 設置自定義屬性environment.getSystemProperties().put("custom.property", "Hello Spring!");// 打印 Environment 變量System.out.println("? Environment 自定義屬性:" + environment.getProperty("custom.property"));}
}
(2)注冊ApplicationContextInitializer
Spring 提供了 3 種方式 來注冊 ApplicationContextInitializer
。
①??在 SpringApplication
中添加 addInitializers
:
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;@SpringBootApplication
public class MyApplication {public static void main(String[] args) {SpringApplication application = new SpringApplication(MyApplication.class);application.addInitializers(new MyApplicationContextInitializer()); // 注冊自定義 ApplicationContextInitializerapplication.run(args);}
}
②?在 spring.factories
中自動加載
創建 META-INF/spring.factories
文件:
org.springframework.context.ApplicationContextInitializer=com.example.MyApplicationContextInitializer
③?在測試中使用 @ContextConfiguration
如果只想在測試環境中使用 ApplicationContextInitializer
,可以使用 @ContextConfiguration
:
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ContextConfiguration;@SpringBootTest
@ContextConfiguration(initializers = MyApplicationContextInitializer.class)
public class MyApplicationTests {@Testvoid contextLoads() {System.out.println("🔍 運行測試...");}
}