这是目录哦
一. 前言
前段时间一直在忙着另一门课程的SpringCloud微服务项目,其中各个微服务之间使用的是OpenFeign进行服务之间的接口调用。这时候刚好在网络程序设计的课程中学习到了gRPC这个RPC框架,便想着在项目中使用gRPC替代部分的OpenFeign客户端,于是便有了这篇文章。
二. 代码仓库
github地址:CampusForum项目
三. 关于gRPC和OpenFeign
在介绍两者之前先来看看RPC。
RPC(Remote Procedure Call)是远程过程调用协议,是一种通过网络从远程计算机上请求服务,而不需要了解底层网络技术的协议。它使得开发者可以像调用本地方法一样调用远程的过程,从而简化了分布式系统的开发过程。
gRPC和OpenFeign都是RPC框架,它们实现RPC的具体工具或者库,它提供了一种透明调用机制,让使用者不必显式的区分本地调用和远程调用。然而,它们在实现细节和使用场景上有所不同:
gRPC:
- gRPC是Google开发的一个高性能、开源的RPC框架,其协议设计在HTTP/2上实现,可以提供更好的性能。
- gRPC支持多种语言,包括Java、C++、Python等,这使得gRPC可以在多种语言环境中使用。
- gRPC使用Protocol Buffers作为接口定义语言,这不仅可以用于定义服务接口,还可以用于定义服务接口的消息格式。
OpenFeign:
- OpenFeign是Spring Cloud中的一个子项目,用于简化HTTP API的开发。
- OpenFeign主要用于微服务架构中,它可以使HTTP请求像本地方法调用一样简单。
- OpenFeign的一个重要特性是其声明式的编程风格,开发者只需要定义一个接口并在接口上添加注解,就可以完成一个HTTP API的开发。
接下来介绍在SpringCloud微服务项目使用gRPC替代OpenFeign的具体过程。
四. 使用gRPC替代OpenFeign
1. 原OpenFeign客户端
在项目目录的resource-service\resource-client\src\main\java\com\ustc\resource\client
下,具体代码如下,其中的uploadAvatarImage
和getUniversityName
这两个接口将会由user-service
中的user-server
微服务进行调用。后面将会由gRPC来实现这两个接口的远程调用。
@FeignClient(value = "resource-server", fallback = ResourceClientFuse.class, contextId = "ResourceClient")
public interface ResourceClient {
/**
* 上传图片接口
*
* @param file 图片
* @return 图片链接
*/
@PostMapping(value = "/private/resource/image", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
RestResult<String> uploadBlogImage(@RequestPart("file") MultipartFile file);
/**
* 上传头像接口
*
* @param file 头像图片文件
* @param name 图片命名
* @return 访问链接
*/
@PostMapping(value = "/private/resource/avatar", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
RestResult<String> uploadAvatarImage(@RequestPart("file") MultipartFile file, @RequestParam("name") String name);
/**
* 通过院校代码获取名称
*
* @param schoolCode 院校代码
* @return 高校名称
*/
@GetMapping("/resource/university/name")
RestResult<String> getUniversityName(@RequestParam("schoolCode") Integer schoolCode);
}
2. proto接口定义
对于gRPC,首先需要进行proto接口的定义。在本项目中,我把接口放在了common\common-grpc
模块,该模块需要在pom文件中添加如下的依赖。
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<protobuf.version>3.21.7</protobuf.version>
<protobuf-plugin.version>0.6.1</protobuf-plugin.version>
<grpc.version>1.52.1</grpc.version>
</properties>
<dependencies>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-stub</artifactId>
<version>${grpc.version}</version>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-protobuf</artifactId>
<version>${grpc.version}</version>
</dependency>
<dependency>
<groupId>jakarta.annotation</groupId>
<artifactId>jakarta.annotation-api</artifactId>
<version>1.3.5</version>
<optional>true</optional>
</dependency>
</dependencies>
然后再添加如下的插件,gRPC是使用Protocol Buffers来定义接口的,所以我们只要编写对应的proto文件,即可通过下面的插件将编写的proto文件自动转化为对应的java类。
<build>
<extensions>
<extension>
<groupId>kr.motd.maven</groupId>
<artifactId>os-maven-plugin</artifactId>
<version>1.7.0</version>
</extension>
</extensions>
<plugins>
<plugin>
<groupId>org.xolstice.maven.plugins</groupId>
<artifactId>protobuf-maven-plugin</artifactId>
<version>${protobuf-plugin.version}</version>
<configuration>
<protocArtifact>com.google.protobuf:protoc:${protobuf.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文件的编写,proto必须放在模块的src/main/proto
文件夹下,因为插件会默认转换该文件夹下的proto文件,否则的话还要在配置插件时指定proto文件的位置。
创建一个resource.proto
文件,接口定义如下:
syntax = "proto3"; //protocol buffers 的版本
option java_multiple_files = true; //设置为 true,表示每一个 message 文件都会有一个单独的 class 文件
option java_package = "com.ustc.common.grpc"; //用于标识生成的 java 文件的 packagev
option java_outer_classname = "ResourceProto"; //用于指定 proto 文件生成的 java 类的 outerclass 类名。
package resource; //用来定义 message 的包名,包名是用来唯一标识 message 的
//根据Feign客户端定义的服务接口
service ResourceActions {
rpc getUniversityName (SchoolCode) returns (University);
rpc uploadAvatarImage (AvatarImage) returns (ImageUrl);
}
message SchoolCode {
int32 code = 1;
}
message University {
string name = 1;
}
//由于protobuf中不支持java的MultipartFile类型,因此使用字节数组来传输MultipartFile
//这样在传输过程中MultipartFile的文件类型ContentType也会丢失,因此需要添加contentType参数
message AvatarImage{
bytes image = 1;
string name = 2;
string contentType = 3;
}
message ImageUrl {
string url = 1;
}
最后使用compile
和compile-custom
插件来生成对应的java对象。由于compile
用来编译消息对象,compile-custom
则依赖消息对象,生成接口服务。因此要先使用compile
插件,再使用compile-custom
插件。
3. gRPC服务端
在服务的调用方resource-service\resource-server
中编写gRPC服务端代码。
首先还是需要在pom文件中导入如下的服务端依赖。第一个依赖是前面的common-grpc
模块,需要大家导入自己对应的proto接口的模块。
<!-- grpc服务端的api-->
<dependency>
<groupId>com.ustc</groupId>
<artifactId>common-grpc</artifactId> # 导入自己的proto接口的模块
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>net.devh</groupId>
<artifactId>grpc-server-spring-boot-starter</artifactId>
<version>2.14.0.RELEASE</version>
</dependency>
<!-- grpc服务端的api-->
然后在服务端提供gRPC方法的实现。这里需要使用@GrpcService
注解去标注服务。
@GrpcService
public class ResourceActionsImpl extends ResourceActionsGrpc.ResourceActionsImplBase {
@Resource
private UniversityService universityService;
@Resource
private ImageService imageService;
// 获得大学名称
@Override
public void getUniversityName(SchoolCode request, StreamObserver<University> responseObserver){
String universityName = universityService.getUniversityName(request.getCode());
responseObserver.onNext(University.newBuilder().setName(universityName).build());
responseObserver.onCompleted();
}
// 上传头像
@Override
public void uploadAvatarImage(AvatarImage request, StreamObserver<ImageUrl> responseObserver){
// 将request中的字节数组转化为MultipartFile文件
InputStream inputStream = new ByteArrayInputStream(request.getImage().toByteArray());
MultipartFile image = null;
try {
image = new MockMultipartFile(request.getName(), request.getName(), request.getContentType(), inputStream);
} catch (IOException e) {
throw new RuntimeException(e);
}
// 调用图片服务进行上传
String url = imageService.uploadAvatar(image, request.getName());
// 传回URL参数
responseObserver.onNext(ImageUrl.newBuilder().setUrl(url).build());
responseObserver.onCompleted();
}
}
最后,需要在application.yaml 中进行配置,将当前服务注册到 nacos 配置中心里。
grpc:
server:
port: 40001
spring:
application:
name: @artifactId@ # artifactId为resource-server
cloud:
nacos:
server-addr: xxx.xxx.xxx.xxx:端口号 # nacos地址
4. gRPC客户端
在服务的调用方user-service\user-server
中编写gRPC客户端代码。
首先需要在pom文件中导入如下的客户端依赖。第一个也是前面的common-grpc
模块,注意这里的第二个依赖和服务端导入的第二个依赖是不同的。一个是grpc-server-spring-boot-starter
,另一个是grpc-client-spring-boot-starter
。
<!-- grpc客户端的api-->
<dependency>
<groupId>com.ustc</groupId>
<artifactId>common-grpc</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>net.devh</groupId>
<artifactId>grpc-client-spring-boot-starter</artifactId>
<version>2.14.0.RELEASE</version>
</dependency>
<!-- grpc客户端的api-->
然后就是客户端代码的编写,客户端在调用远程服务时,需要使用注解@GrpcClient("对应的服务端名称")
才能将stub注入。
@Service
public class GrpcClientService {
@GrpcClient("resource-server")
ResourceActionsGrpc.ResourceActionsBlockingStub resourceActionsBlockingStub;
// 获得大学名称
public String getUniversityName(int schoolCode){
University university = resourceActionsBlockingStub.getUniversityName(
SchoolCode.newBuilder().setCode(schoolCode).build()
);
return university.getName();
}
// 上传头像
public String uploadAvatarImage(MultipartFile file, String name){
// 将MultipartFile转化为需要传输的ImageUrl类型后,调用远程服务
ImageUrl imageUrl = null;
try {
imageUrl = resourceActionsBlockingStub.uploadAvatarImage(
AvatarImage.newBuilder()
.setImage(ByteString.copyFrom(file.getBytes()))
.setName(name)
.setContentType(file.getContentType())
.build()
);
} catch (IOException e) {
throw new RuntimeException(e);
}
return imageUrl.getUrl();
}
}
最后也需要将客户端在nacos上注册服务。
spring:
application:
name: @artifactId@ # artifactId为user-server
cloud:
nacos:
server-addr: xxx.xxx.xxx.xxx:端口号 # nacos地址
grpc:
client:
resource-server: # 这个需要是服务端的名称
negotiation-type: plaintext # 表示这个服务的通信不使用TLS加密
5. 服务测试
最后,使用postman对gRPC替换的两个服务进行测试,user-server
分别在登录服务和头像上传服务中使用到了gRPC的远程调用。通过图片可以看到,服务均能正常运行。说明确实可以使用gRPC在微服务项目中进行远程调用。
五. 总结
通过此次学习,成功将网络程序设计这门课程中学习到的gRPC的知识运用到了具体的项目中,使用了SpringBoot+Nacos+gRPC实现了微服务之间的远程调用。