[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 int code; private 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 int code; private 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 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"); } }
网友评论