Data Type Conversion in Spring MVC (Part Two)

Posted by landysaccount on Mon, 08 Jul 2019 19:41:29 +0200

In Spring MVC, two annotations @RequestBody and @ResponseBody can be used to complete the conversion of request message to object and object to response message respectively. The underlying flexible message conversion mechanism is described. The HTTP MessageConverter, which is configured by default, is used for parsing, and then the corresponding data is bound to the object to be returned.

HttpInputMessage

This class is the abstraction of a Http request message in Spring MVC. In the read() method of HttpMessageConverter, there is a parameter of HttpInputMessage, which is the internal abstraction of the "request message" of the receptor that the message converter of Spring MVC acts on. The message converter extracts the message from the "request message" according to the rules. Converts to the object declared in the method parameter.

package org.springframework.http;

import java.io.IOException;
import java.io.InputStream;

public interface HttpInputMessage extends HttpMessage {

    InputStream getBody() throws IOException;

}

HttpOutputMessage

In the write() method of HttpMessageConverter, there is a parameter of HttpOutputMessage, which is the internal abstraction of the "response message" of the message converter of Spring MVC. The message converter writes the "response message" into the response message according to certain rules.

package org.springframework.http;

import java.io.IOException;
import java.io.OutputStream;

public interface HttpOutputMessage extends HttpMessage {

    OutputStream getBody() throws IOException;

}

HttpMessageConverter

/*
 * Copyright 2002-2010 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.http.converter;

import java.io.IOException;
import java.util.List;

import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;


public interface HttpMessageConverter<T> {


    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;

}

The HttpMessageConverter interface provides five methods:


  • canRead: Determines whether the converter can convert request content into Java objects
  • canWrite: Determines whether the converter can convert Java objects into returned content
  • getSupportedMediaTypes: The MediaType type supported by the converter
  • read: read the request content and convert it into Java objects
  • write: write the returned content after converting the Java object

The parameters of read and write methods are HttpInputMessage and HttpOutputMessage objects, which represent the request and response parts of a Http communication respectively. The corresponding input and output streams can be obtained by getBody method.
Currently, quite a number of converters have been provided by default in Spring, including:

Name Effect Reading Support MediaType Write Support MediaType
ByteArrayHttpMessageConverter Conversion between data and byte arrays / application/octet-stream
StringHttpMessageConverter Conversion between Data and String Types text/* text/plain
FormHttpMessageConverter Conversion between forms and MultiValueMap <string, string="> application/x-www-form-urlencoded application/x-www-form-urlencoded
SourceHttpMessageConverter Conversion between data and javax.xml.transform.Source text/xml and application/xml text/xml and application/xml
MarshallingHttpMessageConverter Conversion of XML data using Spring Marshaller/Unmarshaller text/xml and application/xml text/xml and application/xml
MappingJackson2HttpMessageConverter Conversion of Json data using Jackson's ObjectMapper application/json application/json
MappingJackson2XmlHttpMessageConverter Conversion of XML data using Jackson's Xml Mapper application/xml application/xml
BufferedImageHttpMessageConverter Conversion between data and java.awt.image.BufferedImage All types supported by Java I/O API All types supported by Java I/O API

HttpMessageConverter matching process:

@ RequestBody annotation: According to the Content-Type type of the header part of the Request object, the appropriate HttpMessageConverter is matched one by one to read the data.

private Object readWithMessageConverters(MethodParameter methodParam, HttpInputMessage inputMessage, Class paramType) throws Exception {  

    MediaType contentType = inputMessage.getHeaders().getContentType();  
    if (contentType == null) {  
        StringBuilder builder = new StringBuilder(ClassUtils.getShortName(methodParam.getParameterType()));  
        String paramName = methodParam.getParameterName();  
        if (paramName != null) {  
            builder.append(' ');  
            builder.append(paramName);  
        }  
        throw new HttpMediaTypeNotSupportedException("Cannot extract parameter (" + builder.toString() + "): no Content-Type found");  
    }  

    List<MediaType> allSupportedMediaTypes = new ArrayList<MediaType>();  
    if (this.messageConverters != null) {  
        for (HttpMessageConverter<?> messageConverter : this.messageConverters) {  
            allSupportedMediaTypes.addAll(messageConverter.getSupportedMediaTypes());  
            if (messageConverter.canRead(paramType, contentType)) {  
                if (logger.isDebugEnabled()) {  
                    logger.debug("Reading [" + paramType.getName() + "] as \"" + contentType  + "\" using [" + messageConverter + "]");  
                }  
                return messageConverter.read(paramType, inputMessage);  
            }  
        }  
    }  
    throw new HttpMediaTypeNotSupportedException(contentType, allSupportedMediaTypes);  
}

@ ResponseBody annotation: According to the Accept attribute (comma-separated) of the header part of the Request object, press the type in the accept one by one to traverse to find the HttpMessageConverter that can be processed.

private void writeWithMessageConverters(Object returnValue,  HttpInputMessage inputMessage, HttpOutputMessage outputMessage)  
                throws IOException, HttpMediaTypeNotAcceptableException {  
    List<MediaType> acceptedMediaTypes = inputMessage.getHeaders().getAccept();  
    if (acceptedMediaTypes.isEmpty()) {  
        acceptedMediaTypes = Collections.singletonList(MediaType.ALL);  
    }  
    MediaType.sortByQualityValue(acceptedMediaTypes);  
    Class<?> returnValueType = returnValue.getClass();  
    List<MediaType> allSupportedMediaTypes = new ArrayList<MediaType>();  
    if (getMessageConverters() != null) {  
        for (MediaType acceptedMediaType : acceptedMediaTypes) {  
            for (HttpMessageConverter messageConverter : getMessageConverters()) {  
                if (messageConverter.canWrite(returnValueType, acceptedMediaType)) {  
                    messageConverter.write(returnValue, acceptedMediaType, outputMessage);  
                    if (logger.isDebugEnabled()) {  
                        MediaType contentType = outputMessage.getHeaders().getContentType();  
                        if (contentType == null) {  
                            contentType = acceptedMediaType;  
                        }  
                        logger.debug("Written [" + returnValue + "] as \"" + contentType +  
                                "\" using [" + messageConverter + "]");  
                    }  
                    this.responseArgumentUsed = true;  
                    return;  
                }  
            }  
        }  
        for (HttpMessageConverter messageConverter : messageConverters) {  
            allSupportedMediaTypes.addAll(messageConverter.getSupportedMediaTypes());  
        }  
    }  
    throw new HttpMediaTypeNotAcceptableException(allSupportedMediaTypes);  
}

Customize a Json converter

class CustomJsonHttpMessageConverter implements HttpMessageConverter {

    //Jackson's Json Mapping Class
    private ObjectMapper mapper = new ObjectMapper();

    //The type of support for this converter: application/json
    private List supportedMediaTypes = Arrays.asList(MediaType.APPLICATION_JSON);

    /**
     * Determine whether the converter can convert input into Java type
     * @param clazz     Java types that need to be converted
     * @param mediaType MediaType for this request
     * @return
     */
    @Override
    public boolean canRead(Class clazz, MediaType mediaType) {
        if (mediaType == null) {
            return true;
        }
        for (MediaType supportedMediaType : getSupportedMediaTypes()) {
            if (supportedMediaType.includes(mediaType)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Determine whether the converter can convert Java types to specified output
     * @param clazz     Java types that need to be converted
     * @param mediaType MediaType for this request
     * @return
     */
    @Override
    public boolean canWrite(Class clazz, MediaType mediaType) {
        if (mediaType == null || MediaType.ALL.equals(mediaType)) {
            return true;
        }
        for (MediaType supportedMediaType : getSupportedMediaTypes()) {
            if (supportedMediaType.includes(mediaType)) {
                return true;
            }
        }
        return false;
    }

    /**
     * MediaType to get support for this converter
     * @return
     */
    @Override
    public List getSupportedMediaTypes() {
        return supportedMediaTypes;
    }

    /**
     * Read the request content and convert the Json into a Java object
     * @param clazz         Java types that need to be converted
     * @param inputMessage  Request object
     * @return
     * @throws IOException
     * @throws HttpMessageNotReadableException
     */
    @Override
    public Object read(Class clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
        return mapper.readValue(inputMessage.getBody(), clazz);
    }

    /**
     * Converting Java objects to Json return content
     * @param o             Objects to be transformed
     * @param contentType   Return type
     * @param outputMessage Receipt object
     * @throws IOException
     * @throws HttpMessageNotWritableException
     */
    @Override
    public void write(Object o, MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
        mapper.writeValue(outputMessage.getBody(), o);
    }
}

As can be seen from the write method in AbstractHttpMessageConverter, the parent of Mapping Jackson 2HttpMessageConverter, the method writes data to the output stream of the returned result through the writeInternal method, so it only needs to rewrite the method:

@Bean
public MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter() {
    return new MappingJackson2HttpMessageConverter() {
        //Rewrite the writeInternal method to encrypt before returning the content
        @Override
        protected void writeInternal(Object object,
                                     HttpOutputMessage outputMessage) throws IOException,
                HttpMessageNotWritableException {
            //Converting Java objects into Json String using Jackson's ObjectMapper
            ObjectMapper mapper = new ObjectMapper();
            String json = mapper.writeValueAsString(object);
            LOGGER.error(json);
            //encryption
            String result = json + "Encrypted!";
            LOGGER.error(result);
            //output
            outputMessage.getBody().write(result.getBytes());
        }
    };
}

After that, you need to configure this custom converter into Spring, where you add a custom converter by rewriting the configureMessageConverters method in WebMvcConfigurer:

//Add custom converter
@Override
public void configureMessageConverters(List<httpmessageconverter<?>> converters) {
    converters.add(mappingJackson2HttpMessageConverter());
    super.configureMessageConverters(converters);
}

Topics: Java xml JSON Spring