Spring型別轉換(Converter)

baka-sky發表於2018-12-30

Spring的型別轉換

以前在面試中就有被問到關於spring資料繫結方面的問題,當時對它一直只是朦朦朧朧的概念,最近稍微閒下來有時間看了一下其中資料轉換相關的內容,把相應的內容做個記錄。

下面先說明如何去用,然後再放一下個人看引數繫結原始碼的一些筆記,可能由於實力不夠,有些地方說的不是很正確,如果有紕漏還請各位指出。

ConversionService

原生的Java是有一個可以提供資料轉換功能的工具——PropertyEditor。但是它的功能有限,它只能將字串轉換為一個Java物件。在web專案中,如果只看與前端互動的那一部分,這個功能的確已經足夠了。但是在後臺專案內部可就得重新想辦法了。

Spring針對這個問題設計了Converter模組,它位於org.springframework.core.converter包中。該模組足以替代原生的PropertyEditor,但是spring選擇了同時支援兩者,在Spring MVC處理引數繫結時就用到了。

該模組的核心是ConversionService介面,內容如下:

public interface ConversionService {

    boolean canConvert(@Nullable Class<?> sourceType, Class<?> targetType);

    boolean canConvert(@Nullable TypeDescriptor sourceType, TypeDescriptor targetType);

    @Nullable
    <T> T convert(@Nullable Object source, Class<T> targetType);

    @Nullable
    Object convert(@Nullable Object source, @Nullable TypeDescriptor sourceType, TypeDescriptor targetType);

}

介面裡的方法定義的還是比較直觀的,見名知意。其中的TypeDescriptor是spring自己定義的類,它提供了獲取型別更多資訊的便捷方法。比如是否含有註解、是否實現map介面、獲取map的key與value的TypeDescriptor等等。

由此可見,converter模組不僅支援任意型別之間的轉換,而且能更簡單地獲得更多的型別資訊從而做出更細緻的型別轉換。

轉換器

ConversionService只是個Service,對於每個型別轉換的操作,它並不是最終的操作者,它會將相應操作交給對應型別的轉換器。而在實際專案中,由於業務複雜,對型別轉換的要求也不一樣,因此spring提供了幾個介面來方便自定義轉換器。

Converter<S, T>

介面定義如下:

@FunctionalInterface
public interface Converter<S, T> {
    @Nullable
    T convert(S var1);
}

該介面非常簡單,只定義了一個轉換方法,兩個泛型引數則是需要轉換的兩個型別。在單獨處理兩個型別的轉換時這是首選,即一對一,但是倘若有同一父類(或介面)的型別需要進行型別轉化,為每個型別都寫一個Converter顯然是十分不理智的。對於這種情況,spring提供了一個ConverterFactory介面。

ConverterFactory<S, R>

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

我們可以看到,該工廠方法可以生產從S型別到T型別的轉換器,而T型別必定繼承或實現R型別,我們可以形象地稱為“一對多”,因此該介面更適合實現需要轉換為同一型別的轉換器。

對於大部分需求上面兩個介面其實已經足夠了(至少我感覺是),但是不是還沒用到TypeDescriptor嗎?如果要實現更為複雜的轉換功能的話,spring提供了擁有TypeDescriptor引數的GenericConverter介面。

GenericConverter

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;
        }
        
        // 省去了一些Override方法
    }
}

GenericConverter中擁有一個內部類ConvertiblePair,這個內部類的作用只是封裝轉換的源型別與目標型別。

對於GenericConvertergetConvertibleTypes方法就返回這個轉換器支援的轉換型別(一對一,一對多,多對多都可以滿足),convert方法和以前一樣是負責處理具體的轉換邏輯。

而且,如果你覺得對於一個轉換器來說只通過判斷源型別和目標型別是否一致來決定是否支援轉換還不夠,Spring還提供了另一個介面ConditionalGenericConverter

ConditionalGenericConverter

public interface ConditionalConverter {

   boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType);

}

