Java 序列化和反序列化(三)Serializable 源码分析 - 2

在上一篇文章中围绕 ObjectOutputStream#writeObject 讲解了一下序列化的整个流程,这中间很多地方涉及到了 ObjectStreamClass 和 ObjectStreamField 这两个类。

  • ObjectStreamField 按官方的说法是是字段的序列化描述符,本质是对 Field 字段的包装,包括字段名、字段值等。可以通过 ObjectStreamClass#getFields 获取所有需要序列化的字段信息。

  • ObjectStreamClass 按官方的说法是类的序列化描述符,本质是对 Class 类的包装,提取了序列化时类的一些信息,包括字段的描述信息和 serialVersionUID。可以使用 lookup 方法找到/创建在此 Java VM 中加载的具体类的 ObjectStreamClass。

1. ObjectStreamField

ObjectStreamField 只是一个简单的 JavaBean,保存了序列化过程中字段的元数据信息,包括字段的类型、类型代码、签名等。 可以通过 ObjectStreamClass#getFields 获取所有需要序列化的字段信息。

1.1 数据结构


private final String name; // 1. field name
private final String signature; // 2. canonical JVM signature of field type
private final Class<?> type; // 3. 字段类型
private final boolean unshared; // 4. 序列化时字段是否是 unshared
private final Field field; // 5. Field
private int offset = 0; // 6. 序列化时数据在 buffer 中的偏移量
  • offset 在序列化的过程中,当一个对象的成员属性个数超过一个时,JVM 会将会把所有的成员属性打包成一个“组”来操作,而 offset 就是这个组中当前描述的成员属性的偏移量,上层的 ObjectStreamClass 在调用当前这个成员属性的时候就使用偏移量进行引用定位操作;

  • signature 该属性描述了 JVM 中成员属性的类型签名

