天天看點

Java RESTful Web Service實戰(第2版) 2.2 資源定位

<b>2.2 資源定位</b>

rest使用uri實作資源定位,從這個角度上講,對外提供rest式的web服務的接口就是公布一系列的uri及其參數,這使得rest的實踐過程簡單到了極緻。但是uri形式上的簡單并不意味着我們可以将uri的定義信手拈來,正所謂“沒有規矩,不成方圓”。

在設計rest式的web服務過程中,資源位址的設計是非常嚴謹的,如果設計不得體,不僅rest接口的風格無法統一,使系統的擴充性和易用性降低,也很難實作資源準确地被定位。

資源位址的設計過程是面向資源的,資源名稱應是準确描述該資源的名詞,資源位址應具有直覺的描述性。比如一個班級的資源位址可以是:學校/學院/學級/班級。值得注意的是一個uri資源位址唯一對應一個資源,但是一個資源可以擁有多個uri資源位址。比如jersey最新版本的文檔位址和jersey2.7版本的文檔位址指向同一個資源(本書寫作時)。

閱讀指南

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

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

<b>2.2.1 資源位址設計</b>

資源位址的設計對整個rest式的web服務至關重要,涉及系統的可用性、可維護性和可擴充性等諸多方面的表現。是以,本節關注如何對資源位址進行設計。

1. 資源路徑概覽

資源位址的路徑變量是用來表達邏輯上的層次結構的,資源和子資源的形式是自左至右、斜杠分割的名詞。它們的關系可以是從整體到局部,比如學校到班級,城市到鄉鎮;也可以是從一般到具體,比如一個生物的“門、綱、目、科、屬、種”的資源路徑。資源位址具體可以分為5個部分,以scheme://host:port/path?querystring為例,如表2-1所示。

表2-1 資源位址路徑分解

元素         描  述

scheme    協定名稱,通常是http和https

host (dns)主機名稱或者ip位址

port 服務端口

path 資源位址,使用“/”符号來分隔邏輯上的層次結構

?       用來分隔資源位址和查詢字元串符号

querystring      查詢字元串,方法作用域資訊

使用“&amp;”符号來分隔查詢條件

使用逗号分隔有次序的作用域資訊

使用分号分隔無次序的作用域資訊

一個典型的uri如表2-1所示,包括協定名稱、主機名稱、服務端口、資源位址和查詢字元串等5個部分。其中資源位址部分,根據具體部署的不同或有差别,如圖2-2所示。

圖2-2 資源位址示例

圖2-2中,通常使用contextpath、servletpath和pathinfo來細分資源位址。contextpath是上下文名稱,通常和部署伺服器的配置或者rest服務的web.xml配置有關。servletpath是servlet的名稱,與rest服務中定義的@applicationpath注解或者web.xml的配置有關。jax-rs2定義了@path注解來定義資源位址。pathinfo是資源路徑資訊,與資源類、子類以及類中的方法定義的@path注解有關。

現在我們對資源位址的層次結構有了認識,此時需要思考一個問題:資源位址是否可以唯一定位一個資源?

答案是否定的。資源位址相同,但http方法不同的兩個方法是兩個不同的rest接口。http方法和資源位址結合在一起才可以完成對資源的定位。細心的讀者也許已經從3.1節的示例中看出端倪。示例中,get方法用于讀取/檢索、查詢/過濾一個資源,put方法用于修改/更新資源、建立用戶端維護主鍵資訊的資源,delete方法用于删除資源,post方法用于建立資源。但這些方法的資源位址是相同的,都是"book"。

當上述的标準http方法無法滿足業務需求時,比如對于圖書資源,除了基本的crud之外,若需要公布像借閱、折舊、電子版下載下傳等實際生活中的更新操作的接口時,單單公布一個put方法就不夠用了。這些操作是動詞性的,無法簡單地使用一個book名詞定位。在路徑變量難以準确描述的情況下,一種方案是可以考慮使用動詞作為查詢參數;另一種方案是可以在rest設計過程中引入rpc風格的post方法,輔助完成複雜業務的接口設計,這就是rest和rpc混合型的web服務了。

