SpringBoot——Web開發三(錯誤處理機制)

1.SpringBoot默認的錯誤處理機制

1.1默認效果

1.瀏覽器,會返回一個默認的錯誤頁面

2.如果是其他客戶端,默認響應一個JSON數據

1.2 原理

可以參照ErrorMvcAutoConfiguration,錯誤處理的自動配置,這個給容器中添加了以下組件。

1.DefaultErrorAttributes

//幫我們共享信息 
public Map<String, Object> getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) {
        Map<String, Object> errorAttributes = this.getErrorAttributes(webRequest, options.isIncluded(Include.STACK_TRACE));
        if (this.includeException != null) {
            options = options.including(new Include[]{Include.EXCEPTION});
        }

        if (!options.isIncluded(Include.EXCEPTION)) {
            errorAttributes.remove("exception");
        }

        if (!options.isIncluded(Include.STACK_TRACE)) {
            errorAttributes.remove("trace");
        }

        if (!options.isIncluded(Include.MESSAGE) && errorAttributes.get("message") != null) {
            errorAttributes.put("message", "");
        }

        if (!options.isIncluded(Include.BINDING_ERRORS)) {
            errorAttributes.remove("errors");
        }

        return errorAttributes;
    }

    /** @deprecated */
    @Deprecated
    public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
        Map<String, Object> errorAttributes = new LinkedHashMap();
        errorAttributes.put("timestamp", new Date());
        this.addStatus(errorAttributes, webRequest);
        this.addErrorDetails(errorAttributes, webRequest, includeStackTrace);
        this.addPath(errorAttributes, webRequest);
        return errorAttributes;
    }

2.BasicErrorController:處理默認/error請求

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

在其方法中又通過兩種方式返回不同的數據(這就是爲啥通過瀏覽器請求返回html,通過其他客戶端返回json)

//產生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, getErrorAttributeOptions(request, MediaType.TEXT_HTML)));
		response.setStatus(status.value());
//去哪個頁面作爲錯誤頁面,包含頁面地址和頁面內容
		ModelAndView modelAndView = resolveErrorView(request, response, status, model);
		return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
	}

    //產生JSON數據,其他客戶端來到這個方法處理;
	@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, getErrorAttributeOptions(request, MediaType.ALL));
		return new ResponseEntity<>(body, status);
	}

查看瀏覽器的請求頭:

查看客戶端的請求頭:

可以看到這兩個對應BasicErrorController處理的兩種方式。

獲得所有的ErrorViewResolver,而這個ErrorViewResolver就是下面的DefaultErrorViewResolver組件,去哪個頁面就是由它來解析的的,通過DefaultErrorViewResolver組件得知

protected ModelAndView resolveErrorView(HttpServletRequest request, HttpServletResponse response, HttpStatus status,
			Map<String, Object> model) {
		for (ErrorViewResolver resolver : this.errorViewResolvers) {
			ModelAndView modelAndView = resolver.resolveErrorView(request, status, model);
			if (modelAndView != null) {
				return modelAndView;
			}
		}
		return null;
	}

3.ErrorPageCustomizer

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

//查看getpath方法,看到path
	@Value("${error.path:/error}")
	private String path = "/error";

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

private ModelAndView resolveResource(String viewName, Map<String, Object> model) {
		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;
	}

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

響應頁面:去哪個頁面是由DefaultErrorViewResolver解析得到的。

2.如何定製錯誤響應

2.1 如何定製錯誤頁面(瀏覽器)

1.在由模板引擎的情況下:error/狀態碼。將錯誤頁面命名爲 錯誤狀態碼.html放在模板引擎文件夾裏面的error文件夾下,發生此狀態碼的錯誤就會來到對應的頁面。

我們可以使用4xx和5xx作爲錯誤頁面的文件名來匹配這種類型的所有錯誤,精確優先(優先尋找精確的狀態碼.html)。如下所示:

頁面能獲取的信息(這個信息可以從上面DefaultErrorAttributes源碼中看到):

