Spring Boot与gRPC的整合

一、gRPC的介绍

在gRPC中,客户机应用程序可以直接调用不同机器上的服务器应用程序上的方法,就像它是本地对象一样,使您更容易创建分布式应用程序和服务。与许多RPC系统一样,gRPC基于定义服务的思想,指定可以远程调用的方法及其参数和返回类型。在服务器端,服务器实现这个接口,并运行gRPC服务器来处理客户端调用。在客户端,客户端有一个存根(在某些语言中称为客户端),它提供与服务器相同的方法。

gRPC客户端和服务器可以在各种环境中运行并相互通信 - 从Google内部的服务器到您自己的桌面 - 并且可以用任何gRPC支持的语言编写。因此,例如,您可以轻松地用Java创建gRPC服务器,用Go、Python或Ruby创建客户端。此外,最新的Google api将提供gRPC版本的接口,让您可以轻松地将Google功能构建到应用程序中。

gRPC的官网地址如下:

https://grpc.io/docs/

gRPC 使用 proto buffers 作为服务定义语言,编写 proto 文件,即可完成服务的定义。

二、前置准备

在resources目录下创建proto文件夹,根据protobuf协议编写 message.proto 文件和 file.proto 文件,server和client端都要编写。

file.proto 文件的内容如下:

syntax = "proto3";
package protocol;option java_package = "com.example.demo.protos";message File {string name = 1;int32 size = 2;
}

message.proto 文件的内容如下: 

syntax = "proto3";
package protocol;import "file.proto";option java_multiple_files = true;
option java_package = "com.example.demo.protos";message User {reserved 6 to 7;reserved "userId2";int32 userId = 1;string username = 2;oneof msg {string error = 3;int32 code = 4;}string name = 8;UserType userType = 9;repeated int32 roles = 10;protocol.File file = 11;map<string, string> hobbys = 12;
}enum UserType {UNKNOW = 0;ADMIN = 1;BUSINESS_USER = 2;
};service UserService {rpc getUser (User) returns (User) {}rpc getUsers (User) returns (stream User) {}
}service FileService {rpc getFile(User) returns(File) {}
}

接着根据下述地址去官网中下载Protoc生成Java业务代码插件(protoc-gen-grpc-java),此处我选择的是1.68.0版本(protoc-gen-grpc-java-1.68.0-windows-x86_64.exe): 

https://repo1.maven.org/maven2/io/grpc/protoc-gen-grpc-java/

使用PowerShell切换到proto的bin路径下,使用下述命令生成model类: 

.\protoc.exe --proto_path=E:\Code\Java\demo\src\main\resources\proto --java_out=E:\Code\Java\demo\src\main\java message.proto
.\protoc.exe --proto_path=E:\Code\Java\demo\src\main\resources\proto --java_out=E:\Code\Java\demo\src\main\java file.proto

接着再利用插件生成其对应的Service类

.\protoc.exe --plugin=protoc-gen-grpc-java=E:\Protoc\bin\protoc-gen-grpc-java-1.68.0-windows-x86_64.exe --proto_path=E:\Code\Java\demo\src\main\resources\proto --grpc-java_out=E:\Code\Java\demo\src\main\java message.proto

执行上述三个命令后,可以看到大量的类文件(server和client端都要生成):

  

三、语法介绍

proto3语法的官方文档如下所示:

https://developers.google.com/protocol-buffers/docs/proto3

3.1 方法声明

grpc使用下述关键字来描述一个grpc服务: 

关键字说明
service申明定义的是一个grpc的Service
rpc申明这一行定义的是服务下的一个远程调用方法
returns声明本行定义的rpc的返回值形式
stream声明这个数据是个流数据

3.2 枚举

使用 enum 关键字来定义数组,student.proto 文件的内容如下: 

syntax = "proto3";
package protocol;option java_multiple_files = true;
option java_package = "com.example.demo.protos";message Student {int32 id = 1;string name = 2;int32 age = 3;Sex sex = 4;
}enum Sex {NONE = 0;MAN = 1;WOMAN = 2;
}

