一.SpringBoot全局異常處理
1.1 全局異常處理的好處
- 后端開發(fā)人員職責(zé)單一蝴猪,只需要將異常捕獲并轉(zhuǎn)換為自定義異常一直對外拋出。不需要去想頁面跳轉(zhuǎn)404膊爪,以及異常響應(yīng)的數(shù)據(jù)結(jié)構(gòu)的設(shè)計自阱。
- 面向前端人員友好,后端返回給前端的數(shù)據(jù)應(yīng)該有統(tǒng)一的數(shù)據(jù)結(jié)構(gòu)米酬,統(tǒng)一的規(guī)范沛豌。而在此過程中不需要后端開發(fā)人員做更多的工作,交給全局異常處理器去處理“異吃叨睿”到“響應(yīng)數(shù)據(jù)結(jié)構(gòu)”的轉(zhuǎn)換加派。
- 面向用戶友好叫确,用戶能夠清楚的知道異常產(chǎn)生的原因。這就要求自定義異常芍锦,全局統(tǒng)一處理竹勉,接口請求響應(yīng)統(tǒng)一的異常數(shù)據(jù)結(jié)構(gòu),頁面模板請求統(tǒng)一跳轉(zhuǎn)到404頁面娄琉。
- 面向運維友好次乓,將異常信息合理規(guī)范的持久化,以日志的形式存儲起來孽水,以便查詢票腰。
1.2開發(fā)規(guī)范
- Controller、Service匈棘、Repository層攔截異常轉(zhuǎn)換為自定義異常丧慈,不允許將異常私自截留。必須對外拋出主卫。
- 統(tǒng)一數(shù)據(jù)響應(yīng)代碼逃默,默認使用http狀態(tài)碼(200、400簇搅、500等少數(shù)幾個)完域,特殊場景可自定義。
- 自定義異常里面有message屬性瘩将,用對用戶友好的語言描述異常的發(fā)生情況吟税,并賦值給message。
- 不允許對父類Exception統(tǒng)一catch姿现,要分小類catch肠仪,這樣能夠清楚地將異常轉(zhuǎn)換為自定義異常傳遞給前端。
二.自定義異常和相關(guān)數(shù)據(jù)結(jié)構(gòu)
2.1 自定義異常
1.枚舉異常的類型
public enum CustomExceptionType {
OK(HttpStatus.OK.value(),null),
BAD_REQUEST(HttpStatus.BAD_REQUEST.value(),"您輸入的數(shù)據(jù)錯誤或您沒有權(quán)限訪問資源备典!"),
INTERNAL_SERVER_ERROR (HttpStatus.INTERNAL_SERVER_ERROR.value(),"系統(tǒng)出現(xiàn)異常异旧,請您稍后再試或聯(lián)系管理員!"),
OTHER_ERROR(999,"系統(tǒng)出現(xiàn)未知異常提佣,請聯(lián)系管理員吮蛹!");
CustomExceptionType(int code, String desc) {
this.code = code;
this.desc = desc;
}
private String desc;//異常類型中文描述
private int code; //code
public String getDesc() {
return desc;
}
public int getCode() {
return code;
}
}
2.自定義異常
public class CustomException extends RuntimeException {
//異常錯誤編碼
private int code ;
//異常信息
private String message;
private CustomException(){}
public CustomException(CustomExceptionType exceptionTypeEnum) {
this.code = exceptionTypeEnum.getCode();
this.message = exceptionTypeEnum.getDesc();
}
public CustomException(CustomExceptionType exceptionTypeEnum, String message) {
this.code = exceptionTypeEnum.getCode();
this.message = message;
}
public int getCode() {
return code;
}
@Override
public String getMessage() {
return message;
}
}
- 自定義異常有兩個核心內(nèi)容,一個是code拌屏。使用CustomExceptionType 來限定范圍潮针。
- 另外一個是message,這個message信息是要最后返回給前端的倚喂,所以需要用友好的提示來表達異常發(fā)生的原因或內(nèi)容
2.2 請求接口統(tǒng)一響應(yīng)數(shù)據(jù)結(jié)構(gòu)
對于1.2節(jié)中的配置文件每篷,可以通過@ConfigurationProperties注解獲取配置信息并對的Java類屬性進行賦值。
@Data
public class ApiResponse {
private boolean isok; //請求是否處理成功
private int code; //請求響應(yīng)狀態(tài)碼(200、400雳攘、500)
private String message; //請求結(jié)果描述信息
private Object data; //請求結(jié)果數(shù)據(jù)(通常用于查詢操作)
private ApiResponse(){}
public static ApiResponse error(CustomException e) {
ApiResponse resultBean = new ApiResponse();
resultBean.setIsok(false);
resultBean.setCode(e.getCode());
resultBean.setMessage(e.getMessage());
return resultBean;
}
public static ApiResponse error(CustomExceptionType customExceptionType,
String errorMessage) {
ApiResponse resultBean = new ApiResponse();
resultBean.setIsok(false);
resultBean.setCode(customExceptionType.getCode());
resultBean.setMessage(errorMessage);
return resultBean;
}
public static ApiResponse success(){
ApiResponse apiResponse = new ApiResponse();
apiResponse.setIsok(true);
apiResponse.setCode(200);
apiResponse.setMessage("請求響應(yīng)成功!");
return apiResponse;
}
public static ApiResponse success(Object obj){
ApiResponse apiResponse = new ApiResponse();
apiResponse.setIsok(true);
apiResponse.setCode(200);
apiResponse.setMessage("請求響應(yīng)成功!");
apiResponse.setData(obj);
return apiResponse;
}
public static ApiResponse success(Object obj,String message){
ApiResponse apiResponse = new ApiResponse();
apiResponse.setIsok(true);
apiResponse.setCode(200);
apiResponse.setMessage(message);
apiResponse.setData(obj);
return apiResponse;
}
}
為了解決不同的開發(fā)人員使用不同的結(jié)構(gòu)來響應(yīng)給前端带兜,導(dǎo)致規(guī)范不統(tǒng)一枫笛,開發(fā)混亂的問題吨灭。我們使用如下代碼定義統(tǒng)一數(shù)據(jù)響應(yīng)結(jié)構(gòu)
- isok表示該請求是否處理成功(即是否發(fā)生異常)。true表示請求處理成功刑巧,false表示處理失敗喧兄。
- code對響應(yīng)結(jié)果進一步細化,200表示請求成功啊楚,400表示用戶操作導(dǎo)致的異常吠冤,500表示系統(tǒng)異常,999表示其他異常恭理。與CustomExceptionType枚舉一致拯辙。
- message:友好的提示信息,或者請求結(jié)果提示信息颜价。如果請求成功這個信息通常沒什么用涯保,如果請求失敗,該信息需要展示給用戶周伦。
- data:通常用于查詢數(shù)據(jù)請求夕春,成功之后將查詢數(shù)據(jù)響應(yīng)給前端。
三.通用全局異常處理邏輯
程序員的異常處理邏輯要十分的單一:無論在Controller層专挪、Service層還是什么其他位置及志,程序員只負責(zé)一件事:那就是捕獲異常,并將異常轉(zhuǎn)換為自定義異常寨腔。使用用戶友好的信息去填充CustomException的message,并將CustomException拋出去速侈。###3.1 對綁定的屬性值進行校驗
3.1 全局異常處理器
通過團隊內(nèi)的編碼規(guī)范的要求,我們已經(jīng)知道了:不允許程序員截留處理Exception迫卢,必須把異常轉(zhuǎn)換為自定義異常CustomException全都拋出去倚搬。那么程序員把異常跑出去之后由誰來處理?那就是ControllerAdvice靖避。
ControllerAdvice注解的作用就是監(jiān)聽所有的Controller潭枣,一旦Controller拋出CustomException,就會在@ExceptionHandler(CustomException.class)注解的方法里面對該異常進行處理幻捏。處理方法很簡單就是使用ApiResponse.error(e)
包裝為通用的接口數(shù)據(jù)結(jié)構(gòu)返回給前端盆犁。
@ControllerAdvice
public class WebExceptionHandler {
//處理程序員主動轉(zhuǎn)換的自定義異常
@ExceptionHandler(CustomException.class)
@ResponseBody
public ApiResponse customerException(CustomException e) {
if(e.getCode() == CustomExceptionType.SYSTEM_ERROR.getCode()){
//400異常不需要持久化,將異常信息以友好的方式告知用戶就可以
//TODO 將500異常信息持久化處理篡九,方便運維人員處理
}
return ApiResponse.error(e);
}
//處理程序員在程序中未能捕獲(遺漏的)異常
@ExceptionHandler(Exception.class)
@ResponseBody
public ApiResponse exception(Exception e) {
return ApiResponse.error(new CustomException(CustomExceptionType.OTHER_ERROR));
}
//處理異常校驗失敗
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseBody
public ApiResponse handleBindException(MethodArgumentNotValidException ex) {
FieldError fieldError = ex.getBindingResult().getFieldError();
return ApiResponse.error(new CustomException(CustomExceptionType.BAD_REQUEST,
fieldError.getDefaultMessage()));
}
//處理異常校驗失敗
@ExceptionHandler(BindException.class)
@ResponseBody
public ApiResponse handleBindException(BindException ex) {
FieldError fieldError = ex.getBindingResult().getFieldError();
return ApiResponse.error(new CustomException(CustomExceptionType.BAD_REQUEST,
fieldError.getDefaultMessage()));
}
@ExceptionHandler(IllegalArgumentException.class)
@ResponseBody
public ApiResponse handleIllegalArgumentException(IllegalArgumentException e) {
return ApiResponse.error(
new CustomException(CustomExceptionType.BAD_REQUEST, e.getMessage())
);
}
@ExceptionHandler(ModelViewException.class)
public ModelAndView viewExceptionHandler(HttpServletRequest req, ModelViewException e) {
ModelAndView modelAndView = new ModelAndView();
//將異常信息設(shè)置如modelAndView
modelAndView.addObject("exception", e);
modelAndView.addObject("url", req.getRequestURL());
modelAndView.setViewName("error");
//返回ModelAndView
return modelAndView;
}
}
3.2 讓業(yè)務(wù)狀態(tài)與HTTP協(xié)議狀態(tài)一致
@ControllerAdvice
public class GlobalResponseAdvice implements ResponseBodyAdvice {
@Override
public boolean supports(MethodParameter methodParameter, Class aClass) {
return true;
}
@Override
public Object beforeBodyWrite(Object body,
MethodParameter methodParameter,
MediaType mediaType,
Class aClass,
ServerHttpRequest serverHttpRequest,
ServerHttpResponse serverHttpResponse) {
//如果響應(yīng)結(jié)果是JSON數(shù)據(jù)類型
if(mediaType.equalsTypeAndSubtype(MediaType.APPLICATION_JSON)) {
if(body instanceof ApiResponse) {
ApiResponse apiResponse = (ApiResponse)body;
if(apiResponse.getCode() != 999) { //999 不是標準的HTTP狀態(tài)碼谐岁,特殊處理
serverHttpResponse.setStatusCode(HttpStatus.valueOf(apiResponse.getCode()));
}
return body;
} else {
serverHttpResponse.setStatusCode(HttpStatus.OK);
return ApiResponse.success(body);
}
}
return body;
}
}