Spring常见问题解决 - @Value注解注入的值出错了?
- 一. @Value注解注入的值与预想不一致
- 1.1 案例
- 1.2 原理分析
- 1.2.1 寻找@Value
- 1.2.2 解析Value
- 1.3 解决方案
- 1.4 # 和 $ 的区别
- 二. 总结
- 2.1 总结
一. @Value注解注入的值与预想不一致
首先来说下@Value
这个注解,它的功能也就是给某个对象赋值。不过,我相信大部分人用它都是用来给String
类型赋值的。例如:
@Value("Hello")
private String name;
因为这个写法太常见了,以至于很多人可能不知道(包括我),@Value
还能用来做装配对象成员,即和@Autowired
一样的作用。例如:
自定义一个Bean
:
@Configuration
public class MyBean {
@Bean
public User getUser() {
User user = new User();
user.setName("LJJ");
return user;
}
}
然后通过@Value
装配:
@Value("#{getUser}")
private User user;
@Value("Hello")
private String name;
@Value("#{getUser.name}")
private String username;
@GetMapping("/hello")
@ResponseBody
public String hello() {
return user.getName();
}
结果如下:
那么接下来就来看下本文针对的案例:@Value
注解注入的值并非我们预期的值。
1.1 案例
1.首先我们在application.properties
文件中添加两个自定义的属性:
username=admin
password=pass
2.Controller
代码如下:
@Value("${username}")
private String username;
@Value("${password}")
private String password;
@GetMapping("/hello")
@ResponseBody
public String hello() {
return username + ": " + password;
}
运行结果如下:
输出的username
并不是配置文件中的admin
,而是我的计算机名。
1.2 原理分析
关于@Value
注解相关的装配,可以从DefaultListableBeanFactory.doResolveDependency()
类中开始看起。这个方法主要是类型匹配的一个通用方法。我在Spring源码系列:Bean的加载中也贴出了下面这行代码,只不过我写了个注释就一笔带过。
// 寻找注解Value
Object value = getAutowireCandidateResolver().getSuggestedValue(descriptor);
if (value != null) {
// 如果是String类型的,就赋值即可
if (value instanceof String) {
// 解析值
String strVal = resolveEmbeddedValue((String) value);
BeanDefinition bd = (beanName != null && containsBean(beanName) ?
getMergedBeanDefinition(beanName) : null);
value = evaluateBeanDefinitionString(strVal, bd);
}
// 否则可能是Object对象,需要做对应的类型转换
TypeConverter converter = (typeConverter != null ? typeConverter : getTypeConverter());
try {
// 进行装配,即将value转化为所需要的对象然后赋值。因为解析的结果可能是个对象也可能是个字符串
return converter.convertIfNecessary(value, type, descriptor.getTypeDescriptor());
}
catch (UnsupportedOperationException ex) {
// ...
}
}
1.2.1 寻找@Value
这里我们先看下debug
下的结果:
紧接着我们重点看下getSuggestedValue()
的实现:
public Object getSuggestedValue(DependencyDescriptor descriptor) {
// 根据描述符来寻找对应的注解
Object value = findValue(descriptor.getAnnotations());
// ...
return value;
}
可见,传入的参数descriptor:field user
。意思就是MyController
类下的一个字段属性。同时还能得到这个字段上有着对应的注解@Value
随后通过findValue
将注解中对应的值取出来:
protected Object findValue(Annotation[] annotationsToSearch) {
// 如果某个字段上的注解个数>0
if (annotationsToSearch.length > 0) {
// 查看这个注解是否为Value。这里的this.valueAnnotationType = Value.class;
AnnotationAttributes attr = AnnotatedElementUtils.getMergedAnnotationAttributes(
AnnotatedElementUtils.forAnnotations(annotationsToSearch), this.valueAnnotationType);
// 得到注解里面的值
if (attr != null) {
return extractValue(attr);
}
}
return null;
}
1.2.2 解析Value
从本文的案例出发,我们知道我的value
就是一个很简单的字符串:${username}
。那么我们需要从解析角度来看:
String strVal = resolveEmbeddedValue((String) value);
↓↓↓↓↓↓↓↓↓
public abstract class AbstractBeanFactory extends FactoryBeanRegistrySupport implements ConfigurableBeanFactory {
public String resolveEmbeddedValue(@Nullable String value) {
// ..
// 解析
result = resolver.resolveStringValue(result);
return result;
}
}
↓↓↓↓↓↓↓↓↓
private class PlaceholderResolvingStringValueResolver implements StringValueResolver {
public String resolveStringValue(String strVal) throws BeansException {
// 解析
String resolved = this.helper.replacePlaceholders(strVal, this.resolver);
if (trimValues) {
resolved = resolved.trim();
}
return (resolved.equals(nullValue) ? null : resolved);
}
}
↓↓↓↓↓↓↓↓↓
public String replacePlaceholders(String value, PlaceholderResolver placeholderResolver) {
// 解析
return parseStringValue(value, placeholderResolver, null);
}
↓↓↓↓↓↓↓↓↓
protected String parseStringValue(String value, PlaceholderResolver placeholderResolver, @Nullable Set<String> visitedPlaceholders) {
// this.placeholderPrefix = "${" ,如果以他为开头
int startIndex = value.indexOf(this.placeholderPrefix);
if (startIndex == -1) {
return value;
}
StringBuilder result = new StringBuilder(value);
while (startIndex != -1) {
// 找到占位符的结束位置。
int endIndex = findPlaceholderEndIndex(result, startIndex);
if (endIndex != -1) {
// 获取括号里的位置 ,以${username}为例,这里获取到的就是username
String placeholder = result.substring(startIndex + this.placeholderPrefix.length(), endIndex);
String originalPlaceholder = placeholder;
if (visitedPlaceholders == null) {
visitedPlaceholders = new HashSet<>(4);
}
if (!visitedPlaceholders.add(originalPlaceholder)) {
throw new IllegalArgumentException(
"Circular placeholder reference '" + originalPlaceholder + "' in property definitions");
}
// 递归调用本方法,因为属性键中可能仍然有占位符
placeholder = parseStringValue(placeholder, placeholderResolver, visitedPlaceholders);
// 获取属性键placeholder对应的属性值,这里将会得到结果jj.lin
String propVal = placeholderResolver.resolvePlaceholder(placeholder);
// 如果对应的属性值为空,做的对应处理
if (propVal == null && this.valueSeparator != null) {
// ...
}
// 做值的替换
if (propVal != null) {
// Recursive invocation, parsing placeholders contained in the
// previously resolved placeholder value.
propVal = parseStringValue(propVal, placeholderResolver, visitedPlaceholders);
result.replace(startIndex, endIndex + this.placeholderSuffix.length(), propVal);
// ...
}
else if (this.ignoreUnresolvablePlaceholders) {
// Proceed with unprocessed value.
startIndex = result.indexOf(this.placeholderPrefix, endIndex + this.placeholderSuffix.length());
}
// ...
}
else {
startIndex = -1;
}
}
return result.toString();
}
可以看到,上述代码的本质,就是占位符的一个替换操作。解析${xxx}
格式的字符串。将中间的内容提取出来得到对应的值,然后进行替换操作。
我们可以看到,值的寻找是通过以下函数来执行的:
String propVal = placeholderResolver.resolvePlaceholder(placeholder);
resolvePlaceholder
是一个接口,它的底层实现最终都是通过PropertySourcesPropertyResolver.getProperty()
来实现:
protected <T> T getProperty(String key, Class<T> targetValueType, boolean resolveNestedPlaceholders) {
if (this.propertySources != null) {
for (PropertySource<?> propertySource : this.propertySources) {
if (logger.isTraceEnabled()) {
logger.trace("Searching for key '" + key + "' in PropertySource '" +
propertySource.getName() + "'");
}
Object value = propertySource.getProperty(key);
if (value != null) {
if (resolveNestedPlaceholders && value instanceof String) {
value = resolveNestedPlaceholders((String) value);
}
logKeyFound(key, propertySource, value);
return convertValueIfNecessary(value, targetValueType);
}
}
}
if (logger.isTraceEnabled()) {
logger.trace("Could not find key '" + key + "' in any property source");
}
return null;
}
首先,会循环遍历不同的属性资源配置类,例如本案例debug
后,就有两个:
我们看下第一个环境变量相关的数据源:
这里存在一个相同名称的USERNAME
(忽略大小写),因此能够找到对应的变量值。同时根据代码逻辑,如果找到的value != null
,就会直接return
。而我们application.properties
中的值在哪呢?
1.3 解决方案
最简单的方法,但也是最需要引起注意和有一定规范的方法:自定义属性名遵循一定的规范,不要和系统变量重名了。 (user.name
也是不行的)
xxx.user.name=xxx
xxx.user.password-xxx
1.4 # 和 $ 的区别
在上文案例中,有这么两种Value
的写法:
@Value("#{getUser}")
@Value("${username}")
先说结论:
-
$
:去找外部配置的参数,将值赋过来。 -
#
:SpEL
表达式,去寻找对应变量的内容。
首先说下第一种 $
,上文分析的代码面向的就是这种类型:
- 如果不是以
${
为开头,就会直接返回。例如常规的@Valie("hello")
,就是单纯的将括号里面的值赋值给对象罢了。 -
${
为开头的表达式,则会去所有的配置属性里面去寻找对应的键值对。找到了则进行字符串的替换操作。
接下来说下如果是#
为开头的字符串该怎么办:
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());
try {
return converter.convertIfNecessary(value, type, descriptor.getTypeDescriptor());
}
如果我们解析出来的字符串是一个SpEL
表达式,那么就会通过以下这个函数进行二次解析:
value = evaluateBeanDefinitionString(strVal, bd);
原理如下:
public abstract class AbstractBeanFactory extends FactoryBeanRegistrySupport implements ConfigurableBeanFactory {
@Nullable
protected Object evaluateBeanDefinitionString(@Nullable String value, @Nullable BeanDefinition beanDefinition) {
// ...省略
return this.beanExpressionResolver.evaluate(value, new BeanExpressionContext(this, scope));
}
}
这里的this.beanExpressionResolver
,实际上是一个接口BeanExpressionResolver
,而这个接口的实现只有一个!就是StandardBeanExpressionResolver
,其evaluate
的具体实现如下:
public Object evaluate(@Nullable String value, BeanExpressionContext evalContext) throws BeansException {
if (!StringUtils.hasLength(value)) {
return value;
}
try {
Expression expr = this.expressionCache.get(value);
if (expr == null) {
// ... 不考虑为null的情况
}
StandardEvaluationContext sec = this.evaluationCache.get(evalContext);
if (sec == null) {
// ... 不考虑为null的情况
}
// 终极实现
return expr.getValue(sec);
}
catch (Throwable ex) {
throw new BeanExpressionException("Expression parsing failed", ex);
}
}
最后的 expr.getValue(sec);
这段代码,使用的是org.springframework.expression
包下的SpEL
表达式解析器相关逻辑。我们只要知道,Spring
本身就支持SpEL
表达式的解析。可能用于Bean
的自动装配过程。
二. 总结
2.1 总结
总结下就是:
- 对于
${}
这样的占位符,值的注入本质上就是字符串的替换操作。 - 对于
#{}
这样的占位符。值的注入本质上就是SpEL
表达式的计算。 - 对于
${}
的原理。先看这个字段上是否有@Value
注解,如果有拿到它的占位符。 - 如果这个占位符是字符串,就会进行对应的解析。去掉
${}
,拿中间的部分,例如本案例中的username
。 - 根据
username
去整个系统的数据源中寻找有相同名称的键值对 。返回第一个找到的值。 - 由于系统环境变量数据源
OriginAwareSystemEnvironmentPropertySource {name='systemEnvironment'}
,遍历顺序优先于OriginTrackedMapPropertySource {name='applicationConfig: [classpath:/application.properties]'}
这类外部数据源配置。 - 而系统变量数据源恰好有
username
这个属性,因此提前返回。导致最终注入的值和我们预想的不一致。 - 因此实际开发过程中,我们需要根据业务、场景、层级关系等因素,命名具有一定规范的变量名,即可大大降低这类自定义命名和系统变量命名重复的概率。