SpringBoot 如何統一後端返回格式?老鳥們都是這樣玩的!

JAVA日知錄發表於2021-07-16

大家好,我是飄渺。

今天我們來聊一聊在基於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部分:

  1. status 狀態值:由後端統一定義各種返回結果的狀態碼
  2. message 描述:本次介面呼叫的結果描述
  3. data 資料:本次返回的資料。
{
  "status":"100",
  "message":"操作成功",
  "data":"hello,javadaily"
}

當然也可以按需加入其他擴充套件值,比如我們就在返回物件中新增了介面呼叫時間

  1. 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()這行程式碼對結果進行包裝,重複勞動,浪費體力;而且還很容易被其他老鳥給嘲笑。

image-20210716084136689

所以呢我們需要對程式碼進行優化,目標就是不要每個介面都手工制定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註解的增強,可以實現三個方面的功能:

    1. 全域性異常處理
    2. 全域性資料繫結
    3. 全域性資料預處理
  • 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";
}

是不是感覺很完美,別急,還有個問題在等著你呢。

image-20210716084552589

介面異常問題

此時有個問題,由於我們沒對Controller的異常進行處理,當我們呼叫的方法一旦出現異常,就會出現問題,比如下面這個介面

@GetMapping("/wrong")
public int error(){
    int i = 9/0;
    return i;
}

返回的結果為:

image-20210708191106503

這顯然不是我們想要的結果,介面都報錯了還返回操作成功的響應碼,前端看了會打人的。

別急,接下來我們進入第二個議題,如何優雅的處理全域性異常。

SpringBoot為什麼需要全域性異常處理器

  1. 不用手寫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了,而是希望由全域性異常處理器處理。

  1. 對於自定義異常,只能通過全域性異常處理器來處理
@GetMapping("error1")
public void empty(){
	throw  new RuntimeException("自定義異常");
}
  1. 當我們引入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());
    }

}

有三個細節需要說明一下:

  1. @RestControllerAdvice,RestController的增強類,可用於實現全域性異常處理器

  2. @ExceptionHandler,統一處理某一類異常,從而減少程式碼重複率和複雜度,比如要獲取自定義異常可以@ExceptionHandler(BusinessException.class)

  3. @ResponseStatus指定客戶端收到的http狀態碼

體驗效果

這時候我們呼叫如下介面:

@GetMapping("error1")
public void empty(){
    throw  new RuntimeException("自定義異常");
}

返回的結果如下:

{
  "status": 500,
  "message": "自定義異常",
  "data": null,
  "timestamp": 1625795902556
}

基本滿足我們的需求了。

但是當我們同時啟用統一標準格式封裝功能ResponseAdviceRestExceptionHandler全域性異常處理器時又出現了新的問題:

{
  "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的視訊教程,還不趕緊上車?

相關文章