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

關於 Spring 的全局處理,我有兩方面要說:前端

  1. 統一數據返回格式
  2. 統一異常處理
    爲了將兩個問題說明清楚,將分兩個章節分別說明,本章主要說第一點

有童鞋說,咱們項目都作了這種處理,就是在每一個 API 都單獨工具類將返回值進行封裝,但這種不夠優雅;我想寫最少的代碼完成這件事,也許有童鞋說,加幾個註解就解決問題了,說的沒錯,但這篇文章主要是爲了說明爲何加了幾個註解就解決問題了,目的是但願你們知其因此然git

爲了更好的說明問題,本文先說明如何實現,而後再詳細剖析實現原理(這很關鍵)web

爲何要作統一數據返回格式

先後端分離是當今服務形式的主流,如何設計一個好的 RESTful API ,以及如何讓前端小夥伴能夠處理標準的 response JSON 數據結構都相當重要,爲了讓前端有更好的邏輯展現與頁面交互處理,每一次 RESTful 請求都應該包含如下幾個信息:面試

名稱
描述
status
狀態碼,標識請求成功與否,如 [1:成功;-1:失敗]
errorCode
錯誤碼,給出明確錯誤碼,更好的應對業務異常;請求成功該值可爲空
errorMsg
錯誤消息,與錯誤碼相對應,更具體的描述異常信息
resultBody
返回結果,一般是 Bean 對象對應的 JSON 數據, 一般爲了應對不一樣返回值類型,將其聲明爲泛型類型

實現

通用返回值類定義

根據上面的描述,用 Java Bean 來體現這個結構就是這樣:spring

@Data
public final class CommonResult<T> {

    private int status = 1;

    private String errorCode = "";

    private String errorMsg = "";

    private T resultBody;

    public CommonResult() {
    }

    public CommonResult(T resultBody) {
        this.resultBody = resultBody;
    }
}複製代碼

配置

沒錯,咱們須要藉助幾個關鍵註解來完成一下相關配置:後端

@EnableWebMvc
@Configuration
public class UnifiedReturnConfig {

    @RestControllerAdvice("com.example.unifiedreturn.api")
    static class CommonResultResponseAdvice implements ResponseBodyAdvice<Object>{
        @Override
        public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) {
            return true;
        }

        @Override
        public Object beforeBodyWrite(Object body, MethodParameter methodParameter, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
            if (body instanceof CommonResult){
                return body;
            }

            return new CommonResult<Object>(body);
        }
    }
}複製代碼

到這裏就結束了,咱們就能夠縱情的寫任何 RESTful API 了,全部的返回值都會有統一的 JSON 結構設計模式

測試

新建 UserController,添加相應的 RESTful API,測試用例寫的比較簡單,只爲了說明返回值的處理api

@RestController
@RequestMapping("/users")
public class UserController {


    @GetMapping("")
    public List<UserVo> getUserList(){
        List<UserVo> userVoList = Lists.newArrayListWithCapacity(2);
        userVoList.add(UserVo.builder().id(1L).name("日拱一兵").age(18).build());
        userVoList.add(UserVo.builder().id(2L).name("tan").age(19).build());
        return userVoList;
    }
}複製代碼

打開瀏覽器輸入地址測試: http://localhost:8080/users/ ,咱們能夠看到返回了 List JSON 數據瀏覽器

繼續添加 RESTful API,根據用戶 ID 查詢用戶信息數據結構

@GetMapping("/{id}")
public UserVo getUserByName(@PathVariable Long id){
    return UserVo.builder().id(1L).name("日拱一兵").age(18).build();
}複製代碼

打開瀏覽器輸入地址測試: http://localhost:8080/users/1 ,咱們能夠看到返回了單個 User JSON 數據

添加一個返回值類型爲 ResponseEntity 的 API

@GetMapping("/testResponseEntity")
public ResponseEntity getUserByAge(){
    return new ResponseEntity(UserVo.builder().id(1L).name("日拱一兵").age(18).build(), HttpStatus.OK);
}複製代碼

打開瀏覽器輸入地址測試: http://localhost:8080/users/testResponseEntity ,咱們能夠看到一樣返回了單個 User JSON 數據

解剖實現過程

我會將關鍵部分一一說明清楚,斷案還需小夥伴本身去案發現場(打開本身的 IDE 查看)

故事要從 @EnableWebMvc 這個註解提及,打開該註解看:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(DelegatingWebMvcConfiguration.class)
public @interface EnableWebMvc {
}複製代碼

