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

Posted 大忽悠爱忽悠

tags:

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

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


系列文章:

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

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

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

之所以有这些番外篇,是因为在阅读spring主线源码的过程中,遇到了很多不熟悉的知识点,那么怎么办,只能属性它,本系列也只是带领大家粗浅认识spring中常会用到的一些工具类吧


本篇文章写作来源于spring中大量用到的类型转换,无论是pouplateBean中属性注入时用到的类型转换,还是dataBinder中的类型转换,无不让我们头疼,如果总是不搞懂,遇到看不懂的源码直接跳过,那么这部分就一直是搞不懂的地方了。

并且,就算我们以后自己尝试去写一些小框架,类型转换也是一个绕不开的话题,我们可以借鉴spring的设计手法,毕竟大佬的成功案例就在眼前,何不借鉴一下呢?


Spring类型转换器升级历史


本图借鉴于1. 揭秘Spring类型转换 - 框架设计的基石


古老的PropertyEditor

PropertyEditor是jdk提供服务于gui的类型转换器,但是由于和spring初期xml类型转换需求相同,因此spring就没有重复造轮子,而是复用了此接口,该接口我们只需要关注两个暴露出来的接口方法即可:

public interface PropertyEditor 
	...
	// String -> Object
	void setAsText(String text) throws java.lang.IllegalArgumentException;
	// Object -> String
	String getAsText();
	...

缺陷:

职责不单一,类型不安全,只能实现String类型的转换等。虽然自Spring 3.0起提供了现代化的类型转换接口,但是此部分机制一直得以保留,保证了向下兼容性。


先进的Converter、GenericConverter

为了解决PropertyEditor作为类型转换方式的设计缺陷,Spring 3.0版本重新设计了一套类型转换接口,其中主要包括:

  • Converter<S, T>:Source -> Target类型转换接口,适用于1:1转换
    • StringToPropertiesConverter:将String类型转换为Properties
  • StringToBooleanConverter:将String类型转换为Boolean
    • EnumToIntegerConverter:将Enum类型转换为Integer
  • ConverterFactory<S, R>:Source -> R类型转换接口,适用于1:N转换
    • StringToEnumConverterFactory:将String类型转任意Enum
    • StringToNumberConverterFactory:将String类型转为任意数字(可以是int、long、double等等)
    • NumberToNumberConverterFactory:数字类型转为数字类型(如int到long,long到double等等)
  • GenericConverter:更为通用的类型转换接口,适用于N:N转换
    • ObjectToCollectionConverter:任意集合类型转为任意集合类型(如List< String >转为List< Integer > / Set< Integer >都使用此转换器)
    • CollectionToArrayConverter:解释基本同上
    • MapToMapConverter:解释基本同上
  • ConditionalConverter:条件转换接口。可跟上面3个接口组合使用,提供前置条件判断验证

先进的转换服务接口:ConversionService

Converter、ConverterFactory、GenericConverter它们三都着力于完成类型转换。对于使用者而言,如果做个类型转换需要了解到这三套体系无疑成本太高,因此就有了ConversionService用于整合它们三,统一化接口操作。

此接口也是Spring 3.0新增,用于统一化 底层类型转换实现的差异,对外提供统一服务,所以它也被称作类型转换的门面接口,从接口名称xxxService也能看出来其设计思路。它主要有两大实现:

  • GenericConversionService:提供模版实现,如转换器的注册、删除、匹配查找等,但并不内置转换器实现
  • DefaultConversionService:继承自GenericConversionService。在它基础上默认注册了非常多的内建的转换器实现,从而能够实现绝大部分的类型转换需求

ConversionService转换服务它贯穿于Spring上下文ApplicationContext的多项功能,包括但不限于:BeanWrapper处理Bean属性、DataBinder数据绑定、PropertySource外部化属性处理等等。因此想要进一步深入了解的话,ConversionService是你绕不过去的坎。

WebConversionService并非Spirng Framework的API,而属于Spring Boot提供的增强,且起始于2.x版本


类型转换整合格式化器Formatter

