Spring MVC requests data conversion through HttpMessageConverter

Java Web people often design restful APIs( How to design a RESTful API ), interact with json data. So how can the json data passed in from the front-end be parsed into Java objects as API input parameters, and how can the API return result parse the Java objects into json format data and return them to the front-end? In fact, in the whole data flow process, HttpMessageConverter plays an important role; in addition, what customized content can we add in the conversion process?

Introduction to HttpMessageConverter

org.springframework.http.converter.HttpMessageConverter is a policy interface. The interface description is as follows:

Strategy interface that specifies a converter that can convert from and to HTTP request s and response s. There are only 5 methods in the interface, which is simply to obtain the supported MediaType (application/json, etc.). When receiving the request, it is judged whether it can be canRead, read if it can be read; when returning the result, it is judged whether it can be canWrite and write if it can be written. These methods have an impression at first:

boolean canRead(Class<?> clazz, MediaType mediaType);
boolean canWrite(Class<?> clazz, MediaType mediaType);
List<MediaType> getSupportedMediaTypes();
T read(Class<? extends T> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException;
void write(T t, MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException;

Default configuration

In general, we don't configure any MessageConverter when we write Demo, but data transmission before and after is still easy to use, because spring MVC will automatically configure some httpmessageconverters when it starts, and the default MessageConverter is added to the WebMvcConfigurationSupport class:

protected final void addDefaultHttpMessageConverters(List<HttpMessageConverter<?>> messageConverters) {
        StringHttpMessageConverter stringConverter = new StringHttpMessageConverter();
        stringConverter.setWriteAcceptCharset(false);

        messageConverters.add(new ByteArrayHttpMessageConverter());
        messageConverters.add(stringConverter);
        messageConverters.add(new ResourceHttpMessageConverter());
        messageConverters.add(new SourceHttpMessageConverter<Source>());
        messageConverters.add(new AllEncompassingFormHttpMessageConverter());

        if (romePresent) {
            messageConverters.add(new AtomFeedHttpMessageConverter());
            messageConverters.add(new RssChannelHttpMessageConverter());
        }

        if (jackson2XmlPresent) {
            ObjectMapper objectMapper = Jackson2ObjectMapperBuilder.xml().applicationContext(this.applicationContext).build();
            messageConverters.add(new MappingJackson2XmlHttpMessageConverter(objectMapper));
        }
        else if (jaxb2Present) {
            messageConverters.add(new Jaxb2RootElementHttpMessageConverter());
        }

        if (jackson2Present) {
            ObjectMapper objectMapper = Jackson2ObjectMapperBuilder.json().applicationContext(this.applicationContext).build();
            messageConverters.add(new MappingJackson2HttpMessageConverter(objectMapper));
        }
        else if (gsonPresent) {
            messageConverters.add(new GsonHttpMessageConverter());
        }
    }

We see the familiar mappingjackson 2httpmessageconverter. If we introduce jackson related packages, Spring will add the MessageConverter for us, but we usually add and configure mappingjackson 2httpmessageconverter manually when building the framework. Why? Think about it first:

When we configure our own MessageConverter, the SpringMVC startup process will not call the addDefaultHttpMessageConverters method. Look at the following code if condition, which is also to customize our own MessageConverter

protected final List<HttpMessageConverter<?>> getMessageConverters() {
        if (this.messageConverters == null) {
            this.messageConverters = new ArrayList<HttpMessageConverter<?>>();
            configureMessageConverters(this.messageConverters);
            if (this.messageConverters.isEmpty()) {
                addDefaultHttpMessageConverters(this.messageConverters);
            }
            extendMessageConverters(this.messageConverters);
        }
        return this.messageConverters;
    }

Class relation diagram

Only two converters, mappingjackson 2httpmessageconverter and StringHttpMessageConverter, are listed here. We find that the former implements the GenericHttpMessageConverter interface, while the latter does not, leaving this key impression. This is the key logic judgment in the data flow process

Analysis of data flow

The data request and response are processed by the dotdispatch (HttpServletRequest request, httpservletresponse response) method of the DispatcherServlet class

Request process resolution

Look at the key code in the doDispatch method:

// The Adapter here is actually RequestMappingHandlerAdapter
HandlerAdapter ha = this.getHandlerAdapter(mappedHandler.getHandler()); 
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
    return;
}
// handler actually processed
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());            mappedHandler.applyPostHandle(processedRequest, response, mv);

