如何将ConstraintViolationException 500错误转换为400错误请求?

29

如果我使用@NotNull这样的约束,然后在控制器中

public User createUser(
            @Validated
            @RequestBody User user) {}

它会给出一个带有详细信息的400异常提示。

但是,如果我使用自己的定制验证器,像这样:

public User createUser(
            @UserConstraint
            @RequestBody User user) {}

它会抛出一个500服务器错误,就像这样:

javax.validation.ConstraintViolationException: createUser.user: Error with field: 'test35'
    at org.springframework.validation.beanvalidation.MethodValidationInterceptor.invoke(MethodValidationInterceptor.java:117) ~[spring-context-5.1.10.RELEASE.jar:5.1.10.RELEASE]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) ~[spring-aop-5.1.10.RELEASE.jar:5.1.10.RELEASE]
    at org.springframework.security.access.intercept.aopalliance.MethodSecurityInterceptor.invoke(MethodSecurityInterceptor.java:69) ~[spring-security-core-5.1.6.RELEASE.jar:5.1.6.RELEASE]

有没有办法让响应获得漂亮的400消息?

理想情况下,400消息应该与Spring的验证JSON相同。

{
    "timestamp": "2019-10-30T02:33:15.489+0000",
    "status": 400,
    "error": "Bad Request",
    "errors": [
        {
            "codes": [
                "Size.user.lastName",
                "Size.lastName",
                "Size.java.lang.String",
                "Size"
            ],
            "arguments": [
                {
                    "codes": [
                        "user.lastName",
                        "lastName"
                    ],
                    "arguments": null,
                    "defaultMessage": "lastName",
                    "code": "lastName"
                },
                25,
                1
            ],
            "defaultMessage": "size must be between 1 and 25",
            "objectName": "user",
            "field": "lastName",
            "rejectedValue": "",
            "bindingFailure": false,
            "code": "Size"
        }
    ],
    "message": "Validation failed for object='user'. Error count: 1",
    "path": "/api/v1/users"
}

问题及解决方案:https://github.com/spring-projects/spring-boot/issues/10471#issuecomment-446841640 - chill appreciator
3个回答

17

是的,您可以创建一个自定义错误处理程序,以便在响应和状态中添加任何内容。这是更改状态的简单方法:

1.- 当抛出ConstraintViolationException时,更改status的简单方法。

import javax.validation.ConstraintViolationException;

@ControllerAdvice
public class CustomErrorHandler {

    @ExceptionHandler(ConstraintViolationException.class)
    public void handleConstraintViolationException(ConstraintViolationException exception,
            ServletWebRequest webRequest) throws IOException {
        webRequest.getResponse().sendError(HttpStatus.BAD_REQUEST.value(), exception.getMessage());
    }
}    

2. - 当发生ConstraintViolationException异常时,自定义响应的方法。

@ControllerAdvice
public class CustomErrorHandler {

    @ExceptionHandler(ConstraintViolationException.class)
    public ResponseEntity<CustomError> handleConstraintViolationException(ConstraintViolationException exception) {
        CustomError customError = new CustomError();
        customError.setStatus(HttpStatus.BAD_REQUEST);
        customError.setMessage(exception.getMessage());
        customError.addConstraintErrors(exception.getConstraintViolations());
        return ResponseEntity.badRequest().body(customError);
    }
}   

1
我想将JSON格式化为与Spring的JSON相同。你知道怎么做吗?我已经编辑了问题以显示格式。 - erotsppa
最重要的是,在MVC环境中,该如何继续执行控制器?Spring会抛出400错误,即使我已经使用BindingResult参数捕获了所有的错误,并将它们显示在提交表单中。 - TheRealChx101
@TheRealChx101,请提出一个问题,这样我才能帮助你完成你想要做的事情。 - Jonathan JOhx
2
这不是一个好的解决方案,这将使所有的 ConstraintViolationException 都返回 400,而当问题只是希望控制器验证失败时返回 400... 将控制器异常与数据源异常分开是很重要的,这个解决方案会给整个项目的异常处理带来一堆麻烦。 - Rafael Lima
1
加入到 Rafael,这是一个毫无疑问的初级方法。如果你这样做,你没有考虑任何事情,只是为了以最简单直接的方式解决你的问题。一个底层的数据库验证也可能冒泡到这一点,返回给你 http 400,而在那个级别上的约束违规通常是编码错误,因此是服务器错误,而不是客户端错误。在任何情况下都不要使用这种方法。 - undefined
显示剩余2条评论

13

由于上面的解决方案并没有产生预期的结果,这里提供一个可能会有帮助的链接:https://sterl.org/2020/02/spring-boot-hateoas-jsr303-validation/

有趣的是,如果类或方法请求体被注释为@Validated,则Spring的行为会有所不同。

换句话说,在类中,您可能会遇到500错误。如果您将验证注释移到方法中(就像您已经做的那样),则正常行为应该是400。

长话短说,一旦您拥有自己的contains等功能,您需要稍微调整一下--因为在Spring中,它是MethodArgumentNotValidException而不是ConstraintViolationException,Spring已经作为控制器建议了。

可以快速解决问题:

@Autowired
private MessageSource messageSource;

@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(ConstraintViolationException.class)
public @ResponseBody Map<String, Object> handleConstraintViolation(ConstraintViolationException e, ServletWebRequest request) {
    // emulate Spring DefaultErrorAttributes
    final Map<String, Object> result = new LinkedHashMap<>();
    result.put("timestamp", new Date());
    result.put("path", request.getRequest().getRequestURI());
    result.put("status", HttpStatus.BAD_REQUEST.value());
    result.put("error", HttpStatus.BAD_REQUEST.getReasonPhrase());
    result.put("message", e.getMessage());
    result.put("errors", e.getConstraintViolations().stream().map(cv -> SimpleObjectError.from(cv, messageSource, request.getLocale())));
    return result;
}

@Getter @ToString
static class SimpleObjectError {
    String defaultMessage;
    String objectName;
    String field;
    Object rejectedValue;
    String code;

    public static SimpleObjectError from(ConstraintViolation<?> violation, MessageSource msgSrc, Locale locale) {
        SimpleObjectError result = new SimpleObjectError();
        result.defaultMessage = msgSrc.getMessage(violation.getMessageTemplate(),
                new Object[] { violation.getLeafBean().getClass().getSimpleName(), violation.getPropertyPath().toString(),
                        violation.getInvalidValue() }, violation.getMessage(), locale);
        result.objectName = Introspector.decapitalize(violation.getRootBean().getClass().getSimpleName());
        result.field = String.valueOf(violation.getPropertyPath());
        result.rejectedValue = violation.getInvalidValue();
        result.code = violation.getMessageTemplate();
        return result;
    }
}

e.getConstraintViolations().stream().map(...) 之后,你应该将 Stream 收集到 List 中。 - Krzysztof

2

简单说,只需在一个使用了@ControllerAdvice注解的类中定义一个用@ExceptionHandler注解修饰的方法:

@ControllerAdvice
public class YourControllerAdvice {

    @ResponseBody
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(ConstraintViolationException.class)
    public void handleConstraintViolationException() {
    // Intentionally left blank
    }
}

被标记为@ControllerAdvice的类用于处理控制器层级的异常。


2
如果您将方法留空,则响应主体将为空。此外,您可以将此建议扩展到各种异常:@ExceptionHandler(value = {ConstraintViolationException.class, ValidationException.class}) - chill appreciator

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接