Java 自定义序列化

问:说说你对 Java 的 transient 关键字理解?

答:对于不需要被序列化的属性就可以通过加上 transient 关键字来处理。一旦属性被 transient 修饰就不再是对象持久化的一部分,该属性的内容在序列化后将无法获得访问,transient 关键字只能修饰属性变量成员而不能修饰方法和类(注意局部变量是不能被 transient 关键字修饰的),属性成员如果是引用类型也需要保证实现 Serializable 接口;此外在 Java 中对象的序列化可以通过实现两种接口来实现,若实现的是 Serializable 接口则所有的序列化将会自动进行,若实现的是 Externalizable 接口则没有任何东西可以自动序列化,需要在 writeExternal 方法中进行手工指定所要序列化的变量,这与是否被 transient 修饰无关。

问:对于 transient 修饰的属性如何在不删除修饰符的情况下让其可以序列化?

答:本题其实就是在考察实现 Serializable 接口情况下通过 writeObject() 与 readObject()方法进行自定义序列化的机制。具体实现如下:

        public class Item {
            public String name;
            public String id;

            public School() {
            }

            public School(String name, String id) {
                this.name = name;
                this.id = id;
            }
        }
        public class Info implements Serializable { 
            ...
            transient private Item item = null; 
            ...

            private void writeObject(ObjectOutputStream out) throws IOException {
                //invoke default serialization method 
                out.defaultWriteObject();
                if (item == null) {
                    Item = new Item();
                }
                out.writeObject(item.name);
                out.writeObject(item.id);
            }

            private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
                //invoke default serialization method 
                in.defaultReadObject();
                String name = (String) in.readObject();
                String id = (String) in.readObject();
                item = new Item(name, id);
            }
        }

上面在 writeObject() 方法中先调用了 ObjectOutputStream 的 defaultWriteObject() 方法,该方法会执行默认的序列化机制(忽略 item 字段),然后再调用 writeXXX() 方法显示地将每个字段写入到 ObjectOutputStream 中;readObject() 方法的作用是对象的读取,其原理与 writeObject() 方法相同。必须要注意的是 writeObject() 与 readObject() 都是 private 方法,其在 ObjectOutputStream 的 writeSerialData() 方法和 ObjectInputStream 的 readSerialData() 方法中通过反射进行调用。

问:简单说说 Externalizable 与 Serializable 有什么区别?

答:使用 transient 还是用 writeObject() 和 readObject() 方法都是基于 Serializable 接口的序列化;JDK 提供的另一个序列化接口 Externalizable 继承自 Serializable,使用该接口后基于 Serializable 接口的序列化机制就会失效(包括 transient,因为 Externalizable 不会主动序列化),当使用该接口时序列化的细节需要由我们自己去实现,另外使用 Externalizable 主动进行序列化时当读取对象时会调用被序列化类的无参构方法去创建一个新的对象,然后再将被保存对象的字段值分别填充到新对象中,所以实现 Externalizable 接口的类必须提供一个无参 public 的构造方法。关于 Externalizable 的实例如下:

        public class Info implements Externalizable {
            private String name;
            private int age;

            public Info() {
            }//必须定义无参构造方法

            public Info(String name, int age) {
                this.name = name;
                this.age = age;
            } //实现此方法反序列化时使用

            public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
                this.name = (String) in.readObject();
                this.age = in.readInt();
            } //实现此方法序列化时使用 

            public void writeExternal(ObjectOutput out) throws IOException {
                out.writeObject(this.name);
                out.writeInt(this.age);
            }
        }

特别注意使用 Externalizable 方式时必须提供无参构造方法,且 readExternal 方法必须按照与 writeExternal 方法写入值时相同的顺序和类型来读取属性值。

问:Serializable 序列化中自定义 readObjectNoData() 方法有什么作用?

答:这个方法主要用来保证通过继承扩容后对老版本的兼容性,适用场景如下:比如类 Person 被序列化到硬盘后存储为文件 old.txt,接着 Person 被修改继承自 Animal,为了保证用新的 Person 反序列化老版本 old.txt 文件且 Animal 类的成员有默认值则可以在 Animal 类中定义 readObjectNoData 方法返回成员的默认值,具体可以参见 ObjectInputStream 类中的 readSerialData 方法判断。

问:Java 序列化中 writeReplace() 方法有什么作用?

答:Serializable 除了提供 writeObject 和 readObject 标记方法外还提供了另外两个标记方法可以实现序列化对象的替换(即 writeReplace 和 readResolve),序列化类一旦实现了 writeReplace 方法后则在序列化时就会先调用 writeReplace 方法将当前对象替换成另一个对象(该方法会返回替换后的对象),接着系统将再次调用另一个对象的 writeReplace 方法,直到该方法不再返回另一个对象为止,程序最后将调用该对象的 writeObject() 方法来保存该对象的状态。通过下面例子可以说明上面这段话(AdapterBean 只是用来说明问题,实际应用中可能是转为 Map 或者列表等其他结构):

class AdapterBean implements Serializable {
    private String name;
    private int age;

    public AdapterBean(String name, int age) {
        this.name = name;
        this.age = age;
    }

    private void writeObject(java.io.ObjectOutputStream out) throws IOException {
        out.defaultWriteObject();
        System.out.println("AdapterBean writeObject.");
    }

    private void readObject(java.io.ObjectInputStream in) throws Exception {
        in.defaultReadObject();
        System.out.println("AdapterBean readObject.");
    }
}