public interface ConditionalGenericConverter extends GenericConverter, ConditionalConverter {

}

ConditionalGenericConverter介面繼承了GenericConverterConditionalConverter介面,在matches方法中就可以在源型別與目標型別已經匹配的基礎上再進行判斷是否支援轉換。

Spring官方實現ConditionalGenericConverter介面的轉換器大多用來處理有集合或陣列參與的轉換,這其中的matches方法就用來判斷集合或陣列中的元素是否能夠成功轉換。而且因為GenericConverterConditionalGenericConverter介面功能太類似,索性就直接實現ConditionalGenericConverter介面了。

如何使用

那麼如何使用轉換器呢,Spring要求我們要把所有需要使用轉換器註冊到ConversionService,這樣Spring在遇到型別轉換的情況時,會去ConversionService中尋找支援的轉換器,進行必要的格式轉換。

支援轉換器註冊的介面為ConverterRegistry

public interface ConverterRegistry {

    void addConverter(Converter<?, ?> converter);

    <S, T> void addConverter(Class<S> sourceType, Class<T> targetType, Converter<? super S, ? extends T> converter);

    void addConverter(GenericConverter converter);

    void addConverterFactory(ConverterFactory<?, ?> factory);

    void removeConvertible(Class<?> sourceType, Class<?> targetType);

}

但我們使用的是另一個繼承了ConversionServiceConverterRegistry的介面ConfigurableConversionService,通過這個介面,就可以註冊自定義的轉換器了。

格式化

轉換器提供的功能是一個型別到另一個型別的單向轉換,而在web專案中,有些資料是需要經常做雙向轉換,最常見的就是日期時間了。將請求中一定格式的字串轉換為日期型別,而在返回的相應中將日期型別再做指定格式的格式化,Spring中提供的工具就是Formatter介面。

Formatter<T>

@FunctionalInterface
public interface Printer<T> {
    String print(T object, Locale locale);
}

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

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

Formatter介面中擁有兩個方法,一個是解析字串的parse,一個是將字串格式化的print,兩個方法都擁有Locale型別的引數,因此還可根據地區來做出相應的定製。

那麼如何使用Formatter呢?由於註解的出現,大量需要在xml中的配置項都直接換為註解的方式,Formatter也是,Spring提供了AnnotationFormatterFactory這個介面。

AnnotationFormatterFactory<A extends Annotation>

public interface AnnotationFormatterFactory<A extends Annotation> {

    Set<Class<?>> getFieldTypes();

    Printer<?> getPrinter(A annotation, Class<?> fieldType);

    Parser<?> getParser(A annotation, Class<?> fieldType);
    
}

getFieldTypes方法返回的是當這些型別有A註解的時候我才會做格式化操作,getPrinter方法和getParser則分別獲取相應的物件,我們也可以直接將Formatter物件返回。

如何使用

格式化的操作,本質上來說也是型別轉換,即String => ? 和? => String。因此Spring將轉換器與格式化同質化,在程式碼實現中,Formatter也是被轉換為相應的Printer轉換器和Parser轉換器,那麼,Formatter也就可以註冊到ConversionService中了。

可以註冊Formatter的介面為FormatterRegistry,該介面繼承自ConverterRegistry,將它與ConversionService一起實現的類是FormattingConversionService

public interface FormatterRegistry extends ConverterRegistry {

    void addFormatter(Formatter<?> formatter);

    void addFormatterForFieldType(Class<?> fieldType, Formatter<?> formatter);

    void addFormatterForFieldType(Class<?> fieldType, Printer<?> printer, Parser<?> parser);

    void addFormatterForFieldAnnotation(AnnotationFormatterFactory<? extends Annotation> annotationFormatterFactory);

}

令人非常遺憾的是,除了通過ConversionServiceconvert直接使用,Formatterprint方法通過框架使用的條件比較特殊,它需要spring標籤的支援才能做到在頁面上的格式化,parse只需要在相應欄位上打上註解即可。

寫寫程式碼

說了這麼多,自然還是來點程式碼更實在。

對於ConverterConverterFactory以及Formatter,使用在SpringMVC的引數繫結上的機會會更多,所以直接在web專案裡寫。而ConditionalGenericConverter介面官方實現的例子已經很豐富了,至少我沒想到什麼新的需求,想要看程式碼的話可以直接去看官方的原始碼(比如ArrayToCollectionConverter),我就不自己寫了。

以下程式碼基於SpringBoot 2.1.1,對應的SpringMVC為5.1.3,使用了lombok

@RestController
@RequestMapping("test")
public class TestController {
    @GetMapping("/index")
    public UserEntity test(UserEntity user) {
        return user;
    }
}
@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addFormatters(FormatterRegistry registry) {
        // 為webMVC註冊轉換器
        registry.addConverter(new String2StatusEnumConverter());
        registry.addConverterFactory(new String2EnumConverterFactory());
        registry.addFormatterForFieldAnnotation(new GenderFormatterFactory());
    }
}
@Data
@Component
public class UserEntity {

    private String username;
    private String password;

    // 加上註解的含義為使用列舉的name欄位進行列舉的格式化,可改為id
    @GenderEnumFormat("name")
    private GenderEnum gender;

    private StatusEnum status;

}
public interface EnumInterface {
    Integer getId();
}
@Getter
@AllArgsConstructor
public enum GenderEnum implements EnumInterface {

    MALE(0, "男"),
    FEMALE(1, "女"),
    ;

    private Integer id;
    private String name;
}
@Getter
@AllArgsConstructor
public enum StatusEnum implements EnumInterface {
    ON(1, "啟用"),
    OFF(0, "停用"),
    ;

    private Integer id;
    private String name;

}
/**
 * String to StatusEnum 的轉換器
 */
public class String2StatusEnumConverter implements Converter<String, StatusEnum> {

    @Override
    public StatusEnum convert(String s) {
        // 注意,這裡是通過id匹配
        for (StatusEnum e : StatusEnum.values()) {
            if (e.getId().equals(Integer.valueOf(s))) {
                return e;
            }
        }
        return null;
    }
}
/**
 * String to EnumInterface 的轉換器工廠
 */
public class String2EnumConverterFactory implements ConverterFactory<String, EnumInterface> {

    @Override
    public <T extends EnumInterface> Converter<String, T> getConverter(Class<T> targetType) {
        return new String2Enum<>(targetType);
    }

    /**
     * 轉換器
     */
    private class String2Enum<T extends EnumInterface> implements Converter<String, T> {

        private final Class<T> targetType;

        private String2Enum(Class<T> targetType) {
            this.targetType = targetType;
        }

        @Override
        public T convert(String source) {
            for (T enumConstant : targetType.getEnumConstants()) {
                if (enumConstant.getId().toString().equals(source)) {
                    return enumConstant;
                }
            }
            return null;
        }
    }
}
/**
 * 將打上註解的GenderEnum通過特定的欄位轉換為列舉
 */
