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

本來地址:Spring Boot乾貨系列:(十三)Spring Boot全局異常處理整理
博客地址:tengj.top/javascript

前言

今天來一塊兒學習一下Spring Boot中的異常處理,在平常web開發中發生了異常,每每是須要經過一個統一的異常處理來保證客戶端可以收到友好的提示。html

正文

本篇要點以下java

  • 介紹Spring Boot默認的異常處理機制
  • 如何自定義錯誤頁面
  • 經過@ControllerAdvice註解來處理異常

介紹Spring Boot默認的異常處理機制

默認狀況下,Spring Boot爲兩種狀況提供了不一樣的響應方式。git

一種是瀏覽器客戶端請求一個不存在的頁面或服務端處理髮生異常時,通常狀況下瀏覽器默認發送的請求頭中Accept: text/html,因此Spring Boot默認會響應一個html文檔內容,稱做「Whitelabel Error Page」。 github

image.png

另外一種是使用Postman等調試工具發送請求一個不存在的url或服務端處理髮生異常時,Spring Boot會返回相似以下的Json格式字符串信息web

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

原理也很簡單,Spring Boot 默認提供了程序出錯的結果映射路徑/error。這個/error請求會在BasicErrorController中處理,其內部是經過判斷請求頭中的Accept的內容是否爲text/html來區分請求是來自客戶端瀏覽器(瀏覽器一般默認自動發送請求頭內容Accept:text/html)仍是客戶端接口的調用,以此來決定返回頁面視圖仍是 JSON 消息內容。 相關BasicErrorController中代碼以下: ajax

image.png

如何自定義錯誤頁面

好了,瞭解完Spring Boot默認的錯誤機制後,咱們來點有意思的,瀏覽器端訪問的話,任何錯誤Spring Boot返回的都是一個Whitelabel Error Page的錯誤頁面,這個很不友好,因此咱們能夠自定義下錯誤頁面。spring

一、先從最簡單的開始,直接在/resources/templates下面建立error.html就能夠覆蓋默認的Whitelabel Error Page的錯誤頁面,我項目用的是thymeleaf模板,對應的error.html代碼以下: 數據庫

image.png

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

這樣運行的時候,請求一個不存在的頁面或服務端處理髮生異常時,展現的自定義錯誤界面以下: json

image.png

二、此外,若是你想更精細一點,根據不一樣的狀態碼返回不一樣的視圖頁面,也就是對應的404,500等頁面,這裏分兩種,錯誤頁面能夠是靜態HTML(即,添加到任何靜態資源文件夾下),也可使用模板構建,文件的名稱應該是確切的狀態碼。

  • 若是隻是靜態HTML頁面,不帶錯誤信息的,在resources/public/下面建立error目錄,在error目錄下面建立對應的狀態碼html便可 ,例如,要將404映射到靜態HTML文件,您的文件夾結構以下所示:
    image.png

靜態404.html簡單頁面以下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    靜態404錯誤頁面
</body>
</html>
複製代碼

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

image.png

注:這時候若是存在上面第一種介紹的error.html頁面,則狀態碼錯誤頁面將覆蓋error.html,具體狀態碼錯誤頁面優先級比較高。

  • 若是是動態模板頁面,能夠帶上錯誤信息,在resources/templates/下面建立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> 
    動態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

注:若是同時存在靜態頁面500.html和動態模板的500.html,則後者覆蓋前者。即templates/error/這個的優先級比resources/public/error高。

總體歸納上面幾種狀況,以下:

  • error.html會覆蓋默認的 whitelabel Error Page 錯誤提示
  • 靜態錯誤頁面優先級別比error.html高
  • 動態模板錯誤頁面優先級比靜態錯誤頁面高

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

@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就是對應500錯誤碼,也就是說程序若是發生500錯誤,就會將請求轉發到/error/500這個映射來,那咱們只要實現一個方法是對應這個/error/500映射便可捕獲這個異常作出處理

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

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

image.png

這裏咱們就只對500作了特殊處理,而且返還的是字符串,若是想要返回視圖,去掉 @ResponseBody註解,並返回對應的視圖頁面。若是想要對其餘狀態碼自定義映射,在customize方法中添加便可。

上面這種方法雖然咱們重寫了/500映射,可是有一個問題就是沒法獲取錯誤信息,想獲取錯誤信息的話,咱們能夠繼承BasicErrorController或者乾脆本身實現ErrorController接口,除了用來響應/error這個錯誤頁面請求,能夠提供更多類型的錯誤格式等(BasicErrorController在上面介紹SpringBoot默認異常機制的時候有提到)

這裏博主選擇直接繼承BasicErrorController,而後把上面 /error/500映射方法添加進來便可

@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);
    }
}
複製代碼

代碼也很簡單,只是實現了自定義的500錯誤的映射解析,分別對瀏覽器請求以及json請求作了迴應。

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

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

再次訪問請求http://localhost:8080/spring/ex ,結果以下

image.png

## 經過@ControllerAdvice註解來處理異常

Spring Boot提供的ErrorController是一種全局性的容錯機制。此外,你還能夠用@ControllerAdvice註解和@ExceptionHandler註解實現對指定異常的特殊處理。

這裏介紹兩種狀況:

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

