Spring類型轉換(Converter)

来源:https://www.cnblogs.com/baka-sky/archive/2018/12/30/10200845.html
-Advertisement-
Play Games

Spring的類型轉換 以前在面試中就有被問到關於spring數據綁定方面的問題,當時對它一直只是朦朦朧朧的概念,最近稍微閑下來有時間看了一下其中數據轉換相關的內容,把相應的內容做個記錄。 下麵先說明如何去用,然後再放一下個人看參數綁定源碼的一些筆記,可能由於實力不夠,有些地方說的不是很正確,如果有 ...


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序列化框架,但是參數綁定這裡還是值得看一看,等到需要用的時候就可以直接拿出來用。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 迭代器模式在 .Net 中使用很廣泛,其迴圈遍歷對於的集合已經實現了迭代器模式 介紹 迭代器模式屬於行為型模式,它通過提供一種方法來順序訪問集合對象中的每個元素,而又不用暴露其內部表示。 類圖描述 代碼實現 1、創建介面 2、創建可遍歷的實體類 C public class NameReposito ...
  • 作為架構師,首先要明確架構師的責任,要不然會再多的技術也是枉然。 簡單的說,帶領方向和難點攻剋。 帶領方向是指架構師應不斷地多讀書,多學習,跟隨最新技術,不斷地升華自己,並不停的為團隊傳輸最新知識,讓整個團隊不斷地進步。 難點攻剋是指架構師要有逢山開路,遇水搭橋的能力。當團隊遇到開發難題的時候,架構 ...
  • 假設文件名為:loga.txt 內容為:你說什麼呢 1 -- open() 打開文件 參數1: 要打開的文件路徑 + 文件名 參數2: 打開方式 r 只讀模式,文本必須存在 r+ 讀寫模式,文件必須存在( 常用這種方式操作文件 ) w 只寫模式,不能調用read()進行讀操作,如果打開一個已存在的文 ...
  • 題意 "題目鏈接" Sol 枚舉第二個球放的位置,用首碼和推一波之後發現可以斜率優化 cpp // luogu judger enable o2 include define Pair pair define MP(x, y) make_pair(x, y) define fi first defi ...
  • 導讀:切片系列文章連續寫了三篇,本文是對它們做的彙總。為什麼要把序列文章合併呢?在此說明一下,本文絕不是簡單地將它們做了合併,主要是修正了一些嚴重的錯誤(如自定義序列切片的部分),還對行文結構與章節銜接做了大量改動,如此一來,本文結構的完整性與內容的質量都得到了很好的保證。 眾所周知,我們可以通過索 ...
  • 開篇來自於經典的“保全的哲學三問”(你是誰,在哪兒,要幹嘛) 問題一、ElasticSearch是什麼?有什麼用處? 答:截至2018年12月28日,從ElasticSearch官網(https://www.elastic.co/cn/products)上,得知:ElasticSearch是基於 J ...
  • url配置就像Django所支撐網站的目錄。它的本質是url與要被該url調用的視圖函數之間的映射表;通過這個映射表可以告知Django,對於客戶端發來的某個url該執行那些代碼。 一、簡單的路由配置 二、有名分組 上面我們說了,帶()就是進行了分組,就會作為位置參數傳給視圖函數,視圖函數也要以位置 ...
  • 一 集合 2018-12-30 集合是一個無序不重覆元素的集。基本功能包括關係測試和消除重覆元素。 創建集合:大括弧或 set() 函數可以用來創建集合。註意:想要創建空集合,你必須使用 set() 而不是 {},後者用於創建空字典。大括弧也不可以創建元素含有字典與列表的集合。 二 、 文件操作 1 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...