REST 控制器中的 Spring Boot 绑定和验证错误处理
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/34728144/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
Spring Boot binding and validation error handling in REST controller
提问by Jaap van Hengstum
When I have the following model with JSR-303 (validation framework) annotations:
当我有以下带有 JSR-303(验证框架)注释的模型时:
public enum Gender {
MALE, FEMALE
}
public class Profile {
private Gender gender;
@NotNull
private String name;
...
}
and the following JSON data:
以及以下 JSON 数据:
{ "gender":"INVALID_INPUT" }
In my REST controller, I want to handle both the binding errors (invalid enum value for gender
property) and validation errors (name
property cannot be null).
在我的 REST 控制器中,我想处理绑定错误(gender
属性的无效枚举值)和验证错误(name
属性不能为空)。
The following controller method does NOT work:
以下控制器方法不起作用:
@RequestMapping(method = RequestMethod.POST)
public Profile insert(@Validated @RequestBody Profile profile, BindingResult result) {
...
}
This gives com.fasterxml.Hymanson.databind.exc.InvalidFormatException
serialization error before binding or validation takes place.
这会com.fasterxml.Hymanson.databind.exc.InvalidFormatException
在绑定或验证发生之前产生序列化错误。
After some fiddling, I came up with this custom code which does what I want:
经过一番摆弄之后,我想出了这个自定义代码,它可以满足我的需求:
@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.
...
}
Basically what this code does, is serialize to a generic map instead of model and then use custom code to bind to model and check for errors.
基本上这段代码的作用是序列化为通用映射而不是模型,然后使用自定义代码绑定到模型并检查错误。
I have the following questions:
我有以下问题:
- Is custom code the way to go here or is there a more standard way of doing this in Spring Boot?
- How does the
@Validated
annotation work? How can I make my own custom annotation that works like@Validated
to encapsulate my custom binding code?
- 自定义代码是这里的方式,还是在 Spring Boot 中有更标准的方式来做到这一点?
@Validated
注释是如何工作的?如何制作我自己的自定义注释,其作用类似于@Validated
封装我的自定义绑定代码?
回答by Ali Dehghani
Usually when Spring MVC fails to read the http messages (e.g. request body), it will throw an instance of HttpMessageNotReadableException
exception. So, if spring could not bind to your model, it should throw that exception. Also, if you do NOTdefine a BindingResult
after each to-be-validated model in your method parameters, in case of a validation error, spring will throw a MethodArgumentNotValidException
exception. With all this, you can create ControllerAdvice
that catches these two exceptions and handles them in your desirable way.
通常当 Spring MVC 无法读取 http 消息(例如请求正文)时,它会抛出一个HttpMessageNotReadableException
异常实例。因此,如果 spring 无法绑定到您的模型,它应该抛出该异常。此外,如果您没有BindingResult
在方法参数中定义每个待验证模型之后,则在验证错误的情况下,spring 将抛出MethodArgumentNotValidException
异常。有了这一切,您可以创建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
}
}
回答by al_mukthar
This is the code what i have used in one of my project for validating REST api in spring boot,this is not same as you demanded,but is identical.. check if this helps
这是我在我的一个项目中使用的代码,用于在 Spring Boot 中验证 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
成功.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
错误.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;
}
}
You can also have a look here : Spring REST Validation
您也可以在这里查看:Spring REST Validation
回答by Janning
You can't get BindException with @RequestBody. Not in the controller with an Errors
method parameter as documented here:
你不能用@RequestBody 得到 BindException。不在带有Errors
方法参数的控制器中,如此处所述:
Errors, BindingResultFor access to errors from validation and data binding for a command object (that is, a @ModelAttribute argument) or errors from the validation of a @RequestBody or @RequestPart arguments. You must declare an Errors, or BindingResult argument immediately after the validated method argument.
Errors, BindingResult用于访问来自命令对象(即@ModelAttribute 参数)的验证和数据绑定的错误或来自@RequestBody 或@RequestPart 参数的验证的错误。您必须在经过验证的方法参数之后立即声明 Errors 或 BindingResult 参数。
It states that for @ModelAttribute
you get binding AND validation errors and for your @RequestBody
you get validation errors only.
它指出,@ModelAttribute
您会收到绑定和验证错误,而您只会@RequestBody
收到验证错误。
https://docs.spring.io/spring/docs/current/spring-framework-reference/web.html#mvc-ann-methods
https://docs.spring.io/spring/docs/current/spring-framework-reference/web.html#mvc-ann-methods
And it was discussed here:
并在这里讨论:
For me it still does not make sense from a user point of view. It is often very important to get the BindExceptions to show the user a proper error message. The argument is, you should do client side validation anyway. But this is not true if a developer is using the API directly.
对我来说,从用户的角度来看,这仍然没有意义。获取 BindExceptions 以向用户显示正确的错误消息通常非常重要。论点是,无论如何您都应该进行客户端验证。但如果开发人员直接使用 API,则情况并非如此。
And imagine your client side validation is based on an API request. You want to check if a given date is valid based on a saved calendar. You send the date and time to the backend and it just fails.
假设您的客户端验证基于 API 请求。您想根据保存的日历检查给定日期是否有效。您将日期和时间发送到后端,但它失败了。
You can modify the exception you get with an ExceptionHAndler reacting on HttpMessageNotReadableException, but with this exception I do not have proper access to which field was throwing the error as with a BindException. I need to parse the exception message to get access to it.
您可以使用 ExceptionHANdler 对 HttpMessageNotReadableException 做出反应来修改您获得的异常,但由于此异常,我无法像 BindException 那样正确访问哪个字段引发错误。我需要解析异常消息才能访问它。
So I do not see any solution, which is kind of bad because with @ModelAttribute
it is so easy to get binding AND validation errors.
所以我没有看到任何解决方案,这有点糟糕,因为@ModelAttribute
它很容易出现绑定和验证错误。
回答by Jaap van Hengstum
I've given up on this; it is just not possible to get the binding errors using @RequestBody
without a lot of custom code. This is different from controllers binding to plain JavaBeans arguments because @RequestBody
uses Hymanson to bind instead of the Spring databinder.
我已经放弃了;如果@RequestBody
没有大量自定义代码,就不可能获得绑定错误。这与绑定到普通 JavaBeans 参数的控制器不同,因为@RequestBody
使用 Hymanson 而不是 Spring 数据绑定器进行绑定。
See https://jira.spring.io/browse/SPR-6740?jql=text%20~%20%22RequestBody%20binding%22
见https://jira.spring.io/browse/SPR-6740?jql=text%20~%20%22RequestBody%20binding%22
回答by anders.norgaard
According to this post https://blog.codecentric.de/en/2017/11/dynamic-validation-spring-boot-validation/- you can add an extra parameter "Errors" to your controller method - eg.
根据这篇文章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) {
...
}
to then get validation errors, if any, in that.
然后在其中获取验证错误(如果有)。
回答by adrhc
One of the main blocker for solving this problem is the default eagerly-failingnature of the Hymanson data binder; one would have to somehow convinceit to continue parsing instead of just stumble at first error. One would also have to collectthese parsing errors in order to ultimately convert them to BindingResult
entries. Basically one would have to catch, suppressand collectparsing exceptions, convertthem to BindingResult
entries then addthese entries to the right @Controller
method BindingResult
argument.
解决这个问题的主要障碍之一是 Hymanson 数据绑定器的默认急切失败特性;人们必须以某种方式说服它继续解析,而不是在第一个错误时绊倒。人们还必须收集这些解析错误,以便最终将它们转换为BindingResult
条目。基本上,必须捕获、抑制和收集解析异常,将它们转换为BindingResult
条目,然后将这些条目添加到正确的@Controller
方法BindingResult
参数中。
The catch & suppresspart could be done by:
在捕捉和抑制部分可以这样做:
- custom Hymanson deserializerswhich would simply delegate to the default related ones but would also catch, suppress and collect their parsing exceptions
- using
AOP
(aspectj version) one could simply intercept the default deserializers parsing exceptions, suppress and collect them - using other means, e.g. appropriate
BeanDeserializerModifier
, one could also catch, suppress and collect the parsing exceptions; this might be the easiest approach but requires some knowledge about this Hymanson specific customization support
- 自定义 Hymanson 反序列化器,它会简单地委托给默认相关的反序列化器,但也会捕获、抑制和收集它们的解析异常
- 使用
AOP
(aspectj 版本)可以简单地拦截解析异常的默认反序列化器,抑制并收集它们 - 使用其他方式,例如适当的
BeanDeserializerModifier
,还可以捕获、抑制和收集解析异常;这可能是最简单的方法,但需要一些有关此Hyman逊特定自定义支持的知识
The collectingpart could use a ThreadLocal
variable to store all necessary exceptions related details. The conversionto BindingResult
entries and the additionto the right BindingResult
argument could be pretty easily accomplished by an AOP
interceptor on @Controller
methods (any type of AOP
, Spring variant including).
将收集的部分可以使用一个ThreadLocal
变量来存储所有必要的异常相关的细节。通过方法上的拦截器(任何类型的,包括 Spring 变体)可以很容易地完成到条目的转换BindingResult
和对正确参数的添加。BindingResult
AOP
@Controller
AOP
What's the gain
有什么收获
By this approach one gets the data bindingerrors (in addition to the validationones) into the BindingResult
argument the same way as would expect for getting them when using an e.g. @ModelAttribute
. It will also work with multiple levels of embedded objects - the solution presented in the question won't play nice with that.
通过这种方法,可以将数据绑定错误(除了验证错误之外)以与BindingResult
使用 eg 时获取它们相同的方式获取到参数中@ModelAttribute
。它也适用于多个级别的嵌入式对象 - 问题中提出的解决方案不会很好地解决这个问题。
Solution Details(custom Hymanson deserializersapproach)
解决方案详情(自定义Hyman逊解串器方法)
I created a small project proving the solution(run the test class) while here I'll just highlight the main parts:
我创建了一个小项目来证明解决方案(运行测试类),而在这里我只强调主要部分:
/**
* 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
// Hymanson 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 Hymanson,
* I only added the logic for gathering the binding errors.
*/
public class CustomIntegerDeserializer extends StdDeserializer<Integer> {
/**
* Hymanson 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 ...
With these you'll get in BindingResult
something like this:
有了这些,你会得到这样的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]
where the 1th line is determined by a validationerror (setting 1
as the value for a @Min(5) private Integer nr12;
) while the 2nd is determined by a bindingone (setting "x"
as value for a @JsonDeserialize(using = CustomIntegerDeserializer.class) private Integer nr11;
). 3rd line tests binding errorswith embedded objects: level1
contains a level2
which contains a level3
object property.
其中第 1 行由验证错误(设置1
为 a 的值@Min(5) private Integer nr12;
)确定,而第 2 行由绑定错误确定(设置"x"
为 a 的值@JsonDeserialize(using = CustomIntegerDeserializer.class) private Integer nr11;
)。第 3 行测试嵌入对象的绑定错误:level1
包含一个level2
包含level3
对象属性的。
Note how other approachescould simply replace the usage of custom Hymanson deserializerswhile keeping the rest of the solution (AOP
, JsonParsingFeedBack
).
请注意其他方法如何可以简单地替换自定义 Hymanson 反序列化器的使用,同时保留解决方案的其余部分 ( AOP
, JsonParsingFeedBack
)。
回答by Sebin Vincent
I think I should answer your questions in reverse order.
我想我应该以相反的顺序回答你的问题。
For your second question, The @Validate annotation throws MethodArgumentNotValidException if there is an error during field validation. The object of this annotation contains two methods, getBindingResult(),getAllErrors() which gives details of validation error. You may create your custom annotation with AspectJ (AOP). But that's not needed here. Because your situation can be solved using the ExceptionHandler of SpringBoot.
对于您的第二个问题,如果字段验证期间出现错误,@Validate 注释会抛出 MethodArgumentNotValidException。该注解的对象包含两个方法,getBindingResult(),getAllErrors() 提供验证错误的详细信息。您可以使用 AspectJ (AOP) 创建自定义注释。但是这里不需要。因为你的情况可以使用SpringBoot的ExceptionHandler来解决。
Now your first question,
现在你的第一个问题,
Please go through section 5of this link Link. Actually it covers whole bean validation in spring boot. Your problem can be solved by section 5. Basic knowledge on general exception handling in spring boot may be good to understand it better. For that, I can share the query link on google for this topic ExceptionHandling.Please go through the first few results of it.
请浏览此链接Link 的第5部分。实际上它涵盖了 Spring Boot 中的整个 bean 验证。您的问题可以通过第5节解决。Spring Boot 中一般异常处理的基础知识可能有助于更好地理解它。为此,我可以在 google 上分享这个主题ExceptionHandling的查询链接。请查看它的前几个结果。