Java 序列化全解密

何为序列化,反序列化

将Java对象序列化为二进制形式->序列化
将二进制形式数据在内存中重建为java对象->反序列化

二进制中包含了当前实例的类的元数据,以及存储的数据等。

Java 提供了一种对象序列化的机制,该机制中,一个对象可以被表示为一个字节序列,该字节序列包括该对象的数据、有关对象的类型的信息和存储在对象中数据的类型。也就是将Java对象序列化为二进制形式。

目的:

  • 网络传输
  • 持久化 (文件,DB等)

将序列化对象写入文件之后,可以从文件或网络中中读取出来,并且对它进行反序列化,也就是说,对象的类型信息、对象的数据,还有对象中的数据类型可以用来在内存中新建对象。

如何序列化,反序列化

类能够序列化和反序列化,
1.类必须实现Serializable接口。
2.需要序列化的属性必须实现Serializable接口(不需要的属性可以使用transient关键字修饰)
3.第一个未实现Serializable接口的父类必须有无参构造器

Java中的内置序列化机制需要使用ObjectOutputStream完成序列化。
使用ObjectInputStream完成反序列化。

ObjectOutputStream

ObjectOutputStream能够让你把对象写入到输出流中,而不需要每次写入一个字节。你可以把OutputStream包装到ObjectOutputStream中,然后就可以把对象写入到该输出流中了。代码如下:

    // Serialize today's date to a file.
    FileOutputStream f = new FileOutputStream("tmp");
    ObjectOutput s = new ObjectOutputStream(f);
    s.writeObject("Today");
    s.writeObject(new Date());
    s.flush();

ObjectInputStream

ObjectInputStream能够让你从输入流中读取Java对象,而不需要每次读取一个字节。你可以把InputStream包装到ObjectInputStream中,然后就可以从中读取对象了。代码如下:

    // Deserialize a string and date from a file.
    FileInputStream in = new FileInputStream("tmp");
    ObjectInputStream s = new ObjectInputStream(in);
    String today = (String)s.readObject();
    Date date = (Date)s.readObject();

在这个例子中,写入和读取的顺序必须一致,数据类型也必须一致。

注意:可序列化的字段有两种方式确定
1.默认的:序列化不会处理静态和transient的属性。
2.使用serialPersistentFields

如何定制序列化的过程

1.使用默认的readObject/writeObject方法
方法签名:

    private void readObject(ObjectInputStream ois) throws IOException,ClassNotFoundException{
        ois.defaultReadObject();
        //其他read代码
    }
    private void writeObject(ObjectOutputStream oos) throws IOException,ClassNotFoundException {
        oos.defaultWriteObject();
        //其他write代码
    }

序列化时ObjectOutputStream调用writeObject方法,反序列化时ObjectInputStream调用readObject方法。

注意:
- 必须调用defaultWriteObject()和defaultReadObject()方法
- 写和读的顺序要一致,写了一个int,读的时候第一个也是读int.
- 序列化、反序列化过程中没有调用任何构造器(类及其父类均实现了Serializable接口时,如父类未实现Serializable,父类里的属性丢失,无法从流中反序列化)

2.使用Externalizable接口
方法签名

    @Override
    public void writeExternal(ObjectOutput out) throws IOException {

    }

    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {

    }

Externalizable是Serializable接口的子接口。
序列化、反序列化过程中调用了默认构造器

第一种方式可以部分定制序列化过程,序列化、反序列化的过程部分由Java控制,部分由程序员自己控制。
第二种方式则是完全定制序列化过程,程序员控制序列化、反序列化的过程.

序列化,反序列化在Java中是如何发生的

序列化:ObjectOutputStream开始向文件中写数据->反射调用writeObject方法并把自己传入->写完后转化为byte数组->写入文件

反序列化:读取文件->反射调用Object(如果某个父类没有实现Serializable接口,则调用该类的默认无参构造器(父类里的属性丢失))的构造器获取实例->设置
类型信息及其他实例化相关的元数据->初始化静态属性值->反射调用readObject方法->完成反序列化。

Externalizable 与 Serializable 的区别

SerializableExternalizable
标志接口,无必须实现的方法必须实现writeExternal() 和 readExternal()
JVM负责序列化实例,数据和数据类型等程序员控制实例化及数据处理过程
默认不调用构造器默认必须有无参构造器
修改类结构容易导致序列化失败,分析比较困难分析和修改类容易
性能不好性能受程序员实现限制,一般会比较好

类结构的兼容性

在序列化和反序列化的两端,java的类结构可能会有变动,有的变动会导致反序列化失败或引起业务系统的不兼容,称为非兼容的改动,
有的变动不会导致反序列化失败或业务系统的不兼容,称为兼容的改动。
注意:以下兼容性是在类实现了Serializable接口并定义了固定的serialVersionUID的情况下,如果没有实现该接口则由于serialVersionUID不同的原因,部分兼容的改动也会序列化失败。

兼容的改动

  • 修改属性的修饰符:public, package, protected, private 不影响序列化
  • 新增属性:新增的属性被设置默认值,可以通过readObject设置特定值。
  • 将static属性改为非static,将transient属性改为非transient:类似于新增属性,被设置为默认值。
  • 添加删除类:在类结构中添加的类对应的属性被设置为默认值。删除类,则类对应的属性被丢弃,但是引用到的类会被创建,在随后的垃圾回收中被回收掉。
  • 某个类中添加实现Serializable接口:同添加类一样,该的属性被设置为默认值。
  • 添加删除writeObject/readObject方法:添加时,方法会被调用,则可以对额外属性做序列化操作。删除时,额外添加的属性被丢弃。

不兼容的改动

  • 删除属性:删除属性会导致旧版本的属性被设置为默认值,引起业务逻辑的改动。
  • 类结构中提升或降低某个类的位置:不会允许,可能导致属性顺序混乱。
  • 修改基础类型的属性(primitive field)的类型:由于类型不匹配导致序列化失败。引用类型会最终递归到基础类型。
  • 修改writeObject/readObject方法的实现,使得某些属性不再使用默认序列化机制:即将交给jvm序列化的属性修改为直接代码处理了。类似于删除属性。writeObject/readObject对属性的处理应当保持一致。
  • 将非static属性改为static,将非transient属性改为transient:类似于删除属性。
    以下四种会导致数据的不兼容。
  • 修改类从实现Serializable接口改为实现Externalizable接口或者相反
  • 删除Serializable或Externalizable接口
  • 修改枚举类型为非枚举或者相反
  • 添加writeReplace或readResolve方法

需要注意的一些问题

serialVersionUID

    private static final long serialVersionUID = -4116638233879810430L;

serialVersionUID是序列化的版本,用来在反序列化时,确保发送者和接收者加载的类是否兼容。其值可自定义。如果没有显示定义serialVersionUID属性,
则Java根据类定义使用SHA-1算法得出的哈希值替代。
serialVersionUID不一致时,报错InvalidCastException.
使用serialVersionUID可以在以上发生的兼容或不兼容的改动时,强制客户端更新。

transient 关键字 与 static属性

Java的序列化不考虑transient修饰的属性及static属性。
transient关键字一般用于:

  1. 某些属性无法被序列化
  2. 某些属性没有必要被序列化
  3. 某些属性序列化后会破坏业务逻辑,比如某个单例的属性
  4. 某些属性是由业务逻辑或者根据其他属性生成的
  5. 由于安全原因,不能序列化某些属性

父类的序列化

父类对象的序列化,需要让父类也实现Serializable 接口。
如果父类不实现,就需要有默认的无参的构造函数。
在父类没有实现 Serializable 接口时,虚拟机是不会序列化父对象的,而一个 Java 对象的构造必须先有父对象,才有子对象,
反序列化也不例外。所以反序列化时,为了构造父对象,只能调用父类的无参构造函数作为默认的父对象。
因此当我们取父对象的变量值时,它的值是调用父类无参构造函数后的值。
如果你考虑到这种序列化的情况,在父类无参构造函数中对变量进行初始化,否则的话,父类变量值都是默认声明的值,
如 int 型的默认是 0,string 型的默认是 null。

readObjectNoData、writeReplace、readResolve

这三个方法都是Serializable接口的除readObject,writeObject方法之外的接口方法。

    ANY-ACCESS-MODIFIER Object writeReplace() throws ObjectStreamException;
    ANY-ACCESS-MODIFIER Object readResolve() throws ObjectStreamException;
       private void readObjectNoData()
           throws ObjectStreamException;

readObjectNoData是1.4新增的,目的在于发生:

  1. 序列化版本不兼容
  2. 输入流被篡改或者损坏
    时,给对象提供一个合理的值。

写入时替换对象:writeReplace

如果实现了writeReplace方法后,那么在序列化时会先调用writeReplace方法将当前对象替换成另一个对象(该方法会返回替换后的对象)并将其写入流中.
方法执行顺序:writeReplace -> writeObject/writeExternal
导致的结果:所有当前类型的实例都被替换了。假如一个类的100个不同的实例(某些属性的值完全不同)被序列化了,
反序列化时你得到了100个不同的实例,但是所有的属性可能都是一样的(取决于你的writeReplace方法的具体实现)。

读取时替换对象:readResolve

如果实现了readResolve方法后,那么在序列化时会最后调用writeReplace方法将当前对象替换成另一个对象。
方法执行顺序:readObject/readExternal -> readResolve
导致的结果:所有当前类型的实例都被替换了。

可以被用于单例模式,确保实例序列化后的唯一性。

默认实例化时,各方法的总结

功能点readObjectwriteObjectreadExternalwriteExternalreadResolvewriteReplace
目的读取定制内容写入定制内容读取完全定制的内容,类的实例化自己控制写入完全定制的内容,写入当前对象的那些属性及写入顺序等自定义覆盖读取的内容覆盖写入的内容
是否使用默认序列化机制是,但readObject/readExternal读到的内容被丢弃是,但写入流的原类的实例都被丢弃
调用顺序readObject -> readResolvewriteReplace -> writeObjectreadExternal -> readResolvewriteReplace -> writeExternalreadObject/readExternal -> readResolvewriteReplace -> writeObject/writeExternal
是否成对是,必须与writeObject一起添加是必须与writeExternal一起添加否,可独立存在否,可独立存在

