Spring的@Value注入復(fù)雜類型(通過@value注入自定義類型)
之前寫了一篇關(guān)于Spring的@Value注入的文章《兩種SpringBoot讀取yml文件中配置數(shù)組的方法》。
里面列出了@Value和@ConfigurationProperties的對比,其中有一條是寫的@value不支持復(fù)雜類型封裝(數(shù)組、Map、對象等)。
但是后來有小伙伴留言說他用@value測試的時候,是可以注入的數(shù)組和集合的。于是我就跟著做了一些測試,發(fā)現(xiàn)確實(shí)可以。但是只有在以,分割的字符串的時候才可以。
為什么用,分割的字符串可以注入數(shù)組?于是我就去一步一步的斷點(diǎn)去走了一遍@value注入屬性的過程,才發(fā)現(xiàn)了根本原因。
@Value不支持復(fù)雜類型封裝(數(shù)組、Map、對象等)這個說法確實(shí)是有問題的,不夠嚴(yán)謹(jǐn),因?yàn)樵谔厥馇闆r下,是可以注入復(fù)雜類型的。
先來梳理一下@Value對屬性的注入流程
先交代一下我們的代碼:
一個yml文件a.yml
test: a,b,c,d
一個Bean A.java
@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);
}
}
main方法:
@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 注解的解析并注入的功能。
此方法中完成了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);
}
}
2、接著進(jìn)入InjectionMetadata的inject()方法
inject()方法就是一個循環(huán)上面一步解析出來的注解信息,注解的方法或者字段包裝后的對象是InjectedElement類型的類,InjectedElement是一個抽象類,他的實(shí)現(xiàn)主要有兩個:對注解字段生成的是AutowiredFieldElement類,對注解方法生成的是AutowiredMethodElement類。
我們這里只分析@Value注解字段的注入流程,所以下一步會進(jìn)到AutowiredFieldElement類的inject()方法.
此方法就兩大步驟:
- 獲取要注入的value
- 通過反射,把值去set字段上
其中獲取要注入的value過程比較復(fù)雜,第二步set值就兩行代碼搞定
具體邏輯看下面代碼上我寫的注釋
protected void inject(Object bean, @Nullable String beanName, @Nullable PropertyValues pvs) throws Throwable {
Field field = (Field) this.member;
Object value;
if (this.cached) {
/// 優(yōu)先從緩存中獲取
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");
這個對我們今天討論的問題很關(guān)鍵
獲取一個 類型轉(zhuǎn)換器
TypeConverter typeConverter = beanFactory.getTypeConverter();
try {
/// 獲取值(重點(diǎn),這里把一個TypeConverter傳進(jìn)去了)
value = beanFactory.resolveDependency(desc, beanName, autowiredBeanNames, typeConverter);
/// 經(jīng)過上面的方法返回來的 value 就是要注入的值了
/// 通過斷點(diǎn)調(diào)試,我們可以發(fā)現(xiàn)我們在配置文件yml中配置的 “a,b,c,d”字符串已經(jīng)變成了一個String[]數(shù)組
}
catch (BeansException ex) {
throw new UnsatisfiedDependencyException(null, beanName, new InjectionPoint(field), ex);
}
synchronized (this) {
.....
這里不是我們本次討論的重點(diǎn)所以就去掉了
}
}
if (value != null) {
這里就是第二步,賦值
ReflectionUtils.makeAccessible(field);
field.set(bean, value);
}
}
}
從上面代碼來看,所有重點(diǎn)就都落到了這行代碼
value = beanFactory.resolveDependency(desc, beanName, autowiredBeanNames, typeConverter);
推斷下來resolveDependency方法里應(yīng)該是讀取配置文件字符串,然后將字符串用,分割轉(zhuǎn)換了數(shù)組。
那么具體怎么轉(zhuǎn)換的呢?我們繼續(xù)跟進(jìn)!
進(jìn)入resolveDependency()方法,里面邏輯很簡單做了一些判斷,真正實(shí)現(xiàn)其實(shí)是doResolveDependency()方法,進(jìn)行跟進(jìn)。
根據(jù)@Value注解,從配置文件a.yml中解析出配置的內(nèi)容:“a,b,c,d”


到這里我們得到值還是配置文件配置的字符串,并沒有變成我們想要的String[]字符串?dāng)?shù)組類型。
我們繼續(xù)往下走,下面是獲取一個TypeConverter類型轉(zhuǎn)換器,這里的類型轉(zhuǎn)換器是上面?zhèn)鬟M(jìn)來的,具體類型SimpleTypeConverter類。
然后通過這個類型轉(zhuǎn)換器的convertIfNecessary方法把,我們的字符串"a,b,c,d"轉(zhuǎn)換成了String[]數(shù)組。