timestamp:時間戳

status:狀態碼

error:錯誤提示

exception:異常對象

message:異常消息

errors:JSR303數據校驗的錯誤都在這裏

可以看到html頁面取屬性:

	<main role="main" class="col-md-9 ml-sm-auto col-lg-10 pt-3 px-4">
					<h1>status:[[${status}]]</h1>
					<h1>timestamp:[[${timestamp}]]</h1>
					<h1>message:[[${message}]]</h1>
					<h1>exception:[[${exception}]]</h1>
					<!--配置文件配置-->
   </main>

2.沒有模板引擎(模板引擎找不到這個錯誤頁面),靜態資源文件夾下找。

3.以上都沒有錯誤頁面,就是默認來到SpringBoot的默認錯誤提示頁面。

2.2 如何定製錯誤的JSON數據

自己先編寫一個異常:


public class UserNotExistException extends RuntimeException{
    public UserNotExistException() {
        super("用戶不存在");
    }
}

1.自定義異常處理和返回JSON數據,這個沒有自適應的效果,也就是沒有上面說的時間戳等信息

@ControllerAdvice
public class MyExceptionHandler  {
    //第一種寫法,瀏覽器和客戶端返回都是JSON數據
      @ResponseBody
      @ExceptionHandler(UserNotExistException.class)
     public Map handleException(Exception e){
         Map<String,Object> map=new HashMap<>();
         map.put("code","user.notexist");
         map.put("message",e.getMessage());
         return map;
     }
}

這個原理的就是:我們在看BasicErrorController這個源碼的時候(用來處理/error請求),可以看到添加這個組件的時候,就可以看到這個上面有這個註解:

	@ConditionalOnMissingBean(value = ErrorController.class, search = SearchStrategy.CURRENT)

意思是沒有這個ErrorController類的話,這個組件才生效。現在我們自定義了MyExceptionHandler用來處理UserNotExistException異常。

2.轉發到/error請求

 @ExceptionHandler(UserNotExistException.class)
    public String handleException(Exception e, HttpServletRequest request){
        //傳入我們自己的錯誤狀態碼 4xx 5xx,否則就不會進入定製錯誤頁面的解析流程
       /**
         * Integer statusCode = (Integer) request
         .getAttribute("javax.servlet.error.status_code");
        */

        Map<String,Object> map=new HashMap<>();
        request.setAttribute("javax.servlet.error.status_code",400);
        map.put("code","user.notexist");
        map.put("message",e.getMessage());
        request.setAttribute("ext",map);
        //轉發到/error請求
        return "forward:/error";
    }

3.將定製的數據攜帶出去

出現錯誤以後,會來到/error請求,會被BasicErrorController處理,響應出去可以獲取的數據是由getErrorAttributes得到的(是AbstractErrorController(ErrorController)規定的方法)。

1.完全來編寫ErrorController的實現類(或者是編寫AbstractErrorController的子類),放在容器中。

2.頁面上用的數據,或者是JSON返回能用的數據都是通過errorAttributes.getErrorAttributes得到。容器中DefaultErrorAttributes.getErrorAttributes();默認進行數據處理的。

自定義ErrorAttributes:

@Component
//給容器中加入我們自定義ErrorAttributes
public class MyErrorAttributes extends DefaultErrorAttributes {
    //返回的map就是頁面和JSON獲取的所有字段
    //WebRequest繼承了RequestAttributes
    @Override
    public Map<String, Object> getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) {
      Map<String,Object> map=super.getErrorAttributes(webRequest, options);
      //我們異常處理器攜帶的數據
      Map<String,Object> ext= (Map<String, Object>) webRequest.getAttribute("ext",0);
      map.put("ext",ext);
      Throwable error=getError(webRequest);
      if(error!=null)
      {
          map.put("exception",error.getClass().getName());
      }
      return map;
    }
}

最終的效果:響應是自適應的,可以通過定製ErrorAttributes改變需要返回的內容:

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