“This is the 28th day of my participation in the Gwen Challenge in November. Check out the details: The Last Gwen Challenge in 2021.”

This article was included in the column “Spring Boot Actual Combat”.

Hello, I’m looking at the mountains.

As we discussed in Principles for Elegant Use of enumerated Parameters, Spring uses different processing classes for different parameter forms, which are somewhat similar to policy patterns. Split the processing logic for different parameter forms into different processing classes, reducing coupling and various if-else logic. This article will take a look at how enumeration parameters are used in the RequestBody parameter.

To find the entrance

For those of you who are familiar with Spring, the request entry is DispatcherServlet, All requests end up in the ha.handle(processedRequest, Response, mappedHandler.gethandler ()) logic of the doDispatch method. We start here, layer by layer, and work our way in.

Following code, we will find org. Springframework. Web. Method. Support. InvocableHandlerMethod# invokeForRequest logic:

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);
}
Copy the code

As you can see, the parameter is handled by the getMethodArgumentValues method, and then the doInvoke method is called to get the return value. GetMethodArgumentValues within a method and processing parameters through HandlerMethodArgumentResolverComposite instance. This class is a HandlerMethodArgumentResolver instance list, the list is a collection of Spring processing parameter logic, follow the Debug code, you can see there are 27 elements. These classes can also be custom extended to implement their own parameter parsing logic, which I’ll cover later.

Choose the Resolver

This Resolver list contains several commonly used processing classes. Get request of ordinary parameters by RequestParamMethodArgumentResolver processing, wrapper classes by ModelAttributeMethodProcessor processing parameters, RequestBody form of parameters, By RequestResponseBodyMethodProcessor processing parameters. This is the use of the strategy pattern in the Spring, by implementing the org. Springframework. Web. Method. Support. HandlerMethodArgumentResolver# supportsParameter method, Determines whether the input parameters can be parsed. The realization of the labeled RequestResponseBodyMethodProcessor below:

public boolean supportsParameter(MethodParameter parameter) {
    return parameter.hasParameterAnnotation(RequestBody.class);
}

Copy the code

As you can see, RequestResponseBodyMethodProcessor is judged by judging whether parameters with RequestBody annotations, whether can parse the current parameters.

Analytical parameters

RequestResponseBodyMethodProcessor inherited from AbstractMessageConverterMethodArgumentResolver, True parsing RequestBody parameters of logic in the org. Springframework. Web. Servlet. MVC) method. The annotation. AbstractMessageConverterMethodArgumentResolve R# readWithMessageConverters method. Let’s take a look at the source code (because the source code is quite long, only the core logic is left in the article). :

protected <T> Object readWithMessageConverters(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException {
    MediaType contentType = inputMessage.getHeaders().getContentType();/ / 1Class<? > 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) {/ / 5Class<HttpMessageConverter<? >> converterType = (Class<HttpMessageConverter<? >>) converter.getClass(); GenericHttpMessageConverter<? > genericConverter = (converterinstanceofGenericHttpMessageConverter ? (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;
}
Copy the code

Follow the code to explain the purpose of each part:

  1. Gets the request Content-type
  2. Gets the parameter container class
  3. Gets the target parameter type
  4. Convert the request parameters toEmptyBodyCheckingHttpInputMessagetype
  5. Loop through various RequestBody parameter parsers, all of which areHttpMessageConverterThe implementation class of the interface. Spring covers every situation, and there’s always one for you. At the end of the article is givenHttpMessageConverterA class diagram of each extended class.
  6. The body of the for loop is to select the appropriate one and parse it
    1. First callcanReadMethod to check whether it is available
    2. Determine whether the request request parameter is null, and if so, pass the AOPadviceProcess the empty request body and return
    3. Not null, first through AOPadviceDo the pre-processing and then callreadMethod to convert an object in the passadviceDo the post-processing

Spring’S AOP is outside the scope of this article, so I’ll skip it. Special notes will follow.

This case, the HttpMessageConverter is using MappingJackson2HttpMessageConverter, this class inherits from AbstractJackson2HttpMessageConverter. As the name indicates, this class uses Jackson to handle request parameters. After the read method, the internal private readJavaType method is called. The core logic of this method is given below:

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);/ / 2Assert.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);
            returnobjectMapper.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); }}Copy the code

Follow the code to explain the purpose of each part:

  1. Fetch requestedcontent-typeThis is the extension logic implemented by Spring, depending on thecontent-typeYou can choose different onesObjectMapperInstance. That’s the logic of step 2
  2. According to thecontent-typeAnd target type, selectObjectMapperInstance. In this case, the direct return is the default, which is passedJackson2ObjectMapperBuilder.cbor().build()Method created by.
  3. Check if the request is a Unicode character, which is what everyone currently usesUTF-8the
  4. throughObjectMapperConvert the request JSON to an object. There’s actually a judgment hereinputMessageWhether it isMappingJacksonInputMessageFor example, given the version you’re using, I won’t mention that.

At this point, the Spring logic is all over, and we still don’t seem to have found the JsonCreator annotation or JsonDeserialize logic we used. But you can also imagine that these are both Jackson’s classes, so that must be Jackson’s logic. Next, take a look at Jackson’s conversion logic.

Dig into Jackson’s ObjectMapper logic

With Jackson’s logic are mainly distributed in AbstractMessageConverterMethodArgumentResolver# readWithMessageConverters and ObjectMapper# readValue these two methods. Take a look at the logic of the ObjectMapper#readValue method, which calls the GenderIdCodeEnum#create method to perform the type conversion.

The ObjectMapper#readValue method directly calls the _readMapAndClose method in the current class, Ctxt. readRootValue(p, valueType, _findRootDeserializer(CTXT, valueType), null). This method converts the input JSON to an object. DeserializeFromObject = BeanDeserializer = BeanDeserializer = deserializeFromObject = BeanDeserializer = BeanDeserializer = BeanDeserializer = BeanDeserializer = BeanDeserializer = BeanDeserializer = BeanDeserializer = BeanDeserializer = BeanDeserializer = BeanDeserializer = BeanDeserializer = BeanDeserializer = BeanDeserializer

public Object deserializeFromObject(JsonParser p, DeserializationContext ctxt) throws IOException {
    _valueInstantiator is an instance of 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();

            // Find the property object based on the field name. For the gender field, the type is MethodProperty.
            SettableBeanProperty prop = _beanProperties.find(propName);
            if(prop ! =null) { // normal case
                try {
                    // Start decoding and write the decoding result to the object
                    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;
}
Copy the code

Let’s look at the logic for MethodProperty#deserializeAndSet (leaving only the key code) :

public void deserializeAndSet(JsonParser p, DeserializationContext ctxt, Object instance) throws IOException {
    Object value;
    / / call FactoryBasedEnumDeserializer instance decoding method
    value = _valueDeserializer.deserialize(p, ctxt);
    // Write values to objects by reflection
    _setter.invoke(instance, value);
}
Copy the code

Which is _valueDeserializer FactoryBasedEnumDeserializer instance, almost close to the target, look at this piece of logic:

public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
    // Get the value in json
    Object value = _deser.deserialize(p, ctxt);
    // Call the GenderIdCodeEnum#create method
    return _factory.callOnWith(_valueClass, value);
}
Copy the code

_factory is an AnnotatedMethod instance, which is essentially a wrapper around a method defined in the JsonCreator annotation, and then calls the Java.lang.reflect. Method#invoke reflection method in callOnWith, Perform GenderIdCodeEnum# create.

At this point, we finally string together all the logic.

At the end of the article to summarize

This article uses an example to string together the @JsonCreator annotation logic in action. The logic of the JsonDeserializer interface and its type can be debug patiently. The class diagram for the main classes is shown below:

Recommended reading

  • SpringBoot: elegant response to achieve results in one move
  • SpringBoot: How to handle exceptions gracefully
  • SpringBoot: Dynamically injects the ID generator through the BeanPostProcessor
  • SpringBoot: Customizes Filter to gracefully obtain request parameters and response results
  • SpringBoot: Elegant use of enumeration parameters
  • SpringBoot: Elegant use of enumeration parameters (Principles)
  • SpringBoot: Gracefully use enumeration parameters in the RequestBody
  • SpringBoot: Gracefully using enumeration parameters in the RequestBody
  • Do unit tests with JUnit5+MockMvc+Mockito
  • SpringBoot: Loads and reads resource files

Hello, I’m looking at the mountains. Swim in the code, play to enjoy life. If this article is helpful to you, please like, bookmark, follow. Welcome to follow the public account “Mountain Hut”, discover a different world.