2. 資源位址和作用域

在路徑變量裡可以使用标點符号以輔助增強邏輯清晰性。這些輔助符号用在表2-2中的查詢字元串,作為資源位址的查詢變量,用來表達算法的輸入,實作對方法的作用域的限制。下面來逐一講述這些對資源位址設計至關重要的符号。

(1)問号(?)是用來分隔資源位址和查詢字元串的,與符号(&amp;)是用來分隔查詢條件的參數的。示例代碼如下。

get /books?start=0&amp;size=10

這行代碼中的作用是查詢圖書清單,開始行參數為0,條目參數為10,即從第0行開始取10條并傳回該圖書清單。

(2)逗号(,)是用來分隔有次序的作用域資訊。需要注意的是逗号分隔的邏輯上的順序資訊,這種順序可以是約定俗成的,比如先寫經度後寫緯度;也可以是系統約定的,比如月、日、年的順序等。舉例來說,按時間區間查詢圖書,日期資訊在資源位址中是采用月、年順序,示例如下。

get /books/01,2002-12,2014

這行代碼中的作用是查詢2002年1月到2014年12月這個時間段(出版)的圖書。這個例子中還使用了連字元(-),有時候也可以使用下橫線(_)來做邏輯上的輔助分隔。

(3)分号(;)是用來分隔無次序的作用域資訊。通常這些資訊是邏輯上并列存在的,比如并列的查詢條件,示例如下所示。

get /books/restful;program=java;type=web

這行代碼中的作用是查詢滿足圖書内容為restful的、使用的程式設計語言是java的、講述的類型是web的圖書清單。這樣的邏輯沒有順序,互換順序的查詢條件不會影響資源的表述。

基于上述理論,這裡抛磚引玉,列出常用的資源位址設計示例如表2-2所示。

表2-2 資源位址設計

功能         資源位址

添加/建立        post /books

put /books/{id}

删除         delete /books/{id}

修改/更新        put /books/{id}

查詢全部         get /books http1.1

主鍵查詢         get /books/{id}

http1.1

get /books?id=12345678

分頁作用域查詢     get

/books?start=0&amp;size=10

get /books?limit=100&amp;sort=bookname

如果讀者可以輕松領會表2-2列出的這些典型的rest接口和資源定位的設計,就可以放手實作了,否則建議回顧本節内容。接下來,我們完成從設計到實作的跨越,看看jax-rs2标準是如何通過注解來支援資源定位的,并使用jersey完成上述設計的實踐。

<b>2.2.2 @queryparam注解</b>

查詢條件決定了方法的作用域,查詢參數組成了查詢條件。jax-rs2定義了@queryparam注解來定義查詢參數,本節使用@queryparam示範3個rest查詢接口的實作示例如表2-3所示。

表2-3 @queryparam示例清單

接口描述         資源位址

分頁查詢清單資料         /query-resource/yijings?start=24&amp;size=10

排序并分頁查詢清單資料     /query-resource/sorted-yijings?limit=5&amp;sort=pronounce

查詢單項資料         /query-resource/yijing?id=8

1. 分頁查詢

分頁查詢是使用@queryparam解析參數的基本示例,實作代碼如下所示。

public yijings

getbypaging(@queryparam("start")final int start,

@queryparam("size")final int

size){//關注點1:資源方法入參

...

int listsize = globallist.size();

final int max = size &gt; listsize ? listsize : size;

//關注點2:分頁疊代邏輯

for(int i = 0, index = start; i &lt; max; i++) {

final yijing yijing = globallist.get(index + i);

//關注點3:添加link以保證rest的連通性

final uri location = ub.clone().queryparam("id",

yijing.getsequence()).build();

final link link =

new link("detail",

location.toasciistring(), mediatype.application_xml);

links.add(link);

yijings.add(yijing);

    }

result.setlinks(links);

result.setguas(yijings);

return result;

}

