跳转至

序列化

示例项目

序列化 (Serialization)是将对象的状态信息转换为可以存储或传输的形式的过程。

  • 特定语言:如 JDK 序列化,Kryo 序列化;
  • 跨语言:如 Hessian, Protobuf, Thrift, Avro, Json 等;

JDK

序列版本UID(serial version UID),流的唯一标识符:

  • 默认根据类名称、实现接口、成员等信息自动生成,因此很容易出现兼容性问题;
  • transient修饰的变量,默认序列化不会进行序列化;

自定义序列化

readObject & writeObject

Java实现序列化,实现Serializable接口,该接口中没有任何的方法,但是要实现自定义的序列化方法时,需要重载readObject, writeObject且都是private

  • ObjectOutputStream调用writeObject时,会根据instanceof Serializable,以及类的ObjectStreamClass(硬编码读取该三个方法),判断是否调用用户实现的方法还是默认的序列化方法;
private void writeObject(java.io.ObjectOutputStream out) throws IOException
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException;

除了基本的writeObjectreadObject之外,以下三个函数也可以控制序列化和反序列化:

// readObjectNoData is used in an unusual case where the serializer (writer) is working with a version of a class with no base class, whereas the deserializer (reader) of the class has a version of the class that IS based on a subclass. The subclass can say "it's ok if my base class isn't in the serialized data - just make an empty one" by implementing readObjectNoData.
// 序列化时用旧类,反序列化用新版本类(且变成子类),可以在父类上定义该方法,生成默认的父对象的成员值
private void readObjectNoData() throws ObjectStreamException;

// 实际序列化的对象将是作为writeReplace方法返回值的对象,而且序列化过程的依据是实际被序列化对象的序列化实现
private Object writeReplace()

// 再readObject()调用之后自动调用, 将反序列化之后的对象替换掉(可用于单例/枚举等场景)
Object readResolve() throws ObjectStreamException;

writeReplace & readResolve

Serializable还有两个标记接口方法可以实现序列化对象的替换,即writeReplacereadResolve:

  • Object writeReplace() throws ObjectStreamException
  • 序列化时会先调用writeReplace方法将当前对象替换成另一个对象(该方法会返回替换后的对象,替换后的对象必须可序列化)并将其写入流中,调用过程为 writeObject(writeReplace())
  • Object readResolve() throws ObjectStreamException
  • readResolve会在readObject调用之后自动调用readResolve里再对该对象进行一定的修改,而最终修改后的结果将作为readObject的返回结果;
  • 应用就是保护性恢复单例的对象
  • 对单例对象,重写readResolve方法,保证返回的是系统中唯一的单例。

枚举类型的序列化

在枚举类型的序列化和反序列化上,Java做了特殊的规定,即在序列化的时候Java仅仅是将枚举对象的name属性输出到结果中,反序列化的时候则是通过java.lang.Enum的valueOf方法来根据名字查找枚举对象

同时,编译器是不允许任何对这种序列化机制的定制,因此禁用了writeObject、readObject、readObjectNoData、writeReplace和readResolve等方法。

Lambda 表达式的序列化(TODO)

val writeReplace = closure.getClass.getDeclaredMethod("writeReplace")
writeReplace.setAccessible(true)
writeReplace.invoke(closure).asInstanceOf[java.lang.invoke.SerializedLambda]

lambda 会生成如下字节码

// lambda生成的字节码
@FunctionalInterface
interface Print<T> {
    public void print(T x);
}
public class Lambda {  
    public static void PrintString(String s, Print<String> print) {
        print.print(s);
    }
    private static void lambda$0(String x) {
        System.out.println(x);
    }
    final class $Lambda$1 implements Print{
        @Override
        public void print(Object x) {
            lambda$0((String)x);
        }
    }
    public static void main(String[] args) {
        PrintString("test", new Lambda().new $Lambda$1());
    }
}

Kryo

Java 主流的序列化框架。

线程

非线程安全Each thread should have its own Kryo, Input, and Output instances.

使用 ThreadLocal 或者 Pool 。

Hessian

跨语言,一种动态类型、二进制序列化和 Web 服务协议,专为面向对象的传输而设计

注意子类和父类不能有同名字段,子类的属性总是会被父类的覆盖

  • 当序列化对象是一个对象继承另外一个对象的时候,当一个属性在子类和有一个相同属性的时候,反序列化后子类属性总是为null。