SpringBoot RESTful API 架構風格實踐


如果你要問 Spring Boot 做什么最厲害,我想答案就在本章標題 RESTful API 簡稱 REST API 。

本項目源碼下載

1 RESTful API 概述

1.1 什么是 RESTful API

Rest 是一種規范,符合 Rest 的 Api 就是 Rest Api。簡單的說就是可聯網設備利用 HTTP 協議通過 GET、POST、DELETE、PUT、PATCH 來操作具有URI標識的服務器資源,返回統一格式的資源信息,包括 JSON、XML、CSV、ProtoBuf、其他格式。

1.2 RESTful API 設計規范

RESTful API

1.2.1 協議

建議使用基於 HTTPS 協議

1.2.2 域名

建議使用 api.domain.com

1.2.3 版本

  • 建議把版本放到 URL 中,即顯式設計。如 https://api.domain.com/v1
  • 建議吧版本放到 HTTP 頭信息中。不如放到 URL 中直觀。

1.2.4 路徑

在RESTful架構中,每個網址代表一種資源(resource),所以網址中不能有動詞,只能有名詞,而且所用的名詞往往與數據庫的表格名對應。

1.2.5 HTTP 動詞

常用的動詞包括了5個

  • GET(SELECT):從服務器取出資源(一項或多項)。
  • POST(CREATE):在服務器新建一個資源。
  • PUT(UPDATE):在服務器更新資源(客戶端提供改變后的完整資源)。
  • PATCH(UPDATE):在服務器更新資源(客戶端提供改變的屬性)。
  • DELETE(DELETE):從服務器刪除資源。

1.2.6 過濾信息

如果記錄數量很多,服務器不可能都將它們返回給用戶。API應該提供參數,過濾返回結果。

  • api.domain.com/v1/users?limit=10 :指定返回記錄的數量
  • api.domain.com/v1/users?offset=10:指定返回記錄的開始位置
  • api.domain.com/v1/users?page=2&per_page=100:指定第幾頁,以及每頁的記錄數
  • api.domain.com/v1/users?sortby=name&order=asc:指定返回結果按照哪個屬性排序,以及排序順序
  • api.domain.com/v1/users?animal_type_id=1:指定篩選條件

1.2.7 狀態碼

建議使用 HTTP 的狀態碼

  • 200 OK - [GET]:服務器成功返回用戶請求的數據,該操作是冪等的(Idempotent)。
  • 201 CREATED - [POST/PUT/PATCH]:用戶新建或修改數據成功。
  • 202 Accepted - [*]:表示一個請求已經進入后台排隊(異步任務)
  • 204 NO CONTENT - [DELETE]:用戶刪除數據成功。
  • 400 INVALID REQUEST - [POST/PUT/PATCH]:用戶發出的請求有錯誤,服務器沒有進行新建或修改數據的操作,該操作是冪等的。
  • 401 Unauthorized - [*]:表示用戶沒有權限(令牌、用戶名、密碼錯誤)。
  • 403 Forbidden - [*] 表示用戶得到授權(與401錯誤相對),但是訪問是被禁止的。
  • 404 NOT FOUND - [*]:用戶發出的請求針對的是不存在的記錄,服務器沒有進行操作,該操作是冪等的。
  • 406 Not Acceptable - [GET]:用戶請求的格式不可得(比如用戶請求JSON格式,但是只有XML格式)。
  • 410 Gone -[GET]:用戶請求的資源被永久刪除,且不會再得到的。
  • 422 Unprocesable entity - [POST/PUT/PATCH] 當創建一個對象時,發生一個驗證錯誤。
  • 500 INTERNAL SERVER ERROR - [*]:服務器發生錯誤,用戶將無法判斷發出的請求是否成功。

詳細狀態碼

1.2.8 錯誤處理

返回指定錯誤信息內容

{
    "error":"err message"
}

1.2.9 返回結果

