1. 程式人生 > >SpringBoot——web開發之錯誤處理機制

SpringBoot——web開發之錯誤處理機制

一、SpringBoot提供的預設錯誤處理

1、在瀏覽器端訪問時,出現錯誤時響應一個錯誤頁面:

2、在其他客戶端訪問時,響應json資料:

3、錯誤處理機制的原理,參照錯誤自動配置類——ErrorMvcAutoConfiguration,在錯誤自動配置類中,配置了以下元件:

①ErrorPageCustomizer:定製錯誤的響應規則

@Value("${error.path:/error}")
private String path = "/error";

從主配置檔案中獲取error.path的值,如果沒有則預設"/error",即一旦系統出現4xx或5xx之類的錯誤時就會發送/error請求,或者我們自定義的error.path的值的請求,該請求會有BasicErrorController類處理

②BasicErrorController:處理預設的/error請求,但是該類提供了兩種/error請求的處理方式,一種產生"text/html"響應(瀏覽器端請求),一種產生JSON格式資料

@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {
	...
	@RequestMapping(produces = "text/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);
	}

	@RequestMapping
	@ResponseBody//處理其他客戶端傳送的請求
	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<>(body, status);
	}
	...
}

那SpringBoot怎麼區分請求是來自瀏覽器還是其他客戶端呢?是根據請求頭來判斷的,來自瀏覽器的請求的請求頭Accept攜帶有瀏覽器請求的資訊:

其他客戶端中Accept請求頭的資訊:沒有要求優先接收html資料

這兩個方法會根據錯誤的狀態碼判斷去哪個錯誤頁面或者響應什麼JSON資料,響應頁面的解析:遍歷所有的錯誤檢視解析器(ErrorViewResolver),如果得到異常檢視則返回,否則返回null,這裡的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;
}

③DefaultErrorViewResolver:4xx為客戶端錯誤,5xx為服務端錯誤

public class DefaultErrorViewResolver implements ErrorViewResolver, Ordered {
	...
	static {
		Map<Series, String> views = new EnumMap<>(Series.class);
		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) {
		String errorViewName = "error/" + viewName;
		TemplateAvailabilityProvider provider = this.templateAvailabilityProviders
				.getProvider(errorViewName, this.applicationContext);
		if (provider != null) {
			return new ModelAndView(errorViewName, model);
		}
		return resolveResource(errorViewName, model);
	}
	...
}

從這段程式碼中可以看出:在解析錯誤檢視的時候會呼叫resolve方法,將狀態碼拼接到error/後面作為檢視名稱返回,也就是說SpringBoot預設會在error資料夾下找狀態碼對應的錯誤頁面,同時如果模板引擎可用則返回到模板引擎指定的資料夾下的errorViewName檢視,否則會執行下面這段程式碼:從所有的靜態資原始檔夾下找對應的檢視(.html),找到則返回,找不到返回null

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

④DefaultErrorAttributes:幫我們在頁面共享資訊,檢視中會有哪些資料是在該類中封裝的

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

出錯時,SpringBoot為我們放入的資料有:這些資料我們都可以在頁面中獲取到

timestamp(時間戳)、status(狀態碼)、error(錯誤資訊)

<h1>status:[[${status}]]</h1>
<h1>timestamp:[[${timestamp}]]</h1>

整體步驟:系統出現4xx或者5xx之類的錯誤時,ErrorPageCustomizer就會生效從而定製錯誤的響應規則,就會來到/error
請求,該請求會被BasicErrorController處理,處理的結果由DefaultErrorViewResolver返回

二、如何定製錯誤處理

1、如何定製錯誤的頁面

①有模板引擎時:在模板引擎靜態資原始檔夾(templates)下新建一個error資料夾,並提供與錯誤碼對應的錯誤頁面即可(error/狀態碼.html)

也可以提供一個4xx.html來響應以4開頭的狀態碼頁面,此時若有精確匹配的則優先使用精確匹配的:

②沒有模板引擎:可以放在任何一個靜態資原始檔夾下,因為檢視解析時會一個個的解析,解析到則返回,解析不到則返回null,注意必須是靜態資原始檔夾下的error/狀態碼.html,若所有的靜態資原始檔夾下都沒有error/狀態碼.html則會響應SpringBoot為我們提供的預設錯誤頁面,也即是下面的程式碼

@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
	public BeanNameViewResolver beanNameViewResolver() {
		BeanNameViewResolver resolver = new BeanNameViewResolver();
		resolver.setOrder(Ordered.LOWEST_PRECEDENCE - 10);
		return resolver;
	}

}

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

①編寫一個自定義異常

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

②編寫一個異常處理器

a、異常處理方法使用@ResponseBody

@ControllerAdvice
public class MyExceptionHandler {

    @ResponseBody
    @ExceptionHandler(UserNotExistException.class)//處理的異常
    public Map handlerException(Exception e){
        Map<String,Object> map = new HashMap<>();
        map.put("code","user.notExist");
        map.put("message",e.getMessage());
        //map.put("exception",e);
        return map;
    }
}

異常的處理方法中通過map放置異常資訊,這個map中的key和value就是我們能夠在出現異常時定製的資訊

結果:通過postMan請求和通過瀏覽器請求都會是如下結果,並沒有自適應效果(瀏覽器訪問應該響應頁面),這是因為在異常攔截器中使用了@ResponseBody

{
    "code":"user.notExist",
    "message":"使用者不存在!"
}

b、不使用@ResponseBody,而將請求轉發至/error,達到自適應效果

@ControllerAdvice
public class MyExceptionHandler {

    @ExceptionHandler(UserNotExistException.class)//處理的異常
    public String handlerException(Exception e){
        Map<String,Object> map = new HashMap<>();
        map.put("code","user.notExist");
        map.put("message",e.getMessage());
        //map.put("exception",e);
        return "forward:/error";
    }
}

SpringBoot提供了處理/error請求的Controller,自適應效果會在該Controller中實現,但此時響應錯誤頁面並不是我們自定義的錯誤頁面,而是SpringBoot提供的錯誤頁面,且狀態碼為200:

這是因為我們在異常處理方法中沒有設定狀態碼,修改一下處理方法:

@ControllerAdvice
public class MyExceptionHandler {

    @ExceptionHandler(UserNotExistException.class)//處理的異常
    public String handlerException(Exception e, HttpServletRequest request){
        request.setAttribute("javax.servlet.error.status_code",400);
        Map<String,Object> map = new HashMap<>();
        map.put("code","user.notExist");
        map.put("message",e.getMessage());
        return "forward:/error";
    }
}

錯誤狀態碼的key值是BasicErrorController在解析錯誤時需要從請求中獲取的:

@RequestMapping
@ResponseBody
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 HttpStatus getStatus(HttpServletRequest request) {
	Integer statusCode = (Integer) request
			.getAttribute("javax.servlet.error.status_code");
	if (statusCode == null) {
		return HttpStatus.INTERNAL_SERVER_ERROR;
	}
	try {
		return HttpStatus.valueOf(statusCode);
	}
	catch (Exception ex) {
		return HttpStatus.INTERNAL_SERVER_ERROR;
	}
}

此時再在瀏覽器中訪問時:

但是在postMan中請求時會發現並沒有將我們自定義的定製資訊(code、message等)帶回到響應中來,

③如果我們想使用自適應同時又想定製錯誤資訊,有一下兩種方式:出現錯誤以後,會來到/error請求,會被BasicErrorController處理,響應出去可以獲取的資料是由getErrorAttributes得到的(是AbstractErrorController(ErrorController)規定的方法):

a、在容器中新增ErrorController的實現類或者繼承ErrorController的子類AbstractErrorController

b、頁面上能獲取到的資料,或者是json返回的資料都是通過errorAttributes.getErrorAttributes()得到的,SpringBoot容器中通過DefaultErrorAttributes.getErrorAttributes()進行資料處理,因此我們可以自定義ErrorAttributes,重寫該類的getErrorAttributes方法,在從父類獲取的map中塞入定製的資訊即可:

@Component//給容器中加入自定義的錯誤屬性類ErrorAttributes
public class MyErrorAttributes extends DefaultErrorAttributes {

    @Override
    public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
        Map<String, Object> map = super.getErrorAttributes(webRequest,includeStackTrace);
        map.put("company","bdm");
        return map;
    }
}

但此時已然只能放置一些公共的定製資訊,不能放入一些具體的資訊,比如在出現某個異常時的提示資訊,此時需要我們改動一下異常處理器和上面的ErrorAttributes,將異常處理器中的map放入請求物件中攜帶到ErrorAttributes中:

@ControllerAdvice
public class MyExceptionHandler {

    @ExceptionHandler(UserNotExistException.class)//處理的異常
    public String handlerException(Exception e, HttpServletRequest request){
        request.setAttribute("javax.servlet.error.status_code",400);
        Map<String,Object> map = new HashMap<>();
        map.put("code","user.notExist");
        map.put("message",e.getMessage());
        request.setAttribute("ext",map);
        return "forward:/error";
    }
}
@Component//給容器中加入自定義的錯誤屬性類ErrorAttributes
public class MyErrorAttributes extends DefaultErrorAttributes {


    @Override
    public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
        Map<String, Object> map = super.getErrorAttributes(webRequest,includeStackTrace);
        map.put("company","bdm");
        Map<String,Object> ext = (Map<String,Object>)webRequest.getAttribute("ext", 0);
        map.put("ext",ext);
        return map;
    }
}

這樣我們就可以在頁面和響應的json中獲取到定製的錯誤資訊了: