Java 关于readObject和defaultReadObject的区别和相关序列化以及单例序列化的知识点
前言:作为一个类重写了readObject的时候,其中readObject和defaultReadObject的区别所在,这篇笔记一直都有更新,关于序列化和反序列化的细节的流程和知识点利用都会放到上面
主要在分析CC6/shiro的反序列化的时候,突然就卡住了,自己也从来没有真正理解过readObject的流程(可能就是在学反序列化的时候就没用心理解),这篇就会自己去详细的记录下反序列化的过程,打好基础!
讲解的内容
首先自己想将readObject/defaultReadObject总共分为四个点:
1、第一种是ObjectInputStream自身的readObject
2、第二种是一个继承Serializable重写的readObject和其中的大家俗称的默认反序列化方法defaultReadObject
3、上面之间的调用流程和关系
4、关于transient修饰的属性的序列化/反序列化
5、ObjectInputstream的调用流程
6、关于resolveClass来设置反序列化拦截的注意点
9、关于单例模式在序列化的注意点
OjbectInputStream的readObject
测试代码:
People类:
public class People implements Serializable {
public String name;
public String age;
public transient String height;
People(String name, String age, String height){
this.name = name;
this.age = age;
this.height = height;
}
}
调用代码如下:
public class Test {
public static void main(String[] args) throws IOException, ClassNotFoundException {
People people = new People("aaaa","bbbb","cccc");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("people.txt"));
objectOutputStream.writeObject(people);
objectOutputStream.close();
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("people.txt"));
People serPeople = (People)objectInputStream.readObject();
System.out.println(serPeople.height);
}
}
首先来到objectInputStream.readObject
的方法中,如下所示
接着来到readObject0的方法中,这里的话有用的信息就是如下,它会tc = bin.peekByte()) == TC_RESET
进行读取一个字节的数据来进行判断,先判断是否是TC_RESET
,这里的话读取出来的不符合,所以继续往下走
小知识点:这里的peekByte方法,不是读取当前的字节,而是后一个字节。但是readByte方法则是读取当前所在的字节,不是后一个。这两个方法还是不一样的!
因为这里的objectInputStream在反序列化的时候,读取出来的第一个字节为0x73,十进制为115,下面的分支走到的就是代表反序列化的对象为TC_OBJECT
,意味着反序列化的对象是一个类的实例化对象
接着就进入到了readOrdinaryObject
方法中
这里会进入到readClassDesc
方法中,这一次又会读取之后的一个字节,这时读取的tc的变量数据为114,所以走的就是TC_CLASSDESC的分支点,那么就会调用readNonProxyDesc
方法
接着这里就进入到了readNonProxyDesc
方法中,其中readClassDescriptor
方法,会将反序列化的对象的相关类描述信息读取出来赋值给readDesc
变量中
在readClassDescriptor
方法中readNonProxy
方法主要就是进行了对对应的相关类的属性的读取操作,如下图所示
这里的话就从readNonProxyDesc
这个方法里面走出来了,接着它又会对这个相关描述类对象作为参数,放入到resolveClass
方法中进行调用,返回对应的Class对象
这里的resolveClass
方法需要讲下其中的操作,它会进行类加载操作Class.forName
接着就走出readNonProxyDesc,又走出readClassDesc
方法,走到这里如下readOrdinaryObject
方法中,大家需要了解的就是到这里的时候,我们需要反序列化的对象的相关信息(需要反序列化的非瞬时字段)都返回保存到了descriptor属性当中
那么现在开始,就是开始进行对这个这个Class对象进行实例化操作
因为是反序列化,所以还需要对这个实例化对象进行字段的填充操作,相关操作体现在readSerialData方法中
这个readSerialData
方法有两种情况,这里详细的说下
这个方法首先会对当前传入的对应的描述符对象判断是否存在readObjectMethod
属性,这里用来判断的方法就是hasReadObjectMethod()
而当前的People描述符对象就没有对应的readObjectMethod
属性,所以这里走的也就是下面的分支defaultReadFields(obj,slotDesc)
defaultReadFields
这个方法的作用就是默认反序列化这个对应中的属性字段,如下图所示
也就是如下还原的obj对象,因为height
属性我设置的修饰符为transient
,所以这里不会对其这个属性进行反序列化,所以就存在了name
和age
这两个属性
到这里反序列化就完成了吗?并没有,到这里的话字段的反序列化已经完成,但是方法还没有完成啊,所以接下来又开始了方法的反序列化操作hasReadResolveMethod
因为这里我写的People类中没有相关的方法,所以这里分支就直接跳过去进行返回了,这里的话就完成了一次正常对象的序列化和反序列化的操作
简单思考
上面我们讲述了一个People的对象,其对象构造为如下,其实上面还有一些方法就只是简单的概括了下...
public class People implements Serializable {
public String name;
public String age;
public transient String height;
People(String name, String age, String height){
this.name = name;
this.age = age;
this.height = height;
}
}
就比如readSerialData
方法中就存在两种不同的处理,而上面只讲了默认的,那么什么时候才会走另一种方法呢?也就是什么时候才会走hasReadObjectMethod()
这个方法?
重写readObject
这里如果想要走hasReadObjectMethod()
的方法的话,我们这里就需要重写readObject
方法来进行实现,People类的定义为如下:
public class People implements Serializable {
public String name;
public String age;
public transient String height;
People(String name, String age, String height){
this.name = name;
this.age = age;
this.height = height;
}
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException {
}
}
重新调试,来到readSerialData
方法的时候就可以看到走的就是hasReadObjectMethod()
为true的分支语句了,如下图所示
这里继续走,这里就开始调用重写过后的readOjbect
方法了,如下图所示
通过反射来进行调用该重写的readOjbect方法
继续走
一直F7,来到如下,则进行调用操作
上面java通过反射调用了重写过后的readOjbect
方法之后,后面的就跟正常没有重写的一样了,最后还是一样打印serPeople.age
...
但是会发现这次打印出来的age
属性竟然是为空?我们的类定义是如下,age
属性是公有的
defaultReadObject
这里就需要引出关于defaultReadObject
,我们这次在重写readObject
中进行调用默认的defaultReadObject
,如下代码所示:
public class People implements Serializable {
public String name;
public String age;
public transient String height;
People(String name, String age, String height){
this.name = name;
this.age = age;
this.height = height;
}
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
}
}
继续上面的步骤调试,来到如下
继续F7,来到重写的readObject
方法中,这里进行调用defaultReadObject
方法,单步过去之后,会发现公有属性被进行赋值
这次进行打印age
属性的话,就可以进行打印了
那么这里的defaultReadObject
做了什么事呢?我们在分析ObjectInputStream的readObject方法中的时候,会发现在readSerialData
中会进行默认赋值字段的值,那么这里是不是也是相同的效果?经过下面的图中所示,确实会同样调用defaultReadFields
这个方法
深入
我这里还想了解一种情况,当一个序列化对象中的一个属性是一个序列化的对象,那么如果这个对象在序列化,那么在被反序列化的过程中是如何走的?
应该同样也有两种情况:
1、一个序列化对象的其中一个属性为一个序列化的对象,这个序列化没重写readObject
的情况
2、一个序列化对象的其中一个属性为一个序列化的对象,这个序列化有重写readObject
的情况
第一种情况
模拟代码:
Father.java
public class Father implements Serializable {
public String name;
public String age;
public transient String height;
public Son son;
Father(String name, String age, String height, Son son){
this.name = name;
this.age = age;
this.height = height;
this.son = son;
}
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
}
}
Son.java
public class Son implements Serializable {
public String name;
public String age;
public transient String height;
Son(String name, String age, String height){
this.name = name;
this.age = age;
this.height = height;
}
}
调用类
public class Test {
public static void main(String[] args) throws IOException, ClassNotFoundException {
Son son = new Son("dddd","eeee","ffff");
Father father = new Father("aaaa","bbbb","cccc", son);
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("people.txt"));
objectOutputStream.writeObject(father);
objectOutputStream.close();
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("people.txt"));
Father father1 = (Father) objectInputStream.readObject();
System.out.println(father1.son.age);
}
}
先运行一下,如下结果,那么的确打印了father1.son.age
这个属性
接下来开始调试其过程,这里直接先走到Father类中的defaultReadObject
方法中,如下图所示
继续走进去,可以发现对每个字段属性都进行readObject0
方法调用,当在反序列化Son对象的时候,可以进到这个方法中进行观察,可以发现这个情景有点似曾相识,就是对序列化对象反序列化的操作
一样的判断,此时Son为Object,所以这里走的就是TC_OBJECT
的分支
一样的获取该Son对象的描述符对象
一样的获取该Son对象的Class对象
一样的进行readSerialData
方法调用
一样的进行填充,因为这里我们的Son对象没有重写readObject方法,所以走的是defaultReadFields
最后Father反序列化的过程结束
最后打印处Son反序列化出来的字段值
到这里整个反序列化过程就结束了,我们这里来总结下,其实反序列化的过程就是一种递归式的反序列化,对流中的数据根据描述符对象来进行反序列化,也就是对其中进行读取,最后填充到对应对象中的指定属性中。
第二种情况
我写到这里,感觉第二种情况就没必要写了,因为上面的过程因为就已经知道了是咋走的,唯一不同的就是在readSerialData
中走了另一个分支,会多调用下重写的readObject方法中的流程。
强行序列化/反序列化transient属性
在默认的序列化/反序列化的过程中,对应transient属性是无法保存在流中呢
那么有什么办法可以进行保存?
答案:重写readObject和writeObject
为什么无法序列化transient的属性呢?原因就是在写入writeObject,写入数据的时候没有写入进去,导致反序列化读取的时候没有读取出来
上面我们重写readObject了,那么这里继续重写writeObject。
重新定义下Father类:
public class Father implements Serializable {
public String name;
public String age;
public transient String height;
public Son son;
Father(String name, String age, String height, Son son){
this.name = name;
this.age = age;
this.height = height;
this.son = son;
}
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
this.height = (String) in.readObject();
}
private void writeObject(java.io.ObjectOutputStream ou) throws IOException, ClassNotFoundException {
ou.defaultWriteObject();
ou.writeObject(this.height);
}
}
重新运行下,如下所示:
参考文章:https://blog.csdn.net/weixin_42653621/article/details/82534820
ObjectInputstream的调用流程
有这么一道题,代码如下所示
public class MyObjectInputStream
extends ObjectInputStream
{
private static ArrayList<String> blackList = new ArrayList();
static
{
blackList.add("org.apache.commons.collections.functors");
blackList.add("java.rmi.server");
}
public MyObjectInputStream(InputStream inputStream)
throws Exception
{
super(inputStream);
}
protected Class<?> resolveClass(ObjectStreamClass desc)
throws IOException, ClassNotFoundException
{
for (String s : blackList) {
if (desc.getName().contains(s)) {
throw new ClassNotFoundException("go out!");
}
}
return super.resolveClass(desc);
}
protected Class<?> resolveProxyClass(String[] interfaces)
throws IOException, ClassNotFoundException
{
return super.resolveProxyClass(interfaces);
}
}
public class MarshalledObject
implements Serializable
{
private byte[] bytes = null;
public Object readResolve()
throws Exception
{
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(this.bytes);
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
Object obj = objectInputStream.readObject();
objectInputStream.close();
return obj;
}
}
触发点就是这里,环境中存在的能够利用的依赖库只有commons collection3.2.1,那么这里的话只能观察MarshalledObject如何配合利用
ObjectInputStream objectInputStream = new MyObjectInputStream(inputStream);
根据上面的图直接来到java.io.ObjectInputStream#readOrdinaryObject中会看到如下一段代码,这段则是如果当前反序列化的类是serializable or externalizable的实现类并且存在readResolve,那么则会调用其readResolve方法,所以这里的话MarshalledObject就可以进行利用了
/**
* Returns true if represented class is serializable or externalizable and
* defines a conformant readResolve method. Otherwise, returns false.
*/
boolean hasReadResolveMethod() {
requireInitialized();
return (readResolveMethod != null);
}
private Object readOrdinaryObject(boolean unshared)
throws IOException
{
.......
if (obj != null &&
handles.lookupException(passHandle) == null &&
desc.hasReadResolveMethod())
{
Object rep = desc.invokeReadResolve(obj);
if (unshared && rep.getClass().isArray()) {
rep = cloneArray(rep);
}
if (rep != obj) {
// Filter the replacement object
if (rep != null) {
if (rep.getClass().isArray()) {
filterCheck(rep.getClass(), Array.getLength(rep));
} else {
filterCheck(rep.getClass(), -1);
}
}
handles.setObject(passHandle, obj = rep);
}
}
......
}
关于resolveClass来设置反序列化拦截的知识点
该方法在java.io.ObjectInputStream#readNonProxyDesc中进行调用
如果在反序列化的时候不允许部分类进行反序列化,那么就可以实现ObjectInputStream重写其resolveClass方法,在其方法中进行判断当前反序列化的类是不是自己想要的
protected Class<?> resolveClass(ObjectStreamClass desc)
throws IOException, ClassNotFoundException
{
for (String s : blackList) {
if (desc.getName().contains(s)) {
throw new ClassNotFoundException("go out!");
}
}
return super.resolveClass(desc);
}