Изучение Grails: Grails и архитектура REST

Проектирование ресурсно-ориентированной архитектуры

Мы живем в эпоху mashup-приложений. Создание Web-страниц, содержащих нужную пользователям информацию – это неплохо, но гораздо лучше предоставлять доступ к сырым данным, которые Web-разработчики могут легко интегрировать в собственные приложения. В этом выпуске серии Изучение Grails Скотт Дэвис рассказывает о различных способах заставить Grails выдавать информацию в формате XML вместо традиционного HTML.

Скотт Дэвис, главный редактор, AboutGroovy.com

Скотт Дэвис (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.



23.06.2011

В этом выпуске речь пойдет о том, как приложения Grails могут служить источником сырых данных, в частности, в формате XML, которые затем могут использоваться другими Web-приложениями. Данную тему было бы предпочтительно осветить в контексте создания Web-сервисов для приложения Grails, но, к сожалению, у этого термина в последнее время появилось слишком много сторонних значений. Для многих разработчиков термин "Web-сервисы" тесно связан с SOAP и влечет за собой все аспекты сервисно-ориентированной архитектуры (SOA). Если это то, что вам необходимо, то вы можете использовать два подключаемых модуля Grails, чтобы предоставить доступ вашему приложению к интерфейсу SOA (см. раздел Ресурсы). Однако вместо рассмотрения одной конкретной реализации, например SOAP, в статье рассказывается о том, как выдавать информацию в формате XML (Plain Old XML – POX) с использованием интерфейса, базирующегося на принципах передачи репрезентативного состояния (Representational State Transfer – REST).

При работе с Web-сервисами, построенными по принципам REST, очень важно понимать не только как они работают, но также и почему они работают именно так. В докторской диссертации Роя Филдинга (Roy Fielding, см. раздел Ресурсы), в которой впервые была предложена аббревиатура REST, описываются два подхода к созданию Web-сервисов: сервисно-ориентированный и ресурсно-ориентированный. Перед тем как перейти к реализации вашего первого приложения с ресурсно-ориентированной архитектурой (ROA) в соответствии с принципами REST, я поясню различия между этими двумя подходами к проектированию. Кроме того, мы обсудим два популярных, но неравнозначных определения REST. В качестве награды за чтение философской первой части статьи вы найдете далее множество примеров кода с использованием Grails.

Просто о REST

Говоря о предоставлении Web-сервисов, работающих по принципам REST, разработчики, как правило, имеют в виду желание реализовать простой и беспроблемный способ получения данных в формате XML от своих приложений. Такие Web-сервисы обычно публикуют URL, отправив по которому HTTP-запрос типа GET, можно получить документ XML. Чуть ниже будет приведено более строгое определение REST, которое вносит в данное определение маленькое, но важное уточнение.

Yahoo! предоставляет ряд Web-сервисов REST (см. раздел Ресурсы), которые возвращают POX-данные в ответ на простые GET-запросы HTTP. Например, попробуйте обратиться по адресу http://api.search.yahoo.com/WebSearchService/V1/webSearch?appid=YahooDemo&query=beatles. В ответ вы получите те же результаты поиска, что и при обычном поиске через Web-сайт Yahoo!, однако вместо HTML они будут представлены в XML.

Об этой серии

Grails – это инфраструктура разработки Web-приложений, сочетающая использование знакомых Java™-технологий, таких как Spring и Hibernate, с такими современными походами как, например, "соглашения по конфигурации" (convention over configuration). Grails написан на Groovy, обеспечивая бесшовную интеграцию с Java-кодом, добавляя в то же время гибкость и динамизм скриптового языка. Освоив Grails, вы навсегда измените свою точку зрения на разработку Web-приложений.

Если гипотетически представить, что Yahoo! поддерживает интерфейс SOAP (на самом деле это не так), то в ответ на SOAP-запрос должны возвращаться те же данные, однако отправка запроса требовала бы больше усилий от разработчика. Клиент должен был бы отправить корректно-сформированный документ XML с заголовком SOAP и основной секцией вместо простого набора пар типа "имя-значение" в строке запроса. Кроме того, SOAP-запросы должны передаваться методом POST, а не GET. Наконец, выполнив всю ту дополнительную работу, клиентское приложение получит в ответ XML-документ в строгом формате SOAP, имеющий,запрос, секции заголовка и тела, которые надо отбросить, чтобы добраться до самих результатов. Из-за этого подход к реализации Web-сервисов на основе принципов REST иногда называют облегченной альтернативой SOAP.

Можно наблюдать несколько тенденций, говорящих об увеличивающейся популярности архитектуры REST при создании Web-сервисов. В частности, Amazon.com предоставляет как сервисы на основе SOA, так и REST, но статистика указывает на то, что в девяти из десяти случаев пользователи предпочитают REST. Другим примером является Google, который прекратил поддержку Web-сервисов на основе SOAP в декабре 2006 года. В настоящее время его Web-сервисы компании под общим названием Google Data API работают в соответствии с принципами REST.


Сервисно-ориентированные Web-сервисы

Если бы различия между REST и SOAP заключались только в использовании разных типов запросов (GET и POST), то разница была бы очевидна. В данном же случае тип HTTP-запроса хотя и играет важную роль, но не ту, что можно было бы ожидать. Для того чтобы полностью осознать разницу между SOAP и REST, необходимо глубоко разобраться в семантике обеих стратегий. SOAP является реализацией сервисно-ориентированного взгляда на Web-сервисы, в соответствии с которым главную роль при взаимодействии со службами играют их методы (или операции). В основе же REST лежит ресурсно-ориентированный подход, в котором центральное место отводится объектам (ресурсам).

Вызовы сервисов SOA похожи на удаленные вызовы процедур (RPC). В теории, имея Java-класс Weather с методом getForecast(String zipcode)), можно легко предоставить доступ к этому методу через Web-сервис. Именно так работают Web-сервисы Yahoo!. Наберите http://weather.yahooapis.com/forecastrss?p=94089 в адресной строке браузера, заменив значение параметра p на ваш почтовый индекс (ZIP-код). Данный сервис также поддерживает еще один параметр – u , в котором указывается шкала измерения (f означает Фаренгейт, а c – Цельсий). Нетрудно себе представить метод Java-класса с сигнатурой getForecast("94089", "f"), принимающий также и второй параметр.

