1、Spring Boot默認的錯誤處理機制
1)瀏覽器訪問,返回一個默認的錯誤處理頁面
2)如果是其他客戶端訪問,返回JSON格式的數據
3)原理
查看ErrorMvcAutoConfiguration類,發現添加了如下4個組件:
- ErrorPageCustomizer
- BasicErrorController
- DefaultErrorAttributes
- DefaultErrorViewResolver
實現步驟:
【1】ErrorPageCustomizer
一旦系統出現4xx或者5xx之類的錯誤,ErrorPageCustomizer
就會生效(定製錯誤的響應規則),就會來到/error
請求
/**
* Path of the error controller.
* 作用:系統出現錯誤以後,來到/error請求進行處理
* 相當於在web.xml中註冊的錯誤頁面規則
*/
@Value("${error.path:/error}")
private String path = "/error";
【2】BasicErrorController
來到/error
請求後,就會被BasicErrorController
處理,她裏面有兩種處理機制
@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {
//1.產生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 modelAndView = resolveErrorView(request, response, status, model);
return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
}
//2.產生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, isIncludeStackTrace(request, MediaType.ALL));
return new ResponseEntity<>(body, status);
}
}
注意:同樣都是/error請求,怎麼區分是瀏覽器還是客戶端發請求呢?
主要是通過請求頭中的Accept來
區分:
【1】瀏覽器發請求:
【2】客戶端發請求:
【2.1】響應頁面處理實現代碼
/**
* Resolve any specific error views. By default this method delegates to
* {@link ErrorViewResolver ErrorViewResolvers}.
* @param request the request
* @param response the response
* @param status the HTTP status
* @param model the suggested model
* @return a specific {@link ModelAndView} or {@code null} if the default should be
* used
* @since 1.4.0
*/
protected ModelAndView resolveErrorView(HttpServletRequest request, HttpServletResponse response, HttpStatus status,
Map<String, Object> model) {
//拿到所有的異常處理解析器(ErrorViewResolver ),如果得到了就返回modelAndView,得不到就返回null
for (ErrorViewResolver resolver : this.errorViewResolvers) {
ModelAndView modelAndView = resolver.resolveErrorView(request, status, model);
if (modelAndView != null) {
return modelAndView;
}
}
return null;
}
響應頁面去哪個頁面是由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) {
//Spring Boot默認去找一個頁面(error/狀態碼)
String errorViewName = "error/" + viewName;
//模板引擎能夠解析頁面地址就用模板引擎解析
TemplateAvailabilityProvider provider = this.templateAvailabilityProviders.getProvider(errorViewName,
this.applicationContext);
if (provider != null) {//模板引擎可用的情況下返回errorViewName指定的ModelAndView
return new ModelAndView(errorViewName, model);
}
//模板引擎不可用的情況下,就到靜態資源文件夾下尋找errorViewName對應的頁面
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()) {//如果靜態資源文件夾下有error/狀態碼.html就進行解析返回ModelAndView
return new ModelAndView(new HtmlResourceView(resource), model);
}
}
catch (Exception ex) {
}
}
//如果沒有,返回null
return null;
}
2、如何定製錯誤響應
【1】如何定製錯誤頁面
- 有模板引擎的情況下:error/狀態碼。將錯誤頁面命名爲狀態碼.html放到模板引擎的error目錄下,發生該狀態碼的錯誤,就會來到相應的錯誤頁面。
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);
}
另外,我們可以使用4xx或者5xx作爲錯誤信息處理頁面的文件名來匹配所有以4或者5開頭的錯誤,但精確優先。
錯誤信息頁面能獲取到的信息:
【1】timestamp:時間戳
【2】status:狀態碼
【3】error:錯誤提示
【4】exception:異常對象
【5】message:異常消息
【6】errors:JSR303數據校驗的錯誤
- 模板引擎中找不到錯誤頁面,在靜態資源文件夾下找。
- 以上都沒有,默認來到Spring Boot的錯誤提示頁面。
【2】如何定製錯誤的JSON數據
【2.1】自定義異常處理,返回定製的JSON數據
@ControllerAdvice
public class MyExceptionHandler {
@ResponseBody
@ExceptionHandler(UserNotExistsException.class)
public Map<String,Object> exceptionHandler(Exception ex){
HashMap<String, Object> map = new HashMap<>();
map.put("status",500);
map.put("message",ex.getMessage());
map.put("cause",ex.getCause());
return map;
}
}
缺點: 不能自適應,瀏覽器與客戶端請求返回的都是JSON類型的數據。
【2.2】轉發到/error進行自適應效果
//@ResponseBody 【1】去掉@ResponseBody 註解
@ExceptionHandler(UserNotExistsException.class)
public String exceptionHandler(Exception ex, HttpServletRequest req){
//設置狀態碼
req.setAttribute("javax.servlet.error.status_code",500);
//【2】設置響應的JSON數據
HashMap<String, Object> map = new HashMap<>();
map.put("message",ex.getMessage());
map.put("cause",ex.getCause());
//【3】請求轉發到/error
return "forward:/error";
}
注意:一定要傳入自己的狀態碼,否則使用的是Spring Boot的默認錯誤頁面。
【2.3】既要能自適應效果還能將定製的數據攜帶出去
出現錯誤以後,會執行/error請求,被BasicErrorController處理,響應出去可以獲取的數據是由 getErrorAttributes得到的(是AbstractErrorController(ErrorController)
規定的方法)。
方法一:完全手動地編寫一個ErrorController
的實現類(或者編寫AbstractErrorController)的子類,然後將該類放入容器中。(麻煩)
方法二:在錯誤頁面顯示的數據或者是通過JSON顯示的數據都是通過errorAttributes.getErrorAttributes(webRequest, includeStackTrace);
得到的。容器中的DefaultErrorAttributes.getErrorAttributes();
默認進行數據處理。
自定義的ErrorAttributes:
@Component
public class MyErrorAttributes extends DefaultErrorAttributes {
//返回值map就是響應頁面和JSON能夠獲取到的所有數據
@Override
public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
Map<String, Object> map = super.getErrorAttributes(webRequest, includeStackTrace);
map.put("path","http://localhost:8080/crud/aa");
Map<String,Object> ext = (Map<String, Object>) webRequest.getAttribute("ext", 0);
map.put("ext",ext);
return map;
}
}
自定義的異常處理器:
@ControllerAdvice
public class MyExceptionHandler {
//@ResponseBody
@ExceptionHandler(UserNotExistsException.class)
public String exceptionHandler(Exception ex, HttpServletRequest req){
//設置狀態碼
req.setAttribute("javax.servlet.error.status_code",500);
//設置響應的JSON數據
HashMap<String, Object> map = new HashMap<>();
map.put("type","UserNotExistsException");
//將我們自己額外添加的數據也添加到錯誤頁面上
req.setAttribute("ext",map);
//請求轉發到/error
return "forward:/error";
}
}
webRequest.getAttribute((String var1, int var2)的使用:
int SCOPE_REQUEST = 0;
int SCOPE_SESSION = 1;
String REFERENCE_REQUEST = "request";
String REFERENCE_SESSION = "session";
@Nullable
Object getAttribute(String var1, int var2);
void setAttribute(String var1, Object var2, int var3);
效果:響應是自適應的,返回的內容可以通過ErrorAttributes定製。