知識庫 / Spring / Spring Web RSS 訂閱

Spring 函數端點驗證

Spring Web
HongKong
5
01:37 PM · Dec 06 ,2025

1. 概述

對我們的API實施輸入驗證通常很有用,可以避免在稍後處理數據時出現意外錯誤。

不幸的是,在Spring 6中,無法像基於註解的端點那樣,自動在函數式端點上運行驗證。我們必須手動管理它們。

儘管如此,我們仍然可以利用Spring提供的某些有用的工具,以簡潔而清晰的方式驗證我們的資源是否有效。

2. 使用 Spring 驗證

讓我們先配置我們的項目,創建一個可運行的函數式端點,然後再深入研究實際的驗證。

想象一下我們有以下 RouterFunction

@Bean
public RouterFunction<ServerResponse> functionalRoute(
  FunctionalHandler handler) {
    return RouterFunctions.route(
      RequestPredicates.POST("/functional-endpoint"),
      handler::handleRequest);
}

此路由器使用以下控制器類提供的 handler 函數:

@Component
public class FunctionalHandler {

    public Mono<ServerResponse> handleRequest(ServerRequest request) {
        Mono<String> responseBody = request
          .bodyToMono(CustomRequestEntity.class)
          .map(cre -> String.format(
            "Hi, %s [%s]!", cre.getName(), cre.getCode()));
 
        return ServerResponse.ok()
          .contentType(MediaType.APPLICATION_JSON)
          .body(responseBody, String.class);
    }
}

我們能看到,這個功能性端點所做的一切都是格式化和檢索請求體中收到的信息,該請求體以CustomRequestEntity對象結構化而成:

public class CustomRequestEntity {
    
    private String name;
    private String code;

    // ... Constructors, Getters and Setters ...
    
}

這段代碼可以正常工作,但假設現在我們需要檢查輸入是否符合某些給定約束,例如,所有字段都不能為null,並且代碼長度應大於6位數。

我們需要找到一種方法來高效地執行這些校驗,並且,如果可能的話,將這些校驗與我們的業務邏輯分離。

2.1. 實現驗證器

如 Spring 框架參考文檔 中所述,我們可以使用 Spring 的 Validator 接口來評估資源的有效性:

public class CustomRequestEntityValidator 
  implements Validator {

    @Override
    public boolean supports(Class<?> clazz) {
        return CustomRequestEntity.class.isAssignableFrom(clazz);
    }

    @Override
    public void validate(Object target, Errors errors) {
        ValidationUtils.rejectIfEmptyOrWhitespace(
          errors, "name", "field.required");
        ValidationUtils.rejectIfEmptyOrWhitespace(
          errors, "code", "field.required");
        CustomRequestEntity request = (CustomRequestEntity) target;
        if (request.getCode() != null && request.getCode().trim().length() < 6) {
            errors.rejectValue(
              "code",
              "field.min.length",
              new Object[] { Integer.valueOf(6) },
              "The code must be at least [6] characters in length.");
        }
    }
}

我們不會詳細介紹驗證器的工作原理。 重要的是要知道,所有錯誤都將在驗證對象時收集起來——一個空錯誤集合意味着該對象符合所有約束

因此,現在我們已經有了驗證器,在執行業務邏輯之前,必須明確地調用它的validate方法。

2.2. 執行驗證

首先,我們可以認為使用 HandlerFilterFunction 在我們的情況下是合適的。

但是,我們必須記住,在這些過濾器(與處理程序一樣)中,我們處理的是異步構造——例如 MonoFlux

這意味着,我們將能夠訪問 PublisherMonoFlux 對象),但不能訪問它最終提供的任何數據。

因此,我們最好的做法是在處理程序函數中實際處理時驗證主體。

讓我們修改我們的處理程序方法,包括驗證邏輯:

public Mono<ServerResponse> handleRequest(ServerRequest request) {
    Validator validator = new CustomRequestEntityValidator();
    Mono<String> responseBody = request
      .bodyToMono(CustomRequestEntity.class)
      .map(body -> {
        Errors errors = new BeanPropertyBindingResult(
          body,
          CustomRequestEntity.class.getName());
        validator.validate(body, errors);

        if (errors == null || errors.getAllErrors().isEmpty()) {
            return String.format("Hi, %s [%s]!", body.getName(), body.getCode());
        } else {
            throw new ResponseStatusException(
              HttpStatus.BAD_REQUEST,
              errors.getAllErrors().toString());
        }
    });
    return ServerResponse.ok()
      .contentType(MediaType.APPLICATION_JSON)
      .body(responseBody, String.class);
}

簡而言之,我們的服務現在將在請求體不符合我們的限制時返回一個“Bad Request”響應。

我們可以説我們已經實現了我們的目標嗎?嗯,我們已經很接近了。 我們正在執行驗證,但這種方法存在許多弊端。

我們正在將驗證與業務邏輯混合,更糟糕的是,在任何需要執行輸入驗證的處理程序中,我們必須重複上述代碼。

讓我們嘗試改進一下。

3. 採用 DRY 設計原則

為了創建一個更簡潔的解決方案,我們首先聲明一個抽象類,其中包含處理請求的基本流程。

