Spring基础十:@Value属性值注入

@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数组
ClassEditorClass对象
CustomBooleanEditorBoolean值
CustomCollectionEditor集合类型
CustomDateEditor处理Date,默认未注册,应用需指定日期格式并注册
CustomNumberEditor支持NSNumber的子类型
FileEditor支持java.io.File
InputStreamEditor打开字符串指向的Resource,并打开一个InputStream,需要使用者来关闭这个InputStream
LocaleEditor支持Locale
PatternEditor支持java.util.regex.Pattern
PropertiesEditor支持java.util.Properties
StringTrimmerEditortrim字符串,默认未注册,应用需指定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的章节还会涉及这个话题。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值