本文已經(jīng)與(1)整合后發(fā)在了我的個(gè)人網(wǎng)站,歡迎訪問(wèn):http://www.wendev.site/article/25
寫在前面
前一段時(shí)間學(xué)習(xí)SpringCloud時(shí)嘗試了SpringCloud整合Dubbo,感覺非常棒。那比Dubbo更快、還有著跨語(yǔ)言特性的gRPC與SpringCloud又會(huì)碰撞出什么樣的火花呢?今天就來(lái)試一試。
當(dāng)然,一開始寫的也并不復(fù)雜,還是從最簡(jiǎn)單的一個(gè)服務(wù)提供者,一個(gè)服務(wù)消費(fèi)者,發(fā)送一條HelloWorld并顯示端口號(hào)開始。
服務(wù)注冊(cè)與發(fā)現(xiàn)中心選擇了Consul,本來(lái)用的是Nacos,結(jié)果出了莫名其妙的Bug(這個(gè)在下面會(huì)寫),換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啟動(dòng)Consul
以前一直是使用直接運(yùn)行可執(zhí)行文件啟動(dòng)Consul,而使用docker比直接運(yùn)行jar包更方便管理,所以這次就使用Docker來(lái)運(yùn)行。
這里順便記錄一下Nacos的Docker啟動(dòng)方式:
Nacos
docker pull nacos/nacos-server
docker run --env MODE=standalone --name nacos -d -p 8848:8848 nacos/nacos-server
Consul
只啟動(dòng)一個(gè)實(shí)例的話可以這樣啟動(dòng):
docker pull consul
docker run --name consul -p 8500:8500 -d consul
運(yùn)行完畢就可以通過(guò)localhost:8500訪問(wèn)了。
開始!
整個(gè)項(xiàng)目的目錄結(jié)構(gòu):

父級(jí)依賴管理工程
<?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>
這個(gè)grpc-spring-boot-starter貌似是國(guó)人寫的,贊!
公共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文件夾下建立一個(gè)proto文件夾,寫個(gè)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;
}
然后像上一篇講的那樣把代碼生成出來(lái),放到相應(yīng)目錄下,再執(zhí)行mvn clean install把這個(gè)模塊安裝在本地,供其他服務(wù)調(diào)用。
服務(wù)提供者

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,這樣寫是“端口號(hào)隨機(jī)”的意思,可以保證每次啟動(dòng)端口都不同,就不用手動(dòng)修改了。server.port寫死了是因?yàn)樾枰堰@個(gè)值注入,返回給服務(wù)消費(fèi)者,如果不需要返回端口號(hào)也可以寫0。
然后編寫服務(wù)提供者:
/**
* 服務(wù)提供者:返回服務(wù)消費(fèi)者發(fā)送的信息和端口號(hào)
* 使用<code>@GrpcService</code>注解聲明這個(gè)服務(wù)提供者
*
* @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();
}
}
題外話:大家可以從注釋中看到創(chuàng)建時(shí)間是昨天早上,所以我踩坑踩了整整一天。。。寫此文的目的除了總結(jié),就是希望大家不要繼續(xù)踩我踩過(guò)的坑。
邏輯很簡(jiǎn)單,主要就是生成一條響應(yīng)信息,然后返回響應(yīng)。代碼并不比使用Dubbo復(fù)雜多少。
這里遇到個(gè)大坑,差點(diǎn)沒(méi)把我坑死。。。
最新的gRPC版本是1.27.0,然而grpc-spring-boot-starter只支持到1.25,還不兼容1.27。。。暈死,為了換個(gè)版本還得重新下一個(gè)3.10版本的protoc(1.27是用的3.11.3版本的),然后重新生成代碼。。。說(shuō)不定我應(yīng)該去grpc-spring-boot-starter的倉(cāng)庫(kù)里提個(gè)issue?
服務(wù)消費(fèi)者

首先是依賴,與服務(wù)提供者非常相似,就是grpc-spring-boot-starter換成了服務(wù)消費(fèi)者的:
<?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>
配置,也沒(méi)什么區(qū)別。因?yàn)闆](méi)有CA證書,用OpenSSL生成又一直報(bào)各種奇奇怪怪的錯(cuò)誤,就設(shè)定為不加密的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
業(yè)務(wù)邏輯代碼,首先是Service:
/**
* 服務(wù)消費(fèi)者:遠(yuǎn)程調(diào)用服務(wù)提供者,并且接收消息返回給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();
}
}
這個(gè)@GrpcClient加在stub上,或者如同網(wǎng)上絕大多數(shù)資料那樣加在channel上都可以,不過(guò)官方推薦加在stub上,就按照推薦的來(lái)。
代碼同樣也不復(fù)雜。
然后是Controller層,調(diào)用Service里的rpc方法,返回調(diào)用結(jié)果:
/**
* Controller層,只有一個(gè)方法。
*
* @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;
}
}
運(yùn)行
先說(shuō)一下,這里又一個(gè)巨坑!比上一個(gè)要大得多!排查這個(gè)花了我整整一天,最后發(fā)現(xiàn)居然不是我的錯(cuò)(雖說(shuō)選了不合適的注冊(cè)中心也算是我的錯(cuò)吧)。。。
本來(lái)是用的Nacos做的服務(wù)注冊(cè)與發(fā)現(xiàn)中心,結(jié)果請(qǐng)求總是出錯(cuò),報(bào)network closed for unknown reason的異常。后來(lái)?yè)Q了Consul,就改了依賴和加上了Consul服務(wù)注冊(cè)與發(fā)現(xiàn)的配置代碼,業(yè)務(wù)代碼改都沒(méi)改一下子就好了。。。
看來(lái)這是Nacos的bug,具體原因不明。不過(guò)用Consul不會(huì)出問(wèn)題,似乎用Eureka也不會(huì)。
先啟動(dòng)服務(wù)提供者,修改server.port多啟動(dòng)幾份,然后啟動(dòng)服務(wù)消費(fèi)者。在Consul里可以看到,啟動(dòng)成功了:

不過(guò)這個(gè)健康檢查失敗也不知道是哪里出了問(wèn)題,但是服務(wù)間調(diào)用正常,可能因?yàn)槲覜](méi)在yml文件里配置?
可以看到Tags里出現(xiàn)了gRPC的端口號(hào),gRPC客戶端(服務(wù)消費(fèi)者)就是通過(guò)這個(gè)Tag找到gRPC服務(wù)端(服務(wù)提供者)的端口的。
請(qǐng)求127.0.0.1:8720/HelloWorld可以發(fā)現(xiàn)消息成功返回了:

多刷新幾次,可以看到Consul提供的負(fù)載均衡效果:


通過(guò)這個(gè)例子,我們發(fā)現(xiàn)gRPC和Spring Cloud在grpc-spring-boot-strater的幫助下可以配合得非常好。雖然坑有些多,但畢竟gRPC不是Dubbo那種無(wú)縫集成的,出現(xiàn)一些坑也是可以理解的。