13 września 2006

Zgłębianie tajników działania środowiska JavaServer Faces (JSF)

0 komentarzy
UWAGA: Przez środowisko JSF rozumie się implementację JavaServer Faces, której środowiskiem uruchomieniowym jest kontener servletów, ewentualnie kontener portletów.

JSF działa w ramach kontenera servletów. Innymi słowy JSF jest aplikacją internetową ze wszystkimi jej konsekwencjami i niewątliwie znajomość technologii Java Servlets znacznie ułatwia zrozumienie działania mechanizmów JSF. Podstawową konsekwencją wykorzystania kontenera servletów jako fundamentu jest sposób uruchamiania aplikacji opartej o JSF. Punktem początkowym obsługującym zlecenia aplikacji JSF jest odpowiednie mapowanie javax.faces.webapp.FacesServlet w deskryptorze rozmieszczenia aplikacji internetowej (ang. deployment descriptor - /WEB-INF/web.xml). Najczęściej wykorzystywanym mapowaniem jest mapowanie per ścieżka (/faces/*) albo per rozszeżenie (*.faces i/lub *.jsf). Wyzwolenie servletu FacesServlet to tak na prawdę uruchomienie środowiska JSF. FacesServlet jest punktem wejścia do JSF - spoiwem łączącym dwie technologie Java Servlets i JavaServer Faces. Jak każdy servlet tak i FacesServlet wczytuje swoją konfigurację z deskryptora instalacji. Jest kilka elementów fundamentalnych dla działania FacesServlet (a tym samym i JSF). Są nimi:

  • fabryka obiektów FacesContext, które dostarczają informacji o aktualnie przetwarzanym zleceniu i odpowiedzi.
  • fabryka obiektów Lifecycle, które odpowiedzialne są za przetwarzanie zleceń według ustalonych etapów.

Parametrów konfiguracyjnych może być znacznie więcej i zależą od implementacji specyfikacji JSF. Konfiguracja obu fabryk obsługiwana jest przez implementację, z której korzystamy i zazwyczaj nie potrzeba się o nią martwić. Najczęściej wymieniane implementacje (środowiska uruchomieniowe) specyfikacji JSF to implementacja referencyjna JSF (ang. JSF Reference Implementation) oraz Apache MyFaces.

Każde zlecenie obsługiwane przez FacesServlet może uczestniczyć w 6 etapach, podczas których wykonywane są odpowiednie czynności związane z przetwarzanym zleceniem. Ważne jest, aby pamiętać, że przejście wszystkich etapów nie jest obowiązkowe i co istotne może być kontrolowane przez samego autora aplikacji JSF. Dodatkowo środowisko JSF może dostarczać własne etapy poza tymi wymienionymi w specyfikacji. Nie można zmienić kolejności wykonywania etapów. Dodać należy również, że lista etapów jest uporządkowana i ominięcie pojedyńczego implikuje zaniechanie wykonania następnych poza ostatnim.

Etapy w życiu zlecenia obsługiwanego przez JSF to:

  1. Restore View
  2. Apply Request Values
  3. Process Validations
  4. Update Model Values
  5. Invoke Application
  6. Render Response

Praca FacesServlet zaczyna się (i jednocześnie kończy) po odebraniu zlecenia i przekazaniu do obiektu implementującego interfejs javax.faces.lifecycle.Lifecycle. Referencyjna implementacja JSF udostępnia go jako obiekt typu com.sun.faces.lifecycle.LifecycleImpl. Po utworzeniu obiektu FacesContext (przy pomocy fabryki FacesContext) przekazuje się go kolejno do metod Lifecycle.execute(FacesContext) i Lifecycle.render(FacesContext). Zakończenie pracy serwletu kończy przetwarzanie zlecenia przez JSF.

Z każdym etapem można związać słuchacza (ang. listener), który jest uruchamiany przy wejściu do i wyjściu z wybranego etapu. Słuchacz implementuje interfejs javax.faces.event.PhaseListener. Domyślnie, konfiguracja słuchacza odbywa się w pliku konfiguracyjnym JSF - faces-config.xml przy pomocy znacznika faces-config/lifecycle/phase-listener (wartością znacznika jest nazwa klasy).

Wywołanie etapu poprzedza sprawdzenie, czy wywołano metodę FacesContext.renderResponse() na obiekcie typu FacesContext związanym z przetwarzanym zleceniem. Wywołanie metody FacesContext.renderResponse() jest sygnałem dla JSF, że zakończenie aktualnie przetwarzanego etapu powinno przekazać kontrolę zlecenia do ostatniego etapu - Render Response - omijając jakiekolwiek etapy pośrednie.

Poza sprawdzeniem stanu związanego z wywołaniem FacesContext.renderResponse(), następuje sprawdzenie, czy wywołano metodę FacesContext.responseComplete() na bieżącym obiekcie typu FacesContext. Informuje ona JSF, że należy zakończyć przetwarzanie zlecenia omijając jakiekolwiek etapy pośrednie. Jest to moment, w którym odpowiedź HTTP została już wysłana do klienta i zakończenie etapu to całkowite zakończenie przetwarzania zlecenia. Możliwym przykładem mogłoby być przekierowanie żądania (ang. HTTP redirect), albo wysłanie strumienia binarnego, np. obrazka.

Każdorazowo przed uruchomieniem etapu następuje wzbudzenie słuchaczy (jeśli takowe zostały zarejestrowane w faces-config.xml). Jeśli słuchacz wykona dowolną z w/w metod (FacesContext.responseComplete() albo FacesContext.renderResponse() w innym etapie niż Render Response) następuje zaniechanie wykonania etapu i zakończenie obsługi żądania, albo przejście do etapu Render Response. Wywołanie słuchaczy odbywa się poprzez wywołanie metod javax.faces.event.PhaseListener.beforePhase(PhaseEvent) bądź javax.faces.event.PhaseListener.afterPhase(PhaseEvent), odpowiednio przed i po etapie. Słuchacz rejestruje swoje zainteresowanie obsługą wejścia do i wyjścia z etapu implementując odpowiednią metodę, a same etapy poprzez metodę javax.faces.event.PhaseListener.getPhaseId().

Jak opisano wyżej, obie metody FacesContext.renderResponse() oraz FacesContext.responseComplete() mogą być wywołane przez elementy stworzone przez programistę w ramach aplikacji JSF (np. słuchacze) i w ten sposób wpływać na przebieg obsługi żądania.

29 sierpnia 2006

Java EE 5 - wpływ projektów wolnodostępnych na uproszczenie specyfikacji

10 komentarzy
Najnowsza wersja specyfikacji Java Platform Enterprise Edition (w skrócie Java EE czy JEE) to wersja 5. Każda ze specyfikacji wprowadzała użytkownika języka Java w coraz to bardziej zaawansowane arkana sztuki tworzenia aplikacji przemysłowych. Niestety szło to w parze z koniecznością poznania wielu zawiłości specyfikacji składowych (przede wszystkim Enterprise JavaBeans (EJB)). Stanowiło to nie lada wyzwanie nawet dla zaawansowanych programistów.

Jednym z podstawowych problemów nie tylko dla świeżych adeptów poprzednich wersji Java EE (nazywanych poprzednio Java 2 Enterprise Edition - J2EE) było utrzymywanie plików pomocniczych wymaganych przez poszczególne specyfikacje, m.in. deskryptory instalacji i dodatkowe klasy pomocnicze. Ich istnienie było krytykowane, ponieważ nie dostarczały wartości dodanej, a osoby korzystające z usług serwerów aplikacyjnych J2EE musiały borykać się z utrzymywaniem dodatkowych plików, powiązanych jedynie w środowisku uruchomieniowym. Była to bezpardonowa walka nie tylko dla osób dopiero wkraczających na teren J2EE (zauważ na skrót, który wskazuje na wersje specyfikacji wcześniejszych od Java EE 5, np. J2EE 1.4). Szczęśliwie pojawiło się panaceum. Środkiem zaradczym stały się projekty wolnodostępne, np. XDoclet, których cykl wytwórczy był krótszy i wdrożenia udogodnień następowały na bieżąco. XDoclet pozwalał na opisanie kodu źródłowego aplikacji i utworzenie plików pomocniczych automatycznie podczas budowania aplikacji. Opisywanie kodu źródłowego następowało za pomocą znaczników JavaDoc, które precyzowały stosowanie naszych klas. Pozwalało to na znaczne uproszczenie tworzenia aplikacji J2EE kosztem wprowadzenia do cyklu wytwarzania narzędzi pomocniczych. Koszt nie był wielki, a z pewnością zdecydowanie mniejszy niż realizowanie zadania na własną rękę. XDoclet stał się de facto standardem - narzędziem wykorzystywanym podczas tworzenia aplikacji J2EE (świadomie używam skrótu poprzedniej specyfikacji) w każdym zespole, który niwelował potrzebę utrzymywania niezliczonej ilości plików pomocniczych. Ci, którzy nie stosowali XDoclet najczęściej znajdowali inne rozwiązania semi-J2EE. Tak, czy owak stosowanie J2EE sprowadziło się głównie do budowania aplikacji z użyciem Java Servlets i JavaServer Pages (choć i dla nich znaleziono również alternatywne rozwiązania). Szczęśliwie, zalety stosowania narzędzi wspierających tworzenie aplikacji, jak XDoclet, dostrzeżono nie tylko w zespołach rozwijających specyfikację Java 2 Enterprise Edition, ale również i samego języka Java. W najnowszej produkcyjnej odsłonie języka Java SE 5 wprowadzono mechanizm anotacji (ang. annotations) zwanego również meta-danymi. Tym samym anotacje stały się częścią języka Java, co powodowało, że stał się on powszechnie stosowanym rozwiązaniem opisu kodu źródłowego na podstawie, którego można tworzyć inne artefakty projektowe, np. deskryptory instalacji. Ważny podkreślenia jest fakt udostępnienia tego mechanizmu nie tylko dla osób związanych z platformą Java EE, ale przede wszystkim Java SE (Java Standard Edition 5), czyli w samym języku Java (!) Zaletę stosowania anotacji każdy z nas, programujących w Javie, dostrzegł już wcześniej korzystając ze znaczników JavaDoc do opisywania kodu źródłowego. W ten sposób tworzono dodatkowy artefakt projektowy jakim jest dokumentacja publicznego interfejsu (API) projektu. Za pomocą JavaDoc, niewielkim kosztem można stworzyć dokumentację, której układ i znaczenie znane są każdemu użytkownikowi aplikacji Java. Dla wielu, problemy związane z dokumentacją już nie istnieją, albo sprowadzają się jedynie do utrzymywania JavaDoc (co i tak niestety jeszcze dla wielu stanowi wyzwanie). Grupa standaryzująca specyfikację Java EE 5 wykorzystała dobrodziejstwa płynące z wdrożenia anotacji do opisywania klas tak, aby na ich podstawie utworzyć pozostałe części składowe aplikacji JEE. Życie twórcy aplikacji przemysłowych w Javie stało się zauważalnie prostsze. Na uwagę zasługuje fakt, że stosowanie anotacji stało się tak popularne, że już nie ma osób, które nie korzystają, albo nie planują z nich skorzystać. W naturalny sposób programiści języka Java nabyli użytecznej umiejętności utrzymywania dodatkowych, zewnętrznych plikach aplikacji w ryzach implementacyjnych bez konieczności nauki dodatkowej (i zazwyczaj niepotrzebnej) wiedzy o niskopoziomowych zasadach działania specyfikacji J2EE (ponownie uwaga na akronim). W zasadzie można było stwierdzić, że stosowanie J2EE "zanieczyszczało" nam nasz projekt i dlatego stosowanie projektów wolnodostępnych jako antidotum na bolączki wdrożenia zasad J2EE zdobywało uznanie. Powoli znikały "czyste" aplikacje J2EE i pojawiały się rozwiązania alternatywne, łatwiejsze w implementacji i utrzymaniu. Serwer aplikacyjny J2EE stał się jedynie zbędnym dodatkiem do środowiska uruchomieniowego udostępniającym wiele z usług, których użycie sprowadzało się do wykorzystania kontenera serwletów (jako łącznika ze światem zewnętrznym) i zarządcy transakcji. Reszta istniała bez większego wykorzystania.

"Wpływowych" projektów wolnodostępnych rewolucjonizującymi tworzenie aplikacji przemysłowych z wykorzystaniem języka Java było wiele. Na uwagę jednakże zasługują dwa: Hibernate oraz Spring Framework. Nie były doskonałe, ale ich wdrożenie nie nastręczało trudności i stosunkowo szybko reagowały na zmieniające się wymagania użytkowników (w przeciwieństwie do serwerów aplikacyjnych J2EE nie wspominając już samej specyfikacji J2EE). Oba uzupełniają się i tworzą kompletną platformę do tworzenia aplikacji przemysłowych poza ramami J2EE. Oba projekty były odpowiedzią na rosnącą komplikację i trudność w pomyślnym zastosowaniu specyfikacji J2EE (ponowie uwaga na skrót). I na szczęście oba wpłynęły na ostateczny wizerunek specyfikacji Java EE 5. Pierwszy z nich Hibernate był podwaliną dla powstania uproszczonej specyfikacji Enterprise JavaBeans (EJB) w wersji 3.0 w obszarze nazywanym Java Persistence, natomiast drugi Spring Framework upowszechnił stosowanie wzorca Wstrzeliwanie Zależności (ang. DI = Dependency Injection bądź IoC = Inversion Of Control). Oba korzystały z udogodnień wprowadzanych "u innych", aż wypracowano niepisany standard korzystania z rozwiązań specyfikacji JavaBeans (nie mylić ze specyfikacją Enterprise JavaBeans!) dostępnych bezpośrednio w języku Java do tworzenia aplikacji. I to bez konieczności wdrażania serwerów aplikacyjnych J2EE, które były trudne w administracji a ilość informacji do przyswojenia "wykańczała" niejeden zespół projektowy. Pojawił się termin POJO.

POJO (ang. Plain Old Java Object), czyli stary, dobry obiekt Javy jest klasą Javy ze zbiorem metod do ustawiania i pobierania właściwości obiektu (tzw. settery i gettery). Brak wymagania na dziedziczenie klas, czy implementację interfejsów szkieletu programistycznego (ang. framework) był strzałem w dziesiątkę. Krzywa uczenia znacznie się spłaszczyła, a zalet stosowania POJO nie trzeba było nikomu przedstawiać. Zacznijmy od najważniejszej - brak wplatania własnych klas w hierarchię klas wykorzystywanego szkieletu, np. tak jak to było na platformie J2EE. Brak konieczności realizacji interfejsów i klas pomocniczych uniezależnia aplikację od docelowego środowiska uruchomieniowego. Korzystanie z POJO pozwala na tworzenie aplikacji w zespołach, których jedynym tematem dyskusji jest realizacja wymagań biznesowych a nie wymagań tej czy innej platformy. Temat wdrożenia aplikacji na daną platformę uruchomieniową pozostawiany jest architektom i administratorom. Zespół programistów koncentruje się wyłącznie na aspektach biznesowych projektu a nie implementacyjnych docelowej platformy. Mimo tak oczywistych zalet istnieje jeszcze jedna, często tak oczywista, że się o niej po prostu zapomina. Każdy programista Java potrafi stworzyć POJO. Nie ma potrzeby wdrażać zespołów do stosowania POJO (chyba że, aby zastosować szkolenie jako terapię wstrząsową po wielomiesięcznych projektach J2EE). Technologia JavaBeans istniała od zawsze w Javie, przede wszystkim w arsenale programistów aplikacji graficznych, ale teraz powraca do łask twórców aplikacji przemysłowych. Istnienie rozwiązań opartych o POJO wymaga jednakże specjalizowanego środowiska, którym nie był serwer aplikacyjny J2EE (ponownie uwaga na skrót). Wymagano kontenerów IoC/DI, które wstrzeliwały zadeklarowane zależności. Można powiedzieć, że podobnie było w przypadku serwerów J2EE, gdzie deklarowało się zależność w deskryptorze instalacji, z tą jednak różnicą, że w kontenerze IoC wystarczy jedynie dostarczyć publiczny setter bądź odpowiedni konstruktor i otrzymuje się gotową do użycia zależność (bez jej wyszukiwania jak to miało miejsce w J2EE, np. wyszukiwanie komponentów EJB w JNDI). Wraz z wdrożeniem kontenera IoC i zastosowania POJO w projekcie, kod korzystający ze specyficznych rozwiązań środowiska zewnętrznego maleje. Operujemy wyłącznie na publicznym interfejsie, co dodatkowo go uelastycznia. Wynika to ze sposobu działania kontenera IoC - zadeklarowana zależność za pomocą anotacji (preferowany sposób), czy pliku konfiguracyjnego musi być dostępna zanim aplikacja zostanie w pełni uruchomiona. Druga zaleta to brak konieczności wyszukiwania zależności. To nie aplikacja ich szuka, ale kontener, który podaje je (wstrzeliwuje) podczas inicjalizacji aplikacji. Stąd bierze się określenie wzorca programowania - Odwórcenie kontroli znanego również jako Wstrzeliwanie zależności. Z nim związana jest zasada nazwana Hollywood Principle, które odzwierciedla istotę jego działania (jak i istnienia samego Hollywood): Don't call us we'll call you, co oznacza Nie dzwoń do nas, to my zadzwonimy do Ciebie. W kontenerze IoC nasza aplikacja deklaruje zależności i ich oczekuje. Wstrzeliwanie zależności odbywa się za pomocą konstruktora (podczas tworzenia obiektu jeden z parametrów jest zależnością), albo za pomocą metody-settera (po stworzeniu obiektu wywoływany jest setter zanim nastąpi wywołanie innych metod biznesowych). Zalety stosowania zasady Hollywood (m.in. w Spring Framework) dostrzeżono w grupie standaryzującej platformę Java EE 5 i wdrożono ją m.in. w specyfikacji JavaServer Faces (nowość w specyfikacji Java EE, gdzie - obok Java Servlets i JavaServer Pages - jest kolejnym szkielet do budowania aplikacji internetowych, aczkolwiek specyfikacja wykracza poza zakres środowiska aplikacji internetowych), czy Enterprise JavaBeans (komponenty biznesowe reprezentujące procesy biznesowe, w tym i trwałe dane aplikacji przy współpracy innych usług serwera aplikacyjnego JEE, m.in. usługi bezpieczeństwa, czy monitorowania transakcji).

Kolejną innowacją w środowisku Java EE 5, która wspaniala spisywaly się na rynku aplikacji pisanych w języku Java na długo zanim sfinalizowano specyfikację JEE było uproszczenie zarządzania trwałymi danymi. Prym w tej dziedzinie wiódł projekt Hibernate (rozwiązanie należące do rodziny JBoss Enterprise Middleware Suite (JEMS)). Prostota dostępu i modyfikacji danymi trwałymi przechowywanymi w relacyjnych bazach danych za pomocą Hibernate była wprost porażająca w porównaniu z wykorzystaniem niezwykle skomplikowanego rozwiązania Enterprise JavaBeans 2.1, a szczególnie jej części dotyczącej dostępu do danych - Container-Managed Persistence (CMP) entity beans. Komponenty CMP są komponentami biznesowymi, które odpowiadają za obiektową reprezentację relacyjnych danych. Zastosowanie J2EE (uwaga na skrót) wymagało serwera aplikacyjnego, gdzie otrzymywano skomplikowany mechanizm dostępu do relacyjnych danych podczas, gdy Hibernate nie narzucał takich obostrzeń i mógł być stosowany i w aplikacjach desktopowych i przemysłowych, czyli wszędzie. Jedynym zauważalnym problemem było poznanie produktu Hibernate (poza , które nie było trywialne, ale alternatywą było stosowanie jeszcze bardziej nietrywialnych komponentów CMP (rozpatrując jedynie Hibernate i J2EE) i to wyłącznie w pewnych zastosowaniach - wyłącznie w rozwiązaniach przemysłowych. W aplikacjach desktopowych realizację dostępu do bazy danych dostarczał standardowy mechanizm JDBC, który jednak był zbyt niskopoziomowy w stosunku do możliwości Hibernate. Te i inne dobrodziejstwa Hibernate spowodowały odejście programistów Java od stosowania kolejnego ustandaryzowanego rozwiązania J2EE. Hibernate, podobnie jak Spring Framework, korzysta z POJO, aby "przenosić" dane obiektowe z i do relacyjnej bazy danych. Oba rozwiązania współpracują ze sobą, co stanowiło poważną alternatywę dla platformy J2EE. Z wprowadzeniem specyfikacji JEE (uwaga - zmiana akronimu) wielu programistów zapewne ponownie rozpatrzy zastosowanie Spring Framework + Hibernate versus platforma Java EE 5 szczególnie, że Hibernate realizuje specyfikację Java Persistence i może występować w roli dostawcy usługi. Byłoby niestosownym nie wspomnieć o możliwości uruchomienia implementacji Java Persistence poza środowiskiem Java EE, więc nasze aplikacje staną się jeszcze bardziej modularne i jedynie jednym z wielu implementacji (a nie jedyną) stanie się Hibernate. Spodziewam się przechylenia szali zainteresowania ze stosowania Hibernate na rzecz ustandaryzowanych rozwiązań realizujących specyfikację Java Persistence, którą pokreślam jedną z wielu jest Hibernate.

Nie sądzę, aby pojawienie się serwerów zgodnych ze specyfikacją Java EE 5 spowodowało znaczny spadek zainteresowania rozwiązaniem Spring Framework + Hibernate, czy podobnych. Sądzę jednak, że najnowsza odsłona platformy Java EE 5 stanowi interesującą alternatywę dla tego zestawienia. Wykorzystanie w JEE konstrukcji dostępnych bezpośrednio w języku Java do budowania aplikacji przemysłowych znacznie zmniejszy wymagania początkowe dla poznania specyfikacji Java EE 5 i znacząco zwiększy zainteresowanie platformą programistów innych rozwiązań, a nawet języków programowania. Taki stan rzeczy musi przełożyć się na jeszcze większą konkurencyjność projektów wolnodostępnych i ich komercyjnych odpowiedników. Nadszedł czas na zapoznanie się z Java EE 5 w praktyce.

07 kwietnia 2006

Podstawowe pojęcia Maven2: sekwencje, etapy, wtyczki i zadania

4 komentarzy
Tworzenie oprogramowania zawsze wiąże się z tymi samymi etapami (phases). Nie ma to związku z zastosowanymi technologiami i sprowadza się do wykonywania ustalonych sekwencji (lifecycle), aż do osiągnięcia zamierzonego celu - finalnego uruchomienia aplikacji w środowisku produkcyjnym u klienta. Zanim jednak to nastąpi, niezliczone ilości razy wykonujemy te same etapy przy wsparciu dedykowanych narzędzi (plugins), które składają się z funkcjonalności wspomagających wykonanie zadań (goals) w określonych etapach tworzenia oprogramowania. Jedną z sekwencji mogłaby być: generowanie kodu pomocniczego (nie mylić z samym programowaniem, który jest zadaniem niezastępowalnym przez narzędzia i musi być wykonane przez człowieka), kompilacja, testowanie jednostkowe, przygotowanie wersji dystrybucyjnej, testowanie integracyjne, instalacja i ostatecznie uruchomienie (nie wyróżniam samego etapu programowania, który według mnie jest nie do obsłużenia przez żadne narzędzia, tzn. któryś z etapów wymaga nasz interwencji, czy to jest modelowanie, czy samo programowanie, czy inna czynność inicjująca proces tworzenia oprogramowania). Oczywiście wyróżnić można mniej złożone sekwencje wykonywane przez pojedyńczego członka zespołu, jak np. generowanie kodu pomocniczego, kompilacja i testowanie jednostkowe, wykonywane częściej niż inne bardziej złożone. Pozostałymi etapami zainteresowani mogliby być inni członkowie zespołu, np. testerzy czy wdrożeniowcy. W ten sposób określamy pewien z góry ustalony schemat wytwarzania oprogramowania bez względu na złożoność technologiczną projektu informatycznego i wszystkie inne projekty są jedynie jego kopią (czasami Różnica może polegać na długości czasu w jakim wymienione etapy są wykonywane. Mimo, że najczęściej spotykam się z komentarzami, że to czas jest główną przyczyną porażki wielu projektów informatycznych, to uważam, że główny ciężar odpowiedzialności spada na zastosowane narzędzia, którymi posługują się członkowie zespołu. Obecnie jestem przekonany, że brak wykorzystania pewnych narzędzi jest wyraźnym sygnałem o marnotrastwie czasu. Jednym z takich narzędzi jest z pewnością Maven2. Przyjrzyjmy się dlaczego.

Czytając początkowe zdania artykułu, Twoją uwagę mogły przykuć angielskie terminy w nawiasach - lifecycle, phase, plugin oraz goal. Wplotłem je świadomie, ponieważ reprezentują podstawowe pojęcia Maven2, które odpowiadają pojęciom projektowym - sekwencja (również cykl), etap, wtyczka (również narzędzie) oraz zadanie, odpowiednio. Zrozumienie działania Maven2 to zrozumienie istoty tych pojęć. Usystematyzowanie działań podczas tworzenia oprogramowania, wprowadzone wdrożeniem Maven2, ma zauważalnie pozytywny efekt na kondycję i postępy projeku. Pozwala skorzystać z dobrych praktyk projektowych i programistycznych bez dodatkowych nakładów czasowych oraz finansowych na ich dogłębne poznanie. Prostota korzystania z Maven2 ma niebagatelne znaczenie do osiągnięcia zamierzonego celu - ukończenia projektu na czas z jakością tworzenia "z górnej półki". I wszystko za darmo (pomijając aspekt czasu, który wierzę, że dzięki temu artykułowi spadnie do niezbędnego minimum).

Jako narzędzie pretendujące do miana narzędzia wspierającego zarządzanie projektem informatycznym, Maven2 dostarcza 3 predefiniowanie sekwencje projektowe: default, clean oraz site. Każda sekwencja składa się z etapów. Ilość etapów jest specyficzna dla sekwencji, ale istnieje możliwość ich zmiany. Poniższa tabela przedstawia nazwy etapów dla predefiniowanych sekwencji projektowych w Maven2.

Sekwencja
Etap
default
validate
generate-sources
process-sources
generate-resources
process-resources
compile
process-classes
generate-test-sources
process-test-sources
generate-test-resources
process-test-resources
test-compile
test
package
pre-integration-test
integration-test
post-integration-test
verify
install
deploy
clean
pre-clean
clean
post-clean
site
pre-site
site
post-site
site-deploy

Z sekwencją i poszczególnym jej etapem związane są pewne wtyczki i ich zadania (aka czynności). Jednym z bardzo trafnych stwierdzeń z jakim się spotkałem to określenie Maven jako środowiska do uruchamiania wtyczek, które z kolei dostarczają zadań (porównać to można do wirtualnej maszyny Javy, która jest środowiskiem uruchomieniowym klas, bądź też do kontenera serwletów, które jest środowiskiem dla serwletów). Etap może nie posiadać przypisanego zadania wtyczki, albo może istnieć ich więcej. Istnieje dodatkowo możliwość przypisania zadania wtyczki do etapu sekwencji w konfiguracji projektu (pom.xml) za pomocą znacznika execution (który będzie tematem przewodnim kolejnego artykułu). Sam typ projektu (wartość znacznika packaging, tj. pom, jar, maven-plugin, ejb, war, ear, rar, par, ejb3) również wyznacza określone wtyczki i ich zadania do poszczególnych etapów. Poniższa tabela przedstawia nazwy etapów dla wspomnianych typów packaging.

packagingsekwencjaetapwtyczka:zadanie
pomdefaultpackageorg.apache.maven.plugins:maven-site-plugin:attach-descriptor


installorg.apache.maven.plugins:maven-install-plugin:install


deployorg.apache.maven.plugins:maven-deploy-plugin:deploy
jardefaultprocess-resourcesorg.apache.maven.plugins:maven-resources-plugin:resources


compileorg.apache.maven.plugins:maven-compiler-plugin:compile


process-test-resourcesorg.apache.maven.plugins:maven-resources-plugin:testResources


test-compileorg.apache.maven.plugins:maven-compiler-plugin:testCompile


testorg.apache.maven.plugins:maven-surefire-plugin:test


packageorg.apache.maven.plugins:maven-jar-plugin:jar


installorg.apache.maven.plugins:maven-install-plugin:install


deployorg.apache.maven.plugins:maven-deploy-plugin:deploy
maven-plugindefaultgenerate-resourcesorg.apache.maven.plugins:maven-plugin-plugin:descriptor


process-resourcesorg.apache.maven.plugins:maven-resources-plugin:resources


compileorg.apache.maven.plugins:maven-compiler-plugin:compile


process-test-resourcesorg.apache.maven.plugins:maven-resources-plugin:testResources


test-compileorg.apache.maven.plugins:maven-compiler-plugin:testCompile


testorg.apache.maven.plugins:maven-surefire-plugin:test


package
org.apache.maven.plugins:maven-jar-plugin:jar
org.apache.maven.plugins:maven-plugin-plugin:addPluginArtifactMetadata


install
org.apache.maven.plugins:maven-install-plugin:install
org.apache.maven.plugins:maven-plugin-plugin:updateRegistry


deployorg.apache.maven.plugins:maven-deploy-plugin:deploy
ejbdefaultprocess-resourcesorg.apache.maven.plugins:maven-resources-plugin:resources


compileorg.apache.maven.plugins:maven-compiler-plugin:compile


process-test-resourcesorg.apache.maven.plugins:maven-resources-plugin:testResources


test-compileorg.apache.maven.plugins:maven-compiler-plugin:testCompile


testorg.apache.maven.plugins:maven-surefire-plugin:test


packageorg.apache.maven.plugins:maven-ejb-plugin:ejb


installorg.apache.maven.plugins:maven-install-plugin:install


deployorg.apache.maven.plugins:maven-deploy-plugin:deploy
wardefaultprocess-resourcesorg.apache.maven.plugins:maven-resources-plugin:resources


compileorg.apache.maven.plugins:maven-compiler-plugin:compile


process-test-resourcesorg.apache.maven.plugins:maven-resources-plugin:testResources


test-compileorg.apache.maven.plugins:maven-compiler-plugin:testCompile


testorg.apache.maven.plugins:maven-surefire-plugin:test


packageorg.apache.maven.plugins:maven-war-plugin:war


installorg.apache.maven.plugins:maven-install-plugin:install


deployorg.apache.maven.plugins:maven-deploy-plugin:deploy
eardefaultgenerateresourcesorg.apache.maven.plugins:maven-ear-plugin:generate-application-xml


process-resourcesorg.apache.maven.plugins:maven-resources-plugin:resources


packageorg.apache.maven.plugins:maven-ear-plugin:ear


installorg.apache.maven.plugins:maven-install-plugin:install


deployorg.apache.maven.plugins:maven-deploy-plugin:deploy
rardefaultprocess-resourcesorg.apache.maven.plugins:maven-resources-plugin:resources


compileorg.apache.maven.plugins:maven-compiler-plugin:compile


process-test-resourcesorg.apache.maven.plugins:maven-resources-plugin:testResources


test-compileorg.apache.maven.plugins:maven-compiler-plugin:testCompile


testorg.apache.maven.plugins:maven-surefire-plugin:test


packageorg.apache.maven.plugins:maven-rar-plugin:rar


installorg.apache.maven.plugins:maven-install-plugin:install


deployorg.apache.maven.plugins:maven-deploy-plugin:deploy
pardefaultprocess-resourcesorg.apache.maven.plugins:maven-resources-plugin:resources


compileorg.apache.maven.plugins:maven-compiler-plugin:compile


process-test-resourcesorg.apache.maven.plugins:maven-resources-plugin:testResources


test-compileorg.apache.maven.plugins:maven-compiler-plugin:testCompile


testorg.apache.maven.plugins:maven-surefire-plugin:test


packageorg.apache.maven.plugins:maven-par-plugin:par


installorg.apache.maven.plugins:maven-install-plugin:install


deployorg.apache.maven.plugins:maven-deploy-plugin:deploy
ejb3defaultprocessresourcesorg.apache.maven.plugins:maven-resources-plugin:resources


compileorg.apache.maven.plugins:maven-compiler-plugin:compile


process-testresourcesorg.apache.maven.plugins:maven-resources-plugin:testResources


test-compileorg.apache.maven.plugins:maven-compiler-plugin:testCompile


testorg.apache.maven.plugins:maven-surefire-plugin:test


packageorg.apache.maven.plugins:maven-ejb3-plugin:ejb3


installorg.apache.maven.plugins:maven-install-plugin:install


deployorg.apache.maven.plugins:maven-deploy-plugin:deploy

Uruchomienie Maven2 odbywa się na 2 podstawowe sposoby, badź ich kombinacje. Pierwszy z nich polega na uruchomieniu polecenia mvn z wyspecyfikowanym etapem. Maven2 wymaga, aby nazwy etapów były unikatowe, co jednoznacznie określa sekwencję i poszczególne etapy poprzedzające ten zadany. Znajomość etapów określa wtyczki i ich zadania, które zostaną wykonane zgodnie z kolejnością etapów w sekwencji. Pamiętajmy, że istotne znacznie ma konfiguracja projektu (pom.xml) w sekcji packaging i executions (temat kolejnego artykułu). Obie sekcje wyznaczają ostateczną listę wtyczek i zadań do wykonania, a ich brak w pom.xml wskazuje na predefiniowaną konfigurację Maven2 przedstawioną w tabelach powyżej.

Rozpoczynając pracę, Maven2 rozpoczyna od wyznaczenia sekwencji i jej etapów. Następnie mapuje je na odpowiednie zadania wtyczek (w tym celu odczytuje predefiniowaną konfigurację sekwencji i nakłada na to definicję etapów związaną z packaging i executions) i ostatecznie wykonuje zadania zgodnie z ich kolejnością wyznaczoną przez etapy w sekwencji. Kolejność wykonywania zadań można prześledzić podczas uruchomienia mvn, gdzie każda wtyczka i jej zadanie wypisywane są w schemacie [INFO] [wtyczka:zadanie], np. [INFO] [jar:jar]. Poza wyspecyfikowaniem etapu istnieje również możliwość uruchomienia Maven2 z konkretnym zadaniem wtyczki. Definicja domyślnie wykonywanego zadania bądź etapu w projekcie może również zostać wyspecyfikowana w sekcji <build> jako <defaultGoal> w pom.xml. Jeśli nie podano etapu bądź zadania na linii poleceń, Maven2 oczekuje wspomnianej konfiguracji projektu w sekcji <defaultGoal>.

Przyjrzyjmy się kilku przykładom dla zobrazowania przedstawionego materiału. Zakłada się, że wykonanie poniższych poleceń odbywa się w projekcie obsługiwanym przez Maven2 (innymi słowy istnieje plik pom.xml).
  1. Uruchamiamy polecenie mvn bez wyspecyfikowanego etapu bądź zadania wtyczki.
    $ mvn
    [INFO] Scanning for projects...
    [INFO] ----------------------------------------------------------------------------
    [ERROR] BUILD FAILURE
    [INFO] ----------------------------------------------------------------------------
    [INFO] You must specify at least one goal. Try 'install'
    [INFO] ----------------------------------------------------------------------------
    ...
    Osobiście uważam, że komunikat jest mylący, gdyż install nie jest zadaniem (goal), ale etapem (phase) w sekwencji (lifecycle) default. Należy zatem rozumieć ten komunikat jako wskazanie do wykonania wszystkich zadań przypisanych do etapów w sekwencji default, począwszy od pierwszego a skończywszy na install.

    Sprawdźmy, czy istnieje Polskie tłumaczenie, które może być bardziej adekwatne.
    $ mvn -Duser.language=pl_PL
    [INFO] Scanning for projects...
    [INFO] ----------------------------------------------------------------------------
    [ERROR] BUILD FAILURE
    [INFO] ----------------------------------------------------------------------------
    [INFO] You must specify at least one goal. Try 'install'
    [INFO] ----------------------------------------------------------------------------
    ...
    Niestety w tym przypadku takie tłumaczenie nie istnieje i uruchomienie z opcją -e również nie dostarcza więcej informacji. Niektóre jednak komentarze Maven2 zostały spolonizowane.

  2. Uruchommy polecenie mvn z nazwą nieistniejącego etapu, np. etap.
    $ mvn etap
    [INFO] Scanning for projects...
    [INFO] ----------------------------------------------------------------------------
    [ERROR] BUILD FAILURE
    [INFO] ----------------------------------------------------------------------------
    [INFO] Invalid task 'etap': you must specify a valid lifecycle phase, or a goal in the format plugin:goal or pluginGroupId:pluginArtifactId:pluginVersion:goal
    [INFO] ----------------------------------------------------------------------------
    ...
    Komunikat jest tym razem bardziej dokładny i dowiadujemy się, że konieczne jest podanie istniejącego etapu bądź zadania w odpowiednim formacie (temat wtyczek będzie tematem jednego z kolejnych artykułów).

  3. Uruchommy polecenie mvn z poprawnym etapem w sekwencji default, np. test.
    $ mvn test
    [INFO] Scanning for projects...
    [INFO] ----------------------------------------------------------------------------
    [INFO] Building Projekt Demonstracyjny Maven2
    [INFO] task-segment: [test]
    [INFO] ----------------------------------------------------------------------------
    [INFO] [resources:resources]
    ...
    [INFO] [compiler:compile]
    ...
    [INFO] [resources:testResources]
    ...
    [INFO] [compiler:testCompile]
    ...
    [INFO] [surefire:test]
    ...
    [INFO] ----------------------------------------------------------------------------
    [INFO] BUILD SUCCESSFUL
    [INFO] ----------------------------------------------------------------------------
    ...
    Jak można zauważyć wykonanie etapu test zakończy się pomyślnie, a w trakcie wykonywania kolejnych etapów zostaną wykonane zadania wtyczek przypisanych do nich.

  4. Uruchommy powyższe polecenie ponownie zmodyfikowawszy wartość packaging w pom.xml, np. maven-plugin.
    $ mvn test
    [INFO] Scanning for projects...
    [INFO] ----------------------------------------------------------------------------
    [INFO] Building Projekt Demonstracyjny Maven2
    [INFO] task-segment: [test]
    [INFO] ----------------------------------------------------------------------------
    ...
    [INFO] [plugin:descriptor]
    ...
    [INFO] [resources:resources]
    ...
    [INFO] [compiler:compile]
    ...
    [INFO] [resources:testResources]
    ...
    [INFO] [compiler:testCompile]
    ...
    [INFO] [surefire:test]
    ...
    [INFO] ----------------------------------------------------------------------------
    [INFO] BUILD SUCCESSFUL
    [INFO] ----------------------------------------------------------------------------
    Tym razem wykonane zadania różnią się o początkową wtyczkę plugin. Jest to potwierdzenie wpływu konfiguracji sekcji packaging na listę zadań do wykonania.

  5. Uruchommy polecenie mvn z podaniem wtyczki surefire bez wskazania jej zadania.
    $ mvn surefire
    [INFO] Scanning for projects...
    [INFO] ----------------------------------------------------------------------------
    [ERROR] BUILD FAILURE
    [INFO] ----------------------------------------------------------------------------
    [INFO] Invalid task 'surefire': you must specify a valid lifecycle phase, or a goal in the format plugin:goal or pluginGroupId:pluginArtifactId:pluginVersion:goal
    [INFO] ----------------------------------------------------------------------------
    [INFO] For more information, run Maven with the -e switch
    [INFO] ----------------------------------------------------------------------------
    Zgodnie z komunikatem parametr bez dwukropka traktowany jest jako nazwa etapu, a skoro taki nie istnieje Maven2 kończy działanie z komunikatem błędu.

  6. Uruchommy polecenie mvn z wyspecyfikowaną wtyczką surefire z zadanym zadaniem, np. test, który mogliśmy zauważyć uruchamiany we wcześniejszych przykładach (wraz z innymi poprzedzającymi zadaniami).
    $ mvn surefire:test
    [INFO] Scanning for projects...
    [INFO] Searching repository for plugin with prefix: 'surefire'.
    [INFO] ----------------------------------------------------------------------------
    [INFO] Building Projekt Demonstracyjny Maven2
    [INFO] task-segment: [surefire:test]
    [INFO] ----------------------------------------------------------------------------
    [INFO] [surefire:test]
    ...
    [INFO] ----------------------------------------------------------------------------
    [INFO] BUILD SUCCESSFUL
    [INFO] ----------------------------------------------------------------------------
    Na uwagę zasługuje fakt, że wykonanie wtyczki nie zostało poprzedzone wykonaniem żadnego innego zadania, co pozwala na uruchomienie wtyczek pojedyńczo. Jest to szczególnie przydatne z wtyczkami, które nie są przypisane do żadnej sekwencji.

  7. Na koniec uruchommy polecenie mvn z 2 etapami należącymi do różnych sekwencji oraz zadanie wtyczki, której wykonanie jest częścią uruchamiania etapów.
    $ mvn clean package surefire:test
    [INFO] Scanning for projects...
    [INFO] Searching repository for plugin with prefix: 'surefire'.
    [INFO] ----------------------------------------------------------------------------
    [INFO] Building Projekt Demonstracyjny Maven2
    [INFO] task-segment: [clean, package, surefire:test]
    [INFO] ----------------------------------------------------------------------------
    [INFO] [clean:clean]
    ...
    [INFO] [plugin:descriptor]
    ...
    [INFO] [resources:resources]
    ...
    [INFO] [compiler:compile]
    ...
    [INFO] [resources:testResources]
    ...
    [INFO] [compiler:testCompile]
    ...
    [INFO] [surefire:test]
    ...
    [INFO] [jar:jar]
    ...
    [INFO] [plugin:addPluginArtifactMetadata]
    [INFO] [surefire:test]
    ...
    [INFO] ----------------------------------------------------------------------------
    [INFO] BUILD SUCCESSFUL
    [INFO] ----------------------------------------------------------------------------
    Wszystkie etapy oraz zadanie zostały poprawnie rozpoznane przez Maven2. Wykonanie etapów wiąże się z wyznaczeniem wtyczek i ich zadań, a następnie uruchomieniu zgodnie z kolejnością związanych z nimi etapów. Wyjątek stanowi wykonanie zadania surefire:test, który jest wykonany zgodnie z jego pozycją na linii poleceń, czyli po pomyślnym uruchomieniu zadań związanych z etapami.

Znajomość wtyczek i dostarczanych przez nie zadań jest istotnym elementem zrozumienia działania Apache Maven. Wersja druga projektu - Maven2 - wprowadza kolejne pojęcia - sekwencja (lifecycle) oraz etap (phase). Wyróżniamy 3 predefiniowane sekwencje z zadanymi etapami, do których przypisane są zadania wtyczek. Dzięki takiemu podejściu projekty mogą skorzystać z dobrych praktyk inżynierskich podnoszących jakość projektu niewielkim kosztem. Znajomość Maven2 i jego konfiguracji oraz umiejętność zrozumienia zawartości pom.xml pozwala na łatwe wdrożenie nowych osób do projektu z punktu widzenia jego zarządzania. Poza samym tworzeniem elementów składowych projektu - klasy, konfiguracje, modele, etc. - jest to kluczowy składnik wpływający na czasowe zakończenie projektu. Znosi to również konieczność wprowadzania tych samych ustawień z projektu na projekt (nawet mimo możliwości ich kopiowania, które zazwyczaj kończy się błędami trudnymi do zdiagnozowania). Temat jest stosunkowo zawiły, ale sądzę, że lektura artykułu pozwoli na łatwiejsze zrozumienie działania Maven2.

ADNOTACJA: Nie spotkałem się jeszcze z polskimi odpowiednikami pojęć - lifecycle, phase oraz goal w kontekście Maven, więc pokusiłem się na ich własne spolszczenie. Jeśli znasz lepsze dla nich polskie tłumaczenia, koniecznie się ze mną skontaktuj. Jako rekompensatę mogę jedynie obiecać opublikowanie imienia i nazwiska wraz z ciekawymi propozycjami w kolejnych artykułach.

Komentarze, spostrzeżenia mile widziane. Skorzystaj z możliwości komentowania artykułu, poniżej, albo wyślij komentarz na moją skrzynkę.

14 lutego 2006

XDoclet2 + Maven2 - wdrożenie wtyczki do generowania plików mapowania Hibernate

9 komentarzy
XDoclet2 jest nowym wcieleniem szeroko rozpowszechnionego projektu XDoclet. Celem projektu jest uproszczenie zarządzania plikami pomocniczymi aplikacji (np. deskryptory Java EE, czy pliki mapowania Hibernate), które są tworzone za pomocą wtyczek (rozszeżeń) uruchamianych podczas procesu budowania na podstawie danych zawartych w kodzie źródłowym. XDoclet2 jest jedynie motorem uruchamiania wtyczek i dostarcza środowiska do ich poprawnego działania. Właściwa praca związana z utworzeniem plików pomocniczych leży w gestii wtyczek.

Działanie XDoclet2 polega na analizie kodu źródłowego i generowaniu plików pomocniczych na podstawie specjalnych znaczników (ang. tags) JavaDoc umieszczonych w komentarzu. Jest on identyczny w sposobie działania do JavaDoc, który udostępnia mechanizm dokumentowania kodu źródłowego za pomocą specjalnych znaczników (np. @see, @version, @return). W ten sposób wszystkie pliki dodatkowe (wspierające uruchamianie aplikacji) są generowane automatycznie przez narzędzie - XDoclet2, na podstawie analizy klasy/interfejsu, które dostarczają
aktualnych informacji - ich nazwy, nazwy i typów pól i metod, etc. wraz pomocniczymi informacjami umieszczonymi w komentarzu.

Lista wszystkich dostępnych wtyczek XDoclet2 i wspieranych przez nie znaczników znajduje się pod adresem http://xdoclet.codehaus.org/XDoclet+Plugins.

Zaletą wdrożenia XDoclet2 to przede wszystkim ujednolicone zarządzanie danymi źródłowymi w klasie/interfejsie, której te dane dotyczą. Gwarantuje to wiekszą ich aktualność i likwiduje konieczność utrzymywania zewnętrznych plików pomocniczych. Skrócenie czasu tworzenia aplikacji i zmniejszenie ryzyka wystąpienia niespójnych danych to główne cechy wdrożenia XDoclet2, a możliwość uruchamiania z poziomu Maven2 to kolejny zysk w postaci skrócenia kosztów koniecznym do realizacji projektu.

Podobnie jak znaczniki JavaDoc, znaczniki XDoclet2, są umieszczane w komentarzu klasy, interfejsu, pól czy metod, dostarczając dodatkowych informacji, na podstawie, których wtyczki XDoclet2 generują pliki pomocnicze. Sukces JavaDoc był inspiracją dla twórców XDoclet2 (poprzednio XDoclet), który następnie zainspirował osoby koordynujące rozwojem Java SE, co ostatecznie zakończyło się opracowaniem specyfikacji JSR 175: A Metadata Facility for the JavaTM Programming Language, czyli sposobu dostarczania dodatkowych informacji (metadanych) dla klas, interfejsów, pól i metod za pomocą znaczników. Proces opisywania kodu za pomocą znaczników XDoclet2 nazywamy Attribute-Oriented Programming (nie mylić z AOP - Aspect-Oriented Programming, które jest odmienną techniką tworzenia oprogramowania opartą o aspekty - funkcjonalności wplatane w aplikację przed lub podczas jej uruchamiania). Wdrożenie JSR-175 do ostatniej, produkcyjnej wersji standardowej Javy - Java SE 5.0 - wprowadza nowe pojęcie annotacji (ang. annotations) bądź metadanych (ang. metadata), a sama technika opisywania została nazwana Java Annotations.

Podobnie jak wolnodostępny projekt Apache log4j miał duży wpływ na stworzenie standardowego mechanizmu zapisywania zdarzeń w Javie - Java Logging API - tak projekt XDoclet2 miał wpływ na stworzenie standardowego mechanizmu Java Annotations. Jest to przykład wpływu projektów wolnodostępnych na rozwój języka Java.

Generowanie plików mapowania Hibernate za pomocą XDoclet2

Celem XDoclet2 jest automatyczne tworzenie plików pomocniczych aplikacji. Wykorzystajmy go do utworzenia plików mapowania dla Hibernate podczas budowania aplikacji. Do automatyzacji zadań wykonywanych w projekcie wykorzystujemy projekt Apache Maven 2. Kwestią do rozwiązania pozostaje zatem zestawienie środowiska, aby wywołując proces budowania aplikacji, odpowiednie pliki zostały utworzone za pomocą wtyczki Hibernate dla XDoclet2.

W poprzednim artykule Zarządzanie projektem za pomocą Apache Maven 2 przedstawiłem sposób działania Maven2 oraz
sposób utworzenia przykładowego projektu za jego pomocą. Stworzenie projektu to wywołanie archetype:create
mvn archetype:create -DgroupId=pl.net.laskowski -DartifactId=aplikacja
W tym artykule rozszeżymy konfigurację Maven2 o wywołanie wtyczki Hibernate dla XDoclet2 podczas budowania naszej przykładowej aplikacji. Otwieramy projekt w wybranym środowisku programistycznym i rozpoczynamy edycję koniecznych plików. Otwarcie projektu opartego o Maven2 w NetBeans wymaga zainstalowania wtyczki Mevenide for NetBeans, natomiast w Eclipse z pomocą przychodzi nam polecenie:
mvn eclipse:eclipse
uruchomione w katalogu głównym projektu (u nas aplikacja), a następnie jego import.

Sposób działania Hibernate i przykładowa aplikacja oparta o niego przedstawiona została w innym artykule Hibernate - tniemy koszty dostępu do danych relacyjnych.

Nasze wdrożenie rozpoczynamy od udekorowania klasy pl.net.laskowski.User z w/w artykułu o Hibernate znacznikami wtyczki Hibernate dla XDoclet2. Klasę należy zapisać w katalogu src/main/java/pl/net/laskowski.
package pl.net.laskowski;

/**
* @hibernate.class table"User"
*/
public final class User {

/**
* @hibernate.id generator-class="native"
*/
private int id;

/**
* @hibernate.property column="T_IMIE" length="10" not-null="true"
*/
private String imie;

/**
* @hibernate.property column="T_NAZWISKO" length="25" not-null="true"
*/
private String nazwisko;

public int getId() {
return id;
}

public void setId(int id) {
this.id = id;
}

public String getImie() {
return imie;
}

public void setImie(String imie) {
this.imie = imie;
}

public String getNazwisko() {
return nazwisko;
}

public void setNazwisko(String nazwisko) {
this.nazwisko = nazwisko;
}
}
Kluczem w zrozumieniu działania XDoclet2 jest poznanie znaczników, które instruują uruchamianą wtyczkę. W naszym przykładzie korzystamy z wtyczki Hibernate. Znaczenie poszczególnych znaczników jest specyficzne dla wtyczki i w przypadku generowania plików XML odpowiadają nazwą odpowiednim elementom i atrybutom pliku docelowego w XML. Wszystkie nazwy znaczników należące do pojedyńczej wtyczki poprzedzone są tym samym prefiksem, np. hibernate w przypadku wtyczki Hibernate. Lista dostępnych wtyczek i ich znaczniki znajduje się na stronie XDoclet Plugins.

Kolejnym krokiem wdrożenia XDoclet2 do projektu jest konfiguracja Maven2. Sprowadza się to do edycji pliku pom.xml. W sekcji build definiujemy wtyczkę XDoclet2 dla Maven2 - maven2-xdoclet2-plugin (tak, nie jest to pomyłka - podobnie jak XDoclet2, Maven2 jest jedynie motorem wtyczek i wymaga wskazania, które wtyczki wykonać). Obecna wersja wtyczki nie jest skonfigurowana jako projekt Maven2, więc wykorzystanie przechodności zależności (ang. dependencies) Maven2 nie jest możliwe i konieczne jest ich zdefiniowanie wprost. Dodatkowo, w sekcji config wskazujemy na wtyczkę XDoclet2 do uruchomienia, tj. org.xdoclet.plugin.hibernate.HibernateMappingPlugin. Wartości parametrów wtyczki Hibernate deklarujemy w sekcji params. Kolejną modyfikacją pom.xml związaną z wdrożeniem XDoclet2 to zdefiniowanie dodatkowego repozytorium wtyczek XDoclet2 w sekcji pluginRepository. Oczywiście skoro nasz projekt korzysta z Hibernate musimy zdefiniować to jako zależność projektu w głównej sekcji dependencies (nie mylić z podobną sekcją w ramach definicji wtyczki).
Dla uproszczenia uruchamiania aplikacji, plik pom.xml konfiguruje klasę startową pl.net.laskowski.HibernateExample jako część definicji wtyczki maven-jar-plugin. Kończymy edycję pom.xml konfiguracją wtyczki maven-assembly-plugin, która utworzy dystrybucję naszego oprogramowania ze wszystkimi niezbędnymi bibliotekami (zależnościami).

Ostatecznie, kompletny plik pom.xml naszego przykładowego projektu przedstawia się następująco:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>pl.net.laskowski</groupId>
<artifactId>aplikacja</artifactId>
<packaging>jar</packaging>
<version>1.0-SNAPSHOT</version>
<name>Maven Quick Start Archetype</name>
<url>http://maven.apache.org</url>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>3.8.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate</artifactId>
<version>3.1.2</version>
</dependency>
<dependency>
<groupId>hsqldb</groupId>
<artifactId>hsqldb</artifactId>
<version>1.8.0.1</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>xdoclet</groupId>
<artifactId>maven2-xdoclet2-plugin</artifactId>
<dependencies>
<dependency>
<groupId>xdoclet-plugins</groupId>
<artifactId>xdoclet-plugin-hibernate</artifactId>
<version>1.0.3</version>
</dependency>
<dependency>
<groupId>xdoclet-plugins</groupId>
<artifactId>xdoclet-plugin-qtags</artifactId>
<version>1.0.3</version>
</dependency>
<dependency>
<groupId>xdoclet-plugins</groupId>
<artifactId>xdoclet-plugin-plugin</artifactId>
<version>1.0.3</version>
</dependency>
</dependencies>
<configuration>
<configs>
<config>
<plugin>org.xdoclet.plugin.hibernate.HibernateMappingPlugin</plugin>
<params>
<destdir>${project.build.outputDirectory}</destdir>
<version>3.0</version>
</params>
</config>
</configs>
</configuration>
<executions>
<execution>
<goals>
<goal>xdoclet</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<manifest>
<addClasspath>true</addClasspath>
<mainClass>pl.net.laskowski.HibernateExample</mainClass>
</manifest>
</archive>
</configuration>
</plugin>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<version>2.0.1</version>
<configuration>
<finalName>${artifactId}</finalName>
<descriptors>
<descriptor>src/main/assembly/dist.xml</descriptor>
</descriptors>
</configuration>
</plugin>
</plugins>
</build>
<pluginRepositories>
<pluginRepository>
<id>codehaus-plugins</id>
<url>http://dist.codehaus.org/</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
<releases>
<enabled>true</enabled>
</releases>
</pluginRepository>
</pluginRepositories>
</project>
Pozostaje utworzenie kilku plików pomocniczych projektu - pliku src/main/resources/hibernate.cfg.xml będącego konfiguracją Hibernate dla naszej aplikacji
<?xml version='1.0' encoding='utf-8'?>
<!DOCTYPE hibernate-configuration PUBLIC "-//Hibernate/Hibernate Configuration DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
<session-factory>
<property name="connection.driver_class">org.hsqldb.jdbcDriver</property>
<property name="connection.url">jdbc:hsqldb:.</property>
<property name="connection.username">sa</property>
<property name="connection.password"></property>
<property name="connection.pool_size">1</property>
<property name="dialect">org.hibernate.dialect.HSQLDialect</property>
<property name="current_session_context_class">thread</property>
<property name="cache.provider_class">org.hibernate.cache.NoCacheProvider</property>
<property name="show_sql">true</property>
<property name="hbm2ddl.auto">create</property>
<mapping resource="pl/net/laskowski/User.hbm.xml" />
</session-factory>
</hibernate-configuration>
, klasy testowej pl.net.laskowski.HibernateExample w katalogu src/main/java/pl/net/laskowski
package pl.net.laskowski;

import java.util.Iterator;
import java.util.List;

import org.hibernate.Criteria;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.Transaction;
import org.hibernate.cfg.Configuration;

public class HibernateExample {

private final static SessionFactory factory;
static {
// 1. Inicjalizacja Hibernate
Configuration cfg = new Configuration().configure();

// 2. Utworzenie fabryki sesji Hibernate
factory = cfg.buildSessionFactory();
}

public static void main(String[] args) {
HibernateExample m = new HibernateExample();
m.createUsers();
m.displayUsers();
}

public void createUsers() {
// 3. Otwarcie sesji Hibernate
Session session = factory.openSession();

// 4. Rozpoczęcie transakcji
Transaction tx = session.beginTransaction();

// 5. Utworzenie użytkownika
User u = new User();
u.setImie("Jacek");
u.setNazwisko("Laskowski");

// 6. Zapisanie użytkownika w bazie danych
session.save(u);

// 7. Zatwierdzenie transakcji
tx.commit();

// 8. Zamknięcie sesji Hibernate
session.close();
}

public void displayUsers() {
// 3. Otwarcie sesji Hibernate
Session session = factory.openSession();

// 4. Rozpoczęcie transakcji
Transaction tx = session.beginTransaction();

// 5. Utworzenie zapytania SQL do bazy o listę użytkowników
Criteria criteria = session.createCriteria(User.class);

// 6. Wykonanie zapytania SQL
List users = criteria.list();

// 7. Iterowanie po wyniku zapytania SQL
for (Iterator it = users.iterator(); it.hasNext();) {
User user = (User) it.next();
System.out.println(user);
}

// 8. Zatwierdzenie transakcji
tx.commit();

// 9. Zamknięcie sesji Hibernate
session.close();
}
}
, pliku src/main/assembly/dist.xml
<?xml version="1.0" encoding="UTF-8"?>
<assembly>
<id>dist</id>
<formats>
<format>zip</format>
</formats>
<fileSets>
<fileSet>
<directory>target</directory>
<outputDirectory>/</outputDirectory>
<includes>
<include>*.jar</include>
</includes>
</fileSet>
</fileSets>
<dependencySets>
<dependencySet>
<outputDirectory>/</outputDirectory>
<unpack>false</unpack>
<scope>runtime</scope>
</dependencySet>
</dependencySets>
</assembly>
i wywołać polecenie mvn assembly:directory, które wywoła wtyczkę Hibernate w trakcie budowania dystrybucji oprogramowania.
mvn assembly:directory
Poprawnie utworzona wersja dystrybucyjna naszego projektu znajduje się w katalogu target/aplikacja-dist/aplikacja.

Analiza pliku aplikacja-1.0-SNAPSHOT.jar upewnia nas, że plik - pl/net/laskowski/User.hbm.xml został utworzony podczas budowania aplikacji i jest włączony do pliku wynikowego.
$ jar -tf target/aplikacja-dist/aplikacja/aplikacja-1.0-SNAPSHOT.jar
META-INF/
META-INF/MANIFEST.MF
pl/
pl/net/
pl/net/laskowski/
hibernate.cfg.xml
log4j.xml
pl/net/laskowski/App.class
pl/net/laskowski/HibernateExample.class
pl/net/laskowski/User.class
pl/net/laskowski/User.hbm.xml
META-INF/maven/
META-INF/maven/pl.net.laskowski/
META-INF/maven/pl.net.laskowski/aplikacja/
META-INF/maven/pl.net.laskowski/aplikacja/pom.xml
META-INF/maven/pl.net.laskowski/aplikacja/pom.properties
Uruchomienie aplikacji to wykonanie następującego polecenia:
java -jar target/aplikacja-dist/aplikacja/aplikacja-1.0-SNAPSHOT.jar
PODPOWIEDŹ: Po otwarciu projektu w Eclipse i dodaniu zależności Hibernate w pom.xml, klasa pl.net.laskowski.HibernateExample będzie zawierała zaznaczone klasy Hibernate jako nieznane. Należy w takim wypadku ponowie wygenerować pliki konfiguracyjne projektu
mvn eclipse:eclipse
i odświeżyć projekt w Eclipse.

PROBLEM 1: Generowanie nazwanych zapytań (ang. named query) Hibernate przy pomocy znacznika @hibernate.query za pomocą wtyczki Hibernate dla XDoclet2 w wersji 1.0.3.

PROBLEM 2: Konfiguracja Apache log4j, aby wyłączyć komunikaty Hibernate (poziom INFO) w docelowej aplikacji.

Jeśli znasz rozwiązania problemów 1 i 2, koniecznie się ze mną skontaktuj. Z przyjemnością dodam Twoje uwagi do artykułu.

10 lutego 2006

Wtyczka Maven2 - assembly - przygotowanie wersji dystrybucyjnych oprogramowania

0 komentarzy
Jednym z etapów pracy w projekcie jest tworzenie wersji instalacyjnych (dystrybucji) oprogramowania. Najczęściej wykorzystywanym sposobem budowania projektu w przypadku Maven2 to wywołanie wtyczki install bądź package. Jest to wystarczający sposób na zbudowanie projektu do pliku wynikowego zgodnego z typem projektu (najczęściej jar). Jednakże wcześniej czy później pojawi się potrzeba utworzenia wersji dystrybucyjnej projektu, który ma określoną strukturę katalogów i ich zawartości. Najczęstsze formaty wersji dystrybucyjnych to zip, tar.gz, czy tar.bz2. Reprezentują one nasz projekt wraz ze wszystkim zależnościami i są wykorzystywane do dystrybucji oprogramowania, np. do instalacji na środowisku testowym, czy ostatecznie produkcyjnym.

Apache Maven2 dostarcza możliwość przygotowywania wersji dystrybucyjnych za pomocą wtyczki assembly, która na podstawie danych projektu oraz pliku konfiguracyjnego wskazanego w pom.xml tworzy tworzy plik(i) dystrybucyjne
ze zdefiniowaną strukturą katalogów i plików.

Konfiguracja wtyczki assembly odbywa się poprzez plik konfiguracyjny wskazany w sekcji project/build/plugins pliku pom.xml. W pliku znajduje się definicja wtyczki jako maven-assembly-plugin wraz z jej plikiem konfiguracyjnym, określającym strukturę plików wynikowych.
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>pl.org.laskowski</groupId>
<artifactId>testy</artifactId>
<packaging>jar</packaging>
<version>1.0-SNAPSHOT</version>
<name>Maven Quick Start Archetype</name>
<url>http://maven.apache.org</url>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>3.8.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate</artifactId>
<version>3.1.2</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<version>2.0.1</version>
<configuration>
<descriptors>
<descriptor>src/main/assembly/prodenv.xml</descriptor>
<descriptor>src/main/assembly/testenv.xml</descriptor>
<descriptor>src/main/assembly/src.xml</descriptor>
</descriptors>
<finalName>${artifactId}-PROD</finalName>
</configuration>
</plugin>
</plugins>
</build>
</project>
Zgodnie z zaleceniami dotyczącymi preferowanej struktury katalogów projektów Maven2, zakłada się, że konfiguracje deskryptorów znajdują się w katalogu src/main/assembly. W powyższym przypadku wskazano kilka desktyptorów, z których prodenv.xml jest opisany poniżej. Pozostałe pliki są odmianą prodenv.xml.
<?xml version="1.0" encoding="UTF-8"?>
<assembly>
<id>prodenv</id>
<formats>
<format>zip</format>
</formats>
<fileSets>
<fileSet>
<includes>
<include>README*</include>
</includes>
</fileSet>
<fileSet>
<directory>target</directory>
<outputDirectory>lib</outputDirectory>
<includes>
<include>*.jar</include>
</includes>
<excludes>
<exclude>szczegolny.jar</exclude>
</excludes>
</fileSet>
</fileSets>
<dependencySets>
<dependencySet>
<outputDirectory>lib</outputDirectory>
<unpack>false</unpack>
<scope>runtime</scope>
<excludes>
<exclude>junit:junit</exclude>
</excludes>
</dependencySet>
</dependencySets>
</assembly>
Struktura deskryptorów wtyczek jest specyficzna dla nich samych i w przypadku wtyczki assembly umożliwia konfigurację formatów plików wynikowych (znacznik format) oraz ich zawartość. W ten sposób istnieje możliwość zdefiniowania dowolnego produktu wynikowego naszego projektu z określoną zawartością, np. dystrybucja binarna (zawierająca pełną dystrybucję projektu do uruchomienia), czy źródłowa (zawierająca źródła naszego projektu), czy też różne wersje dla różnych środowisk uruchomieniowych. Rozmieszczenie składowych pliku wynikowego konfigurowane jest przez przez znacznik fileSet, a dokładniej przez jego elementy: directory (położenie pliku źródłowego w strukturze katalogów projektu), outputDirectory (docelowe położenie pliku w strukturze katalogów w pliku wynikowym), include (podobnie jak w Ant włącza plik do pliku wynikowego), czy exclude (podobnie jak w Ant wyłącza plik z przetwarzania).
Istnieje możliwość dołączenia zależności projektu za pomocą znacznika dependencySet, co czyni proces tworzenia dystrybucji bardzo ułatwionym. Znacznik dependencySet udostępnia mechanizm dołączania zależności projektu wraz ze wszystkimi zależnościami pochodnymi (funkcjonalność oparta o przechodniość zależności, która jest nową cechą Maven2). Można wyłaczyć zależność z przetwarzania
za pomocą znacznika exclude. Część znaczników dependencySet jest analogiczna do fileSet, co sprawia, że są one intuicyjne i znacznie skracają proces definiowania struktury i wdrożenia zespołu do stosowania wtyczki.

Istnieje możliwość konfiguracji nazwy docelowej pliku wraz z odzwierciedleniem wartości zmiennych. Służy do tego znacznik finalName. W naszym przykładzie skorzystaliśmy z predefiniowanej przez Maven2 zmiennej artifactId, która zdefiniowana jest wyżej jako wartość elementu artifactId (pom.xml jest skryptem napisanym w Apache Jelly - znajomość języka jest wielce przydatna podczas pracy z Maven2, chociaż jego znaczenie znacznie zmalało w porównaniu z wcześniejszą wersją). Istnieje możliwość odwołania się do dowolnej zmiennej, które są predefiniowane przez Maven2, albo przez nas samych, np. poprzez plik settings.xml, czy opcję -D.

Uruchomienie wtyczki (a właściwie jej celu o tej samej nazwie) i zbudowanie produktu końcowego (dystrybucji) w określonym formacie i zawartości to uruchomienie następującego polecenia:
mvn assembly:assembly
Ważne jest, aby pierwsze uruchomienie polecenia było wykonane podczas podłączenia do repozytorium Maven2 tak, aby konieczne pliki zależne wtyczki mogły zostać pobrane do lokalnego repozytorium. Kolejne uruchomienia można wykonywać w trybie bezpodłączeniowym (ang. offline).

Konfiguracja wtyczki assembly w pliku pom.xml daje możliwość zdefiniowania wielu deskryptorów struktur plików wynikowych. Wybór specyficznego deskryptora polega na zdefiniowaniu zmiennej descriptorId, która wskazuje na unikalny identyfikator
w zbiorze wszystkich deskryptorów (element id w deskryptorze). Nie ma zależności między identyfikatorem a nazwą pliku deskryptora, jednakże dobrym zwyczajem jest nazywanie ich zgodnie z definiowanym deskryptorem. Możliwość definiowania różnych deskryptorów pozwala na stworzenie definicji różnych dystrybucji i ich zawartości, np. testenv - dystrybucja naszego projektu dla środowiska testowego, prodenv - dystrybucja dla środowiska produkcyjnego, etc.

Wywołanie wtyczki assembly z podaniem identyfikatora deskryptora:
mvn assembly:assembly -Dmaven.assembly.descriptorId=prodenv
Istnieje predefiniowana lista struktur plików wynikowych: src, bin i jar-with-dependencies, których użycie nie wymaga wcześniejszego definiowania w pliku pom.xml projektu.

Innym celem wtyczki assembly jest directory, który tworzy strukturę katalogową pliku wynikowego w postaci katalogu, bez finalnego pakowania dystrybucji do odpowiedniego formatu. Jest to bardzo przydatne podczas wykonywania lokalnych uruchomień projektu.
mvn assembly:directory
Wszystkie wyżej wymienione zmienne są również wykorzystywane w tym celu.

08 lutego 2006

Zarządzanie projektem za pomocą Apache Maven 2

3 komentarzy
Zazwyczaj wiele czynności jakie programista wykonuje w projekcie odbywa się z poziomu wybranego środowiska programistycznego (ang. IDE = Integrated Development Environment). W nim tworzy strukturę katalogów projektu korzystając z dostępnych wizardów. Dalsze kroki związane z tworzeniem aplikacji: budowanie (kompilacja oraz przeniesienie wymaganych plików do odpowiednich katalogów), testowanie (wykonywanie testów jednostkowych), zatwierdzanie/pobieranie zmian do/z systemu kontroli wersji, generowanie dokumentacji (javadoc) - odbywają się z poziomu IDE, jako przyciski na pasku zadań. To, co nie jest dostępne w środowisku programistycznym IDE, zazwyczaj nie jest wykorzystywane w projekcie. Dodatkowo, skoro czynności są wykonywane z poziomu IDE i są od niego zależne, wymusza to na zespole stosowanie identycznego środowiska oraz struktura projektu nie jest wynikiem decyzji zespołu, ale podyktowane jest wymogami środowiska programistycznego. Wszystko jest do zaakceptowania, aż do momentu, kiedy zdecydujemy się wykonywać część zadań bez uruchamiania IDE, np. okresowe wykonywanie testów jednostkowych bądź budowanie wersji instalacyjnych naszego projektu. Kolejną uciążliwością może być sprawne wykonywanie testów w środowisku serwera aplikacyjnego Java EE. Bezsprzecznie, w pewnym momencie będziemy potrzebować narzędzia do wykonywania zadań projektowych za nas, które umożliwi nam uniezależnienie się od wymagań IDE. Wielu w takim momencie sięgnie po rozwiązania specyficzne dla środowiska systemu operacyjnego, na którym pracuje, np. skrypty powłoki, ale jak wiadomo ich utrzymywanie nie jest przyjemne, szczególnie jeśli ilość zadań do wykonania przy ich pomocy wymusza zatrudnienie kolejnej osoby do ich utrzymania.

Bardziej zaawansowanym narzędziem pozwalającym na automatyzację zadań jest darmowy projekt Apache Ant. Jego wadą jest jednak konieczność tworzenia skryptów, nawet dla tak podstawowych zadań jak kompilacja, wykonanie testów jednostkowych, generowanie javadoc czy po prostu tworzenie wersji dystrybucyjnej. Celem Anta było uniezależnienie zespołów od korzystania ze specyficznego dla Uniksa narzędzia make, jak i rozbudowywanie narzędzia o nowe funkcjonalności (zadania) w Javie. Cel z pewnością został osiągnięty. Ant szybko okazał się najbardziej popularnym narzędziem wykorzystywanym przez programistów, a ilość zadań Ant rosła z każdym wydaniem. Niestety, Ant, oprócz zalet ma i swoje wady. Najbardziej doskwierającą i bardzo czasochłonną jest konieczność tworzenia skryptów, nawet dla bardzo podstawowych czynności projektowych. Komu chce się powtarzać te same kroki w kolejnych projektach podczas ich konfiguracji, jeśli część z nich już wykonano wcześniej, w innym projekcie? Brakowało narzędzia, którego głównym celem byłoby spojrzenie na wykonywane przez programistów zadania jako części projektu wraz z odziedziczeniem możliwości Anta. Jego następca musiał być równie prosty w obsłudze jak on sam i udostępniać możliwość uruchamiania już napisanych skryptów Ant. Dałoby to możliwość łagodnego przejścia użytkownikom Anta do nowego narzędzia. Dodatkowo, cechą wiodącą nowego narzędzia musiało być nie tylko utrzymywanie więzi z prekursorem pomysłu - Antem - ale również nowatorskie rozwiązanie w zakresie zarządzania zależnościami projektu - bibliotekami. Zarządzanie nimi wymaga dodatkowej pracy, której wykonanie dobrze byłoby, abyśmy mogli zlecić nowemu narzędziu.

Wszystkie wymienione cechy są dostępne w wolnym oprogramowaniu Apache Maven, który w porównaniu z Antem wprowadza swoistą rewolucję w zarządzaniu zadaniami projektowymi, a linia rozwojowa 2.x (z ostatnią wersją 2.0.2) jest bezsprzecznie liderem w tej kategorii. Wymienione cechy są jedynie namiastką możliwości Maven 2. Ich realizacja znacząco wpływa na obniżenie kosztów wytwarzania oprogramowania i skrócenie czasu realizacji projektu do niezbędnego maksimum przy zauważalnym podniesieniu jakości przez dostarczenie gotowych do wykonania celi (ang. goal). W wielu aktualnie prowadzonych projektach brak użycia Mavena wymusza manualne napisanie odpowiednich skryptów do wykonania raportów z przeprowadzenia testów, pokrycia przez nie kodu źródłowego, czy generowaniu rozmaitych dokumentacji projektowych, jak javadoc czy strona domowa projektu. Wszystkie te cechy powodują, że przy niezauważalnym nakładzie pracy podczas wdrożenia Maven jakość projektu zauważalnie wzrasta, a czas i koszty jego realizacji obniżają się.

Przez swoją prostotę w użyciu i dostępność wielu wtyczek wytwarzanie oprogramowania z jego pomocą nie kończy się na bardzo podstawowych czynnościach programistycznych jak kompilacja, czy wykonywanie testów, ale zachęca do użycia innych wtyczek, które automatycznie, bezkosztowo podnoszą jakość i zmniejszają koszt rozwoju oprogramowania. W innym przypadku wdrożenie ich wymagałoby dodatkowej wiedzy i czasu, co wielu z nas wie, że są najbardziej cennymi zasobami projektowymi i ciągle doskwiera ich brak.

W zasadzie nie ma żadnych wstępnych wymagań do korzystania z Mavena, poza wirtualną maszyną Java. Nie ma znaczenia, czy wzdrażamy go w projekcie w zaawansowanym stanie, czy przy jego pomocy tworzymy nowy. Rozpoczynamy od instalacji Mavena (co jest zwyczajnym rozpakowaniem paczki instalacyjnej do wybranego katalogu) i możemy rozpocząć pracę z jego pomocą. Sprawdzamy poprawność instalacji wykonując polecenie mvn z linii poleceń.
$ mvn --version
Maven version: 2.0.2
Wszystko co wykonujemy w Maven jest wynikiem uruchomienia wtyczki (ang. plugin). W skrócie, Maven jest systemem uruchamiania wtyczek, które mogą być napisane w językach skryptowych (Apache Jelly, BeanShell) lub Javie. Wtyczki mogą być zależne od innych wtyczek i wykonanie zadania będzie pociągało inne, jednakże podstawowe uruchamianie zadań zazwyczaj nie implikuje dodatkowych prac z naszej strony - kwestia definiowania zależności jest zazwyczaj po stronie autora wtyczki. Jest to bardzo podobne do zależności między zadaniami w Ant, w którym bardziej zaawansowany skrypt obarczony był dużym nakładem monotonnej pracy.

Istotną kwestią podczas pierwszego uruchamiania Mavena jest dostępność połączenia do centralnego repozytorium Maven. Jest to repozytorium artefaktów - bibliotek, wtyczek, wersji instalacyjnych, modułów J2EE i w sytuacji braku jednego podczas uruchomienia wtyczki, Maven automatycznie pobiera brakującą część z repozytorium. Upewnij się, że pracujesz podłączony do Internetu zanim rozpoczniesz jakiekolwiek przykłady z Maven (jeśli jesteś za ścianą ogniową - ang. firewall - uruchom Mavena wcześniej zdefiniowawszy MAVEN_OPTS="-Dhttp.proxyHost=numerIpProxy -Dhttp.proxyPort=numerPortu").

Pierwszą rzeczą z jaką każdy z nas się spotka podczas konfiguracji projektu jest określenie jego struktury katalogowej. Maven dostarcza wtyczkę archetype, której jednym z celi jest create. Wykonanie następującego polecenia stworzy predefiniowaną strukturę katalogów, która jest wystarczająca w większości z konfiguracji. Jeśli wdrażamy Maven do istniejącego projektu, istnieje możliwość konfiguracji Maven, aby respektował jego strukturę.
mvn archetype:create -DgroupId=pl.org.laskowski -DartifactId=aplikacja
Uruchomienie tego polecenia stworzy szkielet struktury katalogów na podstawie danych z pom.xml, z katalogiem głównym wyznaczonym przez artifactId (w naszym przypadku aplikacja) i podkatalogami src/main/java, który zawiera klasy projektu oraz src/test/java z klasami testów jednostkowych (klasy należą do pakietu pl.org.laskowski jak wskazano w groupId). Istotnym elementem tworzonym podczas wykonania powyższego polecenia jest plik pom.xml. POM to skrót od angielskiego Project Object Model, czyli model opisujący projekt. Jest to plik w formacie XML, w którym definiujemy wszystkie elementy związane z projektem, w tym docelową nazwę pliku wynikowego (najczęściej jar), który będzie tworzony w projekcie oraz zależności - biblioteki, które posłużą do budowania projektu (kompilacji klas). Konfiguracja projektu zarządzanego przez Maven to najczęściej modyfikacja jego pom.xml. Podczas dystrybucji projektu istnieje możliwość opublikowania pom.xml do repozytorium tak, że każdy projekt oparty o niego nie potrzebuje definiować dalszych zależności wyznaczanych przez zależny projekt.

Maven 2 wprowadził bardzo istotną funkcjonalność, która była niedostępna w poprzednich wersjach - przechodność zależności (ang. transitive dependencies). Jej zalety można dostrzec podczas konfiguracji zależności naszego projektu. Ile to razy borykamy się z rozwiązywaniem problemów z kompilacją, kiedy chcemy skorzystać z pewnej biblioteki. Jeśli dołączana biblioteka zależy od innej, wtedy musimy tę zależność dołączyć również do naszego projektu. Każdy kto pracował z Ant wie ile trudów przysparza definiowanie CLASSPATH i później jego utrzymywanie. Dodatkowo należy zadbać o dostępność samych plików. Jest to bardzo czasochłonne zadanie, a udostępnienie przechodności zależności w Maven uwalnia nas od niego raz na zawsze. W sytuacji, kiedy zdecydujemy się na wdrożenie biblioteki X, a ona zależy od kolejnych bibliotek, nasz pom.xml zadeklaruje jedynie zależność od biblioteki X, a pozostałe zależności będą pobrane automatycznie przez Maven na podstawie pom.xml projektu, w którym powstała biblioteka X. Dodatkową cechą nowej wersji Maven związaną z zarządzaniem zależnościami jest jest możliwość definiowania zakresu wersji zależności projektu (ang. version ranges). Możemy w ten sposób wyrazić zakres wersji zależności, która jest wymagana w projekcie. W naszym przykładowym projekcie pom.xml deklaruje zależność od biblioteki JUnit w wersji 3.8.1.

Jak już wspomniałem Maven cechuje się różnorodnością wtyczek, których dostarczona konfiguracja wstępna jest wystarczająca w początkowej fazie projektu. Brak konieczności konfiguracji Maven pozwala na wdrożenie go nawet w niedojrzałym zespole programistów oszczędzając im trudów początkowej integracji z narzędziem, jednocześnie oszczędzając czas na konfigurację narzędzi pomocniczych, które dostarczane są w Maven (m.in. checkstyle, jdepend, xdoclet).

Po stworzeniu struktury katalogów, otwórzmy projekt w wybranym zintegrowanym środowisku programistycznym (IDE). W zależności od wyboru Maven dostarcza wtyczki do generowania deskryptorów projektu dla Eclipse i IntelliJ IDEA. NetBeans IDE 5.0 posiada wsparcie dla projektów opartych o Maven2 poprzez moduły projektu Mevenide for NetBeans. Zainstalowawszy moduły mamy możliwość uruchamiania wtyczek Maven2 z poziomu NetBeans IDE i nie jest wymagane generowanie plików konfiguracyjnych projektu. Wygenerowanie deskryptora dla Eclipse IDE to uruchomienie polecenia (ważne jest, aby wykonywać wszystkie następne polecenia w katalogu aplikacja, który został stworzony wcześniej podczas uruchomienia mvn archetype:create):
mvn -Declipse.workspace=<ścieżka-do-przestrzeni-roboczej-Eclipse> eclipse:add-maven-repo
gdzie ścieżka-do-przestrzeni-roboczej-Eclipse to np. c:\eclipse\workspace. W ten sposób definiujemy zmienną M2_REPO w Eclipse, która będzie wskazywała na katalog repozytorium lokalnego Maven. Kolejnym krokiem jest uruchomienie polecenia mvn eclipse:eclipse.
mvn eclipse:eclipse
Po wykonaniu polecenia, które powinno zakończyć się BUILD SUCCESSFUL możemy otworzyć projekt w Eclipse (Import > Existing Projects into Workspace). Nasz projekt zawiera dwa katalogi ze źródłami (src/main/java oraz src/test/java) oraz jedną bibliotekę składowaną w katalogu M2_REPO.

Katalog M2_REPO, lub innymi słowy lokalne repozytorium Maven, to kopia struktury repozytorium Maven z bibliotekami (zależnościami), które były pobrane podczas uruchamiania Maven. Standardowo, katalog z lokalnym repozytorium Maven znajduje się w katalogu domowym użytkownika (~/.m2/repository na Uniksie i %USERPROFILE%/.m2/repository na MS Windows). Podkatalogi są ułożone w ten sposób, że najpierw są katalogi, które składają się na groupId z pom.xml uruchamianych projektów z plikami, których nazwy odpowiadają artifactId i packaging w pom.xml. Tworzenie kopii repozytorium zdalnego Maven umożliwia pracę bez podłączenia z centralnym repozytorium Maven (tj. http://repo1.maven.org/maven2, czyli http://www.ibiblio.org/maven2/). Każde wykonanie polecenia mvn z opcją -o bądź --offline wykonywane jest bez próby pobrania zależności z repozytorium i brak jednego powoduje zatrzymanie wykonania Maven.

Po utworzeniu klas, które tworzą projekt czas na ich kompilację. Jakkolwiek IDE wykonują tę czynność wspaniale, to wykonanie tego polecenia pozwoli na zbudowanie projektu automatycznie, bez konieczności uruchamiania dodatkowego oprogramowania.
mvn compile
Pomyślne zakończenie polecenia to wyświetlenie komunikatu BUILD SUCCESSFUL. Wcześniejsze komunikaty informują o postępie prac i pomagają zdiagnozować problem, jeśli wystąpi. Bardzo pomocną opcją podczas uruchamiania Maven, który zakończył się błędem, jest opcja -e lub --errors, który dostarcza dodatkowych informacji do analizy przyczyny błędnego uruchomienia wtyczki.

Standardowy katalog ze skompilowanymi klasami to target/classes. W ogólności katalog target jest docelowym katalogiem wykorzystywanym przez wtyczki do zapisywania plików wynikowych swojej pracy. W przypadku wtyczki compile będą to klasy, jednakże uruchomienie innych wtyczek dostarczy innych artefaktów projektowych, np. raportów. Uruchommy polecenie mvn clover bądź mvn checkstyle, aby zobaczyć wynik ich działania - raporty jakościowe dotyczące pokrycia klas projektu przez testy jednostkowe w pierwszym przypadku i poprawności stylu kodu w drugim. Oba raporty są uruchamiane i generowane bez dodatkowej, w innym przypadku bardziej absorbującej, pracy. Zanim jednak wykonamy obie wtyczki ważna uwaga na temat pracy wtyczek i dostarczania przez nich celi.

Każda wtyczka ma swoją nazwę i jest zbiorem celi. Jeden z celi może być celem domyślnym, a więc wykonywanym, kiedy uruchomimy Maven z nazwą wtyczki. Część wtyczek nie definiuje domyślnego celu, więc konieczne jest wywołanie wtyczki z nazwą wybranego przez nas celu. Konfiguracja wtyczki odbywa się przez zmienne projektu, które definiuje się albo podczas uruchamiania, na linii poleceń (opcja -D), albo w pliku settings.xml. W przypadku wtyczki checkstyle mamy do dyspozycji dwa cele: checkstyle oraz check. Wykonanie jednego z nich to uruchomienie wtyczki po nazwie której występuje nazwa celu odseparowanego dwukropkiem, np. checkstyle:checkstyle lub checkstyle:check. Podobnie jest w przypadku wtyczki clover, gdzie dostępnych mamy, aż 4 cele: check, instrument, log i clover. Najczęściej jeden z dostępnych celi jest użyteczny podczas, gdy pozostałe są pomocnicze dla działania wtyczki i ich uruchomienie nie przyniesie wymiernych korzyści. Dostępne cele poznamy analizując dokumentacje wtyczki. Lista części dostępnych wtyczek w standardowej dystrybucji Maven znajduje się na http://maven.apache.org/plugins/index.html.

W celu uruchomienia raportów o jakości naszego projektu wykonajmy polecenia mvn checkstyle:checkstyle oraz mvn clover:clover. Podczas ich uruchomienia można zauważyć pobieranie dodatkowych zależności, które zapisywane są w naszym lokalnym repozytorium tak, że kolejne uruchomienie nie będzie wymagało pobrania ich ponownie. Oczywiście może się zdarzyć (tak jak w przypadku wtyczki clover), że licencja na komercyjne oprogramowanie do użycia niekomercyjnego poprzez Maven wygaśnie i nie będzie możliwe jego wykorzystanie, aż do uaktualnienia licencji w repozytorium. W takiej sytuacji nie pozostaje nic innego jak skorzystanie z innej, alternatywnej wtyczki.

Pomyślne uruchomienie wtyczki checkstyle:checkstyle zapisze wynik swego działania w katalogu target/site. Otwórz checkstyle.html, aby zobaczyć jak niewielkim kosztem można wygenerować ciekawy raport o stanie jakości projektu. Oczywiście przydatnymi wtyczkami mogą być również javadoc (wykonaj cel javadoc:javadoc i otwórz target/javadoc/index.html), czy wręcz site (uruchom site i otwórz target/site/index.html).

Możliwe jest również uruchomienie kilku wtyczek i ich celi w jednym uruchomieniu Maven, np.
mvn checkstyle:checkstyle javadoc:javadoc site
W takiej sytuacji Maven wyznaczy zależności do pobrania i będzie pobierał je raz. Jeśli zależność znajduje się już w repozytorium lokalnym Maven (zazwyczaj) nie podejmuje ponownego pobrania ze zdalnego repozytorium, co znacznie skraca czas wykonania polecenia.

Utworzenie i instalacja biblioteki projektu w lokalnym repozytorium to uruchomienie wtyczki install.
mvn install
Poniżej znajdują się pozostałe, ważne wtyczki Maven dostępne w domyślnej konfiguracji.
  • test (mvn test) spowoduje uruchomienie testów jednostkowych (domyślnie zapisywanych w src/test/java)
  • package (mvn package) - uruchamia proces tworzenia paczki dystrybucyjnej i zapisuje wynik działania w katalogu target
  • clean (mvn clean) - usuwa katalog target
Przejrzyj dokumentację Mavena w poszukiwaniu innych wtyczek. Centralne repozytorium wtyczek i zależności - http://www.ibiblio.org/maven2/ - jest podstawowym źródłem ich dystrybucji.

02 lutego 2006

Hibernate - tniemy koszty dostępu do danych relacyjnych

10 komentarzy
Bardzo częstym problemem z jakim borykają się programiści podczas tworzenia oprogramowania jest dostęp do danych, które zazwyczaj przechowywane są w relacyjnej bazie danych. Mimo, że jest to w sprzeczności z obiektowym programowaniem w Javie, które wręcz wymusza obiektowe podejście do danych, w tym i tych składowanych w bazach danych, wielu z nas pozostaje wiernym relacyjnym systemom składowania danych. Właściwie można z dużą pewnością napisać, że bardzo niewielu doświadczyło prostoty z jaką przechowuje się dane w obiektowych bazach danych i nie zapowiada się, aby liczba "szczęśliwców" miała się powiększyć. Zdumiewający jest również upór z jakim programiści piszą własne rozwiązania, bez wcześniejszej ewaluacji istniejących, a przede wszystkim darmowych szkieletów persystencji danych. Aż trudno w to uwierzyć, kiedy utarło się twierdzić, że czas potrzebny na realizację projektu jest zazwyczaj niewystarczający.

Bez cienia wątpliwości można napisać, że najbardziej popularnym, wolnym oprogramowaniem, które realizuje podejście mapowania obiektów na struktury danych składowanych w relacyjnych bazach danych (ang. ORM = Object-to-Relational Mapping) jest Hibernate. Ostatnia finalna wersja 3.1.2 wprowadza, aż tyle udogodnień dostępu do danych relacyjnych, że trudno wyobrazić sobie sytuację, w której pojawiłaby się potrzeba stworzenia alternatywnego rozwiązania. Dostarczana dokumentacja jest na tyle pełna, aby bez niepotrzebnego ryzyka oprzeć kolejny projekt na Hibernate.

Rozpocznijmy naukę Hibernate od prostego przykładu. Załóżmy, że tworzymy aplikację - katalog użytkowników. Podstawowym elementem naszej aplikacji będzie pojęcie użytkownika. Tworzymy klasę pl.net.laskowski.User.

package pl.net.laskowski;

public final class User {
private int id;

private String imie;

private String nazwisko;

public int getId() {
return id;
}

public void setId(int id) {
this.id = id;
}

public String getImie() {
return imie;
}

public void setImie(String imie) {
this.imie = imie;
}

public String getNazwisko() {
return nazwisko;
}

public void setNazwisko(String nazwisko) {
this.nazwisko = nazwisko;
}
}
Nasz użytkownik składa się z identyfikatora, imienia i nazwiska. Nawet dla tak prostej klasy, przechowywanej w bazie danych musimy stworzyć strukturę bazy danych, a następnie obsłużyć operacje przechowywania i odczytu danych przy pomocy JDBC. Nawet dla zaawansowanego programisty jest to zadanie na kilka dobrych kwadransów, a skoro ów zaawansowany programista robił to wiele razy to jaki miałby być cel robić to kolejny raz? Kompletna nuda. Zatem ów zaawansowany programista będzie szukał rozwiązań, które zaoszczędzą mu czas i wcześniej czy później trafi na Hibernate, bo czyż może być przyjemniej, jeśli większość płatnego zadania wykona za nas darmowe oprogramowanie? Dzięki temu nasz kwadrans możemy spędzić na twórczej pracy zamiast powtórnie pisać obsługę dostępu do danych i skoncentrować się na właściwym pisaniu kodu źródłowego naszej nowej aplikacji.

...po kwadransie...

Zakończywszy bardzo twórczą pracę, w której poznaliśmy tajniki stosowania Hibernate w czasie jego wstępnej ewaluacji, przykładowa klasa korzystająca z udogodnień Hibernate w celu przechowywania danych o użytkownikach - pl.net.laskowski.HibernateExample - wygląda następująco:

package pl.net.laskowski;

import java.util.Iterator;
import java.util.List;

import org.hibernate.Criteria;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.Transaction;
import org.hibernate.cfg.Configuration;

public class HibernateExample {

private final static SessionFactory factory;
static {
// 1. Inicjalizacja Hibernate
Configuration cfg = new Configuration().configure();

// 2. Utworzenie fabryki sesji Hibernate
factory = cfg.buildSessionFactory();
}

public static void main(String[] args) {
HibernateExample m = new HibernateExample();
m.createUsers();
m.displayUsers();
}

public void createUsers() {
// 3. Otwarcie sesji Hibernate
Session session = factory.openSession();

// 4. Rozpoczęcie transakcji
Transaction tx = session.beginTransaction();

// 5. Utworzenie użytkownika
User u = new User();
u.setImie("Jacek");
u.setNazwisko("Laskowski");

// 6. Zapisanie użytkownika w bazie danych
session.save(u);

// 7. Zatwierdzenie transakcji
tx.commit();

// 8. Zamknięcie sesji Hibernate
session.close();
}

public void displayUsers() {
// 3. Otwarcie sesji Hibernate
Session session = factory.openSession();

// 4. Rozpoczęcie transakcji
Transaction tx = session.beginTransaction();

// 5. Utworzenie zapytania SQL do bazy o listę użytkowników
Criteria criteria = session.createCriteria(User.class);

// 6. Wykonanie zapytania SQL
List users = criteria.list();

// 7. Iterowanie po wyniku zapytania SQL
for (Iterator it = users.iterator(); it.hasNext();) {
User user = (User) it.next();
System.out.println(user);
}

// 8. Zatwierdzenie transakcji
tx.commit();

// 9. Zamknięcie sesji Hibernate
session.close();
}
}
Na czym polega praca Hibernate? Hibernate odpowiedzialny jest za mapowanie (transfer) danych w postaci obiektów Java do postaci rekordów w bazie danych i vice versa. Rozpoczęcie pracy z Hibernate zawsze rozpoczyna się od jego konfiguracji - wywołania odpowiedniej metody Configuration.configure(), która odszukuje plik konfiguracyjny i wykonuje zawarte w nim instrukcje (o pliku konfiguracyjnym Hibernate napiszemy za moment). Jest to linia opisana jako "1. Inicjalizacja Hibernate". Po tym następuje utworzenie fabryki sesji Hibernate za pomocą metody Configuration.buildSessionFactory(). W tym momencie, jeśli nie został rzucony żaden wyjątek (a nasza przykładowa klasa jest nieprzyzwoicie uboga w obsługę sytuacji wyjątkowych), zakłada się, że Hibernate jest w pełni skonfigurowany. Oba kroki - wywołanie metod Configuration.configure() oraz Configuration.buildSessionFactory() - wykonujemy raz w całej naszej aplikacji i stąd obie umieszczone są najczęściej w statycznym bloku pewnej klasy narzędziowej (np. HibernateUtils). Należy jednak pamiętać, że zastosowanie takiego podejścia wiąże się z pewnymi konsekwencjami w środowisku wielowątkowym, jak serwer aplikacyjny J2EE i wymagałoby innego podejścia, np. umieszczenie fabryki w drzewie JNDI tak, aby zapewnić jej pojedyńcze stworzenie. Bez względu na zastosowane podejście, pamiętajmy, że obie metody tworzą obiekty typu Configuration i SessionFactory, które wystarczy, aby występowały pojedyńczo w naszej aplikacji. Dla naszego rozwiązania zaproponowane rozwiązanie (utworzenie tworzenie obiektu session w statycznym bloku) w zupełności wystarczy.

Fabryka sesji Hibernate pozwala na otwieranie sesji, które reprezentują pewną jednostkę pracy, podobnie jak rozpoczęcie transakcji, jednakże zależność między sesją (obiekt typu org.hibernate.Session), a transakcją (obiekt typu org.hibernate.Transaction) jest jeden do wielu (1-*), w szczególności żadna transakcja nie musi być uruchomiona podczas otwarcia sesji Hibernate, co nie jest prawdą w odwrotnej sytuacji (rozpoczynamy transakcję po otwarciu sesji).

Do tej pory wszystkie kroki - od 1. do 4. - związane były całkowicie z Hibernate i będą nierozłącznym elementem aplikacji opartej na Hibernate. Po ich stworzeniu, rozpoczynamy właściwą pracę naszej aplikacji. Dla celów poglądowych klasa - p.n.l.HibernateExample - zawiera dwie metody - jedna do stworzenia użytkownika - HibernateExample.createUsers(), a druga do jego wyświetlenia - HibernateExample.displayUsers(). Po linii "5. Utworzenie użytkownika" następuje seria wywołań nie związanych z Hibernate - z funkcjonalnego punktu widzenia tworzymy użytkownika, co przekłada się na utworzenie instancji typu p.n.l.User. W tym momencie może nastąpić utworzenie dowolnej liczby obiektów, które ostatecznie trafią do bazy danych poprzez Hibernate. Tworzenie i inicjowanie pól obiektów w żaden sposób nie jest związane z Hibernate i odbywa się w dowolny, wybrany przez projektanta sposób. Zaraz po linii "6. Zapisanie użytkownika w bazie danych" następuje próba transferu obiektów do bazy danych za pomocą wywołania metody Session.save(Object). Należy pamiętać, że obiekty są zapamiętywane przez Hibernate i nie są natychmiast wysyłane do bazy wraz z ich wyczyszczeniem w pamięci podręcznej Hibernate (bardzo istotne miejsce optymalizacji kodu korzystającego z Hibernate).

Zanim przejdziemy do kolejnych kroków w naszej przykładowej aplikacji spójrzmy na plik konfiguracyjny Hibernate i plik z mapowaniem naszej klasy p.n.l.User.

Poniżej znajduje się plik konfiguracyjny Hibernate - hibernate.cfg.xml. Istnieje kilka innych sposobów konfiguracji Hibernate, jednakże konfiguracja poprzez plik hibernate.cfg.xml jest najczęściej wykorzystywanym sposobem. Metoda odszukiwania pliku przez Hibernate polega na przeszukaniu CLASSPATH. Podczas budowania projektu, należy pamiętać o umieszczeniu pliku hibernate.cfg.xml do odpowiedniego katalogu, który jest włączony do CLASSPATH.

<?xml version='1.0' encoding='utf-8'?>
<!DOCTYPE hibernate-configuration PUBLIC "-//Hibernate/Hibernate Configuration DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
<session-factory>
<property name="connection.driver_class">org.hsqldb.jdbcDriver</property>
<property name="connection.url">jdbc:hsqldb:.</property>
<property name="connection.username">sa</property>
<property name="connection.password"></property>
<property name="connection.pool_size">1</property>
<property name="dialect">org.hibernate.dialect.HSQLDialect</property>
<property name="current_session_context_class">thread</property>
<property name="cache.provider_class">org.hibernate.cache.NoCacheProvider</property>
<property name="show_sql">true</property>
<property name="hbm2ddl.auto">create</property>
<mapping resource="pl/net/laskowski/User.hbm.xml"/>
</session-factory>
</hibernate-configuration>
W zasadzie, znaczenie poszczególnych elementów pliku jest samowyjaśniające się. Hibernate posiada bardzo bogaty zbiór parametrów konfiguracyjnych (ang. property), jednakże powyższy plik jest dobrym punktem wyjścia do bardziej wyrafinowanej konfiguracji dla innych projektów. Przede wszystkim należy zaznaczyć, że struktura pliku hibernate.hbm.xml jest zgodna z formatem pliku XML, czyli wszystko znajduje się pomiędzy znacznikami, każdy zaczynający znacznik ma swój kończący odpowiednik i żadne dwa znaczniki nie przecinają się, tj. kończące znaczniki są w odwrotnej kolejności do początkowych. W powyższym pliku konfiguracyjnym korzystamy z darmowej, relacyjnej bazy danych HSQLDB, która ma możliwość uruchamiania w trybie wbudowanym, tj. bez konieczności uruchamiania osobnego procesu z bazą danych (co jest również możliwe w HSQLDB). Bardzo upraszcza to testowanie i w wielu przypadkach jest również wystarczającym repozytorium danych dla budowanych aplikacji. Ważnymi parametrami Hibernate w przedstawionym pliku są show_sql oraz hbm2ddl.auto. Ich wartości pozwalają na wyświetlanie zapytań SQL na konsolę (show_sql wynosi true) oraz tworzenie struktury bazy danych przy starcie aplikacji (hbm2ddl.auto wynosi create). Po przetestowaniu aplikacji parametr hbm2ddl.auto zaleca się ustawić na validate, albo po prostu wyłączyć poprzez usunięcie, czy zakomentowanie linii zawierającej parametr. Inną ciekawostką tego pliku konfiguracyjnego jest wskazanie pliku z ustawieniami mapowania klas na reprezentencję relacyjną, która będzie potrzebna podczas zapisu danych przez Hibernate do bazy danych. Służy do tego znacznik mapping i tylko te klasy, które są zawarte w plikach wskazanych przez niego będą wykorzystywane przez Hibernate. Kolejna częsta pomyłka podczas konfiguracji Hibernate, która potrafi zająć sporo czasu.

Poniżej znajduje się plik - pl/net/laskowski/User.hbm.xml, który wskazany został przez znacznik mapping w pliku konfiguracyjnym hibernate.cfg.xml.

<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
<hibernate-mapping package="pl.net.laskowski">
<class name="User" table="User">
<id name="id"><generator class="native"/></id>
<property name="imie" column="T_IMIE" length="10" not-null="true"/>
<property name="nazwisko" column="T_NAZWISKO" length="25" not-null="true"/>
</class>
</hibernate-mapping>
Plik - User.hbm.xml - przedstawia konfigurację mapowania klasy pl.net.laskowski.User (atrybut package znacznika hibernate-mapping oraz atrybut name znacznika class). W zasadzie nazwa pliku i jego umiejscowienie w strukturze plików nie jest istotne, jednakże przyjęło się, że każda klasa, która będzie obsługiwana przez Hibernate, posiada swój własny plik mapowania, który znajduje się w katalogu odpowiadającym pakietowi mapowanej klasy z nazwą odpowiadającą nazwie klasy z rozszeżeniem hbm.xml. Jest to jedynie konwencja, której stosowanie znacząco przyspiesza proces wdrażania się przez nowe osoby do projektu, kiedy w użyciu jest Hibernate (oczywiście bardziej zaawansowana osoba rozpocznie swoje poznawanie warstwy persystencji od zapoznania się z plikiem konfiguracyjnym Hibernate). Metoda odszukania pliku polega na odszukaniu pliku, wskazanego w hibernate.cfg.xml jako atrybut resource znacznika mapping i mimo, że nazwa pliku i jego ścieżka nie mają znaczenia, to jego umiejscowienie w CLASSPATH już tak. Należy upewnić się, że Hibernate będzie mógł odszukać plik poprzez odszukanie zasobu w CLASSPATH.

Plik mapowania pl/net/laskowski/User.hbm.xml składa się ze znacznika class, którego atrybuty wskazują na nazwę klasy, której dotyczą (atrubut name) oraz tablicę w bazie danych, która będzie przechowywała obiekty (atrybut table). Pomiędzy znacznikiem class znajduje się właściwe mapowanie pól klasy na odpowiadające im pola w bazie danych. Mapowanie pól odbywa się za pomocą znaczników id oraz property. Są one identyczne ze względu na akceptowane atrybuty, jednakże pierwszy - id - wskazuje na nazwę zmiennej instancji klasy (atrybut name), która reprezentuje klucz główny tabeli. W naszym przykładzie, dla zmiennej instancji klasy o nazwie id, skonfigurowany został generator kolejnych wartości, który będzie oparty o mechanizmy wbudowane w wybranej bazie danych. Za dobranie właściwego mechanizmu dba dialekt specyficzny dla każdej bazy danych i jest konfigurowany w pliku hibernate.cfg.xml jako wartość parametru hibernate.dialect. Poprzedziłem nazwę parametru prefiksem hibernate mimo, że taki parametr nie istnieje w pliku hibernate.cfg.xml, a jedynie dialect. Wynika to z zaszłości stosowania pliku o formacie properties, w który każdy parametr Hibernate jest poprzedzany przez prefiks hibernate. Stosowanie go nie jest konieczne podczas konfigurowania Hibernate przez plik hibernate.cfg.xml i jest jedynie używane w dokumentacji do wyróżnienia pochodzenia parametru. Atrybut name znaczników id oraz property wskazuje na nazwę zmiennej instancji klasy (świat obiektowy - Java), podczas gdy column wskazuje na nazwę kolumny, która będzie przechowywała dane (świat relacyjny - SQL). Dalsze atrybuty pozwalają na dokładniejsze wyrażenie mapowania i odpowiadają charakterystyce kolumn w tabeli. W naszym przykładzie wykorzystałem atrybuty column, length oraz not-null.

Po wykonaniu zapisu do bazy danych zatwierdzamy transakcję (linia "7. Zatwierdzenie transakcji") oraz zamykamy sesję Hibernate (linia "8. Zamknięcie sesji"). W ten sposób kończymy metodę createUsers, której celem było utworzenie użytkownika i jego zapis do bazy danych.

Kolejną metodą naszej przykładowej aplikacji jest displayUsers().

public void displayUsers() {
// 3. Otwarcie sesji Hibernate
Session session = factory.openSession();

// 4. Rozpoczęcie transakcji
Transaction tx = session.beginTransaction();

// 5. Utworzenie zapytania SQL do bazy o listę użytkowników
Criteria criteria = session.createCriteria(User.class);

// 6. Wykonanie zapytania SQL
List users = criteria.list();

// 7. Iterowanie po wyniku zapytania SQL
for (Iterator it = users.iterator(); it.hasNext();) {
User user = (User) it.next();
System.out.println(user);
}

// 8. Zatwierdzenie transakcji
tx.commit();

// 9. Zamknięcie sesji Hibernate
session.close();
}
W poprzedniej metodzie dokonaliśmy zapisu informacji w bazie danych, teraz nastąpi jej odczyt. Procedura odczytu danych z bazy danych poprzez Hibernate rozpoczyna się tradycyjnie otwarciem sesji ("3. Otwarcie sesji Hibernate") i rozpoczęciu transakcji ("4. Rozpoczęcie transakcji"). Ta część programu jest już nam znana. Przejdźmy do ciekawszego jego elementu stworzeniem zapytania do pobrania danych. W tym celu korzysta się z klasy org.hibernate.Criteria. Do utworzenia instancji klasy służy metoda Session.createCriteria(Class). Jako parametr wejściowy podaje się klasę, której instancje chcemy zainicjować danymi pochodzącymi z bazy danych. Konfiguracja Hibernate wskazuje, która tabela przechowuje dane relacyjne reprezentujące instancje klasy (dla przypomnienia: jest to część konfiguracji hibernate.cfg.cml znacznik mapping). Naturalnie, w klasie o.h.Criteria znajdziemy metody, które pozwalają na zawężenie zapytania do interesujących nas rekordów. Zaletą tworzenia zapytania za pomocą takiego sposobu, to przede wszystkim wyniesienie informacji dotyczącej poszczególnych pól biorących udział w zapytaniu do pliku zewnętrznego (cf. pl/net/laskowski/User.hbm.xml), który można w prosty sposób zmodyfikować bez nadmiarowej rekompilacji aplikacji. Wywołanie metody Criteria.list() zwraca listę obiektów (utworzonych na podstawie danych relacyjnych) typu będącego parametrem wejściowym podczas tworzenia instancji klasy o.h.Criteria. Iterowanie po wynikach nie jest już niczym nadzwyczajnym i nie jest związane z Hibernate w żaden sposób (poza tym, że implementacja zwróconego obiektu typu java.util.Iterator pochodzi z Hibernate i jest możliwa bardziej specjalizowana konfiguracja sposobu pobierania danych podczas odpytania iteratora o kolejny obiekt, który będzie zmaterializowany na podstawie danych relacyjnych). Jak przedstawiono w poprzedniej metodzie, koniec pracy z Hibernate to zamknięcie obiektów z nim związanych - transakcji i sesji (cf. "8. Zatwierdzenie transakcji" oraz "9. Zamknięcie sesji Hibernate").

Do uruchomienia aplikacji będą nam potrzebne następujące biblioteki, które dostarczane są razem z Hibernate (w katalogu głównym, bądź lib):

  • antlr-2.7.6rc1.jar
  • cglib-2.1.3.jar
  • commons-collections-2.1.1.jar
  • commons-logging-1.0.4.jar
  • dom4j-1.6.1.jar
  • ehcache-1.1.jar
  • hibernate3.jar
  • hsqldb.jar
  • jta.jar
Po ustawieniu środowiska tak, aby wspomniane biblioteki były dostępne, uruchomienie aplikacji sprowadza się do wywołania klasy p.n.k.HibernateExample.
java pl.net.laskowski.HibernateExample
. Należy pamiętać o prawidłowym rozmieszczeniu plików konfiguracyjnych - hibernate.cfg.xml oraz User.hbm.xml w odpowiednich katalogach.