Springboot異常處理只會@ControllerAdvice+@ExceptionHandler?還遠遠不夠!

  • 2019 年 10 月 5 日
  • 筆記

當系統出現異常時候,或404,或500,默認返回的錯誤頁面通常非常簡陋,用戶也看不懂,這時候我們想通過一些手段,提示用戶訪問的資源不存在,或者請稍後再試。

同時有個統一的異常處理機制可以提高我們系統的健壯性,微服務化之後系統之間的調用結果會影響到整個服務的可用性。如果被調用方出現異常沒有返回統一的異常處理結果,很容易會調用方疑惑,然後滾大整個異常,這時候你看到整個服務之間都在報錯,這不是我們想看到的~

那麼基於springboot,我們有多少種異常處理方式呢?

靜態處理

這是一種比較偷懶也是最簡單的處理方式,直接放置一個靜態的頁面。我們靜態看到有些項目直接就返回一個大大的404圖片作為異常的處理顯示,其實就是這裡說到的靜態處理方式。

我們來看下錯誤頁面的存放位置:

可以看到,我是存放在了static目錄的error文件夾下,新建了一個404.html用於處理404錯誤。既然是靜態頁面,那麼就不能使用動態渲染,所以通常靜態的異常頁面都會寫得比較死,要麼就直接就是一個404圖片。

靜態頁面中如果寫了中文,這是顯示的內容容易亂碼,我們只需在配置文件application.properties中添加以下encoding程式碼:

spring.http.encoding.force=true

我們先來訪問一個不存在的路徑http://localhost:8080/xxxx,看下效果:

  • 未處理前:
  • 靜態處理後:

我們的404.html頁面起作用啦,如果不存在404.html,或者出現401異常的時候,系統就會自動匹配到4xx.html頁面,所以這個4xx相當於可以通配處理所有的客戶端錯誤:4xx。類似的500.html和5xx.html處理伺服器錯誤:5xx。

好,上面的靜態處理異常我們已經可以懂了,那麼你知道它的原理嗎?

其實在springboot項目啟動的時候,會去載入異常處理的默認配置ErrorMvcAutoConfiguration,而在ErrorMvcAutoConfiguration裡面,有個默認的異常處理控制器BasicErrorController(org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController),我們在這構造方法中打個端點,可以看到異常處理器errorViewResolvers的resourceProperties中就默認初始化好了所有可以存放靜態異常頁面的地方。

然後你再把端點打在ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response)方法上,你就會清晰看到,其實springboot項目會循環搜索這4個位置的文件夾,看時候有404.html頁面,如果有就直接返回,沒有就返回異常的默認處理頁面。

總結一下:靜態處理的錯誤頁面可以存放4個位置,分別按先後順序搜索

/META-INF/resources/error/404.html -> /resources/error/404.html  -> /static/error/404.html -> /public/error/404.html

當找不到精確匹配404.html的時候,就會找通配的4xx.html。

ok,靜態處理就先講到這裡~

動態處理

剛才我們說了一種靜態處理異常的方式,但是缺點很明顯,不能在靜態頁面中動態渲染數據啊!這無疑是比較致命的,有什麼辦法讓頁面能動態處理呢?

這裡給大家介紹4種方式

  • 直接在templates下寫error頁面,如果freemaker的error.ftl
  • 重寫ErrorController,覆蓋BasicErrorController
  • 繼承ErrorPageRegistrar,重寫registerErrorPages方法
  • @ControllerAdvice+@ExceptionHandler組合

1、直接寫error.ftl

這個其實和靜態處理中一樣,頁面處理器在靜態資源中找不到對應的頁面之後就會直接去templates下找view直接返回,默認的名字就叫做error,所以當我們直接在tempates下寫error.ftl時候,我們就可以直接展示動態錯誤處理頁面了。

但是這樣我們直接返回頁面,沒辦法自己控制錯誤的業務邏輯處理,所以,只有當我們出現錯誤之後沒有相關的處理,我們才這樣去展示。

2、重寫ErrorController

在靜態處理程式碼分析的時候我們說到了項目啟動時候就會自動載入默認異常處理配置ErrorMvcAutoConfiguration,會默認載入BasicErrorController作為異常處理的控制器。那麼我們想要自己定義處理邏輯的話,我們就直接覆蓋掉BasicErrorController,然後重寫一個就好了。

我們先來看下ErrorController長什麼樣子的

  • org.springframework.boot.web.servlet.error.ErrorController
@FunctionalInterface  public interface ErrorController {      String getErrorPath();  }

getErrorPath()其實表示的就是出現異常之後應該調用的鏈接,所以當我們如果返回的鏈接是/error時候,我們應該新建一個controller處理方法對應/error鏈接。

總結上面的邏輯,我寫了如下程式碼:

  • 1、實現ErrorController介面
  • 2、重寫getErrorPath()方法
  • 3、定義web頁面異常處理和非同步異常處理方法
@Configuration  @Controller  public class IErrorController implements ErrorController {        private final static String ERROR_PATH = "/error";        @Override      public String getErrorPath() {          return ERROR_PATH;      }        @RequestMapping(value = ERROR_PATH, produces = "text/html")      public String errorView(HttpServletRequest request) {            Object status = request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);          Object errorMess = request.getAttribute(RequestDispatcher.ERROR_MESSAGE);            request.setAttribute("message", "這是錯誤提示!!" + errorMess);            if (status != null) {              Integer statusCode = Integer.valueOf(status.toString());                if (statusCode == HttpStatus.NOT_FOUND.value()) {                  return "/common/404";              } else if (statusCode == HttpStatus.INTERNAL_SERVER_ERROR.value()) {                  return "/common/500";              }          }          return "/common/error";      }        @ResponseBody      @RequestMapping(value = ERROR_PATH)      public Object errorJson() {          return "這裡放回json數據~";      }  }

