23 kwietnia 2009

Z rozdziału 13. o wtyczkach w Grails z DGG2

Jak to ujęli Graeme i Jeff - autorzy książki "The Definitive Guide to Grails, Second Edition" - we wstępie do rozdziału 13. Wtyczki:

Now it's time to turn the tables and become a plugin author.

Grails jest w zasadzie niczym innym niż środowiskiem uruchomieniowym wtyczek. Grails wie, jak załadować i uruchomić je, a one odwdzięczają mu się nowymi funkcjonalnościami - może to być dodanie nowej metody do istniejących klas w aplikacji lub pełna funkcjonalność biznesowa w stylu blog. Domyślnie Grails jest już dystrybuowany z zestawem wtyczek, np. GORM czy Grails MVC, i bez większego wysiłku możemy stworzyć własne. Do pracy z wtyczkami, np. odszukiwanie i instalacja, Grails udostępnia dedykowany zbiór poleceń.

Dzięki wtyczkom tworzymy moduły funkcjonalne, które możemy ponownie użyć w kolejnych projektach grailsowych. Tworzymy wtyczki, a w zasadzie tworzymy nowe moduły funkcjonalne i włączamy/wyłączamy je w kolejnych projektach.

Wtyczka jest pełnoprawną aplikacją Grails. Jeśli stworzymy aplikację Grails z pewną funkcjonalnością, której będziemy potrzebować w innym projekcie, możemy (i powinniśmy!) stworzyć na jej bazie wtyczkę - dodać deskryptor wtyczki (skrypt Groovy, oczywiście), spakować w plik zip i opublikować, np. na serwerze HTTP.

Odszukiwanie wtyczek to polecenie grails list-plugins, które skontaktuje się z centralnym repozytorium wtyczek i pobierze aktualną ich listę (potrzebne jest połączenie z siecią). Po liście dostępnych wtyczek z centralnego repozytorium wyświetlania jest lista już tych zainstalowanych w projekcie. Na liście znajdziemy pierwszą kolumnę z nazwą wtyczki, kolejną z wersją i ostatnią z opisem wtyczki. Dokładniejsza informacja o wtyczce to polecenie grails plugin-info.

Instalacja wtyczki to polecenie grails install-plugin [nazwa-wtyczki] [wersja-wtyczki].

Domyślnie wtyczka instalowana jest w pojedynczym projekcie. Instalacja globalna, współdzielona między wszystkie projekty, jest możliwa poleceniem grails install-plugin z opcją -global, np. (Listing 13-6):
 grails install-plugin -global code-coverage
Odinstalowanie wtyczki to grails uninstall-plugin [nazwa-wtyczki].

Wtyczki grailsowe są dystrybuowane jako pliki zip, więc możemy zainstalować je po wcześniejszym pobraniu z dowolnego, innego repozytorium lub projektu wykonując polecenie "grails install-plugin" z podaniem ścieżki do pliku zip wtyczki, np. (Listing 13-9):
 grails install-plugin ~/grails-audit-logging-0.3.zip
Możemy również zainstalować wtyczkę, która jest opublikowana na serwerze HTTP podając adres URL do pliku zip.

Tworzenie wtyczki to polecenie grails create-plugin [nazwa-wtyczki]. W katalogu głównym (projektu) wtyczki znajdziemy deskryptor wtyczki, który jest skryptem Groovy z nazwą zakończoną "GrailsPlugin". W ramach klasy-deskryptora wtyczki definiujemy jej metadane - autora, wersję, opis i in. Wszystkie metadane są opcjonalne.

Za pomocą właściowości dependsOn, która jest mapą, określamy wtyczki zależne, np.:
 def dependsOn = [hibernate:"1.1"]
