W
e
l
c
o
m
e
: )

Java序列化与反序列化

一、什么是序列化和反序列化

序列化(Serialization) 和 反序列化(Deserialization) 是计算机科学中数据处理的核心概念,主要用于数据的 存储 、传输和状态恢复 。

简单来说:

  • 序列化 :将内存中的对象/数据结构转换为可存储或传输的格式(如字节流、JSON字符串)。
  • 反序列化:将存储或传输的格式还原为内存中的对象/数据结构。

二、序列化有哪些使用场景

1. 网络传输 — 最经典的是RPC调用

比如Dubbo这样的框架,客户端调用远程服务时,需要把方法参数(各种Java对象)通过网络传给服务端,结果也要传回来。这就必须先把对象序列化成字节流,到了对端再反序列化回来。

2. 持久化存储

把对象存到文件或数据库里,下次程序启动时再恢复。比如保存用户会话状态、游戏存档、复杂配置等。Redis缓存Java对象时,也常先把对象序列化再存储。

3. HTTP会话管理

Web应用集群环境下,用户Session需要在多台服务器间同步。Tomcat等容器会把Session对象序列化后复制到其他节点,或者存到外部Redis中,确保用户不管被分配到哪台机器,都能拿到自己的会话。

4. 深拷贝

想完全复制一个对象(包括它引用的所有子对象),一种简便方式就是先序列化再反序列化,能得到一个全新的独立副本。不过性能不高,一般不推荐在频繁调用的业务代码里这样用。

5. 消息队列

往RabbitMQ、Kafka发消息时,消息体如果是对象,就需要序列化。常见做法是序列化成JSON字符串或字节数组再发送。

三、序列化协议有哪些

常用的序列化协议有Hessian2、Protobuf、Kryo等,相对而言,Java原生序列化不仅效率低并且还存在安全隐患。下面我们通过代码来看看它们具体该如何使用:

一、Java原生序列化

使用Java原生序列化我们需要让需要序列化的类实现 java.io.Serializable接口。

import java.io.Serializable;

public class User implements Serializable {
    // 强烈建议显式声明,避免类变更后反序列化失败
    private static final long serialVersionUID = 1L;

    private String name;
    private int age;
    // transient 修饰的字段不会被序列化
    private transient String password;

    // 静态变量属于类,不属于对象,不会被序列化
    private static String platform = "Android";

    public User(String name, int age, String password) {
        this.name = name;
        this.age = age;
        this.password = password;
    }

