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

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 Reference Documentation で説明されているように、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.");
        }
    }
}

https://www.baeldung.com/javax-validation Validator についての詳細は説明しません。オブジェクトの検証時にすべてのエラーが収集されることを知っていれば十分です。 空のエラー収集は、オブジェクトがすべての制約に準拠していることを意味します

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

2.2. 検証の実行

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

しかし、これらのフィルタでは、ハンドラと同じように、https://www.baeldung.com/spring-webflux[asynchronous construction]を扱うことができます。

つまり、 Publisher 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);
}

簡単に言うと、リクエストの本文が我々の制限に違反している場合、Googleのサービスは「 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);
        }
    });
}

ご覧のとおり、まだ作成していない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. Bean Validation APIアノテーションのサポート

  • このアプローチでは、 javax.validation パッケージによって提供される強力なhttps://www.baeldung.com/javax-validation#validation[Bean Validationのアノテーション]を利用することもできます。

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

public class AnnotatedRequestEntity {

    @NotNull
    private String user;

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

   //... Constructors, Getters and Setters ...
}
  • これで、 LocalValidatorFactoryBean 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
@Primary
public Validator springValidator() {
    return new LocalValidatorFactoryBean();
}

5.まとめ

まとめると、この記事ではSpring 5の機能的エンドポイントで入力データを検証する方法を学びました。

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

  • もちろん、提案された解決策はどんなシナリオにも適していないかもしれません。状況を分析し、構造をニーズに合わせて調整する必要があります。

実用的な例全体を見たい場合は、https://github.com/eugenp/tutorials/tree/master/spring-5-reactive[私たちのGithubリポジトリ]で見つけることができます。