springboot返回统一数据格式及其原理浅析

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了springboot返回统一数据格式及其原理浅析相关的知识,希望对你有一定的参考价值。

大家都知道,前后分离之后,后端响应最好以统一的格式的响应.

譬如:

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

 

 

 

 

 

话不多说,直接上代码

 

1. 定义一个统一响应结果类CommonResult<T>

import lombok.Data;

@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;
    }
}

 

2. 自定义一个ResponseBodyAdvice类

import org.springframework.context.annotation.Configuration;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
import qinfeng.zheng.data.common.CommonResult;

@EnableWebMvc
@Configuration
@RestControllerAdvice(basePackages = "qinfeng.zheng.data.api")
public class CommonResultResponseAdvice implements ResponseBodyAdvice<Object> {
    @Override
    public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
        return true;
    }

    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
       Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) { if (body instanceof CommonResult) { return body; } return new CommonResult<>(body); } }

 

3. 因为springmvc默认的message converter是Jackson框架,这个框架有点渣, 所以我们改成Fastjson

import com.alibaba.fastjson.serializer.SerializerFeature;
import com.alibaba.fastjson.support.config.FastJsonConfig;
import com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;

@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        FastJsonHttpMessageConverter fastJsonConverter = new FastJsonHttpMessageConverter();
        FastJsonConfig config = new FastJsonConfig();
        config.setCharset(Charset.forName("UTF-8"));
        config.setDateFormat("yyyyMMdd HH:mm:ssS");
        //设置允许返回为null的属性
        config.setSerializerFeatures(SerializerFeature.WriteMapNullValue);
        fastJsonConverter.setFastJsonConfig(config);
        List<MediaType> list = new ArrayList<>();
        list.add(MediaType.APPLICATION_JSON_UTF8);
        fastJsonConverter.setSupportedMediaTypes(list);
        converters.add(fastJsonConverter);
    }

}

 

4. 写一个Vo类和一个Controller类做测试,但是需要注解Controller类的包路径一定要与RestControllerAdvice注解中定义的package路径一致

import lombok.Data;
import lombok.experimental.Accessors;

@Accessors(chain = true)
@Data(staticConstructor = "of")
public class UserVo {
    private Integer id;
    private String name;
    private Integer age;

}

 

package qinfeng.zheng.data.api;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import qinfeng.zheng.data.entity.UserVo;

import java.util.ArrayList;
import java.util.List;

@RestController
public class TestController {
    @GetMapping("/index")
    public String index() {
        return "index";
    }

    @GetMapping("/list")
    public List<UserVo> list() {
        List<UserVo> list = new ArrayList<>();
        UserVo userVo1 = UserVo.of().setId(1).setName("admin").setAge(12);
        UserVo userVo2 = UserVo.of().setId(2).setName("root").setAge(100);
        list.add(userVo1);
        list.add(userVo2);
        return list;
    }

    @GetMapping("/entity")
    public UserVo entity() {
        UserVo userVo1 = UserVo.of().setId(1).setName("admin").setAge(12);
        return userVo1;
    }


    @GetMapping("/responseEntity")
    public ResponseEntity responseEntity() {
        return new ResponseEntity(UserVo.of().setId(1).setName("大象").setAge(18), HttpStatus.OK);
    }
}

 

5. 启动springboot项目,测试即可

 

 

 

 

测试应该是没有问题.,现在来看看源码,先看ResponseBodyAdvice接口

/**
 * Allows customizing the response after the execution of an {@code @ResponseBody}
 * or a {@code ResponseEntity} controller method but before the body is written
 * with an {@code HttpMessageConverter}.
 *
 * <p>Implementations may be registered directly with
 * {@code RequestMappingHandlerAdapter} and {@code ExceptionHandlerExceptionResolver}
 * or more likely annotated with {@code @ControllerAdvice} in which case they
 * will be auto-detected by both.
 *
 * @author Rossen Stoyanchev
 * @since 4.1
 * @param <T> the body type
 */
public interface ResponseBodyAdvice<T> {

    /**
     * Whether this component supports the given controller method return type
     * and the selected {@code HttpMessageConverter} type.
     * @param returnType the return type
     * @param converterType the selected converter type
     * @return {@code true} if {@link #beforeBodyWrite} should be invoked;
     * {@code false} otherwise
     */
    boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType);

    /**
     * Invoked after an {@code HttpMessageConverter} is selected and just before
     * its write method is invoked.
     * @param body the body to be written
     * @param returnType the return type of the controller method
     * @param selectedContentType the content type selected through content negotiation
     * @param selectedConverterType the converter type selected to write to the response
     * @param request the current request
     * @param response the current response
     * @return the body that was passed in or a modified (possibly new) instance
     */
    @Nullable
    T beforeBodyWrite(@Nullable T body, MethodParameter returnType, MediaType selectedContentType,
            Class<? extends HttpMessageConverter<?>> selectedConverterType,
            ServerHttpRequest request, ServerHttpResponse response);

}

 

