1. 程式人生 > >http序列化/反序列化之HttpMessageConverter

http序列化/反序列化之HttpMessageConverter

開發十年,就只剩下這套架構體系了! >>>   

http序列化(或者叫做http編碼解碼),就是將http報文轉換為程式內部的Java類,以及將Java類轉化為二進位制流輸出給http body的過程,這樣就不用再去一個個request.getParam("xxx")來獲取引數、通過response.getWriter.write來輸出結果。使用過原生netty http的人可能對http序列化比較熟悉,springmvc中的意思跟netty中的意思一樣。http序列化是一個合格的controller框架都應該具備的功能,在很多場景下(程式碼封裝良好,controller的方法的程式碼只有呼叫service一行程式碼),也是controller的主要工作。

在springmvc中可以在controller方法的程式碼中直接寫Java類作為引數,這樣預設是通過引數名和屬性名的配對,使用request.getParam("xxx")來完成的。在前後端分離時,一般需要固定req和resp的傳輸格式,這時候通過引數名匹配並不是一個很好的選擇。我之前的工作就有使用前端所有請求都使用json/xml,後端返回的也全部使用json/xml的,簡單的就是通過@RequestBody和@ResponseBody直接完成的,這個相信使用過springmvc的都知道怎麼用。

那麼不使用json格式來傳輸資料行不行,當然可以,http雖然名字是叫文字,但是一樣可以用來傳輸二進位制資料,也就是所有格式的資料。這樣看起來很奇怪,但是在一些與非web前端進行http通訊的地方,自定義http的資料格式很常見,比如基於http的rpc,rpc為了滿足通用性、低消耗性,一般會選擇跨語言、效能好、壓縮比高的序列化格式,比如protobuf。springmvc自己就提供了protobuf的http序列化,在spring-web包中有個類叫做org.springframework.http.converter.protobuf.ProtobufHttpMessageConverter,就是用來處理protobuf格式的資料的,有興趣可以去試試。

參考ProtobufHttpMessageConverter,我們可以寫一個自己的http序列化,使用Java原生序列化讀寫物件,程式碼如下。

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.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;

import pr.study.springboot.bean.BaseBean;

