一站式統一返回值封裝、異常處理、異常錯誤碼解決方案—最強的Sping Boot介面優雅響應處理器

京东云开发者發表於2024-06-20

1. 前言

統一返回值封裝、統一異常處理和異常錯誤碼體系的意義在於提高程式碼的可維護性和可讀性,使得程式碼更加健壯和穩定。統一返回值封裝可以避免每一個介面都需要手工拼裝響應報文;統一異常處理可以將異常處理的邏輯集中到一個地方,避免程式碼中出現大量的try-catch語句,降低了程式碼的複雜度,提高了程式碼的可讀性;異常體系的設計可以清晰地區分不同型別的異常,使得開發者能夠更加精準地處理異常情況,並且能夠更好地定位和解決問題。

Graceful Response是一個Spring Boot體系下的優雅響應處理元件,提供一站式統一返回值封裝、全域性異常處理、自定義異常錯誤碼、自定義引數校驗異常碼等功能,使用Graceful Response進行web介面開發不僅可以節省大量的時間,還可以提高程式碼質量,使程式碼邏輯更清晰。

強烈推薦你花3分鐘學會它!

Graceful Response的Github地址: https://github.com/feiniaojin/graceful-response ,歡迎star!

Graceful Response的案例工程程式碼:https://github.com/feiniaojin/graceful-response-example.git

2. Spring Boot Web API介面資料返回的現狀

我們進行Spring Boo Web API介面開發時,通常大部分的Controller程式碼是這樣的:

public class Controller {
    @GetMapping("/query")
    @ResponseBody
    public Response query(Parameter params) {

        Response res = new Response();
        try {
            //1.校驗params引數,非空校驗、長度校驗
            if (illegal(params)) {
                res.setCode(1);
                res.setMsg("error");
                return res;
            }
            //2.呼叫Service的一系列操作
            Data data = service.query(params);
            //3.執行正確時,將操作結果設定到res物件中
            res.setData(data);
            res.setCode(0);
            res.setMsg("ok");
            return res;
        } catch (BizException1 e) {
            //4.異常處理:一堆醜陋的try...catch,如果有錯誤碼的,還需要手工填充錯誤碼
            res.setCode(1024);
            res.setMsg("error");
            return res;
        } catch (BizException2 e) {
            //4.異常處理:一堆醜陋的try...catch,如果有錯誤碼的,還需要手工填充錯誤碼
            res.setCode(2048);
            res.setMsg("error");
            return res;
        } catch (Exception e) {
            //4.異常處理:一堆醜陋的try...catch,如果有錯誤碼的,還需要手工填充錯誤碼
            res.setCode(1);
            res.setMsg("error");
            return res;
        }
    }
}

這段程式碼存在什麼問題呢?真正的業務邏輯被冗餘程式碼淹沒,可讀性太差。

真正執行業務的程式碼只有

Data data=service.query(params);

其他程式碼不管是正常執行還是異常處理,都是為了異常封裝、把結果封裝為特定的格式,例如以下格式:

{
  "code": 0,
  "msg": "ok",
  "data": {
    "id": 1,
    "name": "username"
  }
}

這樣的邏輯每個介面都需要處理一遍,都是繁瑣的重複勞動。

現在,只需要引入Graceful Response元件並透過@EnableGracefulResponse啟用,就可以直接返回業務結果並自動完成response的格式封裝。

以下是使用Graceful Response之後的程式碼,實現同樣的返回值封裝、異常處理、異常錯誤碼功能,但可以看到程式碼變得非常簡潔,可讀性非常強。

public class Controller {
    @GetMapping("/query")
    @ResponseBody
    public Data query(Parameter params) {
       return service.query(params);
    }
}

3. 快速入門

3.1 引入maven依賴

graceful-response已釋出至maven中央倉庫,可以直接引入到專案中,maven依賴如下:

<dependency>
    <groupId>com.feiniaojin</groupId>
    <artifactId>graceful-response</artifactId>
    <version>{此處替換為最新的版本號}</version>
</dependency>

以下連結可以檢視maven中央倉庫中最新的版本:

https://central.sonatype.com/artifact/com.feiniaojin/graceful-response/3.0/versions

3.2 在啟動類中引入@EnableGracefulResponse註解

@EnableGracefulResponse
@SpringBootApplication
public class ExampleApplication {
    public static void main(String[] args) {
        SpringApplication.run(ExampleApplication.class, args);
    }
}

3.3 Controller方法直接返回結果

•普通的查詢