所以我們現(xiàn)在知道了,我們從配置文件獲取到的值,通過了Spring轉(zhuǎn)換器,調(diào)用了convertIfNecessary方法后,進(jìn)行了類型自動轉(zhuǎn)換。那么這轉(zhuǎn)換器到底是怎么進(jìn)行工作的呢?
繼續(xù)研究~~
那接下來要研究的就是Spring的TypeConverter的工作原理問題了
首先我們這里知道了外面?zhèn)鬟M(jìn)來的那個轉(zhuǎn)換器是一個叫SimpleTypeConverter 的轉(zhuǎn)換器。
這轉(zhuǎn)換器是org.springframework.beans.factory.support.AbstractBeanFactory#getTypeConverter方法得到的
@Override
public TypeConverter getTypeConverter() {
TypeConverter customConverter = getCustomTypeConverter();
if (customConverter != null) {
return customConverter;
}
else {
/// 如果沒有 用戶自定的TypeConverter 那就用 默認(rèn)的SimpleTypeConverter吧
// Build default TypeConverter, registering custom editors.
SimpleTypeConverter typeConverter = new SimpleTypeConverter();
注冊一些默認(rèn)的ConversionService
typeConverter.setConversionService(getConversionService());
再注冊一些默認(rèn)的CustomEditors
registerCustomEditors(typeConverter);
return typeConverter;
}
}
默認(rèn)的SimpleTypeConverter里面注冊了一些轉(zhuǎn)換器,從debug過程我們可以看到默認(rèn)是注入了12個PropertyEditor

