本文共 5157 字,大约阅读时间需要 17 分钟。
异常处理 由发表在
Spring MVC框架提供了多种机制用来处理异常,初次接触可能会对他们用法以及适用的场景感到困惑。现在以一个简单例子来解释这些异常处理的机制。
假设现在我们开发了一个博客应用,其中最重要的资源就是文章(Post),应用中的URL设计如下:
GET /posts/
POST /posts/
GET /posts/{id}
PUT /posts/{id}
DELETE /posts/{id}
这是非常标准的复合RESTful风格的URL设计,在Spring MVC实现的应用过程中,相应也会有5个对应的用@RequestMapping
注解的方法来处理相应的URL请求。在处理某一篇文章的请求中(获取、更新、删除),无疑需要做这样一个判断——请求URL中的文章id是否在于系统中,如果不存在需要返回404 Not Found
。
在默认情况下,Spring MVC处理Web请求时如果发现存在没有应用代码捕获的异常,那么会返回HTTP 500(Internal Server Error)错误。但是如果该异常是我们自己定义的并且使用@ResponseStatus
注解进行修饰,那么Spring MVC则会返回指定的HTTP状态码:
@ResponseStatus(value = HttpStatus.NOT_FOUND, reason = "No Such Post")//404 Not Foundpublic class PostNotFoundException extends RuntimeException {}
在Controller
中可以这样使用它:
@RequestMapping(value = "/posts/{id}", method = RequestMethod.GET)public String showPost(@PathVariable("id") long id, Model model) { Post post = postService.get(id); if (post == null) throw new PostNotFoundException("post not found"); model.addAttribute("post", post); return "postDetail";}
这样如果我们访问了一个不存在的文章,那么Spring MVC会根据抛出的PostNotFoundException
上的注解值返回一个HTTP 404 Not Found给浏览器。
上述场景中,除了获取一篇文章的请求,还有更新和删除一篇文章的方法中都需要判断文章id是否存在。在每一个方法中都加上if (post == null) throw new PostNotFoundException("post not found");
是一种解决方案,但如果有10个、20个包含/posts/{id}
的方法,虽然只有一行代码但让他们重复10次、20次也是非常不优雅的。
为了解决这个问题,可以将这个逻辑放在Service中实现:
@Servicepublic class PostService { @Autowired private PostRepository postRepository; public Post get(long id) { return postRepository.findById(id) .orElseThrow(() -> new PostNotFoundException("post not found")); }}这里`PostRepository`继承了`JpaRepository`,可以定义`findById`方法返回一个`Optional`——如果不存在则Optional为空,抛出异常。
这样在所有的Controller
方法中,只需要正常活取文章即可,所有的异常处理都交给了Spring MVC。
Controller
中处理异常 Controller
中的方法除了可以用于处理Web请求,还能够用于处理异常处理——为它们加上@ExceptionHandler
即可:
@Controllerpublic 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; }}
首先需要明确的一点是,在Controller
方法中的@ExceptionHandler
方法只能够处理同一个Controller
中抛出的异常。这些方法上同时也可以继续使用@ResponseStatus
注解用于返回指定的HTTP状态码,但同时还能够支持更加丰富的异常处理:
ModelAndView
返回更多的业务信息大多数网站都会使用一个特定的页面来响应这些异常,而不是直接返回一个HTTP状态码或者显示Java异常调用栈。当然异常信息对于开发人员是非常有用的,如果想要在视图中直接看到它们可以这样渲染模板(以JSP为例):
Error Page
Application has encountered an error. Please contact support on ...
提供了和上一节一样的异常处理能力,但是可以被应用于Spring应用上下文中的所有@Controller
:
@ControllerAdviceclass GlobalControllerExceptionHandler { @ResponseStatus(HttpStatus.CONFLICT) // 409 @ExceptionHandler(DataIntegrityViolationException.class) public void handleConflict() { // Nothing to do }}
Spring MVC默认对于没有捕获也没有被@ResponseStatus
以及@ExceptionHandler
声明的异常,会直接返回500,这显然并不友好,可以在@ControllerAdvice
中对其进行处理(例如返回一个友好的错误页面,引导用户返回正确的位置或者提交错误信息):
@ControllerAdviceclass 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; }}
Spring在异常处理方面提供了一如既往的强大特性和支持,那么在应用开发中我们应该如何使用这些方法呢?以下提供一些经验性的准则:
@Controller
中自己进行异常处理逻辑。即使它只是一个Controller相关的特定异常,在@Controller
中添加一个@ExceptionHandler
方法处理。@ResponseStatus
注解@ControllerAdvice
处理通用异常(例如资源不存在、资源存在冲突等)