SpringBoot2.1.4中的錯(cuò)誤處理機(jī)制
SpringBoot 2.1.4 錯(cuò)誤處理機(jī)制
springboot的自動(dòng)配置中幫我們配置了相關(guān)的錯(cuò)誤處理組件,例如訪問(wèn)一個(gè)不存在的頁(yè)面,就會(huì)出現(xiàn)下面的錯(cuò)誤頁(yè)面,上面也會(huì)顯示相應(yīng)的信息
在Postman軟件中模擬移動(dòng)端訪問(wèn),會(huì)獲取如下響應(yīng)的json數(shù)據(jù):
可以發(fā)現(xiàn)springboot的錯(cuò)誤處理機(jī)制很好的適應(yīng)了不同客戶端訪問(wèn),瀏覽器返回頁(yè)面,移動(dòng)端返回json,那這背后springboot是如何處理的,顯示的頁(yè)面我想自己設(shè)計(jì),或者返回的這些信息我們自己能夠定制嗎?
SpringBoot錯(cuò)誤機(jī)制原理
springboot版本:2.1.4.RELEASE
1、默認(rèn)錯(cuò)誤頁(yè)面生成機(jī)制
當(dāng)我們?cè)谠L問(wèn)一個(gè)不存在的路徑時(shí),會(huì)出現(xiàn)上面的錯(cuò)誤頁(yè)面,這個(gè)頁(yè)面不是我們自己創(chuàng)建的,而是由springboot幫我們生成的,那下面我們首先弄清楚這個(gè)默認(rèn)的錯(cuò)誤頁(yè)面(Whitelabel Error Page)是怎么生成的。
1.1 springboot關(guān)于error的自動(dòng)配置
在package org.springframework.boot.autoconfigure.web.servlet.error包下有如下的類(lèi):
- BasicErrorController、AbstractErrorController:錯(cuò)誤請(qǐng)求控制器
- DefaultErrorViewResolver:錯(cuò)誤視圖解析器
- ErrorMvcAutoConfiguration:error的自動(dòng)配置類(lèi)
ErrorMvcAutoConfiguration
在這個(gè)配置類(lèi)中注冊(cè)了一些組件:
@Bean @ConditionalOnMissingBean( value = {ErrorAttributes.class}, search = SearchStrategy.CURRENT ) // 關(guān)于error錯(cuò)誤信息的相關(guān)類(lèi) public DefaultErrorAttributes errorAttributes() { return new DefaultErrorAttributes(this.serverProperties.getError().isIncludeException()); } @Bean @ConditionalOnMissingBean( value = {ErrorController.class}, search = SearchStrategy.CURRENT ) // 處理錯(cuò)誤請(qǐng)求的控制器 public BasicErrorController basicErrorController(ErrorAttributes errorAttributes) { return new BasicErrorController(errorAttributes, this.serverProperties.getError(), this.errorViewResolvers); } @Bean // 錯(cuò)誤頁(yè)面定制器 public ErrorMvcAutoConfiguration.ErrorPageCustomizer errorPageCustomizer() { return new ErrorMvcAutoConfiguration.ErrorPageCustomizer(this.serverProperties, this.dispatcherServletPath); }
第一步:ErrorPageCustomizer
private static class ErrorPageCustomizer implements ErrorPageRegistrar, Ordered { private final ServerProperties properties; private final DispatcherServletPath dispatcherServletPath; protected ErrorPageCustomizer(ServerProperties properties, DispatcherServletPath dispatcherServletPath) { this.properties = properties; this.dispatcherServletPath = dispatcherServletPath; } public void registerErrorPages(ErrorPageRegistry errorPageRegistry) { // getPath()獲取到一個(gè)路徑“/error” ErrorPage errorPage = new ErrorPage(this.dispatcherServletPath.getRelativePath(this.properties.getError().getPath())); // 關(guān)鍵點(diǎn):這里講將/error的errorPage注冊(cè)到了servlet,在發(fā)生異常時(shí)就會(huì)轉(zhuǎn)發(fā)到/error errorPageRegistry.addErrorPages(new ErrorPage[]{errorPage}); } public int getOrder() { return 0; } }
注意上面的注釋?zhuān)@里是為什么發(fā)生錯(cuò)誤就會(huì)發(fā)起/error,很多博客都未說(shuō)明,當(dāng)然這里沒(méi)有討論其內(nèi)部原理。
第二步:BasicErrorController
在錯(cuò)誤發(fā)生后,發(fā)起 “/error” 請(qǐng)求,那這個(gè) “/error” 就會(huì)由上面已經(jīng)注冊(cè)的BasicErrorController 接收處理。
@Controller // 表明是個(gè)控制器 @RequestMapping({"${server.error.path:${error.path:/error}}"}) // 映射的路徑:/error public class BasicErrorController extends AbstractErrorController { private final ErrorProperties errorProperties; public BasicErrorController(ErrorAttributes errorAttributes, ErrorProperties errorProperties) { this(errorAttributes, errorProperties, Collections.emptyList()); } public BasicErrorController(ErrorAttributes errorAttributes, ErrorProperties errorProperties, List<ErrorViewResolver> errorViewResolvers) { super(errorAttributes, errorViewResolvers); Assert.notNull(errorProperties, "ErrorProperties must not be null"); this.errorProperties = errorProperties; } public String getErrorPath() { return this.errorProperties.getPath(); } // 處理瀏覽器的請(qǐng)求 @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); } // 處理移動(dòng)端的請(qǐng)求 @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 boolean isIncludeStackTrace(HttpServletRequest request, MediaType produces) { IncludeStacktrace include = this.getErrorProperties().getIncludeStacktrace(); if (include == IncludeStacktrace.ALWAYS) { return true; } else { return include == IncludeStacktrace.ON_TRACE_PARAM ? this.getTraceParameter(request) : false; } } protected ErrorProperties getErrorProperties() { return this.errorProperties; } }
這里可以解決一個(gè)疑惑,springboot怎么區(qū)分是瀏覽器還是移動(dòng)端的,主要看這個(gè)方法的注解 produces={“text/html”} ,表示響應(yīng)的數(shù)據(jù)是以html形式返回,這樣當(dāng)瀏覽器訪問(wèn)時(shí)就會(huì)調(diào)用這個(gè)方法
@RequestMapping(produces = {"text/html"}) public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response){ ...
客戶端訪問(wèn)時(shí)就會(huì)調(diào)用下面的error方法。
@RequestMapping public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
下面再來(lái)具體分析默認(rèn)錯(cuò)誤頁(yè)面如何生成,還是來(lái)看到errorHTML方法:
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) { // 獲取錯(cuò)誤狀態(tài)碼,封裝到HttpStatus里面 HttpStatus status = this.getStatus(request); // 獲取錯(cuò)誤信息,以map形式返回,這個(gè)后面我們具體來(lái)看,到底我們能獲取到哪些數(shù)據(jù) Map<String, Object> model = Collections.unmodifiableMap(this.getErrorAttributes(request, this.isIncludeStackTrace(request, MediaType.TEXT_HTML))); // 設(shè)置響應(yīng)體中狀態(tài)碼 response.setStatus(status.value()); // 關(guān)鍵點(diǎn):這里就是在創(chuàng)建視圖對(duì)象 ModelAndView modelAndView = this.resolveErrorView(request, response, status, model); return modelAndView != null ? modelAndView : new ModelAndView("error", model); }
下面來(lái)看這個(gè)resolveErrorView方法,這個(gè)方法是父類(lèi)AbstractErrorController 中的:
protected ModelAndView resolveErrorView(HttpServletRequest request, HttpServletResponse response, HttpStatus status, Map<String, Object> model) { // errorViewResolvers是一個(gè)list,存放ErrorViewResolver對(duì)象 Iterator var5 = this.errorViewResolvers.iterator(); ModelAndView modelAndView; // 遍歷集合 do { if (!var5.hasNext()) { return null; } ErrorViewResolver resolver = (ErrorViewResolver)var5.next(); // 關(guān)鍵點(diǎn):解析器對(duì)象進(jìn)行視圖解析 modelAndView = resolver.resolveErrorView(request, status, model); } while(modelAndView == null); return modelAndView; }
這里的resolveErrorView方法屬于DefaultErrorViewResolver:
public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) { // 調(diào)用下面的方法解析視圖,傳入?yún)?shù)為錯(cuò)誤狀態(tài)碼,錯(cuò)誤信息的map ModelAndView modelAndView = this.resolve(String.valueOf(status.value()), model); if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) { modelAndView = this.resolve((String)SERIES_VIEWS.get(status.series()), model); } return modelAndView; } private ModelAndView resolve(String viewName, Map<String, Object> model) { // 定義視圖名,這里我們可以確定視圖名:error/錯(cuò)誤碼,例如:error/404, String errorViewName = "error/" + viewName; // 這里結(jié)合上面的errorViewName,其實(shí)就是在template目錄下的error目錄進(jìn)行查找 // 我們默認(rèn)情況下是沒(méi)有error目錄,這里的provide最終值為null,代碼較多就不一一展示,有興趣的可以跟下去 TemplateAvailabilityProvider provider = this.templateAvailabilityProviders.getProvider(errorViewName, this.applicationContext); // 根據(jù)判定,這里會(huì)接著調(diào)用下面的resolveResource方法 return provider != null ? new ModelAndView(errorViewName, model) : this.resolveResource(errorViewName, model); } private ModelAndView resolveResource(String viewName, Map<String, Object> model) { // getStaticLocations()獲取的是靜態(tài)資源路徑:"classpath:/META-INF/resources/", "classpath:/resources/","classpath:/static/", "classpath:/public/" String[] var3 = this.resourceProperties.getStaticLocations(); int var4 = var3.length; // 遍歷上面的4個(gè)靜態(tài)資源路徑 for(int var5 = 0; var5 < var4; ++var5) { String location = var3[var5]; try { Resource resource = this.applicationContext.getResource(location); // 創(chuàng)建resource對(duì)象,例如error/404.html resource = resource.createRelative(viewName + ".html"); // 查找在對(duì)應(yīng)靜態(tài)資源目錄下是否有上面的這個(gè)資源對(duì)象,有就創(chuàng)建視圖對(duì)象 if (resource.exists()) { return new ModelAndView(new DefaultErrorViewResolver.HtmlResourceView(resource), model); } } catch (Exception var8) { ; } } // 都沒(méi)找到就返回null,默認(rèn)情況下是不存在error目錄的,所以這里最終返回null return null; }
當(dāng)resolveResource方法執(zhí)行完返回null,resolve方法也就返回null,在回到resolveErrorView
public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) { // 調(diào)用下面的方法解析視圖,傳入?yún)?shù)為錯(cuò)誤狀態(tài)碼,錯(cuò)誤信息的map ModelAndView modelAndView = this.resolve(String.valueOf(status.value()), model); // 上面分析得到modelAndView的值為null,下面的if中SERIES_VIEWS.containsKey(status.series())是在判斷錯(cuò)誤碼的首位是否為1,2,3,4,5,這個(gè)大家下去可以跟一下 if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) { modelAndView = this.resolve((String)SERIES_VIEWS.get(status.series()), model); } // if里面的resolve方法分析跟上面一樣,默認(rèn)情況下是沒(méi)有4xx.html/5xx.html頁(yè)面文件的,所以最終這里返回null return modelAndView; }
這個(gè)resolveErrorView方法執(zhí)行完后,我們就可以回到最開(kāi)始處理 “/error” 請(qǐng)求的errorHtml方法了
@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); // modelAnView根據(jù)上面的分析其值為null return modelAndView != null ? modelAndView : new ModelAndView("error", model); }
當(dāng)modelAndView為null時(shí),將會(huì)執(zhí)行'new ModelAndView(“error”, model),那這個(gè)“error”又是什么呢?看下面WhitelabelErrorViewConfiguration 里面有個(gè)組件其 name就是error,這個(gè)組件是StaticView,就是一個(gè)View,里面的視圖渲染方法render中的內(nèi)容就是最開(kāi)始我們看到的那個(gè)錯(cuò)誤頁(yè)面的內(nèi)容。
@Conditional({ErrorMvcAutoConfiguration.ErrorTemplateMissingCondition.class}) protected static class WhitelabelErrorViewConfiguration { private final ErrorMvcAutoConfiguration.StaticView defaultErrorView = new ErrorMvcAutoConfiguration.StaticView(); protected WhitelabelErrorViewConfiguration() { } @Bean(name = {"error"}) @ConditionalOnMissingBean(name = {"error"}) public View defaultErrorView() { return this.defaultErrorView; } ... } private static class StaticView implements View { private static final Log logger = LogFactory.getLog(ErrorMvcAutoConfiguration.StaticView.class); private StaticView() { } public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception { if (response.isCommitted()) { String message = this.getMessage(model); logger.error(message); } else { StringBuilder builder = new StringBuilder(); Date timestamp = (Date)model.get("timestamp"); Object message = model.get("message"); Object trace = model.get("trace"); if (response.getContentType() == null) { response.setContentType(this.getContentType()); } builder.append("<html><body><h1>Whitelabel Error Page</h1>").append("<p>This application has no explicit mapping for /error, so you are seeing this as a fallback.</p>").append("<div id='created'>").append(timestamp).append("</div>").append("<div>There was an unexpected error (type=").append(this.htmlEscape(model.get("error"))).append(", status=").append(this.htmlEscape(model.get("status"))).append(").</div>"); if (message != null) { builder.append("<div>").append(this.htmlEscape(message)).append("</div>"); } if (trace != null) { builder.append("<div style='white-space:pre-wrap;'>").append(this.htmlEscape(trace)).append("</div>"); } builder.append("</body></html>"); response.getWriter().append(builder.toString()); } } private String htmlEscape(Object input) { return input != null ? HtmlUtils.htmlEscape(input.toString()) : null; } private String getMessage(Map<String, ?> model) { Object path = model.get("path"); String message = "Cannot render error page for request [" + path + "]"; if (model.get("message") != null) { message = message + " and exception [" + model.get("message") + "]"; } message = message + " as the response has already been committed."; message = message + " As a result, the response may have the wrong status code."; return message; } public String getContentType() { return "text/html"; } }
所以,整個(gè)大致的過(guò)程到此結(jié)束了,默認(rèn)情況下錯(cuò)誤請(qǐng)求處理完成后就返回的這個(gè)StaticView定義的頁(yè)面,下圖做個(gè)基本的梳理。后續(xù)再來(lái)做自定義錯(cuò)誤頁(yè)面、自定義錯(cuò)誤數(shù)據(jù)的原理分析。
SpringBoot 2.1.3 錯(cuò)誤處理機(jī)制
引用的問(wèn)題做個(gè)標(biāo)記
以前的引用好像在新版本中無(wú)法引用了
錯(cuò)誤處理機(jī)制
其他的程序的類(lèi)的聲明直接用IDEA的提示來(lái)用就可以了。
如果還是有錯(cuò)誤的話,就進(jìn)入到lib中看看引用的類(lèi)的方法就可以了
import org.springframework.boot.autoconfigration.web.DefaultErrorAttributes;//這是以前的 import org.springframework.boot.web.servlet.error.DefaultErrorAttributes;//這是現(xiàn)在的
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
SpringCloud Eureka服務(wù)的基本配置和操作方法
Eureka是Netflix開(kāi)源的一個(gè)基于REST的服務(wù)治理框架,主要用于實(shí)現(xiàn)微服務(wù)架構(gòu)中的服務(wù)注冊(cè)與發(fā)現(xiàn),Eureka是Netflix開(kāi)源的服務(wù)發(fā)現(xiàn)框架,用于在分布式系統(tǒng)中實(shí)現(xiàn)服務(wù)的自動(dòng)注冊(cè)與發(fā)現(xiàn),本文介紹SpringCloud Eureka服務(wù)的基本配置和操作方法,感興趣的朋友一起看看吧2023-12-12VerifyCodeServlet(一次性驗(yàn)證碼)
這篇文章主要介紹了VerifyCodeServlet一次性驗(yàn)證碼的使用方法2017-05-05Swing中依據(jù)鼠標(biāo)拖拽來(lái)畫(huà)出矩形的實(shí)現(xiàn)方法
這篇文章主要介紹了Swing中依據(jù)鼠標(biāo)拖拽來(lái)畫(huà)出矩形的實(shí)現(xiàn)方法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-11-11Spring Bean創(chuàng)建和循環(huán)依賴
這篇文章主要介紹了Spring Bean創(chuàng)建和循環(huán)依賴,講述了Spring容器中?Bean?的創(chuàng)建過(guò)程已經(jīng)主要的方法,另外也著重分析了循環(huán)依賴的問(wèn)題,需要的小伙伴可以參考一下2022-05-05Java算法練習(xí)題,每天進(jìn)步一點(diǎn)點(diǎn)(2)
方法下面小編就為大家?guī)?lái)一篇Java算法的一道練習(xí)題(分享)。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧,希望可以幫到你2021-07-07詳解Spring Security如何在權(quán)限中使用通配符
小伙伴們知道,在Shiro中,默認(rèn)是支持權(quán)限通配符的?,F(xiàn)在給用戶授權(quán)的時(shí)候,可以一個(gè)權(quán)限一個(gè)權(quán)限的配置,也可以直接用通配符。本文將介紹Spring Security如何在權(quán)限中使用通配符,需要的可以參考一下2022-06-06Java中負(fù)數(shù)的絕對(duì)值竟然不一定是正數(shù)
這篇文章主要介紹了Java中負(fù)數(shù)的絕對(duì)值竟然不一定是正數(shù),文中給大家提到Java 中怎么把負(fù)數(shù)轉(zhuǎn)換為正數(shù),需要的朋友可以參考下2021-07-07