Spring COC TypeConverter

Spring的核心思想是IOC(Inversion of Control),DI其實就是IOC的另一種說法。所謂IoC,對於spring框架來講,就是由spring來負責控制對象的生命週期和對象間的關係。當一個對象須要使用其它對象時,經過Spring容器動態的向這個對象提供它所須要的其餘對象。這一點是經過DI(Dependency Injection,依賴注入)來實現的。html

這裏提到Spring IOC主要是爲了說明Spring IOC中的(Convention over configuration) -- 約定優於配置的一個體現,那就是類型轉換。Spring把它包裝得太好了,可能你們都沒有意識到。我下面簡單的舉一個例子:前端

一、User.java -- 實體類java

public class User {
    
    private String name;
    
    private Integer age;

    // getter and setter
    
}

二、beans.xml -- Spring配置文件spring

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans 
       http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="user" class="com.carlzone.springboot.mybatis.User">
        <property name="name" value="carl" />
        <property name="age" value="27" />
    </bean>
    
</beans>

三、Main.java 測試類segmentfault

public class Main {

    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("test-beans.xml");
        User user = context.getBean(User.class);
        System.out.println(user);
    }
    
}

結果毫無疑問,控制檯會把User類的name和age輸出出來。可是你們有不有想過這樣一個問題,在對象實體User中,咱們的屬性類型這裏有String,也有Integer.固然這裏舉了2個簡單的數據類型,Spring還支持更加複雜的數據類型。Spring是如何把咱們配置在xml裏面的屬性轉換成咱們須要的類型呢?是否是以前沒有想過這個問題,下面咱們就來分析一下Spring內部是如何這個類型轉換的。springboot

一、原因

其實我以前在看Spring 源碼的時候,對於Spring IOC這塊一直都看得不是很明白。直到以前看公司代碼的時候讓我看到了項目中使用了 FormattingConversionServiceFactoryBean這個對象。其實這個對象是一個Factory Bean,若是你們對於這個概念不太明白能夠看我以前的blog -- Spring bean 之 FactoryBean。經過對這個對象的源碼分析讓我明白了Spring的類型轉換是若是實現的。mybatis

二、Type Conversion SPI

Spring從Spring 3開始新添加了一個包core.conver用來提供通常類型的轉換系統。這個系統中定義了SPI在運行時期來實現類型轉換邏輯。在Spring容器中,這個系統可使用PropertyEditors把bean的屬性值轉換成須要的類型。一樣的這個API一樣會在你的應用中被使用到。下面咱們來看一下Spring的類型轉換API。app

2.1 Converter SPI

這個SPI用於實現類型轉換邏輯。框架

package org.springframework.core.convert.converter;

public interface Converter<S, T> {

    T convert(S source);

}

2.2 Formatter SPI

Formatter SPI用於實現格式化邏輯。ide

package org.springframework.format;

public interface Formatter<T> extends Printer<T>, Parser<T> {
}

Formatter是繼承自Printer,Parser接口

public interface Printer<T> {
    String print(T fieldValue, Locale locale);
}
import java.text.ParseException;

public interface Parser<T> {
    T parse(String clientValue, Locale locale) throws ParseException;
}

不難看出雖然Format接口實際上是Converter接口的一個子集,它只是類型轉換的一種特例。

  • Format : Printer接口實現 T -> String,而Parser接口實現 String -> T.
  • Converter : 而Converter接口是實現 S -> T,從任意對象轉換成任意對象。

這裏只是簡單的介紹了一下Spring關於的Spring Type Conversion與Spring Field Formatting接口方便後續的分析。若是你們想要了解更多詳情能夠查看Spring官網的介紹。下面咱們就來看看Spring類型轉換的內部實現。

三、Type Converter Internal

咱們仍是首先來看看咱們最開始提到的類,FormattingConversionServiceFactoryBean。最開始也說到這個類實際上是一個FactoryBean。Spring IOC在進行容器初始的時候會經過它的getObject()獲取到它想建立的對象。因此說個人目標就轉換到了FormattingConversionService這個對象。其實Spring真正建立的對象是DefaultFormattingConversionService。下面咱們就來看一下它的類繼承體系。

這裏寫圖片描述

3.1 相關接口與類

