完善的異常處理可以讓客戶端有一個(gè)良好的體驗(yàn),并且有利于定位出錯(cuò)原因,幫助解決問(wèn)題。
Spring Boot 內(nèi)置了一個(gè) /error 處理,當(dāng)拋出異常之后,會(huì)被轉(zhuǎn)到這個(gè)映射進(jìn)行處理, 就是常見的 Whitelable Error Page 。

但是這個(gè)界面長(zhǎng)相難看,不夠友好,需要我們做一些工作。
一、傳統(tǒng)項(xiàng)目中的錯(cuò)誤頁(yè)面
項(xiàng)目中發(fā)生異常之后,最好能給予一些必要的提示,在開發(fā)階段甚至可以打印出完整的堆棧信息,以快速定位解決問(wèn)題。
1)創(chuàng)建一個(gè)錯(cuò)誤界面error.html,用于展示錯(cuò)誤內(nèi)容
在resources>templates文件夾下,新建一個(gè)error.html的模板文件
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Spring Boot 錯(cuò)誤處理</title>
</head>
<body>
<h1>錯(cuò)誤處理</h1>
<h3 th:text="'錯(cuò)誤信息:'+${msg}"></h3>
<h3 th:text="'請(qǐng)求地址:'+${url}"></h3>
<h2>異常堆棧跟蹤日志StackTrace</h2>
<div th:each="line:${stackTrace}">
<div th:text="${line}"></div>
</div>
</body>
</html>
2)創(chuàng)建一個(gè)全局處理類。使用 @ControllerAdvice 和 @ExceptionHandler注解來(lái)處理錯(cuò)誤信息
新建一個(gè)exception的包,然后在此包下新建一個(gè)PageExceptionHandler的類
/**
* 傳統(tǒng)系統(tǒng)中的有錯(cuò)誤界面
*
* @author xiaozhao
* @date 2018/10/23下午1:59
*/
@ControllerAdvice
public class PageExceptionHandler {
/**
* 返回到錯(cuò)誤頁(yè)面,位于resources>templates>error.html文件
*
* @param req
* @param e
* @return
* @throws Exception
*/
@ExceptionHandler(value = Exception.class)
public ModelAndView pageExceptionHandle(HttpServletRequest req, Exception e) throws Exception {
ModelAndView modelAndView = new ModelAndView();
modelAndView.addObject("msg", e.getMessage());
modelAndView.addObject("url", req.getRequestURL());
// 模板名稱
modelAndView.setViewName("error");
return modelAndView;
}
}
現(xiàn)在使用一個(gè)控制器來(lái)測(cè)試一下,在控制器的代碼中拋出異常,例如:
/**
* 傳統(tǒng)項(xiàng)目中的界面
*
* @return
* @throws Exception
*/
@GetMapping("/page")
public String page() throws Exception {
throw new Exception("發(fā)生了界面錯(cuò)誤");
}
運(yùn)行之后,發(fā)出存在錯(cuò)誤的請(qǐng)求,顯示如下的界面

二、rest類型的錯(cuò)誤展示
這種場(chǎng)景下是不需要error.html的,只需要修改下@ControllerAdvice修飾的類即可,修改方法的返回值,然后添加@ResponseBody的注解
或者直接把@ControllerAdvice修改為@RestControllerAdvice即可,下面的方法中不再需要@ResponseBody注解
@RestControllerAdvice
public class RestExceptionHandler {
@ExceptionHandler(value = Exception.class)
public HttpResult<String> restExceptionHandle(Exception e) {
//......
}
}
或者
@ControllerAdvice
public class RestExceptionHandler {
@ExceptionHandler(value = Exception.class)
@ResponseBody
public HttpResult<String> restExceptionHandle(Exception e) {
//......
}
}
這里使用第一種方式
/**
* 處理類似rest風(fēng)格的接口,返回json
*
* @author xiaozhao
* @date 2018/10/23下午1:32
*/
@RestControllerAdvice
public class RestExceptionHandler {
private final Logger logger = LoggerFactory.getLogger(RestExceptionHandler.class);
/**
* 返回json
*
* @param e
* @return
*/
@ExceptionHandler(value = Exception.class)
public HttpResult<String> restExceptionHandle(Exception e) {
logger.error(e.getMessage());
HttpResult<String> result = new HttpResult<>();
result.setCode(-1);
result.setMsg(e.getMessage());
return result;
}
}
其中的HttpResult是一個(gè)包裝類,統(tǒng)一返回格式
/**
* http請(qǐng)求返回的最外層對(duì)象
*
* @author xiaozhao
* @date 2018/10/23下午1:34
*/
public class HttpResult<T> {
/**
* 錯(cuò)誤碼
*/
private Integer code;
/**
* 提示信息
*/
private String msg;
/**
* 具體的內(nèi)容
*/
private T data;
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
@Override
public String toString() {
return "HttpResult{" +
"code=" + code +
", msg='" + msg + '\'' +
", data=" + data +
'}';
}
}
控制器調(diào)用代碼
/**
* rest服務(wù),返回json數(shù)據(jù)
*
* @return
* @throws RestException
*/
@GetMapping("/rest")
@ResponseBody
public String hello() throws RestException {
throw new RestException("發(fā)生自定義rest錯(cuò)誤");
}
運(yùn)行后的結(jié)果

三、統(tǒng)一2種顯示
一般項(xiàng)目中可能會(huì)同時(shí)存在2種形式,上面2種形式差別就是返回值的不同,可以統(tǒng)一起來(lái),都返回Object類型,然后內(nèi)部根據(jù)請(qǐng)求頭的信息,判斷是那種類型的請(qǐng)求。
當(dāng)判斷請(qǐng)求為jons格式時(shí),使用HttpResult進(jìn)行返回,當(dāng)請(qǐng)求為界面時(shí),返回ModelAndView。
完整代碼如下:
**
* 處理項(xiàng)目中的異常,既包含傳統(tǒng)項(xiàng)目中的界面,也包含了rest形式的json返回
*
* @author xiaozhao
* @date 2018/10/23下午3:12
*/
@RestControllerAdvice
public class GlobalExceptionHandler2 {
private final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler2.class);
@ExceptionHandler(Exception.class)
public Object defaultExceptionHandle(Exception e, HttpServletRequest request) {
boolean isAjax = isAjax(request);
if (isAjax) {
logger.error("Ajax 請(qǐng)求異常" + e.getMessage());
HttpResult<String> result = new HttpResult<>();
result.setCode(-1);
result.setMsg(e.getMessage());
return result;
} else {
logger.error("界面請(qǐng)求異常" + e.getMessage());
ModelAndView modelAndView = new ModelAndView();
modelAndView.addObject("msg", e.getMessage());
modelAndView.addObject("url", request.getRequestURL());
modelAndView.addObject("stackTrace", e.getStackTrace());
modelAndView.setViewName("error");
return modelAndView;
}
}
/**
* 判斷網(wǎng)絡(luò)請(qǐng)求是否為ajax
*
* @param req
* @return
*/
private boolean isAjax(HttpServletRequest req) {
String contentTypeHeader = req.getHeader("Content-Type");
String acceptHeader = req.getHeader("Accept");
String xRequestedWith = req.getHeader("X-Requested-With");
return (contentTypeHeader != null && contentTypeHeader.contains("application/json"))
|| (acceptHeader != null && acceptHeader.contains("application/json"))
|| "XMLHttpRequest".equalsIgnoreCase(xRequestedWith);
}
}
最終代碼
https://github.com/xiaozhaowen/spring-boot-in-action/tree/master/springboot-handle-exception