@Controller
public class Controller {
    @RequestMapping("/get")
    @ResponseBody
    public UserInfoView get(Long id) {
        log.info("id={}", id);
        return UserInfoView.builder().id(id).name("name" + id).build();
    }
}

UserInfoView的原始碼:

@Data
@Builder
public class UserInfoView {
    private Long id;
    private String name;
}

這個介面直接返回了 UserInfoView的例項物件,呼叫介面時,Graceful Response將自動封裝為以下格式:

{
  "status": {
    "code": "0",
    "msg": "ok"
  },
  "payload": {
    "id": 1,
    "name": "name1"
  }
}

可以看到UserInfoView被自動封裝到payload欄位中。

Graceful Response提供了兩種風格的Response,可以透過在application.properties檔案中配置gr.responseStyle=1,將以以下的格式進行返回:

{
  "code": "0",
  "msg": "ok",
  "data": {
    "id": 1,
    "name": "name1"
  }
}

如果這兩種風格也不能滿足需要,我們還可以根據自己的需要進行自定義返回的Response格式。詳細見本文 4.3自定義Respnse格式。

•異常處理的場景

透過Graceful Response,我們不需要專門在Controller中處理異常,詳細見 4.1 Graceful Response異常錯誤碼處理。

•返回值為空的場景

某些Command型別的方法只執行修改操作,不返回資料,這個時候我們可以直接在Controller中返回void,Graceful Response會自動封裝預設的操作成功Response報文。

@Controller
public class Controller {
    @RequestMapping("/void")
    @ResponseBody
    public void testVoidResponse() {
        //省略業務操作
    }
}

testVoidResponse方法的返回時void,呼叫這個介面時,將返回:

{
  "status": {
    "code": "200",
    "msg": "success"
  },
  "payload": {}
}

3.4 Service方法業務處理

在引入Graceful Response後,Service層的方法的可讀性可以得到極大的提升。

•介面直接返回業務資料型別,而不是Response,更具備可讀性

public interface ExampleService {
    UserInfoView query1(Query query);
}

•Service介面實現類中,直接拋自定義的業務異常,Graceful Response將其轉化為返回錯誤碼和錯誤提示

public class ExampleServiceImpl implements ExampleService {
    @Resource
    private UserInfoMapper mapper;

    public UserInfoView query1(Query query) {
        UserInfo userInfo = mapper.findOne(query.getId());
        if (Objects.isNull(userInfo)) {
           //這裡直接拋自定義異常,異常透過@ExceptionMapper修飾,提供異常碼和異常提示
           throw new NotFoundException();
        }
        // 省略後續業務操作
    }
}
/**
 * NotFoundException的定義,使用@ExceptionMapper註解修飾
 * code:代表介面的異常碼
 * msg:代表介面的異常提示
 */
@ExceptionMapper(code = "1404", msg = "找不到物件")
public class NotFoundException extends RuntimeException {

}
//Controller不再捕獲處理異常
@RequestMapping("/get")
@ResponseBody
public UserInfoView get(Query query)) {
    return exampleService.query1(query);
}

當Service方法丟擲NotFoundException異常時,介面將直接返回錯誤碼,不需要手工set,極大地簡化了異常處理邏輯。

{
  "status": {
    "code": "1404",
    "msg": "找不到物件"
  },
  "payload": {}
}

驗證:啟動example工程後,請求

http://localhost:9090/example/notfound

3.5 通用異常類和通用工具類

@ExceptionMapper設計的初衷是將異常與錯誤碼關聯起來,使用者只需要拋異常,不需要再關注異常與錯誤碼的對應關係。

部分使用者反饋,希望在不自定義新異常類的情況下,也能可以按照預期返回錯誤碼和異常資訊,因此從2.1版本開始,新增了GracefulResponseException異常類,使用者只需要丟擲該異常即可。

public class Service {
  
  public void method() {
    throw new GracefulResponseException("自定義的錯誤碼","自定義的錯誤資訊");
  }
}

為簡化使用,從2.1版本開始提供GracefulResponse通用工具類,在需要丟擲GracefulResponseException時,只需要呼叫raiseException方法即可。 這樣設計原因是使用者拋通用異常,其實已經不關心具體是什麼異常了,使用者實際上只是想要錯誤碼和錯誤資訊。

示例如下:

public class Service {

    public void method() {
        
        //當condition==true時,丟擲GracefulResponseException異常,返回自定義的錯誤碼和錯誤資訊
        if (condition) {
            GracefulResponse.raiseException("自定義的錯誤碼", "自定義的錯誤資訊");
        }
        //省略其他業務邏輯
    }
}

3.6 引數校驗異常以及錯誤碼

