Spring 5における機能的エンドポイントの検証

Spring 5の機能エンドポイントの検証

1. 概要

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

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

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

2. Spring Validationの使用

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

次のRouterFunctionがあるとします。

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

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

@Component
public class FunctionalHandler {

    public Mono handleRequest(ServerRequest request) {
        Mono 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. バリデーターの実装

As it’s explained in this Spring Reference Documentation, we can use the Spring’s Validator interface to evaluate our resource’s values

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

howValidatorの動作については詳しく説明しません。 オブジェクトを検証するときにすべてのエラーが収集されることを知っていれば十分です–an empty error collection means that the object adheres to all our constraints

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

2.2. 検証の実行

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

ただし、これらのフィルターでは、ハンドラーと同じように、MonoFluxなどのasynchronous constructionsを処理することに注意する必要があります。

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

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

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

public Mono handleRequest(ServerRequest request) {
    Validator validator = new CustomRequestEntityValidator();
    Mono 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」応答を取得するようになりました。

目標を達成したと言えますか? さて、もうすぐです。 We’re running the validations, but there are many drawbacks in this approach.

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

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

3. DRYアプローチでの作業

To create a cleaner solution we’ll start by declaring an abstract class containing the basic procedure to process a request

入力検証を必要とするすべてのハンドラーは、メインスキームを再利用するために、この抽象クラスを拡張します。したがって、DRY(繰り返してはいけません)の原則に従います。

ジェネリックを使用して、あらゆる体型とそれぞれのバリデーターをサポートするのに十分な柔軟性を持たせます。

public abstract class AbstractValidationHandler {

    private final Class validationClass;

    private final U validator;

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

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

次に、handleRequestメソッドを標準の手順でコーディングしましょう。

public Mono 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 onValidationErrors(
  Errors errors,
  T invalidBody,
  ServerRequest request) {
    throw new ResponseStatusException(
      HttpStatus.BAD_REQUEST,
      errors.getAllErrors().toString());
}

これは単なるデフォルトの実装ですが、子クラスによって簡単にオーバーライドできます。

Finally, we’ll set the processBody method undefined -we’ll leave it up to the child classes to determine how to proceed in that case

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

このクラスでは、分析する側面がいくつかあります。

まず、ジェネリックスを使用することにより、子の実装は、期待するコンテンツのタイプと、それを評価するために使用されるバリデーターを明示的に宣言する必要があります。

これにより、メソッドのシグネチャが制限されるため、構造が堅牢になります。

実行時に、コンストラクターは実際のバリデーターオブジェクトとリクエストボディのキャストに使用されるクラスを割り当てます。

完全なクラスhereを見ることができます。

この構造からどのように利益を得ることができるかを見てみましょう。

3.1. ハンドラーの適応

明らかに、最初に行う必要があるのは、この抽象クラスからハンドラーを拡張することです。

そうすることで、we’ll be forced to use the parent’s constructor and to define how we’ll handle our request in the processBody method

@Component
public class FunctionalHandler
  extends AbstractValidationHandler {

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

    @Override
    protected Mono 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アノテーションのサポート

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

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

public class AnnotatedRequestEntity {

    @NotNull
    private String user;

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

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

We can now simply create a new handler injected with the default Spring Validator provided by the LocalValidatorFactoryBean bean

public class AnnotatedRequestEntityValidationHandler
  extends AbstractValidationHandler {

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

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

        // ...

    }
}

コンテキストに他のValidator Beanが存在する場合は、@Primaryアノテーションを使用してこれを明示的に宣言する必要がある可能性があることに注意する必要があります。

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

5. 結論

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

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

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

動作例全体を見たい場合は、our GitHub repoで見つけることができます。