其實咱們看類繼承體系(e.g.:這裏只畫出了相關接口),主要仍是看它實現的接口,這樣就能夠大概知道這個類幹了哪些事。這個體系裏面有4個接口。

  • ConversionService:類型轉換服務,提供判斷類型之間是否能夠轉換,以及轉換方法。
  • ConverterRegistry :類型轉換服務註冊接口,提供類型轉換服務的註冊接口。
  • ConfigurableConversionService:這個是個空接口,只是同時繼承了ConversionService與ConverterRegistry接口。
  • FormatterRegistry:Formatter服務接口註冊接口。

其實這裏最須要關注的還轉換服務的註冊以及轉換服務的獲取。在解釋這2個方法以前,再來介紹2個類:

一、GenericConverter

格式轉換包裝類,包裝Formatter以及Converter.內部類ConvertiblePair提供這兩種的索引。

public interface GenericConverter {

    Set<ConvertiblePair> getConvertibleTypes();

    Object convert(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;
        }

        @Override
        public boolean equals(Object other) {
            if (this == other) {
                return true;
            }
            if (other == null || other.getClass() != ConvertiblePair.class) {
                return false;
            }
            ConvertiblePair otherPair = (ConvertiblePair) other;
            return (this.sourceType == otherPair.sourceType && this.targetType == otherPair.targetType);
        }

        @Override
        public int hashCode() {
            return (this.sourceType.hashCode() * 31 + this.targetType.hashCode());
        }

        @Override
        public String toString() {
            return (this.sourceType.getName() + " -> " + this.targetType.getName());
        }
    }

}

二、ConditionalConverter

轉換條件類,判斷這個GenericConverter對象是否能夠進行轉換。

public interface ConditionalConverter {

    boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType);

}

3.2 註冊

其實類型轉換的具體實現是在分爲Formatter與Converter的註冊。

  • Converter的註冊發生在GenericConversionService類中。也就是裏面各類不一樣的重載方法addConverter().
  • Formatter的註冊發生在FormattingConversionService類中。也就是裏面各類不一樣的addFormatterXXX()方法。

它會把這兩個接口的實現都會轉換成上面提到的GenericConverter接口實現,而且註冊到GenericConversionService.Converters對象中,裏面有2個屬性。converters與globalConverters這兩個屬性中。

private static class Converters {

        private final Set<GenericConverter> globalConverters = new LinkedHashSet<GenericConverter>();

        private final Map<ConvertiblePair, ConvertersForPair> converters =
                new LinkedHashMap<ConvertiblePair, ConvertersForPair>(36);

        public void add(GenericConverter converter) {
            Set<ConvertiblePair> convertibleTypes = converter.getConvertibleTypes();
            if (convertibleTypes == null) {
                Assert.state(converter instanceof ConditionalConverter,
                        "Only conditional converters may return null convertible types");
                this.globalConverters.add(converter);
            }
            else {
                for (ConvertiblePair convertiblePair : convertibleTypes) {
                    ConvertersForPair convertersForPair = getMatchableConverters(convertiblePair);
                    convertersForPair.add(converter);
                }
            }
        }

        public GenericConverter find(TypeDescriptor sourceType, TypeDescriptor targetType) {
            // Search the full type hierarchy
            List<Class<?>> sourceCandidates = getClassHierarchy(sourceType.getType());
            List<Class<?>> targetCandidates = getClassHierarchy(targetType.getType());
            for (Class<?> sourceCandidate : sourceCandidates) {
                for (Class<?> targetCandidate : targetCandidates) {
                    ConvertiblePair convertiblePair = new ConvertiblePair(sourceCandidate, targetCandidate);
                    GenericConverter converter = getRegisteredConverter(sourceType, targetType, convertiblePair);
                    if (converter != null) {
                        return converter;
                    }
                }
            }
            return null;
        }
    }

當你實現Formatter、Converter接口時,它會把轉換接口以轉換源對象sourceType(Class<?>)與轉換目標對象targetType(Class<?>)生成ConvertiblePair對象插入到一個converters屬性中。若是你實現GenericConverter接口分爲兩種狀況:

1) 若是實現的getConvertibleTypes()返回你須要轉換的源對象與目標對象構成的Set<ConvertiblePair>不爲空。它就會把轉換對象添加到converters屬性中。

2) 若是實現的getConvertibleTypes()返回你須要轉換的源對象與目標對象構成的Set<ConvertiblePair>爲空。它會檢查它的類型是否是ConditionalConverter。因此若是你要實現GenericConverter而且實現getConvertibleTypes()方法返回爲空,那麼你同時須要實現ConditionalConverter。Spring提供了實現了這2個接口的接口ConditionalGenericConverter,你只須要實現它就好了。並且它會把這個轉換器添加到globalConverters屬性中。

3.3 查詢

在Spring中的自定義轉換中,當首先會查詢GenericConversionService.Converters中的converters屬性,而後纔會查詢GenericConversionService.Converters中的globalConverters屬性。因此說實現ConditionalGenericConverter的方法getConvertibleTypes()若是返回爲空,那麼它就是一個備胎。

四、Spring IOC Type Converter

Spring IOC在進行類型轉換的時候最終會調用在TypeConverterDelegate類的convertIfNecessary方法。下面咱們來看一這個方法的具體實現。

class TypeConverterDelegate {

    public <T> T convertIfNecessary(String propertyName, Object oldValue, Object newValue,
            Class<T> requiredType, TypeDescriptor typeDescriptor) throws IllegalArgumentException {

        // Custom editor for this type?
        PropertyEditor editor = this.propertyEditorRegistry.findCustomEditor(requiredType, propertyName);

        ConversionFailedException conversionAttemptEx = null;

        // No custom editor but custom ConversionService specified?
        ConversionService conversionService = this.propertyEditorRegistry.getConversionService();
        if (editor == null && conversionService != null && newValue != null && typeDescriptor != null) {
            TypeDescriptor sourceTypeDesc = TypeDescriptor.forObject(newValue);
            if (conversionService.canConvert(sourceTypeDesc, typeDescriptor)) {
                try {
                    return (T) conversionService.convert(newValue, sourceTypeDesc, typeDescriptor);
                }
                catch (ConversionFailedException ex) {
                    // fallback to default conversion logic below
                    conversionAttemptEx = ex;
                }
            }
        }

        Object convertedValue = newValue;

        // Value not of required type?
        if (editor != null || (requiredType != null && !ClassUtils.isAssignableValue(requiredType, convertedValue))) {
            if (typeDescriptor != null && requiredType != null && Collection.class.isAssignableFrom(requiredType) &&
                    convertedValue instanceof String) {
                TypeDescriptor elementTypeDesc = typeDescriptor.getElementTypeDescriptor();
                if (elementTypeDesc != null) {
                    Class<?> elementType = elementTypeDesc.getType();
                    if (Class.class == elementType || Enum.class.isAssignableFrom(elementType)) {
                        convertedValue = StringUtils.commaDelimitedListToStringArray((String) convertedValue);
                    }
                }
            }
            if (editor == null) {
                editor = findDefaultEditor(requiredType);
            }
            convertedValue = doConvertValue(oldValue, convertedValue, requiredType, editor);
        }

        boolean standardConversion = false;

        if (requiredType != null) {
            // Try to apply some standard type conversion rules if appropriate.

            if (convertedValue != null) {
                if (Object.class == requiredType) {
                    return (T) convertedValue;
                }
                else if (requiredType.isArray()) {
                    // Array required -> apply appropriate conversion of elements.
                    if (convertedValue instanceof String && Enum.class.isAssignableFrom(requiredType.getComponentType())) {
                        convertedValue = StringUtils.commaDelimitedListToStringArray((String) convertedValue);
                    }
                    return (T) convertToTypedArray(convertedValue, propertyName, requiredType.getComponentType());
                }
                else if (convertedValue instanceof Collection) {
                    // Convert elements to target type, if determined.
                    convertedValue = convertToTypedCollection(
                            (Collection<?>) convertedValue, propertyName, requiredType, typeDescriptor);
                    standardConversion = true;
                }
                else if (convertedValue instanceof Map) {
                    // Convert keys and values to respective target type, if determined.
                    convertedValue = convertToTypedMap(
                            (Map<?, ?>) convertedValue, propertyName, requiredType, typeDescriptor);
                    standardConversion = true;
                }
                if (convertedValue.getClass().isArray() && Array.getLength(convertedValue) == 1) {
                    convertedValue = Array.get(convertedValue, 0);
                    standardConversion = true;
                }
                if (String.class == requiredType && ClassUtils.isPrimitiveOrWrapper(convertedValue.getClass())) {
                    // We can stringify any primitive value...
                    return (T) convertedValue.toString();
                }
                else if (convertedValue instanceof String && !requiredType.isInstance(convertedValue)) {
                    if (conversionAttemptEx == null && !requiredType.isInterface() && !requiredType.isEnum()) {
                        try {
                            Constructor<T> strCtor = requiredType.getConstructor(String.class);
                            return BeanUtils.instantiateClass(strCtor, convertedValue);
                        }
                        catch (NoSuchMethodException ex) {
                            // proceed with field lookup
                            if (logger.isTraceEnabled()) {
                                logger.trace("No String constructor found on type [" + requiredType.getName() + "]", ex);
                            }
                        }
                        catch (Exception ex) {
                            if (logger.isDebugEnabled()) {
                                logger.debug("Construction via String failed for type [" + requiredType.getName() + "]", ex);
                            }
                        }
                    }
                    String trimmedValue = ((String) convertedValue).trim();
                    if (requiredType.isEnum() && "".equals(trimmedValue)) {
                        // It's an empty enum identifier: reset the enum value to null.
                        return null;
                    }
                    convertedValue = attemptToConvertStringToEnum(requiredType, trimmedValue, convertedValue);
                    standardConversion = true;
                }
                else if (convertedValue instanceof Number && Number.class.isAssignableFrom(requiredType)) {
                    convertedValue = NumberUtils.convertNumberToTargetClass(
                            (Number) convertedValue, (Class<Number>) requiredType);
                    standardConversion = true;
                }
            }
            else {
                // convertedValue == null
                if (javaUtilOptionalEmpty != null && requiredType == javaUtilOptionalEmpty.getClass()) {
                    convertedValue = javaUtilOptionalEmpty;
                }
            }

            if (!ClassUtils.isAssignableValue(requiredType, convertedValue)) {
                if (conversionAttemptEx != null) {
                    // Original exception from former ConversionService call above...
                    throw conversionAttemptEx;
                }
                else if (conversionService != null) {
                    // ConversionService not tried before, probably custom editor found
                    // but editor couldn't produce the required type...
                    TypeDescriptor sourceTypeDesc = TypeDescriptor.forObject(newValue);
                    if (conversionService.canConvert(sourceTypeDesc, typeDescriptor)) {
                        return (T) conversionService.convert(newValue, sourceTypeDesc, typeDescriptor);
                    }
                }

                // Definitely doesn't match: throw IllegalArgumentException/IllegalStateException
                StringBuilder msg = new StringBuilder();
                msg.append("Cannot convert value of type '").append(ClassUtils.getDescriptiveType(newValue));
                msg.append("' to required type '").append(ClassUtils.getQualifiedName(requiredType)).append("'");
                if (propertyName != null) {
                    msg.append(" for property '").append(propertyName).append("'");
                }
                if (editor != null) {
                    msg.append(": PropertyEditor [").append(editor.getClass().getName()).append(
                            "] returned inappropriate value of type '").append(
                            ClassUtils.getDescriptiveType(convertedValue)).append("'");
                    throw new IllegalArgumentException(msg.toString());
                }
                else {
                    msg.append(": no matching editors or conversion strategy found");
                    throw new IllegalStateException(msg.toString());
                }
            }
        }

        if (conversionAttemptEx != null) {
            if (editor == null && !standardConversion && requiredType != null && Object.class != requiredType) {
                throw conversionAttemptEx;
            }
            logger.debug("Original ConversionService attempt failed - ignored since " +
                    "PropertyEditor based conversion eventually succeeded", conversionAttemptEx);
        }

        return (T) convertedValue;
    }


}

