本文已经与(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那种无缝集成的,出现一些坑也是可以理解的。