1. 程式人生 > >21 錯誤處理機制

21 錯誤處理機制

1 SpringBoot預設的錯誤處理機制

1.1 預設效果

1.1.1 瀏覽器,返回一個預設的錯誤頁面

預設錯誤頁面

1.1.2 非瀏覽器,預設響應一個json資料

非瀏覽器

1.2 原理

  • org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration
1.2.1 錯誤出現

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

// 錯誤定製頁面
@Bean
public ErrorMvcAutoConfiguration.ErrorPageCustomizer errorPageCustomizer() {
    return new ErrorMvcAutoConfiguration.ErrorPageCustomizer(this.serverProperties, this.dispatcherServletPath);
}
  • ErrorPageCustomizer
private static class ErrorPageCustomizer implements ErrorPageRegistrar
, Ordered { private final ServerProperties properties; private final DispatcherServletPath dispatcherServletPath; protected ErrorPageCustomizer(ServerProperties properties, DispatcherServletPath dispatcherServletPath) { this.properties = properties; this.dispatcherServletPath =
dispatcherServletPath; } // 註冊錯誤頁面的響應規則 public void registerErrorPages(ErrorPageRegistry errorPageRegistry) { /* 系統出現錯誤後,來到error請求進行處理(web.xml註冊的錯誤頁面規則) this.properties.getError().getPath() ----> /error */ ErrorPage errorPage = new ErrorPage(this.dispatcherServletPath.getRelativePath(this.properties.getError().getPath())); errorPageRegistry.addErrorPages(new ErrorPage[]{errorPage}); } public int getOrder() { return 0; } }
1.2.2 接收並處理 /error 請求
@Bean
@ConditionalOnMissingBean(
    value = {ErrorController.class},
    search = SearchStrategy.CURRENT
)
public BasicErrorController basicErrorController(ErrorAttributes errorAttributes) {
    return new BasicErrorController(errorAttributes, this.serverProperties.getError(), this.errorViewResolvers);
}
  • BasicErrorController
/*
	${server.error.path:${error.path:/error}}
		如果 server.error.path 未配置,則使用 ${error.path:/error}

		如果 error.path 未配置,則使用 "/error"
*/
@Controller
@RequestMapping({"${server.error.path:${error.path:/error}}"})
public class BasicErrorController extends AbstractErrorController {
	// 產生html型別的資料,瀏覽器傳送的請求來到這個方法處理
	@RequestMapping(
        produces = {"text/html"}
    )
    public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
    	// 錯誤狀態碼
        HttpStatus status = this.getStatus(request);

        // 請求資訊
        Map<String, Object> model = Collections.unmodifiableMap(this.getErrorAttributes(request, this.isIncludeStackTrace(request, MediaType.TEXT_HTML)));
        response.setStatus(status.value());

        // 去哪個頁面作為錯誤頁面;包含頁面地址和頁面內容
        ModelAndView modelAndView = this.resolveErrorView(request, response, status, model);
        return modelAndView != null ? modelAndView : new ModelAndView("error", model);
    }


    // 產生json資料,非瀏覽器來到這個方法處理
    @RequestMapping
    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 Map<String, Object> getErrorAttributes(HttpServletRequest request, boolean includeStackTrace) {
    WebRequest webRequest = new ServletWebRequest(request);
 
    /*
    	getErrorAttributes --> ErrorAttributes(介面) --> DefaultErrorAttributes(實現)

    	DefaultErrorAttributes: 
    */
    return this.errorAttributes.getErrorAttributes(webRequest, includeStackTrace);
}
  • DefaultErrorAttributes --> getErrorAttributes
// 頁面資訊
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;
}
  • 檢視解析
protected ModelAndView resolveErrorView(HttpServletRequest request, HttpServletResponse response, HttpStatus status, Map<String, Object> model) {
	// 獲取所有的檢視解析器
    Iterator var5 = this.errorViewResolvers.iterator();

    ModelAndView modelAndView;
    do {
        if (!var5.hasNext()) {
            return null;
        }

        ErrorViewResolver resolver = (ErrorViewResolver)var5.next();

        // 返回相應錯誤的檢視
        modelAndView = resolver.resolveErrorView(request, status, model);
    } while(modelAndView == null);

    return modelAndView;
}
1.2.3 解析檢視
  • org.springframework.boot.autoconfigure.web.servlet.error.DefaultErrorViewResolver