這個Spring IOC類型轉換分爲如下4個步驟:

  1. 經過Java中的PropertyEditor的內省機制對Spring的對象屬性進行類型轉換
  2. 經過Spring中的ConversionService的自定義類型轉換實現對象屬性進行類型轉換
  3. 經過通常類型判斷對對象的屬性進行類型轉換(Array, Collection, Map, String, Number, Optional)
  4. 報錯(不遵循COC -- 約定大於配置)。

五、應用

在Spring經過它的約定大於配置,它幫助咱們實現了一些默認的類型轉換。具體的默認的類型轉換在DefaultFormattingConversionService接口。能夠若是你的包依賴中沒有joda-time,Spring就不會提供String轉換Date的轉換服務。下面咱們就來自定義類型轉換服務:

5.1 Order.java -- 實體類

public class Order {

    private Date createDt;

    public Date getCreateDt() {
        return createDt;
    }

    public void setCreateDt(Date createDt) {
        this.createDt = createDt;
    }

    @Override
    public String toString() {
        return "Order{" +
                "createDt=" + createDt +
                '}';
    }
}

5.2 StringToDateConverter -- 實現Formatter接口

public class StringToDateConverter implements Formatter<Date> {

    private String pattern;

    public StringToDateConverter(String pattern) {
        this.pattern = pattern;
    }

