Spring Boot干貨系列:(十三)Spring Boot全局異常處理整理

前言

今天來一起學(xué)習(xí)一下Spring Boot中的異常處理,在日常web開發(fā)中發(fā)生了異常,往往是需要通過一個統(tǒng)一的異常處理來保證客戶端能夠收到友好的提示。

正文

本篇要點(diǎn)如下

  • 介紹Spring Boot默認(rèn)的異常處理機(jī)制
  • 如何自定義錯誤頁面
  • 通過@ControllerAdvice注解來處理異常

介紹Spring Boot默認(rèn)的異常處理機(jī)制

默認(rèn)情況下,Spring Boot為兩種情況提供了不同的響應(yīng)方式。

一種是瀏覽器客戶端請求一個不存在的頁面或服務(wù)端處理發(fā)生異常時,一般情況下瀏覽器默認(rèn)發(fā)送的請求頭中Accept: text/html,所以Spring Boot默認(rèn)會響應(yīng)一個html文檔內(nèi)容,稱作“Whitelabel Error Page”。


image.png

另一種是使用Postman等調(diào)試工具發(fā)送請求一個不存在的url或服務(wù)端處理發(fā)生異常時,Spring Boot會返回類似如下的Json格式字符串信息

{
    "timestamp": "2018-05-12T06:11:45.209+0000",
    "status": 404,
    "error": "Not Found",
    "message": "No message available",
    "path": "/index.html"
} 

原理也很簡單,Spring Boot 默認(rèn)提供了程序出錯的結(jié)果映射路徑/error。這個/error請求會在BasicErrorController中處理,其內(nèi)部是通過判斷請求頭中的Accept的內(nèi)容是否為text/html來區(qū)分請求是來自客戶端瀏覽器(瀏覽器通常默認(rèn)自動發(fā)送請求頭內(nèi)容Accept:text/html)還是客戶端接口的調(diào)用,以此來決定返回頁面視圖還是 JSON 消息內(nèi)容。
相關(guān)BasicErrorController中代碼如下:


image.png

如何自定義錯誤頁面

好了,了解完Spring Boot默認(rèn)的錯誤機(jī)制后,我們來點(diǎn)有意思的,瀏覽器端訪問的話,任何錯誤Spring Boot返回的都是一個Whitelabel Error Page的錯誤頁面,這個很不友好,所以我們可以自定義下錯誤頁面。

1、先從最簡單的開始,直接在/resources/templates下面創(chuàng)建error.html就可以覆蓋默認(rèn)的Whitelabel Error Page的錯誤頁面,我項目用的是thymeleaf模板,對應(yīng)的error.html代碼如下:

image.png

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
動態(tài)error錯誤頁面
<p th:text="${error}"></p>
<p th:text="${status}"></p>
<p th:text="${message}"></p>
</body>
</html>

這樣運(yùn)行的時候,請求一個不存在的頁面或服務(wù)端處理發(fā)生異常時,展示的自定義錯誤界面如下:


image.png

2、此外,如果你想更精細(xì)一點(diǎn),根據(jù)不同的狀態(tài)碼返回不同的視圖頁面,也就是對應(yīng)的404,500等頁面,這里分兩種,錯誤頁面可以是靜態(tài)HTML(即,添加到任何靜態(tài)資源文件夾下),也可以使用模板構(gòu)建,文件的名稱應(yīng)該是確切的狀態(tài)碼。

  • 如果只是靜態(tài)HTML頁面,不帶錯誤信息的,在resources/public/下面創(chuàng)建error目錄,在error目錄下面創(chuàng)建對應(yīng)的狀態(tài)碼html即可 ,例如,要將404映射到靜態(tài)HTML文件,您的文件夾結(jié)構(gòu)如下所示:


    image.png

靜態(tài)404.html簡單頁面如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    靜態(tài)404錯誤頁面
</body>
</html>

這樣訪問一個錯誤路徑的時候,就會顯示靜態(tài)404錯誤頁面錯誤頁面

image.png

注:這時候如果存在上面第一種介紹的error.html頁面,則狀態(tài)碼錯誤頁面將覆蓋error.html,具體狀態(tài)碼錯誤頁面優(yōu)先級比較高。

  • 如果是動態(tài)模板頁面,可以帶上錯誤信息,在resources/templates/下面創(chuàng)建error目錄,在error目錄下面命名即可:
    image.png

這里我們模擬下500錯誤,控制層代碼,模擬一個除0的錯誤:

