在Java应用中使用gRPC

2021-01-19

在复杂微服务架构体系下,不可避免的就是跨语言通信场景下,而在日常的开发又可能面临着跨平台通信。gRPC是Google在2015年开源的跨平台,跨语言的通信框架,结合Protocol Buffers工具做序列化,拥有很好的性能表现。有兴趣可以了解下grpc的动机与设计原则。在Java领域已经有Dubbo这种较为全面的服务治理型的RPC框架,为什么还要重视起gRPC?未来微服务架构会走向Mesh,所有服务治理的功能会下层到Sidecar,客户端会轻量到只能最基础的通信功能,再加上应用层的逻辑;以及越来越多的偏基础层组件需要满足上层多语言的请求并保证高性能,这都会使得gRPC得到广泛的应用。

如何使用

Maven配置

使用gRPC时Maven主要有3个依赖:

  • grpc-netty-shaded
  • grpc-protobuf
  • grpc-stub

生成的类需要这些包里的基础类,所以这些依赖是必须的。

使用gRPC时Maven主要有2个插件:

  • os-maven-plugin,用来获取当前操作系统的的信息并设置到maven属性中
  • protobuf-maven-plugin,根据约定的方式,在编译期对proto文件进行编译
<properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>

    <os-maven-plugin.version>1.6.2</os-maven-plugin.version>
    <protobuf-maven-plugin.version>0.6.1</protobuf-maven-plugin.version>
    <protoc.version>3.14.0</protoc.version>
    <grpc.version>1.34.1</grpc.version>
</properties>

<dependencies>
    <dependency>
        <groupId>io.grpc</groupId>
        <artifactId>grpc-netty-shaded</artifactId>
        <version>${grpc.version}</version>
    </dependency>
    <dependency>
        <groupId>io.grpc</groupId>
        <artifactId>grpc-protobuf</artifactId>
        <version>${grpc.version}</version>
    </dependency>
    <dependency>
        <groupId>io.grpc</groupId>
        <artifactId>grpc-stub</artifactId>
        <version>${grpc.version}</version>
    </dependency>
</dependencies>

<build>
    <plugins>
        <plugin>
            <groupId>kr.motd.maven</groupId>
            <artifactId>os-maven-plugin</artifactId>
            <version>${os-maven-plugin.version}</version>
            <executions>
                <execution>
                    <phase>initialize</phase>
                    <goals>
                        <goal>detect</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>
        <plugin>
            <groupId>org.xolstice.maven.plugins</groupId>
            <artifactId>protobuf-maven-plugin</artifactId>
            <version>${protobuf-maven-plugin.version}</version>
            <configuration>
                <protocArtifact>com.google.protobuf:protoc:${protoc.version}:exe:${os.detected.classifier}</protocArtifact>
                <pluginId>grpc-java</pluginId>
                <pluginArtifact>io.grpc:protoc-gen-grpc-java:${grpc.version}:exe:${os.detected.classifier}</pluginArtifact>
            </configuration>
            <executions>
                <execution>
                    <goals>
                        <goal>compile</goal>
                        <goal>compile-custom</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

编写proto

protobuf-maven-plugin默认会在扫描src/main/proto目录下的proto文件,基于约定大于配置原则,创建src/main/proto/HelloWorld.proto

syntax = "proto3";

option java_package = "io.github.nisiyong.example";

service Greeter {
  rpc SayHello (HelloRequest) returns (HelloReply) {}
  rpc SayHelloAgain (HelloRequest) returns (HelloReply) {}
}

message HelloRequest {
  string name = 1;
}

message HelloReply {
  string message = 1;
}

这里借助maven插件直接生成源码,生成后的代码默认在target/generated-sources/protobuf目录下。

$ mvn clean compile
$ tree target/generated-sources/protobuf
target/generated-sources/protobuf
├── grpc-java
│   └── io
│       └── github
│           └── nisiyong
│               └── example
│                   └── GreeterGrpc.java
└── java
    └── io
        └── github
            └── nisiyong
                └── example
                    └── HelloWorld.java

为了在src/main/java的其他类使用生成的类,IDEA需要设置以下目录为Generated Sources Root

  • target/generated-sources/protobuf/grpc-java
  • target/generated-sources/protobuf/java

服务端示例

根据生成的*ImplBase类,编写接口实现

class GreeterImpl extends GreeterGrpc.GreeterImplBase {
    @Override
    public void sayHello(HelloWorld.HelloRequest request, StreamObserver<HelloWorld.HelloReply> responseObserver) {
        String name = request.getName();
        System.out.println(new Date().toString() + "Receive: " + name);
        HelloWorld.HelloReply helloReply = HelloWorld.HelloReply.newBuilder()
                .setMessage("Hello, " + name)
                .build();

        responseObserver.onNext(helloReply);
        responseObserver.onCompleted();
    }
}

设置监听端口,关联接口实现类后启动服务

private void start(int port) throws IOException {
    server = ServerBuilder.forPort(port)
            .addService(new GreeterImpl())
            .build()
            .start();
    System.out.println("Server started, listening on " + port);

    Runtime.getRuntime().addShutdownHook(new Thread(() -> {
        try {
            System.err.println("*** shutting down gRPC server since JVM is shutting down");
            HelloWorldServer.this.stop();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.err.println("*** server shut down");
    }));
}

客户端示例

根据服务端提供的地址端口创建连接,然后创建接口stub对象

public HelloWorldClient(Channel channel) {
    this.blockingStub = GreeterGrpc.newBlockingStub(channel);
}

public static void main(String[] args) throws InterruptedException {
    String target = System.getProperty("server.address", "localhost:8081");
    String name = "world";
    if (args.length > 0) {
        name = args[0];
    }

    ManagedChannel channel = ManagedChannelBuilder.forTarget(target)
            .usePlaintext()
            .build();

    try {
        HelloWorldClient helloWorldClient = new HelloWorldClient(channel);
        helloWorldClient.greet(name);
    } catch (Exception e) {
        channel.shutdownNow().awaitTermination(5, TimeUnit.SECONDS);
    }
}

使用stub对象,客户端发起远程调用

public void greet(String name) {
    System.out.println("Will try to greet " + name + " ...");
    HelloWorld.HelloRequest helloRequest = HelloWorld.HelloRequest.newBuilder()
            .setName(name)
            .build();

    HelloWorld.HelloReply response;
    try {
        response = blockingStub.sayHello(helloRequest);
    } catch (StatusRuntimeException e) {
        System.err.println("RPC failed: " + e.getStatus());
        return;
    }

    System.out.println("Greeting: " + response.getMessage());
}

完整示例代码可参考:https://github.com/nisiyong/grpc-java-example

总结

本文主要介绍了如何在Java项目中使用gRPC,主要是介绍了对应的maven插件使用,IDE相关设置,并用一个基本的示例演示了gRPC的使用。使用原生的protoc在项目中开发中会比较低效,快速熟悉工具后便可以高效的进行开发。

References