JAVA

SpringBoot HttpMessageConverter消息转换器

在构建RESTful数据服务过程中,我们定义了controller、repositories,并用一些注解修饰它们,但是到现在为止我们还没执行过对象的转换——将java实体对象转换成HTTP的数据输出流。Spring Boot底层通过HttpMessageConverters依靠Jackson库将Java实体类输出为JSON格式。当有多个转换器可用时,根据消息对象类型和需要的内容类型选择最适合的转换器使用。

在整个数据流转过程中,前端的请求报文转化为Java对象,Java对象转化为响应报文,这里就用到了消息转换器HttpMessageConverter

消息转换器的位置

消息转化器的作用

  • 将请求报文转化为Java对象
  • 将Java对象转化为响应报文

消息转化器的主要方法

  • getSupportedMediaTypes:获取支持的MediaType集合(如:text/html,text/plain,application/json)
  • canRead:判断是否能读(请求)
  • read:将请求数据进行格式转换(canRead方法返回值为true时调用)
  • canWrite:判断是否能写(响应)
  • write:将响应数据进行格式转换(canWrite方法返回值为true时调用)

SpringMVC启动时会自动配置一些HttpMessageConverter(WebMvcConfigurationSupport类的addDefaultHttpMessageConverters)方法

protected final void addDefaultHttpMessageConverters(List<HttpMessageConverter<?>> messageConverters) {
    messageConverters.add(new ByteArrayHttpMessageConverter());
    messageConverters.add(new StringHttpMessageConverter());
    messageConverters.add(new ResourceHttpMessageConverter());
    messageConverters.add(new ResourceRegionHttpMessageConverter());
    try {
        messageConverters.add(new SourceHttpMessageConverter<>());
    }
    catch (Throwable ex) {
        // Ignore when no TransformerFactory implementation is available...
    }
    messageConverters.add(new AllEncompassingFormHttpMessageConverter());
    if (romePresent) {
        messageConverters.add(new AtomFeedHttpMessageConverter());
        messageConverters.add(new RssChannelHttpMessageConverter());
    }
    if (jackson2XmlPresent) {
        Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.xml();
        if (this.applicationContext != null) {
            builder.applicationContext(this.applicationContext);
        }
        messageConverters.add(new MappingJackson2XmlHttpMessageConverter(builder.build()));
    }
    else if (jaxb2Present) {
        messageConverters.add(new Jaxb2RootElementHttpMessageConverter());
    }
    if (jackson2Present) {
        Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.json();
        if (this.applicationContext != null) {
            builder.applicationContext(this.applicationContext);
        }
        messageConverters.add(new MappingJackson2HttpMessageConverter(builder.build()));
    }
    else if (gsonPresent) {
        messageConverters.add(new GsonHttpMessageConverter());
    }
    else if (jsonbPresent) {
        messageConverters.add(new JsonbHttpMessageConverter());
    }
    if (jackson2SmilePresent) {
        Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.smile();
        if (this.applicationContext != null) {
            builder.applicationContext(this.applicationContext);
        }
        messageConverters.add(new MappingJackson2SmileHttpMessageConverter(builder.build()));
    }
    if (jackson2CborPresent) {
        Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.cbor();
        if (this.applicationContext != null) {
            builder.applicationContext(this.applicationContext);
        }
        messageConverters.add(new MappingJackson2CborHttpMessageConverter(builder.build()));
    }

部分消息转换器解析

注意事项

  • 系统有默认配置的消息转换器集合。
  • 处理过程会按集合顺序匹配合适的消息转换器,如果有合适的,就会使用该消息转换器处理(读、写),后续的消息转换器不再执行。
  • 自定义的消息转换器要想生效,必须放到集合中相同类型的消息转换器前面,原因参考第二点。
  • 思考:既然自定义的消息转换器必须放到集合中相同类型的消息转换器前面,那是否能直接改动集合中原有的消息转换器来达到自定义的效果,而不必在加一个(暂未没研究)。
  • 添加自定义消息转换器时注意默认消息转换器是否生效

例子:Spring Boot添加消息转换器HttpMessageConverter

问题

Spring Boot项目开发过程中,前后端分离的项目,前后端通过json的数据格式交互,接口采用@ResponseBody注解返回json数据,如果接口返回的数据类型是String,会导致中文乱码。

原因

因为我们的Http消息转换都是通过spring框架定义的消息转换器进行转换的,不同类型的消息有不同的消息类型转换器处理。大概如下:

  • StringHttpMessageConverter的作用:负责读取字符串格式的数据和写出二进制格式的数据(当返回值是或者接受值是String类型时,是由这个处理)
  • MappingJacksonHttpMessageConverter:负责读取和写入json格式的数据;(当返回值是对象或者List,就由这个处理)
  • ByteArrayHttpMessageConverter:负责读取二进制格式的数据和写出二进制格式的数据;
  • FormHttpMessageConverter:负责读取form提交的数据(能读取的数据格式为 application/x-www-form-urlencoded,不能读取multipart/form-data格式数据);负责写入application/x-www-from-urlencoded和multipart/form-data格式的数据;ResourceHttpMessageConverter:负责读取资源文件和写出资源文件数据;
  • SourceHttpMessageConverter:负责读取和写入 xml 中javax.xml.transform.Source定义的数据;
  • Jaxb2RootElementHttpMessageConverter:负责读取和写入xml 标签格式的数据;
  • AtomFeedHttpMessageConverter: 负责读取和写入Atom格式的数据;
  • RssChannelHttpMessageConverter: 负责读取和写入RSS格式的数据;

当我们的响应数据是string类型是,框架自动识别到消息类型(MediaType),会采用StringHttpMessageConverter进行消息转换,但是StringHttpMessageConverter默认的字符集是ISO-8859-1,这就导致了响应头中Content-Type为”xxx;charset=ISO-8859-1″。所以导致中文乱码

解决

  • @ReqeustMapping中指定produces属性:produces=”application/json;charset=UTF-8″
  • 添加消息转换器StringHttpMessageConverter,自己创建消息转换器,并制定编码集为:UTF-8
@Configuration

public class WebConfig extends WebMvcConfigurationSupport {

    @Bean
    public HttpMessageConverter<String> responseBodyConverter() {

        // 框架默认的StringHttpMessageConverter编码是ISO-8859-1,@Response注解当接口返回的是字符串时会中文乱码
        return new StringHttpMessageConverter(Charset.forName("UTF-8"));
    }

    /**
     * 消息转换器
     * @param converters
     */
    @Override
    protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {

        super.extendMessageConverters(converters);

        // 请注意顺序,因为其实底层就是用list进行存储的所有可以通过指定下标来指定顺序
        // 目前这种写法可以解决问题,但是因为我们指定的StringHttpMessageConverter
        // 框架也指定了StringHttpMessageConverter,所有我们要将自己的排在前面,否者依然无法使用自定义的消息转换器
        converters.add(0, responseBodyConverter());
    }
}

源码

Spring Boot或者说Spring或者说SpringMVC之所以能将http请求消息映射成我们controller接口的方法参数的实体,以及将响应结果转换成http消息,是因为框架Spring框架定义了很多的消息转换器,流程如下:

消息转换器都是实现了HttpMessageConverter接口的java类。HttpMessageConverter共有如下几个方法,,方法的大概意思见注释,中文注释仅供参考,详情见英文注释。

public interface HttpMessageConverter<T> {

  /**
   * Indicates whether the given class can be read by this converter.
   * @param clazz the class to test for readability
   * @param mediaType the media type to read (can be {@code null} if not specified);
   * typically the value of a {@code Content-Type} header.
   * @return {@code true} if readable; {@code false} otherwise
   */

  // 是否支持读mediaType类型的消息

  boolean canRead(Class<?> clazz, @Nullable MediaType mediaType);

  /**
   * Indicates whether the given class can be written by this converter.
   * @param clazz the class to test for writability
   * @param mediaType the media type to write (can be {@code null} if not specified);
   * typically the value of an {@code Accept} header.
   * @return {@code true} if writable; {@code false} otherwise
   */

  // 是否支持写mediaType类型的消息

  boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType);

  /**
   * Return the list of {@link MediaType} objects supported by this converter.
   * @return the list of supported media types, potentially an immutable copy
   */

  // 支持消息类型集合

  List<MediaType> getSupportedMediaTypes();

  /**
   * Read an object of the given type from the given input message, and returns it.
   * @param clazz the type of object to return. This type must have previously been passed to the
   * {@link #canRead canRead} method of this interface, which must have returned {@code true}.
   * @param inputMessage the HTTP input message to read from
   * @return the converted object
   * @throws IOException in case of I/O errors
   * @throws HttpMessageNotReadableException in case of conversion errors
   */

  // 具体实现读,这里可以修改我们的请求消息

  T read(Class<? extends T> clazz, HttpInputMessage inputMessage)

      throws IOException, HttpMessageNotReadableException;

  /**

   * Write an given object to the given output message.
   * @param t the object to write to the output message. The type of this object must have previously been
   * passed to the {@link #canWrite canWrite} method of this interface, which must have returned {@code true}.
   * @param contentType the content type to use when writing. May be {@code null} to indicate that the
   * default content type of the converter must be used. If not {@code null}, this media type must have
   * previously been passed to the {@link #canWrite canWrite} method of this interface, which must have
   * returned {@code true}.
   * @param outputMessage the message to write to
   * @throws IOException in case of I/O errors
   * @throws HttpMessageNotWritableException in case of conversion errors
   */

  // 具体实现写,可以修改我们的响应消息

  void write(T t, @Nullable MediaType contentType, HttpOutputMessage outputMessage)

      throws IOException, HttpMessageNotWritableException;

}

