Подтвердить что ты не робот

Почему Spring MVC не позволяет выставлять модель или BindingResult для @ExceptionHandler?

Ситуация

Я пытаюсь сгруппировать код, который регистрирует исключения и визуализирует прекрасный вид несколькими способами. В настоящий момент логика когда-то находится в самом @RequestHandler (в блоке catch), othertimes делегируется классу утилиты (который работает, но отводит логику от места, где генерируется исключение).

Spring @ExceptionHandler представлял собой способ группировать все в одном месте (сам контроллер или родительский элемент) и избавляться от некоторого кода (нет необходимости ставить логику в try-catch и нет необходимости в классе утилиты)... пока я не понял, что методы @ExceptionHandler не будут иметь параметры ModelMap или BindingResult. В настоящее время эти объекты используются для визуализации представления с разумным сообщением об ошибке, и мы также хотим зарегистрировать некоторую информацию, содержащуюся в этих объектах.

Вопрос

Почему Spring не поддерживает аргументы метода ModelMap или BindingResult для @ExceptionHandler? В чем причина этого?

Возможное решение

В исходном коде Spring (3.0.5) аргументы для метода разрешены в HandlerMethodInvoker.invokeHandlerMethod. Исключение, отправленное обработчиком запроса, попадает туда и повторно бросается. Параметры @ExceptionHandler и его параметры разрешены в другом месте. В качестве обходного пути я решил проверить, реализует ли Exception гипотетический интерфейс ModelAware или BindingResultAware и в этом случае устанавливает атрибуты Model и BindingResult перед повторным запуском. Как это звучит?

4b9b3361

Ответ 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

Я столкнулся с той же проблемой некоторое время назад. ModelMap или BindingResult явно не указаны в качестве поддерживаемых типов аргументов в JavaDocs @ExceptionHandler, поэтому это должно было быть преднамеренным.

Я считаю, что причина этого в том, что бросание исключений вообще может оставить ваш ModelMap в несогласованном состоянии. Поэтому в зависимости от вашей ситуации вы можете рассмотреть

  • Явно поймаю исключение, чтобы сообщить Spring MVC, что вы знаете, что делаете (вы можете использовать шаблон Template для логической обработки обработки исключений в одном месте)
  • Если вы контролируете иерархию исключений, вы можете передать BindingResult в исключение и извлечь его из исключения позже для целей рендеринга
  • В первую очередь исключить исключение, но использовать некоторый код результата (например, как BeanValidation)

НТН

Ответ 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 поймать его и обновить bindingresult или model (если они являются аргументами метода).

Метод должен быть аннотирован с конкретной аннотацией, содержащей путь ошибки (при необходимости настраивается для конкретного исключения).

Это не лучший способ, потому что разработчик не должен забывать добавлять аргументы, которые он не использует в своем методе, но Spring не предоставляет простую систему для этой необходимости.

Ответ 5

Я тоже подумал об этом.

Чтобы обработать bean валидацию таким образом, чтобы неглобальный вид ошибки отображал любой ConstraintViolationException, который может быть выброшен, я выбрал решение в соответствии с тем, что предложил @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 Validation. У меня все еще есть глобальный @ExceptionHandler, но это касается только исключений "oh crap".

Ответ 6

Собственно, просто создайте метод @ExceptionHandler для MethodArgumentNotValidException.

Этот класс предоставляет вам доступ к объекту BindingResult.