Spring Boot中使用gRPC与Protobuf验证教程源码

尽管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 存储库中找到该项目的最终源代码。