@Target({ElementType.TYPE, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface GenderEnumFormat {
    String value();
}
public class GenderFormatterFactory implements AnnotationFormatterFactory<GenderEnumFormat> {
    @Override
    public Set<Class<?>> getFieldTypes() {
        return Collections.singleton(GenderEnum.class);
    }

    @Override
    public Printer<?> getPrinter(GenderEnumFormat annotation, Class<?> fieldType) {
        return new GenderFormatter(annotation.value());
    }

    @Override
    public Parser<?> getParser(GenderEnumFormat annotation, Class<?> fieldType) {
        return new GenderFormatter(annotation.value());
    }

    final class GenderFormatter implements Formatter<GenderEnum> {
        private final String fieldName;
        private Method getter;

        private GenderFormatter(String fieldName) {
            this.fieldName = fieldName;
        }

        @Override
        public GenderEnum parse(String text, Locale locale) throws ParseException {
            if (getter == null) {
                try {
                    getter = GenderEnum.class.getMethod("get" + fieldName.substring(0, 1).toUpperCase() + fieldName.substring(1));
                } catch (NoSuchMethodException e) {
                    throw new ParseException(e.getMessage(), 0);
                }
            }
            for (GenderEnum e : GenderEnum.values()) {
                try {
                    if (getter.invoke(e).equals(text)) {
                        return e;
                    }
                } catch (IllegalAccessException | InvocationTargetException e1) {
                    throw new ParseException(e1.getMessage(), 0);
                }
            }
            throw new ParseException("輸入引數有誤,不存在這樣的列舉值:" + text, 0);
        }

        @Override
        public String print(GenderEnum object, Locale locale) {
            try {
                // 這裡應該也判斷一下getter是否為null然後選擇進行初始化,但是因為print方法沒有效果所以也懶得寫了
                return getter.invoke(object).toString();
            } catch (IllegalAccessException | InvocationTargetException e) {
                return e.getMessage();
            }
        }
    }
}

原始碼筆記

之前一直說型別轉換在Spring MVC的引數繫結中有用到,下面就放一下本人的一些筆記。由於實力問題有些地方也有些懵逼,也歡迎大家交流。

(看原始碼的時候突然遇到IDEA無法下載原始碼,搜出來的結果大致都是說更換maven版本,懶得更改就直接用maven命令下載原始碼了:

mvn dependency:sources -DincludeArtifactIds=spring-webmvc

不加引數的話會預設下載全部的原始碼)

public class InvocableHandlerMethod extends HandlerMethod {
        // ...
        protected Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception {
            if (ObjectUtils.isEmpty(this.getMethodParameters())) {
                return EMPTY_ARGS;
            } else {
                // 得到處理方法的方法引數
                MethodParameter[] parameters = this.getMethodParameters();
                Object[] args = new Object[parameters.length];

                for (int i = 0; i < parameters.length; ++i) {
                    MethodParameter parameter = parameters[i];
                    // 初始化,之後可以呼叫MethodParameter物件的getParameterName方法
                    parameter.initParameterNameDiscovery(this.parameterNameDiscoverer);
                    // 如果providedArgs包含當前引數的型別就賦值
                    args[i] = findProvidedArgument(parameter, providedArgs);
                    if (args[i] == null) {
                        // resolvers包含了所有的引數解析器(HandlerMethodArgumentResolver的實現類,常見的比如RequestParamMethodArgumentResolver,PathVariableMethodArgumentResolver等,就是在引數前加的註解的處理類,有對應的註解的話就會用對應的解析器去處理引數繫結,如果沒有註解的話通常會和有ModelAttribute註解一樣使用ServletModelAttributeMethodProcessor,具體判斷在每個實現類的supportsParameter方法裡)
                        if (!this.resolvers.supportsParameter(parameter)) {
                            throw new IllegalStateException(formatArgumentError(parameter, "No suitable resolver"));
                        }

                        try {
                            // 使用解析器開始解析引數
                            args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);
                        } catch (Exception var10) {
                            if (this.logger.isDebugEnabled()) {
                                String error = var10.getMessage();
                                if (error != null && !error.contains(parameter.getExecutable().toGenericString())) {
                                    this.logger.debug(formatArgumentError(parameter, error));
                                }
                            }

                            throw var10;
                        }
                    }
                }

                return args;
            }
        }
        // ...
    }

public abstract class AbstractNamedValueMethodArgumentResolver implements HandlerMethodArgumentResolver {
    // ...
    public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
                                        NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
        // 獲取paramter的資訊,NamedValueInfo包含引數的名稱、是否必填、預設值,其實就是該引數在RequestParam註解中的配置
        NamedValueInfo namedValueInfo = getNamedValueInfo(parameter);
        // 如果parameter是Optional型別,那麼就產生一個指向相同引數物件但巢狀等級(nestingLevel)+1的MethodParameter
        MethodParameter nestedParameter = parameter.nestedIfOptional();
        // 先後解析配置項與SPEL表示式(即${}、#{})
        Object resolvedName = resolveStringValue(namedValueInfo.name);
        if (resolvedName == null) {
            throw new IllegalArgumentException(
                    "Specified name must not resolve to null: [" + namedValueInfo.name + "]");
        }
        // 從請求(request)中獲取對應名稱的資料,如果非上傳檔案,就相當於servlet中的request.getParameter(),另外如果有多個符合name的值會返回String[]
        Object arg = resolveName(resolvedName.toString(), nestedParameter, webRequest);
        if (arg == null) {
            if (namedValueInfo.defaultValue != null) {
                // 請求中沒有這個引數並且有預設值就將解析defaultValue後值的設為引數
                arg = resolveStringValue(namedValueInfo.defaultValue);
            } else if (namedValueInfo.required && !nestedParameter.isOptional()) {
                // 引數必填且方法的型別要求不是Optional的話拋異常
                handleMissingValue(namedValueInfo.name, nestedParameter, webRequest);
            }
            // 處理null值。如果引數型別(或者被Optional包裹的型別)是Boolean會轉換成false,而如果引數型別是基本型別的話會丟擲異常(因為基本型別值不能為null)
            arg = handleNullValue(namedValueInfo.name, arg, nestedParameter.getNestedParameterType());
        } else if ("".equals(arg) && namedValueInfo.defaultValue != null) {
            // 如果有預設值將會把空字串處理為預設值
            arg = resolveStringValue(namedValueInfo.defaultValue);
        }

        if (binderFactory != null) {
            // biner中有conversionService的例項,而conversionService中就包含著全部可用的轉換器。
            WebDataBinder binder = binderFactory.createBinder(webRequest, null, namedValueInfo.name);
            try {
                // 開始真正的型別轉換
                arg = binder.convertIfNecessary(arg, parameter.getParameterType(), parameter);
            } catch (ConversionNotSupportedException ex) {
                throw new MethodArgumentConversionNotSupportedException(arg, ex.getRequiredType(),
                        namedValueInfo.name, parameter, ex.getCause());
            } catch (TypeMismatchException ex) {
                throw new MethodArgumentTypeMismatchException(arg, ex.getRequiredType(),
                        namedValueInfo.name, parameter, ex.getCause());

            }
        }

        // 鉤子方法,重寫這個方法的暫時只有PathVariableMethodArgumentResolver
        handleResolvedValue(arg, namedValueInfo.name, parameter, mavContainer, webRequest);

        return arg;
    }
    // ...
}
class TypeConverterDelegate {
    // ...