具体有如下几种实现:

传统的业务接口代码常用的两种Http消息转换器有两种一种是字符串转换器一种是JSON转换器,分别对应

StringHttpMessageConverter和MappingJackson2HttpMessageConverter。

StringHttpMessageConverter继承AbstractHttpMessageConverter<string>,

AbstractHttpMessageConverter<string>实现HttpMessageConverter<T>如上图和上面的代码

public class StringHttpMessageConverter extends AbstractHttpMessageConverter<String> {

    // ...... 

} 

MappingJackson2HttpMessageConverter和HttpMessageConverter的关系就比较深一些,直接上图:

public class MappingJackson2HttpMessageConverter extends AbstractJackson2HttpMessageConverter {

    // ......

}

如果我们要添加自己的消息转换器到框架中,那么我们就应该知道消息转换器是什么时候在哪里被创建的。

消息转换器是在项目启动的时候通过WebMvcConfigurationSupport进行加载,当getMessageConverters被调用的时候会通过configureMessageConverters、addDefaultHttpMessageConverters和extendMessageConverters三个方法进行初始话消息转换器。生成的消息转换器放在 List<HttpMessageConverter<?>> messageConverters集合中。

系统默认加载的消息转换器就是在addDefaultHttpMessageConverters方法中加载的。

public class WebMvcConfigurationSupport implements ApplicationContextAware, ServletContextAware {