經過 @Import 註解引入了 DelegatingWebMvcConfiguration.class,那來看這個類吧:

@Configuration
public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport {
    ...
}複製代碼

@Configuration 註解,你應該很熟悉了,該類的父類 WebMvcConfigurationSupport 中卻隱藏着一段關鍵代碼:

@Bean
public RequestMappingHandlerAdapter requestMappingHandlerAdapter() {
    RequestMappingHandlerAdapter adapter = createRequestMappingHandlerAdapter();
    ...
    return adapter;
}複製代碼

RequestMappingHandlerAdapter 是每一次請求處理的關鍵,來看該類的定義:

public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter
        implements BeanFactoryAware, InitializingBean {
    ...
}複製代碼

該類實現了 InitializingBean 接口,我在 Spring Bean 生命週期之「我從哪裏來」? 這篇文章中明確說明了 Spring Bean 初始化的幾個關鍵,其中 InitializingBean 接口的afterPropertiesSet 方法就是關鍵之一,在 RequestMappingHandlerAdapter 類中一樣重寫了該方法:

@Override
public void afterPropertiesSet() {
    // Do this first, it may add ResponseBody advice beans
    initControllerAdviceCache();

    if (this.argumentResolvers == null) {
        List<HandlerMethodArgumentResolver> resolvers = getDefaultArgumentResolvers();
        this.argumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);
    }
    if (this.initBinderArgumentResolvers == null) {
        List<HandlerMethodArgumentResolver> resolvers = getDefaultInitBinderArgumentResolvers();
        this.initBinderArgumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);
    }
    if (this.returnValueHandlers == null) {
        List<HandlerMethodReturnValueHandler> handlers = getDefaultReturnValueHandlers();
        this.returnValueHandlers = new HandlerMethodReturnValueHandlerComposite().addHandlers(handlers);
    }
}複製代碼

該方法內容都很是關鍵,但咱們先來看 initControllerAdviceCache 方法,其餘內容後續再單獨說明:

private void initControllerAdviceCache() {
        ...
    if (logger.isInfoEnabled()) {
        logger.info("Looking for @ControllerAdvice: " + getApplicationContext());
    }

    List<ControllerAdviceBean> beans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());
    AnnotationAwareOrderComparator.sort(beans);

    List<Object> requestResponseBodyAdviceBeans = new ArrayList<Object>();

    for (ControllerAdviceBean bean : beans) {
        ...
        if (ResponseBodyAdvice.class.isAssignableFrom(bean.getBeanType())) {
            requestResponseBodyAdviceBeans.add(bean);
        }
    }
}複製代碼

經過 ControllerAdviceBean 靜態方法掃描 ControllerAdvice 註解,但是咱們在實現上使用的是 @RestControllerAdvice 註解,打開看該註解:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@ControllerAdvice
@ResponseBody
public @interface RestControllerAdvice {複製代碼

該註解由 @ControllerAdvice@ResponseBody 標記,就比如你熟悉的 @RestController 註解由 @Controller@ResponseBody 標記是同樣的

到這裏你已經知道咱們用 @RestControllerAdvice 標記的 Bean 是如何被加載到 Spring 上下文的,接下來就要知道是 Spring 是如何使用咱們的 bean 以及對返回 body 作處理的

其實在 HttpMessageConverter是如何轉換數據的? 這篇文章中已經說明了一部分,但願小夥伴先看這篇文章,下面的部分就會秒懂了,咱們在這裏作進一步的說明

在 AbstractMessageConverterMethodProcessor 的 writeWithMessageConverters 方法中,有一段核心代碼:

if (messageConverter instanceof GenericHttpMessageConverter) {
    if (((GenericHttpMessageConverter) messageConverter).canWrite(
            declaredType, valueType, selectedMediaType)) {
        outputValue = (T) getAdvice().beforeBodyWrite(outputValue, returnType, selectedMediaType,
                (Class<? extends HttpMessageConverter<?>>) messageConverter.getClass(),
                inputMessage, outputMessage);
            ...
        return;
    }
}複製代碼

能夠看到經過 getAdvice() 調用了 beforeBodyWrite 方法,咱們已經接近真相了

protected RequestResponseBodyAdviceChain getAdvice() {
    return this.advice;
}複製代碼

RequestResponseBodyAdviceChain,看名字帶有 Chain,很明顯用到了「責任鏈設計模式」,這些內容在 不得不知的責任鏈設計模式 文章中明確說明過,只不過它傳遞責任鏈以循環的方式完成:

class RequestResponseBodyAdviceChain implements RequestBodyAdvice, ResponseBodyAdvice<Object> {

    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType contentType,
            Class<? extends HttpMessageConverter<?>> converterType,
            ServerHttpRequest request, ServerHttpResponse response) {

        return processBody(body, returnType, contentType, converterType, request, response);
    }

