精通 Grails: RESTful Grails

构建一个面向资源的架构

我们生活在 mashup 的时代。创建能够给用户提供所需信息的 Web 页面固然是一个好的出发点,但是要是能够提供原始数据的源,从而让其他的 Web 开发人员能够轻易将其与他们自己的应用程序相融合的话,这样岂不是更好。在本期的 精通 Grails中,Scott Davis 将介绍用 Grails 生成 XML 而不是通常的 HTML 的各种方法。

Scott Davis , 主编, AboutGroovy.com

Scott Davis 是国际知名作家、演讲家和软件开发人员。他出版的书籍包括 Groovy Recipes: Greasing the Wheels of JavaGIS for Web Developers: Adding Where to Your ApplicationThe Google Maps APIJBoss At Work



2008 年 10 月 07 日

本月,我将向您呈现如何让您的 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 代码。

REST 简介

当开发人员说要提供 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 的搜寻结果是一样的。

关于本系列

Grails 是一种新型 Web 开发框架,它将常见的 Spring 和 Hibernate 等 Java™技术与当前流行的实践(比如约定优于配置)相结合。在加入脚本语言的灵活性和动态性的同时,用 Groovy 编写的 Grails 可以无缝地集成遗留的 Java 代码。学习完 Grails 之后,您将改变看待 Web 开发的方式。

如果假设 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 风格的方法。


面向服务的 Web 服务

如果把 REST 和 SOAP 之间的差异归结为 GETPOST之间的优劣,那就很容易区分了。所使用的 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 GETPOST……”

这个问题在 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 所作的原始定义。


面向资源的 Web 服务

POSTPUT

在 REST 社区存在着有关 POSTPUT在插入新资源方面所起的作用的争议。在 HTTP 1.1 的原始的 RFC(Fielding 是主要作者)中对 PUT的定义称:如果不存在资源的话,服务器可以创建资源。而如果已经存在资源的话,那么 “……封装的实体必须被当作是对驻留在初始服务器上的实体修改后的版本”。因此如果不存在资源的话,PUT就等于 INSERT。如果存在资源的话,PUT就等于 UPDATE。 如果按如下的方式定义 POST的话,事情就复杂了:

POST旨在用一个统一的方法来涵盖以下所有功能:

  • 注释现有资源;
  • 将一则消息发布到告示板、新闻组、邮件列表或者类似文章上;
  • 将诸如表格提交结果这样的数据块提供给数据处理进程;
  • 通过追加操作扩展数据库。”

“注释现有资源” 似乎暗指 UPDATE,而 “将一则消息发布到告示板”、“扩展数据库” 似乎暗指 INSERT。

由于所有的浏览器在提交 HTML 表单数据时都不支持 PUT方法(它们只支持 GETPOST),所以很难确定在哪种情况下使用哪种方法最为明智。

Atom 发布协议(Atom Publishing Protocol)是一个遵循 RESTful 原则的流行的聚合格式。Atom 的 RFC 作者试图给 POSTPUT之间的争议做个了结:

“Atom Publishing Protocol 对如下的 Member Resource 使用 HTTP 方法:

  • GET用于检索已知的 Resource 表示。
  • POST用于创建新的、动态命名的 Resource ……
  • PUT用于编辑已知 Resource。不用它来创建 Resource。
  • DELETE用于删除已知 Resource。”

在本文中,我将以 Atom 为指引,使用 POST来 INSERT,用 PUT来 UPDATE。但如果您在您的应用程序中反其道而行的话,那么也是可以的 —RESTful Web Services一书(参见 参考资料)支持使用 PUT来 INSERT。

那么要成为真正的面向资源的服务要满足哪些条件呢?可以这样归结:创建一个好的统一资源标识符(Uniform Resource Identifier,URI),并以标准化的方式来使用 HTTP 动词(GETPOSTPUTDELETE),而不是使用与自定义的方法调用相结合的动词(GET)。

再回到 Beatles 的查询上,要想更接近正式的 RESTful 接口,第一步就是要调试 URI。Beatles不是作为参数而被传入到 webSearch方法,而是成为了 URI 的中心资源。例如,关于 Beatles 的 Wikipedia 文章的 URI 为 http://en.wikipedia.org/wiki/Beatles。

但是真正把 GETful 原理和 RESTful 原理区别开来的是用于返回资源表示的方法。Yahoo! RPC 接口定义了很多自定义方法(webSearchalbumSearchnewsSearch等等)。如果不读取文档的话,是无法得知方法调用的名称的。就 Yahoo! 而言,我可以跟随它的模式并猜出它有 songSearchimageSearchvideoSearch这几个方法调用,但却不敢保证一定是这样。同样,其他的 Web 站点可能使用不同的命名约定,如 findSong或者 songQuery。就 Grails 而言,像 aiport/listairport/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 接口不提供创建新资源的标准化方式。自定义的方法调用可以是 createnewinsertadd抑或是其他任何调用。在 RESTful 接口中,每向 URI 发送一个 POST请求就会插入一个新资源。PUT可以更新资源,而 DELETE可以删除资源(参见 POSTPUT侧边栏)。

现在您已经对 GETful 与 RESTful Web 服务之间的差异有了更充分的了解了,并已经准备好用 Grails 创建自己的服务了。这两种服务的例子您都将看得到,但我要从简单的 POX 例子开始说起。


用 Grails 实现 GETful Web 服务

从 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 输出
来自于 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 输出
使用 Groovy MarkupBuilder的自定义 XML 输出

注意源代码和 XML 输出之间的对应的紧密程度。您可以随意定义元素名称(airports、airport、city),无需顾及它们是否与类的真实字段名称对应。如果您想提供一个以连字符链接的元素名称的话(诸如 official-name),又或者想要添加名称空间支持的话,只要给元素名称加上引号就可以了。而属性(诸如 idiata)是用 Groovy 散列映射 语法定义的。要填充元素的正文,需要提供一个不带 :的值。

内容协商与 Accept报头

创建一个返回数据的 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

allxml值是哪来的呢?看一下 grails-app/conf/Config.groovy。在文件顶部,您应该看到了一个散列映射,它对所有的键都使用了简单名称(像 allxml这样的名称),而且所有的值都使用了与之对应的 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'
                    ]

高级的内容协商

典型的 Web 浏览器提供的 Accept报头要比您与 cURL一起使用的稍微复杂些。例如,Mac OS X 10.5.4 上的 Firefox 3.0.1 提供的 Accept报头大致是这样的:

text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8

它是一个用逗号隔开的列表,它带有可选的 q属性,用以支持 MIME 类型(q值 —quality 的缩写 —是 float值,范围是 0.0 到 1.0)。由于 application/xml被赋予了一个为 0.9 的 q值,所以与其他类型的数据相比,Firefox 更偏好 XML 数据。

下面是 Mac OS X 10.5.4 上的 Safari 3.1.2 版本提供的 accept 报头:

text/xml,application/xml,application/xhtml+xml,
text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5

text/htmlMIME 类型被赋予了一个为 0.9 的 q值,所以首选的输出类型是 HTML,0.8 时为 text/plain,0.5 时为 */*

参见 参考资料,查看更多关于服务器端内容协商的信息。

那么,现在您应该对内容协商有了更多的了解了,您可以将 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 
    } 
  } 
 }

每一个块的最后一行一定会是一个 renderreturn或者 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 接口。


用 Grails 实现 RESTful Web 服务

首先,需要确保您的控制器已经开始响应那四个 HTTP 方法了。回想一下,如果用户不指定一个像 listshow这样的操作的话,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

实现 GET

由于您已经知道如何返回 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 映射到了 AiportControllershow操作,而 params.id值被设置成 1。操作和 ID 结尾的问号说明 URL 元素是可以选择的。

如清单 10 所示,向将 RESTful 请求映射回 AirportControllerstatic 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/airporthttp://localhost:9090/trip/rest/airport/den,确认自定义 URL 映射已经就位。

通过 HTTP 方法实现的自定义 URL 映射

您可以使用不同的方法来建立 RESTful URL 映射。您可以依照 HTTP 请求将请求传送到具体操作。例如,按照如下的方法可以将 GETPUTPOSTDELETE映射到已经存在的相应 Grails 操作:

static mappings = {
"/airport/$id"(controller:"airport"){
action = [GET:"show", PUT:"update", DELETE:"delete", POST:"save"]
} }

实现 DELETE

添加 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

实现 POST

接下来您的目标是要插入一个新的 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")

要测试新代码,就要使用 cURLPOSTsimpleAirport.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

实现 PUT

您需要支持的最后一个 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 GETPOSTPUTDELETE方法构成了完整的 CRUD 功能。无论您是喜欢 GETful 方法还是 RESTful 方法,Grails 都为输出和轻易地获取 XML 提供了强有力的支持。

在下一期的 精通 Grails中,我将把重点转向测试。Grails 配有优良的开箱即用的测试工具。而那些没有提供的功能则可以在以后以插件的形式添加进去。既然已经在 Grails 开发中投入了这么多的时间了,那么就一定要确保它在无错误的情况下开始运行并可以在应用程序的整个生命周期中都可以保持这种无错误的状态。在达到这个目标之前,继续关注精通 Grails 系列文章吧。

参考资料

学习

获得产品和技术

  • Grails:下载最新版本的 Grails。
  • XFireApache Axis2 Plugin:下载用于公开 SOAP 接口的 Grails 插件。
  • cURLcURL默认安装在大多数的 UNIX®、Linux®和 Mac OS X 系统上。您可以在此 下载一个适用于 Windows®和所有其他 OS 的版本。

讨论

条评论

developerWorks: 登录

标有星(*)号的字段是必填字段。


需要一个 IBM ID?
忘记 IBM ID?


忘记密码?
更改您的密码

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件

 


在您首次登录 developerWorks 时,会为您创建一份个人概要。您的个人概要中的信息(您的姓名、国家/地区,以及公司名称)是公开显示的,而且会随着您发布的任何内容一起显示,除非您选择隐藏您的公司名称。您可以随时更新您的 IBM 帐户。

所有提交的信息确保安全。

选择您的昵称



当您初次登录到 developerWorks 时,将会为您创建一份概要信息,您需要指定一个昵称。您的昵称将和您在 developerWorks 发布的内容显示在一起。

昵称长度在 3 至 31 个字符之间。 您的昵称在 developerWorks 社区中必须是唯一的,并且出于隐私保护的原因,不能是您的电子邮件地址。

标有星(*)号的字段是必填字段。

(昵称长度在 3 至 31 个字符之间)

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件.

 


所有提交的信息确保安全。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Java technology, Web development, Open source
ArticleID=343722
ArticleTitle=精通 Grails: RESTful Grails
publish-date=10072008