Http Message Converter Introduction

Http Message Converter is responsible for serializing Java Object to JSON/XML data representation and deserializing JSON/XML data representation to Java Object.

When we configure: <mvc:annotation-driven /> in based XML or @EnableWebMvc in based Java (both are equivalent), AnnotationDrivenBeanDefinitionParser will register a series of conversion service, validators, and message-converters.

If there is no custom <message-converters> tag in <mvc:annotation-driven />, Spring will register the following set of message-converters by default.

  • ByteArrayHttpMessageConverter
  • StringHttpMessageConverter
  • ResourceHttpMessageConverter
  • SourceHttpMessageConverter
  • AllEncompassingFormHttpMessageConverter

Also, Spring registers the following message converters by default if the corresponding lib package is detected in the CLASSPATH.

  • Jaxb2RootElementHttpMessageConverter - Java Object <-> XML
  • MappingJackson2HttpMessageConverter - JSON -> Java Object
  • MappingJacksonHttpMessageConverter - JSON -> Java Object
  • AtomFeedHttpMessageConverter - Atom feeds -> Java Object
  • RssChannelHttpMessageConverter - RSS feeds -> Java Object

The order of detection of lib packages is defined in WebMvcConfigurationSupport.java.

How does Http Message Converter work?

When the server needs to respond to a request from a client, Spring determines the type of data to return based on the media type of the Accept parameter value in the request header. Spring then tries to find a suitable registered converter to handle this media type, uses this converter to perform the type conversion and returns it to the client.

Spring has a content negotiation policy in @ResponseBody, the last of which is to determine the return value type based on the Accept media type in the request Header. This policy is configurable and is explained below.

When the server receives a request from the client, Spring determines the data type of the request message body based on the media type class of the Content-Type parameter value in the request header, and then Spring tries to find a suitable registration converter to convert the data in the message body into a Java Object.

The Http Message Converter is involved in a complete client-side request to server-side response process as follows.

  • View the Content-Type of the request header.
  • Finding the appropriate HttpMessageConverter based on the media type of the Content-Type.
  • Deserialize request data to Java Object.
  • Get request parameters and path variables (Path Variable)
  • Business Logic
  • Determine the Accept header (based on the content negotiation policy, explained below)
  • Find the appropriate HttpMessageConverter based on the Accept header
  • Return the response to the client

Serialization process @ResponseBody

Spring Content Negotiation

When a client requests data from the server, the server needs to decide in which form it will return the data to the client, and this decision is called Content Negotiation.

The server-side decision on what form to return the data relies on the ContentNegotiationStrategy, which Spring has by default or can be customized through configuration.

How does Spring Content Negotiation work?

The normal HTTP protocol works by using the Accept header to determine the type of data to be returned, but this approach relies too much on the client’s request parameters ( Accept header) and is sometimes less convenient to use, so Spring defines a number of content negotiation policies by default (including the native HTTP Accept header approach).

Spring defines default content negotiation policies.

  • The first is the suffix of the path in the URL ( Path Extension Strategy). If the suffix is .html, then HTML formatted data is returned; if the suffix is .xls, then Excel formatted data is returned. This is turned on by default.
  • The second is the URL parameter format (which can be customized) ( Parameter Strategy). For example: http://myserver/myapp/accounts/list?format=xls, Spring will determine the format of the returned data based on the definition of format. This is turned off by default.
  • The last one is Accept header( Header Strategy ). This is how real HTTP works. This is on by default.

These three methods are checked by Spring in order of priority to see if they are on, and if they are on they are used and not checked further down the list. These methods are defined in the class ContentNegotiationConfigurer.java.

Spring Content Negotiation Custom Configuration

The Java Config approach.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
@Configuration
@EnableWebMvc
public class WebConfig extends WebMvcConfigurerAdapter {

    @Override
    public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
        configurer.favorPathExtension(false)//默认后缀模式是开启的,自定义关闭
        .mediaType("xml", MediaType.APPLICATION_XML)//如果后缀是xml,返回媒体类型为application/xml
        .mediaType("json", MediaType.APPLICATION_JSON)//如果后缀是json,返回媒体类型为application/json
        .favorParameter(true)//默认是关闭的,自定义开启
        .defaultContentType(MediaType.APPLICATION_XML)//自定义默认返回媒体类型为application/xml
        .ignoreAcceptHeader(true)//关闭Header Strategy
    }
}

Deserialization process @RequestMapping

@RequestMapping can deserialize different forms of data representation to Java Object, the server side needs to find the appropriate HttpMessageConverter according to the Content-Type in the Request Header.

The deserialization process can not only be deserialized to POJO, but also other data support such as Map, File.

Custom HttpMessageConverter

Inherit AbstractHttpMessageConverter and customize ReportConverter.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
package com.fmz.learn.spring.utils;

public class ReportConverter
        extends AbstractHttpMessageConverter<Report> {

    public ReportConverter() {
        super(new MediaType("text", "report"));
    }

    @Override
    protected boolean supports(Class<?> clazz) {
        return Report.class.isAssignableFrom(clazz);
    }

    @Override
    protected Report readInternal(Class<? extends Report> clazz, HttpInputMessage inputMessage)
            throws IOException, HttpMessageNotReadableException {
        String requestBody = toString(inputMessage.getBody());
        int i = requestBody.indexOf("\n");
        if (i == -1) {
            throw new HttpMessageNotReadableException("No first line found");
        }

        String reportName = requestBody.substring(0, i).trim();
        String content = requestBody.substring(i).trim();

        Report report = new Report();
        report.setReportName(reportName);
        report.setContent(content);
        return report;
    }

    @Override
    protected void writeInternal(Report report, HttpOutputMessage outputMessage)
            throws IOException, HttpMessageNotWritableException {
        try {
            OutputStream outputStream = outputMessage.getBody();
            String body = report.getReportName() + "\n" +
                    report.getContent();
            outputStream.write(body.getBytes());
            outputStream.close();
        } catch (Exception e) {
        }
    }

    private static String toString(InputStream inputStream) {
        Scanner scanner = new Scanner(inputStream, "UTF-8");
        return scanner.useDelimiter("\\A").next();
    }
}

Configuring a custom ReportConverter.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
@Configuration
@EnableWebMvc
public class WebConfig extends WebMvcConfigurerAdapter {

    // 添加新的HTTPMessageConverter实现
    @Override
    public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
        converters.add(new ReportConverter());
    }
}

Reference https://fengmengzhao.github.io/2019/01/17/understand-spring-httpmessageconverter.html