    /**
     * Convert the value to the required type (if necessary from a String),
     * for the specified property.
     *
     * @param propertyName   name of the property
     * @param oldValue       the previous value, if available (may be {@code null})
     * @param newValue       the proposed new value
     * @param requiredType   the type we must convert to
     *                       (or {@code null} if not known, for example in case of a collection element)
     * @param typeDescriptor the descriptor for the target property or field
     * @return the new value, possibly the result of type conversion
     * @throws IllegalArgumentException if type conversion failed
     */
    @SuppressWarnings("unchecked")
    @Nullable
    public <T> T convertIfNecessary(@Nullable String propertyName, @Nullable Object oldValue, @Nullable Object newValue,
                                    @Nullable Class<T> requiredType, @Nullable TypeDescriptor typeDescriptor) throws IllegalArgumentException {
        // 在當前的流程中propertyName、oldValue為null,newValue為前臺傳過來的真實引數值,requiredType為處理方法要求的型別,typeDescriptor為要求型別的描述封裝類

        // 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) {
            // 上述條件成立
            // 在現在的邏輯裡sourceTypeDesc必然為String的TypeDescriptor
            TypeDescriptor sourceTypeDesc = TypeDescriptor.forObject(newValue);
            if (conversionService.canConvert(sourceTypeDesc, typeDescriptor)) {
                // 可以轉換
                // canConvert實際上是嘗試獲取符合條件的GenericConverter,如果有就說明可以轉換
                // 對於String -> Integer的轉換,會先將String型別拆為 [String,Serializable,Comparable,CharSequence,Object]的型別層,Integer同樣拆為自己的型別層,之後先後遍歷每個型別來準確判斷是否存在可以轉換的轉換器
                try {
                    // 最終會呼叫到自定義的轉換器
                    return (T) conversionService.convert(newValue, sourceTypeDesc, typeDescriptor);
                } catch (ConversionFailedException ex) {
                    // fallback to default conversion logic below
                    // 轉換失敗,暫存異常,將會執行預設的轉換邏輯
                    conversionAttemptEx = ex;
                }
            }
        }
        // 因為spring自帶了很多常見型別的轉換器,大部分都可以通過上面的轉換器完成。
        // 程式執行到這裡沒有結束的話很可能說明型別是沒有定義轉換器的自定義型別或者引數格式真的不正確

