spring boot異常處理
在spring mvc異常處理一文中,我介紹了在spring mvc中如何配置異常處理鏈、如何捕獲異常、如何構(gòu)建統(tǒng)一的異常處理。今天來(lái)談?wù)勅绾卧趕pring boot配置異常的統(tǒng)一處理方式。
spring boot和spring mvc中異常處理的不同
在spring mvc中,如果最終有未處理的異常,DispatcherServlet將會(huì)重新發(fā)送/error請(qǐng)求用于返回最終的錯(cuò)誤信息,但是Servlet本身并不提供全局的錯(cuò)誤頁(yè)面,而是需要開發(fā)者在web.xml中配置:
<error-page>
<location>/error</location>
</error-page>
也就是說(shuō),即便是采用Spring中的Bean配置方式一文中介紹的Java注解的方式實(shí)現(xiàn)無(wú)xml的系統(tǒng)配置,仍然需要開發(fā)者提供微型的web.xml配置文件來(lái)實(shí)現(xiàn)全局錯(cuò)誤文件的配置。
而在spring boot中,當(dāng)最終有未處理的異常拋出的時(shí)候,Servlet容器仍然會(huì)發(fā)送/error請(qǐng)求,但是和spring mvc不同的是,spring boot提供了內(nèi)置的BasicErrorController處理全局的錯(cuò)誤信息,不需要任何其他的配置。
下面通過一個(gè)簡(jiǎn)單的例子驗(yàn)證一下spring boot中默認(rèn)的異常處理流程:
-
首先在
HomeController中映射index請(qǐng)求,接口中什么都不做,僅拋出一個(gè)RuntimeException異常。@Controller public class HomeController { @GetMapping(value = "/index") @ResponseBody public String index() { throw new RuntimeException("runtime exception in /index"); } } 然后請(qǐng)求該接口,結(jié)果如下:

在這里,處理在一個(gè)請(qǐng)求接口中排出了異常,我們沒有做其他任何操作,spring boot自動(dòng)的給我們展示了這個(gè)whitelabel錯(cuò)誤信息。那么這是怎么實(shí)現(xiàn)的呢?
BasicErrorController完成默認(rèn)的異常處理
實(shí)際上,spring boot已經(jīng)為我們提供了/error請(qǐng)求的controller,它就是BasicErrorController。
BasicErrorController的源碼如下:
@Controller
@RequestMapping({"${server.error.path:${error.path:/error}}"})
public class BasicErrorController extends AbstractErrorController {
// ... 省略構(gòu)造函數(shù)
public String getErrorPath() {
return this.errorProperties.getPath();
}
@RequestMapping(
produces = {"text/html"}
)
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
HttpStatus status = this.getStatus(request);
Map<String, Object> model = Collections.unmodifiableMap(this.getErrorAttributes(request, this.isIncludeStackTrace(request, MediaType.TEXT_HTML)));
response.setStatus(status.value());
ModelAndView modelAndView = this.resolveErrorView(request, response, status, model);
return modelAndView != null ? modelAndView : new ModelAndView("error", model);
}
@RequestMapping
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
Map<String, Object> body = this.getErrorAttributes(request, this.isIncludeStackTrace(request, MediaType.ALL));
HttpStatus status = this.getStatus(request);
return new ResponseEntity(body, status);
}
protected ErrorProperties getErrorProperties() {
return this.errorProperties;
}
// ... 省略其他方法
}
從源碼可知:
-
BasicErrorController處理${server.error.path:${error.path:/error}請(qǐng)求。意思是:- 如果在
application.properties中設(shè)置了server.error.path,就映射該值; - 否則,如果
error.path有值就映射該值 - 否則映射
/error
可以通過修改
server.error.path和error.path讓BasicErrorController不再處理error請(qǐng)求 - 如果在
-
BasicErrorController有errorHtml和error兩種不同的處理接口處理請(qǐng)求,其中errorHtml特指http請(qǐng)求中accept屬性值為text/html的請(qǐng)求。如果請(qǐng)求的返回類型不同,可以為一個(gè)請(qǐng)求通過設(shè)置
produces指定特定的返回類型
自定義錯(cuò)誤頁(yè)面
spring boot默認(rèn)的錯(cuò)誤頁(yè)面顯然不能滿足開發(fā)的正常需求,通過在src/main/resources/templates文件夾中添加error.ftl(基于freemaker模板)錯(cuò)誤頁(yè)面實(shí)現(xiàn)自定義錯(cuò)誤信息。還可以通過在src/main/resources/templates/error中添加404.ft l等以http錯(cuò)誤碼開頭的頁(yè)面實(shí)現(xiàn)不同http錯(cuò)誤狀態(tài)的不同展現(xiàn)。結(jié)構(gòu)如下圖:

如果
error文件夾下有對(duì)應(yīng)的狀態(tài)碼錯(cuò)誤頁(yè)面,則會(huì)渲染該頁(yè)面;否則,渲染error.flt頁(yè)面。
具體的錯(cuò)誤頁(yè)面的內(nèi)容,大家根據(jù)自己項(xiàng)目的情況,設(shè)計(jì)自己的展示樣式。
統(tǒng)一異常處理
前文說(shuō)過,/error請(qǐng)求的觸發(fā)前提是系統(tǒng)中拋出的異常到最終都沒有被處理掉,在spring mvc異常處理中提到可以通過@ControllerAdvice和@ExceptionHandler實(shí)現(xiàn)捕獲系統(tǒng)中的異常,在spring boot中該方法同樣奏效。需要注意的是,如果@ControllerAdvice中如果有其他異常沒有捕獲到,最終仍然會(huì)通過BasicErrorController處理這些異常。
統(tǒng)一異常處理部分代碼如下:
@RestControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
private static final Logger LOGGER = LoggerFactory.getLogger(GlobalExceptionHandler.class);
/**
* 接口參數(shù)異常
* @param e
* @return
*/
@ExceptionHandler(value = {InterfaceIllegalArgumentException.class})
public Map<String, Object> handleIllegalArgumentException(InterfaceIllegalArgumentException e) {
LOGGER.error(e.getMessage(), e);
return OutPut.failure(HttpStatusWrapper.ILLEGAL_REQUEST_PARAMETERS, e.getMessage());
}
/**
* 其他未知異常
* @param e
* @return
*/
@ExceptionHandler(value = {Exception.class})
public Map<String, Object> handleException(Exception e) {
LOGGER.error(e.getMessage(), e);
return OutPut.failure(HttpStatusWrapper.INTERNAL_SERVER_ERROR, ResponseMsg.QUERY_FAILURE);
}
/**
* 請(qǐng)求闡述不匹配錯(cuò)誤
* @param ex
* @param headers
* @param status
* @param request
* @return
*/
@Override
protected ResponseEntity<Object> handleTypeMismatch(TypeMismatchException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
return ResponseEntity.status(HttpStatusWrapper.REQUEST_PARAMETER_TYPE_MISMATCH.getCode()).body(OutPut.failure(HttpStatusWrapper.REQUEST_PARAMETER_TYPE_MISMATCH, ex.getValue() + "的類型不匹配,需要" + ex.getRequiredType()));
}
/**
* 請(qǐng)求的類型不支持
* @param ex
* @param headers
* @param status
* @param request
* @return
*/
@Override
protected ResponseEntity<Object> handleHttpMediaTypeNotSupported(HttpMediaTypeNotSupportedException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
final String supportMediaTypes = ex.getSupportedMediaTypes().stream()
.map(MimeType::getType)
.collect(Collectors.joining(","));
return ResponseEntity.status(HttpStatusWrapper.UNSUPPORTED_MEDIA_TYPE.getCode()).body(OutPut.failure(HttpStatusWrapper.UNSUPPORTED_MEDIA_TYPE, ex.getContentType() + "is not supported, the support media type are" + String.join(",", supportMediaTypes)));
}
}