1. 程式人生 > >Restful API開發利器——RestPack專案教程(統一api返回json格式)

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 是程式產生的日誌檔案的名稱), 就可以在大量日誌內容中快速過濾出這條請求的日誌了。