web-dev-qa-db-ja.com

RESTコントローラのSpring Bootバインディングと検証エラー処理

JSR-303(検証フレームワーク)注釈付きの次のモデルがある場合:

public enum Gender {
    MALE, FEMALE
}

public class Profile {
    private Gender gender;

    @NotNull
    private String name;

    ...
}

および次のJSONデータ:

{ "gender":"INVALID_INPUT" }

私のRESTコントローラでは、バインディングエラー(genderプロパティの無効な列挙値)と検証エラー(nameプロパティをnullにすることはできません)の両方を処理したい。

次のコントローラーメソッドは機能しません。

@RequestMapping(method = RequestMethod.POST)
public Profile insert(@Validated @RequestBody Profile profile, BindingResult result) {
    ...
}

これにより、バインドまたは検証が行われる前にcom.fasterxml.jackson.databind.exc.InvalidFormatExceptionシリアル化エラーが発生します。

少しいじってから、私がやりたいことをするこのカスタムコードを思いつきました。

@RequestMapping(method = RequestMethod.POST)
public Profile insert(@RequestBody Map values) throws BindException {

    Profile profile = new Profile();

    DataBinder binder = new DataBinder(profile);
    binder.bind(new MutablePropertyValues(values));

    // validator is instance of LocalValidatorFactoryBean class
    binder.setValidator(validator);
    binder.validate();

    // throws BindException if there are binding/validation
    // errors, exception is handled using @ControllerAdvice.
    binder.close(); 

    // No binding/validation errors, profile is populated 
    // with request values.

    ...
}

基本的にこのコードが行うことは、モデルの代わりに汎用マップにシリアル化し、カスタムコードを使用してモデルにバインドし、エラーをチェックします。

次の質問があります。

  1. カスタムコードはここに行く方法ですか、Spring Bootでこれを行うより標準的な方法がありますか?
  2. @Validatedアノテーションはどのように機能しますか? @Validatedのように動作してカスタムバインディングコードをカプセル化する独自のカスタムアノテーションを作成するにはどうすればよいですか。
9

これは、春のブートでREST API

