Spring Cloud + Spring gRPC + Nacos实现远程调用

小伙伴们,你们好呀,我是老寇,好几个月不见了,有没有想我呀!!!废话不多说,跟我一起学习gRPC远程调用。

gRPC

在学习之前,我们要了解什么是gRPC?和Dubbo,OpenFeign对比有什么优劣势?

gRPC介绍

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介绍

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-javagRPC官方基于Java实现的gRPC协议,无需了解gRPC底层的具体实现细节,可以直接通过API创建gRPC服务端gRPC客户端

spring-grpc 项目旨在简化gRPC应用程序的开发

项目实践

Spring-Cloud负载均衡代码贡献

实现思路

众所周知,微服务项目的服务节点会出现节点宕机或节点扩容,服务节点是动态变化,我们需要基于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

我是老寇,我们下次再见啦!

posted @ 2026-07-06 22:01  k↑  阅读(5)  评论(0)    收藏  举报