SpringBoot中統一API返回格式的兩種方式

微服務中,由於各業務團隊之間的對接,各個團隊之間需要統一返回格式,這樣解析時不容易出現錯誤。因此,有必要統一返回格式。下面我說下項目中常見的兩種統一和變更返回值格式的方式

ResponseBodyAdvice切面方式

這種方式簡單易實現,僅僅只需要實現ResponseBodyAdvice方法,然後指定要攔截的包路徑即可

@ControllerAdvice("com.example.ut")
public class RestControllerAdvice implements ResponseBodyAdvice<Object> {

    private static final String VOID = "void";
    //判斷是否要執行beforeBodyWrite方法,true爲執行,false不執行
    @Override
    public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) {
        return true;
    }

    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
                                  Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request,
                                  ServerHttpResponse response) {
        if (VOID.equals(getReturnName(returnType))) {
            return null;
        }

        //基礎類型做特殊處理,如實際操作過程沒有此場景可無須使用
        if (isBasicType(returnType)) {
            return body;
        }


        if (body == null) {
            return  ApiResponse.of(null,StatusCode.OK);
        }
        if (!(body instanceof ApiResponse)) {
            return  ApiResponse.of(body, StatusCode.OK);
        }
        else {
            ApiResponse commonResult = (ApiResponse) body;
            return commonResult;
        }
    }

    private String getReturnName(MethodParameter returnType) {
        if (returnType == null || returnType.getMethod() == null) {
            return StringUtils.EMPTY;
        }
        return returnType.getMethod().getReturnType().getName();

    }

    private boolean isBasicType(MethodParameter returnType) {

        if (returnType == null || returnType.getMethod() == null) {
            return true;
        }

        String name = returnType.getMethod().getReturnType().getSimpleName();
        switch (name) {
            case "String":
            case "byte[]":
            case "ResponseEntity":
                return true;
            default:
                return false;
        }

    }
}

測試時使用通用的返回通用類作爲測試依據
當我們再返回值沒有使用ApiResponse作爲包裝對象時,此切面仍然爲我們實現了包裝

@RestController
public class ResponseController {
    @PostMapping("test")
    public Circle testReturn(){
       Circle circle =  new Circle();
       circle.setRadius(5.0d);
        return circle;
    }
}

返回值爲

{
    "code": 0,
    "message": "OK",
    "body": {
        "radius": 5.0,
        "area": 78.53981633974483
    }
}

究其原因則是因爲在源碼中,初始化時就對切面進行了處理,從而可以執行相應的操作,具體可以參考RequestMappingHandlerAdapter#initControllerAdviceCache

使用更爲底層的HandlerMethodReturnValueHandler來自定義返回值類型

在操作的過程中也是同樣的邏輯

public class ApiResponseHandlerMethodReturnValueHandler implements HandlerMethodReturnValueHandler {

    private MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();

    //是否支持handleReturnValue
    @Override
    public boolean supportsReturnType(MethodParameter returnType) {
        return (AnnotatedElementUtils.hasAnnotation(returnType.getContainingClass(), ResponseBody.class) ||
                returnType.hasMethodAnnotation(ResponseBody.class))
                && !ApiResponse.class.equals(returnType.getParameterType());
    }

    @Override
    public void handleReturnValue(Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer,
                                  NativeWebRequest webRequest) throws Exception {
        // TODO 可通過客戶端的傳遞的請求頭來切換不同的響應體的內容
        mavContainer.setRequestHandled(true);
        // returnValue =  POJO
        ApiResponse apiResponse = ApiResponse.ok(returnValue);
        HttpServletResponse response = (HttpServletResponse) webRequest.getNativeResponse();
        response.addHeader("v", "3");
        ServletServerHttpResponse httpOutMessage = createOutputMessage(webRequest);
        converter.write(apiResponse, MediaType.APPLICATION_JSON, httpOutMessage);
    }

    protected ServletServerHttpResponse createOutputMessage(NativeWebRequest webRequest) {
        HttpServletResponse response = webRequest.getNativeResponse(HttpServletResponse.class);
        Assert.state(response != null, "No HttpServletResponse");
        return new ServletServerHttpResponse(response);
    }
}

但是由於spring的默認處理類是RequestResponseBodyMethodProcessor,它是根據判斷是否有@ResponseBody註解來處理的
且他是在自定義HandlerMethodReturnValueHandler之前執行的,所以我們需要把我們的自定義且他是在自定義HandlerMethodReturnValueHandler放到最前面執行纔可以

@Configuration
public class WebMvcConfiguration {

    @Autowired
    public void resetRequestMappingHandlerAdapter(RequestMappingHandlerAdapter requestMappingHandlerAdapter) {
        List<HandlerMethodReturnValueHandler> oldReturnValueHandlers = requestMappingHandlerAdapter.getReturnValueHandlers();
        List<HandlerMethodReturnValueHandler> newReturnValueHandlers = new ArrayList<>(oldReturnValueHandlers);
        newReturnValueHandlers.add(0, new ApiResponseHandlerMethodReturnValueHandler());
        requestMappingHandlerAdapter.setReturnValueHandlers(newReturnValueHandlers);
    }

}

源碼執行順序如下

    private List<HandlerMethodReturnValueHandler> getDefaultReturnValueHandlers() {
        List<HandlerMethodReturnValueHandler> handlers = new ArrayList(20);
        handlers.add(new ModelAndViewMethodReturnValueHandler());
        handlers.add(new ModelMethodProcessor());
        handlers.add(new ViewMethodReturnValueHandler());
        handlers.add(new ResponseBodyEmitterReturnValueHandler(this.getMessageConverters(), this.reactiveAdapterRegistry, this.taskExecutor, this.contentNegotiationManager));
        handlers.add(new StreamingResponseBodyReturnValueHandler());
        handlers.add(new HttpEntityMethodProcessor(this.getMessageConverters(), this.contentNegotiationManager, this.requestResponseBodyAdvice));
        handlers.add(new HttpHeadersReturnValueHandler());
        handlers.add(new CallableMethodReturnValueHandler());
        handlers.add(new DeferredResultMethodReturnValueHandler());
        handlers.add(new AsyncTaskMethodReturnValueHandler(this.beanFactory));
        handlers.add(new ServletModelAttributeMethodProcessor(false));
        //先執行了RequestResponseBodyMethodProcessor
        handlers.add(new RequestResponseBodyMethodProcessor(this.getMessageConverters(), this.contentNegotiationManager, this.requestResponseBodyAdvice));
        handlers.add(new ViewNameMethodReturnValueHandler());
        handlers.add(new MapMethodProcessor());
        //後執行自定義HandlerMethodReturnValueHandler
        if (this.getCustomReturnValueHandlers() != null) {
            handlers.addAll(this.getCustomReturnValueHandlers());
        }

        if (!CollectionUtils.isEmpty(this.getModelAndViewResolvers())) {
            handlers.add(new ModelAndViewResolverMethodReturnValueHandler(this.getModelAndViewResolvers()));
        } else {
            handlers.add(new ServletModelAttributeMethodProcessor(true));
        }

        return handlers;
    }

這樣即可達到與上述ResponseBodyAdvice切面方式一樣的效果

參考文章Spring Boot 中如何統一 API 接口響應格式?

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章