最近在使用Spring Boot構(gòu)建RESTful服務的時候遇到了一些不清楚的地方,查閱資料后寫下此文,試圖大致解釋清楚什么是RESTful,什么是正確的RESTful,以及怎么在SpringBoot中定義和使用RESTful。
- 什么是RESTful
- REST這個詞,是Roy Thomas Fielding在他2000年的博士論文中提出的。翻譯過來就是"表現(xiàn)層狀態(tài)轉(zhuǎn)化。”

Fielding在論文中將REST定位為“分布式超媒體應用(Distributed Hypermedia System)”的架構(gòu)風格,它在文中提到一個名為“HATEOAS(Hypermedia as the engine of application state)”的概念。
HATEOAS又是什么鬼?
我們知道REST是使用標準的HTTP方法來操作資源的,但僅僅因此就理解成帶CURD的Web數(shù)據(jù)庫架構(gòu)就太過于簡單了。 這種說法忽略了一個核心概念: “超媒體即應用狀態(tài)引擎(hypermedia as the engine of application state)”。<u> 超媒體是什么? 當你瀏覽Web網(wǎng)頁時,從一個連接跳到一個頁面,再從另一個連接跳到另外一個頁面,就是利用了超媒體的概念: 把一個個把資源鏈接起來。</u>
要達到這個目的,就要求在表述格式里邊加入鏈接來引導客戶端。在《RESTFul Web Services》一書中,作者把這種具有鏈接的特性成為連通性。
RESTful API最好做到Hypermedia,或HATEOAS,即返回結(jié)果中提供鏈接,連向其他API方法,使得用戶不查文檔,也知道下一步應該做什么。比如,當用戶向api.example.com的根目錄發(fā)出請求,會得到這樣一個文檔。
{"link": {
"rel": "collection https://www.example.com/zoos",
"href": "https://api.example.com/zoos",
"title": "List of zoos",
"type": "application/vnd.yourformat+json"
}}
上面代碼表示,文檔中有一個link屬性,用戶讀取這個屬性就知道下一步該調(diào)用什么API了。rel表示這個API與當前網(wǎng)址的關(guān)系(collection關(guān)系,并給出該collection的網(wǎng)址),href表示API的路徑,title表示API的標題,type表示返回類型。
Hypermedia API的設(shè)計被稱為HATEOAS。Github的API就是這種設(shè)計,訪問api.github.com會得到一個所有可用API的網(wǎng)址列表。
{
"current_user_url": "https://api.github.com/user",
"authorizations_url": "https://api.github.com/authorizations",
// ...
}
從上面可以看到,如果想獲取當前用戶的信息,應該去訪問api.github.com/user,然后就得到了下面結(jié)果。
{
"message": "Requires authentication",
"documentation_url": "https://developer.github.com/v3"
}
以上內(nèi)容都摘自阮一峰和其它作者博客,如有冒犯,請及時告知
我當時第一眼看到HATEOAS也是一臉懵逼,因為在Spring依賴中看到過這個詞,所以就留意了一下。其實在我看來,HATEOAS是符合RESTful規(guī)范的一個方面,客戶端在消費RESTful服務的時候,出了得到資源本身以外,還可以得到一些相關(guān)其他信息,比如,其他相關(guān)鏈接,返回類型等等。
2.構(gòu)建RESTful服務最佳實踐
- 第一條也是最容易犯錯的:<u>URI中不應該包含動詞</u>。 因為"資源"表示一種實體,所以應該是名詞,URI不應該有動詞,動詞應該放在HTTP協(xié)議中。舉例來說,某個URI是/posts/show/1,其中show是動詞,這個URI就設(shè)計錯了,正確的寫法應該是/posts/1,然后用GET方法表示show。如果某些動作是HTTP動詞表示不了的,你就應該把動作做成一種資源。比如網(wǎng)上匯款,從賬戶1向賬戶2匯款500元,錯誤的URI是:
POST /accounts/1/transfer/500/to/2,正確的寫法是把動詞transfer改成名詞transaction,然后以參數(shù)的方式注明其它參數(shù)
POST /accounts/transaction?from=1&to=2&amount=500.00
- RESTful API最好做到Hypermedia(HATEOAS),即返回結(jié)果中提供鏈接,連向其他API方法,使得用戶不查文檔,也知道下一步應該做什么。
- 其它需要注意的地方參見文末貼出的鏈接
下面重頭戲來了:
3.使用SpringBoot構(gòu)建符合Hypermedia規(guī)范的RESTful 服務
我以后每次都要說一遍:SpringBoot框架是所有Java開發(fā)者的福音
在SpringBoot中構(gòu)建符合Hypermedia規(guī)范的RESTful 服務簡單到不能再簡單-----只需要添加一條依賴:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-rest</artifactId>
</dependency>
添加一個簡單的領(lǐng)域類:
@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
一切都和我們正常開發(fā)web沒啥區(qū)別,但是現(xiàn)在,見證奇跡的時刻到了:
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,請求數(shù)據(jù)用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列表,包含所有數(shù)據(jù)
page標簽的出現(xiàn),是由于我們的repository繼承了PagingAndSortingRepository接口
search標簽的出現(xiàn),是由于我們的repository聲明了一個方法,List<Person> findByLastName(@Param("name") String name);這個方法可以像search標簽描述的那樣調(diào)用: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"
}
}
}
資源也被更新了。
PUT和PATCH有什么區(qū)別?
PUT相當于整體替換,也就是說,如果我把上面PATCH換成PUT,(我們注意到傳過去的參數(shù)中沒有firstname),那么資源對象的firstname將為空:
{
"firstName": null,
"lastName": "大雷",
....
}
而PATCH則沒有這個問題,---只改該改的
所以,數(shù)據(jù)庫的update操作最好用PATCH,但是這個方法也有一個問題,一些老舊瀏覽器不支持,什么時候用,自己權(quán)衡吧。
DELETE 就略了
這篇文章到這里就收尾了。

我相信各位看官和我一樣,有一個奇怪的問題縈繞在心頭。。。。。
where is controller?
where is controller?
where is controller?
從頭至尾我們并沒有寫任何控制器,這不科學??!
這恰恰體現(xiàn)了RESTful的思想,我們要的是資源,不是服務。所以我們只要規(guī)定好怎么獲取資源就行了,其它的萬能的SpringBoot已經(jīng)幫我們做了,看到這里有沒有對Sping有了森森的感激之情?
Spring你這是要逆天啊,不用寫三層,不用寫業(yè)務邏輯也就完了,你現(xiàn)在連controller也不讓我寫了,作為一個程序猿我還有什么樂趣???
實際上這也是將來的趨勢,現(xiàn)在無后端應用越來越多,比如基于firebase的,以后的后端程序猿不是寫三層了,應該重點關(guān)注服務器性能優(yōu)化、分布式計算方面,三層啊數(shù)據(jù)庫啊這些事就交給框架自動去完成吧,保證又快又好。
參考文章: