天天看点

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都支持哪些类型的传输格式。