七、Spring Boot 錯誤處理原理 & 定製錯誤頁面

丶Dream_RM發表於2018-09-30

【1】錯誤預設處理機制

1)瀏覽器,返回一個預設的錯誤頁面
在這裡插入圖片描述
請求頭:
在這裡插入圖片描述

2) 其他客戶端訪問,預設響應 JSON 資料
在這裡插入圖片描述
請求頭:
在這裡插入圖片描述

為什麼會產生這樣的預設效果?

原理:可以參照 ErrorMvcAutoConfiguration;錯誤處理的自動配置;

ErrorMvcAutoConfiguration 給容器中新增了以下元件

1)、DefaultErrorAttributes

@Bean
//@ConditionalOnMissingBean:  容器中沒有  ErrorAttributes 元件 時,會新增 DefaultErrorAttributes 元件
@ConditionalOnMissingBean(value = ErrorAttributes.class, search = SearchStrategy.CURRENT)
public DefaultErrorAttributes errorAttributes() {
	return new DefaultErrorAttributes();
}

ErrorMvcAutoConfiguration 原始碼:

@Order(Ordered.HIGHEST_PRECEDENCE)
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);
	}

}

2)、BasicErrorController: 處理預設/error請求

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

	private final ErrorProperties errorProperties;

	/**
	 * Create a new {@link BasicErrorController} instance.
	 * @param errorAttributes the error attributes
	 * @param errorProperties configuration properties
	 */
	public BasicErrorController(ErrorAttributes errorAttributes,
			ErrorProperties errorProperties) {
		this(errorAttributes, errorProperties,
				Collections.<ErrorViewResolver>emptyList());
	}

	/**
	 * Create a new {@link BasicErrorController} instance.
	 * @param errorAttributes the error attributes
	 * @param errorProperties configuration properties
	 * @param errorViewResolvers error view resolvers
	 */
	public BasicErrorController(ErrorAttributes errorAttributes,
			ErrorProperties errorProperties, List<ErrorViewResolver> errorViewResolvers) {
		super(errorAttributes, errorViewResolvers);
		Assert.notNull(errorProperties, "ErrorProperties must not be null");
		this.errorProperties = errorProperties;
	}

	@Override
	public String getErrorPath() {
		return this.errorProperties.getPath();
	}

	@RequestMapping(produces = "text/html")//產生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());
		//去哪個頁面作為錯誤頁面; 包含頁面地址和頁面內容
		ModelAndView modelAndView = resolveErrorView(request, response, status, model); //①
		return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);


		//此處為 ① resolveErrorView(request, response, status, model); 裡執行的程式碼
		protected ModelAndView resolveErrorView(HttpServletRequest request, HttpServletResponse response, HttpStatus status, Map<String, Object> model) {
			//拿到所有的 異常檢視解析器 得到 ModelAndView
			for (ErrorViewResolver resolver : this.errorViewResolvers) {
				ModelAndView modelAndView = resolver.resolveErrorView(request, status, model);
				if (modelAndView != null) {
					return modelAndView;
				}
			}
			return null;
		}
	}

	@RequestMapping
	@ResponseBody  //產生json資料,其他客戶端來到這個方法處理;
	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);
	}

	/**
	 * Determine if the stacktrace attribute should be included.
	 * @param request the source request
	 * @param produces the media type produced (or {@code MediaType.ALL})
	 * @return if the stacktrace attribute should be included
	 */
	protected boolean isIncludeStackTrace(HttpServletRequest request,
			MediaType produces) {
		IncludeStacktrace include = getErrorProperties().getIncludeStacktrace();
		if (include == IncludeStacktrace.ALWAYS) {
			return true;
		}
		if (include == IncludeStacktrace.ON_TRACE_PARAM) {
			return getTraceParameter(request);
		}
		return false;
	}

	/**
	 * Provide access to the error properties.
	 * @return the error properties
	 */
	protected ErrorProperties getErrorProperties() {
		return this.errorProperties;
	}

}

3)、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) {
		//發生錯誤以後, 來到 error 請求進行處理 --> this.properties.getError().getPath() == "/error"
		ErrorPage errorPage = new ErrorPage(this.properties.getServletPrefix() + this.properties.getError().getPath());
		errorPageRegistry.addErrorPages(errorPage);
	}

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

}

在這裡插入圖片描述

4)、DefaultErrorViewResolver

public class DefaultErrorViewResolver implements ErrorViewResolver, Ordered {

	private static final Map<Series, String> SERIES_VIEWS;

	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) {
		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) {
		//Spring Boot  可以找到 error/404.html
		String errorViewName = "error/" + viewName;
		//如果模板引擎可以解析地址就用模板引擎解析
		TemplateAvailabilityProvider provider = this.templateAvailabilityProviders.getProvider(errorViewName, this.applicationContext);
		if (provider != null) {
			//模板引擎可用的情況下返回到 errorViewName 指定的檢視
			return new ModelAndView(errorViewName, model);
		}
		//模板引擎不可用,在靜態資原始檔夾下找 errorViewName 對應的頁面
		return resolveResource(errorViewName, model);
	}

步驟:
  一但系統出現4xx或者5xx之類的錯誤;ErrorPageCustomizer就會生效(定製錯誤的響應規則);就會來到/error請求;就會被BasicErrorController處理;

【2】定製錯誤響應

1.如何定製錯誤頁面
  1)、有模板引擎的情況下,  error/404.html 【將錯誤頁面命名為 錯誤狀態碼.html 放在模板引擎資料夾裡面的error資料夾下】,發生此狀態碼的錯誤就會來到 對應的頁面;
  路徑圖:
  在這裡插入圖片描述
  效果圖
   在這裡插入圖片描述

我們可以使用4xx和5xx作為錯誤頁面的檔名來匹配這種型別的所有錯誤,精確優先(優先尋找精確的狀態碼.html);
頁面能獲取的資訊;
timestamp時間戳
status狀態碼
error錯誤提示
exception異常物件
message異常訊息
errorsJSR303資料校驗的錯誤都在這裡
在這裡插入圖片描述
在這裡插入圖片描述
  2)、沒有模板引擎(模板引擎找不到這個錯誤頁面),靜態資原始檔夾下找;
  在這裡插入圖片描述
在這裡插入圖片描述
  3)、以上都沒有錯誤頁面,就是預設來到SpringBoot預設的錯誤提示頁面;
Spring Boot 預設提示頁面原始碼:

@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;
    }
}

2.如何定製錯誤的 JSON 資料

第一種,使用SpringMVC的異常處理器

@ControllerAdvice
public class MyExceptionHandler {
    @ResponseBody
    @ExceptionHandler(UserNotExistException.class)
    public Map<String,Object> handlerException(Exception e, HttpServletRequest request) {
        Map<String,Object> map = new HashMap<>();
        map.put("code","user.notexist");
        map.put("message","使用者出錯啦");
        return map;
    }
 }

這樣無論瀏覽器還是客戶端返回的都是JSON!
在這裡插入圖片描述

第二種,轉發到/error請求進行自適應效果處理

  @ExceptionHandler(UserNotExistException.class)
  public String handleException(Exception e, HttpServletRequest request){
       Map<String,Object> map = new HashMap<>();
       //傳入我們自己的錯誤狀態碼  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","使用者出錯啦");
       //轉發到/error
       return "forward:/error";
   }

在這裡插入圖片描述
但是此時沒有將自定義 code message傳過去!

第三種,註冊MyErrorAttributes繼承自DefaultErrorAttributes(推薦)

BasicErrorController 中對 /error 請求有兩種方式,兩種方式的錯誤資料都是通過DefaultErrorAttributes.getErrorAttributes()方法獲取,如下所示:

//BasicErrorController 類部分程式碼
@RequestMapping(produces = "text/html")
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
Map<String, Object> model = Collections.unmodifiableMap(getErrorAttributes(
				request, isIncludeStackTrace(request, MediaType.TEXT_HTML)));
}
@RequestMapping
@ResponseBody
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
Map<String, Object> body = getErrorAttributes(request,
				isIncludeStackTrace(request, MediaType.ALL));
}

//DefaultErrorAttributes 類部分程式碼
@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;
 }

我們可以編寫一個MyErrorAttributes繼承自DefaultErrorAttributes重寫其getErrorAttributes方法將我們的錯誤資料新增進去。

示例如下:

//給容器中加入我們自己定義的ErrorAttributes
@Component
public class MyErrorAttributes extends DefaultErrorAttributes {

    //返回值的map就是頁面和json能獲取的所有欄位
    @Override
    public Map<String, Object> getErrorAttributes(RequestAttributes requestAttributes, boolean includeStackTrace) {
        //DefaultErrorAttributes的錯誤資料
        Map<String, Object> map = super.getErrorAttributes(requestAttributes, includeStackTrace);
        map.put("company","SpringBoot");
        //① 我們的異常處理器攜帶的資料
        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<>();
     //傳入我們自己的錯誤狀態碼  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","使用者出錯啦");
    //將自定義錯誤資料放入request中
     request.setAttribute("ext",map);
     //轉發到/error
     return "forward:/error";
 }

5xx.html頁面程式碼如下:
在這裡插入圖片描述

瀏覽器測試效果如下:
在這裡插入圖片描述

客戶端測試效果如下:
在這裡插入圖片描述

相關文章