博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Java对象序列化与反序列化
阅读量:3929 次
发布时间:2019-05-23

本文共 11652 字,大约阅读时间需要 38 分钟。

一、序列化和反序列化介绍

在Java中,对象的序列化与反序列化被广泛应用到RMI(远程方法调用)及网络传输中。

序列化:指将Java对象数据保存到磁盘文件中或者传递给其他网络的节点(在网络上传输)。

反序列化:指将磁盘文件中的对象数据或者把网络节点上的对象数据,恢复成Java对象的过程为反序列化。

对象序列化机制允许把内存中的Java对象转换成与平台无关的二进制流,从而允许把这种二进制流持久地保存在磁盘上,或通过网络将这种二进制流传输到另一个网络节点。其他程序一旦获得了这种二进制流,都可以将这种二进制流恢复成原来的Java对象。

1、为什么要做序列化?

        1)在分布式系统中,需要共享数据的JavaBean对象,都得做序列化,此时需要把对象在网络上传输,就得把对象数据转换为二进制形式(只有实现序列化接口的类,才能做序列化操作)。

        2)服务钝化:如果服务发现某些对象好久都没有活动了,此时服务器就会把这些内存中的对象,持久化在本地磁盘文件中(Java对象-->二进制文件)。 如果某些对象需要活动的时候,先在内存中去寻找,找到就使用,找不到再去磁盘文件中,找到反序列化得对象数据,恢复成Java对象。

2、Java序列化对象版本号--serialVersionUID

1)随着项目的升级,系统的class文件也会改变(如增加/删除一个字段),如何保证两个class文件的兼容性?

Java的序列化机制是通过在运行时判断类的serialVersionUID(序列化版本号)来验证版本的一致性。在进行反序列化时,JVM会把传来的字节流中的serialVersionUID与本地相应实体(类)的serialVersionUID进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常。

如果不显示定义 serialVersionUID类变量,该类变量的值由JVM根据类相关信息计算,而修改后的类的计算方式和之前往往不同,从而造成了对象反序列化因为版本不兼容而失败的问题。所以, 解决方案:在类中提供一个固定的 serialVersionUID 值。

2)显式地定义 serialVersionUID 有两种用途

(1)在某些场合,希望类的不同版本对序列化兼容,因此需要确保类的不同版本具有相同的serialVersionUID;

         在某些场合,不希望类的不同版本对序列化兼容,因此需要确保类的不同版本具有不同的serialVersionUID。

(2)如果不设置serialVersionUID, 当序列化了一个类实例后,如果更改一个字段或添加一个字段, 对类实例所做的任何更改都将导致无法反序化旧有实例,并在反序列化时抛出一个异常。

    如果设置了serialVersionUID,在反序列旧有实例时,新添加或更改的字段值将设为初始化值(对象型为null,基本类型为其初始默认值),字段被删除将不设置初始化值。

3、序列化需要注意的几个问题

static 和 transient 修饰的字段是不会被序列化的。字段的值被设为初始值,(对象型为null,基本类型为其初始默认值),静态成员属于类级别的,所以不能序列化。参考文章:

1)Transient 关键字

Transient 关键字的作用是控制变量的序列化,在变量声明前加上该关键字,可以阻止该变量被序列化到文件中,

Transient 关键字只能用于修饰Field,不可修饰Java程序中的其他成分。

2)Java对象的class文件

必须确保该读取程序的 CLASSPATH 中包含有 Java对象的class文件,否则会抛出 ClassNotFoundException。

3)字段为引用对象时

需要序列化的Java对象和字段为引用对象的这两个都必须是可序列化的,否则Java对象将不可序列化。

二、对象序列化机制

简单来说,Java 对象序列化就是把对象写入到输出流中,用来存储或传输;反序列化就是从输入流中读取对象。

如果需要让某个java对象支持序列化机制,实现方式有两种。

注意:

1)对象的序列化是基于字节的流,不能使用基于字符的流。