    @SuppressWarnings("unchecked")
    private <T> Object processBody(Object body, MethodParameter returnType, MediaType contentType,
            Class<? extends HttpMessageConverter<?>> converterType,
            ServerHttpRequest request, ServerHttpResponse response) {

        for (ResponseBodyAdvice<?> advice : getMatchingAdvice(returnType, ResponseBodyAdvice.class)) {
            if (advice.supports(returnType, converterType)) {
                body = ((ResponseBodyAdvice<T>) advice).beforeBodyWrite((T) body, returnType,
                        contentType, converterType, request, response);
            }
        }
        return body;
    }
}複製代碼

咱們重寫的 beforeBodyWrite 方法終究會被調用到,真相就是這樣了!!!

其實還沒完,你有沒有想過,若是咱們的 API 方法返回值是 org.springframework.http.ResponseEntity 類型,咱們能夠指定 HTTP 返回狀態碼,可是這個返回值會直接放到咱們的 beforeBodyWrite 方法的 body 參數中嗎?若是這樣作很明顯是錯誤的,由於 ResponseEntity 包含不少咱們非業務數據在裏面,那 Spring 是怎麼幫咱們處理的呢?

在咱們方法取得返回值而且在調用 beforeBodyWrite 方法以前,還要選擇 HandlerMethodReturnValueHandler 用於處理不一樣的 Handler 來處理返回值

在類 HandlerMethodReturnValueHandlerComposite 中的 handleReturnValue 方法中

@Override
public void handleReturnValue(Object returnValue, MethodParameter returnType,
        ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {

    HandlerMethodReturnValueHandler handler = selectHandler(returnValue, returnType);
    if (handler == null) {
        throw new IllegalArgumentException("Unknown return value type: " + returnType.getParameterType().getName());
    }
    handler.handleReturnValue(returnValue, returnType, mavContainer, webRequest);
}複製代碼

經過調用 selectHandler 方法來選擇合適的 handler,Spring 內置了不少個 Handler,咱們來看類圖:

HttpEntityMethodProcessor 就是其中之一,它重寫了 supportsParameter 方法,支持 HttpEntity 類型,即支持 ResponseEntity 類型:

@Override
public boolean supportsParameter(MethodParameter parameter) {
    return (HttpEntity.class == parameter.getParameterType() ||
            RequestEntity.class == parameter.getParameterType());
}複製代碼

因此當咱們返回的類型爲 ResponseEntity 時,就要經過 HttpEntityMethodProcessor 的 handleReturnValue 方法來處理咱們的結果:

@Override
public void handleReturnValue(Object returnValue, MethodParameter returnType,
        ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {

    ...
    if (responseEntity instanceof ResponseEntity) {
        int returnStatus = ((ResponseEntity<?>) responseEntity).getStatusCodeValue();
        outputMessage.getServletResponse().setStatus(returnStatus);
        if (returnStatus == 200) {
            if (SAFE_METHODS.contains(inputMessage.getMethod())
                    && isResourceNotModified(inputMessage, outputMessage)) {
                // Ensure headers are flushed, no body should be written.
                outputMessage.flush();
                // Skip call to converters, as they may update the body.
                return;
            }
        }
    }

    // Try even with null body. ResponseBodyAdvice could get involved.
    writeWithMessageConverters(responseEntity.getBody(), returnType, inputMessage, outputMessage);

    // Ensure headers are flushed even if no body was written.
    outputMessage.flush();
}複製代碼

該方法提取出 responseEntity.getBody(),並傳遞個 MessageConverter,而後再繼續調用 beforeBodyWrite 方法,這纔是真相!!!

這是 RESTful API 正常返回內容的狀況,下一篇文章,讓咱們來偵查一下統一異常狀況的處理以及實現原理

靈魂追問

  1. 返回值是非 ResponseEntity 類型時,用的是什麼 handler?它支持的返回值類型是什麼?看過你也許就知道爲何要用 @ResponseBody 註解了
  2. 你有追蹤過 DispatchServlet 的整個請求過程嗎?

提升效率工具

推薦閱讀

--------

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

- 前沿 Java 技術乾貨分享

- 高效工具彙總 | 回覆「工具」

- 面試問題分析與解答

- 技術資料領取 | 回覆「資料」

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

相關文章
相關標籤/搜索