JavaType TypeCode
byte B
short S
int I
long J
float F
double D
char C
boolean Z
class L
arrary [

1.2 构造函数

public ObjectStreamField(String name, Class<?> type, boolean unshared) {
this.name = name;
this.type = type;
this.unshared = unshared;
signature = getClassSignature(type).intern();
field = null;
}

unshared 在 ObjectOutputStream源码分析的writeObject和writeUnshared区别进行了简单的说明,这里重点说一下 signature 这个属性,具体的方法如下:

// JVM 中类型签名
private static String getClassSignature(Class<?> cl) {
StringBuilder sbuf = new StringBuilder();
while (cl.isArray()) {
sbuf.append('[');
cl = cl.getComponentType();
}
if (cl.isPrimitive()) {
if (cl == Integer.TYPE) {
sbuf.append('I');
} else if (cl == Byte.TYPE) {
sbuf.append('B');
} else if (cl == Long.TYPE) {
sbuf.append('J');
} else if (cl == Float.TYPE) {
sbuf.append('F');
} else if (cl == Double.TYPE) {
sbuf.append('D');
} else if (cl == Short.TYPE) {
sbuf.append('S');
} else if (cl == Character.TYPE) {
sbuf.append('C');
} else if (cl == Boolean.TYPE) {
sbuf.append('Z');
} else if (cl == Void.TYPE) {
sbuf.append('V');
} else {
throw new InternalError();
}
} else {
sbuf.append('L' + cl.getName().replace('.', '/') + ';');
}
return sbuf.toString();
}

2. ObjectStreamClass

ObjectStreamClass 按官方的说法是类的序列化描述符,本质是对 Class 类的包装,提取了类序列化时的一些信息,包括字段的描述信息和 serialVersionUID 和需要序列化的字段 fields。本文只介绍一些 ObjectStreamClass 常用用法,更多关于ObjectStreamClass源码分析

2.1 数据结构

// 类的基本信息
private Class<?> cl; // 1. Class
private String name; // 2. cl.getName()
private volatile Long suid; // 3. serialVersionUID private boolean isProxy; // 4. Proxy.isProxyClass(cl)
private boolean isEnum; // 5. Enum.class.isAssignableFrom(cl)
private boolean serializable; // 6. Serializable.class.isAssignableFrom(cl)
private boolean externalizable; // 7. Externalizable.class.isAssignableFrom(cl) // Serializable 接口默认的方法,通过反射调用
private Constructor<?> cons; // 默认的构造函数
private Method writeObjectMethod; // writeObject
private Method readObjectMethod; // readObject
private Method readObjectNoDataMethod; // readObjectNoData
private Method writeReplaceMethod; // writeReplace
private Method readResolveMethod; // readResolve
private boolean hasWriteObjectData; // writeObjectMethod!=null // localDesc表示本类的描述信息,superDesc表示父类的描述信息
private ObjectStreamClass localDesc; // this
private ObjectStreamClass superDesc; // 父类,superDesc=lookup(superCl, false) // 要序列化的字段信息,通过 getSerialFields(c1) 获取
private ObjectStreamField[] fields; // 序列化的字段信息

总结: 通过这些字段信息可以看到 ObjectStreamClass 提取了类序列化时的一些基本信息,这些信息大部分都是在其构造时就提取出来了。

2.2 构造函数

private ObjectStreamClass(final Class<?> cl) {
// 1. 类的基本信息获取
this.cl = cl;
name = cl.getName();
isProxy = Proxy.isProxyClass(cl);
isEnum = Enum.class.isAssignableFrom(cl);
serializable = Serializable.class.isAssignableFrom(cl);
externalizable = Externalizable.class.isAssignableFrom(cl); Class<?> superCl = cl.getSuperclass();
superDesc = (superCl != null) ? lookup(superCl, false) : null;
localDesc = this; // 2. Serializable 接口默认的方法,通过反射调用
if (serializable) {
if (isEnum) {
suid = Long.valueOf(0);
fields = NO_FIELDS;
return null;
}
if (cl.isArray()) {
fields = NO_FIELDS;
return null;
} suid = getDeclaredSUID(cl); // serialVersionUID
try {
fields = getSerialFields(cl); // 序列化的字段信息
computeFieldOffsets();
} catch (InvalidClassException e) {
serializeEx = deserializeEx = new ExceptionInfo(e.classname, e.getMessage());
fields = NO_FIELDS;
} if (externalizable) {
cons = getExternalizableConstructor(cl);
} else {
cons = getSerializableConstructor(cl);
writeObjectMethod = getPrivateMethod(cl, "writeObject",
new Class<?>[] { ObjectOutputStream.class }, Void.TYPE);
readObjectMethod = getPrivateMethod(cl, "readObject",
new Class<?>[] { ObjectInputStream.class }, Void.TYPE);
readObjectNoDataMethod = getPrivateMethod(cl, "readObjectNoData", null, Void.TYPE);
hasWriteObjectData = (writeObjectMethod != null);
}
writeReplaceMethod = getInheritableMethod(cl, "writeReplace", null, Object.class);
readResolveMethod = getInheritableMethod(cl, "readResolve", null, Object.class);
return null;
} else {
suid = Long.valueOf(0);
fields = NO_FIELDS;
} // 省略异常处理 ...
initialized = true;
}

总结: 这个构造函数是私有的,可以通过 lookup 获取一个类的 ObjectStreamClass。

// false 表示只获取实现了 Serializable 接口的类
public static ObjectStreamClass lookup(Class<?> cl) {
return lookup(cl, false);
}
// true 表示不管是否实现这个接口都提取相关的信息
public static ObjectStreamClass lookupAny(Class<?> cl) {
return lookup(cl, true);
}

2.3 提取序列号:getDeclaredSUID

// 提取 serialVersionUID 字段信息
private static Long getDeclaredSUID(Class<?> cl) {
try {
Field f = cl.getDeclaredField("serialVersionUID");
int mask = Modifier.STATIC | Modifier.FINAL;
if ((f.getModifiers() & mask) == mask) {
f.setAccessible(true);
return Long.valueOf(f.getLong(null));
}
} catch (Exception ex) {
}
return null;
} public long getSerialVersionUID() {
if (suid == null) { // 显示的配置了 serialVersionUID 就直接返回
return computeDefaultSUID(cl); // 生成一个默认的序列号 id
}
return suid.longValue();
}

总结: getDeclaredSUID 方法提取 serialVersionUID 字段信息。如果没有配置,getSerialVersionUID 方法会通过 computeDefaultSUID 生成一个默认的序列号。

2.4 提取需要序列化字段:getSerialFields

// 提取需要序列化字段
private static ObjectStreamField[] getSerialFields(Class<?> cl)
throws InvalidClassException {
ObjectStreamField[] fields;
if (Serializable.class.isAssignableFrom(cl) &&
!Externalizable.class.isAssignableFrom(cl) &&
!Proxy.isProxyClass(cl) &&
!cl.isInterface()) {
// serialPersistentFields 配置需要序列化的字段
if ((fields = getDeclaredSerialFields(cl)) == null) {
// 默认的序列化字段
fields = getDefaultSerialFields(cl);
}
Arrays.sort(fields);
} else {
fields = NO_FIELDS;
}
return fields;
} // 对外暴露的方法,获取可序列化的字段
public ObjectStreamField[] getFields() {
return getFields(true);
}
ObjectStreamField[] getFields(boolean copy) {
return copy ? fields.clone() : fields;
}

总结: getDeclaredSerialFields 提取的是 serialPersistentFields 字段显示配置的 serialPersistentFields 需要序列化的字段,如果没有配置(大部分情况都是这样的)则提取默认的字段。

// getDeclaredFields 提取本类中的所有字段,只要不是 static 或 transient 修辞的都会序列化
private static ObjectStreamField[] getDefaultSerialFields(Class<?> cl) {
Field[] clFields = cl.getDeclaredFields();
ArrayList<ObjectStreamField> list = new ArrayList<>();
int mask = Modifier.STATIC | Modifier.TRANSIENT; for (int i = 0; i < clFields.length; i++) {
if ((clFields[i].getModifiers() & mask) == 0) {
list.add(new ObjectStreamField(clFields[i], false, true));
}
}
int size = list.size();
return (size == 0) ? NO_FIELDS :
list.toArray(new ObjectStreamField[size]);
}

2.5 其它方法

private void writeSerialData(Object obj, ObjectStreamClass desc) throws IOException {
// 获取要序列化的类,包括实现了 Serializable 接口的父类
ObjectStreamClass.ClassDataSlot[] slots = desc.getClassDataLayout();
for (int i = 0; i < slots.length; i++) {
defaultWriteFields(obj, slots[i].desc);
}
} private void defaultWriteFields(Object obj, ObjectStreamClass desc) throws IOException {
// 1. Java 原生类型序列化
int primDataSize = desc.getPrimDataSize(); // 1.1 获取原生类型字段的长度
if (primVals == null || primVals.length < primDataSize) {
primVals = new byte[primDataSize];
}
desc.getPrimFieldValues(obj, primVals); // 1.2 获取原生类型字段的值
bout.write(primVals, 0, primDataSize, false); // 1.3 原生类型序列化 // 2. Java 对象类型序列化,递归调用 writeObject0 方法
ObjectStreamField[] fields = desc.getFields(false); // 2.1 获取所有序列化的字段
Object[] objVals = new Object[desc.getNumObjFields()];
int numPrimFields = fields.length - objVals.length;
desc.getObjFieldValues(obj, objVals); // 2.2 获取所有序列化字段的值
for (int i = 0; i < objVals.length; i++) { // 2.3 递归完成序列化
writeObject0(objVals[i], fields[numPrimFields + i].isUnshared());
}
}

总结: 其它用到的方法了解一下用法即可,就不往下深究了。

参考:

  1. 《ObjectStreamClass源码分析》:https://blog.csdn.net/silentbalanceyh/article/details/8250096

每天用心记录一点点。内容也许不重要,但习惯很重要!

Java 序列化和反序列化(三)Serializable 源码分析 - 2的更多相关文章

  1. Java 序列化和反序列化(二)Serializable 源码分析 - 1

    目录 Java 序列化和反序列化(二)Serializable 源码分析 - 1 1. Java 序列化接口 2. ObjectOutputStream 源码分析 2.1 ObjectOutputSt ...

  2. Java入门系列之集合HashMap源码分析(十四)

    前言 我们知道在Java 8中对于HashMap引入了红黑树从而提高操作性能,由于在上一节我们已经通过图解方式分析了红黑树原理,所以在接下来我们将更多精力投入到解析原理而不是算法本身,HashMap在 ...

  3. Java入门系列之集合LinkedList源码分析(九)

    前言 上一节我们手写实现了单链表和双链表,本节我们来看看源码是如何实现的并且对比手动实现有哪些可优化的地方. LinkedList源码分析 通过上一节我们对双链表原理的讲解,同时我们对照如下图也可知道 ...

  4. Java ThreadPoolExecutor线程池原理及源码分析

    一.源码分析(基于JDK1.6) ThreadExecutorPool是使用最多的线程池组件,了解它的原始资料最好是从从设计者(Doug Lea)的口中知道它的来龙去脉.在Jdk1.6中,Thread ...

  5. Java入门系列之集合Hashtable源码分析(十一)

    前言 上一节我们实现了散列算法并对冲突解决我们使用了开放地址法和链地址法两种方式,本节我们来详细分析源码,看看源码中对于冲突是使用的哪一种方式以及对比我们所实现的,有哪些可以进行改造的地方. Hash ...

  6. Java入门系列之集合ArrayList源码分析(七)

    前言 上一节我们通过排队类实现了类似ArrayList基本功能,当然还有很多欠缺考虑,只是为了我们学习集合而准备来着,本节我们来看看ArrayList源码中对于常用操作方法是如何进行的,请往下看. A ...

  7. 2.Java集合-ConcurrentHashMap实现原理及源码分析

    一.为何用ConcurrentHashMap 在并发编程中使用HashMap可能会导致死循环,而使用线程安全的HashTable效率又低下. 线程不安全的HashMap 在多线程环境下,使用HashM ...

  8. Java集合框架之接口Collection源码分析

    本文我们主要学习Java集合框架的根接口Collection,通过本文我们可以进一步了解Collection的属性及提供的方法.在介绍Collection接口之前我们不得不先学习一下Iterable, ...

  9. java并发锁ReentrantReadWriteLock读写锁源码分析

    1.ReentrantReadWriterLock 基础 所谓读写锁,是对访问资源共享锁和排斥锁,一般的重入性语义为如果对资源加了写锁,其他线程无法再获得写锁与读锁,但是持有写锁的线程,可以对资源加读 ...

随机推荐

  1. Mac-VScode

    1) 安装 xcode. 打开App Store,搜索xcode,进行下载安装. 2)执行命令: xcode-select --install 3)安装VS Code https://code.vis ...

  2. jupyter notebook的魔法命令 % %%

    Magic单元分为两种,一种是line magics,另外一种cell magics. Line magic是通过在前面加%,表示magic只在本行有效. Cell magic是通过在前面加%%,表示 ...

  3. [轉]User namespaces – available to play!

    User namespaces – available to play! Posted on May 10, 2012by s3hh Over the past few months, Eric Bi ...

  4. 纯Delphi 原生写的 上传到七牛的功能

    上传文件到七牛, 支持分片分段上传, 适用于Delphi XE, 10等新版本 分两个函数: uploadToQiniu 和 directUploadToQiniu uploadToQiniu 这个函 ...

  5. SpringCloud-技术专区-Hystrix-使用指南

    Maven依赖配置 <dependency> <groupId>org.springframework.cloud</groupId> <artifactId ...

  6. SpringMVC学习(4):数据绑定1 @RequestParam

    在系列(3)中我们介绍了请求是如何映射到一个action上的,下一步当然是如何获取到请求中的数据,这就引出了本篇所要讲的内容-数据绑定. 首先看一下都有哪些绑定数据的注解: 1.@RequestPar ...

  7. PHP通过KMP算法实现strpos

    起因 昨天看了阮一峰老师的一篇博客<字符串匹配的KMP算法>,讲的非常棒.这篇文章也是解决了: 有一个字符串"BBC ABCDAB ABCDABCDABDE",里面是否 ...

  8. window下Mysql 恢复Delete删除的数据

    转载:https://www.cnblogs.com/q149072205/p/11940591.html 本机用的Navicat连mysql测试DB又连了正式DB,因为本地与正式要频繁操作所以都打开 ...

  9. Android 增量更新完全解析 是增量不是热修复(转)

    转自:http://blog.csdn.net/lmj623565791/article/details/52761658 本文在我的微信公众号:鸿洋(hongyangAndroid)首发. 转载请标 ...

  10. 避免 Java 代码中的“坏味道”

    1.需要 Map 的主键和取值时,应该迭代 entrySet() 当循环中只需要 Map 的主键时,迭代 keySet() 是正确的.但是,当需要主键和取值时,迭代 entrySet() 才是更高效的 ...