@Controller 
public class BaseErrorController extends  AbstractController{ 
private Logger logger = LoggerFactory.getLogger(this.getClass()); 

    @RequestMapping(value="/ex") 
    @ResponseBody 
    public String error(){ 
        int i=5/0; 
        return "ex"; 
    } 
} 

500.html代碼:

<!DOCTYPE html> 
<html xmlns:th="http://www.thymeleaf.org"> 
<head> 
<meta charset="UTF-8"> 
<title>Title</title> 
</head> 
<body> 
    動態(tài)500錯誤頁面 
    <p th:text="${error}"></p> 
    <p th:text="${status}"></p> 
    <p th:text="${message}"></p> 
</body> 
</html> 

這時訪問 http://localhost:8080/spring/ex 即可看到如下錯誤,說明確實映射到了500.html

image.png

注:如果同時存在靜態(tài)頁面500.html和動態(tài)模板的500.html,則后者覆蓋前者。即templates/error/這個的優(yōu)先級比resources/public/error高。

整體概括上面幾種情況,如下:

  • error.html會覆蓋默認(rèn)的 whitelabel Error Page 錯誤提示
  • 靜態(tài)錯誤頁面優(yōu)先級別比error.html高
  • 動態(tài)模板錯誤頁面優(yōu)先級比靜態(tài)錯誤頁面高

3、上面介紹的只是最簡單的覆蓋錯誤頁面的方式來自定義,如果對于某些錯誤你可能想特殊對待,則可以這樣

@Configuration 
public class ContainerConfig { 
    @Bean 
    public EmbeddedServletContainerCustomizer containerCustomizer(){ 
        return new EmbeddedServletContainerCustomizer(){ 
           @Override 
           public void customize(ConfigurableEmbeddedServletContainer container) { 
               container.addErrorPages(new ErrorPage(HttpStatus.INTERNAL_SERVER_ERROR, "/error/500")); 
           } 
        }; 
   } 
} 

上面這段代碼中HttpStatus.INTERNAL_SERVER_ERROR就是對應(yīng)500錯誤碼,也就是說程序如果發(fā)生500錯誤,就會將請求轉(zhuǎn)發(fā)到/error/500這個映射來,那我們只要實現(xiàn)一個方法是對應(yīng)這個/error/500映射即可捕獲這個異常做出處理

@RequestMapping("/error/500") 
@ResponseBody 
public String showServerError() { 
    return "server error"; 
} 

這樣,我們再請求前面提到的異常請求 http://localhost:8080/spring/ex 的時候,就會被我們這個方法捕獲了。

image.png

這里我們就只對500做了特殊處理,并且返還的是字符串,如果想要返回視圖,去掉 @ResponseBody注解,并返回對應(yīng)的視圖頁面。如果想要對其他狀態(tài)碼自定義映射,在customize方法中添加即可。

上面這種方法雖然我們重寫了/500映射,但是有一個問題就是無法獲取錯誤信息,想獲取錯誤信息的話,我們可以繼承BasicErrorController或者干脆自己實現(xiàn)ErrorController接口,除了用來響應(yīng)/error這個錯誤頁面請求,可以提供更多類型的錯誤格式等(BasicErrorController在上面介紹SpringBoot默認(rèn)異常機(jī)制的時候有提到)

這里博主選擇直接繼承BasicErrorController,然后把上面 /error/500映射方法添加進(jìn)來即可

@Controller
public class MyBasicErrorController extends BasicErrorController {

    public MyBasicErrorController() {
        super(new DefaultErrorAttributes(), new ErrorProperties());
    }

    /**
    * 定義500的ModelAndView
    * @param request
    * @param response
    * @return
    */

    @RequestMapping(produces = "text/html",value = "/500")
    public ModelAndView errorHtml500(HttpServletRequest request,HttpServletResponse response) {
        response.setStatus(getStatus(request).value());
        Map<String, Object> model = getErrorAttributes(request,isIncludeStackTrace(request, MediaType.TEXT_HTML));
        model.put("msg","自定義錯誤信息");
        return new ModelAndView("error/500", model);
    }

    /**
    * 定義500的錯誤JSON信息
    * @param request
    * @return
    */

    @RequestMapping(value = "/500")
    @ResponseBody

    public ResponseEntity<Map<String, Object>> error500(HttpServletRequest request) {
        Map<String, Object> body = getErrorAttributes(request,isIncludeStackTrace(request, MediaType.TEXT_HTML));
        HttpStatus status = getStatus(request);
        return new ResponseEntity<Map<String, Object>>(body, status);
    }
}

代碼也很簡單,只是實現(xiàn)了自定義的500錯誤的映射解析,分別對瀏覽器請求以及json請求做了回應(yīng)。

BasicErrorController默認(rèn)對應(yīng)的@RequestMapping是/error,固我們方法里面對應(yīng)的@RequestMapping(produces = "text/html",value = "/500")實際上完整的映射請求是/error/500,這就跟上面 customize 方法自定義的映射路徑對上了。

errorHtml500 方法中,我返回的是模板頁面,對應(yīng)/templates/error/500.html,這里順便自定義了一個msg信息,在500.html也輸出這個信息<p th:text="${msg}"></p>,如果輸出結(jié)果有這個信息,則表示我們配置正確了。

再次訪問請求http://localhost:8080/spring/ex ,結(jié)果如下

image.png

通過@ControllerAdvice注解來處理異常

Spring Boot提供的ErrorController是一種全局性的容錯機(jī)制。此外,你還可以用@ControllerAdvice注解和@ExceptionHandler注解實現(xiàn)對指定異常的特殊處理。

這里介紹兩種情況:

  • 局部異常處理 @Controller + @ExceptionHandler
  • 全局異常處理 @ControllerAdvice + @ExceptionHandler

局部異常處理 @Controller + @ExceptionHandler

局部異常主要用到的是@ExceptionHandler注解,此注解注解到類的方法上,當(dāng)此注解里定義的異常拋出時,此方法會被執(zhí)行。如果@ExceptionHandler所在的類是@Controller,則此方法只作用在此類。如果@ExceptionHandler所在的類帶有@ControllerAdvice注解,則此方法會作用在全局。

該注解用于標(biāo)注處理方法處理那些特定的異常。被該注解標(biāo)注的方法可以有以下任意順序的參數(shù)類型:

  • Throwable、Exception 等異常對象;

  • ServletRequest、HttpServletRequest、ServletResponse、HttpServletResponse;

  • HttpSession 等會話對象;

  • org.springframework.web.context.request.WebRequest;

  • java.util.Locale;

  • java.io.InputStream、java.io.Reader;

  • java.io.OutputStream、java.io.Writer;

  • org.springframework.ui.Model;

并且被該注解標(biāo)注的方法可以有以下的返回值類型可選:

  • ModelAndView;

  • org.springframework.ui.Model;

  • java.util.Map;

  • org.springframework.web.servlet.View;

  • @ResponseBody 注解標(biāo)注的任意對象;

  • HttpEntity<?> or ResponseEntity<?>;

  • void;

以上羅列的不完全,更加詳細(xì)的信息可參考:Spring ExceptionHandler。

舉個簡單例子,這里我們對除0異常用@ExceptionHandler來捕捉。

@Controller
public class BaseErrorController extends  AbstractController{ 
    private Logger logger = LoggerFactory.getLogger(this.getClass()); 

    @RequestMapping(value="/ex") 
    @ResponseBody 
    public String error(){ 
        int i=5/0; 
        return "ex"; 
  } 

    //局部異常處理 
    @ExceptionHandler(Exception.class) 
    @ResponseBody 
    public String exHandler(Exception e){ 
      // 判斷發(fā)生異常的類型是除0異常則做出響應(yīng) 
      if(e instanceof ArithmeticException){ 
          return "發(fā)生了除0異常"; 
      } 
      // 未知的異常做出響應(yīng) 
      return "發(fā)生了未知異常"; 
    }
} 
image.png

全局異常處理 @ControllerAdvice + @ExceptionHandler

在spring 3.2中,新增了@ControllerAdvice 注解,可以用于定義@ExceptionHandler、@InitBinder、@ModelAttribute,并應(yīng)用到所有@RequestMapping中。

簡單的說,進(jìn)入Controller層的錯誤才會由@ControllerAdvice處理,攔截器拋出的錯誤以及訪問錯誤地址的情況@ControllerAdvice處理不了,由SpringBoot默認(rèn)的異常處理機(jī)制處理。

我們實際開發(fā)中,如果是要實現(xiàn)RESTful API,那么默認(rèn)的JSON錯誤信息就不是我們想要的,這時候就需要統(tǒng)一一下JSON格式,所以需要封裝一下。

/**
* 返回數(shù)據(jù)
*/
public class AjaxObject extends HashMap<String, Object> {
    private static final long serialVersionUID = 1L;
 
    public AjaxObject() {
        put("code", 0);
    }
    
    public static AjaxObject error() {
        return error(HttpStatus.SC_INTERNAL_SERVER_ERROR, "未知異常,請聯(lián)系管理員");
    }
    
    public static AjaxObject error(String msg) {
        return error(HttpStatus.SC_INTERNAL_SERVER_ERROR, msg);
    }
    
    public static AjaxObject error(int code, String msg) {
        AjaxObject r = new AjaxObject();
        r.put("code", code);
        r.put("msg", msg);
        return r;
    }

    public static AjaxObject ok(String msg) {
        AjaxObject r = new AjaxObject();
        r.put("msg", msg);
        return r;
    }
    
    public static AjaxObject ok(Map<String, Object> map) {
        AjaxObject r = new AjaxObject();
        r.putAll(map);
        return r;
    }
    
    public static AjaxObject ok() {
        return new AjaxObject();
    }

    public AjaxObject put(String key, Object value) {
        super.put(key, value);
        return this;
    }
    
    public AjaxObject data(Object value) {
        super.put("data", value);
        return this;
    }

    public static AjaxObject apiError(String msg) {
        return error(1, msg);
    }
}

上面這個AjaxObject就是我平時用的,如果是正確情況返回的就是:

{
    code:0,
    msg:“獲取列表成功”,
    data:{ 
        queryList :[]
    }
}

正確默認(rèn)code返回0,data里面可以是集合,也可以是對象,如果是異常情況,返回的json則是:

{
    code:500,
    msg:“未知異常,請聯(lián)系管理員”
}

然后創(chuàng)建一個自定義的異常類:

public class BusinessException extends RuntimeException implements Serializable {

    private static final long serialVersionUID = 1L;
    private String msg;
    private int code = 500;
    
    public BusinessException(String msg) {
        super(msg);
        this.msg = msg;
    }
    
    public BusinessException(String msg, Throwable e) {
        super(msg, e);
        this.msg = msg;
    }
    
    public BusinessException(int code,String msg) {
        super(msg);
        this.msg = msg;
        this.code = code;
    }
    
    public BusinessException(String msg, int code, Throwable e) {
        super(msg, e);
        this.msg = msg;
        this.code = code;
    }

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }

    public int getCode() {
        return code;
    }

    public void setCode(int code) {
        this.code = code;
    }
}

注:spring 對于 RuntimeException 異常才會進(jìn)行事務(wù)回滾

Controler中添加一個json映射,用來處理這個異常

@Controller
public class BaseErrorController{
    @RequestMapping("/json")
    public void json(ModelMap modelMap) {
        System.out.println(modelMap.get("author"));
        int i=5/0;
    }
}

最后創(chuàng)建這個全局異常處理類:

/**
 * 異常處理器
 */
@RestControllerAdvice
public class BusinessExceptionHandler {
    private Logger logger = LoggerFactory.getLogger(getClass());



    /**
     * 應(yīng)用到所有@RequestMapping注解方法,在其執(zhí)行之前初始化數(shù)據(jù)綁定器
     * @param binder
     */
    @InitBinder
    public void initBinder(WebDataBinder binder) {
        System.out.println("請求有參數(shù)才進(jìn)來");
    }

    /**
     * 把值綁定到Model中,使全局@RequestMapping可以獲取到該值
     * @param model
     */
    @ModelAttribute
    public void addAttributes(Model model) {
        model.addAttribute("author", "嘟嘟MD");
    }

    @ExceptionHandler(Exception.class)
    public Object handleException(Exception e,HttpServletRequest req){
        AjaxObject r = new AjaxObject();
        //業(yè)務(wù)異常
        if(e instanceof BusinessException){
            r.put("code", ((BusinessException) e).getCode());
            r.put("msg", ((BusinessException) e).getMsg());
        }else{//系統(tǒng)異常
            r.put("code","500");
            r.put("msg","未知異常,請聯(lián)系管理員");
        }

        //使用HttpServletRequest中的header檢測請求是否為ajax, 如果是ajax則返回json, 如果為非ajax則返回view(即ModelAndView)
        String contentTypeHeader = req.getHeader("Content-Type");
        String acceptHeader = req.getHeader("Accept");
        String xRequestedWith = req.getHeader("X-Requested-With");
        if ((contentTypeHeader != null && contentTypeHeader.contains("application/json"))
                || (acceptHeader != null && acceptHeader.contains("application/json"))
                || "XMLHttpRequest".equalsIgnoreCase(xRequestedWith)) {
            return r;
        } else {
            ModelAndView modelAndView = new ModelAndView();
            modelAndView.addObject("msg", e.getMessage());
            modelAndView.addObject("url", req.getRequestURL());
            modelAndView.addObject("stackTrace", e.getStackTrace());
            modelAndView.setViewName("error");
            return modelAndView;
        }
    }
}

