HttpMessageConverter(後續簡稱 MessageConverter)是 Spring MVC 裡的序列化 / 反序列化組件。主要的職責就是擔任 HTTP 請求/響應和 Java 之間的轉換橋梁。它本身是個介面,內部定義了 read, write, canRead, canWrite ...等不同的處理方法,還有一個 getSupportedMediaTypes,用來指定這個轉換器可以處理哪些 Accept, Content-Type 的資料格式。
跟 Spring MVC 的其他組件一樣,MessageConverter 在容器內也存在多個實例,分別對應不同資料流的轉換邏輯,且同樣也是以 List 的方式存儲,並被 RequestMappingHandlerAdapter(RMHA)持有跟調度。具體來說:當一個實體的 Adapter(假設就是 RMHA 吧)接收到一個 Http 請求之後,首先會委託內部的 HandlerMethodArgumentResolver(簡稱 Resolver)將請求體反序列化為 Java 物件。在這裡,Resolver 就會察看手邊的列表清單,用輪詢的方式嘗試將 Request Body 委託給 MessageConverter。而如果遍歷整個清單,都沒有可以執行轉換的轉換組件,Resolver 就會直接拋出 Unsupported Media Type Exception,並透過接手的組件,將響應調整為 415 的 status code 進行回傳。
說完了這個,我們再來講講 MessageConverter 的輪詢順序:在實際的情況,IoC 容器會有一個字串相關的 MessageConverter,它在響應的階段,會將資料封裝成帶有 Content-type: text/plain 的 Http response。此外,容器還會有一個專門處理 json 序列化的 MessageConverter,會將資料封裝成帶有 Content-type: application/json 的 Http response。也因為在順序上,String 的 MessageConverter 會比 json 的 MessageConverter 還要早被輪詢到。這代表如果有一個 Controller 方法長得像 public String getMember(int id) 這樣,即使我們最後將 member 的資料進行序列化之後回傳,資料在離開 Controller 之後,還是會被 String 的 MessageConverter 接手,導致 response 的 Content-type 超出原本的預期。
這個問題偶爾會出現在跟快取有關的請求上面:有時候為了追求效能,我們會把熱點資料以 JSON 字串的形式直接存在 Redis。後續當請求命中快取時,為了省下「把資料反序列化成 Java DTO、再重新序列化回 JSON」的無謂開銷,當時的開發者就會直覺地把這串快取字串 return 給前端,進而觸發了這個非預期的 Content-Type 錯誤。針對這個情況,最常見的解決方式是把回傳值從 String 調整為 ResponseEntity<String>。透過設定 ResponseEntity 的 contentType,使 String MessageConverter 不再將之設為 text/plain,進而避免相關的資料異常。此外,如果有些老舊的系統不允許調整原方法的 String 回傳,我們也可以在 @XxxMapping 的註解上加入一個 produces 參數,強制限制 Content-Type 為指定的 application/json,進而解決 text/plain 的 Content-type 錯誤。
最後,我們簡單聊聊跟 Json 序列化有關的相關內容:主流在 Spring 專案使用到的套件大概可以分成 Jackson, fastjson, 和 Gson 三種。雖然 Spring 預設使用的是 Jackson 的 JSON 解析套件(所以 MessageConverter 用的也是 Jackson 提供的),但開發者還是可以透過依賴 fastjson 和 gson,在專案內使用(或混用)這幾個不同的 json 套件,當然也可以改用這幾個套件的 MessageConverter 來取代預設的相關邏輯。我自己是比較不喜歡這樣的混合用法,雖然透過混用,我們可以同時取得不同套件在不同場景下的語法便利(或效能優勢),但各套件對 DTO 註解、日期格式的處理、或 null 值的預設行為 ...等,都是需要額外理解的相關成本。比起多學好幾套流程、對不同的 json 套件劃好職責邊界跟轉換邏輯,我更傾向從一開始就使用唯一的官方套件,並向上封裝一層 JsonUtils 工具類,達到業務邏輯跟底層依賴的相關解耦。
期待後續各位的閱讀與分享,我是 Pax,我們明天見。