1. 概要

この記事では、SpringDataRESTバリデーターの基本的な概要について説明します。 最初にSpringDataRESTの基本を確認する必要がある場合は、この記事にアクセスして、基本をブラッシュアップしてください。

簡単に言えば、Spring Data RESTを使用すると、REST APIを介してデータベースに新しいエントリを追加するだけで済みますが、もちろん、実際にデータを永続化する前に、データが有効であることを確認する必要もあります。

この記事は既存の記事に続き、そこで設定した既存のプロジェクトを再利用します。

 

そして、最初に Spring Data REST を使い始めることを検討している場合は、次の方法で実行を開始できます。

2. バリデーターの使用

Spring 3以降、フレームワークは Validator インターフェースを備えています。これは、オブジェクトの検証に使用できます。

2.1. 動機

前回の記事では、nameemailの2つのプロパティを持つエンティティを定義しました。

したがって、新しいリソースを作成するには、次のコマンドを実行する必要があります。

curl -i -X POST -H "Content-Type:application/json" -d 
  '{ "name" : "Test", "email" : "test@test.com" }' 
  http://localhost:8080/users

このPOSTリクエストは、提供されたJSONオブジェクトをデータベースに保存し、操作は次を返します。

{
  "name" : "Test",
  "email" : "test@test.com",
  "_links" : {
    "self" : {
        "href" : "http://localhost:8080/users/1"
    },
    "websiteUser" : {
        "href" : "http://localhost:8080/users/1"
    }
  }
}

有効なデータを提供したため、前向きな結果が期待されました。 しかし、プロパティ name を削除するか、値を空の String に設定すると、どうなりますか?

最初のシナリオをテストするために、前から変更されたコマンドを実行し、プロパティnameの値として空の文字列を設定します。

curl -i -X POST -H "Content-Type:application/json" -d 
  '{ "name" : "", "email" : "Baggins" }' http://localhost:8080/users

このコマンドを使用すると、次の応答が得られます。

{
  "name" : "",
  "email" : "Baggins",
  "_links" : {
    "self" : {
        "href" : "http://localhost:8080/users/1"
    },
    "websiteUser" : {
        "href" : "http://localhost:8080/users/1"
    }
  }
}

2番目のシナリオでは、プロパティnameをリクエストから削除します。

curl -i -X POST -H "Content-Type:application/json" -d 
  '{ "email" : "Baggins" }' http://localhost:8080/users

そのコマンドに対して、次の応答が返されます。

{
  "name" : null,
  "email" : "Baggins",
  "_links" : {
    "self" : {
        "href" : "http://localhost:8080/users/2"
    },
    "websiteUser" : {
        "href" : "http://localhost:8080/users/2"
    }
  }
}

ご覧のとおり、どちらのリクエストもOKであり、201のステータスコードとオブジェクトへのAPIリンクで確認できます。

部分的なデータをデータベースに挿入することを避けたいので、この動作は受け入れられません。

2.2. SpringDataRESTイベント

Spring Data REST APIを呼び出すたびに、SpringDataRESTエクスポーターは以下にリストされているさまざまなイベントを生成します。

  • BeforeCreateEvent
  • AfterCreateEvent
  • BeforeSaveEvent
  • AfterSaveEvent
  • BeforeLinkSaveEvent
  • AfterLinkSaveEvent
  • BeforeDeleteEvent
  • AfterDeleteEvent

すべてのイベントは同様の方法で処理されるため、新しいオブジェクトがデータベースに保存される前に生成されるbeforeCreateEventの処理方法のみを示します。

2.3. バリデーターの定義

独自のバリデーターを作成するには、org.springframework.validation.Validatorインターフェースをsupportsおよびvalidateメソッドで実装する必要があります。

Supports は、バリデーターが提供されたリクエストをサポートしているかどうかをチェックし、 validate メソッドは、リクエストで提供されたデータを検証します。

WebsiteUserValidatorクラスを定義しましょう。

public class WebsiteUserValidator implements Validator {

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

    @Override
    public void validate(Object obj, Errors errors) {
        WebsiteUser user = (WebsiteUser) obj;
        if (checkInputString(user.getName())) {
            errors.rejectValue("name", "name.empty");
        }
   
        if (checkInputString(user.getEmail())) {
            errors.rejectValue("email", "email.empty");
        }
    }

    private boolean checkInputString(String input) {
        return (input == null || input.trim().length() == 0);
    }
}

エラーオブジェクトは、validateメソッドで提供されるすべてのエラーを含むように設計された特別なクラスです。 この記事の後半では、エラーオブジェクトに含まれる提供されたメッセージを使用する方法を示します。 新しいエラーメッセージを追加するには、 errors.rejectValue(nameOfField、errorMessage)を呼び出す必要があります。

バリデーターを定義したら、リクエストが受け入れられた後に生成される特定のイベントにバリデーターをマッピングする必要があります。

たとえば、この場合、データベースに新しいオブジェクトを挿入するため、beforeCreateEventが生成されます。 ただし、リクエストでオブジェクトを検証する必要があるため、最初にバリデーターを定義する必要があります。

これは、次の3つの方法で実行できます。

  • Componentアノテーションを「beforeCreateWebsiteUserValidator」という名前で追加します。 Spring Bootは、キャッチするイベントを決定するプレフィックス beforeCreate を認識し、Component名からWebsiteUserクラスも認識します。
    @Component("beforeCreateWebsiteUserValidator")
    public class WebsiteUserValidator implements Validator {
        ...
    }
  • @Bean アノテーションを使用して、アプリケーションコンテキストでBeanを作成します。
    @Bean
    public WebsiteUserValidator beforeCreateWebsiteUserValidator() {
        return new WebsiteUserValidator();
    }
  • 手動登録:
    @SpringBootApplication
    public class SpringDataRestApplication implements RepositoryRestConfigurer {
        public static void main(String[] args) {
            SpringApplication.run(SpringDataRestApplication.class, args);
        }
    
        @Override
        public void configureValidatingRepositoryEventListener(
          ValidatingRepositoryEventListener v) {
            v.addValidator("beforeCreate", new WebsiteUserValidator());
        }
    }
    • この場合、WebsiteUserValidatorクラスにアノテーションは必要ありません。

2.4. イベント検出バグ

現在、Spring DataRESTバグが存在します。これはイベントの検出に影響します。

beforeCreate イベントを生成するPOSTリクエストを呼び出すと、このバグのためにイベントが検出されないため、アプリケーションはバリデーターを呼び出しません。

この問題の簡単な回避策は、すべてのイベントをSpring Data REST ValidatingRepositoryEventListenerクラスに挿入することです。

@Configuration
public class ValidatorEventRegister implements InitializingBean {

    @Autowired
    ValidatingRepositoryEventListener validatingRepositoryEventListener;

    @Autowired
    private Map<String, Validator> validators;

    @Override
    public void afterPropertiesSet() throws Exception {
        List<String> events = Arrays.asList("beforeCreate");
        for (Map.Entry<String, Validator> entry : validators.entrySet()) {
            events.stream()
              .filter(p -> entry.getKey().startsWith(p))
              .findFirst()
              .ifPresent(
                p -> validatingRepositoryEventListener
               .addValidator(p, entry.getValue()));
        }
    }
}

3. テスト

セクション2.1。では、バリデーターがないと、データの整合性をチェックしないため、名前プロパティのないオブジェクトをデータベースに追加できることを示しました。これは望ましくない動作です。

name プロパティなしで、提供されたバリデーターを使用して同じオブジェクトを追加する場合、次のエラーが発生します。

curl -i -X POST -H "Content-Type:application/json" -d 
  '{ "email" : "test@test.com" }' http://localhost:8080/users
{  
   "timestamp":1472510818701,
   "status":406,
   "error":"Not Acceptable",
   "exception":"org.springframework.data.rest.core.
    RepositoryConstraintViolationException",
   "message":"Validation failed",
   "path":"/users"
}

ご覧のとおり、リクエストからの欠落データが検出され、オブジェクトはデータベースに保存されませんでした。 500HTTPコードと内部エラーのメッセージを含むリクエストが返されました。

エラーメッセージは、リクエストの問題について何も述べていません。 より多くの情報を提供したい場合は、応答オブジェクトを変更する必要があります。

Springの記事の例外処理では、フレームワークによって生成された例外を処理する方法を示したので、この時点で間違いなく良い読み物です。

このアプリケーションはRepositoryConstraintViolationException例外を生成するため、この特定の例外のハンドラーを作成して、応答メッセージを変更します。

これは私たちのRestResponseEntityExceptionHandlerクラスです。

@ControllerAdvice
public class RestResponseEntityExceptionHandler extends
  ResponseEntityExceptionHandler {

    @ExceptionHandler({ RepositoryConstraintViolationException.class })
    public ResponseEntity<Object> handleAccessDeniedException(
      Exception ex, WebRequest request) {
          RepositoryConstraintViolationException nevEx = 
            (RepositoryConstraintViolationException) ex;

          String errors = nevEx.getErrors().getAllErrors().stream()
            .map(p -> p.toString()).collect(Collectors.joining("\n"));
          
          return new ResponseEntity<Object>(errors, new HttpHeaders(),
            HttpStatus.PARTIAL_CONTENT);
    }
}

このカスタムハンドラーを使用すると、戻りオブジェクトに、検出されたすべてのエラーに関する情報が含まれます。

4. 結論

この記事では、データ挿入のためのセキュリティの追加レイヤーを提供するすべてのSpring DataRESTAPIにバリデーターが不可欠であることを示しました。

また、アノテーションを使用して新しいバリデーターを作成するのがいかに簡単かについても説明しました。

いつものように、このアプリケーションのコードはGitHubプロジェクトにあります。