2)自定义的枚举类是直接可以被序列化和反序列化。因为每个枚举类都会默认继承java.lang.Enum类,而Enum类实现了Serializable接口,所以枚举类型对象都是默认可以被序列化的。

方式一:实现Serializable接口,通过序列化流

       java.io.Serializable接口是个标志接口,用于标识该类可以被序列化,没有抽象方法。在Java中大多数类都已经实现Serializable接口。底层会判断,如果当前对象是Serializable的实例,才允许做序列化.。 boolean  ret = Java对象  instanceof  Serializable;

public class User implements Serializable {    private static final long serialVersionUID = 5301525230834919001L;    private Long id;    private String username;    transient private String passwoord;    private int age;    public User() {        System.out.println("调用无参数构造器");    }    public User(Long id, String username, String passwoord, int age) {        this.id = id;        this.username = username;        this.passwoord = passwoord;        this.age = age;        System.out.println("调用有参数构造器");    }...}

1、实例demo

需要做序列化的java对象实现Serializable接口,然后通过对象字节流将对象序列化和反序列化。

public static void main(String[] args) {        User user = new User();        user.setId(1L);        user.setUsername("lisi");        user.setPasswoord("123456");        ObjectOutputStream objectOutputStream = null;        ObjectInputStream objectInputStream = null;        try {            // 将 user对象序列化到 user.txt文件(二进制数据)            File file = new File("D:/E/user.txt");            objectOutputStream = new ObjectOutputStream(new FileOutputStream(file));            objectOutputStream.writeObject(user);            // 从user.txt文件反序列化输出 user对象            objectInputStream = new ObjectInputStream(new FileInputStream(file));            User user1 = (User) objectInputStream.readObject();            System.out.println(user1);        } catch (Exception e) {            e.printStackTrace();        } finally {            if(objectOutputStream != null){                try {                    objectOutputStream.close();                } catch (IOException e) {                    e.printStackTrace();                }            }        }    }

 

方式二:实现Externalizable接口,重写writeExternal和readExternal方法

java.io.Externalizable接口继承了Serializable接口,使用Externalizable接口需要实现writeExternal(用于序列化)以及readExternal(用于反序列化)方法。

注意:这种方式 transient修饰词将失去作用,即使你使用transient修饰属性,只要在writeExternal方法中序列化了该属性,照样也会进行序列化。

1、实例demo

public class User implements Externalizable {    private static final long serialVersionUID = 5301525230834919001L;    private Long id;    private String username;    transient private String passwoord;    private int age;    @Override    public void writeExternal(ObjectOutput out) throws IOException {        // 需要序列化的字段        out.writeObject(username);        out.writeObject(passwoord);        out.writeInt(age);    }    @Override    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {        // 被序列化的字段,注意:必须和序列化的字段顺序保持一致        username = (String) in.readObject();        passwoord = (String) in.readObject();        age = in.readInt();    }...}

 main 中流处理同上

   

注意:使用 Externalizable 接口进行序列化时,读取对象会调用被序列化类的无参构造器去创建一个新的对象,然后再将被保存对象的字段的值分别填充到新对象中,所以,实现 Externalizable 接口的类必须要提供一个无参构造器,且它的访问权限为public

如果只想将Java对象中部分属性进行序列化,可以使用使用Serializable接口和transient关键字配合使用,也可以使用Externalizable接口,重写writeExternal和readExternal方法。这也是它们的区别。

1、readResolve()方法——单例模式的反序列化(了解)

当使用Singleton单例模式时,某个类的实例是唯一的,但如果该类是可序列化的,那么情况可能略有不同。反序列化的时候为创建对象,所以是不唯一的。如果在序列化过程仍要保持单例的特性,可以在Java对象中添加一个readResolve()方法,在该方法中直接返回Person的单例对象即可。

原理就是当从 I/O 流中读取对象时,ObjectInputStream 类里有 readResolve() 方法,该方法会被自动调用,然后经过种种逻辑,最后会调用到可序列化类里的 readResolve()方法,这样可以用 readResolve() 中返回的单例对象直接替换在反序列化过程中创建的对象,实现单例特性。也就是说,无论如何,反序列化都会额外创建对象,只不过使用 readResolve() 方法可以替换之。

public class User implements Serializable {    private static final long serialVersionUID = 5301525230834919001L;    private Long id;    private String username;    transient private String passwoord;    private int age;    private User() {        System.out.println("调用无参数构造器");    }    public static final User INSTANCE = new User();    public static  User getInstance(){        return INSTANCE;    }// 在该方法中直接返回类的单例对象//    public Object readResolve(){//        return INSTANCE;//    }...}

实例demo 

public static void main(String[] args) {        User user = User.getInstance();        user.setId(1L);        user.setUsername("lisi");        user.setPasswoord("123456");        ObjectOutputStream objectOutputStream = null;        ObjectInputStream objectInputStream = null;        try {            // 将 user对象序列化到 user.txt文件(二进制数据)            File file = new File("D:/E/user.txt");            objectOutputStream = new ObjectOutputStream(new FileOutputStream(file));            objectOutputStream.writeObject(user);            // 从user.txt文件反序列化输出 user对象            objectInputStream = new ObjectInputStream(new FileInputStream(file));            User user1 = (User) objectInputStream.readObject();            System.out.println(user1);            System.out.println("反序列化后的对象是不是前面的单例user对象:" + (user == user1));        } catch (Exception e) {            e.printStackTrace();        } finally {            if(objectOutputStream != null){                try {                    objectOutputStream.close();                } catch (IOException e) {                    e.printStackTrace();                }            }        }    }

    

 

三、序列化的安全性

服务器端给客户端发送序列化对象数据,序列化二进制格式的数据写在文档中,并且完全可逆。要是被别人抓包就能获取数据内容。所以,在序列化与反序列化时,可以对数据进行加解密操作,从而一定程度上保证序列化对象的数据安全。

Java提供了对整个对象进行加密和签名的方式。就是将Java对象包装在 javax.crypto.SealedObject 或 java.security.SignedObject中,然后进行序列化机制。

在 SealedObject(一个秘钥) 与SignedObject(一对秘钥) 中,指明使用哪种加密算法,然后通过秘钥对实现加解密操作,从而校验数据是否被人篡改和序列化的安全性。

demo:序列化之后篡改一下数据,然后反序列化,看结果。

1、使用 SealedObject, 算法用 DESede

public static void main(String[] args) throws IOException {        User user = User.getInstance();        user.setId(1L);        user.setUsername("lisi");        user.setPasswoord("123456");        ObjectOutputStream objectOutputStream = null;        ObjectInputStream objectInputStream = null;        ObjectOutputStream objoutEncryptKey = null;        ObjectInputStream objGetEncryptKey = null;        try {            // 将 user对象序列化到 user.txt文件(二进制数据),加密            File file = new File("D:/E/user.txt");           /* objectOutputStream = new ObjectOutputStream(new FileOutputStream(file));            KeyGenerator keyGenerator = KeyGenerator.getInstance("DESede");            // 获取秘钥            SecretKey encryptKey = keyGenerator.generateKey();            System.out.println(encryptKey);            Cipher cipher = Cipher.getInstance("DESede");            cipher.init(Cipher.ENCRYPT_MODE, encryptKey);            SealedObject sealedObject = new SealedObject(user, cipher);            objectOutputStream.writeObject(sealedObject);            //将秘钥保存            objoutEncryptKey = new ObjectOutputStream(new FileOutputStream(new File("D:/E/encryptKey.txt")));            objoutEncryptKey.writeObject(encryptKey);*/            // 从user.txt文件反序列化输出 user对象,解密            objectInputStream = new ObjectInputStream(new FileInputStream(file));            SealedObject sealedObjectResult = (SealedObject) objectInputStream.readObject();            objGetEncryptKey = new ObjectInputStream(new FileInputStream(new File("D:/E/encryptKey.txt")));            SecretKey openKey = (SecretKey) objGetEncryptKey.readObject();            User user1 = (User) sealedObjectResult.getObject(openKey);            System.out.println(user1);        } catch (Exception e) {            e.printStackTrace();        } finally {            objectOutputStream.close();            objectInputStream.close();            objoutEncryptKey.close();            objGetEncryptKey.close();        }    }

     

2、使用 SignedObject, 算法用 DSA

public static void main(String[] args) throws IOException {        User user = User.getInstance();        user.setId(1L);        user.setUsername("lisi");        user.setPasswoord("123456");        ObjectOutputStream objectOutputStream = null;        ObjectInputStream objectInputStream = null;        ObjectOutputStream objoutEncryptKey = null;        ObjectInputStream objGetEncryptKey = null;        try {            // 将 user对象序列化到 user.txt文件(二进制数据),私钥加密            File file = new File("D:/E/user.txt");            objectOutputStream = new ObjectOutputStream(new FileOutputStream(file));            KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("DSA");            keyPairGenerator.initialize(1024);            KeyPair keyPair = keyPairGenerator.generateKeyPair();            // 获取秘钥对            PrivateKey privateKey = keyPair.getPrivate();            PublicKey publicKey = keyPair.getPublic();            Signature signature = Signature.getInstance("DSA");            SignedObject signedObject = new SignedObject(user, privateKey, signature);            objectOutputStream.writeObject(signedObject);            //将公钥保存,供客户端使用            objoutEncryptKey = new ObjectOutputStream(new FileOutputStream(new File("D:/E/publicKey.txt")));            objoutEncryptKey.writeObject(publicKey);            // 从user.txt文件反序列化输出 user对象,公钥解密            objectInputStream = new ObjectInputStream(new FileInputStream(file));            SignedObject signedObjectResult = (SignedObject) objectInputStream.readObject();            objGetEncryptKey = new ObjectInputStream(new FileInputStream(new File("D:/E/publicKey.txt")));            PublicKey openKey = (PublicKey) objGetEncryptKey.readObject();            //方法verify,判断盒子里的对象有没有没篡改            Signature verifySignature = Signature.getInstance("DSA");            if (signedObjectResult.verify(openKey, verifySignature)) {                //内容没被篡改                System.out.println("内容没被篡改");                User user1 = (User) signedObjectResult.getObject();                System.out.println(user1);            }else{                System.out.println("内容被篡改过!");            }        } catch (Exception e) {            e.printStackTrace();        } finally {            objectOutputStream.close();            objectInputStream.close();            objoutEncryptKey.close();            objGetEncryptKey.close();        }    }

 

—— Stay Hungry. Stay Foolish. 求知若饥,虚心若愚。

转载地址:http://hgdgn.baihongyu.com/

你可能感兴趣的文章
让你不再害怕指针——C指针详解
查看>>
十张图解释机器学习的基本概念
查看>>
端口复用及其实现分析[Google Patch]
查看>>
红黑树:自平衡的二叉查找树
查看>>
回收站功能在 Linux 中的实现
查看>>
数据包头分析---网络字节序与主机字节序
查看>>
linux sh/bash 编程常用
查看>>
《Debug Hacks》和调试技巧
查看>>
x86寄存器和栈帧
查看>>
计算机科学经典论文(zz)
查看>>
了解云计算的漏洞
查看>>
ECC加密算法入门介绍
查看>>
欢迎使用CSDN-markdown编辑器
查看>>
spark && Tachyon
查看>>
计算机科学不等于数学
查看>>
文件系统与NoSQL分布式存储技术对比
查看>>
rootkit技术
查看>>
调试寄存器(debug registers, DRx)理论及实践
查看>>
Linux下逻辑地址-线性地址-物理地址图解
查看>>
vim安装SrcExpl 插件,实现自动显示跳转函数及变量定义功能
查看>>