20 kwietnia 2009

Z rozdziału 12. o integracji Grails z DGG2

W rozdziale 12. "Integrating Grails" dowiadujemy się o włączaniu Grails do istniejących środowisk - systemu budowania, środowisk IDE, konfigurację raportów jakościowych naszego kodu i środowiska serwerowego. Wszystko to, co sprawia, że tworzenie aplikacji Grails jest łatwiejsze, przyjemniejsze, ale również wysokiej jakości.

Siłą Grails jest podejście "konwencja ponad konfigurację", więc przy zachowaniu odpowiednich reguł nie potrzeba nic konfigurować. Mimo wszystko, Grails nie wzbrania nas przez konfiguracją, jeśli mamy taką potrzebę. Jak to ujęli autorzy - konwencja ponad konfigurację, ale nie zamiast niej (ang. "Crucially, however, it's convention over configuration, not instead of it"). Do mnie przemawia.

Konfiguracja ogólna dla całej aplikacji Grails znajduje się w grails-app/conf/Config.groovy. Jest to skrypt Groovy przypominający plik Java typu properties - seria klucz-wartość. Ustawiamy zmienne korzystając z operatora dereferencji (kropka), np.:
 grails.views.gsp.encoding="UTF-8"
Dostęp do tej zmiennej w poziomu aplikacji jest możliwy przy pomocy właściwości config egzemplarza grailsApplication, który jest dostępny w kontrolerach i widokach, np.:
 assert grailsApplication.config.grails.views.gsp.encoding == "UTF-8"
Możemy grupować konfigurację korzystając z bloków, np. (Listing 12-1):
 grails.mime {
file.extensions = true
types = [html: 'text/html']
}
Przy takiej konfiguracji zostaną utworzone dwa wpisy konfiguracyjne w config - grails.mime.file.extensions oraz grails.mime.types.

Istnieje możliwość konfiguracji per środowisko, np.:
 // set per-environment serverURL stem for creating absolute links
environments {
production {
grails.serverURL = "http://www.changeme.com"
}
development {
grails.serverURL = "http://www.rozwojowy.com"
}
}
System komunikatów oparty jest o log4j. Grails upraszcza jego konfigurację z własnym, dedykowanym DSL. Wystarczy zadeklarować zmienną log4j i przypisać jej domknięcie, które stanie się konfiguracją log4j, np.:
 // log4j configuration
log4j = {
// Example of changing the log pattern for the default console
// appender:
//
//appenders {
// console name:'stdout', layout:pattern(conversionPattern: '%c{2} %m%n')
//}

error 'org.codehaus.groovy.grails.web.servlet', // controllers
'org.codehaus.groovy.grails.web.pages', // GSP
'org.codehaus.groovy.grails.web.sitemesh', // layouts
'org.codehaus.groovy.grails."web.mapping.filter', // URL mapping
'org.codehaus.groovy.grails."web.mapping', // URL mapping
'org.codehaus.groovy.grails.commons', // core / classloading
'org.codehaus.groovy.grails.plugins', // plugins
'org.codehaus.groovy.grails.orm.hibernate', // hibernate integration
'org.springframework',
'org.hibernate'

warn 'org.mortbay.log'
}
Jest to domyślna konfiguracja log4j z grails-app/conf/Config.groovy. W ramach domknięcia wywołujemy metody off, fatal, error, warn, info, debug, trace, all (nazwy odpowiadają poziomom komunikatów) z parametrami będącymi listą nazw pakietów, które mają mieć wskazany poziom.

W każdym kontrolerze, usłudze i bibliotece znaczników dostępna jest właściwość log. Domyślna konfiguracja to poziom error. Nasze artefakty grailsowe możemy ustawiać na inne przez wskazanie nazwy klasy, poprzedzonej grails.app, np. (Listing 12-4):
 log4j = {
debug 'grails.app.controller.UserController',
'grails.app.service.AlbumArtService'
}
Domyślnie komunikaty odnotowywane są na standardowym wyjściu. Grails DSL dla log4j pozwala na stworzenie własnego wyjścia (ang. appender) z blokiem appenders, np. (Listing 12-5):
 log4j = {
appenders {
rollingFile name:"myLog",
file:"/var/log/gtunes.log",
maxFileSize:"1MB",
layout: pattern(conversionPattern: '%c{2} %m%n')
}
...
}
Dodatkowe "wyjścia" to jdbc (org.apache.log4j.jdbc.JDBCAppender), null (org.apache.log4j.varia.NullAppender), console (org.apache.log4j.ConsoleAppender), file (org.apache.log4j.FileAppender) i rollingFile (org.apache.log4j.RollingFileAppender).