@ExceptionHandler 攔截了異常,我們可以通過該注解實現(xiàn)自定義異常處理。其中,@ExceptionHandler 配置的 value 指定需要攔截的異常類型,上面我配置了攔截Exception,
再根據(jù)不同異常類型返回不同的相應(yīng),最后添加判斷,如果是Ajax請求,則返回json,如果是非ajax則返回view,這里是返回到error.html頁面。

為了展示錯誤的時候更友好,我封裝了下error.html,不僅展示了錯誤,還添加了跳轉(zhuǎn)百度谷歌以及StackOverFlow的按鈕,如下:

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org" layout:decorator="layout">
<head>
    <title>Spring Boot管理后臺</title>
    <script type="text/javascript">
    </script>
</head>
<body>
<div layout:fragment="content" th:remove="tag">
    <div  id="navbar">
        <h1>系統(tǒng)異常統(tǒng)一處理</h1>
        <h3 th:text="'錯誤信息:'+${msg}"></h3>
        <h3 th:text="'請求地址:'+${url}"></h3>

        <h2>Debug</h2>
        <a th:href="@{'https://www.google.com/webhp?hl=zh-CN#safe=strict&hl=zh-CN&q='+${msg}}"
           class="btn btn-primary btn-lg" target="_blank" id="Google">Google</a>
        <a th:href="@{'https://www.baidu.com/s?wd='+${msg}}" class="btn btn-info btn-lg"  target="_blank" id="Baidu">Baidu</a>
        <a th:href="@{'http://stackoverflow.com/search?q='+${msg}}"
           class="btn btn-default btn-lg"  target="_blank" id="StackOverFlow">StackOverFlow</a>
        <h2>異常堆棧跟蹤日志StackTrace</h2>
        <div th:each="line:${stackTrace}">
            <div th:text="${line}"></div>
        </div>
    </div>
</div>
<div layout:fragment="js" th:remove="tag">
</div>
</body>
</html>

訪問http://localhost:8080/json的時候,因為是瀏覽器發(fā)起的,返回的是error界面:

image.png

如果是ajax請求,返回的就是錯誤:

{ "msg":"未知異常,請聯(lián)系管理員", "code":500 }

這里我給帶@ModelAttribute注解的方法通過Model設(shè)置了author值,在json映射方法中通過 ModelMwap 獲取到改值。

認(rèn)真的你可能發(fā)現(xiàn),全局異常類我用的是@RestControllerAdvice,而不是@ControllerAdvice,因為這里返回的主要是json格式,這樣可以少寫一個@ResponseBody。

總結(jié)

到此,SpringBoot中對異常的使用也差不多全了,本項目中處理異常的順序會是這樣,當(dāng)發(fā)送一個請求:

  • 攔截器那邊先判斷是否登錄,沒有則返回登錄頁。
  • 在進(jìn)入Controller之前,譬如請求一個不存在的地址,返回404錯誤界面。
  • 在執(zhí)行@RequestMapping時,發(fā)現(xiàn)的各種錯誤(譬如數(shù)據(jù)庫報錯、請求參數(shù)格式錯誤/缺失/值非法等)統(tǒng)一由@ControllerAdvice處理,根據(jù)是否Ajax返回json或者view。

想要查看更多Spring Boot干貨教程,可前往:Spring Boot干貨系列總綱

源碼下載

( ̄︶ ̄)↗[相關(guān)示例完整代碼]

  • chapter13==》Spring Boot干貨系列:(十三)Spring Boot全局異常處理整理

一直覺得自己寫的不是技術(shù),而是情懷,一篇篇文章是自己這一路走來的痕跡??繉I(yè)技能的成功是最具可復(fù)制性的,希望我的這條路能讓你少走彎路,希望我能幫你抹去知識的蒙塵,希望我能幫你理清知識的脈絡(luò),希望未來技術(shù)之巔上有你也有我。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容