背景
專案裡想用@Value
注入一個欄位,可沒想到怎麼都注入不成功,但換另一種方式就可以,於是就想了解一下@Value
註解不成功的原因。
本文的程式碼是基於Spring的5.3.8版本
模擬@Value
成功的場景
首先為了搞清楚@Value
註解不成功的原理,我們先用最簡單的程式碼模擬一下它注入成功的例子:
在resources資料夾下定義了application.yml,內容如下:
my:
value: hello
定義一個配置類:
@Configuration
@Data
public class Config {
@Value("${my.value}")
private String myValue;
}
定義一個測試類:
public class Main {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Config.class);
Config config = context.getBean(Config.class);
System.out.println(config);
}
}
輸出:
Config(myValue=${my.value})
上面的程式碼做了幾件事情:
- 在
resources/application.yml
檔案中定義了my.value=hello
- 定義了一個
Config
類,利用@value
註解將hello
注入到欄位myValue
中 - 定義了一個
Main
類測試效果
測試類做了幾件事情:
- 使用
AnnotationConfigApplicationContext
這個容器載入配置類 - 獲取配置類
Config
- 輸出注入的欄位
myValue
從結果來看,並沒有注入成功,我的第一感覺就是沒有把我們的application.yml
檔案裡的內容載入到environment
裡面,那我們就來看看environment
裡面都有什麼內容,如下程式碼:
public class Main {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Config.class);
ConfigurableEnvironment environment = context.getEnvironment();
System.out.println(environment);
}
}
從結果來看:
environment
並沒有包含我們application.yml
檔案裡的內容- 但它包含了其他兩個東西,分別是
systemProperties
和systemEnvironment
那我們就需要把application.yml
檔案裡的內容載入到environment
,需要考慮以下兩個問題:
- 怎麼解析
yml
檔案的內容 - 怎麼把解析的內容放到
environment
中
針對問題一:可以利用spring
自帶的YamlPropertySourceLoader
這個類的load()
方法,它會返回一個List<PropertySource<?>>
針對問題二:我們可以先來看一下預設的內容是怎麼放進去的,看一下getEnvironment()
的原始碼:
public abstract class AbstractApplicationContext extends DefaultResourceLoader
implements ConfigurableApplicationContext {
public ConfigurableEnvironment getEnvironment() {
if (this.environment == null) {
this.environment = createEnvironment();
}
return this.environment;
}
protected ConfigurableEnvironment createEnvironment() {
return new StandardEnvironment();
}
}
從上面可以看出預設建立的是一個StandardEnvironment
,我們再來看一下它的初始化:
public class StandardEnvironment extends AbstractEnvironment {
public static final String SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME = "systemEnvironment";
public static final String SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME = "systemProperties";
@Override
protected void customizePropertySources(MutablePropertySources propertySources) {
propertySources.addLast(
new PropertiesPropertySource(SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME, getSystemProperties()));
propertySources.addLast(
new SystemEnvironmentPropertySource(SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, getSystemEnvironment()));
}
}
public abstract class AbstractEnvironment implements ConfigurableEnvironment {
public AbstractEnvironment() {
this(new MutablePropertySources());
}
protected AbstractEnvironment(MutablePropertySources propertySources) {
this.propertySources = propertySources;
this.propertyResolver = createPropertyResolver(propertySources);
customizePropertySources(propertySources);
}
}
從上面程式碼可以看出,在StandardEnvironment.customizePropertySources()
的方法中,是通過propertySources.addLast()
方法新增進去的,那我們可以照葫蘆畫瓢,如下:
public class Main {
public static void main(String[] args) throws IOException {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Config.class);
ConfigurableEnvironment environment = context.getEnvironment();
System.out.println(environment);
YamlPropertySourceLoader loader = new YamlPropertySourceLoader();
List<PropertySource<?>> propertySources = loader.load("my-properties",
new FileSystemResource("/Users/tiger/spring-boot-study/src/main/resources/application.yml"));
environment.getPropertySources().addLast(propertySources.get(0));
System.out.println(environment);
}
}
從上面結果可以看出,我們已經成功把我們的application.yml
檔案內容放到environment
中了
那我們把測試程式碼改成:
public class Main {
public static void main(String[] args) throws IOException {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Config.class);
YamlPropertySourceLoader loader = new YamlPropertySourceLoader();
List<PropertySource<?>> propertySources = loader.load("my-properties",
new FileSystemResource("/Users/tiger/spring-boot-study/src/main/resources/application.yml"));
context.getEnvironment().getPropertySources().addLast(propertySources.get(0));
Config config = context.getBean(Config.class);
System.out.println(config);
}
}
輸出:
Config(myValue=${my.value})
從上面的結果可以看出,還是沒有得到我們想要的結果,這是因為conig
類會提前初始化,是在refresh()
方法中的finishBeanFactoryInitialization()
方法進行的,所以我們要在這一步之前把我們的內容放到environment
中
翻了一翻refresh()
這個方法,發現在prepareRefresh()
這個方法裡有一個initPropertySources()
的方法,註釋寫著初始化一系列的資源,所以我們可以在這個方法裡面載入我們的配置檔案,於是變成:
public class Main {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Config.class) {
@SneakyThrows
@Override
public void initPropertySources() {
YamlPropertySourceLoader loader = new YamlPropertySourceLoader();
List<PropertySource<?>> propertySources = loader.load("my-properties",
new FileSystemResource("/Users/tiger/spring-boot-study/src/main/resources/application.yml"));
getEnvironment().getPropertySources().addLast(propertySources.get(0));
}
};
Config config = context.getBean(Config.class);
System.out.println(config);
}
}
輸出:
Config(myValue=hello)
到目前為止,我們模擬了@Value
注入成功的場景,專案裡面應該不會出現這種資源沒有載入的問題,因為這些事情spring boot
都幫我們做好了
所以直接在@Configuration
類下直接用@Value
是沒有問題的
模擬注入不成功的場景
現在我們就來模擬一下注入不成功的場景,配置類改成如下:
@Configuration
@Data
public class Config {
@Value("${my.value}")
private String myValue;
@Bean
public MyBeanFactoryPostProcessor myBeanFactoryPostProcessor() {
return new MyBeanFactoryPostProcessor();
}
public static class MyBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
}
}
}
輸出結果:
Config(myValue=null)
這就是我專案上遇到的問題,在配置類中再生成一個BeanFactoryPostProcessor
後,@Value
就注入不成功了
但只要把這個方法寫成static
就可以了,如下:
@Configuration
@Data
public class Config {
@Value("${my.value}")
private String myValue;
@Bean
public static MyBeanFactoryPostProcessor myBeanFactoryPostProcessor() {
return new MyBeanFactoryPostProcessor();
}
public static class MyBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
}
}
}
輸出結果:
Config(myValue=hello)
看看為什麼沒有注入成功
@Value
是由AutowiredAnnotationBeanPostProcessor.postProcessProperties()
處理的,所以我們就以這裡為入口進行除錯。
我們先把static
去掉:
發現沒有執行到上述方法,那我們再把static
加上,看一下成功的情況:
可以看到,是可以到這個方法的,而且知道這個方法是被AbstractAutowireCapableBeanFactory.populateBean()
呼叫的,我們再看一下這裡的情況:
從上圖可以看出,getBeanPostProcessorCache().instantiationAware
是有AutowiredAnnotationBeanPostProcessor
這個例項的
那我們再來看一下不加static
這裡的情況:
果然,沒有注入成功的原因是在建立config
例項的時候,還沒有建立AutowiredAnnotationBeanPostProcessor
例項
我們來看一下這個getBeanPostProcessorCache().instantiationAware
是什麼東西,又是如何生成的
發現只有在AbstractBeanFactory.getBeanPostProcessorCache()
這個方法會將InstantiationAwareBeanPostProcessor
新增到instantiationAware
,如下:
public abstract class AbstractBeanFactory extends FactoryBeanRegistrySupport implements ConfigurableBeanFactory {
BeanPostProcessorCache getBeanPostProcessorCache() {
BeanPostProcessorCache bpCache = this.beanPostProcessorCache;
if (bpCache == null) {
bpCache = new BeanPostProcessorCache();
for (BeanPostProcessor bp : this.beanPostProcessors) {
if (bp instanceof InstantiationAwareBeanPostProcessor) {
bpCache.instantiationAware.add((InstantiationAwareBeanPostProcessor) bp);
if (bp instanceof SmartInstantiationAwareBeanPostProcessor) {
bpCache.smartInstantiationAware.add((SmartInstantiationAwareBeanPostProcessor) bp);
}
}
if (bp instanceof DestructionAwareBeanPostProcessor) {
bpCache.destructionAware.add((DestructionAwareBeanPostProcessor) bp);
}
if (bp instanceof MergedBeanDefinitionPostProcessor) {
bpCache.mergedDefinition.add((MergedBeanDefinitionPostProcessor) bp);
}
}
this.beanPostProcessorCache = bpCache;
}
return bpCache;
}
}
從上面的程式碼看出,本質還是從this.beanPostProcessors
獲取的,我們來看一下什麼時候會把AutowiredAnnotationBeanPostProcessor
新增到容器中,如下:
從上圖可知:AutowiredAnnotationBeanPostProcessor
是在refresh()
方法中的registerBeanPostProcessors()
方法注入的
我們再來看一下加static
方法的config
類是什麼時候載入的:
再來看一下不加static
方法的config
類是什麼時候載入的
我們來總結一下提到的方法在refresh()
方法中的順序:
invokeBeanFactoryPostProcessors(); ——> 不加static的時候,在這一步載入config類
registerBeanPostProcessors(); ——> 註冊AutowiredAnnotationBeanPostProcessor
finishBeanFactoryInitialization(); 加static的時候,在這一步載入config類
所以我們就知道原因了:當不加static
欄位時候,載入config
類的時候,我們的AutowiredAnnotationBeanPostProcessor
還沒有註冊,所以就會不成功,而當加上static
後,我們載入config
類的時候,我們的AutowiredAnnotationBeanPostProcessor
已經註冊好了。
為什麼加static
和不加static
的載入順序是不一樣的呢
spring
容器會在invokeBeanFactoryPostProcessors()
這一步會載入所有的BeanFactoryPostProcessor
,如果用static
修飾的話,則不會載入config
類,反之會載入。原因如下:
上圖已經給出了原因,如果生成bean
的工廠方法是static
方法就不會載入,反之會載入。
我們不加static
,能不能也讓它注入成功呢?
那無非就是在載入config
類之前,把AutowiredAnnotationBeanPostProcessor
提前載入到容器就可以了,那我們來看一下原始碼是怎麼載入這個例項的:
我們同樣可以依葫蘆畫瓢,看看在哪裡提前載入比較合適,發現postProcessBeanFactory()
這個方法比較合適,於是改成:
public class Main {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Config.class) {
@SneakyThrows
@Override
public void initPropertySources() {
YamlPropertySourceLoader loader = new YamlPropertySourceLoader();
List<PropertySource<?>> propertySources = loader.load("my-properties",
new FileSystemResource("/Users/tiger/spring-boot-study/src/main/resources/application.yml"));
getEnvironment().getPropertySources().addLast(propertySources.get(0));
}
@Override
protected void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {
String ppName = "org.springframework.context.annotation.internalAutowiredAnnotationProcessor";
beanFactory.addBeanPostProcessor(getBean(ppName, BeanPostProcessor.class));
}
};
Config config = context.getBean(Config.class);
System.out.println(config);
}
}
輸出:
Config(myValue=${my.value})
從結果來看,還是沒注入成功啊,經過一番除錯,發現是在下面步驟中出了問題:
我們來看一下載入成功的情況:
embeddedValueResolver
是在下面步驟中被新增進去的:
可以看出是在refresh()
中的finishBeanFactoryInitialization()
這個方法裡面新增進去的,所以我們也要提前搞一下:
public class Main {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Config.class) {
@SneakyThrows
@Override
public void initPropertySources() {
YamlPropertySourceLoader loader = new YamlPropertySourceLoader();
List<PropertySource<?>> propertySources = loader.load("my-properties",
new FileSystemResource("/Users/tiger/spring-boot-study/src/main/resources/application.yml"));
getEnvironment().getPropertySources().addLast(propertySources.get(0));
}
@Override
protected void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {
String ppName = "org.springframework.context.annotation.internalAutowiredAnnotationProcessor";
beanFactory.addBeanPostProcessor(getBean(ppName, BeanPostProcessor.class));
beanFactory.addEmbeddedValueResolver(strVal -> getEnvironment().resolvePlaceholders(strVal));
}
};
Config config = context.getBean(Config.class);
System.out.println(config);
}
}
輸出:
Config(myValue=hello)
好了,大功告成!
總結
看到這裡,相信大家都知道@Value
為什麼載入不成功了吧,主要就是因為載入順序的關係,可以看出最簡單的方法就是在方法上加一個static
,後面的探究主要是地對Spring容器
載入順序的理解
本文探究的是在配置類裡存在BeanFactoryPostProcessor
,如果換成BeanPostProcessor
呢?同樣會載入不成功嗎?又是因為什麼原因呢?其實也可以用同樣的方法來測試,和本文講的如出一轍,小夥伴們可自行探究一下。
有什麼問題歡迎一起探討~~~