作者:京東物流 覃玉傑
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