1、引言
在之前的《SpringBoot 自動裝配》文章中,我介紹了ConfigurationClassPostProcessor
這個類,它是 SpringBoot 作為擴展 Spring 一系列功能的基礎路口,它所衍生的ConfigurationClassParser
作為解析職責的基本處理類,涵蓋了各種解析處理的邏輯,如@Configuration
、@Bean
、@Import
、@ImportSource
、@PropertySource
、@ComponentScan
等注解都在這個解析類中完成。由于ConfigurationClassPostProcessor
是BeanDefinitionRegistryPostProcessor
的實現類,于是其解析時機是在AbstractApplicationContext#invokeBeanFactoryPostProcessors
方法中,并且是在處理BeanFactoryPostProcessor
之前。
以上的注解,都是將 bean 信息注入到 Spring 容器,那么當我們需要讀取配置文件的信息時,則需要使用到@Value
或者@ConfigurationProperties
注解。那么接下來,我們就深入源碼,了解@Value
的實現機制。
2、原理
在探索它的實現原理之前,我們首先定位關鍵字然后反推代碼邏輯。我們通過搜索 "Value.class" 進行反推:
找到了一個看起來像是調用點的地方,進入
QualifierAnnotationAutowireCandidateResolver
,查看其類的注釋說明:
/**
* {@link AutowireCandidateResolver} implementation that matches bean definition qualifiers
* against {@link Qualifier qualifier annotations} on the field or parameter to be autowired.
* Also supports suggested expression values through a {@link Value value} annotation.
*
* <p>Also supports JSR-330's {@link javax.inject.Qualifier} annotation, if available.
*
* @author Mark Fisher
* @author Juergen Hoeller
* @author Stephane Nicoll
* @since 2.5
* @see AutowireCandidateQualifier
* @see Qualifier
* @see Value
*/
大致的意思是,它是AutowireCandidateResolver
的實現類,用于匹配在屬性或者方法參數上的@Qualifier
注解所需要的 bean 信息;同時支持@Value
注解中的表達式解析。
于是我們可以肯定QualifierAnnotationAutowireCandidateResolver
就是我們要找的處理類,它負責處理@Qualifier
和@Value
兩個注解的取值操作。接下來我們看處理@Value
的getSuggestedValue
方法:
@Override
public Object getSuggestedValue(DependencyDescriptor descriptor) {
// 在屬性上查找注解信息
Object value = findValue(descriptor.getAnnotations());
if (value == null) {
MethodParameter methodParam = descriptor.getMethodParameter();
if (methodParam != null) {
// 在方法屬性上查找注解信息
value = findValue(methodParam.getMethodAnnotations());
}
}
return value;
}
/**
* Determine a suggested value from any of the given candidate annotations.
*/
protected Object findValue(Annotation[] annotationsToSearch) {
if (annotationsToSearch.length > 0) { // qualifier annotations have to be local
// 查找 @Value 的注解信息
AnnotationAttributes attr = AnnotatedElementUtils.getMergedAnnotationAttributes(
AnnotatedElementUtils.forAnnotations(annotationsToSearch), this.valueAnnotationType);
if (attr != null) {
// 返回注解中的表達式
return extractValue(attr);
}
}
return null;
}
該方法的目的是獲取@Value
注解中的表達式,查找范圍是在目標類的屬性和方法參數上。
現在要解決兩個疑問:
- 表達式對應的值是在哪里被替換的?
- 表達式替換后的值又是如何與原有的 bean 整合的?
帶著這兩個疑問,我們順著調用棧繼續查找線索,發現getSuggestedValue
方法是被DefaultListableBeanFactory#doResolveDependency
方法調用了:
public Object doResolveDependency(DependencyDescriptor descriptor, String beanName,
Set<String> autowiredBeanNames, TypeConverter typeConverter) throws BeansException {
InjectionPoint previousInjectionPoint = ConstructorResolver.setCurrentInjectionPoint(descriptor);
try {
Object shortcut = descriptor.resolveShortcut(this);
if (shortcut != null) {
return shortcut;
}
Class<?> type = descriptor.getDependencyType();
// 獲取 @Value 中的表達式
Object value = getAutowireCandidateResolver().getSuggestedValue(descriptor);
if (value != null) {
if (value instanceof String) {
// 處理表達式,這里就會替換表達式的值
String strVal = resolveEmbeddedValue((String) value);
BeanDefinition bd = (beanName != null && containsBean(beanName) ? getMergedBeanDefinition(beanName) : null);
value = evaluateBeanDefinitionString(strVal, bd);
}
TypeConverter converter = (typeConverter != null ? typeConverter : getTypeConverter());
// 轉換為對應的類型,并且注入原有 bean 屬性或者方法參數中
return (descriptor.getField() != null ?
converter.convertIfNecessary(value, type, descriptor.getField()) :
converter.convertIfNecessary(value, type, descriptor.getMethodParameter()));
}
...
}
finally {
ConstructorResolver.setCurrentInjectionPoint(previousInjectionPoint);
}
}
了解 Spring 中getBean
流程的同學應該知道,DefaultListableBeanFactory#doResolveDependency
作用是處理 bean 中的依賴。由此可見,處理@Value
注解的時機是在getBean
方法中,即SpringApplication#run
的最后一步,實例化 bean。
當獲取@Value
注解中的表達式之后,進入了resolveEmbeddedValue
方法,來替換表達式的值:
public String resolveEmbeddedValue(String value) {
if (value == null) {
return null;
}
String result = value;
// 遍歷 StringValueResolver
for (StringValueResolver resolver : this.embeddedValueResolvers) {
result = resolver.resolveStringValue(result);
if (result == null) {
return null;
}
}
return result;
}
通過代碼邏輯我們看到,對于屬性的解析已經委托給了StringValueResolver
對應的實現類,接下來我們就要分析一下這個StringValueResolver
是如何初始化的。
2.1 初始化 StringValueResolver
StringValueResolver
功能實現依賴 Spring 的切入點是PropertySourcesPlaceholderConfigurer
,我們看一下它的結構。
它的關鍵是實現了
BeanFactoryPostProcessor
接口,從而利用實現對外擴展函數postProcessBeanFactory
來進行對 Spring 的擴展:
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
if (this.propertySources == null) {
this.propertySources = new MutablePropertySources();
if (this.environment != null) {
this.propertySources.addLast(
new PropertySource<Environment>(ENVIRONMENT_PROPERTIES_PROPERTY_SOURCE_NAME, this.environment) {
@Override
public String getProperty(String key) {
return this.source.getProperty(key);
}
}
);
}
try {
PropertySource<?> localPropertySource =
new PropertiesPropertySource(LOCAL_PROPERTIES_PROPERTY_SOURCE_NAME, mergeProperties());
if (this.localOverride) {
this.propertySources.addFirst(localPropertySource);
}
else {
this.propertySources.addLast(localPropertySource);
}
}
catch (IOException ex) {
throw new BeanInitializationException("Could not load properties", ex);
}
}
// 創建替換 ${...} 表達式的處理器
processProperties(beanFactory, new PropertySourcesPropertyResolver(this.propertySources));
this.appliedPropertySources = this.propertySources;
}
上面的核心步驟是processProperties(beanFactory, new PropertySourcesPropertyResolver(this.propertySources))
,這里會創建處理 ${...} 表達式的StringValueResolver
:
protected void processProperties(ConfigurableListableBeanFactory beanFactoryToProcess,
final ConfigurablePropertyResolver propertyResolver) throws BeansException {
// 設置占位符的前綴:"{"
propertyResolver.setPlaceholderPrefix(this.placeholderPrefix);
// 設置占位符的后綴:"}"
propertyResolver.setPlaceholderSuffix(this.placeholderSuffix);
// 設置默認值分隔符:":"
propertyResolver.setValueSeparator(this.valueSeparator);
// 生成處理 ${...} 表達式的處理器
StringValueResolver valueResolver = new StringValueResolver() {
@Override
public String resolveStringValue(String strVal) {
String resolved = (ignoreUnresolvablePlaceholders ?
propertyResolver.resolvePlaceholders(strVal) :
propertyResolver.resolveRequiredPlaceholders(strVal));
if (trimValues) {
resolved = resolved.trim();
}
return (resolved.equals(nullValue) ? null : resolved);
}
};
// 將處理器放入 Spring 容器
doProcessProperties(beanFactoryToProcess, valueResolver);
}
在上面的代碼中,resolvePlaceholders
表示如果變量無法解析則忽略,resolveRequiredPlaceholders
表示如果變量無法解析則拋異常(默認情況)。最后將生成的StringValueResolver
存入 Spring 容器中:
protected void doProcessProperties(ConfigurableListableBeanFactory beanFactoryToProcess,
StringValueResolver valueResolver) {
BeanDefinitionVisitor visitor = new BeanDefinitionVisitor(valueResolver);
String[] beanNames = beanFactoryToProcess.getBeanDefinitionNames();
for (String curName : beanNames) {
// Check that we're not parsing our own bean definition,
// to avoid failing on unresolvable placeholders in properties file locations.
if (!(curName.equals(this.beanName) && beanFactoryToProcess.equals(this.beanFactory))) {
BeanDefinition bd = beanFactoryToProcess.getBeanDefinition(curName);
try {
visitor.visitBeanDefinition(bd);
}
catch (Exception ex) {
throw new BeanDefinitionStoreException(bd.getResourceDescription(), curName, ex.getMessage(), ex);
}
}
}
// New in Spring 2.5: resolve placeholders in alias target names and aliases as well.
beanFactoryToProcess.resolveAliases(valueResolver);
// 將 StringValueResolver 存入 BeanFactory 中
beanFactoryToProcess.addEmbeddedValueResolver(valueResolver);
}
最后將StringValueResolver
實例注冊到ConfigurableListableBeanFactory
中,也就是在真正解析變量時使用的StringValueResolver
實例。
經過resolveEmbeddedValue
方法之后,我們就拿到了替換后的值,接下來就是與原 bean 進行整合了。其操作是在TypeConverter#convertIfNecessary
方法中,這里分為兩種情況:
- 如果目標類存在
@Value
修飾的屬性。
如:
@Configuration
public class RedisProperties {
@Value("${redis.url}")
private String url;
geter/setter...
}
該情況直接通過反射調用目標 bean 的Field.set
方法(注意,不是屬性對應的 set 方法),直接給屬性賦值。
- 如果目標類不存在
@Value
修飾的屬性。
如:
@Configuration
public class RedisProperties {
@Value("${redis.url}")
public void resolveUrl(String redisUrl){
...
}
}
該情況依舊使用反射,調用Method.invoke
方法,給方法參數進行賦值。
2.2 Enviroment 的初始化
這里面有一個關鍵點,就是在初始化MutablePropertySources
的時候依賴的一個變量enviroment
。Enviroment 是 Spring 所有配置文件轉換為 KV 的基礎,而后續的一系列操作都是在enviroment
基礎上做的進一步封裝,那么我們就來探索一下enviroment
的初始化時機。
enviroment
的初始化過程并不是之前通用的在 PostProcessor 類型的擴展接口上做擴展,而是通過ConfigFileAoolicationListener
監聽機制完成的。我們看其onApplicationEvent
監聽方法:
public void onApplicationEvent(ApplicationEvent event) {
if (event instanceof ApplicationEnvironmentPreparedEvent) {
onApplicationEnvironmentPreparedEvent(
(ApplicationEnvironmentPreparedEvent) event);
}
if (event instanceof ApplicationPreparedEvent) {
onApplicationPreparedEvent(event);
}
}
當加載完成配置文件之后,SpringBoot 就會發布ApplicationEnvironmentPreparedEvent
事件,ConfigFileAoolicationListener
監聽到該事件之后,就會調用onApplicationEnvironmentPreparedEvent
方法:
private void onApplicationEnvironmentPreparedEvent(
ApplicationEnvironmentPreparedEvent event) {
List<EnvironmentPostProcessor> postProcessors = loadPostProcessors();
// 將 ConfigFileAoolicationListener 存入 postProcessors
postProcessors.add(this);
AnnotationAwareOrderComparator.sort(postProcessors);
// 遍歷執行 EnvironmentPostProcessor 的 postProcessEnvironment 方法
for (EnvironmentPostProcessor postProcessor : postProcessors) {
postProcessor.postProcessEnvironment(event.getEnvironment(),
event.getSpringApplication());
}
}
由于ConfigFileAoolicationListener
實現了EnvironmentPostProcessor
,于是這里首先將其納入postProcessors
,然后遍歷postProcessors
,執行其postProcessEnvironment
方法,于是ConfigFileApplicationListener#postProcessEnvironment
方法就會被執行:
@Override
public void postProcessEnvironment(ConfigurableEnvironment environment,
SpringApplication application) {
// 將配置文件信息存入 environment
addPropertySources(environment, application.getResourceLoader());
configureIgnoreBeanInfo(environment);
// 將 environment 與 Spring 應用上下文綁定
bindToSpringApplication(environment, application);
}
該方法的作用是將配置文件信息存入environment
,并將environment
與 Spring 應用上下文進行綁定。我們不妨深入addPropertySources
方法,繼續探討配置文件讀取流程,其核心流程是在ConfigFileApplicationListener.Loader#load()
方法中:
public void load() {
this.propertiesLoader = new PropertySourcesLoader();
this.activatedProfiles = false;
this.profiles = Collections.asLifoQueue(new LinkedList<Profile>());
this.processedProfiles = new LinkedList<Profile>();
// 通過 profile 標記不同的環境,可以通過設置 spring.profiles.active 和 spring.profiles.default。
// 如果設置了 active,default 便失去了作用。如果兩個都沒設置。那么帶有 profiles 標識的 bean 不會被創建。
Set<Profile> initialActiveProfiles = initializeActiveProfiles();
this.profiles.addAll(getUnprocessedActiveProfiles(initialActiveProfiles));
if (this.profiles.isEmpty()) {
for (String defaultProfileName : this.environment.getDefaultProfiles()) {
Profile defaultProfile = new Profile(defaultProfileName, true);
if (!this.profiles.contains(defaultProfile)) {
this.profiles.add(defaultProfile);
}
}
}
// 支持不添加任何 profile 注解的 bean 的加載
this.profiles.add(null);
while (!this.profiles.isEmpty()) {
Profile profile = this.profiles.poll();
// SpringBoot 默認從 4 個位置查找 application.properties/yml 文件
// classpath:/,classpath:/config/,file:./,file:./config/
for (String location : getSearchLocations()) {
if (!location.endsWith("/")) {
// location is a filename already, so don't search for more
// filenames
load(location, null, profile);
}
else {
for (String name : getSearchNames()) {
load(location, name, profile);
}
}
}
this.processedProfiles.add(profile);
}
addConfigurationProperties(this.propertiesLoader.getPropertySources());
}
這里涉及到我們以前經常用的 profile 機制,現在大部分公司都是使用配置中心(如 apollo)對配置文件統一管理的。SpringBoot 默認從 4 個位置查找 application.properties/yml 文件:classpath:/,classpath:/config/,file:./,file:./config/。
2.3 PropertySourcesPlaceholderConfigurer 的注冊
上面提到StringValueResolver
功能實現依賴 Spring 的切入點是PropertySourcesPlaceholderConfigurer
,那么它又是何時創建的呢?
我們搜索該類的調用棧,發現其在PropertyPlaceholderAutoConfiguration
中創建的:
@Configuration
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
public class PropertyPlaceholderAutoConfiguration {
@Bean
@ConditionalOnMissingBean(search = SearchStrategy.CURRENT)
public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() {
return new PropertySourcesPlaceholderConfigurer();
}
}
沒錯,他就是通過 SpringBoot 的自動裝配特性創建的。
3. 小結
@Value
的處理器StringValueResolver
初始化時機是PropertySourcesPlaceholderConfigurer#postProcessBeanFactory
中,而處理@Value
屬性解析的時機是在getBean
中的依賴處理resolveDependency
方法中。
4. 彩蛋
獲取配置文件信息除了@Value
以外,還可以使用@ConfigurationProperties
,它是 SpringBoot 特有的,關于用法讀者自己網上去搜,我這里只講大概的原理。
SpringBoot 通過自動裝配類ConfigurationPropertiesAutoConfiguration
引入了ConfigurationPropertiesBindingPostProcessorRegistrar
,它是ImportBeanDefinitionRegistrar
的實現類,其registerBeanDefinitions
方法會將ConfigurationPropertiesBindingPostProcessor
的 bean 信息注入 Spring 容器。而ConfigurationPropertiesBindingPostProcessor
是BeanPostProcessor
的實現類,于是會在 bean 實例化(調用 getBean
)之前,調用AbstractApplicationContext#registerBeanPostProcessors
方法,將其注冊為beanPostProcessors
。于是會在 bean 初始化之前,調用postProcessBeforeInitialization
方法,該方法會解析@ConfigurationProperties
注解,讀取enviroment
中的對應的配置,并且與當前對象進行綁定。
探討下@Value
和@Bean
的執行先后順序!
在本文中,我們知道@Value
屬性解析的時機是在@Value
所屬的配置類在進行getBean
時的依賴處理resolveDependency
方法中,而@Bean
注解的處理原理是,在refresh()
時的invokeBeanFactoryPostProcessors(beanFactory)
方法中,會根據@Bean
修飾的方法作為factory-method
(工廠方法),從而生成一個其返回值類型的BeanDefinition
信息,并且存入 Spring 容器中。在該 Bean 實例化的時候,即在getBean
時的createBeanInstance
方法中,會進行實例化操作,就會調用@Bean
修飾的方法。
于是@Value
和@Bean
的執行先后順序,取決于@Value
所屬的目標類和@Bean
修飾方法的返回類的加載先后順序,而 Spring 默認情況下,加載這些沒有依賴關系的 bean 是沒有順序的。要想干預他們的順序,就必須加一些手段了,比如@DependsOn
。
但是如果@Value
修飾的是@Bean
的方法,比如:
@Bean
@Value("${access.environment}")
public EnvironmentTool environmentTool(String env) {
EnvironmentTool environmentTool = new EnvironmentTool();
environmentTool.setEnv(env);
return environmentTool;
}
此時@Value
所屬的目標類為@Bean
修飾方法的返回類,由于getBean
的createBeanInstance
方法中,在處理factory-method
的時候,會調用instantiateUsingFactoryMethod
方法,其底層會調用resolveDependency
方法來處理其屬性的填充邏輯,比如@Value
的處理邏輯。最后會通過反射調用目標方法,即@Bean
修飾的方法邏輯。所以,當@Value
修飾的是@Bean
的方法時,@Value
的處理時機是早于@Bean
所修飾的方法的。