Apache Wicket z JPA z pomocą Spring Framework i Apache Maven 2

Z Jacek Laskowski - Wiki Projektanta Java EE

Rozpoznanie projektu Apache Wicket zatrzymałem na temacie integracji z Java Persistence API (JPA). Do tej pory korzystałem z Jetty jako środowiska uruchomieniowego dla aplikacji webowej korzystającej z Wicket. Nie miałem dostępnych wszystkich usług jakie oferuje serwer aplikacji Java EE w stylu Apache Geronimo czy GlassFish, więc postanowiłem wdrożyć Spring Framework, który związał mi wszystkie konieczne usługi symulując serwer aplikacyjny. Dodając do tego Apache Maven 2, za pomocą którego zarządzam projektem i jego zależnościami, i tworzenie aplikacji z wykorzystaniem Wicket, JPA (Apache OpenJPA), Spring Framework, HSQLDB i Maven stało się trywialne.

Dla osób zainteresowanych Apache Wicket zapraszam na strony mojego Notatnika w kategorii wicket.

Utwórzmy projekt aplikacji webowej opartej o Wicket korzystając z jego wtyczki dla Maven - wicket-archetype-quickstart (więcej w notatce Wicket prościej z mavenowym archetypem wicket-archetype-quickstart).

jlaskowski@dev /cygdrive/c
$ mvn archetype:create \
    -DarchetypeGroupId=org.apache.wicket \
    -DarchetypeArtifactId=wicket-archetype-quickstart \
    -DarchetypeVersion=1.3.2 \
    -DgroupId=pl.jaceklaskowski.wicket -DartifactId=wicket-jpa-spring-demo
[INFO] Scanning for projects...
[INFO] Searching repository for plugin with prefix: 'archetype'.
[INFO] ----------------------------------------------------------------------------
[INFO] Building Maven Default Project
[INFO]    task-segment: [archetype:create] (aggregator-style)
[INFO] ----------------------------------------------------------------------------
Downloading: http://repo1.maven.org/maven2/pl/jaceklaskowski/wicket/wagon-http-shared/1.0-beta-2/wagon-http-shared-1.0-beta-2.pom
Downloading: http://repo1.maven.org/maven2/pl/jaceklaskowski/wicket/wagon-http-shared/1.0-beta-2/wagon-http-shared-1.0-beta-2.pom
[INFO] Setting property: classpath.resource.loader.class => 'org.codehaus.plexus.velocity.ContextClassLoaderResourceLoader'.
[INFO] Setting property: velocimacro.messages.on => 'false'.
[INFO] Setting property: resource.loader => 'classpath'.
[INFO] Setting property: resource.manager.logwhenfound => 'false'.
[INFO] [archetype:create]
[INFO] Defaulting package to group ID: pl.jaceklaskowski.wicket
[INFO] ----------------------------------------------------------------------------
[INFO] Using following parameters for creating OldArchetype: wicket-archetype-quickstart:1.3.2
[INFO] ----------------------------------------------------------------------------
[INFO] Parameter: groupId, Value: pl.jaceklaskowski.wicket
[INFO] Parameter: packageName, Value: pl.jaceklaskowski.wicket
[INFO] Parameter: basedir, Value: c:\
[INFO] Parameter: package, Value: pl.jaceklaskowski.wicket
[INFO] Parameter: version, Value: 1.0-SNAPSHOT
[INFO] Parameter: artifactId, Value: wicket-jpa-spring-demo
[INFO] ********************* End of debug info from resources from generated POM ***********************
[INFO] OldArchetype created in dir: c:\wicket-jpa-spring-demo
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESSFUL
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 2 seconds
[INFO] ------------------------------------------------------------------------

Przechodzimy do katalogu wicket-jpa-spring-demo i uruchamiamy naszą aplikację webową poleceniem mvn clean jetty:run.