Запрос к Yahoo!, приведенный несколькими абзацами выше (http://api.search.yahoo.com/WebSearchService /V1/webSearch?appid=YahooDemo&query=beatles), также легко можно свести к вызову метода WebSearchService.webSearch("YahooDemo", "beatles").

Таким образом, вызовы сервисов Yahoo! представляют собой, по сути, вызовы RPC. Не противоречит ли это утверждению, что сервисы Yahoo! обладают архитектурой REST? К сожалению, противоречит. Однако далеко не только я один грешу подобными противоречиями. Сама компания Yahoo! называет свои сервисы REST, хотя и признает, что они не полностью удовлетворяют строгому определению. Страница часто задаваемых вопросов (FAQ) на сайте Yahoo! говорит следующее в ответ на вопрос "Что такое REST?": "REST расшифровывается как передача репрезентативного состояния. Большинство Web-сервисов Yahoo! используют аналогичные REST-операции в стиле RPC, вызываемые при помощи HTTP-запросов типа GET или POST...".

Эта тема вызывает непрекращающиеся споры в сообществе REST. Проблема заключается в отсутствии запоминающегося термина, который бы кратко охарактеризовал "Web-сервисы в стиле RPC, которые предпочитают метод GET методу POST, а также простые запросы по URL-запросам, сформулированным в XML". Некоторые называют такие сервисы HTTP/POX или REST/RPC. Другие используют термин "Low REST" (низшие REST Web-сервисы), чтобы отличать их от "высших" (High REST) Web-сервисов, которые лучше соответствуют принципам, изначально заложенным Филдингом.

Лично я называю сервисы, подобные Yahoo!, GETful, т.е. базирующиеся на методе GET. В этом нет ничего оскорбительного, скорее наоборот – Yahoo! проделала чрезвычайно полезную работу по созданию набора простых в использовании Web-сервисов. Данный термин отражает ключевое преимущество сервисов Yahoo!, а именно выдачу результатов в ответ на простые HTTP-запросы типа GET, не искажая при этом смысла оригинального определения REST, предложенного Филдингом.


Ресурсно-ориентированные Web-сервисы

POST или PUT

В сообществе REST идут споры на тему использования POST и PUT для добавления новых ресурсов. Определение PUT в изначальной версии RFC для HTTP 1.1 (ее основным автором был Филдинг) гласит, что сервер может создать ресурс, если он не существует. Если же ресурс существует, то "... сущность в запросе ДОЛЖНА рассматриваться как модифицированная версия ресурса, присутствующего на сервере". Таким образом, если ресурс отсутствует, то PUT означает его создание, а если присутствует – то обновление. Все было бы просто, если бы в определении POST не было бы следующего:

"POST выступает в качестве унифицированного метода, выполняющего следующие функции:

  • аннотирование существующих ресурсов;
  • добавление сообщений на доску объявлений, новостную группу, список рассылки или подобный набор объектов;
  • передача блоков данных, например, содержимого форм, процессу-обработчику;
  • расширение баз данных при помощи операции добавления (append)".

"Аннотирование существующих ресурсов" означает обновление, а "добавление сообщений на доску объявлений" – добавление ресурсов.

Принимая во внимание отсутствие поддержки метода PUT в основных браузерах при отправке данных форм (в основном они поддерживают только GET и POST), сложно ожидать, что сообщество придет к единому мнению насчет того, какой метод и в каких случаях должен использоваться.

Протокол публикаций Atom (Atom Publishing Protocol) – это популярный формат распространения информации, следующий принципам REST. Авторы RFC Atom попытались поставить точку в спорах на тему POST или PUT.

"В протоколе публикаций Atom методы HTTP-запросов используются для выполнения операций над ресурсами следующим образом:

  • GET используется для получения представления известного ресурса;
  • POST используется для создания нового, динамически именуемого ресурса...
  • PUT используется для редактирования известного ресурса. Данный метод не используется для создания ресурсов;
  • DELETE используется для удаления известного ресурса".

В этой статье я буду придерживаться принципов Atom, используя POST для добавления, а PUT – для редактирования ресурсов. Тем не менее, если вы предпочтете использовать PUT для добавления ресурсов, то также не останетесь в одиночестве – именно такой подход рекомендуется в книге RESTful Web Services (см. раздел Ресурсы).

Итак, что необходимо сделать, чтобы сервис стал ресурсно-ориентированным? Самое главное – это создать хороший универсальный идентификатор ресурса (URI) и использовать HTTP-запросы (типов GET, POST, PUT и DELETE) стандартным образом вместо использования только GET в сочетании с вызовами специализированных методов.

Если вернуться назад к запросу о Beatles, то первым шагом на пути к более строгому интерфейсу REST было бы изменение URI. Вместо передачи строки Beatles в качестве аргумента метода webSearch, Beatles начнет играть роль ресурса, занимающего центральное место в URI. Например, URI статьи о Битлз в Википедии выглядит как http://en.wikipedia.org/wiki/Beatles.

Однако главным различием между философиями GETful и REST является метод получения представления ресурса. RPC-интерфейс Yahoo! определяет набор специальных методов (webSearch, albumSearch, newsSearch и т.д.). При этом узнать имя необходимого метода можно только из документации. В данном случае очевиден принцип именования, поэтому можно предположить, что также существуют методы songSearch, imageSearch и videoSearch, но в этом нет никакой гарантии. Кроме того, другие Web-сайты могут использовать другие соглашения по именованию методов, например, называть их findSong или songQuery. В Grails используется стандартная нотация именования действий, например aiport/list и airport/show, но не существует единого стандарта, который бы действовал для всех Web-инфраструктур.

В REST используется другой подход, а именно: для получения представления ресурса всегда используется метод GET. Таким образом, что бы я ни искал в Википедии (http://en.wikipedia.org/wiki/Beatles, http://en.wikipedia.org/wiki/United_Airlines или http://en.wikipedia.org/wiki/Peanut_butter_and_jelly_sandwich), я всегда знаю, каков стандартный метод получения искомого ресурса.

Польза от стандартизации имен методов проявляется еще ярче при работе с полным циклом CRUD (создание/получение/обновление/удаление) ресурсов. Интерфейсы RPC не предлагают никаких стандартных способов для создания новых ресурсов. Специализированные методы могут называться как угодно - create, new, insert, add и т.д. Напротив, в интерфейсах REST отправка запроса методом POST по указанному URI означает добавление нового ресурса. PUT обновляет ресурсы, а DELETE служит для их удаления (см. заметку POST или PUT).

Теперь, когда вы лучше представляете себе различия между принципами GETful и REST, можно переходить к созданию собственного сервиса в Grails. Ниже будут приведены примеры обоих подходов, а начнем мы с простого POX-сервиса.


Реализация Web-сервиса по принципам GETful в Grails

Наиболее простой способ заставить ваше приложение Grails выдавать информацию в формате XML – это импортировать пакет 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
  }
  
  //... остальной код контроллера
}

