Exception Handling in Spring MVC

13 篇文章 0 订阅

转自:http://spring.io/blog/2013/11/01/exception-handling-in-spring-mvc

Exception Handling in Spring MVC

Spring MVC provides several complimentary approaches to exception handling but, when teaching Spring MVC, I often find that my students are confused or not comfortable with them.

Today I'm going to show you thevarious options available. Our goal is to not handle exceptions explicitly in Controller methodswhere possible. They are a cross-cutting concern better handled separately in dedicated code.

There are three options: per exception, per controller or globally.

A demonstration application that shows the points discussed here can be found athttp://github.com/paulc4/mvc-exceptions.See Sample Application and Spring Boot below for details.

Using HTTP Status Codes

Normally any unhandled exception thrown when processing a web-request causes the server to return anHTTP 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 HTTPspecification). 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 exceptionsthrown by request handling (@RequestMapping) methods in the same controller. Such methods can:

  1. Handle exceptions without the @ResponseStatus annotation (typically predefined exceptionsthat you didn't write)
  2. Redirect the user to a dedicated error view
  3. Build a totally custom error response

The following controller demonstrates these three options:

@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 theexception.

Handler methods have flexible signatures so you can pass in obvious servlet-related objects suchas HttpServletRequest, HttpServletResponse, HttpSession and/or Principle. Important Note: theModel may not be a parameter of any @ExceptionHandler method. Instead, setup a model inside the methodusing a ModelAndView as shown by handleError() above.

Exceptions and Views

Be careful when adding exceptions to the model. Your users do not want to seeweb-pages containing Java exception details and stack-traces. However, it can be useful to put exceptiondetails in the page source as a comment, to assist your support people. If using JSP, you coulddo 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>
    -->

The result looks like this (see alsosupport.jspin the demo application):

Example of an error page with a hidden exception for support

Global Exception Handling

Using @ControllerAdvice Classes

A controller advice allows you to use exactly the same exception handling techniques but apply themacross the whole application, not just to an individual controller. You can think of them as an annotationdriven interceptor.

Any class annotated with @ControllerAdvice becomes a controller-advice and three types of methodare 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.

We are only going to look at exception handling - see the online manual for more on@ControllerAdvice methods.

Any of the exception handlers you saw above can be defined on a controller-advice class - but now theyapply 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 ensureannotated 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 the DispatcherServlet's application context that implementsHandlerExceptionResolver will be used to intercept and process any exception raisedin 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 thebehaviours discussed above:

  • ExceptionHandlerExceptionResolver matches uncaught exceptions against forsuitable @ExceptionHandler methods on both the handler (controller) and on any controller-advices.
  • ResponseStatusExceptionResolver looks for uncaught exceptionsannotated by @ResponseStatus (as described in Section 1)
  • DefaultHandlerExceptionResolver converts standard Spring exceptions and converts themto HTTP Status Codes (I have not mentioned this above as it is internal to Spring MVC).

These are chained and processed in the order listed (internally Spring creates a dedicated bean - theHandlerExceptionResolverComposite to do this).

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 customexception handling system. Handlers typically implement Spring's Ordered interface so you can define theorder that the handlers run in.

SimpleMappingExceptionResolver

Spring has long provided a simple but convenient implementation of HandlerExceptionResolverthat 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 named exception. Set to null to disable. Rememberthat views returned from @ExceptionHandler methods do not have access to the exception but viewsdefined to SimpleMappingExceptionResolver do.

Here is a typical configuration using XML:

    <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 generatesa suitable application defined error page. (The default for most application servers is to display a Javastack-trace - something your users should never see).

Extending SimpleMappingExceptionResolver

It is quite common to extend SimpleMappingExceptionResolver for several reasons:

  • Use the constructor to set properties directly - for example to enable exception logging and set thelogger to use
  • Override the default log message by overriding buildLogMessage. The default implementationalways returns this fixed text:
    • Handler execution resulted in exception
  • To make additional information available to the error view by overriding doResolveException

For example:

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;
    }
}

This code is in the demo application asExampleSimpleMappingExceptionResolver

Extending ExceptionHandlerExceptionResolver

It is also possible to extend ExceptionHandlerExceptionResolver and override itsdoResolveHandlerMethodException 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 ofyour new class) to a value less than MAX_INT so it runs before the defaultExceptionHandlerExceptionResolver instance (it is easier to create your own handler instance than try tomodify/replace the one created by Spring). SeeExampleExceptionHandlerExceptionResolverin the demo app for more.

Errors and REST