3.0版本以前,如果引用validation框架併發生了校驗異常,Graceful Response在預設情況下會捕獲並返回code=1,引數校驗發生的異常資訊會丟失;如果使用異常別名功能,可以對大的校驗異常返回統一的錯誤碼,但是不夠靈活且依舊沒有解決引數異常提示的問題。

Graceful Response從3.0版本開始,引入@ValidationStatusCode註解,可以非常方便地支援validation校驗異常。

@ValidationStatusCode註解目前只有一個code屬性,用於指定引數校驗異常時的錯誤碼,錯誤提示則取自validation校驗框架。

  • 對入參類進行引數校驗

@Data
public class UserInfoQuery {

    @NotNull(message = "userName is null !")
    @Length(min = 6, max = 12)
    @ValidationStatusCode(code = "520")
    private String userName;

}

userName欄位任意一項校驗不透過時,介面將會返回異常碼520和校驗註解中的message

{
  "status": {
    "code": "520",
    "msg": "userName is null !"
  },
  "payload": {}
}

詳細見example工程ExampleController的validateDto方法
http://localhost:9090/example/validateDto

注意:@ValidationStatusCode校驗引數物件欄位的情況,code取值順序為:會先取欄位上的註解,再去該屬性所在物件的類(即UserInfoQuery類)上的註解,再取全域性配置的引數異常碼gr.defaultValidateErrorCode,最後取預設的全域性預設的錯誤碼(預設code=1)

  • 直接在Controller中校驗方法入參

直接在Controller方法中進行引數校驗:

@Validated
public class ExampleController {

  @RequestMapping("/validateMethodParam")
  @ResponseBody
  @ValidationStatusCode(code = "1314")
  public void validateMethodParam(@NotNull(message = "userId不能為空") Long userId,
                                  @NotNull(message = "userName不能為空") Long userName) {
      //省略業務邏輯
  }
}

當userId、或者userName校驗不透過時,將會返回code=1314,msg為對應的校驗資訊。

{
  "status": {
    "code": "1314",
    "msg": "userId不能為空"
  },
  "payload": {}
}

詳細見example工程ExampleController的validateMethodParam方法
http://localhost:9090/example/validateMethodParam

注意:@ValidationStatusCode校驗Controller方法引數欄位的情況,code取值順序為:會先取當前方法上的註解,再去該方法所在類(即ExampleController類)上的註解,再取全域性配置的引數異常碼gr.defaultValidateErrorCode,最後取預設的全域性預設的錯誤碼(預設code=1)

4. 進階用法

4.1 Graceful Response異常錯誤碼處理

以下是使用Graceful Response進行異常、錯誤碼處理的開發步驟。

•建立自定義異常

透過繼承RuntimeException類建立自定義的異常,採用 @ExceptionMapper註解修飾,註解的 code屬性為返回碼,msg屬性為錯誤提示資訊。

關於是繼承RuntimeException還是繼承Exception,讀者可以根據實際情況去選擇,Graceful Response對兩者都支援。

@ExceptionMapper(code = "1007", msg = "有內鬼,終止交易")
public static final class RatException extends RuntimeException {

}

•Service執行具體邏輯

Service執行業務邏輯的過程中,需要拋異常的時候直接丟擲去即可。由於已經透過@ExceptionMapper定義了該異常的錯誤碼,我們不需要再單獨的維護異常碼列舉與異常類的關係。

//Service層虛擬碼
public class Service {
    public void illegalTransaction() {
        //需要拋異常的時候直接拋
        if (check()) {
            throw new RatException();
        }
        doIllegalTransaction();
    }
}

Controller層呼叫Service層虛擬碼:

public class Controller {
    @RequestMapping("/test3")
    public void test3() {
        //Controller中不會進行異常處理,也不會手工set錯誤碼,只關心核心操作,其他的統統交給Graceful Response
        exampleService.illegalTransaction();
    }
}

在瀏覽器中請求controller的/test3方法,有異常時將會返回:

{
  "status": {
    "code": "1007",
    "msg": "有內鬼,終止交易"
  },
  "payload": {
  }
}

4.2 外部異常別名

