这张表怎么用
这不是教程,是把正文里出现过的类和方法压成一张可查的表。写 linkd-v3 时先在这里定位类,再回到对应章节看为什么这样用。
约定:
- 版本固定为 JDK 25、gRPC-Java 1.76.0、Protobuf 4.33.2。方法签名省略返回值类型,只保留参数;需要返回值时看「用途」列。
- 「包」列只写类所在的包,import 时补全类名。gRPC 的类集中在
io.grpc与其子包,Protobuf 在com.google.protobuf与com.google.rpc。 - 「章节」指向正文节号,附录不重复解释机制。
- 表中
...表示可变参数或省略的重载,不是可以照抄的省略号。 - 只列白名单内的 API;不在表里的类,先确认版本再使用。
包名先记四个容易混的:
| 类 | 正确包 | 常见错写 |
|---|---|---|
ManagedChannel | io.grpc | io.grpc.netty |
StreamObserver | io.grpc.stub | io.grpc |
JsonFormat | com.google.protobuf.util | com.google.protobuf |
ErrorInfo | com.google.rpc | com.google.protobuf |
ManagedChannel 是接口,不是 Netty 的类;StreamObserver 在 io.grpc.stub 下,import 写错是最常见的编译错误之一。
gRPC 核心:Channel、Server 与 Stub
Channel 是客户端的长生命周期对象,Server 是服务端的长生命周期对象,Stub 是每次调用前的轻量视图。三者都不要在请求里反复创建。
| 类 | 包 | 成员 | 用途 | 章节 |
|---|---|---|---|---|
ManagedChannelBuilder | io.grpc | forAddress(String host, int port) | 按地址创建 builder | 4.1 |
ManagedChannelBuilder | io.grpc | forTarget(String target) | 按 URI 目标创建,配合 NameResolver | 5.2 |
ManagedChannelBuilder | io.grpc | usePlaintext() | 关闭 TLS,仅测试与内网明文 | 10.1 |
ManagedChannelBuilder | io.grpc | maxInboundMessageSize(int) | 设置入站消息上限,默认 4 MiB | 4.3 |
ManagedChannelBuilder | io.grpc | build() | 构建 channel,不发起连接 | 4.1 |
ManagedChannel | io.grpc | shutdown() | 停止新调用,等待在途完成 | 6.3 |
ManagedChannel | io.grpc | shutdownNow() | 立即取消在途调用 | 6.3 |
ManagedChannel | io.grpc | awaitTermination(long, TimeUnit) | 等待关闭完成 | 6.3 |
ManagedChannel | io.grpc | getState(boolean requestConnection) | 读连接状态,true 会触发连接 | 5.3 |
ServerBuilder | io.grpc | forPort(int port) | 创建服务端 builder,0 表示随机端口 | 4.1 |
ServerBuilder | io.grpc | addService(BindableService) | 注册服务实现 | 4.1 |
ServerBuilder | io.grpc | intercept(ServerInterceptor...) | 注册服务端拦截器,按顺序执行 | 4.2 |
ServerBuilder | io.grpc | maxInboundMessageSize(int) | 服务端入站上限 | 4.3 |
ServerBuilder | io.grpc | build() / start() | 构建并启动,start 后才监听 | 4.1 |
Server | io.grpc | getPort() | 实际监听端口,随机端口时使用 | 15.2 |
Server | io.grpc | shutdown() / shutdownNow() | 优雅 / 强制关闭 | 15.3 |
Server | io.grpc | awaitTermination(long, TimeUnit) | 等待排空 | 15.3 |
Server | io.grpc | isShutdown() / isTerminated() | 判断关闭阶段 | 15.3 |
AbstractStub | io.grpc.stub | withDeadlineAfter(long, TimeUnit) | 为单次调用设 deadline | 3.2 |
AbstractStub | io.grpc.stub | withDeadline(Deadline) | 用绝对 deadline 覆盖 | 3.2 |
AbstractStub | io.grpc.stub | withInterceptors(ClientInterceptor...) | 为 stub 附加拦截器 | 4.2 |
ClientCalls | io.grpc.stub | blockingUnaryCall(Channel, MethodDescriptor, CallOptions, ReqT) | 手写阻塞一元调用 | 3.1 |
ClientCalls | io.grpc.stub | asyncUnaryCall(...) | 手写异步一元调用 | 3.1 |
ServerCalls | io.grpc.stub | asyncUnaryCall(UnaryMethod, StreamObserver) | 手写服务端一元适配 | 3.1 |
ManagedChannelBuilder.build() 不建立连接,第一次调用或 getState(true) 才会。启动自检如果只 build 不调用,看到的「成功」没有意义。
调用、拦截器与流式
拦截器分服务端和客户端两套,方法签名对称但泛型位置不同。Forwarding* 是包装调用的入口,不要直接实现 ServerCall 的全部方法。
| 类 | 包 | 成员 | 用途 | 章节 |
|---|---|---|---|---|
ServerInterceptor | io.grpc | interceptCall(ServerCall, Metadata, ServerCallHandler) | 服务端拦截入口 | 4.2 |
ClientInterceptor | io.grpc | interceptCall(MethodDescriptor, CallOptions, Channel) | 客户端拦截入口 | 4.2 |
ServerCall | io.grpc | getMethodDescriptor() | 取方法元数据 | 4.2 |
ServerCall | io.grpc | sendHeaders(Metadata) | 发送响应头 | 4.2 |
ServerCall | io.grpc | sendMessage(RespT) | 发送一条响应消息 | 3.1 |
ServerCall | io.grpc | close(Status, Metadata) | 关闭调用,trailers 随状态返回 | 1.2 |
ServerCall | io.grpc | isCancelled() | 客户端是否已取消 | 3.2 |
ServerCallHandler | io.grpc | startCall(ServerCall, Metadata) | 启动业务处理,返回 listener | 4.2 |
ForwardingServerCall.SimpleForwardingServerCall | io.grpc | (ServerCall) | 包装服务端调用 | 13.2 |
ForwardingClientCall.SimpleForwardingClientCall | io.grpc | (ClientCall) | 包装客户端调用 | 13.2 |
ClientCall | io.grpc | start(Listener, Metadata) | 发起调用并写请求头 | 4.2 |
ClientCall | io.grpc | sendMessage(ReqT) / halfClose() | 发送请求、关闭写方向 | 3.1 |
ClientCall | io.grpc | cancel(String, Throwable) | 主动取消 | 3.2 |
StreamObserver | io.grpc.stub | onNext(T) / onError(Throwable) / onCompleted() | 流式响应三件套 | 3.1 |
ServerCallStreamObserver | io.grpc.stub | setOnCancelHandler(Runnable) | 客户端取消回调 | 3.2 |
ServerCallStreamObserver | io.grpc.stub | setOnReadyHandler(Runnable) | 可写回调,背压用 | 8.3 |
ServerCallStreamObserver | io.grpc.stub | isCancelled() / isReady() | 取消与可写状态 | 8.3 |
ClientCallStreamObserver | io.grpc.stub | cancel(String, Throwable) | 客户端流取消 | 3.2 |
ClientCallStreamObserver | io.grpc.stub | isReady() / setOnReadyHandler(Runnable) | 客户端背压 | 8.3 |
ServerCall.close 只能调用一次,重复调用会抛 IllegalStateException。在拦截器里提前关闭后,不要再把调用交给 delegate;13.2 的 abort 就是直接返回空 listener。
Metadata、Deadline 与 Context
Metadata 是 ASCII 键值对加二进制值的容器,key 必须通过 Metadata.Key.of 创建,不能直接用字符串。Deadline 是绝对时间点,Context 是调用范围内的隐式状态。
| 类 | 包 | 成员 | 用途 | 章节 |
|---|---|---|---|---|
Metadata | io.grpc | Key.of(String, BinaryMarshaller) | 创建 key,ASCII 或二进制 | 4.2 |
Metadata | io.grpc | ASCII_STRING_MARSHALLER | 字符串值的 marshaller | 4.2 |
Metadata | io.grpc | BINARY_BYTE_MARSHALLER | 二进制值的 marshaller | 4.2 |
Metadata | io.grpc | put(Key, T) / get(Key) | 写入 / 读取,重复键用 getAll | 4.2 |
Metadata | io.grpc | keys() / containsKey(Key) | 遍历与判断 | 4.2 |
Metadata | io.grpc | merge(Metadata) | 合并两组元数据 | 9.1 |
Deadline | io.grpc | after(long, TimeUnit) | 相对当前时间创建 | 3.2 |
Deadline | io.grpc | timeRemaining(TimeUnit) | 剩余时间,可为负 | 3.2 |
Deadline | io.grpc | isExpired() | 是否已过期 | 3.2 |
Context | io.grpc | current() | 当前调用上下文 | 3.2 |
Context | io.grpc | current().getDeadline() | 取传播过来的 deadline | 3.2 |
Context | io.grpc | current().isCancelled() | 是否已取消 | 3.2 |
Context | io.grpc | withDeadline(Deadline, ScheduledExecutorService) | 派生带 deadline 的上下文 | 3.2 |
Context | io.grpc | attach() / detach(Context) | 手动挂载与恢复 | 3.2 |
CallOptions | io.grpc | withDeadline(Deadline) | 在调用选项上设 deadline | 3.2 |
CallOptions | io.grpc | withDeadlineAfter(long, TimeUnit) | 相对 deadline | 3.2 |
CallOptions | io.grpc | getDeadline() | 读当前 deadline | 3.2 |
Deadline.timeRemaining 可能返回负数,表示已经过期;不要直接用返回值算 sleep,先判断 isExpired()。Context.attach() 必须与 detach() 配对,否则线程复用时上下文会串。
Status 与错误详情
Status 是 gRPC 的错误模型。StatusRuntimeException 是同步调用抛出的形式,StatusException 是异步/流式常用的受检形式。错误原因码放在 ErrorInfo 里,通过 StatusProto 与 Status 互转。
| 类 | 包 | 成员 | 用途 | 章节 |
|---|---|---|---|---|
Status | io.grpc | fromCode(Status.Code) | 由枚举创建 | 1.2 |
Status | io.grpc | withDescription(String) | 附一句人类可读描述 | 1.2 |
Status | io.grpc | withCause(Throwable) | 附根因,不传给对端 | 1.2 |
Status | io.grpc | asRuntimeException() | 转成可抛出的异常 | 1.2 |
Status | io.grpc | asException() | 转成受检异常 | 3.1 |
Status.Code | io.grpc | OK / NOT_FOUND / UNAVAILABLE / DEADLINE_EXCEEDED | 常用状态码 | 1.2 |
Status.Code | io.grpc | RESOURCE_EXHAUSTED / ABORTED / PERMISSION_DENIED | 限流、冲突、鉴权 | 1.2 |
StatusRuntimeException | io.grpc | getStatus() / getTrailers() | 取状态与 trailing metadata | 1.2 |
StatusException | io.grpc | getStatus() | 受检异常取状态 | 3.1 |
com.google.rpc.Status | com.google.rpc | newBuilder().setCode(int).setMessage(String) | 构造带详情的状态 | 1.2 |
com.google.rpc.Status | com.google.rpc | addDetails(Any) | 附加 ErrorInfo、RetryInfo | 1.2 |
ErrorInfo | com.google.rpc | newBuilder().setReason(String).setDomain(String) | 原因码与域 | 1.2 |
ErrorInfo | com.google.rpc | putMetadata(String, String) | 附加上下文,如 isbn | 1.2 |
RetryInfo | com.google.rpc | newBuilder().setRetryDelay(Duration) | 告诉客户端多久后重试 | 7.1 |
StatusProto | io.grpc.protobuf | toStatusRuntimeException(com.google.rpc.Status) | 带详情的状态转异常 | 1.2 |
StatusProto | io.grpc.protobuf | fromThrowable(Throwable) | 从异常还原 com.google.rpc.Status | 9.3 |
ProtoUtils | io.grpc.protobuf | keyForProto(Message) | 为 protobuf 消息创建 metadata key | 4.2 |
Status.withCause 只影响本地日志,不会把堆栈序列化给对端。要把原因码传给客户端,必须用 ErrorInfo 加 StatusProto。
Protobuf 与 JSON
Protobuf 的消息对象不可变,修改要通过 toBuilder()。JsonFormat 用于调试和 HTTP 网关的 JSON 转换,不适合作为 gRPC 的线格式。
| 类 | 包 | 成员 | 用途 | 章节 |
|---|---|---|---|---|
Message | com.google.protobuf | toByteArray() / getSerializedSize() | 序列化与大小 | 2.1 |
Message | com.google.protobuf | toBuilder() | 基于现有消息构造 builder | 2.1 |
Message | com.google.protobuf | getDefaultInstanceForType() | 取默认实例 | 2.1 |
ByteString | com.google.protobuf | copyFromUtf8(String) / copyFrom(byte[]) | 构造字节串 | 2.1 |
ByteString | com.google.protobuf | toByteArray() / toStringUtf8() | 转回字节或字符串 | 2.1 |
Descriptors.Descriptor | com.google.protobuf | getFields() / findFieldByName(String) | 反射读字段定义 | 2.3 |
Descriptors.FieldDescriptor | com.google.protobuf | getNumber() / getType() | 字段号与类型 | 2.3 |
JsonFormat.Printer | com.google.protobuf.util | printer().print(Message) | 消息转 JSON | 2.2 |
JsonFormat.Parser | com.google.protobuf.util | parser().merge(String, Builder) | JSON 转消息 | 2.2 |
JsonFormat | com.google.protobuf.util | printer().includingDefaultValueFields() | 输出默认值字段 | 2.2 |
JsonFormat | com.google.protobuf.util | parser().ignoringUnknownFields() | 忽略未知字段,兼容演进 | 2.3 |
JsonFormat 默认不输出默认值字段,网关如果直接透传,客户端可能看不到 stock: 0。需要显式开启 includingDefaultValueFields,并注意这与 proto3 的「默认值不序列化」语义不同。
JDK HttpClient
网关调用 libraryd 用 JDK 自带客户端,不引入额外 HTTP 库。HttpClient 是线程安全的,应该复用;HttpRequest 是每次请求构造的不可变对象。
| 类 | 包 | 成员 | 用途 | 章节 |
|---|---|---|---|---|
HttpClient | java.net.http | newBuilder().connectTimeout(Duration) | 连接超时 | 9.1 |
HttpClient | java.net.http | newBuilder().version(HttpClient.Version.HTTP_2) | 指定协议版本 | 9.1 |
HttpClient | java.net.http | newBuilder().executor(Executor) | 指定异步执行器 | 9.1 |
HttpClient | java.net.http | send(HttpRequest, BodyHandler) | 同步发送,阻塞当前线程 | 9.1 |
HttpClient | java.net.http | sendAsync(HttpRequest, BodyHandler) | 异步发送,返回 CompletableFuture | 9.1 |
HttpRequest | java.net.http | newBuilder(URI) | 构造请求 | 9.1 |
HttpRequest | java.net.http | timeout(Duration) | 单请求超时 | 9.1 |
HttpRequest | java.net.http | header(String, String) | 添加请求头 | 9.1 |
HttpRequest | java.net.http | GET() / POST(BodyPublisher) | 指定方法与 body | 9.1 |
HttpRequest.BodyPublishers | java.net.http | ofString(String) / ofByteArray(byte[]) | 构造请求体 | 9.2 |
HttpResponse | java.net.http | statusCode() / body() | 状态码与响应体 | 9.3 |
HttpResponse | java.net.http | headers().firstValue(String) | 读响应头,返回 Optional | 9.3 |
HttpResponse.BodyHandlers | java.net.http | ofString() / ofByteArray() / discarding() | 响应体处理器 | 9.1 |
HttpClient.send 会阻塞调用线程。在 gRPC 服务端直接调用它,会占住服务线程;要么用 sendAsync,要么把阻塞调用移到专用执行器。connectTimeout 和 HttpRequest.timeout 是两个不同的超时,前者管建连,后者管整个请求。
HTTP server 与并发
com.sun.net.httpserver.HttpServer 适合写测试替身和轻量网关,不适合高并发生产服务。并发工具用来实现限流、Bulkhead 和延迟统计。
| 类 | 包 | 成员 | 用途 | 章节 |
|---|---|---|---|---|
HttpServer | com.sun.net.httpserver | create(InetSocketAddress, int backlog) | 创建服务器 | 9.1 |
HttpServer | com.sun.net.httpserver | createContext(String, HttpHandler) | 注册路径 | 9.1 |
HttpServer | com.sun.net.httpserver | setExecutor(Executor) | 设置处理线程池 | 9.1 |
HttpServer | com.sun.net.httpserver | start() / stop(int delaySeconds) | 启动 / 停止 | 9.1 |
HttpExchange | com.sun.net.httpserver | getRequestMethod() / getRequestURI() | 方法与路径 | 9.1 |
HttpExchange | com.sun.net.httpserver | getRequestBody() / getResponseBody() | 请求与响应流 | 9.1 |
HttpExchange | com.sun.net.httpserver | sendResponseHeaders(int, long) | 状态码与长度,-1 表示未知 | 9.1 |
HttpHandler | com.sun.net.httpserver | handle(HttpExchange) | 处理入口 | 9.1 |
ExecutorService | java.util.concurrent | submit(Callable) / shutdown() | 任务提交与关闭 | 8.2 |
Executors | java.util.concurrent | newFixedThreadPool(int) | 固定线程池 | 8.2 |
Executors | java.util.concurrent | newVirtualThreadPerTaskExecutor() | 每任务一个虚拟线程 | 7.3 |
Semaphore | java.util.concurrent | acquire() / tryAcquire(long, TimeUnit) / release() | Bulkhead 并发许可 | 7.3 |
CountDownLatch | java.util.concurrent | countDown() / await() | 等待一批任务完成 | 14.2 |
CompletableFuture | java.util.concurrent | thenApply / whenComplete / join | 异步编排 | 9.2 |
LongAdder | java.util.concurrent.atomic | increment() / sum() | 高并发计数 | 11.1 |
AtomicLong | java.util.concurrent.atomic | incrementAndGet() / get() | 序号与计数 | 11.1 |
ConcurrentHashMap | java.util.concurrent | computeIfAbsent / merge | 并发缓存与注册表 | 5.1 |
Duration | java.time | ofMillis(long) / toSeconds() | 时间间隔 | 3.2 |
Instant | java.time | now() / toEpochMilli() | 时间点与时间戳 | 11.2 |
Clock | java.time | systemUTC() | 可替换时钟,便于测试 | 3.2 |
HttpServer.stop(delay) 的 delay 是秒,不是毫秒;传 0 会立即关闭,可能截断在途响应。Executors.newVirtualThreadPerTaskExecutor() 适合阻塞调用多的场景,但不适合 CPU 密集任务,也不解决下游容量问题。
在 linkd-v3 里落地
一次带元数据、deadline 和错误映射的一元调用:
package com.plume.linkd.v3.gateway;
import com.plume.linkd.v3.api.Book;
import com.plume.linkd.v3.api.GetBookRequest;
import com.plume.linkd.v3.api.LibraryGrpc;
import io.grpc.CallOptions;
import io.grpc.Channel;
import io.grpc.ClientCall;
import io.grpc.ClientInterceptor;
import io.grpc.ForwardingClientCall;
import io.grpc.ManagedChannel;
import io.grpc.Metadata;
import io.grpc.MethodDescriptor;
import java.util.concurrent.TimeUnit;
public final class UnaryCallExample {
private static final Metadata.Key<String> REQUEST_ID =
Metadata.Key.of("x-request-id", Metadata.ASCII_STRING_MARSHALLER);
public static Book fetch(ManagedChannel channel, String isbn, String requestId) {
ClientInterceptor attach = new ClientInterceptor() {
@Override
public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(
MethodDescriptor<ReqT, RespT> method, CallOptions options, Channel next) {
return new ForwardingClientCall.SimpleForwardingClientCall<>(
next.newCall(method, options)) {
@Override
public void start(Listener<RespT> listener, Metadata headers) {
headers.put(REQUEST_ID, requestId);
super.start(listener, headers);
}
};
}
};
LibraryGrpc.LibraryBlockingStub stub = LibraryGrpc.newBlockingStub(channel)
.withInterceptors(attach)
.withDeadlineAfter(300, TimeUnit.MILLISECONDS);
return stub.getBook(GetBookRequest.newBuilder().setIsbn(isbn).build());
}
}
带 ErrorInfo 的错误构造与解析:
package com.plume.linkd.v3.core;
import com.google.protobuf.Any;
import com.google.protobuf.InvalidProtocolBufferException;
import com.google.rpc.ErrorInfo;
import io.grpc.Status;
import io.grpc.StatusRuntimeException;
import io.grpc.protobuf.StatusProto;
public final class RpcErrors {
public static StatusRuntimeException bookNotFound(String isbn) {
ErrorInfo info = ErrorInfo.newBuilder()
.setReason("BOOK_NOT_FOUND")
.setDomain("library")
.putMetadata("isbn", isbn)
.build();
com.google.rpc.Status status = com.google.rpc.Status.newBuilder()
.setCode(Status.Code.NOT_FOUND.value())
.setMessage("book not found")
.addDetails(Any.pack(info))
.build();
return StatusProto.toStatusRuntimeException(status);
}
public static String reasonOf(Throwable error) {
com.google.rpc.Status status = StatusProto.fromThrowable(error);
if (status == null) {
return "UNKNOWN";
}
for (Any detail : status.getDetailsList()) {
if (detail.is(ErrorInfo.class)) {
try {
return detail.unpack(ErrorInfo.class).getReason();
} catch (InvalidProtocolBufferException e) {
return "MALFORMED_ERROR_INFO";
}
}
}
return "UNKNOWN";
}
}
Any.is(Class) 与 Any.unpack(Class) 是 Protobuf 的标准方法;原因码放在 ErrorInfo 里,而不是拼在 Status.description 字符串里,客户端才能稳定解析。
常见错误与边界
错误一:ManagedChannel 每次请求新建。 连接、TLS 握手、NameResolver 全部重来,延迟和 fd 都会失控。Channel 应该是进程级单例。
错误二:Server.shutdownNow() 当默认关闭。 在途请求被取消,客户端看到 UNAVAILABLE。默认用 shutdown() 加 awaitTermination。
错误三:Metadata key 用字符串直接构造。 Metadata.Key 必须通过 of 创建,直接 put("x-request-id", ...) 编译不过。
错误四:把 Status.description 当原因码。 描述是给人看的,可能变化;原因码要放 ErrorInfo.reason。
错误五:在服务线程里调用 HttpClient.send。 阻塞会占住 gRPC 执行线程,高并发时线程池迅速耗尽。用 sendAsync 或专用执行器。
错误六:HttpServer.stop(0)。 参数是秒,0 表示立即关闭,会截断在途响应。
边界:本表只覆盖卷三用到的 API。 gRPC 还有 NameResolver、LoadBalancer、Credentials 等接口,正文按需展开;不在表里的方法,使用前先查 1.76.0 的 javadoc。
边界:版本差异会改变默认值。 消息大小上限、压缩算法、keepalive 默认值都可能随版本变化;升级 gRPC 时重新核对本文的默认参数。
小结
- Channel 与 Server 是长生命周期对象,Stub 是轻量视图;Channel 不要每次请求新建。
- 拦截器用
Forwarding*包装,ServerCall.close只能调用一次。 - Metadata key 必须用
Key.of创建;Deadline 是绝对时间点,剩余时间可能为负。 - 原因码放
ErrorInfo,用StatusProto与 Status 互转;description不是稳定契约。 JsonFormat默认不输出默认值字段,网关透传时要显式开启。HttpClient复用、sendAsync优先;HttpServer.stop的参数是秒。- 并发工具按场景选:Semaphore 做 Bulkhead、LongAdder 做计数、虚拟线程处理阻塞调用。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。