針對不同操作,服務器向用戶返回的結果應該符合以下規范。

  • GET /collection:返回資源對象的列表(數組)
  • GET /collection/resource:返回單個資源對象
  • POST /collection:返回新生成的資源對象
  • PUT /collection/resource:返回完整的資源對象
  • PATCH /collection/resource:返回完整的資源對象
  • DELETE /collection/resource:返回一個空文檔

1.2.10 超級鏈接

RESTful API最好做到Hypermedia,即返回結果中提供鏈接,連向其他API方法,使得用戶不查文檔,也知道下一步應該做什么。

以上信息主要來自RESTful API 設計指南

2 Spring Boot 中如何使用 RESTful API

本章節需要編寫的是對一個用戶的增刪改查操作,如下表是一個非 RESTful 和 標准 RESTful 的對比表。

Api Name 非RESTful RESTful Api
獲取用戶 /user/query/1 /users/1 GET
新增用戶 /user/add /users POST
更新用戶 /user/edit /users PUT
刪除用戶 /user/delete /users DELETE

2.1 新建 Spring Boot 項目

1)File > New > Project,如下圖選擇 Spring Initializr 然后點擊 【Next】下一步

2)填寫 GroupId(包名)、Artifact(項目名) 即可。點擊 下一步
groupId=com.fishpro
artifactId=restful

3)選擇依賴 Spring Web Starter 前面打鈎。

4)項目名設置為 spring-boot-study-restful

2.2 編寫示例代碼

本次新增2個文件,其中UserController類中包括了對用戶的4個操作增刪改查。

  1. controller/UserController.java
  2. domain/UserDO.java

2.2.1 UserDO 實體類代碼

public class UserDO {
    private Integer userId;
    private String userName;

    public Integer getUserId() {
        return userId;
    }

    public void setUserId(Integer userId) {
        this.userId = userId;
    }

    public String getUserName() {
        return userName;
    }

    public void setUserName(String userName) {
        this.userName = userName;
    }
}

2.2.2 UserController 類代碼

/**
 * RESTful API 風格示例 對資源 user 進行操作
 * 本示例沒有使用數據庫,也沒有使用 service 類來輔助完成,所有操作在本類中完成
 * */
@RestController
@RequestMapping("/api/user")
public class UserController {



    /**
     * 模擬一組數據
     * */
    private List<UserDO> getData(){
        List<UserDO> list=new ArrayList<>();

        UserDO userDO=new UserDO();
        userDO.setUserId(1);
        userDO.setUserName("admin");
        list.add(userDO);

        userDO=new UserDO();
        userDO.setUserId(2);
        userDO.setUserName("heike");
        list.add(userDO);

        userDO=new UserDO();
        userDO.setUserId(3);
        userDO.setUserName("tom");
        list.add(userDO);

        userDO=new UserDO();
        userDO.setUserId(4);
        userDO.setUserName("mac");
        list.add(userDO);

        return  list;
    }


    /**
     * SELECT 查詢操作,返回一個JSON數組
     * 具有冪等性
     * */
    @GetMapping("/users")
    @ResponseStatus(HttpStatus.OK)
    public Object getUsers(){
        List<UserDO> list=new ArrayList<>();

        list=getData();

        return list;
    }

    /**
     * SELECT 查詢操作,返回一個新建的JSON對象
     * 具有冪等性
     * */
    @GetMapping("/users/{id}")
    @ResponseStatus(HttpStatus.OK)
    public Object getUser(@PathVariable("id") String id){

        if(null==id){
            return  null;
        }

        List<UserDO> list= getData();
        UserDO userDO=null;
        for (UserDO user:list
             ) {
            if(id.equals(user.getUserId().toString())){
                userDO=user;
                break;
            }
        }

        return userDO;
    }

    /**
     * 新增一個用戶對象
     * 非冪等
     * */
    @PostMapping("/users")
    @ResponseStatus(HttpStatus.CREATED)
    public Object addUser(@RequestBody UserDO user){

        List<UserDO> list= getData();
        list.add(user);//模擬向列表中增加數據
        return user;
    }

    /**
     * 編輯一個用戶對象
     * 冪等性
     * */
    @PutMapping("/users/{id}")
    @ResponseStatus(HttpStatus.CREATED)
    public Object editUser(@PathVariable("id") String id,@RequestBody UserDO user){
        List<UserDO> list= getData();
        for (UserDO userDO1:list
                ) {
            if(id.equals(userDO1.getUserId().toString())){
                userDO1=user;
                break;
            }
        }

        return user;
    }

    /**
     * 刪除一個用戶對象
     * 冪等性
     * */
    @DeleteMapping("/users")
    @ResponseStatus(HttpStatus.NO_CONTENT)
    public Object deleteUser(@PathVariable("id") String id){
        List<UserDO> list= getData();
        UserDO userDO=null;
        for (UserDO user:list
                ) {
            if(id.equals(user.getUserId().toString())){
                //刪除用戶
                userDO=user;
                break;
            }
        }
        return  userDO;//返回被刪除的對象
    }
}

2.2.3 使用 PostMan 測試

獲取全部資源 獲取所有用戶

GET http://localhost:8086/api/user/users/

[
    {
        "userId": 1,
        "userName": "admin"
    },
    {
        "userId": 2,
        "userName": "heike"
    },
    {
        "userId": 3,
        "userName": "tom"
    },
    {
        "userId": 4,
        "userName": "mac"
    }
]

獲取單個資源 獲取用戶

GET http://localhost:8086/api/user/users/3

{
    "userId": 3,
    "userName": "tom"
}

新增一個資源 新增一個用戶
POST http://localhost:8086/api/user/users
請求參數

{
    "userId": 4,
    "userName": "newname"
}

編輯更新一個資源

POST http://localhost:8086/api/user/users
請求參數

{
    "userId": 1,
    "userName": "editname"
}

刪除一個資源

DELETE http://localhost:8086/api/user/users
請求參數

{
    "userId": 1
}

2.2.4 其他輔助設計

RESTful API 可以結合 @RestControllAdvie 做全局異常處理,可以使用自定義標簽做日志攔截,可以做全局日志攔截,可以做自動數據驗證等等。

3 為什么不推薦使用 RESTful API

RESTful API 固然很好但大多數互聯網公司都沒有按照其規則來設計。因為 REST 本來就是一種風格,並沒有什么固定的規則來約束,基於過於理想的 RESTful API 只會付出更多的人力成本和時間成本。

3.1 操作方式繁瑣,沒有效率,且意義不大

使用 HTTP 的 GET\POST\PUT\DELETE 來區分操作資源,HTTP Method 本身就對外部不友好,是隱藏的方法,把動詞加入到 url 中,反而清晰可見,簡單易懂,為什么一定要用 url 來表示資源而不能加動詞呢。

GET\POST\PUT\DELETE 的兼容性有待認證,首先是兼容老的系統,大部分 HTTP 應用是基於 GET/POST 來實現的。

3.2 過分強調單一資源

這可能是人的理解問題,就看我們對資源的定義,太多的場景,如果使用 Restful API 規則行事,勢必要把一個 API 拆分多個 API,框架多個 API 之間的狀態又成了問題。

3.3 返回值問題

使用 HTTP Status 狀態碼是沒有問題的,但使用不常見的狀態碼表示操作層面的含義,對開發不是很友好。
對返回集中定義 status、message 例如下面的 json 返回是很常見的返回,簡單易懂。並沒有什么不妥,但在 RESTful Api 擁護者眼里就是錯誤的格式,錯誤的返回。

{
    "status":1,
    "message":"",
    "data":[]
}

3.4 更高的成本

統一為 RESTful API 風格,強化要求做的完美,比如帶來更多的時間成本、人力成本。溝通成本。

本項目源碼下載


關聯閱讀:

Spring Boot Log 日志使用教程

Spring Boot 全局異常處理


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM