《Java 服务端通信工程》前言:读法、环境与约定

交代本书的三种读法、前置知识、JDK 25 与 gRPC 1.76.0 的环境搭建、comm-api 等四模块与 9093/9094/9095 端口约定,并给出全卷统一的代码风格、默认参数、验证命令和本机能力边界,方便在动手前把团队口径对齐。

这本书怎么读

全书 50 篇,正文 15 章 45 节。三种读法对应三种目标,选一种为主,不要同时推进:

目标主线建议顺序可以后置
把 RPC 用对契约、编码、调用模型第 1–3 章 → 第 4 章 → 第 12 章第 13–14 章
把服务治理做起来发现、负载均衡、重试熔断、限流第 4–9 章,回看第 3 章第 10 章的 mTLS 细节
把系统跑稳观测、灰度、混沌、容量、验收第 10–15 章,边做边补第 3、7 章第 2 章的编码细节

每条路径的入口都在目录页。正文每节末尾有一行阅读导航,按白名单顺序指向前后节;跳读时用它确认自己跳过了什么。

需要的前置知识

本书假设你具备以下基础,不重复讲解:

  • 能用 Java 写多线程程序,知道 ExecutorService、CompletableFuture、ConcurrentHashMap 的基本用法。
  • 用过 Maven 多模块工程,知道 dependencyManagement 和 mvn -pl 的作用。
  • 理解 TCP 的连接、半关闭和超时,知道 HTTP/1.1 的请求响应模型。
  • 写过一个能启动、能收请求、能打日志的服务。

不要求你用过 gRPC、Protobuf、注册中心或服务网格。这些概念会在第一次出现时给定义,但不会从「什么是 RPC」讲起。

如果卷一的 Selector 循环、卷二的 ByteBuf 引用计数完全没有概念,建议至少补读卷一第 6–8 章和卷二第 5–6 章。本卷不会再用它们写代码,但会用它们解释「为什么这里不能阻塞」「为什么消息边界不能靠一次读」。

环境与版本

版本在全书锁定,正文不漂移:

组件版本说明
JDKTemurin 25.0.5+8javac --release 25
Maven3.9.12构建与依赖管理
gRPC-Java1.76.0grpc-netty-shaded、grpc-protobuf、grpc-stub、grpc-services
Protobuf4.33.2protobuf-java、protobuf-java-util
JUnit Jupiter5.14.4单元测试与契约测试
librarydHTTP 8080 + PostgreSQL 17已有服务,不重写

本机默认 java 可能指向 JDK 8。每个终端会话先设置 JAVA_HOME,再执行构建:

export JAVA_HOME=~/.jdks/jdk-25.0.5+8/Contents/Home
export PATH="$JAVA_HOME/bin:$PATH"
java -version
mvn -v

本机实际输出版本行如下(java -version 的输出,保留原始格式):

openjdk version "25.0.5" 2026-10-20 LTS
OpenJDK Runtime Environment Temurin-25.0.5+8 (build 25.0.5+8-LTS)
OpenJDK 64-Bit Server VM Temurin-25.0.5+8 (build 25.0.5+8-LTS, mixed mode, sharing)

comm-api 的依赖如下。gRPC 的四个构件版本必须一致,Protobuf 的运行时版本必须与 protoc 生成的代码匹配:

<dependencies>
  <dependency>
    <groupId>io.grpc</groupId>
    <artifactId>grpc-netty-shaded</artifactId>
    <version>1.76.0</version>
  </dependency>
  <dependency>
    <groupId>io.grpc</groupId>
    <artifactId>grpc-protobuf</artifactId>
    <version>1.76.0</version>
  </dependency>
  <dependency>
    <groupId>io.grpc</groupId>
    <artifactId>grpc-stub</artifactId>
    <version>1.76.0</version>
  </dependency>
  <dependency>
    <groupId>io.grpc</groupId>
    <artifactId>grpc-services</artifactId>
    <version>1.76.0</version>
  </dependency>
  <dependency>
    <groupId>com.google.protobuf</groupId>
    <artifactId>protobuf-java</artifactId>
    <version>4.33.2</version>
  </dependency>
  <dependency>
    <groupId>com.google.protobuf</groupId>
    <artifactId>protobuf-java-util</artifactId>
    <version>4.33.2</version>
  </dependency>
</dependencies>

不要在同一个 classpath 里再引入非 shaded 的 gRPC 传输实现,也不要直接依赖 Netty 4.2。shaded 构件内部已经打包了一份 Netty,两套 Netty 同时出现时,类加载顺序决定谁生效,表现为「本机好的、换台机器就 NoSuchMethodError」。

项目结构与端口

linkd-v3 是四模块工程,groupId com.plume.linkd.v3:

模块包前缀职责
comm-apicom.plume.linkd.v3.api.proto、生成的 stub、错误码常量
comm-corecom.plume.linkd.v3.core服务发现、负载均衡、重试、熔断、限流、指标、追踪
comm-servercom.plume.linkd.v3.servergRPC 服务实现、拦截器、健康检查、反射
comm-gatewaycom.plume.linkd.v3.gatewayHTTP/BFF 网关、协议转换、下游调用

依赖方向是单向的:comm-gateway 和 comm-server 依赖 comm-core,comm-core 依赖 comm-api;comm-api 不依赖任何业务模块。这样 .proto 和生成的类型可以被所有模块共享,又不会把服务实现拖进客户端。

端口固定,排障时按端口定位第一现场:

端口归属协议用途
8080librarydHTTP既有服务,本卷不重写
9093linkd-v3gRPC服务端通信主端口
9094comm-gatewayHTTP对外网关、BFF
9095comm-serverHTTP指标、健康检查、管理

端口和两档 deadline 不是散落在各处的字面量,代码里用同一个记录承载,避免文档、配置和实现各说一套:

package com.plume.linkd.v3.core;

import java.time.Duration;

public record LinkdDefaults(
        int grpcPort,
        int gatewayPort,
        int adminPort,
        Duration clientDeadline,
        Duration hopDeadline) {

    public static final LinkdDefaults PRODUCTION =
            new LinkdDefaults(9093, 9094, 9095,
                    Duration.ofMillis(800), Duration.ofMillis(300));

    public static void main(String[] args) {
        System.out.println("ports=" + PRODUCTION.grpcPort()
                + "/" + PRODUCTION.gatewayPort()
                + "/" + PRODUCTION.adminPort());
        System.out.println("budget=" + PRODUCTION.clientDeadline().toMillis()
                + "/" + PRODUCTION.hopDeadline().toMillis());
    }
}

预期输出(手算,未在本机单独编译运行):

ports=9093/9094/9095
budget=800/300

gRPC 服务名是 plume.linkd.v3.Library,四个方法固定:

syntax = "proto3";
package plume.linkd.v3;
option java_package = "com.plume.linkd.v3.api";
option java_multiple_files = true;
option java_outer_classname = "LibraryProto";

service Library {
  rpc GetBook(GetBookRequest) returns (Book);
  rpc SearchBooks(SearchBooksRequest) returns (stream Book);
  rpc WatchStock(WatchStockRequest) returns (stream StockUpdate);
  rpc Chat(stream ChatMessage) returns (stream ChatMessage);
}

字段号一旦发布就不再改。第 2.3 节会给出完整的兼容矩阵,这里只需要记住一条:新增字段用新编号,删除字段用 reserved,永远不要复用旧编号。

代码约定

全书的代码遵循同一组约定,目的是让片段之间可以互相引用:

  • 包名按模块划分,不从 com.plume.linkd.v3 之外引入业务类。
  • 共享类型(RequestContext、RpcError、RetryPolicy 等)的签名在前言定死,后续章节只增方法、不改构造参数。
  • 不引入 Lombok、日志门面绑定或响应式框架。日志用 System.Logger,测试用 JUnit Jupiter。
  • 时间统一用 java.time.Duration 和 java.time.Instant,不传裸 long 毫秒;协议里的 int64 时间戳在边界处转换。
  • 异常只在边界处转换。业务层抛 RpcError,gRPC 边界把它转成 StatusRuntimeException,网关再转成 HTTP 响应。
  • 代码片段尽量完整到能编译;省略 import 时,会在正文里说明依赖的包。
  • 「本机输出」只贴真实执行过的结果;没有执行过的写成「预期输出」,并说明是手算还是示例。

一个最小的环境自检程序如下,它不需要 gRPC 依赖,只用 JDK:

package com.plume.linkd.v3.demo;

import java.time.Duration;

public final class EnvironmentProbe {

    public static final Duration CLIENT_DEADLINE = Duration.ofMillis(800);
    public static final Duration LIBRARYD_DEADLINE = Duration.ofMillis(300);

    public static void main(String[] args) {
        System.out.println("java.version=" + Runtime.version());
        System.out.println("grpc.version=1.76.0");
        System.out.println("protobuf.version=4.33.2");
        System.out.println("client.deadline.ms=" + CLIENT_DEADLINE.toMillis());
        System.out.println("libraryd.deadline.ms=" + LIBRARYD_DEADLINE.toMillis());
        System.out.println("budget.ok=" + (LIBRARYD_DEADLINE.multipliedBy(3).compareTo(CLIENT_DEADLINE) > 0));
    }
}

