RestFul API 統一格式返回 + 全局異常處理

1、背景

在分佈式、微服務盛行的今天,絕大部分項目都採用的微服務框架,先後端分離方式。前端和後端進行交互,前端按照約定請求URL路徑,並傳入相關參數,後端服務器接收請求,進行業務處理,返回數據給前端。html

因此統一接口的返回值,保證接口返回值的冪等性很重要,本文主要介紹博主當前使用的結果集。前端

2、統一格式設計

2.1 統一結果的通常形式

  • 示例:
{
	# 是否響應成功
	success: true,
	# 響應狀態碼
	code: 200,		
	# 響應數據
	data: Object
	# 返回錯誤信息
	message: "",
}
複製代碼

2.2 結果類枚舉

public enum ResultCodeEnum {
    /*** 通用部分 100 - 599***/
    // 成功請求
    SUCCESS(200, "successful"),
    // 重定向
    REDIRECT(301, "redirect"),
    // 資源未找到
    NOT_FOUND(404, "not found"),
    // 服務器錯誤
    SERVER_ERROR(500,"server error"),

    /*** 這裏能夠根據不一樣模塊用不一樣的區級分開錯誤碼,例如: ***/

    // 1000~1999 區間表示用戶模塊錯誤
    // 2000~2999 區間表示訂單模塊錯誤
    // 3000~3999 區間表示商品模塊錯誤
    // 。。。

    ;
    /** * 響應狀態碼 */
    private Integer code;
    /** * 響應信息 */
    private String message;

    ResultCodeEnum(Integer code, String msg) {
        this.code = code;
        this.message = msg;
    }

    public Integer getCode() {
        return code;
    }

    public String getMessage() {
        return message;
    }
}
複製代碼
  • code:響應狀態碼

通常小夥伴們是在開發的時候須要什麼,就添加什麼。可是,爲了規範,咱們應當參考HTTP請求返回的狀態碼。java

code區間 類型 含義
1** 100-199 信息 服務器接收到請求,須要請求者繼續執行操做
2** 200-299 成功 請求被成功接收並處理
3** 300-399 重定向 須要進一步的操做以完成請求
4** 400-499 客戶端錯誤 請求包含語法錯誤或沒法完成請求
5** 500-599 服務器錯誤 服務器在處理的時候發生錯誤

常見的HTTP狀態碼:git

  1. 200 - 請求成功;
  2. 301 - 資源(網頁等)被永久轉移到其它URL
  3. 404 - 請求的資源(網頁等)不存在;
  4. 500 - 內部服務器錯誤。
  • message:錯誤信息

在發生錯誤時,如何友好的進行提示?github

  1. 根據code 給予對應的錯誤碼定位;
  2. 把錯誤描述記錄到message中,便於接口調用者更詳細的瞭解錯誤。

2.3 統一結果類

public class HttpResult <T> implements Serializable {

    /** * 是否響應成功 */
    private Boolean success;
    /** * 響應狀態碼 */
    private Integer code;
    /** * 響應數據 */
    private T data;
    /** * 錯誤信息 */
    private String message;

    // 構造器開始
    /** * 無參構造器(構造器私有,外部不能夠直接建立) */
    private HttpResult() {
        this.code = 200;
        this.success = true;
    }
    /** * 有參構造器 * @param obj */
    private HttpResult(T obj) {
        this.code = 200;
        this.data = obj;
        this.success = true;
    }

    /** * 有參構造器 * @param resultCode */
    private HttpResult(ResultCodeEnum resultCode) {
        this.success = false;
        this.code = resultCode.getCode();
        this.message = resultCode.getMessage();
    }
    // 構造器結束

    /** * 通用返回成功(沒有返回結果) * @param <T> * @return */
    public static<T> HttpResult<T> success(){
        return new HttpResult();
    }

    /** * 返回成功(有返回結果) * @param data * @param <T> * @return */
    public static<T> HttpResult<T> success(T data){
        return new HttpResult<T>(data);
    }

    /** * 通用返回失敗 * @param resultCode * @param <T> * @return */
    public static<T> HttpResult<T> failure(ResultCodeEnum resultCode){
        return  new HttpResult<T>(resultCode);
    }

    public Boolean getSuccess() {
        return success;
    }

    public void setSuccess(Boolean success) {
        this.success = success;
    }

    public Integer getCode() {
        return code;
    }

    public void setCode(Integer code) {
        this.code = code;
    }

    public T getData() {
        return data;
    }

    public void setData(T data) {
        this.data = data;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }

    @Override
    public String toString() {
        return "HttpResult{" +
                "success=" + success +
                ", code=" + code +
                ", data=" + data +
                ", message='" + message + '\'' +
                '}';
    }
}
複製代碼

說明:spring

  1. 構造器私有,外部不能夠直接建立;
  2. 只能夠調用統一返回類的靜態方法返回對象;
  3. success 是一個Boolean 值,經過這個值,能夠直接觀察到該次請求是否成功;
  4. data 表示響應數據,用於請求成功後,返回客戶端須要的數據。

3、測試及總結

3.1 簡單的接口測試

@RestController
@RequestMapping("/httpRest")
@Api(tags = "統一結果測試")
public class HttpRestController {

    @ApiOperation(value = "通用返回成功(沒有返回結果)", httpMethod = "GET")
    @GetMapping("/success")
    public HttpResult success(){
        return HttpResult.success();
    }

    @ApiOperation(value = "返回成功(有返回結果)", httpMethod = "GET")
    @GetMapping("/successWithData")
    public HttpResult successWithData(){
        return HttpResult.success("風塵博客");
    }