    @Override
    public String toString() {
        return "User{name='" + name + "', age=" + age +
                ", password='" + password + "', platform='" + platform + "'}";
    }
}
public class Code01 {
    public static void main(String[] args) {
        //1.序列化:对象--》文件
        User user = new User("zs",23,"123456");
        try(FileOutputStream fos = new FileOutputStream("user.ser");
            ObjectOutputStream oos = new ObjectOutputStream(fos)){
            oos.writeObject(user);
        }catch (IOException e){
            e.printStackTrace();
        }
    }
}
public class Code01 {
    public static void main(String[] args) {
        //2.反序列化:文件--》对象
        User user = null;
        try(FileInputStream fis = new FileInputStream("user.ser");
            ObjectInputStream ois = new ObjectInputStream(fis)) {
            user = (User) ois.readObject();
            //反序列化结果:User{name='zs', age=23, password='null', platform='Android'}
            System.out.println("反序列化结果:"+user);
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

需要注意的是:Java反序列化有个著名的安全问题。反序列化时会调用 readObjectreadResolve等方法,如果字节流是恶意构造的,就能在反序列化时执行任意代码。历史上Log4j2、WebLogic等都爆出过相关高危漏洞。

二、Kryo

Kryo是一个高性能的 Java 序列化框架,常用于需要高效序列化/反序列化的场景(如分布式缓存、RPC 通信、数据存储等)。Kryo比原生序列化快很多,而且更省空间。下面来看demo示例:

<dependency>
    <groupId>com.esotericsoftware</groupId>
    <artifactId>kryo</artifactId>
    <version>5.6.0</version>
</dependency>
public class User {
    private String name;
    private int age;
    private String password;  // 不需要 transient,Kryo 有自己的控制方式

    // 必须有无参构造器,否则 Kryo 无法创建实例
    public User() {}

    public User(String name, int age, String password) {
        this.name = name;
        this.age = age;
        this.password = password;
    }

    @Override
    public String toString() {
        return "User{name='" + name + "', age=" + age + ", password='" + password + "'}";
    }
}
import com.esotericsoftware.kryo.Kryo;
import com.esotericsoftware.kryo.io.Input;
import com.esotericsoftware.kryo.io.Output;
import java.io.*;

public class KryoBasicDemo {
    public static void main(String[] args) {
        User user = new User("张三", 25, "123456");

        // 1. 序列化
        byte[] bytes = serialize(user);
        System.out.println("序列化字节数:" + bytes.length);

        // 2. 反序列化
        User restored = deserialize(bytes, User.class);
        System.out.println("反序列化结果:" + restored);
    }

    // 序列化对象到字节数组
    public static byte[] serialize(Object obj) {
        Kryo kryo = new Kryo();
        // 注册类可提升性能,不注册也能用但会写入完整类名
        kryo.register(User.class);

        try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
             Output output = new Output(baos)) {
            kryo.writeObject(output, obj);
            output.flush();
            return baos.toByteArray();
        } catch (IOException e) {
            throw new RuntimeException("序列化失败", e);
        }
    }

    // 从字节数组反序列化
    public static <T> T deserialize(byte[] bytes, Class<T> clazz) {
        Kryo kryo = new Kryo();
        kryo.register(clazz);

        try (Input input = new Input(new ByteArrayInputStream(bytes))) {
            return kryo.readObject(input, clazz);
        }
    }
}

需要注意的是我们在实际项目里绝不能每次序列化都 new Kryo()(开销很大),也不应该多线程共用一个 Kryo 实例。这里我们可以借助 ThreadLocal来做一层封装:

import com.esotericsoftware.kryo.Kryo;
import com.esotericsoftware.kryo.io.Input;
import com.esotericsoftware.kryo.io.Output;

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

public class KryoThreadLocal {
    private static final ThreadLocal<Kryo> kryoLocal = ThreadLocal.withInitial(() -> {
        Kryo kryo = new Kryo();
        kryo.setRegistrationRequired(false);
        // 可以在这里提前注册常用类
        kryo.register(User.class);
        kryo.register(java.util.ArrayList.class);
        kryo.register(java.util.HashMap.class);
        return kryo;
    });

    public static Kryo get() {
        return kryoLocal.get();
    }

    public static <T> byte[] serialize(T obj) {
        Kryo kryo = get();
        try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
             Output output = new Output(baos)) {
            kryo.writeObject(output, obj);
            output.flush();
            return baos.toByteArray();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    public static <T> T deserialize(byte[] bytes, Class<T> clazz) {
        Kryo kryo = get();
        try (Input input = new Input(new ByteArrayInputStream(bytes))) {
            return kryo.readObject(input, clazz);
        }
    }
}

三、Protobuf

Protobuf(Protocol Buffers)是 Google 出品的一种跨语言序列化方案。和 Kryo 最大的不同是,它需要先定义 .proto 文件,然后生成代码,最后才能序列化。这样做的好处是跨语言、体积小、有 schema 校验。

<dependencies>
    <dependency>
        <groupId>com.google.protobuf</groupId>
        <artifactId>protobuf-java</artifactId>
        <version>3.25.3</version>
    </dependency>
</dependencies>

<build>
    <plugins>
        <!-- 代码生成插件,在 compile 阶段自动把 .proto 生成 Java 类 -->
        <plugin>
            <groupId>org.xolstice.maven.plugins</groupId>
            <artifactId>protobuf-maven-plugin</artifactId>
            <version>0.6.1</version>
            <configuration>
                <protocArtifact>com.google.protobuf:protoc:3.25.3:exe:${os.detected.classifier}</protocArtifact>
                <!-- 自定义 proto 源文件目录,可以有多个 -->
                <protoSourceRoot>${project.basedir}/src/main/resources/proto</protoSourceRoot>
            </configuration>
            <executions>
                <execution>
                    <goals><goal>compile</goal></goals>
                </execution>
            </executions>
        </plugin>

        <!-- 用于自动检测操作系统,配合上面的 ${os.detected.classifier} -->
        <plugin>
            <groupId>kr.motd.maven</groupId>
            <artifactId>os-maven-plugin</artifactId>
            <version>1.7.1</version>
            <executions>
                <execution>
                    <phase>initialize</phase>
                    <goals><goal>detect</goal></goals>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

接下来是编写 user.proto文件,注意其存放目录要和上面配置的一致!!

syntax = "proto3";

// 包名,生成的 Java 类会放在这个包下
package com.hzp.protobuf;

// 如果想让生成的 Java 类名加前缀或放在外层类里,可以设这个
option java_multiple_files = true;

// 消息定义——这就是你要序列化的对象
message User {
  string name = 1;       // =1 是字段编号,不是默认值
  int32  age  = 2;
  string email = 3;

  // 枚举示例
  enum Gender {
    UNKNOWN = 0;
    MALE    = 1;
    FEMALE  = 2;
  }
  Gender gender = 4;

  // 嵌套消息示例
  message Address {
    string city    = 1;
    string street  = 2;
  }
  Address address = 5;

  // repeated 表示集合(List)
  repeated string hobbies = 6;
}

然后就是执行 mvn compile,会在 target/generated-sources下面生成对应的java类。如果我们想使用生成的java类,我们还需要将 protobuf/java标记为源代码根目录,这样就不用拷贝到我们手动编写的代码里了。

image.png

最后就是编写一个demo测试一下:

import com.google.protobuf.InvalidProtocolBufferException;

public class ProtobufDemo {
    public static void main(String[] args) throws InvalidProtocolBufferException {
        // 1. 构建对象(使用 Builder 模式)
        User.Address address = User.Address.newBuilder()
                .setCity("北京")
                .setStreet("长安街")
                .build();

        User user = User.newBuilder()
                .setName("张三")
                .setAge(25)
                .setEmail("zhangsan@example.com")
                .setGender(User.Gender.MALE)
                .setAddress(address)
                .addHobbies("篮球")
                .addHobbies("编程")
                .build();

        System.out.println("原始对象:\n" + user);

        // 2. 序列化为字节数组
        byte[] bytes = user.toByteArray();
        System.out.println("序列化字节数:" + bytes.length);

        // 3. 反序列化
        User restored = User.parseFrom(bytes);
        System.out.println("\n反序列化结果:\n" + restored);

        // 4. 验证字段
        System.out.println("\n姓名:" + restored.getName());
        System.out.println("地址:" + restored.getAddress().getCity());
        System.out.println("爱好数量:" + restored.getHobbiesCount());
        System.out.println("第一个爱好:" + restored.getHobbies(0));
    }
}

四、ProtoStuff

我们发现,上面的这个Protobuf好像有点麻烦,用起来不太顺手,于是它的“升级版”ProtoStuff就诞生了,ProtoStuff基于上面的Protobuf提供了更多的功能和更加简单的用法。下面我们来看一下它的具体用法:

<dependencies>
    <dependency>
        <groupId>io.protostuff</groupId>
        <artifactId>protostuff-core</artifactId>
        <version>1.8.0</version>
    </dependency>
    <dependency>
        <groupId>io.protostuff</groupId>
        <artifactId>protostuff-runtime</artifactId>
        <version>1.8.0</version>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.46</version>
    </dependency>
</dependencies>
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class User {
    private String name;
    private int age;
    // 如果你不想序列化某些字段,用 transient 关键字
    private transient String password;
}
import io.protostuff.LinkedBuffer;
import io.protostuff.ProtostuffIOUtil;
import io.protostuff.Schema;
import io.protostuff.runtime.RuntimeSchema;

public class ProtostuffDemo {
    public static void main(String[] args) {
        // 1. 准备对象
        User user = User.builder()
                .name("张三")
                .age(25)
                .build();

        // 2. 获取运行时动态生成的 Schema(相当于 .proto 文件的作用)
        Schema<User> schema = RuntimeSchema.getSchema(User.class);

        // 3. 序列化
        LinkedBuffer buffer = LinkedBuffer.allocate(512);
        byte[] bytes;
        try {
            bytes = ProtostuffIOUtil.toByteArray(user, schema, buffer);
        } finally {
            buffer.clear();
        }
        System.out.println("序列化字节数: " + bytes.length);

        // 4. 反序列化
        User restored = schema.newMessage(); // 创建空对象
        ProtostuffIOUtil.mergeFrom(bytes, restored, schema);

        System.out.println("反序列化结果: " + restored.getName());
    }
}

五、横向对比

对比维度 Java 原生序列化 Kryo Protobuf (原生) Protostuff
核心机制 JDK内置,实现 Serializable接口 基于字节码生成,直接操作字段 IDL定义 .proto 文件,编译生成代码 运行时动态生成Schema,操作POJO
易用性 最简单实现接口即可 简单POJO无需特殊处理 最麻烦需学IDL,多步生成和配置 较简单操作POJO,无需代码生成
序列化格式 专有二进制格式 专有二进制格式 标准二进制格式跨语言通用 标准Protobuf二进制格式
序列化速度 最慢,大量反射,I/O未优化 极快,字节码直接操作,接近极限 很快,预编译生成高效代码 很快,运行时动态生成,略慢于Kryo
序列化体积 庞大,携带完整类元信息 较小 极小,字段变长编码,紧凑高效 较小,与原生Protobuf相当
跨语言支持 仅Java反序列化依赖JDK 仅Java 原生支持C++, Python, Go等 理论上支持但实际仅限于Java
向后兼容性 ,类结构变化易失败 极弱,字段增删易出错 极强,字段编解码安全升级 ,依赖字段顺序或注解
线程安全性 线程安全 线程不安全需Pool或ThreadLocal 线程安全生成的消息对象不可变 线程安全
安全性 极低,是反序列化漏洞重灾区 较安全,比原生好,但非强项 ,Schema强约束,防篡改 ,基于Schema,防恶意类加载
适用场景 仅遗留系统维护 纯Java高性能缓存、RPC 跨语言通信、长期存储、大型微服务API 纯Java,想要Protobuf格式但不想写 .proto
posted @ 2026-07-29 15:19  寒月静无光  阅读(5)  评论(0)    收藏  举报