首页 > 其他分享 >序列化与反序列化-基本了解使用

序列化与反序列化-基本了解使用

时间:2024-11-16 19:16:55浏览次数:3  
标签:基本 Java Protobuf 对象 了解 User new 序列化

什么是序列化与反序列化

        网络传输的数据必须是二进制数据,但调用方请求的出入参数都是对象。对象是不能直接在网络中传输的,所以我们需要提前把它转成可传输的二进制,并且要求转换算法是可逆的,这个过程我们一般叫做“序列化”。 这时,服务提供方就可以正确地从二进制数据中分割出不同的请求,同时根据请求类型和序列化类型,把二进制的消息体逆向还原成请求对象,这个过程我们称之为“反序列化”。

序列化就是将对象转换成二进制数据的过程,而反序列就是反过来将二进制转换为对象的过程。

RPC通信流程图

常用的序列化

java

         目前主流的微服务框架却几乎没有用到 Java 序列化,SpringCloud 用的是 Json 序列化,Dubbo 虽然兼容了 Java 序列化,但默认使 用的是 Hessian 序列化。这是为什么呢? 

        Java 提供了一种序列化机制,这种机制能够将一个对象序列化为二进制形式(字节数组),用于写入磁盘或输出到网络,同时也能从网络或磁盘中读取字节数组,反序列化成对象,在程序中使用。

        JDK 提供的两个输入、输出流对象 ObjectInputStream 和 ObjectOutputStream,它们只能对实现了 Serializable 接口的类的对象进行反序列化和序列化。

        ObjectOutputStream 的默认序列化方式,仅对对象的非 transient 的实例变量进行序列化,而不会序列化对象的 transient 的实例变量,也不会序列化静态变量。在被反序列化后,transient 变量的值被设为初始值,如 int 型的是 0,对象型的是 null

对象序列化保存的是对象的”状态” 不会序列化静态变量。

        在实现了 Serializable 接口的类的对象中,会生成一个 serialVersionUID 的版本号,它会在反序列化过程中来验证序列化对象是否加载了反序列化的类,如果是具有相同类名的不同版本号的类,在反序列化中是无法获取对象的。

        具体实现序列化的是 writeObject 和 readObject,通常这两个方法是默认的,当然我们也可以在实现 Serializable 接口的类中对其进行重写,定制一套属于自己的序列化与反序列化机制。

        另外,Java 序列化的类中还定义了两个重写方法:writeReplace() 和 readResolve(),前者是用来在序列化之前替换序列化对象的,后者是用来在反序列化之后对返回对象进行处理的。

序列化子父类说明

要想将父类对象也序列化,就需要让父类也实现 Serializable 接口。

@Data
@AllArgsConstructor
public class User implements Serializable {
    @Serial
    private static final long serialVersionUID = 1L;
    private String name;
    private int age;

    @Serial
    private void writeObject(ObjectOutputStream out) throws IOException {
        // 自定义序列化逻辑
        out.defaultWriteObject();
        out.writeInt(age * 2); // 假设我们需要以某种特殊方式序列化年龄
    }