局部異常處理 @Controller + @ExceptionHandler

局部異常主要用到的是@ExceptionHandler註解,此註解註解到類的方法上,當此註解裏定義的異常拋出時,此方法會被執行。若是@ExceptionHandler所在的類是@Controller,則此方法只做用在此類。若是@ExceptionHandler所在的類帶有@ControllerAdvice註解,則此方法會做用在全局。

該註解用於標註處理方法處理那些特定的異常。被該註解標註的方法能夠有如下任意順序的參數類型:

  • 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;

而且被該註解標註的方法能夠有如下的返回值類型可選:

  • ModelAndView;

  • org.springframework.ui.Model;

  • java.util.Map;

  • org.springframework.web.servlet.View;

  • @ResponseBody 註解標註的任意對象;

  • HttpEntity or ResponseEntity;

  • void;

以上羅列的不徹底,更加詳細的信息可參考:[Spring ExceptionHandler](https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/web/bind/annotation/ExceptionHandler.html)。

舉個簡單例子,這裏咱們對除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){ 
      // 判斷髮生異常的類型是除0異常則作出響應 
      if(e instanceof ArithmeticException){ 
          return "發生了除0異常"; 
      } 
      // 未知的異常作出響應 
      return "發生了未知異常"; 
    }
} 
複製代碼

image.png

全局異常處理 @ControllerAdvice + @ExceptionHandler

在spring 3.2中,新增了@ControllerAdvice 註解,能夠用於定義@ExceptionHandler、@InitBinder、@ModelAttribute,並應用到全部@RequestMapping中。

簡單的說,進入Controller層的錯誤纔會由@ControllerAdvice處理,攔截器拋出的錯誤以及訪問錯誤地址的狀況@ControllerAdvice處理不了,由SpringBoot默認的異常處理機制處理。

咱們實際開發中,若是是要實現RESTful API,那麼默認的JSON錯誤信息就不是咱們想要的,這時候就須要統一一下JSON格式,因此須要封裝一下。

/**
* 返回數據
*/
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, "未知異常,請聯繫管理員");
    }
    
    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 :[]
    }
}
複製代碼

正確默認code返回0,data裏面能夠是集合,也能夠是對象,若是是異常狀況,返回的json則是:

{
    code:500,
    msg:「未知異常,請聯繫管理員」
}
複製代碼

而後建立一個自定義的異常類:

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 異常纔會進行事務回滾

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

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

最後建立這個全局異常處理類:

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



	/**
	 * 應用到全部@RequestMapping註解方法,在其執行以前初始化數據綁定器
	 * @param binder
	 */
	@InitBinder
	public void initBinder(WebDataBinder binder) {
		System.out.println("請求有參數才進來");
	}

	/**
	 * 把值綁定到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();
		//業務異常
		if(e instanceof BusinessException){
			r.put("code", ((BusinessException) e).getCode());
			r.put("msg", ((BusinessException) e).getMsg());
		}else{//系統異常
			r.put("code","500");
			r.put("msg","未知異常,請聯繫管理員");
		}

		//使用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 攔截了異常,咱們能夠經過該註解實現自定義異常處理。其中,@ExceptionHandler 配置的 value 指定須要攔截的異常類型,上面我配置了攔截Exception, 再根據不一樣異常類型返回不一樣的相應,最後添加判斷,若是是Ajax請求,則返回json,若是是非ajax則返回view,這裏是返回到error.html頁面。

爲了展現錯誤的時候更友好,我封裝了下error.html,不只展現了錯誤,還添加了跳轉百度谷歌以及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>系統異常統一處理</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的時候,由於是瀏覽器發起的,返回的是error界面:

image.png

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

{ "msg":"未知異常,請聯繫管理員", "code":500 }
複製代碼

這裏我給帶@ModelAttribute註解的方法經過Model設置了author值,在json映射方法中經過 ModelMwap 獲取到改值。

認真的你可能發現,全局異常類我用的是@RestControllerAdvice,而不是@ControllerAdvice,由於這裏返回的主要是json格式,這樣能夠少寫一個@ResponseBody。

總結

到此,SpringBoot中對異常的使用也差很少全了,本項目中處理異常的順序會是這樣,當發送一個請求:

  • 攔截器那邊先判斷是否登陸,沒有則返回登陸頁。
  • 在進入Controller以前,譬如請求一個不存在的地址,返回404錯誤界面。
  • 在執行@RequestMapping時,發現的各類錯誤(譬如數據庫報錯、請求參數格式錯誤/缺失/值非法等)統一由@ControllerAdvice處理,根據是否Ajax返回json或者view。

想要查看更多Spring Boot乾貨教程,可前往:[Spring Boot乾貨系列總綱](http://tengj.top/2017/04/24/springboot0/)

# 源碼下載

( ̄︶ ̄)↗[[相關示例完整代碼](https://github.com/tengj/SpringBootDemo/tree/master)]

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

一直以爲本身寫的不是技術,而是情懷,一篇篇文章是本身這一路走來的痕跡。靠專業技能的成功是最具可複製性的,但願個人這條路能讓你少走彎路,但願我能幫你抹去知識的蒙塵,但願我能幫你理清知識的脈絡,但願將來技術之巔上有你也有我。

相關文章
相關標籤/搜索