【问题标题】:Why Spring MVC does not allow to expose Model or BindingResult to an @ExceptionHandler?为什么 Spring MVC 不允许将 Model 或 BindingResult 暴露给 @ExceptionHandler?
【发布时间】:2011-10-11 13:55:12
【问题描述】:

情况

我正在尝试对记录异常的代码进行分组,并以几种方法呈现漂亮的视图。目前,逻辑有时在@RequestHandler 本身(在 catch 块中),有时被委托给实用程序类(该类有效但将逻辑从异常发生的地方移开抛出)。

Spring 的@ExceptionHandler 似乎是将所有内容(控制器本身或父级)分组到一个位置并摆脱一些代码的方式(无需将逻辑放入 try-catch 并且没有需要一个实用程序类)...直到我意识到@ExceptionHandler 方法不会自动连接ModelMapBindingResult 参数。目前,这些对象用于呈现带有合理错误消息的视图,我们也希望记录这些对象中包含的一些信息。

问题

为什么 Spring 不支持 ModelMapBindingResult@ExceptionHandler 的方法参数?其背后的原理是什么?

可能的解决方案

在 Spring 源代码 (3.0.5) 中,该方法的参数在 HandlerMethodInvoker.invokeHandlerMethod 中解析。请求处理程序抛出的异常被捕获并重新抛出。 @ExceptionHandler 及其参数在别处解析。作为一种解决方法,我想检查异常是否实现了假设的“ModelAware”或“BindingResultAware”接口,在这种情况下,在重新抛出它之前设置 Model 和 BindingResult 属性。 听起来怎么样?