各个方法的目的不同,理解了目的,其他就好理解了。

序列化,反序列化与深克隆(deep clone)

java的克隆是属性到属性的克隆。

  • 如果属性是基础数据类型,新的属性对象会被创建。
  • 如果属性是引用类型,则只有引用被复制,引用指向的对象是同一个。意味着克隆对象的改动会影响被克隆对象。

java实现克隆需要两步。

  1. 实现clonable接口
  2. override Object对象的clone方法

java默认的克隆方式的浅克隆:即只克隆基础数据类型,引用类型只复制了引用。
深克隆:克隆所有的数据,使得对克隆对象的任何操作不会影响到原被克隆对象。
深克隆的方式可使用以下几种方式:

  1. 使用该类型参数构造器
    public class Data {
        private Integer x;
        private Object y;

        public Data(Data point){
            this.x = point.x;
            this.y = point.y;
        }
    }
  1. 使用序列化、反序列化
    public static  <T> T deepCopy(T Object) throws IOException, ClassNotFoundException {
            //Serialization of object
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            ObjectOutputStream out = new ObjectOutputStream(bos);
            System.out.println("begin w");
            out.writeObject(Object);
            System.out.println("end w");

            //De-serialization of object
            ByteArrayInputStream bis = new   ByteArrayInputStream(bos.toByteArray());
            ObjectInputStream in = new ObjectInputStream(bis);
            T copied = (T) in.readObject();
            System.out.println("end r");
            return copied;
        }

使用ByteArrayInputStream和ByteArrayOutputStream在内存中完成克隆

  1. 使用Apache commons工具
    SomeObject cloned = org.apache.commons.lang.SerializationUtils.clone(someObject);

以上三种方式的性能未测试。

反序列化破坏单例

反序列化默认没有调用构造器就实例化了类。对单例的类无法阻止生成多个实例。破坏了单例。
解决方案就是使用保护性反序列化方法readResolve

    protected Object readResolve(){
            return getInstance();
        }

序列化中的异常

异常原因
InvalidClassException以下原因导致的无法还原实例时,1.serialVersionUID 不一致 2.同一属性的基础类型改变时 3.最近的未序列化的父类没有无参构造器 4.Externalizable 的类没有无参构造器
StreamCorruptedException、InvalidObjectException一般是数据损坏,或协议不匹配
OptionalDataException属性本来是基础类型,被改成了引用类型

建议及最佳实践

  • 一定要加serialVersionUID
  • readObject,writeObject最好同时存在,读写数据时保持顺序一致
  • readObject,writeObject必须调用默认的序列化方法
  • 可序列化的对象添加无参构造器
  • 了解transient关键字的常用功能

其他序列化协议,及性能对比

https://www.ibm.com/developerworks/cn/linux/l-cn-gpb/
各序列化方式的性能
https://github.com/eishay/jvm-serializers/wiki
FastJson为什么那么快
http://wenshao.iteye.com/blog/1142031/

参考资料

http://docs.oracle.com/javase/7/docs/api/java/io/Serializable.html
java对象序列化规范
http://docs.oracle.com/javase/7/docs/platform/serialization/spec/serialTOC.html
serialVersionUID及其默认生成方式
http://docs.oracle.com/javase/7/docs/platform/serialization/spec/class.html#4100
https://www.javacodegeeks.com/2012/07/native-cc-like-performance-for-java.html

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Java序列漏洞是一种安漏洞,它可以被恶意攻击者利用来执行未经授权的代码。该漏洞发生在Java应用程序中,当使用ObjectInputStream类反序列来自不可信来源的数据时,攻击者可以通过在序列数据中插入恶意代码来执行任意代码。 攻击者可以利用反序列漏洞来执行各种恶意操作,例如执行远程命令、读取或修改敏感数据、绕过身份验证等。这种漏洞的危害性非常高,因此开发人员需要采取适当的防护措施来减轻这种风险。 为了防止反序列漏洞的利用,可以考虑以下几个建议: 1. 避免反序列不可信的数据:只反序列来自可信来源的数据,不要接受来自未知或不可信的源的数据。 2. 使用白名单机制:限制可反序列的类和包,只允许反序列预定义的类,而不是允许所有类的反序列。 3. 更新和修补程序:及时更新和修补使用的Java库和框架,以获得最新的安补丁。 4. 序列对象验证:在反序列之前,对序列对象进行验证,确保它们没有被篡改或被替换为恶意对象。 5. 序列对象加密:对序列的对象进行加密,以确保只有具有密钥的人可以解密和反序列对象。 6. 安配置:通过配置Java管理器来限制代码执行的权限,以防止恶意代码执行未经授权的操作。 请注意,这些只是一些基本的防护措施,具体的防御方法还需要根据具体的应用场景和需求来进行综合考虑和实施。建议开发人员在编写代码时时刻关注安性,并参考Java官方文档和最佳实践来保护应用程序免受反序列漏洞的影响。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值