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支援的所有型別 | <