Spring Boot + gRPC 构建可扩展的微服务-Spring专区论坛-技术-SpringForAll社区

Spring Boot + gRPC 构建可扩展的微服务

本文是一份详尽且深入的指南,旨在帮助读者理解并实现将 gRPC 通过 Maven 集成到 SpringBoot 项目中的全过程。文章首先以高度概括的方式探讨了 gRPC 的理论基础,为读者提供了对其核心概念的清晰认识。随后,我们将转向更为具体的实践层面,展示 gRPC 在实际应用中的多种实现方式。

虽然全文篇幅较长,但我们强烈建议您耐心阅读,以确保您能够全面掌握 gRPC 的技术细节和应用场景。通过本文的学习,您将不仅能够理解 gRPC 的工作原理,还能在您的 SpringBoot 项目中成功集成和应用这一强大的通信协议,从而提升系统的性能和效率。

什么是gRPC?

gRPC 是最初由 Google 开发的开源远程过程调用 (RPC) 框架。它是云原生计算基金会 (CNCF) 的一部分,旨在实现微服务架构中的服务之间高效、稳健的通信。以下是 gRPC 的一些关键功能和概念:

1. 协议缓冲区(Protobuf):

  • gRPC 使用 Protocol Buffers 作为其接口定义语言 (IDL)。
  • Protobuf 是一种与语言无关、与平台无关的可扩展机制,用于序列化结构化数据,类似于 XML 或 JSON,但更小、更快、更简单。
  • .proto文件用于定义服务方法和消息格式。

 2.基于HTTP/2:

  • gRPC 利用 HTTP/2 作为其传输协议,它允许对 HTTP/1.x 进行许多改进,例如通过单个 TCP 连接复用多个请求、二进制帧和高效的错误处理。

3、四种服务方式:

  • Unary RPC :客户端发送单个请求并获得单个响应。
  • Server Streaming RPC :客户端发送请求并获取响应流。
  • Client Streaming RPC :客户端将一系列消息流式传输到服务器,然后服务器发回单个响应。
  • Bidirectional Streaming RPC :客户端和服务器都独立地向对方发送消息流。

在这个实际的实现中,我们将看到一元实现它等于发送单个请求和响应。我们现在知道 gRPC 使用ProtocolBuffer来定义服务。首先让我们从定义接口定义开始,然后让我们看看项目结构和脚手架。下面的项目结构中的每个模块都是一个 Maven 模块。

d2b5ca33bd20240807221914

 

模块 3(原型服务):

proto-service模块负责保存与 proto 文件相关的所有内容,并将它们编译成 gRPC 相关的存根和接口。

让我们在src/main/proto文件夹中创建一个Product.proto文件并复制以下内容。

/**
* @author vaslabs(M K Pavan Kumar)
* @medium (https://medium.com/@manthapavankumar11)
*/

syntax = "proto3";

option java_multiple_files = true;

package com.vaslabs.proto;

message Product {
int32 product_id = 1;
string name = 2;
string description = 4;
float price = 3;
int32 category_id = 5;
}

message ProductList{
repeated Product product = 1;
}

message Category {
int32 category_id = 1;
string name = 2;
}

service ProductService {

//unary - synchronous
//request-response stype [not streaming]
rpc getProductById(Product) returns(Product){}
rpc getProductByCategoryId(Category) returns(ProductList){}
}

提供的 proto 文件采用 Protocol Buffers 版本 3 ( proto3 ) 的语法编写,是与产品和类别相关的 gRPC 服务的结构化定义。以下是其组成部分的简要说明:

 文件头:

  • syntax = "proto3"; :指定文件使用proto3语法,Protocol Buffers的最新版本。

 选项:

  • option java_multiple_files = true; :指示 Protocol Buffers 编译器为每种消息类型生成单独的 Java 文件,而不是单个文件。

 包裹声明:

  • package com.vaslabs.proto; :定义包名称,有助于防止不同项目之间的名称冲突。

 消息定义:

  • message Product :定义Product消息,其中包含产品 ID、名称、描述、价格和类别 ID 字段。每个字段都有一个唯一的标签号(例如, product_id = 1 )。
  • message ProductList :定义一个ProductList消息,该消息可以包含多个Product消息。 repeated表示它是Product对象的列表。
  • message Category :表示具有类别 ID 和名称的类别。

 服务定义:

  • service ProductService :声明一个名为ProductService服务。
  • rpc getProductById(Product) returns(Product){} :定义一元(单个请求-响应)方法getProductById ,该方法将Product消息作为输入并返回Product消息。
  • rpc getProductByCategoryId(Category) returns(ProductList){} :另一个一元方法getProductByCategoryIdCategory消息作为输入并返回ProductList消息。

 RPC 类型:

  • RPC 方法是一元的,这意味着它们遵循简单的请求-响应模式,无需流式传输。

让我们使用 maven 插件运行“mvncompile”来编译并获取 gRPC 风格生成的源代码以获取源代码。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.vaslabs</groupId>
<artifactId>springboot-grpc-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>

<artifactId>proto-service</artifactId>

<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>

<dependencies>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-stub</artifactId>
<version>1.56.1</version>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-protobuf</artifactId>
<version>1.54.2</version>
</dependency>
<dependency>
<groupId>javax.annotation</groupId>
<artifactId>javax.annotation-api</artifactId>
<version>1.3.2</version>
</dependency>
</dependencies>

<build>
<extensions>
<extension>
<groupId>kr.motd.maven</groupId>
<artifactId>os-maven-plugin</artifactId>
<version>1.6.1</version>
</extension>
</extensions>
<plugins>
<plugin>
<groupId>org.xolstice.maven.plugins</groupId>
<artifactId>protobuf-maven-plugin</artifactId>
<version>0.6.1</version>
<configuration>
<protocArtifact>
com.google.protobuf:protoc:3.17.3:exe:${os.detected.classifier}
</protocArtifact>
<pluginId>grpc-java</pluginId>
<pluginArtifact>
io.grpc:protoc-gen-grpc-java:1.53.0:exe:${os.detected.classifier}
</pluginArtifact>
</configuration>
<executions>
<execution>
<goals>
<goal>compile</goal>
<goal>compile-custom</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

模块 2(产品服务)

这是保存原型服务的实现的服务,接收原型请求并将原型响应发送给调用它的客户端。该服务由grpc-spring-boot-starter提供支持

将以下内容复制到该模块的 pom.xml 中。观察 pom 文件,我们已将proto-service模块添加为依赖项,因为我们将在此模块中实现 proto-services。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.vaslabs</groupId>
<artifactId>springboot-grpc-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>

<artifactId>product-service</artifactId>

<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>

<dependencies>
<dependency>
<groupId>org.vaslabs</groupId>
<artifactId>proto-service</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>net.devh</groupId>
<artifactId>grpc-spring-boot-starter</artifactId>
<version>2.14.0.RELEASE</version>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>

让我们在src/main/java中创建一个ProductServiceImpl.java类。观察该类扩展了由protoc生成的基类ProductServiceGrpc.ProductServiceImplBase

package org.vaslabs;

import com.vaslabs.proto.Category;
import com.vaslabs.proto.Product;
import com.vaslabs.proto.ProductList;
import com.vaslabs.proto.ProductServiceGrpc;
import io.grpc.stub.StreamObserver;
import net.devh.boot.grpc.server.service.GrpcService;

import java.util.List;

