关于java中的序列化

关于java中的序列化

Context

最近在精简项目代码,去除无用的依赖以及没有没有使用的模块。这其中免不了删除实体类中的无效字段。于是,,问题来了:

1
java.io.InvalidClassException: entity.User; local class incompatible: stream classdesc serialVersionUID = 8550471390860600205, local class serialVersionUID = -7433272285406297333

用户未登录?

改完之后,在测试某接口的时候,只是返回用户未登录的信息,没想太多,,于是调用模拟测试账号登录行为的接口,然后重新测试刚才的接口,但还是返回用户未登录的信息。。

这就怪了,,正常来说,用户在正常登陆时,通过SQL查到用户信息后,保存部分字段到Redis中,之后每次用户发送请求时直接通过用户ID从Redis中获取用户相关信息。所以,,首先怀疑的是用户在登陆后发送其他请求时,通过Redis获取用户信息时出问题了,然后一点点看代码。调试到这儿的时候发生问题了👇

1
2
3
4
5
6
7
8
9
10
ByteArrayInputStream bais = null;
try {
// 反序列化
bais = new ByteArrayInputStream(bytes);
ObjectInputStream ois = new ObjectInputStream(bais);
return ois.readObject();
} catch (Exception e) {
//Tools.error(e);
}
return null;

👆这是从Redis中获取用户信息后的反序列化时的部分代码,,正常来说,应该是返回从流中反序列化后得到的User实例。但实际调试过程中,运行到第6行之后,又跳到第10行,,咦,什么鬼??两个return??嗯,,当然不是,肯定是在ois.readObject()报错了,再看第8行,妈蛋,,打印报错信息的代码被注释了!OK,反注释之后就出现了Context中的报错信息。

序列化

报错信息大意为,,entity.User中本地类文件冲突,流中的类描述serialVersionUID与本地相应类的描述serialVersionUID不一致。出于安全考虑,反序列化失败。
关于序列化,之前不甚了解,,正好借此机会好好了解下~

什么时候使用序列化
  1. 把的内存中的对象持久化到一个文件中或者数据库中时候;
  2. 套接字在网络上传送对象的时候;

serialVersionUID适用于Java的序列化机制。简单来说,Java 的序列化机制是通过判断类的serialVersionUID来验证版本一致性的。在进行反序列化时,JVM 会把传来的字节流中的serialVersionUID与本地相应实体类的serialVersionUID进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常,即是InvalidCastException

serialVersionUID有两种显示的生成方式:

  1. 默认的1L,比如:private static final long serialVersionUID = 1L;
  2. 根据类名、接口名、成员方法及属性等来生成一个64位的哈希字段,比如:
    private static final long serialVersionUID = xxxxL;

当一个类实现了Serializable接口,如果没有显示的定义serialVersionUID,Eclipse 会提供相应的提醒。面对这种情况,我们只需要在 Eclipse 中点击类中 warning 图标一下,Eclipse就会自动给定两种生成的方式。

当实现 java.io.Serializable 接口的类没有显式地定义一个serialVersionUID变量时候,Java序列化机制会根据编译的 Class 自动生成一个serialVersionUID作序列化版本比较用,这种情况下,如果 Class 文件(类名,方法明等)没有发生变化(增加空格,换行,增加注释等等),就算再编译多次,serialVersionUID也不会变化的。

所以,在项目中 User 类中并没有显式的定义serialVersionUID。所以,在我精简代码后,serialVersionUID发生变化,导致反序列化失败。解决方法也很简单,,显式声明与流中相同的serialVersionUID即可。

1
private static final long serialVersionUID = 8550471390860600205L;

静态变量序列化

