大家好,我是飄渺。
今天我們來聊一聊在基於SpringBoot前后端分離開發模式下,如何友好的返回統一的標准格式以及如何優雅的處理全局異常。
首先我們來看看為什么要返回統一的標准格式?
為什么要對SpringBoot返回統一的標准格式
在默認情況下,SpringBoot的返回格式常見的有三種:
第一種:返回 String
@GetMapping("/hello")
public String getStr(){
return "hello,javadaily";
}
此時調用接口獲取到的返回值是這樣:
hello,javadaily
第二種:返回自定義對象
@GetMapping("/aniaml")
public Aniaml getAniaml(){
Aniaml aniaml = new Aniaml(1,"pig");
return aniaml;
}
此時調用接口獲取到的返回值是這樣:
{
"id": 1,
"name": "pig"
}
第三種:接口異常
@GetMapping("/error")
public int error(){
int i = 9/0;
return i;
}
此時調用接口獲取到的返回值是這樣:
{
"timestamp": "2021-07-08T08:05:15.423+00:00",
"status": 500,
"error": "Internal Server Error",
"path": "/wrong"
}
基於以上種種情況,如果你和前端開發人員聯調接口她們就會很懵逼,由於我們沒有給他一個統一的格式,前端人員不知道如何處理返回值。
還有甚者,有的同學比如小張喜歡對結果進行封裝,他使用了Result對象,小王也喜歡對結果進行包裝,但是他卻使用的是Response對象,當出現這種情況時我相信前端人員一定會抓狂的。
所以我們項目中是需要定義一個統一的標准返回格式的。
定義返回標准格式
一個標准的返回格式至少包含3部分:
- status 狀態值:由后端統一定義各種返回結果的狀態碼
- message 描述:本次接口調用的結果描述
- data 數據:本次返回的數據。
{
"status":"100",
"message":"操作成功",
"data":"hello,javadaily"
}
當然也可以按需加入其他擴展值,比如我們就在返回對象中添加了接口調用時間
- timestamp: 接口調用時間
定義返回對象
@Data
public class ResultData<t> {
/** 結果狀態 ,具體狀態碼參見ResultData.java*/
private int status;
private String message;
private T data;
private long timestamp ;
public ResultData (){
this.timestamp = System.currentTimeMillis();
}
public static <t> ResultData<t> success(T data) {
ResultData<t> resultData = new ResultData<>();
resultData.setStatus(ReturnCode.RC100.getCode());
resultData.setMessage(ReturnCode.RC100.getMessage());
resultData.setData(data);
return resultData;
}
public static <t> ResultData<t> fail(int code, String message) {
ResultData<t> resultData = new ResultData<>();
resultData.setStatus(code);
resultData.setMessage(message);
return resultData;
}
}
定義狀態碼
public enum ReturnCode {
/**操作成功**/
RC100(100,"操作成功"),
/**操作失敗**/
RC999(999,"操作失敗"),
/**服務限流**/
RC200(200,"服務開啟限流保護,請稍后再試!"),
/**服務降級**/
RC201(201,"服務開啟降級保護,請稍后再試!"),
/**熱點參數限流**/
RC202(202,"熱點參數限流,請稍后再試!"),
/**系統規則不滿足**/
RC203(203,"系統規則不滿足要求,請稍后再試!"),
/**授權規則不通過**/
RC204(204,"授權規則不通過,請稍后再試!"),
/**access_denied**/
RC403(403,"無訪問權限,請聯系管理員授予權限"),
/**access_denied**/
RC401(401,"匿名用戶訪問無權限資源時的異常"),
/**服務異常**/
RC500(500,"系統異常,請稍后重試"),
INVALID_TOKEN(2001,"訪問令牌不合法"),
ACCESS_DENIED(2003,"沒有權限訪問該資源"),
CLIENT_AUTHENTICATION_FAILED(1001,"客戶端認證失敗"),
USERNAME_OR_PASSWORD_ERROR(1002,"用戶名或密碼錯誤"),
UNSUPPORTED_GRANT_TYPE(1003, "不支持的認證模式");
/**自定義狀態碼**/
private final int code;
/**自定義描述**/
private final String message;
ReturnCode(int code, String message){
this.code = code;
this.message = message;
}
public int getCode() {
return code;
}
public String getMessage() {
return message;
}
}
統一返回格式
@GetMapping("/hello")
public ResultData<string> getStr(){
return ResultData.success("hello,javadaily");
}
此時調用接口獲取到的返回值是這樣:
{
"status": 100,
"message": "hello,javadaily",
"data": null,
"timestamp": 1625736481648,
"httpStatus": 0
}
這樣確實已經實現了我們想要的結果,我在很多項目中看到的都是這種寫法,在Controller層通過ResultData.success()對返回結果進行包裝后返回給前端。
看到這里我們不妨停下來想想,這樣做有什么弊端呢?
最大的弊端就是我們后面每寫一個接口都需要調用ResultData.success()這行代碼對結果進行包裝,重復勞動,浪費體力;而且還很容易被其他老鳥給嘲笑。

所以呢我們需要對代碼進行優化,目標就是不要每個接口都手工制定ResultData返回值。
高級實現方式
要優化這段代碼很簡單,我們只需要借助SpringBoot提供的ResponseBodyAdvice即可。
ResponseBodyAdvice的作用:攔截Controller方法的返回值,統一處理返回值/響應體,一般用來統一返回格式,加解密,簽名等等。
先來看下ResponseBodyAdvice的源碼:
public interface ResponseBodyAdvice<t> {
/**
* 是否支持advice功能
* true 支持,false 不支持
*/
boolean supports(MethodParameter var1, Class<!--? extends HttpMessageConverter<?-->> var2);
/**
* 對返回的數據進行處理
*/
@Nullable
T beforeBodyWrite(@Nullable T var1, MethodParameter var2, MediaType var3, Class<!--? extends HttpMessageConverter<?-->> var4, ServerHttpRequest var5, ServerHttpResponse var6);
}
我們只需要編寫一個具體實現類即可
/**
* @author jam
* @date 2021/7/8 10:10 上午
*/
@RestControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice<object> {
@Autowired
private ObjectMapper objectMapper;
@Override
public boolean supports(MethodParameter methodParameter, Class<!--? extends HttpMessageConverter<?-->> aClass) {
return true;
}
@SneakyThrows
@Override
public Object beforeBodyWrite(Object o, MethodParameter methodParameter, MediaType mediaType, Class<!--? extends HttpMessageConverter<?-->> aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
if(o instanceof String){
return objectMapper.writeValueAsString(ResultData.success(o));
}
return ResultData.success(o);
}
}
需要注意兩個地方:
-
@RestControllerAdvice注解@RestControllerAdvice是@RestController注解的增強,可以實現三個方面的功能:- 全局異常處理
- 全局數據綁定
- 全局數據預處理
-
String類型判斷
if(o instanceof String){
return objectMapper.writeValueAsString(ResultData.success(o));
}
這段代碼一定要加,如果Controller直接返回String的話,SpringBoot是直接返回,故我們需要手動轉換成json。
經過上面的處理我們就再也不需要通過ResultData.success()來進行轉換了,直接返回原始數據格式,SpringBoot自動幫我們實現包裝類的封裝。
@GetMapping("/hello")
public String getStr(){
return "hello,javadaily";
}
此時我們調用接口返回的數據結果為:
@GetMapping("/hello")
public String getStr(){
return "hello,javadaily";
}
是不是感覺很完美,別急,還有個問題在等着你呢。

接口異常問題
此時有個問題,由於我們沒對Controller的異常進行處理,當我們調用的方法一旦出現異常,就會出現問題,比如下面這個接口
@GetMapping("/wrong")
public int error(){
int i = 9/0;
return i;
}
返回的結果為:

這顯然不是我們想要的結果,接口都報錯了還返回操作成功的響應碼,前端看了會打人的。
別急,接下來我們進入第二個議題,如何優雅的處理全局異常。
SpringBoot為什么需要全局異常處理器
-
不用手寫try...catch,由全局異常處理器統一捕獲
使用全局異常處理器最大的便利就是程序員在寫代碼時不再需要手寫
try...catch了,前面我們講過,默認情況下SpringBoot出現異常時返回的結果是這樣:
{
"timestamp": "2021-07-08T08:05:15.423+00:00",
"status": 500,
"error": "Internal Server Error",
"path": "/wrong"
}
這種數據格式返回給前端,前端是看不懂的,所以這時候我們一般通過try...catch來處理異常
@GetMapping("/wrong")
public int error(){
int i;
try{
i = 9/0;
}catch (Exception e){
log.error("error:{}",e);
i = 0;
}
return i;
}
我們追求的目標肯定是不需要再手動寫try...catch了,而是希望由全局異常處理器處理。
- 對於自定義異常,只能通過全局異常處理器來處理
@GetMapping("error1")
public void empty(){
throw new RuntimeException("自定義異常");
}
-
當我們引入Validator參數校驗器的時候,參數校驗不通過會拋出異常,此時是無法用
try...catch捕獲的,只能使用全局異常處理器。SpringBoot集成參數校驗請參考這篇文章SpringBoot開發秘籍 - 集成參數校驗及高階技巧
如何實現全局異常處理器
@Slf4j
@RestControllerAdvice
public class RestExceptionHandler {
/**
* 默認全局異常處理。
* @param e the e
* @return ResultData
*/
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ResultData<string> exception(Exception e) {
log.error("全局異常信息 ex={}", e.getMessage(), e);
return ResultData.fail(ReturnCode.RC500.getCode(),e.getMessage());
}
}
有三個細節需要說明一下:
-
@RestControllerAdvice,RestController的增強類,可用於實現全局異常處理器 -
@ExceptionHandler,統一處理某一類異常,從而減少代碼重復率和復雜度,比如要獲取自定義異常可以@ExceptionHandler(BusinessException.class) -
@ResponseStatus指定客戶端收到的http狀態碼
體驗效果
這時候我們調用如下接口:
@GetMapping("error1")
public void empty(){
throw new RuntimeException("自定義異常");
}
返回的結果如下:
{
"status": 500,
"message": "自定義異常",
"data": null,
"timestamp": 1625795902556
}
基本滿足我們的需求了。
但是當我們同時啟用統一標准格式封裝功能ResponseAdvice和RestExceptionHandler全局異常處理器時又出現了新的問題:
{
"status": 100,
"message": "操作成功",
"data": {
"status": 500,
"message": "自定義異常",
"data": null,
"timestamp": 1625796167986
},
"timestamp": 1625796168008
}
此時返回的結果是這樣,統一格式增強功能會給返回的異常結果再次封裝,所以接下來我們需要解決這個問題。
全局異常接入返回的標准格式
要讓全局異常接入標准格式很簡單,因為全局異常處理器已經幫我們封裝好了標准格式,我們只需要直接返回給客戶端即可。
@SneakyThrows
@Override
public Object beforeBodyWrite(Object o, MethodParameter methodParameter, MediaType mediaType, Class<!--? extends HttpMessageConverter<?-->> aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
if(o instanceof String){
return objectMapper.writeValueAsString(ResultData.success(o));
}
if(o instanceof ResultData){
return o;
}
return ResultData.success(o);
}
關鍵代碼:
if(o instanceof ResultData){
return o;
}
如果返回的結果是ResultData對象,直接返回即可。
這時候我們再調用上面的錯誤方法,返回的結果就符合我們的要求了。
{
"status": 500,
"message": "自定義異常",
"data": null,
"timestamp": 1625796580778
}
好了,今天的文章就到這里了,希望通過這篇文章你能掌握如何在你項目中友好實現統一標准格式到返回並且可以優雅的處理全局異常。
github地址:https://github.com/jianzh5/cloud-blog/
最后,我是飄渺Jam,一名寫代碼的架構師,做架構的程序員,期待你的關注。咱們下期見!
求贊求關注
飄渺Jam,一位寫代碼的架構師,做架構的程序員,他的公眾號主要分享Java后端,SpringCloud微服務架構,數據庫等方向的文章,如果你對微服務比較感興趣,建議掃描下方二維碼加個關注!

關注即送10個G的視頻教程,還不趕緊上車?