    @Serial
    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        // 自定义反序列化逻辑
        in.defaultReadObject();
        age = in.readInt() / 2; // 对应地反序列化
    }

    public static void main(String[] args) {
        User user = new User("John Doe", 30);

        // 序列化
        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("user.ser"))) {
            oos.writeObject(user);
            System.out.println("User serialized.");
        } catch (IOException e) {
            e.printStackTrace();
        }

        // 反序列化
        try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("user.ser"))) {
            User deserializedUser = (User) ois.readObject();
            System.out.println("User deserialized: " + deserializedUser.getName() + ", " + deserializedUser.getAge());
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

ObjectOutputStream序列化过程图

序列化过程就是在读取对象数据的时候,不断加入一些特殊分隔符,这些特殊分隔符用于在反序列化过程中截断用。

头部数据用来声明序列化协议、序列化版本,用于高低版本向后兼容

对象数据主要包括类名、签名、属性名、属性类型及属性值,当然还有开头结尾等数据,除了属性值属于真正的对象值,其他都是为了反序列化用的元数据

存在对象引用、继承的情况下,就是递归遍历“写对象”逻辑

 实际上任何一种序列化框架,核心思想就是设计一种序列化协议,将对象的类型、属性类型、属性值一一按照固定的格式写到二进制字节流中来完成序列化,再按照固定的格式一一读出对象的类型、属性类型、属性值,通过这些信息重新创建出一个新的对象,来完成反序列化。

  1. 应用场景:包括网络传输、远程调用、持久化存储(如保存到文件或数据库)、以及分布式系统中数据交换。
  2. Java序列化关键类和接口:ObjectOutputStream用于序列化,ObjectInputStream用于反序列化。类必须实现Serializable接口才能被序列化。
  3. transient关键字:在序列化过程中,有些字段不需要被序列化,例如敏感数据,可以使用transient关键字标记不需要序列化的字段。
  4. serialVersionUlD:每个Serializable类都应该定义一个serialVersionUID,用于在反序列化时验证版本一致性。如果没有明确指定,Jva会根据类的定义自动生成一个UD,版本不匹配可能导致反序列化失败。
  5. 序列化性能问题:Jva的默认序列化机制可能比较慢,尤其是对于大规模分布式系统,可能会选择更加高效的序列化框架(如Protobuf、.Kryo)。
  6. 安全性:反序列化是一个潜在的安全风险,因为通过恶意构造的字节流,可能会加载不安全的类或执行不期望的代码。因此,反序列化过程需要进行输入验证,避免反序列化漏洞。

Java 序列化的缺陷

无法跨语言

现在的系统设计越来越多元化,很多系统都使用了多种语言来编写应用程序。比如,我们公司开发的一些大型游戏就使用了多种语言,C++ 写游戏服务,Java/Go 写周边服务,Python 写一些监控应用。而 Java 序列化目前只适用基于 Java 语言实现的框架,其它语言大部分都没有使用 Java 的序列化框架,也没有实现 Java 序列化这套协议。因此,如果是两个基于不同语言编写的应用程序相互通信,则无法实现两个应用服务之间传输对象的序列化与反序列化。

易被攻击

Java 官网安全编码指导方针中说明:“对不信任数据的反序列化,从本质上来说是危险的,应该予以避免”。可见 Java 序列化是不安全的。 我们知道对象是通过在ObjectInputStream 上调用 readObject() 方法进行反序列化的, 这个方法其实是一个神奇的构造器,它可以将类路径上几乎所有实现了 Serializable 接口的对象都实例化。这也就意味着,在反序列化字节流的过程中,该方法可以执行任意类型的代码,这是非常危险的。对于需要长时间进行反序列化的对象,不需要执行任何代码,也可以发起一次攻击。攻击者可以创建循环对象链,然后将序列化后的对象传输到程序中反序列化,这种情况会导致hashCode 方法被调用次数呈次方爆发式增长, 从而引发栈溢出异常。

Set root = new HashSet(); 
 Set s1 = root; 
 Set s2 = new HashSet(); 
 for (int i = 0; i < 100; i++) { 
 	Set t1 = new HashSet(); 
	 Set t2 = new HashSet(); 
 	t1.add("foo"); // 使 t2 不等于 t1 
 	s1.add(t1); 
	 s1.add(t2); 
 	s2.add(t1); 
	 s2.add(t2); 
 	s1 = t1; 
 	s2 = t2; 
 }

        2015 年 FoxGlove Security 安全团队的 breenmachine 发布过一篇长博客,主要内容是:通过 Apache Commons Collections,Java 反序列化漏洞可以实现攻击。一度横扫了 WebLogic、WebSphere、JBoss、Jenkins、OpenNMS 的最新版,各大 Java Web Server 纷纷躺枪。

        其实,Apache Commons Collections 就是一个第三方基础库,它扩展了 Java 标准库里的 Collection 结构,提供了很多强有力的数据结构类型,并且实现了各种集合工具类。

        实现攻击的原理就是:Apache Commons Collections 允许链式的任意的类函数反射调用,攻击者通过“实现了 Java 序列化协议”的端口,把攻击代码上传到服务器上,再由 Apache Commons Collections 里的 TransformedMap 来执行

序列化后的流太大

JSON

JSON 可能是我们最熟悉的一种序列化格式了,JSON 是典型的 Key-Value 方式,没有数据类型,是一种文本型序列化框架。

无论是前台 Web 用 Ajax 调用、用磁盘存储文本类型的数据,还是基于 HTTP 协议的 RPC 框架通信,都会选择 JSON 格式。

但用 JSON 进行序列化有这样两个问题:

  1. 进行序列化的额外空间开销比较大,对于大数据量服务这意味着需要巨大的内存和磁盘开销;
  2. 没有类型,但像 Java 这种强类型语言,需要通过反射统一解决,所以性能不会太好。所以如果 RPC 框架选用 JSON 序列化,服务提供者与服务调用者之间传输的数据量要相对较小,否则将严重影响性能。

 Hessian

 Hessian 是动态类型、二进制、紧凑的,并且可跨语言移植的一种序列化框架。Hessian 协 议要比 JDK、JSON 更加紧凑,性能上要比 JDK、JSON 序列化高效很多,而且生成的字节数也更小。

import com.caucho.hessian.io.Hessian2Input;
import com.caucho.hessian.io.Hessian2Output;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;

public class User {
    private String name;
    private int age;

    public static void main(String[] args) {
        User user = new User("John Doe", 30);

        // 序列化
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        Hessian2Output h2o = new Hessian2Output(baos);
        try {
            h2o.writeObject(user);
            h2o.flush();
            byte[] serializedData = baos.toByteArray();

            System.out.println("Serialized Data: " + bytesToHex(serializedData));

            // 反序列化
            ByteArrayInputStream bais = new ByteArrayInputStream(serializedData);
            Hessian2Input h2i = new Hessian2Input(bais);
            User deserializedUser = (User) h2i.readObject(User.class);

            System.out.println("Deserialized User: " + deserializedUser);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private static String bytesToHex(byte[] bytes) {
        StringBuilder sb = new StringBuilder();
        for (byte b : bytes) {
            sb.append(String.format("%02x", b));
        }
        return sb.toString();
    }
}

相对于 JDK、JSON,由于 Hessian 更加高效,生成的字节数更小,有非常好的兼容性和稳定性,所以 Hessian 更加适合作为 RPC 框架远程通信的序列化协议。

但 Hessian 本身也有问题,官方版本对 Java 里面一些常见对象的类型不支持,比如:

Linked 系列,LinkedHashMap、LinkedHashSet 等,但是可以通过扩展

CollectionDeserializer 类修复;

Locale 类,可以通过扩展 ContextSerializerFactory 类修复;

Byte/Short 反序列化的时候变成 Integer

Protobuf

        Protobuf 是 Google 公司内部的混合语言数据标准,是一种轻便、高效的结构化数据存储格式,可以用于结构化数据序列化,支持 Java、Python、C++、Go 等语言。

        Protobuf使用的时候需要定义 IDL(Interface description language),然后使用不同语言的 IDL 编译器,生成序列化工具类,

        Protocol Buffers 是一种轻便高效的结构化数据存储格式。它使用 T-L-V(标识 - 长度 - 字段值)的数据格式来存储数据,T 代表字段的正数序列 (tag),Protocol Buffers 将对象中的每个字段和正数序列对应起来,对应关系的信息是由生成的代码来保证的。在序列化的时候用整数值来代替字段名称,于是传输流量就可以大幅缩减;L 代表 Value 的字节长度,一般也只占一个字节;V 则代表字段值经过编码后的值。这种数据格式不需要分隔符,也不需要空格,同时减少了冗余字段名。

        Protobuf 的这种数据存储格式,不仅压缩存储数据的效果好, 在编码和解码的性能方面也很高效。Protobuf 的编码和解码过程结合.proto 文件格式,加上 Protocol Buffer 独特的编码格式,只需要简单的数据运算以及位移等操作就可以完成编码与解码。可以说Protobuf 的整体性能非常优秀。

优点是:

  1. 序列化后体积相比 JSON、Hessian 小很多;
  2. IDL 能清晰地描述语义,所以足以帮助并保证应用程序之间的类型不会丢失,无需类似 XML 解析器;
  3. 序列化反序列化速度很快,不需要通过反射获取类型;
  4. 消息格式升级和兼容性不错,可以做到向后兼容。
<dependencies>
  <!-- 其他依赖 -->
  <dependency>
    <groupId>com.google.protobuf</groupId>
    <artifactId>protobuf-java</artifactId>
    <version>3.17.3</version> <!-- 请检查是否有更新版本 -->
  </dependency>
  <dependency>
    <groupId>io.grpc</groupId>
    <artifactId>grpc-protobuf</artifactId>
    <version>1.40.1</version> <!-- 请检查是否有更新版本 -->
  </dependency>
  <dependency>
    <groupId>io.grpc</groupId>
    <artifactId>grpc-netty-shaded</artifactId>
    <version>1.40.1</version> <!-- 请检查是否有更新版本 -->
  </dependency>
  <dependency>
    <groupId>io.grpc</groupId>
    <artifactId>grpc-stub</artifactId>
    <version>1.40.1</version> <!-- 请检查是否有更新版本 -->
  </dependency>
</dependencies>

<!-- 插件配置,用于编译 .proto 文件 -->
<build>
  <extensions>
    <extension>
      <groupId>kr.motd.maven</groupId>
      <artifactId>os-maven-plugin</artifactId>
      <version>1.6.2</version>
    </extension>
  </extensions>
  <plugins>
    <plugin>
      <groupId>org.xolstice.maven.plugins</groupId>
      <artifactId>protobuf-maven-plugin</artifactId>
      <version>0.6.1</version>
      <configuration>
        <protocArtifact>com.google.protobuf:protoc:3.17.3:exe:${os.detected.classifier}</protocArtifact>
        <pluginId>grpc-java</pluginId>
        <pluginArtifact>io.grpc:protoc-gen-grpc-java:1.40.1:exe:${os.detected.classifier}</pluginArtifact>
      </configuration>
      <executions>
        <execution>
          <goals>
            <goal>compile</goal>
            <goal>compile-custom</goal>
          </goals>
        </execution>
      </executions>
    </plugin>
  </plugins>
</build>

接下来,创建一个 .proto 文件来定义你的消息格式。例如,在 src/main/proto/user.proto 文件

syntax = "proto3";

option java_multiple_files = true;
option java_package = "com.example.protobuf";
option java_outer_classname = "UserProto";

message User {
  string name = 1;
  int32 id = 2;
  repeated string email = 3;
}

编译 .proto 文件

当你运行 mvn compile 命令时,Maven 插件会自动编译 .proto 文件,并生成相应的 Java 代码。生成的代码将位于 target/generated-sources/protobuf/java 目录下。

使用生成的类

现在,你可以在 Spring Boot 应用中使用生成的 Protobuf 类。

import com.example.protobuf.User;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/users")
public class UserController {

  @PostMapping
  public String createUser(@RequestBody User user) {
    // 处理新创建的用户
    System.out.println("Received user: " + user);
    return "User created successfully.";
  }

  @GetMapping("/{id}")
  public User getUser(@PathVariable int id) {
    // 构建并返回一个示例用户
    return User.newBuilder()
        .setName("John Doe")
        .setId(id)
        .addEmail("[email protected]")
        .build();
  }
}

配置 Content-Type

为了确保客户端发送的请求和接收的响应都正确处理 Protobuf 格式,你可能需要配置 Content-Type。Spring Boot 默认支持 JSON,但对于 Protobuf,你可能需要自定义 HttpMessageConverter

import com.google.protobuf.util.JsonFormat;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.protobuf.ProtobufHttpMessageConverter;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import java.util.List;

@Configuration
public class WebConfig implements WebMvcConfigurer {

  @Override
  public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
    converters.add(new ProtobufHttpMessageConverter());
  }
}

Protobuf 非常高效,但是对于具有反射和动态能力的语言来说,这样用起来很费劲,这一点就不如 Hessian,比如用 Java 的话,这个预编译过程不是必须的,可以考虑使用Protostuff。

Protostuff 不需要依赖 IDL 文件,可以直接对 Java 领域对象进行反 / 序列化操作,在效率上跟 Protobuf 差不多,生成的二进制格式和 Protobuf 是完全相同的,可以说是一个 Java版本的 Protobuf 序列化框架。

Thrift  

如何选择序列化?

        在序列化的选择上,与序列化协议的效率、性能、序列化协议后的体积相比,其通用性和兼容性的优先级会更高,因为他是会直接关系到服务调用的稳定性和可用率的,对于服务的性能来说,服务的可靠性显然更加重要。我们更加看重这种序列化协议在版本升级后的兼容性是否很好,是否支持更多的对象类型,是否是跨平台、跨语言的,是否有很多人已经用过并且踩过了很多的坑,其次我们才会去考虑性能、效率和空间开销。

        还有一点我要特别强调。除了序列化协议的通用性和兼容性,序列化协议的安全性也是非常重要的一个参考因素,甚至应该放在第一位去考虑。以 JDK 原生序列化为例,它就存在漏洞。如果序列化存在安全漏洞,那么线上的服务就很可能被入侵

        我们首选的还是 Hessian 与 Protobuf,因为他们在性能、时间开销、空间开销、通用性、 兼容性和安全性上,都满足了我们的要求。其中 Hessian 在使用上更加方便,在对象的兼 容性上更好;Protobuf 则更加高效,通用性上更有优势

注意问题

        对象构造得过于复杂:属性很多,并且存在多层的嵌套,比如 A 对象关联 B 对象,B 对象又聚合 C 对象,C 对象又关联聚合很多其他对象,对象依赖关系过于复杂。序列化框架在序列化与反序列化对象时,对象越复杂就越浪费性能,消耗 CPU,这会严重影响 RPC 框架整体的性能;另外,对象越复杂,在序列化与反序列化的过程中,出现问题的概率就越高。

        对象过于庞大:我经常遇到业务过来咨询,为啥他们的 RPC 请求经常超时,排查后发现他们的入参对象非常得大,比如为一个大 List 或者大 Map,序列化之后字节长度达到了上兆字节。这种情况同样会严重地浪费了性能、CPU,并且序列化一个如此大的对象是很耗费时间的,这肯定会直接影响到请求的耗时。

        使用序列化框架不支持的类作为入参类:比如 Hessian 框架,他天然是不支持LinkHashMap、LinkedHashSet 等,而且大多数情况下最好不要使用第三方集合类,如Guava 中的集合类,很多开源的序列化框架都是优先支持编程语言原生的对象。因此如果入参是集合类,应尽量选用原生的、最为常用的集合类,如 HashMap、ArrayList。

        对象有复杂的继承关系:大多数序列化框架在序列化对象时都会将对象的属性一一进行序列化,当有继承关系时,会不停地寻找父类,遍历属性。就像问题 1 一样,对象关系越复杂,就越浪费性能,同时又很容易出现序列化上的问题。

在 RPC 框架的使用过程中,我们要尽量构建简单的对象作为入参和返回值对象

标签:基本,Java,Protobuf,对象,了解,User,new,序列化
From: https://blog.csdn.net/qq_26594041/article/details/143697644

相关文章

  • 深入了解内容分发网络(CDN)
    在当今的电商、直播、社交工具和视频网站等互联网应用中,存在着大量的图片、视频、文档等资源需要分发给用户。对于一些体量较大的应用而言,若将大量资源集中在单一节点进行分发,几乎没有哪个机房能够承受如此巨大的流量。例如,一个日活达100W的小型互联网产品,若每次请求需1M......
  • Java序列化与反序列化深度解析
    一、引言在Java开发中,序列化与反序列化是非常重要的概念和技术手段。它允许我们将对象转换为字节流以便于存储或传输,然后在需要的时候再将字节流还原为对象。这一机制在很多场景中都有着广泛的应用,例如数据持久化、分布式系统中的远程方法调用(RMI)、缓存等。本文将深入探讨......
  • 看过这个,你可能更了解指针一点(2)
    先来看下图你认为以下的打印的结果是什么?接下来,我们先来分析****在1中arr单独放在sizeof内表示整个数组,因此计算的为整个数组大小。即6乘1得到61的答案为6****在2中arr没有被单独放在sizeof中,arr此时表示数组首元素的地址,+0则表示计算的是第一个元素地址的大小,其结......
  • Java反序列化-Commons Collections3利用链分析详解
    介绍CC3与CC1和CC6的主要区别在于,CC1和CC6依赖反射机制来执行Runtime.getRuntime().exec()等危险命令,而如果服务器将这些方法列入黑名单,这两种方式就会失效。相比之下,CC3通过类加载器动态加载恶意类来执行危险函数,绕过黑名单限制,从而达到命令执行的目的。公众号:T......
  • 【Pikachu】PHP反序列化RCE实战
    痛是你活着的证明1.PHP反序列化概述在理解PHP中serialize()和unserialize()这两个函数的工作原理之前,我们需要先了解它们各自的功能及其潜在的安全隐患。接下来,我会对相关概念做更详细的扩展解释。1.序列化serialize()序列化(serialization)是指将一个对象或数据......
  • 5.APM32-TMR-基本定时器定时
    效果展示TMR-基本定时器定时使用基本定时器进行定时,使LED按1Hz的频率闪烁硬件原理图我们这次只用到PA8引脚,其他几个引脚不看源代码关于LED的代码就不贴出了,LED相关代码可以在流水灯文章中找到。定时器部分#ifndef__BSP_BASE_TMR_H__#define__BSP_BASE_......
  • 短视频app搭建,了解Java基本数据类型
    短视频app搭建,了解Java基本数据类型Java条件语句if…else一个if语句包含一个布尔表达式和一条或多条语句。//如果只有一条语句可以不用{}if(--)a=0;//多条语句需要{}if(--){a=0;b=0;}//if()的()里面填条件 if…else语句if语句后面可以跟else语句,当if......
  • 我谈二值形态学基本运算——腐蚀、膨胀、开运算、闭运算
    Gonzalez从集合角度定义膨胀和腐蚀,不易理解。Throughthesedefinitions,youcaninterpretdilationanderosionasslidingneighborhoodoperationsanalogoustoconvolution(orspatialfiltering).禹晶、肖创柏、廖庆敏《数字图像处理(面向新工科的电工电子信息......
  • Tensorflow基本概念
    简介:本文从Graph讲到Session,同时讲解了tf.constant创建tensor的用法和variable需要初始化的知识点,可以给你打好一个学习Tensorflow的基础。本文都是基于TensorFlow1.14.0的版本下运行。本专栏将会系统的讲解TensorFlow在1.14.0版本下的各种用法并且会讲解各种常用的神经网......
  • 正在车载测试的你,ADAS测试了解多少?
    随着智能驾驶技术的飞速发展,汽车行业的竞争从硬件转向了软件生态。高级驾驶辅助系统(ADAS)已成为汽车智能化的标配,从车道保持、自动紧急刹车到自适应巡航,这些功能无一不在改变我们的驾驶体验。然而,ADAS测试作为保障这些技术安全可靠的关键环节,你对它了解多少?ADAS的测试覆盖哪些......