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,所以这里不会对其这个属性进行反序列化,所以就存在了nameage这两个属性

到这里反序列化就完成了吗?并没有,到这里的话字段的反序列化已经完成,但是方法还没有完成啊,所以接下来又开始了方法的反序列化操作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);
  }

关于单例序列化的知识点

参考文章:https://zhuanlan.zhihu.com/p/136769959

posted @ 2021-08-10 22:40  zpchcbd  阅读(1630)  评论(1编辑  收藏  举报