本文主要是介绍springboot学习——使用HttpMessageConverter进行http序列化和反序列化,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
本文转发自:https://segmentfault.com/a/1190000012658289
对象的序列化/反序列化大家应该都比较熟悉:序列化就是将object转化为可以传输的二进制,反序列化就是将二进制转化为程序内部的对象。序列化/反序列化主要体现在程序I/O这个过程中,包括网络I/O和磁盘I/O。
那么什么是http序列化和反序列化呢?
在使用springmvc时,我们经常会这样写:
@RestController
@RequestMapping("/users")
public class UserController {@Autowiredprivate UserService userService;@GetMapping("/{id}")public User getUserById(@PathVariable long id) {return userService.getUserById(id);}@PostMappingpublic 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。
@Beanpublic 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)的类型,指明这个类型才会使用这个convertersuper(new MediaType("application", "x-java-serialization", Charset.forName("UTF-8")));}@Overrideprotected boolean supports(Class<?> clazz) {// 使用Serializable,这里可以直接返回true// 使用object,这里还要加上Serializable接口实现类判断// 根据自己的业务需求加上其他判断return true;}@Overrideprotected 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;}}@Overrideprotected 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的规则时,优先使用我们这个@Beanpublic JavaSerializationConverter javaSerializationConverter() {return new JavaSerializationConverter();}// 添加converter的第二种方式// 通常在只有一个自定义WebMvcConfigurerAdapter时,会把这个方法里面添加的converter(s)依次放在最高优先级(List的头部)// 虽然第一种方式的代码先执行,但是bean的添加比这种方式晚,所以方式二的优先级 大于 方式一@Overridepublic 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的头部)@Overridepublic 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]
演示结果如下,包含了一个接口多种报文格式支持的演示:
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 {@Beanpublic 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,这样霸道,什么格式都要管,为哪般!?
这篇关于springboot学习——使用HttpMessageConverter进行http序列化和反序列化的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!