funciona - ¿Por qué Spring MVC no permite exponer Model o BindingResult a un @ExceptionHandler?
spring mvc configuration (6)
Como se indicó anteriormente, puede generar una excepción envolviendo un objeto de resultado vinculante en algún método de su controlador:
if (bindingResult.hasErrors()) {
logBindingErrors(bindingResult);
//return "users/create";
// Exception handling happens later in this controller
throw new BindingErrorsException("MVC binding errors", userForm, bindingResult);
}
Con su excepción definida como se ilustra aquí:
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;
}
}
A continuación, solo tiene que extraer la información requerida de la excepción detectada y capturada. Aquí, suponiendo que tiene un controlador de excepciones adecuado definido en su controlador. Puede ser en un consejo de controlador en lugar de o incluso en otros lugares. Consulte la documentación de Spring para ubicaciones adecuadas y apropiadas.
@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;
}
}
Situación
Estoy tratando de agrupar el código que registra las excepciones y generar una buena vista en algunos métodos. En el momento en que la lógica se encuentra en el @RequestHandler
(en el bloque a catch ), otras veces se delega a una clase de utilidad (que funciona pero aleja la lógica del lugar donde se lanza la excepción).
La @ExceptionHandler
de Spring parecía la forma de agrupar todo en un solo lugar (el propio controlador o uno de los padres) y deshacerse de algún código (no hay necesidad de poner lógica en el try-catch ni de una clase de utilidad) ... hasta que se dio cuenta de que los métodos @ExceptionHandler
no tendrán los parámetros ModelMap o BindingResult
autowired. Actualmente, esos objetos se utilizan para representar la vista con un mensaje de error razonable y también queremos registrar parte de la información contenida en estos objetos.
Pregunta
¿Por qué Spring no admite argumentos de método como ModelMap
o BindingResult
para @ExceptionHandler
? ¿Cuál es la razón detrás de esto?
Solución posible
En el código fuente de Spring (3.0.5), los argumentos para el método se resuelven en HandlerMethodInvoker.invokeHandlerMethod
. Una excepción lanzada por el controlador de solicitudes se captura allí y se vuelve a lanzar. El @ExceptionHandler
y sus parámetros se resuelven en otro lugar. Como solución, pensé en comprobar si la Excepción implementa una interfaz hipotética "ModelAware" o "BindingResultAware" y, en ese caso, establecer los atributos Model y BindingResult antes de volver a lanzarla. ¿Como suena?
En realidad lo hace, simplemente crea un método MethodArgumentNotValidException
para MethodArgumentNotValidException
.
Esa clase le da acceso a un objeto BindingResult
.
Me encontré con el mismo problema hace un tiempo. El ModelMap
o BindingResult
no se enumeran explícitamente como tipos de argumentos compatibles en los JavaDocs de @ExceptionHandler
, por lo que esto debe haber sido intencional.
Reconozco que la razón detrás de esto es que lanzar excepciones en general podría dejar su ModelMap
en un estado inconsistente. Así que dependiendo de tu situación puedes considerar
- Detecte explícitamente la excepción para decirle a Spring MVC que sabe lo que está haciendo (puede usar el patrón de plantilla para refactorizar la lógica de manejo de excepciones en un solo lugar)
- Si tiene el control de la jerarquía de excepciones, puede entregar
BindingResult
a la excepción y extraerla de la excepción más adelante para fines de representación. - No lanzar una excepción en primer lugar, sino usar algún código de resultado (como
BeanValidation
haceBeanValidation
, por ejemplo)
HTH
Para mejorar la primera respuesta:
@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);
}
Ya existe MethodArgumentNotValidException ya tiene un objeto BindingResult, y puede usarlo, si no necesita crear una excepción específica para este propósito.
También me he preguntado esto.
Con el fin de manejar la validación del bean de una manera que permita que una vista de error no global muestre cualquier ConstraintViolationException
que se pueda lanzar, opté por una solución en la línea de lo que propuso @Stefan Haberl:
Detecte explícitamente la excepción para decirle a Spring MVC que sabe lo que está haciendo (puede usar el patrón de plantilla para refactorizar la lógica de manejo de excepciones en un solo lugar)
He creado una interfaz de Action
simple:
public interface Action {
String run();
}
Y una clase ActionRunner
que realiza el trabajo de garantizar que las ConstraintViolationException
se manejen bien (básicamente, los mensajes de cada ConstraintViolationException
simplemente se agregan a un Set
y se agregan al modelo):
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 hace que esto sea bastante agradable de ejecutar dentro del método de acción del controlador:
@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";
});
}
Esto es para terminar los métodos de acción para los cuales me gustaría manejar explícitamente las excepciones que se podrían lanzar debido a la Validación de Bean. Todavía tengo un @ExceptionHandler
global pero esto trata solo con excepciones "oh crap".
Tuve el mismo problema para "agregar" la excepción Functinal a ourthe BindingResult
Para resolverlo, usamos aop, si el método del controlador lanza una excepción de tiempo de ejecución (o la que usted desea), el aop lo captura y actualiza el resultado o el modelo de enlace (si son argumentos del método).
El método debe anotarse con una anotación específica que contenga la ruta del error (configurable para una excepción específica si es necesario).
No es la mejor manera porque el desarrollador no debe olvidar agregar argumentos que no utiliza en su método, pero Spring no proporciona un sistema simple para hacer esto.