private ModelAndView resolve(String viewName, Map<String, Object> model) {
	// 遇到錯誤時,SpringBoot根據errorViewName查詢頁面 eg:error/404.html
    String errorViewName = "error/" + viewName;

    // 模板引擎對頁面進行解析
    TemplateAvailabilityProvider provider = this.templateAvailabilityProviders.getProvider(errorViewName, this.applicationContext);

    //解析成功,返回到errorViewName指定的檢視地址
    //解析失敗,前往靜態資原始檔夾下找errorViewName對應的頁面   error/404.html
    return provider != null ? new ModelAndView(errorViewName, model) : this.resolveResource(errorViewName, model);
}

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

2 自定義錯誤響應

2.1 自定義錯誤頁面

有模板引擎的情況下,將錯誤頁面命名為  錯誤狀態碼.html(404.html) 放在模板引擎資料夾裡面的 error資料夾下,發生此狀態碼的錯誤就會來到對應的頁面

自定義錯誤頁面

2.1.1 使用4xx和5xx作為錯誤頁面的檔名來匹配這種型別的所有錯誤,精確優先(優先尋找精確的狀態碼.html);
  • 頁面能獲取的資訊
​	timestamp:時間戳
	​status:狀態碼
​	error:錯誤提示
​	exception:異常物件
	message:異常訊息 
​	errors:JSR303資料校驗的錯誤都在這裡
2.1.2 沒有模板引擎(模板引擎找不到這個錯誤頁面),靜態資原始檔夾下找
2.1.3 以上都沒有錯誤頁面,就是預設來到SpringBoot預設的錯誤提示頁面

2.2 自定義json響應

2.2.1 自定義異常處理&返回定製json資料;
  • 自定義異常
@package com.gp6.springboot18.exception;

public class UserNotExistException extends RuntimeException {

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

  • 測試自定義異常
package com.gp6.springboot18.controller;


import com.gp6.springboot18.exception.UserNotExistException;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class HelloController {

    /*
       訪問
           當前專案(localhost:8080)
           或
           localhost:8080/index.htm
       都會被模板引擎自動解析,查詢templates/index.html

    */
    @RequestMapping({"/","/index.html"})
    public String index(){
        return "login";
    }

    /*
        測試自定義異常
    */
    @RequestMapping({"testException"})
    @ResponseBody
    public String testException(@RequestParam("userName") String userName){
       if("gp6".equals(userName)){
            throw  new UserNotExistException();
       }
       return "測試自定義異常";
    }
}


自定義異常

2.2.2 自定義異常處理器(不能自適應)
package com.gp6.springboot18.config;

import com.gp6.springboot18.exception.UserNotExistException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;

@ControllerAdvice
public class MyExceptionHandler {

    // 瀏覽器客戶端返回的都是json
    @ResponseBody

    // 捕獲UserNotExistException異常
    @ExceptionHandler(UserNotExistException.class)
    public Map<String,Object> handleException(Exception e){
        Map<String,Object> map = new HashMap<>();
        map.put("code","user.notExist");
        map.put("message",e.getMessage());
        return map;
    }
}

  • 瀏覽器和非瀏覽器都返回json資料,不能自動跳轉錯誤頁面
2.2.3 轉發到/error進行自適應響應效果處理
package com.gp6.springboot18.config;

import com.gp6.springboot18.exception.UserNotExistException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;

@ControllerAdvice
public class MyExceptionHandler {

    @ExceptionHandler(UserNotExistException.class)
    public String handleException2(Exception e, HttpServletRequest request){
        Map<String,Object> map = new HashMap<>();
        map.put("code","user.notExist");
        map.put("message","該使用者不存在啊");

        // 如果不設定狀態碼,不能跳轉自定義錯誤頁面
        request.setAttribute("javax.servlet.error.status_code",500);

        // 轉發到/error
        return "forward:/error";
    }
}

  • 測試
    測試結果
2.2.4 將自定義資料返回
  • 響應資料返回原理
	出現錯誤後,會來到/error請求,被BasicErrorController處理,響應出去可以獲取的資料是由getErrorAttributes得到的(是AbstractErrorController(ErrorController)規定的方法)
  • 相應資料返回方法
	1、編寫一個ErrorController的實現類【或者是編寫AbstractErrorController的子類】,放在容器中
​	2、頁面上能用的資料,或者是json返回能用的資料都是通過errorAttributes.getErrorAttributes()得到
​			容器中DefaultErrorAttributes.getErrorAttributes();預設進行資料處理的
  • 自定義ErrorAttributes
package com.gp6.springboot18.config;

import org.springframework.boot.web.servlet.error.DefaultErrorAttributes;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.WebRequest;

import java.util.Map;

//給容器中加入我們自己定義的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("company","gp6");

        //我們的異常處理器攜帶的資料
        Map<String,Object> ext = (Map<String, Object>) webRequest.getAttribute("ext", 0);
        map.put("ext",ext);
        return map;
    }
}

  • 測試
    自定義json返回資料