所有需要進行輸入驗證的處理程序都將擴展該抽象類,以便重用其主要方案,從而遵循 DRY(不要重複自己)原則。

我們將使用泛型,以便使其具有足夠的靈活性,以支持任何類型的請求主體及其相應的驗證器。

public abstract class AbstractValidationHandler<T, U extends Validator> {

    private final Class<T> validationClass;

    private final U validator;

    protected AbstractValidationHandler(Class<T> clazz, U validator) {
        this.validationClass = clazz;
        this.validator = validator;
    }

    public final Mono<ServerResponse> handleRequest(final ServerRequest request) {
        // ...here we will validate and process the request...
    }
}

現在讓我們用標準流程來編寫我們的 handleRequest 方法:

public Mono<ServerResponse> handleRequest(final ServerRequest request) {
    return request.bodyToMono(this.validationClass)
      .flatMap(body -> {
        Errors errors = new BeanPropertyBindingResult(
          body,
          this.validationClass.getName());
        this.validator.validate(body, errors);

        if (errors == null || errors.getAllErrors().isEmpty()) {
            return processBody(body, request);
        } else {
            return onValidationErrors(errors, body, request);
        }
    });
}

如我們所見,我們正在使用兩種我們尚未創建的方法。

首先,讓我們定義當存在驗證錯誤時被調用的方法:

protected Mono<ServerResponse> onValidationErrors(
  Errors errors,
  T invalidBody,
  ServerRequest request) {
    throw new ResponseStatusException(
      HttpStatus.BAD_REQUEST,
      errors.getAllErrors().toString());
}

這只是一個默認實現,但可以通過子類輕鬆覆蓋。

最後,我們將設置processBody方法為未定義——我們將讓子類決定在這種情況下如何處理

abstract protected Mono<ServerResponse> processBody(
  T validBody,
  ServerRequest originalRequest);

本課程涉及幾個需要分析的方面。

首先,通過使用泛型,子實現必須明確聲明他們期望的內容類型以及用於評估該內容使用的驗證器。

這也有助於增強結構的健壯性,因為這限制了方法簽名。

在運行時,構造函數將分配實際的驗證器對象和用於轉換請求體的類。

我們可以查看完整類在源代碼項目中的實現。

現在讓我們看看如何從這種結構中獲益。

3.1. 調整我們的處理程序

首先,我們需要從該抽象類中擴展我們的處理程序。

通過這樣做,我們將被迫使用父類的構造函數,並在 processBody 方法中定義如何處理我們的請求

@Component
public class FunctionalHandler
  extends AbstractValidationHandler<CustomRequestEntity, CustomRequestEntityValidator> {

    private CustomRequestEntityValidationHandler() {
        super(CustomRequestEntity.class, new CustomRequestEntityValidator());
    }

    @Override
    protected Mono<ServerResponse> processBody(
      CustomRequestEntity validBody,
      ServerRequest originalRequest) {
        String responseBody = String.format(
          "Hi, %s [%s]!",
          validBody.getName(),
          validBody.getCode());
        return ServerResponse.ok()
          .contentType(MediaType.APPLICATION_JSON)
          .body(Mono.just(responseBody), String.class);
    }
}

正如我們所能看到的,我們的子資源處理程序現在比我們在上一部分獲得的程序要簡單得多,因為它避免了與實際資源驗證打交道。

4. 支持 Bean Validation API 註解

採用這種方法,我們還可以利用 Bean Validation 提供強大的註解,這些註解來自 javax.validation 包。

例如,讓我們定義一個帶有註解的全新實體:

public class AnnotatedRequestEntity {
 
    @NotNull
    private String user;

    @NotNull
    @Size(min = 4, max = 7)
    private String password;

    // ... Constructors, Getters and Setters ...
}

我們現在可以簡單地創建一個新的處理程序,該處理程序被注入了由<em>LocalValidatorFactoryBean</em> Bean 提供的默認 Spring Validator:

public class AnnotatedRequestEntityValidationHandler
  extends AbstractValidationHandler<AnnotatedRequestEntity, Validator> {

    private AnnotatedRequestEntityValidationHandler(@Autowired Validator validator) {
        super(AnnotatedRequestEntity.class, validator);
    }

    @Override
    protected Mono<ServerResponse> processBody(
      AnnotatedRequestEntity validBody,
      ServerRequest originalRequest) {

        // ...

    }
}

我們必須考慮到如果在上下文中存在其他Validator Bean,則可能需要顯式地使用 @Primary 註解聲明此 Bean:

@Bean
@Primary
public Validator springValidator() {
    return new LocalValidatorFactoryBean();
}

5. 結論

總結一下,在本篇帖子中,我們學習瞭如何在 Spring 5 的函數式端點中驗證輸入數據。

我們創建了一種優雅的處理驗證邏輯的方法,避免將其與業務邏輯混合。

當然,所提出的解決方案可能並不適用於所有場景。我們需要分析我們的具體情況,並可能需要調整結構以適應我們的需求。

user avatar
0 位用戶收藏了這個故事!
收藏

發佈 評論

Some HTML is okay.