SpringBoot(九)---錯誤處理機制源碼分析

一、默認處理機制

1.瀏覽器

當瀏覽器發送錯誤請求時,SpringBoot會有一個默認的錯誤處理,例如:

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-8iCAEfAq-1588145231789)(C:\Users\four and ten\Desktop\筆記\JAVA\Spring Boot\32.png)]

SpringBoot會返回一個錯誤頁面,這個錯誤頁面裏有狀態碼和錯誤信息

2.postman

在這裏插入圖片描述

會返回json數據

二、默認錯誤處理機制代碼分析

錯誤處理機制的自動配置都在ErrorMvcAutoConfiguration這個類裏,這個

1.ErrorPageCustomizer

ErrorMvcAutoConfiguration添加了ErrorPageCustomizer這個組件

@Bean
public ErrorPageCustomizer errorPageCustomizer() {
    return new ErrorPageCustomizer(this.serverProperties);
}

ErrorPageCustomizer這個組件是添加錯誤映射

private static class ErrorPageCustomizer implements ErrorPageRegistrar, Ordered {

    private final ServerProperties properties;

    protected ErrorPageCustomizer(ServerProperties properties) {
        this.properties = properties;
    }

    @Override
    public void registerErrorPages(ErrorPageRegistry errorPageRegistry) {
        ErrorPage errorPage = new ErrorPage(this.properties.getServletPrefix()
                                            + this.properties.getError().getPath());
        errorPageRegistry.addErrorPages(errorPage);
    }

    @Override
    public int getOrder() {
        return 0;
    }
}

那麼一但發生錯誤式,會去到那個頁面呢,就在getPath()方法裏找答案

	/**
	 * Path of the error controller.
	 */
	@Value("${error.path:/error}")
	private String path = "/error";

也就是說,如果瀏覽器一但發送錯誤請求,會發送/error請求

2.BasicErrorController

當服務器收到/error請求時,來到BasicErrorController來處理/error請求

@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {

很顯然,這個Controller就是專門用來處理/error請求的,根據一開始。當使用不同的客戶端時,響應的數據格式不一樣,那麼具體代碼就在這個Controller中

@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
	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());
		ModelAndView modelAndView = resolveErrorView(request, response, status, model);
		return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
	}

	@RequestMapping
	public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
		HttpStatus status = getStatus(request);
		if (status == HttpStatus.NO_CONTENT) {
			return new ResponseEntity<>(status);
		}
		Map<String, Object> body = getErrorAttributes(request, isIncludeStackTrace(request, MediaType.ALL));
		return new ResponseEntity<>(body, status);
	}

當使用瀏覽器發生錯誤頁面時:

@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)

public static final String TEXT_HTML_VALUE = "text/html";

瀏覽器的請求頭帶有test/html,進入第一個RequestMapping,返回ModelAndView

在這裏插入圖片描述

當使用其他客戶端時,進入另外一個RequestMapping中處理請求,返回鍵值對集合,也就是json數據。

3.DefaultErrorViewResolver

錯誤頁面的Controller是帶有text/html的請求頭的請求

@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
	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());
        //生成錯誤頁面
		ModelAndView modelAndView = resolveErrorView(request, response, status, model);
		return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
	}

實現resolveErrorView的具體實現類是DefaultErrorViewResolver,打開這個類

	@Override
	public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) {
		ModelAndView modelAndView = resolve(String.valueOf(status.value()), 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) {
        //默認SpringBoot去找一個頁面 error/狀態碼
		String errorViewName = "error/" + viewName;
        //如果模板引擎可以解析這個頁面地址就用模板引擎解析
		TemplateAvailabilityProvider provider = this.templateAvailabilityProviders.getProvider(errorViewName,
				this.applicationContext);
		if (provider != null) {
            //模板引擎可用的情況下返回到errorViewName指定的視圖地址
			return new ModelAndView(errorViewName, model);
		}
        ////模板引擎不可用,就在靜態資源文件夾下找errorViewName對應的頁面 error/狀態碼.html
		return resolveResource(errorViewName, model);
	}

到這裏就可以得出定製錯誤頁面的方法,可以在靜態資源文件夾下創建error/狀態碼.html或者在template文件夾下創建error/狀態碼.html

4.DefaultErrorAttributes

在BasicErrorController調用了getErrorAttributes方法,而這個方法的具體實現是DefaultErrorAttributes中的,作用就是共享錯誤信息

@Override
	public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
		Map<String, Object> errorAttributes = new LinkedHashMap<>();
		errorAttributes.put("timestamp", new Date());
		addStatus(errorAttributes, webRequest);
		addErrorDetails(errorAttributes, webRequest, includeStackTrace);
		addPath(errorAttributes, webRequest);
		return errorAttributes;
	}
5.總結

當瀏覽器(或者其他客戶端)發送了錯誤請求時,會來到/error請求,這個/error請求映射是在ErrorPageCustomizer這個組件中完成的,BasicErrorController收到/error請求開始處理這個請求,然後根據請求頭來判斷是瀏覽器還是其他客戶端,如果是瀏覽器,那麼返回ModelAndView;如果是其他客戶端返回json數據

DefaultErrorViewResolver這個組件便是生成錯誤頁面的,返回一個ModelAndView對象,而這些視圖對象中有一些共享的數據,例如timestamp,status等等,DefaultErrorAttributes完成了這些共享數據的解析。

三、定製錯誤頁面

DefaultErrorViewResolver的resolve方法告訴我們如何去定製自己的錯誤頁面

private ModelAndView resolve(String viewName, Map<String, Object> model) {
        //默認SpringBoot去找一個頁面 error/狀態碼
		String errorViewName = "error/" + viewName;
        //如果模板引擎可以解析這個頁面地址就用模板引擎解析
		TemplateAvailabilityProvider provider = this.templateAvailabilityProviders.getProvider(errorViewName,
				this.applicationContext);
		if (provider != null) {
            //模板引擎可用的情況下返回到errorViewName指定的視圖地址
			return new ModelAndView(errorViewName, model);
		}
        ////模板引擎不可用,就在靜態資源文件夾下找errorViewName對應的頁面 error/狀態碼.html
		return resolveResource(errorViewName, model);
	}
1.使用模板引擎

​ 有模板引擎的情況下,將錯誤頁面放在templates(thymeleaf的默認解析文件夾)的error文件夾下並命名爲 狀態碼.html,也可以使用4xx和5xx作爲錯誤頁面的文件名來匹配這種類型的錯誤

DefaultErrorAttribute共享了一些錯誤信息,意味着我們可以在模板引擎中使用這些信息,

  • timestamp:時間戳
  • status:狀態碼
  • error:錯誤提示
  • exception:異常對象
  • message:異常消息
  • errors:JSR303數據校驗的錯誤都在這裏

這些錯誤信息在DefaultErrorAttributes都能找到

]

2.不使用模板引擎

如果不使用模板引擎,那麼DefaultErrorAttributes中的信息無法使用。方法還是一樣的,在靜態資源文件夾下找error/狀態碼.html

3.都不用

如果都不用,也就是返回的ModelAndView爲空

	@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
	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());
		ModelAndView modelAndView = resolveErrorView(request, response, status, model);
		return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
	}

那麼,默認去找"error"這個視圖對象,在ErrorMvcAutoConfiguration這個類中加入了這個組件

private final StaticView defaultErrorView = new StaticView();

@Bean(name = "error")
@ConditionalOnMissingBean(name = "error")
public View defaultErrorView() {
    return this.defaultErrorView;
}
private static class StaticView implements View {

		private static final MediaType TEXT_HTML_UTF8 = new MediaType("text", "html", StandardCharsets.UTF_8);

		private static final Log logger = LogFactory.getLog(StaticView.class);

		@Override
		public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response)
				throws Exception {
			if (response.isCommitted()) {
				String message = getMessage(model);
				logger.error(message);
				return;
			}
			response.setContentType(TEXT_HTML_UTF8.toString());
			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(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(htmlEscape(model.get("error")))
					.append(", status=").append(htmlEscape(model.get("status"))).append(").</div>");
			if (message != null) {
				builder.append("<div>").append(htmlEscape(message)).append("</div>");
			}
			if (trace != null) {
				builder.append("<div style='white-space:pre-wrap;'>").append(htmlEscape(trace)).append("</div>");
			}
			builder.append("</body></html>");
			response.getWriter().append(builder.toString());
		}

那麼錯誤頁面的默認顯示就完成了

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章