• 序列化_原理与应用


    关键字:序列化,java,proto,json,字节序列,字节数组,byte array,serialize
    
    • 1

    序列化简介

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

    如在内存中的java对象(本是方便JVM使用的格式)序列化为硬盘或是网络传输中的二进制文件、或是序列化为json字符串,再通过http/https协议进行传输。

    通常是转换为字节序列,以在网络通信、IO流等中传输,也常见序列化为json字符串

    通过序列化,对象将其当前状态(可理解为数据)写入到临时或持久性存储区。以后,可以通过从存储区中读取或反序列化对象的状态,重新创建该对象。

    什么时候序列化?

    一些例子:

    当你想把的内存中的对象状态保存到一个文件中或者数据库中时候;

    当你想用套接字在网络上传送对象的时候;

    当你想通过RMI传输对象的时候;

    如java对象需序列化才能保存至kafka

    对象引用持久化问题

    对象B引用了对象A,在持久化B时,会多储存一份A?似乎ORM是这样的

    以下序列化机制的解决方案: 1.保存到磁盘的所有对象都获得一个序列号(1, 2, 3等等)

    2.当要保存一个对象时,先检查该对象是否被保存了

    3.如果以前保存过,只需写入"与已经保存的具有序列号x的对象相同"的标记,否则,保存该对象

    通过以上的步骤序列化机制解决了对象引用的问题!

    • Q:不实现序列化接口直接IO如何
      • A:会报错2333不让这么写
    • Q:redis保存的对象要实现序列化接口么?
      • A:需要,用java自带的序列化方法即可

    序列化与反序列化的一摞一摞实现方式

    java自带的序列化

    常见的实现Serializable接口的方式,用起来十分方便。该方法会将对象序列化为字节序列。

    但其实也可以实现Externalnalizable接口,虽然冷门到几乎没用。

    使用流程为:实现Serializable接口,指定serialVersionUID;

    然后就可以将对象直接传入IO流等方法中了。

    IO流:

    • java.io.ObjectOutputStream:对象输出流。 该类的writeObject(Object obj)方法将将传入的obj对象进行序列化,把得到的字节序列写入到目标输出流中进行输出。

    • java.io.ObjectInputStream:对象输入流。该类的readObject()方法从输入流中读取字节序列,然后将字节序列反序列化为一个对象并返回。
      
      • 1

    (以自定义的Student类为例,该类怎么实现并不重要):

    ①若Student类仅仅实现了Serializable接口,则可以按照以下方式进行序列化和反序列化。

    ObjectOutputStream采用默认的序列化方式,对Student对象的非transient的实例变量进行序列化。 
    ObjcetInputStream采用默认的反序列化方式,对Student对象的非transient的实例变量进行反序列化。
    
    • 1
    • 2

    ②若Student类仅仅实现了Serializable接口,并且还定义了readObject(ObjectInputStream in)和writeObject(ObjectOutputSteam out),则采用以下方式进行序列化与反序列化。

    ObjectOutputStream调用Student对象的writeObject(ObjectOutputStream out)的方法进行序列化。 
    ObjectInputStream会调用Student对象的readObject(ObjectInputStream in)的方法进行反序列化。
    
    • 1
    • 2

    ③若Student类实现了Externalnalizable接口,且Student类必须实现readExternal(ObjectInput in)和writeExternal(ObjectOutput out)方法,则按照以下方式进行序列化与反序列化。

    ObjectOutputStream调用Student对象的writeExternal(ObjectOutput out))的方法进行序列化。 
    ObjectInputStream会调用Student对象的readExternal(ObjectInput in)的方法进行反序列化。
    
    • 1
    • 2

    序列化运行时使用一个称为 serialVersionUID 的版本号与每个可序列化类相关联,该序列号在反序列化过程中用于验证序列化对象的发送者和接收者是否为该对象加载了与序列化兼容的类。为它赋予明确的值。显式地定义serialVersionUID有两种用途:

    在某些场合,希望类的不同版本对序列化兼容,因此需要确保类的不同版本具有相同的serialVersionUID;

    在某些场合,不希望类的不同版本对序列化兼容,因此需要确保类的不同版本具有不同的serialVersionUID。

    为什么一个类实现了Serializable接口,它就可以被序列化呢?在上节的示例中,使用ObjectOutputStream来持久化对象,在该类中有如下代码:

    private void writeObject0(Object obj, boolean unshared) throws IOException {
          ...
        if (obj instanceof String) { 
            writeString((String) obj, unshared);
        } else if (cl.isArray()) { 
            writeArray(obj, desc, unshared);
        } else if (obj instanceof Enum) {
            writeEnum((Enum) obj, desc, unshared);
        } else if (obj instanceof Serializable) {
            writeOrdinaryObject(obj, desc, unshared);
        } else {
            if (extendedDebugInfo) {
                throw new NotSerializableException(cl.getName() + "\n"
                        + debugInfoStack.toString());
            } else {
                throw new NotSerializableException(cl.getName());
            }
        }
        ...
    } 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    从上述代码可知,如果被写对象的类型是String,或数组,或Enum,或Serializable,那么就可以对该对象进行序列化,否则将抛出NotSerializableException。

    阿里巴巴开发规范强制要求:二方库参数可以枚举,返回值不能为枚举,就是因为如此

    而JSON在反序列化的过程中,对于一个枚举类型,会尝试调用对应的枚举类的valueOf方法来获取到对应的枚举。

    而我们查看枚举类的valueOf方法的实现时,就可以发现,如果从枚举类中找不到对应的枚举项的时候,就会抛出IllegalArgumentException

    protobuf序列化

    这种方式我们需要写proto文件,还要使用工具来编译生成java文件,实在有些麻烦。

    Google Protocol Buffer( 简称 Protobuf) 是 Google 公司内部的混合语言数据标准。他们用于 RPC 系统和持续数据存储系统。
    Protocol Buffers 是一种轻便高效的结构化数据存储格式,可以用于结构化数据串行化,或者说序列化。它很适合做数据存储或 RPC 数据交换格式。可用于通讯协议、数据存储等领域的语言无关、平台无关、可扩展的序列化结构数据格式。提供了多种语言的 API

    首先要引入protobuf的依赖

            <dependency>
                <groupId>com.google.protobufgroupId>
                <artifactId>protobuf-java-utilartifactId>
                <version>${protobuf-java-util.version}version>
                <exclusions>
                    <exclusion>
                        <groupId>com.google.guavagroupId>
                        <artifactId>guavaartifactId>
                    exclusion>
                exclusions>
            dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    说明:编写此篇博客时,我采用的protobuf依赖版本是3.19.1,且由于在其他地方单独导入了guava工具包,所以在此处pom.xml文件中排除了guava的依赖

    书写 .proto 文件

    首先我们需要编写一个 proto 文件,定义我们程序中需要处理的结构化数据,在 protobuf 的术语中,结构化数据被称为 Message。proto 文件非常类似 java 或者 C 语言的数据定义。

    下面是一个proto文件的例子。

    syntax = "proto3";
    package com.***.common.entity.message;
    
    message Data
    {
        uint64 id = 1;
    
        string app = 2;
    
        uint32 priority = 4;
        //optional 可选的
        optional int32 time = 5;
    
        Code code = 6;
    
        required string msg = 7;
    
        repeated Field fields = 8;
    }
    
    message Field
    {
        string name = 1;
    
        uint32 type = 2;
    
        string unit = 3;
    }
    
    message ValueRow
    {
    
        string instance = 1;
    
        repeated string columns = 2;
    }
    
    enum Code
    {
        SUCCESS = 0;
    
        UN_AVAILABLE = 1;
    
        UN_REACHABLE = 2;
    
        UN_CONNECTABLE = 3;
    
        FAIL = 4;
    
        TIMEOUT = 5;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51

    定义了自己的报文格式(message)之后,我们就可以运行ProtocolBuffer编译器(windows下为protoc.exe),将 .proto 文件编译成特定语言的类或是文件。这些类提供了简单的方法访问每个字段(像是 query() 和set_query()),像是访问类的方法一样将结构串行化或反串行化。

    protoc --java_out=src/main/java src/protobuf/***.proto
    
    • 1

    后面跟着个空格然后跟的是 proto描述文件的位置

    回车后如果没有报错,并且已经生成DataInfo类,则说明编译成功

    注意这里的新建对象并对属性进行赋值时必须采用链式

    DataInfo.Student student = DataInfo.Student.newBuilder()       
        .setName("").setAge(100).setAddress("中国").build();
    
    • 1
    • 2

    然后将对象进行序列化为字节数组就可以通过

    byte[] bytes = student.toByteArray();

    将字节数据反序列化为对象就可以通过

    DataInfo.Student student1 = DataInfo.Student.parseFrom(bytes);

    protostuff序列化

    protostuff基于protobuf发展而来的,相对于protobuf提供了更多的功能和更简易的用法。

    但也是有局限性的,据说在序列化的文件在10M以下的时候,还是使用java自带的序列化机制比较好,但是文件比较大的时候还是protostuff好一点,这里的10M不是严格的界限。

    
           io.protostuff
           protostuff-core
           1.6.0
    
    
          io.protostuff
          protostuff-runtime
          1.6.0
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    protoStuff序列化工具类

    public class ProtostuffUtils {
        //static,避免每次序列化都重新申请缓冲空间
        private static LinkedBuffer buffer = LinkedBuffer.allocate(LinkedBuffer.DEFAULT_BUFFER_SIZE);
        //缓存Schema
        private static Map<Class<?>, Schema<?>> schemaCache = new ConcurrentHashMap<Class<?>, Schema<?>>();
        //序列化方法,把指定对象序列化成字节数组
        @SuppressWarnings("unchecked")
        public static <T> byte[] serialize(T obj) {
            Class<T> clazz = (Class<T>) obj.getClass();
            Schema<T> schema = getSchema(clazz);
            byte[] data;
            try {
                data = ProtostuffIOUtil.toByteArray(obj, schema, buffer);
            } finally {
                buffer.clear();
            }
            return data;
        }
        //反序列化方法,将字节数组反序列化成指定Class类型
        public static <T> T deserialize(byte[] data, Class<T> clazz) {
            Schema<T> schema = getSchema(clazz);
            T obj = schema.newMessage();
            ProtostuffIOUtil.mergeFrom(data, obj, schema);
            return obj;
        }
        @SuppressWarnings("unchecked")
        private static <T> Schema<T> getSchema(Class<T> clazz) {
            Schema<T> schema = (Schema<T>) schemaCache.get(clazz);
            if (schema == null) {
                schema = RuntimeSchema.getSchema(clazz);
                if (schema == null) {
                    schemaCache.put(clazz, schema);
                }
            }
            return schema;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37

    首先获得要序列化对象的类,然后为其分配一个缓存空间,其次获得这个类的Schema。最后一行代码ProtostuffIOUtil.toByteArray进行序列化。

    注释:

    • LinkedBuffer.DEFAULT_BUFFER_SIZE为512字节,MIN_BUFFER_SIZE
    • schemaCache这个字段表示缓存的Schema。那这个Schema就是一个组织结构,就好比是数据库中的表、视图等等这样的组织机构,在这里表示的就是序列化对象的结构。?

    以JSON或String为中间手段序列化为字节序列

    java的String类有getBytes()方法可以获得字符串对应的、默认以操作系统编码的字节序列。而new String(byte[])方法可以以字节序列构造字符串。

    但是,虽然业务上有很多类实现了toString方法转换为字符串,却不常有从字符串还原为具体对象的构造方法。我们可以通过先将对象转换为JSON,再将JSON字符串序列化的方式得到字节序列。

    适用于类没有实现Serializable接口的情况

    JSONUtil方法在各个项目中有不同实现方式,此处代码仅展示序列化思路。

    //序列化
    JsonUtil.toJson(alert).getBytes());
    //反序列化
    JsonUtil.fromJson(new String(bytes), Alert.class);
    
    • 1
    • 2
    • 3
    • 4
    • JS 自身只能把对象转为字符序列,即 JSON 字符串,这是因为 JS 自身是基于字符串的,没有二进制类型,即 Byte 类型,因此不能像Java那序列化成字节序列。现在可以通过 Node.js 操作二进制数据了,不知道能不能通过 Node.js 实现字节序列的序列化。

    补充

    序列化可能引起的问题

    通常,对象实例的所有字段都会被序列化,这意味着数据会被表示为实例的序列化数据。这样,能够解释该格式的代码有可能能够确定这些数据的值,而不依赖于该成员的可访问性。类似地,反序列化从序列化的表示形式中提取数据并直接设置对象状态,这也与可访问性规则无关。

    破坏了对象封装的特性,即序列化后,可以直接访问属性,而不再是只能用用定义的方法访问。(因为不受jvm控制了?封装怎么实现的?)

    对于任何可能包含重要的安全性数据的对象,如果可能,应该使该对象不可序列化。如果它必须为可序列化的,请尝试生成特定字段来保存不可序列化的重要数据。如果无法实现这一点,则应注意该数据会被公开给任何拥有序列化权限的代码,并确保不让任何恶意代码获得该权限。

    Serializable不能序列化的情况:

    资源分配方面的类,比如socket,thread类,即使序列化,进行传输或者保存,也无法对他们进行重新的资源分配,而且,也没有必要这样实现;

    声明为static和transient类型的成员数据不能(不会)被序列化。因为static代表类的状态,transient代表对象的临时数据。

    • Java有很多基础类已经实现了serializable接口,比如String,Vector等。
    • 如果一个对象的成员变量是一个对象,那么这个对象的数据成员也会被保存!这是能用序列化解决深拷贝的重要原因;
      • 注意:浅拷贝请使用Clone接口的原型模式。
    • 默认的java序列化机制效率太低?

    当序列化一个对象到文件时, 按照 Java 的标准约定是给文件一个 .ser 扩展名。

    更多相关内容:

    序列化和反序列化的详解_tree_ifconfig的博客-CSDN博客

    https://blog.csdn.net/m0_58559010/article/details/117925076

    https://blog.csdn.net/i_wavelet/article/details/107874371

  • 相关阅读:
    六月集训(30)拓扑排序
    27、Flink 的SQL之SELECT (SQL Hints 和 Joins)介绍及详细示例(2-1)
    Linux安全—linux三剑客之sed(持续更新)
    11.多进程与多线程
    打造一站式采购结算平台,纸业B2B电子商务交易平台促进企业降本增效
    BS框架说明
    k8s--基础--6.2--环境搭建--单master高可用集群
    Zookeeper
    5个优秀设计网站,素材、灵感一步到位。
    如何使用SOLIDWORKS添加装饰螺纹线规格
  • 原文地址:https://blog.csdn.net/qq_44915801/article/details/130915360