天天看點

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架構定義了很多的消息轉換器,流程如下:

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兩個注解的原理,有興趣的可以去翻一翻源碼。