        Object convertedValue = newValue;

        // Value not of required type?
        if (editor != null || (requiredType != null && !ClassUtils.isAssignableValue(requiredType, convertedValue))) {
            // 最後的條件為 當newValue不是requiredType的例項
            if (typeDescriptor != null && requiredType != null && Collection.class.isAssignableFrom(requiredType) &&
                    convertedValue instanceof String) {
                // isAssignableFrom用來判斷Collection是否為requiredType的父類或者介面,或者二者是否為同一型別或介面
                TypeDescriptor elementTypeDesc = typeDescriptor.getElementTypeDescriptor();
                if (elementTypeDesc != null) {
                    Class<?> elementType = elementTypeDesc.getType();
                    if (Class.class == elementType || Enum.class.isAssignableFrom(elementType)) {
                        // 相當於convertedValue.split(",")
                        convertedValue = StringUtils.commaDelimitedListToStringArray((String) convertedValue);
                    }
                }
            }
            if (editor == null) {
                editor = findDefaultEditor(requiredType);
            }
            // 使用預設的editor進行轉換,不過預設的editor的轉換有可能與期望的不一致。(比如 "1,2,3,4" -> ArrayList<String>{"1,2,3,4"},結果是隻有一個元素的list)
            convertedValue = doConvertValue(oldValue, convertedValue, requiredType, editor);
        }

        boolean standardConversion = false;

        // 加下來會根據requiredType來做出相應的轉換
        if (requiredType != null) {
            // Try to apply some standard type conversion rules if appropriate.

            if (convertedValue != null) {
                if (Object.class == requiredType) {
                    // requiredType是Object
                    return (T) convertedValue;
                } else if (requiredType.isArray()) {
                    // requiredType是陣列
                    // 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) {
                    // 將convertedValue轉換為集合,內部對每個元素呼叫了convertIfNecessary(即本方法)
                    // Convert elements to target type, if determined.
                    convertedValue = convertToTypedCollection(
                            (Collection<?>) convertedValue, propertyName, requiredType, typeDescriptor);
                    standardConversion = true;
                } else if (convertedValue instanceof Map) {
                    // 將convertedValue轉換為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() && trimmedValue.isEmpty()) {
                        // 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 (requiredType == Optional.class) {
                    convertedValue = Optional.empty();
                }
            }

            if (!ClassUtils.isAssignableValue(requiredType, convertedValue)) {
                if (conversionAttemptEx != null) {
                    // Original exception from former ConversionService call above...
                    throw conversionAttemptEx;
                } else if (conversionService != null && typeDescriptor != 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;
    }
    // ...
}

最後

看原始碼雖然很費時間,但是的確是能學到很多東西的,而且也能發現很多以前不知道的事情(比如RequestParam註解的name和defaultName引數是可以巢狀引用配置檔案中的內容,也可以寫SPEL表示式),但其中還是有一些地方不是很清楚。

雖說現在專案都直接使用JSON做前後端互動,大部分型別轉換的任務都交給了JSON序列化框架,但是引數繫結這裡還是值得看一看,等到需要用的時候就可以直接拿出來用。

相關文章