
該圖片由minka2507在Pixabay上發布
你好,我是看山,
SpringBoot 實戰:優雅的使用列舉引數 中聊了怎么優雅的使用列舉引數,本文就來扒一扒 Spring 是如何找到對應轉換器 Converter 的,
找入口
對 Spring 有一定基礎的同學一定知道,請求入口是DispatcherServlet,所有的請求最終都會落到doDispatch方法中的ha.handle(processedRequest, response, mappedHandler.getHandler())邏輯,我們從這里出發,一層一層向里扒,
跟著代碼深入,我們會找到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方法處理引數,然后呼叫doInvoke方法獲取回傳值,
繼續深入,能夠找到org.springframework.web.method.annotation.RequestParamMethodArgumentResolver#resolveArgument方法,這個方法就是決議引數的邏輯,
試想一下,如果是我們自己實作這段邏輯,會怎么做呢?
- 獲取輸入引數
- 找到目標引數
- 檢查是否需要特殊轉換邏輯
- 如果需要,進行轉換
- 如果不需要,直接回傳

獲取輸入引數的邏輯在org.springframework.web.method.annotation.RequestParamMethodArgumentResolver#resolveName,單引數回傳的是 String 型別,多引數回傳 String 陣列,核心代碼如下:
String[] paramValues = request.getParameterValues(name);
if (paramValues != null) {
arg = (paramValues.length == 1 ? paramValues[0] : paramValues);
}
所以說,無論我們的目標引數是什么,輸入引數都是 String 型別或 String 陣列,然后 Spring 把它們轉換為我們期望的型別,
找到目標引數的邏輯在DispatcherServlet中,根據 uri 找到對應的 Controller 處理方法,找到方法就找到了目標引數型別,
接下來就是檢查是否需要轉換邏輯,也就是org.springframework.validation.DataBinder#convertIfNecessary,顧名思義,如果需要就轉換,將字串型別轉換為目標型別,在我們的例子中,就是將 String 轉換為列舉值,
查找轉換器
繼續深扒,會在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;
}
}
這段邏輯中,呼叫了org.springframework.core.convert.support.GenericConversionService#canConvert方法,檢查是否可轉換,如果可以轉換,將會執行型別轉換邏輯,
檢查是否可轉換的本質就是檢查是否能夠找到對應的轉換器,如果能找到,就用找到的轉換器開始轉換邏輯,如果找不到,那就是不能轉換,走其他邏輯,
我們可以看看查找轉換器的代碼org.springframework.core.convert.support.GenericConversionService#getConverter,可以對我們自己寫代碼有一些啟發:
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;
}
轉換為偽代碼就是:
- 根據引數型別和目標型別,構造快取 key
- 根據快取 key,從快取中查詢轉換器
- 如果能找到且不是 NO_MATCH,回傳轉換器;如果是 NO_MATCH,回傳 null;如果未找到,繼續
- 通過
org.springframework.core.convert.support.GenericConversionService.Converters#find查詢轉換器 - 如果未找到,檢查源型別和目標型別是否可以強轉,也就是型別一致,如果是,回傳 NoOpConverter,如果否,回傳 null,
- 檢查找到的轉換器是否為 null,如果不是,將轉換器加入到快取中,回傳該轉換器
- 如果否,在快取中添加 NO_MATCH 標識,回傳 null

Spring 內部使用Map作為快取,用來存盤通用轉換器介面GenericConverter,這個介面會是我們自定義轉換器的包裝類,我們還可以看到,轉換器快取用的是ConcurrentReferenceHashMap,這個類是執行緒安全的,可以保證并發情況下,不會出現例外存盤,但是getConverter方法沒有使用同步邏輯,換句話說,并發請求時,可能存在性能損耗,不過,對于 web 請求場景,并發損耗好過阻塞等待,
我們在看下 Spring 是如何查找轉換器的,在org.springframework.core.convert.support.GenericConversionService.Converters#find中就是找到對應轉換器的核心邏輯:
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 是通過源型別和目標型別組合起來,查找對應的轉換器,而且,Spring 還通過getClassHierarchy方法,將源型別和目標型別的家族族譜全部列出來,用雙層 for 回圈遍歷查找,
上面的代碼中,還有一個matches方法,在這個方法里面,呼叫了ConverterFactory#getConverter方法,也就是用這個工廠方法,創建了指定型別的轉換器,
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;
}
型別轉換
經過上面的邏輯,已經找到判斷可以進行轉換,其核心邏輯就是已經找到對應的轉換器了,下面就是轉換邏輯,在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中呼叫了一次,然后在后續真正轉換的時候又呼叫一次,這是引數轉換邏輯,我們該怎么優化這種同一請求內多次呼叫相同邏輯或者請求相同引數呢?那就是使用快取,為了保持一次請求中前后兩次資料的一致性和請求的高效,推薦使用記憶體快取,
執行到這里,直接呼叫ConversionUtils.invokeConverter(converter, source, sourceType, targetType)轉換,其內部是使用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);
}
這里就是呼叫ConverterFactory工廠類構建轉換器(即IdCodeToEnumConverterFactory類的getConverter方法),然后呼叫轉換器的conver方法(即IdCodeToEnumConverter類的convert方法),將輸入引數轉換為目標型別,具體實作可以看一下實戰篇中的代碼,這里不做贅述,
至此,我們把整個路程通了下來,
文末總結
在本文中,我們跟隨原始碼找到自定義轉換器工廠類和轉換器類的實作邏輯,這里需要強調一下的是,由于實戰篇中我們用到的例子是簡單引數的方式,也就是Controller的方法引數都是直接引數,沒有包裝成物件,這樣的話,Spring 是通過RequestParamMethodArgumentResolver處理引數,如果是包裝成物件,會使用ModelAttributeMethodProcessor處理引數,這兩個處理類中查找型別轉換器邏輯都是相同的,
無論是GET請求,還是傳參式的POST請求(即Form模式),都可以使用上面這種方式,實作列舉引數的型別轉換,但是是 HTTP Body 方式卻不行,為什么呢?
Spring 對于 body 引數是通過RequestResponseBodyMethodProcessor處理的,其內部使用了MappingJackson2HttpMessageConverter轉換器,邏輯完全不同,所以,想要實作 body 的型別轉換,還需要走另外一種方式,將在下一篇中給出,
你好,我是看山,游于碼界,戲享人生,如果文章對您有幫助,請點贊、收藏、關注,我還整理了一些精品學習資料,關注公眾號「看山的小屋」,回復“資料”即可獲得,
個人主頁:https://www.howardliu.cn
個人博文:SpringBoot 實戰:優雅的使用列舉引數(原理篇)
CSDN 主頁:https://kanshan.blog.csdn.net/
CSDN 博文:SpringBoot 實戰:優雅的使用列舉引數(原理篇)

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/294459.html
標籤:java
