There are three options: per exception,
per controller or globally.
Using HTTP Status Codes
Normally any unhandled exception thrown when processing a web-request causes the server to return an HTTP 500 response. However, any exception that you write yourself can be annotated with the@ResponseStatus
annotation (which
supports all the HTTP status codes defined by the HTTP
specification). When an annotated exception is thrown from a
controller method, and not handled elsewhere, it will automatically
cause the appropriate HTTP response to be returned with the specified
status-code.For example, here is an exception for a missing order.
@ResponseStatus(value=HttpStatus.NOT_FOUND, reason="No such Order") // 404 public class OrderNotFoundException extends RuntimeException { // ... }And here is a controller method using it:
@RequestMapping(value="/orders/{id}", method=GET) public String showOrder(@PathVariable("id") long id, Model model) { Order order = orderRepository.findOrderById(id); if (order == null) throw new OrderNotFoundException(id); model.addAttribute(order); return "orderDetail"; }A familiar HTTP 404 response will be returned if the URL handled by this method includes an unknown order id.
Controller Based Exception Handling
Using @ExceptionHandler
You can add extra (@ExceptionHandler
)
methods to any controller to specifically handle exceptions thrown by
request handling (@RequestMapping
)
methods in the same controller. Such methods can:- Handle exceptions without the
@ResponseStatus
annotation (typically predefined exceptions that you didn't write) - Redirect the user to a dedicated error view
- Build a totally custom error response
@Controller public class ExceptionHandlingController { // @RequestHandler methods ... // Exception handling methods // Convert a predefined exception to an HTTP Status code @ResponseStatus(value=HttpStatus.CONFLICT, reason="Data integrity violation") // 409 @ExceptionHandler(DataIntegrityViolationException.class) public void conflict() { // Nothing to do } // Specify the name of a specific view that will be used to display the error: @ExceptionHandler({SQLException.class,DataAccessException.class}) public String databaseError() { // Nothing to do. Returns the logical view name of an error page, passed to // the view-resolver(s) in usual way. // Note that the exception is _not_ available to this view (it is not added to // the model) but see "Extending ExceptionHandlerExceptionResolver" below. return "databaseError"; } // Total control - setup a model and return the view name yourself. Or consider // subclassing ExceptionHandlerExceptionResolver (see below). @ExceptionHandler(Exception.class) public ModelAndView handleError(HttpServletRequest req, Exception exception) { logger.error("Request: " + req.getRequestURL() + " raised " + exception); ModelAndView mav = new ModelAndView(); mav.addObject("exception", exception); mav.addObject("url", req.getRequestURL()); mav.setViewName("error"); return mav; } }In any of these methods you might choose to do additional processing - the most common example is to log the exception.
Handler methods have flexible signatures so you can pass in obvious servlet-related objects such as
HttpServletRequest
,
HttpServletResponse
, HttpSession
and/or Principle
. Important
Note: the Model
may not
be a parameter of any @ExceptionHandler
method. Instead, setup a model inside the method using a ModelAndView
as shown by handleError()
above.Exceptions and Views
Be careful when adding exceptions to the model. Your users do not want to see web-pages containing Java exception details and stack-traces. However, it can be useful to put exception details in the page source as a comment, to assist your support people. If using JSP, you could do something like this to output the exception and the corresponding stack-trace (using a hidden<div>
is another option).<h1>Error Page</h1> <p>Application has encountered an error. Please contact support on ...</p> <!-- Failed URL: ${url} Exception: ${exception.message} <c:forEach items="${exception.stackTrace}" var="ste"> ${ste} </c:forEach> -->@Controller
public class ExceptionHandlingController {
@ExceptionHandler(Exception.class)
public ModelAndView getExceptionPage(Exception e, HttpServletRequest request) {
// Catches all exception
// Do anything with Exception object here
if (isAjax(request)) {
// If exception comes for Ajax requests
ModelAndView model = new ModelAndView("forward:/app/webExceptionHandler/ajaxErrorRedirectPage");
model.addObject("exception", exception);
model.addObject("url", req.getRequestURL());
model.setViewName("error");
return model;
} else {
// If exception comes for all non Ajax requests
ModelAndView model = new ModelAndView("forward:/app/webExceptionHandler/nonAjaxErrorRedirectPage");
model.addObject("exception", exception);
model.addObject("url", req.getRequestURL());
model.setViewName("error");
return model;
}
}
private static boolean isAjax(HttpServletRequest request) {
return "XMLHttpRequest".equals(request.getHeader("X-Requested-With"));
}
}
Global Exception Handling
Using @ControllerAdvice Classes
A controller advice allows you to use exactly the same exception handling techniques but apply them across the whole application, not just to an individual controller. You can think of them as an annotation driven interceptor.Any class annotated with
@ControllerAdvice
becomes a controller-advice and three types of method are supported:- Exception handling methods annotated with
@ExceptionHandler
. - Model enhancement methods (for adding additional data to the model) annotated with
@ModelAttribute
. Note that these attributes are not available to the exception handling views. - Binder initialization methods (used for configuring
form-handling) annotated with
@InitBinder
.
@ControllerAdvice
methods.Any of the exception handlers you saw above can be defined on a controller-advice class - but now they apply to exceptions thrown from any controller. Here is a simple example:
@ControllerAdvice class GlobalControllerExceptionHandler { @ResponseStatus(HttpStatus.CONFLICT) // 409 @ExceptionHandler(DataIntegrityViolationException.class) public void handleConflict() { // Nothing to do } }If you want to have a default handler for any exception, there is a slight wrinkle. You need to ensure annotated exceptions are handled by the framework. The code looks like this:
@ControllerAdvice class GlobalDefaultExceptionHandler { public static final String DEFAULT_ERROR_VIEW = "error"; @ExceptionHandler(value = Exception.class) public ModelAndView defaultErrorHandler(HttpServletRequest req, Exception e) throws Exception { // If the exception is annotated with @ResponseStatus rethrow it and let // the framework handle it - like the OrderNotFoundException example // at the start of this post. // AnnotationUtils is a Spring Framework utility class. if (AnnotationUtils.findAnnotation(e.getClass(), ResponseStatus.class) != null) throw e; // Otherwise setup and send the user to a default error-view. ModelAndView mav = new ModelAndView(); mav.addObject("exception", e); mav.addObject("url", req.getRequestURL()); mav.setViewName(DEFAULT_ERROR_VIEW); return mav; } }
Going Deeper
HandlerExceptionResolver
Any Spring bean declared in theDispatcherServlet
's
application context that implements HandlerExceptionResolver
will be used to intercept and process any exception raised in the MVC
system and not handled by a Controller. The interface looks like
this:public interface HandlerExceptionResolver { ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex); }The
handler
refers to the controller
that generated the exception (remember that @Controller
instances are only one type of handler supported by Spring MVC. For
example: HttpInvokerExporter
and the
WebFlow Executor are also types of handler).
Behind the scenes, MVC creates three such resolvers by default. It is these resolvers that implement the behaviours discussed above:
ExceptionHandlerExceptionResolver
matches uncaught exceptions against for suitable@ExceptionHandler
methods on both the handler (controller) and on any controller-advices.ResponseStatusExceptionResolver
looks for uncaught exceptions annotated by@ResponseStatus
(as described in Section 1)DefaultHandlerExceptionResolver
converts standard Spring exceptions and converts them to HTTP Status Codes (I have not mentioned this above as it is internal to Spring MVC).
Notice that the method signature of
resolveException
does not include the Model
. This is why
@ExceptionHandler
methods cannot be
injected with the model.You can, if you wish, implement your own
HandlerExceptionResolver
to setup your own custom exception handling system. Handlers
typically implement Spring's Ordered
interface so you can define the order that the handlers run in.SimpleMappingExceptionResolver
Spring has long provided a simple but convenient implementation ofHandlerExceptionResolver
that you may
well find being used in your appication already - the
SimpleMappingExceptionResolver
. It
provides options to:- Map exception class names to view names - just specify the classname, no package needed.
- Specify a default (fallback) error page for any exception not handled anywhere else
- Log a message (this is not enabled by default).
- Set the name of the
exception
attribute to add to the Model so it can be used inside a View (such as a JSP). By default this attribute is namedexception
. Set tonull
to disable. Remember that views returned from@ExceptionHandler
methods do not have access to the exception but views defined toSimpleMappingExceptionResolver
do.
<bean id="simpleMappingExceptionResolver" class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver"> <property name="exceptionMappings"> <map> <entry key="DatabaseException" value="databaseError"/> <entry key="InvalidCreditCardException" value="creditCardError"/> </map> </property> <!-- See note below on how this interacts with Spring Boot --> <property name="defaultErrorView" value="error"/> <property name="exceptionAttribute" value="ex"/> <!-- Name of logger to use to log exceptions. Unset by default, so logging disabled --> <property name="warnLogCategory" value="example.MvcLogger"/> </bean>Or using Java Configuration:
@Configuration @EnableWebMvc // Optionally setup Spring MVC defaults if you aren't doing so elsewhere public class MvcConfiguration extends WebMvcConfigurerAdapter { @Bean(name="simpleMappingExceptionResolver") public SimpleMappingExceptionResolver createSimpleMappingExceptionResolver() { SimpleMappingExceptionResolver r = new SimpleMappingExceptionResolver(); Properties mappings = new Properties(); mappings.setProperty("DatabaseException", "databaseError"); mappings.setProperty("InvalidCreditCardException", "creditCardError"); r.setExceptionMappings(mappings); // None by default r.setDefaultErrorView("error"); // No default r.setExceptionAttribute("ex"); // Default is "exception" r.setWarnLogCategory("example.MvcLogger"); // No default return r; } ... }The defaultErrorView property is especially useful as it ensures any uncaught exception generates a suitable application defined error page. (The default for most application servers is to display a Java stack-trace - something your users should never see).
Extending SimpleMappingExceptionResolver
It is quite common to extendSimpleMappingExceptionResolver
for several reasons:- Use the constructor to set properties directly - for example to enable exception logging and set the logger to use
- Override the default log message by overriding
buildLogMessage
. The default implementation always returns this fixed text:Handler execution resulted in exception - To make additional information available to the error view by
overriding
doResolveException
public class MyMappingExceptionResolver extends SimpleMappingExceptionResolver { public MyMappingExceptionResolver() { // Enable logging by providing the name of the logger to use setWarnLogCategory(MyMappingExceptionResolver.class.getName()); } @Override public String buildLogMessage(Exception e, HttpServletRequest req) { return "MVC exception: " + e.getLocalizedMessage(); } @Override protected ModelAndView doResolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception exception) { // Call super method to get the ModelAndView ModelAndView mav = super.doResolveException(request, response, handler, exception); // Make the full URL available to the view - note ModelAndView uses addObject() // but Model uses addAttribute(). They work the same. mav.addObject("url", request.getRequestURL()); return mav; } }
Extending ExceptionHandlerExceptionResolver
It is also possible to extendExceptionHandlerExceptionResolver
and override its doResolveHandlerMethodException
method in the same way. It has almost the same signature (it just
takes the new HandlerMethod
instead of a
Handler
).To make sure it gets used, also set the inherited order property (for example in the constructor of your new class) to a value less than
MAX_INT
so it runs before
the default ExceptionHandlerExceptionResolver instance (it is easier
to create your own handler instance than try to modify/replace the
one created by Spring).
Errors and REST
RESTful GET requests may also generate exceptions and we have already seen how we can return standard HTTP Error response codes. However, what if you want to return information about the error? This is very easy to do. Firstly define an error class:public class ErrorInfo { public final String url; public final String ex; public ErrorInfo(String url, Exception ex) { this.url = url; this.ex = ex.getLocalizedMessage(); } }Now we can return an instance from a handler as the
@ResponseBody
like this:@ResponseStatus(HttpStatus.BAD_REQUEST) @ExceptionHandler(MyBadDataException.class) @ResponseBody ErrorInfo handleBadRequest(HttpServletRequest req, Exception ex) { return new ErrorInfo(req.getRequestURL(), ex); }
What to Use When?
As usual, Spring likes to offer you choice, so what should you do? Here are some rules of thumb. However if you have a preference for XML configuration or Annotations, that's fine too.- For exceptions you write, consider adding
@ResponseStatus
to them. - For all other exceptions implement an
@ExceptionHandler
method on a@ControllerAdvice
class or use an instance ofSimpleMappingExceptionResolver
. You may well haveSimpleMappingExceptionResolver
configured for your application already, in which case it may be easier to add new exception classes to it than implement a@ControllerAdvice
. - For Controller specific exception handling add
@ExceptionHandler
methods to your controller. - Warning: Be careful mixing too many of these options
in the same application. If the same exception can be handed in more
than one way, you may not get the behavior you wanted.
@ExceptionHandler
methods on the Controller are always selected before those on any@ControllerAdvice
instance. It is undefined what order controller-advices are processed.
web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
<!-- The definition of the Root Spring Container shared by all Servlets and Filters -->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/spring/root-context.xml</param-value>
</context-param>
<!-- Creates the Spring Container shared by all Servlets and Filters -->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!-- Processes application requests -->
<servlet>
<servlet-name>appServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/spring/spring.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>appServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
<error-page>
<error-code>404</error-code>
<location>/resources/404.jsp</location>
</error-page>
</web-app>
Spring Bean Configuration File
Our spring bean configuration file looks like below.
spring.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="http://www.springframework.org/schema/mvc"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:beans="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
<!-- DispatcherServlet Context: defines this servlet's request-processing infrastructure -->
<!-- Enables the Spring MVC @Controller programming model -->
<annotation-driven />
<!-- Handles HTTP GET requests for /resources/** by efficiently serving up static resources in the ${webappRoot}/resources directory -->
<resources mapping="/resources/**" location="/resources/" />
<!-- Resolves views selected for rendering by @Controllers to .jsp resources in the /WEB-INF/views directory -->
<beans:bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<beans:property name="prefix" value="/WEB-INF/views/" />
<beans:property name="suffix" value=".jsp" />
</beans:bean>
<beans:bean id="simpleMappingExceptionResolver" class="com.journaldev.spring.resolver.MySimpleMappingExceptionResolver">
<beans:property name="exceptionMappings">
<beans:map>
<beans:entry key="Exception" value="generic_error"></beans:entry>
</beans:map>
</beans:property>
<beans:property name="defaultErrorView" value="generic_error"/>
</beans:bean>
<!-- Configure to plugin JSON as request and response in method handler -->
<beans:bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter">
<beans:property name="messageConverters">
<beans:list>
<beans:ref bean="jsonMessageConverter"/>
</beans:list>
</beans:property>
</beans:bean>
<!-- Configure bean to convert JSON to POJO and vice versa -->
<beans:bean id="jsonMessageConverter" class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter">
</beans:bean>
<context:component-scan base-package="com.journaldev.spring" />
</beans:beans>
No comments:
Post a Comment