Powyższe definiuje zależność wtyczki od (tak na prawdę GORM, który i tak zawsze jest). Możliwe jest określenie zakresów wersji zależności, np. "1.0 > 1.1" (dowolna wersja między 1.0 i 1.1 włącznie) czy "* > 1.1" (dowolna wersja aż do 1.1 włącznie). Przy instalacji wtyczki z określonym dependsOn, wszystkie zależności zostaną pobrane i zainstalowane automatycznie (dzięki przechodniemu rozwiązywaniu zależności, jak to ma miejsce w Ivy czy Maven).

W deskryptorze dostępne są zmienne odpowiadające zdarzeniom związanym z wtyczkami, którym przypisuje się domknięcie, dzięki któremu wtyczka ma możliwość uczestniczenia (przechwycenia zajścia zdarzenia) w jej cyklu rozwojowym.
  • doWithWebDesriptor - na wejściu domknięcia przekazywany jest XML dla web.xml w postaci GPathResult (dzięki XmlSlurper z Groovy)
  • doWithSpring - możliwość udziału w etapie konfiguracji springowego ApplicationContext; brak parametrów wejściowych
  • doWithDynamicMethods - wykonane po stworzeniu ApplicationContext, gdzie wtyczki mogą dodawać nowe metody do klas grailsowych; przekazywany ApplicationContext na wejściu
  • doWithApplicationContext - stworzony ApplicationContext przekazany na wejściu; możliwość jego modyfikacji
Domyślnie, podczas tworzenia wtyczki, wszystkie wymienione domknięcia są puste.

W ramach zdarzeń (definiowanych jako domknięcia) możemy korzystać z dostępnych zmiennych inicjowanych przez Grails - application (org.codehaus.groovy.grails.commons.GrailsApplication z informacjami o załadowanych klasach i dostępnych artefaktach), manager (org.codehaus.groovy.grails.plugins.GrailsPluginManager do pracy z wtyczkami) oraz plugin (org.codehaus.grails.plugins.GrailsPlugin, który reprezentuje aktualnie konfigurowaną wtyczkę).

Wtyczka jest aplikacją grailsową, więc jej rola może się sprowadzić do dostarczenia pojedynczego kontrolera, biblioteki znaczników czy usługi, albo ich całego zbioru, który dostarcza większej funkcjonalności biznesowej. Korzystamy z normalnych poleceń, jak przy tworzeniu aplikacji grailsowej - create-controller, create-taglib czy create-service. Możemy nawet uruchomić wtyczkę jak zwykłą aplikację grailsową z grails run-app!

Grails umożliwa zdefiniowanie własnych typów artefaktów, podobnie do już istniejących kontrolerów, klas dziedzinowych, usług czy znaczników, np. wtyczka quartz dostarcza własnego typu artefaktowego Job. Wystarczy zaimplementować org.codehaus.groovy.grails.commons.ArtefactHandler lub rozszerzyć org.codehaus.groovy.grails.commons.ArtefactHandlerAdapter, która odpytana odpowiada na pytanie "Czy dany obiekt jest obiektem pewnego artefaktu?" i pozwala na ich właściwe utworzenie. Określenie klasy nowego rodzaju artefaktu to zdefiniowanie właściwości artefacts w deskryptorze wtyczki, np.
 def artefacts = [new JobArtefactHandler()]
Domyślnie stosowane jest podejście - jeśli jesteś w katalogu grails-app/[nazwa] i nazwa kończy się "Nazwa", wtedy artefakt jest typu Nazwa, tzw. duck typing w Groovy (idziesz jak kaczka, kwaczesz jak kaczka, więc jesteś...kaczka). Możemy również sprawdzić, czy posiada odpowiednią metodę i na tej podstawie podjąć decyzję, czy dana klasa jest danym artefaktem czy nie.

Domknięcie doWithSpring pozwala nam na zdefiniowanie nowych ziaren springowych za pomocą grailsowego BeanBuilder i jego DSL dla Spring Framework, np. (Lisitng 13-20):
 class SimpleCacheGrailsPlugin {
def doWithSpring = {
globalCache(org.springframework.cache.ehcache.EhCacheFactoryBean) {
timeToLive = 300
}
}
}
Nazwa ziarna to nazwa metody w ramach doWithSpring, natomiast pierwszy parametr wejściowy to typ ziarna, a kolejny, domknięcie, to sposób na przekazanie parametrów ziarna.

