Java集成GRPC
Java集成GRPC
什么是grpc?
GRPC是一个高性能、开源的RPC(远程过程调用)框架,由Google开发并于2015年发布。支持多种语言,如Java、Python、Go等,并且能够自动生成客户端和服务器端的代码。它使用HTTP/2协议进行通信,并默认使用Google的Protocol Buffer进行数据的序列化和反序列化。
GRPC的主要特点包括:
- 基于HTTP/2协议,支持双向流、头部压缩和多路复用等特性
- 支持多种序列化协议,如Protobuf、JSON等
- 自动生成客户端和服务器端的代码,简化开发流程
- 良好的跨语言支持
本文将教你如何在java项目中整合GRPC。我们将分步骤逐步引导你完成整个过程,确保你能够理解每一步和相关的代码。
完整的代码在我的 github 上,可以查看下载。
引入依赖
在pom文件中添加如下依赖项:
由于没有官方支持的 gRPC 和 Spring Boot 集成的 starter,我们将选择较流行的第三方项目 - grpc-spring-boot-starter
。该项目在 GitHub 上有大约 3.1k 个星星。
<dependency>
<groupId>net.devh</groupId>
<artifactId>grpc-spring-boot-starter</artifactId>
<version>2.15.0.RELEASE</version>
</dependency>
定义.proto文件
创建.proto文件,这些文件描述了服务接口和消息格式
syntax = "proto3";
option java_multiple_files = true;
package helloworld;
option java_package = "com.example.grpc";
option java_outer_classname = "HelloWorldProto";
// 问好类
service Greeter {
// 问候方法
rpc SayHello (HelloRequest) returns (HelloReply);
}
// 请求类
message HelloRequest {
string name = 1;
}
// 回应类
message HelloReply {
string message = 1;
}
引入maven插件
为了从*.proto
schema 生成 Java 类,我们将使用 Maven 插件。
引入插件protoc-jar-maven-plugin
你可以选择一些可用的插件。我选择的是 protoc-jar-maven-plugin
插件。在配置中,我们需要将 *.proto
schema 的默认位置覆写为 src/main/proto
(指定./proto文件路径)。如果我们还引入了其他的.proto
文件,就还需要使用 includeDirectories
标签,指定路径,加入其他 Protobuf schema。默认情况下:输出的 target 目录是 src/main/generated
。默认情况下,插件不会生成 gRPC service。为了启用它,我们需要在 outputTarget 中包含 grpc-java 类型。为了生成类,我们将使用 protoc-gen-grpc-java
库。
<plugin>
<groupId>com.github.os72</groupId>
<artifactId>protoc-jar-maven-plugin</artifactId>
<version>3.11.4</version>
<executions>
<execution>
<phase>generate-sources</phase>
<goals>
<goal>run</goal>
</goals>
<configuration>
<addProtoSources>all</addProtoSources>
<includeMavenTypes>direct</includeMavenTypes>
<outputDirectory>src/main/generated</outputDirectory>
<inputDirectories>
<!-- 指定./proto 文件路径 -->
<include>src/main/proto</include>
</inputDirectories>
<!-- import的其他./proto 文件路径 -->
<!-- <includeDirectories>
<include>src/main/proto-imports</include>
</includeDirectories> -->
<!-- 指定生成的文件路径。默认在target下 -->
<outputTargets>
<outputTarget>
<type>java</type>
<outputDirectory>src/main/generated</outputDirectory>
</outputTarget>
<outputTarget>
<type>grpc-java</type>
<pluginArtifact>io.grpc:protoc-gen-grpc-java:1.57.2</pluginArtifact>
<outputDirectory>src/main/generated</outputDirectory>
</outputTarget>
</outputTargets>
</configuration>
</execution>
</executions>
</plugin>
引入插件build-helper-maven-plugin
我们还将使用 build-helper-maven-plugin
Maven 插件将生成的 Java 代码作为源代码附加到指定目录下。
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId>
<executions>
<execution>
<id>add-source</id>
<phase>generate-sources</phase>
<goals>
<goal>add-source</goal>
</goals>
<configuration>
<sources>
<source>src/main/generated</source>
</sources>
</configuration>
</execution>
</executions>
</plugin>
执行mvn clean package
命令(注意: 在这里需要的maven版本需要是3.6.3
以上版本)后,Maven 将生成所需的 Java 类。下面是生成 Java 类后,应用程序中目录的最终结构。
$ tree
.
├── pom.xml
└── src
├── main
│ ├── generated
│ │ └── com
│ │ └── example
│ │ └── grpc
│ │ ├── GreeterGrpc
│ │ ├── HelloReply
│ │ ├── HelloReplyOrBuilder
│ │ ├── HelloRequest
│ │ ├── HelloRequestOrBuilder
│ │ └── HelloWorldProto
│ ├── java
│ │ └── com
│ │ └── test
│ │ ├── client
│ │ ├── controller
│ │ └── server
│ ├── proto
│ │ └── account.proto
│ ├── proto-imports
│ │ └── XXX.proto
│ └── resources
└── test
└── java
编写客户端代码
在client中进行编写客户端代码
如下是一个简单的示例:
import com.example.grpc.GreeterGrpc;
import com.example.grpc.HelloReply;
import com.example.grpc.HelloRequest;
import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;
import io.grpc.StatusRuntimeException;
import java.util.concurrent.TimeUnit;
public class HelloWorldClient {
private final ManagedChannel channel;
private final GreeterGrpc.GreeterBlockingStub blockingStub;
public HelloWorldClient(String host, int port) {
channel = ManagedChannelBuilder.forAddress(host, port).usePlaintext().build();
blockingStub = GreeterGrpc.newBlockingStub(channel);
}
public void shutdown() throws InterruptedException {
channel.shutdown().awaitTermination(5, TimeUnit.SECONDS);
}
public void greet(String name) {
HelloRequest request = HelloRequest.newBuilder().setName(name).build();
HelloReply response;
try {
response = blockingStub.sayHello(request);
} catch (StatusRuntimeException e) {
System.out.println("RPC failed: {0}");
return;
}
System.out.println(("Message from gRPC-Server: " + response.getMessage()));
}
public static void main(String[] args) throws InterruptedException {
HelloWorldClient client = new HelloWorldClient("127.0.0.1", 50051);
try {
String user = "world";
client.greet(user);
} finally {
client.shutdown();
}
}
}
编写服务端代码
在server中进行编写服务端代码
如下是一个简单的示例:
import com.example.grpc.GreeterGrpc;
import com.example.grpc.HelloReply;
import com.example.grpc.HelloRequest;
import io.grpc.Server;
import io.grpc.ServerBuilder;
import io.grpc.stub.StreamObserver;
import java.io.IOException;
public class HelloWorldServer {
private int port = 50051;
private Server server;
private void start() throws IOException {
server = ServerBuilder.forPort(port).addService(new GreeterImpl()).build().start();
System.out.println("服务启动");
Runtime.getRuntime().addShutdownHook(new Thread() {
@Override
public void run() {
System.err.println("*** shutting down gRPC server since JVM is shutting down");
HelloWorldServer.this.stop();
System.err.println("*** server shut down");
}
});
}
private void stop() {
if (server != null) {
server.shutdown();
}
}
// block 一直到退出程序
private void blockUntilShutdown() throws InterruptedException {
if (server != null) {
server.awaitTermination();
}
}
public static void main(String[] args) throws IOException, InterruptedException {
final HelloWorldServer server = new HelloWorldServer();
server.start();
server.blockUntilShutdown();
}
// 实现 定义一个实现服务接口的类
private class GreeterImpl extends GreeterGrpc.GreeterImplBase {
@Override
public void sayHello(HelloRequest req, StreamObserver<HelloReply> responseObserver) {
HelloReply reply = HelloReply.newBuilder().setMessage(("Hello " + req.getName())).build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
System.out.println("Message from gRPC-Client:" + req.getName());
}
}
}
完整的pom文件:
<?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>com.test</groupId>
<artifactId>grpc</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.7.3</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- SpringBoot Web容器 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>net.devh</groupId>
<artifactId>grpc-spring-boot-starter</artifactId>
<version>2.15.0.RELEASE</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>com.github.os72</groupId>
<artifactId>protoc-jar-maven-plugin</artifactId>
<version>3.11.4</version>
<executions>
<execution>
<phase>generate-sources</phase>
<goals>
<goal>run</goal>
</goals>
<configuration>
<addProtoSources>all</addProtoSources>
<includeMavenTypes>direct</includeMavenTypes>
<outputDirectory>src/main/generated</outputDirectory>
<inputDirectories>
<include>src/main/proto</include>
</inputDirectories>
<!-- <includeDirectories>
<include>src/main/proto-imports</include>
</includeDirectories>-->
<outputTargets>
<outputTarget>
<type>java</type>
<outputDirectory>src/main/generated</outputDirectory>
</outputTarget>
<outputTarget>
<type>grpc-java</type>
<pluginArtifact>io.grpc:protoc-gen-grpc-java:1.57.2</pluginArtifact>
<outputDirectory>src/main/generated</outputDirectory>
</outputTarget>
</outputTargets>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId>
<executions>
<execution>
<id>add-source</id>
<phase>generate-sources</phase>
<goals>
<goal>add-source</goal>
</goals>
<configuration>
<sources>
<source>src/main/generated</source>
</sources>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
到这里就完成了!有问题的话,欢迎大家指导交流、共同进步!!!
扩展知识
在Protocol Buffers中定义请求类或响应类时,你可以使用多种数据类型。以下是一些常见的数据类型,它们可以被用来定义消息字段:
标量类型
- double: 双精度浮点数
- float: 单精度浮点数
- int32: 使用变长编码。对于负数效率不高,如果你的变量可能是负数,请使用sint32代替。
- int64: 使用变长编码。对于负数效率不高,如果你的变量可能是负数,请使用sint64代替。
- uint32: 使用变长编码的无符号32位整数。
- uint64: 使用变长编码的无符号64位整数。
- sint32: 使用变长编码的有符号32位整数。这些比常规的int32更有效地编码负数。
- sint64: 使用变长编码的有符号64位整数。这些比常规的int64更有效地编码负数。
- fixed32: 总是4字节。如果值经常大于2^28,这比uint32更有效。
- fixed64: 总是8字节。如果值经常大于2^56,这比uint64更有效。
- sfixed32: 总是4字节。
- sfixed64: 总是8字节。
- bool: 布尔值。
- string: 字符串必须始终包含UTF-8编码或7位ASCII文本。
- bytes: 可以包含任意字节序列。
复合类型
- enum: 枚举类型,允许你通过一组命名的常量来定义一个类型。
- message: 可以嵌套在其他消息定义中,用来定义复杂的数据结构。
特殊字段
- map: 映射类型,允许你定义键值对,键可以是任何整数或字符串类型(除了浮点类型和bytes),值可以是任何类型除了map。
以下是一个示例,展示了如何在消息定义中使用这些类型:
message ExampleMessage {
// 标量类型
double double_field = 1;
float float_field = 2;
int32 int32_field = 3;
int64 int64_field = 4;
uint32 uint32_field = 5;
uint64 uint64_field = 6;
sint32 sint32_field = 7;
sint64 sint64_field = 8;
fixed32 fixed32_field = 9;
fixed64 fixed64_field = 10;
sfixed32 sfixed32_field = 11;
sfixed64 sfixed64_field = 12;
bool bool_field = 13;
string string_field = 14;
bytes bytes_field = 15;
// 枚举类型
enum EnumType {
DEFAULT = 0;
TYPE_A = 1;
TYPE_B = 2;
}
EnumType enum_field = 16;
// 嵌套消息类型
message NestedMessage {
string nested_field = 1;
}
NestedMessage nested_message_field = 17;
// 映射类型
map<string, int32> map_field = 18;
}
在Protocol Buffers中,还定义一个字段来存储重复的值,这可以看作是列表(list)的概念。使用 repeated 关键字可以定义一个可以包含多个相同类型元素的集合。以下是如何定义一个列表的示例:
syntax = "proto3"; // 使用Protocol Buffers版本3
// 定义一个消息,其中包含一个字符串列表
message ExampleMessage {
repeated string items = 1; // "items" 是一个字符串列表
}
在这个例子中,items 字段是一个 repeated 字段,这意味着它可以包含任意数量的字符串值。当你序列化和反序列化这个消息时,items 字段将作为一个列表来处理。
在生成的代码中,items 将通常映射到编程语言中的数组或列表类型。例如,在Python中,items 将是一个列表,在Java中,它将是一个 List。
这些类型可以灵活地组合来定义复杂的消息结构,满足不同的业务需求。