@GrpcService
public class ProductServiceImpl extends ProductServiceGrpc.ProductServiceImplBase {
@Override
public void getProductById(Product request, StreamObserver<Product> responseObserver) {
InMemoryData.getProducts()
.stream()
.filter(product -> product.getProductId() == request.getProductId())
.findFirst()
.ifPresent(responseObserver::onNext);
responseObserver.onCompleted();
}

@Override
public void getProductByCategoryId(Category request, StreamObserver<ProductList> responseObserver) {
List<Product> products = InMemoryData.getProducts()
.stream()
.filter(product -> product.getCategoryId() == request.getCategoryId())
.toList();

ProductList productList = ProductList.newBuilder().addAllProduct(products).build();

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

使用主类,我们可以启动服务ProductServiceApplication.java ,如下所示。

package org.vaslabs;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class ProductServiceApplication {
public static void main(String[] args) {
SpringApplication.run(ProductServiceApplication.class, args);
}
}

如果要更改默认的 gRPC 服务端口,请在src/main/resources/application.yaml中保留以下内容。

grpc:
server:
port: 9090

模块 1(产品-客户端)

该模块是一个纯Spring Boot Web客户端,它将以Http形式侦听Web请求,并使用proto请求与产品服务通信并接收proto响应,然后转换为DTO并将其作为Http响应发送给调用客户端。

让我们将以下内容复制到pom.xml,该模块由grpc-client-spring-boot-starter提供支持,以发出启用 grpc 的请求和响应。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.vaslabs</groupId>
<artifactId>springboot-grpc-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>

<artifactId>product-client</artifactId>

<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.vaslabs</groupId>
<artifactId>proto-service</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>net.devh</groupId>
<artifactId>grpc-client-spring-boot-starter</artifactId>
<version>2.14.0.RELEASE</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
<scope>provided</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>3.1.2</version>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>

让我们创建一个服务,它将使用生成的存根与 grpc 服务进行通信,如下所示。 src/main/java/org/vaslabs/service/ProductServiceGRPC.java

package org.vaslabs.service;

import com.vaslabs.proto.Category;
import com.vaslabs.proto.Product;
import com.vaslabs.proto.ProductList;
import com.vaslabs.proto.ProductServiceGrpc;
import net.devh.boot.grpc.client.inject.GrpcClient;
import org.springframework.stereotype.Service;

import java.util.List;

import static org.vaslabs.service.helpers.DtoMappingHelper.mapProductListToProductDTO;
import static org.vaslabs.service.helpers.DtoMappingHelper.mapProductToProductDTO;

@Service
public class ProductServiceRPC {

@GrpcClient("grpc-product-service")
ProductServiceGrpc.ProductServiceBlockingStub productServiceBlockingStub;

public org.vaslabs.dto.Product getProductById(int id) {
Product product = Product.newBuilder().setProductId(id).build();
Product response = productServiceBlockingStub.getProductById(product);
return mapProductToProductDTO(response);
}

public List<org.vaslabs.dto.Product> getProductByCategoryId(int id) {
Category category = Category.newBuilder().setCategoryId(id).build();
ProductList response = productServiceBlockingStub.getProductByCategoryId(category);
return mapProductListToProductDTO(response);
}
}

一旦收到原始响应,我们将使用以下辅助方法将其转换为 DTO。 **/helpers/DtoMappingHelper.java

package org.vaslabs.service.helpers;

import com.vaslabs.proto.ProductList;
import org.vaslabs.dto.Product;

import java.util.ArrayList;
import java.util.List;

public class DtoMappingHelper {

public static List<org.vaslabs.dto.Product> mapProductListToProductDTO(ProductList productList) {
List<Product> products = new ArrayList<>();
productList.getProductList().forEach(product -> {
Product product1 = getProduct();
product1.setId(product.getProductId());
product1.setCategoryId(product.getCategoryId());
product1.setName(product.getName());
product1.setDescription(product.getDescription());
product1.setPrice(product.getPrice());

products.add(product1);
});
return products;
}

public static org.vaslabs.dto.Product mapProductToProductDTO(com.vaslabs.proto.Product product) {
Product product1 = getProduct();
product1.setId(product.getProductId());
product1.setCategoryId(product.getCategoryId());
product1.setName(product.getName());
product1.setDescription(product.getDescription());
product1.setPrice(product.getPrice());
return product1;
}

private static Product getProduct(){
return new Product();
}
}

**/dto/Product.java

package org.vaslabs.dto;

import lombok.Data;
import lombok.Getter;
import lombok.Setter;

@Data
@Setter
@Getter
public class Product {
private int id;
private int categoryId;
private String name;
private String description;
private float price;
}

让我们创建一个名为**/ProductController.java的控制器,如下所示。

package org.vaslabs.controller;

import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import org.vaslabs.dto.Product;
import org.vaslabs.service.ProductServiceRPC;

import java.util.List;

@RestController
public class ProductController {

private final ProductServiceRPC productServiceRPC;

public ProductController(ProductServiceRPC productServiceRPC) {
this.productServiceRPC = productServiceRPC;
}

@GetMapping("/product/{id}")
public ResponseEntity<Product> getProductById(@PathVariable String id){
return ResponseEntity.ok().body(productServiceRPC.getProductById(Integer.parseInt(id)));
}

@GetMapping(value = "/product/category/{id}", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<List<Product>> getProductByCategoryId(@PathVariable String id){
return ResponseEntity.ok().body(productServiceRPC.getProductByCategoryId(Integer.parseInt(id)));
}
}

在此模块的资源文件夹中,请将以下内容添加到application.yaml文件中

grpc:
client:
grpc-product-service:
address: static://localhost:9090
negotiationType: plaintext

为此,存根将创建一个名为grpc-product-service通道来与服务进行通信。

 执行:

分别运行产品服务和产品客户端,如果下面的所有内容应该是您应该看到的输出。

d2b5ca33bd20240807221954

d2b5ca33bd20240807222015

我使用 HTTPie 来测试服务。

d2b5ca33bd20240807222044

d2b5ca33bd20240807222050

总结

在微服务架构的背景下,采用 gRPC 和 Protobuf 进行服务间通信已被证明是提升效率、稳健性和跨语言互操作性的关键策略。gRPC 以其基于 HTTP/2 的通信机制和对多种编程语言的广泛支持,极大地促进了高性能和低延迟的服务交互,这与微服务架构的分布式特性完美契合。

Protobuf(Protocol Buffers)通过提供一种紧凑的二进制序列化格式,进一步强化了这一优势,确保服务之间交换的数据不仅体积小,而且序列化和反序列化的速度极快。这种高效的序列化机制使得数据传输更为迅速,同时也减少了网络带宽的占用,对于构建现代、高效且有效的微服务生态系统至关重要。

综上所述,gRPC 与 Protobuf 的结合,不仅为微服务架构提供了一种强大的通信工具,还为构建高性能、低延迟的分布式系统奠定了坚实的基础。这种组合无疑成为了推动微服务生态系统发展的强大动力。

 

请登录后发表评论

    没有回复内容