天天看點

Java RESTful Web Service實戰(第2版) 2.1 統一接口

<b>2.1 統一接口</b>

rest服務和rpc服務在接口定義上的差別是:rest使用http協定的通用方法作為統一接口的标準詞彙,rest服務所提供的方法資訊都在http方法裡,而rpc服務所提供的方法資訊在soap/http信封裡(其封裝的格式通常是http或soap),每一個rpc式的web服務都會公布一套符合自己商業邏輯的方法詞彙。

閱讀指南

本節示例源代碼位址:https://github.com/feuyeux/jax-rs2-guide-ii/tree/master/2.simple-service-3。

相關包:com.example.annotation.method。

每一種http請求方法都可以從安全性和幂等性兩方面考慮,這對正确了解http請求方法和設計統一接口具有決定性的意義。換句話說,要定義嚴謹的rest統一接口,就需要真正了解http方法的安全性和幂等性。

安全性是指外系統對該接口的通路,不會使伺服器端資源的狀态發生改變;幂等性(idempotence)是指外系統對同一rest接口的多次通路,得到的資源狀态是相同的。

這裡讨論的安全性對應的英文是safety而不是security,系統安全請參考第10章。以下,将從rest統一接口的定義角度,逐個講述http方法。

<b>2.1.1 get方法</b>

rest使用http的get方法擷取服務提供的資源。get方法是隻讀的,那麼它是幂等和安全的嗎?答案馬上揭曉。

1. 幂等性和安全性

http的get方法用于讀取資源。get方法是幂等的,因為讀取同一個資源,總是得到相同的資料。get方法也是安全的,因為讀取資源不會對其狀态做改動。jax-rs2定義了@get注解對資源方法定義,使得該方法用于處理get請求。

值得注意的是,雖然get方法的特性是幂等和安全的,但這不意味着任何一個定義為處理get請求的方法都是幂等和安全的。換句話說,設計不良的api有可能違背get的特性,将一個不該是get的方法定義為之。

舉個例子,在系統b中設計一個rest的api,在用戶端調用時讀取系統a中x類型的資料,然後将a.x與系統b内的y類型資料做比較,如果兩個集合的内容、最後更新時間上有不同,需要執行同步資料,即将a.x追加或者更新到b.y中。最後,将同步結果資訊傳回給向系統b發起請求的用戶端,如圖2-1所示。

圖2-1 請求資源示意圖

從圖2-1左側部分乍看上去,這是一個擷取同步資訊的api,是以這個api的設計應該使用get請求方法。但是,稍作分析後即可知道該場景并不具備使用get的基本條件。因為同步過程中對系統b内的資源有寫操作的可能,是以不具備安全性;而寫的内容又不是每次相同,是以不具有幂等性。是以,這個例子應該定義的正确的請求方法是post。

2. 資源方法命名

不妨一起探讨一下圖2-1中的同步資訊的api該如何命名?既然是同步功能,那就以sync一類的字根作為字首,這樣所有的同步api都具有相同的開頭,字迹也很工整。遺憾的是,這樣的設計并不符合rest風格。筆者的了解是,從字面上看有兩個問題。第一,sync字根具有非名詞性的含義,從roa角度上看,sync是rpc風格的命名:動詞、自定義方法名稱。第二,這樣命名後,資源名稱從一個主語變成了賓語,從roa角度上看,面向的不再是資源,而是要執行的動作。

是以,标準的命名方式應該是單數的同步操作以資源名稱命名;批量的同步操作以資源名稱的複數名稱命名。比如這個api是用于同步裝置的,那麼命名可以使用device和devices。如果擔心與普通查詢業務資源位址混淆,可以在資源路徑中增加查詢或者路徑參數,比如device/id=1&amp;source=a_b、device/b/a/等。

3. 抽象層注解資源

jax-rs2的http方法注解可以定義在接口和pojo中,置于接口中的方法名更具抽象性和通用性。示例代碼如下。

@path("book")

public interface bookresource {

//關注點1:get注解從抽象類上移到接口

@get

public string getweight();

}

public class ebookresourceimpl implements

