[读书笔记]《Effective Java》第11章序列化

本文详细探讨了Java序列化中的各种最佳实践,包括谨慎实现Serializable接口、考虑使用自定义序列化形式、保护性地编写readObject方法等内容。文章强调了序列化对类演变的限制、如何确保对象的有效性和安全性,以及如何通过序列化代理增强序列化机制。

第74条:谨慎地实现Serializable接口

  • 一个类实现Serializable接口并被发布,就大大降低了“改变这个类的实现”的灵活性。
  1. 如果一个类实现了Serializable接口,它的字节流编码(或者说序列化形式)就变成了它的导出的API的一部分,以后发布代码就要保持这种序列化形式。如果采用的是默认的序列化形式,这个类中私有的和包级私有的实例域都将变成API的一部分,这不符合“最低限度地访问域”的准则。
  2. 如果使用了默认的序列化形式,以后又要改变这个类的内部表示法,可能会导致序列化形式不兼容。改变内部表示法时可以用ObjectOutputStream.putFields和ObjectInputStream.readFields来维持原来的序列化形式,但是做起来困难。
  3. 序列化会使类的演变受到限制,例如受到序列版本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个缺点:
  1. 它使这个类的导出API永远地束缚在该类的内部表示法上。
  2. 它会消耗过多空间。会保存不必保存的实现细节。
  3. 它会消耗过多时间。图遍历过程。
  4. 它会引起栈溢出。默认的序列化过程要对对象图执行一次递归遍历。
  • 使用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方法的一些建议:
  1. 对于对象引用域必须保持为私有的类,要保护性地拷贝这些域中的每一个对象。不可变类的可变组件就属于这一类。
  2. 对于任何约束条件,如果检查失败,则抛出一个InvalidObjectException异常。这些检查动作应该跟在所有的保护性拷贝之后。
  3. 如果整个对象图在被反序列化之后必须进行验证,就应该使用ObjectInputValidation接口(通过ObjectInputStream的registerValidation方法添加ObjectInputValidation接口的实现,接口有一个方法validateObject,用来验证反序列化之后得到的对象是否合法)。
  4. 无论是直接方式还是间接方式,都不要调用类中任何可能被覆盖的方法。

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条:考虑用序列化代理代替序列化实例

  • 序列化代理步骤
  1. 为可序列化的类设计一个私有的静态嵌套类。这个类有一个单独的构造器,参数类型就是外围类。构造器只从它的参数中复制数据,不需要进行一致性检查和保护性拷贝。和外围一样都实现Serializable接口。
  2. 外围类添加writeReplace()方法,这个方法返回嵌套类实例。这个方法会在序列化之前调用,将外围类的实例转变成了它的序列化代理。
  3. 有了writeReplace方法之后,序列化系统永远不会产生外围类的序列化实例,为防止攻击者伪造,在外围类中添加readObject方法,方法直接抛出异常。
  4. 在嵌套类中添加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 }
  • 使用序列化代理的好处
  1. 像保护性拷贝方法一样,可以阻止伪字节流的攻击以及内部域的盗用攻击。
  2. 可以允许外围类的域为final的。
  3. 这种方法不需要太费心思。
  4. 允许反序列化实例有着与原始序列化实例不同的类。实例是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,这是我们想要的结果。

  • 序列化代理的局限性:
  1. 不能与可以被客户端扩展的类兼容。
  2. 不能与对象图中包含循环的某些类兼容,如果从一个对象的序列化代理的readResolve方法内部调用这个对象中的方法,就会得到一个ClassCastException异常,因为还没有这个对象,只有它的序列化代理。
  3. 比保护性拷贝相比可能会增加开销。

 

转载于:https://www.cnblogs.com/itlivemore/p/7123320.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值