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.