Restful API開發利器——RestPack專案教程(統一api返回json格式)
Restful API開發利器——RestPack專案教程
目錄
- 專案背景
- RestPack 簡介
- 引入 RestPack 依賴
- 啟用 RestPack
- @RestPackController 註解
- RestPack 異常處理
- 日誌輸出
- 資源分享與技術交流
專案背景
在網際網路、移動網際網路、車聯網、物聯網繁榮的今天, 各種客戶端裝置層出不窮,為了能用同一套服務端程式處理各種客戶端的訪問, HTTP Restful API 變得流行起來。
但是客戶端與服務端互動時,往往會有一些通用的需求,比如:
- 服務端返回的報文,有一套統一的標準,這樣有利於開發和維護。
- 服務端在處理一個 API 請求時,如果出異常了, 總是希望在請求的返回結果中給出一個明確的錯誤碼, 客戶端可以根據錯誤碼作進一步的處理。
- 為了方便排查問題,總是希望對於每個請求,服務端會返回一個 requestId, 後臺可以將這個請求產生的日誌與這個 requestId 相關聯。 這樣一旦前後端聯調時發現了問題,前端工程師只要給出 requestId , 後臺工程師就可以拿著這個 requestId 快速找出相關日誌,方便分析排查問題。 ......
為了滿足這些非功能性需求,筆者總結了之前很多專案的開發經驗, 歸納出一套統一的資料返回格式,如下(分成功和失敗兩種情況):
成功響應內容:
{ "requestId" : "d56c24d006aa4d5e9b8903b3256bf3e3", "serverTime" : 1502592752449, "spendTime" : 5, "resultCode" : "success", "data" : { "key1": "value1", "key2": "value2" } }
- requestId : 服務端生成的請求唯一ID號, 當這個請求有問題時,可以拿著這個 ID 號, 在海量日誌快速查詢到此請求的日誌資訊,以方便排查問題。
- serverTime : 伺服器時間, 很多場景下需要使用當前時間值,但客戶端本地的時間有可能不準, 因為這裡返回伺服器端時間供客戶端使用。
- spendTime : 本次請求在伺服器端處理所消耗的時間, 這裡顯示出來以方便診斷慢請求相關問題。
- resultCode : 結果碼, "success" 表示成功,其它表示一個錯誤的錯誤碼, 錯誤碼的值及具體含意由專案中客戶端與服務端約定。
- data : 實際的業務資料,內容由每個 API 的業務邏輯決定。
錯誤響應內容:
{
"requestId" : "d7ab68ac513e4549896aa33f0cda3518",
"serverTime" : 1502594589673,
"spendTime" : 8,
"resultCode" : "name.duplicate",
"message" : "暱稱重複: terran4j,請換個暱稱!",
"props" : {
"name": "terran4j"
}
}
與成功響應類似,都有 requestId、serverTime、spendTime 等欄位。 不同的是 resultCode 是一個自定義的錯誤碼,並且多了message 、props 兩個欄位:
- message : 錯誤資訊描述, 是一段易於人理解的字串資訊,方便開發人員知曉錯誤原因。
- props : 錯誤上下文相關屬性, 本項可選,有的錯誤碼可能需要前端在程式中作進一步處理, 所以後臺可以在 props 中提供一些 key - value 的屬性值, 方便程式讀取(而不是讓前端程式從 message 中解析文字內容獲取這些值)。
RestPack 簡介
若要讓專案中每個 API 的實現都遵循這套統一的資料規範, 無疑要在每個API方法中編寫一些重複性的程式碼。 因此筆者根據實際專案經驗總結,開發了一套名為 RestPack 的工具包, 可以幫助 Restful API 的開發者將API 的返回結果自動包裝成統一格式的報文。
RestPack 一詞中, Rest 代表 Http Restful API 的意思, 而 Pack 是 "包裝、包裹" 的意思,合起來的意思就是在原本的 Http Restful API 基礎上, 將返回資料再包裹一層,以符合之前所講的資料規範。
本文主要目標是介紹 RestPack 的用法。
引入 RestPack 依賴
然後,您就可以在您的專案的 pom.xml 檔案中,引用 restpack 的 jar 包了,如下所示:
<dependency>
<groupId>terran4j</groupId>
<artifactId>terran4j-commons-restpack</artifactId>
<version>${restpack.version}</version>
</dependency>
整個 pom.xml 內容類似於:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>terran4j</groupId>
<artifactId>terran4j-demo-restpack</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>terran4j-demo-restpack</name>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.9.RELEASE</version>
</parent>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>terran4j</groupId>
<artifactId>terran4j-commons-restpack</artifactId>
<version>${restpack.version}</version>
</dependency>
</dependencies>
</project>
如果是 gradle,請在 build.gradle 中新增依賴,如下所示:
compile "com.github.terran4j:terran4j-commons-restpack:${restpack.version}"
${restpack.version} 最新穩定版,請參考 這裡
啟用 RestPack
為了在應用程式中啟用 RestPack,需要在 SpringBootApplication 類上加@EnableRestPack
註解, 整個 main 程式程式碼,如下所示:
package com.terran4j.demo.restpack;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import com.terran4j.commons.restpack.EnableRestPack;
@EnableRestPack
@SpringBootApplication
public class RestPackDemoApp {
public static void main(String[] args) {
SpringApplication.run(RestPackDemoApp.class, args);
}
}
加上 @EnableRestPack 才能啟用 RestPack 的功能,否則本文下面所講的效果都不會起作用。
@RestPackController 註解
以前實現 HTTP Restful API,就是用 Spring Boot MVC 編寫一個 Controller 類, 並在類上加上 @RestController 註解 (對於這一點不清楚的讀者,請先閱讀筆者之前寫過的 《 Spring Boot快速入門 》 一書,其中《 Spring Boot MVC 》 這章詳細描述了這一點)。
要在原有的 Controller 類上啟用 RestPack 功能, 僅僅是將類上的註解由 @RestController 改成 @RestPackController 就可以了, 程式碼如下所示:
package com.terran4j.demo.restpack;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import com.terran4j.commons.restpack.HttpResultPackController;
import com.terran4j.commons.util.error.BusinessException;
@RestPackController
@RequestMapping("/demo/restpack")
public class RestPackDemoController {
private static final Logger log = LoggerFactory.getLogger(RestPackDemoController.class);
@RequestMapping(value = "/echo", method = RequestMethod.GET)
public String echo(@RequestParam(value = "msg") String msg) throws BusinessException {
log.info("echo, msg = {}", msg);
return msg;
}
}
編寫好這個類後,我們啟動 main 程式,然後瀏覽器輸入URL:
http://localhost:8080/demo/restpack/echo?msg=abc
瀏覽器中顯示結果為:
{
"requestId" : "2141d927f1de453ba3edd83306ecdf3e",
"serverTime" : 1502597485688,
"spendTime" : 21,
"resultCode" : "success",
"data" : "abc"
}
如果我們去掉 @EnableRestPack (或將 @RestPackController 還原成 @RestController), 再訪問的結果僅為:
abc
說明 RestPack 可以將原本的返回資料,自動包裝成我們定義的資料規範格式了。
對於無返回值的方法, RestPack 同樣有效果, 比如我們在上面的 RestPackDemoController 類中新增如下方法:
@RequestMapping(value = "/void", method = RequestMethod.GET)
public void doVoid(@RequestParam(value = "msg") String msg) throws BusinessException {
log.info("doVoid, msg = {}", msg);
}
重啟程式後在瀏覽器輸入URL:
http://localhost:8080/demo/restpack/void?msg=abc
顯示的結果如下:
{
"requestId" : "2df4aa14dfab46e196ebf7e79b2b35d6",
"serverTime" : 1502627058784,
"spendTime" : 35,
"resultCode" : "success"
}
由於方法沒有返回值,所以"data"欄位也不出現了,但其它欄位都有了。
如果返回值是自定義的複雜物件,RestPack 同樣能轉化成 json 格式放在 "data" 欄位中, 比如我們再新增如下程式碼:
@RequestMapping(value = "/hello", method = RequestMethod.GET)
public HelloBean hello(@RequestParam(value = "name") String name) throws BusinessException {
log.info("hello, name = {}", name);
HelloBean bean = new HelloBean();
bean.setName(name);
bean.setMessage("Hello, " + name + "!");
bean.setTime(new Date());
return bean;
}
類 HelloBean 的定義如下:
package com.terran4j.demo.restpack;
import java.util.Date;
public class HelloBean {
private String name;
private String message;
private Date time;
// 省略 getter /setter 方法。
}
重啟程式後在瀏覽器輸入URL:
http://localhost:8080/demo/restpack/hello?name=neo
顯示的結果如下:
{
"requestId" : "ab5c43c3415042b682b290e17fad1358",
"serverTime" : 1502957833154,
"spendTime" : 30,
"resultCode" : "success",
"data" : {
"name" : "neo",
"message" : "Hello, neo!",
"time" : "2017-08-17 16:17:13"
}
}
發現 "data" 中的欄位與 HelloBean 的屬性是對應的。
RestPack 異常處理
當服務端丟擲異常時,RestPack 會將異常包裝成錯誤報文返回。
從客戶端的角度來看,異常分兩種:
- 一種是業務異常, 如: 註冊時使用者名稱已存在、使用者輸入錯誤,等。這種情況下, 客戶端需要明確的異常原因及關鍵欄位資料, 以便於客戶端程式知曉如何在介面上給予使用者提示。
- 另一種是系統異常, 如: 資料庫無法訪問、程式BUG,等。 這種異常需要客戶端模糊處理(儘量避免暴露系統本身的問題), 比如彈出一個“對不起,系統開小差了”, 或“系統維護中,請稍後重試”之類的提示。
RestPack 提供了一個叫 BusinessException 的異常類來代表業務異常, 如果方法丟擲的異常類是 BusinessException 類或其子類, RestPack 就按業務異常處理,如果不是就按系統異常處理。 為了檢視執行效果,我們新增一個新的方法:
@RequestMapping(value = "/regist", method = RequestMethod.GET)
public void regist(@RequestParam(value = "name") String name) throws BusinessException {
log.info("regist, name = {}", name);
if (name.length() < 3) {
String suggestName = name + "123";
throw new BusinessException("name.invalid")
.setMessage("您輸入的名稱太短了,建議為:${suggestName}")
.put("suggestName", suggestName);
}
log.info("regist done, name = {}", name);
}
在 BusinessException 類中,構造方法中的引數(如上面的 "name.invalid" ) 就是錯誤碼, put(String, Object)
方法用於設定一些異常上下文屬性,會出現在返回報文的 props 欄位中, setMessage(String)
方法用於設定異常資訊,可以用 ${}
來引用 put
方法出現的欄位。
重啟程式,在瀏覽器中訪問URL:
http://localhost:8080/demo/restpack/regist?name=ne
結果如下:
{
"requestId" : "22e5651199f645628fdf724e9f0826a3",
"serverTime" : 1502627761012,
"spendTime" : 1,
"resultCode" : "name.invalid",
"message" : "您輸入的名稱太短了,建議為:ne123",
"props" : {
"suggestName" : "ne123"
}
}
日誌輸出
RestPack 在開始處理請求時,會生成唯一的 requestId, 這個 requestId 不但會在返回報文中出現,還會一開始就放到日誌的MDC中, 對於 log4j 或 logback (它們都支援 MDC), 你可以在配置將 requestId 資訊輸出到日誌中,這樣每條日誌就用 requestId 相關聯了。
比如在專案中,將 logback.xml 配置如下:
<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="false" scan="true" scanPeriod="1000">
<appender name="stdout" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%date %level requestId=%X{requestId} -- %-40logger{35}[%line]: %msg%n</pattern>
</encoder>
</appender>
<appender name="file" class="ch.qos.logback.core.FileAppender">
<file>./restpack.log</file>
<encoder>
<pattern>%date %level requestId=%X{requestId} -- %-40logger{35}[%line]: %msg%n</pattern>
</encoder>
</appender>
<root level="info">
<appender-ref ref="stdout" />
<appender-ref ref="file" />
</root>
</configuration>
重點是日誌輸出格式,也就是<pattern>中加上requestId=%X{requestId}:
%date %level requestId=%X{requestId} -- %-40logger{35}[%line]: %msg%n
%X{} 是使用 MDC 中的欄位,有關 logback / log4j 中 MDC 的用法,不清楚的讀者請自行百度搜索。
logback.xml 配置好後,再重啟服務,在瀏覽器中輸入URL:
http://localhost:8080/demo/restpack/echo?msg=abc
結果控制檯輸出如下:
2017-08-17 16:34:08,570 INFO requestId=ca2a12a0031f493db97856a3300b917a -- c.t.commons.restpack.RestPackAspect [120]: request '/demo/restpack/echo' begin, params:
{
"msg" : "abc"
}
2017-08-17 16:34:08,571 INFO requestId=ca2a12a0031f493db97856a3300b917a -- c.t.d.r.RestPackDemoController [29]: echo, msg = abc
2017-08-17 16:34:08,572 INFO requestId=ca2a12a0031f493db97856a3300b917a -- c.t.commons.restpack.RestPackAdvice [63]: request '/demo/restpack/echo' end, response:
{
"requestId" : "ca2a12a0031f493db97856a3300b917a",
"serverTime" : 1502958848570,
"spendTime" : 2,
"resultCode" : "success",
"data" : "abc"
}
可以看到日誌中有requestId=ca2a12a0031f493db97856a3300b917a
這段內容。
這樣的好處是排查日誌方便,比如在 linux 環境中,對日誌檔案執行類似如下命令:
grep -n "requestId=ca2a12a0031f493db97856a3300b917a" xxx.log
(xxx.log 是程式產生的日誌檔案的名稱), 就可以在大量日誌內容中快速過濾出這條請求的日誌了。