详述序列化和反序列化问题

目录

一、为什么要序列化

二、怎么实现序列化

  序列化操作:

  外部化操作:

三、序列化、反序列化造成的问题

  1、序列化对单例模式的破坏

  2、反序列化无视控制修饰符,对数据安全性的破坏 

  3、序列化底层源码 --> 序列化攻击 

四、transient关键字的作用 

五、并非所有的类都需要序列化

需要和不需要序列化?

  1.普通成员变量需要序列化

  2.静态变量无需序列化

  3.方法无需序列化

  4.属性为一个引用(需要被序列化)

  5.有父类(较为复杂)

  6.有实现接口(接口内容无需序列化)

  7.用transient保护的敏感信息不用序列化

六、序列化(Serializable)和外部化(Externalizable)的区别

七、序列化之使用 serialVersionUID的作用

八、序列化的应用场景

结束


一、为什么要序列化

  1、存在堆栈空间中的实例对象在java进程执行结束时,内存中的对象会被gc回收 --> 无法在新的程序中使用该对象

  2、远程接口调用,两个服务器中的内存并不共享,例如dubbo中RPC远程调用,进程间通信调用另一地址空间的过程和函数,服务之间进行传参和返回值接收的前提就是把对象给序列化,转化成流后再通过sockets进行网络传输,接收方再进行反序即可

  3、对象、文件、数据、有许多不同格式,很难统一传输和保存: 序列化之后就统一都是字节流了 --> 所以可以进行通用的格式传输或保存,传输后要使用时,进行反序列化还原 --> 得到对象还是对象,文件还是文件

  4、可以实现深克隆:如果引用类型里面还包含多个引用类型(或者内存引用类型里又包含引用类型),使用clone()将会变得非常麻烦(每个引用类型都需要实现可复制化),如果通过序列化将对象写到流中(相当于一次拷贝),原对象还在内存中,而序列化不仅能复制对象本身,而且可以复制其引用的成员对象,写到流中,再从流中读出来 --> 完成深拷贝

二、怎么实现序列化

  序列化操作:

    简单地实现 Serializable接口即可使用 ObjectOutputStream --> writeObject(对象) 和 ObjectIntputStream --> readObject() ,利用对象输入输出流实现

  外部化操作:

    其实和序列化类似,但是主要根据程序需求来定,必须实现 externalizable 接口两个方法writeExternal、readExternal,外部化不熟悉,贴下代码

@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class Student implements Externalizable {

    private int id;
    private String name;
    private int sex;

    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeObject(id);
        out.writeObject(name);
        out.writeObject(sex);
    }

    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        id = (Integer)in.readObject();
        name = (String)in.readObject();
        sex = (Integer) in.readObject();
    }
}

三、序列化、反序列化造成的问题

  1、序列化对单例模式的破坏

    反序列化生成实例对象是通过调用类提供的无参构造方法来实现的  --> 那么就会有个问题,单例模式实现序列化呢,私有化的属性、构造器都可以被暴力反射出来,从而生成一个新的对象,造成单例模式的破坏(constructor.setAccessible(true)) --> instance1 != instance2

/*
这里省去一些创建Student类、获取class对象等操作
*/

//1、通过Declared获得私有变量name(直接getField是获得public修饰的)
Field name = StudentClass.getDeclaredField("name");

//2、借助AccessibleObject.setAccessible方法暴力调用私有构造器、私有属性等
name.setAccessible(true)   //暴力反射
name.setName("暴力修改私有姓名属性");

 解决单例被序列化破坏:自定义一个readResolve()方法,指定要返回对象的生成策略

反序列化源码片段:具体源码分析可看一博主写的

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) {
                handles.setObject(passHandle, obj = rep);
            }
        }

  hasReadResovleMethod: 如果实现了serializable 或者 externalizable接口的类中包含readResolve则返回true

  invokeReadResovle: 通过反射的方式调用要被反序列化的类的readResolve方法

  简言之 --> 实现 serializable 接口的可序列化类,若是我还定义了 readResolve(),则会通过反射的方式调用类的这个 readResovle()

  所以这好办,我们来给单例加上此方法,并指定返回对象策略即可 --> instance1 == Instance2

