Why Spring MVC does not allow to expose Model or BindingResult to an @ExceptionHandler?

后端 未结 6 1082
时光说笑
时光说笑 2021-02-12 18:41

Situation

I\'m trying to group the code that logs the exceptions and render a nice view in a few methods. At the moment the logic is sometime in the @RequestHand

相关标签:
6条回答
  • 2021-02-12 18:48

    I had the same problem to "add" FunctinalException to ourthe BindingResult

    To resolve it, we use aop, if the controller method throws a runtime exception (or the one you want), the aop catch it and update the bindingresult or model (if they are args of the method).

    The method has to be annoted with a specific annotation containing the error path (configurable for specific exception if necessary).

    It is not the best way because developer must not forget to add args that he don't use in its method but Spring does not provide a simple system to do this need.

    0 讨论(0)
  • 2021-02-12 18:56

    I've wondered this too.

    In order to handle bean validation in a way that allows for a non-global error view to display any ConstraintViolationExceptions that may be thrown, I opted for a solution along the lines of what @Stefan Haberl proposed:

    Explicitly catch the exception to tell Spring MVC that you know what you're doing (you could use the Template pattern to refactor exception handling logic into one single place)

    I created a simple Action interface:

    public interface Action {
      String run();
    }
    

    And an ActionRunner class which does the work of ensuring ConstraintViolationExceptions are handled nicely (basically the messages from each ConstraintViolationException is simply added to a Set and added to the model):

    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 makes this pretty nice to run within the controller action method:

    @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";
      });
    }
    

    This is to wrap up those action methods for which I'd like to explicitly handle exceptions that could be thrown due to Bean Validation. I still have a global @ExceptionHandler but this deals with only "oh crap" exceptions.

    0 讨论(0)
  • 2021-02-12 19:02

    To improve the first answer:

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

    There is already MethodArgumentNotValidException has already a BindingResult object, and you can use it, if you don't need to create an specific exception for this purpose.

    0 讨论(0)
  • 2021-02-12 19:04

    Actually it does, just create an @ExceptionHandler method for MethodArgumentNotValidException.

    That class gives you access to a BindingResult object.

    0 讨论(0)
  • 2021-02-12 19:05

    As stated before you can raise an exception wrapping a binding result object in some method of your controller:

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

    With your exception defined as illustrated here:

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

    Next you just have to extract the required information from the raised then caught exception. Here assuming you have a suitable exception handler defined on your controller. It might be in a controller advice instead or even elewhere. See the Spring documentation for suitable and appropriate locations.

    @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;            
        }
    }
    
    0 讨论(0)
  • 2021-02-12 19:13

    I ran into to same problem a while ago. The ModelMap or BindingResult are explicitly not listed as supported argument types in the JavaDocs of @ExceptionHandler, so this must have been intentional.

    I reckon the reason behind it being that throwing exceptions in general could leave your ModelMap in an inconsistent state. So depending on your situation you might consider

    • Explicitly catch the exception to tell Spring MVC that you know what you're doing (you could use the Template pattern to refactor exception handling logic into one single place)
    • If you're in control of the exception hierarchy you could hand over the BindingResult to the exception and extract it from the exception later for rendering purposes
    • Not throw an exception in the first place, but use some result code (just like BeanValidation does for example)

    HTH

    0 讨论(0)
提交回复
热议问题