本文目录java
前言
Oracle 公司计划废除 Java 中的古董:序列化技术,由于它带来了许多严重的安全问题(如序列化存储安全、反序列化安全、传输安全等),据统计,至少有3分之1的漏洞是序列化带来的,这也是 1997 年诞生序列化技术的一个巨大错误。可是,序列化技术如今在 Java 应用中无处不在,特别是如今的持久化框架和分布式技术中,都须要利用序列化来传输对象,如:Hibernate、Mybatis、Java RMI、Dubbo等,即对象要存储或者传输都不可避免要用到序列化技术,因此删除序列化技术将是一个长期的计划。程序员
你在实际工做中可能会很难有机会真正用到Java自带的序列化技术了,工业界通常也会选择一些更安全的对象编解码方案例如Google的Protobuf等。因此,对于Java序列化,咱们没必要再投入过多的精力学习,你花20分钟读完本文所掌握的知识,对于应付平常源码阅读中遇到的遗留的Java序列化技术应该是足够了。算法
1、序列化是什么
序列化机制容许将实现序列化的Java对象转换成字节序列,这些字节序列能够保存在磁盘上,或经过网络传输,以备之后从新恢复成原来的对象。序列化机制使得对象能够脱离程序的运行而独立存在。编程
本文中用序列化来简称整个序列化和反序列化机制。 浏览器
2、为何须要序列化
全部可能在网络上传输的对象的类都应该是可序列化的,不然程序将会出现异常,好比RMI(Remote Method Invoke,即远程方法调用,是JavaEE的基础)过程当中的参数和返回值;全部须要保存到磁盘里的对象的类都必须可序列化,好比Web应用中须要保存到HttpSession或ServletContext属性的Java对象。安全
由于序列化是RMI过程的参数和返回值都必须实现的机制,而RMI又是Java EE技术的基础——全部的分布式应用经常须要跨平台、跨网络,因此要求全部传递的参数、返回值必须实现序列化。所以序列化机制是Java EE平台的基础。一般建议:程序建立的每一个JavaBean类都实现Serializable。服务器
3、序列化怎么用
若是一个类的对象须要序列化,那么在Java语法层面,这个类须要:网络
下面咱们经过代码示例来看看序列化最基本的用法。咱们建立了Person类,其拥有两个基本类型的属性,并实现了Serializable接口。testSerialize方法用来测试序列化,testDeserialize方法用来测试反序列化。框架
1 import org.junit.Test; 2 3 import java.io.*; 4 5 public class SerializableTest { 6 7 @Test 8 public void testSerialize() { 9 Person one = new Person(12, 148.2); 10 Person two = new Person(35, 177.8); 11 12 try (ObjectOutputStream output = 13 new ObjectOutputStream(new FileOutputStream("Person.txt"))) { 14 output.writeObject(one); 15 output.writeObject(two); 16 } catch (IOException e) { 17 e.printStackTrace(); 18 } 19 } 20 21 @Test 22 public void testDeserialize() { 23 24 try (ObjectInputStream input = 25 new ObjectInputStream(new FileInputStream("Person.txt"))) { 26 Person one = (Person) input.readObject(); 27 Person two = (Person) input.readObject(); 28 29 System.out.println(one); 30 System.out.println(two); 31 } catch (IOException e) { 32 e.printStackTrace(); 33 } catch (ClassNotFoundException e) { 34 e.printStackTrace(); 35 } 36 } 37 } 38 39 class Person implements Serializable { 40 int age; 41 double height; 42 43 public Person(int age, double height) { 44 this.age = age; 45 this.height = height; 46 } 47 48 @Override 49 public String toString() { 50 return "Person{" + 51 "age=" + age + 52 ", height=" + height + 53 '}'; 54 } 55 }
4、序列化深度探秘
若是某个类须要支持序列化功能,那么它必须实现Serializable接口,不然会报 java.io.NotSerializableException。Serializable接口是一个标志性接口(Marker Interface),也就是说,该接口并不包含任何具体的方法,是一个空接口,仅仅用来判断该类是否可以序列化。JDK8中Serializable接口的源码以下:dom
1 package java.io; 2 3 public interface Serializable { 4 }
在 ObjectOutputStream.java 的 writeObject0 方法中,咱们确实能够看到对对象是否实现了 Serializable接口进行了验证(第15行),不然会抛出 NotSerializableException 异常(第22行)。
1 private void writeObject0(Object obj, boolean unshared) 2 throws IOException 3 { 4 boolean oldMode = bout.setBlockDataMode(false); 5 depth++; 6 try { 7 ... 8 // remaining cases 9 if (obj instanceof String) { 10 writeString((String) obj, unshared); 11 } else if (cl.isArray()) { 12 writeArray(obj, desc, unshared); 13 } else if (obj instanceof Enum) { 14 writeEnum((Enum<?>) obj, desc, unshared); 15 } else if (obj instanceof Serializable) { 16 writeOrdinaryObject(obj, desc, unshared); 17 } else { 18 if (extendedDebugInfo) { 19 throw new NotSerializableException( 20 cl.getName() + "\n" + debugInfoStack.toString()); 21 } else { 22 throw new NotSerializableException(cl.getName()); 23 } 24 } 25 } finally { 26 depth--; 27 bout.setBlockDataMode(oldMode); 28 } 29 }
在第三部分“序列化怎么用”部分的示例中,Person类的字段全都是基本类型,咱们知道基本类型其地址中直接存放的就是它的值,那若是是引用类型呢?引用类型其地址中存放的是指向堆内存中的一个地址,难道序列化时就是将这个地址进行了保存吗?显然,这是说不通的,由于对象的内存地址是可变的,在同一系统的不一样运行时刻或者是不一样系统中,对象的地址确定是不一样的,所以,序列化内存地址没有意义。
若是被序列化对象的字段是引用,那么要求该引用的类型也是可序列化实现了Serializable接口的,不然没法序列化。当对某个对象进行序列化时,系统会自动把该对象的全部Field依次进行序列化,若是某个Field引用到另外一个对象,则被引用的对象也会被序列化;若是被引用的对象的Field也引用了其余对象,则被引用的对象也会被序列化,这种状况被称为递归序列化。
若是对象A和对象B同时引用了对象C,那么,当序列化对象A和对象B时,对象C会被序列化两次吗?答案显然是不会。
要解释这个问题,就不得不说一下Java序列化的基本算法了:
在一些特殊的场景下,若是一个类里包含的某些Field值是敏感信息,例如银行帐户信息等,这时不但愿系统将该Field值进行序列化;或者某个Field的类型是不可序列化的,所以不但愿对该Field进行递归序列化,以免引起java.io.NotSerializableException异常。
此时,咱们就须要自定义序列化了。自定义序列化的经常使用方式有两种:
咱们先看第一种方式,使用transient关键字。transient关键字只能用于修饰Field,不可修饰Java程序中的其余成分。使用transient修饰的属性,java序列化时,会忽略掉此字段,因此反序列化出的对象,被transient修饰的属性是默认值。对于引用类型,值是null;基本类型,值是0;boolean类型,值是false。
下列代码中,咱们把People的height字段设置为transient,在反序列化时,可观察到输出为默认值0.0。
1 import org.junit.Test; 2 3 import java.io.*; 4 5 public class SerializableTest { 6 7 @Test 8 public void testSerialize() { 9 Person one = new Person(12, 156.6); 10 Person two = new Person(16, 177.7); 11 12 try (ObjectOutputStream output = 13 new ObjectOutputStream(new FileOutputStream("Person.txt"))) { 14 output.writeObject(one); 15 output.writeObject(two); 16 } catch (IOException e) { 17 e.printStackTrace(); 18 } 19 } 20 21 @Test 22 public void testDeserialize() { 23 24 try (ObjectInputStream input = 25 new ObjectInputStream(new FileInputStream("Person.txt"))) { 26 Person one = (Person) input.readObject(); 27 Person two = (Person) input.readObject(); 28 29 System.out.println(one); 30 System.out.println(two); 31 } catch (IOException e) { 32 e.printStackTrace(); 33 } catch (ClassNotFoundException e) { 34 e.printStackTrace(); 35 } 36 } 37 } 38 39 class Person implements Serializable{ 40 protected int age; 41 protected transient double height; 42 43 public Person() { 44 } 45 46 public Person(int age, double height) { 47 this.age = age; 48 this.height = height; 49 } 50 51 @Override 52 public String toString() { 53 return "Person{" + 54 "age=" + age + 55 ", height=" + height + 56 '}'; 57 } 58 }
程序输出:
Person{age=12, height=0.0} Person{age=16, height=0.0} Process finished with exit code 0
使用transient关键字修饰Field虽然简单、方便,但被transient修饰的Field将被彻底隔离在序列化机制以外,这样致使在反序列化恢复Java对象时没法取得该Field值。Java还提供了一种自定义序列化机制,经过这种自定义序列化机制可让程序控制如何序列化各Field,甚至彻底不序列化某些Field(与使用transient关键字的效果相同)。在序列化和反序列化过程当中须要特殊处理的类应该提供以下特殊签名的方法,这些特殊的方法用以实现自定义序列化。
private void writeObject(java.io.ObjectOutputStream out) throws IOException private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException; private void readObjectNoData() throws ObjectStreamException;
下面的示例代码中,咱们在writeObject方法中对Person的字段进行了简单的加密处理,在readObject方法中对其进行了相应的解密。
1 import org.junit.Test; 2 3 import java.io.*; 4 5 public class SerializableTest { 6 7 @Test 8 public void testSerialize() { 9 Person one = new Person(12, 156.6); 10 Person two = new Person(16, 177.7); 11 12 try (ObjectOutputStream output = 13 new ObjectOutputStream(new FileOutputStream("Person.txt"))) { 14 output.writeObject(one); 15 output.writeObject(two); 16 } catch (IOException e) { 17 e.printStackTrace(); 18 } 19 } 20 21 @Test 22 public void testDeserialize() { 23 24 try (ObjectInputStream input = 25 new ObjectInputStream(new FileInputStream("Person.txt"))) { 26 Person one = (Person) input.readObject(); 27 Person two = (Person) input.readObject(); 28 29 System.out.println(one); 30 System.out.println(two); 31 } catch (IOException e) { 32 e.printStackTrace(); 33 } catch (ClassNotFoundException e) { 34 e.printStackTrace(); 35 } 36 } 37 } 38 39 class Person implements Serializable{ 40 protected int age; 41 protected double height; 42 43 public Person() { 44 } 45 46 public Person(int age, double height) { 47 this.age = age; 48 this.height = height; 49 } 50 51 private void writeObject(java.io.ObjectOutputStream out) 52 throws IOException { 53 System.out.println("Encryption!"); 54 out.writeInt(age + 1); 55 out.writeDouble(height - 1); 56 } 57 private void readObject(java.io.ObjectInputStream in) 58 throws IOException, ClassNotFoundException { 59 System.out.println("Decryption!"); 60 this.age = in.readInt() - 1; 61 this.height = in.readDouble() + 1; 62 } 63 64 @Override 65 public String toString() { 66 return "Person{" + 67 "age=" + age + 68 ", height=" + height + 69 '}'; 70 } 71 }
被序列化对象具备继承关系时无非就两种状况,第一,该类具备子类,第二,该类具备父类。
当该类实现了Serializable接口且具备子类时,根据官方文档中的说明,其子类自然具备可被序列化的属性,不须要显式实现Serializable接口;。
All subtypes of a serializable class are themselves serializable.
当该类实现了Serializable接口且具备父类时,,该类的父类须要实现Serializable接口吗?在JDK8中Serializable接口的官方文档中有这样一段话:
1 /** 2 * ...... 3 * 4 * To allow subtypes of non-serializable classes to be serialized, the 5 * subtype may assume responsibility for saving and restoring the 6 * state of the supertype's public, protected, and (if accessible) 7 * package fields. The subtype may assume this responsibility only if 8 * the class it extends has an accessible no-arg constructor to 9 * initialize the class's state. It is an error to declare a class 10 * Serializable if this is not the case. The error will be detected at 11 * runtime. 12 * 13 * During deserialization, the fields of non-serializable classes will 14 * be initialized using the public or protected no-arg constructor of 15 * the class. A no-arg constructor must be accessible to the subclass 16 * that is serializable. The fields of serializable subclasses will 17 * be restored from the stream. 18 */
阅读文档咱们得知,为了使得不可序列化类的子类可以序列化,其子类必须担负起保存和恢复其超类的public、protected 和 package(if accessible)实例域的责任,且要求其父类必须有一个可访问的无参构造函数以使得在反序列化时可以初始化实例域。
咱们写代码验证一下,若是父类中没有可访问的无参构造函数会发生什么,注意Person类中没有无参构造函数。
1 import org.junit.Test; 2 3 import java.io.*; 4 5 public class SerializableTest { 6 7 @Test 8 public void testSerialize() { 9 Student one = new Student(12, 156.6, "1234"); 10 Student two = new Student(16, 177.7, "5678"); 11 12 try (ObjectOutputStream output = 13 new ObjectOutputStream(new FileOutputStream("Student.txt"))) { 14 output.writeObject(one); 15 output.writeObject(two); 16 } catch (IOException e) { 17 e.printStackTrace(); 18 } 19 } 20 21 @Test 22 public void testDeserialize() { 23 24 try (ObjectInputStream input = 25 new ObjectInputStream(new FileInputStream("Student.txt"))) { 26 Student one = (Student) input.readObject(); 27 Student two = (Student) input.readObject(); 28 29 System.out.println(one); 30 System.out.println(two); 31 } catch (IOException e) { 32 e.printStackTrace(); 33 } catch (ClassNotFoundException e) { 34 e.printStackTrace(); 35 } 36 } 37 } 38 39 class Person{ 40 protected int age; 41 protected double height; 42 43 public Person(int age, double height) { 44 this.age = age; 45 this.height = height; 46 } 47 48 @Override 49 public String toString() { 50 return "Person{" + 51 "age=" + age + 52 ", height=" + height + 53 '}'; 54 } 55 } 56 57 class Student extends Person implements Serializable{ 58 private String id; 59 60 public Student(int age, double height, String id) { 61 super(age, height); 62 this.id = id; 63 } 64 65 @Override 66 public String toString() { 67 return "Student{" + 68 "age=" + age + 69 ", height=" + height + 70 ", id='" + id + '\'' + 71 '}'; 72 } 73 }
程序输出产生异常:
java.io.InvalidClassException: Student; no valid constructor at java.io.ObjectStreamClass$ExceptionInfo.newInvalidClassException(ObjectStreamClass.java:150) at java.io.ObjectStreamClass.checkDeserialize(ObjectStreamClass.java:768) at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1775) at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1351) at java.io.ObjectInputStream.readObject(ObjectInputStream.java:371) at SerializableTest.testDeserialize(SerializableTest.java:26) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:497) ... Process finished with exit code 0
当咱们为Person类添加默认构造函数时:
1 class Person{ 2 protected int age; 3 protected double height; 4 5 public Person() { 6 } 7 8 public Person(int age, double height) { 9 this.age = age; 10 this.height = height; 11 } 12 13 @Override 14 public String toString() { 15 return "Person{" + 16 "age=" + age + 17 ", height=" + height + 18 '}'; 19 } 20 }
程序输出以下,咱们可观察到,父类中的字段都是默认值,只有子类中的字段获得了正确的序列化。出现这种状况的缘由是子类并无担负起序列化父类中字段的责任。
Student{age=0, height=0.0, id='1234'} Student{age=0, height=0.0, id='5678'} Process finished with exit code 0
为了解决上述问题,咱们须要借助上一节中学到的知识,使用自定义的序列化方法writeObject和readObject来主动将父类中的字段进行序列化。
1 import org.junit.Test; 2 3 import java.io.*; 4 5 public class SerializableTest { 6 7 @Test 8 public void testSerialize() { 9 Student one = new Student(12, 156.6, "1234"); 10 Student two = new Student(16, 177.7, "5678"); 11 12 try (ObjectOutputStream output = 13 new ObjectOutputStream(new FileOutputStream("Studnet.txt"))) { 14 output.writeObject(one); 15 output.writeObject(two); 16 } catch (IOException e) { 17 e.printStackTrace(); 18 } 19 } 20 21 @Test 22 public void testDeserialize() { 23 24 try (ObjectInputStream input = 25 new ObjectInputStream(new FileInputStream("Studnet.txt"))) { 26 Student one = (Student) input.readObject(); 27 Student two = (Student) input.readObject(); 28 29 System.out.println(one); 30 System.out.println(two); 31 } catch (IOException e) { 32 e.printStackTrace(); 33 } catch (ClassNotFoundException e) { 34 e.printStackTrace(); 35 } 36 } 37 } 38 39 class Person{ 40 protected int age; 41 protected double height; 42 43 public Person() { 44 } 45 46 public Person(int age, double height) { 47 this.age = age; 48 this.height = height; 49 } 50 51 @Override 52 public String toString() { 53 return "Person{" + 54 "age=" + age + 55 ", height=" + height + 56 '}'; 57 } 58 } 59 60 class Student extends Person implements Serializable{ 61 private String id; 62 63 public Student(int age, double height, String id) { 64 super(age, height); 65 this.id = id; 66 } 67 68 private void writeObject(java.io.ObjectOutputStream out) 69 throws IOException { 70 out.defaultWriteObject(); 71 out.writeInt(age); 72 out.writeDouble(height); 73 } 74 75 private void readObject(java.io.ObjectInputStream in) 76 throws IOException, ClassNotFoundException { 77 in.defaultReadObject(); 78 this.age = in.readInt(); 79 this.height = in.readDouble(); 80 } 81 82 @Override 83 public String toString() { 84 return "Student{" + 85 "age=" + age + 86 ", height=" + height + 87 ", id='" + id + '\'' + 88 '}'; 89 } 90 }
程序输出以下,能够看到彻底正确。
Student{age=12, height=156.6, id='1234'} Student{age=16, height=177.7, id='5678'} Process finished with exit code 0
5、serialVersionUID的做用及自动生成
咱们知道,反序列化必须拥有class文件,但随着项目的升级,class文件也会升级,序列化怎么保证升级先后的兼容性呢?
java序列化提供了一个private static final long serialVersionUID 的序列化版本号,只有版本号相同,即便更改了序列化属性,对象也能够正确被反序列化回来。若是反序列化使用的class的版本号与序列化时使用的不一致,反序列化会报InvalidClassException异常。下面是JDK 8中ArrayList的源码中的serialVersionUID。
1 public class ArrayList<E> extends AbstractList<E> 2 implements List<E>, RandomAccess, Cloneable, java.io.Serializable 3 { 4 private static final long serialVersionUID = 8683452581122892189L; 5 6 /** 7 * Default initial capacity. 8 */ 9 private static final int DEFAULT_CAPACITY = 10; 10 ... 11 }
序列化版本号可自由指定,若是不指定,JVM会根据类信息本身计算一个版本号,这样随着class的升级,就没法正确反序列化;不指定版本号另外一个明显隐患是,不利于jvm间的移植,可能class文件没有更改,但不一样jvm可能计算的规则不同,这样也会致使没法反序列化。
什么状况下须要修改serialVersionUID呢?分三种状况。
咱们在平常编程实践中,通常会选择使用IDE来自动生成serialVersionUID,这样能够最大化地减小重复的可能性。对于IntelliJ IDEA,自动生成serialVersionUID有三步:
6、序列化的缺点
Java序列化存在四个致命缺点,致使其不适用于网络传输:
在真正的生产环境中,通常会选择其它编解码框架,领先的跨平台结构化数据表示是 JSON 和 Protocol Buffers,也称为 protobuf。JSON 由 Douglas Crockford 设计用于浏览器与服务器通讯,Protocol Buffers 由谷歌设计用于在其服务器之间存储和交换结构化数据。JSON 和 protobuf 之间最显著的区别是 JSON 是基于文本的,而且是人类可读的,而 protobuf 是二进制的,但效率更高。
7、参考文献