Работа с пакетом grails.converters демонстрировалась в статье Реализация отношений типа "многие-ко-многим" в Ajax. Этот пакет предоставляет простые в использовании средства для представления информации в форматах XML и JSON (объектная нотация JavaScript). Результаты вызова операции xmlList показаны на рисунке 1.

Рисунок 1. Формат XML по умолчанию в Grails

Формат XML по умолчанию отлично подходит для отладки, но вполне вероятно, что вам потребуется его изменить. К счастью, метод render() принимает на вход экземпляр MarkupBuilder, который позволяет динамически определять нужный формат (более подробную информацию об этом классе можно получить по ссылке в разделе Ресурсы). Пример вывода информации в измененном формате приведен в листинге 2.

Листинг 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. Вывод информации в специализированном формате при помощи GroovyMarkupBuilder

Обратите внимание на очевидное соответствие между кодом и фрагментом XML. Вы можете использовать любые имена элементов (airports, airport, city) вне зависимости от того, соответствуют они именам полей в классах или нет. В случае, если требуется имя с дефисом (например, official-name) или пространство имен, то достаточно просто заключить наименование элемента в кавычки. Атрибуты (например, id и iata) создаются при помощи конструкции ключ:значение, использующейся для работы с хэш-таблицами в Groovy. Ключи не используются при присвоении значения телу элемента.

