Protobuf 语法精讲与 gRPC 概念

Protobuf 语法与 gRPC 概念 一、⚡ Protobuf 是什么:为什么非要学一门"新语言"? 前面讲了 JSON 序列化——Jackson 和 Fastjson2 把 Java 对象和 JSON 之间互转。JSON 是人眼可读的文本——但文本格式有两个天然的劣势: JSON 消息体(108 字节): { "orderId": 10001, "userId": 2001, "productName": "iPhone 15", "amount": 6999.00 } 同样的信息——Protobuf 二进制(约 35 字节): \x08\x91N\x12\x04...(肉眼不可读的二进制) JSON Protobuf 文本格式——每个字符占 1 字节 二进制格式——用最少的字节表达同样的数据 字段名重复传输("orderId" 每次都要传) 字段名不传——用数字编号代替(orderId = 1) 解析慢——文本解析器 解析快——二进制解码器 人眼可读——curl 能调 人眼不可读——需要专用工具 Protobuf 省的不是几字节——是高并发场景下每一条消息都省 60%~80% 带宽。这就是为什么 gRPC 选择 Protobuf 作为默认序列化格式。 二、🧬 Protobuf 语法逐一拆解 Protobuf 的语法就是定义数据结构和服务接口的语言。文件后缀是 .proto。先看一个完整的例子,然后每个语法元素拆开讲: // order.proto —— 订单服务的完整 proto 定义 syntax = "proto3"; // ① 语法版本 package com.example.order; // ② 包名 option java_multiple_files = true; // ③ 编译选项 option java_package = "com.example.order.dto"; import "google/protobuf/timestamp.proto"; // ④ 导入其他 proto // ⑤ 枚举定义 enum OrderStatus { ORDER_STATUS_UNSPECIFIED = 0; // 枚举第一个值必须是 0 ORDER_STATUS_CREATED = 1; ORDER_STATUS_PAID = 2; ORDER_STATUS_CANCELLED = 3; } // ⑥ 消息定义——Protobuf 的核心 message Order { int64 order_id = 1; // 字段编号 = 1 string product_name = 2; // 字段编号 = 2 double amount = 3; // 字段编号 = 3 OrderStatus status = 4; // 使用上面定义的枚举 google.protobuf.Timestamp created_at = 5; // 使用导入的时间戳类型 repeated string tags = 6; // repeated = 数组/列表 map<string, string> metadata = 7; // map 类型 } // ⑦ 服务定义——gRPC 的方法声明 service OrderService { rpc GetOrder(GetOrderRequest) returns (Order); rpc ListOrders(ListOrdersRequest) returns (stream Order); // 服务端流 rpc CreateOrder(stream CreateOrderRequest) returns (stream Order); // 双向流 } // 消息定义可以在 service 之后——顺序不要求 message GetOrderRequest { int64 order_id = 1; } message ListOrdersRequest { int64 user_id = 1; int32 page_size = 2; } message CreateOrderRequest { string product_name = 1; double amount = 2; } 2.1 syntax —— 声明语法版本 syntax = "proto3"; // 必须在文件第一行(注释上面可以,语法上面不能有东西) 版本 特点 当前状态 proto2 老版本——有 required/optional/default 关键字 gRPC 也支持,但不推荐新项目用 proto3 新版本——去掉了 required 和 default,所有字段默认可选 当前主流——新项目统一用 proto3 2.2 package —— 包名 package com.example.order; // 防止命名冲突——和 Java 的 package 概念一样 // 生成 Java 代码时:类放在 com.example.order 包下 2.3 option —— 编译选项 // java_multiple_files: true → 每个 message 生成一个独立的 .java 文件 // false(默认) → 所有 message 生成到一个巨大的外部类中 option java_multiple_files = true; // java_package: 指定生成的 Java 文件所在的包——如果不写,用 package 的值 option java_package = "com.example.order.dto"; // java_outer_classname: java_multiple_files = false 时——指定外部类的类名 option java_outer_classname = "OrderProto"; // optimize_for: 生成代码优化方向——SPEED / CODE_SIZE / LITE_RUNTIME option optimize_for = SPEED; 选项 默认值 建议 java_multiple_files false true——每个 message 一个文件,方便 IDE 导航 java_package package 的值(但它是默认的,不是必然) 写成和 Java 项目一致的包名 optimize_for SPEED 保持默认 2.4 import —— 导入其他 proto 文件 import "google/protobuf/timestamp.proto"; // 导入 Google 内置类型 import "common/common.proto"; // 导入自己项目的公共 proto import public "common/new.proto"; // 公开导入——谁 import 你,谁也看得到 new.proto 的类型 常用内置类型(google/protobuf/ 下的类型): ...

十一月 28, 2022 · 7 分钟 · 1378 字 · yaomingye

Jackson vs Fastjson2 终极对比

Jackson vs Fastjson2 📖 前置阅读:本文假设读者已经阅读过前两篇——如果还不熟悉 Jackson 或 Fastjson2,建议先阅读 序列化本质与 Jackson 全操作指南 和 Fastjson 进化史:从 1.x 漏洞到 Fastjson2。 一、⚡ 同一个对象,两种写法 先在同一个 Order 类上对比两套注解——直观感受差异: // ===== Jackson 写法 ===== @JsonPropertyOrder({"order_id", "user_id", "product_name", "amount", "create_time"}) @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(JsonInclude.Include.NON_NULL) public class Order { @JsonProperty("order_id") // Jackson: 改字段名 private Long orderId; @JsonProperty("user_id") private Long userId; @JsonProperty("product_name") private String productName; @JsonFormat(shape = JsonFormat.Shape.STRING) // Jackson: 金额用字符串 private BigDecimal amount; @JsonIgnore // Jackson: 忽略字段 private String internalNote; @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") // Jackson: 日期格式 private LocalDateTime createTime; } // ===== Fastjson2 写法 ===== @JSONType(orders = {"order_id", "user_id", "product_name", "amount", "create_time"}) public class Order { @JSONField(name = "order_id") // Fastjson2: 改字段名 private Long orderId; @JSONField(name = "user_id") private Long userId; @JSONField(name = "product_name") private String productName; @JSONField(serializeFeatures = JSONWriter.Feature.WriteBigDecimalAsPlain) private BigDecimal amount; // Fastjson2: 金额不用科学计数法 @JSONField(serialize = false) // Fastjson2: 忽略字段 private String internalNote; @JSONField(format = "yyyy-MM-dd HH:mm:ss") // Fastjson2: 日期格式 private LocalDateTime createTime; } Facjson2 不需要 @JsonIgnoreProperties——它默认忽略未知字段。Jackson 必须在每个类上加这个注解或全局配置。对于有 50+ 个 DTO 类的项目来说,这少写不少样板。 ...

十一月 27, 2022 · 5 分钟 · 875 字 · yaomingye

Fastjson 进化史:从 1.x 漏洞到 Fastjson2

Fastjson 进化史 📖 前置阅读:本文假设读者已理解序列化的基本概念和 JSON 序列化工具的用法。如果还不熟悉,建议先阅读 序列化本质与 Jackson 全操作指南。 一、⚡ Fastjson 曾经有多火 Fastjson 是阿里巴巴 2011 年开源的 JSON 序列化库。在 Jackson 还比较沉重、Gson 性能一般的年代,Fastjson 凭三个特点迅速占领了国内 Java 项目: 卖点 具体表现 快 号称"Java 语言最快的 JSON 处理库"——字节码生成 + ASM 动态优化,比 Jackson 快 2x API 简洁 JSON.toJSONString(obj) 一行搞定——比 Jackson 的 ObjectMapper 样板代码少 阿里出品 阿里巴巴开源——国内 Java 圈号召力最强背书 最火的那些年,几乎所有国内 Java 项目引入 Fastjson——Dubbo、RocketMQ、Nacos 内部都依赖了它。 二、💣 autoType:从"核心卖点"到"最大漏洞" 2.1 autoType 是什么 Fastjson 有一个 Jackson 没有的独特功能——autoType。它的作用是:JSON 中有一个 @type 字段,Fastjson 根据它自动反序列化为对应的 Java 类。 // Fastjson 的 autoType 机制 // 序列化时——自动写入类的全限定名 String json = JSON.toJSONString(order); // 结果:{"@type":"com.example.Order","orderId":10001,"amount":6999.00} // ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ // @type 字段记录了类的全限定名 // 反序列化时——根据 @type 字段自动还原为正确的子类型 // JSON 数据:"{\"@type\":\"com.example.Order\",\"orderId\":10001}" // 即使你只声明为 Object,Fastjson 也能还原出 Order 对象 Object obj = JSON.parse(json); // 实际返回 Order 实例 这个功能在 Jackson 中需要 @JsonTypeInfo 注解手动声明——Fastjson 默认自动开启。 ...

十一月 26, 2022 · 5 分钟 · 1055 字 · yaomingye

序列化本质与 Jackson 全操作指南

序列化本质与 Jackson 一、⚡ 序列化是什么:Java 对象和 JSON 之间的翻译官 日常写的代码里全是 Java 对象——Order、User、Product。但网络传输只能传二进制/文本,数据库只能存文本,前端浏览器只认得 JSON。怎么把 Java 对象变成 JSON、再把 JSON 变回 Java 对象?这就是序列化和反序列化: 序列化(Serialization): Java 对象 → JSON / XML / 二进制 反序列化(Deserialization): JSON / XML / 二进制 → Java 对象 如果不做序列化,你就得手动拼 JSON: // 没有序列化工具——手动拼 JSON(又臭又长) public String orderToJson(Order order) { return "{" + "\"orderId\":" + order.getOrderId() + "," + "\"userId\":" + order.getUserId() + "," + "\"productName\":\"" + escape(order.getProductName()) + "\"," + "\"amount\":" + order.getAmount() + "}"; } // 手写这段代码的时候,你就知道自己需要一个序列化工具了 序列化框架做的事就是自动完成这个转换——你要做的只是加几个注解、调一行方法。 ...

