<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 查詢字元串,方法作用域資訊
使用“&”符号來分隔查詢條件
使用逗号分隔有次序的作用域資訊
使用分号分隔無次序的作用域資訊
一個典型的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)問号(?)是用來分隔資源位址和查詢字元串的,與符号(&)是用來分隔查詢條件的參數的。示例代碼如下。
get /books?start=0&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&size=10
get /books?limit=100&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&size=10
排序并分頁查詢清單資料 /query-resource/sorted-yijings?limit=5&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 > listsize ? listsize : size;
//關注點2:分頁疊代邏輯
for(int i = 0, index = start; i < 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<yijing>() {
@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&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<pathsegment>
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<string, string> matrixparameters =
condition.getmatrixparameters();
final iterator<entry<string, list<string>>>
iterator =
matrixparameters.entryset().iterator();
while (iterator.hasnext()) {
final entry<string, list<string>> 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&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<string, string>
pathmap = uriinfo.getpathparameters();
querymap = uriinfo.getqueryparameters();
final list<pathsegment> segmentlist =
uriinfo.getpathsegments();
headermap = headers.getrequestheaders();
在這段代碼中,uriinfo類是路徑資訊的上下文,從中可以擷取路徑參數集合getpath-parameters()和查詢參數集合getqueryparameters()。類似地,我們可以從httpheaders類中擷取頭資訊集合getrequestheaders()。這些業務邏輯進行中常用的輔助資訊的擷取,要通過@context注解定義方法的參數或者類的字段來實作。
到此,統一接口和資源定位的設計和實作已經講述完畢。但是,設計rest接口還需要在此基礎上,掌握請求實體和響應實體的傳輸格式。接下來讓我們看看jersey都支援哪些類型的傳輸格式。