  // ......
  // 初始话消息转换器集合
  protected final List<HttpMessageConverter<?>> getMessageConverters() {

        if (this.messageConverters == null) {
            this.messageConverters = new ArrayList<>();

      // 1、加载消息转换器
            configureMessageConverters(this.messageConverters);

            if (this.messageConverters.isEmpty()) {

        // 2、如果消息转换器集合为空那么久系统默认加载
                addDefaultHttpMessageConverters(this.messageConverters);

            }

      // 3、扩展开发者自己的加载器
            extendMessageConverters(this.messageConverters);

        }
        return this.messageConverters;
    }

  protected final void addDefaultHttpMessageConverters(List<HttpMessageConverter<?>> messageConverters) {

        messageConverters.add(new ByteArrayHttpMessageConverter());
        messageConverters.add(new StringHttpMessageConverter());
        messageConverters.add(new ResourceHttpMessageConverter());
        messageConverters.add(new ResourceRegionHttpMessageConverter());

        try {
            messageConverters.add(new SourceHttpMessageConverter<>());
        }

        catch (Throwable ex) {
            // Ignore when no TransformerFactory implementation is available...
        }

        messageConverters.add(new AllEncompassingFormHttpMessageConverter());


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


        if (jackson2XmlPresent) {
            Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.xml();
            if (this.applicationContext != null) {
                builder.applicationContext(this.applicationContext);
            }

            messageConverters.add(new MappingJackson2XmlHttpMessageConverter(builder.build()));
        }

        else if (jaxb2Present) {
            messageConverters.add(new Jaxb2RootElementHttpMessageConverter());
        }


        if (jackson2Present) {

            Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.json();

            if (this.applicationContext != null) {
                builder.applicationContext(this.applicationContext);
            }

            messageConverters.add(new MappingJackson2HttpMessageConverter(builder.build()));

        }

        else if (gsonPresent) {
            messageConverters.add(new GsonHttpMessageConverter());
        }

        else if (jsonbPresent) {
            messageConverters.add(new JsonbHttpMessageConverter());
        }


        if (jackson2SmilePresent) {
            Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.smile();
            if (this.applicationContext != null) {
                builder.applicationContext(this.applicationContext);
            }
            messageConverters.add(new MappingJackson2SmileHttpMessageConverter(builder.build()));
        }

        if (jackson2CborPresent) {
            Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.cbor();
            if (this.applicationContext != null) {
                builder.applicationContext(this.applicationContext);
            }
            messageConverters.add(new MappingJackson2CborHttpMessageConverter(builder.build()));
        }
    }

  // ......
}

下图是我们自己添加了一个消息转换器后消息转换器的集合和框架默认的消息转换器的集合对比

根据以上所述,知道了消息转换器的加载顺,所有我们可以通过继承WebMvcConfigurationSupport类,重extendMessageConverters方法实现添加自己的消息转换器。