@Autowire可以用来注入bean依赖,@Value则用于注入属性值。本章介绍@Value的使用方法,属性值来源以及属性值类型转换的原理。
回顾PropertySourcesPlaceholderConfigurer
在介绍BeanFactoryPostProcessor的时候,我们讲过,可以通过向容器注册一个PropertySourcesPlaceholderConfigurer来替换属性值占位符,下面是非常典型的一个配置。
<beans>
<bean class= "com.example.ExampleBean">
<property name="pname" value=”${pname.value}“/>
</bean>
<beans>
<bean class="org.springframework.beans.factory.config.PropertySourcesPlaceholderConfigurer">
<property name="locations">
<value>classpath:com/something/strategy.properties</value>
</property>
</bean/
//strategy.propertie文件内容
pname.value=This is a great value
PropertySourcesPlaceholderConfigurer是一个BeanFactoryPostProcessor,在容器加载完xml之后,实例化bean之前,他会替换所有BeanDefinition包含的占位符(上面将${pname.value}
替换为This is a great value
。
@Value
@Value注解的工作原理有所不同,我们应该将@Value看成是和@Autowire的类似的东西,它是以依赖注入的方式工作的,只不过,前者注入动态计算的属性值,后者注入依赖的bean。
基本上,所有能够用@Autowire的地方,也可以使用@Value,只要对应的java类型可以从String转换而来。实际上@Autowire和@Value是被同一个BeanPostProcessor处理的,它是AutowiredAnnotationBeanPostProcessor。
下面的代码展示了@Value的基本用法,${...}
这个模式表明这是一个属性名,在ExampleBean实例化的时候,容器应当注入对应的属性值。
@Component
public class ExampleBean {
@Value("${property.value}")
private String value1;
}
字面量
@Value还可以包含字面量,就像下面这样:
@Value("this is literal value")
private String value1;
只不过这种定义没啥意义;字面量可以和占位符混合:
@Value("This is good value: ${property.value}")
private String value1;
SPEL表达式
@Value中可以包含SPEL表达式,它形如#{}
:
@Value(”#{systemProperties['property']")
private String value1;
SPEL表达式是Spring定义的一种DSL,它本身是一个很大的主题,本章不会讲解;本章后续集中讲解@Value注入属性值的原理,因为这是实际项目中使用最多的方式。
属性来源
StringValueResolver
上一节说@Value注解是被AutowiredAnnotationBeanPostProcessor所处理的,但最终是交给ApplicationContext内的StringValueResolver来解析;StringValueResolver的定义如下,它在context内的作用是将属性占位符解析为属性值:
@FunctionalInterface
public interface StringValueResolver {
@Nullable
String resolveStringValue(String strVal);
}
默认StringValueResolver
容器内默认的StringValueResolver就是从上一章讲解的Environment里面查找属性。如果所有的属性值都被配置到Environment里面,那么不需要配置PropertySourcesPlaceholderConfigurer,@Value就能正常工作。
如果Environment没有找到某个属性,那么@Value里面的占位符就不会被替换,也不会报错。
PropertySourcesPlaceholderConfigurer
PropertySourcesPlaceholderConfigurer实际上干了两件事,第一件事就是把BeanDefinition里面的占位符替换一遍,另一件事就是向容器提注册一个StringValueResolver(替代默认的),以支持@Value注解。
下面的代码定义了一个典型的PropertySourcesPlaceholderConfigurer:
@Configuration
public class AppConfig {
//注意,定义BeanFactryPostProcessor的方法是static的
@Bean
public static PropertySourcesPlaceholderConfigurer placeholderConfigurer() {
PropertySourcesPlaceholderConfigurer configurer = new PropertySourcesPlaceholderConfigurer();
configurer.setLocation(new ClassPathResource("propertyConfigure.properties"));
return configurer;
}
}
PropertySourcesPlaceholderConfigure会从两个地方查询属性值,一是Environment,一是本地属性;所谓本地属性,指PropertySourcesPlaceholderConfigure配置的属性源(configurer.setLocation
),该属性源不会被添加到Environment里面。
PropertySourcesPlaceholderConfigurer还允许我们更精细地控制属性占位符的替换行为:
//本地属性是否优先于Environment,默认false
configurer.setLocalOverride();
//是否忽略未解析的占位符,默认false,即会抛出异常
configurer.setIgnoreUnresolvablePlaceholders();
//占位符统一的前缀&后缀,默认空
configurer.setPlaceholderPrefix();
configurer.setPlaceholderSuffix();
PropertyPlaceholderConfigurer
Spring里还有一个类似的类叫做PropertyPlaceholderConfigurer,这个是Spring 3.0之前使用的;PropertySourcesPlaceholderConfigurer正是用来取代它的。
PropertyPlaceholderConfigurer的区别是不会从Environment里面查询属性值,只会从java系统属性里面查询。
在新版本中,我们不应该再使用PropertyPlaceholderConfigurer,但它并没有被声明为Deprecated,而且类名又非常相似,需要注意一下,避免混淆。
类型转换
如果@Value修饰的变量或参数类型,不是String,那么需要执行类型转换。由于各种原因,Spring实际上支持3个相关技术体系,分别是PropertyEditor,Converter,Formmatter。这是Spring一个非常绕的技术点,本文试着把他讲清楚。
官方文档有一个章节专门讲解Spring的类型转换的技术原理,位于这里。这个系列文章将这部分类容拆散了,将涉及@Value注解的部分放到本章,因为讲完用法接着讲背后原理,大家更容易明白。
类型转换需求
假设我们有一个Java类型Person,定义如下:
public class Person {
protected String name;
protected int age;
public void setName(String name) {
this.name = name;
}
public void setAge(int age) {
this.age = age;
}
}
属性文件定义了一个属性:property.person = Iron Man:50
。
如果要通过@Value注入Person类型的属性,那么String类型的属性值必须转换成Person类型对象:
public class ExampleBean {
@Value("${property.persion}")
private Person person;
}
PropertyEditor
PropertyEditor是java bean规范关于如何对bean的属性进行编辑而定义的接口,Spring支持这个规范。相关的文档可以看看这里。
这个规范涉及的东西比较多,我们只考虑如何从String类型转换到我们想要的bean属性类型。基本的思路是对于每种类型Type,有一个对应的PropertyEditor,命名为TypeEditor。
Spring内置了一批PropertyEditor,见下表:
PropertyEditor | 说明 |
---|---|
ByteArrayPropertyEditor | 将字符串转换byte数组 |
ClassEditor | Class对象 |
CustomBooleanEditor | Boolean值 |
CustomCollectionEditor | 集合类型 |
CustomDateEditor | 处理Date,默认未注册,应用需指定日期格式并注册 |
CustomNumberEditor | 支持NSNumber的子类型 |
FileEditor | 支持java.io.File |
InputStreamEditor | 打开字符串指向的Resource,并打开一个InputStream,需要使用者来关闭这个InputStream |
LocaleEditor | 支持Locale |
PatternEditor | 支持java.util.regex.Pattern |
PropertiesEditor | 支持java.util.Properties |
StringTrimmerEditor | trim字符串,默认未注册,应用需指定trim规则并注册 |
URLEditor | 支持java.net.URL |
实现PropertyEditor
PropertyEditor接口包含的方法比较多,我们只关注void setAsText(String text)
;我们一般不直接实现该接口,而是实现PropertyEditorSupport,此时只需要重写setAsText方法即可。
因此,Person对应的Editor定义如下:
public class PersonEditor extends PropertyEditorSupport {
@Override
public void setAsText(String text) throws IllegalArgumentException {
String[] components = text.split(":");
Person person = new Person();
person.setName(components[0]);
person.setAge(Integer.parseInt(components[1]));
person.setResolver("PersonEditor");
setValue(person);
}
}
注册PropertyEditor
PropertyEditor需要注册到容器才能生效,我们可以直接调用context.getBeanFactory().registerCustomEditor();
来注册,但是更好的方式是声明一个CustomEditorConfigurer:
@Configuration
public class AppConfig {
@Bean
public static CustomEditorConfigurer editorConfigurer() {
CustomEditorConfigurer editorConfigurer = new CustomEditorConfigurer();
editorConfigurer.setCustomEditors(Collections.singletonMap(Person, PersonEditor.class));
return editorConfigurer;
}
}
CustomEditorConfigurer是一个BeanFactoryPostProcessor,它能注册多个PropertyEditor到容器。
Java Bean规范有一个规则是,如果Type和TypeEditor位于同一个package下面,那么这个PropertyEditor会自动被扫描到,因此如果PersonEditor和Person的包名一致,那么不需要做任何注册操作。
Converter
Converter是Spring 3.0提供的一套类型转换系统,它适用的场景比PropertyEditor更多。
Converter是一个泛型接口,其定义如下,实现者必须保证该接口是线程安全的,如果发现source参数不合法,请抛出IllegalArgumentException:
public interface Converter<S, T> {
T convert(S source);
}
org.springframework.core.convert.support
下面有Spring内置的一系列Converter实现。
ConverterFactory
如果想集中实现到某个类型及其子类型的converter,可以实现ConverterFactory接口。
public interface ConverterFactory<S, R> {
<T extends R> Converter<S, T> getConverter(Class<T> targetType);
}
Spring内置了一个非常典型的实现:StringToEnumConverterFactory,可以实现String到所有枚举的转换。
GenericConverter
GenericConverter用来实现更复杂的类型转换逻辑,它的定义如下:
public interface GenericConverter {
public Set<ConvertiblePair> getConvertibleTypes();
Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType);
}
getConvertibleTypes方法查询Converter支持的所有转换,convert方法的TypeDescriptor参数可以提供更多的信息给转换逻辑。
GenericConverter有个子接口ConditionalConverter,可以依据一些运行时的条件来决定是否执行转换。
注册Converter
Spring定义了ConversionService接口,来统一所有的转换需求,它的定义如下:
public interface ConversionService {
boolean canConvert(Class<?> sourceType, Class<?> targetType);
<T> T convert(Object source, Class<T> targetType);
boolean canConvert(TypeDescriptor sourceType, TypeDescriptor targetType);
Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType)
}
Spring容器默认并没有注册ConversionService,要想使用Converter机制,我们需要手动声明这个bean。可以使用GenericConversionService这个实现类型,更好的方式是使用ConversionServiceFactoryBean。
@Configuration
public class AppConfig {
@Bean("conversionService")
public ConversionServiceFactoryBean conversionService() {
ConversionServiceFactoryBean factoryBean =new ConversionServiceFactoryBean();
factoryBean.setConverters(xxx);
return factoryBean;
}
}
ConversionServiceFactoryBean是一个factoryBean,它创建的bean类型是DefaultConversionService,后者注册了Spring内置的所有Converter;外加自定义的Converter。无论是Converter、GenericConverter还是ConverterFactory,打包成一个集合作为参数调用ConversionServiceFactoryBean.setConverters方法即可。
注意这个bean的名字必须为conversionService
,否则容器不会识别,(调试一下Spring源码就能知道,容器在初始化的过程中,直接通过"conversionService"来定位这个bean)。
PropertyEditor和Converter的优先级
如果没有定义conversionService,那么@Value进行的类型转换,全部通过PropertyEditor来进行。如果定义了conversionService,那么优先级顺序如下:
- 客户代码添加的Converter
- 客户代码添加的PropertyEditor
- Spring内置的Converter
- Spring内置的PropertyEditor
放在Type同包名下的PropertyEditor,则与Spring内置的PropertyEditor同一优先级。可以看出,Spring的原则是,如果客户端代码手动添加了类型转换器,那么要优先使用。
Environment.getProperty
我们在向Spring的Environment查询属性值时,可以使用内置的Converter进行类型转换。
Type object = context.getEnvironment().getProperty("property.value", Type.class);
这个接口背后使用DefaultConversionService的静态实例来进行类型转换,这与注入到容器的ConversionService无关。因此该接口只能使用Spring内置Converter进行转换,非常地违反直觉且蛋疼。
Formatter
Converter机制是一种通用的类型转换机制,Formatter要解决的是客户端环境(比如web应用)下的本地化文本到后端类型的之间互相转换。Formatter接口是Printer和Parser接口的组合:
public interface Formatter<T> extends Printer<T>, Parser<T> {
}
public interface Printer<T> {
String print(T object, Locale locale);
}
public interface Parser<T> {
T parse(String text, Locale locale) throws ParseException;
}
实现一个PersonFormatter如下,我们仅用Formatter来支持属性注入,所以没有实现print方法:
public class PersonFormatter implements Formatter<Person> {
@Override
public Person parse(String text, Locale locale) throws ParseException {
String[] components = text.split(":");
Person person = new Person();
person.setName(components[0]);
person.setAge(Integer.parseInt(components[1]));
person.setResolver("PersonFormatter");
return person;
}
@Override
public String print(Person object, Locale locale) {
return null;
}
}
注册Formatter实现的方式是,将上面的ConversionService实现替换为FormattingConversionService,它统一管理Converter和Formatter:
@Configuration
public class AppConfig {
@Bean("conversionService")
public FormattingConversionServiceFactoryBean conversionService() {
FormattingConversionServiceFactoryBean factoryBean =new FormattingConversionServiceFactoryBean();
HashSet<Object> converts = new HashSet<>();
converts.add(new TeacherConverter());
converts.add(new StudentConverter());
factoryBean.setConverters(converts);
factoryBean.setFormatters(Collections.singleton(new PersonFormatter()));
return factoryBean;
}
}
在处理@Value注入的时候,如果容器有对应类型的Formatter,那么它的优先比Converter和PropertyEditor更高。
总结
在基于注解或Java代码的Spring配置风格里,@Value是注入bean属性值的首选方式。@Value接受一个表达式,这个表达式可以包含属性占位符、SPEL语言表达式、或字面值。当包含属性占位符时,它从配置的属性源里查询Property。
默认情况下@Value查询的目标属性源是Spring Environment;通过给容器配置一个PropertyPlaceholderConfigurer,可以使用额外的属性源,还可以定制相关行为。
@Value在注入属性的时候,有可能需要将属性从String转换到目标类型,Sping支持3种转换方式,PropertyEditor,Converter,Formmater。PropertyEditor是java bean规范的一部分,Converter和Formmater是Spring 3.0提出的类型转换子系统;虽然都能用于支持@Value属性注入,但各有特定设计目标,PropertyEditor支持传统的java bean属性编辑(java桌面应用开发),Converter是一种通用的类型转换机制,支持任意类型互相转换;Formmater的目标则是支持客户端本地化文本和后端对象之间的互相转换。
自动类型转换机制有点绕,但总体原则是:对于基本类型,Spring默认都会支持;手动注册的转换器总会优先(无论是PropertyEditor,Converter,还是Formmater),所以只要别对同一种类型注册多个转换器就不会出乱子。
注:@Value只用到了类型转换功能的一部分,在讲解Spring Web MVC的章节还会涉及这个话题。