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.
...
}
基本的にこのコードが行うことは、モデルの代わりに汎用マップにシリアル化し、カスタムコードを使用してモデルにバインドし、エラーをチェックします。
次の質問があります。
@Validated
アノテーションはどのように機能しますか? @Validated
のように動作してカスタムバインディングコードをカプセル化する独自のカスタムアノテーションを作成するにはどうすればよいですか。これは、春のブートで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
@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
そして、それはここで議論されました:
私にとっては、ユーザーの観点からはまだ意味がありません。多くの場合、BindExceptionsを取得してユーザーに適切なエラーメッセージを表示することが非常に重要です。引数は、とにかくクライアント側の検証を行う必要があります。ただし、開発者がAPIを直接使用している場合、これは当てはまりません。
また、クライアント側の検証がAPIリクエストに基づいていることを想像してください。保存されたカレンダーに基づいて、指定された日付が有効かどうかを確認する必要があります。日付と時刻をバックエンドに送信すると、失敗します。
HttpMessageNotReadableExceptionに反応するExceptionHAndlerで取得した例外を変更できますが、この例外では、BindExceptionの場合のように、どのフィールドがエラーをスローしているかへの適切なアクセスがありません。例外メッセージにアクセスするには、例外メッセージを解析する必要があります。
だから私は解決策を見ていませんが、@ModelAttribute
バインディングおよび検証エラーを取得するのはとても簡単です。
通常、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
}
}
私はこれをあきらめました。多くのカスタムコードなしでは、@RequestBody
を使用してバインディングエラーを取得することはできません。これは、@RequestBody
がSpringデータバインダーの代わりにジャクソンを使用してバインドするため、プレーンJavaBeans引数にバインドするコントローラーとは異なります。
https://jira.spring.io/browse/SPR-6740?jql=text%20~%20%22RequestBody%20binding%22 を参照してください
この問題を解決するための主要なブロッカーの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
_が含まれます。
他のアプローチは、残りのソリューション(AOP
、JsonParsingFeedBack
)を維持しながら、カスタムジャクソンデシリアライザーの使用を単純に置き換える方法に注意してください。
この投稿によると 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) {
...
}
検証エラーがある場合は、その中に取得します。