Spring读源码系列番外篇---05----类型转换---中---三种全新的类型转换器

Posted 大忽悠爱忽悠

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Spring读源码系列番外篇---05----类型转换---中---三种全新的类型转换器相关的知识,希望对你有一定的参考价值。

Spring读源码系列番外篇---05----类型转换---中


系列文章:

Spring读源码系列番外篇—01–PropertyValue相关类

Spring读源码系列番外篇—02—PropertyResolver的结构体系剖析—上

Spring读源码系列番外篇—03—PropertyResolver的结构体系剖析—下

Spring读源码系列番外篇03----类型转换–上


新一代类型转换机制


旧版的PropertyEditor设计缺陷

  • 职责不单一:该接口有非常多的方法,但只用到2个而已
  • 类型不安全:setValue()方法入参是Object,getValue()返回值是Object,依赖于约定好的类型强转,不安全
  • 线程不安全:依赖于setValue()后getValue(),实例是线程不安全的
  • 语义不清晰:从语义上根本不能知道它是用于类型转换的组件
  • 只能用于String类型:它只能进行String <-> 其它类型的转换,而非更灵活的Object <-> Object

新一代类型转换Converter

Spring 3.0版本重新设计了一套类型转换接口,有3个核心接口:

  • Converter<S, T>:Source -> Target类型转换接口,适用于1:1转换
  • ConverterFactory<S, R>:Source -> R类型转换接口,适用于1:N转换
  • GenericConverter:更为通用的类型转换接口,适用于N:N转换,因为没有泛型约束,所以是通用
  • ConditionalConverter:前置条件判断,决定是否进行转换

Converter

将源类型S转换为目标类型T。

@FunctionalInterface
public interface Converter<S, T> 
	@Nullable
	T convert(S source);

它是个函数式接口,接口定义非常简单。适合1:1转换场景:可以将任意类型 转换为 任意类型。它的实现类非常多,部分截图如下:


值得注意的是:几乎所有实现类的访问权限都是default/private,只有少数几个是public公开的。


实例

class StringToFileConverter implements Converter<String, File> 
    private static final ResourceLoader resourceLoader = new DefaultResourceLoader((ClassLoader)null);

    StringToFileConverter() 
    

    public File convert(String source) 
        if (ResourceUtils.isUrl(source)) 
            return this.getFile(resourceLoader.getResource(source));
         else 
        //先尝试直接从文件系统去定位资源
            File file = new File(source);
            if (file.exists()) 
                return file;
             else 
            //再尝试从类路径下,或者网络资源定位资源
                Resource resource = resourceLoader.getResource(source);
                return resource.exists() ? this.getFile(resource) : file;
            
        
    

    private File getFile(Resource resource) 
        try 
            return resource.getFile();
         catch (IOException var3) 
            throw new IllegalStateException("Could not retrieve file for " + resource + ": " + var3.getMessage());
        
    


缺陷

Converter用于解决1:1的任意类型转换,因此它必然存在一个不足:解决1:N转换问题需要写N遍,造成重复冗余代码。

譬如:输入是字符串,它可以转为任意数字类型,包括byte、short、int、long、double等等,如果用Converter来转换的话每个类型都得写个转换器,想想都麻烦有木有。

Spring早早就考虑到了该场景,提供了相应的接口来处理,它就是ConverterFactory<S, R>。


ConverterFactory

从名称上看它代表一个转换工厂:可以将对象S转换为R的所有子类型,从而形成1:N的关系。

public interface ConverterFactory<S, R> 
	<T extends R> Converter<S, T> getConverter(Class<T> targetType);

它同样也是个函数式接口。该接口的实现类并不多(访问权限全部为default):


实例

final class StringToNumberConverterFactory implements ConverterFactory<String, Number> 

	@Override
	public <T extends Number> Converter<String, T> getConverter(Class<T> targetType) 
		return new StringToNumber<>(targetType);
	

    // 私有内部类:实现Converter接口。用泛型边界约束一类类型
	private static final class StringToNumber<T extends Number> implements Converter<String, T> 

		private final Class<T> targetType;

		public StringToNumber(Class<T> targetType) 
			this.targetType = targetType;
		

		@Override
		@Nullable
		public T convert(String source) 
			if (source.isEmpty()) 
				return null;
			
			return NumberUtils.parseNumber(source, this.targetType);
		
	



优点

ConverterFactory作为Converter的工厂,对Converter进行包装,从而达到屏蔽内部实现的目的,对使用者友好,这不正是工厂模式的优点么,符合xxxFactory的语义。但你需要清除的是,工厂内部实现其实也是通过众多if else之类的去完成的,本质上并无差异。


缺陷

既然有了1:1、1:N,自然就有N:N。比如集合转换、数组转换、Map到Map的转换等等,这些N:N的场景,就需要借助下一个接口GenericConverter来实现。


GenericConverter

它是一个通用的转换接口,用于在两个或多个类型之间进行转换。相较于前两个,这是最灵活的SPI转换器接口,但也是最复杂的。

public interface GenericConverter 
	@Nullable
	Set<ConvertiblePair> getConvertibleTypes();
	
	@Nullable
	Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType);

	final class ConvertiblePair 

		private final Class<?> sourceType;

		private final Class<?> targetType;

		public ConvertiblePair(Class<?> sourceType, Class<?> targetType) 
			Assert.notNull(sourceType, "Source type must not be null");
			Assert.notNull(targetType, "Target type must not be null");
			this.sourceType = sourceType;
			this.targetType = targetType;
		

		public Class<?> getSourceType() 
			return this.sourceType;
		

		public Class<?> getTargetType() 
			return this.targetType;
		
		//省略equals,toString和hashcode方法
        ...
	

该接口并非函数式接口,虽然方法不多但稍显复杂。现对出现的几个类型做简单介绍:

  • ConvertiblePair:维护sourceType和targetType的POJO
    • getConvertibleTypes()方法返回此Pair的Set集合。由此也能看出该转换器是可以支持N:N的(大多数情况下只写一对值而已,也有写多对的)
  • TypeDescriptor:类型描述。该类专用于Spring的类型转换场景,用于描述from or to的类型
    • 比单独的Type类型强大,内部借助了ResolvableType来解决泛型议题

GenericConverter的内置实现也比较多,部分截图如下:

ConditionalGenericConverter是GenericConverter和条件接口ConditionalConverter的组合,作用是在执行GenericConverter转换时增加一个前置条件判断方法。

public interface ConditionalGenericConverter extends GenericConverter, ConditionalConverter 

分割线下面的4个转换器比较特殊,字面上不好理解其实际作用,比较“高级”。它们如果能被运用在日常工作中可以事半功弎,


例子

final class CollectionToCollectionConverter implements ConditionalGenericConverter 

	private final ConversionService conversionService;

//这是唯一构造器,必须传入ConversionService:
//元素与元素之间的转换是依赖于conversionService转换服务去完成的,最终完成集合到集合的转换。
	public CollectionToCollectionConverter(ConversionService conversionService) 
		this.conversionService = conversionService;
	

// 集合转集合:如String集合转为Integer集合
//ConvertiblePair是描述SourceType--->TargetType
	@Override
	public Set<ConvertiblePair> getConvertibleTypes() 
		return Collections.singleton(new ConvertiblePair(Collection.class, Collection.class));
	

//前置判断---判断是否能进行转换
//判断能否转换的依据:集合里的元素与元素之间是否能够转换,底层依赖于ConversionService#canConvert()这个API去完成判断。
	@Override
	public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) 
		return ConversionUtils.canConvertElements(
				sourceType.getElementTypeDescriptor(), targetType.getElementTypeDescriptor(), this.conversionService);
	

//match判断认可后,才可以进行convert具体转换
	@Override
	@Nullable
	public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) 
		if (source == null) 
			return null;
		
		Collection<?> sourceCollection = (Collection<?>) source;
       //快速返回:对于特殊情况,做快速返回处理
        
        //1.若目标元素类型是源元素类型的子类型(或相同),就没有转换的必要了(copyRequired = false)
        
		//TargetType instance of SourceType的话,就不需要进行转换工作了,直接返回即可
		//例如TargetType是ArrayList,SourceType是List
		boolean copyRequired = !targetType.getType().isInstance(source);
		if (!copyRequired && sourceCollection.isEmpty()) 
			return source;
		
		//getElementTypeDescriptor():如果是集合,数组或者stream流,返回里面的元素类型
		//但是注意如果Collection集合时泛型化的,才会返回元素类型
		//如果Collection不是泛型化的,那么返回null
		TypeDescriptor elementDesc = targetType.getElementTypeDescriptor();
		if (elementDesc == null && !copyRequired) 
		//此时Collection里面的元素可以认为是Object,那这样的话,Collection to Collection直接返回就可以了
			return source;
		

//若没有触发快速返回。给目标创建一个新集合,然后把source的元素一个一个的放进新集合里去,这里又分为两种处理case		
		Collection<Object> target = CollectionFactory.createCollection(targetType.getType(),
				(elementDesc != null ? elementDesc.getType() : null), sourceCollection.size());
//若新集合(目标集合)没有指定泛型类型(那就是Object),就直接putAll即可,并不需要做类型转换
		if (elementDesc == null) 
			target.addAll(sourceCollection);
		
		else 
		// 遍历:一个一个元素的转,时间复杂度还是蛮高的
		// 元素转元素委托给conversionService去完成
			for (Object sourceElement : sourceCollection) 
				Object targetElement = 
               this.conversionService.convert(sourceElement,
						sourceType.elementTypeDescriptor(sourceElement), elementDesc);
				target.add(targetElement);
				if (sourceElement != targetElement) 
					copyRequired = true;
				
			
		

		return (copyRequired ? target : source);
	



缺陷

如果说它的优点是功能强大,能够处理复杂类型的转换(PropertyEditor和前2个接口都只能转换单元素类型),那么缺点就是使用、自定义实现起来比较复杂。这不官方也给出了使用指导意见:在Converter/ConverterFactory接口能够满足条件的情况下,可不使用此接口就不使用。


ConditionalConverter

条件接口,@since 3.2。它可以为Converter、GenericConverter、ConverterFactory转换增加一个前置判断条件。

public interface ConditionalConverter 
	boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType);

该接口的实现,截图如下:

可以看到,只有通用转换器GenericConverter和它进行了合体。这也很容易理解,作为通用的转换器,加个前置判断将更加严谨和更安全。对于专用的转换器如Converter,它已明确规定了转换的类型,自然就不需要做前置判断喽。


四个兜底的GenericConverter转换器

上文留下了4个类型转换器,下面来讲讲:

  • StreamConverter:将Stream流与集合/数组之间的转换,必要时转换元素类型

这三个比较特殊,属于“最后的”“兜底类”类型转换器:

  • ObjectToObjectConverter:通用的将原对象转换为目标对象(通过工厂方法or构造器)
  • IdToEntityConverter:给个ID自动帮你兑换成一个Entity对象
  • FallbackObjectToStringConverter:将任何对象调用toString()转化为String类型。当匹配不到任何转换器时,它用于兜底

默认转换器注册情况

Spring新一代类型转换内建了非常多的实现,这些在初始化阶段大都被默认注册进去。注册点在DefaultConversionService提供的一个static静态工具方法里:

	public static void addDefaultConverters(ConverterRegistry converterRegistry) 
	    // 1、添加标量转换器(和数字相关)
		addScalarConverters(converterRegistry);
		// 2、添加处理集合的转换器
		addCollectionConverters(converterRegistry);

       	// 3、添加对JSR310时间类型支持的转换器
		converterRegistry.addConverter(new ByteBufferConverter((ConversionService) converterRegistry));
		converterRegistry.addConverter(new StringToTimeZoneConverter());
		converterRegistry.addConverter(new ZoneIdToTimeZoneConverter());
		converterRegistry.addConverter(new ZonedDateTimeToCalendarConverter());

        // 4、添加兜底转换器(上面处理不了的全交给这几个哥们处理)
		converterRegistry.addConverter(new ObjectToObjectConverter());
		converterRegistry.addConverter(new IdToEntityConverter((ConversionService) converterRegistry));
		converterRegistry.addConverter(new FallbackObjectToStringConverter());
		converterRegistry.addConverter(new ObjectToOptionalConverter((ConversionService) converterRegistry));
	

