0. What you need
- Spring framework 4.x
1. Goal of Exception handling
First let's clarify the goal for exception handling in Spring mvc/rest application:
Goal 1. Deprive exception handling code from business logic to make code cleaner. The fundamental try-catch mix the business code with exception handling code in methods of Controller, we want to separate them.
Goal 2. Fully cover all exception. Final user should not see any exception trace or server default return page. No pages like below should return to final user. Or in other words, there should be a default exception view for unhandled exceptions in spring application.
Goal 3. Different exceptions can result in different views. e.g any **CustomerException return /customerError page, any **OrderException return /orderError page. The web application can has more views for different exceptions
Goal 4. Mappings between exception and the output view should be configurable, not hard coded.
Goal 5. Modify mappings between exception and the output view doesn't need to change any business code.
2. Four common ways to handle exception in Sping MVC
2.1 @ResponseStatus + User defined Exceptions (Not recommend).
@ResponseStatus can be used at user defined exception class, which means if this exception is not handled by anyone else , the return code will be set to a specified value. It's not recommend because it only set the response http status code and then let the server to use its default page for that status code. Usually it'a a ugly html page. For example if a ResourceNotFoundException defined as below, with @ResponseStatus before class definition.
@ResponseStatus(value=HttpStatus.NOT_FOUND,reason="some description text") public class ResourceNotFoundException extends RuntimeException { }
Notice the application defined exception extends from RuntimeException, it's an unchecked exception. The controller BookController.java doesn't seem to have any exception related code.
package com.shengwang.demo.controller; //... import ignored ... @RestController @RequestMapping("/book") public class BookController { @Autowired private BookService bookService; @RequestMapping(value = "/{bookId}", method = RequestMethod.GET) public ResponseEntityfindById(@PathVariable long bookId) { Book book = bookService.findById(bookId); // may throw ResourceNotFoundException return ResponseEntity.ok(book); } }
If the bookService.findById() throw a application defined exception ResourceNotFoundException, The final result looks like below.
This page is ugly to final user. Furthurmore, this html error page is unsuitable for REST web service. For such case, it's preferable to use ResponseEntity as a return type and avoid the use of @ResponseStatus altogether. The 'not recommend' isn't for using your own exceptions, but for @ResponseStatus.
2.2 @ExceptionHandler in Controller
@ExceptionHandler is the key annotation to separate exception handling code from the normal business logic. @ExceptionHandler in a controller class only works for that controller.
package com.shengwang.demo.controller; //... import ignored .... @RestController @RequestMapping("/book") public class BookController { @Autowired private BookService bookService; @RequestMapping(value = "/{bookId}", method = RequestMethod.GET) public ResponseEntity<Book> findById(@PathVariable long bookId) { Book book = bookService.findById(bookId); // throw ResourceNotFoundException here return ResponseEntity.ok(book); } // handle any ResourceNotFoundException thrown from all methods of this controller @ExceptionHandler(ResourceNotFoundException.class) private ResponseEntity<Void> handleResourceNotFoundException(ResourceNotFoundException e) { return ResponseEntity.notFound().build(); } }
By using @ExceptionHandler, we extract exception handling to another method. make the code cleaner, especially when there are more methods may throw this ResourceNotFoundException exception. The exception handler method can return ResponseEntity<Void or YourErrorInfoClass> for REST APIs or ModelAndView / String as view name for other kind of Web applications to display error page.
2.3 @ControllerAdvice + @ExceptionHandler
@ExceptionHandler in a controller class only works for that controller. What if more controllers all need to handle the same exception? Annotation @ControllerAdvice can help to weaving exception handler method into more controller classes using the AOP way. This is pretty much same as using @ExceptionHandler in controller, the only difference is extracting all @ExceptionHandler method originally located in controllers in to an independent class with annotation @ControllerAdvice.
Another benefit of using @ControllerAdvice is making a default exception handler for all unhandled exceptions(handle Exception.class) easily.
package com.shengwang.demo.controller; //... import ignored .... @ControllerAdvice public class GlobalDefaultExceptionHandler { @ExceptionHandler(Exception.class) private ResponseEntity<Void> defaultExceptionHandler(Exception e) { // usually will log the exception first return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); } }
This can make sure final user will not see the exception call trace.
2.4 Defined your own HandlerExceptionResolver
What does an excpetion resolver in spring mvc do? It decides which view to display when a certain exception is thrown and not unhandled in other place of your code.
What is a excetion resolver in spring mvc? Any bean implement interface org.springframework.web.servlet.HandlerExceptionResolver.
public interface HandlerExceptionResolver { //only one method in this interface, return ModelAndView ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex); }
Notice the return type is ModelAndView, so it may be used more in NON-REST web applications to display an error page.
How to define you own HandlerExceptionResolver? Usually by using spring's SimpleMappingExceptionResolver class directly, or extending from it. Class SimpleMappingExceptionResolver allows you to set a mapping between exception and view's name, which can fulfill the goal 3,4, different excpetions can result in different error page. It also allow you to set a default view for unhandled exceptions. In xml styled spring configuration, add SimpleMappingExceptionResolver to context.
<bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver"> <!-- set default view for unhandled exception --> <property name="defaultErrorView" value="/defaultError"/> <property name="exceptionMappings"> <map> <entry key="ResourceNotFoundException" value="/resourceError" /> <!-- another way to set default view for unhandled exception <entry key="Exception" value="/defaultError" /> --> </map> </property> <!-- Name of logger to use to log exceptions. Unset by default, so logging disabled --> <property name="warnLogCategory" value="example.MvcLogger"/> </bean>
By using placeholder, you can move the hard-coded exception name and view name to a properties file to achieve goal 5.
3. When to use what?
There's no fixed rules in spring mvc. My preferable choices are:
- Extend your own exceptions from RuntimeException.
- @ExceptionHandler in controller class only for exceptions not common among controllers.
- For REST APIs, use @ControllerAdvice + @ExceptionHandler (return ResponseEntity<Void or YourErrorInfoClass>)
- For NON-REST web application, choice1, @ControllerAdvice+@ExceptionHandler(return ModelAndView or just String as view name). Or choice 2, your own HandlerExceptionResolver (for more flexible exception-view mapping configuration)
4. More
Besides the above ways, there are more ways in spring mvc for exception handling. e.g. extend the abstract class org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler to return a ResponseEntity. So it can also be used in RESTful applications. The ways to handle exception in Spring mvc are so flexible, but in real use, don't mix them too much, keep it simple, keep it clean.
See also Spring blog Exception Handling in Spring MVC by PAUL and his demo code on github
0 comments:
Post a Comment