本文是一份详尽且深入的指南,旨在帮助读者理解并实现将 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 模块。
模块 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
通道来与服务进行通信。
执行:
分别运行产品服务和产品客户端,如果下面的所有内容应该是您应该看到的输出。
我使用 HTTPie 来测试服务。
总结
在微服务架构的背景下,采用 gRPC 和 Protobuf 进行服务间通信已被证明是提升效率、稳健性和跨语言互操作性的关键策略。gRPC 以其基于 HTTP/2 的通信机制和对多种编程语言的广泛支持,极大地促进了高性能和低延迟的服务交互,这与微服务架构的分布式特性完美契合。
Protobuf(Protocol Buffers)通过提供一种紧凑的二进制序列化格式,进一步强化了这一优势,确保服务之间交换的数据不仅体积小,而且序列化和反序列化的速度极快。这种高效的序列化机制使得数据传输更为迅速,同时也减少了网络带宽的占用,对于构建现代、高效且有效的微服务生态系统至关重要。
综上所述,gRPC 与 Protobuf 的结合,不仅为微服务架构提供了一种强大的通信工具,还为构建高性能、低延迟的分布式系统奠定了坚实的基础。这种组合无疑成为了推动微服务生态系统发展的强大动力。
没有回复内容