Configuring global exception-handling in Spring MVC
It took a couple of hours to figure this out - the mighty Google and even StackOverflow let me down - in the end I had to actually read Spring's DispatcherServlet code! (I know, right!?)
Here's the problem I was having - I'm using Spring MVC's data-binding tricks to inject objects into my @Controller
's methods like this:
@Controller
@RequestMapping("things/{thing}.html")
class MyController {
public ModelAndView thing(@PathVariable Thing aThing) {
// Thing should be magically mapped from
// the {thing} part of the url
return new ModelAndView(..blah..);
}
}
I have global Formatter
's configured as described in my previous post, and I want my method parameters to be automatically conjured from @PathVariable
's and so on.
So far so good .. until I make a screw-up and parameter binding fails for any reason, at which point Spring's exception-handling kicks in. When that happens, Spring eats the exception and dumps me on the worlds shittiest error page saying:
HTTP ERROR 400
Problem accessing /your/url/whatever.html. Reason:
Bad Request
Wow, thanks Spring!
To blame here are Spring's default set of HandlerExceptionResolver
's, which are specified in DispatcherServlet.properties
in the spring-webmvc jar. In 3.1.2 it says:
org.springframework.web.servlet.HandlerExceptionResolver=
org.springfr..AnnotationMethodHandlerExceptionResolver,
org.springfr..ResponseStatusExceptionResolver,
org.springfr..DefaultHandlerExceptionResolver
(I've shortened the package-names to keep things readable)
Beats me why the default is to eat the exception without even logging it when Spring is normally so chatty about everything it does, but there you go. OK, so we need to configure some custom exception-handling so we can find out what's actually going wrong. There are two ways (that I know of) to do that:
- Use
@ExceptionHandler
annotated methods in our@Controller
's to handle exceptions on a per-controller basis (or across more than one@Controller
if you have a hierarchy and implement the@ExceptionHandler
method high-up in the hierarchy). - Register a
HandlerExceptionResolver
implementation to deal with exceptions globally (ie. across all@Controller
's, regardless of hierarchy).
@ExceptionHandler
These bad-boys are straight-forward to use - just add a method in your @Controller
and annotate it with @ExceptionHandler(SomeException.class)
- something like this:
@Controller
class MyExceptionalController {
@ExceptionHandler(Exception.class)
public void handleExceptions(Exception anExc) {
anExc.printStackTrace(); // do something better than this ;)
}
@RequestMapping("/my/favourite/{thing}")
public void showThing(@PathVariable Thing aThing) {
throw new RuntimeException("boom");
}
}
That exception-handler method will now be triggered for any exceptions that occur while processing this controller - including any exceptions that occur while trying to format the Thing parameter.
There's a bit more to it, for example you can parameterise the annotation with an array of exception-types. Shrug.
Just for completeness its worth mentioning that when formatting/conversion fails the exception presented to the @ExceptionHandler
will be a TypeMismatchException
, possibly wrapping a ConversionFailedException
which in turn would wrap any exception thrown by your Formatter
classes.
Custom HandlerExceptionResolver
This is the better approach, IMHO: Set up a HandlerExceptionResolver to deal with exceptions across all @Controller
's and override with @ExceptionHandler
's if you have specific cases that need special handling.
A deadly simple HandlerExceptionResolver
might look like this:
package com.sjl.web;
import org.springframework.core.*;
import org.springframework.web.servlet.*
public class LoggingHandlerExceptionResolver
implements HandlerExceptionResolver, Ordered {
public int getOrder() {
return Integer.MIN_VALUE; // we're first in line, yay!
}
public ModelAndView resolveException(
HttpServletRequest aReq, HttpServletResponse aRes,
Object aHandler, Exception anExc
) {
anExc.printStackTrace(); // again, you can do better than this ;)
return null; // trigger other HandlerExceptionResolver's
}
}
Two things worth pointing out here:
- We are implementing
Ordered
and returningInteger.MIN_VALUE
- this puts us at the front of the queue for resolving exceptions (and ahead of the default). If we don't implementOrdered
we won't see the exception before one of the default handlers grabs and handles it. The default handlers appear to be registered with orders ofInteger.MAX_VALUE
, so any int below that will do. - We are returning
null
from theresolveException
method - doing this means that the other handlers in the chain get a chance to deal with the exception. Alternatively we can return aModelAndView
if we want to (and if we know how to deal with this particular kind of exception), which will prevent handlers further down the chain from seeing the exception.
There are some classes in Spring's HandlerExceptionResolver
hierarchy that you might want to look at sub-classing - AbstractHandlerMethodExceptionResolver
and SimpleMappingExceptionResolver
are good ones to check first.
Of course we need to make Spring's DispatcherServlet
aware of our custom HandlerExceptionResolver
. The only configuration we need is:
<bean class="com.sjl.web.LoggingHandlerExceptionResolver"/>
No really, that's it.
There's an unusually high level of magic surrounding the DispatcherServlet
, so although you must define your resolver as a bean in your spring config you do not need to inject it into any other spring beans. The DispatcherServlet
will search for beans implementing the interface and automagically use them.