Java对象序列化与反序列化

墨蓝 2023-03-01 06:13 206阅读 0赞

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

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

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

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

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

1、为什么要做序列化?

  1. 1)在分布式系统中,需要共享数据的JavaBean对象,都得做序列化,此时需要把对象在网络上传输,就得把对象数据转换为二进制形式(只有实现序列化接口的类,才能做序列化操作)。
  2. 2)服务钝化:如果服务发现某些对象好久都没有活动了,此时服务器就会把这些内存中的对象,持久化在本地磁盘文件中(Java对象-->二进制文件)。 如果某些对象需要活动的时候,先在内存中去寻找,找到就使用,找不到再去磁盘文件中,找到反序列化得对象数据,恢复成Java对象。

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

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

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

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

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

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

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

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

  1. 如果设置了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接口,通过序列化流

  1. java.io.Serializable接口是个标志接口,用于标识该类可以被序列化,没有抽象方法。在Java中大多数类都已经实现Serializable接口。底层会判断,如果当前对象是Serializable的实例,才允许做序列化.。 boolean ret = Java对象 instanceof Serializable;
  2. public class User implements Serializable {
  3. private static final long serialVersionUID = 5301525230834919001L;
  4. private Long id;
  5. private String username;
  6. transient private String passwoord;
  7. private int age;
  8. public User() {
  9. System.out.println("调用无参数构造器");
  10. }
  11. public User(Long id, String username, String passwoord, int age) {
  12. this.id = id;
  13. this.username = username;
  14. this.passwoord = passwoord;
  15. this.age = age;
  16. System.out.println("调用有参数构造器");
  17. }
  18. ...
  19. }

1、实例demo

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

  1. public static void main(String[] args) {
  2. User user = new User();
  3. user.setId(1L);
  4. user.setUsername("lisi");
  5. user.setPasswoord("123456");
  6. ObjectOutputStream objectOutputStream = null;
  7. ObjectInputStream objectInputStream = null;
  8. try {
  9. // 将 user对象序列化到 user.txt文件(二进制数据)
  10. File file = new File("D:/E/user.txt");
  11. objectOutputStream = new ObjectOutputStream(new FileOutputStream(file));
  12. objectOutputStream.writeObject(user);
  13. // 从user.txt文件反序列化输出 user对象
  14. objectInputStream = new ObjectInputStream(new FileInputStream(file));
  15. User user1 = (User) objectInputStream.readObject();
  16. System.out.println(user1);
  17. } catch (Exception e) {
  18. e.printStackTrace();
  19. } finally {
  20. if(objectOutputStream != null){
  21. try {
  22. objectOutputStream.close();
  23. } catch (IOException e) {
  24. e.printStackTrace();
  25. }
  26. }
  27. }
  28. }

20200722210332335.png

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

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

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

1、实例demo

  1. public class User implements Externalizable {
  2. private static final long serialVersionUID = 5301525230834919001L;
  3. private Long id;
  4. private String username;
  5. transient private String passwoord;
  6. private int age;
  7. @Override
  8. public void writeExternal(ObjectOutput out) throws IOException {
  9. // 需要序列化的字段
  10. out.writeObject(username);
  11. out.writeObject(passwoord);
  12. out.writeInt(age);
  13. }
  14. @Override
  15. public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
  16. // 被序列化的字段,注意:必须和序列化的字段顺序保持一致
  17. username = (String) in.readObject();
  18. passwoord = (String) in.readObject();
  19. age = in.readInt();
  20. }
  21. ...
  22. }

main 中流处理同上

20200722210137661.png

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

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

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

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

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

  1. public class User implements Serializable {
  2. private static final long serialVersionUID = 5301525230834919001L;
  3. private Long id;
  4. private String username;
  5. transient private String passwoord;
  6. private int age;
  7. private User() {
  8. System.out.println("调用无参数构造器");
  9. }
  10. public static final User INSTANCE = new User();
  11. public static User getInstance(){
  12. return INSTANCE;
  13. }
  14. // 在该方法中直接返回类的单例对象
  15. // public Object readResolve(){
  16. // return INSTANCE;
  17. // }
  18. ...
  19. }

实例demo

  1. public static void main(String[] args) {
  2. User user = User.getInstance();
  3. user.setId(1L);
  4. user.setUsername("lisi");
  5. user.setPasswoord("123456");
  6. ObjectOutputStream objectOutputStream = null;
  7. ObjectInputStream objectInputStream = null;
  8. try {
  9. // 将 user对象序列化到 user.txt文件(二进制数据)
  10. File file = new File("D:/E/user.txt");
  11. objectOutputStream = new ObjectOutputStream(new FileOutputStream(file));
  12. objectOutputStream.writeObject(user);
  13. // 从user.txt文件反序列化输出 user对象
  14. objectInputStream = new ObjectInputStream(new FileInputStream(file));
  15. User user1 = (User) objectInputStream.readObject();
  16. System.out.println(user1);
  17. System.out.println("反序列化后的对象是不是前面的单例user对象:" + (user == user1));
  18. } catch (Exception e) {
  19. e.printStackTrace();
  20. } finally {
  21. if(objectOutputStream != null){
  22. try {
  23. objectOutputStream.close();
  24. } catch (IOException e) {
  25. e.printStackTrace();
  26. }
  27. }
  28. }
  29. }
  30. ![20200722212013936.png][]

三、序列化的安全性

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

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

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

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

1、使用 SealedObject, 算法用 DESede

  1. public static void main(String[] args) throws IOException {
  2. User user = User.getInstance();
  3. user.setId(1L);
  4. user.setUsername("lisi");
  5. user.setPasswoord("123456");
  6. ObjectOutputStream objectOutputStream = null;
  7. ObjectInputStream objectInputStream = null;
  8. ObjectOutputStream objoutEncryptKey = null;
  9. ObjectInputStream objGetEncryptKey = null;
  10. try {
  11. // 将 user对象序列化到 user.txt文件(二进制数据),加密
  12. File file = new File("D:/E/user.txt");
  13. /* objectOutputStream = new ObjectOutputStream(new FileOutputStream(file));
  14. KeyGenerator keyGenerator = KeyGenerator.getInstance("DESede");
  15. // 获取秘钥
  16. SecretKey encryptKey = keyGenerator.generateKey();
  17. System.out.println(encryptKey);
  18. Cipher cipher = Cipher.getInstance("DESede");
  19. cipher.init(Cipher.ENCRYPT_MODE, encryptKey);
  20. SealedObject sealedObject = new SealedObject(user, cipher);
  21. objectOutputStream.writeObject(sealedObject);
  22. //将秘钥保存
  23. objoutEncryptKey = new ObjectOutputStream(new FileOutputStream(new File("D:/E/encryptKey.txt")));
  24. objoutEncryptKey.writeObject(encryptKey);*/
  25. // 从user.txt文件反序列化输出 user对象,解密
  26. objectInputStream = new ObjectInputStream(new FileInputStream(file));
  27. SealedObject sealedObjectResult = (SealedObject) objectInputStream.readObject();
  28. objGetEncryptKey = new ObjectInputStream(new FileInputStream(new File("D:/E/encryptKey.txt")));
  29. SecretKey openKey = (SecretKey) objGetEncryptKey.readObject();
  30. User user1 = (User) sealedObjectResult.getObject(openKey);
  31. System.out.println(user1);
  32. } catch (Exception e) {
  33. e.printStackTrace();
  34. } finally {
  35. objectOutputStream.close();
  36. objectInputStream.close();
  37. objoutEncryptKey.close();
  38. objGetEncryptKey.close();
  39. }
  40. }
  41. ![20200722220801761.png][]

2、使用 SignedObject, 算法用 DSA

  1. public static void main(String[] args) throws IOException {
  2. User user = User.getInstance();
  3. user.setId(1L);
  4. user.setUsername("lisi");
  5. user.setPasswoord("123456");
  6. ObjectOutputStream objectOutputStream = null;
  7. ObjectInputStream objectInputStream = null;
  8. ObjectOutputStream objoutEncryptKey = null;
  9. ObjectInputStream objGetEncryptKey = null;
  10. try {
  11. // 将 user对象序列化到 user.txt文件(二进制数据),私钥加密
  12. File file = new File("D:/E/user.txt");
  13. objectOutputStream = new ObjectOutputStream(new FileOutputStream(file));
  14. KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("DSA");
  15. keyPairGenerator.initialize(1024);
  16. KeyPair keyPair = keyPairGenerator.generateKeyPair();
  17. // 获取秘钥对
  18. PrivateKey privateKey = keyPair.getPrivate();
  19. PublicKey publicKey = keyPair.getPublic();
  20. Signature signature = Signature.getInstance("DSA");
  21. SignedObject signedObject = new SignedObject(user, privateKey, signature);
  22. objectOutputStream.writeObject(signedObject);
  23. //将公钥保存,供客户端使用
  24. objoutEncryptKey = new ObjectOutputStream(new FileOutputStream(new File("D:/E/publicKey.txt")));
  25. objoutEncryptKey.writeObject(publicKey);
  26. // 从user.txt文件反序列化输出 user对象,公钥解密
  27. objectInputStream = new ObjectInputStream(new FileInputStream(file));
  28. SignedObject signedObjectResult = (SignedObject) objectInputStream.readObject();
  29. objGetEncryptKey = new ObjectInputStream(new FileInputStream(new File("D:/E/publicKey.txt")));
  30. PublicKey openKey = (PublicKey) objGetEncryptKey.readObject();
  31. //方法verify,判断盒子里的对象有没有没篡改
  32. Signature verifySignature = Signature.getInstance("DSA");
  33. if (signedObjectResult.verify(openKey, verifySignature)) {
  34. //内容没被篡改
  35. System.out.println("内容没被篡改");
  36. User user1 = (User) signedObjectResult.getObject();
  37. System.out.println(user1);
  38. }else{
  39. System.out.println("内容被篡改过!");
  40. }
  41. } catch (Exception e) {
  42. e.printStackTrace();
  43. } finally {
  44. objectOutputStream.close();
  45. objectInputStream.close();
  46. objoutEncryptKey.close();
  47. objGetEncryptKey.close();
  48. }
  49. }

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

发表评论

表情:
评论列表 (有 0 条评论,206人围观)

还没有评论,来说两句吧...

相关阅读

    相关 Java对象序列序列

    一、序列化和反序列化介绍 在Java中,对象的序列化与反序列化被广泛应用到RMI(远程方法调用)及网络传输中。 序列化:指将Java对象数据保存到磁盘文件中或者传递给其