尽管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 ; } |
我们还将使用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 存储库中找到该项目的最终源代码。