    @Override
    public Date parse(String text, Locale locale) throws ParseException {
        DateFormat dateFormat = new SimpleDateFormat(pattern, locale);
        return dateFormat.parse(text);
    }

    @Override
    public String print(Date date, Locale locale) {
        DateFormat dateFormat = new SimpleDateFormat(pattern, locale);
        return dateFormat.format(date);
    }
}

5.3 ConverterController.java

@RestController
public class ConverterController {

    @InitBinder
    public void init(DataBinder dataBinder){
        dataBinder.addCustomFormatter(new StringToDateConverter("yyyy-MM-dd"));
    }

    @RequestMapping("converter")
    public Order converter(Order order){
        return order;
    }

}

5.4 SpringBootMybatisApplication.java

@SpringBootApplication
public class SpringBootMybatisApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringBootMybatisApplication.class, args);
    }
}

5.4 測試

經過訪問http://localhost:8080/conveter?createDt=2017-08-12,根據以上的測試代碼就會返回如下的結果。

這裏寫圖片描述

在Spring MVC中由於前端HttpServletRequest的傳值只會涉及到String,因此在Spring MVC在進行數據綁定的時候只開放的Formatter接口,而沒有開放Converter接口。

可是咱們可使用FormattingConversionServiceFactoryBean來註冊Converter接口。

public class FormattingConversionServiceFactoryBean
        implements FactoryBean<FormattingConversionService>, EmbeddedValueResolverAware, InitializingBean {

    private Set<?> converters;

    private Set<?> formatters;

    private Set<FormatterRegistrar> formatterRegistrars;

}

它能夠註冊Converter與Formatter接口.Spring會在容器開始依賴注入以前檢測容器中是否有名稱有conversionService,就會把conversionService設計到BeanFactory當中,當類型轉換的時候就會把這個對象設置進去。

protected void finishBeanFactoryInitialization(ConfigurableListableBeanFactory beanFactory) {
        // Initialize conversion service for this context.
        if (beanFactory.containsBean(CONVERSION_SERVICE_BEAN_NAME) &&
                beanFactory.isTypeMatch(CONVERSION_SERVICE_BEAN_NAME, ConversionService.class)) {
            beanFactory.setConversionService(
                    beanFactory.getBean(CONVERSION_SERVICE_BEAN_NAME, ConversionService.class));
        }

        // Initialize LoadTimeWeaverAware beans early to allow for registering their transformers early.
        String[] weaverAwareNames = beanFactory.getBeanNamesForType(LoadTimeWeaverAware.class, false, false);
        for (String weaverAwareName : weaverAwareNames) {
            getBean(weaverAwareName);
        }

        // Stop using the temporary ClassLoader for type matching.
        beanFactory.setTempClassLoader(null);

        // Allow for caching all bean definition metadata, not expecting further changes.
        beanFactory.freezeConfiguration();

        // Instantiate all remaining (non-lazy-init) singletons.
        beanFactory.preInstantiateSingletons();
    }

能夠看到在代碼最開始的時候就是判斷容器是否有這個對象。若是有就設置到BeanFactory裏面。代碼的最後面纔是Spring容器初始化單例bean的邏輯。

beanFactory.preInstantiateSingletons();
相關文章
相關標籤/搜索