Согласование типа содержимого и поле Accept в заголовке

Нет ничего сложного в использования отдельных замыканий для выдачи информации в форматах HTML и XML. Однако что делать, если желательно использовать одно замыкание для обоих форматов? Это возможно благодаря заголовку Accept в запросах HTTP. Этот небольшой набор метаданных в запросе говорит серверу, что у клиента есть предпочтения относительно представления ресурса, находящегося по указанному 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. Изменение поля Accept при помощи cURL
$ 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. В начале файла вы должны увидеть хэш-таблицу, в которой значениями являются MIME-типы, а ключами – простые строковые имена, среди которых есть all и xml. Содержимое хэш-таблицы grails.mime.types приведено в листинге 6.

Листинг 6. Хэш-таблица grails.mime.types в файле Config.groovy
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'
                    ]

Согласование типа содержимого на практике

Значения Accept в заголовке запроса, передаваемые типичными Web-браузерами несколько сложнее, чем те, которые выше были заданы при помощи cURL. Например, Firefox 3.01 под Mac OS X 10.5.4 передает Accept следующего вида:

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

Это значение представляет собой разделенный запятыми список необязательных атрибутов q (сокращение от quality – качество) для указания приоритетов одних MIME-типов над другими. Значением каждого атрибута может быть число типа float в диапазоне от 0.0 до 1.0. В данном примере типу application/xml соответствует значение 0.9, поэтому Firefox будет предпочитать данные в формате XML любому другому MIME-типу.

Ниже приведено значение Accept для браузера Safari 3.3.1, работающего под Mac OS X 10.5.4:

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

В этом случае MIME-типу text/html соответствует приоритет 0.9, поэтому предпочитаемым форматом является HTML, за которым следуют text/plain (приоритет 0.8) и */* (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
    }
  }
}

Последней строкой в каждом блоке форматирования должна быть render, return или redirect , как и в любом обработчике. Если значением Accept является "all" (*/*), то используется первая запись в блоке.

Варьировать значение Accept при помощи cURL достаточно удобно, однако в целях тестирования можно также изменять URI. Например, для явного изменения Accept можно использовать URI http://localhost:8080/trip/airport/list.xml или http://localhost:8080/trip/airport/list?format=xml. Поэкспериментируйте с cURL и различными URI чтобы удостовериться, что ваш блок withFormat работает корректно.

Если вы захотите сделать такое поведение стандартным в Grails, то не забудьте, что для этого можно выполнить команду grails install-templates, а затем подредактировать файлы в директории /src/templates.

Теперь, выполнив все подготовительные действия, нам осталось сделать последний шаг: перейти от Web-сервиса GETful к полноценному сервису RESTful.


Реализация Web-сервиса с архитектурой REST в Grails

Первым делом необходимо сделать так, чтобы контроллер обрабатывал все четыре основных типа 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
  }   
}

Для того чтобы убедиться, что оператор switch работает корректно, можно вновь использовать cURL, как показано в листинге 9.

Листинг 9. Отправка запросов всех четырех типов при помощи 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 не должна представлять трудностей. В этом есть только одна проблема. В ответ на запрос типа GET по адресу http://localhost:9090/trip/airport должен возвращаться список аэропортов, а по адресу http://localhost:9090/trip/airport/den – описание аэропорта с кодом 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 является необязательным.

Добавьте строку в блок static mappings, который связывает REST-запросы с контроллером AirportController, как показано в листинге 10. В данном примере имя контроллера задается жестко, поскольку другие контроллеры пока не способны обрабатывать REST-запросы. Позже вам, скорее всего, потребуется заменить airport в URL выражением $controller.

Листинг 10. Пример специализированного отображения URL
class UrlMappings {
    static mappings = {
      "/$controller/$action?/$id?"{
         constraints { // добавьте сюда ограничения
         }
        }		  
        "/rest/airport/$iata?"(controller:"airport",action:"index")
     "500"(view:'/error')
   }
}

Данное отображение гарантирует, что все запросы по URI, начинающиеся с /rest, будут направлены обработчику index (таким образом, больше нет необходимости в соглашениях по выбору типа контента). Кроме того, это означает, что вы можете проверять наличие параметра params.iata, от которого зависит, что надо возвращать, – конкретное описание аэропорта или список аэропортов.

Измените обработчик index, как показано в листинге 11.

Листинг 11. Возвращение результата 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": //...
  }      
}

Чтобы проверить эффект от нового отображения URL, обратитесь в браузере по адресам http://localhost:9090/trip/rest/airport и http://localhost:9090/trip/rest/airport/den.

Отображение URL на основании метода HTTP-запроса

Существует альтернативный подход к отображению URL в сервисах REST. Запросы можно маршрутизировать на основании метода в HTTP-заголовке. Ниже приведен пример связывания методов GET, PUT, POST и DELETE с предварительно созданными обработчиками Grails:

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

Реализация метода DELETE

Реализация метода DELETE не слишком отличается от GET. Однако в данном случае удаляться могут только конкретные аэропорты, идентифицируемые по коду IATA. Если пользователь отправит HTTP-запрос методом DELETE без кода IATA, то приложение вернет ответ с кодом 400 в заголовке HTTP (некорректный запрос). Если же аэропорт, соответствующий указанному в запросе коду, не найден, то в ответе вернется самый популярный код ошибки – 404 (ресурс не найден). Стандартный код 200 (ОК) будет возвращаться только в случае успешного удаления ресурса (за более подробной информацией о кодах состояния HTTP обратитесь к разделу Ресурсы).

