gRpc最佳实践-Java


介绍

gRpc是一种可以跨语言的Rpc框架,它可以跨语言的原因在于其接口定义文件的平台无关性。
这一点与Dubbo形成鲜明对比,Dubbbo的接口文件直接就是Java的接口类,进而在云原生时代限制了Dubbo的跨语言发展。

使用gRpc开发的主要步骤有:

  1. 定义proto接口文件,文件中约定了接口函数和入参出参的数据结构;
  2. 利用proto生成相应语言的接口代码,例如go,Java,python语言的代码等;
  3. 基于上述生产的代码进行编码,之后的服务端与客户端只需要约定好域名和端口即可完成调用。

例子

下面以Java语言为例,给出示例代码。

调用流程

编写接口定义文件 .proto

syntax = "proto3";

package hello;

option java_multiple_files = true;

option java_package = "cn.liyongwei.api.hello";

service HelloService {
  rpc sayHello(HelloRequest) returns (HelloReply) {}
}

message HelloRequest {
  string name = 1;
}

message HelloReply {
  string message =1;
}

生成Java接口类

  1. 新建一个maven项目;
  2. src/main下新建一个proto文件夹,然后把上述的接口定义保存为hello.proto文件;
  3. pom文件引入grpc相关依赖,包括生成Java接口类的maven插件;
    <?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>cn.liyongwei.grpc</groupId>
        <artifactId>demo-api</artifactId>
        <version>2.0</version>
    
        <properties>
            <maven.compiler.source>1.8</maven.compiler.source>
            <maven.compiler.target>1.8</maven.compiler.target>
            <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
            <protobuf.version>3.21.7</protobuf.version>
            <grpc.version>1.53.0</grpc.version><!-- CURRENT_GRPC_VERSION -->
        </properties>
    
        <dependencyManagement>
            <dependencies>
                <dependency>
                    <groupId>io.grpc</groupId>
                    <artifactId>grpc-bom</artifactId>
                    <version>${grpc.version}</version>
                    <type>pom</type>
                    <scope>import</scope>
                </dependency>
            </dependencies>
        </dependencyManagement>
    
        <dependencies>
            <dependency>
                <groupId>io.grpc</groupId>
                <artifactId>grpc-netty-shaded</artifactId>
                <scope>runtime</scope>
            </dependency>
            <dependency>
                <groupId>io.grpc</groupId>
                <artifactId>grpc-protobuf</artifactId>
            </dependency>
            <dependency>
                <groupId>io.grpc</groupId>
                <artifactId>grpc-stub</artifactId>
            </dependency>
            <dependency>
                <groupId>com.google.protobuf</groupId>
                <artifactId>protobuf-java-util</artifactId>
                <version>${protobuf.version}</version>
            </dependency>
        </dependencies>
    
        <build>
            <extensions>
                <extension>
                    <groupId>kr.motd.maven</groupId>
                    <artifactId>os-maven-plugin</artifactId>
                    <version>1.4.1.Final</version>
                </extension>
            </extensions>
            <plugins>
                <plugin>
                    <groupId>org.xolstice.maven.plugins</groupId>
                    <artifactId>protobuf-maven-plugin</artifactId>
                    <version>0.5.0</version>
                    <configuration>
                        <protocArtifact>com.google.protobuf:protoc:3.3.0:exe:${os.detected.classifier}</protocArtifact>
                        <pluginId>grpc-java</pluginId>
                        <pluginArtifact>io.grpc:protoc-gen-grpc-java:1.4.0:exe:${os.detected.classifier}</pluginArtifact>
                    </configuration>
                    <executions>
                        <execution>
                            <goals>
                                <goal>compile</goal>
                                <goal>compile-custom</goal>
                            </goals>
                        </execution>
                    </executions>
                </plugin>
            </plugins>
        </build>
    </project>
  4. 在maven的插件中找到proto,右键点击compile,选择第二项进行编译。编译成功后,在target/generated-class中可以找到接口相关的类;
    调用流程
  5. 点击maven/deploy把这个接口项目发布到自己电脑的maven仓库中,就可以在其他项目中使用了。

服务端类

  1. 新建一个maven项目,在pom文件中引入如下依赖;
    <dependency>
        <groupId>cn.liyongwei.grpc</groupId>
        <artifactId>demo-api</artifactId>
        <version>2.0</version>
    </dependency>
  2. 实现接口类逻辑
    package cn.liyongwei.grpc.service;
    
    import cn.liyongwei.api.hello.HelloReply;
    import cn.liyongwei.api.hello.HelloRequest;
    import cn.liyongwei.api.hello.HelloServiceGrpc;
    import io.grpc.stub.StreamObserver;
    
    import java.net.InetAddress;
    import java.net.UnknownHostException;
    
    @GrpcService(name = "helloService")
    public class HelloService extends HelloServiceGrpc.HelloServiceImplBase {
    
        @Override
        public void sayHello(HelloRequest request, StreamObserver<HelloReply> responseObserver) {
            String hostname = null;
            try {
                hostname = InetAddress.getLocalHost().getHostName();
            } catch (UnknownHostException e) {
                e.printStackTrace();
            }
            HelloReply reply = HelloReply.newBuilder().setMessage("hello, " + request.getName() + "! from " + hostname).build();
            responseObserver.onNext(reply);
            responseObserver.onCompleted();
        }
    }
  3. 定义注解,主要作用就是实现自动载入服务
    package cn.liyongwei.grpc.service;
    
    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;
    
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.TYPE)
    public @interface GrpcService {
        String name() default "";
    }
  4. 启动类
    package cn.liyongwei.grpc.service;
    
    import io.grpc.BindableService;
    import io.grpc.Grpc;
    import io.grpc.InsecureServerCredentials;
    import io.grpc.Server;
    import io.grpc.ServerServiceDefinition;
    
    import java.io.File;
    import java.io.IOException;
    import java.lang.reflect.InvocationTargetException;
    import java.net.URL;
    import java.util.ArrayList;
    import java.util.List;
    import java.util.concurrent.TimeUnit;
    import java.util.logging.Logger;
    
    public class Application {
    
        private static final Logger logger = Logger.getLogger(Application.class.getName());
    
        private Server server;
    
        public static void main(String[] args) throws InterruptedException, IOException {
            final Application application = new Application();
            application.start();
            application.blockUntilShutdown();
        }
    
        private void start() throws IOException {
            List<ServerServiceDefinition> gRpcServices = scanServices();
            int port = 50051;
            server = Grpc.newServerBuilderForPort(port, InsecureServerCredentials.create())
                    .addServices(gRpcServices)
                    .build()
                    .start();
            logger.info("Server started, listening on " + port);
    
            Runtime.getRuntime().addShutdownHook(new Thread() {
                @Override
                public void run() {
                    // Use stderr here since the logger may have been reset by its JVM shutdown hook.
                    System.err.println("*** shutting down gRPC server since JVM is shutting down");
                    try {
                        Application.this.stop();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.err.println("*** server shut down");
                }
            });
        }
    
        private void stop() throws InterruptedException {
            if (server != null) {
                server.shutdown().awaitTermination(30, TimeUnit.SECONDS);
            }
        }
    
        private void blockUntilShutdown() throws InterruptedException {
            if (server != null) {
                server.awaitTermination();
            }
        }
    
        private List<ServerServiceDefinition> scanServices() {
            List<ServerServiceDefinition> gRpcServices = new ArrayList<>();
            String path = this.getClass().getPackage().getName();
            path = path.replace(".", "/");
            ClassLoader classLoader = this.getClass().getClassLoader();
            URL resources = classLoader.getResource(path);
            File currentDirectory = new File(resources.getFile());
            if(currentDirectory.isDirectory()) {
                File[] files = currentDirectory.listFiles();
                for(File f : files) {
                    String fileName = f.getAbsolutePath();
                    String className = fileName.substring(fileName.indexOf("cn"), fileName.indexOf(".class"));
                    className = className.replace("\\", ".");
                    try {
                        Class<?> clazz = classLoader.loadClass(className);
                        if(clazz.isAnnotationPresent(GrpcService.class)) {
                            BindableService instance = (BindableService) clazz.getDeclaredConstructor().newInstance();
                            gRpcServices.add(instance.bindService());
                        }
                    } catch (ClassNotFoundException e) {
    
                    } catch (InvocationTargetException e) {
                        throw new RuntimeException(e);
                    } catch (InstantiationException e) {
                        throw new RuntimeException(e);
                    } catch (IllegalAccessException e) {
                        throw new RuntimeException(e);
                    } catch (NoSuchMethodException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
            return gRpcServices;
        }
    }
  5. 运行main方法,开始监听本地50051端口

客户端类

  1. 创建一个SpringBoot项目,引入web和上述的接口类
    <dependency>
        <groupId>cn.liyongwei.grpc</groupId>
        <artifactId>demo-api</artifactId>
        <version>2.0</version>
    </dependency>
    
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
        <version>2.7.8-SNAPSHOT</version>
    </dependency>
  2. 配置类
    package cn.liyongwei.grpc.client.config;
    
    import cn.liyongwei.api.hello.HelloServiceGrpc;
    import io.grpc.Grpc;
    import io.grpc.InsecureChannelCredentials;
    import io.grpc.ManagedChannel;
    import org.springframework.context.annotation.Bean;
    import org.springframework.stereotype.Component;
    
    import java.util.concurrent.TimeUnit;
    
    /**
     * gRPC Client的配置——启动、建立channel、获取stub、关闭等
     * 需要注册为Spring Bean
     *
     * @author 江文
     * @date 2020/4/12 3:27 下午
     */
    @Component
    public class GrpcClientConfiguration {
        private ManagedChannel channel;
    
        public GrpcClientConfiguration() {
            start();
            Runtime.getRuntime().addShutdownHook(new Thread() {
                @Override
                public void run() {
                    // Use stderr here since the logger may have been reset by its JVM shutdown hook.
                    System.err.println("*** shutting down gRPC server since JVM is shutting down");
                    try {
                        GrpcClientConfiguration.this.stop();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.err.println("*** server shut down");
                }
            });
        }
    
        public void start() {
            String target = "localhost:50051";
            // 开启channel
            channel = Grpc.newChannelBuilder(target, InsecureChannelCredentials.create())
                    .build();
        }
    
        public void stop() throws InterruptedException {
            channel.shutdown().awaitTermination(1, TimeUnit.SECONDS);
        }
    
        @Bean(name = "helloService")
        public HelloServiceGrpc.HelloServiceBlockingStub getHelloService() {
            // 通过channel获取到服务端的stub
            return HelloServiceGrpc.newBlockingStub(channel);
        }
    }
  3. Controller类
    package cn.liyongwei.grpc.client.controller;
    
    import cn.liyongwei.api.hello.HelloReply;
    import cn.liyongwei.api.hello.HelloRequest;
    import cn.liyongwei.api.hello.HelloServiceGrpc;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.PathVariable;
    import org.springframework.web.bind.annotation.RestController;
    
    import javax.annotation.Resource;
    
    @RestController
    public class GRpcController {
    
        @Resource(name = "helloService")
        private HelloServiceGrpc.HelloServiceBlockingStub helloService;
    
        @GetMapping("/hello/{name}")
        public String sayHello(@PathVariable String name) {
            HelloReply helloReply = helloService.sayHello(HelloRequest.newBuilder().setName(name).build());
            return helloReply.getMessage();
        }
    }
  4. 启动项目,通过浏览器调用一下试试吧!

文章作者: Hiper
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Hiper !
  目录