Java(SpringBoot接入)实现gPRC看这一篇就够了

  • Post author:
  • Post category:java




前言

初次接触gRPC 集成SpringBoot 在学习的过程中发现网上资料 零散 繁杂 片面

故总结此文 当做索引给后人节省时间

(文中的超链都是给大家点击查看的)

入篇之前需要了解以下名词



名词解释

  • gRPC:一个高性能、开源的通用RPC框架,它可以使用Protobuf定义服务
  • Protobuf:协议缓冲区是一种与语言无关、与平台无关的可扩展机制,用于序列化结构化的数据(参考JSON)
  • proto3:proto是一种语言规范,Protobuf就遵循这种语言规范,目前最高版本是proto3



gRPC 介绍

一句话介绍:

gRPC是一款基于HTTP/2设计,支持双向流,多路复用的接口定义语言RPC框架



接入依赖

当前gRPC接入SpringBoot有两种方式

一种是git大神为SpringBoot封装的开源包 接入方便 配置方便 但是是个人维护更新没有保障

一种是官方推荐的java通用接入方法 配置相比上面方法复杂 但是灵活度高



git开源包接入

这里不再赘述 有大神的详细教程 甚至还有源码分析


gRPC与SpringBoot集成



官方推荐接入

在官方git下面有介绍(仅看Download部分就好,下面会介绍Generated Code部分)


gRPC官方提供的接入方法

说明:

  • 注意官方的注释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源码


源码git地址



接入测试



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


作者在

介绍博客

中强调了


这个开源工具对集合没有适合的注解,而且也没有说明和其他语言传输的时候,如何保证一致性,所以需要使用的人考虑清楚,需要慎用。


但是在

项目中的说明文档

并没有提及 我没有验证 欢迎评论区补充



版权声明:本文为weixin_37697242原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。