Spring Boot中BindingResult失效,如何让Spring绑定不抛出异常?
这个问题我之前也踩过坑,核心原因是:@RequestBody的参数绑定是由Jackson这类HttpMessageConverter在参数解析阶段完成的,如果此时发生类型不匹配(比如给Integer字段传字符串),错误会直接抛出HttpMessageNotReadableException,根本到不了控制器方法执行的环节,所以BindingResult自然不会被填充。
要实现你想要的“用BindingResult自行处理绑定错误”的需求,我们需要把Jackson的反序列化错误转化为Spring的校验错误,让BindingResult能捕获到这些问题。下面是具体的实现步骤:
1. 自定义Jackson反序列化错误处理器
首先创建一个DeserializationProblemHandler的子类,用来捕获Jackson反序列化时的各种类型错误,并把这些错误存起来:
import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.deser.DeserializationProblemHandler; import org.springframework.validation.FieldError; import org.springframework.validation.ObjectError; import java.util.ArrayList; import java.util.List; public class ValidationDeserializationProblemHandler extends DeserializationProblemHandler { private final List<ObjectError> errors = new ArrayList<>(); // 捕获类型不匹配、意外Token等常见反序列化错误 @Override public boolean handleUnexpectedToken(DeserializationContext ctxt, Class<?> targetType, com.fasterxml.jackson.core.JsonToken t, com.fasterxml.jackson.core.JsonParser p, String failureMsg) { String fieldName = p.getCurrentName(); errors.add(new FieldError(targetType.getSimpleName(), fieldName, failureMsg)); return true; // 告诉Jackson我们已处理错误,继续完成反序列化 } @Override public boolean handleWrongTokenException(DeserializationContext ctxt, com.fasterxml.jackson.databind.exc.WrongTokenException e) { String fieldName = e.getPath().isEmpty() ? "" : e.getPath().get(0).getFieldName(); errors.add(new FieldError(e.getTargetType().getSimpleName(), fieldName, e.getMessage())); return true; } // 可以根据需要添加更多错误处理方法,比如handleInstantiationProblem等 public List<ObjectError> getErrors() { return errors; } }
2. 创建自定义校验器,将Jackson错误注入BindingResult
接下来写一个Validator,把上面处理器捕获的错误添加到Spring的Errors(也就是BindingResult)中:
import org.springframework.validation.Errors; import org.springframework.validation.Validator; public class JacksonValidationValidator implements Validator { private final ValidationDeserializationProblemHandler problemHandler; public JacksonValidationValidator(ValidationDeserializationProblemHandler problemHandler) { this.problemHandler = problemHandler; } @Override public boolean supports(Class<?> clazz) { return true; // 支持所有类型的校验 } @Override public void validate(Object target, Errors errors) { // 将Jackson捕获的错误全部添加到BindingResult里 problemHandler.getErrors().forEach(errors::addError); } }
3. 配置WebMvc,整合Jackson和校验器
在Spring配置类中,把我们的Jackson处理器和自定义校验器注册进去,让Spring在校验阶段把Jackson的错误整合到BindingResult:
import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.validation.Validator; import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration public class WebConfig implements WebMvcConfigurer { // 实例化我们的Jackson错误处理器 private final ValidationDeserializationProblemHandler deserializationProblemHandler = new ValidationDeserializationProblemHandler(); @Bean public ObjectMapper objectMapper() { return new ObjectMapper() .addHandler(deserializationProblemHandler); // 注册错误处理器 } @Override public Validator getValidator() { // 获取默认的JSR-380校验器(处理@NotNull、@Size等注解) LocalValidatorFactoryBean defaultValidator = new LocalValidatorFactoryBean(); // 组合默认校验器和我们的Jackson错误校验器 return (target, errors) -> { defaultValidator.validate(target, errors); new JacksonValidationValidator(deserializationProblemHandler).validate(target, errors); }; } }
4. 修改控制器方法,开启校验
最后,在你的控制器方法中,给@RequestBody参数加上@Valid注解,这样Spring会自动触发校验,包括Jackson反序列化产生的错误:
@PostMapping public ResponseEntity<Book> saveBook( @Valid @RequestBody Book book, BindingResult bindingResult, HttpServletRequest request ) { if (bindingResult.hasErrors()) { throw new RestArgumentValidationException(bindingResult, request.getRequestURI()); } return new ResponseEntity<>(bookRepository.save(book), HttpStatus.CREATED); }
这样配置后,当你发送无效的类型输入时,Jackson的反序列化错误会被转化为校验错误并填充到BindingResult里,你就可以通过bindingResult.hasErrors()自行处理这些错误,而不会再直接抛出HttpMessageNotReadableException了。
内容的提问来源于stack exchange,提问作者user335870