【问题讨论】:

    标签: spring-mvc exception-handling


    【解决方案1】:

    如前所述,您可以在控制器的某些方法中引发包装绑定结果对象的异常:

        if (bindingResult.hasErrors()) {
            logBindingErrors(bindingResult);
            //return "users/create";
            // Exception handling happens later in this controller
            throw new BindingErrorsException("MVC binding errors", userForm, bindingResult);
        }
    

    您的例外定义如下所示:

    public class BindingErrorsException extends RuntimeException {
        private static final Logger log = LoggerFactory.getLogger(BindingErrorsException.class); 
        private static final long serialVersionUID = -7882202987868263849L;
    
        private final UserForm userForm;
        private final BindingResult bindingResult;
    
        public BindingErrorsException(
            final String message, 
            final UserForm userForm, 
            final BindingResult bindingResult
        ) {
            super(message);
            this.userForm = userForm;
            this.bindingResult = bindingResult;
    
            log.error(getLocalizedMessage());
        }
    
        public UserForm getUserForm() {
            return userForm;
        }
    
        public BindingResult getBindingResult() {
            return bindingResult;
        }
    }
    

    接下来,您只需从引发然后捕获的异常中提取所需信息。这里假设您在控制器上定义了合适的异常处理程序。它可能在控制器建议中,甚至在其他地方。有关合适的位置,请参阅 Spring 文档。

    @ExceptionHandler(BindingErrorsException.class)
    public ModelAndView bindingErrors(
        final HttpServletResponse resp, 
        final Exception ex
    ) {
        if(ex instanceof BindingErrorsException) {
            final BindingErrorsException bex = (BindingErrorsException) ex;
            final ModelAndView mav = new ModelAndView("users/create", bex.getBindingResult().getModel());
            mav.addObject("user", bex.getUserForm());
            return mav;
        } else {
            final ModelAndView mav = new ModelAndView("users/create");
            return mav;            
        }
    }
    

    【讨论】:

      【解决方案2】:

      我不久前遇到了同样的问题。 ModelMapBindingResult@ExceptionHandler 的 JavaDocs 中明确未列为受支持的参数类型,因此这一定是故意的。

      我认为其背后的原因是抛出异常通常会使您的ModelMap 处于不一致的状态。所以根据你的情况你可以考虑

      • 显式捕获异常以告诉 Spring MVC 您知道自己在做什么(您可以使用模板模式将异常处理逻辑重构到一个地方)
      • 如果您可以控制异常层次结构,则可以将 BindingResult 移交给异常,并稍后从异常中提取它以用于呈现目的
      • 首先不抛出异常,而是使用一些结果代码(就像BeanValidation 做的例子)

      HTH

      【讨论】:

        【解决方案3】:

        改进第一个答案:

            @ExceptionHandler(value = {MethodArgumentNotValidException.class})
        @ResponseStatus(HttpStatus.BAD_REQUEST)
        @ResponseBody
        public VndErrors methodArgumentNotValidException(MethodArgumentNotValidException ex, WebRequest request) {
            List<FieldError> fieldErrors = ex.getBindingResult().getFieldErrors();
            List<ObjectError> globalErrors = ex.getBindingResult().getGlobalErrors();
            List<VndError> errors = new ArrayList<>(fieldErrors.size() + globalErrors.size());
            VndError error;
            for (FieldError fieldError : fieldErrors) {
                error = new VndError(ErrorType.FORM_VALIDATION_ERROR.toString(), fieldError.getField() + ", "
                        + fieldError.getDefaultMessage());
                errors.add(error);
            }
            for (ObjectError objectError : globalErrors) {
                error = new VndError(ErrorType.FORM_VALIDATION_ERROR.toString(),  objectError.getDefaultMessage());
                errors.add(error);
            }
            return new VndErrors(errors);
        }
        

        已经有 MethodArgumentNotValidException 已经有一个 BindingResult 对象,你可以使用它,如果你不需要为此目的创建一个特定的异常。

        【讨论】:

          【解决方案4】:

          我在将 FunctinalException “添加”到我们的 BindingResult 时遇到了同样的问题

          为了解决它,我们使用 aop,如果控制器方法抛出运行时异常(或您想要的异常), aop 捕获它并更新绑定结果或模型(如果它们是方法的参数)。

          该方法必须使用包含错误路径的特定注释进行注释(必要时可针对特定异常进行配置)。

          这不是最好的方法,因为开发人员不能忘记在其方法中添加他不使用的参数,但 Spring 并没有提供一个简单的系统来满足此需求。

          【讨论】:

            【解决方案5】:

            我也想知道这个。

            为了以允许非全局错误视图显示任何可能抛出的ConstraintViolationExceptions 的方式处理 bean 验证,我选择了类似于 @Stefan Haberl 建议的解决方案:

            显式捕获异常以告诉 Spring MVC 您知道自己在做什么(您可以使用模板模式将异常处理逻辑重构到一个地方)

            我创建了一个简单的Action 接口:

            public interface Action {
              String run();
            }
            

            还有一个ActionRunner 类,它可以确保ConstraintViolationExceptions 得到很好的处理(基本上来自每个ConstraintViolationException 的消息只是简单地添加到Set 并添加到模型中):

            public class ActionRunner {
              public String handleExceptions(Model model, String input, Action action) {
                try {
                  return action.run();
                }
                catch (RuntimeException rEx) {
                  Set<String> errors = BeanValidationUtils.getErrorMessagesIfPresent(rEx);
                  if (!errors.isEmpty()) {
                    model.addAttribute("errors", errors);
                    return input;
                  }
                  throw rEx;
                }
              }
            }
            

            Java 8 让它在控制器操作方法中运行起来非常好:

            @RequestMapping(value = "/event/save", method = RequestMethod.POST)
            public String saveEvent(Event event, Model model, RedirectAttributes redirectAttributes) {
              return new ActionRunner().handleExceptions(model, "event/form", () -> {
                eventRepository.save(event);
                redirectAttributes.addFlashAttribute("messages", "Event saved.");
                return "redirect:/events";
              });
            }
            

            这是为了总结那些我想显式处理可能由于 Bean 验证而引发的异常的操作方法。我仍然有一个全局 @ExceptionHandler 但这仅处理“哦,废话”异常。

            【讨论】:

              【解决方案6】:

              其实也可以,只需为MethodArgumentNotValidException创建一个@ExceptionHandler方法即可。

              该类使您可以访问BindingResult 对象。

              【讨论】:

                猜你喜欢
                • 2019-03-10
                • 1970-01-01
                • 2016-03-02
                • 2018-12-05
                • 1970-01-01
                • 2011-09-07
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                相关资源
                最近更新 更多