public class Singleton implements Serializable{
    private volatile static Singleton singleton;
    private Singleton (){}
    public static Singleton getSingleton() {
        if (singleton == null) {
            synchronized (Singleton.class) {
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
    
   // 加上 readResolve 方法,返回同一个 singleton对象
    private Object readResolve() {
        return singleton;
    }
}

  2、反序列化无视控制修饰符,对数据安全性的破坏 

     不仅单例的private会被破坏,final等严格访问控制修饰符都可以被忽略 ,对象中任何private字段几乎都是以明文方式出现在套字节流中,--> 严重破坏数据的安全性

    解决方案:可以类中自定义writeObject和readObject方法,进行用户自定义的序列化和反序列化-->可以在序列化和反序列化的过程中给数据加密和签名(当然还可控制做其他动态改变)

//私有属性 age
private int age;

/*
  序列化给自定义规则加密,反序列化再按规则进行解开
*/
private void writeObject(java.io.ObjectOutputStream stream)
        throws java.io.IOException {
        age = age << 2;
        stream.defaultWriteObject();
    }

private void readObject(java.io.ObjectInputStream stream)
        throws java.io.IOException, ClassNotFoundException {
        stream.defaultReadObject();
        age = age << 2;
}

  3、序列化底层源码 --> 序列化攻击 

    底层源码:不用类显示声明无参构造方法,而是通过一种语言之外的对象创建机制 --> java.reflect.Constructor的newInstance() 方法,最终调用无参构造创建实例,你其他有限制条件的构造器也会被无视(我规定构造参数 a > b,而你却调无参再赋值),造成序列化攻击(详情得查资料)

// 用反射生成实例
public T newInstance(Object... initargs) throws InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
        // ... 此次省略
        return (T) constructorAccessor.newInstance(initargs);
}

四、transient关键字的作用 

实现了Serializable接口的类中,所有被transient关键字修饰的属性都不会被序列化和反序列化(注意:序列化只会关注对象的状态,即动态属性,静态属性由于在JVM加载类的时候就会初始化,所以不会被实例化

@ToString
@NoArgsConstructor
@AllArgsConstructor
@Data
public class User implements Serializable {

    private String name;

    //使用transient关键字控制该属性不被序列化  --> null
    private transient String transit;
    //int类型使用transient关键字控制该属性不被序列化  --> 0
    private transient int age;

    //静态属性(类初始化的时候就加载好了,也不会被序列化)
    private static String sta = "static";
}

public static void main(String[] args) {
   SpringApplication.run(TestmodelApplication.class, args);
   try {
            ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("transit.obj"));
            //通过有参构造给User赋值(若我有参构造规定了name属性字符串长度大于 transit呢)
            out.writeObject(new User("xiaoliao","test",18));
            out.close();

            ObjectInputStream in = new ObjectInputStream(new FileInputStream("transit.obj"));
            //反射底层会通过newInstance()得到的对象obj ==>属性.get(obj) 得到属性值,属性.set(obj,"值")设置值(我可以不按照原先的构造规则进行赋值)
            User user = (User) in.readObject();  
            in.close();
            //User(name=xiaoliao, transit=null, age=0)  <-- 经 transient 关键字控制不序列化的属性 结果是 除int类型属性,其他属性值都为 null,静态属性压根就不会出来
            System.out.println(user);       
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
}

结论:

  ①.经transient关键字标识不被序列化的属性 --> int型默认为0,其他属性默认为 null

  ②.静态属性本就是随类加载的时候加载 --> 不会被序列化

  ③.序列化时底层调用newInstance无参构造创建实例obj,而被序列化的对象本身是通过一定规则限定的构造函数建造的,在进行反序列化时,底层是通过 "属性名.get(obj)" 和 "属性名.set(obj,设置值)" 给无参构造来的obj重新赋上原先的值

五、并非所有的类都需要序列化

  1.安全问题:有的类是敏感的类,里面的数据不能公开。而实现了序列化就很容易被破解,可以说没有秘密可言,隐私性没有保证。

  2.资源问题:序列化可以任意生成实例而不受限制,如果有的对象生成的实例是受限制的,比如只能生成10个实例,或者是单例的,这个很难保证。

  3.消耗问题:主要的序列化反序列化要占用IO流生成一个16进制的字节序列,并可能用于网络传输,所以没必要序列化的不必也进行序列化

需要和不需要序列化?

参考序列化梳理_麦田里的码农-CSDN博客

  1.普通成员变量需要序列化

    无论是用什么权限标识符修饰(public/private/protected)的成员变量,他们都是对象的状态,不序列化成员变量的话,反序列化的实例也是不完整的。所以,普通成员变量必须序列化。

  2.静态变量无需序列化

    静态变量其实是类属性,并不属于某个具体实例,所以也不用保存。当恢复对象的时候,直接取类当前的静态变量即可。

  3.方法无需序列化

    方法只是类的无状态指令。重建类的时候,可以直接从类的信息中获取,所以也不需要被序列化

  4.属性为一个引用(需要被序列化)

    类a有b、c两个引用属性,b有引用d,d又引用f….看下图:

   若序列化该对象时,引用对象不进行序列化,那么反序列化出来的值都是null,上图描述的场景中,含有对f实例的重复引用,序列化时f实例只会被序列化一次循环引用问题不展开介绍,具体看其他博文

  其实关于成员变量是引用的序列化问题与"深克隆"问题相似,而且序列化将整个对象层次写入字节流中保存起来再进行传递,本就可用来进行对象的 "深拷贝"

  5.有父类(较为复杂)

    子继承父类,会继承父类型特征,而一个JAVA对象的构造必须先有父再有子,序列化也一样

    1.子类实现 Serializable接口,父类不实现 --> 反序列化时,子类反序列化要生成实例对象,就只能递归调用父类的默认构造函数,来保持继承下来的父类型特征(所以若该父类声明有参构造且没声明默认无参,则会找不到默认构造而报错的)

    2.子类和父类都实现 Serializable接口 --> 序列化子类时,就不需要再调用父类构造函数了

    3.父类实现 Serializable接口,子类不实现 --> 父类实现序列化时,子类会自动实现序列化而不需要显示实现 Serializable 接口,想想原因:父类不就是用来被继承的吗,子类不跟着一起?哈哈

  两个特殊情况的序列化操作

    ①.能不能父类不实现Serializable接口也被序列化呢,当然可以,这就要从序列化和反序列化父子之间无参构造方法调用关系出发:因为父类没有实现Serializable接口,所以虚拟机不会序列化父对象(即序列化后的流中没有父类信息),但是一个Java对象的构造又必须先有父对象才有子对象,反序列化也是如此 --> 反序列化时为了构造父对象只能调用父类的无参构造方法作为默认的父对象因此要让一个没有实现Serializable 接口的父类能够序列化则必须要求父类有无参构造方法且子类负责序列化(反序列化),这操作需要贴一下代码了:

//定义父类,含属性father
abstract class Fu {
    public int father;

    public Fu(int father) {
        this.father = father;
    }

    public Fu() {
    }
}

//定义子类
class Zi extends Fu implements Serializable {
    public int son;

    public Zi(int father, int son) {
        super(father);  //初始化当前对象的父类型特征,通过子类构造函数调用父类构造方法传递参数
        this.son = son;
    }

    private void writeObject(java.io.ObjectOutputStream out) throws IOException, IOException {
        out.defaultWriteObject();
        //通过子类定制化的对象输出流将继承父类下来的属性也写入流中
        out.writeInt(father);   
    }

    private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException {
        in.defaultReadObject();
        //通过子类定制化的对象输入流,反射子对象同时也将该属性读到内存当中
        father = in.readInt(); 
    }
}


//测试
public static void main(String[] args) {
        try {
            ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("test.obj"));
            out.writeObject(new Zi(1,2));     //正常序列化子对象(父对象属性也随之写入流)
            out.close();

            ObjectInputStream in = new ObjectInputStream(new FileInputStream("test.obj"));
            Fu fu = (Fu) in.readObject();  
            System.out.println(fu)  //com.example.testmodel.FuSerializable.Zi@27fa135a
            System.out.println(fu.father);    //1

        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }

    不难看出,我们可以在子类中自定义writeObject、readObject中同时使用对应的输入输出对象流主动给父类属性进行写入流和读入内存操作,既可通过子类传参给父类属性设值,又在序列化和反序列化同时给父类属性完成对应操作,完成了对父类数据在对象流中的操作传输,但值得注意的是,通过测试输出发现 fu 还是指向子对象,是无法真正new出fu对象来的(变相序列化)

  ②. 父类实现了 Serializable 接口后如何不修改父类的情况下让子类不可序列化,也是有办法做到的,但是却违背了 里氏替换原则(子类对象可以无条件的替换父类对象),不过能有想法,且能做到总可一试,还是利用序列化的自定义操作,如下:

class SubClass extends SuperSerializableClass {
    private void writeObject(java.io.ObjectOutputStream out) throws IOException {
        throw new NotSerializableException("XXX");
    }

    private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException {
        throw new NotSerializableException("XXX");
    }

    private void readObjectNoData() throws ObjectStreamException {
        throw new NotSerializableException("XXX");
    }
}

  其实就是让子类自定义序列化操作 writeObject、readObject、readObjectNoData方法中主动抛出异常NotSerializableException(),这样就可限制父类序列化带着子类一起序列化

  6.有实现接口(接口内容无需序列化)

    接口一般是无状态的,就算有也是static的,那么毫无疑问,接口的信息也不会被序列化

  7.用transient保护的敏感信息不用序列化

    类中的非静态属性是要被序列化的,但是有些敏感字段(如密码口令等标识),毕竟序列化不按上文说的定制writeObject、readObject保护处理,正常情况下序列化传输过程中,对象的private域是不受保护的

六、序列化(Serializable)和外部化(Externalizable)的区别

  外部化和序列化是实现同一目标的两种不同方法。下面让我们分析一下序列化和外部化之间的主要区别。
  通过Serializable接口对对象序列化的支持是内建于核心 API 的,但是java.io.Externalizable的所有实现者必须提供读取和写出的实现。Java 已经具有了对序列化的内建支持,也就是说只要制作自己的类java.io.Serializable,Java 就会试图存储和重组你的对象。如果使用外部化,你就可以选择完全由自己完成读取和写出的工作,Java 对外部化所提供的唯一支持是接口:
  void readExternal(ObjectInput in)
  void writeExternal(ObjectOutput out)

  现在如何实现readExternal() 和writeExternal() 就完全看你自己了。

  看源码:我大致dubug了一下,序列化的执行链大概是这样的,writeObject -> writeObject0 -> writeOrdinaryObject -> writeSerialData(obj,desc)      //序列化

                                 -> writeExternalData((Externalizable) obj)   //外部化

//执行链走到writeOrdinaryObject方法内
/*
省略................代码
*/
}
        try {
            desc.checkSerialize();

            bout.writeByte(TC_OBJECT);
            writeClassDesc(desc, false);
            handles.assign(unshared ? null : obj);
            if (desc.isExternalizable() && !desc.isProxy()) {
                writeExternalData((Externalizable) obj);        //走外部化
            } else {
                writeSerialData(obj, desc);                     //走正常序列化
            }
        } finally {
            if (extendedDebugInfo) {
                debugInfoStack.pop();
            }
        }

  ①.先看序列化链走的 writeSerialData 方法,序列化会自动进行一系列逻辑判断,其中会有一些循环啥的,new 对象之类的操作,最终按照一定的存储和重组规则给完成序列化对象,源码如下:

private void writeSerialData(Object obj, ObjectStreamClass desc)
        throws IOException
    {
        ObjectStreamClass.ClassDataSlot[] slots = desc.getClassDataLayout();
        for (int i = 0; i < slots.length; i++) {
            ObjectStreamClass slotDesc = slots[i].desc;
            if (slotDesc.hasWriteObjectMethod()) {
                PutFieldImpl oldPut = curPut;
                curPut = null;
                SerialCallbackContext oldContext = curContext;

                if (extendedDebugInfo) {
                    debugInfoStack.push(
                        "custom writeObject data (class \"" +
                        slotDesc.getName() + "\")");
                }
                try {
                    curContext = new SerialCallbackContext(obj, slotDesc);
                    bout.setBlockDataMode(true);
                    slotDesc.invokeWriteObject(obj, this);
                    bout.setBlockDataMode(false);
                    bout.writeByte(TC_ENDBLOCKDATA);
                } finally {
                    curContext.setUsed();
                    curContext = oldContext;
                    if (extendedDebugInfo) {
                        debugInfoStack.pop();
                    }
                }

                curPut = oldPut;
            } else {
                defaultWriteFields(obj, slotDesc);
            }
        }
    }

   ②.再看外部化链走的 writeExternalData 方法,直接就会走进我们实现外部化必须重写的方法writeExternal,完全都交由我们自己来决定存储什么

总结:

  Serializable接口
  · 优点1:内建支持
  · 优点2:易于实现
  · 缺点1:占用空间过大
  · 缺点2:由于额外的开销导致速度变比较慢
  Externalizable接口
  · 优点1:开销较少(程序员决定存储什么)
  · 优点2:可能的速度提升
  · 缺点1:虚拟机不提供任何帮助,也就是说所有的工作都落到了开发人员的肩上。

  在两者之间如何选择要根据应用程序的需求来定。serializable通常是最简单的解决方案,但是虚拟机必须弄清楚每个成员属性的结构,所以可能会导致不可接受的性能问题或空间问题;在出现这些问题的情况下,externalizable可能是一条可行之路。

  要记住一点,如果一个类是可外部化的(externalizable),那么externalizable方法将被用于序列化类的实例,即使这个类型也提供了serializable方法。 

// write方法用于实现定制序列化,read方法用于实现定制反序列化
public interface Externalizable extends java.io.Serializable {
    void writeExternal(ObjectOutput out) throws IOException;
    void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
}

七、序列化之使用 serialVersionUID的作用

  JAVA序列化的机制是通过判断类的serialVersionUID来验证的版本一致的。在进行反序列化时,JVM会把传进来的字节流中的serialVersionUID于本地相应实体类的serialVersionUID进行比较。如果相同说明是一致的,可进行反序列化,否则反序列化异常(InvalidCastException)

  序列化接口上的注释说明:

  如果用户没有自己声明一个serialVersionUID,接口会默认生成一个serialVersionUID(根据包名、类名、继承关系、非私有的方法和属性以及参数、返回值等诸多因子计算得出的,生成极度复杂的一个64位的long值。基本上计算出来的这个值是唯一的),但是强烈建议用户自定义一个serialVersionUID,因为默认的serialVersinUID对于class的细节非常敏感,类修改后默认的serialVersionUID也会发生变化。如果序列化和反序列化时用的serialversionUID不同,会导致InvalidClassException异常

  根据class自动生成的serialVersionUID对class的细节太过敏感,这种情况下,如果Class文件(类名,方法明等)没有发生变化(增加空格,换行,增加注释等等),就算再编译多次,serialVersionUID也不会变化的,但是一些细微却又无关紧要的变化很容易发生。 

  当指定序列化版本serialversionUID时,序列化端 和 反序列化端的对象属性个数、名字即使对应不上,也不会影响反序列化,对应不上的属性会有默认值,也就是说对象显式指定serialversionUID时,序列化和反序列化时,兼容性更好。这里所说的兼容性,仅仅以反序列化成功为标准。在很多复杂的业务中,兼容性还要满足其他约束条件,不仅仅是满足反序列化成功而已。

  这里不贴代码,都是些基本操作,分别对虽然序列化版本serialversionUID一致的几种情况进行简要说明:

    前提:A端和B端都需要一个相同的类(对比两边的类中serialVersionUID )
    ①.A端增加字段 -> B端反序不变(忽略A新增的字段)

    ②.A端不序列化 -> B端减少字段反序(被B端忽略了)

    ③.A端不变序列化,B端增加字段反序化(B端新增的字段被赋予0(int))

八、序列化的应用场景

  1、dubbo RPC序列化

    在dubbo RPC中,服务调用的入参和出参就是通过序列化和反序列化实现的。dubbo RPC同时支持多种序列化方式,例如:

    1).dubbo序列化:阿里尚未开发成熟的高效java序列化实现,阿里不建议在生产环境使用它
    2).hessian2序列化:hessian是一种跨语言的高效二进制序列化方式。但这里实际不是原生的hessian2序列化,而是阿里修改过的hessian lite,它是dubbo RPC默认启用的序列化方式
    3).json序列化:目前有两种实现,一种是采用的阿里的fastjson库,另一种是采用dubbo中自己实现的简单json库,但其实现都不是特别成熟,而且json这种文本序列化性能一般不如上面两种二进制序列化。
    4).java序列化:主要是采用JDK自带的Java序列化实现,性能很不理想。

    对于dubbo RPC远程调用方式,只了解到开篇解释过的,进程间通信调用另一地址空间的过程和函数,服务之间进行传参和返回值接收的前提就是把对象给序列化,转化成流后再通过sockets进行网络传输,接收方再进行反序即可,dubbo RPC默认采用hessian2序列化

  2、缓存序列化

    在企业开发中缓存能提高系统的性能。无论是redis、memcache等存储的都是序列化后的信息。

    对于了解redis来说,不进行序列化操作,存储的数据回到redis查看数据时就会出现乱码,因为redis底层默认RedisTemplate的所有序列化器都是使用这个序列化器:defaultSerializer,所以必须为使用 redisTemplate 进行set操作时指定序列化绑定

结束

更多的也还不懂,就不深入下去了先,这里先留下几个点以后再研究:

①序列化攻击问题

②枚举实现单例模式(避免反射序列化问题)

③jackson类库的JSON操作方法(序列化问题):ObjectMapper

④各种序列化的区别联系和应用场景

总结

  总结搬运

 

参考博客:

1、序列化梳理_麦田里的码农-CSDN博客

2、java类中serialVersionUID的作用_u014750606的博客-CSDN博客_serialversionuid

  • 1
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值