Продолжается подписка на наши издания! Вы не забыли подписаться?

Работа с Grails

Автор: Скотт Дэвис Источник:
Опубликовано: 02.07.2010
Версия текста: 1.1
The article subtitle (in English, you can delete it)
Тестирование
Пишем первый тест
Пишем первый тест, достойный написания
Модульные тесты как исполняемая документация
Добавляем контроллер и представления
Unit-тесты против интеграционных
Пишем тесты, проверяющие корректность сообщений об ошибках
Создание и тестирование ручной проверки корректности (custom validation)
Тестирование пользовательских библиотек TagLib
Заключение
Асинхронный Grails с JSON и Ajax
Краткая история Ajax и JSON
Преимущества JSON
Создание JSON в Grails-контроллере
Тестирование контроллера
Инициализация исходной карты Google Map
Добавление полей формы
Добавляем JavaScript для обработки JSON
Удаленный или локальный JSON?
Создание метода контроллера для выполнения удаленных JSON-запросов
Добавляем ссылку ShowHotels
Вызов Ajax.Remote
Обработка ошибок
Обработка удачного запроса
Заключение
Grails на предприятии
Реализуем JMX-инструментирование
MBean-сервер, Grails и Spring
Использование log4j с Grails
Изменение ConversionPattern в log4j
Глядя на вывод Hibernate DEBUG
Использование Spring Bean Builder
JMX в Groovy
Заключение
Список литературы

Тестирование

Я большой сторонник разработки через тестирование (test-driven development, TDD). Нил Форд (автор книги The Productive Programmer) говорит, что "писать код, который не тестируется – это профессиональная безответственность" (см. Ресурсы). <…> Я же часто говорю, что на один фунт кода в проекте должно приходиться два фунта тестов.

В этой серии TDD пока не рассматривалось, поскольку до сих пор мы концентрировались на том, как воспользоваться функциональностью Grails. Есть определенный смысл в тестировании кода инфраструктуры (то есть кода, который писали не вы), но на практике я этим редко занимаюсь. Я доверяю Grails корректно переводить мои POGO в XML, или сохранять Trip в БД, когда я вызываю trip.save(). Реальная ценность тестирования проявляется при проверке написанного вами кода. Если вы реализуете сложный алгоритм, у вас должно быть несколько модульных (unit) тестов, доказывающих, что этот алгоритм действительно делает то, что он должен делать. В этой статье я покажу, как Grails помогает тестировать приложения.

Пишем первый тест

Чтобы приступить к тестированию, я создам новый доменный класс. Класс будет в итоге содержать некую функциональность, которую нельзя выпускать в свет без тестирования. Введите grails create-domain-class HotelStay, как показано в листинге 1.

Листинг 1. Создание класса HotelStay
$ grails create-domain-class HotelStay

Environment set to development
     [copy] Copying 1 file to /src/trip-planner2/grails-app/domain
Created Domain Class for HotelStay
     [copy] Copying 1 file to /src/trip-planner2/test/integration
Created Tests for HotelStay

Как показано в листинге 1, Grails создает пустой класс в каталоге grails-app/domain. Он также создает класс GroovyTestCase с пустым методом testSomething() в каталоге test/integration (подробнее о разнице между модульными и интеграционными тестами я расскажу чуть ниже). В листинге 2 показаны пустой класс HotelStay и сгенерированный тест.

Листинг 2. Пустой класс и сгенерированный для него тест
class HotelStay { }

class HotelStayTests extends GroovyTestCase 
{
  void testSomething() { }
}

GroovyTestCase – это тонкий Groovy-фасад над модульными тестами JUnit 3.x. Если вы знакомы с TestCase из JUnit, вы уже знаете, как рабтает GroovyTestCase. В обоих случаях вы тестируете код, предполагая, что он делает то, что заявлено. JUnit содержит различные методы для таких предположений – например, assertEquals, assertTrue, and assertNull – позволяющие программно заявить "Я предполагаю, что этот код делает то, что он должен делать".

ПРИМЕЧАНИЕ

Почему JUnit 3.x, а не 4.x?

По историческим причинам GroovyTestCase использует JUnit 3.x TestCase. При выпуске Groovy 1.0 в январе 2007 года поддерживались конструкции языка Java 1.4. Фреймворк работал на JVM от Java 1.4, 1.5 и 1.6, но на уровне языка был совместим с Java 1.4.

Следующая версия Groovy, 1.5, вышла в январе 2008 г. Groovy 1.5 поддерживает все возможности языка Java 1.5 – generic-и, циклы for/in, статический импорт и, что в данном случае важнее всего – аннотации. Тем не менее, Groovy 1.5 тоже работает на Java 1.5 JVM. Команда разработчиков Groovy обещает, все 1.х-версии Groovy будут обратно совместимы с Java 1.4. Поддержка Java 1.4 прекратиться только с выходом Groovy 2.x (скорее всего в 2010 году).

Однако какова же связь между всем этим и версией JUnit, обернутой в GroovyTestCase? В JUnit 4.x появились аннотации, например, @test, @before и @after. Эти новые возможности представляют интерес, но в основе GroovyTestCase остается JUnit 3.x по причинам совместимости с Java 1.4.

Таким образом, ничто не мешает вам использовать JUnit 4.x на свой страх и риск (ссылку на соответствующую документацию на сайте Groovy см. в разделе Ресуры). В равной степени возможно использование фреймворков, использующих аннотации и возможности Java 5 (см. Ресурсы). Groovy совместим с Java на уровне байткода, так что можно использовать любой Java-Framework тестирования.

Добавьте в grails-app/domain/HotelStay.groovy и test/in­tegration/HotelStayTests.groovy код из листинга 3:

Листинг 3. Простой тест
class HotelStay
{
  String hotel
}

class HotelStayTests extends GroovyTestCase 
{
  void testSomething()
  {
    HotelStay hs = new HotelStay(hotel:"Sheraton")
    assertEquals "Sheraton", hs.hotel
  }
}

Да, листинг 3 – это как раз пример того малоценного тестирования инфраструктуры Grails, о котором я говорил выше. Вам следует считать, что Grails выполняет это действие корректно, так что это прекрасный пример неверного типа теста. Но это работает в интересах нашей статьи, позволяя написать простейший тест из возможных и посмотреть, как он работает.

Чтобы запустить все тесты, введите grails test-app (благодаря соглашениям по конфигурации, суффикс Test можно опустить). Какую бы форму команды вы не ввели, в командной строке вы должны увидеть вывод, приведенный в листинге 4 (примечание: я вырезал много лишнего, чтобы выделить то, что действительно важно).

Листинг 4. Результат исполнения теста
$ grails test-app
Environment set to test

No tests found in test/unit to execute ...

-------------------------------------------------------
Running 1 Integration Test...
Running test HotelStayTests...
                    testSomething...SUCCESS
Integration Tests Completed in 253ms
-------------------------------------------------------
Tests passed. View reports in /src/trip-planner2/test/reports

Произошло 4 важных вещи:

  1. Как вы можете видеть, текущее окружение теперь – test. Это означает, что настройки БД в блоке test в файле conf/DataSource.groovy работают.
  2. Скрипты из test/unit исполнены. Вы еще не написали никаких модульных тестов, так что неудивительно, что никаких модульных тестов не найдено.
  3. Скрипты из test/integration исполнены. Вы можете видеть результат скрипта HotelStayTests.groovy – после него идет крупное SUCCESS.
  4. Скрипт предоставляет ссылку на набор отчетов.

Если открыть в браузере open /src/trip-plan­ner2/test/reports/html/index.html, вы увидите отчет по выполнению всех тестов, показанный на рисунке 1.


Рисунок 1. JUnit-отчет верхнего уровня

Если щелкнуть по ссылке HotelStayTests, вы увидите тест doSomething(), как показано на рисунке 2:


Рисунок 2. JUnit-отчет уровня классов

Если тест по каким-то причинам не прошел, командная строка и HTML-отчет (рисунок 3) оповестят вас об этом:


Рисунок 3. JUnit-тест, который не прошел

Пишем первый тест, достойный написания

Теперь, когда первый простой тест работает, займемся написанием более реалистичного примера теста. Предположим, что у класса HotelStay есть два поля: Date checkIn и Date checkOut. Согласно требованиям пользователей, результат метода toString должен выглядеть так: Hilton (Wednesday to Sunday). Получить даты в нужном формате довольно просто благодаря классу java.text.SimpleDateFormat. Нужно написать тест, но не такой, который проверяет, что SimpleDateFormat работает корректно. Ваш тест должен делать две вещи: проверять, что метод toString делает то, что должен, и показывать, что вы справились с пользовательским требованием.

Модульные тесты как исполняемая документация

Требования пользователей обычно попадают к вам на стол в виде какого-либо документа. Ваша работа как разработчика – перевести эти требования в работоспособное ПО.

Проблема с документом, содержащим требования, состоит в том, что он устаревает практически сразу после начала реальной разработки. Это не "живой документ", эволюционирующий вместе с программным обеспечением. В этом случае лучше всего соответствует сути слово "артефакт" – документ описывает исходную, историческую идею того, что должно делать ПО, а не то, что делает текущая его реализация.

Наличие хорошего, полного набора тестов делает больше, чем просто избавляет код от багов. Побочная выгода состоит в том, что вы получаете некую форму "исполняемой документации": живое, изменяющееся представление требований проекта в коде. Если отобразить тесты на требования, вы получите что-то, что можно показать пользователям. У вас появляется доказательство, что код корректен, и что требования пользователей выполнены. Если связать эту исполняемую документацию с сервером непрерывной интеграции, например, с CruiseControl (сервером, который прогоняет тесты непрерывно, раз за разом), вы получите защитный механизм, который гарантирует, что новые возможности не приведут к регрессу в куске хорошего, годного кода.

Behavior-Driven Development (BDD) полностью отражает идею исполняемой документации. easyb - это BDD-фреймворк, написанный на Groovy и позволяющий писать тесты как пользовательские требования в виде, понятном и пользователям, и разработчикам (см. Ресурсы). Если у вас есть прогрессивно мыслящие пользователи, не боящиеся выйти из Microsoft Word (например), easyb может устранить потребность в устаревших документах с требованиями. Требования к проекту станут исполняемыми с самого начала.

Введите в HotelStay.groovy и HotelStayTests.groovy код из листинга 5:

Листинг 5. Использование assertToString
import java.text.SimpleDateFormat
class HotelStay 
{
  String hotel
  Date checkIn
  Date checkOut
   
  String toString(){
    def sdf = new SimpleDateFormat("EEEE")
    "${hotel} (${sdf.format(checkIn)} to ${sdf.format(checkOut)})"
  }  
}

import java.text.SimpleDateFormat
class HotelStayTests extends GroovyTestCase {

    void testSomething(){...}

    void testToString() {
      def h = new HotelStay(hotel:"Hilton")
      def df = new SimpleDateFormat("MM/dd/yyyy")
      h.checkIn = df.parse("10/1/2008")
      h.checkOut = df.parse("10/5/2008")
      println h
      assertToString h, "Hilton (Wednesday to Sunday)"
    }
}

Введите grails test-app, чтобы убедиться, что этот тест проходит.

Метод testToString использует один из новых assert-методов – assertToString – который вводит в игру GroovyTestCase. Конечно, вы могли добиться того же результата с помощью JUnit-метода assertEquals, но assertToString несколько более выразителен. Имя тестового метода и финальное предположение не оставляют сомнений в том, что тест пытается сделать (см. в Ресурсах ссылку на полный лист предположений, поддерживаемых GroovyTestCase, включающий assertArrayEquals, assertContains и assertLength).

Добавляем контроллер и представления

До этого момента вы взаимодействовали с классом HotelStay программно. Добавьте HotelStayController, показанный в листинге 6, чтобы иметь возможность поиграть с этим классом через Web-браузер.

Листинг 6. HotelStayController
class HotelStayController 
{
  def scaffold = HotelStay
}