Grails to aplikacja springowa, więc w ramach Grails istnieje pojęcie springowego ApplicationContext. Wszystkie ziarna springowe (również te, z których zbudowany jest Grails) są w nim dostępne. Domyślnie, każde ziarno springowe jest "jedynakiem" (singletonem), a więc co najwyżej jeden egzemplarz ziarna będzie dostępny w systemie. Mechanizm wstrzeliwania zależności Spring Framework dostępny jest w ramach kontrolerów, bibliotek znaczników oraz usług. Wystarczy zadeklarować atrybut o nazwie odpowiadającej nazwie ziarna springowego, aby odpowiedni egzemplarz został przekazany (wstrzelony), np. (Listing 13-21):
 import net.sf.ehcache.Ehcache

class CacheService {
static transactional = false

Ehcache globalCache
}
Groovy, podobnie jak inne języki dynamiczne, np. Smalltalk, Ruby czy Lisp, udostępnia Meta Object Protocol (MOP). Dla każdej klasy java.lang.Class Groovy tworzy klasę-bliźniaka typu MetaClass. Zachowanie metod, konstruktora, właściwości wyznaczane jest właśnie przez MetaClass. Z jego pomocą możemy dynamicznie dodawać nowe metody, właściwości, konstruktory czy metody statyczne do dowolnej klasy w trakcie jej działania, np. (Listing 13-27):
 class Dog {}