    @ApiOperation(value = "通用返回失敗", httpMethod = "GET")
    @GetMapping("/failure")
    public HttpResult failure(){
        return HttpResult.failure(ResultCodeEnum.NOT_FOUND);
    }

}
複製代碼

這裏 Swagger以及SpringMVC的配置就沒貼出來了,詳見Github 示例代碼。後端

3.2 返回結果

http://localhost:8080/swagger-ui.html#/api

{
  "code": 200,
  "success": true
}
複製代碼
{
  "code": 200,
  "data": "風塵博客",
  "success": true
}
複製代碼
{
  "code": 404,
  "message": "not found",
  "success": false
}
複製代碼

4、全局異常處理

使用統一返回結果時,還有一種狀況,就是程序的報錯是因爲運行時異常致使的結果,有些異常是咱們在業務中拋出的,有些是沒法提早預知。springboot

所以,咱們須要定義一個統一的全局異常,在Controller捕獲全部異常,而且作適當處理,並做爲一種結果返回。服務器

4.1 設計思路:

  1. 自定一個異常類(如:TokenVerificationException),捕獲針對項目或業務的異常;
  2. 使用@ExceptionHandler註解捕獲自定義異常和通用異常;
  3. 使用@ControllerAdvice集成@ExceptionHandler的方法到一個類中;
  4. 異常的對象信息補充到統一結果枚舉中;

4.2 自定義異常

public class TokenVerificationException extends RuntimeException {

    /** * 錯誤碼 */
    protected Integer code;

    protected String msg;

    public Integer getCode() {
        return code;
    }

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }

    /** * 有參構造器,返回碼在枚舉類中,這裏能夠指定錯誤信息 * @param msg */
    public TokenVerificationException(String msg) {
        super(msg);
    }
}
複製代碼

4.3 統一異常處理器

@ControllerAdvice註解是一種做用於控制層的切面通知(Advice),可以將通用的@ExceptionHandler@InitBinder@ModelAttributes方法收集到一個類型,並應用到全部控制器上。

@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

    /** * 異常捕獲 * @param e 捕獲的異常 * @return 封裝的返回對象 **/
    @ExceptionHandler(Exception.class)
    public HttpResult handlerException(Exception e) {
        ResultCodeEnum resultCodeEnum;
        // 自定義異常
        if (e instanceof TokenVerificationException) {
            resultCodeEnum = ResultCodeEnum.TOKEN_VERIFICATION_ERROR;
            resultCodeEnum.setMessage(getConstraintViolationErrMsg(e));
            log.error("tokenVerificationException:{}", resultCodeEnum.getMessage());
        }else {
            // 其餘異常,當咱們定義了多個異常時,這裏能夠增長判斷和記錄
            resultCodeEnum = ResultCodeEnum.SERVER_ERROR;
            resultCodeEnum.setMessage(e.getMessage());
            log.error("common exception:{}", JSON.toJSONString(e));
        }
        return HttpResult.failure(resultCodeEnum);
    }

    /** * 獲取錯誤信息 * @param ex * @return */
    private String getConstraintViolationErrMsg(Exception ex) {
        // validTest1.id: id必須爲正數
        // validTest1.id: id必須爲正數, validTest1.name: 長度必須在有效範圍內
        String message = ex.getMessage();
        try {
            int startIdx = message.indexOf(": ");
            if (startIdx < 0) {
                startIdx = 0;
            }
            int endIdx = message.indexOf(", ");
            if (endIdx < 0) {
                endIdx = message.length();
            }
            message = message.substring(startIdx, endIdx);
            return message;
        } catch (Throwable throwable) {
            log.info("ex caught", throwable);
            return message;
        }
    }
}
複製代碼
  • 說明
  1. 我使用的是@RestControllerAdvice ,等同於@ControllerAdvice + @ResponseBody
  2. 錯誤枚舉類這裏省略了,詳見Github代碼

5、測試及總結

5.1 測試接口

@RestController
@RequestMapping("/exception")
@Api(tags = "異常測試接口")
public class ExceptionRestController {

    @ApiOperation(value = "業務異常(token 異常)", httpMethod = "GET")
    @GetMapping("/token")
    public HttpResult token() {
        // 模擬業務層拋出 token 異常
        throw new TokenVerificationException("token 已通過期");
    }


    @ApiOperation(value = "其餘異常", httpMethod = "GET")
    @GetMapping("/errorException")
    public HttpResult errorException() {
        //這裏故意形成一個其餘異常,而且不進行處理
        Integer.parseInt("abc123");
        return HttpResult.success();
    }
}
複製代碼

5.2 返回結果

http://localhost:8080/swagger-ui.html#/

{
  "code": 500,
  "message": "For input string: \"abc123\"",
  "success": false
}
複製代碼
{
  "code": 4000,
  "message": "token 已通過期",
  "success": false
}
複製代碼

5.3 小結

@RestControllerAdvice@ExceptionHandler會捕獲全部Rest接口的異常並封裝成咱們定義的HttpResult的結果集返回,可是:處理不了攔截器裏的異常

6、總結

沒有哪種方案是適用於各類狀況的,如:分頁狀況,還能夠增長返回分頁結果的靜態方案,具體實現,這裏就不展現了。因此,適合本身的,具備必定可讀性都是很好的,歡迎持不一樣意見的大佬給出意見建議。

6.1 示例代碼

Github 示例代碼

6.2 技術交流

  1. 風塵博客
  2. 風塵博客-掘金
  3. 風塵博客-博客園
  4. Github
  5. 公衆號
    風塵博客
相關文章
相關標籤/搜索