Sex的第一个枚举值(NONE)必须为0,因为0是默认值,保持和proto2的语法兼容。

import com.example.demo.protos.Sex;
import com.example.demo.protos.Student;
import com.google.protobuf.TextFormat;public class Demo {public static void main(String[] args) {Student student =Student.newBuilder().setId(1).setName("张三").setAge(25).setSex(Sex.MAN).build();String result = TextFormat.printer().escapingNonAscii(false).printToString(student);System.out.println(result);}
}

执行上述代码,其输出结果如下: 

id: 1
name: "张三"
age: 25
sex: MAN

3.3 数组

使用 repeated 关键字来定义数组,student.proto 文件的内容如下: 

syntax = "proto3";
package protocol;option java_multiple_files = true;
option java_package = "com.example.demo.protos";message Student {int32 id = 1;string name = 2;repeated string cellPhones = 3;
}
import com.example.demo.protos.Student;
import com.google.protobuf.TextFormat;public class Demo {public static void main(String[] args) {Student student =Student.newBuilder().setId(1).setName("张三").addCellPhones("10086").addCellPhones("10010").build();String result = TextFormat.printer().escapingNonAscii(false).printToString(student);System.out.println(result);}
}

执行上述代码,其输出结果如下:  

id: 1
name: "张三"
cellPhones: "10086"
cellPhones: "10010"

3.4 map类型 

使用 map 关键字来定义集合,student.proto 文件的内容如下: 

syntax = "proto3";package protocol;option java_multiple_files = true;option java_package = "com.example.demo.protos";message Student {int32 id = 1;string name = 2;map<string,string> otherMap = 3;}
import com.example.demo.protos.Student;
import com.google.protobuf.TextFormat;public class Demo {public static void main(String[] args) {Student student =Student.newBuilder().setId(1).setName("张三").putOtherMap("address","北京市海淀区中央电视台").putOtherMap("phone","10086").build();String result = TextFormat.printer().escapingNonAscii(false).printToString(student);System.out.println(result);}
}

执行上述代码,其输出结果如下:  

id: 1
name: "张三"
otherMap {key: "address"value: "北京市海淀区中央电视台"
}
otherMap {key: "phone"value: "10086"
}

注意:map字段前面不能是repeated

3.5 嵌套对象

以下为嵌套对象的定义示例,student.proto 文件的内容如下: 

syntax = "proto3";
package protocol;option java_multiple_files = true;
option java_package = "com.example.demo.protos";message Student {int32 id = 1;string name = 2;OtherMsg otherMsg = 3;// 嵌套对象message OtherMsg {string ext1 = 1;string ext2 = 2;}
}
import com.example.demo.protos.Student;
import com.google.protobuf.TextFormat;public class Demo {public static void main(String[] args) {Student.OtherMsg otherMsg = Student.OtherMsg.newBuilder().setExt1("扩展信息1").setExt2("扩展信息2").build();Student student = Student.newBuilder().setId(1).setName("张三").setOtherMsg(otherMsg).build();String result = TextFormat.printer().escapingNonAscii(false).printToString(student);System.out.println(result);}
}

执行上述代码,其输出结果如下:   

id: 1
name: "张三"
otherMsg {ext1: "扩展信息1"ext2: "扩展信息2"
}

3.6 oneof

oneof 是 Protocol Buffers (Proto) 语言中的一个关键特性,它允许你在定义数据结构时,在同一个消息中定义一组字段,但是每次只能设置其中的一个字段。这意味着如果你在一个 oneof 组内设置了多个字段,最后设置的字段会覆盖之前设置的字段值。oneof 的这种特性使得数据结构更加灵活,同时也可以用来节省存储空间,因为只有一个字段会被存储。

oneof 的使用场景包括但不限于:

  • 可选字段:当一个消息中的多个字段是互斥的,即在任何给定时间只有一个字段会被设置。
  • 节省空间:在存储或传输时,只有被设置的字段占用空间,这对于资源受限的环境非常有用。
  • 类型安全的联合:oneof 可以看作是一种类型安全的联合体(union),确保了类型的正确性和使用的安全性。

