美文网首页
Spring Boot - 统一数据下发接口格式

Spring Boot - 统一数据下发接口格式

作者: Whyn | 来源:发表于2020-09-08 23:15 被阅读0次

    [TOC]

    前言

    当前主流的 Web 应用开发通常采用前后端分离模式,前端和后端各自独立开发,然后通过数据接口沟通前后端,完成项目。

    因此,定义一个统一的数据下发格式,有利于提高项目开发效率,减少各端开发沟通成本。

    本篇博文主要介绍下在 Spring Boot 中配置统一数据下发格式的搭建步骤。

    统一数据格式

    数据的类型多种多样,但是可以简单划分为以下三种类型:

    • 简单数据类型:比如byteintdouble等基本数据类型。
      :在 Java 中,String属于Object类型,但是在数据层面上,我们通常将其看作是简单数据类型。

    • 对象数据类型:常见的比如说自定义 Java Bean,POJO 等数据。

    • 复杂/集合数据类型:比如ListMap等集合类型。

    后端下发的数据肯定会包含上述列举的三种类型数据,通常这些数据都作为响应体主要内容,用字段data进行表示,同时我们会附加codemsg字段来描述请求结果信息,如下表所示:

    字段 描述
    code 状态码,标志请求是否成功
    msg 描述请求状态
    data 返回结果

    到此,统一数据下发的格式就确定了,如下代码所示:

    @Getter
    @AllArgsConstructor
    @ToString
    public class ResponseBean<T> {
        private int code;
        private String msg;
        private T data;
    }
    

    此时,数据下发操作如下所示:

    @RestController
    @RequestMapping("/common")
    public class CommonController {
    
        @GetMapping("/")
        public ResponseBean<String> index() {
            return new ResponseBean<>(200, "操作成功", "Hello World");
        }
    }
    

    进阶配置

    在上文的统一数据ResponseBean中,还可以对其再进行封装,使代码更健壮:

    • 抽象codemsgcodemsg用于描述请求结果信息,直接放置再ResponseBean中,程序员可以随便设置这两个字段,请求结果一般就是成功、失败等常见的几种结果,可以将其再进行封装,提供常见的请求结果信息,缩小权限:

      @Getter
      @ToString
      public class ResponseBean<T> {
          private int code;
          private String msg;
          private T data;
      
          public ResponseBean(ResultCode result, T data) {
              this.code = result.code;
              this.msg = result.msg;
              this.data = data;
          }
      
          public static enum ResultCode {
              SUCCESS(200, "操作成功"),
              FAILURE(400, "操作失败");
      
              ResultCode(int code, String msg) {
                  this.code = code;
                  this.msg = msg;
              }
      
              private int code;
              private String msg;
          }
      }
      

      这里使用enum来封装codemsg,并提供两个默认操作SUCCESSFAILURE。此时调用方法如下:

      @GetMapping("/")
      public ResponseBean<String> index() {
          return new ResponseBean<>(ResponseBean.ResultCode.SUCCESS, "Hello World");
      }
      
    • 提供默认操作:前面的调用方法还是不太简洁,这里我们让ResponseBean直接提供相应的默认操作,方便外部调用:

      @Getter
      @ToString
      public class ResponseBean<T> {
          private int code;
          private String msg;
          private T data;
      
          // 成功操作
          public static <E> ResponseBean<E> success(E data) {
              return new ResponseBean<E>(ResultCode.SUCCESS, data);
          }
      
          // 失败操作
          public static <E> ResponseBean<E> failure(E data) {
              return new ResponseBean<E>(ResultCode.FAILURE, data);
          }
      
          // 设置为 private
          private ResponseBean(ResultCode result, T data) {
              this.code = result.code;
              this.msg = result.msg;
              this.data = data;
          }
      
          // 设置 private
          private static enum ResultCode {
              SUCCESS(200, "操作成功"),
              FAILURE(400, "操作失败");
      
              ResultCode(int code, String msg) {
                  this.code = code;
                  this.msg = msg;
              }
      
              private int code;
              private String msg;
          }
      }
      

      我们提供了两个默认操作successfailure,此时调用方式如下:

      @GetMapping("/")
      public ResponseBean<String> index() {
          return ResponseBean.<String>success("Hello World");
      }
      

      到这里,数据下发调用方式就相对较简洁了,但是结合 Spring Boot 还能继续进行优化,参考下文。

    数据下发拦截修改

    Spring 框架提供了一个接口:ResponseBodyAdvice<T>,当控制器方法被@ResponseBody注解或返回一个ResponseEntity时,该接口允许我们在HttpMessageConverter写入响应体前,拦截响应体并进行自定义修改。

    因此,要拦截Controller响应数据,只需实现一个自定义ResponseBodyAdvice,并将其注册到RequestMappingHandlerAdapterExceptionHandlerExceptionResolver,或者直接使用@ControllerAdvice注解进行激活。如下所示:

    @RestControllerAdvice
    public class FormatResponseBodyAdvice implements ResponseBodyAdvice<Object> {
        /**
         * @param returnType 响应的数据类型
         * @param converterType 最终将会使用的消息转换器
         * @return true: 执行 beforeBodyWrite 方法,修改响应体
                   false: 不执行 beforeBodyWrite 方法
         */
        @Override
        public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
            boolean isResponseBeanType = ResponseBean.class.equals(returnType.getParameterType());
            // 如果返回的是 ResponseBean 类型,则无需进行拦截修改,直接返回即可
            // 其他类型则拦截,并进行 beforeBodyWrite 方法进行修改
            return !isResponseBeanType;
        }
    
        /**
         * @param body 响应的数据,也就是响应体
         * @param returnType 响应的数据类型
         * @param selectedContentType 响应的ContentType
         * @param selectedConverterType 最终将会使用的消息转换器
         * @param request
         * @param response
         * @return 被修改后的响应体,可以为null,表示没有任何响应
         */
        @Override
        public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
            return ResponseBean.success(body);
        }
    }
    

    这里需要注意的一个点是,仅仅实现一个自定义ResponseBodyAdvice,对其他类型的数据是可以成功进行拦截并转换,但是对于直接返回String类型的方法,这里会抛出一个异常:

    java.lang.ClassCastException: class com.yn.common.entity.ResponseBean cannot be cast to class java.lang.String

    这是因为请求体在返回给客户端前,会被一系列HttpMessageConverter进行转换,当Controller返回一个String时,beforeBodyWrite方法中的第四个参数selectedConverterType就是一个StringHttpMessageConverter,因此,我们在beforeBodyWrite中将String响应拦截并转换为ResponseBean类型,然后StringHttpMessageConverter就会转换我们的ResponseBean类型,这样转换就会失败,因为类型不匹配。解决这个问题的方法大致有如下三种,任选其一即可:

    1. 转换为String类型:由于采用的是StringHttpMessageConverter,因此,我们需要将ResponseBean转换为String,这样StringHttpMessageConverter就可以处理了:

      @RestControllerAdvice
      public class GlobalExceptionHandler implements ResponseBodyAdvice<Object> {
      
          @Override
          public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
              ResponseBean bean = ResponseBean.success(body);
              try {
                  if (body instanceof String) {
                      response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
                      // String 类型则将 bean 转化为 JSON 字符串
                      return new ObjectMapper().writeValueAsString(bean);
                  }
              } catch (JsonProcessingException e) {
                  e.printStackTrace();
              }
              return bean;
          }
      }
      
    2. 前置 JSON 转换器:能转换我们自定义的ResponseBean应当是一个 JSON 转换器,比如MappingJackson2HttpMessageConverter,因此,这里我们可以配置一下,让MappingJackson2HttpMessageConverter转换器优先级比StringHttpMessageConverter高,这样转换就能成功,如下所示:

      @Configuration
      @EnableWebMvc
      public class WebConfiguration implements WebMvcConfigurer {
      
          @Override
          public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
              converters.add(0, new MappingJackson2HttpMessageConverter());
          }
      }
      

      其实就是在转换器集合中将MappingJackson2HttpMessageConverter排列到StringHttpMessageConverter前面。

    3. 配置 JSON 转换器:如果是 Spring Boot 项目时,通常不建议在配置类上使用@EnableWebMvc注解,因为该注解会失效 Spring Boot 自动加载 SpringMVC 默认配置,这样所有的配置都需要程序员手动进行控制,会很麻烦。大多数配置 Spring Boot 都提供了对应的配置方法,比如,我们可以配置HttpMessageConverter,去除StringHttpMessageConverter等默认填充的转换器,只注入 JSON 转换器即可(因为前后端分离项目,只需 JSON 转换即可):

      @SpringBootApplication
      public class Application {
      
          @Bean
          public HttpMessageConverters converters() {
              return new HttpMessageConverters(
                      false, Arrays.asList(new MappingJackson2HttpMessageConverter()));
          }
      }
      

    现在,Controller可以直接返回任意类型数据,最终都会被ResponseBodyAdvice拦截并更改为ResponseBean类型,如下所示:

    @RestController
    @RequestMapping("/common")
    public class CommonController {
    
        // 简单类型
        @GetMapping("/basic")
        public int basic() {
            return 3;
        }
    
        // 字符串
        @GetMapping("/string")
        public String basicType() {
            return "Hello World";
        }
    
        // 对象类型
        @GetMapping("/obj")
        public User user() {
            return new User("Whyn", "whyncai@gmail.com");
        }
    
        // 复杂/集合类型
        @GetMapping("/complex")
        public List<User> users() {
            return Arrays.asList(
                    new User("Why1n", "Why1n@qq.com"),
                    new User("Why1n", "Why1n@qq.com")
            );
        }
    
        @Data
        @AllArgsConstructor
        private static class User {
            private String name;
            private String email;
        }
    
    }
    

    请求上述接口,结果如下:

    $ curl -X GET localhost:8080/common/basic
    {"code":200,"msg":"操作成功","data":3}
    
    $ curl -X GET localhost:8080/common/string
    {"code":200,"msg":"操作成功","data":"Hello World"}
    
    $ curl -X GET localhost:8080/common/obj
    {"code":200,"msg":"操作成功","data":{"name":"Whyn","email":"whyncai@gmail.com"}}
    
    $ curl -X GET localhost:8080/common/complex
    {"code":200,"msg":"操作成功","data":[{"name":"Why1n","email":"Why1n@qq.com"},{"name":"Why1n","email":"Why1n@qq.com"}]}
    

    最后,当Controller抛出异常时,异常信息也会被我们自定义的RestControllerAdvice拦截到,但是data字段是系统的异常信息,因此最好还是手动对全局异常进行捕获,比如:

    @RestControllerAdvice
    public class FormatResponseBodyAdvice implements ResponseBodyAdvice<Object> {
        @Override
        public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
            boolean isResponseBeanType = ResponseBean.class.equals(returnType.getParameterType());
            // 如果返回的是 ResponseBean 类型,则无需进行拦截修改,直接返回即可
            // 其他类型则拦截,并进行 beforeBodyWrite 方法进行修改
            return !isResponseBeanType;
        }
        //...
        @ExceptionHandler(Throwable.class)
        @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
        public ResponseBean<String> handleException() {
            return ResponseBean.failure("Error occured");
        }
    }
    

    刚好ResponseBodyAdvice需要@RestControllerAdvice进行驱动,而@RestControllerAdvice又能全局捕获Controller异常,所以这里简单地将异常捕获放置到自定义ResponseBodyAdvice中,一个需要注意的点就是:这里我们对异常手动返回ResponseBean对象,因为在自定义ResponseBodyAdvice中,supports方法内我们设置了对ResponseBean数据类型不进行拦截,而如果这里异常处理返回其他类型,最终都都会被自定义ResponseBodyAdvice拦截到,这里需要注意一下。

    更多异常处理详情,可查看本人的另一篇博客:Spring Boot - 全局异常捕获

    附录

    上述内容的完整配置代码如下所示:

    • 数据统一下发实体
      @Getter
      @ToString
      public class ResponseBean<T> {
          private int code;
          private String msg;
          private T data;
      
          // 成功操作
          public static <E> ResponseBean<E> success(E data) {
              return new ResponseBean<E>(ResultCode.SUCCESS, data);
          }
      
          // 失败操作
          public static <E> ResponseBean<E> failure(E data) {
              return new ResponseBean<E>(ResultCode.FAILURE, data);
          }
      
          // 设置为 private
          private ResponseBean(ResultCode result, T data) {
              this.code = result.code;
              this.msg = result.msg;
              this.data = data;
          }
      
          // 设置 private
          private static enum ResultCode {
              SUCCESS(200, "操作成功"),
              FAILURE(400, "操作失败");
      
              ResultCode(int code, String msg) {
                  this.code = code;
                  this.msg = msg;
              }
      
              private int code;
              private String msg;
          }
      }
      
    • 转换器配置类
      @Configuration
      @EnableWebMvc
      public class WebConfiguration implements WebMvcConfigurer {
      
          @Override
          public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
              converters.add(0, new MappingJackson2HttpMessageConverter());
          }
      }
      
    • 数据下发拦截器
      @RestControllerAdvice
      public class FormatResponseBodyAdvice implements ResponseBodyAdvice<Object> {
      
          @Override
          public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
              boolean isResponseBeanType = ResponseBean.class.equals(returnType.getParameterType());
              // 如果返回的是 ResponseBean 类型,则无需进行拦截修改,直接返回即可
              // 其他类型则拦截,并进行 beforeBodyWrite 方法进行修改
              return !isResponseBeanType;
          }
      
          @Override
          public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
              return ResponseBean.success(body);
          }
      
          @ExceptionHandler(Throwable.class)
          @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
          public ResponseBean<String> handleException() {
              return ResponseBean.failure("Error occured");
          }
      }
      

    参考

    相关文章

      网友评论

          本文标题:Spring Boot - 统一数据下发接口格式

          本文链接:https://www.haomeiwen.com/subject/modjektx.html