SpringBoot gRPC 实战

📖 前置阅读:本文假设读者已掌握 Protobuf 语法和 gRPC 四种调用模式的概念。如果还不熟悉,建议先阅读 Protobuf 语法精讲与 gRPC 概念

🎯 第一步:目标说明

上一篇用 protoc 写了 .proto 文件,手动编译生成了 Java 代码。接下来把这一切接入 SpringBoot——用 @GrpcService 暴露 gRPC 服务,用 @GrpcClient 注入远程代理,四种 RPC 模式全部用代码跑通。

📋 第二步:前置条件

前置项具体要求验证命令
JDK17+java -version
SpringBoot3.xmvn dependency:tree | grep spring-boot
protoc3.25+ (Maven 插件会自动下载)
前置知识Protobuf 语法、gRPC 四种模式概念

🔧 第三步:项目结构与依赖

3.1 多模块 Maven 项目

grpc-demo
├── pom.xml                          # 父 POM
├── grpc-api/                        # proto 文件 + 生成的 Java 代码
│   ├── pom.xml                      # 有 protobuf-maven-plugin
│   └── src/main/proto/
│       └── order.proto
├── grpc-server/                     # gRPC 服务端——实现业务逻辑
│   ├── pom.xml                      # 依赖 grpc-api
│   └── src/main/java/...
└── grpc-client/                     # gRPC 客户端——调用远程服务
    ├── pom.xml                      # 依赖 grpc-api
    └── src/main/java/...

为什么要把 proto 放在独立模块? 服务端和客户端都依赖 proto 生成的 Java 代码——放独立模块中双方共享编译结果,而不是各自编译一份。

3.2 依赖

父 POM:

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>io.grpc</groupId>
            <artifactId>grpc-bom</artifactId>
            <version>1.60.0</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

grpc-api 模块:

<dependencies>
    <!-- gRPC 核心依赖——protobuf + gRPC 运行时 -->
    <dependency>
        <groupId>io.grpc</groupId>
        <artifactId>grpc-protobuf</artifactId>
    </dependency>
    <dependency>
        <groupId>io.grpc</groupId>
        <artifactId>grpc-stub</artifactId>
    </dependency>
    <!-- 如果 proto 中用了 google.protobuf.Timestamp 等内置类型 -->
    <dependency>
        <groupId>io.grpc</groupId>
        <artifactId>grpc-protobuf-lite</artifactId>
        <!-- 或者用 com.google.api.grpc:proto-google-common-protos -->
    </dependency>
    <!-- 如果要在 Client 端用注解 @GrpcClient -->
    <dependency>
        <groupId>net.devh</groupId>
        <artifactId>grpc-client-spring-boot-starter</artifactId>
        <version>3.0.0.RELEASE</version>
    </dependency>
</dependencies>

grpc-server 模块:

<dependencies>
    <dependency>
        <groupId>net.devh</groupId>
        <artifactId>grpc-server-spring-boot-starter</artifactId>
        <version>3.0.0.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>org.example</groupId>
        <artifactId>grpc-api</artifactId>
        <version>${project.version}</version>
    </dependency>
</dependencies>

grpc-client 模块:

<dependencies>
    <dependency>
        <groupId>net.devh</groupId>
        <artifactId>grpc-client-spring-boot-starter</artifactId>
        <version>3.0.0.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>org.example</groupId>
        <artifactId>grpc-api</artifactId>
        <version>${project.version}</version>
    </dependency>
</dependencies>

⚠️ 新手提示:grpc-spring-boot-startergrpc-server-spring-boot-starter / grpc-client-spring-boot-starter 是同一个 library 的不同模块。Server 项目只用 server starter,Client 项目只用 client starter。不要一股脑全引——server starter 默认在 9090 端口起 gRPC 服务,client starter 不出端口。

3.3 proto 文件

// grpc-api/src/main/proto/order.proto
syntax = "proto3";
package com.example.grpc;
option java_multiple_files = true;

service OrderService {
  // ① Unary——一问一答
  rpc GetOrder(GetOrderRequest) returns (Order);

  // ② Server Streaming——一问多答
  rpc WatchOrders(WatchRequest) returns (stream Order);

  // ③ Client Streaming——多问一答
  rpc BatchCreateOrders(stream CreateOrderRequest) returns (BatchResult);

  // ④ Bidirectional Streaming——多问多答
  rpc ProcessOrders(stream OrderRequest) returns (stream OrderResponse);
}

message Order {
  int64 order_id = 1;
  string product_name = 2;
  double amount = 3;
  string status = 4;
}

message GetOrderRequest { int64 order_id = 1; }
message WatchRequest { int64 user_id = 1; }
message CreateOrderRequest {
  string product_name = 1;
  double amount = 2;
}
message BatchResult {
  int32 success_count = 1;
  int32 fail_count = 2;
}
message OrderRequest { int64 order_id = 1; }
message OrderResponse {
  int64 order_id = 1;
  string result = 2;
}

3.4 配置文件

Server 的 application.yml

grpc:
  server:
    port: 9090                    # gRPC 服务端口——默认 9090
spring:
  application:
    name: grpc-server

Client 的 application.yml

grpc:
  client:
    order-service:               # 给这个 gRPC 客户端起个名字——下面会用
      address: static://localhost:9090  # 直连模式——static://IP:Port
      negotiation-type: plaintext       # 明文——生产用 TLS
spring:
  application:
    name: grpc-client
server:
  port: 8080                     # REST Controller 的端口——和 gRPC 不冲突

🏗️ 第四步:四种 RPC 模式全部写一遍

4.1 Unary —— 一问一答

最常用的模式——客户端发一个请求,服务端返回一个响应。和 REST 的 GET / POST 一样简单。

Server 端实现:

// grpc-server——@GrpcService 暴露 gRPC 服务
@GrpcService
public class OrderServiceImpl extends OrderServiceGrpc.OrderServiceImplBase {

    // 模拟数据库
    private final Map<Long, Order> orderDB = new ConcurrentHashMap<>();

    @Override
    public void getOrder(GetOrderRequest request,
                         StreamObserver<Order> responseObserver) {

        Order order = orderDB.get(request.getOrderId());

        if (order == null) {
            // gRPC 的错误处理——用 Status 抛异常
            responseObserver.onError(
                Status.NOT_FOUND
                    .withDescription("订单不存在: " + request.getOrderId())
                    .asRuntimeException()
            );
            return;
        }

        // 发送响应
        responseObserver.onNext(order);
        // 告诉客户端——"我说完了"
        responseObserver.onCompleted();
    }
}

StreamObserver 三个方法详解

方法何时调用作用
onNext(response)每产生一个响应时发送一条消息给客户端——Unary 只调一次
onCompleted()所有响应都发完了关闭连接——告诉客户端"我讲完了"
onError(throwable)出现错误时中断连接——告诉客户端"出问题了"并带上异常信息

Client 端调用:

@RestController
@RequestMapping("/api/order")
public class OrderController {

    // @GrpcClient 注入 gRPC 客户端——和 Dubbo 的 @DubboReference 一个意思
    @GrpcClient("order-service")
    private OrderServiceGrpc.OrderServiceBlockingStub blockingStub;
    // BlockingStub = 同步调用——阻塞等待响应
    // OrderServiceStub = 异步调用——返回 StreamObserver

    @GetMapping("/{orderId}")
    public Map<String, Object> getOrder(@PathVariable Long orderId) {
        try {
            Order order = blockingStub
                    .withDeadlineAfter(3, TimeUnit.SECONDS)  // 3s 超时
                    .getOrder(GetOrderRequest.newBuilder()
                            .setOrderId(orderId)
                            .build());

            return Map.of("orderId", order.getOrderId(),
                    "productName", order.getProductName(),
                    "amount", order.getAmount());
        } catch (StatusRuntimeException e) {
            return Map.of("error", e.getStatus().getDescription());
        }
    }
}

三种 Stub(调用方式)

Stub 类型调用方式适用场景
BlockingStub同步——等返回才往下走REST Controller 中——HTTP 请求本身就是同步的
Stub(async)异步——返回 StreamObserver不阻塞当前线程——需要组合多个 gRPC 调用时
FutureStub异步——返回 ListenableFuture和 Java 的 CompletableFuture 配合

4.2 Server Streaming —— 一问多答

需求:客户端发一个 userId,服务端把该用户的订单逐条推回来——像是服务器在"直播"数据库中的订单。

Server 端实现:

