SpringBoot實戰(zhàn)之高效使用枚舉參數(shù)(原理篇)案例詳解
找入口
對 Spring 有一定基礎的同學一定知道,請求入口是DispatcherServlet
,所有的請求最終都會落到doDispatch
方法中的ha.handle(processedRequest, response, mappedHandler.getHandler())
邏輯。我們從這里出發(fā),一層一層向里扒。
跟著代碼深入,我們會找到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
方法,這個方法就是解析參數(shù)的邏輯。
試想一下,如果是我們自己實現(xiàn)這段邏輯,會怎么做呢?
- 獲取輸入?yún)?shù)
- 找到目標參數(shù)
- 檢查是否需要特殊轉(zhuǎ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); }
所以說,無論我們的目標參數(shù)是什么,輸入?yún)?shù)都是 String 類型或 String 數(shù)組,然后 Spring 把它們轉(zhuǎn)換為我們期望的類型。
找到目標參數(shù)的邏輯在DispatcherServlet
中,根據(jù) uri 找到對應的 Controller 處理方法,找到方法就找到了目標參數(shù)類型。
接下來就是檢查是否需要轉(zhuǎn)換邏輯,也就是org.springframework.validation.DataBinder#convertIfNecessary
,顧名思義,如果需要就轉(zhuǎn)換,將字符串類型轉(zhuǎn)換為目標類型。在我們的例子中,就是將 String 轉(zhuǎn)換為枚舉值。
查找轉(zhuǎn)換器
繼續(xù)深扒,會在org.springframework.beans.TypeConverterDelegate#convertIfNecessary
方法中找到這么一段邏輯:
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)換,將會執(zhí)行類型轉(zhuǎn)換邏輯。
檢查是否可轉(zhuǎn)換的本質(zhì)就是檢查是否能夠找到對應的轉(zhuǎn)換器。如果能找到,就用找到的轉(zhuǎn)換器開始轉(zhuǎn)換邏輯,如果找不到,那就是不能轉(zhuǎn)換,走其他邏輯。
我們可以看看查找轉(zhuǎn)換器的代碼org.springframework.core.convert.support.GenericConversionService#getConverter
,可以對我們自己寫代碼有一些啟發(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ù)類型和目標類型,構(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)換器 - 如果未找到,檢查源類型和目標類型是否可以強轉(zhuǎn),也就是類型一致。如果是,返回 NoOpConverter,如果否,返回 null。
- 檢查找到的轉(zhuǎn)換器是否為 null,如果不是,將轉(zhuǎn)換器加入到緩存中,返回該轉(zhuǎn)換器
- 如果否,在緩存中添加 NO_MATCH 標識,返回 null
Spring 內(nèi)部使用Map
作為緩存,用來存儲通用轉(zhuǎn)換器接口GenericConverter
,這個接口會是我們自定義轉(zhuǎn)換器的包裝類。我們還可以看到,轉(zhuǎn)換器緩存用的是ConcurrentReferenceHashMap
,這個類是線程安全的,可以保證并發(fā)情況下,不會出現(xiàn)異常存儲。但是getConverter
方法沒有使用同步邏輯。換句話說,并發(fā)請求時,可能存在性能損耗。不過,對于 web 請求場景,并發(fā)損耗好過阻塞等待。
我們在看下 Spring 是如何查找轉(zhuǎn)換器的,在org.springframework.core.convert.support.GenericConversionService.Converters#find
中就是找到對應轉(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 是通過源類型和目標類型組合起來,查找對應的轉(zhuǎn)換器。而且,Spring 還通過getClassHierarchy
方法,將源類型和目標類型的家族族譜全部列出來,用雙層 for 循環(huán)遍歷查找。
上面的代碼中,還有一個matches
方法,在這個方法里面,調(diào)用了ConverterFactory#getConverter
方法,也就是用這個工廠方法,創(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)找到判斷可以進行轉(zhuǎn)換。其核心邏輯就是已經(jīng)找到對應的轉(zhuǎn)換器了,下面就是轉(zhuǎn)換邏輯,在org.springframework.core.convert.support.GenericConversionService#convert
中:
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)
就是前文中getConverter
方法。此處還是可以給我們編碼上的一些借鑒的:getConverter
方法在canConvert
中調(diào)用了一次,然后在后續(xù)真正轉(zhuǎn)換的時候又調(diào)用一次,這是參數(shù)轉(zhuǎn)換邏輯,我們該怎么優(yōu)化這種同一請求內(nèi)多次調(diào)用相同邏輯或者請求相同參數(shù)呢?那就是使用緩存。為了保持一次請求中前后兩次數(shù)據(jù)的一致性和請求的高效,推薦使用內(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)換為目標類型。具體實現(xiàn)可以看一下實戰(zhàn)篇中的代碼,這里不做贅述。
至此,我們把整個路程通了下來。
文末總結(jié)
在本文中,我們跟隨源碼找到自定義轉(zhuǎn)換器工廠類和轉(zhuǎn)換器類的實現(xiàn)邏輯。這里需要強調(diào)一下的是,由于實戰(zhàn)篇中我們用到的例子是簡單參數(shù)的方式,也就是Controller
的方法參數(shù)都是直接參數(shù),沒有包裝成對象。這樣的話,Spring 是通過RequestParamMethodArgumentResolver
處理參數(shù)。如果是包裝成對象,會使用ModelAttributeMethodProcessor
處理參數(shù)。這兩個處理類中查找類型轉(zhuǎn)換器邏輯都是相同的。
無論是GET
請求,還是傳參式的POST
請求(即Form
模式),都可以使用上面這種方式,實現(xiàn)枚舉參數(shù)的類型轉(zhuǎn)換。但是是 HTTP Body 方式卻不行,為什么呢?
Spring 對于 body 參數(shù)是通過RequestResponseBodyMethodProcessor
處理的,其內(nèi)部使用了MappingJackson2HttpMessageConverter
轉(zhuǎn)換器,邏輯完全不同。所以,想要實現(xiàn) body 的類型轉(zhuǎn)換,還需要走另外一種方式。將在下一篇中給出。
到此這篇關于SpringBoot實戰(zhàn)之高效使用枚舉參數(shù)(原理篇)案例詳解的文章就介紹到這了,更多相關SpringBoot實戰(zhàn)之高效使用枚舉參數(shù)(原理篇)內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
java開發(fā)hutool HttpUtil網(wǎng)絡請求工具使用demo
這篇文章主要為大家介紹了hutool之HttpUtil網(wǎng)絡請求工具使用示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-07-07SpringBoot創(chuàng)建RSocket服務器的全過程記錄
RSocket應用層協(xié)議支持 Reactive Streams語義, 例如:用RSocket作為HTTP的一種替代方案。這篇文章主要給大家介紹了關于SpringBoot創(chuàng)建RSocket服務器的相關資料,需要的朋友可以參考下2021-05-05SpringBoot之如何搭建SpringBoot+Maven項目
這篇文章主要介紹了SpringBoot之如何搭建SpringBoot+Maven項目問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-07-07詳談Java中instanceof和isInstance的區(qū)別
下面小編就為大家?guī)硪黄斦凧ava中instanceof和isInstance的區(qū)別。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-01-01