你不知道的java对象序列化的秘密

8次阅读

共计 6429 个字符,预计需要花费 17 分钟才能阅读完成。

简介

你知道序列化可以使用代理吗?你知道序列化的安全性吗?每个 java 程序员都听说过序列化,要存储对象需要序列化,要在网络上传输对象要序列化,看起来很简单的序列化其实里面还隐藏着很多小秘密,今天本文将会为大家一一揭秘。

更多精彩内容且看:

  • 区块链从入门到放弃系列教程 - 涵盖密码学, 超级账本, 以太坊,Libra, 比特币等持续更新
  • Spring Boot 2.X 系列教程: 七天从无到有掌握 Spring Boot- 持续更新
  • Spring 5.X 系列教程: 满足你对 Spring5 的一切想象 - 持续更新
  • java 程序员从小工到专家成神之路(2020 版)- 持续更新中, 附详细文章教程

更多内容请访问 www.flydean.com

什么是序列化

序列化就是将 java 对象按照一定的顺序组织起来,用于在网络上传输或者写入存储中。而反序列化就是从网络中或者存储中读取存储的对象,将其转换成为真正的 java 对象。

所以序列化的目的就是为了传输对象,对于一些复杂的对象,我们可以使用第三方的优秀框架,比如 Thrift,Protocol Buffer 等,使用起来非常的方便。

JDK 本身也提供了序列化的功能。要让一个对象可序列化,则可以实现 java.io.Serializable 接口。

java.io.Serializable 是从 JDK1.1 开始就有的接口,它实际上是一个 marker interface,因为 java.io.Serializable 并没有需要实现的接口。继承 java.io.Serializable 就表明这个 class 对象是可以被序列化的。

@Data
@AllArgsConstructor
public class CustUser implements java.io.Serializable{
    private static final long serialVersionUID = -178469307574906636L;
    private String name;
    private String address;
}

上面我们定义了一个 CustUser 可序列化对象。这个对象有两个属性:name 和 address。

接下看下怎么序列化和反序列化:

public void testCusUser() throws IOException, ClassNotFoundException {CustUser custUserA=new CustUser("jack","www.flydean.com");
        CustUser custUserB=new CustUser("mark","www.flydean.com");

        try(FileOutputStream fileOutputStream = new FileOutputStream("target/custUser.ser")){ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
            objectOutputStream.writeObject(custUserA);
            objectOutputStream.writeObject(custUserB);
        }
        
        try(FileInputStream fileInputStream = new FileInputStream("target/custUser.ser")){ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
            CustUser custUser1 = (CustUser) objectInputStream.readObject();
            CustUser custUser2 = (CustUser) objectInputStream.readObject();
            log.info("{}",custUser1);
            log.info("{}",custUser2);
        }
    }

上面的例子中,我们实例化了两个 CustUser 对象,并使用 objectOutputStream 将对象写入文件中,最后使用 ObjectInputStream 从文件中读取对象。

上面是最基本的使用。需要注意的是 CustUser class 中有一个 serialVersionUID 字段。

serialVersionUID 是序列化对象的唯一标记,如果 class 中定义的 serialVersionUID 和序列化存储中的 serialVersionUID 一致,则表明这两个对象是一个对象,我们可以将存储的对象反序列化。

如果我们没有显示的定义 serialVersionUID,则 JVM 会自动根据 class 中的字段,方法等信息生成。很多时候我在看代码的时候,发现很多人都将 serialVersionUID 设置为 1L,这样做是不对的,因为他们没有理解 serialVersionUID 的真正含义。

重构序列化对象

假如我们有一个序列化的对象正在使用了,但是突然我们发现这个对象好像少了一个字段,要把他加上去,可不可以加呢?加上去之后原序列化过的对象能不能转换成这个新的对象呢?

答案是肯定的,前提是两个版本的 serialVersionUID 必须一样。新加的字段在反序列化之后是空值。

序列化不是加密

有很多同学在使用序列化的过程中可能会这样想,序列化已经将对象变成了二进制文件,是不是说该对象已经被加密了呢?

这其实是序列化的一个误区,序列化并不是加密,因为即使你序列化了,还是能从序列化之后的数据中知道你的类的结构。比如在 RMI 远程调用的环境中,即使是 class 中的 private 字段也是可以从 stream 流中解析出来的。

如果我们想在序列化的时候对某些字段进行加密操作该怎么办呢?

这时候可以考虑在序列化对象中添加 writeObject 和 readObject 方法:

private String name;
    private String address;
    private int age;

    private void writeObject(ObjectOutputStream stream)
            throws IOException
    {
        // 给 age 加密
        age = age + 2;
        log.info("age is {}", age);
        stream.defaultWriteObject();}

    private void readObject(ObjectInputStream stream)
            throws IOException, ClassNotFoundException
    {stream.defaultReadObject();
        log.info("age is {}", age);
        // 给 age 解密
        age = age - 2;
    }

