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

2023-05-09 12:02:20

作者:京東物流 覃玉傑

1. 簡介

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>2.0</version>
</dependency>

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

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.responseStyleResponse風格,不設定預設為0

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

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

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

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