Spring 中異常處理的各種姿勢 ExceptionHandler

http://imushan.com/2017/11/27/java/spring/Spring%E7%AC%94%E8%AE%B0-%E5%BC%82%E5%B8%B8%E5%A4%84%E7%90%86/

https://juejin.im/post/5dd1fbcff265da0bf175d51d

https://www.cnblogs.com/muxi0407/p/11950475.html

Spring MVC提供了好幾種方法讓我來定製異常的處理。

本文參考:Exception Handling in Spring MVC

爲異常定製HTTP狀態碼

默認如果我們在controller中拋出異常,Spring MVC會給用戶響應500頁面,幷包含詳細的錯誤信息。

如果我們想修改錯誤對應的HTTP狀態碼,我們可以在對應的異常上面添加@ResponseStatus註解,通過這個註解我們可以設置這個異常對應的HTTP狀態碼和錯誤信息,例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Controller
public class ExceptionController {

    @RequestMapping("/")
    public void test(){
        throw new NotFoundException();
    }
}

@ResponseStatus(value = HttpStatus.NOT_FOUND, reason = "not found")
public class NotFoundException extends RuntimeException{

}

然後請求,可以發現頁面不一樣了:

Controller級別的錯誤攔截處理

通過@ResponseStatus註解,我們雖然可以定製HTTP狀態碼和錯誤信息了,但是完全不夠用。

第一,只能設置自己寫的異常,對於已有的異常,無法進行擴展。

第二,無法定製錯誤頁面,默認的錯誤頁面我們基本是不會使用的。

對於以上兩個問題,可以在Controller裏添加方法來攔截處理異常。方法需要使用@ExceptionHandler註解。註解後,方法會攔截當前Controller的請求處理方法(被@RequestMapping註解的方法)所拋出的異常。同時這個異常攔截方法,可以返回視圖,該視圖用於渲染錯誤信息。同時還可以在這個異常攔截方法上,使用@ResponseStatus來實現對已有異常的HTTP狀態碼定製,具體看例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
@Controller
public class ExceptionHandlingController {

  // 請求處理方法
  ...
  
  // 異常處理方法
  
  // 定製一個已有異常的HTTP狀態碼
  @ResponseStatus(value=HttpStatus.CONFLICT,
                  reason="Data integrity violation")  // 409
  @ExceptionHandler(DataIntegrityViolationException.class)
  public void conflict() {
    // 啥也不幹
  }
  
  // 指定view來渲染對應的異常
  @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.
    // 啥也不幹,就返回異常頁面view的名稱
    // 注意這裏的view訪問不到異常,因爲異常沒有添加到model中
    return "databaseError";
  }

  // 攔截該Controller拋出的所有異常,同時把異常信息通過ModelAndView傳給視圖
  // 或者你可以繼承ExceptionHandlerExceptionResolver來實現,見下文
  @ExceptionHandler(Exception.class)
  public ModelAndView handleError(HttpServletRequest req, Exception ex) {
    logger.error("Request: " + req.getRequestURL() + " raised " + ex);

    ModelAndView mav = new ModelAndView();
    mav.addObject("exception", ex);
    mav.addObject("url", req.getRequestURL());
    mav.setViewName("error");
    return mav;
  }
}

注意,使用@ExceptionHandler一定要指定處理的是哪個異常,否則會報異常:java.lang.IllegalArgumentException: No exception types mapped to {public java.lang.String XXController.exceptionHandler()}

全局異常處理

Controller級別的異常控制雖然已經夠強大了,但是我們總不可能每個Controller都寫一個handleError方法吧,所以我們一定需要一個全局的異常處理方法。藉助@ControllerAdvice可以簡單直接的實現這個需求。

@ControllerAdvice是Spring3.2添加的註解,和名字一樣,這個註解提供了增強Controller的功能,可把advice類中的@ExceptionHandler@InitBinder@ModelAttribute註解的方法應用到所有的Controller中去。最常用的就是@ExceptionHandler了。本來我們需要在每個Controller中定義@ExceptionHandler,現在我們可以聲明一個@ControllerAdvice類,然後定義一個統一的@ExceptionHandler方法。

比如上面的例子,用@ControllerAdvice的寫法如下:

1
2
3
4
5
6
7
8
@ControllerAdvice
class GlobalControllerExceptionHandler {
    @ResponseStatus(HttpStatus.CONFLICT)  // 409
    @ExceptionHandler(DataIntegrityViolationException.class)
    public void handleConflict() {
        // 啥也不幹
    }
}

如果你想攔截所有錯誤,那其實和上面的Controller級別的例子一樣,設置攔截的Exception爲Exception.class即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@ControllerAdvice
class GlobalDefaultExceptionHandler {
  public static final String DEFAULT_ERROR_VIEW = "error";

  @ExceptionHandler(value = Exception.class)
  public ModelAndView
  defaultErrorHandler(HttpServletRequest req, Exception e) throws Exception {
    // 這裏需要注意一下,因爲這個方法會攔截所有異常,包括設置了@ResponseStatus註解的異常,如果你不想攔截這些異常,可以過濾一下,然後重新拋出
    if (AnnotationUtils.findAnnotation
                (e.getClass(), ResponseStatus.class) != null)
      throw e;

    // 組裝異常信息給視圖
    ModelAndView mav = new ModelAndView();
    mav.addObject("exception", e);
    mav.addObject("url", req.getRequestURL());
    mav.setViewName(DEFAULT_ERROR_VIEW);
    return mav;
  }
}

更深層的攔截