下面代码中Test实体类实现了Serializable接口,并初始化了一个静态变量staticVar。将对象序列化后,修改静态变量的数值,再将序列化对象读取出来,然后通过读取出来的对象获得静态变量的数值并打印出来那么结果是 10 还是 5 呢?

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
public class Test implements Serializable {
private static final long serialVersionUID = 1L;
public static int staticVar = 5;
public static void main(String[] args) {
try {
//初始时staticVar为5
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("result.obj"));
out.writeObject(new Test());
out.close();

//序列化后修改为10
Test.staticVar = 10;
ObjectInputStream oin = new ObjectInputStream(new FileInputStream("result.obj"));
Test t = (Test) oin.readObject();
oin.close();

//再读取,通过t.staticVar打印新的值
System.out.println(t.staticVar);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}

最后的输出是 10,对于无法理解的读者认为,打印的 staticVar 是从读取的对象里获得的,应该是保存时的状态才对。之所以打印 10 的原因在于序列化时,并不保存静态变量,这其实比较容易理解,序列化保存的是对象的状态,静态变量属于类的状态,因此序列化并不保存静态变量。

但是,serialVersionUID也是static的,那它是怎么被保存的呢?在jdk_7中,,

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
89
90
91
92
java.io.ObjectOutputStream
/**
* Writes class descriptor representing a standard (i.e., not a dynamic
* proxy) class to stream.
*/
private void writeNonProxyDesc(ObjectStreamClass desc, boolean unshared)
throws IOException
{
bout.writeByte(TC_CLASSDESC);
handles.assign(unshared ? null : desc);

if (protocol == PROTOCOL_VERSION_1) {
// do not invoke class descriptor write hook with old protocol
desc.writeNonProxy(this);
} else {
writeClassDescriptor(desc);
}

Class cl = desc.forClass();
bout.setBlockDataMode(true);
if (cl != null && isCustomSubclass()) {
ReflectUtil.checkPackageAccess(cl);
}
annotateClass(cl);
bout.setBlockDataMode(false);
bout.writeByte(TC_ENDBLOCKDATA);

writeClassDesc(desc.getSuperDesc(), false);
}
/*将指定的类描述符写入 ObjectOutputStream。类描述符用于标识写入流中的对象的类。ObjectOutputStream 的子类可以重写此方法,从而定制将类描述符写入序列化流中的方式。然后,应该重写 ObjectInputStream 中的相应方法( readClassDescriptor),以便根据其定制的流表示形式来重构类描述符。默认情况下,此方法根据 Object Serialization 规范中定义的格式写入类描述符。*/
/*注意,仅当 ObjectOutputStream 没有使用旧的序列化流格式(通过调用 ObjectOutputStream 的 useProtocolVersion 方法设置)时才调用此方法。如果此序列化流在使用旧的格式(即 PROTOCOL_VERSION_1),则以不可重写或自定义的方式在内部写入类描述符。*/

java.io.ObjectStreamClass
/**
* Writes non-proxy class descriptor information to given output stream.
*/
void writeNonProxy(ObjectOutputStream out) throws IOException {
out.writeUTF(name);
out.writeLong(getSerialVersionUID());//写入获取的serialVersionUID

byte flags = 0;
if (externalizable) {
flags |= ObjectStreamConstants.SC_EXTERNALIZABLE;
int protocol = out.getProtocolVersion();
if (protocol != ObjectStreamConstants.PROTOCOL_VERSION_1) {
flags |= ObjectStreamConstants.SC_BLOCK_DATA;
}
} else if (serializable) {
flags |= ObjectStreamConstants.SC_SERIALIZABLE;
}
if (hasWriteObjectData) {
flags |= ObjectStreamConstants.SC_WRITE_METHOD;
}
if (isEnum) {
flags |= ObjectStreamConstants.SC_ENUM;
}
out.writeByte(flags);

out.writeShort(fields.length);
for (int i = 0; i < fields.length; i++) {
ObjectStreamField f = fields[i];
out.writeByte(f.getTypeCode());
out.writeUTF(f.getName());
if (!f.isPrimitive()) {
out.writeTypeString(f.getTypeString());
}
}
}

java.io.ObjectStreamClass
/**
* Return the serialVersionUID for this class. The serialVersionUID
* defines a set of classes all with the same name that have evolved from a
* common root class and agree to be serialized and deserialized using a
* common format. NonSerializable classes have a serialVersionUID of 0L.
*
* @return the SUID of the class described by this descriptor
* 回此类的 serialVersionUID。serialVersionUID 定义了一组具有相同名称的类,它们的名称都是从公共根类演化而来的,并且能够使用公共格式进行序列化和反序列化。NonSerializable 类的 serialVersionUID 为 0L。
*/
public long getSerialVersionUID() {
// REMIND: synchronize instead of relying on volatile?
if (suid == null) {
suid = AccessController.doPrivileged(
new PrivilegedAction<Long>() {
public Long run() {
return computeDefaultSUID(cl);
}
}
);
}
return suid.longValue();
}

除此之外,,transient关键字修饰的变量也不会被序列化。

父类的序列化

context:一个子类实现了Serializable接口,它的父类都没有实现Serializable接口,序列化该子类对象,然后反序列化后输出父类定义的某变量的数值,该变量数值与序列化时的数值不同。

解决:要想将父类对象也序列化,就需要让父类也实现Serializable接口。如果父类不实现的话的,就需要有默认的无参的构造函数。在父类没有实现Serializable接口时,虚拟机是不会序列化父对象的,而一个 Java 对象的构造必须先有父对象,才有子对象,反序列化也不例外。所以反序列化时,为了构造父对象,只能调用父类的无参构造函数作为默认的父对象。因此当我们取父对象的变量值时,它的值是调用父类无参构造函数后的值。如果你考虑到这种序列化的情况,在父类无参构造函数中对变量进行初始化,否则的话,父类变量值都是默认声明的值,如 int 型的默认是 0,string 型的默认是 null。

另外,,如果父类已经实现Serializable接口,其子类就无需再显式的实现Serializable接口了。


参考链接:https://www.cnblogs.com/duanxz/p/3511695.html - 学无止境