• 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来实现的.

  • 相关阅读:
    web 单例 多例
    python socket客户端
    foy: 轻量级的基于 nodejs 的通用 build 工具
    Hydux: 一个 Elm-like 的 全功能的 Redux 替代品
    AlarmManager使用注意事项
    【转】android ListView 几个重要属性
    自己写的小工具软件集合
    win8.1 cygwin编译java轻量虚拟机avian
    android 图片缩放抗锯齿
    windows phone和android,ios的touch事件兼容
  • 原文地址:https://www.cnblogs.com/z-qinfeng/p/12189877.html
Copyright © 2020-2023  润新知