After entering the handle, I paste the call stack here first. I hope that the little partner can follow the call stack route to trace the attempt:

readWithMessageConverters:192, AbstractMessageConverterMethodArgumentResolver (org.springframework.web.servlet.mvc.method.annotation)
readWithMessageConverters:150, RequestResponseBodyMethodProcessor (org.springframework.web.servlet.mvc.method.annotation)
resolveArgument:128, RequestResponseBodyMethodProcessor (org.springframework.web.servlet.mvc.method.annotation)
resolveArgument:121, HandlerMethodArgumentResolverComposite (org.springframework.web.method.support)
getMethodArgumentValues:158, InvocableHandlerMethod (org.springframework.web.method.support)
invokeForRequest:128, InvocableHandlerMethod (org.springframework.web.method.support)
 // The following call stack focuses on the fork of processing request and return value
invokeAndHandle:97, ServletInvocableHandlerMethod (org.springframework.web.servlet.mvc.method.annotation)
invokeHandlerMethod:849, RequestMappingHandlerAdapter (org.springframework.web.servlet.mvc.method.annotation)
handleInternal:760, RequestMappingHandlerAdapter (org.springframework.web.servlet.mvc.method.annotation)
handle:85, AbstractHandlerMethodAdapter (org.springframework.web.servlet.mvc.method)
doDispatch:967, DispatcherServlet (org.springframework.web.servlet)

Here we focus on the contents of the readWithMessageConverters method at the top level of the call stack:

// Traverse messageConverters
for (HttpMessageConverter<?> converter : this.messageConverters) {
    Class<HttpMessageConverter<?>> converterType = (Class<HttpMessageConverter<?>>) converter.getClass();
        // The key points to remember in the above class diagram are to determine whether mappingjackson 2httpmessageconverter is of GenericHttpMessageConverter type
    if (converter instanceof GenericHttpMessageConverter) {
        GenericHttpMessageConverter<?> genericConverter = (GenericHttpMessageConverter<?>) converter;
        if (genericConverter.canRead(targetType, contextClass, contentType)) {
            if (logger.isDebugEnabled()) {
                logger.debug("Read [" + targetType + "] as \"" + contentType + "\" with [" + converter + "]");
            }
            if (inputMessage.getBody() != null) {
                inputMessage = getAdvice().beforeBodyRead(inputMessage, parameter, targetType, converterType);
                body = genericConverter.read(targetType, contextClass, inputMessage);
                body = getAdvice().afterBodyRead(body, inputMessage, parameter, targetType, converterType);
            }
            else {
                body = getAdvice().handleEmptyBody(null, inputMessage, parameter, targetType, converterType);
            }
            break;
        }
    }
    else if (targetClass != null) {
        if (converter.canRead(targetClass, contentType)) {
            if (logger.isDebugEnabled()) {
                logger.debug("Read [" + targetType + "] as \"" + contentType + "\" with [" + converter + "]");
            }
            if (inputMessage.getBody() != null) {
                inputMessage = getAdvice().beforeBodyRead(inputMessage, parameter, targetType, converterType);
                body = ((HttpMessageConverter<T>) converter).read(targetClass, inputMessage);
                body = getAdvice().afterBodyRead(body, inputMessage, parameter, targetType, converterType);
            }
            else {
                body = getAdvice().handleEmptyBody(null, inputMessage, parameter, targetType, converterType);
            }
            break;
        }
    }
}

Then we can judge whether it can read. Finally, we go to the following code to deserialize the input content:

protected Object _readMapAndClose(JsonParser p0, JavaType valueType)
        throws IOException
    {
        try (JsonParser p = p0) {
            Object result;
            JsonToken t = _initForReading(p);
            if (t == JsonToken.VALUE_NULL) {
                // Ask JsonDeserializer what 'null value' to use:
                DeserializationContext ctxt = createDeserializationContext(p,
                        getDeserializationConfig());
                result = _findRootDeserializer(ctxt, valueType).getNullValue(ctxt);
            } else if (t == JsonToken.END_ARRAY || t == JsonToken.END_OBJECT) {
                result = null;
            } else {
                DeserializationConfig cfg = getDeserializationConfig();
                DeserializationContext ctxt = createDeserializationContext(p, cfg);
                JsonDeserializer<Object> deser = _findRootDeserializer(ctxt, valueType);
                if (cfg.useRootWrapping()) {
                    result = _unwrapAndDeserialize(p, ctxt, cfg, valueType, deser);
                } else {
                    result = deser.deserialize(p, ctxt);
                }
                ctxt.checkUnresolvedObjectId();
            }
            // Need to consume the token too
            p.clearCurrentToken();
            return result;
        }
    }

This is the end of the process of parsing parameters from the request. Take the chance to see the process of returning the response results to the front end

Return procedure resolution

In the above call stack request and return result fork, the returned content is also processed:

writeWithMessageConverters:224, AbstractMessageConverterMethodProcessor (org.springframework.web.servlet.mvc.method.annotation)
handleReturnValue:174, RequestResponseBodyMethodProcessor (org.springframework.web.servlet.mvc.method.annotation)
handleReturnValue:81, HandlerMethodReturnValueHandlerComposite (org.springframework.web.method.support)
// Fork mouth
invokeAndHandle:113, ServletInvocableHandlerMethod (org.springframework.web.servlet.mvc.method.annotation)

Focus on the top-level content of the call stack. Are you familiar with the same logic? Judge whether you can write canWrite or write if you can

for (HttpMessageConverter<?> messageConverter : this.messageConverters) {
    if (messageConverter instanceof GenericHttpMessageConverter) {
        if (((GenericHttpMessageConverter) messageConverter).canWrite(
                declaredType, valueType, selectedMediaType)) {
            outputValue = (T) getAdvice().beforeBodyWrite(outputValue, returnType, selectedMediaType,
                    (Class<? extends HttpMessageConverter<?>>) messageConverter.getClass(),
                    inputMessage, outputMessage);
            if (outputValue != null) {
                addContentDispositionHeader(inputMessage, outputMessage);
                ((GenericHttpMessageConverter) messageConverter).write(
                        outputValue, declaredType, selectedMediaType, outputMessage);
                if (logger.isDebugEnabled()) {
                    logger.debug("Written [" + outputValue + "] as \"" + selectedMediaType +
                            "\" using [" + messageConverter + "]");
                }
            }
            return;
        }
    }
    else if (messageConverter.canWrite(valueType, selectedMediaType)) {
        outputValue = (T) getAdvice().beforeBodyWrite(outputValue, returnType, selectedMediaType,
                (Class<? extends HttpMessageConverter<?>>) messageConverter.getClass(),
                inputMessage, outputMessage);
        if (outputValue != null) {
            addContentDispositionHeader(inputMessage, outputMessage);
            ((HttpMessageConverter) messageConverter).write(outputValue, selectedMediaType, outputMessage);
            if (logger.isDebugEnabled()) {
                logger.debug("Written [" + outputValue + "] as \"" + selectedMediaType +
                        "\" using [" + messageConverter + "]");
            }
        }
        return;
    }
}

We see this line of code:

outputValue = (T) getAdvice().beforeBodyWrite(outputValue, returnType, selectedMediaType,
                    (Class<? extends HttpMessageConverter<?>>) messageConverter.getClass(),
                    inputMessage, outputMessage);

When designing a RESTful API interface, we usually encapsulate the returned data in a unified format. Usually, we implement the responsebodyadvice < T > interface to process the returned values of all APIs, and encapsulate the data in a unified way before the real write

