天天看點

SpringBoot 實戰:在 RequestBody 中優雅的使用枚舉參數(原理篇)

SpringBoot 實戰:在 RequestBody 中優雅的使用枚舉參數(原理篇)

你好,我是看山。

在優雅的使用枚舉參數(原理篇)中我們聊過,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 實戰:在 RequestBody 中優雅的使用枚舉參數(原理篇)

推薦閱讀

SpringBoot 實戰:一招實作結果的優雅響應

SpringBoot 實戰:如何優雅的處理異常

SpringBoot 實戰:通過 BeanPostProcessor 動态注入 ID 生成器

SpringBoot 實戰:自定義 Filter 優雅擷取請求參數和響應結果

SpringBoot 實戰:優雅的使用枚舉參數

SpringBoot 實戰:優雅的使用枚舉參數(原理篇)

SpringBoot 實戰:在 RequestBody 中優雅的使用枚舉參數

SpringBoot 實戰:在 RequestBody 中優雅的使用枚舉參數(原理篇)