精通 Grails: RESTful Grails构建一个面向资源的架构 ![]() | ![]() |
![]() |
|
级别: 初级
Scott Davis , 主编, AboutGroovy.com
2008 年 10 月 07 日
http://www.ibm.com/developerworks/cn/java/j-grails09168/index.html
我们生活在 mashup 的时代。创建能够给用户提供所需信息的 Web 页面固然是一个好的出发点,但是要是能够提供原始数据的源,从而让其他的 Web 开发人员能够轻易将其与他们自己的应用程序相融合的话,这样岂不是更好。在本期的 精通 Grails 中,Scott Davis 将介绍用 Grails 生成 XML 而不是通常的 HTML 的各种方法。
本月,我将向您呈现如何让您的 Grails 应用程序成为原始数据 — 具体指 XML — 的源,从而让其他的 Web 应用程序也能够使用它。我通常把这种情况表述为:为您的 Grails 应用程序建立 Web 服务,但最近这个说法被赋予了新的含义。很多人把 Web 服务与 SOAP 及成熟的面向服务架构(service-oriented architecture,SOA)联系到一起。如果选择这种方法的话,Grails 拥有两个插件可以用来将 SOAP 接口公开给应用程序(参见 参考资料)。但我将向您呈现的内容并非处理某一个诸如 SOAP 这样的具体实现,而是如何使用一个基于具象状态传输(Representational State Transfer,REST)的接口来返回普通旧式 XML(Plain Old XML,POX)。
说到 RESTful Web 服务,理解缘由 与理解方法 同样重要。Roy Fielding 的博士论文(参见 参考资料)— REST 这个缩略词的发源处 — 概括了实现 Web 服务的两大方法:一个是面向服务,另一个是面向资源。在向您呈现实现自己的 RESTful 面向资源架构(resource-oriented architecture,ROA)的代码前,我将先澄清这两个设计原理之间的差异,并论述普遍使用的 REST 的两种最有争议的定义。学习了本文第一部分的所有内容之后,稍后您就可以学习到很多的 Grails 代码。
当开发人员说要提供 RESTful Web 服务时,他们通常是指想要提供一个简单的、无争议的方法来从他们的应用程序中获取 XML。RESTful Web 服务通常提供一个可以响应 HTTP GET
请求而返回 XML 的 URL(稍后我将给出 REST 的更正式的定义,它对这个定义进行了改良,虽然改动不大,但仍然很重要)。
Yahoo! 提供了大量的 RESTful Web 服务(参见 参考资料),它们响应简单的 HTTP GET
请求,而返回 POX。例如,在 Web 浏览器的位置字段键入 http://api.search.yahoo.com/WebSearchService/V1/webSearch?appid=YahooDemo&query=beatles
。您将获得使用 XML 的 Web 搜索结果,它和在 Yahoo! 主页的搜索框里键入 beatles
而获得的使用 HTML 的搜寻结果是一样的。
![]() |
|
如果假设 Yahoo! 支持 SOAP 接口的话(实际上并不支持),那么发出一个 SOAP 请求将会返回相同的数据,但对于开发人员来说,发出请求可能更费劲一些。在查询字符串里,请求方将需要呈交的不是简单的一组名称/值对,而是一份定义明确的、带有一个 SOAP 报头和正文部分的 XML 文档 — 而且要用一个 HTTP POST
而非 GET
来提交请求。所有这些额外的工作完成后,响应会以一个正式 XML 文档的形式返回,它与请求一样,也有一个 SOAP 报头和正文部分,但要获得查询结果,需要去掉这些内容。Web 服务常常作为复杂 SOAP 的一种简单替代品而被采用。
有几种趋势可以表明 Web 服务的 RESTful 方法越来越普及了。Amazon.com 既提供了 RESTful 服务又提供了基于 SOAP 的服务。现实的使用模式表明十个用户中几乎有九个都偏爱 RESTful 接口。另外还有一个值得注意的情况,Google 于 2006 年 12 月正式宣布反对基于 SOAP 的 Web 服务。它的所有数据服务(归类为 Google Data API)都包含了一个更加具有 REST 风格的方法。
![]() ![]() |
![]()
|
如果把 REST 和 SOAP 之间的差异归结为 GET
和 POST
之间的优劣,那就很容易区分了。所使用的 HTTP 方法是很重要的,但重要的原因与您最初预想的不同。要充分了解 REST 和 SOAP 之间的差异,您需要先掌握这两个策略的更深层语义。SOAP 包含了一个 Web 服务的面向对象的方法 — 其中包含的方法(或动词)是您与服务相交互的主要方式。REST 采取面向资源的方法,方法中的对象(或名词)是最重要的部分。
在一个 SOA 中,一个服务调用看起来就像是一个远程过程调用(remote procedure call,RPC)。设想,如果您有一个带有 getForecast(String zipcode)
方法的 Java Weather
类的话,就可以轻易地将这个方法公开为一个 Web 服务了。实际上,Yahoo! 就有这样一个 Web 服务。在浏览器中输入 http://weather.yahooapis.com/forecastrss?p=94089
,这样就会用你自己的 ZIP 代码来替代 p
参数了。Yahoo! 服务还支持第二参数 — u
—,该参数既接受华氏温度(Fahrenheit)符号 f
,又接受摄氏温度(Celsius)符号 c
。不难想象,在假想的类上重载方法签名就可以接受第二参数:getForecast("94089", "f")
。
回过来再看一下我刚才做的 Yahoo! 搜索查询,同样,不难想象出,可以将它重写为一个方法调用。http://api.search.yahoo.com/WebSearchService /V1/webSearch?appid=YahooDemo&query=beatles 轻松转换成了 WebSearchService.webSearch("YahooDemo", "beatles")
。
所以如果 Yahoo! 调用实际上为 RPC 调用的话,那这跟我先前所称的 Yahoo! 服务是 RESTful 的岂不是互相矛盾的么?很不幸,就是矛盾的。但犯这种错误的不只我一个。Yahoo! 也称这些服务是 RESTful 的,但它也坦言:从最严格的意义上讲这些服务并不符合 RESTful 服务的定义。在 Yahoo! Web Services FAQ 中寻找 “什么是 REST?”,答案是:“REST 代表 Representational State Transfer。大多数的 Yahoo! Web Services 都使用 ‘类 REST’ 的 RPC 样式的操作,而非 HTTP GET
或 POST
……”
这个问题在 REST 社区内一直引发着争论。问题是没有准确的定义可以简单明了地描述这种 “较之 POST
更偏好 HTTP GET
的、较之 XML 请求更偏好简单的 URL 请求的、基于 RPC 的 Web 服务” 。有些人称之为 HTTP/POX 或者 REST/RPC 服务。其他人则对应 High REST Web 服务 — 一种与 Fielding 的面向资源架构的定义更接近的服务 — 而称之为 Low REST Web 服务。
我将类似 Yahoo! 的服务称为 GETful 服务。这并不表示我看轻它 — 正相反,我认为 Yahoo! 在整理不太正式的(low-ceremony)Web 服务的集合方面做的相当好。这个词恰到好处地概括出了 Yahoo! 的 RPC 样式的服务的益处 — 通过发出一个简单的 HTTP GET
请求来获得 XML 结果 —,而且没有滥用 Fielding 所作的原始定义。
![]() ![]() |
![]()
|
![]() |
|
那么要成为真正的面向资源的服务要满足哪些条件呢?可以这样归结:创建一个好的统一资源标识符(Uniform Resource Identifier,URI),并以标准化的方式来使用 HTTP 动词(GET
、POST
、PUT
和 DELETE
),而不是使用与自定义的方法调用相结合的动词(GET
)。
再回到 Beatles 的查询上,要想更接近正式的 RESTful 接口,第一步就是要调试 URI。Beatles
不是作为参数而被传入到 webSearch
方法,而是成为了 URI 的中心资源。例如,关于 Beatles 的 Wikipedia 文章的 URI 为 http://en.wikipedia.org/wiki/Beatles。
但是真正把 GETful 原理和 RESTful 原理区别开来的是用于返回资源表示的方法。Yahoo! RPC 接口定义了很多自定义方法(webSearch
、albumSearch
、newsSearch
等等)。如果不读取文档的话,是无法得知方法调用的名称的。就 Yahoo! 而言,我可以跟随它的模式并猜出它有 songSearch
、imageSearch
和 videoSearch
这几个方法调用,但却不敢保证一定是这样。同样,其他的 Web 站点可能使用不同的命名约定,如 findSong
或者 songQuery
。就 Grails 而言,像 aiport/list
和 airport/show
这样的自定义操作在整个应用程序内都是标准操作,但这些方法名称无法成为其他 Web 框架中的标准。
相反,RESTful 方法通常使用 HTTP GET
来返回所涉及的资源表示。因此对于 Wikipedia 上的任何资源来说(http://en.wikipedia.org/wiki/Beatles、http://en.wikipedia.org/wiki/United_Airlines 或者 http://en.wikipedia.org/wiki/Peanut_butter_and_jelly_sandwich),我都可以得知 GET
是获取它的标准方式。
当处理一个资源的完整的 Create/Retrieve/Update/Delete(CRUD)生命周期时,标准化的方法调用的强大功能就变得更加显而易见了。RPC 接口不提供创建新资源的标准化方式。自定义的方法调用可以是 create
、new
、insert
、add
抑或是其他任何调用。在 RESTful 接口中,每向 URI 发送一个 POST
请求就会插入一个新资源。PUT
可以更新资源,而 DELETE
可以删除资源(参见 POST
与 PUT
侧边栏)。
现在您已经对 GETful 与 RESTful Web 服务之间的差异有了更充分的了解了,并已经准备好用 Grails 创建自己的服务了。这两种服务的例子您都将看得到,但我要从简单的 POX 例子开始说起。
![]() ![]() |
![]()
|
从 Grails 应用程序中获取 POX 的最快捷的方式就是导入 grails.converters.*
包,然后添加一对新的闭包,如清单 1 所示:
清单1. 简单的 XML 输出
import grails.converters.* class AirportController{ def xmlList = { render Airport.list() as XML } def xmlShow = { render Airport.get(params.id) as XML } //... the rest of the controller } |
您在 “精通 Grails:使用 Ajax 实现多对多关系 中见过了使用中的 grails.converters
” 包。该包向您提供了非常简单的 JavaScript Object Notation(JSON)和 XML 输出支持。图 1 展示了调用 xmlList
操作的结果:
图 1. 来自于 Grails 的默认 XML 输出

虽然默认的 XML 输出很好调试,但您还是想稍微自定义一下格式。还好,render()
方法给您提供了一个 Groovy MarkupBuilder
,它允许您动态定义自定义 XML(参见 参考资源,查看更多有关 MarkupBuilder
的消息的链接)。清单 2 创建了一些自定义 XML 输出:
清单 2. 自定义 XML 输出
def customXmlList = { def list = Airport.list() render(contentType:"text/xml"){ airports{ for(a in list){ airport(id:a.id, iata:a.iata){ "official-name"(a.name) city(a.city) state(a.state) country(a.country) location(latitude:a.lat, longitude:a.lng) } } } } } |
图 2 展示了输出结果:
图 2. 使用 Groovy
MarkupBuilder
的自定义 XML 输出
注意源代码和 XML 输出之间的对应的紧密程度。您可以随意定义元素名称(airports、airport、city),无需顾及它们是否与类的真实字段名称对应。如果您想提供一个以连字符链接的元素名称的话(诸如 official-name
),又或者想要添加名称空间支持的话,只要给元素名称加上引号就可以了。而属性(诸如 id
和 iata
)是用 Groovy 散列映射键:值 语法定义的。要填充元素的正文,需要提供一个不带键:的值。
创建一个返回数据的 HTML 和 XML 表示的单独闭包是很简单的,但如果想创建一个既可以返回 HTML 又可以返回 XML 表示的闭包的话,该怎么办呢。这也是可以实现的,这要多亏在 HTTP 请求中包含有 Accept
报头。这个简单的元数据告诉服务器:“嗨,您对这个 URI 中的资源可能有不只一个资源表示 — 我更喜欢这个。”
cURL
是一个方便的开源命令行 HTTP 工具(参见 参考资料)。在命令行输入 curl http://localhost:9090/trip/airport/list
,以此来模拟请求机场列表的浏览器请求。您应该会看到 HTML 响应展现在您的荧屏上。
现在,对请求做两处小小的变动。这回,代替 GET
发出一个 HEAD
请求。HEAD
是一个标准 HTTP 方法,它仅仅返回响应的元数据,而不返回正文(您现在正在进行的调试的类型包含在 HTTP 规范中)。另外,将 cURL
放置于 verbose
模式,这样您就也能够看到请求元数据了,如清单 3 所示:
清单 3. 使用
cURL
来调试 HTTP $ curl --request HEAD --verbose http://localhost:9090/trip/airport/list * About to connect() to localhost port 9090 (#0) * Trying ::1... connected * Connected to localhost (::1) port 9090 (#0) > HEAD /trip/airport/list HTTP/1.1 > User-Agent: curl/7.16.3 (powerpc-apple-darwin9.0) libcurl/7.16.3 OpenSSL/0.9.7l zlib/1.2.3 > Host: localhost:9090 > Accept: */* > < HTTP/1.1 200 OK < Content-Language: en-US < Content-Type: text/html; charset=utf-8 < Content-Length: 0 < Server: Jetty(6.1.4) < * Connection #0 to host localhost left intact * Closing connection #0 |
注意请求中的 Accept
报头。客户机要是提交 */*
的话,就意味着:“返回什么样的格式都无所谓。我将接受任何内容。”
cURL
允许您使用这个值来覆盖 --header
参数。输入 curl --request HEAD --verbose --header Accept:text/xml http://localhost:9090/trip/airport/list
,并验证 Accept
报头正在请求 text/xml
。这就是资源的 MIME 类型了。
那么,Grails 是如何响应服务器端的 Accept
报头的呢?再向 AirportController
添加一个闭包,如清单 4 所示:
清单 4.
debugAccept
操作 def debugAccept = { def clientRequest = request.getHeader("accept") def serverResponse = request.format render "Client: ${clientRequest}/nServer: ${serverResponse}/n" } |
清单 4 中的第一行从请求中检索出了 Accept
报头。第二行展示了 Grails 如何转换请求和它将要发回的响应。
现在,使用 cURL
来做相同的搜索,如清单 5 所示:
清单 5. 调试
cURL
中的 Accept
报头 $ curl http://localhost:9090/trip/airport/debugAccept Client: */* Server: all $ curl --header Accept:text/xml http://localhost:9090/trip/airport/debugAccept Client: text/xml Server: xml |
all
和 xml
值是哪来的呢?看一下 grails-app/conf/Config.groovy。在文件顶部,您应该看到了一个散列映射,它对所有的键都使用了简单名称(像 all
和 xml
这样的名称),而且所有的值都使用了与之对应的 MIME 类型。清单 6 展示了 grails.mime.types
散列映射:
清单 6. Config.groovy 中的
grails.mime.types
散列 grails.mime.types = [ html: ['text/html','application/xhtml+xml'], xml: ['text/xml', 'application/xml'], text: 'text-plain', js: 'text/javascript', rss: 'application/rss+xml', atom: 'application/atom+xml', css: 'text/css', csv: 'text/csv', all: '*/*', json: ['application/json','text/json'], form: 'application/x-www-form-urlencoded', multipartForm: 'multipart/form-data' ] |
![]() |
|
那么,现在您应该对内容协商有了更多的了解了,您可以将 withFormat
块添加到 list
操作,以此来依据请求中的 Accept
报头返回合适的数据类型,如清单 7 所示:
清单 7. 在一个操作中使用
withFormat
块 def list = { if(!params.max) params.max = 10 def list = Airport.list(params) withFormat{ html{ return [airportList:list] } xml{ render list as XML } } } |
每一个块的最后一行一定会是一个 render
、return
或者 redirect
— 与普通操作没什么不同。如果 Accept
报头变成 “all”(*/*
)的话,则会使用块中的第一个条目。
改变 cURL
中的 Accept
报头是不错,但是通过改变 URI 您还可以作一些测试工作。http://localhost:8080/trip/airport/list.xml 和 http://localhost:8080/trip/airport/list?format=xml 都可以用来显式地覆盖 Accept
报头。随便试一下 cURL
和各种 URI 值,确保 withFormat
块能发挥预期作用。
如果想让这个行为成为 Grails 中的标准的话,不要忘记您可以输入 grails install-templates
,并在 /src/templates 中编辑文件。
所有的基本构建块就位之后,最后一步就是将 GETful 接口转化成一个真正的 RESTful 接口。
![]() ![]() |
![]()
|
首先,需要确保您的控制器已经开始响应那四个 HTTP 方法了。回想一下,如果用户不指定一个像 list
或 show
这样的操作的话,index
闭包就是通往控制器的入口点。index
默认重定向到 list
操作:def index = { redirect(action:list,params:params) }
。用清单 8 中的代码替换这个代码:
清单 8. 启动 HTTP 方法
def index = { switch(request.method){ case "POST": render "Create/n" break case "GET": render "Retrieve/n" break case "PUT": render "Update/n" break case "DELETE": render "Delete/n" break } } |
如清单 9 所示,使用 cURL
来验证 switch
语句运行正常:
清单 9. 全部四个 HTTP 方法都使用
cURL
$ curl --request POST http://localhost:9090/trip/airport Create $ curl --request GET http://localhost:9090/trip/airport Retrieve $ curl --request PUT http://localhost:9090/trip/airport Update $ curl --request DELETE http://localhost:9090/trip/airport Delete |
由于您已经知道如何返回 XML 了,实现 GET
方法就应该是小菜一碟了。但有一点需要注意。对 http://localhost:9090/trip/airport 的 GET
请求应该返回一个机场列表。而对 http://localhost:9090/trip/airport/den 的 GET
请求应该返回 IATA 代码为 den 的一个机场实例。要达到这个目的,必须建立一个 URL 映射。
在文本编辑器中打开 grails-app/conf/UrlMappings.groovy。默认的 /$controller/$action?/$id?
映射看起来应该很熟悉。URL http://localhost:9090/trip/airport/show/1 映射到了 AiportController
和 show
操作,而 params.id
值被设置成 1
。操作和 ID 结尾的问号说明 URL 元素是可以选择的。
如清单 10 所示,向将 RESTful 请求映射回 AirportController
的 static mappings
块添加一行。由于还没有在其他控制器中实现 REST 支持,所以我暂时对控制器进行了硬编码。稍候可能会用 $controller
来替代 URL 的 airport
部分。
清单 10. 创建一个自定义 URL 映射
class UrlMappings { static mappings = { "/$controller/$action?/$id?"{ constraints { // apply constraints here } } "/rest/airport/$iata?"(controller:"airport",action:"index") "500"(view:'/error') } } |
该映射确保了所有以 /rest 开头的 URI 都被传送到了 index
操作(这样就不需要协商内容了)。它还意味着您可以检查 params.iata
存在与否,以此来决定是应该返回列表还是一个实例。
按清单 11 所示的方法,修改 index 操作:
清单 11. 从 HTTP
GET
返回 XML def index = { switch(request.method){ case "POST": //... case "GET": if(params.iata){render Airport.findByIata(params.iata) as XML} else{render Airport.list() as XML} break case "PUT": //... case "DELETE": //... } } |
在 Web 浏览器中输入 http://localhost:9090/trip/rest/airport
和 http://localhost:9090/trip/rest/airport/den
,确认自定义 URL 映射已经就位。
![]() |
|
添加 DELETE
支持与添加 GET
支持的差别不大。但在这里,我仅需要通过 IATA 代码逐个删除机场。如果用户提交了一个不带有 IATA 代码的 HTTP DELETE
请求的话,我将返回一个 400 HTTP 状态码 Bad Request
。如果用户提交了一个无法找到的 IATA 代码的话,我将返回一个常见的 404 状态码 Not Found
。只有删除成功了,我才会返回标准的 200 OK
(参见 参考资料,查看更多有关 HTTP 状态码的信息的链接)。
将清单 12 中的代码添加到 index
操作中的 DELETE case
中:
清单 12. 对 HTTP
DELETE
做出响应 def index = { switch(request.method){ case "POST": //... case "GET": //... case "PUT": //... case "DELETE": if(params.iata){ def airport = Airport.findByIata(params.iata) if(airport){ airport.delete() render "Successfully Deleted." } else{ response.status = 404 //Not Found render "${params.iata} not found." } } else{ response.status = 400 //Bad Request render """DELETE request must include the IATA code Example: /rest/airport/iata """ } break } } |
首先,试着删除一个已知确实存在的机场,如清单 13 所示:
清单 13. 删除一个存在的机场
Deleting a Good Airport</heading> $ curl --verbose --request DELETE http://localhost:9090/trip/rest/airport/lga > DELETE /trip/rest/airport/lga HTTP/1.1 < HTTP/1.1 200 OK Successfully Deleted. |
然后,试着删除一个已知不存在的机场,如清单 14 所示:
清单 14. 试着
DELETE
一个不存在的机场 $ curl --verbose --request DELETE http://localhost:9090/trip/rest/airport/foo > DELETE /trip/rest/airport/foo HTTP/1.1 < HTTP/1.1 404 Not Found foo not found. |
最后,试着发出一个不带有 IATA 代码的 DELETE
请求,如清单 15 所示:
清单 15. 试着一次性
DELETE
所有机场 $ curl --verbose --request DELETE http://localhost:9090/trip/rest/airport > DELETE /trip/rest/airport HTTP/1.1 < HTTP/1.1 400 Bad Request DELETE request must include the IATA code Example: /rest/airport/iata |
接下来您的目标是要插入一个新的 Airport
。创建一个如清单 16 所示的名为 simpleAirport.xml 的文件:
清单 16. simpleAirport.xml
<airport> <iata>oma</iata> <name>Eppley Airfield</name> <city>Omaha</city> <state>NE</state> <country>US</country> <lat>41.3019419</lat> <lng>-95.8939015</lng> </airport> |
如果资源的 XML 表示是扁平结构(没有深层嵌套元素),而且每一个元素名称都与类中的一个字段名称相对应的话,Grails 就能够直接从 XML 中构造出新类来。XML 文档的根元素是通过 params
寻址的,如清单 17 所示:
清单 17. 响应 HTTP
POST
def index = { switch(request.method){ case "POST": def airport = new Airport(params.airport) if(airport.save()){ response.status = 201 // Created render airport as XML } else{ response.status = 500 //Internal Server Error render "Could not create new Airport due to errors:/n ${airport.errors}" } break case "GET": //... case "PUT": //... case "DELETE": //... } } |
XML 一定要使用扁平结构,这是因为 params.airport
其实是一个散列(Grails 是在后台将 XML 转换成散列的)。这意味着您在对 Airport
使用命名参数构造函数 — def airport = new Airport(iata:"oma", city:"Omaha", state:"NE")
。
要测试新代码,就要使用 cURL
来 POST
simpleAirport.xml 文件,如清单 18 所示:
清单 18. 使用
cURL
来发出一个 HTTP POST
$ curl --verbose --request POST --header "Content-Type: text/xml" --data @simpleAirport.xml http://localhost:9090/trip/rest/airport > POST /trip/rest/airport HTTP/1.1 > Content-Type: text/xml > Content-Length: 176 > < HTTP/1.1 201 Created < Content-Type: text/xml; charset=utf-8 <?xml version="1.0" encoding="utf-8"?><airport id="14"> <arrivals> <null/> </arrivals> <city>Omaha</city> <country>US</country> <departures> <null/> </departures> <iata>oma</iata> <lat>41.3019419</lat> <lng>-95.8939015</lng> <name>Eppley Airfield</name> <state>NE</state> </airport> |
如果 XML 比较复杂的话,则需要解析它。例如,还记得您先前定义的自定义 XML 格式么?创建一个名为 newAirport.xml 的文件,如清单 19 所示:
清单 19. newAirport.xml
<airport iata="oma"> <official-name>Eppley Airfield</official-name> <city>Omaha</city> <state>NE</state> <country>US</country> <location latitude="41.3019419" longitude="-95.8939015"/> </airport> |
现在,在 index
操作中,用清单 20 中的代码替代 def airport = new Airport(params.airport)
行:
清单 20. 解析复杂的 XML
def airport = new Airport() airport.iata = request.XML.@iata airport.name = request.XML."official-name" airport.city = request.XML.city airport.state = request.XML.state airport.country = request.XML.country airport.lat = request.XML.location.@latitude airport.lng = request.XML.location.@longitude |
request.XML
对象是一个持有原始 XML 的 groovy.util.XmlSlurper
。它是根元素,因此您可以通过名称(request.XML.city
)来寻找子元素。如果名称是用连字符连接的,或者使用了名称空间,就加上引号(request.XML."official-name"
)。元素的属性要使用 @
符号(request.XML.location.@latitude
)来访问(参见 参考资料,查看有关 XmlSlurper
的更多信息的链接)。
最后,使用 cURL
来测试它:curl --request POST --header "Content-Type: text/xml" --data @newAirport.xml http://localhost:9090/trip/rest/airport
。
您需要支持的最后一个 HTTP 方法就是 PUT
。了解了 POST
之后,会知道代码基本是一样的。惟一不同的就是它无法直接从 XML 构造类,您需要向 GORM 寻求现有的类。然后,airport.properties = params.airport
行会用新的 XML 数据来替代现有的字段数据,如清单 21 所示:
清单 21. 响应 HTTP
PUT
def index = { switch(request.method){ case "POST": //... case "GET": //... case "PUT": def airport = Airport.findByIata(params.airport.iata) airport.properties = params.airport if(airport.save()){ response.status = 200 // OK render airport as XML } else{ response.status = 500 //Internal Server Error render "Could not create new Airport due to errors:/n ${airport.errors}" } break case "DELETE": //... } } |
创建一个名为 editAirport.xml 的文件,如清单 22 所示:
清单 22. editAirport.xml
<airport> <iata>oma</iata> <name>xxxEppley Airfield</name> <city>Omaha</city> <state>NE</state> <country>US</country> <lat>41.3019419</lat> <lng>-95.8939015</lng> </airport> |
最后,使用 cURL
: curl --verbose --request PUT --header "Content-Type: text/xml" --data @editAirport.xml http://localhost:9090/trip/rest/airport
来测试它。
![]() ![]() |
![]()
|
我在很短的时间内讲解了很多相关知识。现在,您应该了解到 SOA 和 ROA 之间的不同之处了。您同样也应该意识到,并不是所有的 RESTful Web 服务都如出一辙。有些 Web 服务是 GETful 的 — 使用 HTTP GET
请求来调用类 RPC 方法。而其他的则是纯粹面向资源的,其中 URI 是访问资源的关键,而标准 HTTP GET
、POST
、PUT
和 DELETE
方法构成了完整的 CRUD 功能。无论您是喜欢 GETful 方法还是 RESTful 方法,Grails 都为输出和轻易地获取 XML 提供了强有力的支持。
在下一期的 精通 Grails 中,我将把重点转向测试。Grails 配有优良的开箱即用的测试工具。而那些没有提供的功能则可以在以后以插件的形式添加进去。既然已经在 Grails 开发中投入了这么多的时间了,那么就一定要确保它在无错误的情况下开始运行并可以在应用程序的整个生命周期中都可以保持这种无错误的状态。在达到这个目标之前,继续关注精通 Grails 系列文章吧。
学习
- 您可以参阅本文在 developerWorks 全球网站上的 英文原文。
- 精通 Grails:请阅读这个系列的更多文章,进一步理解 Grail 以及它可以实现的各种功能。
- Grails:访问 Grails Web 站点。
- Grails Framework Reference Documentation:关于 Grails 的经典之作。
- Architectural Styles and the Design of Network-based Software Architectures(Roy Thomas Fielding,University of California at Irvine,2000 年):Fielding 的博士论文,它对 REST 进行了描述。
- Representational State Transfer:Wikipedia 的关于 REST 的文章。
- Creating a REST Request for Yahoo! Search Web Services:想要正式地使用 Yahoo! Web 服务的话,就一定要获取并使用自己的 应用程序密钥,而不是使用
YahooDemo
。 - Google Data APIs:了解更多有关基于 Atom Publishing Protocol 的 Google Web 服务的信息。
- RESTful Web Services(Leonard Richardson and Sam Ruby,O'Reilly Media,2007 年):本书的作者支持使用
PUT
来 INSERT。 - Creating XML using Groovy's MarkupBuilder 和 Reading XML using Groovy's XmlSlurper:用 Groovy 来生成和消费 XML。
- Content Negotiation:学习更多有关服务器端内容协商的知识。
- Status Code Definitions:了解 HTTP 状态码知识。
- “编写 REST 服务”(J. Jeffrey Hanson,developerWorks,2007 年 10 月):在本篇教程中学习使用 Java 技术和 Atom Publishing Protocol 来创建 Create REST 服务。
- “构建 RESTful Web 服务”(Andrew Glover,developerWorks,2008 年 7 月):本教程介绍了 REST 和 Restlet 框架。
- “跨越边界: REST on Rails”(Bruce Tate,developerWorks,2006 年 8 月):了解 Ruby on Rails 是如何支持 RESTful Web 服务的。
- Groovy Recipes:阅读 Scott Davis 的最新力作,学习更多有关 Groovy 和 Grails 的知识。
- 实战 Groovy:developerWorks 上的这个系列的文章主要探索 Groovy 的实际用途,并教您何时、如何成功地应用它们。
- Groovy:在 Groovy 项目的网站上学习关于它的更多内容。
- AboutGroovy.com:关注 Groovy 的最新新闻和文章链接。
- 技术书店:浏览关于这些主题和其他技术主题的图书。
- developerWorks Java 技术专区:找到数百篇关于 Java 编程各个方面的文章。
获得产品和技术
- Grails:下载最新版本的 Grails。
- XFire 和 Apache Axis2 Plugin:下载用于公开 SOAP 接口的 Grails 插件。
cURL
:cURL
默认安装在大多数的 UNIX®、Linux® 和 Mac OS X 系统上。您可以在此 下载 一个适用于 Windows® 和所有其他 OS 的版本。
讨论
![]() | ||
| ![]() | Scott Davis 是国际知名作家、演讲家和软件开发人员。他出版的书籍包括 Groovy Recipes: Greasing the Wheels of Java、GIS for Web Developers: Adding Where to Your Application、The Google Maps API 和 JBoss At Work。 |