Spring Boot与gRPC的整合

news/2024/11/3 3:02:33 标签: spring boot

一、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 {

    @Override
    public 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();
    }

    @Override
    public 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: 8088
  
spring:
  application:
    name: demo
grpc:
  client:
    userClient:
      negotiationType: PLAINTEXT
      address: 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.niftyadmin.cn/n/5735917.html

相关文章

因为Flock,Flutter又凉一次

哈喽&#xff0c;我是老刘 本来不想写这篇文章的&#xff0c;因为有人已经讲过了&#xff0c;但是问的人有点多&#xff0c;就还是写一下吧。 我使用Flutter开发App已经6年多了&#xff0c;刚开始的时候Flutter流行度还不高&#xff0c;很多人还不知道&#xff0c;也不会经常…

Learn QOpenGL 读取obj模型

/* ** File name: OpenGLModelWidget.h ** Author: ** Date: 2024-10-31 ** Brief: 读取模型文件并渲染的OpenGL控件 ** Copyright (C) 1392019713@qq.com All rights reserved. */#ifndef OpenGLModelWidget_H #define OpenGLModelWidget_H#includ…

项目模块十二:TcpServer模块

一、模块设计思路 1、目的 对所有模块整合&#xff0c;实现一个服务器模块供外部快速搭建服务器。 2、管理 监听套接字 主 Reactor&#xff0c;创建 EventLoop _baseloop 对象&#xff0c;进行对监听套接字的管理 哈希表管理所有新连接的 Channel 创建线程池进行连接的事…

掌握ElasticSearch(七):相关性评分

文章目录 一、Elasticsearch的打分机制1.TF-IDFTF-IDF 概述基本公式示例 2.BM25BM25 概述配置参数 二、boosting调整打分1. match 查询2. multi_match 查询3. bool 查询4. boosting 查询5.动态 Boosting 三、Elasticsearch的查询再打分策略查询阶段&#xff08;Query Phase&…

iOS用rime且导入自制输入方案

iPhone 16 的 cantonese 只能打传统汉字&#xff0c;没有繁简转换&#xff0c;m d sh d。考虑用「仓」输入法 [1] 使用 Rime 打字&#xff0c;且希望导入自制方案 [2]。 仓输入法有几种导入方案的方法&#xff0c;见 [3]&#xff0c;此处记录 wifi 上传法。准备工作&#xff1…

七、MapReduce 编程模型:原理、流程与应用场景

MapReduce 编程模型&#xff1a;原理、流程与应用场景 在当今大数据时代&#xff0c;MapReduce 编程模型作为一种强大的分布式计算框架&#xff0c;对于处理海量数据具有至关重要的作用。它以其简洁而高效的设计理念&#xff0c;在众多领域得到了广泛的应用。本文将深入探讨 M…

《使用Gin框架构建分布式应用》阅读笔记:p307-p392

《用Gin框架构建分布式应用》学习第16天&#xff0c;p307-p392总结&#xff0c;总86页。 一、技术总结 1.AWS chapter 08讲使用AWS进行部署&#xff0c;可以根据需要选择是否阅读。因为使用到的概率很小&#xff0c;且还要绑卡&#xff0c;本人选择跳过。 2.CI/CD (1)什么…

微服务基础拆分实践(第一篇)

目录 前言 一、认识微服务 1.1 单体架构 VS 微服务架构 1.2 微服务的集大成者&#xff1a;SpringCloud 1.3 微服务拆分原则 1.4 微服务拆分方式 二、微服务拆分入门步骤 &#xff1a;以拆分商品模块为例 三、服务注册订阅与远程调用&#xff1a;以拆分购物车为例 3.1 …