RESTful levels & HATEOAS
- 什麽是RESTful
- REST這個詞,是Roy Thomas Fielding在他2000年的博士論文中提出的。翻譯過來就是"表現層狀態轉化。”
Fielding在論文中將REST定位為“分布式超媒體應用(Distributed Hypermedia System)”的架構風格,它在文中提到一個名為“HATEOAS(Hypermedia as the engine of application state)”的概念。
HATEOAS又是什麽鬼?
我們知道REST是使用標準的HTTP方法來操作資源的,但僅僅因此就理解成帶CURD的Web數據庫架構就太過於簡單了。 這種說法忽略了一個核心概念: “超媒體即應用狀態引擎(hypermedia as the engine of application state)
要達到這個目的,就要求在表述格式裏邊加入鏈接來引導客戶端。在《RESTFul Web Services》一書中,作者把這種具有鏈接的特性成為連通性。
RESTful API最好做到Hypermedia,或HATEOAS,即返回結果中提供鏈接,連向其他API方法,使得用戶不查文檔,也知道下一步應該做什麽。比如,當用戶向api.example.com的根目錄發出請求,會得到這樣一個文檔。
{"link": {
"rel": "collection https://www.example.com/zoos",
"href": "https://api.example.com/zoos",
"title": "List of zoos",
"type": "application/vnd.yourformat+json"
}}
上面代碼表示,文檔中有一個link屬性,用戶讀取這個屬性就知道下一步該調用什麽API了。rel表示這個API與當前網址的關系(collection關系,並給出該collection的網址),href表示API的路徑,title表示API的標題,type表示返回類型。
Hypermedia API的設計被稱為HATEOAS。Github的API就是這種設計,訪問api.github.com會得到一個所有可用API的網址列表。
{
"current_user_url": "https://api.github.com/user",
"authorizations_url": "https://api.github.com/authorizations",
// ...
}
從上面可以看到,如果想獲取當前用戶的信息,應該去訪問api.github.com/user,然後就得到了下面結果。
{
"message": "Requires authentication",
"documentation_url": "https://developer.github.com/v3"
}
以上內容都摘自阮一峰和其它作者博客,如有冒犯,請及時告知
我當時第一眼看到HATEOAS也是一臉懵逼,因為在Spring依賴中看到過這個詞,所以就留意了一下。其實在我看來,HATEOAS是符合RESTful規範的一個方面,客戶端在消費RESTful服務的時候,出了得到資源本身以外,還可以得到一些相關其他信息,比如,其他相關鏈接,返回類型等等。
2.構建RESTful服務最佳實踐
- 第一條也是最容易犯錯的:<u>URI中不應該包含動詞</u>。 因為"資源"表示一種實體,所以應該是名詞,URI不應該有動詞,動詞應該放在HTTP協議中。舉例來說,某個URI是/posts/show/1,其中show是動詞,這個URI就設計錯了,正確的寫法應該是/posts/1,然後用GET方法表示show。如果某些動作是HTTP動詞表示不了的,你就應該把動作做成一種資源。比如網上匯款,從賬戶1向賬戶2匯款500元,錯誤的URI是:
POST /accounts/1/transfer/500/to/2
,正確的寫法是把動詞transfer改成名詞transaction,然後以參數的方式註明其它參數
POST /accounts/transaction?from=1&to=2&amount=500.00
- RESTful API最好做到Hypermedia(HATEOAS),即返回結果中提供鏈接,連向其他API方法,使得用戶不查文檔,也知道下一步應該做什麽。
- 其它需要註意的地方參見文末貼出的鏈接
下面重頭戲來了:
3.使用SpringBoot構建符合Hypermedia規範的RESTful 服務
我以後每次都要說一遍:SpringBoot框架是所有Java開發者的福音
在SpringBoot中構建符合Hypermedia規範的RESTful 服務簡單到不能再簡單-----只需要添加一條依賴:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-rest</artifactId>
</dependency>
添加一個簡單的領域類:
@Entity
public class Person {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private long id;
private String firstName;
private String lastName;
//getter and setter
}
以及一個dao層接口:
//@RepositoryRestResource(collectionResourceRel = "people",path="people")
public interface PersonRepository extends PagingAndSortingRepository<Person,Long>{
List<Person> findByLastName(@Param("name") String name);
}
註釋掉的標簽可選,主要是在只用RESTful的時候可以改變URI,比如,加上此處就把/person
變成/people
一切都和我們正常開發web沒啥區別,但是現在,見證奇跡的時刻到了:
1.GET localhost:8080
返回:
{
"_links":{
"people":{
"href": "http://localhost:8080/people{?page,size,sort}",
"templated": true
},
"profile":{
"href": "http://localhost:8080/profile"
}
}
}
上面的返回中包括了people這個資源的鏈接明確指出了我們可以用類似http://localhost:8080/people?page=1&size=10&sort=firstname
這樣的方式請求資源。
2.增加一個people資源:POST localhost:8080/people
,請求數據用json{ "firstName" : "李", "lastName" : "雷" }
返回:
{
"firstName": "李",
"lastName": "雷",
"_links":{
"self":{
"href": "http://localhost:8080/people/5"
},
"person":{
"href": "http://localhost:8080/people/5"
}
}
}
返回信息中出了新加入信息的各個字段,還有一個href鏈接指向它--這是合乎情理的,客戶端總是想要看看新加入的這條信息長什麽樣,從這個角度說,這條返回信息還是很貼心的。
3.GET localhost:8080/people/
{
"_embedded":{
"people":[
{
"firstName": "李",
"lastName": "雷",
"_links":{"self":{"href": "http://localhost:8080/people/5" }, "person":{"href": "http://localhost:8080/people/5"…}
}
]
},
"_links":{
"self":{
"href": "http://localhost:8080/people"
},
"profile":{
"href": "http://localhost:8080/profile/people"
},
"search":{
"href": "http://localhost:8080/people/search"
}
},
"page":{
"size": 20,
"totalElements": 1,
"totalPages": 1,
"number": 0
}
}
返回一個people列表,包含所有數據
page標簽的出現,是由於我們的repository繼承了PagingAndSortingRepository接口
search標簽的出現,是由於我們的repository聲明了一個方法,List<Person> findByLastName(@Param("name") String name);
這個方法可以像search標簽描述的那樣調用:http://localhost:8080/people/search/findByLastName{?name}
,示例:http://localhost:8080/people/search/findByLastName?name=雷
4.PUT localhost:8080/people/5
,json:{ "firstName" : "李", "lastName" : "小雷" }
{
"firstName": "李",
"lastName": "小雷",
"_links":{
"self":{
"href": "http://localhost:8080/people/5"
},
"person":{
"href": "http://localhost:8080/people/5"
}
}
}
資源被正確更新
5.PATCH localhost:8080/people/5
,json:{ "lastName" : "大雷" }
{
"firstName": "李",
"lastName": "大雷",
"_links":{
"self":{
"href": "http://localhost:8080/people/5"
},
"person":{
"href": "http://localhost:8080/people/5"
}
}
}
RESTful levels & HATEOAS