jlaskowski@dev /cygdrive/c/wicket-jpa-spring-demo
$ mvn clean jetty:run
[INFO] Scanning for projects...
[INFO] Searching repository for plugin with prefix: 'jetty'.
[INFO] ----------------------------------------------------------------------------
[INFO] Building quickstart
[INFO]    task-segment: [clean, jetty:run]
[INFO] ----------------------------------------------------------------------------
[INFO] [clean:clean]
[INFO] Deleting directory c:\wicket-jpa-spring-demo\target
[INFO] Preparing jetty:run
[INFO] [resources:resources]
[INFO] Using default encoding to copy filtered resources.
[INFO] [compiler:compile]
[INFO] Compiling 2 source files to c:\wicket-jpa-spring-demo\target\classes
[INFO] [resources:testResources]
[INFO] Using default encoding to copy filtered resources.
[INFO] [compiler:testCompile]
[INFO] Compiling 2 source files to c:\wicket-jpa-spring-demo\target\test-classes
[INFO] [jetty:run]
[INFO] Configuring Jetty for project: quickstart
[INFO] Webapp source directory = C:\wicket-jpa-spring-demo\src\main\webapp
[INFO] web.xml file = C:\wicket-jpa-spring-demo\src\main\webapp\WEB-INF\web.xml
[INFO] Classes = C:\wicket-jpa-spring-demo\target\classes
2008-03-15 21:05:15.645::INFO:  Logging to STDERR via org.mortbay.log.StdErrLog
[INFO] Context path = /wicket-jpa-spring-demo
[INFO] Tmp directory =  determined at runtime
[INFO] Web defaults = org/mortbay/jetty/webapp/webdefault.xml
[INFO] Web overrides =  none
[INFO] Webapp directory = C:\wicket-jpa-spring-demo\src\main\webapp
[INFO] Starting jetty 6.1.8 ...
2008-03-15 21:05:15.738::INFO:  jetty-6.1.8
2008-03-15 21:05:15.848::INFO:  No Transaction manager found - if your webapp requires one, please configure one.
INFO  - Application                - [WicketApplication] init: Wicket core library initializer
INFO  - RequestListenerInterface   - registered listener interface [RequestListenerInterface name=IBehaviorListener, method
ener.onRequest()]
INFO  - RequestListenerInterface   - registered listener interface [RequestListenerInterface name=IBehaviorListener, method
ener.onRequest()]
INFO  - RequestListenerInterface   - registered listener interface [RequestListenerInterface name=IFormSubmitListener, meth
rmSubmitListener.onFormSubmitted()]
INFO  - RequestListenerInterface   - registered listener interface [RequestListenerInterface name=IFormSubmitListener, meth
rmSubmitListener.onFormSubmitted()]
INFO  - RequestListenerInterface   - registered listener interface [RequestListenerInterface name=ILinkListener, method=pub
ener.onLinkClicked()]
INFO  - RequestListenerInterface   - registered listener interface [RequestListenerInterface name=ILinkListener, method=pub
ener.onLinkClicked()]
INFO  - RequestListenerInterface   - registered listener interface [RequestListenerInterface name=IOnChangeListener, method
angeListener.onSelectionChanged()]
INFO  - RequestListenerInterface   - registered listener interface [RequestListenerInterface name=IOnChangeListener, method
angeListener.onSelectionChanged()]
INFO  - RequestListenerInterface   - registered listener interface [RequestListenerInterface name=IRedirectListener, method
direct()]
INFO  - RequestListenerInterface   - registered listener interface [RequestListenerInterface name=IRedirectListener, method
direct()]
INFO  - RequestListenerInterface   - registered listener interface [RequestListenerInterface name=IResourceListener, method
sourceRequested()]
INFO  - RequestListenerInterface   - registered listener interface [RequestListenerInterface name=IResourceListener, method
sourceRequested()]
INFO  - WebApplication             - [WicketApplication] Started Wicket version 1.3.2 in development mode
********************************************************************
*** WARNING: Wicket is running in DEVELOPMENT mode.              ***
***                               ^^^^^^^^^^^                    ***
*** Do NOT deploy to your live server(s) without changing this.  ***
*** See Application#getConfigurationType() for more information. ***
********************************************************************
2008-03-15 21:05:16.283::INFO:  Started SelectChannelConnector@0.0.0.0:8080
[INFO] Started Jetty Server

Aplikacja dostępna jest pod adresem http://localhost:8080/wicket-jpa-spring-demo.

Grafika:wicket-jpa-spring-demo-quickstart.gif

Magiczne Ctrl-C zatrzymuje serwer.

Importujemy projekt do ulubionego IDE, np. Eclipse poleceniem mvn eclipse:eclipse.

Modyfikujemy pom.xml (serce projektu opartego o Maven 2) o podanie zależności projektu:

  • Apache Wicket 1.3.2
  • Jetty 6.1.8
  • Spring Framework 2.5.2
  • Apache OpenJPA 1.0.2
  • HSQLDB 1.8.0.7

i kilka innych zmian (nazwa projektu i jego wersja).

