Spring MVC異常處理

由David發表在天碼營

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。

使用HTTP狀態碼

在默認情況下,Spring MVC處理Web請求時如果發現存在沒有應用代碼捕獲的異常,那麼會返回HTTP 500(Internal Server Error)錯誤。但是如果該異常是我們自己定義的並且使用@ResponseStatus註解進行修飾,那麼Spring MVC則會返回指定的HTTP狀態碼:

@ResponseStatus(value = HttpStatus.NOT_FOUND, reason = "No Such Post")//404 Not Foundnpublic class PostNotFoundException extends RuntimeException {n}n

在Controller中可以這樣使用它:

@RequestMapping(value = "/posts/{id}", method = RequestMethod.GET)npublic String showPost(@PathVariable("id") long id, Model model) {n Post post = postService.get(id);n if (post == null) throw new PostNotFoundException("post not found");n model.addAttribute("post", post);n return "postDetail";n}n

這樣如果我們訪問了一個不存在的文章,那麼Spring MVC會根據拋出的PostNotFoundException上的註解值返回一個HTTP 404 Not Found給瀏覽器。

最佳實踐

上述場景中,除了獲取一篇文章的請求,還有更新和刪除一篇文章的方法中都需要判斷文章id是否存在。在每一個方法中都加上if (post == null) throw new PostNotFoundException("post not found");是一種解決方案,但如果有10個、20個包含/posts/{id}的方法,雖然只有一行代碼但讓他們重複10次、20次也是非常不優雅的。

為了解決這個問題,可以將這個邏輯放在Service中實現:

@Servicenpublic class PostService {nn @Autowiredn private PostRepository postRepository;nn public Post get(long id) {n return postRepository.findById(id)n .orElseThrow(() -> new PostNotFoundException("post not found"));n }n}nn這裡`PostRepository`繼承了`JpaRepository`,可以定義`findById`方法返回一個`Optional<Post>`——如果不存在則Optional為空,拋出異常。n

這樣在所有的Controller方法中,只需要正常活取文章即可,所有的異常處理都交給了Spring MVC。

在Controller中處理異常

Controller中的方法除了可以用於處理Web請求,還能夠用於處理異常處理——為它們加上@ExceptionHandler即可:

@Controllernpublic class ExceptionHandlingController {nn // @RequestHandler methodsn ...nn // Exception handling methodsnn // Convert a predefined exception to an HTTP Status coden @ResponseStatus(value=HttpStatus.CONFLICT, reason="Data integrity violation") // 409n @ExceptionHandler(DataIntegrityViolationException.class)n public void conflict() {n // Nothing to don }nn // Specify the name of a specific view that will be used to display the error:n @ExceptionHandler({SQLException.class,DataAccessException.class})n public String databaseError() {n // Nothing to do. Returns the logical view name of an error page, passed ton // the view-resolver(s) in usual way.n // Note that the exception is _not_ available to this view (it is not added ton // the model) but see "Extending ExceptionHandlerExceptionResolver" below.n return "databaseError";n }nn // Total control - setup a model and return the view name yourself. Or considern // subclassing ExceptionHandlerExceptionResolver (see below).n @ExceptionHandler(Exception.class)n public ModelAndView handleError(HttpServletRequest req, Exception exception) {n logger.error("Request: " + req.getRequestURL() + " raised " + exception);nn ModelAndView mav = new ModelAndView();n mav.addObject("exception", exception);n mav.addObject("url", req.getRequestURL());n mav.setViewName("error");n return mav;n }n}n

首先需要明確的一點是,在Controller方法中的@ExceptionHandler方法只能夠處理同一個Controller中拋出的異常。這些方法上同時也可以繼續使用@ResponseStatus註解用於返回指定的HTTP狀態碼,但同時還能夠支持更加豐富的異常處理:

  • 渲染特定的視圖頁面
  • 使用ModelAndView返回更多的業務信息

大多數網站都會使用一個特定的頁面來響應這些異常,而不是直接返回一個HTTP狀態碼或者顯示Java異常調用棧。當然異常信息對於開發人員是非常有用的,如果想要在視圖中直接看到它們可以這樣渲染模板(以JSP為例):

<h1>Error Page</h1>n<p>Application has encountered an error. Please contact support on ...</p>nn<!--nFailed URL: ${url}nException: ${exception.message}n<c:forEach items="${exception.stackTrace}" var="ste"> ${ste} n</c:forEach>n-->n

全局異常處理

@ControllerAdvice提供了和上一節一樣的異常處理能力,但是可以被應用於Spring應用上下文中的所有@Controller:

@ControllerAdvicenclass GlobalControllerExceptionHandler {n @ResponseStatus(HttpStatus.CONFLICT) // 409n @ExceptionHandler(DataIntegrityViolationException.class)n public void handleConflict() {n // Nothing to don }n}n

Spring MVC默認對於沒有捕獲也沒有被@ResponseStatus以及@ExceptionHandler聲明的異常,會直接返回500,這顯然並不友好,可以在@ControllerAdvice中對其進行處理(例如返回一個友好的錯誤頁面,引導用戶返回正確的位置或者提交錯誤信息):

@ControllerAdvicenclass GlobalDefaultExceptionHandler {n public static final String DEFAULT_ERROR_VIEW = "error";nn @ExceptionHandler(value = Exception.class)n public ModelAndView defaultErrorHandler(HttpServletRequest req, Exception e) throws Exception {n // If the exception is annotated with @ResponseStatus rethrow it and letn // the framework handle it - like the OrderNotFoundException examplen // at the start of this post.n // AnnotationUtils is a Spring Framework utility class.n if (AnnotationUtils.findAnnotation(e.getClass(), ResponseStatus.class) != null)n throw e;nn // Otherwise setup and send the user to a default error-view.n ModelAndView mav = new ModelAndView();n mav.addObject("exception", e);n mav.addObject("url", req.getRequestURL());n mav.setViewName(DEFAULT_ERROR_VIEW);n return mav;n }n}n

總結

Spring在異常處理方面提供了一如既往的強大特性和支持,那麼在應用開發中我們應該如何使用這些方法呢?以下提供一些經驗性的準則:

  • 不要在@Controller中自己進行異常處理邏輯。即使它只是一個Controller相關的特定異常,在@Controller中添加一個@ExceptionHandler方法處理。
  • 對於自定義的異常,可以考慮對其加上@ResponseStatus註解
  • 使用@ControllerAdvice處理通用異常(例如資源不存在、資源存在衝突等)

歡迎關注天碼營微信公眾號: TMY-EDU

小編重點推薦:

Spring MVC實戰入門訓練

Spring Data JPA實戰入門訓練

Java Web實戰訓練

Node.js全棧開發

更多精彩內容請訪問天碼營網站
推薦閱讀:

面試官會問關於spring的哪些問題?
SpringMVC 如何與redis整合開發?
webservice,註解mybatis問題?

TAG:SpringBoot | SpringMVC框架 | 异常 |