Spring 3.0还新增了一个Formatter< T >接口,作用为:将Object格式化为类型T。从语义上理解它也具有类型转换(数据转换的作用),相较于Converter<S,T>它强调的是格式化,因此一般用于时间/日期、数字(小数、分数、科学计数法等等)、货币等场景,举例它的实现:

  • DurationFormatter:字符串和Duration类型的互转
  • CurrencyUnitFormatter:字符串和javax.money.CurrencyUnit货币类型互转
  • DateFormatter:字符串和java.util.Date类型互转。这个就使用得太多了,它默认支持什么格式?支持哪些输出方式,这将在后文详细描述

为了和类型转换服务ConversionService完成整合,对外只提供统一的API。Spring提供了FormattingConversionService专门用于整合Converter和Formatter,从而使得两者具有一致的编程体验,对开发者更加友好。


类型转换底层接口TypeConvert

定义类型转换方法的接口,它在Spring 2.0就已经存在。在还没有ConversionService之前,它的类型转换动作均委托给已注册的PropertyEditor来完成。但自3.0之后,这个转换动作可能被PropertyEditor来做,也可能交给ConversionService处理。

它一共提供三个重载方法:

// @since 2.0
public interface TypeConverter 

 	// value:待转换的source源数据
 	// requiredType:目标类型targetType
 	// methodParam:转换的目标方法参数,主要为了分析泛型类型,可能为null
 	// field:目标的反射字段,为了泛型,可能为null
	<T> T convertIfNecessary(Object value, Class<T> requiredType) throws TypeMismatchException;
	<T> T convertIfNecessary(Object value, Class<T> requiredType, MethodParameter methodParam) throws TypeMismatchException;
	<T> T convertIfNecessary(Object value, Class<T> requiredType, Field field) throws TypeMismatchException;


它是Spring内部使用类型转换的入口,最终委托给PropertyEditor或者注册到ConversionService里的转换器去完成。它的主要实现有:

  • TypeConverterSupport:@since 3.2。继承自PropertyEditorRegistrySupport,它主要是为子类BeanWrapperImpl提供功能支撑。作用有如下两方面:
    • 提供对默认编辑器(支持JDK内置类型的转换如:Charset、Class、Class[]、Properties、Collection等等)和自定义编辑器的管理(PropertyEditorRegistry#registerCustomEditor)
    • 提供get/set方法,把ConversionService管理上(可选依赖,可为null)
  • 数据绑定相关:因为数据绑定强依赖于类型转换,因此数据绑定涉及到的属性访问操作将会依赖于此组件,不管是直接访问属性的DirectFieldAccessor还是功能更强大的BeanWrapperImpl均是如此

总的来说,TypeConverter能把类型的各种实现、API收口于此,Spring把类型转换的能力都转嫁到TypeConverter这个API里面去了。虽然方便了使用,但其内部实现原理稍显复杂。


Spring Boot使用增强

在传统Spring Framework场景下,若想使用ConversionService还得手动档去配置,这对于不太了解其运行机制的同学无疑是有使用门槛的。而在Spring Boot场景下这一切都会变得简单许多,可谓使用起来愈发方便了。

另外,Spring Boot在内建转换器的基础上额外扩展了不少实用转换器,形如:

  • StringToFileConverter:String -> File
  • NumberToDurationConverter:
  • DelimitedStringToCollectionConverter:

PropertyEditor—旧的类型转换

下面是官方对该类的一个描述:

  • PropertyEditor 类为希望允许用户编辑给定类型的属性值的 GUI 提供支持。
  • PropertyEditor 支持各种不同的显示和更新属性值的方式。大多数 PropertyEditor 只需要支持此 API 中可用的不同选项的子集。
  • 简单的 PropertyEditor 可能只支持 getAsText 和 setAsText 方法,不需要支持(比如)paintValue 或 getCustomEditor。更复杂的类型可能无法支持 getAsText 和 setAsText,但会支持 paintValue 和 getCustomEditor。

因为该类出现的比较早,当前java gui还很火,因此该类型转换大多服务于gui,但是我们关注的终点在于文本类型的转换,此接口提供的方法挺多的,和本文类型转换有关的最多只有4个:

//该类不是spring提供了----看导入的包名就明白了,这个是jdk提供的类型转换接口
package java.beans;

public interface PropertyEditor 
//设置属性值
    void setValue(Object value);
//获取属性值
    Object getValue();
//输出。把属性值转换成String输出
    String getAsText();
//输入。将String转换为属性值类型输入
    void setAsText(String text) throws java.lang.IllegalArgumentException;--

    /**
为值更改添加侦听器。当属性编辑器更改其值时,
它应在所有已注册的 PropertyChangeListener 上触发 PropertyChangeEvent,将属性名称和自身指定为空值作为源。
     */
    void addPropertyChangeListener(PropertyChangeListener listener);
    void removePropertyChangeListener(PropertyChangeListener listener);

//其余方法不需要关心,因为都是关于GUI的
   ....

JDK对PropertyEditor接口提供了一个默认实现java.beans.PropertyEditorSupport,因此我们若需扩展此接口,仅需继承此类,根据需要复写getAsText/setAsText这两个方法即可,Spring无一例外都是这么做的。


PropertyEditorSupport

PropertyEditor作为一个JDK原生接口,内置了一些基本实现来服务于GUI程序,如:

  • BooleanEditor:将true/false字符串转换为Boolean类型
  • IntegerEditor:将字符串转换为Integer类型
  • 同类别的还有LongEditor、FloatEditor…

JDK内置的实现比较少(如上),功能简陋,但对于服务GUI程序来说已经够用,毕竟界面输入的只可能是字符串,并且还均是基础类型。但这对于复杂的Spring环境、以及富文本的web环境来说就不够用了,所以Spring在此基础上有所扩展,因此才有了本文来讨论。

public class PropertyEditorSupport implements PropertyEditor 
    //需要被修改的值
    private Object value;
    //用于事件触发的源
    private Object source;
    //监听器集合
    private java.util.Vector<PropertyChangeListener> listeners;

    public PropertyEditorSupport() 
        setSource(this);
    

    public PropertyEditorSupport(Object source) 
        if (source == null) 
           throw new NullPointerException();
        
        setSource(source);
    
    
    public Object getSource() 
        return source;
    

    public void setSource(Object source) 
        this.source = source;
    

    public void setValue(Object value) 
        this.value = value;
        //发出事件
        firePropertyChange();
    

    public Object getValue() 
        return value;
    




  
    /**
     * Gets the property value as a string suitable for presentation
     * to a human to edit.
     *
     * @return The property value as a string suitable for presentation
     *       to a human to edit.
     * Returns null if the value can't be expressed as a string.
     *If a non-null value is returned, then the PropertyEditor should
     * be prepared to parse that string back in setAsText().
     */
    public String getAsText() 
        return (this.value != null)
                ? this.value.toString()
                : null;
    

    /**
     * Sets the property value by parsing a given String.  May raise
     * java.lang.IllegalArgumentException if either the String is
     * badly formatted or if this kind of property can't be expressed
     * as text.
     *
     * @param text  The string to be parsed.
     */
    public void setAsText(String text) throws java.lang.IllegalArgumentException 
        if (value instanceof String) 
            setValue(text);
            return;
        
        throw new java.lang.IllegalArgumentException(text);
    

    public synchronized void addPropertyChangeListener(
                                PropertyChangeListener listener) 
         //监听器集合是懒加载的,如果不需要监听器,那么就不会创建集合占用内存---大家学习这种懒加载思想                       
        if (listeners == null) 
            listeners = new java.util.Vector<>();
        
        listeners.addElement(listener);
    

    public synchronized void removePropertyChangeListener(
                                PropertyChangeListener listener) 
        if (listeners == null) 
            return;
        
        listeners.removeElement(listener);
    


    public void firePropertyChange() 
        java.util.Vector<PropertyChangeListener> targets;
        synchronized (this) 
            if (listeners == null) 
                return;
            
            //拷贝一份?----其实我不太理解
            targets = unsafeClone(listeners);
        
        // PropertyChangeEvent 专门的一个属性改变的事件类
        PropertyChangeEvent evt = new PropertyChangeEvent(source, null, null, null);
        //将发生的事件告知所有的监听器
        for (int i = 0; i < targets.size(); i++) 
            PropertyChangeListener target = targets.elementAt(i);
            target.propertyChange(evt);
        
    

    @SuppressWarnings("unchecked")
    private <T> java.util.Vector<T> unsafeClone(java.util.Vector<T> v) 
        return (java.util.Vector<T>)v.clone();
    


大家可以学习一下该类中的懒加载思想和监听器机制


Spring为何基于它扩展?

官方的javadoc都说得很清楚:PropertyEditor设计是为GUI程序服务的,那么Spring为何看上它了呢?

试想一下:那会的Spring只能支持xml方式配置,而XML属于文本类型配置,但是此属性对应的类型却不一定是字符串,可能是任意类型,这种需求和gui其实是一样的。

为了实现这种需求,在PropertyEditorSupport的基础上只需要复写setAsText和getAsText这两个方法就行,然后Spring就这么干了。


Spring内建扩展实现有哪些?

Spring为了扩展自身功能,提高配置灵活性,扩展出了非常非常多的PropertyEditor实现,共计40余个,部分截图如下:

这边就拿一个举个例子,来看看spring的具体实现:

public class CustomBooleanEditor extends PropertyEditorSupport 

	/**
	 * Value of @code "true".
	 */
	public static final String VALUE_TRUE = "true";

	/**
	 * Value of @code "false".
	 */
	public static final String VALUE_FALSE = "false";

	/**
	 * Value of @code "on".
	 */
	public static final String VALUE_ON = "on";

	/**
	 * Value of @code "off".
	 */
	public static final String VALUE_OFF = "off";

	/**
	 * Value of @code "yes".
	 */
	public static final String VALUE_YES = "yes";

	/**
	 * Value of @code "no".
	 */
	public static final String VALUE_NO = "no";

	/**
	 * Value of @code "1".
	 */
	public static final String VALUE_1 = "1";

	/**
	 * Value of @code "0".
	 */
	public static final String VALUE_0 = "0";


	@Nullable
	private final String trueString;

	@Nullable
	private final String falseString;

	private final boolean allowEmpty;


	public CustomBooleanEditor(boolean allowEmpty) 
		this(null, null, allowEmpty);
	


	public CustomBooleanEditor(@Nullable String trueString, @Nullable String falseString, boolean allowEmpty) 
		this.trueString = trueString;
		this.falseString = falseString;
		this.allowEmpty = allowEmpty;
	

    //如果自己设置了trueString或者falseString,那么就拿指定的字符串进行比较,来决定真假
    //否则用默认的来进行比较
	@Override
	public void setAsText(@Nullable String text) throws IllegalArgumentException 
		String input = (text != null ? text.trim() : null);
		if (this.allowEmpty && !StringUtils.hasLength(input)) 
			// Treat empty String as null value.
			setValue(null);
		
		else if (this.trueString != null && this.trueString.equalsIgnoreCase(input)) 
			setValue(Boolean.TRUE);
		
		else if (this.falseString != null && this.falseString.equalsIgnoreCase(input)) 
			setValue(Boolean.FALSE);
		
		else if (this.trueString == null &&
				(VALUE_TRUE.equalsIgnoreCase(input) || VALUE_ON.equalsIgnoreCase(input) ||
						VALUE_YES.equalsIgnoreCase(input) || VALUE_1.equals(input))) 
			setValue(Boolean.TRUE);
		
		else if (this.falseString == null &&
				(VALUE_FALSE.equalsIgnoreCase(input) || VALUE_OFF.equalsIgnoreCase(input) ||
						VALUE_NO.equalsIgnoreCase(input) || VALUE_0.equals(input))) 
			setValue(Boolean.FALSE);
		
		else 
			throw new IllegalArgumentException("Invalid boolean value [" + text + "]");
		
	

	@Override
	public String getAsText() 
		if (Boolean.TRUE.equals(getValue())) 
			return (this.trueString != null ? this.trueString : VALUE_TRUE);
		
		else if (Boolean.FALSE.equals(getValue())) 
			return (this.falseString != null ? this.falseString : VALUE_FALSE);
		
		else 
			return "";
		
	


测试一下:

        //允许为空
        CustomBooleanEditor booleanEditor = new CustomBooleanEditor(true);
        booleanEditor.setAsText("1");
        String asText = booleanEditor.getAsText();
        System.out.println("结果为: "+asText);

       //自定义
        CustomBooleanEditor customBooleanEditor = new CustomBooleanEditor("真的", "假的", true);
        customBooleanEditor.setAsText("真的");
        String asText1 = customBooleanEditor.getAsText();
        System.out.println("结果为: "+asText1);


spring提供的其他类型转换器,大致思路差不多,这里不列举了


特殊实现

把没有放在org.springframework.beans.propertyeditors包下的实现称作特殊实现(前者称为标准实现)。