1. 概要

後でデータを処理するときに予期しないエラーを回避するために、APIの入力検証を実装すると便利なことがよくあります。

残念ながら、Spring 5では、注釈ベースのエンドポイントで行うように、機能エンドポイントで検証を自動的に実行する方法はありません。 それらを手動で管理する必要があります。

それでも、Springが提供するいくつかの便利なツールを利用して、リソースが有効であることを簡単かつクリーンな方法で確認できます。

2. Spring検証の使用

実際の検証に飛び込む前に、機能するエンドポイントを使用してプロジェクトを構成することから始めましょう。

次のRouterFunctionがあるとします。

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

このルーターは、次のコントローラークラスによって提供されるハンドラー関数を使用します。

@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.");
        }
    }
}

どのようにバリデーターが機能するかについては詳しく説明しません。 オブジェクトを検証するときにすべてのエラーが収集されることを知っていれば十分です– 空のエラー収集は、オブジェクトがすべての制約に準拠していることを意味します。

Validator が配置されたので、実際にビジネスロジックを実行する前に、それをvalidateと明示的に呼び出す必要があります。

2.2. 検証の実行

最初は、HandlerFilterFunctionを使用することが私たちの状況に適していると考えることができます。

ただし、これらのフィルターでは、ハンドラーと同じように、[X84X]MonoやFluxなどの非同期構造を処理することに注意する必要があります。

つまり、パブリッシャーMonoまたはFluxオブジェクト)にはアクセスできますが、最終的に提供されるデータにはアクセスできません。

したがって、私たちができる最善のことは、ハンドラー関数で実際に処理しているときに本体を検証することです。

先に進んで、検証ロジックを含むハンドラーメソッドを変更しましょう。

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);
}

簡単に言うと、リクエストの本文が制限に準拠していない場合、サービスは「BadRequest」応答を取得するようになりました。

目的を達成したと言えますか? さて、私たちはほとんどそこにいます。 検証を実行していますが、このアプローチには多くの欠点があります。

検証とビジネスロジックを組み合わせています。さらに悪いことに、入力検証を実行するハンドラーで上記のコードを繰り返す必要があります。

これを改善してみましょう。

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);
        }
    });
}

ご覧のとおり、まだ作成していない2つのメソッドを使用しています。

最初に検証エラーが発生したときに呼び出されるものを定義しましょう。

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. BeanValidationAPIアノテーションのサポート

このアプローチでは、javax.validationパッケージによって提供される強力なBean検証の注釈を利用することもできます。

たとえば、注釈付きのフィールドを使用して新しいエンティティを定義しましょう。

public class AnnotatedRequestEntity {
 
    @NotNull
    private String user;

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

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

LocalValidatorFactoryBeanBeanによって提供されるデフォルトのSpringValidatorが注入された新しいハンドラーを簡単に作成できるようになりました。

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
@Primary
public Validator springValidator() {
    return new LocalValidatorFactoryBean();
}

5. 結論

要約すると、この投稿では、Spring5つの機能エンドポイントで入力データを検証する方法を学びました。

ロジックがビジネスロジックと混ざらないようにすることで、検証を適切に処理するための優れたアプローチを作成しました。

もちろん、提案されたソリューションは、どのシナリオにも適していない可能性があります。 状況を分析し、おそらく構造をニーズに適合させる必要があります。

動作例全体を確認したい場合は、GitHubリポジトリで見つけることができます。