SpringBoot中统一API返回格式的两种方式

2023-02-18 23:49:27 浏览数 (1)

微服务中,由于各业务团队之间的对接,各个团队之间需要统一返回格式,这样解析时不容易出现错误。因此,有必要统一返回格式。下面我说下项目中常见的两种统一和变更返回值格式的方式

ResponseBodyAdvice切面方式

这种方式简单易实现,仅仅只需要实现ResponseBodyAdvice方法,然后指定要拦截的包路径即可

代码语言:txt复制
@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作为包装对象时,此切面仍然为我们实现了包装

代码语言:txt复制
@RestController
public class ResponseController {
    @PostMapping("test")
    public Circle testReturn(){
       Circle circle =  new Circle();
       circle.setRadius(5.0d);
        return circle;
    }
}

返回值为

代码语言:txt复制
{
    "code": 0,
    "message": "OK",
    "body": {
        "radius": 5.0,
        "area": 78.53981633974483
    }
}

究其原因则是因为在源码中,初始化时就对切面进行了处理,从而可以执行相应的操作,具体可以参考RequestMappingHandlerAdapter#initControllerAdviceCache

使用更为底层的HandlerMethodReturnValueHandler来自定义返回值类型

在操作的过程中也是同样的逻辑

代码语言:txt复制
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放到最前面执行才可以

代码语言:txt复制
@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);
    }

}

源码执行顺序如下

代码语言:txt复制
    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 接口响应格式?

0 人点赞