上面說的Controller級別以及Controller Advice級別的攔截,是基於註解的,是高級特性。底層實現上,Spring使用的是HandlerExceptionResolver

所有定義在DispatcherServlet應用上下文中的bean,只要是實現了HandlerExceptionResolver接口,都會用來異常攔截處理。

看一下接口的定義:

1
2
3
4
public interface HandlerExceptionResolver {
    ModelAndView resolveException(HttpServletRequest request, 
            HttpServletResponse response, Object handler, Exception ex);
}

handler參數是拋出異常的Controller的引用。

Spring實現了幾種HandlerExceptionResolver,這些類是上面提到的幾個特性的基礎:

  • ExceptionHandlerExceptionResolver:判斷異常是否可以匹配到對應Controller或者Controller Advice中的@ExceptionHandler方法,如果可以則觸發(前文提到的異常攔截方法的特性就是這個類實現的)
  • ResponseStatusExceptionResolver:判斷異常是否被@ResponseStatus註解,如果是,則使用註解的信息來更新Response(前文提到的自定義HTTP狀態碼就是用這個特性實現的)
  • DefaultHandlerExceptionResolver:轉換Spring異常,並轉換爲HTTP狀態碼(Spring內部使用)

這幾個HandlerExceptionResolver會按照這個順序來執行,也就是異常處理鏈。

這裏可以看到,resolveException方法簽名中沒有Model參數,所以@ExceptionHandler方法也不能注入這個參數,所以上文中,異常攔截方法只能自己新建Model。

所以,如果你需要,你可以自己繼承HandlerExceptionResolver來實現自己的異常處理鏈。然後再實現Ordered接口,這樣就可以控制處理器的執行順序。

SimpleMappingExceptionResolver

Spring提供了一個很方便使用的HandlerExceptionResolver,叫SimpleMappingExceptionResolver。他有很多實用的功能:

  • 映射異常名稱到視圖名稱(異常名稱只需要指定類名,不需要包名)
  • 指定一個默認的錯誤頁面
  • 把異常打印到log上
  • 指定exception到視圖中的屬性名,默認的屬性名就是exception。(@ExceptionHandler方法指定的視圖默認沒法獲取異常,而SimpleMappingExceptionResolver指定的視圖可以)

用法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<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 is disabled unless you set a value. -->
    <property name="warnLogCategory" value="example.MvcLogger"/>
</bean>

Java Configuration:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Configuration
@EnableWebMvc  // Optionally setup Spring MVC defaults (if you aren't using
               // Spring Boot & haven't specified @EnableWebMvc 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;
  }
  ...
}

這裏最有用的可能就是defaultErrorView了,他可以用於定製默認的錯誤頁面。

自己繼承SimpleMappingExceptionResolver來擴展功能也是非常常見的

  • 繼承類可以在構造函數中設置好默認配置
  • 覆蓋buildLogMessage方法來自定義日誌信息,默認返回固定的:Handler execution resulted in exception
  • 覆蓋doResolveException方法,可以向錯誤日誌傳入更多自己需要的信息

例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class MyMappingExceptionResolver extends SimpleMappingExceptionResolver {
  public MyMappingExceptionResolver() {
    // 默認啓用日誌
    setWarnLogCategory(MyMappingExceptionResolver.class.getName());
  }

  @Override
  public String buildLogMessage(Exception e, HttpServletRequest req) {
    return "MVC exception: " + e.getLocalizedMessage();
  }
    
  @Override
  protected ModelAndView doResolveException(HttpServletRequest req,
        HttpServletResponse resp, Object handler, Exception ex) {
    // 調用父類飛方法來獲得ModelAndView
    ModelAndView mav = super.doResolveException(req, resp, handler, ex);
        
    // 添加額外的字段給視圖
    mav.addObject("url", request.getRequestURL());
    return mav;
  }
}

REST異常處理

REST風格下,返回的錯誤信息是一個json而不是一個頁面,要如何做呢?特別簡單,定義一個返回信息的類:

1
2
3
4
5
6
7
8
9
public class ErrorInfo {
    public final String url;
    public final String ex;

    public ErrorInfo(String url, Exception ex) {
        this.url = url;
        this.ex = ex.getLocalizedMessage();
    }
}

然後在錯誤處理函數上加上@ResponseBody就行:

1
2
3
4
5
6
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(MyBadDataException.class)
@ResponseBody ErrorInfo
handleBadRequest(HttpServletRequest req, Exception ex) {
    return new ErrorInfo(req.getRequestURL(), ex);
}

什麼時候用什麼特效?

Spring給我們提供了很多選擇,我們要如何選擇呢?

  • 如果異常是你自己聲明的,可以考慮使用@ResponseStatus註解
  • 其他的異常可以使用@ControllerAdvice中的@ExceptionHandler方法,或者用SimpleMappingExceptionResolver
  • 如果Controller需要定製異常,可以在Controller中添加@ExceptionHandler方法。

如果你混用這幾個特性,那要注意了,Controller中的@ExceptionHandler方法優先級比@ControllerAdvice中的@ExceptionHandler方法高,而如果有多個@ControllerAdvice類,那執行順序是不確定的。

 

如何讓我們的異常得到期望的返回格式,這裏就需要用到了@ControllerAdvice或者RestControllerAdvice(如果全部異常處理返回json,那麼可以使用 @RestControllerAdvice 代替 @ControllerAdvice ,這樣在方法上就可以不需要添加 @ResponseBody。)。類似與@Controller與@RestController的區別

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章