欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

Spring的@Value注入复杂类型(通过@value注入自定义类型)

程序员文章站 2022-03-18 10:00:05
之前写了一篇关于spring的@value注入的文章《介绍两种springboot读取yml文件中配置数组的方法》。里面列出了@value和@configurationproperties的对比,其中...

之前写了一篇关于spring的@value注入的文章《介绍两种springboot读取yml文件中配置数组的方法》。

里面列出了@value和@configurationproperties的对比,其中有一条是写的@value不支持复杂类型封装(数组、map、对象等)。

但是后来有小伙伴留言说他用@value测试的时候,是可以注入的数组和集合的。于是我就跟着做了一些测试,发现确实可以。但是只有在以,分割的字符串的时候才可以。

为什么用,分割的字符串可以注入数组?于是我就去一步一步的断点去走了一遍@value注入属性的过程,才发现了根本原因。

@value不支持复杂类型封装(数组、map、对象等)这个说法确实是有问题的,不够严谨,因为在特殊情况下,是可以注入复杂类型的。

先来梳理一下@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、接着进入injectionmetadata的inject()方法

inject()方法就是一个循环上面一步解析出来的注解信息,注解的方法或者字段包装后的对象是injectedelement类型的类,injectedelement是一个抽象类,他的实现主要有两个:对注解字段生成的是autowiredfieldelement类,对注解方法生成的是autowiredmethodelement类。

我们这里只分析@value注解字段的注入流程,所以下一步会进到autowiredfieldelement类的inject()方法.

此方法就两大步骤:

  • 获取要注入的value
  • 通过反射,把值去set字段上

其中获取要注入的value过程比较复杂,第二步set值就两行代码搞定

具体逻辑看下面代码上我写的注释

		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);
			}
		}
	}

从上面代码来看,所有重点就都落到了这行代码

value = beanfactory.resolvedependency(desc, beanname, autowiredbeannames, typeconverter);

推断下来resolvedependency方法里应该是读取配置文件字符串,然后将字符串用,分割转换了数组。

那么具体怎么转换的呢?我们继续跟进!

进入resolvedependency()方法,里面逻辑很简单做了一些判断,真正实现其实是doresolvedependency()方法,进行跟进。

根据@value注解,从配置文件a.yml中解析出配置的内容:“a,b,c,d”

Spring的@Value注入复杂类型(通过@value注入自定义类型)

Spring的@Value注入复杂类型(通过@value注入自定义类型)

到这里我们得到值还是配置文件配置的字符串,并没有变成我们想要的string[]字符串数组类型。

我们继续往下走,下面是获取一个typeconverter类型转换器,这里的类型转换器是上面传进来的,具体类型simpletypeconverter类。

然后通过这个类型转换器的convertifnecessary方法把,我们的字符串"a,b,c,d"转换成了string[]数组。

Spring的@Value注入复杂类型(通过@value注入自定义类型)

所以我们现在知道了,我们从配置文件获取到的值,通过了spring转换器,调用了convertifnecessary方法后,进行了类型自动转换。那么这转换器到底是怎么进行工作的呢?

继续研究~~

那接下来要研究的就是spring的typeconverter的工作原理问题了

首先我们这里知道了外面传进来的那个转换器是一个叫simpletypeconverter 的转换器。

这转换器是org.springframework.beans.factory.support.abstractbeanfactory#gettypeconverter方法得到的

	@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

Spring的@Value注入复杂类型(通过@value注入自定义类型)

这12个propertyeditor是在哪注入的呢?大家可以看registercustomeditors(typeconverter)方法,这里就不展开了,我直接说了,是通过resourceeditorregistrar类注入进去的。

@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方法

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可以注入的类型还会很多的。

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

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的类型转换器中。

  • 自定义 一个 propertyeditor类:carpropertyeditor,
  • 这里不要直接去实现propertyeditor接口,那样太麻烦了,因为有很多接口要实现
  • 我们这里通过继承propertyeditorsupport类,通过覆盖关键方法来做
  • 主要是两个方法 setastext 和 getastext 方法
/**
 * @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<? 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 {
	///  把我们自定义的 转换器器注册进去
		beanfactory.registercustomeditor(car.class, carpropertyeditor.class);
	}

	@override
	public int getorder() {
		return this.order;
	}
}

下面我运行一下程序,看看结果吧:

@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();
	}
}

Spring的@Value注入复杂类型(通过@value注入自定义类型)

搞定!!!

通过整个分析过程,对@value的注入原理又有了更深入的理解。

以上为个人经验,希望能给大家一个参考,也希望大家多多支持。