七、Spring Boot 錯誤處理原理 & 定製錯誤頁面
【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:異常訊息
errors:JSR303資料校驗的錯誤都在這裡
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頁面程式碼如下:
瀏覽器測試效果如下:
客戶端測試效果如下:
相關文章
- Spring Boot返回靜態錯誤頁面Spring Boot
- Spring boot/Spring 統一錯誤處理方案的使用Spring Boot
- 錯誤處理
- 如何處理 Spring Boot 中與快取相關的錯誤?Spring Boot快取
- Flask教程第七章:錯誤處理Flask
- Go 錯誤處理Go
- Python錯誤處理Python
- PHP 錯誤處理PHP
- php錯誤處理PHP
- 錯誤處理:如何通過 error、deferred、panic 等處理錯誤?Error
- 【翻譯】在Spring WebFlux中處理錯誤SpringWebUX
- openGauss 處理錯誤表
- go的錯誤處理Go
- axios 的錯誤處理iOS
- 自定義OAM錯誤頁面
- grpc中的錯誤處理RPC
- laravel9 錯誤處理Laravel
- PHP 核心特性 - 錯誤處理PHP
- 15-錯誤處理(Error)Error
- Go語言之錯誤處理Go
- 學習Rust 錯誤處理Rust
- Oracle異常錯誤處理Oracle
- 淺談前端錯誤處理前端
- ORACLE 異常錯誤處理Oracle
- 關於laravel的錯誤頁面處理大家都是如何優雅的處理的呢?Laravel
- ogg複製程式報ORA-01438錯誤處理
- mysql多源複製跳過錯誤處理方法MySql
- 教你自定義Flutter錯誤頁面Flutter
- asp.net mvc 錯誤頁面ASP.NETMVC
- PHP安裝後錯誤處理PHP
- go 錯誤處理設計思考Go
- Golang通脈之錯誤處理Golang
- 常用模組 PHP 錯誤處理PHP
- Restful API 中的錯誤處理RESTAPI
- 請教 Element 的錯誤處理
- 異常錯誤資訊處理
- ORA-01591錯誤故障處理
- 程式錯誤型別及其處理型別