预期输出(JDK 25 的 Runtime.version() 格式):

java.version=25.0.5+8-LTS
grpc.version=1.76.0
protobuf.version=4.33.2
client.deadline.ms=800
libraryd.deadline.ms=300
budget.ok=true

最后一行 budget.ok=true 不是在说预算够用,而是在说「3 次单跳尝试已经超过总预算」这个判断成立。第 3.2 节会把这个问题展开:重试不能简单地把单跳 deadline 乘以次数。

测试与验证

每节代码至少能通过编译;涉及网络行为的示例,用 JUnit Jupiter 或一个 main 方法验证。构建与测试命令统一:

mvn -q -DskipTests package
mvn -q -pl comm-api,comm-core -am test
mvn -q -pl comm-server -am -Dtest=LibraryServiceTest test

写文章时不做「看起来对」的验证。涉及以下三类内容时,必须在正文里给出可复现的判据:

类型判据写法
编码/解码输入、字节序列、解析结果三者一致贴十六进制和字段表
调用行为客户端观察到的状态码与消息贴 StatusRuntimeException 的 code 和 description
性能/容量机器口径、并发数、持续时间、指标标明「本机实测」或「估算」

没有机器口径的性能数字不写。没有执行过的命令输出,不写成「本机输出」。

本机能力边界

本机口径固定如下,正文中的「本机」都指这台机器:

Apple M1 Pro,10 核,32 GB,macOS 26.3 (25D125)
Temurin 25.0.5+8
gRPC-Java 1.76.0(grpc-netty-shaded)

三个边界需要提前知道:

第一,JDK 25 上运行 shaded Netty 会打印 sun.misc.Unsafe::allocateMemory 的终结弃用警告。这是依赖访问 JDK 内部内存 API 的真实行为,不影响示例结果,也不应该被伪造成「无警告」。

第二,grpcurl 本机未安装。涉及反射服务的验证,正文用 Java 代码或 ServerServiceDefinition 的反射输出来完成;只有明确写「若已安装 grpcurl」的地方,才给出命令行。

第三,macOS 没有 ss,也没有 epoll。连接诊断用 netstat、lsof -i、tcpdump -i lo0(需要 sudo);原生传输的讨论会区分「本机可验证」和「Linux 部署侧配置」,不把两者混在一起。

遇到问题时先查哪里

初学时最容易卡在四类问题上,先按下面的顺序排查,能省掉大量搜索时间:

现象先查常见原因
编译时报找不到生成的类mvn -pl comm-api clean install 是否执行过生成目录没进源码路径,或 IDE 没刷新
运行时报 NoSuchMethodErrorgRPC / Protobuf 版本是否一致混入了另一套传输实现或旧版运行时
调用返回 UNIMPLEMENTED完整方法名是否匹配包名、大小写、服务名写错
调用一直不返回deadline 是否设置客户端没设预算,服务端在等下游

这些问题在附录 A 有更完整的签名和命令;正文里遇到时,会直接指向对应章节。

小结

  • 三种读法:把 RPC 用对、把治理做起来、把系统跑稳。选一条,按目录页的路径走。
  • 版本锁定 JDK 25.0.5+8、gRPC 1.76.0、Protobuf 4.33.2;只引入 grpc-netty-shaded,不混用非 shaded 传输。
  • 四模块单向依赖,端口 8080/9093/9094/9095 各司其职,服务名与四个方法固定。
  • 共享类型签名在前言定死;时间用 Duration,异常只在边界转换。
  • 「本机输出」只写真实执行过的结果,其余写「预期输出」并说明口径。
  • 本机没有 grpcurl,macOS 没有 ss 和 epoll;这些边界会如实写进正文。
  • 环境自检程序只依赖 JDK,可以在引入 gRPC 依赖之前先确认版本。
  • 四模块的单向依赖和四个端口是排障的第一现场,不要按「看起来相关」跳着查。

约定交代完,下一页是目录与学习路径:十五章怎么分组、每章的验收信号是什么、三条路径分别跳过哪些内容。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「books」更多文章

  1. 《Java 消息中间件·事件驱动架构》附录 B 架构决策记录模板
  2. 《Java 消息中间件·事件驱动架构》附录 A 术语表
  3. 《Java 消息中间件·事件驱动架构》15.3 全书验收