[TOC]
前言
当前主流的 Web 应用开发通常采用前后端分离模式,前端和后端各自独立开发,然后通过数据接口沟通前后端,完成项目。
因此,定义一个统一的数据下发格式,有利于提高项目开发效率,减少各端开发沟通成本。
本篇博文主要介绍下在 Spring Boot 中配置统一数据下发格式的搭建步骤。
统一数据格式
数据的类型多种多样,但是可以简单划分为以下三种类型:
简单数据类型:比如
byte
、int
、double
等基本数据类型。
注:在 Java 中,String
属于Object
类型,但是在数据层面上,我们通常将其看作是简单数据类型。对象数据类型:常见的比如说自定义 Java Bean,POJO 等数据。
复杂/集合数据类型:比如
List
、Map
等集合类型。
后端下发的数据肯定会包含上述列举的三种类型数据,通常这些数据都作为响应体主要内容,用字段data
进行表示,同时我们会附加code
和msg
字段来描述请求结果信息,如下表所示:
字段 | 描述 |
---|---|
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
中,还可以对其再进行封装,使代码更健壮:
-
抽象
code
和msg
:code
和msg
用于描述请求结果信息,直接放置再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
来封装code
和msg
,并提供两个默认操作SUCCESS
和FAILURE
。此时调用方法如下:@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; } } 我们提供了两个默认操作
success
和failure
,此时调用方式如下:@GetMapping("/") public ResponseBean
index() { return ResponseBean. success("Hello World"); } 到这里,数据下发调用方式就相对较简洁了,但是结合 Spring Boot 还能继续进行优化,参考下文。
数据下发拦截修改
Spring 框架提供了一个接口:ResponseBodyAdvice
,当控制器方法被@ResponseBody
注解或返回一个ResponseEntity
时,该接口允许我们在HttpMessageConverter
写入响应体前,拦截响应体并进行自定义修改。
因此,要拦截Controller
响应数据,只需实现一个自定义ResponseBodyAdvice
,并将其注册到RequestMappingHandlerAdapter
和ExceptionHandlerExceptionResolver
,或者直接使用@ControllerAdvice
注解进行激活。如下所示:
@RestControllerAdvice
public class FormatResponseBodyAdvice implements ResponseBodyAdvice
这里需要注意的一个点是,仅仅实现一个自定义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
类型,这样转换就会失败,因为类型不匹配。解决这个问题的方法大致有如下三种,任选其一即可:
-
转换为
String
类型:由于采用的是StringHttpMessageConverter
,因此,我们需要将ResponseBean
转换为String
,这样StringHttpMessageConverter
就可以处理了:@RestControllerAdvice public class GlobalExceptionHandler implements ResponseBodyAdvice
-
前置 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
前面。 -
配置 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", "[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
刚好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
参考
- SpringBoot 中统一包装响应
- 【项目实践】SpringBoot三招组合拳,手把手教你打出优雅的后端接口