@RequestMapping(value = "/person/{id}",method = RequestMethod.PUT)
@ResponseBody
public Object updatePerson(@PathVariable Long id,@Valid Person p,BindingResult bindingResult){
    if (bindingResult.hasErrors()) {
        List<FieldError> errors = bindingResult.getFieldErrors();
        List<String> message = new ArrayList<>();
        error.setCode(-2);
        for (FieldError e : errors){
            message.add("@" + e.getField().toUpperCase() + ":" + e.getDefaultMessage());
        }
        error.setMessage("Update Failed");
        error.setCause(message.toString());
        return error;
    }
    else
    {
        Person person = personRepository.findOne(id);
        person = p;
        personRepository.save(person);
        success.setMessage("Updated Successfully");
        success.setCode(2);
        return success;
    }

Success.Java

public class Success {
int code;
String message;

public int getCode() {
    return code;
}

public void setCode(int code) {
    this.code = code;
}

public String getMessage() {
    return message;
}

public void setMessage(String message) {
    this.message = message;
}
}

Error.Java

public class Error {
int code;
String message;
String cause;

public int getCode() {
    return code;
}

public void setCode(int code) {
    this.code = code;
}

public String getMessage() {
    return message;
}

public void setMessage(String message) {
    this.message = message;
}

public String getCause() {
    return cause;
}

public void setCause(String cause) {
    this.cause = cause;
}

}

こちらもご覧ください: Spring REST Validation

6
al_mukthar

@RequestBodyでBindExceptionを取得することはできません。ここに記載されているErrorsメソッドパラメータを持つコントローラにはありません。

Errors、BindingResultコマンドオブジェクト(つまり@ModelAttribute引数)の検証とデータバインディングからのエラー、または@RequestBodyまたは@RequestPart引数。検証済みのメソッド引数の直後に、Errors、またはBindingResult引数を宣言する必要があります。

@ModelAttributeバインディングおよび検証エラーが発生し、@RequestBodyを取得します検証エラーのみ

https://docs.spring.io/spring/docs/current/spring-framework-reference/web.html#mvc-ann-methods

そして、それはここで議論されました:

https://github.com/spring-projects/spring-framework/issues/11406?jql=text%2520~%2520%2522RequestBody%2520binding%2522

私にとっては、ユーザーの観点からはまだ意味がありません。多くの場合、BindExceptionsを取得してユーザーに適切なエラーメッセージを表示することが非常に重要です。引数は、とにかくクライアント側の検証を行う必要があります。ただし、開発者がAPIを直接使用している場合、これは当てはまりません。

また、クライアント側の検証がAPIリクエストに基づいていることを想像してください。保存されたカレンダーに基づいて、指定された日付が有効かどうかを確認する必要があります。日付と時刻をバックエンドに送信すると、失敗します。

HttpMessageNotReadableExceptionに反応するExceptionHAndlerで取得した例外を変更できますが、この例外では、BindExceptionの場合のように、どのフィールドがエラーをスローしているかへの適切なアクセスがありません。例外メッセージにアクセスするには、例外メッセージを解析する必要があります。

だから私は解決策を見ていませんが、@ModelAttributeバインディングおよび検証エラーを取得するのはとても簡単です。

4
Janning

通常、Spring MVCがhttpメッセージ(リクエストボディなど)の読み取りに失敗すると、HttpMessageNotReadableException例外のインスタンスがスローされます。そのため、springがモデルにバインドできなかった場合、その例外をスローする必要があります。また、[〜#〜] not [〜#〜]を実行すると、検証エラーの場合に、メソッドパラメーターで検証される各モデルの後にBindingResultを定義します。 、springはMethodArgumentNotValidException例外をスローします。これらすべてにより、これらの2つの例外をキャッチし、望ましい方法でそれらを処理するControllerAdviceを作成できます。

@ControllerAdvice(annotations = {RestController.class})
public class UncaughtExceptionsControllerAdvice {
    @ExceptionHandler({MethodArgumentNotValidException.class, HttpMessageNotReadableException.class})
    public ResponseEntity handleBindingErrors(Exception ex) {
        // do whatever you want with the exceptions
    }
}
4
Ali Dehghani

私はこれをあきらめました。多くのカスタムコードなしでは、@RequestBodyを使用してバインディングエラーを取得することはできません。これは、@RequestBodyがSpringデータバインダーの代わりにジャクソンを使用してバインドするため、プレーンJavaBeans引数にバインドするコントローラーとは異なります。

https://jira.spring.io/browse/SPR-6740?jql=text%20~%20%22RequestBody%20binding%22 を参照してください

2

この問題を解決するための主要なブロッカーの1つは、デフォルトの eagerly-failing ジャクソンデータバインダーの性質です。最初のエラーでつまずくのではなく、解析を続行するには、何らかの方法で convince する必要があります。最終的にそれらをBindingResultエントリに変換するには、これらの解析エラーを collect する必要があります。基本的に、 catch suppress および collect 例外の解析、 convert それらをBindingResultエントリに、次に add これらのエントリを右側の_@Controller_メソッドBindingResult引数に。

catch&suppress の部分は次の方法で実行できます。

  • カスタムジャクソンデシリアライザー単にデフォルトの関連するものに委任するだけでなく、解析例外をキャッチ、抑制、および収集します
  • AOP(aspectjバージョン)を使用すると、例外を解析してデフォルトのデシリアライザーをインターセプトし、それらを抑制して収集することができます。
  • その他の手段を使用する。適切なBeanDeserializerModifier、解析例外をキャッチ、抑制、および収集することもできます。これは最も簡単なアプローチかもしれませんが、このジャクソン固有のカスタマイズサポートに関する知識が必要です

collecting 部分では、ThreadLocal変数を使用して、必要な例外関連の詳細をすべて保存できます。 conversion to BindingResultエントリと addition to the right BindingResult引数は、_@Controller_上のAOPインターセプターによって非常に簡単に実現できます。メソッド(あらゆるタイプのAOP、Springバリアントを含む)。

ゲインとは何ですか

このアプローチにより、データ binding エラー( validation onesに加えて)をBindingResult引数に取得します。 egを使用するときにそれらを取得する_@ModelAttribute_。また、複数レベルの埋め込みオブジェクトでも動作します-質問で提示された解決策は、それでニースを再生しません。

ソリューションの詳細カスタムjackson deserializers アプローチ)

ソリューションを証明する小さなプロジェクト (テストクラスを実行する)を作成しましたが、ここでは主な部分のみを強調します。

_/**
* The logic for copying the gathered binding errors 
* into the @Controller method BindingResult argument.
* 
* This is the most "complicated" part of the project.
*/
@Aspect
@Component
public class BindingErrorsHandler {
    @Before("@within(org.springframework.web.bind.annotation.RestController)")
    public void logBefore(JoinPoint joinPoint) {
        // copy the binding errors gathered by the custom
        // jackson deserializers or by other means
        Arrays.stream(joinPoint.getArgs())
                .filter(o -> o instanceof BindingResult)
                .map(o -> (BindingResult) o)
                .forEach(errors -> {
                    JsonParsingFeedBack.ERRORS.get().forEach((k, v) -> {
                        errors.addError(new FieldError(errors.getObjectName(), k, v, true, null, null, null));
                    });
                });
        // errors copied, clean the ThreadLocal
        JsonParsingFeedBack.ERRORS.remove();
    }
}

/**
 * The deserialization logic is in fact the one provided by jackson,
 * I only added the logic for gathering the binding errors.
 */
public class CustomIntegerDeserializer extends StdDeserializer<Integer> {
    /**
    * Jackson based deserialization logic. 
    */
    @Override
    public Integer deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
        try {
            return wrapperInstance.deserialize(p, ctxt);
        } catch (InvalidFormatException ex) {
            gatherBindingErrors(p, ctxt);
        }
        return null;
    }

    // ... gatherBindingErrors(p, ctxt), mandatory constructors ...
}

