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

[TOC]

前言

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

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

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

统一数据格式

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

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

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

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

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

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

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

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

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

@RestController
@RequestMapping("/common")
public class CommonController {

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

进阶配置

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

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

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

    @Getter
    @ToString
    public class ResponseBean {
        private int code;
        private String msg;
        private T data;
    
        // 成功操作
        public static  ResponseBean success(E data) {
            return new ResponseBean(ResultCode.SUCCESS, data);
        }
    
        // 失败操作
        public static  ResponseBean failure(E data) {
            return new ResponseBean(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 index() {
        return ResponseBean.success("Hello World");
    }
    

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

数据下发拦截修改

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

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

@RestControllerAdvice
public class FormatResponseBodyAdvice implements ResponseBodyAdvice {
    /**
     * @param returnType 响应的数据类型
     * @param converterType 最终将会使用的消息转换器
     * @return true: 执行 beforeBodyWrite 方法,修改响应体
               false: 不执行 beforeBodyWrite 方法
     */
    @Override
    public boolean supports(MethodParameter returnType, Class> 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> 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 {
    
        @Override
        public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class> 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> 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()));
        }
    }
    
  4. 现在,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", "[email protected]");
        }
    
        // 复杂/集合类型
        @GetMapping("/complex")
        public List users() {
            return Arrays.asList(
                    new User("Why1n", "[email protected]"),
                    new User("Why1n", "[email protected]")
            );
        }
    
        @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":"[email protected]"}}
    
    $ curl -X GET localhost:8080/common/complex
    {"code":200,"msg":"操作成功","data":[{"name":"Why1n","email":"[email protected]"},{"name":"Why1n","email":"[email protected]"}]}
    

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

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

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

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

    附录

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

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

      参考

      • SpringBoot 中统一包装响应
      • 【项目实践】SpringBoot三招组合拳,手把手教你打出优雅的后端接口

      你可能感兴趣的:(Spring Boot - 统一数据下发接口格式)