Java SpringBoot在RequestBody中高效的使用枚舉參數(shù)原理案例詳解
在優(yōu)雅的使用枚舉參數(shù)(原理篇)中我們聊過(guò),Spring對(duì)于不同的參數(shù)形式,會(huì)采用不同的處理類處理參數(shù),這種形式,有些類似于策略模式。將針對(duì)不同參數(shù)形式的處理邏輯,拆分到不同處理類中,減少耦合和各種if-else邏輯。本文就來(lái)扒一扒,RequestBody參數(shù)中使用枚舉參數(shù)的原理。
找入口
對(duì) Spring 有一定基礎(chǔ)的同學(xué)一定知道,請(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); }
可以看出,這里面通過(guò)getMethodArgumentValues方法處理參數(shù),然后調(diào)用doInvoke方法獲取返回值。getMethodArgumentValues方法內(nèi)部又是通過(guò)HandlerMethodArgumentResolverComposite實(shí)例處理參數(shù)。這個(gè)類內(nèi)部是一個(gè)HandlerMethodArgumentResolver實(shí)例列表,列表中是Spring處理參數(shù)邏輯的集合,跟隨代碼Debug,可以看到有27個(gè)元素。這些類也是可以定制擴(kuò)展,實(shí)現(xiàn)自己的參數(shù)解析邏輯,這部分內(nèi)容后續(xù)再做介紹。
選擇Resolver
這個(gè)Resolver列表中,包含我們常用的幾個(gè)處理類。Get請(qǐng)求的普通參數(shù)是通過(guò)RequestParamMethodArgumentResolver處理參數(shù),包裝類通過(guò)ModelAttributeMethodProcessor處理參數(shù),RequestBody形式的參數(shù),則是通過(guò)RequestResponseBodyMethodProcessor處理參數(shù)。這段就是Spring中策略模式的使用,通過(guò)實(shí)現(xiàn)org.springframework.web.method.support.HandlerMethodArgumentResolver#supportsParameter方法,判斷輸入?yún)?shù)是否可以解析。下面貼上RequestResponseBodyMethodProcessor的實(shí)現(xiàn):
public boolean supportsParameter(MethodParameter parameter) { return parameter.hasParameterAnnotation(RequestBody.class); }
可以看到,RequestResponseBodyMethodProcessor是通過(guò)判斷參數(shù)是否帶有RequestBody注解來(lái)判斷,當(dāng)前參數(shù)是否可以解析。
解析參數(shù)
RequestResponseBodyMethodProcessor繼承自AbstractMessageConverterMethodArgumentResolver,真正解析RequestBody參數(shù)的邏輯在org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodArgumentResolver#readWithMessageConverters方法中。我們看下源碼(因?yàn)樵创a比較長(zhǎng),文中僅留下核心邏輯。):
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; }
跟著代碼說(shuō)明一下各部分用途:
- 獲取請(qǐng)求content-type
- 獲取參數(shù)容器類
- 獲取目標(biāo)參數(shù)類型
- 將請(qǐng)求參數(shù)轉(zhuǎn)換為EmptyBodyCheckingHttpInputMessage類型
- 循環(huán)各種RequestBody參數(shù)解析器,這些解析器都是HttpMessageConverter接口的實(shí)現(xiàn)類。Spring對(duì)各種情況做了全量覆蓋,總有一款適合的。文末給出HttpMessageConverter各個(gè)擴(kuò)展類的類圖。
- for循環(huán)體中就是選擇一款適合的,進(jìn)行解析
- 首先調(diào)用canRead方法判斷是否可用
- 判斷請(qǐng)求請(qǐng)求參數(shù)是否為空,為空則通過(guò)AOP的advice處理一下空請(qǐng)求體,然后返回
- 不為空,先通過(guò)AOP的advice做前置處理,然后調(diào)用read方法轉(zhuǎn)換對(duì)象,在通過(guò)advice做后置處理
Spring的AOP不在本文范圍內(nèi),所以一筆帶過(guò)。后續(xù)有專題說(shuō)明。
本例中,HttpMessageConverter使用的是MappingJackson2HttpMessageConverter,該類繼承自AbstractJackson2HttpMessageConverter??疵Q就知道,這個(gè)類是使用Jackson處理請(qǐng)求參數(shù)。其中read方法之后,會(huì)調(diào)用內(nèi)部私有方法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); } }
跟著代碼說(shuō)明一下各部分用途:
- 獲取請(qǐng)求的content-type,這個(gè)是Spring實(shí)現(xiàn)的擴(kuò)展邏輯,根據(jù)不同的content-type可以選擇不同的ObjectMapper實(shí)例。也就是第2步的邏輯
- 根據(jù)content-type和目標(biāo)類型,選擇ObjectMapper實(shí)例。本例中直接返回的是默認(rèn)的,也就是通過(guò)Jackson2ObjectMapperBuilder.cbor().build()方法創(chuàng)建的。
- 檢查請(qǐng)求是否是unicode字符,目前來(lái)說(shuō),大家用的都是UTF-8的
- 通過(guò)ObjectMapper將請(qǐng)求json轉(zhuǎn)換為對(duì)象。其實(shí)這部分還有一段判斷inputMessage是否是MappingJacksonInputMessage實(shí)例的,考慮到大家使用的版本,這部分就不說(shuō)了。
至此,Spring的邏輯全部結(jié)束,似乎還是沒(méi)有找到我們使用的JsonCreator注解或者JsonDeserialize的邏輯。不過(guò)也能想到,這兩個(gè)都是Jackson的類,那必然應(yīng)該是Jackson的邏輯。接下來(lái),就扒一扒Jackson的轉(zhuǎn)換邏輯了。
深入Jackson的ObjectMapper邏輯
牽扯Jackson的邏輯主要分布在AbstractMessageConverterMethodArgumentResolver#readWithMessageConverters和ObjectMapper#readValue這兩個(gè)方法中。先說(shuō)一下ObjectMapper#readValue方法的邏輯,這里面會(huì)調(diào)用GenderIdCodeEnum#create方法,完成類型轉(zhuǎn)換。
ObjectMapper#readValue方法直接調(diào)用了當(dāng)前類中的_readMapAndClose方法,這個(gè)方法里面比較關(guān)鍵的是ctxt.readRootValue(p, valueType, _findRootDeserializer(ctxt, valueType), null),這個(gè)方法就是將輸入json轉(zhuǎn)換為對(duì)象。咱們?cè)倮^續(xù)深入,可以找到Jackson內(nèi)部是通過(guò)BeanDeserializer這個(gè)類轉(zhuǎn)換對(duì)象的,比較重要的是deserializeFromObject方法,源碼如下(刪除一下不太重要的代碼):
public Object deserializeFromObject(JsonParser p, DeserializationContext ctxt) throws IOException { // 這里根據(jù)上下文中目標(biāo)類型,創(chuàng)建實(shí)例對(duì)象,其中 _valueInstantiator 是 StdValueInstantiator 實(shí)例。 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(); // 根據(jù)字段名找到 屬性對(duì)象,對(duì)于gender字段,類型是 MethodProperty。 SettableBeanProperty prop = _beanProperties.find(propName); if (prop != null) { // normal case try { // 開始進(jìn)行解碼操作,并將解碼結(jié)果寫入到對(duì)象中 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的邏輯(只保留關(guān)鍵代碼):
public void deserializeAndSet(JsonParser p, DeserializationContext ctxt, Object instance) throws IOException { Object value; // 調(diào)用 FactoryBasedEnumDeserializer 實(shí)例的解碼方法 value = _valueDeserializer.deserialize(p, ctxt); // 通過(guò)反射將值寫入對(duì)象中 _setter.invoke(instance, value); }
其中_valueDeserializer是FactoryBasedEnumDeserializer實(shí)例,快要接近目標(biāo)了,看下這段邏輯:
public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { // 獲取json中的值 Object value = _deser.deserialize(p, ctxt); // 調(diào)用 GenderIdCodeEnum#create 方法 return _factory.callOnWith(_valueClass, value); }
_factory是AnnotatedMethod實(shí)例,主要是對(duì)JsonCreator注解定義的方法的包裝,然后callOnWith中調(diào)用java.lang.reflect.Method#invoke反射方法,執(zhí)行GenderIdCodeEnum#create。
至此,我們終于串起來(lái)所有邏輯。
文末總結(jié)
本文通過(guò)一個(gè)示例串起來(lái)@JsonCreator注解起作用的邏輯,JsonDeserializer接口的邏輯與之類型,可以耐心debug一番。下面給出主要類的類圖:
推薦閱讀
- SpringBoot 實(shí)戰(zhàn):一招實(shí)現(xiàn)結(jié)果的優(yōu)雅響應(yīng)
- SpringBoot 實(shí)戰(zhàn):如何優(yōu)雅的處理異常
- SpringBoot 實(shí)戰(zhàn):通過(guò) BeanPostProcessor 動(dòng)態(tài)注入 ID 生成器
- SpringBoot 實(shí)戰(zhàn):自定義 Filter 優(yōu)雅獲取請(qǐng)求參數(shù)和響應(yīng)結(jié)果
- SpringBoot 實(shí)戰(zhàn):優(yōu)雅的使用枚舉參數(shù)
- SpringBoot 實(shí)戰(zhàn):優(yōu)雅的使用枚舉參數(shù)(原理篇)
- SpringBoot 實(shí)戰(zhàn):在 RequestBody 中優(yōu)雅的使用枚舉參數(shù)
- SpringBoot 實(shí)戰(zhàn):在 RequestBody 中優(yōu)雅的使用枚舉參數(shù)(原理篇)
到此這篇關(guān)于Java SpringBoot在RequestBody中高效的使用枚舉參數(shù)原理案例詳解的文章就介紹到這了,更多相關(guān)Java SpringBoot在RequestBody中高效的使用枚舉參數(shù)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
解決Aop @AfterReturning因返回類型不一致導(dǎo)致無(wú)法執(zhí)行切面代碼
這篇文章主要介紹了解決Aop @AfterReturning因返回類型不一致導(dǎo)致無(wú)法執(zhí)行切面代碼問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-07-07解決Java的InputMismatchException異常
這篇文章介紹了解決Java的InputMismatchException異常的方法,文中通過(guò)示例代碼介紹的非常詳細(xì)。對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-12-12Java全面細(xì)致講解Cookie與Session及kaptcha驗(yàn)證碼的使用
web開發(fā)階段我們主要是瀏覽器和服務(wù)器之間來(lái)進(jìn)行交互。瀏覽器和服務(wù)器之間的交互就像人和人之間進(jìn)行交流一樣,但是對(duì)于機(jī)器來(lái)說(shuō),在一次請(qǐng)求之間只是會(huì)攜帶著本次請(qǐng)求的數(shù)據(jù)的,但是可能多次請(qǐng)求之間是會(huì)有聯(lián)系的,所以提供了會(huì)話機(jī)制2022-06-06解決mybatis批量更新(update foreach)失敗的問(wèn)題
這篇文章主要介紹了解決mybatis批量更新(update foreach)失敗的問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-11-11MyBatis-Plus實(shí)現(xiàn)條件查詢的三種格式例舉詳解
本文主要介紹了MyBatis-Plus三中條件查詢格式的示例,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2022-08-08Java模板動(dòng)態(tài)生成word文件的方法步驟
最近項(xiàng)目中需要根據(jù)模板生成word文檔,模板文件也是word文檔。本文使用使用freemarker模板生成word文件,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-07-07java使用ArrayList實(shí)現(xiàn)斗地主(無(wú)序版)
這篇文章主要為大家詳細(xì)介紹了java使用ArrayList實(shí)現(xiàn)斗地主,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-03-03