這12個PropertyEditor是在哪注入的呢?大家可以看registerCustomEditors(typeConverter)方法,這里就不展開了,我直接說了,是通過ResourceEditorRegistrar類注入進(jìn)去的。
@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));
}
}
現(xiàn)在我們回到 SimpleTypeConverter 的convertIfNecessary方法里去,這個方法其實(shí)是SimpleTypeConverter的父類TypeConverterSupport的方法,而這個父類方法里調(diào)用的又是TypeConverterDelegate類的convertIfNecessary方法(一個比一個懶,哈哈哈就是自己不干活)
最后我們重點(diǎn)來分析TypeConverterDelegate的convertIfNecessary方法。
這個方法內(nèi)容比較多,但是整體思路就是 根據(jù)最后想轉(zhuǎn)換的類型,選擇出對應(yīng)的PropertyEditor或者ConversionService,然后進(jìn)行類型轉(zhuǎn)換。
從上面的看的注入的12個PropertyEditor中,我們就可以看出來了,我們匹配到的是
這行代碼doRegisterEditor(registry, Class[].class, new ClassArrayEditor(classLoader));注入的ClassArrayEditor。
所以我ClassArrayEditor這個類就可以了,這個類就很簡單了,主要看setAsText方法
public void setAsText(String text) throws IllegalArgumentException {
if (StringUtils.hasText(text)) {
/// 這里通過StringUtils 把字符串,轉(zhuǎn)換成 String數(shù)組
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)方法把字符串轉(zhuǎn)換成了數(shù)組,方法里就是通過 “,” 進(jìn)行分割的。
到此為止,我們知道了@Value為什么可以把“,”分割的字符串注冊到數(shù)組中了吧。
其實(shí)@Value可以注入URI、Class、File、Resource等等類型,@Value可以注入什么類型完全取決于能不能找到處理 String 到 注入類型的轉(zhuǎn)換器。
上面列出來的12個其實(shí)不是全部默認(rèn)的,系統(tǒng)還有47個其他的轉(zhuǎn)換器,只不過是上面的12個優(yōu)先級比較高而已,其實(shí)還有下面的40多個轉(zhuǎn)換器,所以你看@Value可以注入的類型還會很多的。
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);
}
}
重點(diǎn)來了,分析了這么久了,那么,如果我們想注冊一個我們自定義的類該如何操作呢???
好了,既然知道了@Value的注入的原理和中間類型轉(zhuǎn)換的過程,那我們就知道該從哪里下手了,那就是寫一個我們自己的PropertyEditor,然后注冊到Spring的類型轉(zhuǎn)換器中。
先明確一下我們的需求,就是在yml配置文件中,配置字符串,然后通過@Value注入為一個自定義的對象。
我們的自定義對象 Car.java
public class Car {
private String color;
private String name;
// 省略 get set方法
}
yml配置文件,配置car: 紅色|法拉利,我們這里用|分割
test: a,b,c,d car: 紅色|法拉利
用于測試的Bean A.java
@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的類型轉(zhuǎn)換器中。
- 自定義 一個 propertyEditor類:CarPropertyEditor,
- 這里不要直接去實(shí)現(xiàn)PropertyEditor接口,那樣太麻煩了,因?yàn)橛泻芏嘟涌谝獙?shí)現(xiàn)
- 我們這里通過繼承PropertyEditorSupport類,通過覆蓋關(guān)鍵方法來做
- 主要是兩個方法 setAsText 和 getAsText 方法
/**
* @author KinYang.Lau
* @date 2020/12/18 11:00 上午
*
* 自定義 一個 propertyEditor,
* 這里不要直接去實(shí)現(xiàn)PropertyEditor接口,那樣太麻煩了,因?yàn)橛泻芏嘟涌谝獙?shí)現(xiàn)
* 我們這里通過繼承PropertyEditorSupport類,通過覆蓋關(guān)鍵方法來做
* 主要是兩個方法 setAsText 和 getAsText 方法
*/
public class CarPropertyEditor extends PropertyEditorSupport {
@Override
public void setAsText(String text) throws IllegalArgumentException {
/// 這實(shí)現(xiàn)我們的 字符串 轉(zhuǎn) 自定義對象的 邏輯
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的類型轉(zhuǎn)換器中呢?
這個也簡單,ConfigurableBeanFactory 接口有一個
void registerCustomEditor(Class<?> requiredType, Class<? extends PropertyEditor> propertyEditorClass);方法就是用于注冊CustomEditor的。
所以我們寫一個BeanFactory的后置處理器就可以了。
/**
* @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 {
/// 把我們自定義的 轉(zhuǎn)換器器注冊進(jìn)去
beanFactory.registerCustomEditor(Car.class, CarPropertyEditor.class);
}
@Override
public int getOrder() {
return this.order;
}
}
下面我運(yùn)行一下程序,看看結(jié)果吧:
@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的注入原理又有了更深入的理解。
以上為個人經(jīng)驗(yàn),希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
Java內(nèi)存釋放實(shí)現(xiàn)代碼案例
這篇文章主要介紹了Java內(nèi)存釋放實(shí)現(xiàn)代碼案例,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2020-12-12
Java實(shí)現(xiàn)幾十萬條數(shù)據(jù)插入實(shí)例教程(30萬條數(shù)據(jù)插入MySQL僅需13秒)
這篇文章主要給大家介紹了關(guān)于Java如何實(shí)現(xiàn)幾十萬條數(shù)據(jù)插入的相關(guān)資料,30萬條數(shù)據(jù)插入MySQL僅需13秒,文中通過實(shí)例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2023-04-04
Java中漢字轉(zhuǎn)拼音pinyin4j用法實(shí)例分析
這篇文章主要介紹了Java中漢字轉(zhuǎn)拼音pinyin4j用法,結(jié)合實(shí)例形式較為詳細(xì)的分析了pinyin4j庫的具體使用技巧,需要的朋友可以參考下2015-12-12
簡單了解java標(biāo)識符的作用和命名規(guī)則
這篇文章主要介紹了簡單了解java標(biāo)識符的作用和命名規(guī)則,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2020-01-01
Java concurrency集合之 CopyOnWriteArrayList_動力節(jié)點(diǎn)Java學(xué)院整理
這篇文章主要介紹了Java concurrency集合之 CopyOnWriteArrayList的相關(guān)資料,需要的朋友可以參考下2017-06-06
Mybatis查不到數(shù)據(jù)查詢返回Null問題
mybatis突然查不到數(shù)據(jù),查詢返回的都是Null,但是 select count(*) from xxx查詢數(shù)量,返回卻是正常的。好多朋友遇到這樣的問題不知所措,下面小編通過本教程簡單給大家說明下2016-08-08
Java面試題沖刺第二十六天--實(shí)戰(zhàn)編程
這篇文章主要為大家分享了最有價值的三道java實(shí)戰(zhàn)面試題,涵蓋內(nèi)容全面,包括數(shù)據(jù)結(jié)構(gòu)和算法相關(guān)的題目、經(jīng)典面試編程題等,感興趣的小伙伴們可以參考一下2021-08-08
Javabean轉(zhuǎn)換成json字符并首字母大寫代碼實(shí)例
這篇文章主要介紹了javabean轉(zhuǎn)成json字符并首字母大寫代碼實(shí)例,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2020-02-02

