背景

專案裡想用@Value注入一個欄位,可沒想到怎麼都注入不成功,但換另一種方式就可以,於是就想了解一下@Value註解不成功的原因。

本文的程式碼是基於Spring的5.3.8版本

模擬@Value成功的場景

首先為了搞清楚@Value註解不成功的原理,我們先用最簡單的程式碼模擬一下它注入成功的例子:

  1. resources資料夾下定義了application.yml,內容如下:
  2. my:
  3. value: hello
  1. 定義一個配置類:
  2. @Configuration
  3. @Data
  4. public class Config {
  5. @Value("${my.value}")
  6. private String myValue;
  7. }
  1. 定義一個測試類:
  2. public class Main {
  3. public static void main(String[] args) {
  4. AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Config.class);
  5. Config config = context.getBean(Config.class);
  6. System.out.println(config);
  7. }
  8. }
  1. 輸出:
  2. Config(myValue=${my.value})

上面的程式碼做了幾件事情:

  1. resources/application.yml檔案中定義了my.value=hello
  2. 定義了一個Config類,利用@value註解將hello注入到欄位myValue
  3. 定義了一個Main類測試效果

測試類做了幾件事情:

  1. 使用AnnotationConfigApplicationContext這個容器載入配置類
  2. 獲取配置類Config
  3. 輸出注入的欄位myValue

從結果來看,並沒有注入成功,我的第一感覺就是沒有把我們的application.yml檔案裡的內容載入到environment裡面,那我們就來看看environment裡面都有什麼內容,如下程式碼:

  1. public class Main {
  2. public static void main(String[] args) {
  3. AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Config.class);
  4. ConfigurableEnvironment environment = context.getEnvironment();
  5. System.out.println(environment);
  6. }
  7. }

從結果來看:

  1. environment並沒有包含我們application.yml檔案裡的內容
  2. 但它包含了其他兩個東西,分別是systemPropertiessystemEnvironment

那我們就需要把application.yml檔案裡的內容載入到environment,需要考慮以下兩個問題:

  1. 怎麼解析yml檔案的內容
  2. 怎麼把解析的內容放到environment

針對問題一:可以利用spring自帶的YamlPropertySourceLoader這個類的load()方法,它會返回一個List<PropertySource<?>>

針對問題二:我們可以先來看一下預設的內容是怎麼放進去的,看一下getEnvironment()的原始碼:

  1. public abstract class AbstractApplicationContext extends DefaultResourceLoader
  2. implements ConfigurableApplicationContext {
  3. public ConfigurableEnvironment getEnvironment() {
  4. if (this.environment == null) {
  5. this.environment = createEnvironment();
  6. }
  7. return this.environment;
  8. }
  9. protected ConfigurableEnvironment createEnvironment() {
  10. return new StandardEnvironment();
  11. }
  12. }

從上面可以看出預設建立的是一個StandardEnvironment,我們再來看一下它的初始化:

  1. public class StandardEnvironment extends AbstractEnvironment {
  2. public static final String SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME = "systemEnvironment";
  3. public static final String SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME = "systemProperties";
  4. @Override
  5. protected void customizePropertySources(MutablePropertySources propertySources) {
  6. propertySources.addLast(
  7. new PropertiesPropertySource(SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME, getSystemProperties()));
  8. propertySources.addLast(
  9. new SystemEnvironmentPropertySource(SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, getSystemEnvironment()));
  10. }
  11. }
  1. public abstract class AbstractEnvironment implements ConfigurableEnvironment {
  2. public AbstractEnvironment() {
  3. this(new MutablePropertySources());
  4. }
  5. protected AbstractEnvironment(MutablePropertySources propertySources) {
  6. this.propertySources = propertySources;
  7. this.propertyResolver = createPropertyResolver(propertySources);
  8. customizePropertySources(propertySources);
  9. }
  10. }

從上面程式碼可以看出,在StandardEnvironment.customizePropertySources()的方法中,是通過propertySources.addLast()方法新增進去的,那我們可以照葫蘆畫瓢,如下:

  1. public class Main {
  2. public static void main(String[] args) throws IOException {
  3. AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Config.class);
  4. ConfigurableEnvironment environment = context.getEnvironment();
  5. System.out.println(environment);
  6. YamlPropertySourceLoader loader = new YamlPropertySourceLoader();
  7. List<PropertySource<?>> propertySources = loader.load("my-properties",
  8. new FileSystemResource("/Users/tiger/spring-boot-study/src/main/resources/application.yml"));
  9. environment.getPropertySources().addLast(propertySources.get(0));
  10. System.out.println(environment);
  11. }
  12. }

從上面結果可以看出,我們已經成功把我們的application.yml檔案內容放到environment中了

那我們把測試程式碼改成:

  1. public class Main {
  2. public static void main(String[] args) throws IOException {
  3. AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Config.class);
  4. YamlPropertySourceLoader loader = new YamlPropertySourceLoader();
  5. List<PropertySource<?>> propertySources = loader.load("my-properties",
  6. new FileSystemResource("/Users/tiger/spring-boot-study/src/main/resources/application.yml"));
  7. context.getEnvironment().getPropertySources().addLast(propertySources.get(0));
  8. Config config = context.getBean(Config.class);
  9. System.out.println(config);
  10. }
  11. }
  12. 輸出:
  13. Config(myValue=${my.value})

從上面的結果可以看出,還是沒有得到我們想要的結果,這是因為conig類會提前初始化,是在refresh()方法中的finishBeanFactoryInitialization()方法進行的,所以我們要在這一步之前把我們的內容放到environment

翻了一翻refresh()這個方法,發現在prepareRefresh()這個方法裡有一個initPropertySources()的方法,註釋寫著初始化一系列的資源,所以我們可以在這個方法裡面載入我們的配置檔案,於是變成:

  1. public class Main {
  2. public static void main(String[] args) {
  3. AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Config.class) {
  4. @SneakyThrows
  5. @Override
  6. public void initPropertySources() {
  7. YamlPropertySourceLoader loader = new YamlPropertySourceLoader();
  8. List<PropertySource<?>> propertySources = loader.load("my-properties",
  9. new FileSystemResource("/Users/tiger/spring-boot-study/src/main/resources/application.yml"));
  10. getEnvironment().getPropertySources().addLast(propertySources.get(0));
  11. }
  12. };
  13. Config config = context.getBean(Config.class);
  14. System.out.println(config);
  15. }
  16. }
  17. 輸出:
  18. Config(myValue=hello)

到目前為止,我們模擬了@Value注入成功的場景,專案裡面應該不會出現這種資源沒有載入的問題,因為這些事情spring boot都幫我們做好了

所以直接在@Configuration類下直接用@Value是沒有問題的

模擬注入不成功的場景

現在我們就來模擬一下注入不成功的場景,配置類改成如下:

  1. @Configuration
  2. @Data
  3. public class Config {
  4. @Value("${my.value}")
  5. private String myValue;
  6. @Bean
  7. public MyBeanFactoryPostProcessor myBeanFactoryPostProcessor() {
  8. return new MyBeanFactoryPostProcessor();
  9. }
  10. public static class MyBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
  11. @Override
  12. public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
  13. }
  14. }
  15. }

輸出結果:

  1. Config(myValue=null)

這就是我專案上遇到的問題,在配置類中再生成一個BeanFactoryPostProcessor後,@Value就注入不成功了

但只要把這個方法寫成static就可以了,如下:

  1. @Configuration
  2. @Data
  3. public class Config {
  4. @Value("${my.value}")
  5. private String myValue;
  6. @Bean
  7. public static MyBeanFactoryPostProcessor myBeanFactoryPostProcessor() {
  8. return new MyBeanFactoryPostProcessor();
  9. }
  10. public static class MyBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
  11. @Override
  12. public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
  13. }
  14. }
  15. }

輸出結果:

  1. Config(myValue=hello)

看看為什麼沒有注入成功

@Value是由AutowiredAnnotationBeanPostProcessor.postProcessProperties()處理的,所以我們就以這裡為入口進行除錯。

我們先把static去掉:

發現沒有執行到上述方法,那我們再把static加上,看一下成功的情況:

可以看到,是可以到這個方法的,而且知道這個方法是被AbstractAutowireCapableBeanFactory.populateBean()呼叫的,我們再看一下這裡的情況:

從上圖可以看出,getBeanPostProcessorCache().instantiationAware是有AutowiredAnnotationBeanPostProcessor這個例項的

那我們再來看一下不加static這裡的情況:

果然,沒有注入成功的原因是在建立config例項的時候,還沒有建立AutowiredAnnotationBeanPostProcessor例項

我們來看一下這個getBeanPostProcessorCache().instantiationAware是什麼東西,又是如何生成的

發現只有在AbstractBeanFactory.getBeanPostProcessorCache()這個方法會將InstantiationAwareBeanPostProcessor新增到instantiationAware,如下:

  1. public abstract class AbstractBeanFactory extends FactoryBeanRegistrySupport implements ConfigurableBeanFactory {
  2. BeanPostProcessorCache getBeanPostProcessorCache() {
  3. BeanPostProcessorCache bpCache = this.beanPostProcessorCache;
  4. if (bpCache == null) {
  5. bpCache = new BeanPostProcessorCache();
  6. for (BeanPostProcessor bp : this.beanPostProcessors) {
  7. if (bp instanceof InstantiationAwareBeanPostProcessor) {
  8. bpCache.instantiationAware.add((InstantiationAwareBeanPostProcessor) bp);
  9. if (bp instanceof SmartInstantiationAwareBeanPostProcessor) {
  10. bpCache.smartInstantiationAware.add((SmartInstantiationAwareBeanPostProcessor) bp);
  11. }
  12. }
  13. if (bp instanceof DestructionAwareBeanPostProcessor) {
  14. bpCache.destructionAware.add((DestructionAwareBeanPostProcessor) bp);
  15. }
  16. if (bp instanceof MergedBeanDefinitionPostProcessor) {
  17. bpCache.mergedDefinition.add((MergedBeanDefinitionPostProcessor) bp);
  18. }
  19. }
  20. this.beanPostProcessorCache = bpCache;
  21. }
  22. return bpCache;
  23. }
  24. }