Z tak zdefiniowanym "wyjściem" możemy związać go z właściwymi pakietami, np. (Listing 12-6):
 log4j = {
...
trace myLog:'org.hibernate'
debug mylog:['org.codehaus.groovy.grails.web.mapping.filter',
'org.codehaus.groovy.grails.web.mapping']
}
Nazwa "wyjścia" odpowiada wartości parametru name z appenders.

W przypadku wystąpienia wyjątku w Grails, stos wywołań Javy czyszczony jest z wewnętrznych wywołań Groovy i Grails. Jednakże, pełny stos wywołań zapisywany jest do pliku stacktrace.log w katalogu głównym projektu. Nadpisanie tej konfiguracji to wskazanie kanału "StackTrace" na odpowiednie "wyjście" i poziom, np. (Listing 12-7):
 log4j = {
appenders {
rollingFile name:"stacktraceLog",
file:"/var/log/unfiltered-stacktraces.log",
maxFileSize:"1MB",
layout: pattern(conversionPattern: '%c{2} %m%n')
}
error stacktraceLog: "StackTrace"
}
Całkowite wyłączenie wypisywania stosu wywołań to ustawienie zmiennej logicznej grails.full.stacktrace, np.
 grails -Dgrails.full.stacktrace=false run-app
Podczas rozwoju aplikacji, plik Config.groovy kompilowany jest do klasy i włączany do archiwum WAR. Istnieje możliwość wyniesienia konfiguracji poza archiwum przez ustawienie właściwości grails.config.location w Config.groovy, która zawiera listę skryptów Groovy, które po połączeniu razem ustanowią konfigurację aplikacji, np. (Listing 12-8):
 grails.config.locations = ["file:${userHome}/gtunes-logging.groovy"]
Jakkolwiek pliki (skrypty) konfiguracyjne Config.groovy i DataSource.groovy są oddzielnymi plikami, Grails i tak ostatecznie łączy je w pojedynczy obiekt konfiguracyjny podczas uruchomienia aplikacji. W ten sposób możemy również wynieść jakąkolwiek konfigurację poza archiwum, np. konfigurację dostępu do bazy danych (Listing 12-9):
 grails.config.locations = ["file:${userHome}/.settings/gtunes-logging.groovy",
"file:${userHome}/.settings/gtunes-datasource.groovy"]
Jeśli interesuje nas konfiguracja za pomocą plików properties (zamiast skryptów Groovy) wystarczy podać w grails.config.locations plik o rozszerzeniu .properties.

Grailsowy system budowania oparty jest na Gant, który z kolei opakowuje Apache Ant. Nie powinno być zaskoczeniem, że Gant korzysta z Groovy DSL do tworzenia skryptów budowania, np. (wycinek z Listing 12-10):
 targetDir = "build"
target(clean:"Cleans any compiled sources") {
delete(dir:targetDir)
}
target(compile:"The compilation task") {
depends(clean)
mkdir(dir:"$targetDir/classes")
javac(srcdir:"src/java"
destdir:"$targetDir/classes")
}
...
setDefaultTarget(clean)
Odpowiednikiem antowego <target> jest metoda target, zależności między zadaniami buduje się za pomocą depends i każde zadanie w Ant ma swój odpowiednik w Gant DSL.

Wykonanie Gant poza Grails to uruchomienie polecenia gant. Głównym plikiem (skryptem) Gant jest build.gant.