public class JavaSerializationConverter extends AbstractHttpMessageConverter<Object> {
    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) {
        return BaseBean.class.isAssignableFrom(clazz);
    }

    @Override
    protected Object readInternal(Class<? extends Object> 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 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(Object 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

    @Override
    public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
        // 僅僅新增一種新的converter,不刪除預設新增的
        // 如果要刪除可以使用 converters.clear()
        // 僅僅只有一種converter時,代表請求和響應預設都是這個converter代表的mediatype
        // 推薦使用這個方法新增converter
        converters.add(new JavaSerializationConverter());
    }

//    // 新增converter的第二種方式,會刪除原來的converter
//    @Bean
//    public HttpMessageConverter<Object> javaSerializationConverter() {
//        return new JavaSerializationConverter();
//    }

//    // 新增converter的第三種方式,會刪除原來的converter
//    @Override
//    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
//        converters.add(new JavaSerializationConverter());
//    }

上面的程式碼要放在我們寫的WebMvcConfigurerAdapter的子類中,如果使用我的程式碼,那就是在pr.study.springboot.configure.mvc.SpringMvcConfigure中。推薦使用第一種方式,如果你的業務確定只有一種http序列化方式,可以使用下面的幾種,提升一些效率。

converter的程式碼裡面注意一點,構造方法中千萬要指明mediaType的型別(使用父類的構造方法是個很好的選擇),指明這個型別才有機會使用這個converter。具體就是:

  • 通過http請求中的Headers.Content-Type指定的mediaType來決定使用哪個converter(controller方法要支援consumes這種mediaType)來處理這個req的body的序列化;
  • 通過http請求中的Headers.Accept指定的mediaType來決定使用哪個converter(controller方法要支援produces這種mediaType)來處理這個req的對應的resp的body的序列化,處理成功時對應的resp返回一個屬於Accept子集的Content-Type;

json序列化使用下面的json

{"id":123,"name":"helloworld","email":"[email protected]","createTime":"2017-12-17 15:22:55"}

java序列化使用下面的資料

rO0ABXNyAB1wci5zdHVkeS5zcHJpbmdib290LmJlYW4uVXNlcrt1879rvWjlAgAESgACaWRMAApjcmVhdGVUaW1ldAAQTGphdmEvdXRpbC9EYXRlO0wABWVtYWlsdAASTGphdmEvbGFuZy9TdHJpbmc7TAAEbmFtZXEAfgACeHIAIXByLnN0dWR5LnNwcmluZ2Jvb3QuYmVhbi5CYXNlQmVhbklx6Fsr8RKpAgAAeHAAAAAAAAAAe3NyAA5qYXZhLnV0aWwuRGF0ZWhqgQFLWXQZAwAAeHB3CAAAAWBjWqyYeHQAEGhlbGxvd29ybGRAZy5jb210AApoZWxsb3dvcmxk

下面的是一些執行結果圖,對比下可以看出Accept和Content-Type對序列化反序列化的影響。

  • GET + Accept: application/x-java-serialization,resp使用java序列化返回

 

  • GET + Accept: application/json,resp使用json序列化返回

 

  • POST + Content-Type: application/x-java-serialization + Accept: application/x-java-serialization,req和resp都使用java序列化

 

  • POST + Content-Type: application/x-java-serialization + Accept: application/json,req使用java序列化,resp使用json序列化

 

  • POST + Content-Type: application/json + Accept: application/json,req和resp都使用json序列化

 

  • POST + Content-Type: application/json + Accept: application/x-java-serialization,req使用json序列化,resp使用java序列化

 

這裡為了測試,post返回的是User物件,直接返回基本型別(包裝類/BigDecimal)以及String的話,不會走普通物件序列化,直接使用通用格式返回。
實際中根據應用的req/resp應用場景,來決定controller方法的consumes和produces,忽略這兩個屬性通常情況下不會有問題,springmvc內部的mediaType匹配機制還是比較好的。


再說點其他的小內容。
springmvc預設使用的是jackson框架來處理json。jackson再實際使用中還需要進行一些配置,比如關閉null值的屬性的序列化,以及時間的序列化,要配置jackson,只需要自己注入ObjectMapper即可。如下:

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

import java.text.SimpleDateFormat;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.databind.ObjectMapper;

/**
 * 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;
    }
}

如果要使用fastjson怎麼辦(有些公司會要求這些基礎元件都使用相同的jar包,便於擴充套件維護)?跟在springmvc中方式差不多,自己配置一個FastJsonHttpMessageConverter就行。在springboot中的配置方式和上面我們寫的java序列化差不多,mvc配置類中新增下列程式碼即可:

    @Override
    public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
        // 僅僅新增一種新的converter,不刪除預設新增的
        // 如果要刪除可以使用 converters.clear()
        // 僅僅只有一種converter時,代表請求和響應預設都是這個converter代表的mediatype
        // 推薦使用這個方法新增converter
        converters.add(new JavaSerializationConverter());

        // 使用fastJson代替jackson
        FastJsonHttpMessageConverter fastJsonConverter = new FastJsonHttpMessageConverter();

        FastJsonConfig fastJsonConfig = new FastJsonConfig();
        fastJsonConfig.setSerializerFeatures(SerializerFeature.WriteMapNullValue); // 序列化null屬性
        fastJsonConfig.setDateFormat("yyyy-MM-dd HH:mm:ss"); // 預設的時間序列化格式
        fastJsonConverter.setFastJsonConfig(fastJsonConfig);

        converters.add(fastJsonConverter);
        System.err.println(converters);
    }

其他的,官方文件上面有說一個自定義JsonSerializer,這個有上面用呢?簡單說就是你可以自己指定任何物件的json序列化格式,比如時間你可以序列化成中文的一些格式,rgb顏色一般都是三個byte儲存,你可以序列化成css通用的格式。
這個功能我沒用過,不過在網上找了個例子,大家可以參考下,說的是如何在json中將rgb顏色序列化成css格式。

 

Spring已經預設包含了常用的訊息轉換器:

<
名稱 作用 讀支援MediaType 寫支援MediaType
ByteArrayHttpMessageConverter 資料與位元組陣列的相互轉換 / application/octet-stream
StringHttpMessageConverter 資料與String型別的相互轉換 text/* text/plain
FormHttpMessageConverter 表單與MultiValueMap<string, string=””>的相互轉換 application/x-www-form-urlencoded application/x-www-form-urlencoded
SourceHttpMessageConverter 資料與javax.xml.transform.Source的相互轉換 text/xml和application/xml text/xml和application/xml
MarshallingHttpMessageConverter 使用Spring的Marshaller/Unmarshaller轉換XML資料 text/xml和application/xml text/xml和application/xml
MappingJackson2HttpMessageConverter 使用Jackson的ObjectMapper轉換Json資料 application/json application/json
MappingJackson2XmlHttpMessageConverter 使用Jackson的XmlMapper轉換XML資料 application/xml application/xml  
BufferedImageHttpMessageConverter 資料與java.awt.image.BufferedImage的相互轉換 Java I/O API支援的所有型別 Java I/O API支援的所有型別