如下內容,若有問題,煩請指出,謝謝!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
在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,泛型類這裏有幾種方式能夠選擇:
這裏選擇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