Dog.metaClass.bark = { "woof!" }
assert "woof!" == new Dog().bark()
Mając MOPa można..."posprzątać" niezły kawałek z listy zadań ;-) Wystarczy we wtyczce grailsowej skorzystać z domknięcia doWithDynamicMethods, aby dodać pożądaną metodę do wszystkich klas danego typu, np. dla kontrolerów byłoby to (Listing 13-28):
 class SimpleCacheGrailsPlugin {
def doWithDynamicMethods = { applicationContext ->
def cacheService = applicationContext.getBean("cacheService")
application
.controllerClasses
*.metaClass
*.cacheOrReturn = { Serializable cacheKey, Closure callable ->
cacheService.cacheOrReturn(cacheKey, callable)
}
W przykładzie wykorzystane są możliwości Grails, które de facto są odzwierciedleniem możliwości Groovy i Spring Framework w postaci operatora spread (gwiazdka - wykonaj metodę na każdym elemencie z listy), przekazanie (wstrzelenie) zależności cacheService, czy wykorzystanie Closure do dynamicznego przekazania domknięcia, aby możliwe było zapisanie jego wyniku na określony czas w pamięci podręcznej (ang. cache).

Wtyczki mogą reagować na wystąpienie zdarzeń w Grails, tj. onChange, onConfigChange oraz onShutdown. W onChange wtyczka monitoruje zbiór zasobów określonych przez właściwość watchedResources w klasie wtyczki, np.
 def watchedResources = "file:./grails-app/i18n/*.properties"
Właściwość watchedResources działa na bazie org.springframework.core.io.support.PathMatchingResourcePatternResolver oraz pakietu Spring Core IO.

Domknięcie związane z onChange na wejściu dostanie mapę składającą się z source (źródło zdarzenia, które jest typu org.springframework.core.io.Resource dla zasobów innych niż klasy lub java.lang.Class, jeśli obserwuje się klasy Groovy), application (egzemplarz GrailsApplication), manager (egzemplarz GrailsPluginManager) oraz ctx (egzemplarz springowego ApplicationContext), np. (Listing 13-29):
 class QuartzGrailsPlugin {
def watchedResources = "file:./grails-app/jobs/**/*Job.groovy"

def onChange = { event ->
Class changedJob = event.source
GrailsClass newJobClass = application.addArtefact(changedJob)
def newBeans = beans {
"${newJobClass.propertyName}"(JobDetailsBean) {
name = newJobClass.name
jobClass = newJobClass.getClazz()
}
}
new Beans.registerBeans(applicationContext)
}
}
Warto zapoznać się dokładniej z powyższym kodem i zauważyć Grails DSL dla Spring Framework, gdzie zaprezentowano wykonanie metody beans, która przyjmuje domknięcie będącym rozszerzenie konfiguracji Springa w trakcie działania aplikacji. Ponownie, zapożyczone z Groovy, dynamiczne wykonanie metody przez jej nazwę w zmiennej tekstowej. Warto również zwrócić uwagę na wykonanie metody application.addArtefact, które rejestruje nową klasę jako artefakt grailsowy i od tej pory Grails będzie wiedział, jaka jest konwencja nazewnicza zadań quartz'owych. Pretty neat, huh?

Zdarzenie onConfigChange pojawia się przy zmianie głównego pliku konfiguracji aplikacji grailsowej, tj. zmianie grails-app/conf/Config.groovy. Oczywiście, na wejściu onConfigChange dostaniemy się do egzemplarza ConfigObject, który jest również dostępny później jako GrailsApplication.config.

Zdarzenie onShutdown pojawia się, przy wykonaniu metody GrailsPluginManager.shutdown(), np. przy odinstalowaniu aplikacji z kontenera.

Domknięcie doWithWebDescriptor wykonywane jest, po przetworzeniu web.xml przez Grails, a przed faktycznym uruchomieniem aplikacji. Ciekawostką tego rozwiązania jest fakt, że zmiany w web.xml, który jest plikiem XML, dokonujemy za pomocą grailsowego XmlSlurper, który udostępnia Grails DSL do pracy z plikami XML. W zasadzie wykorzystane są jedynie funkcjonalności języka Groovy, co w połączeniu z odpowiednimi nazwami metod sprawia wrażenie czegoś niezwykle odświeżającego (chyba zaczynam się z tym 'odświeżającym' powtarzać?), np. (Listing 13-30):
 def doWithWebDescriptor = {webXml ->
// pobieramy listę wszystkich filtrów przez wykonanie metody getFilter()
def filters = webXml.filter
// przechodzimy za ostatni z filtrów
def lastFilter = filters[filters.size()-1]
// ...i dodajemy własną deklarację filtra z operatorem +
lastFilter + {
// wykonujemy metodę filter, która akceptuje domknięcie będący podelementami filter w XML
filter {
// wykonanie metody filter-name to dodanie elementu filter-name do XMLa
// z wartością urlMapping
'filter-name'('urlMapping')
// podobnie tutaj, ale wartość wyliczana jest dynamicznie
'filter-class'(UrlMappingsFilter.getName())
}
}
...
}
I niech ktoś powie, że to nie jest odświeżające?!

Podobna funkcjonalność, tyle, że statyczna, możliwa jest do uzyskania poleceniem grails install-templates, której podajemy zmodyfikowany szablon web.xml.

Stworzenie paczki dystrybucyjnej wtyczki to polecenie grails package-plugin z poziomu katalogu głównego projektu wtyczki.

Opublikowanie wtyczki w centralnym repozytorium Grails wiąże się z uzyskaniem praw (opisane na stronach Creating, Distributing & Installing) i wydanie polecenia grails release-plugin. Uzyskanie praw do centralnego repo to uzyskanie loginu i hasła do repozytorium SVN, który jest podstawą technologiczną repozytorium wtyczek w Grails. Jeśli chcielibyśmy opublikować wtyczkę we własnym repozytorium wystarczy stworzyć własne repozytorium SVN i wskazać na nie w grails-app/conf/BuildConfig.groovy dla pojedynczej aplikacji lub ogólnie, dla wszystkich, w USER_HOME/.grails/settings.groovy, np. (Listing 13-32):
 grails.plugin.repos.discovery.myRepository="http://foo.bar.com"
grails.plugin.repos.distribution.myRepository="https://foo.bar.com"
Lista adresów w zmiennej discovery wykorzystywana jest przez polecenia list-plugins, install-plugin i plugin-info. Zmienna distribution jest wykorzystywana przez polecenie release-plugin. Wskazanie na zdefiniowany serwer to wykonanie grails release-plugin z opcją repository, której wartością jest nazwa repozytorium, np.
 grails release-plugin -repository=myRepository
Przez cały rozdział autorzy tworzą kilka wtyczek, za pomocą których prezentują ich siłę "krojenia" aplikacji na moduły. Do tego należy dodać sztuczki w Groovy i co jakiś czasy możnaby zejść na serce (ze zdumienia, że wiele z tych usprawnień nie ma co szukać w "czystej" Javie i alternatywnych szkieletach webowych), np. (Listing 13-37):
 albumArtService.cacheService = [cacheOrReturn:{key, callable -> callable() }]
które zaślepia niedostępność mechanizmu wstrzeliwania zależności w klasach-testach jednostkowych. Za pomocą "duck typing" w Groovy "przesłaniamy" wywołanie metody cacheOrReturn() na zmiennej cacheService w obiekcie albumArtService. W Groovy możemy zdefiniować mapę, której klucz wskazuje na przesłanianą metodę, a wartość jej implementację. Cudo!

Dodanie dodatkowej metody do wszystkich kontrolerów w domknięciu doWithDynamicMethods, to (Listing 13-38):
 class AlbumArtGrailsPlugin {
def doWithDynamicMethods = { ctx ->
def albumArtService = ctx.getBean("albumArtService")

application.controllerClasses
*.metaClass
*.getAlbumArt = { String artist, String album ->
return albumArtService.getAlbumArt(artist, album)
}
}
Jeślibyśmy zamiast application.controllerClasses użyli application.domainClasses rozszerzanie dotyczyłoby klas dziedzinowych.

Tworzenie widoków GSP jako części składowej wtyczek wymaga, aby użycie <g:render> wskazywało na wtyczkę, z której zostało wywołane za pomocą atrybutu plugin, gdyż w przeciwnym przypadku Grails próbowałby odszukać szablonu w ramach samej struktury katalogowej aplikacji, np. (Listing 13-44):
 <g:render plugin="blog"
template="post"
var="post"
collection="${postList?.reverse()}" />
Dostęp do wartości zmiennej konfiguracyjnej zdefiniowanej w grails-app/conf/Config.groovy w stronie GSP to ${grailsApplication.config.[nazwa-zmiennej]}, tj.
 <h1>${grailsApplication.config.blog.title ?: 'No Title'}</h1>
Po instalacji wtyczki twórca aplikacji może zdefiniować zmienną "blog.title" w Config.groovy, nadpisując tym samym domyślną wartość. Innym rozwiązaniem mogłoby być skorzystanie ze znacznika <g:message> i użycie plików grails-app/i18n/messages*.properties.

Jak to ujęli autorzy: "Plugins are definitely worth a look, even if you don't intend to become an expert on Grails internals". Wiedzą, co piszą! Zgadzam się z tym w 100%.

UWAGA: Właśnie się dowiedziałem od mojego syna - OBCY istnieją! Po co w takim razie byłyby tabliczki "OBCYM wstęp wzbroniony!"?! ;-)

2 komentarze:

  1. z tymi wtyczkami to PRAWIE JAK WebServicy :) zipowanie coś na wzór stubów itd.

    OdpowiedzUsuń
  2. Nie rozumiem?! Co jest podobnego między wtyczkami a WS? WS (ale i inne technologie również) dostarczają pewnej funkcjonalności i kontakt z nimi jest zdalny, wtyczki dostarczają funkcjonalności (technicznie i biznesowo) lokalnie. Rozwiń proszę myśl, bo przez weekend będę siedział i dumał, a rodzinka wolałaby mnie takiego posępnego nie oglądać ;-)

    OdpowiedzUsuń