Spring Cloud + Spring gRPC + Nacos实现远程调用
小伙伴们,你们好呀,我是老寇,好几个月不见了,有没有想我呀!!!废话不多说,跟我一起学习gRPC远程调用。
gRPC
在学习之前,我们要了解什么是gRPC?和Dubbo,OpenFeign对比有什么优劣势?
gRPC介绍
gRPC是一个现代化的开源高性能远程过程调用 (RPC) 框架,可在任何环境下运行。它能够高效地连接数据中心内部及跨数据中心的服务,并提供可插拔的负载均衡、追踪、健康检查和身份验证支持。此外,它也适用于分布式计算的“最后一公里”,用于连接设备、移动应用和浏览器与后端服务。
gRPC优劣势
| 特性 | gRPC | Dubbo | OpenFeign |
|---|---|---|---|
| 协议 | HTTP/2 + Protobuf(二进制) | 自定义协议(Dubbo、Triple 等) | HTTP/1.1 + JSON |
| 性能 | 🚀 高(低延迟、带宽小) | 🚀 高(RPC协议) | 🐢 较低(基于HTTP + JSON) |
| 序列化 | Protobuf(高效) | Hessian / JSON / Protobuf | JSON(可读但慢) |
| 跨语言性 | ✅ 强(多语言官方支持) | ⚠️ 一般(Dubbo3之前以Java为主) | ✅ 强(HTTP天然支持) |
| 服务治理 | ⚠️ 依赖外部组件(Envoy / Istio) | ✅ 内置(注册、发现、限流、熔断) | ⚠️ 依赖 Spring Cloud Netflix / Alibaba |
| 使用复杂度 | 中等(需 .proto + 代码生成) | 中等(需注册中心、配置) | 简单(基于HTTP注解) |
| 流式通信 | ✅ 支持双向流 | ⚠️ 取决于协议 | ❌ 不支持 |
| 调试可读性 | ❌ 较差(Binary) | ❌ 较差(Binary) | ✅ 优秀(HTTP+JSON) |
| 生态/集成 | CNCF、K8s、Envoy等 | 阿里生态、Spring生态 | Spring Cloud 原生 |
| 典型场景 | 高性能微服务、跨语言调用、AI/IoT | Java企业级微服务系统 | Web服务间调用、BFF层、API聚合 |
Protobuf
学习之前,我们要了解什么是Protobuf?
Protobuf介绍
Protocol Buffers 是一种语言无关、平台无关的可扩展机制,用于序列化结构化数据。
它类似于 JSON,但体积更小、速度更快,并且能够生成原生语言绑定。您只需定义一次数据结构,即可使用专门生成的源代码,轻松地从各种数据流中读取和写入结构化数据,并支持多种语言。
协议缓冲区是由定义语言(在 .proto文件中创建)、proto 编译器生成的用于与数据交互的代码、特定于语言的运行时库、写入文件(或通过网络连接发送)的数据的序列化格式以及序列化数据组合而成的。
Protobuf举例
hello.proto
syntax = "proto3"; // 声明使用 proto3 语法
option java_package = "org.laokou.proto"; // 生成Java代码的包名
option java_outer_classname = "HelloWorldProto"; // 生成Java代码的类名
service Simple { // 服务名
rpc SayHello (HelloRequest) returns (HelloReply) { // 方法名(请求参数) 返回参数
}
}
message HelloRequest { // 请求参数
string name = 1; // 字段类型 字段名 = 唯一编号
}
message HelloReply { // 返回参数
string message = 1; // 字段类型 字段名 = 唯一编号
}
更多的写法参考Protobuf官方文档即可,这里不做过多赘述!
开源项目推荐
grpc-java 是gRPC官方基于Java实现的gRPC协议,无需了解gRPC底层的具体实现细节,可以直接通过API创建gRPC服务端和gRPC客户端
spring-grpc 项目旨在简化gRPC应用程序的开发
项目实践
实现思路
众所周知,微服务项目的服务节点会出现节点宕机或节点扩容,服务节点是动态变化,我们需要基于Spring Cloud的负载均衡来进行设计,还需要考虑到网络因素,导致服务不可用或调用时间过长,导致服务雪崩,因此,需要进行重试,以及服务熔断降级。
通过 discovery://service-name 标识 Spring Cloud gRPC 客户端
代码实现
依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-grpc-client</artifactId>
</dependency>
具体代码
DiscoveryGrpcChannelFactory
// 创建spring cloud grpc channel factory
final class DiscoveryGrpcChannelFactory extends NettyGrpcChannelFactory {
/**
* 构建通道工厂实例.
* @param globalCustomizers 要应用于所有已创建频道的全局自定义设置
* @param interceptorsConfigurer 配置已创建通道上的客户端拦截器
*/
public DiscoveryGrpcChannelFactory(
List<GrpcChannelBuilderCustomizer<@NonNull NettyChannelBuilder>> globalCustomizers,
ClientInterceptorsConfigurer interceptorsConfigurer) {
super(globalCustomizers, interceptorsConfigurer);
setVirtualTargets((p) -> p.substring(12));
}
@Override
public boolean supports(String target) {
return target.startsWith("discovery:");
}
@NonNull
@Override
protected NettyChannelBuilder newChannelBuilder(@NonNull String target, @NonNull ChannelCredentials credentials) {
return NettyChannelBuilder.forTarget(String.format("discovery://%s", target), credentials);
}
}
DiscoveryNameResolverProvider
NameResolverProvider 是 gRPC 中 负责服务名解析(Name Resolution) 的扩展点
@Slf4j
final class DiscoveryNameResolverProvider extends NameResolverProvider {
private final DiscoveryClient discoveryClient;
private final Set<DiscoveryNameResolver> discoveryNameResolvers;
public DiscoveryNameResolverProvider(DiscoveryClient discoveryClient) {
this.discoveryClient = discoveryClient;
this.discoveryNameResolvers = new HashSet<>(512);
}
@Override
protected boolean isAvailable() {
return true;
}
@Override
protected int priority() {
return 6;
}
@Override
public NameResolver newNameResolver(URI uri, NameResolver.Args args) {
DiscoveryNameResolver discoveryNameResolver = new DiscoveryNameResolver(uri.getHost(), discoveryClient);
discoveryNameResolvers.add(discoveryNameResolver);
return discoveryNameResolver;
}
@Override
public String getDefaultScheme() {
return "discovery";
}
// 监听nacos心跳,详情见如下配置
// 开启心跳检测
// spring.cloud.nacos.discovery.heart-beat.enabled=true
// 心跳间隔(毫秒)
// spring.cloud.nacos.discovery.heart-beat-interval=30000
@EventListener(HeartbeatEvent.class)
public void onHeartbeatEvent(HeartbeatEvent event) {
log.debug("Received HeartbeatEvent, refreshing DiscoveryNameResolvers, event: {}", event.getValue());
for (DiscoveryNameResolver discoveryNameResolver : discoveryNameResolvers) {
discoveryNameResolver.refreshFromExternal();
}
}
}
DiscoveryNameResolver
gRPC Server 通过注册 grpc_port 元数据到 Nacos ,因此,gRPC Client可以获取具体的gRPC Port
final class DiscoveryNameResolver extends NameResolver {
private final String serviceId;
private final DiscoveryClient discoveryClient;
private final List<ServiceInstance> serviceInstances;
private final AtomicBoolean resolving;
private Listener2 listener;
public DiscoveryNameResolver(String serviceId, DiscoveryClient discoveryClient) {
this.serviceId = serviceId;
this.discoveryClient = discoveryClient;
this.serviceInstances = new CopyOnWriteArrayList<>();
this.resolving = new AtomicBoolean(false);
}
@Override
public String getServiceAuthority() {
return serviceId;
}
@Override
public void shutdown() {
this.serviceInstances.clear();
}
@Override
public void start(Listener2 listener) {
this.listener = listener;
resolve();
}
@Override
public void refresh() {
resolve();
}
public void refreshFromExternal() {
Thread.startVirtualThread(() -> {
if (ObjectUtils.isNotNull(listener)) {
resolve();
}
});
}
private void resolve() {
if (this.resolving.compareAndSet(false, true)) {
Thread.startVirtualThread(() -> {
this.serviceInstances.addAll(resolveInternal());
this.resolving.set(false);
});
}
}
private List<ServiceInstance> resolveInternal() {
List<ServiceInstance> newServiceInstanceList = this.discoveryClient.getInstances(this.serviceId);
if (CollectionExtUtils.isEmpty(newServiceInstanceList)) {
listener.onError(Status.UNAVAILABLE.withDescription("No servers found for " + serviceId));
return Collections.emptyList();
}
if (!isUpdateServiceInstance(serviceInstances, newServiceInstanceList)) {
return serviceInstances;
}
this.listener.onResult(ResolutionResult.newBuilder()
.setAddressesOrError(StatusOr.fromValue(toAddresses(newServiceInstanceList)))
.build());
return newServiceInstanceList;
}
private boolean isUpdateServiceInstance(List<ServiceInstance> serviceInstances,
List<ServiceInstance> newServiceInstanceList) {
if (serviceInstances.size() != newServiceInstanceList.size()) {
return true;
}
Set<String> oldSet = serviceInstances.stream().map(this::getAddressStr).collect(Collectors.toSet());
Set<String> newSet = newServiceInstanceList.stream().map(this::getAddressStr).collect(Collectors.toSet());
return !ObjectUtils.equals(oldSet, newSet);
}
private List<EquivalentAddressGroup> toAddresses(List<ServiceInstance> newServiceInstanceList) {
List<EquivalentAddressGroup> addresses = new ArrayList<>(newServiceInstanceList.size());
for (ServiceInstance serviceInstance : newServiceInstanceList) {
addresses.add(toAddress(serviceInstance));
}
return addresses;
}
private EquivalentAddressGroup toAddress(ServiceInstance serviceInstance) {
String host = serviceInstance.getHost();
int port = getGrpcPost(serviceInstance);
return new EquivalentAddressGroup(new InetSocketAddress(host, port), getAttributes(serviceInstance));
}
private Attributes getAttributes(ServiceInstance serviceInstance) {
return Attributes.newBuilder()
.set(Attributes.Key.create("serviceId"), serviceId)
.set(Attributes.Key.create("instanceId"), serviceInstance.getInstanceId())
.build();
}
private String getAddressStr(ServiceInstance serviceInstance) {
return serviceInstance.getHost() + ":" + getGrpcPost(serviceInstance);
}
private int getGrpcPost(ServiceInstance serviceInstance) {
Map<String, String> metadata = serviceInstance.getMetadata();
if (ObjectUtils.isNotNull(metadata) && !metadata.isEmpty()) {
return Integer.parseInt(metadata.getOrDefault("grpc_port", "9090"));
}
return 9090;
}
}
DiscoveryNameResolverRegister
注册服务名称解析
record DiscoveryNameResolverRegister(
DiscoveryNameResolverProvider discoveryNameResolverProvider) implements InitializingBean, DisposableBean {
@Override
public void afterPropertiesSet() {
NameResolverRegistry.getDefaultRegistry().register(discoveryNameResolverProvider);
}
@Override
public void destroy() {
NameResolverRegistry.getDefaultRegistry().deregister(discoveryNameResolverProvider);
}
}
gRPC注解
@Inherited
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.ANNOTATION_TYPE, ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER })
public @interface GrpcClient {
String serviceId();
}
GrpcClientBeanPostProcessor
它实现了类似 Spring @Autowired 的功能,不过注入的不是普通 Bean,而是 gRPC Client Stub(代理对象)
public record GrpcClientBeanPostProcessor(GrpcClientFactory grpcClientFactory) implements BeanPostProcessor {
@Override
public Object postProcessBeforeInitialization(@NonNull Object bean, @NonNull String beanName)
throws BeansException {
Class<?> clazz = bean.getClass();
do {
setFields(bean, clazz);
setMethods(bean, clazz);
clazz = clazz.getSuperclass();
}
while (ObjectUtils.isNotNull(clazz));
return bean;
}
private void setFields(Object bean, Class<?> clazz) {
for (Field field : clazz.getDeclaredFields()) {
GrpcClient grpcClient = AnnotationUtils.findAnnotation(field, GrpcClient.class);
if (ObjectUtils.isNotNull(grpcClient)) {
ReflectionUtils.makeAccessible(field);
ReflectionUtils.setField(field, bean, getClient(field.getType(), grpcClient));
}
}
}
private void setMethods(Object bean, Class<?> clazz) {
for (Method method : clazz.getDeclaredMethods()) {
GrpcClient grpcClient = AnnotationUtils.findAnnotation(method, GrpcClient.class);
if (ObjectUtils.isNotNull(grpcClient)) {
ReflectionUtils.makeAccessible(method);
ReflectionUtils.invokeMethod(method, bean, getClient(method.getParameterTypes()[0], grpcClient));
}
}
}
private <T> T getClient(Class<T> type, @NonNull GrpcClient grpcClient) {
String target = String.format("discovery://%s", grpcClient.serviceId());
return grpcClientFactory.getClient(target, type, null);
}
}
GrpcClientConfig
@ImportGrpcClients
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(DiscoveryClient.class)
final class GrpcClientConfig {
@Bean
DiscoveryNameResolverProvider discoveryNameResolverProvider(DiscoveryClient discoveryClient) {
return new DiscoveryNameResolverProvider(discoveryClient);
}
@Bean
DiscoveryNameResolverRegister discoveryNameResolverRegister(
DiscoveryNameResolverProvider discoveryNameResolverProvider) {
return new DiscoveryNameResolverRegister(discoveryNameResolverProvider);
}
@Bean
GrpcClientBeanPostProcessor grpcClientBeanPostProcessor(GrpcClientFactory grpcClientFactory) {
return new GrpcClientBeanPostProcessor(grpcClientFactory);
}
@Bean
DiscoveryGrpcChannelFactory discoveryGrpcChannelFactory(
List<GrpcChannelBuilderCustomizer<@NonNull NettyChannelBuilder>> globalCustomizers,
ClientInterceptorsConfigurer interceptorsConfigurer) {
return new DiscoveryGrpcChannelFactory(globalCustomizers, interceptorsConfigurer);
}
}
测试 gRPC
依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-grpc-server</artifactId>
<scope>test</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-grpc-test</artifactId>
<scope>test</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-discovery</artifactId>
<scope>test</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers-consul</artifactId>
<scope>test</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<scope>test</scope>
<optional>true</optional>
</dependency>
具体代码
hello.proto
syntax = "proto3";
option java_package = "org.laokou.grpc.proto";
option java_outer_classname = "HelloWorldProto";
service Simple {
rpc SayHello (HelloRequest) returns (HelloReply) {
}
}
message HelloRequest {
string name = 1;
}
message HelloReply {
string message = 1;
}
GrpcClientTest
@Testcontainers
@Import(GrpcClientTest.MockOAuth2Config.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = GrpcClientTest.GrpcTest.class)
class GrpcClientTest {
@Container
static ConsulContainer consul = new ConsulContainer(DockerImageNames.consul());
@DynamicPropertySource
static void consulProperties(DynamicPropertyRegistry registry) {
registry.add("spring.cloud.consul.host", consul::getHost);
registry.add("spring.cloud.consul.port", consul::getFirstMappedPort);
registry.add("spring.cloud.consul.discovery.service-name", () -> "laokou-grpc");
registry.add("spring.cloud.consul.discovery.instance-id", () -> "laokou-grpc-1");
registry.add("spring.cloud.consul.discovery.metadata.grpc_port", () -> "9097");
registry.add("spring.cloud.consul.discovery.prefer-ip-address", () -> true);
}
@GrpcClient(serviceId = "laokou-grpc")
private SimpleGrpc.SimpleBlockingV2Stub simpleBlockingV2Stub;
@Test
void test() throws StatusException {
HelloWorldProto.HelloRequest request = HelloWorldProto.HelloRequest.newBuilder().setName("test").build();
HelloWorldProto.HelloReply helloReply = simpleBlockingV2Stub.sayHello(request);
Assertions.assertThat(helloReply).isNotNull();
Assertions.assertThat(helloReply.getMessage()).isEqualTo("Hello ==> test");
}
@EnableDiscoveryClient
@SpringBootApplication(scanBasePackages = { "org.laokou" })
static class GrpcTest {
}
@TestConfiguration
static class MockOAuth2Config {}
}
application.yaml
server:
port: 8979
spring:
application:
name: laokou-grpc
grpc:
server:
servlet:
enabled: false
port: 9097
enabled: true
client:
channel:
laokou-grpc:
target: discovery://laokou-grpc
keepalive:
time: 10s
timeout: 30s
idle:
timeout: 30s
我是老寇,我们下次再见啦!

浙公网安备 33010602011771号