前言
初次接触gRPC 集成SpringBoot 在学习的过程中发现网上资料 零散 繁杂 片面
故总结此文 当做索引给后人节省时间
(文中的超链都是给大家点击查看的)
入篇之前需要了解以下名词
名词解释
- gRPC:一个高性能、开源的通用RPC框架,它可以使用Protobuf定义服务
- Protobuf:协议缓冲区是一种与语言无关、与平台无关的可扩展机制,用于序列化结构化的数据(参考JSON)
- proto3:proto是一种语言规范,Protobuf就遵循这种语言规范,目前最高版本是proto3
gRPC 介绍
一句话介绍:
gRPC是一款基于HTTP/2设计,支持双向流,多路复用的接口定义语言RPC框架
-
gRPC官网
-
gRPC-git-java
(介绍里面有java的集成方法) -
gRPC官方文档(中文版)
-
GRPC简介–知乎
接入依赖
当前gRPC接入SpringBoot有两种方式
一种是git大神为SpringBoot封装的开源包 接入方便 配置方便 但是是个人维护更新没有保障
一种是官方推荐的java通用接入方法 配置相比上面方法复杂 但是灵活度高
git开源包接入
这里不再赘述 有大神的详细教程 甚至还有源码分析
gRPC与SpringBoot集成
官方推荐接入
在官方git下面有介绍(仅看Download部分就好,下面会介绍Generated Code部分)
说明:
- 注意官方的注释org.apache.tomcat 是 necessary for Java 9+ 如果使用1.8不用引入
-
调试阶段建议增加grpc-services包 开启反射 方便调试 参考
gRPC 反射服务
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-services</artifactId>
<version>xxxx</version>
</dependency>
proto介绍
-
gRPC使用
接口定义语言
(interface definition language,IDL) protobuf -
protobuf文件的后缀为
.proto
- proto有proto2.0和proto3.0两个版本 已经2022年了我们本文里介绍使用的3.0版本
语法介绍
protocol官方文档
protocol官方文档(中文版)
(找了几个,这个文档效果最好)
定义比较好理解 主要是any和oneof使用时需要留意
服务定义
服务定义分为四种传输模式
gRPC官方文档-defining-the-service
(可以参考上面的 gRPC官方文档(中文版) 一样的 但是中文文档没法复制指向链接)
- 一元服务(unary)
- 客户端流(client streaming)
- 服务端流(server streaming)
- 双向流(bidirectional streaming)
官方文档中有每种定义的实现方式
gRPC官方文档-implementing-routeguide
不想自己研究的话 找了一个提供demo的实现教程
gRPC-Java(二):一个Demo熟悉gRPC的四种模式
(demo在文章最下面)
自动生成.proto文件
手写proto文件 结构简单的时候还可以 遇到复杂的结构写结构就要写一天 整理了几个生成proto的方式
-
使用java实体类在线生成
根据实体类生成proto文件 但是会给带上一些个方法 需要自己再做调整 -
使用json在线生成
生成的好像是proto2的格式 也需要根据自己需求调整 -
本地代码生成
自己写的硬编码 使用反射读取实体类下面的字段 生成proto格式
当前代码仅简单映射 对于嵌套格式类和list map等不支持 可以自己改造(欢迎改造后贴到评论区)
public class Java2Proto {
public static void main(String[] args) {
java2proto(XXX.class);
}
public static void java2proto(Class aClass){
StringBuilder b = new StringBuilder();
Field[] fields = aClass.getDeclaredFields();
b.append("message ").append("").append(aClass.getSimpleName()).append("{").append("\n");
for (int i = 1; i <= fields.length; i++) {
Field field = fields[i-1];
String name = field.getName();
String type = field.getType().getName();
b.append(" ");
switch (type){
case "java.lang.String":
b.append("string");
break;
case "java.lang.Double":
case "double":
b.append("double");
break;
case "java.lang.Float":
case "float":
b.append("float");
break;
case "java.lang.Long":
case "long":
b.append("int64");
break;
case "java.lang.Integer":
case "int":
b.append("int32");
break;
case "java.lang.Boolean":
case "boolean":
b.append("bool");
break;
default:
b.append(type);
}
b.append(" ").append(name).append(" ").append("=").append(" ").append(i).append(";");
b.append("\n");
}
b.append("}");
b.append("\n");
System.out.println(b);
}
}
代码生成插件
现在我们有了符合自己需求场景的proto文件 接下来要根据proto文件生成java的实例类和客户端
有maven 和 gradle两个版本 使用
官方git中protobuf提供的插件
可以使用命令行
编译说明
也可以使用idea提供的界面操作
生成的代码在target文件夹下 不需要拷贝到ser
如果出现
代码找不到protobuf生成文件的情况
确认下有没有把target/generated-sources/protobuf下面的包设置成sec
接入实现
接入步骤
接入方式的代码网上比较丰富 但是protobuf各个类型的java实现使用不是很清晰
我的代码主要为了提供一个纯净的grpc脚手架 以及各个类型的赋值和取值 就仅以一元请求为例
详细的说明我加在代码的注释里 代码不用复制粘贴 本节最后会附上git地址
目录结构
1.pom.xml依赖文件
<?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>
<groupId>org.example</groupId>
<artifactId>grpc-server</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<!--gRPC-->
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-netty</artifactId>
<version>1.45.1</version>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-protobuf</artifactId>
<version>1.45.1</version>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-stub</artifactId>
<version>1.45.1</version>
</dependency>
</dependencies>
<build>
<extensions>
<extension>
<groupId>kr.motd.maven</groupId>
<artifactId>os-maven-plugin</artifactId>
<version>1.6.2</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.19.2:exe:${os.detected.classifier}</protocArtifact>
<pluginId>grpc-java</pluginId>
<pluginArtifact>io.grpc:protoc-gen-grpc-java:1.45.1:exe:${os.detected.classifier}</pluginArtifact>
</configuration>
<executions>
<execution>
<goals>
<goal>compile</goal>
<goal>compile-custom</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
2.准备proto文件
proto文件中包含了全部支持的类型
syntax = "proto3";
import "google/protobuf/any.proto";
option java_multiple_files = true;
option java_package = "com.demo.grpc.api";
option java_outer_classname = "TestDemo";
package com.demo.grpc.api;
// 定义服务类型:服务接口.定义请求参数和相应结果
service DemoService {
rpc test (DemoRequest) returns (DemoResponse);
}
// 请求体
message DemoRequest {
int32 testInt = 1;
int64 testLong = 2;
string testString = 3;
repeated string testArray = 4;Ω
map<string, string> testMap = 5;
google.protobuf.Any testAny = 6;
message TestNested {
int32 a = 1;
string b = 2;
}
TestNested testNested = 7;
}
// 响应体
message DemoResponse {
oneof testOneof {
int32 code = 1;
string message = 2;
};
}
3.实现server端方法体
继承并实现生成在
target/generated-sources/protobuf
下面的类
package com.demo.grpc;
import com.demo.grpc.api.DemoRequest;
import com.demo.grpc.api.DemoResponse;
import com.demo.grpc.api.DemoServiceGrpc;
import io.grpc.stub.StreamObserver;
import java.util.Random;
//实现生成文件里的ImplBase 并且在类里重写proto中定义的方法
public class MyDemoServiceImpl extends DemoServiceGrpc.DemoServiceImplBase {
@Override
public void test(DemoRequest request, StreamObserver<DemoResponse> responseObserver) {
try {
//打印请求
System.out.println("收到请求:");
System.out.println(request.getTestInt());
System.out.println(request.getTestLong());
System.out.println(request.getTestString());
System.out.println("list size is :" + request.getTestArrayList().size());
System.out.println("map size is :" + request.getTestMapMap().size());
System.out.println("nested.a is : " + request.getTestNested().getA());
System.out.println("nested.b is : " + request.getTestNested().getB());
System.out.println("any is : " + request.getTestAny().unpack(DemoResponse.class).getMessage());
//构建响应
DemoResponse.Builder resBuilder = DemoResponse.newBuilder();
//随机测试oneof
if(new Random().nextBoolean()){
resBuilder.setCode(200);
}else{
resBuilder.setMessage("success");
}
//发送数据
responseObserver.onNext(resBuilder.build());
//因为是🏥请求 结束链接
responseObserver.onCompleted();
} catch (Exception e) {
System.out.println("server 方法体出现错误" + e.getMessage());
//发送错误,onError自动断开
responseObserver.onError(e);
}
}
}
4.gRPC服务启动代码
ServerBuilder下面还有一些参数设置方法 可以详细了解下
package com.demo.grpc;
import io.grpc.Server;
import io.grpc.ServerBuilder;
import java.io.IOException;
public class GrpcServer {
public static void main(String[] args) {
Server server=null;
try {
//这里面还可以加入拦截器,过滤器,超时等
server = ServerBuilder.forPort(6666)
.addService(new MyDemoServiceImpl())
.build().start();
//持续等待消息
server.awaitTermination();
} catch (Exception e) {
System.out.println("服务端出错"+e);
} finally {
if (server!=null) {
server.shutdown();
}
}
}
}
5.gRPC客户端请求代码
package com.demo.grpc;
import com.demo.grpc.api.DemoRequest;
import com.demo.grpc.api.DemoResponse;
import com.demo.grpc.api.DemoServiceGrpc;
import com.google.protobuf.Any;
import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;
import java.util.*;
public class GrpcClient {
public static void main(String[] args) {
//构建链接信息(并没有真正连接)
ManagedChannel channel = ManagedChannelBuilder.forAddress("localhost",6666)
.usePlaintext().build();
try {
List<String> testArray = Arrays.asList(new String[]{"item0","item1","item2","item3",});
Map<String,String> testMap = new HashMap<String, String>(){
{
put("testA","A");
put("testB","B");
}
};
//为了测试 any 不是真正的返回结构体
DemoResponse testAny = DemoResponse.newBuilder().setMessage("for any").build();
//构建请求信息
DemoRequest request = DemoRequest.newBuilder()
.setTestInt(1)
.setTestLong(2L)
.setTestString("string")
//添加repeated要用addList
.addAllTestArray(testArray)
//map可以直接放入Map<>
.putAllTestMap(testMap)
//也可以放入KV对
.putTestMap("testC","C")
//any可以是proto中定义的任意结构体
.setTestAny(Any.pack(testAny))
.build();
//真正创建连接
DemoServiceGrpc.DemoServiceBlockingStub stub = DemoServiceGrpc.newBlockingStub(channel);
//传入请求 接收返回
DemoResponse demoResponse = stub.test(request);
System.out.println("res code is : " + demoResponse.getCode() + ", message is : " + demoResponse.getMessage());
}finally {
channel.shutdown();
}
}
}
测试结果
服务端结果
客户端结果测试oneof 两种结果
demo源码
接入测试
gRPCurl
gRPCurl
类似curl 但是是Go语言的
Postman
大家常用的接口调试工具 Postman 也支持gRPC协议
PostmangRPC功能使用介绍
补充
nginx
nginx 代理gRPC需要特殊配置
Nginx配置gRPC Nginx配置代理gRPC的方法
错误信息
在调试/业务流程中 通常客户端需要拿到服务端提供的错误信息 gRPC也提供了对应的能力
在本人开发过程中 这一部分花费了很多资料查找的时间 特补充一下
以流式请求举例 列举三种可行方案 简单举例
1.在onError时 用Status的withDescription
responseObserver.onError(Status.INVALID_ARGUMENT.withDescription("错误信息").asRuntimeException());
适用于简单的文字情况
2.在onError时 在StatusRuntimeException加入Metadata
Metadata metadata = new Metadata();
metadata.put(Metadata.Key.of("xxx-bin",Metadata.BINARY_BYTE_MARSHALLER),"错误信息".getBytes(StandardCharsets.UTF_8));
//传统写法 两种都可以
//responseObserver.onError(new StatusRuntimeException(Status.INVALID_ARGUMENT, metadata));
responseObserver.onError(Status.INVALID_ARGUMENT.asRuntimeException(metadata));
这种方法不仅可以放入文字 还可以放入对象等丰富结构
- 需要注意的是我在开发的过程中发现 key的后缀”-bin”是必须加上的
ps.1和2可以同时使用
responseObserver.onError(Status.INVALID_ARGUMENT.withDescription("错误信息").asRuntimeException(metadata));
参考
gRPC异常处理流程设计
3.自定义
在proto中自定义error结构 不适用onError而用onNext下发error结构
这样做需要同时在服务端和客户端做错误情况的处理 不推荐
4.无需prorto文件 注解方式自动加载
为了服务端和客户端proto文件统一版本管理 在寻找通过url加载proto文件的过程中发现了用java实体类和注解自动加载的方式 省去了proto文件 可以打成公共jar包供双端引用 但是私人开源代码 请酌情使用
git链接 :
jprotobuf
作者在
介绍博客
中强调了
这个开源工具对集合没有适合的注解,而且也没有说明和其他语言传输的时候,如何保证一致性,所以需要使用的人考虑清楚,需要慎用。
但是在
项目中的说明文档
并没有提及 我没有验证 欢迎评论区补充