1. 程式人生 > >springboot學習(三)————使用HttpMessageConverter進行http序列化和反序列化

springboot學習(三)————使用HttpMessageConverter進行http序列化和反序列化

http 同時 服務 基本上 err cat rod nio decode

以下內容,如有問題,煩請指出,謝謝!


對象的序列化/反序列化大家應該都比較熟悉:序列化就是將object轉化為可以傳輸的二進制,反序列化就是將二進制轉化為程序內部的對象。序列化/反序列化主要體現在程序I/O這個過程中,包括網絡I/O和磁盤I/O。

那麽什麽是http序列化和反序列化呢?

在使用springmvc時,我們經常會這樣寫:

@RestController
@RequestMapping("/users")
public class UserController {
    @Autowired
    private UserService userService;

    @GetMapping
("/{id}") public User getUserById(@PathVariable long id) { return userService.getUserById(id); } @PostMapping public User createUser(@RequestBody User user) { System.err.println("create an user: " + user); return user; } }

@RestController中有@ResponseBody,可以幫我們把User序列化到resp.body中。@RequestBody可以幫我們把req.body的內容轉化為User對象。如果是開發Web應用,一般這兩個註解對應的就是Json序列化和反序列化的操作。這裏實際上已經體現了Http序列化/反序列化這個過程,只不過和普通的對象序列化有些不一樣,Http序列化/反序列化的層次更高,屬於一種Object2Object之間的轉換。

有過Netty使用經驗的對這個應該比較了解,Netty中的Decoder和Encoder就有兩種基本層次,層次低的一種是Byte <---> Message,二進制與程序內部消息對象之間的轉換,就是常見的序列化/反序列化;另外一種是 Message <---> Message,程序內部對象之間的轉換,比較高層次的序列化/反序列化。

Http協議的處理過程,TCP字節流 <---> HttpRequest/HttpResponse <---> 內部對象,就涉及這兩種序列化。在springmvc中第一步已經由Servlet容器(tomcat等等)幫我們處理了,第二步則主要由框架幫我們處理。上面所說的Http序列化/反序列化就是指的這第二個步驟,它是controller層框架的核心功能之一,有了這個功能,就能大大減少代碼量,讓controller的邏輯更簡潔清晰,就像上面示意的代碼那樣,方法中只有一行代碼。

spirngmvc進行第二步操作,也就是Http序列化和反序列化的核心是HttpMessageConverter。用過老版本springmvc的可能有些印象,那時候需要在xml配置文件中註入MappingJackson2HttpMessageConverter這個類型的bean,告訴springmvc我們需要進行Json格式的轉換,它就是HttpMessageConverter的一種實現。

技術分享圖片

在Web開發中我們經常使用Json相關框架來進行第二步操作,這是因為Web應用中主要開發語言是js,對Json支持非常好。但是Json也有很大的缺點,大多數Json框架對循環引用支持不夠好,並且Json報文體積通常比較大,相比一些二進制序列化更耗費流量。很多移動應用也使用Http進行通信,因為這是在手機app中,Json格式報文並沒有什麽特別的優勢。這種情況下我們可能會需要一些性能更好,體積更小的序列化框架,比如Protobuf等等。

當前的SpringMVC 4.3版本已經集成了Protobuf的Converter,org.springframework.http.converter.protobuf.ProtobufHttpMessageConverter,使用這個類可以進行Protobuf中的Message類和http報文之間的轉換。使用方式很簡單,先依賴Protobuf相關的jar,代碼中直接@Bean就行,像下面這樣,springboot會自動註入並添加這種Converter。

    @Bean
    public ProtobufHttpMessageConverter protobufHttpMessageConverter() {
        return new ProtobufHttpMessageConverter();
    }

這裏就不演示protobuf相關的內容了。

另外有很重要的一點需要說明一下,springmvc可以同時配置多個Converter,根據一定的規則(主要是Content-type、Accept、controller方法的consumes/produces、Converter.mediaType以及Converter的排列順序這四個屬性)來選擇到底是使用哪一個,這使得springmvc能夠一個接口支持多種報文格式。這個規則的具體內容,下一篇再詳細說明。


下面重點說下如何自定義一個HttpMessageConverter,就用Java原生序列化為例,叫作JavaSerializationConverter,基本仿照ProtobufHttpMessageConverter來寫。

首先繼承AbstractHttpMessageConverter,泛型類這裏有幾種方式可以選擇:

  • 最廣的可以選擇Object,不過Object並不都是可以序列化的,但是可以在覆蓋的supports方法中進一步控制,因此選擇Object是可以的
  • 最符合的是Serializable,既完美滿足泛型定義,本身也是個Java序列化/反序列化的充要條件
  • 自定義的基類Bean,有些技術規範要求自己代碼中的所有bean都繼承自同一個自定義的基類BaseBean,這樣可以在Serializable的基礎上再進一步控制,滿足自己的業務要求

這裏選擇Serializable作為泛型基類。

其次是選擇一個MediaType,使得springmvc能夠根據Accept和Content-type唯一確定是要使用JavaSerializationConverter,所以這個MediaType不能是通用的text/plain、application/json、*/*這種,得特殊一點,這裏就用application/x-java-serialization;charset=UTF-8。因為Java序列化是二進制數據,charset不是必須的,但是MediaType的構造方法中需要指定一個charset,這裏就用UTF-8。

最後,二進制在電腦上不是可以直接拷貝的內容,為了方便測試,使用Base64再處理一遍,這樣就顯示成正常文本了,便於測試。

整個代碼如下:

package pr.study.springboot.configure.mvc.converter;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.nio.charset.Charset;
import java.util.Base64;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.AbstractHttpMessageConverter;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.HttpMessageNotWritableException;
import org.springframework.util.StreamUtils;

public class JavaSerializationConverter extends AbstractHttpMessageConverter<Serializable> {
    private Logger LOGGER = LoggerFactory.getLogger(JavaSerializationConverter.class);

    public JavaSerializationConverter() {
        // 構造方法中指明consumes(req)和produces(resp)的類型,指明這個類型才會使用這個converter
        super(new MediaType("application", "x-java-serialization", Charset.forName("UTF-8")));
    }

    @Override
    protected boolean supports(Class<?> clazz) {
        // 使用Serializable,這裏可以直接返回true
        // 使用object,這裏還要加上Serializable接口實現類判斷
        // 根據自己的業務需求加上其他判斷
        return true;
    }

    @Override
    protected Serializable readInternal(Class<? extends Serializable> clazz, HttpInputMessage inputMessage)
            throws IOException, HttpMessageNotReadableException {
        byte[] bytes = StreamUtils.copyToByteArray(inputMessage.getBody());
        // base64使得二進制數據可視化,便於測試
        ByteArrayInputStream bytesInput = new ByteArrayInputStream(Base64.getDecoder().decode(bytes));
        ObjectInputStream objectInput = new ObjectInputStream(bytesInput);
        try {
            return (Serializable) objectInput.readObject();
        } catch (ClassNotFoundException e) {
            LOGGER.error("exception when java deserialize, the input is:{}", new String(bytes, "UTF-8"), e);
            return null;
        }
    }

    @Override
    protected void writeInternal(Serializable t, HttpOutputMessage outputMessage)
            throws IOException, HttpMessageNotWritableException {
        ByteArrayOutputStream bytesOutput = new ByteArrayOutputStream();
        ObjectOutputStream objectOutput = new ObjectOutputStream(bytesOutput);
        objectOutput.writeObject(t);
        // base64使得二進制數據可視化,便於測試
        outputMessage.getBody().write(Base64.getEncoder().encode(bytesOutput.toByteArray()));
    }

}

添加一個converter的方式有三種,代碼以及說明如下:

    // 添加converter的第一種方式,代碼很簡單,也是推薦的方式
    // 這樣做springboot會把我們自定義的converter放在順序上的最高優先級(List的頭部)
    // 即有多個converter都滿足Accpet/ContentType/MediaType的規則時,優先使用我們這個
    @Bean
    public JavaSerializationConverter javaSerializationConverter() {
        return new JavaSerializationConverter();
    }

    // 添加converter的第二種方式
    // 通常在只有一個自定義WebMvcConfigurerAdapter時,會把這個方法裏面添加的converter(s)依次放在最高優先級(List的頭部)
    // 雖然第一種方式的代碼先執行,但是bean的添加比這種方式晚,所以方式二的優先級 大於 方式一
    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        // add方法可以指定順序,有多個自定義的WebMvcConfigurerAdapter時,可以改變相互之間的順序
        // 但是都在springmvc內置的converter前面
        converters.add(new JavaSerializationConverter());
    }

    // 添加converter的第三種方式
    // 同一個WebMvcConfigurerAdapter中的configureMessageConverters方法先於extendMessageConverters方法執行
    // 可以理解為是三種方式中最後執行的一種,不過這裏可以通過add指定順序來調整優先級,也可以使用remove/clear來刪除converter,功能強大
    // 使用converters.add(xxx)會放在最低優先級(List的尾部)
    // 使用converters.add(0,xxx)會放在最高優先級(List的頭部)
    @Override
    public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
        converters.add(new JavaSerializationConverter());
    }

使用下面的數據演示:

// java序列化
rO0ABXNyAB1wci5zdHVkeS5zcHJpbmdib290LmJlYW4uVXNlcrt1879rvWjlAgAESgACaWRMAApjcmVhdGVUaW1ldAAQTGphdmEvdXRpbC9EYXRlO0wABWVtYWlsdAASTGphdmEvbGFuZy9TdHJpbmc7TAAEbmFtZXEAfgACeHIAIXByLnN0dWR5LnNwcmluZ2Jvb3QuYmVhbi5CYXNlQmVhbklx6Fsr8RKpAgAAeHAAAAAAAAAAe3NyAA5qYXZhLnV0aWwuRGF0ZWhqgQFLWXQZAwAAeHB3CAAAAWCs8ufxeHQAEGhlbGxvd29ybGRAZy5jb210AApoZWxsb3dvcmxk

// json
{"id":123,"name":"helloworld","email":"[email protected]","createTime":"2017-12-31 22:21:28"}

// 對應的user.toString()
User[id=123, name=helloworld, [email protected], createTime=Sun Dec 31 22:21:28 CST 2017]

演示結果如下,包含了一個接口多種報文格式支持的演示:

1、請求是 GET + Accept: application/x-java-serialization,返回的是 Content-Type: application/x-java-serialization;charset=UTF-8 的Java序列化格式的報文

技術分享圖片

2、請求是 GET + Accept: application/json,返回的是 Content-Type: application/json;charset=UTF-8 的json格式報文

技術分享圖片

3、請求是 POST + Accept: application/x-java-serialization + Content-Type: application/x-java-serialization,返回的是 Content-Type: application/x-java-serialization;charset=UTF-8的Java序列化格式的報文

技術分享圖片

4、請求是 POST + Accept: application/json + Content-Type: application/x-java-serialization,返回的是 Content-Type: application/json;charset=UTF-8 的json格式報文

技術分享圖片

5、請求是 POST + Accept: application/json + Content-Type: application/json,返回的是 Content-Type: application/json;charset=UTF-8 的json格式報文

技術分享圖片

6、請求是 POST + Accept: application/x-java-serialization + Content-Type: application/json,返回的是 Content-Type: application/x-java-serialization;charset=UTF-8的Java序列化格式的報文

技術分享圖片


下面再說些其他的有關Http序列化/反序列化的內容.
1、jackson配置
使用Jackson時,一般我們都會配置下ObjectMapper,常見的兩個是時間序列化格式,以及是否序列化null值。使用springboot時,因為Jackson是內置加載的,那麽如何配置我們想要的的Jackson屬性呢?最賤的的方式,那就是自己註入一個ObjectMapper實例,這樣spring內所有通過依賴註入使用ObjectMapper的地方,都會優先使用我們自己註入的那個,JacksonConverter也不例外。

/**
 * jackson的核心是ObjectMapper,在這裏配置ObjectMapper來控制springboot使用的jackson的某些功能
 */