<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.jaceklaskowski.wicket</groupId>
  <artifactId>wicket-jpa-spring-demo</artifactId>
  <packaging>war</packaging>
  <version>1.0</version>
  <name>Aplikacja webowa z Wicket</name>
  <url>http://www.jaceklaskowski.pl/wiki/Apache_Wicket_z_JPA_z_pomoc%C4%85_Spring_Framework_i_Apache_Maven_2</url>
  <properties>
    <wicket.version>1.3.2</wicket.version>
    <jetty.version>6.1.8</jetty.version>
    <spring.version>2.5.2</spring.version>
    <openjpa.version>1.0.2</openjpa.version>
    <hsqldb.version>1.8.0.7</hsqldb.version>
  </properties>
  <dependencies>
    <dependency>
      <groupId>org.apache.wicket</groupId>
      <artifactId>wicket</artifactId>
      <version>${wicket.version}</version>
    </dependency>
    <dependency>
      <groupId>org.apache.wicket</groupId>
      <artifactId>wicket-spring</artifactId>
      <version>${wicket.version}</version>
      <exclusions>
        <exclusion>
          <groupId>org.springframework</groupId>
          <artifactId>spring</artifactId>
        </exclusion>
      </exclusions>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring</artifactId>
      <version>${spring.version}</version>
    </dependency>
    <dependency>
      <groupId>org.apache.openjpa</groupId>
      <artifactId>openjpa</artifactId>
      <version>${openjpa.version}</version>
    </dependency>
    <dependency>
      <groupId>hsqldb</groupId>
      <artifactId>hsqldb</artifactId>
      <version>${hsqldb.version}</version>
    </dependency>
    <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>slf4j-log4j12</artifactId>
      <version>1.4.2</version>
    </dependency>
    <dependency>
      <groupId>log4j</groupId>
      <artifactId>log4j</artifactId>
      <version>1.2.14</version>
    </dependency>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>3.8.2</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.mortbay.jetty</groupId>
      <artifactId>jetty</artifactId>
      <version>${jetty.version}</version>
      <scope>provided</scope>
    </dependency>
    <dependency>
      <groupId>org.mortbay.jetty</groupId>
      <artifactId>jetty-util</artifactId>
      <version>${jetty.version}</version>
      <scope>provided</scope>
    </dependency>
    <dependency>
      <groupId>org.mortbay.jetty</groupId>
      <artifactId>jetty-management</artifactId>
      <version>${jetty.version}</version>
      <scope>provided</scope>
    </dependency>
  </dependencies>
  <build>
    <resources>
      <resource>
        <filtering>false</filtering>
        <directory>src/main/resources</directory>
      </resource>
      <resource>
        <filtering>false</filtering>
        <directory>src/main/java</directory>
        <includes>
          <include>**</include>
        </includes>
        <excludes>
          <exclude>**/*.java</exclude>
        </excludes>
      </resource>
    </resources>
    <testResources>
      <testResource>
        <filtering>false</filtering>
        <directory>src/test/java</directory>
        <includes>
          <include>**</include>
        </includes>
        <excludes>
          <exclude>**/*.java</exclude>
        </excludes>
      </testResource>
    </testResources>
    <plugins>
      <plugin>
        <groupId>org.mortbay.jetty</groupId>
        <artifactId>maven-jetty-plugin</artifactId>
      </plugin>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <configuration>
          <source>1.5</source>
          <target>1.5</target>
        </configuration>
      </plugin>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-eclipse-plugin</artifactId>
        <configuration>
          <downloadSources>true</downloadSources>
        </configuration>
      </plugin>
    </plugins>
  </build>
</project>

Opcjonalnie można sprawdzić, czy zmiany nie wprowadziły błędu uruchamiając aplikację z mvn jetty:run.

Najpierw tworzymy model aplikacyjny z pojedyńczą encją JPA - Osoba (reprezentowaną przez klasę pl.jaceklaskowski.wicket.model.Osoba):

package pl.jaceklaskowski.wicket.model;

import java.io.Serializable;
import java.text.MessageFormat;

public class Osoba implements Serializable {

    private static final long serialVersionUID = 1L;
    private long id;
    private String imie;
    private String nazwisko;
    private String identyfikator;
    private String haslo;
    private String miejscowosc;

    public Osoba() {
    }

    public Osoba(String imie, String nazwisko, String identyfikator, String haslo, String miejscowosc) {
        this.imie = imie;
        this.nazwisko = nazwisko;
        this.identyfikator = identyfikator;
        this.haslo = haslo;
        this.miejscowosc = miejscowosc;
    }

    public long getId() {
        return id;
    }

    public void setId(long 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;
    }

    public String getIdentyfikator() {
        return this.identyfikator;
    }

    public void setIdentyfikator(String identyfikator) {
        this.identyfikator = identyfikator;
    }

    public String getHaslo() {
        return haslo;
    }

    public void setHaslo(String haslo) {
        this.haslo = haslo;
    }

    public String getMiejscowosc() {
        return miejscowosc;
    }

    public void setMiejscowosc(String miejscowosc) {
        this.miejscowosc = miejscowosc;
    }

    public String toString() {
        return MessageFormat.format("{0} {1} - identyfikator: {2}, haslo: {3}, miejscowosc: {4}", imie, nazwisko,
                identyfikator, haslo, miejscowosc);
    }
}

Ciekawostką tej klasy jest fakt, że nie zawiera ona żadnych elementów wskazujących, że klasa Osoba jest faktycznie encją JPA, tj. klasa nie zawiera żadnych ze znanych (obowiązkowych jeśli używane) adnotacji @Entity czy @Id. Do oznaczenia klasy jako encji JPA użyjemy pliku META-INF/orm.xml, który jest alternatywnym sposobem do adnotacji w JPA.

 <?xml version="1.0" encoding="UTF-8" ?>
 <entity-mappings xmlns="http://java.sun.com/xml/ns/persistence/orm" 
   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
   xsi:schemaLocation="http://java.sun.com/xml/ns/persistence/orm http://java.sun.com/xml/ns/persistence/orm_1_0.xsd"
   version="1.0">
   <entity class="pl.jaceklaskowski.wicket.model.Osoba" metadata-complete="true">
     <named-query name="Osoba.findWszystkieOsoby">
       <query>SELECT o FROM Osoba o</query>
     </named-query>
     <named-query name="Osoba.findById">
       <query>SELECT o FROM Osoba o WHERE o.id = :id</query>
     </named-query>
     <named-query name="Osoba.findByImieNazwisko">
       <query>SELECT o FROM Osoba o WHERE o.imie = :imie AND o.nazwisko = :nazwisko</query>
     </named-query>
     <named-query name="Osoba.findByIdentyfikator">
       <query>SELECT o FROM Osoba o WHERE o.identyfikator = :identyfikator</query>
     </named-query>
     <named-query name="Osoba.findByMiejscowosc">
       <query>SELECT o FROM Osoba o WHERE o.miejscowosc = :miejscowosc</query>
     </named-query>
     <attributes>
       <id name="id">
         <generated-value strategy="AUTO" />
       </id>
       <basic name="imie" />
       <basic name="nazwisko" />
       <basic name="identyfikator" />
       <basic name="haslo" />
       <basic name="miejscowosc" />
     </attributes>
   </entity>
 </entity-mappings>

Plik definiuje encję Osoba oraz związane z nią nazwane zapytania. Atrybut metadata-complete z wartością true elementu entity określa, że wszystkie dane encji są zawarte w tym pliku.

Plik orm.xml zapisujemy w katalogu src/main/resources/META-INF.

Skoro jesteśmy przy JPA to koniecznie musimy stworzyć plik konfiguracyjny JPA - persistence.xml.

<?xml version="1.0" encoding="UTF-8"?>
<persistence xmlns="http://java.sun.com/xml/ns/persistence" 
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd"
  version="1.0">
  <persistence-unit name="JpaStandalonePU" transaction-type="RESOURCE_LOCAL">
    <provider>org.apache.openjpa.persistence.PersistenceProviderImpl</provider>
    <class>pl.jaceklaskowski.wicket.model.Osoba</class>
    <properties>
      <property name="openjpa.jdbc.SynchronizeMappings" value="buildSchema" />
      <property name="openjpa.Log" value="DefaultLevel=WARN,SQL=TRACE" />
    </properties>
  </persistence-unit>
</persistence>

Plik definiuje klasę, która występuje w roli encji JPA, dostawcę JPA (w tym przypadku jest to Apache OpenJPA) oraz konfigurację dostawcy (element properties). Jako, że znajdujemy się poza środowiskiem serwera aplikacji atrybut transaction-type elementu persistence-unit ustawiony jest na wartość RESOURCE_LOCAL.

Plik persistence.xml zapisujemy w katalogu src/main/resources/META-INF, obok orm.xml. Więcej o sposobie konfigurowania Apache OpenJPA jako dostawcy JPA w aplikacjach desktopowych w moim artykule OpenJPA jako dostawca JPA w samodzielnej aplikacji.

Zdefiniujmy klasę DAO dla naszej encji Osoba, które pozwoli nam na manipulowanie danymi w bazie danych za pomocą JPA.

Zaczniemy od zdefiniowania interfejsu pl.jaceklaskowski.wicket.model.OsobaDao (dla celów artykułu minimalizujemy liczbę udostępnianych metod).

package pl.jaceklaskowski.wicket.model;

public interface OsobaDao {

    public Osoba findByIdentyfikator(String identyfikator);

    public void create(Osoba osoba);

}

Interfejs klasy DAO udostępnia metody do odszukania encji po identyfikatorze osoby oraz możliwość utworzenia nowej encji Osoba.

Implementacja interfejsu - klasa pl.jaceklaskowski.wicket.model.OsobaDaoImpl - opiera swoje działanie na JPA.

package pl.jaceklaskowski.wicket.model;

import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.Query;

import org.springframework.transaction.annotation.Transactional;

@Transactional
public class OsobaDaoImpl implements OsobaDao {

    @PersistenceContext
    private EntityManager em;

    public void create(Osoba osoba) {
        em.persist(osoba);
    }

    public Osoba findByIdentyfikator(String identyfikator) {
        Query query = em.createNamedQuery("Osoba.findByIdentyfikator");
        query.setParameter("identyfikator", identyfikator);
        return (Osoba) query.getSingleResult();
    }

}

Klasa OsobaDaoImpl korzysta z mechanizmu wstrzeliwania zależności (DI - dependency injection) do pozyskania zarządcy encji (udekorowane pole em przez adnotację @PersistenceContext). Osoby zaznajomione z tajnikami JPA szybko rozpoznają adnotację @PersistenceContext, jednakże jej użycie w klasie niebędącej klasą zarządzaną przez serwer aplikacyjny Korporacyjnej Javy 5 może powodować lekkie zakłopotanie w zrozumieniu jak to działa. Jeśli do tego dodać nieznaną z JPA adnotację @Transactional zmieszanie będzie jeszcze większe. Jest to pora na wprowadzenie Spring Framework i omówienie jego roli w aplikacji.

Spring Framework jest wspaniałym (żeby nie napisać doskonałym) "kontenerem" udostępniającym mechanizm wstrzeliwania zależności. Z jego właśnie pomocą określimy miejsce przekazania zarządcy trwałego w klasie (pole em w klasie OsobaDaoImpl) oraz metody transakcyjne (użycie adnotacji @Transactional na poziomie klasy). Należy nadmienić, że wykorzystywane usługi byłyby dostępne w ramach serwera aplikacji, jednakże jako, że korzystamy z Jetty, który nie udostępnia usług typu monitor transakcyjny, czy kontener JPA, owe braki wypełni nam Spring Framework. Dzięki odpowiedniej konfiguracji Springa możliwe jest jego użycie bez "infekcji" klas aplikacji specyficznymi dla Springa klasami czy interfejsem (jak się niedługo okaże, poza adnotacją @Transactional klasy i interfejsy aplikacji nie będą w ogóle modyfikowane na poczet użycia Spring Framework).

Sercem aplikacji webowej korzystającej ze Spring Framework jest plik konfiguracyjny applicationContext.xml.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns:context="http://www.springframework.org/schema/context" 
  xmlns:tx="http://www.springframework.org/schema/tx"
  xsi:schemaLocation="
    http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
    http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-2.5.xsd
    http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.5.xsd">
  <context:annotation-config />
  <bean id="osobaDao" class="pl.jaceklaskowski.wicket.model.OsobaDaoImpl" />
  <bean id="wicketApplication" class="pl.jaceklaskowski.wicket.WicketApplication" autowire="byName">
    <!-- wszystkie właściwości ustawiane po nazwie - wiele dzieje się poza naszą kontrolą -->
  </bean>
  <bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
    <property name="dataSource" ref="hsqldbDatasource" />
    <property name="loadTimeWeaver">
      <bean class="org.springframework.instrument.classloading.InstrumentationLoadTimeWeaver" />
    </property>
  </bean>
  <bean id="hsqldbDatasource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
    <property name="driverClassName" value="org.hsqldb.jdbcDriver" />
    <property name="url" value="jdbc:hsqldb:mem:wicket" />
  </bean>
  <tx:annotation-driven />
  <bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
    <property name="entityManagerFactory" ref="entityManagerFactory" />
    <property name="dataSource" ref="hsqldbDatasource" />
  </bean>
</beans>

Zwróćmy uwagę na definicję głównej klasy aplikacyjnej (bean id="wicketApplication") dla naszej aplikacji opartej o Wicket. W ten sposób (oraz z pomocą jeszcze nie prezentowanego pliku web.xml) nastąpi połączenie Wicket ze Springiem.

Kolejną ciekawostką konfiguracyjną jest wskazanie fabryki zarządców encji (bean id="entityManagerFactory") oraz związanej z nią bazą danych - HSQLDB. To, co może zaskoczyć przy użyciu bazy danych w naszej aplikacji jest fakt, że baza danych jest trzymana całkowicie w pamięci, więc nie potrzebne jest samodzielne jej uruchamianie podczas tworzenia aplikacji i jej testowania (parametr url w bean id="hsqldbDatasource" - jdbc:hsqldb:mem:wicket). Użycie elementów <tx:annotation-driven /> oraz <context:annotation-config /> pozwala na korzystanie z adnotacji @Transactional oraz @PersistenceContext w dowolnych klasach aplikacji, odpowiednio (dla zwrócenia uwagi na tę mocną stronę Springa należy wspomnieć, że w przypadku środowiska serwera aplikacji Java EE rodzaj klas podlegających mechanizmowi wstrzeliwania zależności jest bardzo zawężony). Na koniec należy wspomnieć o atrybucie autowire="byName" przy bean id="wicketApplication", który zmniejsza konieczną liczbę elementów konfiguracyjnych w pliku tak, że metody modyfikujące (ustawiające) i odczytujące atrybuty klasy pl.jaceklaskowski.wicket.WicketApplication są ustawiane zgodnie z ich nazwami i nazwami ziaren (elementy bean w pliku applicationContext.xml).

Jakkolwiek konfiguracja Springa jest zwięzła i pozwala na dostarczenie wymaganych usług do aplikacji w sposób naśladujący środowisko serwera aplikacji Java EE to należy zauważyć, że wiedza tajemna pozwalająca na stworzenie odpowiedniego pliku konfiguracyjnego Springa applicationContext.xml nie należy do małych i trywialnych.

Plik applicationContext.xml umieszczamy w katalogu src/main/resources.

Przed zakończeniem prac "administracyjnych" należy zmodyfikować plik konfiguracyjny naszej aplikacji webowej - /WEB-INF/web.xml, który znajduje się w katalogu src/main/webapp.

<?xml version="1.0" encoding="ISO-8859-1"?>
<web-app xmlns="http://java.sun.com/xml/ns/j2ee"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd" 
  version="2.4">
  <display-name>wicket-jpa-spring-demo</display-name>
  <context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>classpath:applicationContext.xml</param-value>
  </context-param>
  <listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
  </listener>
  <filter>
    <filter-name>wicket.wicket-jpa-spring-demo</filter-name>
    <filter-class>org.apache.wicket.protocol.http.WicketFilter</filter-class>
    <init-param>
      <param-name>applicationFactoryClassName</param-name>
      <param-value>org.apache.wicket.spring.SpringWebApplicationFactory</param-value>
    </init-param>
    <init-param>
      <param-name>applicationBean</param-name>
      <param-value>wicketApplication</param-value>
    </init-param>
  </filter>
  <filter-mapping>
    <filter-name>wicket.wicket-jpa-spring-demo</filter-name>
    <url-pattern>/*</url-pattern>
  </filter-mapping>
</web-app>

Istotnymi zmianami w web.xml są dodanie elementów inicjalizacyjnych Springa (listener-class) i wskazanie, że konstruowanie aplikacji webowej z użyciem Wicket będzie następowało na bazie ziaren springowych.

Zmiany w web.xml w kontekście użycia Spring Framework opisane są w mojej notatce "Globalizacja" obiektów Wicketa ze Spring Framework.

Z tak przygotowanymi plikami konfiguracyjnymi Springa oraz aplikacji webowej mamy przygotowane środowisko uruchomieniowe dla naszej aplikacji.

Po krótkiej przerwie poświęconej konfiguracji środowiska uruchomieniowego (można napisać, że występowaliśmy w roli administratora) powróćmy do tworzenia aplikacji - przydziejmy ponownie szaty programisty aplikacji.

Rozpocznijmy od utworzenia przestrzeni dla obiektów, których cykl rozwojowy związany jest z cyklem rozwojowym sesji użytkownika. W Wicket takim bytem będzie klasa rozszerzająca klasę org.apache.wicket.protocol.http.WebSession. Utwórzmy klasę pl.jaceklaskowski.wicket.WebSession.

package pl.jaceklaskowski.wicket;

import org.apache.wicket.Request;
import org.apache.wicket.protocol.http.WebSession;

import pl.jaceklaskowski.wicket.model.Osoba;

public class WicketSession extends WebSession {

    private static final long serialVersionUID = 1L;

    private Osoba osoba;

    public WicketSession(Request request) {
        super(request);
    }

    public Osoba getOsoba() {
        return osoba;
    }

    public void setOsoba(Osoba osoba) {
        this.osoba = osoba;
    }

}

W Wicket utworzenie dedykowanej klasy do zarządzania bytami w sesji służy do udostępnienia silnego typowania przechowywanych w niej obiektów. W klasie deklarujemy co i jakiego typu będzie przechowywane. W naszym przypadku w sesji znajdzie się jedynie obiekt typu pl.jaceklaskowski.wicket.model.Osoba. Jest to jeden z wyróżników Wicketa od innych szkieletów webowych mocno związanych z JSP.

Konfiguracja globalna aplikacji oraz miejsce składowania bytów w obszarze o zasięgu całej aplikacji to rola klasy rozszerzającej org.apache.wicket.protocol.http.WebApplication. Jest to klasa obowiązkowa, utworzona podczas wykonania archetypu wicket-archetype-quickstart. Wprowadźmy kilka zmian do klasy aplikacji pl.jaceklaskowski.wicket.WicketApplication, co ostatecznie da jej następującą postać.

package pl.jaceklaskowski.wicket;

import org.apache.wicket.Request;
import org.apache.wicket.Response;
import org.apache.wicket.Session;
import org.apache.wicket.protocol.http.WebApplication;

import pl.jaceklaskowski.wicket.model.Osoba;
import pl.jaceklaskowski.wicket.model.OsobaDao;

public class WicketApplication extends WebApplication {

    private OsobaDao osobaDao;

    protected void init() {
        mountBookmarkablePage("/home", HomePage.class);
        mountBookmarkablePage("/dane", DaneOsobowe.class);

        przygotujDaneWBazie();
    }

    public Class<?> getHomePage() {
        return HomePage.class;
    }

    @Override
    public Session newSession(Request request, Response response) {
        return new WicketSession(request);
    }

    public OsobaDao getOsobaDao() {
        return osobaDao;
    }

    public void setOsobaDao(OsobaDao osobaDao) {
        this.osobaDao = osobaDao;
    }

    private void przygotujDaneWBazie() {
        getOsobaDao().create(new Osoba("Jacek", "Laskowski", "jacek", "haslo", "Warszawa"));
    }
}

Klasa jest miejscem, z którego dostać można klasę DAO do obsługi danych związanych z osobami w naszej aplikacji (za pomocą interfejsu pl.jaceklaskowski.wicket.model.OsobaDao).

Dzięki metodzie przygotujDaneWBazie() i korzystając z JPA tworzymy dane w bazie danych, aby możliwe było ich późniejsze wykorzystanie. W ten sposób niwelujemy konieczność uruchamiania samodzielnej bazy danych oraz pomocniczych skryptów SQL do jej inicjalizacji.

Poza tym, montujemy jedyną naszą stronę pod adres /home za pomocą metody WebApplication.mountBookmarkablePage() (więcej w mojej notatce Upiększanie URLi w Wicket). Jest to krok opcjonalny, gdyż strona HomePage.html jest i tak wywoływana przy wejściu na adres główny aplikacji (sam kontekst aplikacji - http://localhost:8080/wicket-jpa-spring-demo/).

Stronę HomePage.html zmieniamy tak, aby ostatecznie wyglądała następująco (innymi słowy tworzymy ją od nowa):

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <title>JPA z Wicket z pomocą Spring Framework i Apache Maven 2</title>
  </head>
  <body>
    <strong>JPA z Wicket z pomocą Spring Framework i Apache Maven 2</strong>
    <form wicket:id="dane" action="">
      <table>
        <tr>
          <td>Identyfikator:</td>
          <td><input type="text" wicket:id="identyfikator" /></td>
        </tr>
        <tr>
          <td>Hasło:</td>
          <td><input type="password" wicket:id="haslo" /></td>
        </tr>
        <tr>
          <td colspan="2" style="text-align: center"><input type="submit" value="Wchodzę" /></td>
        </tr>
        <tr>
          <td colspan="3">Wersja Spring Framework: <b><span wicket:id="springVersion">Wersja Spring Framework</span></b></td>
        </tr>
        <tr>
          <td colspan="3">Wersja Apache Wicket: <b><span wicket:id="wicketVersion">Wersja Apache Wicket</span></b></td>
        </tr>
      </table>
    </form>
  </body>
</html>

I słowo wyjaśnienia, co można znaleźć na stronie. Pierwszą rzeczą jaką można zauważyć pracując z Wicket jest konieczność tworzenia dwóch plików odpowiadających stronie - pliku klasy Java (klasa strony) oraz strony HTML. Dla każdej strony HTML będzie istniała odpowiadająca jej klasa Java. Dzięki takiemu podejściu strona HTML jest czysta bez żadnych dodatków podobnych do tych spotykanych w JSP. Strona HTML jest stroną HTML z dodatkowym atrybutem wicket:id, który wskazuje na komponent zdefiniowany w odpowiadającej stronie klasie Java (wrócimy do tego za moment przy prezentacji klasy strony). Oba pliki znajdują się w tym samym katalogu w projekcie, więc strona HTML jest w katalogu odpowiadającym pakietowi jej klasy.

Kolejną istotną zmianą do tradycyjnego "programowania" stron HTML jest brak konieczności specyfikowania elementu action w formularzu, gdyż zatwierdzenie formularza będzie obsłużone przez klasę strony w metodzie protected void onSubmit() egzemplarza Form.

Na uwagę może zasługiwać również wyświelenie wersji używanego oprogramowania dla celów prezentacyjnych - Spring Framework oraz Wicketa. Zawartość elementów span zostanie podmieniona przez wartości obiektów z klasy strony wskazanych przez wicket:id.

Przejdźmy do klasy strony - pl.jaceklaskowski.wicket.HomePage.

package pl.jaceklaskowski.wicket;

import org.apache.wicket.Application;
import org.apache.wicket.PageParameters;
import org.apache.wicket.markup.html.WebPage;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.html.form.Form;
import org.apache.wicket.markup.html.form.PasswordTextField;
import org.apache.wicket.markup.html.form.RequiredTextField;
import org.apache.wicket.model.CompoundPropertyModel;
import org.springframework.core.SpringVersion;

import pl.jaceklaskowski.wicket.model.Osoba;
import pl.jaceklaskowski.wicket.model.OsobaDao;

public class HomePage extends WebPage {

    private static final long serialVersionUID = 1L;

    public HomePage(final PageParameters params) {
        CompoundPropertyModel model = new CompoundPropertyModel(new Osoba());
        Form loginForm = new Form("dane", model) {
            private static final long serialVersionUID = 1L;

            protected void onSubmit() {
                Osoba osoba = (Osoba) getModel().getObject();
                // JPA wchodzi na scenę
                String identyfikator = osoba.getIdentyfikator();
                OsobaDao osobaDao = ((WicketApplication) Application.get()).getOsobaDao();
                osoba = osobaDao.findByIdentyfikator(identyfikator);
                ((WicketSession) getSession()).setOsoba(osoba);
                setResponsePage(new DaneOsobowe(params));
            }
        };
        RequiredTextField identyfikator = new RequiredTextField("identyfikator");
        loginForm.add(identyfikator);
        // obowiązkowe domyślnie
        PasswordTextField haslo = new PasswordTextField("haslo");
        loginForm.add(haslo);
        // wyświetl wykorzystywane wersje oprogramowania - Wicketa + Springa
        loginForm.add(new Label("springVersion", SpringVersion.getVersion()));
        loginForm.add(new Label("wicketVersion", Application.get().getFrameworkSettings().getVersion()));
        add(loginForm);
    }
}

Klasa strony rozszerza klasę org.apache.wicket.markup.html.WebPage. Klasa definiuje formularz za pomocą klasy org.apache.wicket.markup.html.form.Form oraz dodaje do niej obiekty typu odpowiadającemu elementom na stronie HTML. Na zakończenie przypisywane są wersje wykorzystywanego oprogramowania.

Podczas zatwierdzenia formularza dane wykonana jest metoda protected void onSubmit(), która za pomocą klasy DAO - pl.jaceklaskowski.wicket.model.OsobaDao - pozyskanej przez klasę aplikacyjną pobierze pełne informacje o osobie w bazy danych (poprzez JPA zarządzane przez Springa). Wynikiem zatwierdzenia formularza jest wyświetlenie strony DaneOsobowe.html.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <title>JPA z Wicket z pomocą Spring Framework i Apache Maven 2</title>
  </head>
  <body>
    <strong>JPA z Wicket z pomocą Spring Framework i Apache Maven 2</strong>
    <table>
      <tr>
        <td>Imię:</td>
        <td><span wicket:id="imie"></span></td>
      </tr>
      <tr>
        <td>Nazwisko:</td>
        <td><span wicket:id="nazwisko"></span></td>
      </tr>
      <tr>
        <td>Identyfikator:</td>
        <td><span wicket:id="identyfikator"></span></td>
      </tr>
      <tr>
        <td>Miejscowość:</td>
        <td><span wicket:id="miejscowosc"></span></td>
      </tr>
    </table>
    <a href="/home">Jeszcze raz</a>
  </body>
</html>

Stronie towarzyszy klasa strony pl.jaceklaskowski.wicket.DaneOsobowe:

package pl.jaceklaskowski.wicket;

import org.apache.wicket.PageParameters;
import org.apache.wicket.markup.html.WebPage;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.model.CompoundPropertyModel;

import pl.jaceklaskowski.wicket.model.Osoba;

public class DaneOsobowe extends WebPage {

    private static final long serialVersionUID = 1L;

    public DaneOsobowe(PageParameters params) {
        Osoba osoba = ((WicketSession) getSession()).getOsoba();
        setModel(new CompoundPropertyModel(osoba));
        add(new Label("imie"));
        add(new Label("nazwisko"));
        add(new Label("identyfikator"));
        add(new Label("miejscowosc"));
    }
}

Możemy przystąpić do uruchomienia aplikacji. Wykonujemy polecenie mvn clean jetty:run i kierujemy przeglądarkę na adres http://localhost:8080/wicket-jpa-spring-demo/.

Grafika:wicket-jpa-spring-demo-home.gif

Ukaże się strona domowa aplikacji - HomePage.html, której zatwierdzenie formularza prowadzi do strony DaneOsobowe.html z wyświetleniem danych użytkownika uzupełnionych o dane z bazy danych za pomocą JPA.

Grafika:wicket-jpa-spring-demo-dane.gif

Oczywiście każdorazowe wejście na stronę http://localhost:8080/wicket-jpa-spring-demo/dane spowoduje wyświetlenie danych aktualnego użytkownika. Brak obsługi sytuacji, w której wyświetlamy dane użytkownika zanim został on pobrany i umieszczony w sesji pozostawiam jako zadanie domowe.

Kompletny projekt aplikacji dostępny jest do pobrania jako wicket-jpa-spring-demo.zip.

Osobiste