技术开发 频道

精通 Grails: RESTful Grails

  【IT168技术文档】本月,我将向您呈现如何让您的 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 的搜寻结果是一样的。

  如果假设 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 之间的差异归结为 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 所作的原始定义。

  面向资源的 Web 服务

  那么要成为真正的面向资源的服务要满足哪些条件呢?可以这样归结:创建一个好的统一资源标识符(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 实现 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 输出  

  虽然默认的 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 散列映射键:值 语法定义的。要填充元素的正文,需要提供一个不带键:的值。

  内容协商与 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

  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 接口。

  用 Grails 实现 RESTful Web 服务

  首先,需要确保您的控制器已经开始响应那四个 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

  实现 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 映射到了 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

  添加 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")。

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

  实现 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 GET、POST、PUT 和 DELETE 方法构成了完整的 CRUD 功能。无论您是喜欢 GETful 方法还是 RESTful 方法,Grails 都为输出和轻易地获取 XML 提供了强有力的支持。

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

0
相关文章