数据格式 、序列化和反序列化

目录

网络通信的完整层次:

各层的数据形态变化

[1. 应用层 - 我们看到的数据格式](#1. 应用层 - 我们看到的数据格式)

[2. 序列化后 - 准备发送的数据](#2. 序列化后 - 准备发送的数据)

[3. 传输层 - 封装为数据段](#3. 传输层 - 封装为数据段)

[4. 网络层 - 封装为数据包](#4. 网络层 - 封装为数据包)

[5. 数据链路层 - 封装为帧](#5. 数据链路层 - 封装为帧)

[6. 物理层 - 真正的二进制比特流](#6. 物理层 - 真正的二进制比特流)

为什么我们还要关心数据格式?

区别

[1. 序列化/反序列化开销](#1. 序列化/反序列化开销)

[2. 数据体积对比](#2. 数据体积对比)

[3. 解析性能对比](#3. 解析性能对比)

为什么格式选择仍然重要?

[1. 开发效率](#1. 开发效率)

[2. 调试便利性](#2. 调试便利性)

[3. 跨平台兼容性](#3. 跨平台兼容性)

序列化和反序列化

序列化(Serialization)

反序列化(Deserialization)

Java对象的序列化和反序列化(JSON))

[Jackson(Spring Boot 默认)](#Jackson(Spring Boot 默认))

手动进行序列化与反序列化

[WebSocket 中的 JSON 消息处理](#WebSocket 中的 JSON 消息处理)

自定义序列化与反序列化


网络通信的完整层次:

复制代码
应用层:JSON/XML/Protobuf 等格式
    ↓
表示层:数据序列化/反序列化  
    ↓
会话层:建立/维护连接
    ↓
传输层:TCP/UDP 数据段
    ↓
网络层:IP 数据包
    ↓
数据链路层:以太网帧
    ↓
物理层:电信号/光信号/无线电波(二进制比特流)

各层的数据形态变化

1. 应用层 - 我们看到的数据格式

复制代码
// 这是我们在代码中处理的数据
{
  "name": "张三",
  "age": 25,
  "email": "zhangsan@example.com"
}

2. 序列化后 - 准备发送的数据

复制代码
// JSON 序列化为字节
byte[] jsonBytes = "{\"name\":\"张三\",\"age\":25,\"email\":\"zhangsan@example.com\"}".getBytes("UTF-8");

// 或者 Protobuf 序列化
byte[] protobufBytes = user.toByteArray();

3. 传输层 - 封装为数据段

复制代码
[TCP Header][应用层数据字节]
源端口:8080 目标端口:80 序列号:123 确认号:456 ... + jsonBytes/protobufBytes

4. 网络层 - 封装为数据包

复制代码
[IP Header][TCP数据段]
源IP:192.168.1.100 目标IP:93.184.216.34 ... + [TCP数据段]

5. 数据链路层 - 封装为帧

复制代码
[以太网头][IP数据包][CRC校验]
MAC地址:00:1A:2B:3C:4D:5E 目标MAC:00:1A:2B:3C:4D:5F ... + [IP数据包]

6. 物理层 - 真正的二进制比特流

复制代码
01101000 01110100 01110100 01110000 00111010 00101111 00101111  ...
h        t        t        p        :        /        /

为什么我们还要关心数据格式?

虽然底层都是二进制,但不同格式在应用层有重大区别:

不同的应用层数据格式

格式 类型 人类可读 体积 性能 复杂度 主要用途
JSON 文本 中等 中等 Web API、配置
XML 文本 企业系统、文档
YAML 文本 配置文件
CSV 文本 很小 很低 数据表格
Protocol Buffers 二进制 × 很小 很高 微服务、gRPC
MessagePack 二进制 × 网络传输
Avro 二进制 × 很小 很高 大数据
BSON 二进制 × 中等 MongoDB

区别

1. 序列化/反序列化开销
复制代码
// JSON 序列化(文本,相对较慢)
ObjectMapper mapper = new ObjectMapper();
String json = mapper.writeValueAsString(user);  // 需要处理字符串编码
byte[] bytes = json.getBytes("UTF-8");

// Protobuf 序列化(二进制,很快)
byte[] bytes = user.toByteArray();  // 直接生成优化过的二进制
2. 数据体积对比

假设要传输这个用户数据:

复制代码
User user = new User("张三", 25, "zhangsan@example.com");

不同格式的体积:

  • JSON : {"n":"张三","a":25,"e":"zhangsan@example.com"}约 60 字节

  • XML : <user><n>张三</n><a>25</a><e>zhangsan@example.com</e></user>约 80 字节

  • Protobuf : 优化的二进制格式 → 约 20-30 字节

3. 解析性能对比
复制代码
// JSON 解析需要词法分析、语法分析
JSON.parse('{"name":"张三","age":25}');

// Protobuf 解析直接按照预定义格式读取二进制
user.parseFrom(byteArray);

为什么格式选择仍然重要?

1. 开发效率
复制代码
// JSON:前端直接使用
const user = JSON.parse(response);
console.log(user.name); // 直接访问

// Protobuf:前端需要额外处理
const user = UserProto.decode(new Uint8Array(response));
console.log(user.getName()); // 需要通过getter
2. 调试便利性
复制代码
// JSON:人类可读,调试方便
System.out.println("收到数据: " + new String(bytes, "UTF-8"));
// 输出: 收到数据: {"name":"张三","age":25}

// Protobuf:二进制,调试困难
System.out.println("收到数据: " + Arrays.toString(bytes));
// 输出: 收到数据: [10, 6, -28, -67, -96, -27, -91, -67, 16, 25, 26, 21, 122, 104, 97, 110, 103, 115, 97, 110, 64, 101, 120, 97, 109, 112, 108, 101, 46, 99, 111, 109]
3. 跨平台兼容性
复制代码
# Python 解析 JSON(内置支持)
import json
user = json.loads('{"name": "张三", "age": 25}')

# Python 解析 Protobuf(需要额外安装库)
import user_pb2
user = user_pb2.User()
user.ParseFromString(binary_data)

序列化和反序列化

序列化(Serialization)

序列化是指将数据结构或对象状态转换为可以存储或传输的格式的过程。在序列化过程中,对象的状态(包括其数据和其他必要信息)被转换成一种标准格式,比如字节流。这个字节流可以存储在文件中,也可以通过网络传输到另一个系统。

序列化的目的:

  • 持久化:将对象的状态保存到存储介质(如硬盘)中,以便以后重新创建该对象。

  • 网络传输:将对象转换为可以通过网络发送的格式,以便在不同的系统或进程之间交换数据。

例如,在Java中,你可以将一个User对象序列化成JSON字符串,然后把这个字符串保存到文件或者通过网络发送。

反序列化(Deserialization)

反序列化是序列化的逆过程,即将序列化后的数据(如字节流)重新转换回数据结构或对象的过程。这个过程会根据序列化时的规则和格式,将数据还原成原来的对象。

反序列化的目的:

  • 从存储介质中读取数据并重新构建对象。

  • 从网络接收数据并还原成对象。

例如,从网络接收到一个JSON字符串,然后将其反序列化成Java的User对象。

"应用层的数据格式"指的是序列化后所采用的数据表示形式。

网络传输的永远是序列化后的数据,而这些数据是按照某种数据格式规范生成的。

选择数据格式的核心原则:

  1. 不要过早优化:先使用JSON验证业务逻辑

  2. 基于数据决策:用性能测试结果指导选择

  3. 考虑总成本:包括开发、维护、运维成本

  4. 保持灵活性:可以在不同场景使用不同格式

简单决策指南:

  • Web开发:JSON(默认选择)

  • 微服务:Protocol Buffers + gRPC

  • 配置文件:YAML

  • 大数据:Avro/Parquet

  • 实时系统:MessagePack/Protobuf

  • 移动端:Protobuf/FlatBuffers

Java对象的序列化和反序列化(JSON)

在Spring Boot框架下,Java对象的序列化和反序列化(特别是与JSON相关的)有很多工具选择。以下是一些常用的工具和库:

  1. Jackson:Spring Boot默认使用的JSON处理库,功能强大,支持流式处理、树模型、数据绑定等。

  2. Gson:Google提供的JSON库,简单易用。

  3. JSON-B:Java EE的JSON绑定标准,其参考实现是Yasson。

  4. Fastjson:阿里巴巴提供的JSON库,性能较好,但曾经出现过一些安全漏洞。

在Spring Boot中,默认使用Jackson。因此,如果你没有特殊配置,Spring Boot会自动配置Jackson来进行序列化和反序列化。

工具 类型 Spring Boot 默认 性能 易用性 特性
Jackson JSON 处理 功能丰富,生态完善
Gson JSON 处理 × 很高 API 简单
JSON-B JSON 绑定标准 × Java EE 标准
Fastjson JSON 处理 × 很高 阿里出品,性能强

Jackson(Spring Boot 默认)

依赖配置

复制代码
<!-- Spring Boot 已经自动包含,无需手动添加 -->
<!-- 但如果需要明确声明: -->
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
</dependency>

<!-- 额外功能模块 -->
<dependency>
    <groupId>com.fasterxml.jackson.datatype</groupId>
    <artifactId>jackson-datatype-jsr310</artifactId>
</dependency>
<dependency>
    <groupId>com.fasterxml.jackson.datatype</groupId>
    <artifactId>jackson-datatype-jdk8</artifactId>
</dependency>

Jackson 基础配置

在 Spring Boot 中,Jackson 的自动化配置主要通过 application.propertiesapplication.yml 文件完成。

常用配置项示例(application.properties):

复制代码
# 设置日期格式
spring.jackson.date-format=yyyy-MM-dd HH:mm:ss
# 设置时区
spring.jackson.time-zone=GMT+8
# 序列化时,忽略值为 null 的属性
spring.jackson.default-property-inclusion=non_null
# 忽略未知属性(反序列化时),防止解析失败
spring.jackson.deserialization.fail-on-unknown-properties=false

如果需要更精细的控制,可以创建一个自定义的 ObjectMapper Bean。

复制代码
@Configuration
public class JacksonConfig {

    @Bean
    public ObjectMapper objectMapper() {
        ObjectMapper objectMapper = new ObjectMapper();
        // 禁用未知属性导致反序列化失败
        objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        // 序列化时忽略空Bean
        objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
        // 设置日期格式
        objectMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
        // 其他自定义配置...
        return objectMapper;
    }
}

HTTP 接口中的自动化序列化

在 Spring Boot 的 Controller 中,Jackson 的使用几乎是透明的。

1. 对象序列化为 JSON 响应:

使用 @RestController 注解的控制器,其方法返回值会自动被序列化为 JSON

复制代码
@RestController
@RequestMapping("/api/users")
public class UserController {

    @GetMapping("/{id}")
    public User getUser(@PathVariable Long id) {
        // Spring Boot 会自动将 User 对象序列化为 JSON 返回
        return userService.findById(id);
    }
}

2. 反序列化 JSON 请求体:

使用 @RequestBody 注解将请求体中的 JSON 数据转换为 Java 对象。

复制代码
@PostMapping
public User createUser(@RequestBody User user) {
    // user 对象是从请求的 JSON 数据反序列化而来
    return userService.save(user);
}

3. 字段重命名与忽略:

使用 Jackson 注解控制序列化细节。

复制代码
public class User {
    @JsonProperty("user_id") // 序列化后字段名为 "user_id"
    private Long id;

    private String username;

    @JsonIgnore // 忽略该字段,不参与序列化和反序列化
    private String password;

    // getters and setters
}

手动进行序列化与反序列化

在一些非 HTTP 接口的场景(如工具类、日志记录、消息队列等),你需要手动使用 ObjectMapper

复制代码
@Service
public class MyService {

    @Autowired
    private ObjectMapper objectMapper; // 注入 Spring Boot 配置好的 ObjectMapper

    /**
     * 将对象序列化为 JSON 字符串
     */
    public String serializeObject(Object obj) throws JsonProcessingException {
        return objectMapper.writeValueAsString(obj);
    }

    /**
     * 将 JSON 字符串反序列化为对象
     */
    public <T> T deserializeString(String json, Class<T> clazz) throws JsonProcessingException {
        return objectMapper.readValue(json, clazz);
    }
}

美化输出:

使用 writerWithDefaultPrettyPrinter() 方法可以获得格式化的、易于阅读的 JSON 字符串

复制代码
String prettyJson = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(obj);
System.out.println(prettyJson);

WebSocket 中的 JSON 消息处理

在使用 Spring 的 WebSocket 特别是 STOMP 协议时,默认的消息转换器可能不支持 JSON,需要显式配置 Jackson。

1. 引入依赖:

确保项目中包含了 jackson-databind 依赖

复制代码
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
</dependency>

2. 配置 WebSocket 消息转换器:

在 WebSocket 配置中,注册一个 MappingJackson2MessageConverter

复制代码
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        registry.enableSimpleBroker("/topic");
        registry.setApplicationDestinationPrefixes("/app");
    }

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/ws").withSockJS();
    }

    @Override
    public boolean configureMessageConverters(List<MessageConverter> messageConverters) {
        // 添加 Jackson 消息转换器,用于处理 JSON 格式的 WebSocket 消息
        messageConverters.add(new MappingJackson2MessageConverter());
        return false; // 设置为 false 表示不注册默认的转换器
    }
}

3. 在控制器中收发消息:

配置好后,在 WebSocket 控制器中就可以直接使用 Java 对象接收和发送 JSON 消息。

复制代码
@Controller
public class WebSocketController {

    @MessageMapping("/chat")
    @SendTo("/topic/messages")
    public ChatMessage sendMessage(ChatMessage chatMessage) {
        // 直接处理 ChatMessage 对象,Jackson 会自动完成反序列化和序列化
        return chatMessage;
    }
}

自定义序列化与反序列化

对于特殊数据类型(如 LocalDateTime)或复杂转换逻辑,Jackson 提供了强大的自定义功能。

场景:处理 LocalDateTime

默认情况下,Jackson 无法直接序列化 LocalDateTime,需要自定义转换器-10

方法一:使用 @JsonSerialize@JsonDeserialize 注解

  1. 自定义序列化器:

    public class LocalDateTimeSerializer extends JsonSerializer<LocalDateTime> {
    private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

    复制代码
     @Override
     public void serialize(LocalDateTime value, JsonGenerator gen, SerializerProvider provider) throws IOException {
         gen.writeString(value.format(formatter));
     }

    }

2. 自定义反序列化器:

复制代码
public class LocalDateTimeDeserializer extends JsonDeserializer<LocalDateTime> {
    private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

    @Override
    public LocalDateTime deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
        return LocalDateTime.parse(p.getValueAsString(), formatter);
    }
}

3.在实体类字段上应用:

复制代码
public class Event {
    private String name;

    @JsonSerialize(using = LocalDateTimeSerializer.class)
    @JsonDeserialize(using = LocalDateTimeDeserializer.class)
    private LocalDateTime eventTime;

    // getters and setters
}

方法二:通过 Module 注册(推荐,全局生效)

  1. 创建自定义 Module:

    @Configuration
    public class JacksonConfig {

    复制代码
     @Bean
     public ObjectMapper objectMapper() {
         ObjectMapper objectMapper = new ObjectMapper();
         
         // 注册 Java 8 时间模块,这是处理 Java 8 日期时间更标准、简便的方式
         objectMapper.registerModule(new JavaTimeModule());
         // 禁用将日期序列化为时间戳
         objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
         
         return objectMapper;
     }

    }

相关推荐
linux kernel3 小时前
第一部分:网络基础
网络·linux网络
朝新_4 小时前
【EE初阶】JVM
java·开发语言·网络·jvm·笔记·算法·javaee
会开花的二叉树4 小时前
应用层网络协议深度解析:设计、实战与安全
网络·网络协议·安全
星空的资源小屋6 小时前
MkFont,一款开源免费的字体设计工具
网络·人工智能·pdf·电脑
无敌最俊朗@8 小时前
UDP 高频面试题解析
网络·网络协议·udp
网安INF8 小时前
网络攻防技术:防火墙技术
网络·安全·web安全·网络安全·防火墙
nassi_9 小时前
开发板网络配置
linux·网络·嵌入式硬件
ALex_zry9 小时前
论gRPC:基于 TCP/IP 的通用网络模式,以及基于 Unix Domain Sockets (UDS) 的同机进程间通信 (IPC) 模式
网络·tcp/ip·unix
数据与人工智能律师10 小时前
数据淘金时代的法治罗盘:合法收集、使用与变现数据的边界与智慧
大数据·网络·人工智能·云计算·区块链