【SpringBoot】@Value 没有注入预期的值
问题复现
在装配对象成员属性时,我们常常会使用 @Autowired 来装配。但是,有时候我们也使用 @Value 进行装配。不过这两种注解使用风格不同,使用 @Autowired 一般都不会设置属性值,而 @Value 必须指定一个字符串值,因为其定义做了要求,定义代码如下:public @interface Value {
String value ( ) ;
}
另外在比较这两者的区别时,我们一般都会因为 @Value 常用于 String 类型的装配而误以为 @Value 不能用于非内置对象的装配,实际上这是一个常见的误区。例如,我们可以使用下面这种方式来 Autowired 一个属性成员:@Value ( "#{student}" )
private Student student;
其中 student 这个 Bean 定义如下:@Bean
public Student student ( ) {
Student student = createStudent ( 1 , "xie" ) ;
return student;
}
当然,正如前面提及,我们使用 @Value 更多是用来装配 String,而且它支持多种强大的装配方式,典型的方式参考下面的示例:
@Value ( "我是字符串" )
private String text;
@Value ( "${ip}" )
private String ip
@Value ( "#{student.name}" )
private String name;
上面我给你简单介绍了 @Value 的强大功能,以及它和 @Autowired 的区别。那么在使用 @Value 时可能会遇到那些错误呢?这里分享一个最为典型的错误,即使用 @Value 可能会注入一个不是预期的值。 我们可以模拟一个场景,我们在配置文件 application.properties 配置了这样一个属性:username=admin
password=pass
然后我们在一个 Bean 中,分别定义两个属性来引用它们:@RestController
@Slf4j
public class ValueTestController {
@Value ( "${username}" )
private String username;
@Value ( "${password}" )
private String password;
@RequestMapping ( path = "user" , method = RequestMethod . GET )
public String getUser ( ) {
return username + ":" + password;
} ;
}
当我们去打印上述代码中的 username 和 password 时,我们会发现 password 正确返回了,但是 username 返回的并不是配置文件中指明的 admin,而是运行这段程序的计算机用户名。很明显,使用 @Value 装配的值没有完全符合我们的预期。
案例解析
通过分析运行结果,我们可以知道 @Value 的使用方式应该是没有错的,毕竟 password 这个字段装配上了,但是为什么 username 没有生效成正确的值?接下来我们就来具体解析下。 我们首先了解下对于 @Value,Spring 是如何根据 @Value 来查询“值”的。我们可以先通过方法 DefaultListableBeanFactory#doResolveDependency 来了解 @Value 的核心工作流程,代码如下:@Nullable
public Object doResolveDependency ( DependencyDescriptor descriptor, @Nullable String beanName,
@Nullable Set < String > autowiredBeanNames, @Nullable TypeConverter typeConverter) throws BeansException {
Class < ? > type = descriptor. getDependencyType ( ) ;
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 ( ) ) ;
}
catch ( UnsupportedOperationException ex) {
}
}
}
可以看到,@Value 的工作大体分为以下三个核心步骤。
寻找 @value
在这步中,主要是判断这个属性字段是否标记为 @Value,依据的方法参考 QualifierAnnotationAutowireCandidateResolver#findValue:@Nullable
protected Object findValue ( Annotation [ ] annotationsToSearch) {
if ( annotationsToSearch. length > 0 ) {
AnnotationAttributes attr = AnnotatedElementUtils . getMergedAnnotationAttributes (
AnnotatedElementUtils . forAnnotations ( annotationsToSearch) , this . valueAnnotationType) ;
if ( attr != null ) {
return extractValue ( attr) ;
}
}
return null ;
}
解析 @Value 的字符串值
如果一个字段标记了 @Value,则可以拿到对应的字符串值,然后就可以根据字符串值去做解析,最终解析的结果可能是一个字符串,也可能是一个对象,这取决于字符串怎么写。 将解析结果转化为要装配的对象的类型
当拿到第二步生成的结果后,我们会发现可能和我们要装配的类型不匹配。假设我们定义的是 UUID,而我们获取的结果是一个字符串,那么这个时候就会根据目标类型来寻找转化器执行转化,字符串到 UUID 的转化实际上发生在 UUIDEditor 中:public class UUIDEditor extends PropertyEditorSupport {
@Override
public void setAsText ( String text) throws IllegalArgumentException {
if ( StringUtils . hasText ( text) ) {
setValue ( UUID . fromString ( text. trim ( ) ) ) ;
}
else {
setValue ( null ) ;
}
}
}
通过对上面几个关键步骤的解析,我们大体了解了 @Value 的工作流程。结合我们的案例,很明显问题应该发生在第二步,即解析 Value 指定字符串过程,执行过程参考下面的关键代码行:String strVal = resolveEmbeddedValue ( ( String ) value) ;
这里其实是在解析嵌入的值,实际上就是“替换占位符”工作。具体而言,它采用的是 PropertySourcesPlaceholderConfigurer 根据 PropertySources 来替换。不过当使用 ${username} 来获取替换值时,其最终执行的查找并不是局限在 application.property 文件中的。通过调试,我们可以看到下面的这些“源”都是替换依据: [ConfigurationPropertySourcesPropertySource {name='configurationProperties'},
StubPropertySource {name='servletConfigInitParams'}, ServletContextPropertySource {name='servletContextInitParams'}, PropertiesPropertySource {name='systemProperties'}, OriginAwareSystemEnvironmentPropertySource {name='systemEnvironment'}, RandomValuePropertySource {name='random'},
OriginTrackedMapPropertySource {name='applicationConfig: classpath:/application.properties]'},
MapPropertySource {name='devtools'}]
而具体的查找执行,我们可以通过下面的代码(PropertySourcesPropertyResolver#getProperty)来获取它的执行方式:@Nullable
protected < T > T getProperty ( String key, Class < T > targetValueType, boolean resolveNestedPlaceholders) {
if ( this . propertySources != null ) {
for ( PropertySource < ? > propertySource : this . propertySources) {
Object value = propertySource. getProperty ( key) ;
if ( value != null ) {
return convertValueIfNecessary ( value, targetValueType) ;
}
}
}
return null ;
}
从这可以看出,在解析 Value 字符串时,其实是有顺序的(查找的源是存在 CopyOnWriteArrayList 中,在启动时就被有序固定下来),一个一个“源”执行查找,在其中一个源找到后,就可以直接返回了。 如果我们查看 systemEnvironment 这个源,会发现刚好有一个 username 和我们是重合的,且值不是 pass。 所以,讲到这里,你应该知道问题所在了吧?这是一个误打误撞的例子,刚好系统环境变量(systemEnvironment)中含有同名的配置。实际上,对于系统参数(systemProperties)也是一样的,这些参数或者变量都有很多,如果我们没有意识到它的存在,起了一个同名的字符串作为 @Value 的值,则很容易引发这类问题。
问题修正
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/952761.html
如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!