BindingResult 方法参数的存在决定了抛出异常吗?

Rod*_*eas 8 java spring-mvc bean-validation spring-boot

我有一个 Spring @RestController,它的 POST 端点定义如下:

@RestController
@Validated
@RequestMapping("/example")
public class Controller {

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public ResponseEntity<?> create(@Valid @RequestBody Request request,
                                    BindingResult _unused, // DO NOT DELETE
                                    UriComponentsBuilder uriBuilder) {
        // ...
    }
}
Run Code Online (Sandbox Code Playgroud)

它还有一个异常处理程序javax.validation.ConstraintViolationException

@ExceptionHandler({ConstraintViolationException.class})
@ResponseStatus(HttpStatus.BAD_REQUEST)
ProblemDetails handleValidationError(ConstraintViolationException e) {...}
Run Code Online (Sandbox Code Playgroud)

我们的 Spring-Boot 应用程序用于spring-boot-starter-validation验证。该Request对象使用javax.validation.*注释将约束应用到各个字段,如下所示:

public class Request {

    private Long id;

    @Size(max = 64, message = "name length cannot exceed 64 characters")
    private String name;

    // ...
}
Run Code Online (Sandbox Code Playgroud)

如上所述,如果您使用无效请求 POST 请求,验证将抛出 ConstraintViolationException,该异常将由异常处理程序处理。这有效,我们对其进行了单元测试,一切都很好。

我注意到BindingResult没有使用 in the post 方法(名称_unused和评论//DO NOT DELETE有点危险信号。)我继续删除了参数。突然间,我的测试失败了——入站请求仍然经过验证,但它不再抛出 ConstraintValidationException ...现在它抛出MethodArgumentNotValidException!不幸的是,我无法使用这个其他异常,因为它不包含我需要的格式的失败验证(也不包含我需要的所有数据)。

为什么BindingResult参数列表中的存在控制抛出哪个异常?ConstraintViolationException当 javax.validation 确定请求正文无效时,如何删除未使用的变量并仍然抛出异常?


Spring-Boot 2.5.5

  • spring-boot-启动器-web
  • spring-boot-starter-验证

OpenJDK 17。

Ken*_*han 14

这里涉及两层验证,按以下顺序发生:

  1. 控制器层

    • 当控制器方法的参数用@RequestBodyor@ModelAttribute@Validor@Validated或名称以“Valid”开头的任何注释进行注释时启用(请参阅逻辑)。
    • 基于DataBinder东西
    • 只能验证请求
    • BindingResult如果出现验证错误并且控制器方法中没有参数,则抛出异常org.springframework.web.bind.MethodArgumentNotValidExceptionBindingResult否则,继续使用捕获验证错误信息的参数调用控制器方法。
  2. Bean的方法层

    • 如果 spring bean 带有注释,@Validated并且方法参数或返回值仅带有 bean 验证注释,则启用,例如@Valid@Size等)
    • 基于AOP的东西。方法拦截器是MethodValidationInterceptor
    • 可以验证请求和响应
    • 如果出现验证错误,则抛出javax.validation.ConstraintViolationException

最后两层中的验证将委托给 bean 验证来执行实际验证。

因为控制器实际上是一个 spring bean ,所以在调用控制器方法时,两层中的验证都会生效,您的案例完全证明了这一点,并发生以下情况:

  1. DataBinder验证请求不正确,但由于控制器方法有BindingResult参数,因此它会跳过抛出MethodArgumentNotValidException并继续调用控制器方法

  2. MethodValidationInterceptor验证请求不正确,并抛出ConstraintViolationException

文件并没有明确提及此类行为。以上是我阅读源码后总结的。我同意这很令人困惑,尤其是在您的情况下,当在两个层中都启用验证并且还与BindingResult。您可以看到 bean 验证实际上验证了请求两次,这听起来很尴尬......

因此,要解决您的问题,您可以禁用控制器层中的验证DataBinder并始终依赖于 bean 方法级别验证。

要对所有控制器全局禁用它,您可以@ControllerAdvice使用以下@InitBinder方法创建一个:

@ControllerAdvice
public class InitBinderControllerAdvice {

    @InitBinder
    private void initBinder(WebDataBinder binder) {
        binder.setValidator(null);
    }
} 
Run Code Online (Sandbox Code Playgroud)

要仅对一个控制器禁用它,您可以将此@InitBinder方法添加到该控制器:

@RestController
@Validated
@RequestMapping("/example")
public class Controller {

    @InitBinder
    public void initBinder(WebDataBinder binder) {
        binder.setValidator(null);
   }

}
Run Code Online (Sandbox Code Playgroud)

然后,即使从控制器方法中删除BindingResult,它也应该抛出ConstraintViolationException

  • 是的。您可以将“@InitBinder”放入控制器,如果您不希望它全局应用,则该控制器仅对该控制器生效。 (2认同)