且构网

分享程序员开发的那些事...
且构网 - 分享程序员编程开发的那些事

Spring Boot添加消息转换器HttpMessageConverter

更新时间:2022-08-13 23:22:12

问题

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框架定义了很多的消息转换器,流程如下:

Spring Boot添加消息转换器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;
}

具体有如下几种实现:之所以有图和代码,是方便读者看见的时候如果想看源码可以方便粘贴代码到开发工具中(如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> {
  // ...... 
}

Spring Boot添加消息转换器HttpMessageConverter

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

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

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

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

Spring Boot添加消息转换器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()));
        }
    }
  // ......
}

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

Spring Boot添加消息转换器HttpMessageConverter

小结

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

调用

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