Spring Boot之MessageConverter原理及自定义消息转换器

1,976 阅读6分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第17天,点击查看活动详情

前言

在开发的过程当中,前后端进行数据交互的时候,我们可以通过jsonxml等格式进行数据传递,但是我们在编写后端接口的时候可以通过Bean对象或者Map、String、Integer、Long等类型作为接口的参数进行接收前端传递的参数。其实在这个转换的过程中主要是Spring MVC在进行处理,Spring MVC是通过各种MessageConverter进行数据的处理。

前端发起HTTP请求,Http消息转换都是通过Spring MVC框架定义的消息转换器进行转换的,不同类型的消息有不同的消息类型转换器处理。

  • 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格式的数据;

数据转换流程

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

image.png

顶级接口HttpMessageConverter

消息转换器都是实现了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;
}

HttpMessageConverter实现类

具体有如下几种实现:之所以有图和代码,是方便读者看见的时候如果想看源码可以方便粘贴代码到开发工具中(如idea)进行搜索。

public abstract class AbstractHttpMessageConverter<T> implements HttpMessageConverter<T>{
  // ......
}
public class FormHttpMessageConverter implements HttpMessageConverter<MultiValueMap<String, ?>> {
  // ......
}
public class BufferedImageHttpMessageConverter implements HttpMessageConverter<BufferedImage>
  // ......
}
public interface GenericHttpMessageConverter<T> extends HttpMessageConverter<T> {
  // ...... 
}

查看Diagrams图

到idea开发工具中,双击shift,输入HttpMessageConverter进行搜索。

image.png

光标定位到类名称上,不用选中。ctrl+h查看继承关系。

image.png

也可以右键查看Diagrams结构。

image.png

如果只有接口可以选中接口点击右键,选择Show Implementations后。

image.png

可以点击自己感兴趣的类,也可以ctrl+A然后enter

image.png

字符串消息转换器

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

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

public class StringHttpMessageConverter extends AbstractHttpMessageConverter<String> {
    // ...... 
} 
public class MappingJackson2HttpMessageConverter extends AbstractJackson2HttpMessageConverter {
    // ......
}

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

image.png

自定义消息类型转换器

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

消息转换器是在项目启动的时候通过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()));
        }
    }
  // ......
}

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

image.png

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

HttpMessageConverter的调用是RequestResponseBodyMethodProcessor类的解析请求参数的方法resolveArgument()和处理返回值的方法handleReturnValue()中进行调用的。这是关于@RequestBody@ResponseBody两个注解的原理,有兴趣的可以去翻一翻源码。