Содержание


Изучение Grails

Cоздание собственного подключаемого модуля

Узнайте о создании модулей, пригодных для использования в различных приложениях Grails

Comments

Серия контента:

Этот контент является частью # из серии # статей: Изучение Grails

Следите за выходом новых статей этой серии.

Этот контент является частью серии:Изучение Grails

Следите за выходом новых статей этой серии.

Во многих выпусках серии "Изучение Grails" основное внимание уделялось эффективным методикам повторного использования кода. В частности, если вам приходится копировать и вставлять одни и те же фрагменты кода в разные места серверных страниц Groovy (GSP), то имеет смысл подумать о создании частичного шаблона или библиотеки тегов. Если же вы замечаете, что несколько контроллеров или классов модели содержат похожие методы, то, возможно, стоит вынести их в абстрактный родительский класс или добавлять динамически при помощи метакласса ExpandoMetaClass. Кроме того, модули, которые могут использоваться в нескольких приложениях, можно оформить в виде отдельных сервисов или кодеков. 

Однако, все это касается лишь отдельных участков кода. Но что если требуется повторно использовать крупные компоненты со сложной структурой, состоящие из контроллеров, классов модели, кодеков, сервисов и других составных частей приложений Grails? Подобные компоненты, которые могут использоваться разными приложениями, называются подключаемыми модулями или плагинами.

В статье "Изучение Grails: работа с подключаемыми модулями" рассматривался один из доступных на сегодняшний день плагинов - Searchable. Число таких плагинов, которые можно загрузить со специального портала Grails Plugins (см. Ресурсы), достигает 250. Более того, оно постоянно растет, поскольку возможность расширения функциональности существующих приложений при помощи плагинов поддерживается непосредственно ядром Grails. Из этой статьи вы узнаете о том, как создать собственный плагин (ссылка на архив с исходным кодом примеров находится в разделе Загрузка).

Подключаемый модуль ShortenUrl

В эпоху популярности таких сервисов, как Twitter.com, а также общения при помощи SMS-сообщений, длинные URL попросту не помещаются в сообщения, размер которых ограничивается 140 символами. К счастью, существуют несколько Web-сервисов, которые служат для сокращения адресных ссылок. Подобные сервисы являются превосходными кандидатами для интеграции с Grails при помощи плагинов.

Создание плагина несколько отличается от создания обычного приложения Grails. Вместо команды grails create-app следует выполнить grails create-plugin, как показано в листинге 1. Эту команду следует выполнить в пустом каталоге, но ни в коем случае не в каталоге с существующим приложением Grails (ближе к концу статью будет рассказано о том, как интегрировать новый плагин в ранее созданное приложение).

Листинг 1. Создание собственного плагина
$ grails create-plugin shortenurl

Созданное этой командой дерево каталогов имеет ту же структуру, что и для обычного приложения Grails. При этом в корневом каталоге находится файл ShortenurlGrailsPlugin.groovy, который говорит о том, что данный проект является плагином. Фрагмент его содержимого приведен в листинге 2.

Листинг 2. Конфигурационный класс плагина
class ShortenurlGrailsPlugin {
    // версия плагина
    def version = "0.1"
    // версия (или версии) Grails, для которой предназначен плагин
    def grailsVersion = "1.1.1 > *"
    // список требующихся плагинов
    def dependsOn = [:]
    // ресурсы, которые необходимо исключить из финальной сборки плагина
    def pluginExcludes = [
            "grails-app/views/error.gsp"
    ]

    // TODO необходимо заполнить эти поля
    def author = "имя автора"
    def authorEmail = ""
    def title = "название плагина"
    def description = '''\\
Краткое описание плагина
'''

    //остальной код опущен для краткости
}

Этот файл содержит метаданные нового плагина: его версию, версию Grails, список других плагинов, необходимых для работы создаваемого модуля и т. д. Более подробную информацию о конфигурационных файлах можно найти в online-документации (ссылка приведена в разделе Ресурсы).

Если вы планируете сделать ваш плагин открытым и распространять его через портал Grails Plugins, то следует добавить подробное описание, а также информацию об авторе. Содержимое данного файла автоматически обновляется на Web-сайте Grails после каждого изменения плагина в открытом репозитории Subversion (ссылка на источники более подробной информации о публикации плагинов приведена в разделе Ресурсы). Однако в этой статье демонстрируется создание приватного плагина, поэтому информация об авторе и описание имеют не столь важное значение, как для публичных плагинов.

Модуль ShortenUrl не требует корректировки класса ShortenurlGrailsPlugin.groovy, однако, разумеется, на этом его создание не заканчивается. Теперь, имея готовую структуру каталогов, можно переходить к реализации самого плагина. 

Создание класса TinyUrl

TinyUrl.com представляет собой популярный сервис, служащий для сокращения URL. Получив запрос на сокращение заданного URL, сервис сохраняет его для перенаправления последующих HTTP-запросов. Например, откройте страницу сервиса, введите URL http://www.grails.org/The+Plug-in+Developers+Guide  и нажмите на кнопку Make TinyURL!. Полученный в результате адрес - http://tinyurl.com/73495c - будет в два раза короче оригинала (рисунок 1).

Рисунок 1. Сокращение URL при помощи сервиса TinyURL.com
Сокращение URL при помощи сервиса TinyURL.com
Сокращение URL при помощи сервиса TinyURL.com

Узнав о принципах работы сервиса TinyURL.com, можно переходить к его интеграции с Grails при помощи специального плагина. Введите следующий URL в адресной строке вашего браузера:

http://tinyurl.com/api-create.php?url=http://www.grails.org/The+Plug-in+Developers+Guide

Этот минималистичный интерфейс Web-сервиса позволяет получить укороченный URL (но не HTML-содержимое) для указанной страницы.

Далее следует создать Groovy-класс, инкапсулирующий данный интерфейс. Экземпляры этого класса являются классическими примерами простых объектов Groovy (Plain Old Groovy Object - POGO), поскольку они не выполняют функции ни сервисов, ни контроллеров, ни каких либо еще специальных компонентов Grails. Таким образом, наиболее подходящим местом для него будет каталог src/groovy. Создайте файл TinyUrl.groovy и добавьте в него фрагмент кода, показанный в листинге 3.

Листинг 3. Вспомогательный класс TinyUrl
package org.grails.shortenurl

class TinyUrl{
  static String shorten(String longUrl){
    def addr = "http://tinyurl.com/api-create.php?url=${longUrl}"
    return addr.toURL().text
  }
}

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

Как вы помните, перед вводом в эксплуатацию для каждого класса следует создать соответствующий тест. В данном случае необходим интеграционный тест, поскольку класс выполняет запрос к Web-сервису. Продублируйте ранее созданную структуру каталогов org/grails/shortenurl в директории test/integration, а затем создайте файл TinyUrlTests.groovy и добавьте в него код из листинга 4 (этот простой тест примечателен тем, что "укороченная" версия URL оказывается длиннее оригинала)

Листинг 4. Тестирование класса TinyUrl
package org.grails.shortenurl

class TinyUrlTests extends GroovyTestCase{
  def transactional = false

  void testShorten(){    
    def shortUrl = TinyUrl.shorten("http://grails.org")
    assertEquals "http://tinyurl.com/3xfpkv", shortUrl
  }
}

Обратите внимание на строку def transactional = false. Если ее закомментировать, то будет выдано сообщение об ошибке, показанное в листинге 5.

Листинг 5. Пример того, что произойдет, если опустить строчку def transactional = false
Error running integration tests: java.lang.RuntimeException: 
There is no test TransactionManager defined 
and integration test ${test.name} does not set transactional = false

По умолчанию Grails пытается выполнять каждый тест в рамках отдельной транзакции базы данных. Для обычных приложений Grails это не является проблемой, однако при создании плагина нельзя полагаться на существование базы данных. Для решения этой проблемы можно либо установить плагин Hibernate, либо последовать совету, приведенному в сообщении об ошибке, и добавить строку def transactional = false.

Выполните команду grails test-app, чтобы убедиться, что тест выполняется успешно.

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

Создание класса IsGd

Сервис Is.Gd (сокращение от "is good" – качественный) не только сам находится по более краткому адресу, но и эффективнее сокращает URL, чем TinyURL.com. Посетите страницу http://is.gd и поэкспериментируйте с его Web-интерфейсом.