Grails opakowuje Gant we własne polecenie grails, który jest zoptymalizowany do pracy z układem katalogów w Grails. Wykonując jakiekolwiek polecenie grails, Grails poszukuje skryptu do wykonania w kolejności PROJECT_HOME/scripts, GRAILS_HOME/scripts, PLUGINS_HOME/*/scripts i USER_HOME/.grails/scripts. Wykonywany jest domyślne zadanie skryptu. Wykonanie grails create-app poszukuje CreateApp.groovy we wskazanych katalogach (pierwszy wygrywa) - już powinno być jasne, jaka jest konwencja (łatwiej było mi przedstawić na przykładzie niż opisać).

Tworzenie własnych poleceń jest możliwe za pomocą grails create-script [nazwa-polecenia]. W książce pojawiają się dokładnie opisane przykłady tworzenia własnego polecenia (chociażby tworzenie polecenia do wdrażania projektu na Apache Tomcat), jednakże sama analiza dostępnych skryptów w GRAILS_HOME/scripts będzie niemniej wartościową lekcją (zresztą, analiza już stworzonych i działających aplikacji zawsze jest wartościowa - bez względu na poziom zaawansowania programisty, aczkolwiek, czym bardziej zaawansowany tym lepiej).

Podczas uruchomienia skryptów Groovy, Grails zapisuje ich klasy w katalogu USER_HOME/.grails/[wersja-grails]/projects/[nazwa-projektu]/classes. Za pomocą zmiennej grails.work.dir możemy zmienić położenie tego katalogu, np.
 grails -Dgrails.work.dir=/tmp run-app
Warto pamiętać, że polecenie grails nie wspiera łączenia zadań, jak gant czy ant, tj. możliwe jest wykonanie jedynie pojedynczego polecenia w danej chwili.

Ciekawostką, która powinna uprościć nam wdrożenie grailsowego systemu budowania do już istniejącej infrastruktury opartej na Apache Ant (tym samym i Apache Maven, i Apache Ivy, który de facto jest wykorzystywany i tak w Grails) jest możliwość wykorzystania tworzonego automatycznie build.xml z katalogu głównego projektu. On z kolei wywołuje polecenia Grails (wymagana jest instalacja Grails). Wystarczy wykonać polecenie ant test w katalogu głównym projektu, aby przekonać się o tym, np.
 $ ant test
Buildfile: build.xml

download-ivy:

-download-ivy:

init-ivy:

-resolve:
No ivy:settings found for the default reference 'ivy.instance'. A default instance will be used
[ivy:retrieve] :: Ivy 2.0.0 - 20090108225011 :: http://ant.apache.org/ivy/ ::
:: loading settings :: file = c:\projs\sandbox\grailsmysql\ivysettings.xml
[ivy:retrieve] :: resolving dependencies :: org.example#grailsmysql;working@work
...
---------------------------------------------------------------------
| | modules || artifacts |
| conf | number| search|dwnlded|evicted|| number|dwnlded|
---------------------------------------------------------------------
| build | 47 | 0 | 0 | 3 || 44 | 0 |
| compile | 35 | 0 | 0 | 4 || 31 | 0 |
| test | 37 | 0 | 0 | 5 || 32 | 0 |
| runtime | 40 | 0 | 0 | 5 || 35 | 0 |
---------------------------------------------------------------------
[ivy:retrieve] :: retrieving :: org.example#grailsmysql
[ivy:retrieve] confs: [build, compile, test, runtime]
[ivy:retrieve] 0 artifacts copied, 142 already retrieved (0kB/79ms)

-init-grails:

test:
[grailsTask] Running pre-compiled script
[grailsTask] Environment set to test
[grailsTask] [groovyc] Compiling 1 source file to
C:\Documents and Settings\jlaskowski\.grails\1.1\projects\grailsmysql\classes
[grailsTask]
[grailsTask] [mkdir] Created dir: C:\projs\sandbox\grailsmysql\test\reports\html
[grailsTask]
[grailsTask] [mkdir] Created dir: C:\projs\sandbox\grailsmysql\test\reports\plain
[grailsTask]
[grailsTask]
[grailsTask] Starting unit tests ...
[grailsTask] Running tests of type 'unit'
[grailsTask] -------------------------------------------------------
[grailsTask] Running 2 unit tests...
[grailsTask] Running test pl.jaceklaskowski.grails.KsiazkaControllerTests...PASSED
[grailsTask] Running test pl.jaceklaskowski.grails.KsiazkaTests...PASSED
[grailsTask] Tests Completed in 875ms ...
[grailsTask] -------------------------------------------------------
[grailsTask] Tests passed: 2
[grailsTask] Tests failed: 0
[grailsTask] -------------------------------------------------------
[grailsTask]
[grailsTask] Starting integration tests ...
[grailsTask] [groovyc] Compiling 1 source file to
C:\Documents and Settings\jlaskowski\.grails\1.1\projects\grailsmysql\classes
[grailsTask]
[grailsTask] [groovyc] Compiling 1 source file to
C:\Documents and Settings\jlaskowski\.grails\1.1\projects\grailsmysql\classes
[grailsTask]
[grailsTask] Running tests of type 'integration'
[grailsTask] No tests found in test/integration to execute ...
[grailsTask] [junitreport] Processing
C:\projs\sandbox\grailsmysql\test\reports\TESTS-TestSuites.xml to c:\temp\null276154924
[grailsTask]
[grailsTask] [junitreport] Loading stylesheet
jar:file:/c:/projs/sandbox/grailsmysql/lib/build/ant-junit-1.7.1.jar!/org/apache/tools/ant/taskdefs/optional/junit/xsl/junit-frames.xsl
[grailsTask]
[grailsTask] [junitreport] Transform time: 844ms
[grailsTask]
[grailsTask] [junitreport] Deleting: c:\temp\null276154924
[grailsTask]
[grailsTask]
[grailsTask] Tests PASSED - view reports in C:\projs\sandbox\grailsmysql\test\reports.

BUILD SUCCESSFUL
Total time: 22 seconds
Innym sposobem uruchomienia grails z poziomu skryptu w Ant jest zaimportowanie pliku GRAILS_HOME/src/grails/grails-macros.xml, w którym definiuje się zadanie <grails>, np.:
 <property environment="env" />
<import file="${env.GRAILS_HOME}/src/grails/grails-macros.xml" />
...
<grails command="test-app" />
Zadanie grails poszukuje Grails, aczkolwiek instalacja nie jest konieczna, gdyż wystarczy skorzystać z <extend-classpath>, aby wskazać na pliki jar Grails.

Domyślnie Grails nie udostępnia raportów pokrycia testami (ang. code coverage reports). Istnieje dedykowana do tego zadania wtyczka code-coverage. Po jej instalacji (grails install-plugin code-coverage) wystarczy wykonać polecenie test-app, aby uruchomić testy (jednostkowe i integracyjne), po których zostaną utworzone raporty (w książce wspomina się o poleceniu test-app-cobertura, jednakże sprawdziłem, że obecna wersja wtyczki działa na mechaniźmie zdarzeń Gant i nie dodaje własnego polecenia - integracja powinna być na tyle przeźroczysta, jak to tylko możliwe, aby nie było konieczności uczenia się nowych poleceń).
 $ grails install-plugin code-coverage
Welcome to Grails 1.1 - http://grails.org/
Licensed under Apache Standard License 2.0
Grails home is set to: c:/apps/grails

Base Directory: C:\projs\sandbox\grailsmysql
Running script c:\apps\grails\scripts\InstallPlugin.groovy
Environment set to development
Reading remote plugin list ...
Reading remote plugin list ...
Plugin list out-of-date, retrieving..
[delete] Deleting: C:\Documents and Settings\jlaskowski\.grails\1.1\plugins-list-default.xml
[get] Getting: http://plugins.grails.org/.plugin-meta/plugins-list.xml
[get] To: C:\Documents and Settings\jlaskowski\.grails\1.1\plugins-list-default.xml
......................
[get] last modified = Sun Apr 19 21:57:32 CEST 2009
[get] Getting: http://plugins.grails.org/grails-code-coverage/tags/RELEASE_1_1_5/grails-code-coverage-1.1.5.zip
[get] To: C:\Documents and Settings\jlaskowski\.grails\1.1\plugins\grails-code-coverage-1.1.5.zip
.....................
[get] last modified = Sat Mar 07 14:47:44 CET 2009
[copy] Copying 1 file to C:\Documents and Settings\jlaskowski\.grails\1.1\projects\grailsmysql\plugins
Installing plug-in code-coverage-1.1.5
[mkdir] Created dir: C:\Documents and Settings\jlaskowski\.grails\1.1\projects\grailsmysql\plugins\code-coverage-1.1.5
[unzip] Expanding:
C:\Documents and Settings\jlaskowski\.grails\1.1\plugins\grails-code-coverage-1.1.5.zip into
C:\Documents and Settings\jlaskowski\.grails\1.1\projects\grailsmysql\plugins\code-coverage-1.1.5
Executing code-coverage-1.1.5 plugin post-install script ...
Plugin code-coverage-1.1.5 installed
Found events script in plugin code-coverage
Teraz wystarczy uruchomić polecenie grails test-app
 $ grails test-app
Welcome to Grails 1.1 - http://grails.org/
Licensed under Apache Standard License 2.0
Grails home is set to: c:/apps/grails

Base Directory: C:\projs\sandbox\grailsmysql
Running script c:\apps\grails\scripts\TestApp.groovy
Environment set to test
[groovyc] Compiling 1 source file to C:\Documents and Settings\jlaskowski\.grails\1.1\projects\grailsmysql\classes
[mkdir] Created dir: C:\projs\sandbox\grailsmysql\test\reports\html
[mkdir] Created dir: C:\projs\sandbox\grailsmysql\test\reports\plain
Instrumenting classes for coverage ...
[cobertura-instrument] Cobertura 1.9 - GNU GPL License (NO WARRANTY) - See COPYRIGHT file
[cobertura-instrument] Instrumenting 2 files
[cobertura-instrument] Cobertura: Saved information on 2 classes.
[cobertura-instrument] Instrument time: 125ms

Starting unit tests ...
Running tests of type 'unit'
-------------------------------------------------------
Running 2 unit tests...
Running test pl.jaceklaskowski.grails.KsiazkaControllerTests...PASSED
Running test pl.jaceklaskowski.grails.KsiazkaTests...PASSED
Tests Completed in 782ms ...
-------------------------------------------------------
Tests passed: 2
Tests failed: 0
-------------------------------------------------------

Starting integration tests ...
[copy] Copying 1 file to C:\Documents and Settings\jlaskowski\.grails\1.1\projects\grailsmysql\test-classes\integration
[mkdir] Created dir: C:\projs\sandbox\grailsmysql\web-app\plugins\code-coverage-1.1.5
[copy] Copying 28 files to C:\projs\sandbox\grailsmysql\web-app\plugins\code-coverage-1.1.5
[groovyc] Compiling 1 source file to C:\Documents and Settings\jlaskowski\.grails\1.1\projects\grailsmysql\classes
[copy] Copying 1 file to C:\Documents and Settings\jlaskowski\.grails\1.1\projects\grailsmysql\classes
[groovyc] Compiling 1 source file to C:\Documents and Settings\jlaskowski\.grails\1.1\projects\grailsmysql\classes
Running tests of type 'integration'
No tests found in test/integration to execute ...
[junitreport] Processing C:\projs\sandbox\grailsmysql\test\reports\TESTS-TestSuites.xml to c:\temp\null727040804
[junitreport] Loading stylesheet
jar:file:/c:/apps/grails/lib/ant-junit-1.7.0.jar!/org/apache/tools/ant/taskdefs/optional/junit/xsl/junit-frames.xsl
[junitreport] Transform time: 2672ms
[junitreport] Deleting: c:\temp\null727040804

Tests PASSED - view reports in C:\projs\sandbox\grailsmysql\test\reports.
Cobertura: Loaded information on 2 classes.

--------------------------------------------
***********WARNING*************
Unable to flush code coverage data.
This usually happens when tests don't actually test anything;
e.g. none of the instrumented classes were exercised by tests!
--------------------------------------------

[mkdir] Created dir: C:\projs\sandbox\grailsmysql\test\reports\cobertura
[cobertura-report] Cobertura 1.9 - GNU GPL License (NO WARRANTY) - See COPYRIGHT file
[cobertura-report] Cobertura: Loaded information on 2 classes.
[cobertura-report] Report time: 187ms
Done with post processing reports in 15ms
[delete] Deleting: C:\Documents and Settings\jlaskowski\.grails\1.1\projects\grailsmysql\resources\web.xml
[delete] Deleting directory C:\projs\sandbox\grailsmysql\web-app\plugins
[delete] Deleting directory C:\Documents and Settings\jlaskowski\.grails\1.1\projects\grailsmysql\classes
[delete] Deleting directory C:\Documents and Settings\jlaskowski\.grails\1.1\projects\grailsmysql\resources
[delete] Deleting directory C:\Documents and Settings\jlaskowski\.grails\1.1\projects\grailsmysql\test-classes
Cobertura Code Coverage Complete (view reports in: C:\projs\sandbox\grailsmysql\test\reports/cobertura)
i podziwiać stworzony raport:

Kolejnym tematem w książce jest przedstawienie podejścia ciągłej integracji (ang. CI - continuous integration) na przykładzie integracji Grails i Hudson (o CI pisał ostatnio Łukasz Lenart w Ciągła Integracja (CI) - o co chodzi?), po którym kilka stron omówienia narzędzi IDE, rozpoczynając od IntelliJ IDEA ("By far the most complete IDE for Grails available at the moment is JetBrains' Intellij IDEA with the JetGroovy plugin installed"), NetBeans ("Of the open source IDEs available, NetBeans provides the most advanced support for Groovy and Grails development"), Eclipse ("It is still under active development") i TextMate ("If you happen to be lucky to work on a Mac (cue flame wars!), then you can take advantage of the excellent support for Groovy and Grails in the TextMate text editor"). Autorzy przedstawiają integrację aplikacji Grails z serwerem pocztowym, wykonywanie zadań z wtyczką Quartz, aby na zakończenie omówić temat wdrażania aplikacji Grails na serwer i inicjowanie bazy danych.

Najprostszą opcją wdrożenia aplikacji Grails na serwerze aplikacyjnym jest zainstalowanie Grails na serwerze produkcyjnym i uruchomienie aplikacji poleceniem grails run-war, które uruchamia serwer Jetty na porcie 8080. Stawiamy Apache HTTP Server z wtyczką mod_proxy i mamy działające rozwiązanie. Z opcją server.port możemy uruchomić serwer na wskazanym porcie, np.
 grails -Dserver.port=80 run-war
Alternatywą jest stworzenie pliku WAR (polecenie grails war) i samodzielne wdrożenie na serwer. Zaleca się ustawienie opcji -server w JVM serwera, zwiększenie PermGen (-XX:MaxPermSize=256m) oraz samej pamięci dla aplikacji (-Xmx512m). Strojenie aplikacji i konfiguracji serwera zdecydowanie zalecane.

Początkowo, stworzony plik WAR poleceniem grails war ma w nazwie domyślny numer wersji - 0.1. Zmiana wersji aplikacji to grails set-version [numer-wersji]. Istnieje możliwość poznania numeru wersji przy uruchomionej aplikacji przez
 println grailsApplication.metadata."app.version"
albo korzystając ze znacznika <g:meta> w GSP, np. (Listing 12-50):
 Version: <g:meta name="app.version" />
Build with Grails <g:meta name="app.grails.version" />
Wskazanie na własny web.xml to zmiana grails.config.base.webXml w grails-app/conf/Config.groovy, np.:
 grails.config.base.webXml="file:${userHome}/.settings/my-web.xml"
Zmiana miejsca, gdzie zapisany jest wynikowy war to grails.war.destFile, zmiana jego zawartości to grails.war.copyToWebApp (zmienna, która przyjmuje domknięcie z konstrukcjami gantowymi) oraz grails.war.resources (z domknięciem, którego parametrem wejściowym jest stagingDir, gdzie piszemy ala Gant).

Rozdział kończy się tematem wypełniania bazy danych z pomocą skryptu grails-app/conf/BootStrap.groovy z dwoma zmiennymi init oraz destroy, uruchamiane, odpowiednio, przy uruchomieniu serwera, a tym samym i aplikacji i jej zatrzymaniu (niegwarantowane, np. przy padzie serwera). Wystarczy wykonać kilka GORMowych konstrukcji z save() do zapisania zmian i baza przygotowana do pracy. Tworzenie danych w bazie można warunkować środowiskiem za pomocą zmiennej grails.util.GrailsUtil.environment, np. (Listing 12-54):
 def init = {
switch(grails.util.GrailsUtil.environment) {
case "development":
def album = new Album(title:"Because of the Times")
.addToSongs(title:"Knocked Up")
...
def Artist(name:"Kings of Leon")
.addToAlbums(album)
.save(flush:true)
break
case "production":
// tutaj inne "wstawki" produkcyjne
break
}
}
Zamiast nazw można użyć stałych z grails.util.Environment. Okazuje się nawet, że opisany w książce sposób oznaczony jest jako...niezalecany (ang. deprecated) - grails.util.GrailsUtil.getEnvironment()! Zdumiewające.