/**
* A simple classic @Controller used for testing the solution.
*/
@RestController
@RequestMapping("/errormixtest")
@Slf4j
public class MixBindingAndValidationErrorsController {
    @PostMapping(consumes = MediaType.APPLICATION_JSON_UTF8_VALUE)
    public Level1 post(@Valid @RequestBody Level1 level1, BindingResult errors) {
    // at the end I show some BindingResult logging for a @RequestBody e.g.:
    // {"nr11":"x","nr12":1,"level2":{"nr21":"xx","nr22":1,"level3":{"nr31":"xxx","nr32":1}}}
    // ... your whatever logic here ...
_

これらを使用すると、BindingResultに次のように表示されます。

_Field error in object 'level1' on field 'nr12': rejected value [1]; codes [Min.level1.nr12,Min.nr12,Min.Java.lang.Integer,Min]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [level1.nr12,nr12]; arguments []; default message [nr12],5]; default message [must be greater than or equal to 5]
Field error in object 'level1' on field 'nr11': rejected value [x]; codes []; arguments []; default message [null]
Field error in object 'level1' on field 'level2.level3.nr31': rejected value [xxx]; codes []; arguments []; default message [null]
Field error in object 'level1' on field 'level2.nr22': rejected value [1]; codes [Min.level1.level2.nr22,Min.level2.nr22,Min.nr22,Min.Java.lang.Integer,Min]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [level1.level2.nr22,level2.nr22]; arguments []; default message [level2.nr22],5]; default message [must be greater than or equal to 5]
_

1行目は validation エラー(@Min(5) private Integer nr12;の値として_1_を設定)によって決定され、2行目は binding one(@JsonDeserialize(using = CustomIntegerDeserializer.class) private Integer nr11;の値として_"x"_を設定)。 3行目は、バインドエラーを埋め込みオブジェクトでテストします。_level1_には_level2_オブジェクトプロパティを含む_level3_が含まれます。

他のアプローチは、残りのソリューション(AOPJsonParsingFeedBack)を維持しながら、カスタムジャクソンデシリアライザーの使用を単純に置き換える方法に注意してください。

0
adrhc

この投稿によると https://blog.codecentric.de/en/2017/11/dynamic-validation-spring-boot-validation/ -コントローラーに「エラー」パラメーターを追加できます。方法-例.

@RequestMapping(method = RequestMethod.POST)
public Profile insert(@Validated @RequestBody Profile profile, Errors errors) {
   ...
}

検証エラーがある場合は、その中に取得します。

0
anders.norgaard