
該圖片由Johnnys_pic在Pixabay上發布
你好,我是看山,
在優雅的使用列舉引數(原理篇)中我們聊過,Spring對于不同的引數形式,會采用不同的處理類處理引數,這種形式,有些類似于策略模式,將針對不同引數形式的處理邏輯,拆分到不同處理類中,減少耦合和各種if-else邏輯,本文就來扒一扒,RequestBody引數中使用列舉引數的原理,
找入口
對 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方法獲取回傳值,getMethodArgumentValues方法內部又是通過HandlerMethodArgumentResolverComposite實體處理引數,這個類內部是一個HandlerMethodArgumentResolver實體串列,串列中是Spring處理引數邏輯的集合,跟隨代碼Debug,可以看到有27個元素,這些類也是可以定制擴展,實作自己的引數決議邏輯,這部分內容后續再做介紹,
選擇Resolver
這個Resolver串列中,包含我們常用的幾個處理類,Get請求的普通引數是通過RequestParamMethodArgumentResolver處理引數,包裝類通過ModelAttributeMethodProcessor處理引數,RequestBody形式的引數,則是通過RequestResponseBodyMethodProcessor處理引數,這段就是Spring中策略模式的使用,通過實作org.springframework.web.method.support.HandlerMethodArgumentResolver#supportsParameter方法,判斷輸入引數是否可以決議,下面貼上RequestResponseBodyMethodProcessor的實作:
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(RequestBody.class);
}
可以看到,RequestResponseBodyMethodProcessor是通過判斷引數是否帶有RequestBody注解來判斷,當前引數是否可以決議,
決議引數
RequestResponseBodyMethodProcessor繼承自AbstractMessageConverterMethodArgumentResolver,真正決議RequestBody引數的邏輯在org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodArgumentResolver#readWithMessageConverters方法中,我們看下原始碼(因為原始碼比較長,文中僅留下核心邏輯,):
protected <T> Object readWithMessageConverters(HttpInputMessage inputMessage, MethodParameter parameter,
Type targetType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException {
MediaType contentType = inputMessage.getHeaders().getContentType();// 1
Class<?> contextClass = parameter.getContainingClass();// 2
Class<T> targetClass = (targetType instanceof Class ? (Class<T>) targetType : null);// 3
Object body = NO_VALUE;
EmptyBodyCheckingHttpInputMessage message = new EmptyBodyCheckingHttpInputMessage(inputMessage);// 4
for (HttpMessageConverter<?> converter : this.messageConverters) {// 5
Class<HttpMessageConverter<?>> converterType = (Class<HttpMessageConverter<?>>) converter.getClass();
GenericHttpMessageConverter<?> genericConverter =
(converter instanceof GenericHttpMessageConverter ? (GenericHttpMessageConverter<?>) converter : null);
if (genericConverter != null ? genericConverter.canRead(targetType, contextClass, contentType) :
(targetClass != null && converter.canRead(targetClass, contentType))) {
if (message.hasBody()) {
HttpInputMessage msgToUse =
getAdvice().beforeBodyRead(message, parameter, targetType, converterType);
body = (genericConverter != null ? genericConverter.read(targetType, contextClass, msgToUse) :
((HttpMessageConverter<T>) converter).read(targetClass, msgToUse));// 6
body = getAdvice().afterBodyRead(body, msgToUse, parameter, targetType, converterType);
}
else {
body = getAdvice().handleEmptyBody(null, message, parameter, targetType, converterType);
}
break;
}
}
return body;
}
跟著代碼說明一下各部分用途:
- 獲取請求content-type
- 獲取引數容器類
- 獲取目標引數型別
- 將請求引數轉換為
EmptyBodyCheckingHttpInputMessage型別 - 回圈各種RequestBody引數決議器,這些決議器都是
HttpMessageConverter介面的實作類,Spring對各種情況做了全量覆寫,總有一款適合的,文末給出HttpMessageConverter各個擴展類的類圖, - for回圈體中就是選擇一款適合的,進行決議
- 首先呼叫
canRead方法判斷是否可用 - 判斷請求請求引數是否為空,為空則通過AOP的
advice處理一下空請求體,然后回傳 - 不為空,先通過AOP的
advice做前置處理,然后呼叫read方法轉換物件,在通過advice做后置處理
- 首先呼叫
Spring的AOP不在本文范圍內,所以一筆帶過,后續有專題說明,
本例中,HttpMessageConverter使用的是MappingJackson2HttpMessageConverter,該類繼承自AbstractJackson2HttpMessageConverter,看名稱就知道,這個類是使用Jackson處理請求引數,其中read方法之后,會呼叫內部私有方法readJavaType,下面給出該方法的核心邏輯:
private Object readJavaType(JavaType javaType, HttpInputMessage inputMessage) throws IOException {
MediaType contentType = inputMessage.getHeaders().getContentType();// 1
Charset charset = getCharset(contentType);
ObjectMapper objectMapper = selectObjectMapper(javaType.getRawClass(), contentType);// 2
Assert.state(objectMapper != null, "No ObjectMapper for " + javaType);
boolean isUnicode = ENCODINGS.containsKey(charset.name()) ||
"UTF-16".equals(charset.name()) ||
"UTF-32".equals(charset.name());// 3
try {
if (isUnicode) {
return objectMapper.readValue(inputMessage.getBody(), javaType);// 4
} else {
Reader reader = new InputStreamReader(inputMessage.getBody(), charset);
return objectMapper.readValue(reader, javaType);
}
}
catch (InvalidDefinitionException ex) {
throw new HttpMessageConversionException("Type definition error: " + ex.getType(), ex);
}
catch (JsonProcessingException ex) {
throw new HttpMessageNotReadableException("JSON parse error: " + ex.getOriginalMessage(), ex, inputMessage);
}
}
跟著代碼說明一下各部分用途:
- 獲取請求的
content-type,這個是Spring實作的擴展邏輯,根據不同的content-type可以選擇不同的ObjectMapper實體,也就是第2步的邏輯 - 根據
content-type和目標型別,選擇ObjectMapper實體,本例中直接回傳的是默認的,也就是通過Jackson2ObjectMapperBuilder.cbor().build()方法創建的, - 檢查請求是否是unicode字符,目前來說,大家用的都是
UTF-8的 - 通過
ObjectMapper將請求json轉換為物件,其實這部分還有一段判斷inputMessage是否是MappingJacksonInputMessage實體的,考慮到大家使用的版本,這部分就不說了,
至此,Spring的邏輯全部結束,似憾訓是沒有找到我們使用的JsonCreator注解或者JsonDeserialize的邏輯,不過也能想到,這兩個都是Jackson的類,那必然應該是Jackson的邏輯,接下來,就扒一扒Jackson的轉換邏輯了,
深入Jackson的ObjectMapper邏輯
牽扯Jackson的邏輯主要分布在AbstractMessageConverterMethodArgumentResolver#readWithMessageConverters和ObjectMapper#readValue這兩個方法中,先說一下ObjectMapper#readValue方法的邏輯,這里面會呼叫GenderIdCodeEnum#create方法,完成型別轉換,
ObjectMapper#readValue方法直接呼叫了當前類中的_readMapAndClose方法,這個方法里面比較關鍵的是ctxt.readRootValue(p, valueType, _findRootDeserializer(ctxt, valueType), null),這個方法就是將輸入json轉換為物件,咱們再繼續深入,可以找到Jackson內部是通過BeanDeserializer這個類轉換物件的,比較重要的是deserializeFromObject方法,原始碼如下(洗掉一下不太重要的代碼):
public Object deserializeFromObject(JsonParser p, DeserializationContext ctxt) throws IOException
{
// 這里根據背景關系中目標型別,創建實體物件,其中 _valueInstantiator 是 StdValueInstantiator 實體,
final Object bean = _valueInstantiator.createUsingDefault(ctxt);
// [databind#631]: Assign current value, to be accessible by custom deserializers
p.setCurrentValue(bean);
if (p.hasTokenId(JsonTokenId.ID_FIELD_NAME)) {
String propName = p.currentName();
do {
p.nextToken();
// 根據欄位名找到 屬性物件,對于gender欄位,型別是 MethodProperty,
SettableBeanProperty prop = _beanProperties.find(propName);
if (prop != null) { // normal case
try {
// 開始進行解碼操作,并將解碼結果寫入到物件中
prop.deserializeAndSet(p, ctxt, bean);
} catch (Exception e) {
wrapAndThrow(e, bean, propName, ctxt);
}
continue;
}
handleUnknownVanilla(p, ctxt, bean, propName);
} while ((propName = p.nextFieldName()) != null);
}
return bean;
}
咱們看一下MethodProperty#deserializeAndSet的邏輯(只保留關鍵代碼):
public void deserializeAndSet(JsonParser p, DeserializationContext ctxt,
Object instance) throws IOException
{
Object value;
// 呼叫 FactoryBasedEnumDeserializer 實體的解碼方法
value = _valueDeserializer.deserialize(p, ctxt);
// 通過反射將值寫入物件中
_setter.invoke(instance, value);
}
其中_valueDeserializer是FactoryBasedEnumDeserializer實體,快要接近目標了,看下這段邏輯:
public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException
{
// 獲取json中的值
Object value = _deser.deserialize(p, ctxt);
// 呼叫 GenderIdCodeEnum#create 方法
return _factory.callOnWith(_valueClass, value);
}
_factory是AnnotatedMethod實體,主要是對JsonCreator注解定義的方法的包裝,然后callOnWith中呼叫java.lang.reflect.Method#invoke反射方法,執行GenderIdCodeEnum#create,
至此,我們終于串起來所有邏輯,
文末總結
本文通過一個示例串起來@JsonCreator注解起作用的邏輯,JsonDeserializer介面的邏輯與之型別,可以耐心debug一番,下面給出主要類的類圖:



推薦閱讀
- SpringBoot 實戰:一招實作結果的優雅回應
- SpringBoot 實戰:如何優雅的處理例外
- SpringBoot 實戰:通過 BeanPostProcessor 動態注入 ID 生成器
- SpringBoot 實戰:自定義 Filter 優雅獲取請求引數和回應結果
- SpringBoot 實戰:優雅的使用列舉引數
- SpringBoot 實戰:優雅的使用列舉引數(原理篇)
- SpringBoot 實戰:在 RequestBody 中優雅的使用列舉引數
- SpringBoot 實戰:在 RequestBody 中優雅的使用列舉引數(原理篇)
你好,我是看山,游于碼界,戲享人生,如果文章對您有幫助,請點贊、收藏、關注,我還整理了一些精品學習資料,關注公眾號「看山的小屋」,回復“資料”即可獲得,
個人主頁:https://www.howardliu.cn
個人博文:SpringBoot 實戰:在 RequestBody 中優雅的使用列舉引數(原理篇)
CSDN 主頁:https://kanshan.blog.csdn.net/
CSDN 博文:SpringBoot 實戰:在 RequestBody 中優雅的使用列舉引數(原理篇)

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