每天用SpringBoot,还不懂RESTful API返回统一数据格式是怎么实现的?

简介: 每天用SpringBoot,还不懂RESTful API返回统一数据格式是怎么实现的?

关于 Spring 的全局处理,我有两方面要说:


  1. 统一数据返回格式


  1. 统一异常处理


为了将两个问题说明清楚,将分两个章节分别说明,本章主要说第一点


有童鞋说,我们项目都做了这种处理,就是在每个 API 都单独工具类将返回值进行封装,但这种不够优雅;我想写最少的代码完成这件事,也许有童鞋说,加几个注解就解决问题了,说的没错,但这篇文章主要是为了说明为什么加了几个注解就解决问题了,目的是希望大家知其所以然


微信图片_20220509203039.png


为了更好的说明问题,本文先说明如何实现,然后再详细剖析实现原理(这很关键)


为什么要做统一数据返回格式


前后端分离是当今服务形式的主流,如何设计一个好的 RESTful API ,以及如何让前端小伙伴可以处理标准的 response JSON 数据结构都至关重要,为了让前端有更好的逻辑展示与页面交互处理,每一次 RESTful 请求都应该包含以下几个信息:



名称 描述
status 状态码,标识请求成功与否,如 [1:成功;-1:失败]
errorCode 错误码,给出明确错误码,更好的应对业务异常;请求成功该值可为空
errorMsg 错误消息,与错误码相对应,更具体的描述异常信息
resultBody 返回结果,通常是 Bean 对象对应的 JSON 数据, 通常为了应对不同返回值类型,将其声明为泛型类型


实现


通用返回值类定义


根据上面的描述,用 Java Bean 来体现这个结构就是这样:


@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,测试用例写的比较简单,只为了说明返回值的处理


@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 数据


微信图片_20220509203229.jpg


继续添加 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 数据


微信图片_20220509203314.jpg


添加一个返回值类型为 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 数据


微信图片_20220509203500.jpg


解剖实现过程


我会将关键部分一一说明清楚,断案还需小伙伴自己去案发现场(打开自己的 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,我们来看类图:


微信图片_20220509203906.jpg


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 注解了


  1. 你有追踪过 DispatchServlet 的整个请求过程吗?
相关文章
|
5天前
|
存储 安全 Java
Spring Boot 编写 API 的 10条最佳实践
本文总结了 10 个编写 Spring Boot API 的最佳实践,包括 RESTful API 设计原则、注解使用、依赖注入、异常处理、数据传输对象(DTO)建模、安全措施、版本控制、文档生成、测试策略以及监控和日志记录。每个实践都配有详细的编码示例和解释,帮助开发者像专业人士一样构建高质量的 API。
|
6天前
|
JSON API 数据格式
淘系等商品评论Json数据格式参考,API接口测试
通过以上示例和说明,你可以了解淘系商品评论的JSON数据结构和如何使用相关API接口获取评论数据。在实际操作中,你需要参考具体的API接口文档和开放平台的相关说明进行配置和调用。
|
22天前
|
JSON JavaScript 前端开发
深入浅出Node.js:从零开始构建RESTful API
在数字化时代的浪潮中,后端开发作为连接用户与数据的桥梁,扮演着至关重要的角色。本文将引导您步入Node.js的奇妙世界,通过实践操作,掌握如何使用这一强大的JavaScript运行时环境构建高效、可扩展的RESTful API。我们将一同探索Express框架的使用,学习如何设计API端点,处理数据请求,并实现身份验证机制,最终部署我们的成果到云服务器上。无论您是初学者还是有一定基础的开发者,这篇文章都将为您打开一扇通往后端开发深层知识的大门。
38 12
|
25天前
|
XML JSON 缓存
深入理解RESTful API设计原则与实践
在现代软件开发中,构建高效、可扩展的应用程序接口(API)是至关重要的。本文旨在探讨RESTful API的核心设计理念,包括其基于HTTP协议的特性,以及如何在实际应用中遵循这些原则来优化API设计。我们将通过具体示例和最佳实践,展示如何创建易于理解、维护且性能优良的RESTful服务,从而提升前后端分离架构下的开发效率和用户体验。
|
28天前
|
监控 安全 API
深入浅出:构建高效RESTful API的最佳实践
在数字化时代,API已成为连接不同软件和服务的桥梁。本文将带你深入了解如何设计和维护一个高效、可扩展且安全的RESTful API。我们将从基础概念出发,逐步深入到高级技巧,让你能够掌握创建优质API的关键要素。无论你是初学者还是有经验的开发者,这篇文章都将为你提供实用的指导和启示。让我们一起探索API设计的奥秘,打造出色的后端服务吧!
|
26天前
|
JSON 缓存 测试技术
构建高效RESTful API的后端实践指南####
本文将深入探讨如何设计并实现一个高效、可扩展且易于维护的RESTful API。不同于传统的摘要概述,本节将直接以行动指南的形式,列出构建RESTful API时必须遵循的核心原则与最佳实践,旨在为开发者提供一套直接可行的实施框架,快速提升API设计与开发能力。 ####
|
27天前
|
Java 测试技术 API
详解Swagger:Spring Boot中的API文档生成与测试工具
详解Swagger:Spring Boot中的API文档生成与测试工具
36 4
|
26天前
|
JSON API 开发者
深入理解RESTful API设计原则
在数字化时代,API已成为连接不同软件应用的桥梁。本文旨在探讨RESTful API设计的基本原则和最佳实践,帮助开发者构建高效、可扩展的网络服务接口。通过解析REST架构风格的核心概念,我们将了解如何设计易于理解和使用的API,同时保证其性能和安全性。
|
26天前
|
存储 缓存 API
深入理解RESTful API设计原则
在现代软件开发中,RESTful API已成为前后端分离架构下不可或缺的通信桥梁。本文旨在探讨RESTful API的核心设计原则,包括资源导向、无状态、统一接口、以及可缓存性等,并通过实例解析如何在实际应用中遵循这些原则来构建高效、可维护的API接口。我们将深入分析每个原则背后的设计理念,提供最佳实践指导,帮助开发者优化API设计,提升系统整体性能和用户体验。
25 0
|
26天前
|
安全 测试技术 API
构建高效RESTful API:后端开发的艺术与实践####
在现代软件开发的浩瀚星空中,RESTful API如同一座桥梁,连接着前端世界的绚丽多彩与后端逻辑的深邃复杂。本文旨在探讨如何精心打造一款既高效又易于维护的RESTful API,通过深入浅出的方式,剖析其设计原则、实现技巧及最佳实践,为后端开发者提供一份实用的指南。我们不深入晦涩的理论,只聚焦于那些能够即刻提升API品质与开发效率的关键点,让你的API在众多服务中脱颖而出。 ####
32 0