從上面的程式碼看出,本質還是從this.beanPostProcessors獲取的,我們來看一下什麼時候會把AutowiredAnnotationBeanPostProcessor新增到容器中,如下:

從上圖可知:AutowiredAnnotationBeanPostProcessor是在refresh()方法中的registerBeanPostProcessors()方法注入的

我們再來看一下加static方法的config類是什麼時候載入的:

再來看一下不加static方法的config類是什麼時候載入的

我們來總結一下提到的方法在refresh()方法中的順序:

  1. invokeBeanFactoryPostProcessors(); ——> 不加static的時候,在這一步載入config
  2. registerBeanPostProcessors(); ——> 註冊AutowiredAnnotationBeanPostProcessor
  3. finishBeanFactoryInitialization(); static的時候,在這一步載入config

所以我們就知道原因了:當不加static欄位時候,載入config類的時候,我們的AutowiredAnnotationBeanPostProcessor還沒有註冊,所以就會不成功,而當加上static後,我們載入config類的時候,我們的AutowiredAnnotationBeanPostProcessor已經註冊好了。

為什麼加static和不加static的載入順序是不一樣的呢

spring容器會在invokeBeanFactoryPostProcessors()這一步會載入所有的BeanFactoryPostProcessor,如果用static修飾的話,則不會載入config類,反之會載入。原因如下:

上圖已經給出了原因,如果生成bean的工廠方法是static方法就不會載入,反之會載入。

我們不加static,能不能也讓它注入成功呢?

那無非就是在載入config類之前,把AutowiredAnnotationBeanPostProcessor提前載入到容器就可以了,那我們來看一下原始碼是怎麼載入這個例項的:

我們同樣可以依葫蘆畫瓢,看看在哪裡提前載入比較合適,發現postProcessBeanFactory()這個方法比較合適,於是改成:

  1. public class Main {
  2. public static void main(String[] args) {
  3. AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Config.class) {
  4. @SneakyThrows
  5. @Override
  6. public void initPropertySources() {
  7. YamlPropertySourceLoader loader = new YamlPropertySourceLoader();
  8. List<PropertySource<?>> propertySources = loader.load("my-properties",
  9. new FileSystemResource("/Users/tiger/spring-boot-study/src/main/resources/application.yml"));
  10. getEnvironment().getPropertySources().addLast(propertySources.get(0));
  11. }
  12. @Override
  13. protected void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {
  14. String ppName = "org.springframework.context.annotation.internalAutowiredAnnotationProcessor";
  15. beanFactory.addBeanPostProcessor(getBean(ppName, BeanPostProcessor.class));
  16. }
  17. };
  18. Config config = context.getBean(Config.class);
  19. System.out.println(config);
  20. }
  21. }
  22. 輸出:
  23. Config(myValue=${my.value})

從結果來看,還是沒注入成功啊,經過一番除錯,發現是在下面步驟中出了問題:

我們來看一下載入成功的情況:

embeddedValueResolver是在下面步驟中被新增進去的:

可以看出是在refresh()中的finishBeanFactoryInitialization()這個方法裡面新增進去的,所以我們也要提前搞一下:

  1. public class Main {
  2. public static void main(String[] args) {
  3. AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Config.class) {
  4. @SneakyThrows
  5. @Override
  6. public void initPropertySources() {
  7. YamlPropertySourceLoader loader = new YamlPropertySourceLoader();
  8. List<PropertySource<?>> propertySources = loader.load("my-properties",
  9. new FileSystemResource("/Users/tiger/spring-boot-study/src/main/resources/application.yml"));
  10. getEnvironment().getPropertySources().addLast(propertySources.get(0));
  11. }
  12. @Override
  13. protected void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {
  14. String ppName = "org.springframework.context.annotation.internalAutowiredAnnotationProcessor";
  15. beanFactory.addBeanPostProcessor(getBean(ppName, BeanPostProcessor.class));
  16. beanFactory.addEmbeddedValueResolver(strVal -> getEnvironment().resolvePlaceholders(strVal));
  17. }
  18. };
  19. Config config = context.getBean(Config.class);
  20. System.out.println(config);
  21. }
  22. }
  23. 輸出:
  24. Config(myValue=hello)

好了,大功告成!

總結

看到這裡,相信大家都知道@Value為什麼載入不成功了吧,主要就是因為載入順序的關係,可以看出最簡單的方法就是在方法上加一個static,後面的探究主要是地對Spring容器載入順序的理解

本文探究的是在配置類裡存在BeanFactoryPostProcessor,如果換成BeanPostProcessor呢?同樣會載入不成功嗎?又是因為什麼原因呢?其實也可以用同樣的方法來測試,和本文講的如出一轍,小夥伴們可自行探究一下。

有什麼問題歡迎一起探討~~~