以下内容,如有问题,烦请指出,谢谢!
对象的序列化/反序列化大家应该都比较熟悉:序列化就是将object转化为可以传输的二进制,反序列化就是将二进制转化为程序内部的对象。序列化/反序列化主要体现在程序I/O这个过程中,包括网络I/O和磁盘I/O。
那么什么是http序列化和反序列化呢?
在使用springmvc时,我们经常会这样写:
@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之间的转换。
有过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。
@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 {
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> 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> 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 {
@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;
}
}
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,这样霸道,什么格式都要管,为哪般!?