@Override
public void watchOrders(WatchRequest request,
                        StreamObserver<Order> responseObserver) {

    // 从数据库查出该用户的订单——逐条返回
    List<Order> userOrders = orderDB.values().stream()
            .filter(o -> o.getUserId() == request.getUserId())
            .toList();

    for (Order order : userOrders) {
        // 每条订单调一次 onNext——客户端收到一条
        responseObserver.onNext(order);
        try {
            Thread.sleep(500); // 模拟间隔——实际场景可能是消息队列逐条推送
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }

    // 全部发完了——通知客户端
    responseObserver.onCompleted();
}

Client 端调用:

@GetMapping("/watch/{userId}")
public List<Map<String, Object>> watchOrders(@PathVariable Long userId) {
    List<Map<String, Object>> results = new ArrayList<>();

    // 用 Iterator 消费服务端的流——BlockingStub 自动转成 Iterator
    Iterator<Order> iterator = blockingStub
            .withDeadlineAfter(10, TimeUnit.SECONDS)
            .watchOrders(WatchRequest.newBuilder()
                    .setUserId(userId)
                    .build());

    while (iterator.hasNext()) {
        Order order = iterator.next();
        results.add(Map.of(
                "orderId", order.getOrderId(),
                "productName", order.getProductName()
        ));
    }
    // iterator.hasNext() 会在服务端调 onCompleted() 时返回 false
    return results;
}

客户端看到的效果:服务端每 onNext 一次,客户端的 iterator.hasNext() 返回 trueiterator.next() 返回最新一条。服务端 onCompleted() 后——iterator.hasNext() 返回 false,循环结束。

4.3 Client Streaming —— 多问一答

需求:客户端发一组创建订单的请求——全部发完后服务端返回一个批量操作的结果。

Server 端实现:

@Override
public StreamObserver<CreateOrderRequest> batchCreateOrders(
        StreamObserver<BatchResult> responseObserver) {

    // 返回一个 StreamObserver——客户端通过它发送消息
    return new StreamObserver<CreateOrderRequest>() {

        private int successCount = 0;
        private int failCount = 0;

        @Override
        public void onNext(CreateOrderRequest request) {
            // 客户端每发一条——服务端收到就处理
            try {
                createOrder(request);   // 实际业务逻辑
                successCount++;
            } catch (Exception e) {
                failCount++;
            }
        }

        @Override
        public void onError(Throwable t) {
            // 客户端发送过程中出了错
            System.err.println("客户端发送出错: " + t.getMessage());
        }

        @Override
        public void onCompleted() {
            // 客户端说"我发完了"——服务端此时返回汇总结果
            BatchResult result = BatchResult.newBuilder()
                    .setSuccessCount(successCount)
                    .setFailCount(failCount)
                    .build();
            responseObserver.onNext(result);
            responseObserver.onCompleted();
        }
    };
}

Client 端调用:

@PostMapping("/batch-create")
public Map<String, Object> batchCreate(@RequestBody List<Map<String, Object>> orders) {
    // 创建 StreamObserver——服务端会返回一个来接收客户端的数据流
    StreamObserver<CreateOrderRequest> requestObserver =
            blockingStub.batchCreateOrders(
                new StreamObserver<BatchResult>() {
                    @Override
                    public void onNext(BatchResult result) {
                        // 服务端处理完后返回汇总结果
                        System.out.printf("批量创建完成: 成功%d, 失败%d%n",
                                result.getSuccessCount(), result.getFailCount());
                    }

                    @Override
                    public void onError(Throwable t) {
                        System.err.println("批量创建出错: " + t.getMessage());
                    }

                    @Override
                    public void onCompleted() {
                        System.out.println("批量创建流关闭");
                    }
                });

    // 逐条发送——每条都是一个 CreateOrderRequest
    for (Map<String, Object> order : orders) {
        requestObserver.onNext(CreateOrderRequest.newBuilder()
                .setProductName((String) order.get("productName"))
                .setAmount(((Number) order.get("amount")).doubleValue())
                .build());
    }

    // 全部发完——通知服务端
    requestObserver.onCompleted();

    return Map.of("status", "sent", "count", orders.size());
}

4.4 Bidirectional Streaming —— 多问多答

需求:客户端和服务端可以随时互相发消息——像一个聊天室。这里做一个订单处理流水线——客户端发订单号,服务端逐条处理逐条返回结果。

Server 端实现:

@Override
public StreamObserver<OrderRequest> processOrders(
        StreamObserver<OrderResponse> responseObserver) {

    return new StreamObserver<OrderRequest>() {

        @Override
        public void onNext(OrderRequest request) {
            // 客户端发来一个订单请求——即时处理并回复
            OrderResponse response = processAndReply(request.getOrderId());
            responseObserver.onNext(response);  // 每条都立即回复
        }

        @Override
        public void onError(Throwable t) {
            responseObserver.onError(t);  // 把错误传回客户端
        }

        @Override
        public void onCompleted() {
            responseObserver.onCompleted();  // 双向关闭
        }
    };
}

private OrderResponse processAndReply(long orderId) {
    return OrderResponse.newBuilder()
            .setOrderId(orderId)
            .setResult(orderDB.containsKey(orderId) ? "processed" : "not_found")
            .build();
}

Client 端调用:

@PostMapping("/process")
public void processOrders(@RequestBody List<Long> orderIds) {
    CountDownLatch latch = new CountDownLatch(orderIds.size());

    StreamObserver<OrderRequest> requestObserver =
            // 这里用 asyncStub——不用 BlockingStub,因为两边都发消息
            asyncStub.processOrders(new StreamObserver<OrderResponse>() {

                @Override
                public void onNext(OrderResponse response) {
                    System.out.printf("收到处理结果: orderId=%d, result=%s%n",
                            response.getOrderId(), response.getResult());
                    latch.countDown();  // 收到一条——计数器减一
                }

                @Override
                public void onError(Throwable t) {
                    System.err.println("处理出错: " + t.getMessage());
                    while (latch.getCount() > 0) latch.countDown();  // 释放锁
                }

                @Override
                public void onCompleted() {
                    System.out.println("全部处理完成");
                }
            });

    // 逐条发送请求——服务端逐条处理并回复
    for (Long orderId : orderIds) {
        requestObserver.onNext(OrderRequest.newBuilder()
                .setOrderId(orderId)
                .build());
    }

    requestObserver.onCompleted();

    try {
        latch.await(30, TimeUnit.SECONDS);  // 等所有响应返回——最多 30s
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }
}

第五步:拦截器与异常处理

5.1 Server 端拦截器——认证/日志/限流

@GrpcGlobalInterceptor
public class AuthInterceptor implements ServerInterceptor {

    @Override
    public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(
            ServerCall<ReqT, RespT> call,
            Metadata headers,
            ServerCallHandler<ReqT, RespT> next) {

        // 从 Metadata 中提取 Token
        String token = headers.get(
                Metadata.Key.of("Authorization", Metadata.ASCII_STRING_MARSHALLER));

        if (token == null || !token.startsWith("Bearer ")) {
            // 没 Token 或格式不对——直接拒绝
            call.close(Status.UNAUTHENTICATED
                    .withDescription("缺少 Authorization Token"), new Metadata());
            return new ServerCall.Listener<>() {};  // 空 Listener——不处理
        }

        System.out.println("收到 gRPC 请求: " + call.getMethodDescriptor().getFullMethodName());

        // 放行——调下一个拦截器或真正的服务实现
        return next.startCall(call, headers);
    }
}

5.2 Client 端拦截器——自动注入 Token

@GrpcGlobalClientInterceptor
public class TokenClientInterceptor implements ClientInterceptor {

    @Override
    public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(
            MethodDescriptor<ReqT, RespT> method,
            CallOptions callOptions,
            Channel next) {

        return new ForwardingClientCall.SimpleForwardingClientCall<>(
                next.newCall(method, callOptions)) {

            @Override
            public void start(Listener<RespT> responseListener, Metadata headers) {
                // 每个发出的 gRPC 请求——自动附上 Token
                headers.put(
                        Metadata.Key.of("Authorization", Metadata.ASCII_STRING_MARSHALLER),
                        "Bearer " + getServiceToken());
                super.start(responseListener, headers);
            }
        };
    }

    private String getServiceToken() {
        // 从配置或缓存中拿 Token——这里简化
        return "your-service-token";
    }
}

5.3 异常处理——gRPC Status 的完整错误码

// gRPC 的标准状态码——不用自己定义错误码
// Server 端各种错误情况:

// ① 资源不存在——HTTP 404
responseObserver.onError(
    Status.NOT_FOUND.withDescription("订单不存在").asRuntimeException());

// ② 参数不合法——HTTP 400
responseObserver.onError(
    Status.INVALID_ARGUMENT.withDescription("orderId 不能为空").asRuntimeException());

// ③ 未授权——HTTP 401
responseObserver.onError(
    Status.UNAUTHENTICATED.withDescription("Token 已过期").asRuntimeException());

// ④ 权限不足——HTTP 403
responseObserver.onError(
    Status.PERMISSION_DENIED.withDescription("没有访问该订单的权限").asRuntimeException());

// ⑤ 服务端内部错——HTTP 500
responseObserver.onError(
    Status.INTERNAL.withDescription("数据库连接失败").asRuntimeException());

// ⑥ 服务端太忙——HTTP 429
responseObserver.onError(
    Status.RESOURCE_EXHAUSTED.withDescription("请求过多,请稍后重试").asRuntimeException());
gRPC StatusHTTP 对应使用场景
OK200正常——不抛异常
NOT_FOUND404查不到资源
INVALID_ARGUMENT400参数错误
UNAUTHENTICATED401未登录/Token 过期
PERMISSION_DENIED403没权限
INTERNAL500服务端崩了
UNAVAILABLE503服务不可用/熔断
DEADLINE_EXCEEDED504超时——Deadline 到了
RESOURCE_EXHAUSTED429被限流了

第六步:Deadline 超时传播

// 场景:A 调 B,B 调 C——如果 B 超时了,C 也该停止执行
// gRPC Deadline 的传播机制:下游继承上游的 Deadline

// A 调用 B 时设 Deadline = 3s
blockingStub.withDeadlineAfter(3, TimeUnit.SECONDS)
        .getOrder(request);
    // B 收到请求——拿到 Deadline = 当前时间 + 3s
    // B 调用 C 时——必须用同一个 Deadline(已经过了 1s,剩余 2s)
    // C 收到请求——拿到 Deadline = 当前时间 + 2s
    // C 超过 2s 没返回——直接抛 DEADLINE_EXCEEDED

// B 的代码——正确传播 Deadline
@Override
public void getOrder(GetOrderRequest request,
                     StreamObserver<Order> responseObserver) {
    // 从当前 gRPC 上下文中获取 Deadline
    Deadline deadline = Context.current().getDeadline();

    // 调用下游时传递 Deadline
    Order result = downstreamStub
            .withDeadline(deadline)  // ← 传递上游的 Deadline
            .getOrder(request);

    responseObserver.onNext(result);
    responseObserver.onCompleted();
}

如果不传播 Deadline——上游 3 秒超时了,下游还在跑。大 BUG。

第七步:FAQ

问题原因解决
@GrpcClient 注入的 Stub 为 nullgrpc.client.xxx.address 没配——gRPC 找不到目标检查 yml 中 grpc.client.order-service.address 是否正确
io.grpc.StatusRuntimeException: UNAVAILABLE连不上 gRPC Server——端口没开或 Server 没启动telnet localhost 9090——确认端口可连通
客户端收到 CANCELLED 但服务端日志正常客户端超时后取消请求——服务端还在处理导致的设置合理的 withDeadlineAfter()——不要设太小
StreamObserver.onNext()onCompleted() 之后调Unary 模式下——onNext 必须在 onCompleted 之前responseObserver.onNext(resp); responseObserver.onCompleted();——顺序不能反
proto 编译后找不到生成的 Java 类Maven 插件没跑——target/generated-sources 不在 classpath确认 protobuf-maven-plugin 配置正确 + mvn clean compile

🎯 总结

  1. 四个注解/类搞定 gRPC@GrpcService(暴露服务,替代手动 ServerBuilder)、@GrpcClient(注入客户端 Stub,替代手动 ManagedChannel)、StreamObserver(处理流的回调——onNext/onCompleted/onError)、Status(错误码——替代 HTTP 状态码)。

  2. 四种模式三种 Stub:Unary(一问一答)用 BlockingStub,Server Streaming(一问多答)用 BlockingStub + Iterator,Client Streaming(多问一答)返回 StreamObserver,BiDi Streaming(多问多答)双向 StreamObserver

  3. 拦截器有两个位置ServerInterceptor(认证、日志、限流——在服务端拦截所有进入的请求)、ClientInterceptor(注入 Token、传播 Deadline——在客户端拦截所有发出的请求)。

  4. Deadline 必须传播:A 调 B 调 C——如果 A 的超时是 3s,B 调用 C 时必须传递同一个 Deadline。否则 A 等超时了 C 还在跑——雪崩就是这样开始的。

📖 下一步阅读:SpringBoot 的操作搞定了。但在实际微服务项目中——proto 文件怎么组织?DTO 和 Domain 怎么转换?多个微服务之间的 proto 依赖怎么管理?继续阅读 微服务拆分实战:以 proto 为契约