Добавьте содержимое листинга 12 в блок DELETE case в обработчике index.

Листинг 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 //Ресурс не найден
          render "${params.iata} not found."
        }
      }
      else{
        response.status = 400 //Некорректный запрос
        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. Попытка удалить несуществующий аэропорт
$ 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.

Наконец, попробуйте отправить запрос методом DELETE, не указав код IATA вообще (листинг 15).

Листинг 15. Попытка удаления всех аэропортов разом
$ 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. Создайте файл с именем simpleAirport.xml, как показано в листинге 16.

Листинг 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 // аэропорт создан
        render airport as XML
      }
      else{
        response.status = 500 //внутренняя ошибка сервера
        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, отправив файл simpleAirport.xml в теле запроса типа POST (листинг 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>

Замените строку def airport = new Airport(params.airport) в замыкании index на фрагмент кода, показанный в листинге 20.

Листинг 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 представляет собой экземпляр класса groovy.util.XmlSlurper, в котором содержатся сырые данные в формате XML. Фактически он является корневым элементом документа, поэтому вы можете получить доступ к его дочерним вершинам по имени (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

Последним из обязательных методов является 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 //внутренняя ошибка сервера
        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, а также осознавать, что не все Web-сервисы REST одинаковы. Некоторые относятся к так называемому типу GETful, используя HTTP-запросы типа GET для вызовов методов в стиле RPC. Другие же являются подлинно ресурсно-ориентированными. В них ключом для доступа к ресурсам служат URI, а цикл операций CRUD выполняется при помощи запросов типа GET, POST, PUT и DELETE. Вне зависимости от того, предпочитаете вы сервисы RESTful или GETful, вы можете использовать средства Grails для получения и выдачи информации в формате XML.

Следующая статья серии Изучение Grails будет посвящена тестированию. Grails предоставляет отличные встроенные средства для тестирования приложений. Если же какой-либо функциональности не хватает, то ее можно добавить, написав подключаемый модуль. Затрачивая серьезные усилия и время на создание приложений Grails, необходимо позаботиться о том, чтобы ошибки не возникали ни на начальном, ни на последующих этапах разработки. До тех пор получайте удовольствие от изучения Grails.

Ресурсы

Научиться

Получить продукты и технологии

  • Загрузите последний релиз Grails. (EN)
  • Загрузите XFire и Apache Axis2 Plugin - подключаемые модули к Grails, служащие для предоставления интерфейсов SOAP. (EN)
  • cURL: эта утилита входит в стандартную поставку большинства операционных систем семейства UNIX®, Linux®, и Mac OS X. Вы можете загрузить версию для Windows® и практически любую систему. (EN)

Комментарии

developerWorks: Войти

Обязательные поля отмечены звездочкой (*).


Нужен IBM ID?
Забыли Ваш IBM ID?


Забыли Ваш пароль?
Изменить пароль

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Профиль создается, когда вы первый раз заходите в developerWorks. Информация в вашем профиле (имя, страна / регион, название компании) отображается для всех пользователей и будет сопровождать любой опубликованный вами контент пока вы специально не укажите скрыть название вашей компании. Вы можете обновить ваш IBM аккаунт в любое время.

Вся введенная информация защищена.

Выберите имя, которое будет отображаться на экране



При первом входе в developerWorks для Вас будет создан профиль и Вам нужно будет выбрать Отображаемое имя. Оно будет выводиться рядом с контентом, опубликованным Вами в developerWorks.

Отображаемое имя должно иметь длину от 3 символов до 31 символа. Ваше Имя в системе должно быть уникальным. В качестве имени по соображениям приватности нельзя использовать контактный e-mail.

Обязательные поля отмечены звездочкой (*).

(Отображаемое имя должно иметь длину от 3 символов до 31 символа.)

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Вся введенная информация защищена.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Технология Java, Open source
ArticleID=682249
ArticleTitle=Изучение Grails: Grails и архитектура REST
publish-date=06232011