美文网首页
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