第74条:谨慎地实现Serializable接口
- 一个类实现Serializable接口并被发布,就大大降低了“改变这个类的实现”的灵活性。
- 如果一个类实现了Serializable接口,它的字节流编码(或者说序列化形式)就变成了它的导出的API的一部分,以后发布代码就要保持这种序列化形式。如果采用的是默认的序列化形式,这个类中私有的和包级私有的实例域都将变成API的一部分,这不符合“最低限度地访问域”的准则。
- 如果使用了默认的序列化形式,以后又要改变这个类的内部表示法,可能会导致序列化形式不兼容。改变内部表示法时可以用ObjectOutputStream.putFields和ObjectInputStream.readFields来维持原来的序列化形式,但是做起来困难。
- 序列化会使类的演变受到限制,例如受到序列版本UID的影响。每个序列化的类都有一个唯一标识与它相关联。如果没有指定serialVersionUID,系统会调用复杂的运算过程在运行时产生这个标识。如果类被修改了,自动生成的的序列版本UID会发生改变,兼容性将会遭到破坏。
- 实现Serializable增加了出现Bug和安全漏洞的可能性。反序列化机制是一个隐藏的构造器,反序列化过程必须要保证所有“由真正的构造器建立起来的约束关系”,并且不允许攻击者访问正在构造过程中的对象的内部信息。依靠默认的的反序列化机制,很容易使对象的约束关系遭到破坏,以及遭受到非法访问(见第76条)。
- 实现Serializable接口,随着发行新的版本,相关的测试负担也增加了。要测试新旧类“序列化-反序列化”过程成功(二进制兼容性),也要测试结果产生的对象真正是原始对象的复制品(语义兼容)。
- 如果一个类将要加入到某个框架中,并且该框架依赖于序列化来实现对象传输或者持久化,这个类就要实现序列化。
- 值类(如Date和BigInteger)应该实现Serializable接口,大多数的集合类也应该如此。代表活动实体的类,比如线程池,一般不应该实现Serializable。
- 为了继承而设计的类应该尽可能少地去实现Serializable接口,用户的接口也应该尽可能少地继承Serializable接口。
- 在为了继承而设计的类中,真正实现了Serializable接口的有Throwable、Component、HttpServlet类。因为Throwable类实现了Serializable接口,所以RMI(即Remote Method Invoke 远程方法调用)的异常可以从服务器端传到客户端。Component实现了Serializable接口,因此GUI可以被发送、保存和恢复。HttpServlet实现了Serializable接口,因此会话状态可以被缓存。
- 反序列化时,如果类有一些约束条件,当类的实例域被初始化成它们的默认值(整数类型为0,boolean为false,对象引用类型为null)。通过添加readObjectNoData方法来修改默认值。
private void readObjectNoData() throws InvalidObjectException { throw new InvalidObjectException(“Stream data required”); }
- 对于为继承而设计的不可序列化的类,应该提供一个无参构造器。如果类是有状态的,必须要提供参数,即不能使用无参构造器。则要增加一个受保护的无参构造器、一个初始化方法和状态检测方法,初始化方法和正常的构造器具有相同的参数。调用公有及受保护的方法前,要先调用状态检测方法确保其已初始化。
1 import java.util.concurrent.atomic.AtomicReference; 2 3 /** 4 * 不可序列化但可以扩展的类 5 * Created by itlivemore on 17-7-2. 6 */ 7 public class AbstractFoo { 8 private int x, y; // 状态参数 9 10 /*表示类状态的枚举*/ 11 private enum State { 12 NEW, INITIALIZING, INITIALIZED 13 } 14 15 /*对象的状态,是一个原子引用*/ 16 private final AtomicReference<State> init = new AtomicReference<>(State.NEW); 17 18 /*正常构造器*/ 19 public AbstractFoo(int x, int y) { 20 initalize(x, y); 21 } 22 23 /*提供一个无参构造器*/ 24 protected AbstractFoo() { 25 } 26 27 /*初始化方法,参数同正常的构造器*/ 28 protected final void initalize(int x, int y) { 29 /*public final boolean compareAndSet(V expect, V update) 30 set的时候进行对比判断,如果当前值和expect相等,则设置为update值*/ 31 if (!init.compareAndSet(State.NEW, State.INITIALIZING)) { 32 throw new IllegalStateException("Already initialized"); 33 } 34 this.x = x; 35 this.y = y; 36 init.set(State.INITIALIZED); 37 } 38 39 public int getX() { 40 // 调用方法前要检查对象状态 41 checkInit(); 42 return x; 43 } 44 45 public int getY() { 46 checkInit(); 47 return y; 48 } 49 50 /*检查对象状态*/ 51 private void checkInit() { 52 if (init.get() != State.INITIALIZED) { 53 throw new IllegalStateException("Uninitialized"); 54 } 55 } 56 } 57 /** 58 * 继承不可序列化父类,本身需要序列化 59 * Created by itlivemore on 17-7-2. 60 */ 61 public class Foo extends AbstractFoo implements Serializable { 62 private static final long serialVersionUID = 1672997997555918326L; 63 64 private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException { 65 stream.defaultReadObject(); 66 int x = stream.readInt(); 67 int y = stream.readInt(); 68 initalize(x, y); 69 } 70 71 private void writeObject(ObjectOutputStream stream) throws IOException { 72 stream.defaultWriteObject(); 73 stream.writeInt(getX()); 74 stream.writeInt(getY()); 75 } 76 77 public Foo(int x, int y) { 78 super(x, y); 79 } 80 }
- 内部类不应该实现Serializable。它们使用编译器产生的合成域来保存指向外围实例引用,以及保存来自外围作用域的局部变量的值。这些域怎么保存没有明确定义。因此,内部类的默认序列化形式是定义不清楚的。静态成员类可以实现Serializable。
第75条:考虑使用自定义的序列化形式
- 如果一个对象的物理表示法等同于它的逻辑内容,可能就适合于使用默认的序列化形式。
- 使用默认的序列化形式通常还必须提供一个readObject方法保证约束关系和安全性。
- 当一个对象的物理表示法与它的逻辑数据内容有实质性区别的时,使用默认的序列化形式会有以下4个缺点:
- 它使这个类的导出API永远地束缚在该类的内部表示法上。
- 它会消耗过多空间。会保存不必保存的实现细节。
- 它会消耗过多时间。图遍历过程。
- 它会引起栈溢出。默认的序列化过程要对对象图执行一次递归遍历。
- 使用transient(瞬时的)修饰的实例域将从一个类的默认序列化形式中省略掉。反序列化时,这些值将被被始化为它们的默认值:对于引用域,默认值为null;对于数值基本域,默认值为0;对于boolean域,默认值为false。如果修改这些默认值,就必须提供一个readObject方法,它首先调用defaultReadObject,然后把这些transient域恢复为可接受的值。另一种方法是,这些域可以被延迟到第一次被使用的时候才真正被初始化。
- 如果所有的的实例域都是瞬时的,还是推荐在writeObject方法中调用defaultWriteObject,readObject方法中调用defaultReadObject。如果一个实例在前一个版本中没有序列化,在后一个版本中使增加了序列化。如果旧方法没有使用defaultReadObject,在前一个版本中反序列化过程将失败。
- 虽然writeObject和readObject方法是私有的,但也是有文档的。域中使用的是@serial注解,方法中使用@serialData注解。
- 无论是否使用默认的序列化形式,如果在读取整个对象状态的任何其他方法上强制任何同步,则也必须在对象序列化上强制这种同步。在writeObject方法加上synchronized,要注意防止死锁。
- 为每个可序列化的类声明一个显式的序列版本UID。这样可以避免序列版本UID成为潜在的不兼容根源,同时提高性能。如果要为一个没有序列版本UID的类增加UID,则应用旧版本的代码生成UID,否则可能会有兼容性问题。
第76条:保护性地编写readObject方法
- 不严格地说,readObject是一个“用字节流作为唯一参数”的构造器。字节流可以伪造,反序列化之后的对象可以无效,如要求开始日期必须小于结束日期,但是反序列化后的对象相反了。而且字节流中可能包含恶意代码,使反序列化之后得到的对象受到攻击。
- 在readObject反序列化之后,检查对象域的有效性,如检查开始日期是否小于结束日期。
- 当一个对象被反序列化的时候,对于客户端不应该拥有的对象引用,如果哪个域包含了这样的对象引用,就必须要做保护性拷贝。在readObject方法中,必须要对这些域进行保护性拷贝。拷贝避免使用clone方法。保护性拷贝要求域不能是final域。
- 保持性拷贝是在有效性检查之前进行的。
- 在Java1.4发行版中,为了阻止恶意的对象引用攻击,同时节省保护性拷贝的开销,在ObjectOutputSteam中增加了writeUnshared和readUnshared方法,但是都容易受到复杂的攻击,不建议使用。
- readObject方法不可以调用可被覆盖的方法,无论是直接调用还是间接调用都不可以。如果违反了这条规则,并且覆盖了该方法,被覆盖的方法将在的状态被反序列化之前先运行。程序很可能会失败。
- 编写readObject方法的一些建议:
- 对于对象引用域必须保持为私有的类,要保护性地拷贝这些域中的每一个对象。不可变类的可变组件就属于这一类。
- 对于任何约束条件,如果检查失败,则抛出一个InvalidObjectException异常。这些检查动作应该跟在所有的保护性拷贝之后。
- 如果整个对象图在被反序列化之后必须进行验证,就应该使用ObjectInputValidation接口(通过ObjectInputStream的registerValidation方法添加ObjectInputValidation接口的实现,接口有一个方法validateObject,用来验证反序列化之后得到的对象是否合法)。
- 无论是直接方式还是间接方式,都不要调用类中任何可能被覆盖的方法。
ObjectInputValidation参考:http://www.infoq.com/cn/articles/cf-java-object-serialization-rmi/
第77条:对于实例控制,枚举类型优先于readResolve
- 单例模式中,单例对象序列化后,再反序列化后又会重新创建新的实例,新建的实例不同于该类初始化时的创建的实例。这样就违背了单例模式。
- readResolve允许用readObject创建的实例代替另一个实例。readResolve在反序列化之后被调用,然后该方法返回的对象引用将被返回,取代新建的对象。指向新创建对象的引用不需要再被保留,因此立即成为垃圾回收的对象。
1 /** 2 * 反序列化破坏单例模式 3 * Created by itlivemore on 17-7-2. 4 */ 5 public class Elvis implements Serializable { 6 public static final Elvis INSTANCE = new Elvis(); 7 private static final long serialVersionUID = -7029328840767027456L; 8 9 private Elvis() { 10 } 11 12 /*如果删除这个方法,程序返回false,有这个方法,程序返回true*/ 13 private Object readResolve() { 14 return INSTANCE; 15 } 16 17 public static void main(String[] args) throws Exception { 18 String filePath = "test.txt"; 19 // 序列化 20 FileOutputStream fileOutputStream = new FileOutputStream(filePath); 21 ObjectOutputStream outputStream = new ObjectOutputStream(fileOutputStream); 22 outputStream.writeObject(Elvis.INSTANCE); 23 outputStream.flush(); 24 outputStream.close(); 25 fileOutputStream.close(); 26 27 // 反序列化 28 FileInputStream fileInputStream = new FileInputStream(filePath); 29 ObjectInputStream inputStream = new ObjectInputStream(fileInputStream); 30 Elvis elvis2 = (Elvis) inputStream.readObject(); 31 inputStream.close(); 32 fileInputStream.close(); 33 // 判断是否同一对象 34 System.out.println(Elvis.INSTANCE == elvis2); 35 }
- 如果依赖readResolve进行实例控制,带有对象引用类型的所有实例域则必须声明为transient的。否则就有可能在readResolve方法被运行之前,攻击者保护反序列化对象的引用。
- 将一个可序列化的实例受控的类编写成枚举,就可以保证除了所声明的常量之外,不会有别的实例。但是如果必须编写可序列化的实例受控的类,它的实例在编译时还不知道,就无法将类表示成一个枚举类型。
- readResolve的可访问性很重要。如果把readResolve方法放在一个final类上,它就应该是私有的。非final类上,如果readResolve是受保护的或者公有的,并且子类没有覆盖它,对序列化过的子类实例进行反序列化时,就会产生一个超类实例,这样有可能导致ClassCastException异常。
第78条:考虑用序列化代理代替序列化实例
- 序列化代理步骤
- 为可序列化的类设计一个私有的静态嵌套类。这个类有一个单独的构造器,参数类型就是外围类。构造器只从它的参数中复制数据,不需要进行一致性检查和保护性拷贝。和外围一样都实现Serializable接口。
- 外围类添加writeReplace()方法,这个方法返回嵌套类实例。这个方法会在序列化之前调用,将外围类的实例转变成了它的序列化代理。
- 有了writeReplace方法之后,序列化系统永远不会产生外围类的序列化实例,为防止攻击者伪造,在外围类中添加readObject方法,方法直接抛出异常。
- 在嵌套类中添加readResolve方法,返回一个逻辑上相当的外围类。这个方法导致在反序列化时将序列化代理转回外围类的实例。
- 第39条中的不可变类Period使用序列化代理做成序列化。
1 /** 2 * 第39条中的不可变类Period使用序列化代理做成序列化。 3 * Created by itlivemore on 17-7-2. 4 */ 5 public final class Period implements Serializable { 6 private final Date start; 7 private final Date end; 8 9 public Period(Date start, Date end) { 10 this.start = new Date(start.getTime()); 11 this.end = new Date(end.getTime()); 12 if (this.start.compareTo(this.end) > 0) 13 throw new IllegalArgumentException(start + " after " + end); 14 } 15 16 public Date start() { 17 return new Date(start.getTime()); 18 } 19 20 public Date end() { 21 return new Date(end.getTime()); 22 } 23 24 public String toString() { 25 return start + " - " + end; 26 } 27 28 // 私有静态嵌套类 29 private static class SerializationProxy implements Serializable { 30 private final Date start; 31 private final Date end; 32 33 // 构造器参数是外围类 34 SerializationProxy(Period p) { 35 this.start = p.start; 36 this.end = p.end; 37 } 38 39 private static final long serialVersionUID = 234098243823485285L; 40 41 // 返回外围类实例 42 private Object readResolve() { 43 return new Period(start, end); // Uses public constructor 44 } 45 } 46 47 // writeReplace方法,返回嵌套类 48 private Object writeReplace() { 49 return new SerializationProxy(this); 50 } 51 52 // 防止攻击者伪造外围类序列化的字节流进行攻击 53 private void readObject(ObjectInputStream stream) 54 throws InvalidObjectException { 55 throw new InvalidObjectException("Proxy required"); 56 } 57 }
- 使用序列化代理的好处
- 像保护性拷贝方法一样,可以阻止伪字节流的攻击以及内部域的盗用攻击。
- 可以允许外围类的域为final的。
- 这种方法不需要太费心思。
- 允许反序列化实例有着与原始序列化实例不同的类。实例是EnumSet。
- 下面是EnumSet的私有静态嵌套类
1 /** 2 * This class is used to serialize all EnumSet instances, regardless of 3 * implementation type. It captures their "logical contents" and they 4 * are reconstructed using public static factories. This is necessary 5 * to ensure that the existence of a particular implementation type is 6 * an implementation detail. 7 * 8 * @serial include 9 */ 10 private static class SerializationProxy <E extends Enum<E>> 11 implements java.io.Serializable 12 { 13 /** 14 * The element type of this enum set. 15 * 16 * @serial 17 */ 18 private final Class<E> elementType; 19 20 /** 21 * The elements contained in this enum set. 22 * 23 * @serial 24 */ 25 private final Enum<?>[] elements; 26 27 SerializationProxy(EnumSet<E> set) { 28 elementType = set.elementType; 29 elements = set.toArray(ZERO_LENGTH_ENUM_ARRAY); 30 } 31 32 // instead of cast to E, we should perhaps use elementType.cast() 33 // to avoid injection of forged stream, but it will slow the implementation 34 @SuppressWarnings("unchecked") 35 private Object readResolve() { 36 EnumSet<E> result = EnumSet.noneOf(elementType); 37 for (Enum<?> e : elements) 38 result.add((E)e); 39 return result; 40 } 41 42 private static final long serialVersionUID = 362491234563181265L; 43 }
EnumSet的底层有两个实现,如果底层枚举类型小于等于64个元素,则用RegularEnumSet,否则就使用JumboEnumSet。现在考虑这种情况:如果序列化一个枚举集合,它的枚举类型有60个元素,然后再给这上枚举类型再增加5个元素,之后反序列化这个枚举集合。当它被序列化的时候,是一个RegularEnumSet,反序列化之后将会是一个JumboEnumSet,这是我们想要的结果。
- 序列化代理的局限性:
- 不能与可以被客户端扩展的类兼容。
- 不能与对象图中包含循环的某些类兼容,如果从一个对象的序列化代理的readResolve方法内部调用这个对象中的方法,就会得到一个ClassCastException异常,因为还没有这个对象,只有它的序列化代理。
- 比保护性拷贝相比可能会增加开销。