天天用SpringBoot,還不懂RESTful API返回統一數據格式是怎麼實現的?

上一篇文章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」下內容,獲取完整代碼

靈魂追問

  1. 這兩篇文章,你學到了哪些設計模式?
  2. 你能熟練的使用反射嗎?當看源碼是會看到不少反射的應用
  3. 你瞭解 Spring CGLIB 嗎?它的工做原理是什麼?

提升效率工具

JSON-Viewer

JSON-Viewer 是 Chrome 瀏覽器的插件,用於快速解析及格式化 json 內容,在 Chrome omnibox(多功能輸入框)輸入json-viewer + TAB ,將 json 內容拷貝進去,而後輸入回車鍵,將看到結構清晰的 json 數據,同時能夠自定義主題

另外,前端人員打開開發者工具,雙擊請求連接,會自動將 response 中的 json 數據解析出來,很是方便

推薦閱讀


歡迎持續關注公衆號:「日拱一兵」

  • 前沿 Java 技術乾貨分享
  • 高效工具彙總 | 回覆「工具」
  • 面試問題分析與解答
  • 技術資料領取 | 回覆「資料」

以讀偵探小說思惟輕鬆趣味學習 Java 技術棧相關知識,本着將複雜問題簡單化,抽象問題具體化和圖形化原則逐步分解技術問題,技術持續更新,請持續關注......

相關文章
相關標籤/搜索