上一篇文章RESTful API 返回統一JSON數據格式 說明了 RESTful API 統一返回數據格式問題,這是請求一切正常的情形,這篇文章將說明如何統一處理異常,以及其背後的實現原理,老套路,先實現,後說明原理,有了上一篇文章的鋪底,相信,理解這篇文章就得心應手了前端
新建 BusinessException.class 類表示業務異常,注意這是一個 Runtime 異常java
@Data @AllArgsConstructor public final class BusinessException extends RuntimeException { private String errorCode; private String errorMsg; }
在 CommonResult 類中添加靜態方法 errorResult 用於接收異常碼和異常消息:ios
public static <T> CommonResult<T> errorResult(String errorCode, String errorMsg){ CommonResult<T> commonResult = new CommonResult<>(); commonResult.errorCode = errorCode; commonResult.errorMsg = errorMsg; commonResult.status = -1; return commonResult; }
一樣要用到 @RestControllerAdvice
註解,將統一異常添加到配置中:git
@RestControllerAdvice("com.example.unifiedreturn.api") static class UnifiedExceptionHandler{ @ExceptionHandler(BusinessException.class) public CommonResult<Void> handleBusinessException(BusinessException be){ return CommonResult.errorResult(be.getErrorCode(), be.getErrorMsg()); } }
三部搞定,到這裏不管是 Controller 仍是 Service 中,只要拋出 BusinessException, 咱們都會返回給前端一個統一數據格式github
將 UserController 中的方法進行改造,直接拋出異常:面試
@GetMapping("/{id}") public UserVo getUserById(@PathVariable Long id){ throw new BusinessException("1001", "根據ID查詢用戶異常"); }
瀏覽器中輸入: http://localhost:8080/users/1spring
在 Service 中拋出異常:json
@Service public class UserServiceImpl implements UserService { /** * 根據用戶ID查詢用戶 * * @param id * @return */ @Override public UserVo getUserById(Long id) { throw new BusinessException("1001", "根據ID查詢用戶異常"); } }
運行是獲得一樣的結果,因此咱們儘量的拋出異常吧 (做爲一個程序猿這種心理很可拍)設計模式
解剖這個過程是至關糾結的,爲了更好的說(yin)明(wei)問(wo)題(lan),我要說重中之重了,真心但願看該文章的童鞋本身去案發現場發現線索 仍是在 WebMvcConfigurationSupport 類中實例化了 HandlerExceptionResolver Beanapi
@Bean public HandlerExceptionResolver handlerExceptionResolver() { List<HandlerExceptionResolver> exceptionResolvers = new ArrayList<>(); configureHandlerExceptionResolvers(exceptionResolvers); if (exceptionResolvers.isEmpty()) { addDefaultHandlerExceptionResolvers(exceptionResolvers); } extendHandlerExceptionResolvers(exceptionResolvers); HandlerExceptionResolverComposite composite = new HandlerExceptionResolverComposite(); composite.setOrder(0); composite.setExceptionResolvers(exceptionResolvers); return composite; }
和上一篇文章一毛同樣的套路,ExceptionHandlerExceptionResolver 實現了 InitializingBean 接口,重寫了 afterPropertiesSet 方法:
@Override public void afterPropertiesSet() { // Do this first, it may add ResponseBodyAdvice beans initExceptionHandlerAdviceCache(); ... } private void initExceptionHandlerAdviceCache() { if (getApplicationContext() == null) { return; } List<ControllerAdviceBean> adviceBeans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext()); AnnotationAwareOrderComparator.sort(adviceBeans); for (ControllerAdviceBean adviceBean : adviceBeans) { Class<?> beanType = adviceBean.getBeanType(); if (beanType == null) { throw new IllegalStateException("Unresolvable type for ControllerAdviceBean: " + adviceBean); } // 重點看這個構造方法 ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(beanType); if (resolver.hasExceptionMappings()) { this.exceptionHandlerAdviceCache.put(adviceBean, resolver); } if (ResponseBodyAdvice.class.isAssignableFrom(beanType)) { this.responseBodyAdvice.add(adviceBean); } } }
重點看上面我用註釋標記的構造方法,代碼很好懂,仔細看看吧
/** * A constructor that finds {@link ExceptionHandler} methods in the given type. * @param handlerType the type to introspect */ public ExceptionHandlerMethodResolver(Class<?> handlerType) { for (Method method : MethodIntrospector.selectMethods(handlerType, EXCEPTION_HANDLER_METHODS)) { for (Class<? extends Throwable> exceptionType : detectExceptionMappings(method)) { addExceptionMapping(exceptionType, method); } } } /** * Extract exception mappings from the {@code @ExceptionHandler} annotation first, * and then as a fallback from the method signature itself. */ @SuppressWarnings("unchecked") private List<Class<? extends Throwable>> detectExceptionMappings(Method method) { List<Class<? extends Throwable>> result = new ArrayList<>(); detectAnnotationExceptionMappings(method, result); if (result.isEmpty()) { for (Class<?> paramType : method.getParameterTypes()) { if (Throwable.class.isAssignableFrom(paramType)) { result.add((Class<? extends Throwable>) paramType); } } } if (result.isEmpty()) { throw new IllegalStateException("No exception types mapped to " + method); } return result; } private void detectAnnotationExceptionMappings(Method method, List<Class<? extends Throwable>> result) { ExceptionHandler ann = AnnotatedElementUtils.findMergedAnnotation(method, ExceptionHandler.class); Assert.state(ann != null, "No ExceptionHandler annotation"); result.addAll(Arrays.asList(ann.value())); }
到這裏,咱們用 @RestControllerAdvice
和 @ExceptionHandler
註解就會被 Spring 掃描到上下文,供咱們使用
讓咱們回到你最熟悉的調用的入口 DispatcherServlet 類的 doDispatch 方法:
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception { ... try { ModelAndView mv = null; Exception dispatchException = null; try { ... // 當請求發生異常,該方法會經過 catch 捕獲異常 mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); ... } catch (Exception ex) { dispatchException = ex; } catch (Throwable err) { // As of 4.3, we're processing Errors thrown from handler methods as well, // making them available for @ExceptionHandler methods and other scenarios. dispatchException = new NestedServletException("Handler dispatch failed", err); } // 調用該方法分析捕獲的異常 processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException); } ... }
接下來,咱們來看 processDispatchResult 方法,這裏只要展現調用棧你就會眼前一亮了:
上一篇文章的返回統一數據格式是基礎,當異常狀況發生時,只不過須要將異常信息提取出來。文章的好多地方設計方式不可取,好比咱們最好將異常封裝在一個 Enum 類,經過 enum 對象拋出異常等,若是你用到這些,去完善你的設計方案吧
回覆 「demo」,打開連接,查看文件夾 「unifiedreturn」下內容,獲取完整代碼
JSON-Viewer 是 Chrome 瀏覽器的插件,用於快速解析及格式化 json 內容,在 Chrome omnibox(多功能輸入框)輸入json-viewer + TAB
,將 json 內容拷貝進去,而後輸入回車鍵,將看到結構清晰的 json 數據,同時能夠自定義主題
歡迎持續關注公衆號:「日拱一兵」
- 前沿 Java 技術乾貨分享
- 高效工具彙總 | 回覆「工具」
- 面試問題分析與解答
- 技術資料領取 | 回覆「資料」
以讀偵探小說思惟輕鬆趣味學習 Java 技術棧相關知識,本着將複雜問題簡單化,抽象問題具體化和圖形化原則逐步分解技術問題,技術持續更新,請持續關注......