bookresource {

//關注點2:實作類無須get注解

@override

public string getweight() {

return "150m";

    }

public class gettest extends jerseytest {

protected application configure() {

//關注點3:加載的是實作類而不是接口

return new resourceconfig(ebookresourceimpl.class);

@test

public void testget() {

response response = target("book").request().get();

assert.assertequals("150m",

response.readentity(string.class));

    }

在這段代碼中,資源接口bookresource定義了一個get方法getweight(),這個方法使用了http方法注解@get,見關注點1。資源接口bookresource的實作類ebookresourceimpl實作了getweight()方法,但沒有再次使用@get注解。也就是說,在接口中抽象地定義了資源的請求方法類型後,其全部實作類都無須再定義。這使得編碼更整潔和抽象,見關注點2。最後,需要注意的是,在測試類gettest中注冊的是實作類ebookresourceimpl類型而不是接口bookresource類型,見關注點3。

另外,我們一并介紹下head方法和options方法。head方法和get方法相似,隻是伺服器端的傳回值不包括http實體。是以,head方法是安全的和幂等的。jax-rs2定義了@head注解來定義相關資源方法。options方法和get方法相似,是安全的和幂等的。options用于讀取資源所支援的(allow)所有http請求方法。jax-rs2定義了@options注解來定義相關資源方法。

<b>2.1.2 put方法</b>

put方法是一種寫操作的http請求。rest使用http的put方法更新或添加資源。下面講解一下put方法的作用和操作時的媒體類型。

1. 更新資源

因為rest隻是風格,不是技術規範或标準,是以有些實作rest的細節沒有明确的定義,這對實踐而言,不可避免會産生某些誤解。比如在建立和更新某個資源的時候,開發者比較迷茫的是何時該用http的put方法,何時該使用post方法。為了解決這一問題,我們首先應該知道put方法的特性。put方法是幂等的,即多次插入或者更新同一份資料,在伺服器端對資源狀态所産生的改變是相同的。put方法不是安全的,有寫動作的http方法都不是安全的。

我們知道,由于使用同一份資料向伺服器請求更新某一資源,得到的結果應該總是相同的,是以對于更新操作,使用put是沒有疑問的。可能讀者會想到最後更新時間字段每次送出會不同,但那已經不是同一份資料了。

2. 添加資源

建立操作通常每次得到的結果是不同的,因為伺服器端的業務層邏輯通常要求資料的主鍵字段要麼來自于業務平台自增一個邏輯值,要麼來自于資料庫的主鍵自增。是以,相同的資料每一次送出到伺服器端,都會為資料添加一個新的主鍵值,也就是建立一個主鍵值不同的新資源(如果沒有業務或者外鍵沖突)。是以,建立操作通常應當設計為post方法的api。唯有一種場景應當使用put方法來設計api,即用戶端在發起建立請求時,在同一份資料中總可以提供唯一的主鍵值,伺服器不會對其進行修改,這樣的建立請求確定了幂等性,不應再使用post方法。jax-rs2定義了@put注解來定義相關資源方法,示例代碼如下。

//關注點1:put方法

@put

//關注點2:資源方法定義了produces注解和consumes注解

@produces(mediatype.text_plain)

@consumes(mediatype.application_xml)

public string newbook(book book);

public class puttest extends jerseytest {

public static atomiclong clientbooksequence = new atomiclong();

public void testnew() {

final book newbook = new book(clientbooksequence.incrementandget(),

"book-" + system.nanotime());

mediatype contenttypemediatype = mediatype.application_xml_type;

mediatype acceptmediatype = mediatype.text_plain_type;

final entity&lt;book&gt; bookentity = entity.entity(newbook,

contenttypemediatype);

final string lastupdate =

target("book").request(acceptmediatype)

.put(bookentity, string.class);

//關注點3:資源方法定義了produces注解和consumes注解

assert.assertnotnull(lastupdate);

logger.debug(lastupdate);

在這段代碼中,資源接口bookresource使用@put注解定義了newbook()方法,即該方法用于處理相對資源路徑為"book"的put請求,見關注點1。單元測試類puttest對其功能性進行了驗證,對lastupdate使用非空斷言,lastupdate是更新方法newbook()的傳回實體的值,代表最後更新時間戳,見關注點3。我們注意到,newbook()方法上,同時定義了@produces(mediatype.text_plain)注解和@consumes(mediatype.application_xml)注解,見關注點2,下面我們來介紹一下與關注點2相關的媒體類型知識。

3. 媒體類型

put方法執行寫操作的非安全的http方法,需要考慮請求實體媒體類型和響應實體媒體類型。請求實體媒體類型使用http頭的content type定義,響應實體媒體類型使用http頭的accept定義。

在伺服器端,@consumes(mediatype.application_xml)定義了伺服器端要消費的媒體類型,即消費用戶端請求實體的媒體類型。@produces(mediatype.text_plain)定義了伺服器端生産的媒體類型,即伺服器産生的響應實體的媒體類型。用戶端在送出非安全性http請求方法前,在entity類的執行個體中,定義該entity執行個體的媒體類型,即用戶端請求實體的媒體類型。request方法用于定義可接受的http方法的傳回媒體類型,即伺服器的響應實體的媒體類型。

測試資源方法newbook(),将得到如下所示的請求頭資訊,從中可以看到請求媒體類型。

public final static string text_plain =

"text/plain";

public final static string application_xml

= "application/xml";

public final static mediatype

text_plain_type = new mediatype("text", "plain");

application_xml_type = new mediatype("application", "xml");

1 &gt; put http://localhost:9998/book

1 &gt; accept: text/plain

1 &gt; content-type: application/xml

在這段代碼中,javax.ws.rs.core.mediatype類是jax-rs2提供的媒體類型定義類,其中定義了包括示例中使用的mediatype.text_plain,其值為"text/plain"。在mediatype類中,對應的響應實體媒體類型定義為accept: text/plain;mediatype.application_xml值為"application/xml",對應的請求實體媒體類型定義為content-type: application/xml。

<b>2.1.3 delete方法</b>

delete方法是幂等的,即多次删除同一份資料(通常請求中傳遞的參數是資料的主鍵值),在伺服器端産生的改變是相同的。jax-rs2定義了@delete注解來定義相關資源方法。下面來看看具體示例。

執行删除的資源方法,其傳回值可以定義為void,即該方法沒有傳回值。之是以在删除資源的場景中可以采用這樣的方式定義,是因為删除的前提是對該資源資訊已經充分了解,沒有必要再将其從伺服器上傳遞回來,示例代碼如下。

@delete

public void delete(@queryparam("bookid") final long bookid);

在這段代碼中,無傳回值的資源方法delete()傳回的響應實體為空,http狀态碼為204。該定義可以參考jersey的源代碼中的response類,示例代碼如下。

package javax.ws.rs.core;

public abstract class response {

public interface statustype {

public enum status implements statustype {

no_content(204, "no content"),

接下來是删除資源方法的單元測試,示例代碼如下。

public class deletetest extends jerseytest

{

final response response

=target("book").queryparam("bookid", "9527")

.request().delete();

int status = response.getstatus();

logger.debug(status);

assert.assertequals(response.status.no_content.getstatuscode(), status);

在這段代碼中,對rest請求的測試斷言不是針對删除資源的實體,而是響應中http狀态碼。也就是說,删除資源方法的傳回值類型可以定義為void,業務邏輯更關注删除操作的結果狀态。

<b>2.1.4 post方法</b>

post方法是一種寫操作的http請求。rpc的所有寫操作均使用post方法,而rest隻使用http的post方法添加資源。

1. 既不幂等也不安全

定義為post的rest接口用于寫資料,post方法的特性是既不幂等也不安全。由于請求會改變伺服器端資源的狀态,是以它是不是安全的;由于每次請求對伺服器端資源狀态的改變并不是相同的,是以它不是幂等的。

2. 兩種分類

rest中使用的post可以稱之為post(a),即用于建立、添加資源的http方法。這是相對于rpc式的web服務中對post的使用而言的。

在rpc中使用的post可以稱之為post(p),即通過重載的post用于處理某種操作。伺服器接收post(p)的請求後,不是直接處理post請求,由于真正的方法資訊位于信封頭或實體主體裡,是以需要先解析出執行方法。

jax-rs2定義了@post注解來定義相關資源方法。示例代碼如下。

//關注點1:post方法

@post

@produces(mediatype.application_xml)

public book createbook(book book);

public class posttest extends jerseytest {

public void testcreate() {

final book newbook = new book("book-" + system.nanotime());

mediatype acceptmediatype = mediatype.application_xml_type;

final book book

=target("book").request(acceptmediatype).post(bookentity,

 book.class);

//關注點2:測試post方法的斷言

assert.assertnotnull(book.getbookid());

logger.debug("server id="+book.getbookid());

在這段代碼中,資源接口bookresource定義了createbook()方法,該方法使用@post注解,表示該方法處理"book"路徑下的post請求,見關注點1。在測試方法testcreate()中,關注請求結果實體的主鍵是否為空。這是因為在post請求送出的添加資源操作中,主鍵的設定是在伺服器端完成的,是以用戶端成功請求添加資源後,應關注伺服器端傳回的實體結果是否有主鍵資訊,見關注點2。

到此,我們完成了對http的基本方法的講述。除了http協定定義的标準方法,還存在來自其他協定中的http方法。接下來,我們一起探讨這些方法對rest服務的影響。

<b>2.1.5 webdav擴充方法</b>

webdav(web-based

distributed authoring and versioning,基于web的分布式創作與版本控制)是ietf的rfc4918規範(rfc2518規範的替代規範位址是http://tools.ietf.org/html/rfc4918),是對http1.1協定的一組擴充,該協定允許使用者以協作方式編輯和管理遠端web伺服器上的檔案。webdav在http方法的基礎上,增加了如下方法(詳見rfc4918第9章)。

propfind方法:用于從web資源中查詢存儲為xml格式的屬性資料,或者重載為從一個遠端系統中查詢目錄結構的資料。

proppatch方法:用于原子地更改和删除一個資源的多個屬性。

mkcol方法:用于建立目錄。

copy方法:用于将資源從一個uri資源位址複制到另一個uri資源位址。

move方法:用于将資源從一個uri資源位址移動到另一個uri資源位址。

lock 方法:用于鎖定一個資源。webdav支援共享鎖和獨占鎖。

unlock方法:用于解鎖一個資源。

雖然webdav對http方法做出了功能性擴充,使之提供更強大服務,但是從roa角度講,因為webdav在http标準方法的基礎上增加了特殊的方法名稱,webdav破壞了統一接口的原則。是以,對是否應該在rest式的web服務中支援webdav,業内的觀點并不一緻。

筆者的觀點是如果遵從roa,那麼就不使用http标準方法之外的方法。如果業務需求确實超出了标準方法所及,那麼可以使用如下注解實作對webdav的支援。jax-rs2規範沒有闡述對webdav提供支援的文字,但是jax-rs2定義了@httpmethod注解來定義相關的資源方法。在jersey應用中,可以使用@httpmethod注解定義http标準方法之外的方法名稱來支援webdav,示例代碼如下。

@target({elementtype.method})

@retention(retentionpolicy.runtime)

@httpmethod(value = "move")

@documented

public @interface move {

這段代碼是對@move注解的定義,使用@httpmethod注解定義了名為move的http擴充方法。有了擴充方法注解,我們就可以在資源類中定義新的方法來支援擴充方法的請求了,示例代碼如下。

@move

public boolean movebooks(books books);

在這段代碼中,資源類bookresource定義了movebooks方法,該方法使用@move注解定義,表示用于處理"book"路徑下的move請求。下面我們來看看相關的測試代碼。

public class httpmethodtest extends

jerseytest {

resourceconfig resourceconfig = new

resourceconfig(ebookresourceimpl.class);

return resourceconfig;

protected void configureclient(clientconfig clientconfig) {

//關注點1:定義grizzly連接配接器

clientconfig.connectorprovider(new grizzlyconnectorprovider());

super.configureclient(clientconfig);

public void testwebdav() {

//關注點2:http move請求

final response response =

target("book").request().method("move");

boolean result = response.readentity(boolean.class);

//關注點3:move方法測試斷言

assert.assertequals(boolean.true, result);

在這段代碼中,測試方法testwebdav()在請求中定義了move請求,見關注點2;斷言是針對move方法的傳回值,見關注點3;可以看出,使用jersey實作對webdav的支援并不困難。

需要注意的是,jersey預設的連接配接器隻支援http标準方法,是以要使用http的擴充方法就不能直接使用預設的連接配接器,這裡使用了grizzly連接配接器。對應的代碼行是:clientconfig.connectorprovider(new grizzlyconnectorprovider()),即為用戶端配置執行個體提供grizzly連接配接器,見關注點1。這行代碼是jersey2.5+後的寫法,jersey2.5之前的寫法是clientconfig.connector(new

grizzlyconnector(clientconfig))。從中可以看出,jersey在不斷優化中,包括api。這一好處是活躍的社群為使用者帶來越來越便捷、高效的使用體驗,缺點是破壞了向下相容性。

到此,我們全面掌握了http方法在rest統一接口定義中的作用和實作。明白了rest接口該使用什麼樣的請求方法非常重要,這決定了其性質。但是這還不夠,一個接口如何被請求唯一定位還需要深入掌握rest的資源定位。接下來一節将詳述資源定位的細節。