HttpMessageConverter

HttpMessageConverter may not be familiar with the @requestBody and @responseBody annotations, but if you dig into data conversion, you will find that the HttpMessageConverter interface, SpringBoot will select an appropriate HttpMessageConverter implementation class to convert to @RequestBody. There are many internal implementation classes and can also implement their own. If the implementation class can process the data, its canRead() method returns true, and SpringBoot calls its read() method to read from the request and convert to the entity class, as does canWrite.

But that’s not where I learned about HttpMessageConverter, but from RestTemplate, which is a class that executes HTTP requests synchronously and therefore doesn’t require OkHttp or other HTTP client dependencies, You can use it to communicate with other services, but it is prone to conversion problems. If you know about wechat or QQ interfaces, you will definitely get an error when invoking their services using the RestTemplate.

Such as the following in the call QQ interconnection to obtain user information interface, reported error.

org.springframework.web.client.UnknownContentTypeException: Could not extract response: no suitable HttpMessageConverter found for response type [class xxx.xxx.xxxxx] and content type [text/html; charset=utf-8]Copy the code

The error message is an unknown ContentType. This ContentType is the content-Type in the HTTP header returned by a third-party interface. If you look at the HTTP header returned by this interface using other tools, you will find that it has the value text/ HTML. We usually see the application/ JSON type. There is no internal HttpMessageConverter to handle text/ HTML data. None of the implementation classes canRead() returns true.

You usually don’t encounter this error when using OkHttp or any other framework.

Two, in-depth error source code

Only know the reason of the error and the source code, can better solve the problem, so, we’re more error source line number, positioning under the HttpMessageConverterExtractor extractData method, from the structure can tell at a glance about logic: The loop finds an HttpMessageConverter that can handle the contentType, then calls this HttpMessageConverter’s read() and returns.

 public T extractData(ClientHttpResponse response) throws IOException {
     MessageBodyClientHttpResponseWrapper responseWrapper = new MessageBodyClientHttpResponseWrapper(response);
     if(responseWrapper.hasMessageBody() && ! responseWrapper.hasEmptyMessageBody()) { MediaType contentType =this.getContentType(responseWrapper);
         try {
         	// Get the iterator of messageConverters
             Iterator var4 = this.messageConverters.iterator();
             while(var4.hasNext()) {
             	// Next HttpMessageConverterHttpMessageConverter<? > messageConverter = (HttpMessageConverter)var4.next();/ / if the interface is GenericHttpMessageConverter instance, inheritance AbstractHttpMessageConverter will go the the if.
                 if (messageConverter instanceofGenericHttpMessageConverter) { GenericHttpMessageConverter<? > genericMessageConverter = (GenericHttpMessageConverter)messageConverter;// Determine whether the converter can convert this type
                     if (genericMessageConverter.canRead(this.responseType, (Class)null, contentType)) {
                         if (this.logger.isDebugEnabled()) {
                             ResolvableType resolvableType = ResolvableType.forType(this.responseType);
                             this.logger.debug("Reading to [" + resolvableType + "]");
                         }
                         // This means that the current HttpMessageConverter can be converted, then call read and return
                         return genericMessageConverter.read(this.responseType, (Class)null, responseWrapper); }}// Check whether the converter can convert
                 if (this.responseClass ! =null && messageConverter.canRead(this.responseClass, contentType)) {
                     if (this.logger.isDebugEnabled()) {
                         String className = this.responseClass.getName();
                         this.logger.debug("Reading to [" + className + "] as \"" + contentType + "\" ");
                     }
                     //// this means that the current HttpMessageConverter can be converted, which calls read and returns
                     return messageConverter.read(this.responseClass, responseWrapper); }}}catch (HttpMessageNotReadableException | IOException var8) {
             throw new RestClientException("Error while extracting response for type [" + this.responseType + "] and content type [" + contentType + "]", var8);
         }
         // All message converters are unable to process the exception.
         throw new UnknownContentTypeException(this.responseType, contentType, response.getRawStatusCode(), response.getStatusText(), response.getHeaders(), getResponseBody(response));
     } else {
         return null; }}Copy the code

The HttpMessageConverter implementation class added to the RestTemplate constructor is stored in the messageConverters collection.

Customizing HttpMessageConverter

Find the reason, we need to solve the problem, under the use of a simple solution, namely to set MediaType MappingJackson2HttpMessageConverter can handle.

    @Bean
    public RestTemplate restTemplate(a){
        RestTemplate restTemplate = new RestTemplate();
        MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter = new MappingJackson2HttpMessageConverter();
        mappingJackson2HttpMessageConverter.setSupportedMediaTypes(Arrays.asList(MediaType.TEXT_HTML));
        restTemplate.getMessageConverters().add(mappingJackson2HttpMessageConverter);
        return  restTemplate;
    }
Copy the code

Yes, yes, it’s settled, MappingJackson2HttpMessageConverter is also an HttpMessageConverter transformation class, but he can’t handle text/HTML data, The reason is his parent AbstractHttpMessageConverter supportedMediaTypes set of type text/HTML, will be able to take, if any Through setSupportedMediaTypes can specify a new set MediaType, give him the written above leads to MappingJackson2HttpMessageConverter can only handle text/HTML types of data.

However, in order to further study, we want to directly inherited HttpMessageConverter (of course is more recommended inheritance AbstractHttpMessageConverter), before that, look what represent the exact meaning of this a few methods, can continue to write down.

public interface HttpMessageConverter<T> {
    /** * Check whether clazz is readable based on mediaType */
    boolean canRead(Class<? > clazz,@Nullable MediaType mediaType);

    /** * Check whether clazz is writable according to mediaType
    boolean canWrite(Class<? > clazz,@Nullable MediaType mediaType);

    /** * Get the supported mediaType */
    List<MediaType> getSupportedMediaTypes(a);

    /** * Bind data from the HttpInputMessage stream to clazz */
    T read(Class<? extends T> clazz, HttpInputMessage inputMessage)
			throws IOException, HttpMessageNotReadableException;

    /** * writes the t object to the HttpOutputMessage stream */
    void write(T t, @Nullable MediaType contentType, HttpOutputMessage outputMessage)
			throws IOException, HttpMessageNotWritableException;
}
Copy the code

To solve this problem, you don’t need to deal with canWrite, just canRead and read. In the canRead method, it checks whether it is text/ HTML. If it is, it will return true and Spring will call read. StreamUtils is a utility Class that comes with SpringBoot. It reads data from InputStream and returns a String. SpringBoott uses this tool class in many places, so we can use it here. Now we get the String data, we need to convert the String into the corresponding object, which may think of Gson, Fastjson, they can also be done, but also need to add additional JAR package. SpringBoot already has ObjectMapper integrated with it, so let’s borrow it.

package com.hxl.vote.config;

import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.HttpMessageNotWritableException;
import org.springframework.util.StreamUtils;

import java.io.IOException;
import java.nio.charset.Charset;
import java.util.Arrays;
import java.util.List;

public class QQHttpMessageConverter implements HttpMessageConverter<Object> {
    @Override
    public boolean canRead(Class
        aClass, MediaType mediaType) {
        if(mediaType ! =null) {
            return mediaType.isCompatibleWith(MediaType.TEXT_HTML);
        }
        return false;
    }

    @Override
    public boolean canWrite(Class
        aClass, MediaType mediaType) {
        return false;
    }

    @Override
    public List<MediaType> getSupportedMediaTypes(a) {
        return Arrays.asList(MediaType.TEXT_HTML);
    }

    @Override
    public Object read(Class
        aClass, HttpInputMessage httpInputMessage) throws IOException, HttpMessageNotReadableException {
        String json = StreamUtils.copyToString(httpInputMessage.getBody(), Charset.forName("UTF-8"));
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        return objectMapper.readValue(json, aClass);
    }

    @Override
    public void write(Object o, MediaType mediaType, HttpOutputMessage httpOutputMessage) throws IOException, HttpMessageNotWritableException {}}Copy the code

GetMessageConverters () returns an existing set of HttpMessageconverters, so we can add our own custom HttpMessageConverter.

@Bean
public RestTemplate restTemplate(a){
    RestTemplate restTemplate = new RestTemplate();
    restTemplate.getMessageConverters().add(new QQHttpMessageConverter());
    return  restTemplate;
}
Copy the code

Inheritance AbstractHttpMessageConverter

AbstractHttpMessageConverter help we enclosed a part of things, but there are some things he cannot be determined, so want to subclass implementation, use the following method, also can solve the problem of text/HTML.

public class QQHttpMessageConverter extends AbstractHttpMessageConverter<Object> {
    public QQHttpMessageConverter(a) {
        super(MediaType.TEXT_HTML);
    }
    @Override
    protected boolean supports(Class
        aClass) {
        return true;
    }
    @Override
    protected Object readInternal(Class
        aClass, HttpInputMessage httpInputMessage) throws IOException, HttpMessageNotReadableException {
        String json = StreamUtils.copyToString(httpInputMessage.getBody(), Charset.forName("UTF-8"));
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        return objectMapper.readValue(json, aClass);
    }
    @Override
    protected void writeInternal(Object o, HttpOutputMessage httpOutputMessage) throws IOException, HttpMessageNotWritableException {}}Copy the code

Inheritance MappingJackson2HttpMessageConverter

Ok, use MappingJackson2HttpMessageConverter, you just need to give him can handle MediaType, more simple.

public class QQHttpMessageConverter extends MappingJackson2HttpMessageConverter {
    public QQHttpMessageConverter(a) {
        setSupportedMediaTypes(Arrays.asList(MediaType.TEXT_HTML));
    }
Copy the code