[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<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中,还可以对其再进行封装,使代码更健壮:
-
抽象
code和msg:code和msg用于描述请求结果信息,直接放置再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 final int code; private final String msg; } }这里使用
enum来封装code和msg,并提供两个默认操作SUCCESS和FAILURE。此时调用方法如下:@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 final int code; private final String msg; } }我们提供了两个默认操作
success和failure,此时调用方式如下:@GetMapping("/") public ResponseBean<String> index() { return ResponseBean.<String>success("Hello World"); }到这里,数据下发调用方式就相对较简洁了,但是结合 Spring Boot 还能继续进行优化,参考下文。
数据下发拦截修改
Spring 框架提供了一个接口:ResponseBodyAdvice<T>,当控制器方法被@ResponseBody注解或返回一个ResponseEntity时,该接口允许我们在HttpMessageConverter写入响应体前,拦截响应体并进行自定义修改。
因此,要拦截Controller响应数据,只需实现一个自定义ResponseBodyAdvice,并将其注册到RequestMappingHandlerAdapter和ExceptionHandlerExceptionResolver,或者直接使用@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类型,这样转换就会失败,因为类型不匹配。解决这个问题的方法大致有如下三种,任选其一即可:
-
转换为
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; } } -
前置 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前面。 -
配置 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 final int code; private final 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"); } }
