Почему 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 перед повторным запуском.
Как это звучит?
Ответы
Ответ 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
.