Java对象序列化与反序列化

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

在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,基本类型为其初始默认值),静态成员属于类级别的,所以不能序列化。参考文章:JAVA中序列化和反序列化中的静态成员问题

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. 求知若饥,虚心若愚。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值