十一月 25, 2022 · 7 分钟 · 1368 字 · yaomingye

Long类型ID前端精度丢失

Long类型ID前端精度丢失:从IEEE 754根因到Jackson全局序列化方案 🤔 一、问题切入:一个"找不着"的订单 某天业务反馈:用户在订单详情页点进去一片空白,后台日志里看到查的是 ID 1857353925587607500,但数据库里根本没有这条记录。翻看上游接口的原始响应体,后端明明返回的是 1857353925587607552。 差了多少?不多,就差了 52:...552 变成了 ...500。但这 52 的差距足以让一条订单从数据库里彻底"消失"。 写个最简单的演示: // 后端:Java Long 值 long orderId = 1857353925587607552L; System.out.println(orderId); // 输出: 1857353925587607552 ✓ 后端没问题。再看前端: // 前端:直接解析后端返回的 JSON const json = '{"orderId": 1857353925587607552}'; const obj = JSON.parse(json); console.log(obj.orderId); // 输出: 1857353925587607500 ✗ 同一个数字,跨了一道 HTTP 就被"阉割"了最后两位精度。这不是哪家框架的 bug,也不是谁写错了代码——根因在 JavaScript Number 的底层存储格式。 flowchart TD classDef data fill:#052e16,stroke:#16a34a,stroke-width:1.5px,color:#bbf7d0,font-weight:bold; classDef reject fill:#450a0a,stroke:#dc2626,stroke-width:1.5px,color:#fecaca,font-weight:bold; classDef process fill:#1e1e24,stroke:#6b7280,stroke-width:1.5px,color:#e5e7eb; JAVA[Java Long\n1857353925587607552] JSON[JSON 数字\n1857353925587607552] PARSE[JavaScript JSON.parse] NUM[JS Number\n1857353925587607500] QUERY[用错误ID查数据库] MISS[查不到数据] JAVA -->|Jackson序列化| JSON JSON -->|HTTP响应| PARSE PARSE -->|IEEE 754精度丢失| NUM NUM --> QUERY QUERY --> MISS class JAVA,JSON data; class NUM,MISS reject; class PARSE,QUERY process; 这个问题的触发条件很具体:后端 Long 值超过 9007199254740991(即 2^53 ~ 1,约 16 位十进制数)时,前端 JSON.parse() 解析出的数字就会丢失精度。雪花算法生成的 ID 通常 17 ~ 19 位,正好踩在坑里。 ...

九月 25, 2022 · 5 分钟 · 956 字 · yaomingye

Java IO 进阶

Java IO 进阶:基本类型 IO、打印流与对象序列化实用指南 1 🎮 问题场景:从保存游戏分数说起 假设你正在开发一个本地小游戏,需要把玩家的最高分(int)、胜率(double)和昵称(String)保存到文件,下次启动时读回来。用前面学过的 FileWriter 写文本当然可以,但你需要手动处理类型转换——写的时候 int → String,读的时候 String → int,格式稍微不一致就解析失败。 有没有办法直接把 int 按固定 4 字节的二进制格式写入,读的时候也按 int 原样读出?这就是 基本类型 IO 要解决的问题。 更进一步,如果整个游戏状态是一个复杂的 Java 对象(玩家信息、关卡进度、道具列表),能不能 一键保存整个对象,一键读回 ?这就是 对象序列化 要解决的问题。 本篇覆盖 Java IO 的第四个进阶阶段,依次讲解三种机制: 机制 用途 一句话描述 DataInputStream / DataOutputStream 读写基本类型和字符串 按固定字节数的二进制格式读写,必须按序操作 PrintStream / PrintWriter 格式化文本输出 不抛异常,日常 System.out.println() 就在用它 ObjectInputStream / ObjectOutputStream 对象序列化与反序列化 一键将整个对象转为字节流保存或传输 2 💾 基本类型 IO:DataOutputStream / DataInputStream 2.1 ❓ 是什么 DataOutputStream 和 DataInputStream 是 装饰器流 (包装已有的 OutputStream / InputStream),提供直接读写 Java 基本类型和 String 的能力。数据以 平台无关的二进制格式 写入——比如 writeInt(42) 始终写 4 个字节,无论在什么操作系统上,读出来的值都不变。 ...

九月 9, 2022 · 5 分钟 · 937 字 · yaomingye
Cat Radio