在這段代碼中,getbypaging()方法的輸入參數包含了2個使用@queryparam注解定義的查詢參數,分别是起始條目參數"start"和條目數量參數"size",參數的類型是整型,見關注點1。在查詢的疊代中使用這兩個參數擷取圖書清單,見關注點2。在疊代中,每個圖書資源條目的uri都存儲在傳回值中,以保證資源的聯通性,見關注點3。該uri被封裝到link執行個體中,在單項查詢時使用。

另外,參數的定義使用了final,符合checkstyle的程式設計風格,即輸入參數隻作為邏輯算法的依據使用,其本身不會在這過程中被修改。也許這種不變的變量對提高執行效率并沒有多少影響,但跬步積千裡、蟻穴潰長堤。推薦java開發者在rest開發中引入sonarqube平台或者單純使用checkstyle工具對靜态代碼進行品質檢測,以幫助我們改進代碼的品質。

2. 排序查詢

排序查詢是在解析參數的基礎上,額外處理結果集順序的示例,代碼如下。

getbyorder(@queryparam("limit") final int limit,

@queryparam("sort") final string sortname)

{//關注點1:資源方法入參

collections.sort(list, new

comparator&lt;yijing&gt;() {

@override

//關注點2:排序中的比較算法

public int compare(final yijing o1, final yijing o2) {

switch (sortname) {

case "sequence":

                return o1.getsequence().compareto(o2.getsequence());

case "name":

                return

o1.getname().compareto(o2.getname());

case "pronounce":

o1.getpronounce().compareto(o2.getpronounce());

return 0;

});

在這段代碼中,limit參數的用途同分頁查詢示例,而sortname參數則用于排序,見關注點1;排序接口需要額外解析sortname傳遞的排序字段,并将其作為資料庫查詢語句中的排序參數使用。這裡實作了comparator接口的compare()方法來完成根據不同字段對集合的排序,見關注點2。

3. 單項查詢

用戶端在獲得結果集的基礎上,根據表述中連結資訊,向伺服器發起單項查詢的示例,代碼示例如下所示。

public yijing

getbyquery(@queryparam("id") final int seqid) {

return paramcache.find("" + seqid);

在這段代碼中,使用@queryparam定義了"id"參數,該參數來自分頁查詢中傳回的uri資訊。

注解queryparam可以和注解defaultvalue一起使用。注解defaultvalue的作用是預置一個預設值,當請求中不包含此參數時使用,示例如下。

@defaultvalue("100") @queryparam("size")

final integer pagesize

這句話的意思是當請求中不包含分頁參數pagesize時,分頁參數pagesize的預設值為100。

<b>2.2.3 @pathparam注解</b>

jax-rs2定義了@pathparam注解來定義路徑參數—每個參數對應一個子資源,本節使用@pathparam完成如表2-4所示的rest查詢接口。

表2-4 @pathparam示例清單

基本路徑參數         /path-resource/eric

結合查詢參數         /path-resource/eric?hometown=buenos

aires

帶有标點符号的資源路徑     /path-resource/199-1999

/path-resource/01,2012-12,2014

子資源變長的資源路徑         /path-resource/asia/china/northeast/liaoning/shenyang/huangu

/path-resource/q/restful;program=java;type=web

/path-resource/q2/restful;program=java;type=web

1. @path注解

jax-rs2定義了@path注解來定義資源路徑,@path接收一個value參數來解析資源路徑位址。該參數除了前面示例中的books這種靜态定義的方式外,也可以使用動态變量的方式,其格式為:{參數名稱:正規表達式}。這個接口的功能和查詢參數實作的/query-resource/yijings?start=24&amp;size=10相似,也是用于分頁查詢,其資源位址形如:/path-resource/199-1999,參考示例如下。

@get

@path("{from:\\d+}-{to:\\d+}")

public string

getbycondition(@pathparam("from") final integer from,

@pathparam("to") final integer

to) {

在這段代碼中,使用@pathparam注解定義的兩個參數from和to用以定義查詢區間,正規表達式部分是\d+,表示數字。兩個參數中間的連接配接符(-)是路徑的格式資訊。稍顯複雜的例子是:/path-resource/01,2012-12,2014,引入了逗号(,)作為有順序的日期分隔符号,那麼對應的正規表達式為:@path("{beginmonth:\\d+},{beginyear:\\d+}-{endmonth:\\d+},{endyear:\\d+}")

2. 正規表達式

正規表達式的講述超出了本書範圍,這裡隻簡述示例中用到的正規表達式。剛剛的例子中的\\d+,代表參數應為數字并且至少出現一次。第一個反斜杠是java中的轉義字元,第二個反斜杠是正規表達式的起始,加号(+)是至少出現一次的意思,星号(*)則代表出現至少零次,句号(.)是比對任何字元,d是比對數字,w是比對數字和字母。我們有的放矢,示例中使用的正規表達式如表2-5所示,讀者掌握所列的路徑含義即可,我們的目的是學習rest api設計,而非正則本身。

表2-5 正規表達式示例

正規表達式     含  義

[a-za-z][a-za-z_0-9]*       以字母開頭,後面是零到多個“字母_數字”格式的字元組合

{region:.+}/{district:\w+} region變量至少包含一個任意字元。

district變量至少包含一個為數字或者字母的字元

3. 路徑配查詢

查詢參數和路徑參數在一個接口中配合使用,可以更便捷地完成資源定位,這很像戰場上的多兵種協同作戰。前述的圖書資源的複雜設計就需要兩者結合來完成,示例代碼如下。

@path("{user:

[a-za-z][a-za-z_0-9]*}")

@produces(mediatype.text_plain)

getuserinfo(@pathparam("user") final string user,

@defaultvalue("shen

yang")@queryparam("hometown") final string hometown) {

return user + ":" + hometown;

在這段代碼中,路徑參數user中使用了通配符,方法參數中同時使用@pathparam注解和@queryparam,定義了user和hometown兩個參數。以資源位址:/path-resource/eric?hometown=buenos aires為例,rest容器會将該請求比對到getuserinfo()方法,其中eric是路徑變量user的值,buenos

aires作為查詢變量hometown的值。

4. 路徑區間

路徑區間(pathsegment)是對資源位址更靈活的支援,使資源類的一個方法可以支援更廣泛的資源位址的請求。我們從下面定義的資源位址清單來走近pathsegment。

/path-resource/asia/china/northeast/liaoning/shenyang/huangu

/path-resource/china/northeast/liaoning/shenyang/tiexi

/path-resource/china/shenyang/huangu

如上所示的資源位址中含有固定子資源(shenyang)和動态子資源兩部分。對于動态比對變長的子資源資源位址,pathsegment類型的參數結合正規表達式将大顯身手,示例代碼如下。

@path("{region:.+}/shenyang/{district:\\w+}")

getbyaddress(@pathparam("region") final list&lt;pathsegment&gt;

region,

@pathparam("district") final

string district) {

final stringbuilder result = new stringbuilder();

for (final pathsegment pathsegment : region) {

result.append(pathsegment.getpath()).append("-");

result.append("shenyang-" + district);

在這段代碼中,getbyaddress()方法用來比對表的這些資源位址。該方法的region變量是pathsegment類型的數組,以比對至少出現一個字元的正規表達式(+)。pathsegment如其名字所示,是路徑的片段,是子資源的集合。周遊pathsegment集合,對于每一個pathsegment執行個體,可以通過調用其getpath()方法擷取子資源名稱。

對于查詢參數動态給定的場景,可以定義pathsegment作為參數類型,通過getmatrix-parameters()方法擷取multivaluedmap類型的查詢參數資訊,即可将參數條件作為一個整體解析,示例代碼如下。

@path("q/{condition}")

getbycondition3(@pathparam("condition") final pathsegment condition)

{

final multivaluedmap&lt;string, string&gt; matrixparameters =

condition.getmatrixparameters();

final iterator&lt;entry&lt;string, list&lt;string&gt;&gt;&gt;

iterator =

matrixparameters.entryset().iterator();

while (iterator.hasnext()) {

final entry&lt;string, list&lt;string&gt;&gt; entry = iterator.next();

conds.append(entry.getkey()).append("=");

conds.append(entry.getvalue()).append(" ");

return conds.tostring();

在這段代碼中,getbycondition3()方法隻有一個pathsegment類型的參數condition,該參數包含了查詢條件中攜帶的全部參數清單。舉例來說,資源位址為path-resource/q/restful;program=java;type=web的請求可以比對到getbycondition3()方法,其中,multivaluedmap類型的執行個體matrixparameters的值為[program=[java],

type=[web]]。

5. @matrixparam注解

上例中,通過程式設計方式,調用pathsegment類的getmatrixparameters()方法來擷取查詢參數資訊。還有一種方式是通過@matrixparam注解來逐一定義參數,即通過聲明方式來擷取,示例代碼如下。

@path("q2/{condition}")

getbycondition4(@pathparam("condition")

final pathsegment condition,

@matrixparam("program") final string program,

@matrixparam("type") final string type) {

return condition.getpath() + " program=[" + program + "]

type=[" + type + "]";

在這段代碼中,使用@matrixparam注解分别定義了"program"和"type"兩個參數。與上例相比,這段代碼更能清晰地表達可接收的參數名稱和類型,缺點是缺乏對請求資源位址更靈活的支援。

<b>2.2.4 @formparam注解</b>

jax-rs2定義了@formparam注解來定義表單參數,相應的rest方法用以處理請求實體媒體類型為content-type: application/x-www-form-urlencoded的請求,示例代碼如下。

@path("form-resource")

public class formresource {

@post

public string newpassword(

@defaultvalue("feuyeux") @formparam(formresource.user) final

string user,

@encoded @formparam(formresource.pw) final string password,

@encoded @formparam(formresource.npw) final string newpassword,

@formparam(formresource.vnpw) final string verification) {

在這段代碼中,newpassword()方法是@formparam注解定義了user等4個參數,這些參數是容器從請求中擷取并比對的。相關的用戶端測試如圖2-3所示。

圖2-3 表單示例

圖2-3所示的用戶端工具是postman(詳見2.6節),使用postman定義的基本表單資訊與newpassword()方法一緻。

newpassword()方法的測試代碼片段,示例代碼如下。

@test

public void testpost2() {

final form form = new form();

form.param(formresource.user, "feuyeux");

form.param(formresource.pw, "北京");

form.param(formresource.npw, "上海");

form.param(formresource.vnpw, "上海");

final string result = target("form-resource").request().

post(entity.entity(form,

mediatype.application_form_urlencoded_type), string.class);

formtest.logger.debug(result);

assert.assertequals("encoded should let it to disable

decoding",

"feuyeux:%e5%8c%97%e4%ba%ac:%e4%b8%8a%e6%b5%b7:上海",

result);

在這段代碼中,form類執行個體是請求實體,請求實體的類型為mediatype.appli-cation_form_urlencoded_type,即application/x-www-form-urlencoded。這裡還需要注意的是@encoded注解和@defaultvalue注解的使用。

jax-rs2定義了@encoded注解用以辨別禁用自動解碼。示例的測試結果中“%e4%b8%8a%e6%b5%b7”是newpassword()方法的參數值“上海”的編碼值,當對newpassword使用@encoded注解,rest方法得到的參數值就不會被解碼,如果将其直接傳回,那麼用戶端得到的值就會是處于編碼狀态的字元串。

jax-rs2定義了@defaultvalue注解,用以為用戶端沒有為其提供值的參數提供預設值。本例的user參數的預設值為feuyeux。

<b>2.2.5 @beanparam注解</b>

jax-rs2定義了@beanparam注解用于自定義參數組合,使rest方法可以使用簡潔的參數形式完成複雜的接口設計。@beanparam注解的使用示例如下所示。

//關注點1:資源方法入參

public string getbyaddress(@beanparam

jaxrs2guideparam param) {

//關注點2:參數組合

public class jaxrs2guideparam {

@headerparam("accept")

private string acceptparam;

@pathparam("region")

private string regionparam;

@pathparam("district")

private string districtparam;

@queryparam("station")

private string stationparam;

@queryparam("vehicle")

private string vehicleparam;

public void testbeanparam() {

final webtarget querytarget =

target(path).path("china").path("northeast")

.path("shenyang").path("tiexi")

.queryparam("station",

"workers village").queryparam("vehicle", "bus");

result = querytarget.request().get().readentity(string.class);

//關注點3:查詢結果斷言

assert.assertequals("china/northeast:tiexi:workers

village:bus", result);

//關注點4:複雜的查詢請求

http://localhost:9998/ctx-resource/china/shenyang/tiexi?station=workers+village&amp;vehicle=bus

在這段代碼中,getbyaddress()方法隻用了一個使用@beanparam注解定義的jaxrs2guideparam類型的參數,見關注點1;jaxrs2guideparam類定義了一系列rest方法會用到的參數類型,包括示例中使用的查詢參數"station"和路徑參數"region"等,進而使得getbyaddress()方法可以比對更為複雜的資源路徑,見關注點2;在變長子資源的例子基礎上,增加了查詢條件,但測試方法testbeanparam()發起的請求的資源位址見關注點4;可以看出這是一個較為複雜的查詢請求。其中路徑部分包括china/shenyang/tiexi,查詢條件包括station=workers+village和vehicle=bus。這些條件均在jaxrs2guideparam類中可以比對,是以從關注點3的測試斷言中可以看出,該請求響應的預期結果是"china/northeast:tiexi:workers village:bus"。

<b>2.2.6 @cookieparam注解</b>

jax-rs2定義了@cookieparam注解用以比對cookie中的鍵值對資訊,示例如下。

getheaderparams(@cookieparam("longitude") final string longitude,

@cookieparam("latitude") final string latitude,

@cookieparam("population") final double population,

@cookieparam("area") final int area) {//關注點1:資源方法入參

return longitude + "," + latitude + " population=" +

population + ",area=" + area;

public void testcontexts() {

final builder request = target(path).request();

    request.cookie("longitude",

"123.38");

request.cookie("latitude", "41.8");

request.cookie("population", "822.8");

request.cookie("area", "12948");

result = request.get().readentity(string.class);

//關注點2:測試結果斷言

assert.assertequals("123.38,41.8 population=822.8,area=12948",

在這段代碼中,getheaderparams()方法包含4個使用@cookieparam注解定義的參數,用于比對cookie的字段,見關注點1;在測試方法testcontexts中,用戶端builder執行個體填充了相應的cookie鍵值對資訊,其斷言是對cookie字段值的驗證,見關注點2。

<b>2.2.7 @context注解</b>

jax-rs2定義了@context注解來解析上下文參數,jax-rs2中有多種元素可以通過@context注解作為上下文參數使用,示例代碼如下。

public string getbyaddress(

@context final application application,

@context final request request,

@context final javax.ws.rs.ext.providers provider,

@context final uriinfo uriinfo,

@context final httpheaders headers){

在這段代碼中,分别定義了application、request、providers、uriinfo和httpheaders等5種類型的上下文執行個體。從這些執行個體中可以擷取請求過程中的重要參數資訊,示例代碼如下。

final multivaluedmap&lt;string, string&gt;

pathmap = uriinfo.getpathparameters();

querymap = uriinfo.getqueryparameters();

final list&lt;pathsegment&gt; segmentlist =

uriinfo.getpathsegments();

headermap = headers.getrequestheaders();

在這段代碼中,uriinfo類是路徑資訊的上下文,從中可以擷取路徑參數集合getpath-parameters()和查詢參數集合getqueryparameters()。類似地,我們可以從httpheaders類中擷取頭資訊集合getrequestheaders()。這些業務邏輯進行中常用的輔助資訊的擷取,要通過@context注解定義方法的參數或者類的字段來實作。

到此,統一接口和資源定位的設計和實作已經講述完畢。但是,設計rest接口還需要在此基礎上,掌握請求實體和響應實體的傳輸格式。接下來讓我們看看jersey都支援哪些類型的傳輸格式。