之前写了一篇关于Spring的@Value注入的文章《SpringBoot读取yml/properties的几种方法》里面列出了@Value和@ConfigurationProperties的对比,其中有一条是写的@Value不支持复杂类型封装(数组、Map、对象等)。
但是后来有小伙伴留言说他用@Value测试的时候,是可以注入的数组和集合的。于是我就跟着做了一些测试,发现确实可以。但是只有在以,分割的字符串的时候才可以。
为什么用,分割的字符串可以注入数组?于是我就去一步一步的断点去走了一遍@Value注入属性的过程,才发现了根本原因。
@Value不支持复杂类型封装(数组、Map、对象等)这个说法确实是有问题的,不够严谨,因为在特殊情况下,是可以注入复杂类型的。
先来梳理一下@Value对属性的注入流程
先交代一下我们的代码:
一个yml文件a.yml
一个Bean A.java
1
2
3
4
5
6
7
8
9
10
11
12
|
@Component
@PropertySource(Value = {"classpath:a.yml"},ignoreResourceNotFound = true, encoding = "utf-8")
public class A {
@Value("${test}")
private String[] test;
public void test(){
System.out.println("test:"+Arrays.toString(test));
System.out.println("长度:"+test.length);
}
}
|
- @Value("${test}")也可使用冒号语法:@Value("${test:testValue}"),表示当从配置文件中获取不到test时,则test值为testValue
- 2.@Value(“#{}”)是获取bean属性,系统属性,表达式
main方法:
1
2
3
4
5
6
7
8
9
10
11
|
@Configuration
@ComponentScan("com.kinyang")
public class HelloApp {
public static void main(String[] args) {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(HelloApp.class);
A bean = ac.getBean(A.class);
bean.test();
}
}
|
ok!下面开始分析
1、从AutowiredAnnotationBeanPostProcessor后置处理说起吧
过多的Spring初始化Bean的流程就不说了,我们直接定位到Bean的属性注入的后置处理器
AutowiredAnnotationBeanPostProcessor。此类中的processInjection()方法中完成了Bean 中@Autowired、@Inject、 @Value 注解的解析并注入的功能。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
此方法中完成了Bean 中@Autowired、@Inject、 @Value 注解的解析并注入的功能
public void processInjection(Object bean) throws BeanCreationException {
Class<?> clazz = bean.getClass();
/// 找到 类上所有的需要自动注入的元素
// (把@Autowired、@Inject、 @Value注解的字段和方法包装成InjectionMetadata类的对象返回)
InjectionMetadata metadata = findAutowiringMetadata(clazz.getName(), clazz, null);
try {
metadata.inject(bean, null, null);
}
catch (BeanCreationException ex) {
throw ex;
}
catch (Throwable ex) {
throw new BeanCreationException(
"Injection of autowired dependencies failed for class [" + clazz + "]", ex);
}
}
|
inject()方法就是一个循环上面一步解析出来的注解信息,注解的方法或者字段包装后的对象是InjectedElement类型的类,InjectedElement是一个抽象类,他的实现主要有两个:对注解字段生成的是AutowiredFieldElement类,对注解方法生成的是AutowiredMethodElement类。
我们这里只分析@Value注解字段的注入流程,所以下一步会进到AutowiredFieldElement类的inject()方法.
此方法就两大步骤:
- 获取要注入的Value
- 通过反射,把值去set字段上
其中获取要注入的Value过程比较复杂,第二步set值就两行代码搞定
具体逻辑看下面代码上我写的注释
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
|
protected void inject(Object bean, @Nullable String beanName, @Nullable PropertyValues pvs) throws Throwable {
Field field = (Field) this.member;
Object Value;
if (this.cached) {
/// 优先从缓存中获取
Value = resolvedCachedArgument(beanName, this.cachedFieldValue);
}
else {
///缓存中没有的话,走下面的逻辑处理
DependencyDescriptor desc = new DependencyDescriptor(field, this.required);
desc.setContainingClass(bean.getClass());
Set<String> autowiredBeanNames = new LinkedHashSet<>(1);
Assert.state(beanFactory != null, "No BeanFactory available");
这个对我们今天讨论的问题很关键
获取一个 类型转换器
TypeConverter typeConverter = beanFactory.getTypeConverter();
try {
/// 获取值(重点,这里把一个TypeConverter传进去了)
Value = beanFactory.resolveDependency(desc, beanName, autowiredBeanNames, typeConverter);
/// 经过上面的方法返回来的 Value 就是要注入的值了
/// 通过断点调试,我们可以发现我们在配置文件yml中配置的 “a,b,c,d”字符串已经变成了一个String[]数组
}
catch (BeansException ex) {
throw new UnsatisfiedDependencyException(null, beanName, new InjectionPoint(field), ex);
}
synchronized (this) {
.....
这里不是我们本次讨论的重点所以就去掉了
}
}
if (Value != null) {
这里就是第二步,赋值
ReflectionUtils.makeAccessible(field);
field.set(bean, Value);
}
}
}
|
从上面代码来看,所有重点就都落到了这行代码
1
|
Value = beanFactory.resolveDependency(desc, beanName, autowiredBeanNames, typeConverter);
|
推断下来resolveDependency方法里应该是读取配置文件字符串,然后将字符串用,分割转换了数组。
那么具体怎么转换的呢?我们继续跟进!
进入resolveDependency()方法,里面逻辑很简单做了一些判断,真正实现其实是doResolveDependency()方法,进行跟进。
根据@Value注解,从配置文件a.yml中解析出配置的内容:“a,b,c,d”
到这里我们得到值还是配置文件配置的字符串,并没有变成我们想要的String[]字符串数组类型。
我们继续往下走,下面是获取一个TypeConverter类型转换器,这里的类型转换器是上面传进来的,具体类型SimpleTypeConverter类。
然后通过这个类型转换器的convertIfNecessary方法把,我们的字符串"a,b,c,d"转换成了String[]数组。
所以我们现在知道了,我们从配置文件获取到的值,通过了Spring转换器,调用了convertIfNecessary方法后,进行了类型自动转换。
那么这转换器到底是怎么进行工作的呢?
继续研究~~
那接下来要研究的就是Spring的TypeConverter的工作原理问题了
在这里推荐大家去看看 A哥的《3. 搞定收工,PropertyEditor就到这》 这篇文章,一共三篇,看完后,在回来看你会更通透!!
首先我们这里知道了外面传进来的那个转换器是一个叫SimpleTypeConverter 的转换器。
这转换器是org.springframework.beans.factory.support.AbstractBeanFactory#getTypeConverter方法得到的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
@Override
public TypeConverter getTypeConverter() {
TypeConverter customConverter = getCustomTypeConverter();
if (customConverter != null) {
return customConverter;
}
else {
/// 如果没有 用户自定的TypeConverter 那就用 默认的SimpleTypeConverter吧
// Build default TypeConverter, registering custom editors.
SimpleTypeConverter typeConverter = new SimpleTypeConverter();
注册一些默认的ConversionService
typeConverter.setConversionService(getConversionService());
再注册一些默认的CustomEditors
registerCustomEditors(typeConverter);
return typeConverter;
}
}
|
默认的SimpleTypeConverter里面注册了一些转换器,从debug过程我们可以看到默认是注入了12个PropertyEditor
这12个PropertyEditor是在哪注入的呢?大家可以看registerCustomEditors(typeConverter)方法,这里就不展开了,我直接说了,是通过ResourceEditorRegistrar类注入进去的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
@Override
public void registerCustomEditors(PropertyEditorRegistry registry) {
ResourceEditor baseEditor = new ResourceEditor(this.resourceLoader, this.propertyResolver);
doRegisterEditor(registry, Resource.class, baseEditor);
doRegisterEditor(registry, ContextResource.class, baseEditor);
doRegisterEditor(registry, InputStream.class, new InputStreamEditor(baseEditor));
doRegisterEditor(registry, InputSource.class, new InputSourceEditor(baseEditor));
doRegisterEditor(registry, File.class, new FileEditor(baseEditor));
doRegisterEditor(registry, Path.class, new PathEditor(baseEditor));
doRegisterEditor(registry, Reader.class, new ReaderEditor(baseEditor));
doRegisterEditor(registry, URL.class, new URLEditor(baseEditor));
ClassLoader classLoader = this.resourceLoader.getClassLoader();
doRegisterEditor(registry, URI.class, new URIEditor(classLoader));
doRegisterEditor(registry, Class.class, new ClassEditor(classLoader));
doRegisterEditor(registry, Class[].class, new ClassArrayEditor(classLoader));
if (this.resourceLoader instanceof ResourcePatternResolver) {
doRegisterEditor(registry, Resource[].class,
new ResourceArrayPropertyEditor((ResourcePatternResolver) this.resourceLoader, this.propertyResolver));
}
}
|
现在我们回到 SimpleTypeConverter 的convertIfNecessary方法里去,这个方法其实是SimpleTypeConverter的父类TypeConverterSupport的方法,而这个父类方法里调用的又是TypeConverterDelegate类的convertIfNecessary方法(一个比一个懒,哈哈哈就是自己不干活)
最后我们重点来分析TypeConverterDelegate的convertIfNecessary方法。
这个方法内容比较多,但是整体思路就是 根据最后想转换的类型,选择出对应的PropertyEditor或者ConversionService,然后进行类型转换。
从上面的看的注入的12个PropertyEditor中,我们就可以看出来了,我们匹配到的是
这行代码doRegisterEditor(registry, Class[].class, new ClassArrayEditor(classLoader));注入的ClassArrayEditor。
所以我ClassArrayEditor这个类就可以了,这个类就很简单了,主要看setAsText方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
public void setAsText(String text) throws IllegalArgumentException {
if (StringUtils.hasText(text)) {
/// 这里通过StringUtils 把字符串,转换成 String数组
String[] classNames = StringUtils.commaDelimitedListToStringArray(text);
Class<?>[] classes = new Class<?>[classNames.length];
for (int i = 0; i < classNames.length; i++) {
String className = classNames[i].trim();
classes[i] = ClassUtils.resolveClassName(className, this.classLoader);
}
setValue(classes);
}
else {
setValue(null);
}
}
|
这个方法里通过Spring的字符串工具类StringUtils的commaDelimitedListToStringArray(text)方法把字符串转换成了数组,方法里就是通过 “,” 进行分割的。
到此为止,我们知道了@Value为什么可以把",“分割的字符串注册到数组中了吧。
其实@Value可以注入URI、Class、File、Resource等等类型,@Value可以注入什么类型完全取决于能不能找到处理 String 到 注入类型的转换器。
上面列出来的12个其实不是全部默认的,系统还有47个其他的转换器,只不过是上面的12个优先级比较高而已,其实还有下面的40多个转换器,所以你看@Value可以注入的类型还会很多的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
|
private void createDefaultEditors() {
this.defaultEditors = new HashMap<>(64);
// Simple editors, without parameterization capabilities.
// The JDK does not contain a default editor for any of these target types.
this.defaultEditors.put(Charset.class, new CharsetEditor());
this.defaultEditors.put(Class.class, new ClassEditor());
this.defaultEditors.put(Class[].class, new ClassArrayEditor());
this.defaultEditors.put(Currency.class, new CurrencyEditor());
this.defaultEditors.put(File.class, new FileEditor());
this.defaultEditors.put(InputStream.class, new InputStreamEditor());
this.defaultEditors.put(InputSource.class, new InputSourceEditor());
this.defaultEditors.put(Locale.class, new LocaleEditor());
this.defaultEditors.put(Path.class, new PathEditor());
this.defaultEditors.put(Pattern.class, new PatternEditor());
this.defaultEditors.put(Properties.class, new PropertiesEditor());
this.defaultEditors.put(Reader.class, new ReaderEditor());
this.defaultEditors.put(Resource[].class, new ResourceArrayPropertyEditor());
this.defaultEditors.put(TimeZone.class, new TimeZoneEditor());
this.defaultEditors.put(URI.class, new URIEditor());
this.defaultEditors.put(URL.class, new URLEditor());
this.defaultEditors.put(UUID.class, new UUIDEditor());
this.defaultEditors.put(ZoneId.class, new ZoneIdEditor());
// Default instances of collection editors.
// Can be overridden by registering custom instances of those as custom editors.
this.defaultEditors.put(Collection.class, new CustomCollectionEditor(Collection.class));
this.defaultEditors.put(Set.class, new CustomCollectionEditor(Set.class));
this.defaultEditors.put(SortedSet.class, new CustomCollectionEditor(SortedSet.class));
this.defaultEditors.put(List.class, new CustomCollectionEditor(List.class));
this.defaultEditors.put(SortedMap.class, new CustomMapEditor(SortedMap.class));
// Default editors for primitive arrays.
this.defaultEditors.put(byte[].class, new ByteArrayPropertyEditor());
this.defaultEditors.put(char[].class, new CharArrayPropertyEditor());
// The JDK does not contain a default editor for char!
this.defaultEditors.put(char.class, new CharacterEditor(false));
this.defaultEditors.put(Character.class, new CharacterEditor(true));
// Spring's CustomBooleanEditor accepts more flag Values than the JDK's default editor.
this.defaultEditors.put(boolean.class, new CustomBooleanEditor(false));
this.defaultEditors.put(Boolean.class, new CustomBooleanEditor(true));
// The JDK does not contain default editors for number wrapper types!
// Override JDK primitive number editors with our own CustomNumberEditor.
this.defaultEditors.put(byte.class, new CustomNumberEditor(Byte.class, false));
this.defaultEditors.put(Byte.class, new CustomNumberEditor(Byte.class, true));
this.defaultEditors.put(short.class, new CustomNumberEditor(Short.class, false));
this.defaultEditors.put(Short.class, new CustomNumberEditor(Short.class, true));
this.defaultEditors.put(int.class, new CustomNumberEditor(Integer.class, false));
this.defaultEditors.put(Integer.class, new CustomNumberEditor(Integer.class, true));
this.defaultEditors.put(long.class, new CustomNumberEditor(Long.class, false));
this.defaultEditors.put(Long.class, new CustomNumberEditor(Long.class, true));
this.defaultEditors.put(float.class, new CustomNumberEditor(Float.class, false));
this.defaultEditors.put(Float.class, new CustomNumberEditor(Float.class, true));
this.defaultEditors.put(double.class, new CustomNumberEditor(Double.class, false));
this.defaultEditors.put(Double.class, new CustomNumberEditor(Double.class, true));
this.defaultEditors.put(BigDecimal.class, new CustomNumberEditor(BigDecimal.class, true));
this.defaultEditors.put(BigInteger.class, new CustomNumberEditor(BigInteger.class, true));
// Only register config Value editors if explicitly requested.
if (this.configValueEditorsActive) {
StringArrayPropertyEditor sae = new StringArrayPropertyEditor();
this.defaultEditors.put(String[].class, sae);
this.defaultEditors.put(short[].class, sae);
this.defaultEditors.put(int[].class, sae);
this.defaultEditors.put(long[].class, sae);
}
}
|
重点来了,分析了这么久了,那么,如果我们想注册一个我们自定义的类该如何操作呢???
好了,既然知道了@Value的注入的原理和中间类型转换的过程,那我们就知道该从哪里下手了,那就是写一个我们自己的PropertyEditor,然后注册到Spring的类型转换器中。
先明确一下我们的需求,就是在yml配置文件中,配置字符串,然后通过@Value注入为一个自定义的对象。
我们的自定义对象 Car.java
1
2
3
4
5
6
7
|
public class Car {
private String color;
private String name;
// 省略 get set方法
}
|
yml配置文件,配置car: 红色|法拉利,我们这里用|分割
1
2
|
test: a,b,c,d
car: 红色|法拉利
|
用于测试的Bean A.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
@Component
@PropertySource(Value = {"classpath:a.yml"},ignoreResourceNotFound = true, encoding = "utf-8")
public class A {
@Value("${test}")
private String[] test;
@Value("${car}")
private Car car;
public void test(){
System.out.println("test:"+Arrays.toString(test));
System.out.println("长度:"+test.length);
System.out.println("自定的Car 居然通过@Value注册成功了");
System.out.println(car.toString());
}
}
|
下面就是写我们的PropertyEditor然后注册到Spring的Spring的类型转换器中。
- 自定义 一个 propertyEditor类:CarPropertyEditor,
- 这里不要直接去实现PropertyEditor接口,那样太麻烦了,因为有很多接口要实现
- 我们这里通过继承PropertyEditorSupport类,通过覆盖关键方法来做
- 主要是两个方法 setAsText 和 getAsText 方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
/**
* @author KinYang.Lau
* @date 2020/12/18 11:00 上午
*
* 自定义 一个 propertyEditor,
* 这里不要直接去实现PropertyEditor接口,那样太麻烦了,因为有很多接口要实现
* 我们这里通过继承PropertyEditorSupport类,通过覆盖关键方法来做
* 主要是两个方法 setAsText 和 getAsText 方法
*/
public class CarPropertyEditor extends PropertyEditorSupport {
@Override
public void setAsText(String text) throws IllegalArgumentException {
/// 这实现我们的 字符串 转 自定义对象的 逻辑
if (StringUtils.hasText(text)) {
String[] split = text.split("\\|");
Car car = new Car();
car.setColor(split[0]);
car.setName(split[1]);
setValue(car);
}
else {
setValue(null);
}
}
@Override
public String getAsText() {
Car Value = (Car) getValue();
return (Value != null ? Value.toString() : "");
}
}
|
那么如何注册到Spring的Spring的类型转换器中呢?
这个也简单,ConfigurableBeanFactory 接口有一个void registerCustomEditor(Class requiredType, Class propertyEditorClass);方法就是用于注册CustomEditor的。
所以我们写一个BeanFactory的后置处理器就可以了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
/**
* @author KinYang.Lau
* @date 2020/12/18 10:54 上午
*/
@Component
public class MyCustomEditorConfigurer implements BeanFactoryPostProcessor, Ordered {
private int order = Ordered.LOWEST_PRECEDENCE; // default: same as non-Ordered
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
/// 把我们自定义的 转换器器注册进去
beanFactory.registerCustomEditor(Car.class, CarPropertyEditor.class);
}
@Override
public int getOrder() {
return this.order;
}
}
|
下面我运行一下程序,看看结果吧:
1
2
3
4
5
6
7
8
9
10
11
|
@Configuration
@ComponentScan("com.kinyang")
public class HelloApp {
public static void main(String[] args) {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(HelloApp.class);
A bean = ac.getBean(A.class);
bean.test();
}
}
|
搞定!!!
通过整个分析过程,对@Value的注入原理又有了更深入的理解。