class Person implements Serializable {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    private void writeObject(java.io.ObjectOutputStream out) throws IOException {
        out.defaultWriteObject();
        System.out.println("Person writeObject.");
    }

    private void readObject(java.io.ObjectInputStream in) throws Exception {
        in.defaultReadObject();
        System.out.println("Person readObject.");
    }

    private Object writeReplace() throws ObjectStreamException {
        System.out.println("Person writeReplace.");
        return new AdapterBean(name, age);
    }
}

public class Main {
    public static void main(String[] args) throws IOException, Exception {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("test.serial"));
        Person p = new Person("工匠若水", 27);
        oos.writeObject(p);
        oos.close();
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("test.serial"));
        System.out.println(((AdapterBean) ois.readObject()).toString());
    }
}

上面程序的输出结果如下:
Person writeReplace.
AdapterBean writeObject.
AdapterBean readObject.
AdapterBean@24459efb

特别说明,实现了 writeReplace 的序列化类就不要再实现 writeObject 了,因为该类的 writeObject 方法就不会被调用了;实现 writeReplace 的返回对象必须是可序列话的对象;通过 writeReplace 序列化替换的对象在反序列化中无论实现哪个方法都是无法恢复原对象的(即对象被彻底替换了),也就是说使用 ObjectInputStream 读取的对象只能是被替换后的对象,要想恢复只能在读取后自己手动构造恢复;所以 writeObject 只和 readObject 配合使用,一旦实现了 writeReplace 在写入时进行替换就不再需要 writeObject 和 readObject 了,故替换就是彻底的自定义了,比 writeObject 和 readObject 自定义更彻底。

问:Java 序列化中 readResolve() 方法有什么作用?

答:同上 Serializable 除过提供了 writeObject 和 readObject 标记方法外还提供了另外两个标记方法可以实现序列化对象的替换(即 writeReplace 和 readResolve),readResolve() 方法可以实现保护性复制整个对象,紧挨着序列化类实现的 readObject() 之后被调用,该方法的返回值会代替原来反序列化的对象,而原来序列化类中 readObject() 反序列化的对象将会立即丢弃。readObject() 方法在序列化单例类时尤其有用,单例序列化都应该提供 readResolve() 方法,这样才可以保证反序列化的对象依然正常。同理给个直观例子如下:

final class Singleton implements Serializable {
    private Singleton() {
    }

    private static final Singleton INSTANCE = new Singleton();

    public static Singleton getInstance() {
        return INSTANCE;
    }

    private Object readResolve() throws ObjectStreamException {
        return INSTANCE;
    }
}

public class Main {
    public static void main(String[] args) throws IOException, Exception {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("test.serial"));
        Singleton p = Singleton.getInstance();
        oos.writeObject(p);
        oos.close();
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("test.serial"));
        Singleton p1 = (Singleton) ois.readObject();
        System.out.println(p == p1);
    }
}

上面代码运行结果为 true,如果去掉 readResolve 实现则结果为 false。

所以 readResolve 方法可以保护性恢复对象(同时也可以替换对象),调用该方法之前会先调用序列化类的 readObject 反序列化得到对象,在该方法中可以正常通过 this 访问到刚才反序列化得到的对象内容,然后可以根据这些内容进行一定处理返回一个对象,所以其最重要的应用就是保护性恢复单例对象(当然使用枚举类的单例就天生支持此特性)。

问:Java 序列化存储传输为什么不安全?怎么解决?

答:因为序列化二进制格式完全编写在文档中且完全可逆,所以只需将二进制序列化流的内容转储到控制台就可以看清类及其包含的内容,故序列化对象中的任何 private 字段几乎都是以明文的方式出现在序列化流中,如果我们传输的序列化数据中途被截获,截获方通过反序列化就可以获得里面的数据(敏感数据的泄露),甚至对里面的数据进行修改然后发送给接收方(无法确保数据来源的安全性),从而产生了序列化安全问题。

要解决序列化安全问题的核心原理就是避免在序列化中传递敏感数据,所以可以使用关键字 transient 修饰敏感数据的变量,或者通过自定义序列化相关流程对数据进行签名加密机制再存储或者传输(最简单譬如女生年龄可以在序列化时进行移位操作,反序列化时进行反向移位复原操作,或者使用一些加密算法处理)。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,491评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,856评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,745评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,196评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,073评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,112评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,531评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,215评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,485评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,578评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,356评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,215评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,583评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,898评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,497评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,697评论 2 335

推荐阅读更多精彩内容

  • JAVA序列化机制的深入研究 对象序列化的最主要的用处就是在传递,和保存对象(object)的时候,保证对象的完整...
    时待吾阅读 10,825评论 0 24
  • 正如前文《Java序列化心得(一):序列化设计和默认序列化格式的问题》中所提到的,默认序列化方法存在各种各样的问题...
    登高且赋阅读 8,250评论 0 19
  • 官方文档理解 要使类的成员变量可以序列化和反序列化,必须实现Serializable接口。任何可序列化类的子类都是...
    狮_子歌歌阅读 2,376评论 1 3
  • 序列化的意义 1.永久存储某个jvm中运行时的对象。2.对象可以网络传输3.rmi调用都是以序列化的方式传输参数 ...
    炫迈哥阅读 637评论 0 0
  • 将一个对象编码成字节流称作将该对象「序列化」。相反,从字节流编码中重新构建对象被称作「反序列化」。一旦对象被「序列...
    Alent阅读 771评论 0 1