尽管Spring团队没有正式支持gRPC服务,但是强大的Java和Spring社区为我们提供了可能,足见社区的力量。
验证是服务通信的一个关键方面,是软件开发中的一个跨领域关注点。强大的验证机制简化了服务开发并增强了代码的可维护性。在本文中,我们将演示使用Spring Boot、gRPC和Protobuf实现一个简单的服务,并介绍一个有助于在 Spring Boot 应用程序中轻松处理 Protobuf 验证的库。在最后一步中,我们甚至通过使用 Spring AOP 引入一个方面来使其变得更容易。
Spring Boot 生态系统中的 gRPC
Protobuf是开发人员中著名的数据序列化机制,因为它速度快且与语言无关。 gRPC是一个旨在管理服务之间远程过程调用的框架,无论平台如何,并使用 Protobuf 作为其数据序列化格式。如今,随着大型微服务的存在,在服务之间采用高性能的通信方式非常重要。 gRPC 一直是实现此目的的绝佳选择,与其他技术一样,它也有其优点和缺点。
虽然 Spring Boot 没有任何 gRPC 的官方启动库,但有一个第三方库,例如grpc-spring,由gRPC 生态系统团队官方维护,他们可以简化与 Spring Boot 的集成。
在 Spring Boot 中实现 gRPC Echo 服务
在讨论 Spring Boot 中 gRPC 中的 Protobuf 验证之前,我们需要一个提供 gRPC 服务的简单 Spring Boot 应用程序。为此,我们使用 Spring Boot 和grpc-spring 库实现 gRPC Echo 服务。 Echo 服务的 Protobuf 文件将类似于以下内容:
service EchoService { rpc echo ( Message ) returns ( Message ) { } }
message Message { string text = 1 ; }
|
为了简单起见,我们将 proto 文件放在 echo 服务 Spring Boot 应用程序中 ( src/main/proto/echo.proto)。
我们还将使用protobuf-maven-pluginmaven插件将proto文件编译成Java
<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>
|
Echo 服务的实施非常简单:
@GrpcService public class EchoService extends EchoServiceGrpc.EchoServiceImplBase {
@Override public void echo(Message request, StreamObserver<Message> responseObserver) { Message message = Message.newBuilder() .setText("Echo: " + request.getText()) .build(); responseObserver.onNext(message); responseObserver.onCompleted(); } }
|
然后我们需要运行mvn clean package命令从 proto 文件生成 Java 源代码。
您可以在此GitHub 存储库中找到完整的源代码
默认情况下,将以模式在grpc-server端口上启动。我们可以使用gRPCurl命令运行项目并测试 Echo 服务:9090PLAINTEXT
grpcurl --plaintext -d '{"text": "deli"}' localhost:9090 com.saeed.grpcvalidation.EchoService/echo
{ "text" : "Echo: deli" }
|
协议缓冲区验证
protovalidate是一个旨在根据用户定义的验证规则在运行时验证 Protobuf 消息的库。目前,它支持 Go、Java、Python 和 C++。我们希望使用这个库向 Echo 服务添加验证。
将 Protobuf 验证添加到 Spring Boot
首先,我们需要在pom.xml文件中添加protovalidate-java库(Java protovalidate实现):
<dependency> <groupId>build.buf</groupId> <artifactId>protovalidate</artifactId> <version>0.2.1</version> </dependency>
|
定义验证规则:
message Message { // 信息文本长度至少为 3 个字符。 string text = 1 [(buf.validate.field).string.min_len = 3]; }
|
现在一切准备就绪,可以在我们的 Echo 服务中使用 protovalidate-java 库提供的 Validator 类了。在此之前,最好将其封装在一个名为 GrpcValidator 的 Spring Bean 中:
@Component public class GrpcValidator {
private final Validator validator;
public GrpcValidator() { this.validator = new Validator(); }
public void validate(Message message) { try { ValidationResult result = validator.validate(message);
if (!result.getViolations().isEmpty()) { throw new GrpcValidationException(result.getViolations()); } } catch (ValidationException e) { throw new GrpcValidationException(e.getMessage(), e); } } }
|
正如你所看到的,在验证违规的情况下,我们会抛出一个名为 GrpcValidationException 的自定义异常来处理各种异常。
grpc-spring 库有一个很棒的功能,可以声明全局 gRPC 异常处理,类似于 Spring MVC 中的 @ControllerAdvice 注解。
@GrpcAdvice public class GlobalGrpcExceptionHandler {
@GrpcExceptionHandler public Status handleGrpcValidationException(GrpcValidationException e) { return Status.INVALID_ARGUMENT.withDescription(e.getMessage()).withCause(e); }
@GrpcExceptionHandler public Status handleException(Exception e) { return Status.INTERNAL.withDescription(e.getMessage()).withCause(e); } }
|
最后,我们可以在 Echo 服务中使用 GrpcValidator 来验证传入的 gRPC 消息:
@GrpcService public class EchoService extends EchoServiceGrpc.EchoServiceImplBase {
private final GrpcValidator validator;
public EchoService(GrpcValidator validator) { this.validator = validator; }
@Override public void echo(Message request, StreamObserver<Message> responseObserver) { validator.validate(request); Message message = Message.newBuilder() .setText("Echo: " + request.getText()) .build(); responseObserver.onNext(message); responseObserver.onCompleted(); } }
|
现在,我们可以重新运行项目,并再次使用 gRPCurl 命令测试 Echo 服务中的验证:
grpcurl --plaintext -d '{"text": "de"}' localhost:9090 com.saeed.grpcvalidation.EchoService/echo
ERROR: Code: InvalidArgument Message: value length must be at least 3 characters
|
使用 Spring AOP 推广 gRPC 验证
您可能已经注意到,将 注入GrpcValidator每个 gRPC 服务并validate()手动调用该方法并不可取。这使得我们的代码变得杂乱,并且根据DRY原则,我们会产生大量的重复代码,降低系统的可维护性、可读性和可扩展性。
使用 AOP 技术实现横切关注点
解决这个问题的一个好方法是利用 AOP 概念(方面)来实现横切关注点(验证)。横切关注点和面向方面编程(AOP)与解决此类问题密切相关,在实现日志记录、错误处理、安全性、缓存和验证等横切关注点时避免重复代码和维护挑战。
幸运的是,Spring 框架为面向方面的编程提供了强大的支持。我们希望将 gRPC 验证实现为一个方面,并且我们的连接点将是使用自定义注释进行注释的方法执行GrpcValidation。最后,我们的Advice类型是Around,因为 我们需要包围连接点方法。
像往常一样,在开始之前,我们需要将 Spring AOP 依赖添加到我们的 Spring 项目中:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>
|
定义一个名为 GrpcValidation 的新自定义注解:
@Documented @Inherited @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE, ElementType.METHOD}) public @interface GrpcValidation { }
|
然后从 Echo 服务调用 validator.validate(request);方法,并用 GrpcValidation 注解代替 echo 方法:
@GrpcService public class EchoService extends EchoServiceGrpc.EchoServiceImplBase {
@Override @GrpcValidation public void echo(Message request, StreamObserver<Message> responseObserver) { Message message = Message.newBuilder() .setText("Echo: " + request.getText()) .build(); responseObserver.onNext(message); responseObserver.onCompleted(); } }
|
最后一步是将 GrpcValidator 类转换为一个方面,方法是在类级别上添加 @Aspect,并使用 @Around 包围连接点方法:
@Aspect @Component public class GrpcValidator {
private final Validator validator;
public GrpcValidator() { this.validator = new Validator(); }
@Around("@annotation(com.saeed.grpcvalidation.GrpcValidation)") public Object validate(ProceedingJoinPoint proceedingJoinPoint) throws Throwable { Object result; try { final var args = proceedingJoinPoint.getArgs(); for (Object param : args) { if (param instanceof Message message) { ValidationResult validationResult = validator.validate(message);
if (!validationResult.getViolations().isEmpty()) { throw new GrpcValidationException(validationResult.getViolations()); } } } result = proceedingJoinPoint.proceed(args); } catch (ValidationException e) { throw new GrpcValidationException(e.getMessage(), e); }
return result; } }
|
神奇的是,如果我们重新运行该项目,并再次使用 gRPCurl 命令在 Echo 服务中测试验证,结果将是一样的,但 Echo 服务的实现要干净得多:
grpcurl --plaintext -d '{"text": "de"}' localhost:9090 com.saeed.grpcvalidation.EchoService/echo
ERROR: Code: InvalidArgument Message: value length must be at least 3 characters
|
GitHub 存储库中找到该项目的最终源代码。