一、认识序列化和反序列化
Serialization(序列化)是一种将对象以一连串的字节描述的过程;deserialization(反序列化)是一种将这些字节重建成一个对象的过程。将程序中的对象,放入文件中保存就是序列化,将文件中的字节码重新转成对象就是反序列化。
二、为什么要实现序列化和反序列化
- 我们创建的 Java 对象被存储在 Java 堆中,当程序运行结束后,这些对象会被 JVM 回收。但在现实的应用中,可能会要求在程序运行结束之后还能读取这些对象,并在以后检索数据,这时就需要用到序列化。
- 当两个进程进行远程通信时,可以相互发送各种类型的数据,包括文本、图片、音频、视频等, 而这些数据都会以二进制序列的形式在网络上传送。而 java 是面向对象的开发方式,一切都是 java 对象,想要实现 java 对象的网络传输,就可以使用序列化和反序列化来实现。发送方将需要发送的 Java 对象序列化转换为字节序列,然后在网络上传送;接收方接收到字符序列后,使用反序列化从字节序列中恢复出 Java 对象。
总结,在网络中数据的传输必须是序列化形式来进行的。
三、序列化和反序列化的实现
1、JDK 类库提供的序列化 API
- java.io.ObjectOutputStream:表示对象输出流
它的 writeObject(Object obj) 方法可以对参数指定的 obj 对象进行序列化,把得到的字节序列写到一个目标输出流中。
- java.io.ObjectInputStream:表示对象输入流
它的 readObject() 方法从源输入流中读取字节序列,再把它们反序列化成为一个对象,并将其返回。
2、实现序列化的要求
只有实现了 Serializable 或 Externalizable 接口的类的对象才能被序列化,否则抛出异常。
public class SerializableTest {
public static void main(String[] args) throws IOException, ClassNotFoundException {
serializeStudent();
deserializeStudent();
}
// JDK 类库中序列化的步骤
static void serializeStudent() throws IOException, ClassNotFoundException {
// 创建一个对象输出流,它可以包装一个其他类型的目标输出流,如文件输出流:
FileOutputStream fos = new FileOutputStream("F:\\HaC.txt");
ObjectOutputStream oos = new ObjectOutputStream(fos);
Student student1 = new Student("HaC", "HelloCoder", 30);
// 通过对象输出流的 writeObject() 方法写对象
oos.writeObject(student1);
oos.flush();
System.out.println("Student 对象序列化成功!");
oos.close();
}
// JDK 类库中反序列化的步骤
static void deserializeStudent() throws IOException, ClassNotFoundException {
// 创建一个对象输入流,它可以包装一个其它类型输入流,如文件输入流:
FileInputStream fis = new FileInputStream("F:\\HaC.txt");
ObjectInputStream ois = new ObjectInputStream(fis);
// 通过对象输出流的 readObject() 方法读取对象:
Student student2 = (Student) ois.readObject();
System.out.println(student2.getUserName() + " " +
student2.getPassword() + " " + student2.getYear());
System.out.println("Student 对象反序列化成功!");
}
}
@Data
@AllArgsConstructor
class Student implements Serializable {
private static final long serialVersionUID = 3608451818006447637L;
private String userName;
private String password;
private String year;
}
可以看到生成了一个打开是乱码的二进制文件:
其实这个例子就是序列化和反序列化的一个小过程,JVM 通过序列化把对象写到文件,再通过反序列化从文件中读取数据,把数据转成一个对象。
看到控制台输出也是正常的:
Student 对象序列化成功!
HaC HelloCoder 30
Student 对象反序列化成功!
四、serialVersionUID 的作用
虚拟机是否允许反序列化,不仅取决于类路径和功能代码是否一致,一个非常重要的一点是两个类的序列化 ID 是否一致,这个所谓的序列化 ID,就是我们在代码中定义的 serialVersionUID。
serialVersionUID 得生成方法:
- private static final long serialVersionUID = 1L;
- 根据包名,类名,继承关系,非私有的方法和属性,以及参数,返回值等诸多因子计算得出的,极度复杂生成的一个 64 位的哈希字段。基本上计算出来的这个值是唯一的。比如:private static final long serialVersionUID = xxxxL。显示声明 serialVersionUID 可以避免对象不一致
- 如果没有显示的定义 serialVersionUID 变量的时候,JAVA 序列化机制会根据 Class 自动生成一个 serialVersionUID 作序列化版本比较用,这种情况下,如果 Class 文件 (类名,方法名等) 没有发生变化(增加空格,换行,增加注释等等),就算再编译多次,serialVersionUID 也不会变化的。
五、SpringBoot 中的序列化和反序列化
在项目开发中,我们的类并没有实现 Serializable 接口,实际上这是 Spring 框架帮我们做了一些事情,Spring 并不是直接把 User 对象进行网络传输,而是把 User 对象转换成 json 格式的字符串,然后再进行传输的,而 String 类实现了 Serializable 接口并且显示指定了 serializableUID
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
private final char value[];
private int hash; // Default to 0
private static final long serialVersionUID = -6849794470754667710L;
Json 是一种轻量级的文本数据交换格式,在 Json 字符串中 {} 用来表示对象,[ ] 用来表示列表,数据以 key:value 的形式存放,如:
{
"name":"zhangsan",
"age":"22",
"course":["java","python"]
}
在 SpringBoot 中,想要一个接口接收 Json 格式的数据并返回 Json 格式的数据,前端将 http 请求头 “Accept” 设置为 “application/json”,Content-Type 为 "application/json"
中间件只需要在 Controller 类中做如下定义:
@RestController
@RequestMapping("/equity")
public class EquityExpose {
@PostMapping("/list")
@ApiOperation(value = "权益列表", notes = "权益列表")
public ApiResultResponse<GetEquityResponse>> list(@RequestBody GetEquityRequest request) {
return service.list(request);
}
}
在 Controller 中使用 @ResponseBody 注解即可返回 Json 格式的数据,而 @RestController 注解包含了 @ResponseBody 注解,所以默认情况下,@RestController 即可将返回的数据结构转换成 Json 格式。
这些注解之所以可以进行 Json 与 JavaBean 之间的相互转换,就是因为 HttpMessageConverter 发挥着作用。
org.springframework.http.converter.HttpMessageConverter 是一个策略接口,是 Http request 请求和 response 响应的转换器,该接口只有五个方法,它的 canRead() 方法返回 true,然后它的 read() 方法会从请求中读出请求参数,绑定到 readString() 方法的 string 变量中。
当 SpringMVC 执行 readString 方法后,由于返回值标识了 @ResponseBody,SpringMVC 将使用 StringHttpMessageConverter 的 write() 方法,将结果作为 String 值写入响应报文,当然,此时 canWrite() 方法返回 true。
public interface HttpMessageConverter<T> {
//判断当前转换器是否可以解析前端传来的数据
boolean canRead(Class<?> clazz, MediaType mediaType);
//判断当前转换器是否可以将后端数据解析为前端需要的格式
boolean canWrite(Class<?> clazz, MediaType mediaType);
//获取当前转换器可以解析的数据类型
List<MediaType> getSupportedMediaTypes();
//读取前端传来的数据
T read(Class<? extends T> clazz, HttpInputMessage inputMessage)
throws IOException, HttpMessageNotReadableException;
//将后台数据转换,返回给前端
void write(T t, MediaType contentType, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException;
}
流程图如下:
前端发来请求后,先调用 HttpInputMessage 从输入流中获取 Json 字符串,然后在 HttpMessageConverter 中把 Json 转换为接口需要的形参类型。
六、定制化
当出现特定的需求时,比如:此时需要自定义自己的消息转换器,可以使用 Spring 或者第三方提供的 HttpMessageConverter 如(FastJson,Gson, Jackson)
问题引入字符类型字段为 null 时,输出为 “”,而不是 null
1、引入依赖
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.70</version>
</dependency>
2、对 FastJsonHttpMessageConverter 进行配置
@Configuration
public class MyWebmvcConfiguration implements WebMvcConfigurer {
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
FastJsonHttpMessageConverter fjc = new FastJsonHttpMessageConverter();
FastJsonConfig fj = new FastJsonConfig();
//字符类型字段如果为null,则输出"",而非null
fj.setSerializerFeatures(SerializerFeature.WriteNullStringAsEmpty);
fjc.setFastJsonConfig(fj);
// 将该自定义转换器排在第一个,使其生效
converters.add(0, fjc);
}
}
3、SerializerFeature 配置属性的解释
属性名称 | 解释 |
QuoteFieldNames | 输出key时是否使用双引号,默认为true |
UseSingleQuotes | 使用单引号而不是双引号,默认为false |
WriteMapNullValue | 是否输出值为null的字段,默认为false。应用场景:前端必须需要所有字段 |
UseISO8601DateFormat | Date使用ISO8601格式输出,默认为false |
WriteNullListAsEmpty | List字段如果为null,输出为[],而不是null |
WriteNullStringAsEmpty | 字符类型字段如果为null,输出为"",而不是null |
WriteNullNumberAsZero | 数值字段如果为null,输出为0,而非null |
WriteNullBooleanAsFalse | Boolean字段如果为null,输出为false,而非null |
SkipTransientField | 如果是true,类中的Get方法对应的Field是transient,序列化时将会被忽略。默认为true |
SortField | 按字段名称排序后输出。默认为false |
配置前:如果字符串类型为 null 的话,输出是为 null
{
"id": "11",
"name": null
}
配置后:如果字符串类型为 null 的话,输出是为 ""
{
"id": "11",
"name": null
}
七、序列化及反序列化相关知识
- 在Java中,只要一个类实现了 java.io.Serializable 接口,那么它就可以被序列化。
- 通过 ObjectOutputStream 和 ObjectInputStream 对对象进行序列化及反序列化。
- 虚拟机是否允许反序列化,不仅取决于类路径和功能代码是否一致,一个非常重要的一点是两个类的序列化 ID 是否一致(就是 private static final long serialVersionUID )。
- 序列化并不保存静态变量。
- 要想将父类对象也序列化,就需要让父类也实现 Serializable 接口。
- Transient 关键字的作用是控制变量的序列化,在变量声明前加上该关键字,可以阻止该变量被序列化到文件中,在被反序列化后,transient 变量的值被设为初始值,如 int 型的是 0,对象型的是 null。
八、过程中遇到的问题及解决办法
1、FastJsonHttpMessageConverter 不生效,没有注入到 Spring 容器中
原因:项目中同时存在了 WebMvcConfigurationSupport 和 WebMvcConfigurer 这两个配置,只有该 WebMvcConfigurationSupport 的配置生效
解决办法:将 WebMvcConfigurationSupport 改造成 WebMvcConfigurer。因为多个WebMvcConfigurer 的配置的话,都是会生效的
2、FastJsonHttpMessageConverter 注入到 Spring 容器中,但是不起作用
原因:由于 converters 是 HttpMessageConverter 的列表list,而新 add 的消息转换器位于列表的最后,所以可能不生效
解决办法:可以使用列表 list 的 add(0, object),converters.add(0,fastJsonHttpMessageConverter),把 fastJsonHttpMessageConverter 插入列表头
九、注意事项
- 注意区分 configureMessageConverters 和 extendMessageConverters方 法的不同,前者会覆盖掉原有的消息转换器集合,而只保留当前的集合,因此如果使用了这个方法,就会覆盖掉默认的消息转换器集合,因此这里得注意配置了新的会不会引起功能的缺失,比如说默认的实际上是支持基本的 @RequestBody,@ResponseBody 功能的,配置了新的也要支持,不能让这两个注解失效。如果担心的话,可以使用 extendMessageConverters 方法配置消息转换器,这样就不会覆盖,确保了安全。
- @Bean 和 写在 WebMvcConfigurer 里面的区别是,注入bean的方式,这种方法加入的转换器排序是第一位。实现 WebMvcConfigurer 接口,这种方法加入的转换器排序是最后一位。
十、参考文档
- WebMvcConfigurationSupport 和 WebMvcConfigurer 区别和同时使用产生的问题-解决
- SpringBoot的序列化和反序列化
- spring-boot2.x,使用EnableWebMvc注解导致的自定义HttpMessageConverters不可用