Рисунок 4. Вывод даты и времени по умолчанию


Рисунок 5. Вывод одной даты

Придется внести небольшие изменения в форму create. По умолчанию поля даты включают день, месяц, год, час и минуты, как показано на рисунке 4.

В данном случае часть поля даты, содержащую время, вполне можно игнорировать. Введите grails generate-views HotelStay. Чтобы создать измененный UI, показанный на рисунке 5, добавьте precision="day" в элемент <g:datePicker> в views/hotelStay/create.gsp и views/ho­telStay/edit.gsp:

Наличие живого, действующего HotelStay, работающего в контейнере сервлетов, ведет прямо к следующей дискуссии о тестировании: модульное тестирование или интеграционное?

Unit-тесты против интеграционных

Как я уже говорил, Grails поддерживает два вида тестов: модульное и интеграционное. Синтаксической разницы между ними нет – оба написаны как GroovyTestCase с использованием одних и тех же предположений. Разница в семантике. Модульное тестирование предполагает тестирование класса в изоляции, а интеграционные тесты позволяют тестировать класс в полном, работающем окружении.

Между нами говоря, если вы захотите писать все Grails-тесты как интеграционные, мне только лучше. Все Grails-команды create-* генерируют соответствующие интеграционные тесты, так что народ в большинстве использует то, что уже есть. Как вы вскоре увидит, большая часть того, что вы собираетесь тестировать, в любом случае потребует работающего окружения, так что интеграционные тесты – это хорошее умолчание.

Если у вас есть второстепенные классы, требующие тестирования, подойдут модульные тесты. Чтобы создать модульный тест, введите grails create-unit-test MyTestUnit. Поскольку тестовые скрипты создаются в рамках одного пакета, модульным и интеграционным тестам нужно задавать уникальные имена. Если этим пренебречь, вы получите неприличное сообщение, показанное в листинге 7:

Листинг 7. Сообщение об ошибке, выдаваемое при одинаковых именах модульного и интеграционного тестов
The sources 
/src/trip-planner2/test/integration/HotelStayTests.groovy and 
   /src/trip-planner2/test/unit/HotelStayTests.groovy are 
   containing both a class of the name HotelStayTests.
 @ line 3, column 1.
   class HotelStayTests extends GroovyTestCase {
   ^

1 error 

Поскольку интеграционные тесты по умолчанию используют суффикс Tests, я даю всем моим модульным тестам суффикс UnitTests для обеспечения уникальности имен.

Пишем тесты, проверяющие корректность сообщений об ошибках

Следующее требование пользователей гласит, что поле с названием отеля не должно быть пустым. Это довольно просто реализовать, используя встроенный в Grails фреймворк валидации. Добавьте в HotelStay блок static constraints, как показано в листинге 8:

Листинг 8. Добавление блока static constraints в HotelStay
class HotelStay 
{
  static constraints = 
  {
    hotel(blank:false)
    checkIn()
    checkOut()
  }
  
  String hotel
  Date checkIn
  Date checkOut
  
  // остальная часть класса не изменяется
}

Введите grails run-app. Если вы попытаетесь создать HotelStay, не заполняя поле с названием отеля, вы должны будете получить сообщение об ошибке, показанное на рисунке 6.


Рисунок 6. Сообщение об ошибке для незаполненных полей, используемое по умолчанию.

Я уверен, что ваши пользователи будут счастливы от такой возможности, но вот сообщение по умолчанию их не впечатлит. Допусти, что они немного изменят требование: поле "отель" не может быть пустым; если же оно не заполнено, сообщение об ошибке должно гласить "Пожалуйста, введите название отеля".

Теперь, когда вы добавляете некий собственный код – даже просто строку – пора создать тест (конечно, создание теста для доказательства выполнения пользовательского требования – даже если в этом не участвует написанный вами код – тоже вполне допустимо).

Откройте grails-app/i18n/messages.properties и добавьте hotelStay.hotel.blank=Please provide a hotel name. Попытайтесь оставить название отеля пустым в браузере. Вы должны получить сообщение, показанное на рисунке 7.


Рисунок 7. Вывод пользовательского сообщения об ошибке.

Добавьте в HotelStayTests.groovy новый тест, чтобы убедиться, что проверка на пустое поле работает, как показано в листинге 9.

Листинг 9. Тестирование ошибок при проверках
class HotelStayTests extends GroovyTestCase 
{
  void testBlankHotel(){
    def h = new HotelStay(hotel:"")
    assertFalse "there should be errors", h.validate()
    assertTrue "another way to check for errors after you call validate()", 
      h.hasErrors()
  }

  //остальные тесты не изменяются
}

Вы уже видели метод save(), который добавляется к каждому классу в генерируемых контроллерах. Я мог бы вызвать его и здесь, но я не хочу сохранять новый класс в БД. Все, что меня беспокоит – это выполняется ли проверка. Это выполняет метод validate(). Если проверка не срабатывает, он возвращает false, и наоборот.

Метод hasErrors() – это еще один ценный при тестировании метод. После вызова save() или validate() hasErrors() позволяет проверить, были ли ошибки.

Листинг 10 содержит расширенную версию testBlankHotel(), которая содержит еще пару ценных методов валидации.

Листинг 10. Расширенный тест проверок корректности
class HotelStayTests extends GroovyTestCase 
{
  void testBlankHotel()
  {
    def h = new HotelStay(hotel:"")
    assertFalse "there should be errors", h.validate()
    assertTrue "another way to check for errors after you call validate()", 
                h.hasErrors()  
  
   println "\nErrors:"
   println h.errors ?: "no errors found"   
     
   def badField = h.errors.getFieldError('hotel') 
   println "\nBadField:"
   println badField ?: "hotel wasn't a bad field"
   assertNotNull "I'm expecting to find an error on the hotel field", badField


   def code = badField?.codes.find {it == 'hotelStay.hotel.blank'} 
   println "\nCode:"
   println code ?: "the blank hotel code wasn't found"
   assertNotNull "the blank hotel field should be the culprit", code
  }
} 

Когда вы убедитесь, что класс не проходит проверку, можно вызвать метод getErrors() (здесь сокращенный до просто errors благодаря синтаксиcу getter-ов в Groovy), чтобы получить org.springframework.validation.Bean­Pro­pertyBindingResult. Так же как GORM – это тонкий Groovy-фасад над Hibernate, проверки Grails – это на самом деле просто проверки Spring.

Результат вызовов println не будет показан в командной строке, но в HTML-отчете их видно, как показано на рисунке 8:


Рисунок 8. Вывод println

Щелкните по ссылке System.out в правом нижнем углу отчета HotelStayTests.

Оператор, любовно поименованный Элвис (видите его прическу и два глаза?), в листинге 10 – это тернарный оператор Groovy. Если объект слева от ?: равен null, вместо него используется значение, стоящее справа.

Измените поле отеля на "Holiday Inn" и заново запустите тесты. Вы должны увидеть альтернативный вывод оператора Элвис в HTML-отчете, как показано на рисунке 9.


Рисунок 9. Результат теста

Не забудьте очистить поле отеля после того, как увидите Элвиса – вам же не нужно оставлять нерабочие тесты.

Не стоит беспокоиться, что все еще обнаруживаются ошибки проверки для checkIn and checkOut. В том, что касается этого теста, вы можете их спокойно игнорировать. Однако это иллюстрация того, что простой проверки на наличие ошибок недостаточно — нужно удостовериться, что выдается именно нужное вам сообщение.

Обратите внимание, что я не проверяю точный текст сообщения об ошибке. Почему это меня волновало в прошлый раз (при тестировании вывода toString), а в этот раз не волнует? Вывод метода toString был главной темой предыдущего теста. На сей раз меня меньше беспокоит то, что Grails правильно выводит сообщение, чем простое подтверждение, что выполняется код проверки. Это должно показать, что тестирование – больше искусство, чем наука (если бы я хотел проверить корректность точного вывода сообщения, то я, вероятно, использовал бы инструмент тестирования Web-слоя, например, Canoo WebTest или ThoughtWorks Selenium).

Создание и тестирование ручной проверки корректности (custom validation)

Пора браться за следующее требование пользователей. Нужно добиться, чтобы даты заселения (checkout) не шли перед датами выписки (checkIn). Для этого придется написать собственную проверку. Если вы ее пишете, вам ее и тестировать.

Добавьте код проверки из листинга 11 в блок static constraints:

Листинг 11.
class HotelStay 
{
  static constraints = 
  {
    hotel(blank:false)
    checkIn()
    checkOut(validator:{val, obj->
      return val.after(obj.checkIn)
  })
  }
  
  // остальная часть класса не изменяется
}

Переменная val – это текущее поле. Переменная obj представляет текущий экземпляр HotelStay. Groovy добавляет методы before() и after() ко всем объектам Date, так что эта проверка просто возвращает результаты вызова метода after(). Если checkOut следует за checkIn, проверка возвращает true. Если нет, она возвращает false и генерирует сообщение об ошибке.

Теперь введите grails run-app. Проверьте, что вы не можете создать HotelStay с датой checkout более ранней, чем дата checkIn, как показано на рисунке 10:


Рисунок 10. Сообщение об ошибке при проверке, выводимое по умолчанию

Откройте grails-app/i18n/messages.properties и добавьте сообщение для поля checkOut field: hotelStay.check­Out.validator.invalid=Sorry, you cannot check out before you check in.

Сохраните файл messages.properties и еще раз попытайтесь сохранить неверно сформированный HotelStay. Вы должны увидеть сообщение об ошибке, показанное на рисунке 11.


Рисунок 11. Сообщение об ошибке при проверке

Теперь пора написать тест, показанный в листинге 12.

Листинг 12. Тестирование вашей собственной проверки.
import java.text.SimpleDateFormat
class HotelStayTests extends GroovyTestCase 
{
  void testCheckOutIsNotBeforeCheckIn()
  {
    def h = new HotelStay(hotel:"Radisson")
    def df = new SimpleDateFormat("MM/dd/yyyy")
    h.checkIn = df.parse("10/15/2008")
    h.checkOut = df.parse("10/10/2008")
  
    assertFalse "there should be errors", h.validate()
    def badField = h.errors.getFieldError('checkOut') 
    assertNotNull "I'm expecting to find an error on the checkOut field", 
                   badField
    def code = badField?.codes
      .find { it == 'hotelStay.checkOut.validator.invalid' } 
    assertNotNull "the checkOut field should be the culprit", code 
  }
}

Тестирование пользовательских библиотек TagLib

Осталось последнее пользовательское требование. Вы удачно избавились от меток времени в checkIn и checkOut в представлениях create и edit, но они остались в представлениях list и show, как можно видеть на рисунке 12.


Рисунок 12. Вывод даты в Grails по умолчанию

Самый простой образ действий – создать новый TagLib. Тег <g:formatDate> в Grails уже определен, и вы можете им воспользоваться, но создать собственный тег очень несложно. Я хочу создать тег <g:custom­Date­Format>, который можно использовать двумя способами.

Одна форма тега <g:customDateFormat> оборачивает Date и принимает пользовательский атрибут формата, который принимает любой корректный образец SimpleDateFormat.

<g:customDateFormat format="EEEE">${new Date()}</g:customDateFormat>

Поскольку большинство обычных вариантов использования возвращает даты в американском формате "MM/dd/yyyy", я хочу, чтобы этот формат использовался по умолчанию:

<g:customDateFormat>${new Date()}</g:customDateFormat>

Теперь, когда пользовательские требования понятны, введите grails create-tag-lib Date, как показано в листинге 13, чтобы создать новый файл DateTagLib.groovy и соответствующий ему файл DateTagLibTests.groovy.

Листинг 13. Создание нового TagLib
$ grails create-tag-lib Date
[copy] Copying 1 file to /src/trip-planner2/grails-app/taglib
Created TagLib for Date
[copy] Copying 1 file to /src/trip-planner2/test/integration
Created TagLibTests for Date

Внесите в DateTagLib.groovy код из листинга 14.

Листинг 14. Код нового TagLib
import java.text.SimpleDateFormat

class DateTagLib 
{
  def customDateFormat = 
  {
    attrs, body ->
      def b = attrs.body ?: body()
      def d = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").parse(b)
    
      // в отсутствие атрибутов формата используйте эти
      def pattern = attrs["format"] ?: "MM/dd/yyyy"
      out << new SimpleDateFormat(pattern).format(d)
  }
}

TagLib принимает простые строковые значения в виде атрибутов и тела тега и посылает строку в выходной поток. Поскольку вы собираетесь использовать этот тег как обертку для неформатированного поля Date, вам потребуются два объекта SimpleDateFormat. Первый объект используется для распознавания входящей строки, которая содержит строковое представление даты, полученное с помощью метода Date.toString(). Второй объект SimpleDateFormat преобразует ее обратно в строку, используя другой формат даты.

Оберните поля checkIn и checkout из list.gsp и show.gsp в новый TagLib, как показано в листинге 15:

Листинг 15. Использование новой TagLib
<g:customDateFormat>${fieldValue(bean:hotelStay, field:'checkIn')}</g:customDateFormat>

Введите grails run-app и посетите http://local­host:9090/trip/hotelStay/list, чтобы проверить, что ваш TagLib выдает ожидаемый результат, как показано на рисунке 13.


Рисунок 13. Вывод даты с использованием нового TagLib

В листинге 16 показана пара тестов, проверяющих, что TagLib работает как ожидается.

Листинг 16. Тестирование новой TagLib
import java.text.SimpleDateFormat

class DateTagLibTests extends GroovyTestCase 
{
  void testNoFormat() 
  {
    def output = 
      new DateTagLib().customDateFormat(
        format:null, body:"2008-10-01 00:00:00.0")
      println "\ncustomDateFormat using the default format:"
      println output
      
      assertEquals "was the default format used?", "10/01/2008", output
  }

  void testCustomFormat() 
  {
    def output = 
      new DateTagLib().customDateFormat(
        format:"EEEE", body:"2008-10-01 00:00:00.0")
      assertEquals "was the custom format used?", "Wednesday", output
  }
}

Заключение

Теперь, когда вы создали несколько тестов и убедились, насколько просто тестировать Grails-компоненты, вы можете и дальше заниматься разработкой через тестирование. Вы будете более уверены в своей работе, и если ваши тесты будут соответствовать пользовательским требованиям, вы получите выигрыш от наличия постоянно обновляемой исполняемой документации.

В следующей статье речь пойдет о JavaScript Object Notation (JSON). В Grails встроена замечательная поддержка JSON. Вы увидите, как генерировать JSON из контроллера и как использовать его из JSP.

Асинхронный Grails с JSON и Ajax

В этой статье обсуждается имеющаяся в Grails поддержка таких дополнительных технологий, как JSON и Ajax. В предыдущих статьях они играли вспомогательную роль, а в этот раз они займут центральное место. Вы выполните Ajax-запрос, используя библиотеку Prototype, а также Grails-тег <formRemote>. Вы также увидите примеры как работы с локальным JSON, так и динамического использования удаленного JSON через Web.

Чтобы увидеть, как все это работает, вы соберете воедино страницу планирования поездок, на которой пользователь может указать аэропорты отправки и назначения. Когда аэропорты будут отображены на Google Map, ссылки позволят выбрать отель неподалеку от аэропорта назначения. На рисунке 14 показано использование этой страницы:


Рисунок 14. Страница планирования поездки.

Всю эту функциональность можно реализовать, написав около 150 строк кода в одном GSP-файле и трех контроллерах.

Краткая история Ajax и JSON

В середине 90-х, во времена появления Web, браузеры позволяли выполнять только крупнозернистые HTTP-запросы. Щелчок по гиперссылке или кнопке отправки формы приводил к обновлению всей страницы. Это было хорошо при навигации по страницам, но индивидуальные компоненты страницы не могли независимо обновляться.

С выпуском Internet Explorer 5.0 в 1999 году Microsoft представил объект XMLHTTP. Этот новый объект позволил разработчикам делать "микро" HTTP -запросы, не изменяющие окружающую HTML-страницу. Эта возможность не была основана на стандарте World Wide Web Consortium (W3C), но команда Mozilla оценила ее потенциал и ввела в Mozilla 1.0 в 2002 году объект XMLHttpRequest (XHR). С тех пор он стал стандартом де-факто, поддерживаемым всеми основными Web-бра­узерами.

В 2005 году широкой публике были представлены Google Maps. Их широкое использование асинхронных HTTP-запросов представляет резкий контраст с другими современными картографическими Web-сайтами. Вместо того, чтобы щелкнуть мышью и ожидать перезагрузки страницы, в Google Map вы гладко перемещаете карту мышью. Jesse James Garrett в своем сообщении в блоге, описывающем набор технологий, используемых в Google Maps, использовал броскую аббревиатуру Ajax, и эта кличка прилипла навсегда (см. Ресурсы).

В последние годы Ajax стал скорее своего рода "зонтичным" термином для "Web 2.0"-приложений, чем для набора технологий. Запросы как правило асинхронны и производятся с помощью JavaScript, но ответы – это не всегда XML. Проблема с XML в браузерных приложениях – это отсутствие родного, легкого в использовании JavaScript-парсера. Конечно, можно разбирать XML XML с помощью JavaScript DOM API, но для новичка это непросто. Соответственно, Ajax Web-сервисы часто возвращают результат в виде простого текста, кусков HTML или JSON.

В июле 2006 года Дуглас Крокфорд (Douglas Crockford) отправил в Internet Engineering Task Force (IETF) RFC 4627 с описанием JSON. К концу года основные сервис-провайдеры, например, Yahoo! и Google, предлагали JSON-ответы в качестве альтернативы XML (см. Ресурсы, ниже в этой статье вы воспользуетесь JSON Web-сервисами Yahoo!).

Преимущества JSON

В том, что касается Web-разработки, у JSON есть два преимущества перед XML. Прежде всего, он менее многословен. JSON-объект – это просто набор разделенных запятыми пар имя:значение, заключенный в фигурные скобки. XML же использует в качестве обертки для данных дублирующиеся открывающие и закрывающие теги. Это вдвое увеличивает размер метаданных по сравнению с JSON, что позволило Крокфорду назвать JSON "обезжиренной альтернативой XML" (см. Ресурсы). Если вы имеете дело с "тонким каналом", каждое уменьшение количества пересылаемых байт приносит реальный выигрыш в производительности.

В листинге 17 показано, как одна и та же информация организуется в JSON и XML.

Листинг 17. Сравнение JSON и XML
{"city":"Denver", "state":"CO", "country":"US"}

<result>
  <city>Denver</city>
  <state>CO</state>
  <country>US</country>
</result>

Для Groovy-программиста JSON-объекты должны выглядеть знакомо: если заменить фигурные скобки квадратными, вы получите Groovy-определение HashMap. И, если уж речь зашла о квадратных скобках, массив JSON-объектов определяется точно так же, как как массив Groovy-объектов. JSON-массив – это просто серия разделенных запятыми записей, заключенная в квадратные скобки, как показано в листинге 18.

Листинг 18. Список JSON-объектов
[{"city":"Denver", "state":"CO", "country":"US"},
 {"city":"Chicago", "state":"IL", "country":"US"}]

Второе преимущество JSON становится очевидным, когда вы его разбираете и работаете с ним. Загрузка JSON в память – это один вызов eval(). После загрузки вы можете напрямую обращаться к любому полю по имени, как показано в листинге 19.

Листинг 19. Загрузка JSON и обращение к полям
var json = '{"city":"Denver", state:"CO", country:"US"}'
var result = eval( '(' + json + ')' )
alert(result.city)

XmlSlurper в Groovy предоставляет такой же прямой доступ к XML-элементам (с XmlSlurper вы уже работали в статье "Службы Grails и Google Maps"). Если бы современные Web-браузеры поддерживали Groovy как клиенты, меня бы куда меньше интересовал JSON. К сожалению, Groovy – это строго серверное решение. JavaScript – это единственное казино в городе, когда дело доходит до клиентской разработки. Так что я предпочитаю работать с XML в Groovy на серверной стороне и с JSON в JavaScript на клиентской стороне. В обоих случаях я могу добраться до данных с минимальными усилиями.

Теперь, когда вы получили представление о JSON, пора заставить ваше Grails-приложение сгенерировать какой-нибудь JSON.

Создание JSON в Grails-контроллере

Впервые вы возвращали JSON из Grails-контроллера в статье "Отношения многие-ко-многим с ложкой Ajax". Замыкание в листинге 20 похоже на то, что вы создавали в тот раз. Различие в том, что это замыкание доступно через дружественный Uniform Resource Identifier (URI), как это было описано в "RESTful Grails". Оно также использует оператор Elvis, который был показан в предыдущей статье.

Добавьте замыкание iata в класс grails-app/cont­rollers/AirportMappingController.groovy, который вы создали в статье "Grails и унаследованные базы данных", и не забудьте импортировать в начале файла пакет grails.converters, как показано в листинге 20.

Листинг 20. Конвертация Groovy-объектов в JSON
import grails.converters.*
class AirportMappingController 
{
    def iata = 
    {
      def iata = params.id?.toUpperCase() ?: "NO IATA"
      def airport = AirportMapping.findByIata(iata)
      if(!airport)
      {
        airport = new AirportMapping(iata:iata, name:"Not found")
      }
      render airport as JSON
    }
}

Попробуйте открыть в своем браузере страницу http://localhost:9090/trip/airportMapping/iata/den. Вы должны увидеть JSON-результаты, показанные в листинге 21.

Листинг 21. Корректный объект AirportMapping в JSON
{"id":328,
"class":"AirportMapping",
"iata":"DEN",
"lat":"39.858409881591797",
"lng":"-104.666999816894531",
"name":"Denver International",
"state":"CO"}

Можно также вызвать http://localhost:9090/trip/air­port­Mapping/iata или http://localhost:9090/trip/airport­Mapp­ing/iata/foo, чтобы убедиться, что возвращается "Not Found". В листинге 22 показан результирующий некорректный JSON-объект.

Листинг 22. Некорректный объект AirportMapping, сохраненный в JSON
{"id":null,
"class":"AirportMapping",
"iata":"FOO",
"lat":null,
"lng":null,
"name":"Not found",
"state":null}

Конечно, такая "проверка на вшивость" не заменяет реальный набор тестов.

Тестирование контроллера

Создайте файл AirportMappingControllerTests.groovy в каталоге test/integration. Добавьте в него два теста из листинга 23.

Листинг 23. Тестирование Grails-контроллера
class AirportMappingControllerTests extends GroovyTestCase
{
  void testWithBadIata()
  {
    def controller = new AirportMappingController()
    controller.metaClass.getParams = {->
      return ["id":"foo"]
    }
    controller.iata()
    def response = controller.response.contentAsString
    assertTrue response.contains("\"name\":\"Not found\"")
    println "Response for airport/iata/foo: ${response}"
  }
  void testWithGoodIata()
  {
    def controller = new AirportMappingController()
    controller.metaClass.getParams = {->
      return ["id":"den"]
    }
    controller.iata()
    def response = controller.response.contentAsString
    assertTrue response.contains("Denver")
    println "Response for airport/iata/den: ${response}"
  }
}

Введите $grails test-app, чтобы запустить тесты. В HTML-отчетах JUnit вы должны увидеть сообщение об успешном тестировании, как показано на рисунке 15.


Рисунок 15. Результат тестов в JUnit

Вот что происходит в testWithBadIata() в листинге 23. Первая строка (очевидно) создает экземпляр AirportMappingController. Это делается, чтобы позже иметь возможность вызвать controller.iata() и написать утверждение относительно результирующего JSON. Для того чтобы вызов окончился неудачей (в данном случае) или успехом (в случае testWithGoodIata()) нужно инициализировать значение параметра params id некоторым значением. Обычно строка запроса разбирается и помещается в свойство params. Однако в случае теста нет HTTP-запроса, который нужно разбирать. Вместо этого используется Groovy-метапрограммирование для того, чтобы напрямую переопределить метод getParams, заставляя его возвращать требуемые для теста значения. Это делается путем замены в метаклассе ссылки на метод getParams ссылкой на HashMap, содержащие требуемые значения.

Теперь, когда поставщик JSON работет и протестирован, пора перейти к отображению JSON на Web-стра­нице.

Инициализация исходной карты Google Map

Я хочу, чтобы страница планирования была доступна по адресу http://localhost:9090/trip/trip/plan. Это означает добавление замыкания plan в grails-app/cont­rollers/TripController.groovy, как показано в листинге 24.

Листинг 24. Создание контроллера
class TripController 
{
  def scaffold = Trip
  def plan = {}
}

Поскольку plan() не содержит в конце render() или redirect(), соглашение по конфигурации диктует, что будет выведена grails-app/views/trip/plan.gsp. Создайте HTML-файл, приведенный в листинге 25 .

Листинг 25. Настройка Google Map
<html>
  <head>
    <title>Plan</title>
    <script src="http://maps.google.com/maps?file=api&v=2&key=YourKeyHere"
      type="text/javascript"></script>
    <script type="text/javascript">
    var map
    var usCenterPoint = new GLatLng(39.833333, -98.583333)
    var usZoom = 4
    function load() 
    {
      if (GBrowserIsCompatible()) 
      {
        map = new GMap2(document.getElementById("map"))
        map.setCenter(usCenterPoint, usZoom)
        map.addControl(new GLargeMapControl());
        map.addControl(new GMapTypeControl());
      }
    }
    </script>
  </head>
  <body onload="load()" onunload="GUnload()">
    <div class="body">
      <div id="search" style="width:25%; float:left">
      <h1>Where to?</h1>
      </div>
      <div id="map" style="width:75%; height:100%; float:right"></div>
    </div>
  </body>
</html>

Если все в порядке, откройте в браузере страницу http://localhost:9090/trip/trip/plan. Вы должны увидеть что-то похожее на рисунок 16.


Рисунок 16.

Теперь, когда карта на месте, нужно добавить пару полей для аэропортов вылета и прибытия.

Добавление полей формы

В "Отношениях многие-ко-многим с ложкой Ajax" вы использовали объект Ajax.Request из Prototype. Позже в этой статье вы будете его использовать еще раз, когда получите JSON из удаленного источника. А пока воспользуйтесь тегом <g:formRemote>. Добавьте HTML из листинга 26 в grails-app/views/trip/plan.gsp:

Листинг 26. Использование <g:formRemote>
<div id="search" style="width:25%; float:left">
<h1>Where to?</h1>
<g:formRemote name="from_form"
              url="[controller:'airportMapping', action:'iata']"
              onSuccess="addAirport(e, 0)">
  From:<br/>
  <input type="text" name="id" size="3"/>
  <input type="submit" value="Search" />
</g:formRemote>
<div id="airport_0"></div>
<g:formRemote name="to_form"
              url="[controller:'airportMapping', action:'iata']"
              onSuccess="addAirport(e, 1)">
  To: <br/>
  <input type="text" name="id" size="3"/>
  <input type="submit" value="Search" />
</g:formRemote>
<div id="airport_1"></div>
</div>

Нажмите кнопку Refresh в Web-браузере, чтобы увидеть изменения, показанные на рисунке 17:


Рисунок 17. Добавленные поля формы

Использование обычного <g:form> приведет к обновлению всей страницы при отправке формы пользователем. Выбирая <g:formRemote>, вы заставляете Ajax.Re­quest выполнять отправку формы асинхронно, в фоновом режиме. Имя поле для ввода текста – id, что гарантирует, что params.id будет заполнен в контроллере. Атрибут url тега <g:formRemote> ясно показывает, что при щелчке по кнопке submit вызывается Airport­MappingController.iata().

В статье "Отношения многие-ко-многим с ложкой Ajax" нельзя было использовать <g:formRemote>, поскольку нельзя вложить одну HTML-форму в другую. Однако в данном случае можно создать две отдельных формы и не беспокоиться о том, чтобы писать Prototype-код вручную. Результаты асинхронного JSON-запроса будет передан JavaScript-функции addAirport().

Следующая задача – создать addAirport().

Добавляем JavaScript для обработки JSON

Функция addAirport(), которую требуется создать, делает две простые вещи: она загружает в память JSON-объект, а затем использует поля в различных целях. В данном случае используются значения широты и долготы для создания GMarker и добавления его на карту.

Чтобы <g:formRemote> работал, проверьте, что в начало заголовочной секции включена библиотека Prototype, как показано в листинге 27.

Листинг 27. Включение Prototype в GSP
<g:javascript library="prototype" />

Теперь поместите JavaScript из листинга 28 после функции init().

Листинг 28. Реализация addAirport и drawLine
<script type="text/javascript">
var airportMarkers = []
var line
function addAirport(response, position) 
{
  var airport = eval('(' + response.responseText + ')')
  var label = airport.iata + " -- " + airport.name
  var marker = new GMarker(new GLatLng(airport.lat, airport.lng),
    {title:label})
  marker.bindInfoWindowHtml(label)

  if(airportMarkers[position] != null)
    map.removeOverlay(airportMarkers[position])

  if(airport.name != "Not found")
  {
    airportMarkers[position] = marker
    map.addOverlay(marker)           
  }

  document.getElementById("airport_" + position).innerHTML = airport.name
  drawLine()
}
function drawLine()
{
  if(line != null)
    map.removeOverlay(line)
  
  if(airportMarkers.length == 2)
  {
    line = new GPolyline([airportMarkers[0].getLatLng(),
      airportMarkers[1].getLatLng()])
    map.addOverlay(line)
  }
}    
</script>

Первое, что делает код из листинга 28 - объявляет пару новых переменных: одну для того, чтобы хранить линию, и массив для хранения двух маркеров аэропортов. После применения eval() ко входящему JSON вы вызываете поля airport.iata, airport.name, airport.lat и airport.lng напрямую (чтобы вспомнить, как выглядит JSON-объект, вернитесь к листингу 21).

После загрузки объекта airport нужно создать новый GMarker. Это всем знакомый "красный маркер", используемый в Google Maps. Метод bind­Info­Win­dow­Html() говорит API, что отображать при щелчке по маркеру. После добавления маркера на карту вызывается функция drawLine(). Как и следует из ее названия, она рисует линию между двумя маркерами аэропортов, если таковые имеются.


Рисунок 18. Два аэропорта с линией между ними.

Подробнее о таких объектах Google Maps API как GMarker, GLatLng и GPolyline можно прочитать в онлайн-документации (см. Ресурсы).

После ввода пары аэропортов страница должна выглядеть так, как показано на рисунке 18.

Не забывайте обновлять Web-браузер при каждом изменении GSP-файла.

Теперь, когда у вас есть пример использования JSON, возвращаемого локально из Grails-приложения, пора несколько расширить горизонты. В следующем разделе вы динамически получите JSON от удаленного Web-сервиса. Конечно, получив его, вы будете работать с ним так же, как в этом примере: загрузите в память и будете напрямую обращаться к различным атрибутам.

Удаленный или локальный JSON?

Следующая задача – вывести 10 ближайших к аэропорту назначения отелей. Это почти наверняка потребует получения удаленных данных.

На вопрос, хранить ли данные локально или получать их удаленно по запросу, нет стандартного ответа. Набор аэропортов я считаю правильным хранить локально. Эти данные широко доступны и их достаточно легко скачать (в США всего 901 аэропорт, и число основных аэропортов остается неизменным; такой список вряд ли быстро устареет).

Если бы список аэропортов чаще менялся, был слишком большим для локального хранения или просто не был доступен, я бы склонялся к тому, чтобы хранить его удаленно. Служба геокодирования geonames.org, использовавшаяся в статье "Службы Grails и Google Maps", наряду с XML, предлагает выдачу JSON-результатов (см. Ресурсы). Введите в браузере http://ws.geo­names.org/search?name_equals=den&fcode=airp&style=full&type=json. Вы увидите JSON-результат, приведенный в листинге 29.

Листинг 29. JSON-результаты от GeoNames
{"totalResultsCount":1,
"geonames":[
  {"alternateNames":[
    {"name":"DEN","lang":"iata"},
    {"name":"KDEN","lang":"icao"}],
  "adminCode2":"031",
  "countryName":"United States",
  "adminCode1":"CO",
  "fclName":"spot, building, farm",
  "elevation":1655,
  "countryCode":"US",
  "lng":-104.6674674,
  "adminName2":"Denver County",
  "adminName3":"",
  "fcodeName":"airport",
  "adminName4":"",
  "timezone":{
    "dstOffset":-6,
    "gmtOffset":-7,
    "timeZoneId":"America/Denver"},
  "fcl":"S",
  "name":"Denver International Airport",
  "fcode":"AIRP",
  "geonameId":5419401,
  "lat":39.8583188,
  "population":0,
  "adminName1":"Colorado"}]
}

Как видите, сервис GeoNames предлагает больше информации об аэропортах, чем данные USGS, импортированные в статье "Grails и унаследованные БД". Если появятся новые пользовательские требования, например, если нужно знать часовой пояс аэропорта или высоту над уровнем моря, GeoNames станет конкурентоспособной альтернативой. Он содержит также и сведения о зарубежных аэропортах, например, лондонском Хитроу (LHR) или Франкфурте (FRA). Преобразование AirportMapping.iata() для скрытого использования GeoNames я оставлю вам в качестве дополнительного упражнения.

Тем временем, единственная возможность вывести список отелей, находящихся в непосредственной близости от аэропорта назначения – воспользоваться удаленным Web-сервисом. Постоянно изменяющийся список содержит тысячи отелей, так что позволить кому-то еще заниматься содержимым этго списка очень соблазнительно.

Yahoo! предлагает локальный поисковый сервис, позволяющий искать фирмы по адресу, ZIP-коду или даже по широте/долготе (см. Ресурсы). Если в "RESTful Grails" вы зарегистрировались и получили код разработчика, можете использовать его здесь. Неудивительно, что форматы URI поискового запроса, использовавшегося там, и локального поиска, использующегося здесь, очень похожи. В тот раз вы позволили Web-сервису по умолчанию возвращать XML. Добавив еще одну пару имя=значение (то есть output=json), вы получите вместо XML JSON.

Введите в браузере следующее (без разрывов строк), чтобы увидеть JSON-список отелей неподалеку от Денверского международного аэропорта:

http://local.yahooapis.com/LocalSearchService/V3/localSearch?appid=
   YahooDemo&query=hotel&latitude=39.858409881591797&longitude=
   -104.666999816894531&sort=distance

В листинге 30 показаны (сокращенные) JSON-результаты:

Листинг 30. JSON-результаты от Yahoo!
{"ResultSet":
  {"totalResultsAvailable":"803",
  "totalResultsReturned":"10",
  "firstResultPosition":"1",
  "ResultSetMapUrl":"http:\/\/maps.yahoo.com\/broadband\/?tt=hotel&tp=1",
  "Result":[
    {"id":"42712564",
    "Title":"Springhill Suites-Denver Arprt",
    "Address":"18350 E 68th Ave",
    "City":"Denver",
    "State":"CO",
    "Phone":"(303) 371-9400",
    "Latitude":"39.82076",
    "Longitude":"-104.673719",
    "Distance":"2.63",
    [SNIP]

Теперь, когда у вас есть рабочий список отелей, нужно создать метод контроллера, как в случае AirportMapping.iata().

Создание метода контроллера для выполнения удаленных JSON-запросов

У вас уже должен быть HotelController из предыдущей статьи. Добавьте в него замыкание near из листинга 31.

Листинг 31. HotelController
class HotelController 
{
  def scaffold = Hotel
  def near = 
  {
    def addr = "http://local.yahooapis.com/LocalSearchService/V3/localSearch?"
    def qs = []
    qs << "appid=YahooDemo"
    qs << "query=hotel"
    qs << "sort=distance"
    qs << "output=json"
    qs << "latitude=${params.lat}"
    qs << "longitude=${params.lng}"
    def url = new URL(addr + qs.join("&"))
    render(contentType:"application/json", text:"${url.text}")
  }
}

Все параметры строки запроса прописаны в коде, кроме двух последних: широты и долготы. Предпоследняя строка создает новый java.net.URL. Последняя строка вызывает сервис (url.text) и показывает результаты. Поскольку вы не используете JSON-конвертер, нужно явно задать MIME-тип application/json. render возвращает text/plain, если не указано другое.

Введите в браузере следующее (без разрывов):

http://localhost:9090/trip/hotel/near?lat=
   39.858409881591797&lng=-104.666999816894531

Сравните результат с прямым вызовом http://local.yahooapis.com, выполнявшимся ранее – они должны быть идентичны.

ПРИМЕЧАНИЕ

Почему я не могу вызвать удаленные Web-сервисы напрямую из браузера?

Если поместить в Ajax.Request URL local.yahoo­apis.com, ничего не выйдет. Это работает при вводе в адресной строке браузера, но не работает при программном вызове из JavaScript. Поверьте мне – это не баг, а фича.

Ajax-запросы подчиняются правилу "того же источника". Это значит, что Ajax-запросы могут вернуться только в тот же домен, откуда пришла исходная HTML-страница. В вашем случае это значит, что вы можете обращаться с любыми запросами к http://localhost, но http://local.yahooapis.com и другие адреса точно находятся за пределом досягаемости.

Это делается в целях безопасности. Когда вы вводите номер кредитной карты на http://amazon.com, вы должны быть уверены, что эти цифры не будут тихо переправлены на http://hackers.r.us (более формально это известно как XSS, или межсайтовый скриптинг (cross-site scripting)).

Правило "того же источника" применимо только к клиентскому JavaScript, но не к серверному Groovy. Поэтому я заставляю вас вызывать http://local.yahooapis.com из контроллера и прозрачно передавать результат обратно браузеру.

Если вам очень хочется вызвать Web-сервис Yahoo! или Google прямо из браузера, то они оба предоставляют скользкий обход этого правила с помощью обратного вызова. Подробнее об обратных вызовах в JSON см. документацию.

Заставить метод контроллера выполнять JSON-запрос выгодно по двум причинам: это позволяет обойти правило "того же источника" (см. "Почему я не могу вызвать Web-сервисы прямо из браузера") и, что более важно, это обеспечивает некоторую инкапсуляцию. Контроллер становится чем-то вроде Data Access Object (DAO).

Вам не нужны ни голый SQL в представлениях, ни жестко прописанный URL удаленного Web-сервиса. Вызывая локальный контроллер, вы защищаете клиентов от изменений реализации. Изменение имен таблиц или полей сделает встроенное SQL-выражение неработоспособным, а изменение URL лишит работоспособности встроенный Ajax-вызов. При вызове AirportMapping.iata() вы можете свободно менять источник данных с локальной таблицы на удаленный сервис GeoNames, не изменяя пользовательский интерфейс. В долгосрочном плане вы можете даже решить кэшировать вызовы удаленного сервиса в локальной БД ради повышения производительности, пополняя локальный кэш с каждым вызовом.

Теперь, когда сервис работает изолированно, вы можете вызвать его с Web-страницы.

Добавляем ссылку ShowHotels

Нет смысла выводить ссылку Show Nearby Hotels (Отели поблизости), пока пользователь не выбрал аэропорт назначения. Аналогично, нет смысла выполнять удаленный вызов, пока вы не уверены, что пользователю нужен список отелей. Итак, для начала добавьте в блок скрипта в plan.gsp функцию showHotelsLink(). Также добавьте вызов showHotelsLink() в последнюю строку addAirport(), как показано в листинге 32.

Листинг 32. Реализация showHotelsLink()
function addAirport(response, position) 
{
  ...
  drawLine()
  showHotelsLink()
}
function showHotelsLink()
{
  if(airportMarkers[1] != null)
  {
    var hotels_link = document.getElementById("hotels_link")
    hotels_link.innerHTML = "<a href='#' 
      onClick='loadHotels()'>Show Nearby Hotels...</a>"
  }
}

Grails предоставляет тег <g:remoteLink>, который создает асинхронные ссылки (так же, как <g:formRemote> обеспечивает асинхронную отправку форм), но проблемы жизненного цикла не позволяют применить их в данном случае. Теги g: обрабатываются на сервере. А в данном случае ссылка добавляется динамически на клиентской стороне, так что придется положиться на чистое JavaScript-решение.

Вы, возможно, заметили вызов docu­ment.get­Ele­ment­ById("hotels_link"). Добавьте новый <div> внизу search <div>, как показано в листинге 33.

Листинг 33. Добавление hotels_link <div>
<div id="search" style="width:25%; float:left">
  <h1>Where to?</h1>
  <g:formRemote name="from_form" ... >
  <g:formRemote name="to_form" ...>
  <div id="hotels_link"></div>
</div>

Обновите содержимое браузера и убедитесь, что после ввода аэропорта назначения появляется гиперссылка, как показано на рисунке 19.


Рисунок 19. Гиперссылка Show Nearby Hotels

Теперь нужно создать функцию loadHotels().

Вызов Ajax.Remote

Добавьте новую функцию в блок скрипта в plan.gsp, как показано в листинге 34:

Листинг 34. Реализация loadHotels()
function loadHotels()
{
  var url = "${createLink(controller:'hotel', action:'near')}"
  url += "?lat=" + airportMarkers[1].getLatLng().lat()
  url += "&lng=" + airportMarkers[1].getLatLng().lng()
  new Ajax.Request(url,
  {
    onSuccess: function(req) { showHotels(req) },
    onFailure: function(req) { displayError(req) }
  })
}

Здесь безопасно использовать Grails-метод createLink, так как основная часть URL Hotel.near() не будет изменяться при обработке страницы на серверной стороне. Вы присоединяете динамические части URL, используя клиентский JavaScript, и затем выполняете Ajax-запрос, используя уже знакомый Prototype-вызов.

Обработка ошибок

Для краткости я игнорировал обработку ошибок в вызове <g:formRemote>. Теперь, при вызове удаленного сервиса (хотя бы и через локальный контроллер-прокси), благоразумнее будет выдать некий отклик вместо молчаливого отказа. Добавьте функцию displayError() в блок скрипта в plan.gsp, как показано в листинге 35.

Листинг 35. Реализация displayError()
function displayError(response)
{
  var html = "response.status=" + response.status + "<br />"
  html += "response.responseText=" + response.responseText + "<br />"
  var hotels = document.getElementById("hotels")
  hotels.innerHTML = html
}

Надо сказать, эта функция делает не больше, чем простой вывод сообщения об ошибке пользователю в hotels <div>, ниже ссылки Show Nearby Hotels, там, где обычно должны быть результаты. Вы инкапсулируете удаленный вызов в контроллере на серверной стороне, что позволяет реализовать несколько более широкую обработку ошибок.

Добавьте <div> для hotels ниже <div> для hotels_link, который вы добавили раньше, как показано в листинге 36.

Листинг 36. Добавляем <div> для hotels
<div id="search" style="width:25%; float:left">
  <h1>Where to?</h1>
  <g:formRemote name="from_form" ... >
  <g:formRemote name="to_form" ...>
  <div id="hotels_link"></div>
  <div id="hotels"></div>
</div>

Вам осталось сделать одно: добавить функцию для загрузки результатов успешного JSON-запроса и заполнения <div> для hotels.

Обработка удачного запроса

Эта функция принимает JSON-ответ от локального сервиса Yahoo!, строит HTML-список и записывает его в <div> для hotels (листинг 37).

Листинг 37. Реализация showHotels()
function showHotels(response)
{
  var results = eval( '(' + response.responseText + ')')
  var resultCount = 1 * results.ResultSet.totalResultsReturned
  var html = "<ul>"
  for(var i=0; i < resultCount; i++)
  {
    html += "<li>" + results.ResultSet.Result[i].Title + "<br />"
    html += "Distance: " + results.ResultSet.Result[i].Distance + "<br />"
    html += "<hr />"
    html += "</li>"
  }
  html += "</ul>"
  var hotels = document.getElementById("hotels")
  hotels.innerHTML = html
}

Обновите содержимое браузера еще раз и введите пару аэропортов. На экране вы должны увидеть то, что показано на рисунке 14 в начале статьи.

Я заканчиваю этот пример с надеждой, что вы продолжите играть с ним самостоятельно. Вы можете решить отображать отели на карте, используя другой набор GMarker-ов. Вы можете добавить дополнительные поля из результатов ответа Yahoo!, например, адреса и номера телефонов. Возможности безграничны.

Заключение

Не так плохо для 150 строк кода, правда? В этой статье вы увидели, как JSON может стать жизнеспособной альтернативой XML при Ajax-запросах. Вы видели, как легко возвратить JSON локально из Grails-приложения, и что ненамного сложнее получить JSON от удаленного Web-сервиса. Вы можете использовать такие Grails-теги, как <g:formRemote> и <g:linkRemote>, когда HTML формируется на серверной стороне, но знаете также, как использовать нижележащий вызов Ajax.Request, предоставляемый Prototype, в действительно динамических приложениях Web 2.0.

В следующий раз вы увидите работу Java Management Extensions (JMX).

Grails на предприятии

Меня часто спрашивают, считаю ли я, что Grails готов к использованию в масштабах предприятия. Краткий ответ – да. Но обычно я отвечаю длиннее: "Только если вы считаете, что Spring и Hibernate (технологии, лежащие в основе Grails) готовы. Только если вы считаете, что Tomcat или JBoss (или тот Java EE-сервер, который вы используете) готовы. Только если вы считаете, что MySQL или PostgreSQL (или та СУБД, которую вы используете) готовы. Только если вы считаете, что Java-программирование можно использовать в масштабах предприятия".

Компания British Sky Broadcasting Group недавно перевела свои открытые Web-сайты на Grails. У них 110 миллионов хитов в месяц. LinkedIn.com использует Grails в некоторых коммерческих частях сайта. Web-сайт Tropicana Juice в Великобритании уже несколько лет работает на Grails. Сам Grails.org написан на Grails, и обеспечивает 70000 скачиваний в месяц. А то, что недавно SpringSource приобрела G2One (компанию-разработчика Groovy и Grails) должно устранить последние сомнения в том, что Groovy и Grails готовы к работе в масштабах предприятия.

Как экзотично ни выглядит Groovy порой, важно помнить, что оно реализовано на простом Java-коде. Как ни отличается Grails-разработка от других типичных Java Web-фреймворков, вы все равно кончите Java EE-совместимым WAR-файлом.

В этой статье вы встретитесь с несколькими инструментами масштаба предприятия для мониторинга и конфигурирования. Вы узнаете, как инструментировать Grails-приложение с помощью JMX. Вы получите краткое введение в конфигурирование Spring на Grails. Вы также увидите, как в Config.groovy указываются настройки log4j, и узнаете, как их динамически изменять с помощью JMX.

Реализуем JMX-инструментирование

JMX существует с 2000 года. Это один из старейших JSR – JSR 3, если быть точным. По мере роста популярности языка Java на стороне сервера возможность удаленной настройки и конфигурирования живого, исполняющегося приложения стала критически важной частью платформы. В 2004 году Sun инструментировала JVM с помощью JMX и выпустила такие средства поддержки, как JConsole, в Java 1.5 JDK.

JMX обеспечивает рефлексию (introspection) для JVM, сервера приложений и вашего кода через единообразный интерфейс. Эти разнообразные компоненты делаются доступными для консоли управления через managed beans, или короче, MBeans.

ПРИМЕЧАНИЕ

Примечание: подробнее о JMX можно прочитать в статье "Java theory and practice: Instrumenting applications with JMX", http://www.ibm.com/developerworks/library/j-jtp09196/

MBeans подобны шкалам, стрелкам и переключателям на приборной доске вашего автомобиля. Некоторые из инструментов доступны только для чтения, как, например, спидометр. Другие же работают и на запись, как, например, педаль газа. Но эта метафора приборной доски несколько нарушается, если вспомнить, что MBeans предполагают удаленное управление. Представьте удаленное включение поворотников или смену радиостанции в своей машине.

Включаем локальный JMX-агент

ПРИМЕЧАНИЕ

Локально или удаленно?

В целях разработки и тестирования проще всего запускать клиента и агента JMX локально. В реальной рабочей среде, однако, преимущества JMX проявляются при удаленном мониторинге агентов. JConsole использует те же системные ресурсы (RAM, циклы CPU и так далее), что и любой другой Java-процесс. Это может составлять проблему, особенно если рабочий сервер, за которым вы пытаетесь следить, находится под нагрузкой. Но важнее то, что вы можете выполнять мониторинг нескольких серверов, не вставая с места, а это делает вас всезнающим повелителем своего домена.

Конечно, удаленный мониторинг рабочих серверов предполагает заботы о безопасности. Вы можете задать защиту паролем или, в идеале, использовать аутентификацию по открытому/закрытому ключу (см. Ресурсы).

Чтобы использовать JMX для мониторинга, его сперва нужно включить. В Java 5 нужно установить пару JMX-флагов (в Java 6, эти установки уже должны быть на месте, хотя вы можете решить задать их самостоятельно, чтобы переопределить значения по умолчанию). Говоря на языке JMX, вы устанавливаете JMX-агента. В листинге 38 показаны параметры JVM.

Листинг 38. Параметры JVM для включения JMX-мониторинга.
-Dcom.sun.management.jmxremote 
-Djava.rmi.server.hostname=localhost

В некоторых материалах для JMX-флагов предлагается создавать глобальную переменную среды JAVA_OPTS. Другие же предлагают вводить флаги из командной строки:

java -Dcom.sun.management.jmxremote -Djava.rmi.server.hostname=localhost someExampleClass. 

Оба варианта работают, но для рабочей среды ни один из них не оптимален. Я считаю, что лучше всего задавать эти значения в скрипте запуска сервера. Необходимость помнить о вводе этих эзотерических флагов при каждом перезапуске сервера – как минимум ненадежное решение. А создания глобальной переменной типа CLASSPATH или JAVA_OPTS лучше избежать по двум причинам: они создают ненужные дополнительные этапы конфигурирования при клонировании сервера (пусковой скрипт куда проще копировать между серверами), и они заставляют все Java-процессы на машине использовать одну конфигурацию. Да, вы можете составить подробную памятку, чтобы не забыть обо всех мелких деталях конфигурации, но документирование сложности куда менее эффективно, чем удаление сложности.

Под UNIX, Linux и Mac OS X пусковой скрипт Grails находится в $GRAILS_HOME/bin/grails. Отредактируйте этот файл, добавив две строки JAVA_OPTS, показанные в листинге 39.

Листинг 39. Включение JMX-мониторинга в пусковом скрипте Grails под UNIX, Linux и Mac OS X
#!/bin/sh
DIRNAME='dirname "$0"'
. "$DIRNAME/startGrails"

export JAVA_OPTS="-Dcom.sun.management.jmxremote"
export JAVA_OPTS="$JAVA_OPTS -Djava.rmi.server.hostname=localhost"

startGrails org.codehaus.groovy.grails.cli.GrailsScriptRunner "$@"

Под Windows пусковой скрипт Grails – это $GRA­ILS_HOME/bin/grails.bat. Добавьте две строки, показанных в листинге 40, в grails.bat перед вызовом startGrails.bat:

Листинг 40. Включение JMX-мониторинга в пусковом скрипте Grails под Windows
set JAVA_OPTS=-Dcom.sun.management.jmxremote
set JAVA_OPTS=%JAVA_OPTS% -Djava.rmi.server.hostname=localhost

Заметьте, что в обоих скриптах первое назначение JAVA_OPTS переопределяет глобальную переменную среды, если она существует (значение переопределяется только для этого процесса – глобальная переменная для всей системы не переопределяется). Я делаю это специально, чтобы не дать глобальным установкам неумышленно испортить мои локальные установки. Если вы уже зависите от глобальных значений, непременно включите существующую переменную в начало присваивания, как это сделал я во второй строке листингов 39 и 40.

Теперь запустите Grails командой grails run-app. Вы не увидите ничего нового в консольном выводе, но ваш сервер приложений теперь готов к мониторингу.

Для мониторинга JMX-агентов используется JMX-клиент. Это может быть GUI-приложение типа JConsole (которая входит в Java 5 и выше) или Web UI (который входит в большинство серверов типа Tomcat и JBoss). Мониторинг агента можно выполнять даже программно, как вы увидите ниже.

Откройте еще одно окно и введите jconsole. Вы увидите Grails в списке локальных JMX-агентов, как показано на рисунке 20. Щелкните по Grails и нажмите кнопку Connect.


Рисунок 20. Список локальных JMX-агентов в JConsole

Обратите внимание, что в целях безопасности локальный доступ к JMX имеется только на Windows-системах, использующих NTFS. Если ваша система использует FAT или FAT32, вы можете столкнуться с проблемами. Не беспокойтесь. В следующем разделе я покажу, как настроить JMX-агент для удаленного доступа. Несмотря на то, что агент и клиент находятся физически на одной машине, это позволит вам справиться с локальной проблемой безопасности.

Подключившись, вы увидите страницу, похожую на рисунок 21.


Рисунок 21. Страница Summary в JConsole

Посмотрите на закладки Memory, Threads, Classes и VM. Они в реальном времени показывают, что происходит внутри JVM. Вы можете увидеть, что серверу не хватает физической памяти, число работающих потоков, и даже то, сколько времени сервер работает. Эти закладки интересны, но в данный момент наибольший интерес представляет закладка MBeans – на ней отображаются ваши классы.

Использование удаленного JMX-агента

ПРЕДУПРЕЖДЕНИЕ

Не делайте этого на работе!

Эта конфигурация никогда не должна использоваться в рабочей среде. Для демонстрации я отключил всю аутентификацию и шифрование. В разделе Ресурсы есть ссылка на инструкции по настройке безопасности при удаленном управлении JMX-агентом


Рисунок 22. Подключение к удаленному JMX-агенту из JConsole

Чтобы JMX-агент принимал удаленные подключения, нужно указать JVM чуть больше JMX-флагов. Эти дополнительные флаги открывают порт управления и конфигурируют настройки безопасности (или, в данном случае, их отсутствие).

Добавьте в пусковой скрипт Grails три новые строки, приведенные в листинге 41.

Листинг 41. Включение удаленного JMX-мониторинга в пусковом скрипте Grails
export JAVA_OPTS=
  "-Dcom.sun.management.jmxremote"    
export JAVA_OPTS=
  " $JAVA_OPTS -Djava.rmi.server.hostname=localhost"
export JAVA_OPTS=" $JAVA_OPTS -Dcom.sun.management.jmxremote.port=9004"
export JAVA_OPTS=" $JAVA_OPTS -Dcom.sun.management.jmxremote.authenticate=false"
export JAVA_OPTS=" $JAVA_OPTS -Dcom.sun.management.jmxremote.ssl=false"

Перезапустите Grails с новыми настройками. Перезапустите JConsole. На этот раз щелкните по закладке Remote и подключитесь к localhost через порт 9004, как показано на рисунке 22.


Рисунок 23. флаги удаленного JMX-агента, переданные JVM

Вот быстрый способ проверить, что вы работаете с удаленной JVM (даже если технически она работает на той же системе). Щелкните по закладке MBeans. Раскройте дерево java.lang слева. Щелкните по элементу Runtime. Затем, в окне Attributes в правой части экрана, дважды щелкните по InputArguments. Вы должны увидеть все настройки удаленного JMX, показанные на рисунке 23.

Оставьте это окно открытым. Откройте новое подключение, щелкнув по меню Connection. Щелкните по закладке Remote и на этот раз примите настройки по умолчанию (localhost на Port 0). Раскройте InputArguments для MBean Runtime. Обратите внимание – JMX-флагов нет (как показано на рисунке 24).


Рисунок 24. Мониторинг двух разных JMX-агентов

Если еще непонятно по заголовку окна (Monitoring Self), второе окно JConsole, которое вы сейчас открыли, выполняет мониторинг самой JConsole.

Теперь, когда JConsole запущена и выполняет мониторинг Grails-приложения, пришло время сделать с ней что-нибудь полезное, например, изменить настройки журналирования. Но прежде чем вы сможете сделать это, вы должны разобраться с последним кусочком JMX-головоломки: с MBean-сервером.

MBean-сервер, Grails и Spring

Элемент Runtime, по которому вы щелкали в JConso­le – это MBean. Чтобы MBean был виден JMX-клиенту, он должен быть зарегистрирован на MBean-сервере, работающем внутри JMX-агента. Некоторые используют термины "JMX-агент" и "MBean-сервер" как взаимозаменяемые, но технически MBean-сервер – это один из многих компонентов, работающих внутри JMX-агента.

Чтобы программно зарегистрировать MBean, нужно вызвать MBeanServer.registerMBean(). Однако в Grails это делается из конфигурационного файла – точнее, из конфигурационного файла Spring.

Spring – это сердце Grails. Это фреймворк внедрения зависимостей, управляющий тем, как все классы взаимодействуют друг с другом (подробнее см. ресурсы).

С точки зрения JMX, вы можете подумать: "Я регистрирую MBean на MBean-сервере". А с точки зрения Spring вы должны думать "Я встраиваю MBean в MBean-сервер". Глаголы разные, но результат один: ваш MBean будет виден JMX-клиенту.

Для начала создайте файл resources.xml в grails-app/conf/spring (связь между resources.groovy и resources.xml я покажу ниже). Содержимое resources.xml показано в листинге 42.

Листинг 42. Настройки Spring/JMX в resources.xml
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
           http://www.springframework.org/schema/beans/spring-beans-2.5.xsd">

  <bean id="mbeanServer" 
        class="org.springframework.jmx.support.MBeanServerFactoryBean">
    <property name="locateExistingServerIfPossible" value="true" />
  </bean>
  
  <bean id="exporter" class="org.springframework.jmx.export.MBeanExporter">  
    <property name="server" ref="mbeanServer"/>
    <property name="beans">
      <map>
      </map>
    </property>
  </bean>   
</beans>

Вы можете перезагрузить Grails, чтобы убедиться, что базовая конфигурация корректна, но это пока только половина головоломки: у вас есть MBean-сервер, но нет MBean-ов. Два имеющихся здесь бина - mbeanServer и exporter – это инфраструктура, нужная для регистрации MBean-ов. Бин mbeanServer встраивается в бин expor­ter – класс, который предоставляет список MBean-ов JMX-клиентам, например, JConsole. Теперь осталось только зарегистрировать MBean, добаваив его в карту бинов в бине exporter. Это вы сделаете в следующем разделе.

Использование log4j с Grails

Чтобы увидеть настройки log4j (показанные в листинге 43), откройте файл grails-app/conf/Config.groovy.

Листинг 43. Настройки log4j в Config.groovy
log4j 
{
    appender.stdout = "org.apache.log4j.ConsoleAppender"
    appender.'stdout.layout'="org.apache.log4j.PatternLayout"
    appender.'stdout.layout.ConversionPattern'='[%r] %c{2} %m%n'
    // и так далее...
}

При запуске Grails-приложения большинство сообщений, появляющихся в консоли, – это сообщения log4j. За это нужно благодарить org.apache.log4j.ConsoleAppender (подробнее о log4j см. Ресурсы).

Регистрация log4j MBean

Если вы хотите изменить настройки журналирования для Grails-приложения без помощи JMX, нужно просто отредактировать файл grails-app/conf/Config.groovy и перезапустить сервер. Но что если вы хотите изменить эти настройки без перезагрузки сервера или хотите сделать это удаленно? Это как раз работа для JMX. К счастью, log4j поставляется с MBean для выполнения такой задачи. Все, что нужно – зарегистрировать log4j MBean.

Добавьте XML-фрагмент (листинг 44) в resources.xml. Это встроит MBean log4j в MBean-сервер.

Листинг 44. Встраивание MBean в MBean-сервер
<bean id="exporter" class="org.springframework.jmx.export.MBeanExporter">
  <property name="server" ref="mbeanServer"/>
  <property name="beans">
    <map>
      <entry key="log4j:hierarchy=default">
        <bean class="org.apache.log4j.jmx.HierarchyDynamicMBean"/>
      </entry>
    </map>
  </property>
</bean>


Рисунок 25. Просмотр информации MBean

Перезагрузите Grails и перезапустите JConsole. Если вы подключитесь к localhost через порт 9004, на закладке MBeans должен появиться MBean log4j. Раскройте элемент дерева log4j, щелкните по default, а затем откройте закладку Info. Вы должны увидеть изменения конфигурации, добавленные в resources.xml (см. рисунок 25).

Теперь, когда вы видите log4j через JMX, следующий шаг – изменить настройки журналирования.

Изменение настроек log4j на лету

Предположим на минуточку, что ваше Grails-приложение ведет себя странно. Вам хотелось бы лучше понимать, что происходит. Посмотрев в grails-app/­conf/Config.groovy, вы видите, что корневой логгер посылает результат в консоль error—root­Log­ger="error,stdout". Вы хотите изменить уровень журналирования на trace, чтобы увеличить количество вывода на консоль.

Посмотрите на JConsole. В папке log4j вы должны видеть корневой MBean. Вы можете заметить, что атрибут приоритета установлен в ERROR, точно как в Config.groovy. Дважды щелкните по значению ERROR и напишите TRACE, как показано на рисунке 26.


Рисунок 26. Изменение приоритета корневого логгера с ERROR на TRACE

Чтобы проверить, что ваша консоль стала разговорчивее, щелкните по ссылке на AirportMappingController на домашней странице вашего Grails-приложения в браузере. В лавине новых сообщений вы можете найти некоторые подробности того, что Grails реально делает, чтобы вывести исходный список (листинг 45).

Листинг 45. Расширение вывода log4j
 [11277653] metaclass.RedirectDynamicMethod 
  Dynamic method [redirect] looking up URL mapping for 
  controller [airportMapping] and action [list] and 
  params [["action":"index", "controller":"airportMapping"]] 
  with [URL Mappings
------------
org.codehaus.groovy.grails.web.mapping.ResponseCodeUrlMapping@1bab0b
/rest/airport/(*)?
/(*)/(*)?/(*)?
]
[11277653] metaclass.RedirectDynamicMethod Dynamic method 
  [redirect] mapped to URL [/trip/airportMapping/list]
[11277653] metaclass.RedirectDynamicMethod Dynamic method 
  [redirect] forwarding request to [/trip/airportMapping/list]
[11277653] metaclass.RedirectDynamicMethod Executing redirect 
  with response 
  [com.opensymphony.module.sitemesh.filter.PageResponseWrapper@19243f]
ПРИМЕЧАНИЕ

Когда можно спокойно игнорировать Fatal Error?

Если вы некоторое время использовали Grails 1.0.3, вы могли заметить загадочное сообщение об ошибке, часто появляющееся в консоли - [Fatal Error] :-1:-1: Premature end of file. Большинство ее просто игнорирует, поскольку она не связана с реальной ошибкой, ни с фатальной, ни с какой еще.

Если вы включите уровень журналирования trace, вы сможет увидеть детали предполагаемой фатальной ошибки: converters.XMLParsingParameterCreationListener Error parsing incoming XML request: Error parsing XML.

Как объясняет более развернутое сообщение, Grails пытается разбирать каждый входящей запрос как XML. Большинство запросов не являются XML, и обработчик запросов прилежно сообщает об ошибке, но, тем не менее, корректно обрабатывает запрос.

Эта ошибка устранена в версии 1.0.4.

Изменение ConversionPattern в log4j

Теперь вы можете изменить паттерн, используемый при выводе. В Config.groovy паттерн задан следующей строкой: appender.'stdout.layout.ConversionPattern'='[%r] %c{2} %m%n'. Посмотрев в документацию log4j, вы решаете создать что-то более информативное.

Щелкните по MBean stdout в JConsole. Измените исходное значение атрибута conversionPattern на [%5p] %d{hh:mm:ss} (%F:%M:%L)%n%m%n%n. После того, как вы сгенерируете новый лог, я опишу, что делает это магическое заклинание (подробнее о настройках con­ver­sionPattern см. Ресурсы).


Рисунок 27. Изменение conversionPattern в PatternLayout

Теперь щелкните по ссылке home в Web-браузере, а затем снова по ссылке AirportMappingController. Формат вывода изменился радикально, как показано в листин­ге 46.

Листинг 46. Консольный вывод, использующий новый conversionPattern
 [DEBUG] 09:04:47 (RedirectDynamicMethod.java:invoke:127)
Dynamic method [redirect] looking up URL mapping for controller 
[airportMapping] and action [list] and params 
[["action":"index", "controller":"airportMapping"]] with [URL Mappings
------------
org.codehaus.groovy.grails.web.mapping.ResponseCodeUrlMapping@e73cb7
/rest/airport/(*)?
/(*)/(*)?/(*)?
]

[DEBUG] 09:04:47 (RedirectDynamicMethod.java:invoke:144)
Dynamic method [redirect] mapped to URL [/trip/airportMapping/list]

[DEBUG] 09:04:47 (RedirectDynamicMethod.java:redirectResponse:162)
Dynamic method [redirect] forwarding request to [/trip/airportMapping/list]

[DEBUG] 09:04:47 (RedirectDynamicMethod.java:redirectResponse:168)
Executing redirect with response 
   [com.opensymphony.module.sitemesh.filter.PageResponseWrapper@47b2e7]

Теперь, когда вы видите результат, расскажу о том, что происходит. %p выводит уровень приоритета. Эти сообщения имеют уровень DEBUG. %d{hh:mm:ss} показывает метку времени в формате часы:минуты:секунды. (%F:%M:%L) выводит имя файла, метод и номер строки в скобках. Наконец, %n%m%n%n выводит пустую строку, сообщение и еще две пустые строки.

Изменения, сделанные в log4j через JMX, непостоянны. Если перезагрузить Grails, он вернется к настройкам, сохраненным в Config.groovy. Это значит, что вы можете экспериментировать с JMX-настройками как угодно, не беспокоясь о том, что непоправимо испортите что-то. В случае ConversionPattern использование JMX – это отличный способ экспериментировать с настройками, пока вы не подберете наиболее вас устраивающие. Только не забудьте скопировать паттерн в Config.groovy, чтобы сделать настройки постоянными.

Глядя на вывод Hibernate DEBUG

Возвращаясь к предположению о том, что вы отлаживаете работающее Grails-приложение, вы все еще не нашли того, что искали. Верните значение атрибута приоритета корневого MBean обратно в ERROR, чтобы снизить уровень шума.

Возможно, проблема кроется в Hibernate. Посмотрев на Config.groovy, вы замечаете, что журналирование для пакета org.hibernate установлено в off. Вместо того, чтобы изменять уровень вывода для всего приложения, возможно, лучше сосредоточиться на определенном пакете, чтобы получить более подробную информацию.

В JConsole щелкните по MBean default. Кроме изменения значений атрибутов, вы можете также вызывать методы MBean. Откройте закладку Operations. Введите имя org.hibernate и щелкните по кнопке addLoggerMBean. Вы должны увидеть, что в дереве слева появился новый MBean.

Щелкните по новому MBean-у org.hibernate и измените его атрибут приоритета на DEBUG, как показано на рисунке 28.


Рисунок 28. Изменение приоритета для org.hibernate

Теперь вернитесь в Web-браузер, щелкните по ссылке home и снова вернитесь на AirportMappingController. Вы должны увидеть длинный список записей лога DEBUG, как показано в листинге 47.

Листинг 47. Вывод log4j для Hibernate
[DEBUG] 10:05:52 (AbstractBatcher.java:logOpenPreparedStatement:366)
about to open PreparedStatement (open PreparedStatements: 0, globally: 0)

[DEBUG] 10:05:52 (ConnectionManager.java:openConnection:421)
opening JDBC connection

[DEBUG] 10:05:52 (AbstractBatcher.java:log:401)
select this_.airport_id as airport1_0_0_, this_.locid as locid0_0_, 
this_.latitude as latitude0_0_, this_.longitude as longitude0_0_, 
this_.airport_name as airport5_0_0_, this_.state as state0_0_ 
from usgs_airports this_ limit ?

[DEBUG] 10:05:52 (AbstractBatcher.java:logOpenResults:382)
about to open ResultSet (open ResultSets: 0, globally: 0)

[DEBUG] 10:05:52 (Loader.java:getRow:1173)
result row: EntityKey[AirportMapping#1]

[DEBUG] 10:05:52 (Loader.java:getRow:1173)
result row: EntityKey[AirportMapping#2]

Просмотрите вывод DEBUG для Hibernate. Вы получите детальный, пошаговый разбор того, что происходит при выборке данных из БД и их трансформации в ArrayList.

Использование Spring Bean Builder

Теперь, когда вы знаете, как сконфигурировать JMX через resources.xml, пора внести новый поворот. Grails поддерживает конфигурацию Spring через файл reso­urces.groovy. Переименуйте grails-app/conf/spring/re­sour­ces.xml в resources.xml.old. Добавьте код из листинга 48 в resources.groovy:

Листинг 48. Конфигурирование Spring с помощью Bean Builder
import org.springframework.jmx.support.MBeanServerFactoryBean
import org.springframework.jmx.export.MBeanExporter
import org.apache.log4j.jmx.HierarchyDynamicMBean

beans = 
{
  log4jBean(HierarchyDynamicMBean)
  
  mbeanServer(MBeanServerFactoryBean) 
  {
    locateExistingServerIfPossible=true
  }
  
  exporter(MBeanExporter) 
  {
    server = mbeanServer
    beans = ["log4j:hierarchy=default":log4jBean]
  }    
}

Как видите, бины Spring конфигурируются с помощью Groovy-кода вместо XML. Вы уже наблюдали Groovy MarkupBuilder в действии в "Grails и унаследованные базы данных", а также в "RESTful Grails". Это легкая вариация темы - Bean Builder определяет бины для конфигурации Spring.

Перезагрузите Grails и JConsole. Убедитесь, что по сравнению с XML- конфигурацией ничего не изменилось.

Использование XML-диалекта для конфигурирования Spring позволяет вам использовать коллективную мудрость Web – вы можете копировать и вставлять куски из множества источников. Но использование диалекта Bean Builder более соответствует остальной конфигурации Grails. К этому моменту вашей Grails-карьеры вы уже видели DataSource.groovy, Config.groovy, BootStrap.gro­ovy и Events.groovy. Вы выполняете конфигурирование в коде, а это означает, что вы можете, например, показывать или скрывать MBean в зависимости от среды исполнения.

Например, в листинге 49 показано, как сделать log4jBe­an в рабочей среде, но скрыть на время разработки.

Листинг 49.
import org.springframework.jmx.support.MBeanServerFactoryBean
import org.springframework.jmx.export.MBeanExporter
import org.apache.log4j.jmx.HierarchyDynamicMBean
import grails.util.GrailsUtil

beans = 
{
  log4jBean(HierarchyDynamicMBean)
  
  mbeanServer(MBeanServerFactoryBean) 
  {
    locateExistingServerIfPossible=true
  }
  
  switch(GrailsUtil.environment)
  {
    case "development":
    break
    
    case "production":
      exporter(MBeanExporter) 
      {
        server = mbeanServer
        beans = ["log4j:hierarchy=default":log4jBean]
      }
    break
  }
}

Введите grails run-app и убедитесь в JConsole, что MBean не показывается в режиме разработки. Теперь введите grails prod run-app (или grails war и разместите WAR-файл на сервере приложений). Вы сможете увидеть MBean после перезапуска JConsole.

JMX в Groovy

Последнее, что я вам покажу – это как программно настраивать MBean-ы JMX. Как ни хорош GUI JConsole, еще лучше иметь возможность вносить изменения из Groovy-скрипта.

Для начала создайте файл testJmx.groovy. Внесите в него код из листинга 50.

Листинг 50. Вызов удаленного JMX-агента в Groovy
import javax.management.MBeanServerConnection
import javax.management.remote.JMXConnectorFactory
import javax.management.remote.JMXServiceURL

def agentUrl = "service:jmx:rmi:///jndi/rmi://localhost:9004/jmxrmi"
def connector = JMXConnectorFactory.connect(new JMXServiceURL(agentUrl))
def server = connector.mBeanServerConnection 

println "Number of registered MBeans: ${server.mBeanCount}"

println "\nRegistered Domains:"
server.domains.each{println it}

println "\nRegistered MBeans:"
server.queryNames(null, null).each{println it}

Если Grails работает, вы должны увидеть результат, показанный в листинге 51.

Листинг 51. Результат выполнения скрипта testJmx.groovy
$ groovy testJmx.groovy 
Number of registered MBeans: 20

Registered Domains:
java.util.logging
JMImplementation
java.lang
log4j

Registered MBeans:
java.lang:type=MemoryManager,name=CodeCacheManager
java.lang:type=Compilation
java.lang:type=GarbageCollector,name=Copy
java.lang:type=MemoryPool,name=Eden Space
log4j:appender=stdout
java.lang:type=Runtime
log4j:hierarchy=default
log4j:logger=root
log4j:appender=stdout,layout=org.apache.log4j.PatternLayout
java.lang:type=ClassLoading
java.lang:type=MemoryPool,name=Survivor Space
java.lang:type=Threading
java.lang:type=GarbageCollector,name=MarkSweepCompact
java.util.logging:type=Logging
java.lang:type=Memory
java.lang:type=OperatingSystem
java.lang:type=MemoryPool,name=Code Cache
java.lang:type=MemoryPool,name=Tenured Gen
java.lang:type=MemoryPool,name=Perm Gen
JMImplementation:type=MBeanServerDelegate
ПРИМЕЧАНИЕ

Примечание. Должен предупредить – скрипт test­Jmx.groovy может выдавать groovy.lang.MissingMeth­od­Exception, как в листинге 15.

Листинг 52. Возможное JMX-исключение
Caught: groovy.lang.MissingMethodException: No signature of method: 
javax.management.remote.rmi.RMIConnector$RemoteMBeanServerConnection.queryNames() 
 is applicable for argument types: (java.lang.String, null) 

Если это случится, удалите mx4j-3.0.2.jar из каталога $GROOVY_HOME/lib. Он включен в дистрибутив Groovy для поддержки JMX в 1.4 JDK, но он конфликтует с более поздними версиями платформы Java.

Интересующая нас часть этого скрипта начинается с javax.management.MBeanServer, возвращаемого при вызове connector.mBeanServerConnection (помните, что вызов метода getFoo() в Groovy может быть сокращен до foo). Вызов server.mBeanCount возвращает число зарегистрированных MBean-ов. Первая часть идентификатора MBean – это доменное имя. Полное квалифицированное имя – это список пар имя/значение, разделенный запятыми. Вызов server.queryNames(null, null) возвращает набор всех зарегистрированных MBeans (подробнее о методах класса MBeanServer см. Ресурсы).

Что получить конкретный MBean, добавьте в конец скрипта код из листинга 53.

Листинг 53. Получаем MBean
println "\nHere is the Runtime MBean:"
def mbean = new GroovyMBean(server, "java.lang:type=Runtime")
println mbean

Теперь, когда у вас есть подключение к MBean-серверу, и вам известно имя MBean, получить новый GroovyMBean – дело одной строки. В листинге 54 показан результат выполнения скрипта.

Листинг 54. Результат GroovyMBean
Here is the Runtime MBean:
MBean Name:
  java.lang:type=Runtime
  
Attributes:
  (r) javax.management.openmbean.TabularData SystemProperties
  (r) java.lang.String VmVersion
  (r) java.lang.String VmName
  (r) java.lang.String SpecName
  (r) [Ljava.lang.String; InputArguments
  (r) java.lang.String ManagementSpecVersion
  (r) java.lang.String SpecVendor
  (r) long Uptime
  (r) long StartTime
  (r) java.lang.String LibraryPath
  (r) java.lang.String BootClassPath
  (r) java.lang.String VmVendor
  (r) java.lang.String ClassPath
  (r) java.lang.String SpecVersion
  (r) java.lang.String Name
  (r) boolean BootClassPathSupported

Помните InputArguments из начала статьи? Это все –D-параметры, передаваемые JVM. Там вы использовали их, чтобы убедиться, что вы действительно подключены к удаленному JMX-агенту. Добавьте еще две строки кода (приведенные в листинге 55), чтобы вывести String[]:

Листинг 55. Получение InputArguments от рантайма MBean
println "\nHere are the InputArguments:"
mbean.InputArguments.each{println it}

Если вы видите результат из листинга 56, значит, круг замкнулся.

Листинг 56. Результат InputArguments
Here are the InputArguments:
-Xserver
-Xmx512M
-Dcom.sun.management.jmxremote
-Djava.rmi.server.hostname=localhost
-Dcom.sun.management.jmxremote.port=9004
-Dcom.sun.management.jmxremote.authenticate=false
-Dcom.sun.management.jmxremote.ssl=false
-Dprogram.name=grails
-Dgroovy.starter.conf=/opt/grails/conf/groovy-starter.conf
-Dgrails.home=/opt/grails
-Dbase.dir=.
-Dtools.jar=/Library/Java/Home/lib/tools.jar

Подробнее о GroovyMBeans см. Ресурсы.

Заключение

Grails можно использовать в корпоративной разработке. Такие распространенные библиотеки для корпоративной разработки JMX, Spring и log4j доступны из Grails, так как, хотя оно и не похоже, но вы по-прежнему занимаетесь традиционной Java EE-разработкой.

Эта статья завершает годовой цикл, посвященный приложению Trip Planner. Я старался придерживаться предметной области, одновременно фокусируясь на основной функциональности Grails. В будущем году я постараюсь придерживаться того же подхода, но мне также хочется расширить горизонты, чтобы охватить все разнообразие Grails-приложений.

Например, в следующем месяце я представлю новую систему блогов. Вы освежите в памяти, как создается новое Grails-приложение, но это не будет пересказом старого материала. Вы снова заглянете на RESTful-сторону Grails, но в контексте создания полной инфраструктуры Atom. Вы снова будете использовать JSON и Ajax, но в этот раз для создания календарей и облака тегов.

Ресурсы
  1. Mastering Grails: другие статьи данной серии на англ. яз.: http://www.ibm.com/developerworks/views/ja­va/lib­ra­ry­view.jsp?search_by=mastering+grails.
  2. Web-сайт Grails: http://grails.codehaus.org/.
  3. Документация фреймворка Grails: http://gra­ils.org/doc/1.0.x/
  4. Groovy SQL – обучающий материал по работе с SQL с использованием Groovy: http://groovy.co­deha­us.org/Tutorial+6+-+Groovy+SQL
  5. "Static Typing & Bureaucracy Redux" (Neil Ford, Meme Agora, May 2007), в этом блоге Нейл Форд рассказывает, как динамические языки позволяют выполнить больший объем тестирования за меньшее время – http://grails.org/doc/1.0.x/
  6. Использование JUnit 4 с Groovy – http://gro­ovy.co­de­haus.org/Using+JUnit+4+with+Groovy
  7. Test'N'Groove интегрирует Groovy и TestNG – http://code.google.com/p/testngroove/
  8. Модульное тестирование: прочитайте, как в Groovy выполняются тесты JUnit – http://gro­o­vy.co­de­ha­us.org/Unit+Testing
  9. В погоне за качеством кода: приключения в разработке, управляемой поведением (In pursuit of code quality: Adventures in behavior-driven development, Andrew Glover, developerWorks, September 2007) – http://www.­ibm.com/developerworks/java/library/j-cq09187/
  10. Groovy Recipes: Greasing the Wheels of Java (Pragmatic Bookshelf, March 2008): книга Скотта Дэвиса по Groovy и Grails – http://www.prag­prog.com/tit­les/sdgrvr
  11. Practically Groovy: серия статей на страницах developerWorks, в которой исследуется практическое использование Groovy и рассматривается, когда и как его следует применять – http://www.ibm.com/de­veloper­works/ru/views/java/libra­ry­view.jsp?se­arch_by= practically+groovy:
  12. Groovy: Web-сайт проекта Groovy с дополнительной информацией – http://groovy.codehaus.org/
  13. AboutGroovy.com (EN): Web-сайт с последними новостями и ссылками на статьи о Groovy – http://gro­o­vy.codehaus.org/
  14. Ajax: новый подход к Web-приложениям (Ajax: A New Approach to Web Applications, Jesse James Garrett, Adaptive Path, февраль 2005): Гаррет, изобратеатель термина "Ajax," описывает основы архитектуры и пользовательских возможностей – http://adaptive­path.com/publications/essays/archives/000385.php
  15. "Использование JSON (JavaScript Object Notation) с Web-сервисами Yahoo!" (Using JSON (JavaScript Object Notation) with Yahoo! Web Services) и "Использование JSON с Google Data API" Using JSON with Google Data APIs – http://deve­loper.ya­hoo.com/com­mon/json.html и http://code.google.com/apis/gdata/json.html, соответственно.
  16. Посетите сайт JSON и посмотрите "JSON: обезжиренная альтернатива XML" – http://www.js­on.org/xml.html
  17. Метапрограммирование на Groovy: подробнее о ExpandoMetaClass в Groovy – http://gro­ovy.co­de­ha­us.org/ExpandoMetaClass
  18. Google Maps API Reference: подробнее об объектах Google Maps API, например, GMarker, GLatLng и GPolyline – http://code.google.com/apis/maps/do­cu­men­tation/reference.html
  19. Web-сервис GeoNames: http://www.geonames.org/ex­port/geonames-search.html
  20. Web-сервисы локального поиска Yahoo! –http://de­veloper.yahoo.com/search/local/V3/localSearch.html
  21. Теория и практика Java. Инструментирование приложений с помощью JMX (Java theory and practice: Instrumenting applications with JMX, Бриан Гетц, developerWorks, сентябрь 2006) – http://www.ibm.com/­de­ve­lo­perworks/library/j-jtp09196/
  22. Мониторинг и управление с помощью JMX – http://java.sun.com/j2se/1.5.0/docs/guide/management/agent.html
  23. Класс PatternLayout в Javadoc – http://logging.apa­che.org/log4j/1.2/apidocs/org/apache/log4j/PatternLayout.html
  24. Узнайте больше Relocation методах класса MBeanServer – http://java.sun.com/j2se/1.5.0/docs/api/­ja­vax/management/MBeanServer.html
  25. Дополнительная информация о GroovyMBean – http://groovy.codehaus.org/Groovy+and+JMX
  26. Grails Bean Builder – http://grails.org/Sp­ring+Be­an+Buil­der

Список литературы

  1. Первый элемент (элементы должны оформляться стилями LiteratureListOL или LiteratureListUL)


Любой из материалов, опубликованных на этом сервере, не может быть воспроизведен в какой бы то ни было форме и какими бы то ни было средствами без письменного разрешения владельцев авторских прав.

Copyright © 1994-2016 ООО "К-Пресс"