
前言
今天來一起學(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”。

另一種是使用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中代碼如下:

如何自定義錯誤頁面
好了,了解完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代碼如下:

<!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ā)生異常時,展示的自定義錯誤界面如下:

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錯誤頁面錯誤頁面

注:這時候如果存在上面第一種介紹的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

注:如果同時存在靜態(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 的時候,就會被我們這個方法捕獲了。

這里我們就只對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é)果如下

通過@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ā)生了未知異常";
}
}

全局異常處理 @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界面:

如果是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ù)之巔上有你也有我。