@RestControllerAdvice()
public class CommonResultResponseAdvice implements ResponseBodyAdvice<Object> {

    @Override
    public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
        return true;
    }

    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
            Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request,
            ServerHttpResponse response) {
        if (body instanceof CommonResult) {
            return body;
        }
        return new CommonResult<Object>(body);
    }

}

This is the whole process. The details of the whole implementation process need to be tracked and found by the partners themselves. At the beginning of the article, we said that adding our own MessageConverter can better meet our customization. What can be customized?

customization

Null value processing

There are many null values in the request and returned data. Sometimes these values have no practical significance. We can filter out and not return them, or set them as default values. For example, by overriding the getObjectMapper method, the null value of the returned result is not serialized:

converters.add(0, new MappingJackson2HttpMessageConverter(){
    @Override
    public ObjectMapper getObjectMapper() {
        super.getObjectMapper().setSerializationInclusion(JsonInclude.Include.NON_NULL);
                return super.getObjectMapper();
    }
}

XSS script attack

In order to ensure more secure input data and prevent XSS script attacks, we can add a custom deserializer:

//Corresponding cannot directly return String type
converters.add(0, new MappingJackson2HttpMessageConverter(){
    @Override
    public ObjectMapper getObjectMapper() {
        super.getObjectMapper().setSerializationInclusion(JsonInclude.Include.NON_NULL);

                // XSS script filtering
        SimpleModule simpleModule = new SimpleModule();
        simpleModule.addDeserializer(String.class, new StringXssDeserializer());
        super.getObjectMapper().registerModule(simpleModule);

        return super.getObjectMapper();
    }
}

Detail analysis

What is the judgment logic of canRead and canWrite? See the figure below:

Content type and Accept are set in the client Request Header, and can read or write is judged according to the configured MessageConverter. Then the first element of the content type of response.body is the value of the corresponding request.headers.Accept property, also called MediaType. If the server supports this Accept, the format of the response.body should be determined according to this Accept. At the same time, set the response.headers.Content-Type to the MediaType that matches the Accept supported by itself

Summary and thinking

From the perspective of God, the whole process can be summarized as follows: the request message is first converted into HttpInputMessage, and then it is converted into spring MVC java object through HttpMessageConverter, and vice versa.

Summarize the mediatypes and javatypes supported by various common HttpMessageConverter and their corresponding relationships here:

Class name Supported javatypes Supported mediatypes
ByteArrayHttpMessageConverter byte[] application/octet-stream, */*
StringHttpMessageConverter String text/plain, */*
MappingJackson2HttpMessageConverter Object application/json, application/*+json
AllEncompassingFormHttpMessageConverter Map<K, List<?>> application/x-www-form-urlencoded, multipart/form-data
SourceHttpMessageConverter Source application/xml, text/xml, application/*+xml

Finally, consider the following question: why does HttpMessageConverter judge canWrite first and then judge whether there is data encapsulation of responseBodyAdvice in the process of writing? What if we first encapsulate the data of responseBodyAdvice and then judge canWrite?

Efficiency tools

Still introduce some good tools for writing this article

processon

ProcessOn is an aggregation platform of online drawing tools, which can draw flow chart, mind map, UI prototype diagram, UML, network topology diagram, organization chart, etc,
You don't need to worry about downloading and updating. No matter Mac or Windows, a browser can create ideas and plan work anytime and anywhere. At the same time, you can share your work to team members or friends, and everyone can edit, read and comment on the work anytime and anywhere

SequenceDiagram

SequenceDiagram It's a plug-in of IntelliJ IDEA. With this plug-in, you can

  1. Generate a simple sequence diagram.
  2. Click a graphic shape to navigate the code.
  3. Remove the class from the diagram.
  4. Export the chart as an image.
  5. Exclude classes from the chart through Settings > other settings > sequence
    Convenient and fast positioning method and understanding of class calling process

     

     

Published 7 original articles, won praise 4, visited 9775
Private letter follow

Keywords: JSON Java xml Spring

Added by figuringout on Wed, 05 Feb 2020 12:19:05 +0200