相关文章推荐
拉风的包子  ·  How to solve ...·  1 年前    · 
骑白马的毛豆  ·  spring boot - ...·  2 年前    · 
从容的饭卡  ·  WPF ...·  2 年前    · 

前篇: Spring 参数校验详解

对于不同的参数解析方式,Spring 抛出的异常也不同,而且这些异常没有继承关系,异常的内部也各不相同,只能对每种异常单独处理。感觉这块地方 spring 没有设计好,处理起来比较麻烦。

跟参数相关的异常主要有三个需要手动处理。

  • org.springframework.validation. BindException
  • org.springframework.web.bind. MethodArgumentNotValidException
  • javax.validation. ConstraintViolationException
  • 一个一个说

    BindException

    抛出异常的场景

    请求参数绑定到java bean上失败时抛出

    关键词 : @Valid 、 Java bean 、表单(Content-Type: multipart/form-data)

    通过 post 提交表单的方式访问 /register 接口,如果参数校验不通过会抛出 BindException 异常

    异常的默认处理方式

    org.springframework.validation. BindException 异常由 org.springframework.web.servlet.mvc.method.annotation. handleBindException() 方法处理,如下

    异常的默认处理结果

    例子如下,密码要求8到16位,我只填了4位时 完整返回值如下

    可以看到,返回的结果过于详细,把类的内部结构都暴露了,这样肯定是不行的,我们需要对其进行自定义处理。

    自定义异常的处理

    spring 支持统一异常处理,我们可以在自定义的统一异常处理类中处理 BindException 异常

    关于 spring 统一异常处理,请参考 基于spring 的统一异常处理

    继承 org.springframework.web.servlet.mvc.method.annotation. ResponseEntityExceptionHandler 类,重写 handleBindException() 方法

    注意,类上需要打 @ControllerAdvice 注解

    @ControllerAdvice
    public class ApplicationExceptionHandler  extends ResponseEntityExceptionHandler {
        private static final Logger logger = LoggerFactory.getLogger(ApplicationExceptionHandler.class);
         * 表单绑定到 java bean 出错时抛出 BindException 异常
         * @param ex
         * @param headers
         * @param status
         * @param request
         * @return
        @Override
        protected ResponseEntity<Object> handleBindException(BindException ex, HttpHeaders headers, HttpStatus status,
                                                             WebRequest request) {
            logger.error("参数绑定失败", ex);
            if (ex.hasErrors()) {
                List<Map<String, String>> list = new ArrayList<>();
                for (ObjectError objectError : ex.getAllErrors()) {
                    Map<String, String> map = new HashMap<>();
                    if (objectError instanceof FieldError) {
                        FieldError fieldError = (FieldError) objectError;
                        map.put("field", fieldError.getField());
                        map.put("message", fieldError.getDefaultMessage());
                    } else {
                        map.put("field", objectError.getObjectName());
                        map.put("message", objectError.getDefaultMessage());
                    list.add(map);
                return new ResponseEntity<>(new FailResult<>(ApplicationEnum.PARAMETER_BIND_FAIL, list), HttpStatus.OK);
            return super.handleBindException(ex, headers, status, request);
    复制代码

    方式2

    通过 @ExceptionHandler 注解指定要处理的异常,并在处理方法中处理。

    注意,类上需要打 @ControllerAdvice 注解

    @ResponseStatus 注解用来指定http的返回状态
    @ResponseBody 注解将返回对象转换称json

    @ControllerAdvice
    public class ApplicationExceptionHandler {
        private static final Logger logger = LoggerFactory.getLogger(ApplicationExceptionHandler.class);
         * 表单绑定到 java bean 出错时抛出 BindException 异常
         * @param ex
         * @return
        @ExceptionHandler({BindException.class})
        @ResponseStatus(HttpStatus.OK)
        @ResponseBody
        protected FailResult handleBindException(BindException ex) {
            logger.error("参数绑定失败", ex);
            List<Map<String, String>> list = new ArrayList<>();
            for (ObjectError objectError : ex.getAllErrors()) {
                Map<String, String> map = new HashMap<>();
                if (objectError instanceof FieldError) {
                    FieldError fieldError = (FieldError) objectError;
                    map.put("field", fieldError.getField());
                    map.put("message", fieldError.getDefaultMessage());
                } else {
                    map.put("field", objectError.getObjectName());
                    map.put("message", objectError.getDefaultMessage());
                list.add(map);
            return new FailResult<>(ApplicationEnum.PARAMETER_BIND_FAIL, list);
    复制代码

    自定义处理后的返回结果

    MethodArgumentNotValidException

    抛出异常的场景

    请求体绑定到java bean上失败时抛出

    关键词 : @Valid 、 @RequestBody、Java bean 、表单(Content-Type: application/json、Content-Type: application/xml)

    通过 put 提交 json 的方式访问 /info 接口,如果参数校验不通过会抛出 MethodArgumentNotValidException 异常

    上面的抛出异常的场景和 BindException 的很相似。
    接口具体是抛出 MethodArgumentNotValidException 异常还是抛出 BindException 异常,取决于 @RequestBody ,或者说是取决于对请求参数的解析方式。

    异常的默认处理方式

    org.springframework.web.bind. MethodArgumentNotValidException 异常由 org.springframework.web.servlet.mvc.method.annotation. handleMethodArgumentNotValid() 方法处理,如下

    异常的默认处理结果

    如下,age 要求1-99 直接,我填的是0 完整错误如下

    可以看的,返回的数据格式和 BindException 返回的一模一样,同样暴露了类的内部结构。

    自定义异常的处理

    跟 BindException 一样,MethodArgumentNotValidException 异常同样也有两种处理方式。

  • 继承 org.springframework.web.servlet.mvc.method.annotation. ResponseEntityExceptionHandler 类,重写 handleMethodArgumentNotValid() 方法
  • 通过 @ExceptionHandler 注解指定要处理的异常,并在处理方法中处理。
  • 我这里就只介绍其中一种,另一种参考上面的 BindException

    @ControllerAdvice
    public class ApplicationExceptionHandler extends ResponseEntityExceptionHandler {
        private static final Logger logger = LoggerFactory.getLogger(ApplicationExceptionHandler.class);
         * 将请求体解析并绑定到 java bean 时,如果出错,则抛出 MethodArgumentNotValidException 异常
         * @param ex
         * @param headers
         * @param status
         * @param request
         * @return
        @Override
        protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex,
                                                                      HttpHeaders headers, HttpStatus status,
                                                                      WebRequest request) {
            logger.error("请求体绑定失败", ex);
            if (ex.getBindingResult().hasErrors()) {
                List<Map<String, String>> list = new ArrayList<>();
                for (ObjectError objectError : ex.getBindingResult().getAllErrors()) {
                    Map<String, String> map = new HashMap<>();
                    if (objectError instanceof FieldError) {
                        FieldError fieldError = (FieldError) objectError;
                        map.put("field", fieldError.getField());
                        map.put("message", fieldError.getDefaultMessage());
                    } else {
                        map.put("field", objectError.getObjectName());
                        map.put("message", objectError.getDefaultMessage());
                    list.add(map);
                return new ResponseEntity<>(new FailResult<>(ApplicationEnum.PARAMETER_BIND_FAIL, list), HttpStatus.OK);
            } else {
                return super.handleMethodArgumentNotValid(ex, headers, status, request);
    复制代码

    自定义处理后的返回结果

    ConstraintViolationException

    对非 java bean 参数的校验是 spring 框架“额外”提供的支持,需要用到 spring的@Validated注解

    抛出异常的场景

    普通参数(非 java bean)校验出错时抛出

    关键词 : @Validated 、 非Java bean

    注意,@Validated 注解需要打在类上

    异常的默认处理方式

    spring 肯定是对 ConstraintViolationException 异常进行了处理,但是目前我还没有找到在那个类中。

    自定义异常的处理

    id 要求是不能为空的,我没有填id时

    自定义异常的处理

    由于没有找到 ConstraintViolationException 异常的处理类,无法使用重写处理方法来的方式来自定义异常的处理。只能换另一种方法。

    通过 @ExceptionHandler 注解指定要处理的异常,并在处理方法中处理。

    @ControllerAdvice
    public class ApplicationExceptionHandler extends ResponseEntityExceptionHandler {
        private static final Logger logger = LoggerFactory.getLogger(ApplicationExceptionHandler.class);
         * 普通参数(非 java bean)校验出错时抛出 ConstraintViolationException 异常
         * @param e
         * @return
        @ExceptionHandler({ConstraintViolationException.class})
        @ResponseStatus(HttpStatus.OK)
        @ResponseBody
        public Result handleConstraintViolationException(ConstraintViolationException e) {
            logger.error("程序出错", e);
            List<Map<String, String>> list = new ArrayList<>();
            // e.getMessage() 的格式为 getUser.id: id不能为空, getUser.name: name不能为空
            String[] msgs = e.getMessage().split(", ");
            for(String msg : msgs){
                String[] fieldAndMsg = msg.split(": ");
                String field = fieldAndMsg[0].split("\\.")[1];
                String message = fieldAndMsg[1];
                Map<String, String> map = new HashMap<>();
                map.put("field", field);
                map.put("message", message);
                list.add(map);
            return new FailResult<>(ApplicationEnum.PARAMETER_BIND_FAIL, list);
    复制代码

    自定义处理后的返回结果

    不同的参数校验方式会产生不同的异常

  • 表单绑定到 java bean 出错时,会抛出 BindException 异常
  • 将请求体解析并绑定到 java bean 时,如果出错,则抛出 MethodArgumentNotValidException 异常
  • 普通参数(非 java bean)校验出错时,会抛出 ConstraintViolationException 异常
  • BindException 异常和 MethodArgumentNotValidException 异常默认的处理放在在 org.springframework.web.servlet.mvc.method.annotation. ResponseEntityExceptionHandler 类中,可以通过继承该类并重写其中的处理方法到达自定义处理的目的。

    ConstraintViolationException 异常也有默认的处理方法,但是我没找到,这种情况下,可以通过 @ExceptionHandler 注解指定要处理的异常,并在处理方法中处理。

    分类:
    后端
    标签: