14 lutego 2006

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

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.