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

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


對象的序列化/反序列化你們應該都比較熟悉:序列化就是將object轉化爲能夠傳輸的二進制,反序列化就是將二進制轉化爲程序內部的對象。序列化/反序列化主要體如今程序I/O這個過程當中,包括網絡I/O和磁盤I/O。html

那麼什麼是http序列化和反序列化呢?java

在使用springmvc時,咱們常常會這樣寫:git

@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之間的轉換。github

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

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

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

springmvc消息轉換過程

在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。tomcat

@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":"helloworld@g.com","createTime":"2017-12-31 22:21:28"}

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

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

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

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

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

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

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

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


下面再說些其餘的有關Http序列化/反序列化的內容.
一、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;
    }
}

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

三、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

相關文章
相關標籤/搜索