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相机视觉相关操作…

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

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

由浅入深逐步理解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;特别是…

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…

Android App 技能在DuerOS的调试方法

温故知新&#xff0c;我们先回顾一下DuerOS的技能分类。根据不同的视角可以对DuerOS 目前支持的技能类型进行不同的分类&#xff0c;例如&#xff0c;从用户与技能的语音交互方式来看&#xff0c; 可以将技能分为这四种技能类型: L1技能&#xff1a;只支持语音的打开和关闭L2技…

Unity 2D寻路导航 NavMeshPlus解决方案

插件的github主页 h8man/NavMeshPlus: Unity NavMesh 2D Pathfinding 这个插件是基于新版3D寻路导航制作的&#xff0c;所以你可能需要看一下这篇文章 新旧Navmash 寻路导航组件对比 附使用案例与实用教程链接-CSDN博客 这行代码agent.updateUpAxis false 一定要为代理单位…

客户端与微服务之间的桥梁---网关

当我们创建好了N多个微服务或者微服务的实例之后&#xff0c;每个服务暴露出不同的端口地址&#xff0c;一般对于客户端请求&#xff0c;只需要请求一个端口&#xff0c;要隔离客户端和微服务的直接关系&#xff0c;保证微服务的安全性和灵活性&#xff0c;避免敏感信息的泄露。…

@Excel若依导出异常/解决BusinessBaseEntity里面的字段不支持导出

今天发现所有实体类继承BusinessBaseEntity里面的这些通用字段不支持导出&#xff0c;debug时发现是这样&#xff1a; 导出效果 这里我把能查到的方法都汇总了&#xff0c;如果你也遇到这个异常&#xff0c;可以去逐步排查 1.先看库里有没有数据 2.看字段名是否对齐 3.所需要…

Flink系列之:学习理解通过状态快照实现容错

Flink系列之&#xff1a;学习理解通过状态快照实现容错 状态后端检查点存储状态快照状态快照如何工作&#xff1f;确保精确一次&#xff08;exactly once&#xff09;端到端精确一次 状态后端 由 Flink 管理的 keyed state 是一种分片的键/值存储&#xff0c;每个 keyed state…

大数据之文件服务器方案

大数据文件服务器方案 一&#xff0c;文件服务器常用框架 二&#xff0c;文件服务器常用框架的实现技术 文件服务器常用框架 文件服务器是一种专门用于存储、管理和共享文件的服务器&#xff0c;其常用框架的实现技术涉及多个方面&#xff0c;以下是一些主要的实现技术及其详…