SpringBoot實(shí)戰(zhàn):Spring如何找到對(duì)應(yīng)轉(zhuǎn)換器優(yōu)雅使用枚舉參數(shù)
找入口
請(qǐng)求入口是DispatcherServlet
所有的請(qǐng)求最終都會(huì)落到doDispatch方法中的
ha.handle(processedRequest, response, mappedHandler.getHandler())邏輯。
我們從這里出發(fā),一層一層向里扒。
跟著代碼深入,我們會(huì)找到
org.springframework.web.method.support.InvocableHandlerMethod#invokeForRequest的邏輯:
public Object invokeForRequest(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,
Object... providedArgs) throws Exception {
Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs);
if (logger.isTraceEnabled()) {
logger.trace("Arguments: " + Arrays.toString(args));
}
return doInvoke(args);
}
可以看出,這里面通過getMethodArgumentValues方法處理參數(shù),然后調(diào)用doInvoke方法獲取返回值。
繼續(xù)深入,能夠找到
org.springframework.web.method.annotation.RequestParamMethodArgumentResolver#resolveArgument方法
這個(gè)方法就是解析參數(shù)的邏輯。
試想一下,如果是我們自己實(shí)現(xiàn)這段邏輯,會(huì)怎么做呢?
- 輸入?yún)?shù)
- 找到目標(biāo)參數(shù)
- 檢查是否需要特殊轉(zhuǎn)換邏輯
- 如果需要,進(jìn)行轉(zhuǎn)換
- 如果不需要,直接返回

獲取輸入?yún)?shù)的邏輯在
org.springframework.web.method.annotation.RequestParamMethodArgumentResolver#resolveName
單參數(shù)返回的是 String 類型,多參數(shù)返回 String 數(shù)組。
核心代碼如下:
String[] paramValues = request.getParameterValues(name);
if (paramValues != null) {
arg = (paramValues.length == 1 ? paramValues[0] : paramValues);
}
所以說,無論我們的目標(biāo)參數(shù)是什么,輸入?yún)?shù)都是 String 類型或 String 數(shù)組
- 然后 Spring 把它們轉(zhuǎn)換為我們期望的類型。
- 找到目標(biāo)參數(shù)的邏輯在DispatcherServlet中,根據(jù) uri 找到對(duì)應(yīng)的 Controller 處理方法
- 找到方法就找到了目標(biāo)參數(shù)類型。
- 接下來就是檢查是否需要轉(zhuǎn)換邏輯,也就是
- org.springframework.validation.DataBinder#convertIfNecessary
- 顧名思義,如果需要就轉(zhuǎn)換,將字符串類型轉(zhuǎn)換為目標(biāo)類型。
在我們的例子中,就是將 String 轉(zhuǎn)換為枚舉值。
查找轉(zhuǎn)換器
org.springframework.beans.TypeConverterDelegate#convertIfNecessary方法中
繼續(xù)深扒找到這么一段邏輯:
if (conversionService.canConvert(sourceTypeDesc, typeDescriptor)) {
try {
return (T) conversionService.convert(newValue, sourceTypeDesc, typeDescriptor);
}
catch (ConversionFailedException ex) {
// fallback to default conversion logic below
conversionAttemptEx = ex;
}
}
這段邏輯中,調(diào)用了
org.springframework.core.convert.support.GenericConversionService#canConvert方法
檢查是否可轉(zhuǎn)換,如果可以轉(zhuǎn)換,將會(huì)執(zhí)行類型轉(zhuǎn)換邏輯。
檢查是否可轉(zhuǎn)換的本質(zhì)就是檢查是否能夠找到對(duì)應(yīng)的轉(zhuǎn)換器。
- 如果能找到,就用找到的轉(zhuǎn)換器開始轉(zhuǎn)換邏輯
- 如果找不到,那就是不能轉(zhuǎn)換,走其他邏輯。
我們可以看看查找轉(zhuǎn)換器的代碼
org.springframework.core.convert.support.GenericConversionService#getConverter
可以對(duì)我們自己寫代碼有一些啟發(fā):
private final Map<ConverterCacheKey, GenericConverter> converterCache = new ConcurrentReferenceHashMap<>(64);
protected GenericConverter getConverter(TypeDescriptor sourceType, TypeDescriptor targetType) {
ConverterCacheKey key = new ConverterCacheKey(sourceType, targetType);
GenericConverter converter = this.converterCache.get(key);
if (converter != null) {
return (converter != NO_MATCH ? converter : null);
}
converter = this.converters.find(sourceType, targetType);
if (converter == null) {
converter = getDefaultConverter(sourceType, targetType);
}
if (converter != null) {
this.converterCache.put(key, converter);
return converter;
}
this.converterCache.put(key, NO_MATCH);
return null;
}
轉(zhuǎn)換為偽代碼就是:
- 根據(jù)參數(shù)類型和目標(biāo)類型,構(gòu)造緩存 key
- 根據(jù)緩存 key從緩存中查詢轉(zhuǎn)換器
- 如果能找到且不是 NO_MATCH,返回轉(zhuǎn)換器;如果是 NO_MATCH,返回 null;如果未找到,繼續(xù)
- 通過org.springframework.core.convert.support.GenericConversionService.Converters#find查詢轉(zhuǎn)換器
- 如果未找到,檢查源類型和目標(biāo)類型是否可以強(qiáng)轉(zhuǎn),也就是類型一致。如果是,返回 NoOpConverter,如果否,返回 null。
- 檢查找到的轉(zhuǎn)換器是否為 null,如果不是,將轉(zhuǎn)換器加入到緩存中,返回該轉(zhuǎn)換器
- 如果否,在緩存中添加 NO_MATCH 標(biāo)識(shí),返回 null

Spring 內(nèi)部使用Map作為緩存,用來存儲(chǔ)通用轉(zhuǎn)換器接口GenericConverter,這個(gè)接口會(huì)是我們自定義轉(zhuǎn)換器的包裝類。
- 我們還可以看到,轉(zhuǎn)換器緩存用的是ConcurrentReferenceHashMap,這個(gè)類是線程安全的
- 可以保證并發(fā)情況下,不會(huì)出現(xiàn)異常存儲(chǔ)。但是getConverter方法沒有使用同步邏輯。
- 換句話說,并發(fā)請(qǐng)求時(shí),可能存在性能損耗。
不過,對(duì)于 web 請(qǐng)求場景,并發(fā)損耗好過阻塞等待。
Spring 如何查找轉(zhuǎn)換器
org.springframework.core.convert.support.GenericConversionService.Converters#find
就是找到對(duì)應(yīng)轉(zhuǎn)換器的核心邏輯:
private final Map<ConvertiblePair, ConvertersForPair> converters = new ConcurrentHashMap<>(256);
@Nullable
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;
}
@Nullable
private GenericConverter getRegisteredConverter(TypeDescriptor sourceType,
TypeDescriptor targetType, ConvertiblePair convertiblePair) {
// Check specifically registered converters
ConvertersForPair convertersForPair = this.converters.get(convertiblePair);
if (convertersForPair != null) {
GenericConverter converter = convertersForPair.getConverter(sourceType, targetType);
if (converter != null) {
return converter;
}
}
// Check ConditionalConverters for a dynamic match
for (GenericConverter globalConverter : this.globalConverters) {
if (((ConditionalConverter) globalConverter).matches(sourceType, targetType)) {
return globalConverter;
}
}
return null;
}
我們可以看到,Spring 是通過源類型和目標(biāo)類型組合起來,查找對(duì)應(yīng)的轉(zhuǎn)換器。
而且,Spring 還通過getClassHierarchy方法,將源類型和目標(biāo)類型的家族族譜全部列出來,用雙層 for 循環(huán)遍歷查找。
上面的代碼中,還有一個(gè)matches方法,在這個(gè)方法里面,調(diào)用了ConverterFactory#getConverter方法
也就是用這個(gè)工廠方法,創(chuàng)建了指定類型的轉(zhuǎn)換器。
private final ConverterFactory<Object, Object> converterFactory;
public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) {
boolean matches = true;
if (this.converterFactory instanceof ConditionalConverter) {
matches = ((ConditionalConverter) this.converterFactory).matches(sourceType, targetType);
}
if (matches) {
Converter<?, ?> converter = this.converterFactory.getConverter(targetType.getType());
if (converter instanceof ConditionalConverter) {
matches = ((ConditionalConverter) converter).matches(sourceType, targetType);
}
}
return matches;
}
類型轉(zhuǎn)換
經(jīng)過上面的邏輯,已經(jīng)找到判斷可以進(jìn)行轉(zhuǎn)換。
org.springframework.core.convert.support.GenericConversionService#convert
其核心邏輯就是已經(jīng)找到對(duì)應(yīng)的轉(zhuǎn)換器了,下面就是轉(zhuǎn)換邏輯
public Object convert(@Nullable Object source, @Nullable TypeDescriptor sourceType, TypeDescriptor targetType) {
Assert.notNull(targetType, "Target type to convert to cannot be null");
if (sourceType == null) {
Assert.isTrue(source == null, "Source must be [null] if source type == [null]");
return handleResult(null, targetType, convertNullSource(null, targetType));
}
if (source != null && !sourceType.getObjectType().isInstance(source)) {
throw new IllegalArgumentException("Source to convert from must be an instance of [" +
sourceType + "]; instead it was a [" + source.getClass().getName() + "]");
}
GenericConverter converter = getConverter(sourceType, targetType);
if (converter != null) {
Object result = ConversionUtils.invokeConverter(converter, source, sourceType, targetType);
return handleResult(sourceType, targetType, result);
}
return handleConverterNotFound(source, sourceType, targetType);
}
其中的GenericConverter converter = getConverter(sourceType, targetType)就是前文中g(shù)etConverter方法。
- 此處還是可以給我們編碼上的一些借鑒的:getConverter方法在canConvert中調(diào)用了一次
然后在后續(xù)真正轉(zhuǎn)換的時(shí)候又調(diào)用一次這是參數(shù)轉(zhuǎn)換邏輯 - 我們?cè)撛趺磧?yōu)化這種同一請(qǐng)求內(nèi)多次調(diào)用相同邏輯或者請(qǐng)求相同參數(shù)呢?
那就是使用緩存。為了保持一次請(qǐng)求中前后兩次數(shù)據(jù)的一致性和請(qǐng)求的高效,推薦使用內(nèi)存緩存。
執(zhí)行到這里,直接調(diào)用
ConversionUtils.invokeConverter(converter, source, sourceType, targetType)轉(zhuǎn)換
其內(nèi)部是使用
org.springframework.core.convert.support.GenericConversionService.ConverterFactoryAdapter#convert
方法,代碼如下:
public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
if (source == null) {
return convertNullSource(sourceType, targetType);
}
return this.converterFactory.getConverter(targetType.getObjectType()).convert(source);
}
這里就是調(diào)用ConverterFactory工廠類構(gòu)建轉(zhuǎn)換器(即IdCodeToEnumConverterFactory類的getConverter方法)
然后調(diào)用轉(zhuǎn)換器的conver方法(即IdCodeToEnumConverter類的convert方法),將輸入?yún)?shù)轉(zhuǎn)換為目標(biāo)類型。
具體實(shí)現(xiàn)可以看一下實(shí)戰(zhàn)篇中的代碼,這里不做贅述。
至此,我們把整個(gè)路程通了下來。
跟隨源碼找到自定義轉(zhuǎn)換器工廠類和轉(zhuǎn)換器類的實(shí)現(xiàn)邏輯
無論是GET請(qǐng)求,還是傳參式的POST請(qǐng)求(即Form模式)
這里需要強(qiáng)調(diào)一下的是,由于實(shí)戰(zhàn)篇中我們用到的例子是簡單參數(shù)的方式,也就是Controller的方法參數(shù)都是直接參數(shù)
- 沒有包裝成對(duì)象。這樣的話,Spring 是通過RequestParamMethodArgumentResolver處理參數(shù)。
- 如果是包裝成對(duì)象,會(huì)使用ModelAttributeMethodProcessor處理參數(shù)。這兩個(gè)處理類中查找類型轉(zhuǎn)換器邏輯都是相同的。
- 都可以使用上面這種方式,實(shí)現(xiàn)枚舉參數(shù)的類型轉(zhuǎn)換。
但是 HTTP Body 方式卻不行,為什么呢?
- Spring 對(duì)于 body 參數(shù)是通過RequestResponseBodyMethodProcessor處理的
- 其內(nèi)部使用了MappingJackson2HttpMessageConverter轉(zhuǎn)換器,邏輯完全不同。
所以,想要實(shí)現(xiàn) body 的類型轉(zhuǎn)換,還需要走另外一種方式,更多關(guān)于Spring對(duì)應(yīng)轉(zhuǎn)換器的枚舉參數(shù)的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
java使用HashMap實(shí)現(xiàn)斗地主(有序版)
這篇文章主要為大家詳細(xì)介紹了java使用ArrayList實(shí)現(xiàn)斗地主游戲,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-03-03
Sping?Security前后端分離兩種實(shí)戰(zhàn)方案
這篇文章主要介紹了Sping?Security前后端分離兩種方案,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-03-03
九個(gè)動(dòng)畫組圖輪播總結(jié)全棧數(shù)據(jù)結(jié)構(gòu)數(shù)組鏈表
數(shù)據(jù)結(jié)構(gòu)和算法是密不可分的,兩者往往是相輔相成的存在,所以在學(xué)習(xí)數(shù)據(jù)結(jié)構(gòu)過程中,不免會(huì)遇到各種算法,數(shù)據(jù)結(jié)構(gòu)常用操作一般為:增刪改查。基本上所有的數(shù)據(jù)結(jié)構(gòu)都是圍繞這幾個(gè)操作進(jìn)行展開,本文用九張動(dòng)圖來闡述先進(jìn)后出的數(shù)據(jù)結(jié)構(gòu)2021-08-08
如何使用stream從List對(duì)象中獲取某列數(shù)據(jù)
這篇文章主要介紹了如何使用stream從List對(duì)象中獲取某列數(shù)據(jù)問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-12-12
Java 運(yùn)算符 動(dòng)力節(jié)點(diǎn)Java學(xué)院整理
這篇文章主要介紹了Java 運(yùn)算符 動(dòng)力節(jié)點(diǎn)Java學(xué)院整理,需要的朋友可以參考下2017-04-04
MyBatis-plus的五種批量插入方式對(duì)比分析
本文主要介紹了MyBatis-plus的五種批量插入方式對(duì)比分析,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-06-06
MybatisPlus lambdaQueryWrapper中常用方法的使用
本文主要介紹了MybatisPlus lambdaQueryWrapper中常用方法的使用,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-07-07

