Spring Boot中使用gRPC與Protobuf驗證教程原始碼

banq發表於2024-05-21

儘管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(<font>"Echo: " + request.getText())
                .build();
        responseObserver.onNext(message);
        responseObserver.onCompleted();
    }
}

然後我們需要執行mvn clean package命令從 proto 檔案生成 Java 原始碼。
您可以在此GitHub 儲存庫中找到完整的原始碼


預設情況下,將以模式在grpc-server埠上啟動。我們可以使用gRPCurl命令執行專案並測試 Echo 服務:9090PLAINTEXT

grpcurl --plaintext -d '{<font>"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 {
  <font>// 資訊文字長度至少為 3 個字元。<i>
  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(<font>"Echo: " + request.getText())
                .build();
        responseObserver.onNext(message);
        responseObserver.onCompleted();
    }
}

現在,我們可以重新執行專案,並再次使用 gRPCurl 命令測試 Echo 服務中的驗證:

grpcurl --plaintext -d '{<font>"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(<font>"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(<font>"@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 '{<font>"text": "de"}' localhost:9090 com.saeed.grpcvalidation.EchoService/echo

ERROR:
  Code: InvalidArgument
  Message: value length must be at least 3 characters

 GitHub 儲存庫中找到該專案的最終原始碼。

相關文章