为什么Spring MVC不允许将Model或BindingResult暴露给@ExceptionHandler?

时间:2011-07-23 09:04:26

标签: spring-mvc exception-handling

情况

我正在尝试对记录异常的代码进行分组,并在一些方法中呈现一个漂亮的视图。目前逻辑在@RequestHandler本身的某个时刻(在 catch 块中),其他任务被委托给一个实用程序类(它可以工作,但会将逻辑从远离它的地方移开)抛出异常)。

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

问题

为什么Spring不支持ModelMap的{​​{1}}或BindingResult等方法参数?它背后的理由是什么?

可能的解决方案

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

6 个答案:

答案 0 :(得分:4)

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

    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;            
    }
}

答案 1 :(得分:1)

我刚才遇到了同样的问题。 ModelMapBindingResult显式未在@ExceptionHandler的JavaDocs中列为受支持的参数类型,因此这必须是故意的。

我认为抛出异常的原因可能会导致ModelMap处于不一致状态。因此,根据您的情况,您可以考虑

  • 明确捕获异常以告诉Spring MVC您知道自己在做什么(可以使用模板模式将异常处理逻辑重构到一个地方)
  • 如果您控制了异常层次结构,则可以将BindingResult移交给异常并稍后从异常中提取它以进行渲染
  • 首先不抛出异常,但使用一些结果代码(例如BeanValidation就好了)

HTH

答案 2 :(得分:1)

改善第一个答案:

    @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对象,如果你不需要为此目的创建一个特定的异常,你可以使用它。

答案 3 :(得分:0)

我有同样的问题来“添加”FunctinalException来ourthe BindingResult

要解决它,我们使用aop,如果控制器方法抛出运行时异常(或你想要的那个), aop捕获它并更新bindingresult或model(如果它们是方法的args)。

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

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

答案 4 :(得分:0)

我也想知道这一点。

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

  

明确捕获异常,告诉Spring MVC你知道你在做什么(你可以使用Template模式将异常处理逻辑重构到一个地方)

我创建了一个简单的Action界面:

public interface Action {
  String run();
}

并且可以很好地处理确保ActionRunner工作的ConstraintViolationException课程(基本上每个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,但这只涉及“哦垃圾”例外。

答案 5 :(得分:-1)

实际上确实如此,只需为@ExceptionHandler创建一个MethodArgumentNotValidException方法。

该类允许您访问BindingResult对象。