RESTful GET requests may also generate exceptions and we have already seen how we can return standard HTTPError 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 of SimpleMappingExceptionResolver.You may well have SimpleMappingExceptionResolver 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 behanded in more than one way, you may not get the behavior you wanted. @ExceptionHandlermethods on the Controllerare always selected before those on any @ControllerAdvice instance. It is undefinedwhat order controller-advices are processed.

Sample Application and Spring Boot

A demonstration application can be found at github.It uses Spring Boot and Thymeleaf to build a simple web application. Some explanation is needed ...

About the Demo

The demo runs in one of two modes: controller or global. This is set via a boolean property in classMainwhich in turn enables a corresponding Spring Bean profile.

In addition, a SimpleMappingExceptionResolver may optionally be defined. Class Main has asecond property called smerConfig which can take one of three enumerated values:

  • NONE: No resolver defined. Unhandled exceptions processed by the container. Since we are using Tomcatthe familiar Tomcat error page with a full Java exception stack-trace is produced.
  • XML: A SimpleMappingExceptionResolver is configured using XML - seemvc-configuration.xml.A Spring bean profile xml-config makes this happen. If you prefer XML, this is the way to go.
  • JAVA: Also sets up a SimpleMappingExceptionResolver by enabling a different Spring profile, java-config,using Java configuration. SeeExceptionConfiguration

A description of the most important files in the application is in the project'sREADME.md.

The one and only web-page isindex.htmlwhich:

  • Contains several links, all of which deliberately raise exceptions.
  • At the bottom of the page are links to Spring Boot endpoints for those interested in Spring Boot.

Thanks to Spring Boot, you can run this demo as a Java application or as a WAR in your favourite container.Your choice. The home page URL is http://localhost:8080 when running asan application, or http://localhost:8080/mvc-exceptionswhen running in a container.

Warning

  • This project is built using the latest (0.5.0 at time of writing) snapshot release of Spring Boot.
  • APIs may have since changed and this project may not build.
  • Check http://spring.io/spring-boot for snapshot, milestoneand other releases.
  • Update the pom.xml and/or Java code if necessary.

Spring Boot and Error Handling

Spring Boot allows a Spring project to be setup withminimal configuration. Spring Boot creates sensible defaults automatically when it detectscertain key classes and packages on the classpath. For example if it sees that you are using a Servletenvironment, it sets up Spring MVC with the most commonly used view-resolvers, hander mappings and so forth.If it sees JSP and/or Thymeleaf, it sets up these view-layers.

Spring MVC offers no default (fall-back) error page out-of-the-box. The most common way to set a default errorpage has always been the SimpleMappingExceptionResolver (since Spring V1 in fact). HoweverSpring Boot also provides for a fallback error-handling page.

At start-up, Spring Boot tries to find a mapping for /error. By convention, a URL ending in /error maps toa logical view of the same name: error. In the demo application this view maps in turn to the error.htmlThymeleaf template. If using JSP, it would map to error.jsp according to the setup of yourInternalResourceViewResolver.

If no /error mapping can be found, Spring Boot defines its own fall-back error page - the so-called"Whitelabel Error Page" (a minimal page with just the HTTP status information and any error details, such asthe message from an uncaught exception). If you rename the error.html template to, say, error2.htmlthen restart, you will see it being used.

By defining a Java configuration @Beanmethod called defaultErrorView() you can return your ownerror View instance. (see Spring Boot's ErrorMvcAutoConfiguration class for more information).

What if you are already using SimpleMappingExceptionResolver to setup a defaulterror view? Simple, make sure the defaultErrorView defines the same view that Spring Boot uses: error.(Make sure you are using Spring Boot version 0.5.0.BUILD-SNAPSHOT or later. This does not work withmilestone 0.5.0.M5 or earlier). You can disable Spring boot's error page by setting the propertyerror.whitelabel.enabled to false. Your container's default error page is used instead.There are examples of setting this and other Spring Boot properties in the constructor ofMain

Note that in the demo, the defaultErrorView property of the SimpleMappingExceptionResolver isdeliberately set to defaultErrorPage so you can see when the handler is generating the error page and whenSpring Boot is responsible. Normally both would be set to error.

Also in the demo application I show how to create a support-ready error page with a stack-trace hidden in theHTML source (as a comment). Turns out you cannot currently do this with Thymeleaf (next release they say)so I have used JSP instead for just that page. There is some additional configuration in the demo code toallow JSP and Thymeleaf to work side by side (Spring Boot cannot set this up automatically - it needs applicationspecific information. See Javadoc inExtraThymeleafConfigurerfor details).

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值