深入理解 SpringBoot 中的 @Value 注解源码剖析
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
所修饰的方法的。
推荐阅读
-
深入理解Spring中的事务传播行为与隔离级别的详细剖析
-
深入理解Spring源码(二):探讨Spring框架中应用的设计模式
-
深入剖析 SpringCloud 中 @RefreshScope 注解的底层实现机制
-
深入理解Spring源码:配置类处理器(ConfigurationClassPostProcessor)详解(三) - @Import注解的处理机制
-
逐步深入剖析Spring框架中的IOC核心原理:每次研读都带来新理解与启发
-
深入理解SpringBoot(第二部分):启动过程的源代码剖析
-
深入理解Spring MVC中@Controller注解的源码执行流程:映射构建详解
-
深入理解Spring源码:启动过程完结篇——启动完成后的行为剖析
-
深入理解Spring源码:ContextLoaderListener的初始化过程剖析
-
深入理解 SpringBoot 中的 @Value 注解源码剖析