Handling Exceptions using HandlerExceptionResolver
Once upon a time, Spring 2.0 introduced HandlerExceptionResolver
to handle thrown exceptions. While this approach works, it is like throwing a big net to catch all types of fish. If we are only interested in catching piranhas and clownfish, this approach becomes a little tedious because we need to weed out the types of fish we don’t care.
@Controller public class ErrorController implements HandlerExceptionResolver { @Override public ModelAndView resolveException(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) { ModelAndView modelAndView = new ModelAndView("error"); if (e instanceof PiranhaException) { // stir fry it } else if (e instanceof ClownfishException) { // make me laugh first } else { // don't care... } return modelAndView; } @RequestMapping(value = "/error", method = RequestMethod.GET) public String error() { return "error"; } }
Handling Exceptions using @ExceptionHandler
Then, Spring 3.0 introduced a more elegant way to handle exceptions using @ExceptionHandler
. Now, we can easily catch the types of fish we need and handle accordingly.
@Controller @RequestMapping(value = "/fish") public class FishController { @ExceptionHandler(PiranhaException.class) public String piranhaException(Exception e) { // stir fry it return "error"; } @ExceptionHandler(ClownfishException.class) public String clownfishException(Exception e) { // make me laugh first return "error"; } @RequestMapping(method = RequestMethod.GET) public String home() { return "home"; } }
While this is a more elegant solution than the previous approach, @ExceptionHandler
has to be defined within the Controller
class. If we want to apply the same exception handling across multiple controllers, we either have to define @ExceptionHandler
in every Controller
class or create something “clever” such as extending a parent controller that contains the common configurations.
Handling Exceptions using @ControllerAdvice
Then, Spring 3.2 introduced an even better approach to centralize common configurations using @ControllerAdvice
. Instead of defining @ExceptionHandler
all over places or extending a parent controller, we can create a separate class that contains these common configurations and annotate it with @ControllerAdvice
.
@ControllerAdvice public class FishControllerAdvice { @ExceptionHandler(PiranhaException.class) public String piranhaException(Exception e) { // stir fry it return "error"; } @ExceptionHandler(ClownfishException.class) public String clownfishException(Exception e) { // make me laugh first return "error"; } }
… and now, the controller will look as simple as this:-
@Controller @RequestMapping(value = "/fish") public class FishController { @RequestMapping(method = RequestMethod.GET) public String home() { return "home"; } }
In fact, @ControllerAdvice
can be used to define @ExceptionHandler
, @ModelAttribute
and @InitBinder
that apply to all @RequestMapping
methods.
Here’s an example:-
@ControllerAdvice public class MyAdviceToAllFish { @ExceptionHandler(PiranhaException.class) public String piranhaException(Exception e) { // stir fry it return "error"; } @ExceptionHandler(ClownfishException.class) public String clownfishException(Exception e) { // make me laugh first return "error"; } // Catch all other exceptions @ExceptionHandler(Exception.class) public String exception(Exception e) { // when shit happens return "error"; } // Return a list of tech support emails @ModelAttribute("techSupportEmails") public List<String> getTechSupportEmails() { return Arrays.asList("generaltso@chicken.com", "bat@man.com"); } // Handles `String` and Joda Time's `LocalDate` object @InitBinder private void initBinder(WebDataBinder binder) { // Trim string and set empty string as `null` value binder.registerCustomEditor(String.class, new StringTrimmerEditor(true)); // Convert Joda Time's `LocalDate` object to text, vice versa binder.registerCustomEditor(LocalDate.class, new PropertyEditorSupport() { private final DateTimeFormatter pattern = DateTimeFormat.forPattern("MM/dd/yyyy"); @Override public void setAsText(String text) throws IllegalArgumentException { setValue(StringUtils.isBlank(text) ? null : pattern.parseLocalDate(text)); } @Override public String getAsText() throws IllegalArgumentException { LocalDate localDate = (LocalDate) getValue(); return localDate != null ? pattern.print(localDate) : ""; } }); } }