unifirst的技术博客

不断学习 不断进步

Java序列化

问题

最近在dubbo接口扩展上遇到了问题。dubbo的参数及返回对象,肯定是要可序列化的,即实现Serializable接口。需求是需要在接口参数中,加入一个字段,但是担心对原来的consumer产生影响,因此对java序列化进行了一下梳理测试。

顺便说下关于dubbo接口扩展碰到的这个问题,有几点收获:

  • 接口的传参,尽量用对象代替多个简单类型的参数,后者不便于加参数
  • 返回数据,同样尽量用对象代替简单类型
  • 更好的参数或返回数据扩展方案,应该是采用继承原有参数或返回类型的方式

序列化

java序列化,就是将java对象序列化为字节流,可以进行传递或者保存,在使用方对结果进行反序列化,从而获取到原来对象的属性值。

在需要将内存中对象保存到文件,或者直接传输对象时,会用到序列化。dubbo就是在provider和consumer之间传递对象数据。

类定义改变

回到最初的问题,其根本是java类定义在发送方发生改变后,接收方能否正确反序列化数据。

单元测试代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
class Person implements Serializable {

    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

class NewPerson implements Serializable {

    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

}

class ChildPerson extends Person {

    private int age;

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

@Test
public void testSerialize() throws IOException, ClassNotFoundException {
    Person parent = new Person();
    parent.setName("Parent");

    String parentSerFile = "parent.ser";
    String childSerFile = "child.ser";

    // 序列化parent到parentSerFile
    serialize(parent, parentSerFile);

    Person person = unSerialize(parentSerFile);
    System.out.println(person.getName()); // OK

//        NewPerson newPerson = unSerialize(parentSerFile); // java.lang.ClassCastException
//        System.out.println(newPerson.getName());

    // 序列化child到childSerFile
    ChildPerson child = new ChildPerson();
    child.setName("Child");
    child.setAge(10);
    serialize(child, childSerFile);

    person = unSerialize(childSerFile);
    System.out.println(person.getName()); // OK
    System.out.println(((ChildPerson)person).getAge());

}

/**
 * 序列化对象,保存到文件
 */
private void serialize(Object obj, String fileName) throws IOException {
    FileOutputStream fs = new FileOutputStream(fileName);
    ObjectOutputStream os = new ObjectOutputStream(fs);
    os.writeObject(obj);
    os.close();
}

/**
 * 反序列化对象
 */
private <T> T unSerialize(String fileName) throws IOException, ClassNotFoundException {
    ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream(fileName));
    T obj = (T) inputStream.readObject();
    inputStream.close();
    return obj;
}

类路径及类名必须一致

发送方类com.test1.Class1,接收方是com.test1.Class2或者com.test2.Class1,都不能正确反序列化,报java.lang.ClassCastException异常,如上述代码中注掉的newPerson部分。

类定义发生改变

若类定义发生改变,即发送方和接收方,虽然类路径和类名一致,但是发送方和接收方的类版本等不一致,此时亦会报错。

如上述代码,在序列化生成parent.ser文件后,将Person类添加字段sex,如下:

1
2
3
4
5
6
class Person implements Serializable {

    private String name;
    private String sex;

    ...

然后进行反序列化,会抛出异常java.io.InvalidClassException。提示:local class incompatible: stream classdesc serialVersionUID = 4485681234731380735, local class serialVersionUID = 5015652288950510004

这就是serialVersionUID的作用了。在Person的类定义中,加入以下代码就OK了:

1
private static final long serialVersionUID = 4485681234731380735L;

因此,在定义可序列化对象的时候,强烈建议显式定义serialVersionUID,防止类由于版本等的问题,无法匹配从而无法反序列化问题。

在Eclipse中,是会自动提示生成随机serialVersionUID的,在Intellij idea中同样可以,需要开启:File | Settings | Editor | Inspections 中 Java | Serialization issues | Serializable class without ‘serialVersionUID’ , 开启后,用alt+enter就可以显式生成serialVersionUID了。

更好的扩展方式

更好的扩展方式,应该是定义可序列化对象类的子类,将子类对象序列化后,仍然可以反序列化为原父类对象,从而对原来的序列化数据接收方无影响。

如上述测试代码中的将ChildPerson类的对象序列化后的文件,反序列化为Person类对象。

在一个已成熟稳定的系统中,扩展的时候,应尽量采用这种方式;但由于我们的系统刚刚搭建完成,因此我直接显式声明了serialVersionUID