上面的例子中,我们为 CustUser 添加了一个 age 对象, 并在 writeObject 中对 age 进行了加密(加 2),在 readObject 中对 age 进行了解密(减 2)。

注意,writeObject 和 readObject 都是 private void 的方法。他们的调用是通过反射来实现的。

使用真正的加密

上面的例子,我们只是对 age 字段进行了加密,如果我们想对整个对象进行加密有没有什么好的处理办法呢?

JDK 为我们提供了 javax.crypto.SealedObject 和 java.security.SignedObject 来作为对序列化对象的封装。从而将整个序列化对象进行了加密。

还是举个例子:

public void testCusUserSealed() throws IOException, ClassNotFoundException, NoSuchPaddingException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException, InvalidKeyException {CustUser custUserA=new CustUser("jack","www.flydean.com");
        Cipher enCipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
        Cipher deCipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
        SecretKey secretKey = new SecretKeySpec("saltkey111111111".getBytes(), "AES");
        IvParameterSpec iv = new IvParameterSpec("vectorKey1111111".getBytes());
        enCipher.init(Cipher.ENCRYPT_MODE, secretKey, iv);
        deCipher.init(Cipher.DECRYPT_MODE,secretKey,iv);
        SealedObject sealedObject= new SealedObject(custUserA, enCipher);

        try(FileOutputStream fileOutputStream = new FileOutputStream("target/custUser.ser")){ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
            objectOutputStream.writeObject(sealedObject);
        }

        try(FileInputStream fileInputStream = new FileInputStream("target/custUser.ser")){ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
            SealedObject custUser1 = (SealedObject) objectInputStream.readObject();
            CustUser custUserV2= (CustUser) custUser1.getObject(deCipher);
            log.info("{}",custUserV2);
        }
    }

上面的例子中,我们构建了一个 SealedObject 对象和相应的加密解密算法。

SealedObject 就像是一个代理,我们写入和读取的都是这个代理的加密对象。从而保证了在数据传输过程中的安全性。

使用代理

上面的 SealedObject 实际上就是一种代理,考虑这样一种情况,如果 class 中的字段比较多,而这些字段都可以从其中的某一个字段中自动生成,那么我们其实并不需要序列化所有的字段,我们只把那一个字段序列化就可以了,其他的字段可以从该字段衍生得到。

在这个案例中,我们就需要用到序列化对象的代理功能。

首先,序列化对象需要实现 writeReplace 方法,表示替换成真正想要写入的对象:

public class CustUserV3 implements java.io.Serializable{

    private String name;
    private String address;

    private Object writeReplace()
            throws java.io.ObjectStreamException
    {log.info("writeReplace {}",this);
        return new CustUserV3Proxy(this);
    }
}

然后在 Proxy 对象中,需要实现 readResolve 方法,用于从系列化过的数据中重构序列化对象。如下所示:

public class CustUserV3Proxy implements java.io.Serializable{

    private String data;

    public CustUserV3Proxy(CustUserV3 custUserV3){data =custUserV3.getName()+ "," + custUserV3.getAddress();}

    private Object readResolve()
            throws java.io.ObjectStreamException
    {String[] pieces = data.split(",");
        CustUserV3 result = new CustUserV3(pieces[0], pieces[1]);
        log.info("readResolve {}",result);
        return result;
    }
}

我们看下怎么使用:

public void testCusUserV3() throws IOException, ClassNotFoundException {CustUserV3 custUserA=new CustUserV3("jack","www.flydean.com");

        try(FileOutputStream fileOutputStream = new FileOutputStream("target/custUser.ser")){ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
            objectOutputStream.writeObject(custUserA);
        }

        try(FileInputStream fileInputStream = new FileInputStream("target/custUser.ser")){ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
            CustUserV3 custUser1 = (CustUserV3) objectInputStream.readObject();
            log.info("{}",custUser1);
        }
    }

注意,我们写入和读出的都是 CustUserV3 对象。

Serializable 和 Externalizable 的区别

最后我们讲下 Externalizable 和 Serializable 的区别。Externalizable 继承自 Serializable,它需要实现两个方法:

 void writeExternal(ObjectOutput out) throws IOException;
 void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;

什么时候需要用到 writeExternal 和 readExternal 呢?

使用 Serializable,Java 会自动为类的对象和字段进行对象序列化,可能会占用更多空间。而 Externalizable 则完全需要我们自己来控制如何写 / 读,比较麻烦,但是如果考虑性能的话,则可以使用 Externalizable。

另外 Serializable 进行反序列化不需要执行构造函数。而 Externalizable 需要执行构造函数构造出对象,然后调用 readExternal 方法来填充对象。所以 Externalizable 的对象需要一个无参的构造函数。

总结

本文详细分析了序列化对象在多种情况下的使用,并讲解了 Serializable 和 Externalizable 的区别,希望大家能够喜欢。

本文作者:flydean 程序那些事

本文链接:http://www.flydean.com/java-serialization/

本文来源:flydean 的博客

欢迎关注我的公众号: 程序那些事,更多精彩等着您!

正文完
 0