注释说的很明白,该接口方法是在Controller方法执行之后,  且HttpMessageConverter执行之前调用,所以,我们完全可以在这儿对Controller方法返回的结果进行统一格式处理,然后再说消息转换器进行转换,响应到视图层.

 

下面再来分析一下 CommonResultResponseAdvice 类是如何加载到spring的上下文环境中的.

通过@EnableWebMvc注解 ---->  DelegatingWebMvcConfiguration.class  ---->  WebMvcConfigurationSupport.class    

WebMvcConfigurationSupport这个类就很关键了......

在这个类中,有这样一段代码

    /**
     * Returns a {@link RequestMappingHandlerAdapter} for processing requests
     * through annotated controller methods. Consider overriding one of these
     * other more fine-grained methods:
     * <ul>
     * <li>{@link #addArgumentResolvers} for adding custom argument resolvers.
     * <li>{@link #addReturnValueHandlers} for adding custom return value handlers.
     * <li>{@link #configureMessageConverters} for adding custom message converters.
     * </ul>
     */
    @Bean
    public RequestMappingHandlerAdapter requestMappingHandlerAdapter(
            @Qualifier("mvcContentNegotiationManager") ContentNegotiationManager contentNegotiationManager,
            @Qualifier("mvcConversionService") FormattingConversionService conversionService,
            @Qualifier("mvcValidator") Validator validator) {

        RequestMappingHandlerAdapter adapter = createRequestMappingHandlerAdapter();
        adapter.setContentNegotiationManager(contentNegotiationManager);
        adapter.setMessageConverters(getMessageConverters());
        adapter.setWebBindingInitializer(getConfigurableWebBindingInitializer(conversionService, validator));
        adapter.setCustomArgumentResolvers(getArgumentResolvers());
        adapter.setCustomReturnValueHandlers(getReturnValueHandlers());

        if (jackson2Present) {
            adapter.setRequestBodyAdvice(Collections.singletonList(new JsonViewRequestBodyAdvice()));
            adapter.setResponseBodyAdvice(Collections.singletonList(new JsonViewResponseBodyAdvice()));
        }

        AsyncSupportConfigurer configurer = new AsyncSupportConfigurer();
        configureAsyncSupport(configurer);
        if (configurer.getTaskExecutor() != null) {
            adapter.setTaskExecutor(configurer.getTaskExecutor());
        }
        if (configurer.getTimeout() != null) {
            adapter.setAsyncRequestTimeout(configurer.getTimeout());
        }
        adapter.setCallableInterceptors(configurer.getCallableInterceptors());
        adapter.setDeferredResultInterceptors(configurer.getDeferredResultInterceptors());

        return adapter;
    }

 

其它都无所谓,至少我们知道这儿会将 RequestMappingHandlerAdapter注入到spring的上下文环境中去.  而RequestMappingHandlerAdapter又是 InitializingBean 接口的一个实现. 看到InitializingBean 接口我们肯定会看看它的实现方法 afterPropertiesSet, 不巧的是RequestMappingHandlerAdapter真的在afterPropertiesSet方法中干了不少的事.

 

    @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方法, 就会看到下面这段代码 

    public static List<ControllerAdviceBean> findAnnotatedBeans(ApplicationContext context) {
        List<ControllerAdviceBean> adviceBeans = new ArrayList<>();
        for (String name : BeanFactoryUtils.beanNamesForTypeIncludingAncestors(context, Object.class)) {
            if (!ScopedProxyUtils.isScopedTarget(name)) {
                ControllerAdvice controllerAdvice = context.findAnnotationOnBean(name, ControllerAdvice.class);
                if (controllerAdvice != null) {
                    // Use the @ControllerAdvice annotation found by findAnnotationOnBean()
                    // in order to avoid a subsequent lookup of the same annotation.
                    adviceBeans.add(new ControllerAdviceBean(name, context, controllerAdvice));
                }
            }
        }
        OrderComparator.sort(adviceBeans);
        return adviceBeans;
    }

 

 

好吧,前面这一坨其实都是对数据格式的统一的封装, 而如何将java bean转成json格式数据,这块功能其实都是靠HttpMessageConverter来实现的.

以上是关于springboot返回统一数据格式及其原理浅析的主要内容,如果未能解决你的问题,请参考以下文章

springboot统一返回json数据格式并配置系统异常拦截

springboot处理统一返回json数据格式

Spring Boot 统一功能处理(用户登录权限效验-拦截器异常处理数据格式返回)

SpringBoot入门系列如何返回统一的数据格式

springboot定义统一的返回异常提示数据格式

SpringBoot RESTful API返回统一数据格式还不懂?