案例工程( https://github.com/feiniaojin/graceful-response-example.git )啟動後, 透過瀏覽器訪問一個不存在的介面,例如 http://localhost:9090/example/get2?id=1
如果沒開啟Graceful Response,將會跳轉到404頁面,主要原因是應用內部產生了 NoHandlerFoundException異常。如果開啟了Graceful Response,預設會返回code=1的錯誤碼。

這類非自定義的異常,如果需要自定義一個錯誤碼返回,將不得不對每個異常編寫Advice邏輯,在Advice中設定錯誤碼和提示資訊,這樣做也非常繁瑣。

Graceful Response可以非常輕鬆地解決給這類外部異常定義錯誤碼和提示資訊的問題。

以下為操作步驟:

•建立異常別名,並用 @ExceptionAliasFor註解修飾

@ExceptionAliasFor(code = "1404", msg = "Not Found", aliasFor = NoHandlerFoundException.class)
public class NotFoundException extends RuntimeException {
}

code:捕獲異常時返回的錯誤碼

msg:異常提示資訊

aliasFor:表示將成為哪個異常的別名,透過這個屬性關聯到對應異常。

•註冊異常別名

建立一個繼承了AbstractExceptionAliasRegisterConfig的配置類,在實現的registerAlias方法中進行註冊。

@Configuration
public class GracefulResponseConfig extends AbstractExceptionAliasRegisterConfig {

    @Override
    protected void registerAlias(ExceptionAliasRegister aliasRegister) {
        aliasRegister.doRegisterExceptionAlias(NotFoundException.class);
    }
}

•瀏覽器訪問不存在的URL

再次訪問 http://localhost:9090/example/get2?id=1 ,服務端將返回以下json,正是在ExceptionAliasFor中定義的內容

{
  "code": "1404",
  "msg": "not found",
  "data": {
  }
}

4.3 自定義Response格式

Graceful Response內建了兩種風格的響應格式,可以在application.properties檔案中透過gr.responseStyle進行配置。

•gr.responseStyle=0,或者不配置(預設情況)

將以以下的格式進行返回:

{
  "status": {
    "code": "1007",
    "msg": "有內鬼,終止交易"
  },
  "payload": {
  }
}

•gr.responseStyle=1

將以以下的格式進行返回:

{
  "code": "1404",
  "msg": "not found",
  "data": {
  }
}

•自定義響應格式

如果以上兩種格式均不能滿足業務需要,可以透過自定義去滿足,Response

例如以下響應:

public class CustomResponseImpl implements Response {

    private String code;

    private Long timestamp = System.currentTimeMillis();

    private String msg;

    private Object data = Collections.EMPTY_MAP;

    @Override
    public void setStatus(ResponseStatus statusLine) {
        this.code = statusLine.getCode();
        this.msg = statusLine.getMsg();
    }

    @Override
    @JsonIgnore
    public ResponseStatus getStatus() {
        return null;
    }

    @Override
    public void setPayload(Object payload) {
        this.data = payload;
    }

    @Override
    @JsonIgnore
    public Object getPayload() {
        return null;
    }

    public String getCode() {
        return code;
    }

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

    public String getMsg() {
        return msg;
    }

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

    public Object getData() {
        return data;
    }

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

    public Long getTimestamp() {
        return timestamp;
    }
}

注意,不需要返回的屬性可以返回null或者加上@JsonIgnore註解

•配置gr.responseClassFullName

將CustomResponseImpl的全限定名配置到gr.responseClassFullName屬性。

gr.responseClassFullName=com.feiniaojin.gracefuresponse.example.config.CustomResponseImpl

注意,配置gr.responseClassFullName後,gr.responseStyle將不再生效。

實際的響應報文如下:

{
    "code":"200",
    "timestamp":1682489591319,
    "msg":"success",
    "data":{

    }
}

如果還是不能滿足需求,那麼可以考慮同時自定義實現Response和ResponseFactory這兩個介面。

5. 常用配置

Graceful Response在版本迭代中,根據使用者反饋提供了一些常用的配置項,列舉如下:

  • gr.printExceptionInGlobalAdvice
    是否列印異常日誌,預設為false

  • gr.responseClassFullName
    自定義Response類的全限定名,預設為空。 配置gr.responseClassFullName後,gr.responseStyle將不再生效

  • gr.responseStyle
    Response風格,不配置預設為0

  • gr.defaultSuccessCode
    自定義的成功響應碼,不配置則為0

  • gr.defaultSuccessMsg
    自定義的成功提示,預設為ok

  • gr.defaultErrorCode
    自定義的失敗響應碼,預設為1

  • gr.defaultErrorMsg
    自定義的失敗提示,預設為error

  • gr.defaultValidateErrorCode
    全域性的引數校驗錯誤碼,預設等於gr.defaultErrorCode

6. 總結

本文介紹了Graceful Response這個框架的使用,讀者在使用過程中遇到問題,歡迎到GitHub提交issue進行反饋,幫助我們將Graceful Response最佳化得更好。

相關文章