Spring Boot-集成gRPC提供开放接口

gRPC 是谷歌的一个开源 RPC 框架, 因为最近要做一个跨语言平台的交互,所以选用了 gRPC 这个框架,下面记录一下从零开始跟 Spring Boot 集成的过程.

proto文件编写

使用 gRPC 第一步,要先编写一个 .proto 文件,这个文件的作用是定义接口的一些入参返回值等等,比如下面这个 HelloWorld.proto 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
syntax = "proto3";

option java_multiple_files = true;

message HelloRequest{
string txt = 1;
}

message HelloResponse{
int32 status = 1;
string msg = 2;
string data = 3;
}

service HelloService{
// 方法名(参数) 返回值(返回类型)
rpc sayHello(HelloRequest) returns (HelloResponse);
}

syntax

第一行 syntax = "proto3"; 这个是定义这个 proto 文件的版本, 如果学过 docker 的话就不难理解,我们编写 dockerfile 的时候也要定义一下 version 版本号, 这里要注意的是,这个版本要跟你引入的jar包的版本匹配,不然编译之后会有很多包找不到

option

选项, 这个相当于一个配置, 比如上面文件中的 option java_multiple_files = true; 这个配置表示:指定在 proto 文件中定义的所有消息,枚举和服务在生成java类的时候都会生成对应的java类文件,而不是以内部类的形式出现.

到这儿可能有点懵,先来了解一下这个 proto 文件使用的整体流程:

  1. 编写 proto 文件,定义方法名,入参出参等基本信息
  2. 通过工具编译 proto 文件,这步操作会生成一堆 java 类, 对应的就有我们上面定义的入参的model,出参的model, 以及构建入参出参的一些工具类等等
  3. 把生成的类复制到我们的项目中, 然后编写实现类, 实现类中就是我们具体的业务逻辑
  4. 启动服务端,提供给客户端访问

到这儿应该能理解 option java_multiple_files = true; 这个配置的作用了吧, 就是如果配置的是 false, 他会把所有需要的类都放到一个 java 类中, 以内部类的形式存在, 如果设置的是 true 的话, 那就会各生成各的.

option 这个设置还有别的项, Google 一下都有

message

message 这个是消息定义,可以理解为 Java 中的实体类, 声明实体类,然后定义类中的属性. 字段类型跟java中的基本类型差不多,稍微有点不一样,这个可以 Google 一下.

这里要注意一点,上面我们定义的字段 string txt = 1; 这个 1 并不是这个字段的初始值,而是给这个字段分配的标量, 每一个被定义在消息中的字段都会被分配给一个唯一的标量,这些标量用于标识你定义在二进制消息格式中的属性,标量一旦被定义就不允许在使用过程中再次被改变,标量的值在1~15的这个范围里占一个字节编码.

service

这个就相当于定义一个 Java 中的接口, 然后下面的 rpc 是定义具体的接口名以及入参出参, 这部分应该比较好理解

引入依赖

现在 Java 项目基本都是用 spring boot 了吧, 所以找一个开源的 starter 可以帮我们快速集成 grpc 需要用到的一些配置, 引入下面这个包

1
2
3
4
5
<dependency>
<groupId>io.github.lognet</groupId>
<artifactId>grpc-spring-boot-starter</artifactId>
<version>3.4.1</version>
</dependency>

这个项目的GitHub在这里

包导入之后,还需要加一个插件用来把 proto 文件编译成我们需要的 java 文件,插件导入如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<build>
<finalName>grpc-java</finalName>
<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.11.0:exe:${os.detected.classifier}</protocArtifact>
<pluginId>grpc-java</pluginId>
<pluginArtifact>io.grpc:protoc-gen-grpc-java:1.27.2:exe:${os.detected.classifier}</pluginArtifact>
</configuration>
<executions>
<execution>
<goals>
<goal>compile</goal>
<goal>compile-custom</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>

编译代码

上面配置完成之后,使用 mvn clean generate-sources 命令编译 proto 文件, 然后在如下文件夹中可以找到编译结果

image
这些类需要复制到我们的项目对应的包中

编写实现类

上面生成的只是一些接口,构建请求返回的工具类等等, 具体的业务逻辑我们还没有写, 新建一个类,继承 HelloServiceGrpc.HelloServiceImplBase , 继承的这个类是我们上面自动生成的, 找后缀是 Grpc 的, 然后内部类以 ImplBase 后缀结尾的这个

之后重写我们的业务方法, 具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
@GRpcService
public class HelloServiceGrpcImpl extends HelloServiceGrpc.HelloServiceImplBase {

@Override
public void sayHello(HelloRequest request, StreamObserver<HelloResponse> responseObserver) {
System.out.println(request.getTxt());
responseObserver.onNext(HelloResponse.newBuilder().setStatus(200).setMsg("返回消息").build());
responseObserver.onCompleted();
}

}

这里入参出参就都可以拿到了,这个方法里可以写具体的业务逻辑,比如连接数据库,增删改查等等

服务启动

在上面的实现类中,加入 @GRpcService 这个注解,之后就会被容器扫描到,然后对外提供服务, 默认的接口是 5656 , 可以在配置文件中通过 grpc.server.port 这个值修改

到这儿服务端方面的东西就OK了, 一般我们为了不被打, 写完接口还得自测一下嘛, 所以还需要看看客户端的一些配置

客户端配置

客户端需要一个配置类,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Configuration
public class GrpcConfig {
@Bean
ManagedChannel channel(){
return ManagedChannelBuilder.forAddress("127.0.0.1", 6565)
.usePlaintext()
.build();
}

@Bean
HelloServiceGrpc.HelloServiceBlockingStub helloServiceBlockingStub(){
return HelloServiceGrpc.newBlockingStub(channel());
}

}

这个类就是设置一个 channel ,配置地址,当然这个地址可以写在配置文件里, 然后还要 @Bean 注入一个类, 是 Grpc 结尾然后 BlockingStub 结尾的内部类, 注入到容器中, 之后就可以在单元测试或者别的地方使用了

1
2
3
4
5
6
7
8
9
10
11
12
13
@Autowired
private HelloServiceGrpc.HelloServiceBlockingStub helloServiceBlockingStub;

@Test
public void contextLoads() {

HelloResponse helloResponse = helloServiceBlockingStub.sayHello(HelloRequest.newBuilder().setTxt("test").build());

System.out.println(helloResponse.getStatus());
System.out.println(helloResponse.getMsg());
System.out.println(helloResponse.getData());

}

导入公共类

定义 proto 文件的时候,像上面这种 HelloResponse 的 message 其实是通用的,就可以单独用一个文件定义一下,然后用 import 关键字导入进来, 就不用在每个文件中都写一次了,如下:

1
import "common/CommonResponse.proto";

这里有一点主意的是,只能用绝对路劲,不能使用 ./ 或者 ../ 这种,否则编译会报错

以上就是 grpc 的一个简单的示例,更高级的用法可以看一下 grpc-spring-boot-starterGitHub,这个里面提供了很多的示例

就先记到这儿,下课.