2020-02-08 gRPC从入门到破产(2)SpringCloud与gRPC整合

本文已经与(1)整合后发在了我的个人网站,欢迎访问:http://www.wendev.site/article/25

写在前面

前一段时间学习SpringCloud时尝试了SpringCloud整合Dubbo,感觉非常棒。那比Dubbo更快、还有着跨语言特性的gRPC与SpringCloud又会碰撞出什么样的火花呢?今天就来试一试。

当然,一开始写的也并不复杂,还是从最简单的一个服务提供者,一个服务消费者,发送一条HelloWorld并显示端口号开始。

服务注册与发现中心选择了Consul,本来用的是Nacos,结果出了莫名其妙的Bug(这个在下面会写),换Consul之后一下子就好了。。。

版本

  • Java:11
  • Consul:1.6.3
  • Spring Cloud:Hoxton.RELEASE
  • Spring Boot:2.2.4.RELEASE
  • gRPC:1.25.0
  • grpc-spring-boot-strater:2.6.2.RELEASE GitHub地址

使用Docker启动Consul

以前一直是使用直接运行可执行文件启动Consul,而使用docker比直接运行jar包更方便管理,所以这次就使用Docker来运行。

这里顺便记录一下Nacos的Docker启动方式:

Nacos

docker pull nacos/nacos-server
docker run --env MODE=standalone --name nacos -d -p 8848:8848 nacos/nacos-server

Consul

只启动一个实例的话可以这样启动:

docker pull consul
docker run --name consul -p 8500:8500 -d consul

运行完毕就可以通过localhost:8500访问了。

开始!

整个项目的目录结构:

父级依赖管理工程

<?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>site.wendev.grpc</groupId>
    <artifactId>grpc-spring-cloud-final</artifactId>
    <packaging>pom</packaging>
    <version>1.0-SNAPSHOT</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.4.RELEASE</version>
    </parent>

    <modules>
        <module>wendev-api</module>
        <module>wendev-provider</module>
        <module>wendev-consumer</module>
    </modules>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>11</java.version>
        <spring.cloud.version>Hoxton.RELEASE</spring.cloud.version>
        <nacos.version>0.9.0.RELEASE</nacos.version>
        <lombok.version>1.18.10</lombok.version>
        <java.annotation.api.version>1.3.2</java.annotation.api.version>
        <grpc.version>1.25.0</grpc.version>
        <grpc.spring.boot.starter.verison>2.6.2.RELEASE</grpc.spring.boot.starter.verison>
    </properties>

    <dependencyManagement>
        <dependencies>
            <!-- 公共 API 模块 -->
            <dependency>
                <groupId>site.wendev.grpc</groupId>
                <artifactId>wendev-api</artifactId>
                <version>${project.version}</version>
            </dependency>

            <!-- Spring Cloud -->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring.cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>

            <!-- Nacos -->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
                <version>${nacos.version}</version>
            </dependency>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
                <version>${nacos.version}</version>
                <exclusions>
                    <exclusion>
                        <groupId>org.springframework.cloud</groupId>
                        <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
                    </exclusion>
                </exclusions>
            </dependency>

            <!-- gRPC -->
            <dependency>
                <groupId>io.grpc</groupId>
                <artifactId>grpc-all</artifactId>
                <version>${grpc.version}</version>
            </dependency>
            <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>
            <dependency>
                <groupId>javax.annotation</groupId>
                <artifactId>javax.annotation-api</artifactId>
                <version>${java.annotation.api.version}</version>
                <scope>provided</scope>
            </dependency>

            <!-- gRPC Spring Boot Starter -->
            <dependency>
                <groupId>net.devh</groupId>
                <artifactId>grpc-server-spring-boot-starter</artifactId>
                <version>${grpc.spring.boot.starter.verison}</version>
            </dependency>
            <dependency>
                <groupId>net.devh</groupId>
                <artifactId>grpc-client-spring-boot-starter</artifactId>
                <version>${grpc.spring.boot.starter.verison}</version>
            </dependency>
        </dependencies>
    </dependencyManagement>
</project>

这个grpc-spring-boot-starter貌似是国人写的,赞!

公共API模块

Maven依赖,主要是gRPC的依赖:

<?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>

    <parent>
        <artifactId>grpc-spring-cloud-final</artifactId>
        <groupId>site.wendev.grpc</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>

    <artifactId>wendev-api</artifactId>
    <name>wendev-api</name>
    <packaging>jar</packaging>

    <properties>
        <os.plugin.version>1.6.2</os.plugin.version>
        <protoc.version>3.10.0</protoc.version>
        <protobuf.plugin.version>0.6.1</protobuf.plugin.version>
    </properties>

    <dependencies>
        <!-- gRPC -->
        <dependency>
            <groupId>io.grpc</groupId>
            <artifactId>grpc-all</artifactId>
        </dependency>

        <!-- Javax Annotation Api -->
        <dependency>
            <groupId>javax.annotation</groupId>
            <artifactId>javax.annotation-api</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-autoconfigure</artifactId>
        </dependency>
    </dependencies>

    <build>
        <extensions>
            <!-- gRPC 代码生成插件需要此 extension -->
            <extension>
                <groupId>kr.motd.maven</groupId>
                <artifactId>os-maven-plugin</artifactId>
                <version>${os.plugin.version}</version>
            </extension>
        </extensions>
        <plugins>
            <!-- protobuf java 代码生成器 -->
            <plugin>
                <groupId>org.xolstice.maven.plugins</groupId>
                <artifactId>protobuf-maven-plugin</artifactId>
                <version>${protobuf.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:1.25.0:exe:${os.detected.classifier}</pluginArtifact>
                    <protocExecutable>/Users/jiangwen/tools/protoc-3.10.0/bin/protoc</protocExecutable>
                    <clearOutputDirectory>false</clearOutputDirectory>
                </configuration>
                <executions>
                    <execution>
                        <phase>compile</phase>
                        <goals>
                            <goal>compile</goal>
                            <goal>compile-custom</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

首在main文件夹下建立一个proto文件夹,写个proto文件:

hello_world.proto

syntax = "proto3";

option java_package = "site.wendev.grpc.api";
option java_multiple_files = true;
option java_outer_classname = "HelloProto";

service HelloWorld {
    rpc Hello (HelloRequest) returns (HelloResponse) {
    }
}

message HelloRequest {
    string message = 1;
}

message HelloResponse {
    string response = 1;
}

然后像上一篇讲的那样把代码生成出来,放到相应目录下,再执行mvn clean install把这个模块安装在本地,供其他服务调用。

服务提供者

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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <artifactId>grpc-spring-cloud-final</artifactId>
        <groupId>site.wendev.grpc</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>

    <artifactId>wendev-provider</artifactId>
    <version>${project.version}</version>
    <name>wendev-provider</name>

    <properties>
        <java.version>11</java.version>
    </properties>

    <dependencies>
        <!-- 公共 API 模块 -->
        <dependency>
            <groupId>site.wendev.grpc</groupId>
            <artifactId>wendev-api</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>

        <!-- Consul -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-consul-discovery</artifactId>
        </dependency>

        <!-- gRPC Spring Boot Starter -->
        <dependency>
            <groupId>net.devh</groupId>
            <artifactId>grpc-server-spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

配置:

bootstrap.yml

server:
  port: 8763
grpc:
  server:
    port: 0
spring:
  application:
    name: wendev-provider
  cloud:
    consul:
      host: 127.0.0.1
      port: 8500
      discovery:
        hostname: 127.0.0.1

gRPC的端口是0,这样写是“端口号随机”的意思,可以保证每次启动端口都不同,就不用手动修改了。server.port写死了是因为需要把这个值注入,返回给服务消费者,如果不需要返回端口号也可以写0。

然后编写服务提供者:

/**
 * 服务提供者:返回服务消费者发送的信息和端口号
 * 使用<code>@GrpcService</code>注解声明这个服务提供者
 *
 * @author 江文
 * @date 2020/2/7 5:12 上午
 */
@GrpcService
public class HelloWorldService extends HelloWorldGrpc.HelloWorldImplBase {
    @Value("${server.port}")
    private String port;

    @Override
    public void hello(HelloRequest request, StreamObserver<HelloResponse> responseObserver) {
        String message = String.format("Welcome to WenDev, your message is %s, from port %s."
                + "From: Spring Cloud + gRPC.",
                request.getMessage(), port);
        HelloResponse response = HelloResponse.newBuilder().setResponse(message).build();
        responseObserver.onNext(response);
        responseObserver.onCompleted();
    }
}

题外话:大家可以从注释中看到创建时间是昨天早上,所以我踩坑踩了整整一天。。。写此文的目的除了总结,就是希望大家不要继续踩我踩过的坑。

逻辑很简单,主要就是生成一条响应信息,然后返回响应。代码并不比使用Dubbo复杂多少。

这里遇到个大坑,差点没把我坑死。。。

最新的gRPC版本是1.27.0,然而grpc-spring-boot-starter只支持到1.25,还不兼容1.27。。。晕死,为了换个版本还得重新下一个3.10版本的protoc(1.27是用的3.11.3版本的),然后重新生成代码。。。说不定我应该去grpc-spring-boot-starter的仓库里提个issue?

服务消费者

首先是依赖,与服务提供者非常相似,就是grpc-spring-boot-starter换成了服务消费者的:

<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <artifactId>grpc-spring-cloud-final</artifactId>
        <groupId>site.wendev.grpc</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>

    <artifactId>wendev-consumer</artifactId>
    <version>${project.version}</version>
    <name>wendev-consumer</name>

    <properties>
        <java.version>11</java.version>
    </properties>

    <dependencies>
        <!-- 公共 API 模块 -->
        <dependency>
            <groupId>site.wendev.grpc</groupId>
            <artifactId>wendev-api</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>

        <!-- Consul -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-consul-discovery</artifactId>
        </dependency>

        <!-- gRPC Spring Boot Starter -->
        <dependency>
            <groupId>net.devh</groupId>
            <artifactId>grpc-client-spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

配置,也没什么区别。因为没有CA证书,用OpenSSL生成又一直报各种奇奇怪怪的错误,就设定为不加密的plaintext了:

bootstrap.yml

server:
  port: 8720
spring:
  application:
    name: wendev-consumer
  cloud:
    consul:
      host: 127.0.0.1
      port: 8500
      discovery:
        hostname: 127.0.0.1
grpc:
  client:
    GLOBAL:
      security:
        enable-keep-alive: true
      keep-alive-without-calls: true
      negotiation-type: plaintext

业务逻辑代码,首先是Service:

/**
 * 服务消费者:远程调用服务提供者,并且接收消息返回给Controller。
 *
 * @author 江文
 * @date 2020/2/7 5:33 上午
 */
@Service
public class HelloWorldService {
    @GrpcClient("wendev-provider")
    private HelloWorldGrpc.HelloWorldBlockingStub stub;

    public String rpc(String message) {
        HelloRequest request = HelloRequest.newBuilder().setMessage(message).build();
        return stub.hello(request).getResponse();
    }
}

这个@GrpcClient加在stub上,或者如同网上绝大多数资料那样加在channel上都可以,不过官方推荐加在stub上,就按照推荐的来。

代码同样也不复杂。

然后是Controller层,调用Service里的rpc方法,返回调用结果:

/**
 * Controller层,只有一个方法。
 *
 * @author 江文
 * @date 2020/2/7 5:36 上午
 */
@RestController
public class HelloWorldController {
    final HelloWorldService service;

    @GetMapping("/{message}")
    public String helloWorld(@PathVariable String message) {
        return service.rpc(message);
    }

    HelloWorldController(HelloWorldService service) {
        this.service = service;
    }
}

运行

先说一下,这里又一个巨坑!比上一个要大得多!排查这个花了我整整一天,最后发现居然不是我的错(虽说选了不合适的注册中心也算是我的错吧)。。。

本来是用的Nacos做的服务注册与发现中心,结果请求总是出错,报network closed for unknown reason的异常。后来换了Consul,就改了依赖和加上了Consul服务注册与发现的配置代码,业务代码改都没改一下子就好了。。。

看来这是Nacos的bug,具体原因不明。不过用Consul不会出问题,似乎用Eureka也不会。

先启动服务提供者,修改server.port多启动几份,然后启动服务消费者。在Consul里可以看到,启动成功了:

不过这个健康检查失败也不知道是哪里出了问题,但是服务间调用正常,可能因为我没在yml文件里配置?

可以看到Tags里出现了gRPC的端口号,gRPC客户端(服务消费者)就是通过这个Tag找到gRPC服务端(服务提供者)的端口的。

请求127.0.0.1:8720/HelloWorld可以发现消息成功返回了:

多刷新几次,可以看到Consul提供的负载均衡效果:

通过这个例子,我们发现gRPC和Spring Cloud在grpc-spring-boot-strater的帮助下可以配合得非常好。虽然坑有些多,但毕竟gRPC不是Dubbo那种无缝集成的,出现一些坑也是可以理解的。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容