前段時間寫了一篇關于實現統一響應信息的博文,根據文中實戰操作,能夠解決正常響應的一致性,但想要實現優雅響應,還需要優雅的處理異常響應,所以有了這篇內容。
作為后臺服務,能夠正確的處理程序拋出的異常,并返回友好的異常信息是非常重要的,畢竟我們大部分代碼都是為了 處理異常情況。而且,統一的異常響應,有助于客戶端理解服務端響應,并作出正確處理,而且能夠提升接口的服務質量。
SpringBoot提供了異常的響應,可以通過/error
請求查看效果:
這是從瀏覽器打開的場景,也就是請求頭不包括content-type: applicaton/json
,大白板一個,和友好完全不搭邊。
這是請求頭包括content-type: applicaton/json
時的響應,格式還行,但是我們還需要加工一下,實現自定義的異常碼和異常信息。
本文主要是針對RESTful請求的統一響應,想要實現的功能包括:
- 自動封裝異常,返回統一響應
- 異常信息國際化
定義異常響應類
當程序發送錯誤時,不應該將晦澀的堆棧報告信息返回給API客戶端,從某種意義講,這是一種不禮貌的和不負責任的行為。
我們在SpringBoot 實戰:一招實現結果的優雅響應中,定義了一個響應類,為什么還要再定義一個異常響應類呢?其實是為了語義明確且職責單一。類圖如下:
具體代碼如下:
基礎類BaseResponse
:
@Data public abstract class BaseResponse { private Integer code; private String desc; private Date timestamp = new Date(); private String path; public BaseResponse() { } public BaseResponse(final Integer code, final String desc) { this.code = code; this.desc = desc; } public BaseResponse(final Integer code, final String desc, final String path) { this.code = code; this.desc = desc; this.path = path; } }
異常類ErrorResponse
:
@EqualsAndHashCode(callSuper = true) @Data public class ErrorResponse extends BaseResponse { public ErrorResponse(final Integer code, final String desc) { super(code, desc); } public ErrorResponse(final Integer code, final String desc, final WebRequest request) { super(code, desc, extractRequestURI(request)); } public ErrorResponse(final HttpStatus status, final Exception e) { super(status.value(), status.getReasonPhrase() + ": " + e.getMessage()); } public ErrorResponse(final HttpStatus status, final Exception e, final WebRequest request) { super(status.value(), status.getReasonPhrase() + ": " + e.getMessage(), extractRequestURI(request)); } private static String extractRequestURI(WebRequest request) { final String requestURI; if (request instanceof ServletWebRequest) { ServletWebRequest servletWebRequest = (ServletWebRequest) request; requestURI = servletWebRequest.getRequest().getRequestURI(); } else { requestURI = request.getDescription(false); } return requestURI; } }
定義異常枚舉類
為了能夠規范響應碼和響應信息,我們可以定義一個枚舉類。
枚舉接口ResponseEnum
:
public interface ResponseEnum { Integer getCode(); String getMessage(); default String getLocaleMessage() { return getLocaleMessage(null); } String getLocaleMessage(Object[] args); }
枚舉類CommonResponseEnum
:
public enum CommonResponseEnum implements ResponseEnum { BAD_REQUEST(HttpStatus.BAD_REQUEST.value(), "Bad Request"), NOT_FOUND(HttpStatus.NOT_FOUND.value(), "Not Found"), METHOD_NOT_ALLOWED(HttpStatus.METHOD_NOT_ALLOWED.value(), "Method Not Allowed"), NOT_ACCEPTABLE(HttpStatus.NOT_ACCEPTABLE.value(), "Not Acceptable"), REQUEST_TIMEOUT(HttpStatus.REQUEST_TIMEOUT.value(), "Request Timeout"), UNSUPPORTED_MEDIA_TYPE(HttpStatus.UNSUPPORTED_MEDIA_TYPE.value(), "Unsupported Media Type"), INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR.value(), "Server Error"), SERVICE_UNAVAILABLE(HttpStatus.SERVICE_UNAVAILABLE.value(), "Service Unavailable"), ILLEGAL_ARGUMENT(4000, "Illegal Argument"), DATA_NOT_FOUND(4004, "Data Not Found"), USER_NOT_FOUND(4104, "User Not Found"), MENU_NOT_FOUND(4204, "Menu Not Found"), INTERNAL_ERROR(9999, "Server Error"), ; private final Integer code; private final String message; private MessageSource messageSource; CommonResponseEnum(final Integer code, final String message) { this.code = code; this.message = message; } @Override public Integer getCode() { return code; } @Override public String getMessage() { return message; } @Override public String getLocaleMessage(Object[] args) { return messageSource.getMessage("response.error." + code, args, message, LocaleContextHolder.getLocale()); } public void setMessageSource(final MessageSource messageSource) { this.messageSource = messageSource; } @Component public static class ReportTypeServiceInjector { private final MessageSource messageSource; public ReportTypeServiceInjector(final MessageSource messageSource) { this.messageSource = messageSource; } @PostConstruct public void postConstruct() { for (final CommonResponseEnum anEnum : CommonResponseEnum.values()) { anEnum.setMessageSource(messageSource); } } } }
需要注意的是,我們在異常枚舉類中定義了ReportTypeServiceInjector
類,這個類的作用是為枚舉類注入MessageSource
對象,是為了實現異常信息的國際化。這部分功能Spring已經封裝好了,我們只需要在resources目錄中定義一組messages.properties
文件就可以了,比如:
message.properties定義默認描述:
response.error.4000=[DEFAULT] Illegal Arguments response.error.4004=[DEFAULT] Not Found
messages_zh_CN.properties定義中文描述:
response.error.4004=對應數據未找到 response.error.9999=系統異常,請求參數: {0}
messages_en_US.properties定義英文描述:
response.error.4004=Not Found
自定義異常類
Java和Spring中提供了很多可用的異常類,可以滿足大部分場景,但是有時候我們希望異常類可以攜帶更多信息,所以還是需要自定義異常類:
- 可以攜帶我們想要的信息;
- 有更加明確語義;
- 附帶效果,可以知道這是手動拋出的業務異常。
上代碼:
@Data @EqualsAndHashCode(callSuper = true) public class CodeBaseException extends RuntimeException { private final ResponseEnum anEnum; private final Object[] args;// 打印參數 private final String message;// 異常信息 private final Throwable cause;// 異常棧 public CodeBaseException(final ResponseEnum anEnum) { this(anEnum, null, anEnum.getMessage(), null); } public CodeBaseException(final ResponseEnum anEnum, final String message) { this(anEnum, null, message, null); } public CodeBaseException(final ResponseEnum anEnum, final Object[] args, final String message) { this(anEnum, args, message, null); } public CodeBaseException(final ResponseEnum anEnum, final Object[] args, final String message, final Throwable cause) { this.anEnum = anEnum; this.args = args; this.message = message; this.cause = cause; } }
自定義異常信息處理類
前期準備工作完成,接下來定義異常信息處理類。
Spring自帶的異常信息處理類往往不能滿足我們實際的業務需求,這就需要我們定義符合具體情況的異常信息處理類,在自定義異常信息處理類中,我們可以封裝更為詳細的異常報告。我們可以擴展Spring提供的ResponseEntityExceptionHandler類定義自己的異常信息處理類,站在巨人的肩膀上,快速封裝自己需要的類。
通過源碼可以看到,ResponseEntityExceptionHandler
類的核心方法是public final ResponseEntity<Object> handleException(Exception ex, WebRequest request)
,所有的異常都在這個方法中根據類型進行處理,我們只需要實現具體的處理方法即可:
@RestControllerAdvice @Slf4j public class UnifiedExceptionHandlerV2 extends ResponseEntityExceptionHandler { private static final String ENV_PROD = "prod"; private final MessageSource messageSource; private final Boolean isProd; public UnifiedExceptionHandlerV2(@Value("${spring.profiles.active:dev}") final String activeProfile, final MessageSource messageSource) { this.messageSource = messageSource; this.isProd = new HashSet<>(Arrays.asList(activeProfile.split(","))).contains(ENV_PROD); } @Override protected ResponseEntity<Object> handleExceptionInternal(final Exception e, final Object body, final HttpHeaders headers, final HttpStatus status, final WebRequest request) { log.info("請求異常:" + e.getMessage(), e); if (HttpStatus.INTERNAL_SERVER_ERROR.equals(status)) { request.setAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE, e, WebRequest.SCOPE_REQUEST); } return new ResponseEntity<>(new ErrorResponse(status, e), headers, HttpStatus.OK); } @Override protected ResponseEntity<Object> handleBindException(final BindException ex, final HttpHeaders headers, final HttpStatus status, final WebRequest request) { log.info("參數綁定異常", ex); final ErrorResponse response = wrapperBindingResult(status, ex.getBindingResult()); return new ResponseEntity<>(response, headers, HttpStatus.OK); } @Override protected ResponseEntity<Object> handleMethodArgumentNotValid(final MethodArgumentNotValidException ex, final HttpHeaders headers, final HttpStatus status, final WebRequest request) { log.info("參數校驗異常", ex); final ErrorResponse response = wrapperBindingResult(status, ex.getBindingResult()); return new ResponseEntity<>(response, headers, HttpStatus.OK); } @ExceptionHandler(value = CodeBaseException.class) @ResponseBody public ErrorResponse handleBusinessException(CodeBaseException e) { log.error("業務異常:" + e.getMessage(), e); final ResponseEnum anEnum = e.getAnEnum(); return new ErrorResponse(anEnum.getCode(), anEnum.getLocaleMessage(e.getArgs())); } @ExceptionHandler(value = Exception.class) @ResponseBody public ErrorResponse handleExceptionInternal(Exception e) { log.error("未捕捉異常:" + e.getMessage(), e); final Integer code = INTERNAL_SERVER_ERROR.getCode(); return new ErrorResponse(code, getLocaleMessage(code, e.getMessage())); } /** * 包裝綁定異常結果 * * @param status HTTP狀態碼 * @param bindingResult 參數校驗結果 * @return 異常對象 */ private ErrorResponse wrapperBindingResult(HttpStatus status, BindingResult bindingResult) { final List<String> errorDesc = new ArrayList<>(); for (ObjectError error : bindingResult.getAllErrors()) { final StringBuilder msg = new StringBuilder(); if (error instanceof FieldError) { msg.append(((FieldError) error).getField()).append(": "); } msg.append(error.getDefaultMessage() == null ? "" : error.getDefaultMessage()); errorDesc.add(msg.toString()); } final String desc = isProd ? getLocaleMessage(status.value(), status.getReasonPhrase()) : String.join(", ", errorDesc); return new ErrorResponse(status.value(), desc); } private String getLocaleMessage(Integer code, String defaultMsg) { try { return messageSource.getMessage("" + code, null, defaultMsg, LocaleContextHolder.getLocale()); } catch (Throwable t) { log.warn("本地化異常消息發生異常: {}", code); return defaultMsg; } } }
如果感覺Spring的ResponseEntityExceptionHandler
類不夠靈活,也可以完全自定義異常處理類:
@RestControllerAdvice @Slf4j public class UnifiedExceptionHandler { private static final String ENV_PROD = "prod"; private final MessageSource messageSource; private final Boolean isProd; public UnifiedExceptionHandler(@Value("${spring.profiles.active:dev}") final String activeProfile, final MessageSource messageSource) { this.messageSource = messageSource; this.isProd = new HashSet<>(Arrays.asList(activeProfile.split(","))).contains(ENV_PROD); } @ExceptionHandler({ MissingServletRequestParameterException.class,// 缺少servlet請求參數異常處理方法 ServletRequestBindingException.class,// servlet請求綁定異常 TypeMismatchException.class,// 類型不匹配 HttpMessageNotReadableException.class,// 消息無法檢索 MissingServletRequestPartException.class// 缺少servlet請求部分 }) public ErrorResponse badRequestException(Exception e, WebRequest request) { log.info(e.getMessage(), e); return new ErrorResponse(BAD_REQUEST.getCode(), e.getMessage(), request); } @ExceptionHandler({ NoHandlerFoundException.class// 沒有發現處理程序異常 }) public ErrorResponse noHandlerFoundException(Exception e, WebRequest request) { log.info(e.getMessage(), e); return new ErrorResponse(NOT_FOUND.getCode(), e.getMessage(), request); } @ExceptionHandler({ HttpRequestMethodNotSupportedException.class// 不支持的HTTP請求方法異常信息處理方法 }) public ErrorResponse httpRequestMethodNotSupportedException(Exception e, WebRequest request) { log.info(e.getMessage(), e); return new ErrorResponse(METHOD_NOT_ALLOWED.getCode(), e.getMessage(), request); } @ExceptionHandler({ HttpMediaTypeNotAcceptableException.class// 不接受的HTTP媒體類型異常處方法 }) public ErrorResponse httpMediaTypeNotAcceptableException(Exception e, WebRequest request) { log.info(e.getMessage(), e); return new ErrorResponse(NOT_ACCEPTABLE.getCode(), e.getMessage(), request); } @ExceptionHandler({ HttpMediaTypeNotSupportedException.class// 不支持的HTTP媒體類型異常處理方法 }) public ErrorResponse httpMediaTypeNotSupportedException(Exception e, WebRequest request) { log.info(e.getMessage(), e); return new ErrorResponse(UNSUPPORTED_MEDIA_TYPE.getCode(), e.getMessage(), request); } @ExceptionHandler({ AsyncRequestTimeoutException.class// 異步請求超時異常 }) public ErrorResponse asyncRequestTimeoutException(Exception e, WebRequest request) { log.info(e.getMessage(), e); return new ErrorResponse(SERVICE_UNAVAILABLE.getCode(), e.getMessage(), request); } @ExceptionHandler({ MissingPathVariableException.class,// 請求路徑參數缺失異常處方法 HttpMessageNotWritableException.class,// HTTP消息不可寫 ConversionNotSupportedException.class,// 不支持轉換 }) public ErrorResponse handleServletException(Exception e, WebRequest request) { log.error(e.getMessage(), e); return new ErrorResponse(INTERNAL_SERVER_ERROR.getCode(), e.getMessage(), request); } @ExceptionHandler({ BindException.class// 參數綁定異常 }) @ResponseBody public ErrorResponse handleBindException(BindException e, WebRequest request) { log.error("參數綁定異常", e); return wrapperBindingResult(e.getBindingResult(), request); } /** * 參數校驗異常,將校驗失敗的所有異常組合成一條錯誤信息 */ @ExceptionHandler({ MethodArgumentNotValidException.class// 方法參數無效 }) @ResponseBody public ErrorResponse handleValidException(MethodArgumentNotValidException e, WebRequest request) { log.error("參數校驗異常", e); return wrapperBindingResult(e.getBindingResult(), request); } /** * 包裝綁定異常結果 */ private ErrorResponse wrapperBindingResult(BindingResult bindingResult, WebRequest request) { final List<String> errorDesc = new ArrayList<>(); for (ObjectError error : bindingResult.getAllErrors()) { final StringBuilder msg = new StringBuilder(); if (error instanceof FieldError) { msg.append(((FieldError) error).getField()).append(": "); } msg.append(error.getDefaultMessage() == null ? "" : error.getDefaultMessage()); errorDesc.add(msg.toString()); } final String desc = isProd ? getLocaleMessage(BAD_REQUEST.getCode(), "") : String.join(", ", errorDesc); return new ErrorResponse(BAD_REQUEST.getCode(), desc, request); } /** * 業務異常 */ @ExceptionHandler(value = CodeBaseException.class) @ResponseBody public ErrorResponse handleBusinessException(CodeBaseException e, WebRequest request) { log.error("業務異常:" + e.getMessage(), e); final ResponseEnum anEnum = e.getAnEnum(); return new ErrorResponse(anEnum.getCode(), anEnum.getLocaleMessage(e.getArgs()), request); } /** * 未定義異常 */ @ExceptionHandler(value = Exception.class) @ResponseBody public ErrorResponse handleExceptionInternal(Exception e, WebRequest request) { log.error("未捕捉異常:" + e.getMessage(), e); final Integer code = INTERNAL_SERVER_ERROR.getCode(); return new ErrorResponse(code, getLocaleMessage(code, e.getMessage()), request); } private String getLocaleMessage(Integer code, String defaultMsg) { try { return messageSource.getMessage("" + code, null, defaultMsg, LocaleContextHolder.getLocale()); } catch (Throwable t) { log.warn("本地化異常消息發生異常: {}", code); return defaultMsg; } } }
從上面兩個類可以看出,比較核心的是這么幾個注解:
- @ExceptionHandle:負責處理controller標注的類中拋出的異常的注解
-
@RestControllerAdvice:能夠將@ExceptionHandler標注的方法集中到一個地方進行處理的注解,這個注解是復合注解,實現了
@ControllerAdvice
和@ResponseBody
的功能。
借用譚朝紅博文中的圖片(藍色箭頭表示正常的請求和響應,紅色箭頭表示發生異常的請求和響應):
寫個Demo測試一下
接下來我們寫個demo測試一下是否能夠實現異常的優雅響應:
@RestController @RequestMapping("index") @Slf4j public class IndexController { private final IndexService indexService; public IndexController(final IndexService indexService) { this.indexService = indexService; } @GetMapping("hello1") public Response<String> hello1() { Response<String> response = new Response<>(); try { response.setCode(200); response.setDesc("請求成功"); response.setData(indexService.hello()); } catch (Exception e) { log.error("hello1方法請求異常", e); response.setCode(500); response.setDesc("請求異常:" + e.getMessage()); } finally { log.info("執行controller的finally結構"); } return response; } @GetMapping("hello2") public Response<String> hello2(@RequestParam("ex") String ex) { switch (ex) { case "ex1": throw new CodeBaseException(CommonResponseEnum.USER_NOT_FOUND, "用戶信息不存在"); case "ex2": throw new CodeBaseException(CommonResponseEnum.MENU_NOT_FOUND, "菜單信息不存在"); case "ex3": throw new CodeBaseException(CommonResponseEnum.ILLEGAL_ARGUMENT, "請求參數異常"); case "ex4": throw new CodeBaseException(CommonResponseEnum.DATA_NOT_FOUND, "數據不存在"); } throw new CodeBaseException(INTERNAL_ERROR, new Object[]{ex}, "請求異常", new RuntimeException("運行時異常信息")); } }
啟動服務之后,傳入不同參數獲取不同的異常信息:
// 請求 /index/hello2?ex=ex1
{
"code": 4104,
"desc": "User Not Found",
"timestamp": "2020-10-10T05:58:39.433+00:00",
"path": "/index/hello2"
}
// 請求 /index/hello2?ex=ex2
{
"code": 4204, "desc": "Menu Not Found",
"timestamp": "2020-10-10T06:00:34.141+00:00",
"path": "/index/hello2"
}
// 請求 /index/hello2?ex=ex3
{
"code": 4000,
"desc": "[DEFAULT] Illegal Arguments",
"timestamp": "2020-10-10T06:00:44.233+00:00",
"path": "/index/hello2"
}
// 請求 /index/hello2?ex=ex4
{
"code": 4004,
"desc": "對應數據未找到",
"timestamp": "2020-10-10T06:00:54.178+00:00",
"path": "/index/hello2"
}
附上文中的代碼:https://github.com/howardliu-cn/effective-spring/tree/main/spring-exception-handler,收工。
推薦閱讀
- SpringBoot 實戰:一招實現結果的優雅響應
- SpringBoot 實戰:如何優雅的處理異常
- SpringBoot 實戰:通過 BeanPostProcessor 動態注入 ID 生成器
- SpringBoot 實戰:自定義 Filter 優雅獲取請求參數和響應結果
- SpringBoot 實戰:優雅的使用枚舉參數
- SpringBoot 實戰:優雅的使用枚舉參數(原理篇)
- SpringBoot 實戰:在 RequestBody 中優雅的使用枚舉參數
到此這篇關于SpringBoot實戰之處理異常案例詳解的文章就介紹到這了,更多相關SpringBoot實戰之處理異常內容請搜索服務器之家以前的文章或繼續瀏覽下面的相關文章希望大家以后多多支持服務器之家!
原文鏈接:https://www.howardliu.cn/springboot-action-gracefully-response-exception/