以下为oneof语法的使用示例,student.proto 文件的内容如下: 

syntax = "proto3";
package protocol;option java_multiple_files = true;
option java_package = "com.example.demo.protos";message Student {int32 id = 1;oneof test_oneof{string name =2;string nickname = 3;}
}
import com.example.demo.protos.Student;
import com.google.protobuf.TextFormat;public class Demo {public static void main(String[] args) {Student student = Student.newBuilder().setId(1).setName("张三").setNickname("法外狂徒").build();String result = TextFormat.printer().escapingNonAscii(false).printToString(student);System.out.println(result);}
}

执行上述代码,其输出结果如下:   

id: 1
nickname: "法外狂徒"

在上面的例子中,test_oneof 是一个 oneof 组,它包含了两个字段:name和nickName。在任何给定的时间,Student只能包含这两个字段中的一个。

注意事项

  • 当解析一个包含 oneof 字段的消息时,如果有多个 oneof 组内的字段被设置,则只有最后一个被设置的字段会被保留。 
  • 在使用 oneof 时,虽然可以提高数据结构的灵活性和存储效率,但也要注意正确处理逻辑,确保数据的一致性和完整性。

3.7 reserved

reserved 关键字用于保留字段编号和字段名,以确保这些编号和名称在未来的版本更新中不会被重新使用。这主要用于向后兼容性,防止在移除字段后,该字段的编号被新添加的字段使用,从而导致数据解析错误。

在.proto文件中,可以使用reserved来保留字段编号和字段名,例如:

  • 保留字段编号:reserved 3, 5 to 6; 
  • 保留字段名:reserved "sex","address";
syntax = "proto3";
package protocol;option java_multiple_files = true;
option java_package = "com.example.demo.protos";message Student {int32 id = 1;string name = 2;reserved 3, 5 to 6;string nickname = 4;reserved "sex","address";
}

这些保留的字段编号和名称在后续的版本更新中不会被使用,从而保证了数据的兼容性‌。 

使用reserved关键字的主要目的是确保数据的向后兼容性。在开发过程中,如果需要移除某个字段,直接删除或注释掉该字段可能会导致问题,因为其它部分可能还在使用这个字段的编号或名称。 

四、服务端

4.1 引入依赖

特别说明当前Spring Boot的版本为2.1.3,gRPC服务端与Spring Boot整合时,需要引入下述依赖:

<dependency><groupId>net.devh</groupId><artifactId>grpc-server-spring-boot-starter</artifactId><version>2.15.0.RELEASE</version>
</dependency>

4.2 项目配置文件 

在resources目录下新建一个名为application.yml的文件,其配置信息如下所示: 

server:port: 8080
grpc:server:port: 9090

这里 grpc.server 表示是服务端的配置 ,此处服务端的端口为9090。

4.3 服务端业务

import com.example.demo.protos.User;
import com.example.demo.protos.UserServiceGrpc;
import com.example.demo.protos.UserType;
import io.grpc.stub.StreamObserver;
import net.devh.boot.grpc.server.service.GrpcService;@GrpcService
public class UserServiceImpl extends UserServiceGrpc.UserServiceImplBase {@Overridepublic void getUser(User request, StreamObserver<User> responseObserver) {User user;if (request.getUserTypeValue() == 1) {// 模拟根据条件从数据库查询数据user = User.newBuilder().setUserId(1).setName("张三").setCode(1).setUserType(UserType.ADMIN).build();} else {// 模拟根据条件从数据库查询数据user = User.newBuilder().setUserId(2).setName("李四").setCode(1).setUserType(UserType.BUSINESS_USER).build();}responseObserver.onNext(user);responseObserver.onCompleted();}@Overridepublic void getUsers(User request, StreamObserver<User> responseObserver) {// 模拟根据条件(request.getCode() == 1)从数据库查询数据User user1 = User.newBuilder().setUserId(1).setName("张三").setCode(1).setUserType(UserType.ADMIN).build();User user2 = User.newBuilder().setUserId(2).setName("李四").setCode(1).setUserType(UserType.BUSINESS_USER).build();User user3 = User.newBuilder().setUserId(3).setName("王五").setCode(1).setUserType(UserType.UNKNOW).build();User user4 = User.newBuilder().setUserId(4).setName("赵六").setCode(1).setUserType(UserType.ADMIN).build();responseObserver.onNext(user1);responseObserver.onNext(user2);responseObserver.onNext(user3);responseObserver.onNext(user4);responseObserver.onCompleted();}
}

上述UserServiceImpl.java中有几处需要注意:

  1. 使用@GrpcService注解,再继承UserServiceImplBase,这样就可以借助grpc-server-spring-boot-starter库将getUser暴露为gRPC服务
  2. UserServiceImplBase是前面根据proto自动生成的java代码,在grpc-lib模块中
  3. getUser方法中处理完毕业务逻辑后,调用responseObserver.onNext方法填入返回内容
  4. 调用responseObserver.onCompleted方法表示本次gRPC服务完成

五、客户端

5.1 引入依赖

特别说明当前Spring Boot的版本为2.1.3,gRPC客户端与Spring Boot整合时,需要引入下述依赖:

<dependency><groupId>net.devh</groupId><artifactId>grpc-client-spring-boot-starter</artifactId><version>2.15.0.RELEASE</version>
</dependency>

5.2 项目配置文件

在resources目录下新建一个名为application.yml的文件,其配置信息如下所示:

server:port: 8088spring:application:name: demo
grpc:client:userClient:negotiationType: PLAINTEXTaddress: static://localhost:9090    

这里 grpc.client 表示是客户端的配置,userClient 具有特殊的含义,可以理解为gRPC调用服务端的一组配置项,可任意取名。negotiationType 表示的是文本传输配置,此处值为PLAINTEXT(文本传输)。address 表示的是gRPC服务端的地址和端口配置。

5.3 客户端测试

此处 @GrpcClient 注解中的属性值为userClient,表示的是UserServiceGrpc.UserServiceBlockingStub采用userClient配置项调用服务端,这也就和前面的yml文件中的配置形成了呼应。 

import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONObject;
import com.example.demo.protos.User;
import com.example.demo.protos.UserServiceGrpc;
import net.devh.boot.grpc.client.inject.GrpcClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;import java.util.Iterator;@RestController
@RequestMapping("/grpc")
public class GrpcClientController {@GrpcClient("userClient")private UserServiceGrpc.UserServiceBlockingStub userService;@GetMapping("/getUser")public String getUser()     {User ro = User.newBuilder().setUserTypeValue(1).build();User user = userService.getUser(ro);JSONObject jsonObject = new JSONObject(4,true);jsonObject.putOpt("userId",user.getUserId());jsonObject.putOpt("name", user.getName());jsonObject.putOpt("code",user.getCode());jsonObject.putOpt("userType",user.getUserType().getNumber());return jsonObject.toString();}@GetMapping("/getUsers")public String getUsers()     {User ro = User.newBuilder().setCode(1).build();Iterator<User> iterator = userService.getUsers(ro);JSONArray jsonArray =new JSONArray();while (iterator.hasNext()){User user =iterator.next();JSONObject jsonObject = new JSONObject(4,true);jsonObject.putOpt("userId",user.getUserId());jsonObject.putOpt("name", user.getName());jsonObject.putOpt("code",user.getCode());jsonObject.putOpt("userType",user.getUserType().getNumber());jsonArray.add(jsonObject);}return jsonArray.toString();}
}

上述GrpcClientController类有几处要注意的地方:

  1. 用@GrpcClient修饰UserServiceBlockingStub,这样就可以通过grpc-client-spring-boot-starter库发起gRPC调用,被调用的服务端信息来自名为userClient的配置
  2. UserServiceBlockingStub来自前面根据proto文件生成的java代码
  3. UserServiceBlockingStub.getUser方法会远程调用userClient应用的gRPC服务 

调用 /grpc/getUser 接口,其返回结果如下所示:

调用 /grpc/getUsers 接口,其返回结果如下所示:

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/58985.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

代理IPv6知识分享课堂二

嗨朋友们&#xff0c;欢迎来到今天的代理IPv6知识分享课堂&#xff0c;小蝌蚪上堂课跟大家一起认识了它的概念和工作原理等基础内容&#xff0c;我们算是对它有了一个初步的了解&#xff0c;那今天这节课我们讲的会深入点&#xff0c;我们今天来了解了解它的应用场景和切实地教…

恋爱脑学Rust之dyn关键字的作用

在 Rust 语言中&#xff0c;dyn 关键字允许我们在使用特征时创建“动态派发”——即通过一个统一的接口操作多种类型的具体实现。可以把它理解成一种“浪漫的妥协”&#xff1a;当我们不知道未来会爱上谁&#xff0c;只知道对方一定具有某种特征时&#xff0c;dyn 就像一个协议…

android浏览器源码 可输入地址或关键词搜索 android studio 2024 可开发可改地址

Android 浏览器是一种运行在Android操作系统上的应用程序&#xff0c;主要用于访问和查看互联网内容。以下是关于Android浏览器的详细介绍&#xff1a; 1. 基本功能 Android浏览器提供了用户浏览网页的基本功能&#xff0c;如&#xff1a; 网页加载&#xff1a;支持加载静态…

Sketch下载安装,中文版在线免费用!

Sketch是一款轻便、高效的矢量设计工具&#xff0c;全球众多设计师借助它创造出了无数令人惊叹的作品。Sketch在下载安装方面&#xff0c;其矢量编辑、控件以及样式等功能颇具优势&#xff0c;不过&#xff0c;Sketch中文版即时设计在下载安装方面也毫不逊色。即时设计是一个一…

Golang | Leetcode Golang题解之第526题优美的排列

题目&#xff1a; 题解&#xff1a; func countArrangement(n int) int {f : make([]int, 1<<n)f[0] 1for mask : 1; mask < 1<<n; mask {num : bits.OnesCount(uint(mask))for i : 0; i < n; i {if mask>>i&1 > 0 && (num%(i1) 0 |…

8进制在线编码工具--实现8进制编码

具体前往&#xff1a;文本转八进制在线工具-将文本字符串转换为8进制编码,支持逗号&#xff0c;空格和反斜杠分隔符

基于hive分析Flask为后端框架echarts为前端框架的招聘网站可视化大屏项目

基于hive分析Flask为后端框架echarts为前端框架的招聘网站可视化大屏项目 1. 项目概述 项目目标是构建一个大数据分析系统&#xff0c;包含以下核心模块&#xff1a; 1、数据爬取&#xff1a;通过request请求获取猎聘网的就业数据。 2、数据存储和分析&#xff1a;使用 Hive …

SpringBoot【实用篇】- 配置高级

文章目录 目标&#xff1a;1.ConfigurationProperties2.宽松绑定/松散绑定3. 常用计量单位绑定4.数据校验 目标&#xff1a; ConfigurationProperties宽松绑定/松散绑定常用计量单位绑定数据校验 1.ConfigurationProperties ConfigurationProperties 在学习yml的时候我们了解…

QT 机器视觉 (3. 虚拟相机SDK、测试工具)

本专栏从实际需求场景出发详细还原、分别介绍大型工业化场景、专业实验室场景、自动化生产线场景、各种视觉检测物体场景介绍本专栏应用场景 更适合涉及到视觉相关工作者、包括但不限于一线操作人员、现场实施人员、项目相关维护人员&#xff0c;希望了解2D、3D相机视觉相关操作…

数字ic/FPGA,Windows/Linux系统,其他相关领域,软件安装包(matlab、vivado、modelsim。。。)

目录 一、总述 二、软件列表 1、modelsim_10.6c 2、notepad 3、matlab 4、Visio-Pro-2016 5、Vivado2018 6、VMware15 7、EndNote X9.3.1 8、Quartus 9、pycharm 10、CentOS7-64bit 一、总述 过往发了很多数字ic设计领域相关的内容&#xff0c;反响也很好。 最近…

【5.5】指针算法-三指针解决颜色分类

一、题目 给定一个包含红色、白色和蓝色&#xff0c;一共n个元素的数组&#xff0c;原地对它们进行排序&#xff0c;使得相同颜色的元素相邻&#xff0c;并按照红色、白色、蓝色顺序排列。 此题中&#xff0c;我们使用整数0、1和2分别表示红色、白色和蓝色。 示例 1&#xff1…

刘艳兵-DBA023-控制文件是Oracle 数据库用来查找数据库文件,控制文件包含以下哪些信息:

控制文件是Oracle 数据库用来查找数据库文件&#xff0c;控制文件包含以下哪些信息&#xff1a; A 表空间信息 B 创建数据库的时间戳 C 有关数据文件、 联机重做日志文件、和归档重做日志文件的信息 D 数据库名称和数据库唯一标识符&#xff08;DBID) E RMAN备份…

3. 探索 Netty 的粘包与拆包解决方案

序言 在网络编程中&#xff0c;粘包和拆包现象常常是开发者在数据传输时遇到的棘手问题。如果消息在传输过程中没有正确的分包和组合&#xff0c;接收方可能会收到一组拼接在一起的数据&#xff08;粘包&#xff09;&#xff0c;或者一条消息被拆分成了不完整的部分&#xff0…

由浅入深逐步理解spring boot中如何实现websocket

实现websocket的方式 1.springboot中有两种方式实现websocket&#xff0c;一种是基于原生的基于注解的websocket&#xff0c;另一种是基于spring封装后的WebSocketHandler 基于原生注解实现websocket 1&#xff09;先引入websocket的starter坐标 <dependency><grou…

电信诈骗升级到了 FaceTime

最近&#xff0c;网上有消息称一些不法分子正在通过FaceTime来冒充微信、京东等平台的客服&#xff0c;骗取用户转移账号内的资金&#xff0c;或是申请贷款。 虽然从具体的诈骗方式来说还是老一套&#xff0c;但是却更加防不胜防&#xff0c;而且欺诈性更强&#xff0c;特别是…

‌5G SSB(同步信号块)位于物理层‌

‌5G SSB&#xff08;同步信号块&#xff09;位于物理层‌。在5G NR中&#xff0c;SSB由主同步信号&#xff08;PSS&#xff09;、辅同步信号&#xff08;SSS&#xff09;和物理广播信道&#xff08;PBCH&#xff09;组成&#xff0c;这些信号共同构成了SSB。SSB的主要功能是帮…

基于MATLAB驾驶行为的疲劳实时检测研究

[摘要]为了有效地检测出驾驶员的驾驶疲劳&#xff0c;本文设计了一种基于驾驶行为的实时疲劳驾驶检测模型。该算法选取与疲劳相关的驾驶行为指标&#xff0c;包括速度均值与标准差&#xff0c;加速度绝对均值与标准差&#xff0c;综合考虑报警设备的报警频率与驾驶时长来划分疲…

演练纪实丨 同创永益圆满完成10月份灾备切换演练支持

2024年10月&#xff0c;同创永益共支持5家客户圆满完成灾备切换演练&#xff0c;共涉及70多套核心系统总切换与回切步骤6000余个&#xff0c;成功率100%&#xff0c;RTO时长均达到客户要求。 其中耗时最短的一次演练仅花费约3个小时&#xff0c;共涉及32套系统的灾备切换演练&a…

Logback 常用配置详解

1. 配置文件解析 Logback 是 Spring Boot 默认使用的日志框架&#xff0c;Logback 配置主要包含 8 大元素 1.1 configuration Logback 配置文件的根元素&#xff0c;它包含所有的配置信息 1.2 appender 定义一个 Appender&#xff0c;即日志输出的目的地&#xff0c;如控制…

【AI日记】24.11.01 LangChain、openai api和github copilot

【AI论文解读】【AI知识点】【AI小项目】【AI战略思考】【AI日记】 工作 工作1 内容&#xff1a;学习deeplearning.ai的免费课程地址&#xff1a;LangChain Chat with Your DataB站地址&#xff1a;https://www.bilibili.com/video/BV148411D7d2时间&#xff1a;2小时评估&am…