该静态方法用于注册全局的、默认的转换器们,从而让Spring有了基础的转换能力,进而完成绝大部分转换工作。

特别强调:转换器的注册顺序非常重要,这决定了通用转换器的匹配结果(谁在前,优先匹配谁)。

JSR310转换器只看到TimeZone、ZoneId等转换,更为常用的LocalDate、LocalDateTime等这些类型转换,在spring理解中是格式化操作,因此主要由Formatter组件完成


StreamConverter

用于实现集合/数组类型到Stream类型的互转,这从它支持的Set< ConvertiblePair > 集合也能看出来:

//将 Stream 与集合或数组相互转换,必要时转换元素类型。
class StreamConverter implements ConditionalGenericConverter 
     //Stream类型的描述符
	private static final TypeDescriptor STREAM_TYPE = TypeDescriptor.valueOf(Stream.class);
    //可以转换的类型集合
	private static final Set<ConvertiblePair> CONVERTIBLE_TYPES = createConvertibleTypes();
    //转换服务
	private final ConversionService conversionService;
    
	public StreamConverter(ConversionService conversionService) 
		this.conversionService = conversionService;
	
   
	@Override
	public Set<ConvertiblePair> getConvertibleTypes() 
		return CONVERTIBLE_TYPES;
	
    //判断是否可以进行类型转换---可以进行双向转换
	@Override
	public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) 
	//getElementTypeDescriptor()拿到的是集合或者stream流里面的元素类型
		//源类型是Stream类型
		if (sourceType.isAssignableTo(STREAM_TYPE)) 
			return matchesFromStream(sourceType.getElementTypeDescriptor(), targetType);
		
		//目标类型是Stream类型
		if (targetType.isAssignableTo(STREAM_TYPE)) 
			return matchesToStream(targetType.getElementTypeDescriptor(), sourceType);
		
		return false;
	

	/** 
    验证流中包含的元素的 Collection 是否可以转换为指定的 targetType. collection--->stream
	 */
	public boolean matchesFromStream(@Nullable TypeDescriptor elementType, TypeDescriptor targetType) 
	    //获取到集合类型的描述符---集合里面的元素是外部传入的elementType类型的元素
		TypeDescriptor collectionOfElement = TypeDescriptor.collection(Collection.class, elementType);
		//调用conversionService判断是否能够进行类型转换
		return this.conversionService.canConvert(collectionOfElement, targetType);
	


	/**
    验证指定的 sourceType 是否可以转换为流元素类型的 Collection。stream--->collection
	 */
	public boolean matchesToStream(@Nullable TypeDescriptor elementType, TypeDescriptor sourceType) 
		TypeDescriptor collectionOfElement = TypeDescriptor.collection(Collection.class, elementType);
		//调用conversionService判断是否能够进行类型转换
		return this.conversionService.canConvert(sourceType, collectionOfElement);
	
   
   
    //进行转换的api接口,这里支持双向转换,即Stream和Collection可以进行互相转换
	@Override
	@Nullable
	public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) 
		if (sourceType.isAssignableTo(STREAM_TYPE)) 
		//stream--->collection
			return convertFromStream(Spring读源码系列番外篇---06----类型转换---下---ConversionService相关家族

Spring读源码系列番外篇08---BeanWrapper没有那么简单--上

Spring读源码系列番外篇08---BeanWrapper没有那么简单--中

Spring读源码系列番外篇---03---PropertyResolver的结构体系剖析---下

Spring读源码系列番外篇---02---PropertyResolver的结构体系剖析---上

重学springboot系列番外篇之RestTemplate