@Configuration
public class MyObjectMpper {

    @Bean
    public ObjectMapper getObjectMapper() {
        ObjectMapper mapper = new ObjectMapper();
        mapper.setSerializationInclusion(Include.NON_NULL); // 不序列化null的屬性
        mapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")); // 默認的時間序列化格式
        return mapper;
    }
}

2、控制Json中某些屬性的序列化方式
官方文檔中說了個Custom JSON Serializers and Deserializers,我也沒想到怎麽用這個,後來網上發現了個比較好的例子,說的是rgb顏色的序列化。web頁面需要的是css格式的rgb顏色,服務的提供的可能是三個獨立的byte型數字,這時候就需要改變顏色屬性的json序列化/反序列化方式。具體可以看看這裏。

3、FastJson配置
可能某些時候需要使用FastJson,這時候該如何配置呢?基本上和springmvc xml配置差不多,註入一個FastJsonHttpMessageConverter就行了。最簡單的就是上面的配置JavaSerializationConverter的方式一,方式二和方式三也都行。
不過會有奇怪的問題出現(使用@JSONField(serialize=false, deserialize=false)註解createTime,用以區分FastJson和Jackson):
假如你把FastJson配置為優先級最高的,並且同時配置上JavaSerializationConverter,你會發現JavaSerializationConverter不管用了,請求是 GET + Accept: application/x-java-serialization,返回是 Content-Type: application/x-java-serialization;charset=UTF-8;,但是實際內容是json格式的,如下。

技術分享圖片

假如你把FastJson配置為優先級最低的,別的不管,你以為得到會是Jackson序列化後的結果。但實際上,你用瀏覽器直接敲得到的是FastJson的,用上面的 GET 的 fiddler結果是jackson的;

詳細原因在下一篇講解converter匹配規則時說。

這裏說下原因中重要且值得吐槽的一點,那就是FastJsonHttpMessageConverter默認註冊的MediaType的 */*,然後就有了上面的 請求是 GET + Accept: application/x-java-serialization,返回是 Content-Type: application/x-java-serialization;charset=UTF-8;,但是實際內容是json格式的,這種掛羊頭賣狗肉的行為,明著違反HTTP協議的規範。

這個代碼設計真是差,json框架就該只管json,這樣霸道,什麽格式都要管,為哪般!?


相關代碼:
https://gitee.com/page12/study-springboot/tree/springboot-3
https://github.com/page12/study-springboot/tree/springboot-3

springboot學習(三)————使用HttpMessageConverter進行http序列化和反序列化