В данном случае код метода будет несколько длиннее версии, показанной в листинге 3 (она занимала всего две строчки), но зато он будет выводить диагностические сообщения о возможных ошибках при вызове сервиса. Создайте класс IsGd.groovy, как показано в листинге 6, в каталоге src/groovy/org/grails/shortenurl.

Листинг 6. Вспомогательный класс IsGd
package org.grails.shortenurl

class IsGd{
  static String shorten(String longUrl){
    def addr = "http://is.gd/api.php?longurl=${longUrl}"
    def url = addr.toURL()
    def urlConnection = url.openConnection()
    if(urlConnection.responseCode == 200){
      return urlConnection.content.text
    }else{
      return "An error occurred: ${addr}\n" + 
      "${urlConnection.responseCode} : ${urlConnection.responseMessage}"
    }
  }
}

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

Создайте файл IsGdTests.groovy file с содержимым, показанным в листинге 7, в каталоге test/integration/org/grails/shortenurl и выполните команду grails test-app, чтобы убедиться, что класс ведет себя ожидаемым образом.

Листинг 7. Тестирование класса IsGd
package org.grails.shortenurl

class IsGdTests extends GroovyTestCase{
  def transactional = false
  
  void testShorten(){
    def shortUrl = IsGd.shorten("http://grails.org")
    assertEquals "http://is.gd/2oCZR", shortUrl        
  }
  
  void testBadUrl(){
    def shortUrl = IsGd.shorten("IAmNotAValidUrl")
    println shortUrl
    assertTrue shortUrl.startsWith("An error occurred:")
  }
}

Для того чтобы увидеть, что именно происходит при передаче сервису IsGd некорректного URL (в примере выше таковым является строка "IAmNotAValidUrl"), лучше всего воспользоваться консольной утилитой curl, как показано в листинге 8 (данная утилита входит в стандартную поставку операционных систем UNIX®/Linux®/Mac OS X, а версию для Windows® можно установить дополнительно; см. Ресурсы). Обращение по некорректному URL при помощи браузера позволяет увидеть только сообщение об ошибке, но не ее код, в то время как выполнив запрос через curl, вы увидите, что ответ Web-сервиса имеет код состояния 500 вместо ожидаемого 200.

