SpringBoot practice: elegant use of enumeration parameters in RequestBody (principle)

This picture was created by Johnnys_pic stay Pixabay Publish on

Hello, I'm looking at the mountain.

stay Elegant use of enumeration parameters (principle) As we talked in, Spring uses different processing classes to process parameters for different parameter forms. This form is somewhat similar to the policy pattern. Split the processing logic for different parameter forms into different processing classes to reduce coupling and various if else logic. In this article, let's talk about the principle of using enumeration parameters in the RequestBody parameter.

Find the entrance

Students who have a certain foundation in Spring must know that the request entry is DispatcherServlet, and all requests will eventually fall into HA in the doDispatch method Handle (processedrequest, response, mappedhandler. Gethandler()) logic. Let's start from here and dig in layer by layer.

Follow the code and we'll find org springframework. web. method. support. Logic of 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);
}

It can be seen that the parameters are processed through the getMethodArgumentValues method, and then the doInvoke method is called to get the return value. Inside the getmethodargumentvalues method, parameters are processed through the HandlerMethodArgumentResolverComposite instance. Inside this class is a list of HandlerMethodArgumentResolver instances. In the list is a collection of Spring processing parameter logic. Following the code Debug, you can see that there are 27 elements. These classes can also be customized and extended to implement their own parameter parsing logic. This part will be introduced later.

Select Resolver

The Resolver list contains several processing classes we commonly use. The common parameters of Get request are processed by RequestParamMethodArgumentResolver, the packaging class is processed by ModelAttributeMethodProcessor, and the parameters in the form of RequestBody are processed by RequestResponseBodyMethodProcessor. This section is the use of the policy pattern in Spring, through the implementation of org springframework. web. method. support. Handlermethodargumentresolver #supportsparameter method to judge whether the input parameters can be parsed. The implementation of RequestResponseBodyMethodProcessor is pasted below:

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

You can see that the RequestResponseBodyMethodProcessor judges whether the current parameter can be parsed by judging whether the parameter has a RequestBody annotation.

Analytical parameters

The RequestResponseBodyMethodProcessor inherits from AbstractMessageConverterMethodArgumentResolver. The logic that really parses the RequestBody parameters is at org springframework. web. servlet. mvc. method. annotation. AbstractMessageConverterMethodArgumentResolver#readwithmessageconverters method. Let's look at the source code (because the source code is relatively long, only the core logic is left in the text.):

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;
}

Follow the code to explain the purpose of each part:

  1. Get request content type
  2. Get parameter container class
  3. Get target parameter type
  4. Convert the request parameter to the EmptyBodyCheckingHttpInputMessage type
  5. Loop through various RequestBody parameter parsers, which are implementation classes of HttpMessageConverter interface. Spring covers all kinds of situations, and there is always a suitable one. At the end of the paper, the class diagrams of each extension class of HttpMessageConverter are given.
  6. In the for loop body, select a suitable one for parsing
    1. First, call the canRead method to determine whether it is available
    2. Judge whether the request parameter is empty. If it is empty, process the empty request body through AOP advice, and then return
    3. If it is not empty, it will do the preprocessing through the advice of AOP first, then the read method will be used to transform the object and post processing through advice.

Spring's AOP is not within the scope of this article, so I've covered it all. Special notes will follow.

In this example, the HttpMessageConverter uses MappingJackson2HttpMessageConverter, which inherits from AbstractJackson2HttpMessageConverter. You can see from the name that this class uses Jackson to process request parameters. After the read method, the internal private method readJavaType will be 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);// 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);
    }
}

Follow the code to explain the purpose of each part:

  1. Get the requested content type, which is the extension logic implemented by Spring. Different ObjectMapper instances can be selected according to different content types. That is, the logic of step 2
  2. Select the ObjectMapper instance according to the content type and target type. In this case, the default is returned directly, that is, through jackson2objectmapperbuilder cbor(). Created by the build () method.
  3. Check whether the request is unicode. At present, we all use UTF-8
  4. Convert the request json into an object through ObjectMapper. In fact, there is also a section to judge whether inputMessage is an instance of MappingJacksonInputMessage. Considering the version you use, we won't talk about this section.

So far, the logic of Spring is all over. It seems that we still haven't found the JsonCreator annotation or the logic of JsonDeserialize we use. However, it can also be thought that these two are Jackson's classes, which must be Jackson's logic. Next, let's pick up Jackson's conversion logic.

Drill down into Jackson's ObjectMapper logic

The logic involving Jackson is mainly distributed in AbstractMessageConverterMethodArgumentResolver#readWithMessageConverters and ObjectMapper#readValue. Let's talk about the logic of the ObjectMapper#readValue method, which will call the GenderIdCodeEnum#create method to complete the type conversion.

The ObjectMapper#readValue method directly calls the_ readMapAndClose method. The key of this method is ctxt Readrootvalue (P, ValueType, _findrootdeserializer (ctxt, ValueType), null). This method is to convert the input json into an object. Let's go further. We can find that Jackson internally converts objects through the class BeanDeserializer. The more important is the de serializefromobject method. The source code is as follows (delete the less important code):

public Object deserializeFromObject(JsonParser p, DeserializationContext ctxt) throws IOException
{
    // Here, an instance object is created according to the target type in the context, where_ 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 according to the field name. For the gender field, the type is MethodProperty.
            SettableBeanProperty prop = _beanProperties.find(propName);
            if (prop != null) { // normal case
                try {
                    // Start the decoding operation 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;
}

Let's take a look at the logic of MethodProperty#deserializeAndSet (only key code is reserved):

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

Among them_ valueDeserializer is an instance of FactoryBasedEnumDeserializer. It is approaching the target. Take a look at this 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);
}

_ factory is an example of AnnotatedMethod, which mainly packages the method of JsonCreator annotation definition, and then calls java. in callOnWith. lang.reflect. Method #invoke reflection method, execute GenderIdCodeEnum#create.

At this point, we finally string all the logic.

Summary at the end of the paper

This article uses an example to string @ JsonCreator to annotate the working logic, and the logic and type of the JsonDeserializer interface. You can debug patiently. The class diagram of main classes is given below:

Recommended reading

Hello, I'm looking at the mountain. Swim in the code world and enjoy life. If the article is helpful to you, please like, collect and pay attention to it. I also compiled some excellent learning materials, and I would like to pay attention to the official account of "the mountain view cabin" and get the reply to "information".

Personal homepage: https://www.howardliu.cn
Personal blog: SpringBoot practice: elegant use of enumeration parameters in RequestBody (principle)
CSDN home page: https://kanshan.blog.csdn.net/
CSDN blog: SpringBoot practice: elegant use of enumeration parameters in RequestBody (principle)

Keywords: Java Spring Spring Boot

Added by robinjohn on Fri, 17 Dec 2021 08:33:15 +0200