[TOC]
前言
當(dāng)前主流的 Web 應(yīng)用開發(fā)通常采用前后端分離模式丰涉,前端和后端各自獨立開發(fā)拓巧,然后通過數(shù)據(jù)接口溝通前后端,完成項目一死。
因此肛度,定義一個統(tǒng)一的數(shù)據(jù)下發(fā)格式,有利于提高項目開發(fā)效率投慈,減少各端開發(fā)溝通成本承耿。
本篇博文主要介紹下在 Spring Boot 中配置統(tǒng)一數(shù)據(jù)下發(fā)格式的搭建步驟。
統(tǒng)一數(shù)據(jù)格式
數(shù)據(jù)的類型多種多樣伪煤,但是可以簡單劃分為以下三種類型:
簡單數(shù)據(jù)類型:比如
byte
加袋、int
、double
等基本數(shù)據(jù)類型抱既。
注:在 Java 中职烧,String
屬于Object
類型,但是在數(shù)據(jù)層面上防泵,我們通常將其看作是簡單數(shù)據(jù)類型蚀之。對象數(shù)據(jù)類型:常見的比如說自定義 Java Bean,POJO 等數(shù)據(jù)捷泞。
復(fù)雜/集合數(shù)據(jù)類型:比如
List
足删、Map
等集合類型。
后端下發(fā)的數(shù)據(jù)肯定會包含上述列舉的三種類型數(shù)據(jù)肚邢,通常這些數(shù)據(jù)都作為響應(yīng)體主要內(nèi)容壹堰,用字段data
進行表示拭卿,同時我們會附加code
和msg
字段來描述請求結(jié)果信息,如下表所示:
字段 | 描述 |
---|---|
code |
狀態(tài)碼贱纠,標(biāo)志請求是否成功 |
msg |
描述請求狀態(tài) |
data |
返回結(jié)果 |
到此峻厚,統(tǒng)一數(shù)據(jù)下發(fā)的格式就確定了,如下代碼所示:
@Getter
@AllArgsConstructor
@ToString
public class ResponseBean<T> {
private int code;
private String msg;
private T data;
}
此時谆焊,數(shù)據(jù)下發(fā)操作如下所示:
@RestController
@RequestMapping("/common")
public class CommonController {
@GetMapping("/")
public ResponseBean<String> index() {
return new ResponseBean<>(200, "操作成功", "Hello World");
}
}
進階配置
在上文的統(tǒng)一數(shù)據(jù)ResponseBean
中惠桃,還可以對其再進行封裝,使代碼更健壯:
-
抽象
code
和msg
:code
和msg
用于描述請求結(jié)果信息辖试,直接放置再ResponseBean
中辜王,程序員可以隨便設(shè)置這兩個字段,請求結(jié)果一般就是成功罐孝、失敗等常見的幾種結(jié)果呐馆,可以將其再進行封裝,提供常見的請求結(jié)果信息莲兢,縮小權(quán)限:@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
汹来,并提供兩個默認(rèn)操作SUCCESS
和FAILURE
。此時調(diào)用方法如下:@GetMapping("/") public ResponseBean<String> index() { return new ResponseBean<>(ResponseBean.ResultCode.SUCCESS, "Hello World"); }
-
提供默認(rèn)操作:前面的調(diào)用方法還是不太簡潔改艇,這里我們讓
ResponseBean
直接提供相應(yīng)的默認(rèn)操作收班,方便外部調(diào)用:@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); } // 設(shè)置為 private private ResponseBean(ResultCode result, T data) { this.code = result.code; this.msg = result.msg; this.data = data; } // 設(shè)置 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; } }
我們提供了兩個默認(rèn)操作
success
和failure
,此時調(diào)用方式如下:@GetMapping("/") public ResponseBean<String> index() { return ResponseBean.<String>success("Hello World"); }
到這里谒兄,數(shù)據(jù)下發(fā)調(diào)用方式就相對較簡潔了摔桦,但是結(jié)合 Spring Boot 還能繼續(xù)進行優(yōu)化,參考下文承疲。
數(shù)據(jù)下發(fā)攔截修改
Spring 框架提供了一個接口:ResponseBodyAdvice<T>
邻耕,當(dāng)控制器方法被@ResponseBody
注解或返回一個ResponseEntity
時,該接口允許我們在HttpMessageConverter
寫入響應(yīng)體前纪隙,攔截響應(yīng)體并進行自定義修改赊豌。
因此,要攔截Controller
響應(yīng)數(shù)據(jù)绵咱,只需實現(xiàn)一個自定義ResponseBodyAdvice
,并將其注冊到RequestMappingHandlerAdapter
和ExceptionHandlerExceptionResolver
熙兔,或者直接使用@ControllerAdvice
注解進行激活悲伶。如下所示:
@RestControllerAdvice
public class FormatResponseBodyAdvice implements ResponseBodyAdvice<Object> {
/**
* @param returnType 響應(yīng)的數(shù)據(jù)類型
* @param converterType 最終將會使用的消息轉(zhuǎn)換器
* @return true: 執(zhí)行 beforeBodyWrite 方法,修改響應(yīng)體
false: 不執(zhí)行 beforeBodyWrite 方法
*/
@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
boolean isResponseBeanType = ResponseBean.class.equals(returnType.getParameterType());
// 如果返回的是 ResponseBean 類型住涉,則無需進行攔截修改麸锉,直接返回即可
// 其他類型則攔截,并進行 beforeBodyWrite 方法進行修改
return !isResponseBeanType;
}
/**
* @param body 響應(yīng)的數(shù)據(jù)舆声,也就是響應(yīng)體
* @param returnType 響應(yīng)的數(shù)據(jù)類型
* @param selectedContentType 響應(yīng)的ContentType
* @param selectedConverterType 最終將會使用的消息轉(zhuǎn)換器
* @param request
* @param response
* @return 被修改后的響應(yīng)體花沉,可以為null柳爽,表示沒有任何響應(yīng)
*/
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
return ResponseBean.success(body);
}
}
這里需要注意的一個點是,僅僅實現(xiàn)一個自定義ResponseBodyAdvice
碱屁,對其他類型的數(shù)據(jù)是可以成功進行攔截并轉(zhuǎn)換磷脯,但是對于直接返回String
類型的方法,這里會拋出一個異常:
java.lang.ClassCastException: class com.yn.common.entity.ResponseBean cannot be cast to class java.lang.String
這是因為請求體在返回給客戶端前娩脾,會被一系列HttpMessageConverter
進行轉(zhuǎn)換赵誓,當(dāng)Controller
返回一個String
時,beforeBodyWrite
方法中的第四個參數(shù)selectedConverterType
就是一個StringHttpMessageConverter
柿赊,因此俩功,我們在beforeBodyWrite
中將String
響應(yīng)攔截并轉(zhuǎn)換為ResponseBean
類型,然后StringHttpMessageConverter
就會轉(zhuǎn)換我們的ResponseBean
類型碰声,這樣轉(zhuǎn)換就會失敗诡蜓,因為類型不匹配。解決這個問題的方法大致有如下三種胰挑,任選其一即可:
-
轉(zhuǎn)換為
String
類型:由于采用的是StringHttpMessageConverter
万牺,因此,我們需要將ResponseBean
轉(zhuǎn)換為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 轉(zhuǎn)化為 JSON 字符串 return new ObjectMapper().writeValueAsString(bean); } } catch (JsonProcessingException e) { e.printStackTrace(); } return bean; } }
-
前置 JSON 轉(zhuǎn)換器:能轉(zhuǎn)換我們自定義的
ResponseBean
應(yīng)當(dāng)是一個 JSON 轉(zhuǎn)換器脚粟,比如MappingJackson2HttpMessageConverter
,因此蘸朋,這里我們可以配置一下核无,讓MappingJackson2HttpMessageConverter
轉(zhuǎn)換器優(yōu)先級比StringHttpMessageConverter
高,這樣轉(zhuǎn)換就能成功藕坯,如下所示:@Configuration @EnableWebMvc public class WebConfiguration implements WebMvcConfigurer { @Override public void configureMessageConverters(List<HttpMessageConverter<?>> converters) { converters.add(0, new MappingJackson2HttpMessageConverter()); } }
其實就是在轉(zhuǎn)換器集合中將
MappingJackson2HttpMessageConverter
排列到StringHttpMessageConverter
前面团南。 -
配置 JSON 轉(zhuǎn)換器:如果是 Spring Boot 項目時,通常不建議在配置類上使用
@EnableWebMvc
注解炼彪,因為該注解會失效 Spring Boot 自動加載 SpringMVC 默認(rèn)配置吐根,這樣所有的配置都需要程序員手動進行控制,會很麻煩辐马。大多數(shù)配置 Spring Boot 都提供了對應(yīng)的配置方法拷橘,比如,我們可以配置HttpMessageConverter
喜爷,去除StringHttpMessageConverter
等默認(rèn)填充的轉(zhuǎn)換器冗疮,只注入 JSON 轉(zhuǎn)換器即可(因為前后端分離項目,只需 JSON 轉(zhuǎn)換即可):@SpringBootApplication public class Application { @Bean public HttpMessageConverters converters() { return new HttpMessageConverters( false, Arrays.asList(new MappingJackson2HttpMessageConverter())); } }
現(xiàn)在檩帐,Controller
可以直接返回任意類型數(shù)據(jù)术幔,最終都會被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");
}
// 復(fù)雜/集合類型
@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;
}
}
請求上述接口湃密,結(jié)果如下:
$ 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"}]}
最后诅挑,當(dāng)Controller
拋出異常時四敞,異常信息也會被我們自定義的RestControllerAdvice
攔截到,但是data
字段是系統(tǒng)的異常信息拔妥,因此最好還是手動對全局異常進行捕獲忿危,比如:
@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
進行驅(qū)動,而@RestControllerAdvice
又能全局捕獲Controller
異常兜畸,所以這里簡單地將異常捕獲放置到自定義ResponseBodyAdvice
中努释,一個需要注意的點就是:這里我們對異常手動返回ResponseBean
對象,因為在自定義ResponseBodyAdvice
中咬摇,supports
方法內(nèi)我們設(shè)置了對ResponseBean
數(shù)據(jù)類型不進行攔截伐蒂,而如果這里異常處理返回其他類型,最終都都會被自定義ResponseBodyAdvice
攔截到肛鹏,這里需要注意一下逸邦。
更多異常處理詳情,可查看本人的另一篇博客:Spring Boot - 全局異常捕獲
附錄
上述內(nèi)容的完整配置代碼如下所示:
-
數(shù)據(jù)統(tǒng)一下發(fā)實體:
@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); } // 設(shè)置為 private private ResponseBean(ResultCode result, T data) { this.code = result.code; this.msg = result.msg; this.data = data; } // 設(shè)置 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; } }
-
轉(zhuǎn)換器配置類:
@Configuration @EnableWebMvc public class WebConfiguration implements WebMvcConfigurer { @Override public void configureMessageConverters(List<HttpMessageConverter<?>> converters) { converters.add(0, new MappingJackson2HttpMessageConverter()); } }
-
數(shù)據(jù)下發(fā)攔截器:
@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"); } }