序列化深入浅出 —— Java版本

x33g5p2x  于2021-10-29 转载在 Java  
字(3.0k)|赞(0)|评价(0)|浏览(281)

一、概念&作用

将本地已经实例化的某个对象(此对象有参数),有 传输或持久化、重构Java对象 的需求,怎么才能把数据包装到一起?

众所周知,参数指向内存中的地址,这些内存地址存储着实际的数据,这些地址可能并不连续,这就使得在内存中到处都有此对象的参数数据,怎么才能把这些数据全部集合在一起呢?得到集成在一起的数据了,怎么才能把它转化为内存的对象参数呢?

这就需要使用 序列化和反序列化 技术:

  • 序列化:将某个对象集成压缩成 字节流或文本 的形式,将参数引用指向的数据全部集成在一起(参数对象可能有多个引用,指向内存中不同的地方,集成就是把全部数据整理到内存中一块连续的区域)。
  • 反序列化:将 字节流或文本 转换成内存中的对象实例,即在内存中分配对象的内存并赋值(参数地址和引用)。
    注意:即使是经过序列化之后传递的数据,也是经过 计算机网络的五层架构 进行数据传输,最终都是以为二进制形式传输的。

序列化常用类型分为 text and binary 两种形式(不常用不列),即 字符串和字节流

  • text : JSON、XML等都是序列化后的数据。比较成熟的开源类库有Gson、Fastjson。
  • binary:字节流,更快更轻盈(String字符串其就是)。
    从Java9开始,String使用byte[]存储,即字节流。

序列化技术,具有 跨语言、跨平台、可扩展和向下兼容 的优点。比如C语言中序列化一个对象后,可以被对应的Java语言解析出来。

二、Java实现序列化

数据对象,必须被encoding编码以后才能被序列化(UTF-8等)。下面使用Java分别实现序列化实体类实例为 字节流和文本,文本处以JSON数据格式为例。

1.字节流序列化

Java实现序列化注意的点:

  • 某个类可以被序列化,则其子类也可以被序列化
  • 声明为 static 和 transient 的成员变量,不能被序列化。static 成员变量是描述类级别的属性,transient 表示临时数据。
  • 反序列化读取序列化对象的顺序要保持一致。
  • 只有实现了 Serializable及其子类 的对象才能被序列化,否则抛出异常。(好像,json的序列化不用实现)。

以序列化为二进制数据存储到本地为例(字节流序列化实际应用偏向于存储到本地),简单实现:

实体类User

public class User implements Serializable {
    
    // getter,setter,constructor 加上

    private static final long serialVersionUID = 5887391604554532906L;

    private String name;

    private Integer age;

    private Character sex;

}

序列化和反序列化操作

public static void main(String[] args) throws Exception {

    // 序列化 二进制存储到本地
    User user = new User("张三",22,'男');
    ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("User.obj"));
    oos.writeObject(user); // 写到本地文件中

    //反序列化 从本地文件中获取数据
    ObjectInputStream ois = new ObjectInputStream(new FileInputStream("User.obj"));
    User user1 = (User) ois.readObject();
    System.out.println(user1);
    
}

现在,就可在当前项目的根目录看到此文件(也可以自己指定位置),打开它(建议使用sublime text或其他编辑器打开):

2.JSON序列化

【Java、JS与JSON互转,点击我快速跳转!】

以为JSON序列化,进行打印为例。JSON序列化实际应用偏向于传输数据。这里使用Java类库实现简单Java对象转换为JSON字符串:

// user 是前面代码中的实现
// 使用java类库 throws JsonProcessingException
String json = new ObjectMapper().writeValueAsString(user);
System.out.println(json);

输出如下

{“name”:“张三”,“age”:22,“sex”:“男”}

简单实现肯定是有一些问题,那么为什么我这么敷衍?因为我们可以使用阿里的fastjson类库,使用及其广泛,特别强大!【点击我查看并学习fastjson使用】。

3.二者区别

  • 二进制序列化器需要 实体类实现Serializable接口或其子类,而Json序列化器不需要。
  • 二进制序列化器只能序列化字段,而不能序列化属性,也就是说当一个类中不显示定义字段,二进制序列化器是不起作用的,JSON序列化器没有这条规定。二进制序列化器中,若某个字段不需要序列化,可应用[NonSerialized]特性。
  • 二进制序列化后得到的是一个二进制文件,而JSON序列化后得到的是JSON字符串。
  • 二进制序列化JSON所在内存小,JSON易读。json依据逐渐演变成前后端交互数据的定义格式。

三、序列化安全

1.serialVersionUID

针对于二进制序列化

举个例子:在平时我们与人说话交流的时候,大家都用普通话,如果有人使用家乡话,你就会感觉到听不懂了,更别说好好交流了。这里的普通话就是一种协议,作为我们与身边人的语言协议,大家都遵守它才能高效沟通。

而serialVersionUID就是序列化操作的协议,我在解析别人传递给我的数据的时候,会去检查此文件是否遵守协议,遵守我才能正常解析,不遵守我就直接抛出异常(解析也会出现问题)。即反序列化时,JVM会判断serialVersionUID是否一致,不一致就会直接抛出异常。

比如前后使用不同的serialVersionUID:

当我们一个实体类中没有显式的定义一个名为 serialVersionUID、类型为long的变量时,Java序列化机制会根据编译时的class自动生成一个serialVersionUID作为序列化版本比较,这种情况下,只有同一次编译生成的class才会生成相同的serialVersionUID。时间不同,也会出错。那么如何决呢?便是在本地类中加个serialVersionUID变量,值保持不变,便可以进行序列化和反序列化(显式的定义一个serialVersionUID)

2.反序列化不安全

下面这些地方同样可能存在反序列化安全隐患,但很可能不常被关注到:

  • 自定义的HTTP Header。
  • 存储在Redis、MongoDB、MySQL等数据库里的数据,可能被不怀好意的员工修改。
  • 存储在服务器本地,或者某个远程文件服务器里的文件,可能被攻击者替换。
  • 缓存服务器中的数据可能被攻击者污染。

治病需要除根,能从根本上阻止反序列化安全问题的防御方案就是完整性校验,而最常见的例子之一就是JWT。

【更多序列化的问题请移步:https://tech.meituan.com/2015/02/26/serialization-vs-deserialization.html】。

相关文章