在templates/common文件夾下建好對應的頁面:

渲染結果:

3、繼承ErrorPageRegistrar

ErrorPageRegistrar是一個錯誤頁面的註冊器,在ErrorMvcAutoConfiguration中我們依然可以找到對應的源碼程式碼,它默認幫我們寫了一個ErrorPageCustomizer用於處理註冊的錯誤頁面,我們可以看到啟動時候,會默認先把/error頁面註冊進去。

接下來,我們來重寫一個ErrorPageRegistrar,先來看下介面的源程式碼:

public interface ErrorPageRegistrar {     void registerErrorPages(ErrorPageRegistry registry);  }

只有一個方法registerErrorPages,參數是錯誤頁面註冊中心ErrorPageRegistry。接下來我們自己來定義一個類。

  • com.example.config.MyErrorPageRegistrar
@Component  public class MyErrorPageRegistrar implements ErrorPageRegistrar {        @Override      public void registerErrorPages(ErrorPageRegistry errorPageRegistry) {            ErrorPage page404 = new ErrorPage(HttpStatus.NOT_FOUND, "/404");          ErrorPage page500 = new ErrorPage(HttpStatus.INTERNAL_SERVER_ERROR, "/500");            System.out.println("初始化了!");          errorPageRegistry.addErrorPages(page404, page500);      }  }

我們定義了兩個錯誤頁面,一個ErrorPage 404,還要ErrorPage 500,分別鏈接到/404,還有/500,所以需要給controller定義兩個方法:

@Controller  public class IndexController {        @RequestMapping("/404")      public String error404(HttpServletRequest request) {          request.setAttribute("message", "ErrorPageRegistrar的404頁面");          return "common/404";      }        @RequestMapping("/500")      public String error500() {          return "common/500";      }    }

這樣的話,就會分別鏈接到IndexController的方法裡面,然後再跳轉到對應的頁面中。這就實現了錯誤頁面的動態處理了。

我試了一下,當implements ErrorController和implements ErrorPageRegistrar兩種方式同時存在的時候,優先通過ErrorPageRegistrar來處理異常。這點得牢記哈。

4、@ControllerAdvice+@ExceptionHandler組合

接下來再聊聊一個人人都應懂得@ControllerAdvice+@ExceptionHandler組合。

其實不一定需要組合來一起用,當我們需要在某個特定控制器裡面處理特定異常時候,我們的@ExceptionHandler可以直接寫在controller中,這樣的話@ExceptionHandler就只能處理這個單個controller的異常。

那有時候我們想全局處理所有的控制器的異常,於是就有了@ControllerAdvice,它會控制器增強,會應用到所有的controller上,這樣就實現了我們想要的全局異常處理。

@Slf4j  @ControllerAdvice  public class GlobalExceptionHandler {        @ExceptionHandler(value = Exception.class)      public ModelAndView defaultErrorHandler(HttpServletRequest req, HttpServletResponse resp, Exception e) {            log.error("------------------>捕捉到全局異常", e);            if (req.getHeader("accept").contains("application/json")  || (req.getHeader("X-Requested-With")!= null                  && req.getHeader("X-Requested-With").contains("XMLHttpRequest") )) {              try {                  System.out.println(e.getMessage());                  Result result = Result.fail(e.getMessage(), "some error data");                    resp.setCharacterEncoding("utf-8");                  PrintWriter writer = resp.getWriter();                  writer.write(JSONUtil.toJsonStr(result));                  writer.flush();              } catch (IOException i) {                  i.printStackTrace();              }              return null;          }            if(e instanceof HwException) {              //...          }            ModelAndView mav = new ModelAndView();          mav.addObject("exception", e);          mav.addObject("message", e.getMessage());          mav.addObject("url", req.getRequestURL());          mav.setViewName("error");          return mav;      }    }

當然了,這樣的@ExceptionHandler(value = Exception.class)比價偷懶,你完全可以給value賦不同的Exception,然後針對不同的Exception類型做不同的處理。

下面是renren-fast項目的全局異常處理,我們來學習學習:

  • https://gitee.com/renrenio/renren-fast.git
@RestControllerAdvice  public class RRExceptionHandler {     private Logger logger = LoggerFactory.getLogger(getClass());       /**      * 處理自定義異常      */     @ExceptionHandler(RRException.class)     public R handleRRException(RRException e){        R r = new R();        r.put("code", e.getCode());        r.put("msg", e.getMessage());          return r;     }       @ExceptionHandler(NoHandlerFoundException.class)     public R handlerNoFoundException(Exception e) {        logger.error(e.getMessage(), e);        return R.error(404, "路徑不存在,請檢查路徑是否正確");     }       @ExceptionHandler(DuplicateKeyException.class)     public R handleDuplicateKeyException(DuplicateKeyException e){        logger.error(e.getMessage(), e);        return R.error("資料庫中已存在該記錄");     }       @ExceptionHandler(AuthorizationException.class)     public R handleAuthorizationException(AuthorizationException e){        logger.error(e.getMessage(), e);        return R.error("沒有許可權,請聯繫管理員授權");     }       @ExceptionHandler(Exception.class)     public R handleException(Exception e){        logger.error(e.getMessage(), e);        return R.error();     }  }

你可以看到,他對異常的類型分得很多,很清楚,這樣我們就可以讓異常展示的結果越具體。

總結

好了,不容易呀,終於寫完了,2個多小時,堅持原創的第二天(20190918),打卡打卡。希望你們會喜歡。

Tags: