到底什麼樣的 REST 才是最佳 REST?

帶你聊技術發表於2023-01-09

說起 REST API,小夥伴們多多少少都有聽說過,但是如果讓你詳細介紹一下什麼是 REST,估計會有很多人講不出來,或者只講出來其中一部分。

今天松哥就來和大家一起來聊一聊到底什麼是 REST,順便再來看下 Spring HATEOAS 的用法。

1. REST 成熟模型

首先關於 REST,有一個大佬 Leonard Richardson 為 REST 定義了一個成熟度模型,他一共定義了四個不同的層次,分別如下:

  1. Level0:Web 服務單純的使用 HTTP 作為資料傳輸方式,本質上就是遠端方法呼叫,常見的 SOAP 和 RPC 基本上都屬於這一類。
  2. Level1:在這一級別上,引入了資源的概念,服務端的每一個資源,都有一個對應的操作地址。
  3. Level2:在這一級別上,我們引入了不同的 HTTP 請求方法來描述不同的操作,例如 GET 表示查詢、POST 表示插入、PUT 表示更新、DELETE 表示刪除,並且使用 HTTP 的狀態碼來表示不同的響應結果。一般來說,大家在日常的介面開發中,基本上都能做到這一層級。但是這還不是最佳結果。
  4. Level3:按照 Leonard Richardson 的意思,這一層級的 REST 基於 HATEOAS(Hypertext As The Engine Of Application State),在這一級別上,除了返回資源的 JSON 之外,還會額外返回一組 Link,這組 Link 描述了對於該資源可以做哪些操作,以及具體的該怎麼做。

在日常的開發中,我們一般都是隻實現到 Level2 這一層級,真正做到 Level3 的估計很少,不過雖然在工作中一般不會做到 Level3 這一層級,但是,我相信很多小夥伴應該是見過 Level3 層級的 REST 是啥樣子的,特別是看過 vhr 影片的小夥伴,松哥在其中講過,透過 Spring Data Jpa+Spring Rest Repositories 實現的 CURD 介面,其實就是一個達到了 Level3 層級的 REST。

2. Spring HATEOAS

那麼接下來我先用 Spring HATEOAS 寫一個簡單的 REST,然後結合這個案例來和小夥伴們聊一聊到底 Spring HATEOAS 有何不一樣的地方。

首先我們建立一個 Spring Boot 工程,引入 Web 和 Spring HATEOAS 依賴,如下:

到底什麼樣的 REST 才是最佳 REST?

建立好之後,我們首先建立一個 User 實體類:

public class User extends RepresentationModel {
    private Integer id;
    private String username;
    private String address;
    //省略 getter/setter
}

注意這個 User 實體類需要繼承自 RepresentationModel,以方便後續新增不同的 Link(以前舊的版本需要繼承自 ResourceSupport)。

接下來寫一個簡單的測試介面。

查詢所有使用者:

@RestController
@RequestMapping("/users")
public class UserController {

    @GetMapping
    public CollectionModel<User> list() {
        List<User> list = new ArrayList<>();
        User u1 = new User();
        u1.setId(1);
        u1.setUsername("javaboy");
        u1.setAddress(");
        u1.add(WebMvcLinkBuilder.linkTo(UserController.class).slash(u1.getId()).withSelfRel());
        list.add(u1);
        User u2 = new User();
        u2.setId(2);
        u2.setUsername("itboy");
        u2.setAddress(");
        u2.add(WebMvcLinkBuilder.linkTo(UserController.class).slash(u2.getId()).withSelfRel());
        list.add(u2);
        CollectionModel<User> users = CollectionModel.of(list);
        users.add(WebMvcLinkBuilder.linkTo(UserController.class).withRel("users"));
        return users;
    }
}

關於這個介面,我來說幾點:

  1. 首先,對於這種返回一個集合或者陣列的情況,返回的型別都是 CollectionModel
  2. 把集合弄好之後(正常應該去資料庫中查詢,我這裡省事直接建立了),透過 CollectionModel.of(list) 方法去獲取一個 CollectionModel<User> 物件。
  3. 對於每一個 user 物件,我都新增了一個 Link 物件,WebMvcLinkBuilder.linkTo(UserController.class).slash(u1.getId()).withSelfRel() 表示生成當前物件的訪問連結。
  4. WebMvcLinkBuilder.linkTo(UserController.class).withRel("users") 表示訪問所有資料的連結。

好了,這個介面寫完之後,我們訪問看下:

到底什麼樣的 REST 才是最佳 REST?

可以看到,返回的每一個 user 物件中,都有一個連結表示如何單獨訪問這個物件。最下面還有一個訪問所有物件的連結。

對於上面這個案例,可能有小夥伴會質疑,難道我們從資料庫中查詢出來的 List 集合都要遍歷一遍,然後給每一個 User 新增一個 Link 嗎?其實不必,新增 Link 這個事可以直接在 User 類中完成,如下:

public class User extends RepresentationModel {
    private Integer id;
    private String username;
    private String address;

    public User(Integer id) {
        super(WebMvcLinkBuilder.linkTo(UserController.class).slash(id).withSelfRel());
        this.id = id;
    }
    //省略 getter/setter
}

可以看到,直接在構造方法中完成即可。此時介面裡就不用那麼複雜了,如下:

@GetMapping
public CollectionModel<User> list() {
    List<User> list = new ArrayList<>();
    User u1 = new User(1);
    u1.setUsername("javaboy");
    u1.setAddress(");
    list.add(u1);
    User u2 = new User(2);
    u2.setUsername("itboy");
    u2.setAddress(");
    list.add(u2);
    CollectionModel<User> users = CollectionModel.of(list);
    users.add(WebMvcLinkBuilder.linkTo(UserController.class).withRel("users"));
    return users;
}

那麼對於根據 ID 來查詢使用者的需求,我們也應該給一個介面如下:

@RestController
@RequestMapping("/users")
public class UserController {

    @GetMapping("/{id}")
    public EntityModel<User> getOne(@PathVariable Integer id) throws NoSuchMethodException {
        User u = new User(id);
        u.setUsername("javaboy");
        u.setAddress("深圳");
        u.add(Link.of("+id, "getOne"));
        Link users = WebMvcLinkBuilder.linkTo(UserController.class).withRel("users");
        u.add(users);
        Link link = WebMvcLinkBuilder.linkTo(UserController.class).slash(u.getId()).withSelfRel();
        u.add(link);
        Method method = UserController.class.getMethod("getOne", Integer.class);
        Link link2 = WebMvcLinkBuilder.linkTo(method, id).withSelfRel();
        u.add(link2);
        return EntityModel.of(u);
    }
}

關於這個介面,我說如下幾點:

  1. 如果返回型別是一個物件的話,需要使用 EntityModel<User> 型別。
  2. 搞好返回的物件之後,透過 EntityModel.of(u) 方法可以獲取到目標資料型別。
  3. 這個地方,為了給小夥伴們演示不同的 Link 新增方式,我寫了好多個(單純為了演示不同的 Link 新增方式):
    1. Link.of("+id, "getOne") 這種是自己純手工去生成當前物件的訪問連結,很明顯這不是一個很好的方案。當前物件的訪問連結建議使用上文中提到的方式。
    2. WebMvcLinkBuilder.linkTo(UserController.class).withRel("users") 這個是生成當前這個 Controller 的訪問連結,一般就是訪問所有使用者物件的連結。
    3. WebMvcLinkBuilder.linkTo(UserController.class).slash(u.getId()).withSelfRel() 前文已經用過了,不多說了,實際應用中建議使用這種。
    4. 也可以根據某一個方法自動生成,像這樣 WebMvcLinkBuilder.linkTo(method, id).withSelfRel(),這個是生成某一個具體方法的訪問連結。

好了,現在我們來看下這個介面生成的 JSON,如下:

到底什麼樣的 REST 才是最佳 REST?

生成的這段 JSON 我將之標記為了三部分:

  1. 第一部分,self,就是自身的訪問連結,這三個連結分別是 User 的構造方法,以及前面提到的 3.3 和 3.4 的方法生成的。
  2. 第二部分,getOne 這個,是前面 3.1 中提到的方法生成的。
  3. 第三部分,users 這個,是前面提到的 3.2 方法生成的。

當然,其實這塊還有很多其他的生成連結的玩法,但是我就不一一介紹了,小夥伴們可以參考官方文件:

  • https://docs.spring.io/spring-hateoas/docs/current/reference/html

從上面 Spring HATEOAS 中返回的 JSON 我們大致上可以看到它的特點:

當我們使用了 Spring HATEOAS,此時,客戶端就會透過服務端返回的 Link Rel 來獲取請求的 URI(如果沒有使用 Spring HATEOAS,則客戶端訪問的 URI 都是提前在客戶端硬編碼的),現在我們就可以做到服務端在不破壞客戶端實現的情況下動態的完成 URI 的修改,從而進一步解耦客戶端和服務端。

簡而言之,現在客戶端能幹什麼事情,在服務端返回的 JSON 中都會告訴客戶端,客戶端從服務端返回的 JSON 中獲取到請求的 URL,然後直接執行即可。如果這個請求地址發生變化的話,客戶端也會及時拿到最新的地址。

可能上面的例子小夥伴們感受還不是很明顯,我再給大家看一段 JSON:

{
    "tracking_id""666",
    "status""WAIT_PAYMENT",
    "items": [
        {
            "name""book",
            "quantity"1
        }
    ],
    "_Links": {
        "self": {
            "href""
        },
        "cancel": {
            "href""
        },
        "payment": {
            "href""
        }
    }
}

這是電商系統下單之後等待支付的過程中返回的 JSON,這裡的 links 給出了三個:

  • self:訪問這個連結可以檢視當前訂單資訊(GET 請求)。
  • cancel:訪問這個連結可以取消當前訂單(DELETE 請求)。
  • payment:訪問這個連結可以支付當前訂單(POST 請求)。

這個例子就很直白了,就是在返回的 JSON 中,直接告訴你接下來能做哪些操作,對應的 URL 分別是什麼,前端拿到之後直接操作,如果這些操作路徑發生了變化,前端也會立馬拿到最新的路徑。

這就是 Spring HATEOAS 的好處。總之一句話,Spring HATEOAS 提倡在響應返回的 Link 中給出對該資源接下來操作的 URL。這種方式解耦了服務端 URI,也可以讓客戶端開發者更容易地探索 API。

3. REST 的優缺點

雖然我們現在都鼓勵設計 REST 風格的 API,然而 REST 也不全是優點,事物總是具有兩面性,REST 的優缺點分別如下。

3.1 優點

  1. 首先,REST 足夠簡單,有一定 Web 開發經驗的小夥伴都可以快速上手 REST。
  2. REST 風格的介面測試起來也非常方便,利用瀏覽器自帶的一些 REST 外掛或者是 POSTMAN 之類的工具,就可以非常方便的實現 REST 介面的測試。
  3. 不需要中間代理,簡化了系統的結構。
  4. HTTP 對防火牆比較友好。

3.2 缺點

  1. REST 只支援請求-響應的通訊方法,不支援服務端推送訊息到客戶端。
  2. 給請求取一個合適的名字比較困難,特別是有多個相類似的介面時,例如有多個新增介面、多個更新介面等。
  3. 由於沒有中間代理,所以請求/響應的時候,服務端和客戶端都必須線上。

好啦,跟小夥伴們聊了 REST 和 Spring HATEOAS,感興趣的小夥伴可以去試試哦~

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70024922/viewspace-2931346/,如需轉載,請註明出處,否則將追究法律責任。

相關文章