41

I'm writing a web application with Spring 4.0.4 and Spring Boot 1.0.2 using Tomcat as embedded web container and I want to implement a global exception handling which intercepts all exceptions and logs them in a specific way. My simple requirements are:

  • I want to globally handle all exceptions which are not already processed somewhere else (In a controller exception handler for example). I want to log the message and I want to display a custom error message to the user.
  • I don't want Spring or the web container to log any errors by itself because I want to do this myself.

So far my solution looks like this (Simplified, no logging and no redirection to an error view):

@Controller
@RequestMapping("/errors")
public class ErrorHandler implements EmbeddedServletContainerCustomizer
{
    @Override
    public void customize(final ConfigurableEmbeddedServletContainer factory)
    {
        factory.addErrorPages(new ErrorPage("/errors/unexpected"));
        factory.addErrorPages(new ErrorPage(HttpStatus.NOT_FOUND, "/errors/notfound"));
    }

    @RequestMapping("unexpected")
    @ResponseBody
    public String unexpectedError(final HttpServletRequest request)
    {
        return "Exception: " + request.getAttribute("javax.servlet.error.exception");
    }

    @RequestMapping("notfound")
    @ResponseBody
    public String notFound()
    {
        return "Error 404";
    }
}

The result is that exceptions thrown in controllers are correctly handled by the unexpectedError method and 404 status codes are handled by the notFound method. So far so good, but I have the following problems:

  • Tomcat or Spring (not sure who is responsible) is still logging the error message. I don't want that because I want to log it myself (with additional information) and I don't want duplicate error messages in the log. How can I prevent this default logging?
  • The way I access the exception object doesn't seem right. I fetch if from the request attribute javax.servlet.error.exception. And that's not even the thrown exception, it is an instance of org.springframework.web.util.NestedServletException and I have to dive into this nested exception to fetch the real one. I'm pretty sure there is an easier way but I can't find it.

So how can I solve these problems? Or maybe the way I implemented this global exception handler is completely wrong and there is a better alternative?

kayahr
  • 20,913
  • 29
  • 99
  • 147

1 Answers1

57

Have a look at ControllerAdvice You could do something like this:

@ControllerAdvice
public class ExceptionHandlerController {

    public static final String DEFAULT_ERROR_VIEW = "error";

    @ExceptionHandler(value = {Exception.class, RuntimeException.class})
    public ModelAndView defaultErrorHandler(HttpServletRequest request, Exception e) {
            ModelAndView mav = new ModelAndView(DEFAULT_ERROR_VIEW);

        mav.addObject("datetime", new Date());
        mav.addObject("exception", e);
        mav.addObject("url", request.getRequestURL());
        return mav;
    }
}
Michiel
  • 1,061
  • 10
  • 17
  • Thanks, this works great. But now I discovered another requirement which is not part of my original question so I created a new one. Maybe you can help there, too: http://stackoverflow.com/questions/23582534/how-to-handle-exceptions-in-spring-mvc-differently-for-html-and-json-requests – kayahr May 10 '14 at 14:57
  • 2
    Cleanest solution available. One other worthy addition might be the @ResponseStatus to 500, which would return the response in HTTP 500 as expected. – Matthew Cachia Aug 02 '16 at 20:21
  • 3
    Please note that this solution will also catch Spring Security exceptions like AuthenticationException. As a result, redirection to protected resource after login will not work. Explanation: entering protected resource normally redirects you to login page. After logging in, you should go back to protected resource (Spring Security takes care for that by catching AuthenticationException), but when you catch all exceptions like that, Spring Security will not catch it anymore. – Kacper86 Oct 31 '16 at 12:27
  • Your solution is missing a annotation to work. @@EnableWebMvc @@ControllerAdvice public class GlobalExceptionHandler {} – Amit Sharma Feb 16 '17 at 18:23
  • @Kacper86 I believe you could use a separate `@ControllerAdvice` class, have them both implement Ordered and give the more specific Exception handling class higher precedence. – Roger Apr 05 '17 at 22:25
  • Does this work with 404 errors?, etc.: `No mapping found for HTTP request with URI […] in DispatcherServlet”?` – zygimantus Apr 18 '17 at 18:00
  • 1
    for restful api, you may need use `@RestControllerAdvice` instead. – min Jul 06 '17 at 15:35
  • Also note, this solution will break all `@ResponseStatus` exception classes, because as soon as any exception handler handles the exception, it counts as resolved and `ResponseStatusExceptionResolver` don't hit. `ResponseStatusExceptionResolver` looks for uncaught exceptions annotated by `@ResponseStatus`. But this global exception handler will catch them earlier. – Ruslan Stelmachenko Sep 03 '17 at 19:28
  • In some cases you may also want to [handle ClientAbortExceptions](https://stackoverflow.com/a/46286349/2074605). – Parker Sep 18 '17 at 18:58