存在绑定结果方法参数确定引发的异常



我有一个 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) {
// ...
}
}

它还具有用于javax.validation.ConstraintViolationException的异常处理程序:

@ExceptionHandler({ConstraintViolationException.class})
@ResponseStatus(HttpStatus.BAD_REQUEST)
ProblemDetails handleValidationError(ConstraintViolationException e) {...}

我们的 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;
// ...
}

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

我注意到没有使用 post 方法中的BindingResult(名称_unused和评论//DO NOT DELETE是一种危险信号。我继续删除了参数。突然之间,我的测试中断了 - 入站请求仍然被验证,但它将不再抛出约束验证异常...现在它抛出了一个MethodArgumentNotValidException!不幸的是,我不能使用这个其他异常,因为它不包含我需要的格式的失败验证(并且也不包含我需要的所有数据)。

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

<小时 />

弹簧靴 2.5.5

  • 弹簧启动启动网
  • 弹簧启动启动器验证

OpenJDK 17.

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

  1. 控制器层

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

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

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

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

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

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

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

所以为了解决你的问题,你可以在控制器层的DataBinder中禁用验证,并始终依赖于 Bean 方法级别的验证。

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

@ControllerAdvice
public class InitBinderControllerAdvice {
@InitBinder
private void initBinder(WebDataBinder binder) {
binder.setValidator(null);
}
} 

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

@RestController
@Validated
@RequestMapping("/example")
public class Controller {
@InitBinder
public void initBinder(WebDataBinder binder) {
binder.setValidator(null);
}
}

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

我不知道控制器方法中存在BindingResult可以修改引发的异常类型,因为我以前从未将其作为参数添加到控制器方法中。我通常看到的是请求正文验证失败引发MethodArgumentNotValidException,以及请求参数、路径变量和标头值违规引发ConstraintViolationExceptionMethodArgumentNotValidException中的错误详细信息的格式可能与ConstraintViolationException中的格式不同,但它通常包含您需要的有关错误的所有信息。下面是我为您的控制器编写的异常处理程序类:

package com.example.demo;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
public class ControllerExceptionHandler {
public static final Logger LOGGER = LoggerFactory.getLogger(ControllerExceptionHandler.class);
@ExceptionHandler({ ConstraintViolationException.class })
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Map<String, Object> handleValidationError(ConstraintViolationException exception) {
LOGGER.warn("ConstraintViolationException thrown", exception);
Map<String, Object> response = new HashMap<>();
List<Map<String, String>> errors = new ArrayList<>();
for (ConstraintViolation<?> violation : exception.getConstraintViolations()) {
Map<String, String> transformedError = new HashMap<>();

String fieldName = violation.getPropertyPath().toString();
transformedError.put("field", fieldName.substring(fieldName.lastIndexOf('.') + 1));
transformedError.put("error", violation.getMessage());
errors.add(transformedError);
}
response.put("errors", errors);
return response;
}
@ExceptionHandler({ MethodArgumentNotValidException.class })
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Map<String, Object> handleValidationError(MethodArgumentNotValidException exception) {
LOGGER.warn("MethodArgumentNotValidException thrown", exception);
Map<String, Object> response = new HashMap<>();
if (exception.hasFieldErrors()) {
List<Map<String, String>> errors = new ArrayList<>();
for (FieldError error : exception.getFieldErrors()) {
Map<String, String> transformedError = new HashMap<>();
transformedError.put("field", error.getField());
transformedError.put("error", error.getDefaultMessage());
errors.add(transformedError);
}
response.put("errors", errors);
}
return response;
}
}

它将MethodArgumentNotValidExceptionConstraintViolationException转换为相同的错误响应 JSON:

{
"errors": [
{
"field": "name",
"error": "name length cannot exceed 64 characters"
}
]
}

ConstraintViolationException相比,您在MethodArgumentNotValidException中缺少哪些信息?

从规范:

https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/bind/MethodArgumentNotValidException.html

@Valid @RequestBody Request request

如果你的对象无效,我们总是得到一个 MethodArgumentNotValidException。这里的区别取决于绑定结果...

如果没有 BindingResult,则会按预期抛出 MethodArgumentNotValidException。

使用BindingResult,错误将入到BindingResult中。我们经常需要检查 bindresult 是否有错误并对其进行处理。

if (bindingResult.hasErrors()) {  
// handle error or create bad request status
}

BindingResult:"表示绑定结果的常规接口。扩展错误注册功能的接口,允许应用验证程序,并添加特定于绑定的分析和模型构建。">

您可以再次仔细检查绑定结果中的错误。 我没有看到完整的代码,所以我不知道哪个是约束违规异常的原因,但我想你跳过绑定结果中的错误并继续将实体插入数据库并违反一些约束......