Листинг 8. Пример использования curl с целью получения детальной информации о неудачном вызове сервиса
$ curl --verbose "http://is.gd/api.php?longurl=IAmNotAValidUrl"
* About to connect() to is.gd port 80 (#0)
*   Trying 78.31.109.147... connected
* Connected to is.gd (78.31.109.147) port 80 (#0)
> GET /api.php?longurl=IAmNotAValidUrl 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: is.gd
> Accept: */*
> 
< HTTP/1.1 500 Internal Server Error
< X-Powered-By: PHP/5.2.6
< Content-type: text/html; charset=UTF-8
< Transfer-Encoding: chunked
< Date: Wed, 19 Aug 2009 17:33:04 GMT
< Server: lighttpd/1.4.22
< 
* Connection #0 to host is.gd left intact
* Closing connection #0
Error: The URL entered was not valid.

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

Создание сервиса ShortenUrl

Для создания сервиса выполните команду grails create-service ShortenUrl и добавьте содержимое листинга 9 в файл grails-app/services/ShortenUrlService.groovy.

Листинг 9. Сервис ShortenUrl
import org.grails.shortenurl.*

class ShortenUrlService {
    boolean transactional = false

    def tinyurl(String longUrl) {
      return TinyUrl.shorten(longUrl)
    }

    def isgd(String longUrl) {
      def shortUrl = IsGd.shorten(longUrl)
      if(shortUrl.contains("error")){
        log.error(shortUrl)
      }
      return shortUrl
    }
}

Как и в интеграционном тесте выше не забудьте установить флаг transactional в значение false. Базовые классы плагина не обращаются к базе данных, поэтому не имеет смысла начинать транзакции при вызовах методов.

Обратите внимание на то, что метод isgd() отмечает все попытки сокращения некорректных URL. Все объекты Grails на этапе выполнения получают ссылку на специальный объект log, который предоставляет методы, соответствующие уровням журнальных сообщений: debug, info, error и т. д. (за более подробной информацией о журналировании обратитесь по ссылке в разделе Ресурсы). Как вы вскоре увидите, использование данного объекта в юнит-тестах требует определенных подготовительных действий.

В момент создания сервиса Grails также добавляет соответствующий тест в каталог test/unit. Семантически этот тест (ShortenUrlServiceTests.groovy) является интеграционным, поскольку его выполнение зависит от внешних ресурсов, поэтому было бы логично поместить его в каталог test/integration. Однако в данном случае оставьте его в каталоге test/unit для демонстрации некоторых аспектов работы с юнит-тестами. Далее добавьте в тестовый класс фрагмент кода, приведенный в листинге 10.

Листинг 10. Тестирование сервиса ShortenUrl
import grails.test.*

class ShortenUrlServiceTests extends GrailsUnitTestCase {
    def transactional = false
    def shortenUrlService
  
    protected void setUp() {
        super.setUp()
        shortenUrlService = new ShortenUrlService()
    }

    protected void tearDown() {
        super.tearDown()
    }

    void testTinyUrl() {
      def shortUrl = shortenUrlService.tinyurl("http://grails.org")
      assertEquals "http://tinyurl.com/3xfpkv", shortUrl
    }

    void testIsGd() {
      def shortUrl = shortenUrlService.isgd("http://grails.org")
      assertEquals "http://is.gd/2oCZR", shortUrl        
    }

    void testIsGdWithBadUrl() {
      def shortUrl = shortenUrlService.isgd("IAmNotAValidUrl")
      assertTrue shortUrl.startsWith("An error occurred:")
    }
}

Обратите внимание на переменную shortenUrlService, которая объявляется сразу после установки флага transactional в значение false. Далее следует метод setUp(), служащий для инициализации сервиса и вызывающийся (как и метод tearDown())  при запуске каждого теста.

Если бы данный тест был интеграционным, то он бы выполнился без ошибок. Однако, поскольку он является юнит-тестом, вызов метода testIsGdWithBadUrl()  приводит к сообщению "No such property: log for class: ShortenUrlService" (не найдено свойство log для класса ShortenUrlService). Открыв файл test/reports/html/index.html в Web-браузере, вы увидите полное сообщение об ошибке (рисунок 2).

Рисунок 2. Ошибка при выполнении юнит-теста в виду отсутствия объекта log
Ошибка при выполнении юнит-теста в виду отсутствия объекта log
Ошибка при выполнении юнит-теста в виду отсутствия объекта log

Проблема заключается в том, что ссылка на объект log не внедряется в сервис при запусках юнит-тестов (не забывайте, что в теории юнит-тесты должны быть выполнимы в полной изоляции). К счастью, она легко решается путем добавления строчки mockLogging(ShortenUrlService) в метод setUp(), как показано в листинге 11.

Листинг 11. Добавление суррогатного объекта log
protected void setUp() {
    super.setUp()
    mockLogging(ShortenUrlService)
    shortenUrlService = new ShortenUrlService()
}

Данный метод внедряет в класс сервиса суррогатный объект log, который записывает журнальные сообщения в стандартный поток вывода (System.out) вместо сконфигурированных каналов (appenders) log4j. Для того чтобы увидеть эти сообщения, выполните еще раз команду grails test-app  и нажмите на ссылку System.out в нижней части страницы, содержащей HTML-отчет о выполнении тестов ShortenUrlServiceTests.

Рисунок 3. Журнальный вывод суррогатного объекта log
Журнальный вывод суррогатного объекта log
Журнальный вывод суррогатного объекта log

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

Подготовка и развертывание плагина

Для подготовки приложений Grails к развертыванию обычно используется команда grails war, однако в случае плагинов следует использовать grails package-plugin. В данном случае она должна создать архив grails-shortenurl-0.1.zip в корневом каталоге проекта.

Как вы помните из статьи "Изучение Grails: работа с плагинами", все подключаемые модули Grails распространяются в виде архивов ZIP. Подобные архивы, в частности, grails-hibernate-1.1.1.zip and grails-searchable-0.5.5.zip, можно увидеть в каталоге grails/1.1.1/plugins в вашей домашней директории.

Если бы ShortenUrl был публичным плагином, то следовало бы выполнить команду grails release-plugin  для его помещения в репозиторий плагинов Grails. После этого любой разработчик мог бы выполнить команду grails install-plugin shortenurl для интеграции данного плагина со своим проектом. При этом закрытые (личные) плагины устанавливаются не менее легко: все, что требуется — это указать полный путь к ZIP-файлу, находящемуся в локальной файловой системе.

Чтобы убедиться, что плагин был успешно установлен, создайте новый каталог вне директории shortenurl и выполните команду grails create-app foo  для создания простого приложения. Далее перейдите в каталог foo и выполните grails install-plugin /local/path/to/grails-shortenurl-0.1.zip, подставив корректный путь к плагину в вашей системе. Вы должны увидеть результат, подобный показанному в листинге 12.

Листинг 12. Установка локального плагина
$ grails install-plugin /code/grails-shortenurl-0.1.zip
Welcome to Grails 1.1.1 - http://grails.org/
Licensed under Apache Standard License 2.0
Grails home is set to: /opt/grails

Base Directory: /code/foo
Running script /opt/grails/scripts/InstallPlugin.groovy
Environment set to development
     [copy] Copying 1 file to /Users/sdavis/.grails/1.1.1/plugins
     Installing plug-in shortenurl-0.1
     [mkdir] Created dir: 
     /Users/sdavis/.grails/1.1.1/projects/foo/plugins/shortenurl-0.1
     [unzip] Expanding: 
     /Users/sdavis/.grails/1.1.1/plugins/grails-shortenurl-0.1.zip into 
     /Users/sdavis/.grails/1.1.1/projects/foo/plugins/shortenurl-0.1
Executing shortenurl-0.1 plugin post-install script ...
Plugin shortenurl-0.1 installed

Как видите, использование личных плагинов ничем не отличается от работы с открытыми плагинами.

Далее откройте файл foo/application.properties в текстовом редакторе и убедитесь, что в нем указан идентификатор shortenurl, как показано в листинге 13.

Листинг 13. Проверка того, что плагин указан в файле application.properties
#utf-8
#Wed Aug 19 14:38:24 MDT 2009
app.version=0.1
app.servlet.version=2.4
app.grails.version=1.1.1
plugins.hibernate=1.1.1
plugins.shortenurl=0.1
app.name=foo

После установки плагина следует проверить его работоспособность. Для этого выполните команду grails create-controller test, а затем откройте класс grails-app/controllers/TestController.groovy и добавьте в него содержимое листинга 14.

Листинг 14. Использование сервиса в тестовом контроллере
class TestController {
    def shortenUrlService

    def index = { 
      render "This is a test for the ShortenUrl plug-in 
" + "Type test/tinyurl?q=http://grails.org to try it out." } def tinyurl = { render shortenUrlService.tinyurl(params.q) } }

Обратите внимание на строку def shortenUrlService, которая добавляет ссылку на сервис в контроллер. Запустите приложение командой grails run-app  и обратитесь по адресу http://localhost:9090/foo/test/tinyurl?q=http://grails.org в браузере. Результат должен выглядеть как на рисунке 4.

Рисунок 4. Признак того, что плагин был установлен успешно
Признак того, что плагин был установлен успешно
Признак того, что плагин был установлен успешно

Перейдя по ссылке http://tinyurl.com/3xfpkv, вы должны оказаться на сайте grails.org.

Заключение

Как видите, создание плагина не сильно отличается от создания обычного приложения Grails. Вся разница заключается в том, что вместо команды grails create-app следует выполнять grails create-plugin, а вместо grails war - grails package-plugin. За исключением необходимости описания метаданных в файле GrailsPlugin.groovy, все этапы разработки плагинов и приложений (создание сервисов, написание тестов и т. д.) оказываются идентичными.

В этой статье были кратко упомянуты возможности Grails для использования суррогатных объектов в юнит-тестах, в частности, метод mockLogging(). В следующем выпуске будет продемонстрировано использование многих других полезных методов, выполняющих аналогичные функции: mockDomain(), mockForConstraintsTests() и т. д. А пока — получайте удовольствие от изучения Grails.


Ресурсы для скачивания


Похожие темы


Комментарии

Войдите или зарегистрируйтесь для того чтобы оставлять комментарии или подписаться на них.

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Технология Java, Open source
ArticleID=467889
ArticleTitle=Изучение Grails: Cоздание собственного подключаемого модуля
publish-date=02112010