SpringBoot 錯(cuò)誤處理機(jī)制與自定義錯(cuò)誤處理實(shí)現(xiàn)詳解
【1】SpringBoot的默認(rèn)錯(cuò)誤處理
① 瀏覽器訪問(wèn)
請(qǐng)求頭如下:
② 使用“PostMan”訪問(wèn)
{ "timestamp": 1529479254647, "status": 404, "error": "Not Found", "message": "No message available", "path": "/aaa1" }
請(qǐng)求頭如下:
總結(jié):如果是瀏覽器訪問(wèn),則SpringBoot默認(rèn)返回錯(cuò)誤頁(yè)面;如果是其他客戶端訪問(wèn),則默認(rèn)返回JSON數(shù)據(jù)。
【2】默認(rèn)錯(cuò)誤處理原理
SpringBoot默認(rèn)配置了許多xxxAutoConfiguration,這里我們找ErrorMvcAutoConfiguration。
其注冊(cè)部分組件如下:
① DefaultErrorAttributes
@Bean @ConditionalOnMissingBean(value = ErrorAttributes.class, search = SearchStrategy.CURRENT) public DefaultErrorAttributes errorAttributes() { return new DefaultErrorAttributes(); }
跟蹤其源碼如下:
public class DefaultErrorAttributes implements ErrorAttributes, HandlerExceptionResolver, Ordered { private static final String ERROR_ATTRIBUTE = DefaultErrorAttributes.class.getName() + ".ERROR"; @Override public int getOrder() { return Ordered.HIGHEST_PRECEDENCE; } @Override public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { storeErrorAttributes(request, ex); return null; } private void storeErrorAttributes(HttpServletRequest request, Exception ex) { request.setAttribute(ERROR_ATTRIBUTE, ex); } @Override public Map<String, Object> getErrorAttributes(RequestAttributes requestAttributes, boolean includeStackTrace) { Map<String, Object> errorAttributes = new LinkedHashMap<String, Object>(); errorAttributes.put("timestamp", new Date()); addStatus(errorAttributes, requestAttributes); addErrorDetails(errorAttributes, requestAttributes, includeStackTrace); addPath(errorAttributes, requestAttributes); return errorAttributes; } private void addStatus(Map<String, Object> errorAttributes, RequestAttributes requestAttributes) { Integer status = getAttribute(requestAttributes, "javax.servlet.error.status_code"); if (status == null) { errorAttributes.put("status", 999); errorAttributes.put("error", "None"); return; } errorAttributes.put("status", status); try { errorAttributes.put("error", HttpStatus.valueOf(status).getReasonPhrase()); } catch (Exception ex) { // Unable to obtain a reason errorAttributes.put("error", "Http Status " + status); } } private void addErrorDetails(Map<String, Object> errorAttributes, RequestAttributes requestAttributes, boolean includeStackTrace) { Throwable error = getError(requestAttributes); if (error != null) { while (error instanceof ServletException && error.getCause() != null) { error = ((ServletException) error).getCause(); } errorAttributes.put("exception", error.getClass().getName()); addErrorMessage(errorAttributes, error); if (includeStackTrace) { addStackTrace(errorAttributes, error); } } Object message = getAttribute(requestAttributes, "javax.servlet.error.message"); if ((!StringUtils.isEmpty(message) || errorAttributes.get("message") == null) && !(error instanceof BindingResult)) { errorAttributes.put("message", StringUtils.isEmpty(message) ? "No message available" : message); } } private void addErrorMessage(Map<String, Object> errorAttributes, Throwable error) { BindingResult result = extractBindingResult(error); if (result == null) { errorAttributes.put("message", error.getMessage()); return; } if (result.getErrorCount() > 0) { errorAttributes.put("errors", result.getAllErrors()); errorAttributes.put("message", "Validation failed for object='" + result.getObjectName() + "'. Error count: " + result.getErrorCount()); } else { errorAttributes.put("message", "No errors"); } } private BindingResult extractBindingResult(Throwable error) { if (error instanceof BindingResult) { return (BindingResult) error; } if (error instanceof MethodArgumentNotValidException) { return ((MethodArgumentNotValidException) error).getBindingResult(); } return null; } private void addStackTrace(Map<String, Object> errorAttributes, Throwable error) { StringWriter stackTrace = new StringWriter(); error.printStackTrace(new PrintWriter(stackTrace)); stackTrace.flush(); errorAttributes.put("trace", stackTrace.toString()); } private void addPath(Map<String, Object> errorAttributes, RequestAttributes requestAttributes) { String path = getAttribute(requestAttributes, "javax.servlet.error.request_uri"); if (path != null) { errorAttributes.put("path", path); } } @Override public Throwable getError(RequestAttributes requestAttributes) { Throwable exception = getAttribute(requestAttributes, ERROR_ATTRIBUTE); if (exception == null) { exception = getAttribute(requestAttributes, "javax.servlet.error.exception"); } return exception; } @SuppressWarnings("unchecked") private <T> T getAttribute(RequestAttributes requestAttributes, String name) { return (T) requestAttributes.getAttribute(name, RequestAttributes.SCOPE_REQUEST); } }
即,填充錯(cuò)誤數(shù)據(jù)!
② BasicErrorController
@Bean @ConditionalOnMissingBean(value = ErrorController.class, search = SearchStrategy.CURRENT) public BasicErrorController basicErrorController(ErrorAttributes errorAttributes) { return new BasicErrorController(errorAttributes, this.serverProperties.getError(), this.errorViewResolvers); }
跟蹤其源碼:
@Controller @RequestMapping("${server.error.path:${error.path:/error}}") public class BasicErrorController extends AbstractErrorController { //產(chǎn)生html類型的數(shù)據(jù);瀏覽器發(fā)送的請(qǐng)求來(lái)到這個(gè)方法處理 @RequestMapping(produces = "text/html") public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) { HttpStatus status = getStatus(request); Map<String, Object> model = Collections.unmodifiableMap(getErrorAttributes( request, isIncludeStackTrace(request, MediaType.TEXT_HTML))); response.setStatus(status.value()); //去哪個(gè)頁(yè)面作為錯(cuò)誤頁(yè)面;包含頁(yè)面地址和頁(yè)面內(nèi)容 ModelAndView modelAndView = resolveErrorView(request, response, status, model); return (modelAndView == null ? new ModelAndView("error", model) : modelAndView); } //產(chǎn)生json數(shù)據(jù),其他客戶端來(lái)到這個(gè)方法處理; @RequestMapping @ResponseBody public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) { Map<String, Object> body = getErrorAttributes(request, isIncludeStackTrace(request, MediaType.ALL)); HttpStatus status = getStatus(request); return new ResponseEntity<Map<String, Object>>(body, status); } //... }
其中 resolveErrorView(request, response, status, model);方法跟蹤如下:
public abstract class AbstractErrorController implements ErrorController { protected ModelAndView resolveErrorView(HttpServletRequest request, HttpServletResponse response, HttpStatus status, Map<String, Object> model) { //拿到所有的錯(cuò)誤視圖解析器 for (ErrorViewResolver resolver : this.errorViewResolvers) { ModelAndView modelAndView = resolver.resolveErrorView(request, status, model); if (modelAndView != null) { return modelAndView; } } return null; } //... }
③ ErrorPageCustomizer
@Bean public ErrorPageCustomizer errorPageCustomizer() { return new ErrorPageCustomizer(this.serverProperties); }
跟蹤其源碼:
@Override public void registerErrorPages(ErrorPageRegistry errorPageRegistry) { ErrorPage errorPage = new ErrorPage(this.properties.getServletPrefix() + this.properties.getError().getPath()); errorPageRegistry.addErrorPages(errorPage); } //getPath()->go on /** * Path of the error controller. */ @Value("${error.path:/error}") private String path = "/error";
即,系統(tǒng)出現(xiàn)錯(cuò)誤以后來(lái)到error請(qǐng)求進(jìn)行處理(web.xml注冊(cè)的錯(cuò)誤頁(yè)面規(guī)則)。
④ DefaultErrorViewResolver
@Bean @ConditionalOnBean(DispatcherServlet.class) @ConditionalOnMissingBean public DefaultErrorViewResolver conventionErrorViewResolver() { return new DefaultErrorViewResolver(this.applicationContext, this.resourceProperties); }
跟蹤其源碼:
public class DefaultErrorViewResolver implements ErrorViewResolver, Ordered { private static final Map<Series, String> SERIES_VIEWS; //錯(cuò)誤狀態(tài)碼 static { Map<Series, String> views = new HashMap<Series, String>(); views.put(Series.CLIENT_ERROR, "4xx"); views.put(Series.SERVER_ERROR, "5xx"); SERIES_VIEWS = Collections.unmodifiableMap(views); } //... @Override public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) { // 這里如果沒(méi)有拿到精確狀態(tài)碼(如404)的視圖,則嘗試拿4XX(或5XX)的視圖 ModelAndView modelAndView = resolve(String.valueOf(status), model); if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) { modelAndView = resolve(SERIES_VIEWS.get(status.series()), model); } return modelAndView; } private ModelAndView resolve(String viewName, Map<String, Object> model) { //默認(rèn)SpringBoot可以去找到一個(gè)頁(yè)面? error/404||error/4xx String errorViewName = "error/" + viewName; //模板引擎可以解析這個(gè)頁(yè)面地址就用模板引擎解析 TemplateAvailabilityProvider provider = this.templateAvailabilityProviders .getProvider(errorViewName, this.applicationContext); if (provider != null) { //模板引擎可用的情況下返回到errorViewName指定的視圖地址 return new ModelAndView(errorViewName, model); } //模板引擎不可用,就在靜態(tài)資源文件夾下找errorViewName對(duì)應(yīng)的頁(yè)面 error/404.html return resolveResource(errorViewName, model); } private ModelAndView resolveResource(String viewName, Map<String, Object> model) { //從靜態(tài)資源文件夾下面找錯(cuò)誤頁(yè)面 for (String location : this.resourceProperties.getStaticLocations()) { try { Resource resource = this.applicationContext.getResource(location); resource = resource.createRelative(viewName + ".html"); if (resource.exists()) { return new ModelAndView(new HtmlResourceView(resource), model); } } catch (Exception ex) { } } return null; }
總結(jié)如下:
一但系統(tǒng)出現(xiàn)4xx或者5xx之類的錯(cuò)誤,ErrorPageCustomizer就會(huì)生效(定制錯(cuò)誤的響應(yīng)規(guī)則),就會(huì)來(lái)到/error請(qǐng)求,然后被BasicErrorController處理返回ModelAndView或者JSON。
【3】定制錯(cuò)誤響應(yīng)
① 定制錯(cuò)誤響應(yīng)頁(yè)面
1)有模板引擎的情況下
error/狀態(tài)碼–將錯(cuò)誤頁(yè)面命名為 錯(cuò)誤狀態(tài)碼.html 放在模板引擎文件夾里面的error文件夾下,發(fā)生此狀態(tài)碼的錯(cuò)誤就會(huì)來(lái)到 對(duì)應(yīng)的頁(yè)面。
我們可以使用4xx和5xx作為錯(cuò)誤頁(yè)面的文件名來(lái)匹配這種類型的所有錯(cuò)誤,精確優(yōu)先(優(yōu)先尋找精確的狀態(tài)碼.html)。
如下圖所示:
頁(yè)面能獲取的信息;
timestamp:時(shí)間戳
status:狀態(tài)碼
error:錯(cuò)誤提示
exception:異常對(duì)象
message:異常消息
errors:JSR303數(shù)據(jù)校驗(yàn)的錯(cuò)誤都在這里
2)沒(méi)有模板引擎(模板引擎找不到這個(gè)錯(cuò)誤頁(yè)面),靜態(tài)資源文件夾下找。
3)以上都沒(méi)有錯(cuò)誤頁(yè)面,就是默認(rèn)來(lái)到SpringBoot默認(rèn)的錯(cuò)誤提示頁(yè)面。
WebMVCAutoConfiguration源碼如下:
@Configuration @ConditionalOnProperty(prefix = "server.error.whitelabel", name = "enabled", matchIfMissing = true) @Conditional(ErrorTemplateMissingCondition.class) protected static class WhitelabelErrorViewConfiguration { private final SpelView defaultErrorView = new SpelView( "<html><body><h1>Whitelabel Error Page</h1>" + "<p>This application has no explicit mapping for /error, so you are seeing this as a fallback.</p>" + "<div id='created'>${timestamp}</div>" + "<div>There was an unexpected error (type=${error}, status=${status}).</div>" + "<div>${message}</div></body></html>"); @Bean(name = "error") @ConditionalOnMissingBean(name = "error") public View defaultErrorView() { return this.defaultErrorView; } // If the user adds @EnableWebMvc then the bean name view resolver from // WebMvcAutoConfiguration disappears, so add it back in to avoid disappointment. @Bean @ConditionalOnMissingBean(BeanNameViewResolver.class) public BeanNameViewResolver beanNameViewResolver() { BeanNameViewResolver resolver = new BeanNameViewResolver(); resolver.setOrder(Ordered.LOWEST_PRECEDENCE - 10); return resolver; } }
② 定制錯(cuò)誤響應(yīng)數(shù)據(jù)
第一種,使用SpringMVC的異常處理器
@ControllerAdvice public class MyExceptionHandler { //瀏覽器客戶端返回的都是json @ResponseBody @ExceptionHandler(UserNotExistException.class) public Map<String,Object> handleException(Exception e){ Map<String,Object> map = new HashMap<>(); map.put("code","user.notexist"); map.put("message",e.getMessage()); return map; } }
這樣無(wú)論瀏覽器還是PostMan返回的都是JSON!
第二種,轉(zhuǎn)發(fā)到/error請(qǐng)求進(jìn)行自適應(yīng)效果處理
@ExceptionHandler(UserNotExistException.class) public String handleException(Exception e, HttpServletRequest request){ Map<String,Object> map = new HashMap<>(); //傳入我們自己的錯(cuò)誤狀態(tài)碼 4xx 5xx /** * Integer statusCode = (Integer) request .getAttribute("javax.servlet.error.status_code"); */ request.setAttribute("javax.servlet.error.status_code",500); map.put("code","user.notexist"); map.put("message","用戶出錯(cuò)啦"); //轉(zhuǎn)發(fā)到/error return "forward:/error"; }
但是此時(shí)沒(méi)有將自定義 code message傳過(guò)去!
第三種,注冊(cè)MyErrorAttributes繼承自DefaultErrorAttributes(推薦)
從第【2】部分(默認(rèn)錯(cuò)誤處理原理)中知道錯(cuò)誤數(shù)據(jù)都是通過(guò)DefaultErrorAttributes.getErrorAttributes()
方法獲取,如下所示:
@Override public Map<String, Object> getErrorAttributes(RequestAttributes requestAttributes, boolean includeStackTrace) { Map<String, Object> errorAttributes = new LinkedHashMap<String, Object>(); errorAttributes.put("timestamp", new Date()); addStatus(errorAttributes, requestAttributes); addErrorDetails(errorAttributes, requestAttributes, includeStackTrace); addPath(errorAttributes, requestAttributes); return errorAttributes; }
我們可以編寫一個(gè)MyErrorAttributes繼承自DefaultErrorAttributes重寫其getErrorAttributes方法將我們的錯(cuò)誤數(shù)據(jù)添加進(jìn)去。
示例如下:
//給容器中加入我們自己定義的ErrorAttributes @Component public class MyErrorAttributes extends DefaultErrorAttributes { //返回值的map就是頁(yè)面和json能獲取的所有字段 @Override public Map<String, Object> getErrorAttributes(RequestAttributes requestAttributes, boolean includeStackTrace) { //DefaultErrorAttributes的錯(cuò)誤數(shù)據(jù) Map<String, Object> map = super.getErrorAttributes(requestAttributes, includeStackTrace); map.put("company","SpringBoot"); //我們的異常處理器攜帶的數(shù)據(jù) Map<String,Object> ext = (Map<String, Object>) requestAttributes.getAttribute("ext", 0); map.put("ext",ext); return map; } }
異常處理器修改如下:
@ExceptionHandler(UserNotExistException.class) public String handleException(Exception e, HttpServletRequest request){ Map<String,Object> map = new HashMap<>(); //傳入我們自己的錯(cuò)誤狀態(tài)碼 4xx 5xx /** * Integer statusCode = (Integer) request .getAttribute("javax.servlet.error.status_code"); */ request.setAttribute("javax.servlet.error.status_code",500); map.put("code","user.notexist"); map.put("message","用戶出錯(cuò)啦"); //將自定義錯(cuò)誤數(shù)據(jù)放入request中 request.setAttribute("ext",map); //轉(zhuǎn)發(fā)到/error return "forward:/error"; }
5xx.html頁(yè)面代碼如下:
//... <main role="main" class="col-md-9 ml-sm-auto col-lg-10 pt-3 px-4"> <h1>status:[[${status}]]</h1> <h2>timestamp:[[${timestamp}]]</h2> <h2>exception:[[${exception}]]</h2> <h2>message:[[${message}]]</h2> <h2>ext:[[${ext.code}]]</h2> <h2>ext:[[${ext.message}]]</h2> </main> //...
瀏覽器測(cè)試效果如下:
Postman測(cè)試效果如下:
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
SpringBoot Session共享實(shí)現(xiàn)圖解
這篇文章主要介紹了SpringBoot Session共享實(shí)現(xiàn)圖解,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-01-01Java實(shí)現(xiàn)發(fā)送短信驗(yàn)證碼功能
這篇文章主要為大家詳細(xì)介紹了Java實(shí)現(xiàn)發(fā)送短信驗(yàn)證碼功能,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-11-11Springboot并發(fā)調(diào)優(yōu)之大事務(wù)和長(zhǎng)連接
這篇文章主要介紹了Springboot并發(fā)調(diào)優(yōu)之大事務(wù)和長(zhǎng)連接,重點(diǎn)分享長(zhǎng)事務(wù)以及長(zhǎng)連接導(dǎo)致的并發(fā)排查和優(yōu)化思路和示例,具有一定的參考價(jià)值,感興趣的可以了解一下2022-05-05java實(shí)現(xiàn)時(shí)間控制的幾種方案
這篇文章主要介紹了java實(shí)現(xiàn)時(shí)間控制的幾種方案,本文從多個(gè)方面給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2019-07-07IDEA搭建多模塊的Maven項(xiàng)目方式(相互依賴)
這篇文章主要介紹了IDEA搭建多模塊的Maven項(xiàng)目方式(相互依賴),具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-08-08SpringBoot使用自定義注解實(shí)現(xiàn)數(shù)據(jù)脫敏過(guò)程詳細(xì)解析
這篇文章主要介紹了SpringBoot自定義注解之脫敏注解詳解,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-02-02Java?將list集合數(shù)據(jù)按照時(shí)間字段排序的方法
這篇文章主要介紹了Java?將list集合數(shù)據(jù)按照時(shí)間字段排序,本文通過(guò)示例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-03-03Spring MVC項(xiàng)目中l(wèi)og4J和AOP使用詳解
項(xiàng)目日志記錄是項(xiàng)目開(kāi)發(fā)、運(yùn)營(yíng)必不可少的內(nèi)容,有了它可以對(duì)系統(tǒng)有整體的把控,出現(xiàn)任何問(wèn)題都有蹤跡可尋。下面這篇文章主要給大家介紹了關(guān)于Spring MVC項(xiàng)目中l(wèi)og4J和AOP使用的相關(guān)資料,需要的朋友可以參考下。2017-12-12Java語(yǔ)言中&&與& ||與|的區(qū)別是什么
這篇文章主要介紹了Java語(yǔ)言中&&與& ||與|的區(qū)別是什么的相關(guān)資料,需要的朋友可以參考下2017-04-04