本文是一份详尽且深入的指南,旨在帮助读者理解并实现将 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){} :另一个一元方法getProductByCategoryId将Category消息作为输入并返回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 的结合,不仅为微服务架构提供了一种强大的通信工具,还为构建高性能、低延迟的分布式系统奠定了坚实的基础。这种组合无疑成为了推动微服务生态系统发展的强大动力。

 

— 感谢阅读 —
广告

一起交流

分享你的思考,让讨论更进一步。