Skip to content

3. JPA in einer mehrschichtigen Architektur

Um API JPA zu untersuchen, haben wir die folgende Testarchitektur verwendet:

Unsere Testprogramme waren Konsolenanwendungen, die die JPA-Schicht direkt abfragten. Dabei haben wir die wichtigsten Methoden der JPA-Schicht kennengelernt. Wir befanden uns in einer sogenannten „Java SE“-Umgebung (Standard Edition). JPA läuft sowohl in einer Java SE- als auch in einer Java EE5-Umgebung (Enterprise Edition).

Nachdem wir nun sowohl die Konfiguration der Relational-Objekt-Brücke als auch die Verwendung der Methoden der JPA-Schicht einigermaßen beherrschen, kehren wir zu einer klassischeren mehrschichtigen Architektur zurück:

Der Zugriff auf die Schicht [JPA] erfolgt über eine zweischichtige Architektur aus [metier] und [dao]. Das Spring-Framework [7] sowie der Container EJB3 von JBoss und [8] werden verwendet, um diese Schichten miteinander zu verbinden.

Wir haben oben erwähnt, dass JPA in den Umgebungen SE und EE5 verfügbar ist. Die Java-Umgebung EE5 stellt zahlreiche Dienste im Bereich des Zugriffs auf persistente Daten bereit, darunter insbesondere Verbindungspools, Transaktionsmanager usw. Für Entwickler kann es interessant sein, diese Dienste zu nutzen. Die Java-Umgebung EE5 ist (Stand Mai 2007) noch nicht sehr verbreitet. Sie ist derzeit auf dem Sun Application Server 9.x (Glassfish) zu finden. Ein Anwendungsserver ist im Wesentlichen ein Webanwendungsserver. Wenn man eine eigenständige grafische Anwendung vom Typ Swing erstellt, stehen die EE-Umgebung und die damit verbundenen Dienste nicht zur Verfügung. Das ist ein Problem. Es tauchen allmählich „Stand-alone“-Umgebungen wie EE und c.a.d auf. die außerhalb eines Anwendungsservers genutzt werden können. Dies gilt beispielsweise für JBoss und EJB3, die wir in diesem Dokument verwenden werden.

In einer EE5-Umgebung werden die Schichten durch Objekte namens EJB (Enterprise Java Bean) implementiert. In früheren Versionen von EE galten die EJB (EJB, 2.x) als schwer zu implementieren, zu testen und manchmal als leistungsschwach. Man unterscheidet zwischen den „Entity“-EJB2.x und den „Session“-EJB2.x. Kurz gesagt ist ein EJB2.x „Entity“ die Darstellung einer Zeile aus einer Datenbanktabelle und ein EJB2.x „Session“ ein Objekt, das zur Implementierung der Schichten [metier], [dao] einer mehrschichtigen Architektur. Einer der Hauptkritikpunkte an den mit EJB implementierten Schichten ist, dass sie nur innerhalb von EJB-Containern verwendet werden können, einem Dienst, der von der EE-Umgebung bereitgestellt wird. Dies erschwert das Durchführen von Unit-Tests. So sind im obigen Schema würden die Unit-Tests der Schichten [metier] und [dao], die mit EJB erstellt wurden, die Einrichtung eines Anwendungsservers erfordern – ein recht aufwendiger Vorgang, der den Entwickler nicht gerade dazu motiviert, häufig Tests durchzuführen.

Das Spring-Framework entstand als Reaktion auf die Komplexität von EJB2. Spring stellt in einer SE-Umgebung einen Großteil der Dienste bereit, die üblicherweise von EE-Umgebungen bereitgestellt werden. So stellt Spring im Bereich „Datenpersistenz“, der uns hier interessiert, die Verbindungspools und Transaktionsmanager bereit, die Anwendungen benötigen. Das Aufkommen von Spring hat die Kultur der Unit-Tests gefördert, deren Umsetzung dadurch auf einen Schlag wesentlich einfacher wurde. Spring ermöglicht die Implementierung der Schichten einer Anwendung durch klassische Java-Objekte (POJO, Plain Old/Ordinary Java Object), wodurch deren Wiederverwendung in einem anderen Kontext möglich wird. Schließlich integriert es zahlreiche Tools von Drittanbietern auf recht transparente Weise, insbesondere Persistenz-Tools wie Hibernate, iBatis, ...

Java EE5 wurde entwickelt, um die Mängel der vorherigen Spezifikation EE zu beheben. Die EJB und 2.x wurden zu EJB3. Dabei handelt es sich um POJOs, die mit Annotationen versehen sind, wodurch sie zu besonderen Objekten werden, wenn sie sich innerhalb eines EJB3-Containers befinden. In diesem Container kann das EJB3 die Dienste des Containers nutzen (Verbindungspool, Transaktionsmanager usw.). Außerhalb des Containers EJB3 wird das EJB3 zu einem normalen Java-Objekt. Seine Annotationen EJB werden ignoriert.

Oben haben wir Spring und JBoss EJB3 als mögliche Infrastruktur (Framework) unserer mehrschichtigen Architektur dargestellt. Diese Infrastruktur stellt die von uns benötigten Dienste bereit: einen Verbindungspool und einen Transaktionsmanager.

  • Mit Spring werden die Schichten mithilfe von POJOs implementiert. Diese haben Zugriff auf die Dienste von Spring (Verbindungspool, Transaktionsmanager) durch Abhängigkeitsinjektion in diese POJOs: Beim Erstellen dieser Komponenten injiziert Spring ihnen Referenzen auf die Dienste, die sie benötigen werden.
  • JBoss EJB3 ist ein EJB-Container, der außerhalb eines Anwendungsservers betrieben werden kann. Seine Funktionsweise (aus Sicht des Entwicklers) entspricht der für Spring beschriebenen. Es gibt kaum Unterschiede.

Wir schließen das Dokument mit einem einfachen, aber dennoch repräsentativen Beispiel für eine dreischichtige Webanwendung ab:

3.1. Beispiel 1: Spring / JPA mit der Entität „Person“

Wir nehmen die in Abschnitt 2.1 behandelte Entität Personne und integrieren sie in eine mehrschichtige Architektur, bei der die Integration der Schichten mit Spring erfolgt und die Persistenzschicht durch Hibernate implementiert wird.

Es wird vorausgesetzt, dass der Leser über Grundkenntnisse zu Spring verfügt. Sollte dies nicht der Fall sein, kann das folgende Dokument gelesen werden, in dem das Konzept der Abhängigkeitsinjektion erläutert wird, das das Herzstück von Spring bildet:

[ref3]: Spring IoC (Inversion of Control) [http://tahe.developpez.com/java/springioc].

3.1.1. Das Eclipse-/Spring-/Hibernate-Projekt „

Das Eclipse-Projekt lautet wie folgt:

  • in [1]: das Eclipse-Projekt. Es befindet sich in [6] in den Beispielen des Tutorials [5]. Wir werden es importieren.
  • in [2]: die Java-Codes der Schichten, die in Paketen dargestellt sind:
    • [entites]: das Entitäten-Paket JPA
    • [dao]: die Datenzugriffsebene – basiert auf der Ebene JPA
    • [service]: eine eher dienstorientierte als geschäftsorientierte Schicht. Hier wird der Transaktionsdienst der Container genutzt.
    • [tests]: Fasst die Testprogramme zusammen.
  • in [3]: Die Bibliothek [jpa-spring] enthält die für Spring erforderlichen JAR-Dateien (siehe auch [7] und [8]).
  • in [4]: Der Ordner [conf] enthält die Spring-Konfigurationsdateien für jedes der in diesem Tutorial verwendeten SGBD.

3.1.2. Die Entitäten JPA

Hier wird nur eine Entität verwaltet, nämlich die in Abschnitt 2.1 behandelte Entität Personne, deren Konfiguration wir im Folgenden noch einmal zusammenfassen:


package entites;

...
@Entity
@Table(name="jpa01_hb_personne")
public class Personne {

    @Id
    @Column(name = "ID", nullable = false)
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Integer id;

    @Column(name = "VERSION", nullable = false)
    @Version
    private int version;

    @Column(name = "NOM", length = 30, nullable = false, unique = true)
    private String nom;

    @Column(name = "PRENOM", length = 30, nullable = false)
    private String prenom;

    @Column(name = "DATENAISSANCE", nullable = false)
    @Temporal(TemporalType.DATE)
    private Date datenaissance;

    @Column(name = "MARIE", nullable = false)
    private boolean marie;

    @Column(name = "NBENFANTS", nullable = false)
    private int nbenfants;

    // Konstruktoren
    public Personne() {
    }

    public Personne(String nom, String prenom, Date datenaissance, boolean marie,
            int nbenfants) {
...
    }

    // toString
    public String toString() {
        return String.format("[%d,%d,%s,%s,%s,%s,%d]", getId(), getVersion(),
                getNom(), getPrenom(), new SimpleDateFormat("dd/MM/yyyy")
                        .format(getDatenaissance()), isMarie(), getNbenfants());
    }

    // Getter und Setter
...
}

3.1.3. Die Ebene [dao]

Die Schicht [dao] weist die folgende Schnittstelle IDao auf:


package dao;

import java.util.List;

import entites.Personne;

public interface IDao {
    // eine Person anhand ihrer ID abrufen
    public Personne getOne(Integer id);

    // alle Personen abrufen
    public List<Personne> getAll();

    // eine Person speichern
    public Personne saveOne(Personne personne);

    // eine Person aktualisieren
    public Personne updateOne(Personne personne);

    // Eine Person anhand ihrer ID löschen
    public void deleteOne(Integer id);

    // Personen abrufen, deren Name einem Muster entspricht
    public List<Personne> getAllLike(String modele);

}

Die Implementierung [Dao] dieser Schnittstelle lautet wie folgt:


package dao;

import java.util.List;

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

import entites.Personne;

public class Dao implements IDao {

    @PersistenceContext
    private EntityManager em;

    // eine Person anhand ihrer ID löschen
    public void deleteOne(Integer id) {
        Personne personne = em.find(Personne.class, id);
        if (personne == null) {
            throw new DaoException(2);
        }
        em.remove(personne);
    }

    @SuppressWarnings("unchecked")
    // Alle Personen abrufen
    public List<Personne> getAll() {
        return em.createQuery("select p from Personne p").getResultList();
    }

    @SuppressWarnings("unchecked")
    // Personen abrufen, deren Name einem Muster entspricht
    public List<Personne> getAllLike(String modele) {
        return em.createQuery("select p from Personne p where p.nom like :modele")
                .setParameter("modele", modele).getResultList();
    }

    // eine Person anhand ihrer ID abrufen
    public Personne getOne(Integer id) {
        return em.find(Personne.class, id);
    }

    // eine Person speichern
    public Personne saveOne(Personne personne) {
        em.persist(personne);
        return personne;
    }

    // eine Person aktualisieren
    public Personne updateOne(Personne personne) {
        return em.merge(personne);
    }

}
  • Zunächst ist die Einfachheit der Implementierung [Dao] zu beachten. Diese ist auf die Verwendung der Schicht JPA zurückzuführen, die den Großteil der Arbeit beim Datenzugriff übernimmt.
  • Zeile 10: Die Klasse [Dao] implementiert die Schnittstelle [IDao]
  • Zeile 13: Das Objekt vom Typ [EntityManager], das zur Bearbeitung des Persistenzkontexts JPA verwendet wird. Im weiteren Verlauf werden wir es um der Einfachheit willen manchmal mit dem Persistenzkontext selbst verwechseln. Der Persistenzkontext enthält Entitäten vom Typ Personne.
  • Zeile 12: Das Feld [EntityManager em] wird an keiner Stelle im Code initialisiert. Es wird beim Start der Anwendung von Spring initialisiert. Es ist die Annotation JPA @PersistenceContext in Zeile 12, die Spring anweist, einen Persistenzkontext-Manager in em zu injizieren.
  • Zeilen 26–28: Die Liste aller Personen wird über eine Abfrage JPQL abgerufen.
  • Zeilen 32–35: Die Liste aller Personen, deren Name einem bestimmten Muster entspricht, wird über eine Abfrage JPQL abgerufen.
  • Zeilen 38–40: Die Person mit dieser Kennung wird über die Methode „find“ von API JPA abgerufen. Gibt einen Zeiger null zurück, wenn die Person nicht existiert.
  • Zeilen 43–46: Eine Person wird durch die Methode „persist“ des API JPA dauerhaft gespeichert. Die Methode sorgt dafür, dass die Person dauerhaft gespeichert wird.
  • Zeilen 49–51: Die Aktualisierung einer Person erfolgt über die Methode „merge“ von API JPA. Diese Methode ist nur dann sinnvoll, wenn die so aktualisierte Person zuvor nicht persistiert war. Die Methode macht die so erstellte Person persistent.
  • Zeilen 16–22: Das Löschen der Person, deren ID als Parameter übergeben wird, erfolgt in zwei Schritten:
    • Zeile 17: Sie wird im Persistenzkontext gesucht
    • Zeilen 18–20: Wird sie nicht gefunden, wird eine Ausnahme mit dem Fehlercode 2 ausgelöst
    • Zeile 21: Wurde sie gefunden, wird sie mit der Methode „remove“ von API JPA aus dem Persistenzkontext entfernt.
  • Was derzeit noch nicht ersichtlich ist: Jede Methode wird innerhalb einer Transaktion ausgeführt, die von der Schicht [service] gestartet wird.

Die Anwendung verfügt über einen eigenen Ausnahmetyp namens [DaoException]:


package dao;

@SuppressWarnings("serial")
public class DaoException extends RuntimeException {

    // Fehlercode
    private int code;

    public DaoException(int code) {
        super();
        this.code = code;
    }

    public DaoException(String message, int code) {
        super(message);
        this.code = code;
    }

    public DaoException(Throwable cause, int code) {
        super(cause);
        this.code = code;
    }

    public DaoException(String message, Throwable cause, int code) {
        super(message, cause);
        this.code = code;
    }

    // Getter und Setter

    public int getCode() {
        return code;
    }

    public void setCode(int code) {
        this.code = code;
    }

}
  • Zeile 4: [DaoException] leitet sich von [RuntimeException] ab. Es handelt sich also um einen Ausnahmetyp, den der Compiler nicht zwingend mit einem try/catch-Block behandeln oder in die Signatur der Methoden aufnehmen lässt. Aus diesem Grund ist [DaoException] nicht in der Signatur der Methode [deleteOne] der Schnittstelle [IDao] enthalten. Dadurch kann diese Schnittstelle von einer Klasse implementiert werden, die einen anderen Ausnahmetyp auslöst, sofern diese ebenfalls von [RuntimeException] abgeleitet ist.
  • Um die möglicherweise auftretenden Fehler zu unterscheiden, wird der Fehlercode aus Zeile 7 verwendet. Die drei Konstruktoren in den Zeilen 14, 19 und 24 sind diejenigen der übergeordneten Klasse [RuntimeException], denen ein Parameter hinzugefügt wurde: der Fehlercode, der der Ausnahme zugewiesen werden soll.

3.1.4. Die Schicht [metier / service]

Die Schicht [service] weist die folgende Schnittstelle [IService] auf:


package service;

import java.util.List;

import entites.Personne;

public interface IService {
    // Eine Person anhand ihrer ID abrufen
    public Personne getOne(Integer id);

    // Alle Personen abrufen
    public List<Personne> getAll();

    // Eine Person speichern
    public Personne saveOne(Personne personne);

    // Eine Person aktualisieren
    public Personne updateOne(Personne personne);

    // Eine Person anhand ihrer ID löschen
    public void deleteOne(Integer id);

    // Personen abrufen, deren Name einem Muster entspricht
    public List<Personne> getAllLike(String modele);

    // Mehrere Personen gleichzeitig löschen
    public void deleteArray(Personne[] personnes);

    // Mehrere Personen gleichzeitig speichern
    public Personne[] saveArray(Personne[] personnes);

    // Mehrere Personen gleichzeitig aktualisieren
    public Personne[] updateArray(Personne[] personnes);

}
  • Zeilen 8–24: Die Schnittstelle [IService] übernimmt die Methoden der Schnittstelle [IDao]
  • Zeile 27: Die Methode [deleteArray] ermöglicht es, eine Gruppe von Personen innerhalb einer Transaktion zu löschen: Entweder werden alle Personen gelöscht oder keine.
  • Zeilen 30 und 33: Methoden analog zu [deleteArray] zum Speichern (Zeile 30) oder Aktualisieren (Zeile 33) einer Gruppe von Personen innerhalb einer Transaktion.

Die Implementierung [Service] der Schnittstelle [IService] lautet wie folgt:


package service;

...

// Alle Methoden der Klasse werden in einer Transaktion ausgeführt
@Transactional
public class Service implements IService {

    // Schicht [dao]
    private IDao dao;

    public IDao getDao() {
        return dao;
    }

    public void setDao(IDao dao) {
        this.dao = dao;
    }

    // Mehrere Personen gleichzeitig löschen
    public void deleteArray(Personne[] personnes) {
        for (Personne p : personnes) {
            dao.deleteOne(p.getId());
        }
    }

    // Eine Person anhand ihrer ID löschen
    public void deleteOne(Integer id) {
        dao.deleteOne(id);
    }

    // Alle Personen abrufen
    public List<Personne> getAll() {
        return dao.getAll();
    }

    // Personen abrufen, deren Name einem Muster entspricht
    public List<Personne> getAllLike(String modele) {
        return dao.getAllLike(modele);
    }

    // eine Person anhand ihrer ID abrufen
    public Personne getOne(Integer id) {
        return dao.getOne(id);
    }

    // Mehrere Personen gleichzeitig speichern
    public Personne[] saveArray(Personne[] personnes) {
        Personne[] personnes2 = new Personne[personnes.length];
        for (int i = 0; i < personnes.length; i++) {
            personnes2[i] = dao.saveOne(personnes[i]);
        }
        return personnes2;
    }

    // eine Person speichern
    public Personne saveOne(Personne personne) {
        return dao.saveOne(personne);
    }

    // Mehrere Personen gleichzeitig aktualisieren
    public Personne[] updateArray(Personne[] personnes) {
        Personne[] personnes2 = new Personne[personnes.length];
        for (int i = 0; i < personnes.length; i++) {
            personnes2[i] = dao.updateOne(personnes[i]);
        }
        return personnes2;
    }

    // Eine Person aktualisieren
    public Personne updateOne(Personne personne) {
        return dao.updateOne(personne);
    }

}
  • Zeile 6: Die Spring-Annotation @Transactional gibt an, dass alle Methoden der Klasse innerhalb einer Transaktion ausgeführt werden müssen. Eine Transaktion wird vor Beginn der Methodenausführung gestartet und nach der Ausführung abgeschlossen. Tritt während der Ausführung der Methode eine Ausnahme vom Typ [RuntimeException] oder einer davon abgeleiteten Ausnahme auf, wird die gesamte Transaktion durch ein automatisches Rollback rückgängig gemacht; andernfalls wird sie durch ein automatisches Commit bestätigt. Es ist zu beachten, dass sich der Java-Code nicht um Transaktionen kümmern muss. Diese werden von Spring verwaltet.
  • Zeile 10: eine Referenz auf die Schicht [dao]. Wir werden später sehen, dass diese Referenz von Spring beim Start der Anwendung initialisiert wird.
  • Die Methoden von [Service] beschränken sich darauf, die Methoden der Schnittstelle [IDao dao] aus Zeile 10 aufzurufen. Wir überlassen es dem Leser, sich mit dem Code vertraut zu machen. Es gibt keine besonderen Schwierigkeiten.
  • Wir haben zuvor erwähnt, dass jede Methode von [Service] in einer Transaktion ausgeführt wird. Diese ist an den Ausführungsthread der Methode gebunden. In diesem Thread werden Methoden der Schicht [dao] ausgeführt. Diese werden automatisch der Transaktion des Ausführungsthreads zugeordnet. Die Methode [deleteArray] (Zeile 21) muss beispielsweise die Methode [deleteOne] der Schicht [dao] N-mal ausführen. Diese N Ausführungen finden innerhalb des Ausführungsthreads der Methode [deleteArray] statt, also innerhalb derselben Transaktion. Daher werden sie entweder alle bestätigt (Commit), wenn alles reibungslos verläuft, oder alle rückgängig gemacht (Rollback), wenn in einer der N Ausführungen der Methode [deleteOne] der Schicht [dao] eine Ausnahme auftritt.

3.1.5. Konfiguration der Schichten

Die Konfiguration der Schichten [service], [dao] und [JPA] erfolgt über die beiden oben genannten Dateien: [META-INF/persistence.xml] und [spring-config.xml]. Beide Dateien müssen sich im Ordner „classpath“ der Anwendung befinden, weshalb sie im Ordner „[src]“ des Eclipse-Projekts abgelegt sind. Der Name der Datei „[spring-config.xml]“ kann frei gewählt werden.

persistence.xml


<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.0" 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">
    <persistence-unit name="jpa" transaction-type="RESOURCE_LOCAL" />
</persistence>
  • Zeile 4: Die Datei deklariert eine Persistenzeinheit namens jpa, die „lokale“ Transaktionen (c.a.d) verwendet, die nicht von einem Container (EJB3) bereitgestellt werden. Diese Transaktionen werden von Spring erstellt und verwaltet und sind Gegenstand von Konfigurationen in der Datei [spring-config.xml].

spring-config.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:tx="http://www.springframework.org/schema/tx"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.0.xsd">

    <!-- Anwendungsschichten -->
    <bean id="dao" class="dao.Dao" />
    <bean id="service" class="service.Service">
        <property name="dao" ref="dao" />
    </bean>

    <!-- Persistenzschicht JPA -->
    <bean id="entityManagerFactory"
        class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
        <property name="dataSource" ref="dataSource" />
        <property name="jpaVendorAdapter">
            <bean
                class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter">
                <!-- 
                    <property name="showSql" value="true" />
                -->
                <property name="databasePlatform"
                    value="org.hibernate.dialect.MySQL5InnoDBDialect" />
                <property name="generateDdl" value="true" />
            </bean>
        </property>
        <property name="loadTimeWeaver">
            <bean
                class="org.springframework.instrument.classloading.InstrumentationLoadTimeWeaver" />
        </property>
    </bean>

    <!-- die Datenquelle DBCP -->
    <bean id="dataSource"
        class="org.apache.commons.dbcp.BasicDataSource"
        destroy-method="close">
        <property name="driverClassName" value="com.mysql.jdbc.Driver" />
        <property name="url" value="jdbc:mysql://localhost:3306/jpa" />
        <property name="username" value="jpa" />
        <property name="password" value="jpa" />
    </bean>

    <!-- Transaktionsmanager -->
    <tx:annotation-driven transaction-manager="txManager" />
    <bean id="txManager"
        class="org.springframework.orm.jpa.JpaTransactionManager">
        <property name="entityManagerFactory"
            ref="entityManagerFactory" />
    </bean>

    <!-- Ausnahmeübersetzung -->
    <bean
        class="org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor" />

    <!-- Persistenz-Annotationen -->
    <bean
        class="org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor" />

</beans>
  • Zeilen 2–5: Das Stamm-Tag <beans> der Konfigurationsdatei. Wir gehen hier nicht näher auf die verschiedenen Attribute dieses Tags ein. Achten Sie darauf, den Text zu kopieren und einzufügen, da Fehler bei einem dieser Attribute zu Fehlern führen können, die manchmal schwer zu verstehen sind.
  • Zeile 8: Die Bean „dao“ ist eine Referenz auf eine Instanz der Klasse [dao.Dao]. Es wird eine einzige Instanz erstellt (Singleton), die die Schicht [dao] der Anwendung implementiert.
  • Zeilen 9–11: Instanziierung der Schicht [service]. Die Bean „service“ ist eine Referenz auf eine Instanz der Klasse [service.Service]. Es wird eine einzige Instanz erstellt (Singleton), die die Schicht [service] der Anwendung implementiert. Wir haben gesehen, dass die Klasse [service.Service] ein privates Feld [IDao dao] besitzt. Dieses Feld wird in Zeile 10 durch die in Zeile 8 definierte „dao“-Bean initialisiert.
  • Letztendlich haben die Zeilen 8–11 die Schichten [dao] und [service] konfiguriert. Wir werden später sehen, wann und wie sie instanziiert werden.
  • Zeilen 35–42: Eine Datenquelle wird definiert. Wir sind dem Begriff der Datenquelle bereits bei der Betrachtung der Entitäten JPA mit Hibernate begegnet:

Oben hätte [c3p0], das als „Verbindungspool“ bezeichnet wird, auch als „Datenquelle“ bezeichnet werden können. Eine Datenquelle stellt den Dienst „Verbindungspool“ bereit. Bei Spring werden wir eine andere Datenquelle als [c3p0] verwenden. Es handelt sich um [DBCP] aus dem Apache-Commons-Projekt DBCP [http://jakarta.apache.org/commons/dbcp/]. Die Archive von [DBCP] wurden in der Benutzerbibliothek [jpa-spring] abgelegt:

 
  • Zeilen 38–41: Um Verbindungen zur Zieldatenbank herzustellen, muss die Datenquelle den verwendeten JDBC-Treiber (Zeile 38), die URL der Datenbank (Zeile 39) sowie den Benutzernamen und das Passwort für die Verbindung (Zeilen 40–41) kennen.
  • Zeilen 14–32: Konfigurieren die Schicht JPA
  • Zeilen 14–15: Definieren eine Bean vom Typ [EntityManagerFactory], die Objekte vom Typ [EntityManager] erstellen kann, um Persistenzkontexte zu verwalten. Die instanziierte Klasse [LocalContainerEntityManagerFactoryBean] wird von Spring bereitgestellt. Sie benötigt für die Instanziierung eine Reihe von Parametern, die in den Zeilen 16–31 definiert sind.
  • Zeile 16: Die Datenquelle, die verwendet werden soll, um Verbindungen zu SGBD herzustellen. Dabei handelt es sich um die in den Zeilen 35–42 definierte Quelle [DBCP].
  • Zeilen 17–27: Die zu verwendende Implementierung JPA
  • Zeilen 18–26: Definieren Hibernate (Zeile 19) als die zu verwendende Implementierung JPA
  • Zeilen 23–24: Der Dialekt SQL, den Hibernate mit dem Ziel-SGBD verwenden soll, hier MySQL5.
  • Zeile 25: Legt fest, dass beim Start der Anwendung die Datenbank generiert werden soll (drop und create).
  • Zeilen 28–31: Definieren einen „Klassenlader“. Ich kann die Rolle dieses Beans, das von EntityManagerFactory in der Schicht JPA verwendet wird, nicht klar erklären. Jedenfalls bedeutet dies, dass an die JVM, die die Anwendung ausführt, der Name eines Archivs übergeben wird, dessen Inhalt das Laden der Klassen beim Start der Anwendung verwaltet. In diesem Fall ist dieses Archiv [spring-agent.jar], das sich in der Benutzerbibliothek [jpa-spring] befindet (siehe oben). Wir werden sehen, dass Hibernate diesen Agenten nicht benötigt, Toplink jedoch schon.
  • Zeilen 45–50: Definieren den zu verwendenden Transaktionsmanager
  • Zeile 45: Gibt an, dass Transaktionen mit Java-Annotationen verwaltet werden (sie hätten auch in spring-config.xml deklariert werden können). Dabei handelt es sich insbesondere um die Annotation @Transactional, die in der Klasse [Service] (Zeile 6) vorkommt.
  • Zeilen 46–50: Der Transaktionsmanager
  • Zeile 47: Der Transaktionsmanager ist eine von Spring bereitgestellte Klasse
  • Zeilen 48–49: Der Spring-Transaktionsmanager muss die Klasse EntityManagerFactory kennen, die die Schicht JPA verwaltet. Diese ist in den Zeilen 14–32 definiert.
  • Zeilen 57–58: Definieren die Klasse, die die im Java-Code gefundenen Spring-Persistenz-Annotationen verwaltet, wie beispielsweise die Annotation @PersistenceContext der Klasse [dao.Dao] (Zeile 12).
  • Zeilen 53–54: Definieren die Spring-Klasse, die insbesondere die Annotation @Repository verwaltet, wodurch eine so annotierte Klasse für die Übersetzung der nativen Ausnahmen des JDBC-Treibers vom Typ SGBD in generische Spring-Ausnahmen vom Typ [DataAccessException] in Frage kommt. Diese Umwandlung kapseln die native JDBC-Ausnahme in einen Typ [DataAccessException] mit verschiedenen Unterklassen:

Image

Diese Übersetzung ermöglicht es dem Client-Programm, Ausnahmen generisch zu behandeln, unabhängig vom Ziel-SGBD. Wir haben die Annotation @Repository in unserem Java-Code nicht verwendet. Daher sind die Zeilen 53–54 überflüssig. Wir haben sie lediglich zu Informationszwecken belassen.

Damit sind wir mit der Spring-Konfigurationsdatei fertig. Sie ist komplex und vieles bleibt unklar. Sie wurde der Spring-Dokumentation entnommen. Glücklicherweise beschränkt sich ihre Anpassung an verschiedene Situationen oft auf zwei Änderungen:

  • die Anpassung der Zieldatenbank: Zeilen 38–41. Wir werden ein Oracle-Beispiel anführen.
  • die Anpassung der Implementierung JPA: Zeilen 14–32. Wir geben ein Toplink-Beispiel.

3.1.6. Client-Programm [InitDB]

Wir befassen uns nun mit der Erstellung eines ersten Clients für die zuvor beschriebene Architektur:

Der Code von [InitDB] lautet wie folgt:


package tests;

...
public class InitDB {

    // Service-Schicht
    private static IService service;

    // Konstruktor
    public static void main(String[] args) throws ParseException {
        // Anwendungskonfiguration
        ApplicationContext ctx = new ClassPathXmlApplicationContext("spring-config.xml");
        // Service-Schicht
        service = (IService) ctx.getBean("service");
        // Die Datenbank wird geleert
        clean();
        // die Datenbank wird gefüllt
        fill();
        // Sichtprüfung
        dumpPersonnes();
    }

    // Anzeige des Tabelleninhalts
    private static void dumpPersonnes() {
        System.out.format("[personnes]%n");
        for (Personne p : service.getAll()) {
            System.out.println(p);
        }
    }

    // Tabelle füllen
    public static void fill() throws ParseException {
        // Personen anlegen
        Personne p1 = new Personne("p1", "Paul", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2);
        Personne p2 = new Personne("p2", "Sylvie", new SimpleDateFormat("dd/MM/yy").parse("05/07/2001"), false, 0);
        // die gespeichert werden
        service.saveArray(new Personne[] { p1, p2 });
    }

    // Elemente aus der Tabelle löschen
    public static void clean() {
        for (Personne p : service.getAll()) {
            service.deleteOne(p.getId());
        }
    }
}
  • Zeile 12: Die Datei [spring-config.xml] wird verwendet, um ein Objekt [ApplicationContext ctx] zu erstellen, bei dem es sich um ein Speicherabbild der Datei handelt. Die in [spring-config.xml] definierten Beans werden dabei instanziiert.
  • Zeile 14: Vom Anwendungskontext ctx wird eine Referenz auf die Schicht [service] angefordert. Es ist bekannt, dass diese durch eine Bean namens „service“ repräsentiert wird.
  • Zeile 16: Die Datenbank wird mithilfe der Methode clean geleert. Zeilen 41–45:
    • Zeilen 42–44: Vom Persistenzkontext wird die Liste aller Personen angefordert, und in einer Schleife werden diese nacheinander gelöscht. Man erinnert sich vielleicht daran, dass [spring-config.xml] festlegt, dass die Datenbank beim Start der Anwendung generiert werden muss. Daher ist in unserem Fall der Aufruf der Methode clean überflüssig, da wir von einer leeren Datenbank ausgehen.
  • Zeile 18: Die Methode fill füllt die Datenbank. Diese wird in den Zeilen 32–38 definiert:
    • Zeilen 34–35: Es werden zwei Personen angelegt
    • Zeile 37: Die Schicht [service] wird aufgefordert, diese dauerhaft zu speichern.
  • Zeile 20: Die Methode dumpPersonnes zeigt die persistenten Personen an. Sie ist in den Zeilen 24–29 definiert
    • Zeilen 26–28: Von der Schicht [service] wird die Liste aller persistenten Personen angefordert und auf der Konsole angezeigt.

Die Ausführung von [InitDB] liefert folgendes Ergebnis:

1
2
3
[personnes]
[72,0,p1,Paul,31/01/2000,true,2]
[73,0,p2,Sylvie,05/07/2001,false,0]

3.1.7. Einzeltests [TestNG]

Die Installation des Plugins [TestNG] wird in Abschnitt 5.2.4 beschrieben. Der Programmcode von [TestNG] lautet wie folgt:


package tests;

....
public class TestNG {

    // Service-Ebene
    private IService service;

    @BeforeClass
    public void init() {
        // Protokoll
        log("init");
        // Anwendungskonfiguration
        ApplicationContext ctx = new ClassPathXmlApplicationContext("spring-config.xml");
        // Service-Schicht
        service = (IService) ctx.getBean("service");
    }

    @BeforeMethod
    public void setUp() throws ParseException {
        // Datenbank leeren
        clean();
        // die Datenbank wird gefüllt
        fill();
    }

    // Protokolle
    private void log(String message) {
        System.out.println("----------- " + message);
    }

    // Tabelleninhalt anzeigen
    private void dump() {
        log("dump");
        System.out.format("[personnes]%n");
        for (Personne p : service.getAll()) {
            System.out.println(p);
        }
    }

    // Tabelle ausfüllen
    public void fill() throws ParseException {
        log("fill");
        // Personen anlegen
        Personne p1 = new Personne("p1", "Paul", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2);
        Personne p2 = new Personne("p2", "Sylvie", new SimpleDateFormat("dd/MM/yy").parse("05/07/2001"), false, 0);
        // die gespeichert werden
        service.saveArray(new Personne[] { p1, p2 });
    }

    // Tabellenelemente löschen
    public void clean() {
        log("clean");
        for (Personne p : service.getAll()) {
            service.deleteOne(p.getId());
        }
    }

    @Test()
    public void test01() {
...
    }
...
}
  • Zeile 9: Die Anmerkung @BeforeClass bezeichnet die Methode, die ausgeführt werden muss, um die für die Tests erforderliche Konfiguration zu initialisieren. Sie wird ausgeführt, bevor der erste Test gestartet wird. Die hier nicht verwendete Annotation @AfterClass bezeichnet die Methode, die ausgeführt werden soll, sobald alle Tests abgeschlossen sind.
  • Zeilen 10–17: Die mit @BeforeClass annotierte Methode „init“ nutzt die Spring-Konfigurationsdatei, um die verschiedenen Schichten der Anwendung zu instanziieren und eine Referenz auf die Schicht [service] zu erhalten. Alle Tests verwenden anschließend diese Referenz.
  • Zeile 19: Die Annotation @BeforeMethod bezeichnet die Methode, die vor jedem Test ausgeführt werden soll. Die hier nicht verwendete Annotation @AfterMethod bezeichnet die Methode, die nach jedem Test ausgeführt werden soll.
  • Zeilen 20–25: Die mit @BeforeMethod annotierte Methode setUp leert die Datenbank (clean, Zeilen 52–56) und füllt sie anschließend mit zwei Personen (fill, Zeilen 42–49).
  • Zeile 59: Die Anmerkung @Test bezeichnet eine auszuführende Testmethode. Wir beschreiben nun diese Tests.

@Test()
    public void test01() {
        log("test1");
        dump();
        // Liste der Personen
        List<Personne> personnes = service.getAll();
        assert 2 == personnes.size();
    }

    @Test()
    public void test02() {
        log("test2");
        // Suche nach Personen anhand ihres Namens
        List<Personne> personnes = service.getAllLike("p1%");
        assert 1 == personnes.size();
        Personne p1 = personnes.get(0);
        assert "Paul".equals(p1.getPrenom());
    }

    @Test()
    public void test03() throws ParseException {
        log("test3");
        // Anlegen einer neuen Person
        Personne p3 = new Personne("p3", "x", new SimpleDateFormat("dd/MM/yy").parse("05/07/2001"), false, 0);
        // Speichern der Person
        service.saveOne(p3);
        // erneutes Abrufen
        Personne loadedp3 = service.getOne(p3.getId());
        // Anzeige der Person
        System.out.println(loadedp3);
        // Überprüfung
        assert "p3".equals(loadedp3.getNom());
    }
  • Zeilen 2–8: Test 01. Es ist zu beachten, dass die Datenbank zu Beginn jedes Tests zwei Personen mit den Namen p1 und p2 enthält.
  • Zeile 6: Die Liste der Personen wird abgefragt
  • Zeile 7: Es wird überprüft, ob die Anzahl der Personen in der erhaltenen Liste 2 beträgt
  • Zeile 14: Es wird die Liste der Personen angefordert, deren Nachname mit p1 beginnt
  • Es wird überprüft, ob die erhaltene Liste nur ein Element enthält (Zeile 15) und ob der Vorname der einzigen gefundenen Person „Paul“ lautet (Zeile 17)
  • Zeile 24: Es wird eine Person namens p3 angelegt
  • Zeile 25: Sie wird gespeichert
  • Zeile 28: Man fragt sie zur Überprüfung erneut beim Persistenzkontext ab
  • Zeile 32: Es wird überprüft, ob die abgerufene Person tatsächlich den Namen p3 hat.

@Test()
    public void test04() throws ParseException {
        log("test4");
        // Person p1 wird geladen
        List<Personne> personnes = service.getAllLike("p1%");
        Personne p1 = personnes.get(0);
        // Anzeige
        System.out.println(p1);
        // wird überprüft
        assert "p1".equals(p1.getNom());
        int version1 = p1.getVersion();
        // Änderung des Vornamens
        p1.setPrenom("x");
        // Speichern
        service.updateOne(p1);
        // Neu laden
        p1 = service.getOne(p1.getId());
        // Anzeigen
        System.out.println(p1);
        // Es wird überprüft, ob die Version erhöht wurde
        assert (version1 + 1) == p1.getVersion();

    }
  • Zeile 5: Die Person p1 wird angefordert
  • Zeile 10: Wir überprüfen ihren Namen
  • Zeile 11: Man notiert ihre Versionsnummer
  • Zeile 13: Der Vorname wird geändert
  • Zeile 15: Die Änderung wird gespeichert
  • Zeile 17: Die Person p1 wird erneut abgefragt
  • Zeile 21: Man überprüft, ob sich die Versionsnummer um 1 erhöht hat

@Test()
    public void test05() {
        log("test5");
        // Die Person p2 wird geladen
        List<Personne> personnes = service.getAllLike("p2%");
        Personne p2 = personnes.get(0);
        // Anzeigen
        System.out.println(p2);
        // Es wird überprüft
        assert "p2".equals(p2.getNom());
        // Die Person p2 wird gelöscht
        service.deleteOne(p2.getId());
        // die Person p2 wird erneut geladen
        p2 = service.getOne(p2.getId());
        // Es wird überprüft, ob ein Null-Zeiger erhalten wurde
        assert null == p2;
        // die Tabelle wird angezeigt
        dump();
    }
  • Zeile 5: Die Person p2 wird abgefragt
  • Zeile 10: Man überprüft ihren Namen
  • Zeile 12: Sie wird gelöscht
  • Zeile 14: Die Person wird erneut abgefragt
  • Zeile 16: Es wird überprüft, ob sie nicht gefunden wurde

@Test()
    public void test06() throws ParseException {
        log("test6");
        // Es wird ein Array mit zwei Personen mit demselben Namen erstellt (verstoßt gegen die Regel der Namenseindeutigkeit)
        Personne[] personnes = { new Personne("p3", "x", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2),
                new Personne("p4", "x", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2),
                new Personne("p4", "x", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2)};
        // Dieses Array wird gespeichert – es sollte zu einer Ausnahme und einem Rollback kommen
        boolean erreur = false;
        try {
            service.saveArray(personnes);
        } catch (RuntimeException e) {
            erreur = true;
        }
        // Dump
        dump();
        // Überprüfungen
        assert erreur;
        // Suche nach einer Person mit dem Namen p3
        List<Personne> personnesp3 = service.getAllLike("p3%");
        assert 0 == personnesp3.size();
        // Dump
        dump();
    }
  • Zeile 5: Es wird ein Array mit drei Personen erstellt, von denen zwei denselben Namen „p4“ haben. Dies verstößt gegen die Regel zur Eindeutigkeit des Namens der @Entity Personne:

    @Column(name = "NOM", length = 30, nullable = false, unique = true)
private String nom;
  • Zeile 11: Das Array mit den drei Personen wird in den Persistenzkontext gestellt. Das Hinzufügen der zweiten Person „p4“ sollte fehlschlagen. Da die Methode [saveArray] in einer Transaktion ausgeführt wird, werden alle zuvor möglicherweise vorgenommenen Einfügungen rückgängig gemacht. Letztendlich wird kein Eintrag vorgenommen.
  • Zeile 18: Es wird überprüft, ob [saveArray] tatsächlich eine Ausnahme ausgelöst hat
  • Zeilen 20–21: Es wird überprüft, ob die Person p3, die möglicherweise hinzugefügt worden wäre, nicht hinzugefügt wurde.

@Test()
    public void test07() {
        log("test7");
        // Test der optimistischen Sperre
        // Person p1 wird geladen
        List<Personne> personnes = service.getAllLike("p1%");
        Personne p1 = personnes.get(0);
        // Anzeige
        System.out.println(p1);
        // Die Anzahl ihrer Kinder wird erhöht
        int nbEnfants1 = p1.getNbenfants();
        p1.setNbenfants(nbEnfants1 + 1);
        // p1 wird gespeichert
        Personne newp1 = service.updateOne(p1);
        assert (nbEnfants1 + 1) == newp1.getNbenfants();
        System.out.println(newp1);
        // Ein zweites Mal speichern – es sollte eine Ausnahme auftreten, da p1 nicht mehr die richtige Version hat
        // Es ist newp1, das es hat
        boolean erreur = false;
        try {
            service.updateOne(p1);
        } catch (RuntimeException e) {
            erreur = true;
        }
        // Überprüfung
        assert erreur;
        // Die Anzahl der Kinder von newp1 wird erhöht
        int nbEnfants2 = newp1.getNbenfants();
        newp1.setNbenfants(nbEnfants2 + 1);
        // newp1 wird gespeichert
        service.updateOne(newp1);
        // wird neu geladen
        p1 = service.getOne(p1.getId());
        // wird überprüft
        assert (nbEnfants1 + 2) == p1.getNbenfants();
        System.out.println(p1);
    }
  • Zeile 6: Die Person p1 wird abgefragt
  • Zeile 12: Die Anzahl ihrer Kinder wird um 1 erhöht
  • Zeile 14: Die Person p1 wird im Persistenzkontext aktualisiert. Die Methode [updateOne] speichert die neue Version newp1 von p1 dauerhaft ab. Sie unterscheidet sich von p1 durch ihre Versionsnummer, die erhöht worden sein muss.
  • Zeile 15: Die Anzahl der Kinder von newp1 wird überprüft.
  • Zeile 21: Es wird erneut eine Aktualisierung der Person p1 ausgehend von der alten Version p1 angefordert. Dabei muss eine Ausnahme auftreten, da p1 nicht die aktuellste Version der Person p1 ist. Die aktuellste Version ist newp1.
  • Zeile 23: Es wird überprüft, ob der Fehler tatsächlich aufgetreten ist.
  • Zeilen 27–35: Es wird überprüft, ob bei einer Aktualisierung ausgehend von der neuesten Version „newp1“ alles reibungslos verläuft.

@Test()
    public void test08() {
        log("test8");
        // Rollback-Test auf updateArray
        // Person p1 wird geladen
        List<Personne> personnes = service.getAllLike("p1%");
        Personne p1 = personnes.get(0);
        // Anzeige
        System.out.println(p1);
        // Anzahl der Kinder erhöhen
        int nbEnfants1 = p1.getNbenfants();
        p1.setNbenfants(nbEnfants1 + 1);
        // Es werden zwei Änderungen gespeichert, wobei die zweite fehlschlagen muss (Person falsch initialisiert)
        // Aufgrund der Transaktion müssen nun beide rückgängig gemacht werden
        boolean erreur = false;
        try {
            service.updateArray(new Personne[] { p1, new Personne() });
        } catch (RuntimeException e) {
            erreur = true;
        }
        // Prüfungen
        assert erreur;
        // Die Person p1 wird neu geladen
        personnes = service.getAllLike("p1%");
        p1 = personnes.get(0);
        // Die Anzahl ihrer Kinder sollte sich nicht geändert haben
        assert nbEnfants1 == p1.getNbenfants();
    }
  • Test 8 ähnelt Test 6: Er überprüft den Wert „rollback“ anhand eines Werts „updateArray“, der auf ein Array mit zwei Personen angewendet wird, wobei die zweite Person nicht korrekt initialisiert wurde. Aus Sicht von JPA führt die Merge-Operation für die zweite Person, diebereits existiert, einen Befehl SQL insert generieren, der aufgrund der Einschränkungen nullable=false, die für bestimmte Felder der Entität Personne gelten, fehlschlagen wird.

@Test()
    public void test09() {
        log("test9");
        // Rollback-Test auf deleteArray
        // Dump
        dump();
        // Person p1 wird geladen
        List<Personne> personnes = service.getAllLike("p1%");
        Personne p1 = personnes.get(0);
        // Anzeige
        System.out.println(p1);
        // Es werden zwei Löschvorgänge durchgeführt, von denen der zweite fehlschlagen muss (unbekannte Person)
        // Aufgrund der Transaktion müssen beide Vorgänge dann storniert werden
        boolean erreur = false;
        try {
            service.deleteArray(new Personne[] { p1, new Personne() });
        } catch (RuntimeException e) {
            erreur = true;
        }
        // Prüfungen
        assert erreur;
        // Die Person p1 wird neu geladen
        personnes = service.getAllLike("p1%");
        // Prüfung
        assert 1 == personnes.size();
        // Dump
        dump();
    }
  • Test 9 ähnelt dem vorherigen: Er überprüft den rollback anhand eines deleteArray, der auf ein Array mit zwei Personen angewendet wird, wobei die zweite Person nicht existiert. In diesem Fall löst die Methode [deleteOne] der Schicht [dao] jedoch eine Ausnahme aus.

// Optimistic Locking – Multithread-Zugriff
    @Test()
    public void test10() throws Exception {
        // Hinzufügen einer Person
        Personne p3 = new Personne("X", "X", new SimpleDateFormat("dd/MM/yyyy").parse("01/02/2006"), true, 0);
        service.saveOne(p3);
        int id3 = p3.getId();
        // Erstellung von N Threads zur Aktualisierung der Anzahl der Kinder
        final int N = 20;
        Thread[] taches = new Thread[N];
        for (int i = 0; i < taches.length; i++) {
            taches[i] = new ThreadMajEnfants("thread n° " + i, service, id3);
            taches[i].start();
        }
        // Warten auf das Ende der Threads
        for (int i = 0; i < taches.length; i++) {
            taches[i].join();
        }
        // die Person wird abgerufen
        p3 = service.getOne(id3);
        // Sie muss N Kinder haben
        assert N == p3.getNbenfants();
        // Person p3 löschen
        service.deleteOne(p3.getId());
        // Überprüfung
        p3 = service.getOne(p3.getId());
        // Es muss ein Null-Zeiger vorhanden sein
        assert p3 == null;
    }
  • Die Idee von Test 10 besteht darin, N Threads zu starten (Zeile 9), um die Anzahl der Kinder einer Person parallel zu erhöhen. Wir wollen überprüfen, ob das Versionsnummernsystem diesem Szenario standhält. Es wurde genau dafür entwickelt.
  • Zeilen 5–6: Eine Person namens p3 wird angelegt und anschließend gespeichert. Sie hat zu Beginn 0 Kinder.
  • Zeile 7: Ihre Kennung wird notiert.
  • Zeilen 9–14: Es werden N Threads parallel gestartet, die alle die Aufgabe haben, die Anzahl der Kinder von p3 um 1 zu erhöhen.
  • Zeilen 16–18: Es wird auf das Ende aller Threads gewartet.
  • Zeile 20: Die Person p3 wird abgerufen.
  • Zeile 22: Es wird überprüft, ob sie nun N Kinder hat.
  • Zeile 24: Die Person p3 wird gelöscht.

Der Thread [ThreadMajEnfants] lautet wie folgt:


package tests;

...
public class ThreadMajEnfants extends Thread {
    // Name des Threads
    private String name;

    // Referenz auf die Ebene [service]
    private IService service;

    // ID der Person, an der gearbeitet wird
    private int idPersonne;

    // Konstruktor
    public ThreadMajEnfants(String name, IService service, int idPersonne) {
        this.name = name;
        this.service = service;
        this.idPersonne = idPersonne;
    }

    // Kern des Threads
    public void run() {
        // Nachverfolgung
        suivi("lancé");
        // Es wird so lange eine Schleife durchlaufen, bis die Zahl um 1 erhöht werden konnte
        // die Anzahl der Kinder der Person idPersonne
        boolean fini = false;
        int nbEnfants = 0;
        while (!fini) {
            // Es wird eine Kopie der Person aus idPersonne abgerufen
            Personne personne = service.getOne(idPersonne);
            nbEnfants = personne.getNbenfants();
            // Weiter
            suivi("" + nbEnfants + " -> " + (nbEnfants + 1) + " pour la version " + personne.getVersion());
            // Die Anzahl der Kinder der Person wird um 1 erhöht
            personne.setNbenfants(nbEnfants + 1);
            // 10 ms Wartezeit, bevor der Prozessor freigegeben wird
            try {
                // Weiterverfolgung
                suivi("début attente");
                // Unterbrechung, um den Prozessor freizugeben
                Thread.sleep(10);
                // Weiter
                suivi("fin attente");
            } catch (Exception ex) {
                throw new RuntimeException(ex.toString());
            }
            // Wartezeit beendet – Versuch, die Kopie zu validieren
            // In der Zwischenzeit konnten andere Threads das Original ändern
            try {
                // Es wird versucht, das Original zu ändern
                service.updateOne(personne);
                // Erfolgreich – das Original wurde geändert
                fini = true;
            } catch (javax.persistence.OptimisticLockException e) {
                // Falsche Objektversion: Die Ausnahme wird ignoriert, um den Vorgang erneut zu starten
            } catch (org.springframework.transaction.UnexpectedRollbackException e2) {
                // Spring-Ausnahme, die gelegentlich auftritt
            } catch (RuntimeException e3) {
                // Andere Art von Ausnahme – sie wird weitergeleitet
                throw e3;
            }
        }
        // Weiterverfolgung
        suivi("a terminé et passé le nombre d'enfants à " + (nbEnfants + 1));
    }

    // Weiterverfolgung
    private void suivi(String message) {
        System.out.println(name + " [" + new Date().getTime() + "] : " + message);
    }
}
  • Zeilen 15–19: Der Konstruktor speichert die Informationen, die er für seine Arbeit benötigt: seinen Namen (Zeile 16), die Referenz auf die Schicht [service], die er verwenden muss (Zeile 17), und die Kennung der Person p, deren Anzahl an Kindern er erhöhen muss (Zeile 18).
  • Zeilen 22–66: Die Methode [run] wird von allen Threads parallel ausgeführt.
  • Zeile 29: Der Thread versucht wiederholt, die Anzahl der Kinder der Person p zu erhöhen. Er hört erst auf, wenn dies gelungen ist.
  • Zeile 31: Die Person p wird abgefragt
  • Zeile 36: Die Anzahl ihrer Kinder wird im Speicher erhöht
  • Zeilen 38–47: Es wird eine Pause von 10 ms eingelegt. Dadurch können andere Threads dieselbe Version der Person p abrufen. Es gibt also gleichzeitig mehrere Threads, die dieselbe Version der Person p besitzen und diese ändern wollen. Genau das ist beabsichtigt.
  • Zeile 52: Sobald die Pause beendet ist, fordert der Thread die Schicht [service] auf, die Änderung dauerhaft zu speichern. Da bekannt ist, dass es gelegentlich zu Ausnahmen kommen wird, wurde der Vorgang mit einem try/catch-Block umschlossen.
  • Zeile 55: Die Tests zeigen, dass Ausnahmen vom Typ [javax.persistence.OptimisticLockException] auftreten. Das ist normal: Es handelt sich um die Ausnahme, die von der Schicht JPA ausgelöst wird, wenn ein Thread die Person p ändern will, ohne über deren aktuellste Version zu verfügen. Diese Ausnahme wird ignoriert, damit der Thread den Vorgang so lange erneut versuchen kann, bis er erfolgreich ist.
  • Zeile 57: Die Tests zeigen, dass ebenfalls Ausnahmen vom Typ [org.springframework.transaction.UnexpectedRollbackException] auftreten. Das ist ärgerlich und unerwartet. Ich habe dafür keine Erklärung. Nun sind wir von Spring abhängig, obwohl wir dies eigentlich vermeiden wollten. Das bedeutet, dass der Code des Threads geändert werden muss, wenn wir unsere Anwendung beispielsweise in JBoss Ejb3 ausführen. Auch hier wird die Spring-Ausnahme ignoriert, damit der Thread den Inkrementierungsvorgang erneut versuchen kann.
  • Zeile 59: Die anderen Ausnahmetypen werden an die Anwendung weitergeleitet.

Bei der Ausführung von [TestNG] erhält man folgende Ergebnisse:

Image

Alle 10 Tests wurden erfolgreich bestanden.

Test 10 verdient eine näheren Erläuterung, da sein Erfolg etwas von einem Wunder hat. Kommen wir zunächst auf die Konfiguration der Schicht [dao] zurück:


public class Dao implements IDao {

    @PersistenceContext
    private EntityManager em;

  • Zeile 4: Ein Objekt [EntityManager] wird mithilfe der Annotation JPA @PersistenceContext in das Feld „em“ eingefügt. Die Schicht [dao] wird nur einmal instanziiert. Es handelt sich um ein Singleton, das von allen Threads verwendet wird, die die Schicht JPA nutzen. Somit ist das EntityManager „em“ allen Threads gemeinsam. Dies lässt sich überprüfen, indem man den Wert von „em“ in der Methode [updateOne] ausgibt, die von den Threads [ThreadMajEnfants] verwendet wird: Man erhält für alle Threads denselben Wert.

Daher stellt sich die Frage, ob die persistenten Objekte der verschiedenen Threads, die von dem für alle Threads identischen em in EntityManager verwaltet werden, nicht miteinander vermischt werden und Konflikte untereinander verursachen könnten. Ein Beispiel dafür, was passieren könnte, findet sich in [ThreadMajEnfants]:


        while (!fini) {
            // Es wird eine Kopie der Person von idPersonne abgerufen
            Personne personne = service.getOne(idPersonne);
            nbEnfants = personne.getNbenfants();
            // Weiterverfolgung
            suivi("" + nbEnfants + " -> " + (nbEnfants + 1) + " pour la version " + personne.getVersion());
            // Erhöht die Anzahl der Kinder der Person um 1
            personne.setNbenfants(nbEnfants + 1);
            // 10 ms warten, um den Prozessor freizugeben
            try {
                // Weiter
                suivi("début attente");
                // Unterbrechung, um den Prozessor freizugeben
                Thread.sleep(10);
                // Weiterverfolgung
                suivi("fin attente");
            } catch (Exception ex) {
                throw new RuntimeException(ex.toString());
}
  • Zeile 3: Ein Thread T1 ruft die Person p ab
  • Zeile 8: Er erhöht die Anzahl der Kinder von p
  • Zeile 14: Der Thread T1 hält kurz an

Ein Thread mit der ID T2 übernimmt die Kontrolle und führt ebenfalls Zeile 3 aus: Er fragt nach derselben Person p wie T1. Wäre der Persistenzkontext der Threads derselbe, müsste die Person p, die sich dank T1 bereits im Kontext befindet, an T2 zurückgegeben werden. Tatsächlich nutzt die Methode [getOne] die Methode [EntityManager].Diese Methode greift auf API und JPA zu und führt nur dann einen Datenbankzugriff durch, wenn das angeforderte Objekt nicht Teil des Persistenzkontexts ist; andernfalls gibt sie das Objekt aus dem Persistenzkontext zurück. Wäre dies der Fall, würden T1 und T2 dieselbe Person p verwalten. T2 würde dann die Anzahl der Kinder von p erneut um 1 erhöhen (Zeile 8). Wenn einer der Threads seine Aktualisierung nach der Pause erfolgreich durchführt, dann wurde die Anzahl der Kinder von p um 2 erhöht und nicht wie erwartet um 1. Man könnte dann erwarten, dass die N Threads die Anzahl der Kinder nicht auf N, sondern auf einen höheren Wert erhöhen. Dies ist jedoch nicht der Fall. Daraus lässt sich schließen, dass T1 und T2 nicht denselben Verweis p haben. Dies lässt sich überprüfen, indem man die Adresse von p von den Threads ausgeben lässt: Sie ist für jeden von ihnen unterschiedlich.

Es scheint also, dass die Threads:

  • sich denselben Persistenzkontext-Manager (EntityManager) teilen
  • aber jeweils über einen eigenen Persistenzkontext verfügen.

Dies sind jedoch nur Vermutungen, und die Meinung eines Experten wäre hier hilfreich.

3.1.8. Wechseln zu SGBD

Um von SGBD zu wechseln, muss lediglich die Datei [src/spring-config.xml] [2] durch die Datei [spring-config.xml] aus dem betreffenden Ordner [conf] SGBD [1].

Die Oracle-Datei [spring-config.xml] sieht beispielsweise wie folgt aus:


<?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:tx="http://www.springframework.org/schema/tx"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.0.xsd">

...
    <bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
        <property name="dataSource" ref="dataSource" />
        <property name="jpaVendorAdapter">
            <bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter">
                <!-- 
                    <property name="showSql" value="true" />
                -->
                <property name="databasePlatform" value="org.hibernate.dialect.OracleDialect" />
                <property name="generateDdl" value="true" />
            </bean>
        </property>
        <property name="loadTimeWeaver">
            <bean class="org.springframework.instrument.classloading.InstrumentationLoadTimeWeaver" />
        </property>
    </bean>

    <!-- die Datenquelle DBCP -->
    <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
        <property name="driverClassName" value="oracle.jdbc.OracleDriver" />
        <property name="url" value="jdbc:oracle:thin:@localhost:1521:xe" />
        <property name="username" value="jpa" />
        <property name="password" value="jpa" />
    </bean>
...
</beans>

Im Vergleich zu derselben Datei, die zuvor für MySQL5 verwendet wurde, ändern sich nur bestimmte Zeilen:

  • Zeile 14: der Dialekt SQL, den Hibernate verwenden soll
  • Zeilen 25–28: die Eigenschaften der JDBC-Verbindung mit SGBD

Der Leser wird gebeten, die für MySQL5 beschriebenen Tests mit anderen SGBD zu wiederholen.

3.1.9. Wechseln der Implementierung JPA

Kehren wir zur Architektur der vorherigen Tests zurück:

Wir ersetzen die Implementierung JPA / Hibernate durch eine Implementierung JPA / Toplink. Da Toplink nicht dieselben Bibliotheken wie Hibernate verwendet, nutzen wir ein neues Eclipse-Projekt:

  • in [1]: das Eclipse-Projekt. Es ist identisch mit dem vorherigen. Es ändern sich lediglich die Konfigurationsdatei [spring-config.xml] [2] und die Bibliothek [jpa-toplink], die die Bibliothek [jpa-hibernate] ersetzt.
  • In [3]: der Ordner mit den Beispielen zu diesem Tutorial. In [4] das zu importierende Eclipse-Projekt.

Die Konfigurationsdatei [spring-config.xml] für Toplink sieht nun wie folgt aus:


<?xml version="1.0" encoding="UTF-8"?>

<!-- muss JVM mit dem Argument -javaagent:C:\data\2006-2007\eclipse\dvp-jpa\lib\spring\spring-agent.jar 
    (à remplacer par le chemin exact de spring-agent.jar)-->

<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    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.0.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.0.xsd">

    <!-- Anwendungsschichten -->
    <bean id="dao" class="dao.Dao" />
    <bean id="service" class="service.Service">
        <property name="dao" ref="dao" />
    </bean>

    <bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
        <property name="dataSource" ref="dataSource" />
        <property name="jpaVendorAdapter">
            <bean class="org.springframework.orm.jpa.vendor.TopLinkJpaVendorAdapter">
                <!-- 
                    <property name="showSql" value="true" />
                -->
                <property name="databasePlatform" value="oracle.toplink.essentials.platform.database.MySQL4Platform" />
                <property name="generateDdl" value="true" />
            </bean>
        </property>
        <property name="loadTimeWeaver">
            <bean class="org.springframework.instrument.classloading.InstrumentationLoadTimeWeaver" />
        </property>
    </bean>

    <!-- die Datenquelle DBCP -->
    <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
        <property name="driverClassName" value="com.mysql.jdbc.Driver" />
        <property name="url" value="jdbc:mysql://localhost:3306/jpa" />
        <property name="username" value="jpa" />
        <property name="password" value="jpa" />
    </bean>

    <!-- Transaktionsmanager -->
    <tx:annotation-driven transaction-manager="txManager" />
    <bean id="txManager" class="org.springframework.orm.jpa.JpaTransactionManager">
        <property name="entityManagerFactory" ref="entityManagerFactory" />
    </bean>

    <!-- Ausnahmeübersetzung -->
    <bean class="org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor" />

    <!-- Persistenz -->
    <bean class="org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor" />

</beans>

Es müssen nur wenige Zeilen geändert werden, um von Hibernate auf Toplink umzustellen:

  • Zeile 19: Die Implementierung JPA wird nun von Toplink übernommen
  • Zeile 23: Die Eigenschaft [databasePlatform] hat einen anderen Wert als bei Hibernate: den Namen einer Toplink-spezifischen Klasse. Wo dieser Name zu finden ist, wurde in Abschnitt 2.1.15.2 erläutert.

Das war’s. Man beachte, wie einfach es ist, mit Spring von SGBD oder der Implementierung JPA zu wechseln.

Wir sind jedoch noch nicht ganz fertig. Wenn man beispielsweise [InitDB] ausführt, tritt eine Ausnahme auf, die nicht ganz einfach zu verstehen ist:


Exception in thread "main" org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'entityManagerFactory' defined in class path resource [spring-config.xml]: Invocation of init method failed; nested exception is java.lang.IllegalStateException: Must start with Java agent to use InstrumentationLoadTimeWeaver. See Spring documentation.
Caused by: java.lang.IllegalStateException: Must start with Java agent to use 

Die Fehlermeldung in Zeile 1 veranlasst dazu, die Spring-Dokumentation zu lesen. Dort erfährt man dann etwas mehr über die Rolle, die eine obskure Deklaration in der Datei [spring-config.xml] spielt:


    <bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
        <property name="dataSource" ref="dataSource" />
        <property name="jpaVendorAdapter">
            <bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter">
                <!-- 
                    <property name="showSql" value="true" />
                -->
                <property name="databasePlatform" value="org.hibernate.dialect.OracleDialect" />
                <property name="generateDdl" value="true" />
            </bean>
        </property>
        <property name="loadTimeWeaver">
            <bean class="org.springframework.instrument.classloading.InstrumentationLoadTimeWeaver" />
        </property>
</bean>

Zeile 1 der Ausnahme verweist auf eine Klasse namens [InstrumentationLoadTimeWeaver], die in Zeile 13 der Spring-Konfigurationsdatei zu finden ist. Die Spring-Dokumentation erklärt, dass diese Klasse in bestimmten Fällen zum Laden der Anwendungsklassen erforderlich ist und dass die Klasse „JVM“ mit einem Agenten gestartet werden muss, damit sie genutzt werden kann. Dieser Agent wird von Spring bereitgestellt und heißt „[spring-agent]“:

  • Die Datei [spring-agent.jar] befindet sich im Ordner <exemples>/lib [1]. Sie wird mit der Spring-Distribution 2.x mitgeliefert (siehe Abschnitt 5.11).
  • In [3] wird eine Ausführungskonfiguration [Run/Run...]
  • In [4] wird eine Java-Laufzeitkonfiguration angelegt (es gibt verschiedene Arten von Laufzeitkonfigurationen)
  • In [5] wählen Sie die Registerkarte [Main]
  • in [6], man gibt der Konfiguration einen Namen
  • In „[7]“ geben Sie den Namen des Eclipse-Projekts an, das von dieser Konfiguration betroffen ist (verwenden Sie die Schaltfläche „Browse“)
  • in [8] benennen Sie die Java-Klasse, die die Methode [main] enthält (verwenden Sie die Schaltfläche „Browse“)
  • In [9] wechselt man zur Registerkarte [Arguments]. Dort können zwei Arten von Argumenten angegeben werden:
    • in [9] die an die Methode [main] übergebenen
    • in [10]: diejenigen, die an die Methode JVM übergeben werden, die den Code ausführt. Der Spring-Agent wird mithilfe des Parameters -javaagent:Wert der Methode JVM definiert. Der Wert ist der Pfad zur Datei [spring-agent.jar].
  • in [11]: Die Konfiguration wird bestätigt
  • in [12]: Die Konfiguration wird erstellt
  • in [13]: Ausführung

Anschließend wird [InitDB] ausgeführt und liefert dieselben Ergebnisse wie mit Hibernate. Für [TestNG] muss man genauso vorgehen:

  • in [1] erstellen wir eine Ausführungskonfiguration [Run/Run...]
  • In [2] wird eine Ausführungskonfiguration TestNG
  • In [3] wählen Sie die Registerkarte [Test]
  • In [4] benennen wir die Konfiguration
  • in [5] benennen Sie das Eclipse-Projekt, auf das sich diese Konfiguration bezieht (verwenden Sie die Schaltfläche „Browse“)
  • In [6] benennen Sie die Testklasse (verwenden Sie die Schaltfläche „Browse“)
  • In [7] wechselt man zur Registerkarte [Arguments].
  • In [8]: Legen Sie das Argument -javaagent für JVM fest.
  • in [9]: Die Konfiguration wird bestätigt
  • in [10]: Die Konfiguration wird erstellt
  • in [11]: Sie wird ausgeführt

Anschließend wird [TestNG] ausgeführt und liefert dieselben Ergebnisse wie mit Hibernate.

3.2. Beispiel 2: JBoss EJB3 / JPA mit der Entität „Person“

Wir greifen auf dasselbe Beispiel wie zuvor zurück, führen es jedoch in einem Container EJB3 aus, nämlich dem von JBoss:

Ein Ejb3-Container ist normalerweise in einen Anwendungsserver integriert. JBoss stellt einen „standalone“-Ejb3-Container bereit, der außerhalb eines Anwendungsservers verwendet werden kann. Wir werden feststellen, dass er ähnliche Dienste bereitstellt wie Spring. Wir werden versuchen herauszufinden, welcher dieser Container sich als praktischer erweist.

Die Installation des EJB3-Containers JBoss wird in Abschnitt 5.12 beschrieben.

3.2.1. Das Eclipse-/JBoss-EJB3-/Hibernate-Projekt

Das Eclipse-Projekt lautet wie folgt:

  • in [1]: das Eclipse-Projekt. Es befindet sich in [6] in den Beispielen des Tutorials [5]. Wir werden es importieren.
  • in [2]: die Java-Codes der Schichten, die in Paketen dargestellt sind:
    • [entites]: das Entitäten-Paket JPA
    • [dao]: die Datenzugriffsebene – basiert auf der Ebene JPA
    • [service]: eine eher dienstorientierte als geschäftsorientierte Schicht. Hier wird der Transaktionsdienst des EJB3-Containers verwendet.
    • [tests]: Fasst die Testprogramme zusammen.
  • in [3]: Die Bibliothek [jpa-jbossejb3] enthält die für JBoss EJB3 erforderlichen JAR-Dateien (siehe auch [7] und [8]).
  • in [4]: Der Ordner [conf] enthält die Konfigurationsdateien für jedes der in diesem Tutorial verwendeten SGBD. Es gibt jeweils zwei davon: [persistence.xml], das die Schicht JPA konfiguriert, und [jboss-config.xml], das den Ejb3-Container konfiguriert.

3.2.2. Die Entitäten JPA

Hier wird nur eine Entität verwaltet, nämlich die Entität Personne, die bereits in Abschnitt 3.1.2 behandelt wurde.

3.2.3. Die Schicht [dao]

Die Schicht [dao] stellt die zuvor in Abschnitt 3.1.3 beschriebene Schnittstelle [IDao] bereit.

Die Implementierung [Dao] dieser Schnittstelle lautet wie folgt:


package dao;

...
@Stateless
public class Dao implements IDao {

    @PersistenceContext
    private EntityManager em;

    // eine Person anhand ihrer ID löschen
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public void deleteOne(Integer id) {
        Personne personne = em.find(Personne.class, id);
        if (personne == null) {
            throw new DaoException(2);
        }
        em.remove(personne);
    }

    // Alle Personen abrufen
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public List<Personne> getAll() {
        return em.createQuery("select p from Personne p").getResultList();
    }

    // Personen abrufen, deren Name einem Muster entspricht
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public List<Personne> getAllLike(String modele) {
        return em.createQuery("select p from Personne p where p.nom like :modele")
                .setParameter("modele", modele).getResultList();
    }

    // eine Person anhand ihrer ID abrufen
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public Personne getOne(Integer id) {
        return em.find(Personne.class, id);
    }

    // eine Person speichern
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public Personne saveOne(Personne personne) {
        em.persist(personne);
        return personne;
    }

    // eine Person aktualisieren
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public Personne updateOne(Personne personne) {
        return em.merge(personne);
    }

}
  • Dieser Code ist in jeder Hinsicht identisch mit dem, den wir bei Spring hatten. Lediglich die Java-Annotationen ändern sich, und genau darauf gehen wir hier ein.
  • Zeile 4: Die Annotation @Stateless macht die Klasse [Dao] zu einem zustandslosen EJB. Die Annotation @Stateful macht eine Klasse zu einem zustandsbehafteten EJB. Ein zustandsbehafteter EJB verfügt über private Felder, deren Werte über einen längeren Zeitraum hinweg beibehalten werden müssen. Ein klassisches Beispiel ist eine Klasse, die Informationen zum Webnutzer einer Anwendung enthält. Eine Instanz dieser Klasse ist an einen bestimmten Nutzer gebunden, und wenn der Ausführungsthread einer Anfrage dieses Nutzers beendet ist, muss die Instanz beibehalten werden, damit sie bei der nächsten Anfrage desselben Kunden verfügbar ist. Ein @Stateless-EJB hat keinen Zustand. Um das gleiche Beispiel noch einmal aufzugreifen: Am Ende des Ausführungsthreads einer Anfrage eines Benutzers wird das @Stateless-EJB in einen Pool von @Stateless-EJBs aufgenommen und steht dann für den Ausführungsthread einer Anfrage eines anderen Benutzers zur Verfügung.
  • Für den Entwickler ähnelt das Konzept des @Stateless-EJB3 dem des Spring-Singletons. Er wird es in denselben Fällen verwenden.
  • Zeile 7: Die Annotation @PersistenceContext ist dieselbe wie die, die in der Spring-Version der Schicht [dao] vorkommt. Sie bezeichnet das Feld, das den EntityManager aufnehmen wird, der es der Schicht [dao] ermöglicht, den Persistenzkontext zu bearbeiten.
  • Zeile 11: Die auf eine Methode angewendete Annotation @TransactionAttribute dient dazu, die Transaktion zu konfigurieren, in der die Methode ausgeführt wird. Hier sind einige mögliche Werte dieser Annotation:
    • TransactionAttributeType.REQUIRED: Die Methode muss in einer Transaktion ausgeführt werden. Wenn bereits eine Transaktion gestartet wurde, finden die Persistenzoperationen der Methode in dieser statt. Andernfalls wird eine Transaktion erstellt und gestartet.
    • TransactionAttributeType.REQUIRES_NEW: Die Methode muss in einer neuen Transaktion ausgeführt werden. Diese wird angelegt und gestartet.
    • TransactionAttributeType.MANDATORY: Die Methode muss innerhalb einer bestehenden Transaktion ausgeführt werden. Wenn keine solche Transaktion vorhanden ist, wird eine Ausnahme ausgelöst.
    • TransactionAttributeType.NEVER: Die Methode wird niemals in einer Transaktion ausgeführt.
    • ...

Die Annotation hätte auch auf der Klasse selbst platziert werden können:


@Stateless
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public class Dao implements IDao {

Das Attribut gilt dann für alle Methoden der Klasse.

3.2.4. Die Schicht [metier / service]

Die Schicht [service] stellt die Schnittstelle [IService] dar, die zuvor in Abschnitt 3.1.4 behandelt wurde. Die Implementierung [Service] der Schnittstelle [IService] ist identisch mit der zuvor in Abschnitt 3.1.4 behandelten Implementierung, abgesehen von drei Details:



@Stateless
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public class Service implements IService {

    // Ebene [dao]
    @EJB
    private IDao dao;

    public IDao getDao() {
        return dao;
    }

    public void setDao(IDao dao) {
        this.dao = dao;
    }

  • Zeile 2: Die Klasse [Service] ist ein zustandsloser EJB
  • Zeile 3: Alle Methoden der Klasse [Service] müssen innerhalb einer Transaktion ausgeführt werden
  • Zeilen 7–8: Eine Referenz auf das EJB der Schicht [dao] wird vom EJB-Container in das Feld [IDao dao] in Zeile 8 injiziert. Die Annotation @EJB in Zeile 7 fordert diese Injektion an. Das injizierte Objekt muss ein EJB sein. Dies ist ein wesentlicher Unterschied zu Spring, wo jeder Objekttyp in ein anderes Objekt injiziert werden kann.

3.2.5. Konfiguration der Schichten

Die Konfiguration der Schichten [service], [dao] und [JPA] erfolgt über die folgenden Dateien:

  • [META-INF/persistence.xml] konfiguriert die Schicht JPA
  • [jboss-config.xml] konfiguriert den EJB3-Container. Dieser nutzt seinerseits die Dateien [default.persistence.properties, ejb3-interceptors-aop.xml, embedded-jboss-beans.xml, jndi.properties]. Diese Dateien werden mit JBoss EJB3 ausgeliefert und sorgen für eine Standardkonfiguration, an der normalerweise nicht geändert wird. Für den Entwickler ist nur die Datei [jboss-config.xml] von Interesse

Sehen wir uns die beiden Konfigurationsdateien an:

persistence.xml


<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="jpa">

        <!-- Der Provider JPA ist Hibernate -->
        <provider>org.hibernate.ejb.HibernatePersistence</provider>

        <!-- die von der Java-Umgebung verwaltete DataSource JTA -->
        <jta-data-source>java:/datasource</jta-data-source>

        <properties>
            <!-- Suche nach Entitäten der Schicht JBA -->
            <property name="hibernate.archive.autodetection" value="class, hbm" />

            <!-- Hibernate-Protokolle SQL
                <property name="hibernate.show_sql" value="true"/>
                <property name="hibernate.format_sql" value="true"/>
                <property name="use_sql_comments" value="true"/>
            -->

            <!-- verwalteter Typ von SGBD -->
            <property name="hibernate.dialect" value="org.hibernate.dialect.MySQLInnoDBDialect" />

            <!-- Neuerstellung aller Tabellen (drop+create) bei der Bereitstellung der Persistenz-Einheit -->
            <property name="hibernate.hbm2ddl.auto" value="create" />

        </properties>
    </persistence-unit>

</persistence>

Diese Datei ähnelt denen, die wir bereits bei der Untersuchung der Entitäten JPA kennengelernt haben. Sie konfiguriert eine Hibernate-Schicht JPA. Folgende Neuerungen gibt es:

  • Zeile 5: Die Persistenz-Einheit jpa verfügt nicht über das Attribut transaction-type, das bisher immer vorhanden war:

<persistence-unit name="jpa" transaction-type="RESOURCE_LOCAL" />

Wenn kein Wert angegeben ist, hat das Attribut transaction-type den Standardwert „JTA“ (für die Java Transaction API), was angibt, dass der Transaktionsmanager von einem EJB3-Container bereitgestellt wird. Ein Manager vom Typ „JTA“ kann mehr als ein Manager vom Typ „RESOURCE_LOCAL“: Er kann Transaktionen verwalten, die sich über mehrere Verbindungen erstrecken. Mit JTA kann man eine Transaktion t1 über eine Verbindung c1 auf einem SGBD 1 eröffnen, eine Transaktion t2 über eine Verbindung c2 mit einer Verbindung SGBD 2 und dabei (t1, t2) als eine einzige Transaktion betrachten, bei der entweder alle Operationen erfolgreich abgeschlossen werden (Commit) oder keine (Rollback).

Hier arbeiten wir mit dem Manager JTA des JBoss-EJB3-Containers.

  • Zeile 11: Deklariert die Datenquelle, die der Manager JTA verwenden soll. Diese wird in Form eines JNDI-Namens (Java Naming and Directory Interface) angegeben. Diese Datenquelle ist in [jboss-config.xml] definiert.

jboss-config.xml


<?xml version="1.0" encoding="UTF-8"?>

<deployment xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="urn:jboss:bean-deployer bean-deployer_1_0.xsd"
    xmlns="urn:jboss:bean-deployer:2.0">

    <!-- Factory der DataSource -->
    <bean name="datasourceFactory" class="org.jboss.resource.adapter.jdbc.local.LocalTxDataSource">
        <!-- Name JNDI der DataSource -->
        <property name="jndiName">java:/datasource</property>

        <!-- verwaltete Datenbank -->
        <property name="driverClass">com.mysql.jdbc.Driver</property>
        <property name="connectionURL">jdbc:mysql://localhost:3306/jpa</property>
        <property name="userName">jpa</property>
        <property name="password">jpa</property>

        <!-- Eigenschaften des Verbindungspools -->
        <property name="minSize">0</property>
        <property name="maxSize">10</property>
        <property name="blockingTimeout">1000</property>
        <property name="idleTimeout">100000</property>

        <!-- Transaktionsmanager, hier JTA -->
        <property name="transactionManager">
            <inject bean="TransactionManager" />
        </property>
        <!-- Hibernate-Cache-Manager -->
        <property name="cachedConnectionManager">
            <inject bean="CachedConnectionManager" />
        </property>
        <!-- Eigenschaften der Instanziierung von JNDI? -->
        <property name="initialContextProperties">
            <inject bean="InitialContextProperties" />
        </property>
    </bean>

    <!-- DataSource wird von einer Factory angefordert -->
    <bean name="datasource" class="java.lang.Object">
        <constructor factoryMethod="getDatasource">
            <factory bean="datasourceFactory" />
        </constructor>
    </bean>

</deployment>
  • Zeile 3: Das Stamm-Tag der Datei lautet <deployment>. Diese Bereitstellungsdatei dient im Wesentlichen dazu, die Datenquelle java:/datasource zu konfigurieren, die in persistence.xml deklariert wurde.
  • Die Datenquelle wird durch die Bean „datasource“ in Zeile 38 definiert. Es ist ersichtlich, dass die Datenquelle (Zeile 40) von einer „Factory“ bezogen wird, die durch die Bean „datasourceFactory“ in Zeile 7 definiert ist. Um die Datenquelle der Anwendung abzurufen, muss der Client die Methode [getDatasource] der Klasse factory aufrufen (Zeile 39).
  • Zeile 7: Die factory, die die Datenquelle bereitstellt, ist eine JBoss-Klasse.
  • Zeile 9: Der Name der Datenquelle lautet „JNDI“. Dieser muss mit dem Namen übereinstimmen, der im Tag <jta-data-source> der Datei „persistence.xml“ deklariert ist. Tatsächlich verwendet die Schicht JPA diesen Namen JNDI, um die Datenquelle abzufragen.
  • Zeilen 12–15: Etwas Klassischeres: die JDBC-Eigenschaften der Verbindung zu SGBD
  • Zeilen 18–21: Konfiguration des internen Verbindungspools des JBoss-EJB3-Containers.
  • Zeilen 24–26: Der Manager JTA. Die in Zeile 25 injizierte Klasse [TransactionManager] ist in der Datei [embedded-jboss-beans.xml] definiert.
  • Zeilen 28–30: Der Hibernate-Cache, ein Konzept, auf das wir bisher nicht eingegangen sind. Die in Zeile 29 injizierte Klasse [CachedConnectionManager] ist in der Datei [embedded-jboss-beans.xml] definiert. Es ist zu beachten, dass die Konfiguration nun von Hibernate abhängig ist, was uns Probleme bereiten wird, wenn wir auf Toplink umsteigen wollen.
  • Zeilen 32–34: Konfiguration des Dienstes JNDI.

Damit sind wir mit der Konfigurationsdatei von JBoss EJB3 fertig. Sie ist komplex und vieles bleibt unklar. Sie wurde aus der Datei „[ref1]“ übernommen. Wir werden sie jedoch an ein anderes SGBD anpassen können (Zeilen 12–15 von jboss-config.xml, Zeile 24 von persistence.xml). Die Migration zu Toplink war mangels Beispielen nicht möglich.

3.2.6. Client-Programm [InitDB]

Wir beginnen nun mit der Erstellung eines ersten Clients für die zuvor beschriebene Architektur:

Der Code für [InitDB] lautet wie folgt:


package tests;

...
public class InitDB {

    // Service-Schicht
    private static IService service;

    // Konstruktor
    public static void main(String[] args) throws ParseException, NamingException {
        // Der Container wird gestartet: EJB3 JBoss
        // Die Konfigurationsdateien ejb3-interceptors-aop.xml und embedded-jboss-beans.xml werden verarbeitet
        EJB3StandaloneBootstrap.boot(null);

        // Erstellung der anwendungsspezifischen Beans
        EJB3StandaloneBootstrap.deployXmlResource("META-INF/jboss-config.xml");

        // Alle im Klassenpfad gefundenen EJBs bereitstellen (langsam, durchsucht alle)
        // EJB3StandaloneBootstrap.scanClasspath();

        // Alle im Klassenpfad der Anwendung gefundenen EJB-Dateien werden bereitgestellt
        EJB3StandaloneBootstrap.scanClasspath("bin".replace("/", File.separator));

        // Der Kontext JNDI wird initialisiert. Die Datei jndi.properties wird verarbeitet
        InitialContext initialContext = new InitialContext();

        // Instanziierung der Service-Schicht
        service = (IService) initialContext.lookup("Service/local");
        // Die Datenbank wird geleert
        clean();
        // Die Datenbank wird gefüllt
        fill();
        // visuelle Überprüfung
        dumpPersonnes();
        // Der EJB-Container wird angehalten
        EJB3StandaloneBootstrap.shutdown();

    }

    // Anzeige des Tabelleninhalts
    private static void dumpPersonnes() {
        System.out.format("[personnes]-------------------------------------------------------------------%n");
        for (Personne p : service.getAll()) {
            System.out.println(p);
        }
    }

    // Tabelle wird gefüllt
    public static void fill() throws ParseException {
        // Personen anlegen
        Personne p1 = new Personne("p1", "Paul", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2);
        Personne p2 = new Personne("p2", "Sylvie", new SimpleDateFormat("dd/MM/yy").parse("05/07/2001"), false, 0);
        // die gespeichert werden
        service.saveArray(new Personne[] { p1, p2 });
    }

    // Elemente aus der Tabelle löschen
    public static void clean() {
        for (Personne p : service.getAll()) {
            service.deleteOne(p.getId());
        }
    }
}
  • Die Vorgehensweise zum Starten des JBoss-EJB3-Containers wurde in [ref1] gefunden.
  • Zeile 13: Der Container wird gestartet. [EJB3StandaloneBootstrap] ist eine Klasse des Containers.
  • Zeile 16: Die von [jboss-config.xml] konfigurierte Deployment-Einheit wird im Container bereitgestellt: Der Manager JTA, die Datenquelle, der Verbindungspool, der Hibernate-Cache und der Dienst JNDI werden eingerichtet.
  • Zeile 22: Der Container wird aufgefordert, den Ordner „bin“ des Eclipse-Projekts nach EJBs zu durchsuchen. Die EJBs der Schichten [service] und [dao] werden vom Container gefunden und verwaltet.
  • Zeile 25: Ein Kontext JNDI wird initialisiert. Er dient dazu, die EJBs zu lokalisieren.
  • Zeile 28: Das EJB, das der Klasse [Service] der Schicht [service] entspricht, wird beim Dienst JNDI angefordert. Auf ein EJB kann lokal oder über das Netzwerk (remote) zugegriffen werden. Hier bezeichnet der Name „Service/local“ des gesuchten EJB die Klasse [Service] der Schicht [service] für einen lokalen Zugriff.
  • Nun ist die Anwendung bereitgestellt und es liegt eine Referenz auf die Schicht [service] vor. Wir befinden uns in derselben Situation wie nach Zeile 11 des folgenden Codes [InitDB] aus der Spring-Version. Der Code ist also in beiden Versionen identisch.

public class InitDB {

    // Service-Ebene
    private static IService service;

    // Konstruktor
    public static void main(String[] args) throws ParseException {
        // Anwendungskonfiguration
        ApplicationContext ctx = new ClassPathXmlApplicationContext("spring-config.xml");
        // Service-Schicht
        service = (IService) ctx.getBean("service");
        // Die Datenbank wird geleert
        clean();
        // die Datenbank wird gefüllt
        fill();
        // Sichtprüfung
        dumpPersonnes();
    }
...
  • Zeile 36 (JBoss EJB3): Der EJB3-Container wird angehalten.

Die Ausführung von [InitDB] liefert folgende Ergebnisse:

16:07:00,781  INFO LocalTxDataSource:117 - Bound datasource to JNDI name 'java:/datasource'
...
16:07:01,171  INFO Version:94 - Hibernate EntityManager 3.2.0.CR1
...
16:07:01,296  INFO Ejb3Configuration:94 - Processing PersistenceUnitInfo [
    name: jpa
    ...]
16:07:01,312  INFO Ejb3Configuration:94 - found EJB3 Entity bean: entites.Personne
...
16:07:01,375  INFO Configuration:94 - Reading mappings from resource: META-INF/orm.xml
16:07:01,375  INFO Ejb3Configuration:94 - [PersistenceUnit: jpa] no META-INF/orm.xml found
16:07:01,421  INFO AnnotationBinder:94 - Binding entity from annotated class: entites.Personne
16:07:01,468  INFO EntityBinder:94 - Bind entity entites.Personne on table jpa01_hb_personne
...
16:07:01,859  INFO SettingsFactory:94 - RDBMS: MySQL, version: 5.0.41-community-nt
16:07:01,859  INFO SettingsFactory:94 - JDBC driver: MySQL-AB JDBC Driver, version: mysql-connector-java-5.0.5 ( $Date: 2007-03-01 00:01:06 +0100 (Thu, 01 Mar 2007) $, $Revision: 6329 $ )
16:07:01,890  INFO Dialect:94 - Using dialect: org.hibernate.dialect.MySQLInnoDBDialect
16:07:01,890  INFO TransactionFactoryFactory:94 - Transaction strategy: org.hibernate.ejb.transaction.JoinableCMTTransactionFactory
...
16:07:02,234  INFO SchemaExport:94 - Running hbm2ddl schema export
16:07:02,234  INFO SchemaExport:94 - exporting generated schema to database
16:07:02,343  INFO SchemaExport:94 - schema export complete
...
16:07:02,562  INFO EJBContainer:479 - STARTED EJB: dao.Dao ejbName: Dao
...
16:07:02,593  INFO EJBContainer:479 - STARTED EJB: service.Service ejbName: Service
...
[personnes]-------------------------------------------------------------------
[1,0,p1,Paul,31/01/2000,true,2]
[2,0,p2,Sylvie,05/07/2001,false,0]

Der Leser wird gebeten, diese Protokolle zu lesen. Dort finden sich interessante Informationen darüber, was der EJB3-Container tut.

3.2.7. Unit-Tests [TestNG]

Der Programmcode von [TestNG] lautet wie folgt:


package tests;

...
public class TestNG {

    // Service-Ebene
    private IService service = null;

    @BeforeClass
    public void init() throws NamingException, ParseException {
        // Protokoll
        log("init");
        // Der Container wird gestartet EJB3 JBoss
        // Die Konfigurationsdateien ejb3-interceptors-aop.xml und embedded-jboss-beans.xml werden genutzt
        EJB3StandaloneBootstrap.boot(null);

        // Erstellung der anwendungsspezifischen Beans
        EJB3StandaloneBootstrap.deployXmlResource("META-INF/jboss-config.xml");

        // Alle im Klassenpfad gefundenen EJBs bereitstellen (langsam, durchsucht alle)
        // EJB3StandaloneBootstrap.scanClasspath();

        // Alle im Klassenpfad der Anwendung gefundenen EJB-Dateien werden bereitgestellt
        EJB3StandaloneBootstrap.scanClasspath("bin".replace("/", File.separator));

        // Der Kontext JNDI wird initialisiert. Die Datei jndi.properties wird verarbeitet
        InitialContext initialContext = new InitialContext();

        // Instanziierung der Service-Schicht
        service = (IService) initialContext.lookup("Service/local");
        // Die Datenbank wird geleert
        clean();
        // Die Datenbank wird gefüllt
        fill();
        // visuelle Überprüfung
        dumpPersonnes();
    }

    @AfterClass
    public void terminate() {
        // Protokoll
        log("terminate");
        // EJB-Container herunterfahren
        EJB3StandaloneBootstrap.shutdown();
    }

    @BeforeMethod
    public void setUp() throws ParseException {
...
    }

...
}
  • Die Methode „init“ (Zeilen 10–37), die dazu dient, die für die Tests erforderliche Umgebung einzurichten, enthält den zuvor in [InitDB] erläuterten Code.
  • Die Methode „terminate“ (Zeilen 40–45), die am Ende der Tests ausgeführt wird (vorhandene Annotation @AfterClass), beendet den EJB3-Container (Zeile 44).
  • Alles andere entspricht der Spring-Version.

Die Tests sind erfolgreich:

Image

3.2.8. Ändern in SGBD

Um von SGBD zu wechseln, müssen Sie lediglich den Inhalt des Ordners „[META-INF] [2]“ durch den Inhalt des Ordners „SGBD“ im Ordner „[conf] [1]“ ersetzen. Nehmen wir das Beispiel des Servers SQL:

Die Datei [persistence.xml] sieht wie folgt aus:


<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="jpa">

        <!-- Der Anbieter JPA ist Hibernate -->
        <provider>org.hibernate.ejb.HibernatePersistence</provider>

        <!-- die von der Java-Umgebung verwaltete DataSource JTA -->
        <jta-data-source>java:/datasource</jta-data-source>

        <properties>
            <!-- Suche nach Entitäten der Schicht JBA -->
            <property name="hibernate.archive.autodetection" value="class, hbm" />

            <!-- Hibernate-Protokolle SQL
                <property name="hibernate.show_sql" value="true"/>
                <property name="hibernate.format_sql" value="true"/>
                <property name="use_sql_comments" value="true"/>
            -->

            <!-- verwalteter Typ von SGBD -->
            <property name="hibernate.dialect" value="org.hibernate.dialect.SQLServerDialect" />

            <!-- Neuerstellung aller Tabellen (drop+create) bei der Bereitstellung der Persistenz-Einheit -->
            <property name="hibernate.hbm2ddl.auto" value="create" />

        </properties>
    </persistence-unit>

</persistence>

Nur eine Zeile hat sich geändert:

  • Zeile 24: der Dialekt SQL, den Hibernate verwenden soll

Die Datei „[jboss-config.xml]“ des Servers „SQL“ sieht hingegen wie folgt aus:


<?xml version="1.0" encoding="UTF-8"?>

<deployment xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="urn:jboss:bean-deployer bean-deployer_1_0.xsd"
    xmlns="urn:jboss:bean-deployer:2.0">

    <!-- Factory der DataSource -->
    <bean name="datasourceFactory" class="org.jboss.resource.adapter.jdbc.local.LocalTxDataSource">
        <!-- Name JNDI der DataSource -->
        <property name="jndiName">java:/datasource</property>

        <!-- verwaltete Datenbank -->
        <property name="driverClass">com.microsoft.sqlserver.jdbc.SQLServerDriver</property>
        <property name="connectionURL">jdbc:sqlserver://localhost\\SQLEXPRESS:1246;databaseName=jpa</property>
        <property name="userName">jpa</property>
        <property name="password">jpa</property>

        <!-- Eigenschaften des Verbindungspools -->
    ...
    </bean>

</deployment>

Nur die Zeilen 12–15 wurden geändert: Sie geben die Eigenschaften der neuen JDBC-Verbindung an.

Der Leser wird gebeten, die für MySQL5 beschriebenen Tests mit anderen SGBD-Dateien zu wiederholen.

3.2.9. Wechsel der Implementierung JPA

Wie oben bereits erwähnt, haben wir kein Beispiel für die Verwendung des JBoss-EJB3-Containers mit TopLink gefunden. Bis heute (Juni 2007) weiß ich immer noch nicht, ob diese Konfiguration möglich ist.

3.3. Weitere Beispiele

Fassen wir zusammen, was mit der Entität Personne gemacht wurde. Wir haben drei Architekturen aufgebaut, um dieselben Tests durchzuführen:

1 – eine Spring-/Hibernate-Implementierung

2 – eine Spring-/Toplink-Implementierung

3 – eine JBoss-EJB3-/Hibernate-Implementierung

Die Beispiele des Tutorials greifen diese drei Architekturen zusammen mit weiteren Entitäten auf, die im ersten Teil des Tutorials behandelt wurden:

Kategorie – Artikel

  • in [1]: die Spring-/Hibernate-Version
  • in [2]: die Spring-/Toplink-Version
  • in [3]: die Jboss-EJB3-/Hibernate-Version

Person – Adresse – Tätigkeit

  • in [1]: die Spring-/Hibernate-Version
  • in [2]: die Spring-/Toplink-Version
  • in [3]: die JBoss EJB3/Hibernate-Version

Diese Beispiele bringen keine Neuerungen hinsichtlich der Architektur mit sich. Sie sind lediglich in einem Kontext angesiedelt, in dem mehrere Entitäten mit Eins-zu-Viele- oder Viele-zu-Viele-Beziehungen untereinander verwaltet werden müssen, was bei den Beispielen mit der Entität Personne nicht der Fall war.

3.4. Beispiel 3: Spring / JPA in einer Webanwendung

3.4.1. Einführung

Wir greifen hier eine Anwendung auf, die im folgenden Dokument vorgestellt wird:

[ref4]: Grundlagen der Webentwicklung MVC in Java [http://tahe.developpez.com/java/baseswebmvc/].

Dieses Dokument stellt die Grundlagen der Webentwicklung MVC in Java vor. Um das folgende Beispiel zu verstehen, sollte der Leser über diese Grundlagen verfügen. Die Webanwendung nutzt den Tomcat-Server. Die Installation und Verwendung dieses Servers in Eclipse werden in Abschnitt 5.3 beschrieben.

Die Anwendung wurde ursprünglich mit einer [dao]-Schicht entwickelt, die auf dem Tool Ibatis / SqlMap [http://ibatis.apache.org/] basierte, welches die Relational-Objekt-Brücke stellte. Wir beschränken uns darauf, Ibatis durch JPA zu ersetzen. Die Architektur der Anwendung sieht dann wie folgt aus:

Die Webanwendung, die wir erstellen werden, ermöglicht die Verwaltung einer Personengruppe mit vier Funktionen:

  • Liste der Personen in der Gruppe
  • Hinzufügen einer Person zur Gruppe
  • Änderung einer Person in der Gruppe
  • Löschen einer Person aus der Gruppe

Diese vier Grundoperationen sind aus einer Datenbanktabelle bekannt. Die folgenden Screenshots zeigen die Seiten, die die Anwendung mit dem Benutzer austauscht.

 

3.4.2. Das Eclipse-Projekt

Das Eclipse-Projekt der Anwendung sieht wie folgt aus:

  • in [1]: das Webprojekt. Es handelt sich um ein Eclipse-Projekt vom Typ [Dynamic Web Project] [2]. Es befindet sich unter [4] im Ordner [3] der Tutorial-Beispiele. Wir werden es importieren.
  • in [5]: die Quellen und die Konfiguration der Ebenen [service, dao, jpa]. Wir behalten die in Abschnitt 3.1.1 behandelten Ergebnisse [dao, entites, service] aus dem Eclipse-Projekt [hibernate-spring-personnes-metier-dao] bei. Wir entwickeln lediglich die Schicht [web], die hier durch das Paket [web] repräsentiert wird. Außerdem behalten wir die Konfigurationsdateien [persistence.xml, spring-config.xml] dieses Projekts bei, mit der einzigen Ausnahme, dass wir Postgres SGBD verwenden werden, was zu folgenden Änderungen in [spring-config.xml] führt:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" 
...
    <bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
        <property name="dataSource" ref="dataSource" />
        <property name="jpaVendorAdapter">
...
                <property name="databasePlatform" value="org.hibernate.dialect.PostgreSQLDialect" />
...
        </property>
    ...
    </bean>

    <!-- die Datenquelle DBCP -->
    <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
        <property name="driverClassName" value="org.postgresql.Driver" />
        <property name="url" value="jdbc:postgresql:jpa" />
        <property name="username" value="jpa" />
        <property name="password" value="jpa" />
    </bean>
....
</beans>

Die Zeilen 8 und 16–19 wurden an Postgres angepasst.

  • In [6]: Der Ordner [WebContent] enthält die Seiten JSP des Projekts sowie die erforderlichen Bibliotheken. Letztere sind in [8] aufgeführt
  • Die Anwendung kann mit verschiedenen SGBD verwendet werden. Dazu muss lediglich die Datei [spring-config.xml] geändert werden. Der Ordner [conf] [7] enthält die Datei [spring-config.xml], die für verschiedene SGBD angepasst ist.

3.4.3. Die Schicht [web]

Unsere Anwendung weist die folgende mehrschichtige Architektur auf:

Die Schicht [web] stellt dem Benutzer Bildschirme zur Verfügung, über die er die Personengruppe verwalten kann:

  • Liste der Personen in der Gruppe
  • Hinzufügen einer Person zur Gruppe
  • Änderung einer Person in der Gruppe
  • Löschen einer Person aus der Gruppe

Dazu stützt sie sich auf die Schicht [service], die ihrerseits die Schicht [dao] aufruft. Die von der Schicht [web] verwalteten Bildschirmbilder haben wir bereits vorgestellt (Abschnitt 3.4.1). Zur Beschreibung der Webschicht werden wir nacheinander Folgendes vorstellen:

  • ihre Konfiguration
  • ihre Ansichten
  • ihren Controller
  • einige Tests

3.4.3.1. Konfiguration der Webanwendung

Werfen wir noch einmal einen Blick auf die Architektur des Eclipse-Projekts:

 
  • Im Paket [web] befindet sich der Controller der Webanwendung: die Klasse [Application].
  • Die Seiten JSP / JSTL der Anwendung befinden sich in [WEB-INF/vues].
  • Der Ordner [WEB-INF/lib] enthält die für die Anwendung erforderlichen Archive von Drittanbietern. Diese sind im Ordner [Web App Libraries] zu finden.

[web.xml]


Die Datei [web.xml] wird vom Webserver zum Laden der Anwendung verwendet. Ihr Inhalt lautet wie folgt:


<?xml version="1.0" encoding="UTF-8"?>
<web-app id="WebApp_ID" version="2.4" 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">
    <display-name>spring-jpa-hibernate-personnes-crud</display-name>
    <!--  ServletPersonne -->
    <servlet>
        <servlet-name>personnes</servlet-name>
        <servlet-class>web.Application</servlet-class>
        <init-param>
            <param-name>urlEdit</param-name>
            <param-value>/WEB-INF/vues/edit.jsp</param-value>
        </init-param>
        <init-param>
            <param-name>urlErreurs</param-name>
            <param-value>/WEB-INF/vues/erreurs.jsp</param-value>
        </init-param>
        <init-param>
            <param-name>urlList</param-name>
            <param-value>/WEB-INF/vues/list.jsp</param-value>
        </init-param>
    </servlet>
    <!-- Zuordnung ServletPersonne-->
    <servlet-mapping>
        <servlet-name>personnes</servlet-name>
        <url-pattern>/do/*</url-pattern>
    </servlet-mapping>
    <!--  Startseiten -->
    <welcome-file-list>
        <welcome-file>index.jsp</welcome-file>
    </welcome-file-list>
    <!--  Seite für unerwartete Fehler -->
    <error-page>
        <exception-type>java.lang.Exception</exception-type>
        <location>/WEB-INF/vues/exception.jsp</location>
    </error-page>
</web-app>
  • Zeilen 23–26: Die URLs [/do/*] werden vom Servlet [personnes] verarbeitet
  • Zeilen 7–8: Das Servlet [personnes] ist eine Instanz der Klasse [Application], einer Klasse, die wir erstellen werden.
  • Zeilen 9–20: Hier werden drei Parameter [urlList, urlEdit, urlErreurs] definiert, die die URLs der Seiten JSP der Ansichten [list, edit, erreurs] identifizieren.
  • Zeilen 28–30: Die Anwendung verfügt über eine Standard-Startseite [index.jsp], die sich im Stammverzeichnis des Webanwendungsordners befindet.
  • Zeilen 32–35: Die Anwendung verfügt über eine Standard-Fehlerseite, die angezeigt wird, wenn der Webserver eine von der Anwendung nicht behandelte Ausnahme abfängt.
    • Zeile 37: Das Tag <exception-type> gibt den Ausnahmetyp an, der von der Direktive <error-page> behandelt wird, hier den Typ [java.lang.Exception] und davon abgeleitete Typen, also alle Ausnahmen.
    • Zeile 38: Das Tag <location> gibt die Seite JSP an, die angezeigt werden soll, wenn eine Ausnahme des durch <exception-type> definierten Typs auftritt. Die aufgetretene Ausnahme ist auf dieser Seite in einem Objekt namens „exception“ verfügbar, sofern die Seite die folgende Anweisung enthält:

<%@ page isErrorPage="true" %>
  • (Fortsetzung)
    • Wenn <exception-type> einen Typ T1 angibt und eine Ausnahme vom Typ T2, die nicht von T1 abgeleitet ist, bis zum Webserver weitergeleitet wird, sendet dieser dem Client eine proprietäre Ausnahmeseite, die in der Regel wenig benutzerfreundlich ist. Daher ist das <error-page>-Tag in der Datei [web.xml] von Bedeutung.

[index.jsp]


Diese Seite wird angezeigt, wenn ein Benutzer den Anwendungskontext direkt aufruft, ohne eine URL anzugeben, c.a.d. Hier [/spring-jpa-hibernate-personnes-crud]. Ihr Inhalt lautet wie folgt:


<%@ page language="java" pageEncoding="ISO-8859-1" contentType="text/html;charset=ISO-8859-1"%>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>

<c:redirect url="/do/list"/>

[index.jsp] leitet den Kunden (Zeile 4) zur URL [/do/list] weiter. Diese URL zeigt die Liste der Personen der Gruppe an.

3.4.3.2. Die Seiten JSP / JSTL der Anwendung


Die Ansicht [list.jsp]


Sie dient dazu, die Liste der Personen anzuzeigen:

Image

Der Code lautet wie folgt:


<%@ page language="java" pageEncoding="ISO-8859-1" contentType="text/html;charset=ISO-8859-1"%>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<%@ taglib uri="/WEB-INF/taglibs-datetime.tld" prefix="dt" %>

<html>
    <head>
        <title>MVC - Personnes</title>
    </head>
    <body background="<c:url value="/ressources/standard.jpg"/>">
            <c:if test="${erreurs!=null}">
                <h3>Les erreurs suivantes se sont produites :</h3>
                <ul>
                    <c:forEach items="${erreurs}" var="erreur">
                        <li><c:out value="${erreur}"/></li>
                    </c:forEach>
                </ul>
            <hr>
        </c:if>
        <h2>Liste des personnes</h2>
        <table border="1">
            <tr>
                <th>Id</th>
                <th>Version</th>
                <th>Pr&eacute;nom</th>
                <th>Nom</th>
                <th>Date de naissance</th>
                <th>Mari&eacute;</th>
                <th>Nombre d'enfants</th>
                <th></th>
            </tr>
            <c:forEach var="personne" items="${personnes}">
                <tr>
                    <td><c:out value="${personne.id}"/></td>
                    <td><c:out value="${personne.version}"/></td>
                    <td><c:out value="${personne.prenom}"/></td>
                    <td><c:out value="${personne.nom}"/></td>
                    <td><dt:format pattern="dd/MM/yyyy">${personne.datenaissance.time}</dt:format></td>
                    <td><c:out value="${personne.marie}"/></td>
                    <td><c:out value="${personne.nbenfants}"/></td>
                    <td><a href="<c:url value="/do/edit?id=${personne.id}"/>">Modifier</a></td>
                    <td><a href="<c:url value="/do/delete?id=${personne.id}"/>">Supprimer</a></td>
                </tr>
            </c:forEach>
        </table>
        <br>
        <a href="<c:url value="/do/edit?id=-1"/>">Ajout</a>
    </body>
</html>
  • Diese Ansicht erhält zwei Elemente in ihrem Modell:
    • das Element [personnes], das einem Objekt vom Typ [List] zugeordnet ist, das wiederum aus Objekten vom Typ [Personne] besteht: eine Liste von Personen.
    • das optionale Element [erreurs], das einem Objekt vom Typ [List] aus Objekten vom Typ [String] zugeordnet ist: eine Liste von Fehlermeldungen.
  • Zeilen 31–43: Die Liste ${personnes} wird durchlaufen, um ein Array vom Typ HTML anzuzeigen, das die Personen der Gruppe enthält.
  • Zeile 40: Die URL, auf die der Link [Modifier] verweist, wird durch das Feld [id] der aktuellen Person festgelegt, damit der mit der URL [/do/edit] verknüpfte Controller weiß, welche Person geändert werden soll.
  • Zeile 41: Dasselbe gilt für den Link [Supprimer].
  • Zeile 37: Um das Geburtsdatum der Person in der Form JJ/MM/AAAA anzuzeigen, verwendet man das Tag <dt> aus der Tag-Bibliothek [DateTime] des Apache-Projekts [Jakarta Taglibs]:

Image

Die Beschreibungsdatei dieser Tag-Bibliothek ist in Zeile 3 definiert.

  • Zeile 46: Der Link [Ajout] zum Hinzufügen einer neuen Person verweist auf die URL [/do/edit], genau wie der Link [Modifier] in Zeile 40. Der Wert -1 des Parameters [id] gibt an, dass es sich um einen Hinzufügungsvorgang und nicht um eine Änderung handelt.
  • Zeilen 10–18: Befindet sich das Element ${erreurs} in der Vorlage, werden die darin enthaltenen Fehlermeldungen angezeigt.

Die Ansicht [edit.jsp]


Sie dient dazu, das Formular zum Hinzufügen einer neuen Person oder zum Ändern einer bestehenden Person anzuzeigen:

Der Code der Ansicht [edit.jsp] lautet wie folgt:


<%@ page language="java" pageEncoding="ISO-8859-1" contentType="text/html;charset=ISO-8859-1"%>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<%@ taglib uri="/WEB-INF/taglibs-datetime.tld" prefix="dt" %>

<html>
    <head>
        <title>MVC - Personnes</title>
    </head>
    <body background="../ressources/standard.jpg">
        <h2>Ajout/Modification d'une personne</h2>
        <c:if test="${erreurEdit!=''}">
            <h3>Echec de la mise à jour :</h3>
          L'erreur suivante s'est produite : ${erreurEdit}
            <hr>
        </c:if>
        <form method="post" action="<c:url value="/do/validate"/>">
            <table border="1">
                <tr>
                    <td>Id</td>
                    <td>${id}</td>
                </tr>
                <tr>
                    <td>Version</td>
                    <td>${version}</td>
                </tr>
                <tr>
                    <td>Pr&eacute;nom</td>
                    <td>
                        <input type="text" value="${prenom}" name="prenom" size="20">
                    </td>
                    <td>${erreurPrenom}</td>
                </tr>
                <tr>
                    <td>Nom</td>
                    <td>
                        <input type="text" value="${nom}" name="nom" size="20">
                    </td>
                    <td>${erreurNom}</td>
                </tr>
                <tr>
                <td>Date de naissance (JJ/MM/AAAA)</td>
                    <td>
                        <input type="text" value="${datenaissance}" name="datenaissance">
                    </td>
                    <td>${erreurDateNaissance}</td>
                </tr>
                <tr>
                    <td>Mari&eacute;</td>
                    <td>
                        <c:choose>
                            <c:when test="${marie}">
                                <input type="radio" name="marie" value="true" checked>Oui
                                <input type="radio" name="marie" value="false">Non
                            </c:when>
                            <c:otherwise>
                                <input type="radio" name="marie" value="true">Oui
                                <input type="radio" name="marie" value="false" checked>Non
                            </c:otherwise>
                        </c:choose>
                    </td>
                </tr>
                <tr>
                    <td>Nombre d'enfants</td>
                    <td>
                        <input type="text" value="${nbenfants}" name="nbenfants">
                    </td>
                    <td>${erreurNbEnfants}</td>
                </tr>
            </table>
            <br>
            <input type="hidden" value="${id}" name="id">
      <input type="hidden" value="${version}" name="version">
            <input type="submit" value="Valider">
            <a href="<c:url value="/do/list"/>">Annuler</a>
        </form>
    </body>
</html>

Diese Ansicht zeigt ein Formular zum Hinzufügen einer neuen Person oder zum Aktualisieren einer bestehenden Person an. Im weiteren Verlauf und zur Vereinfachung der Darstellung werden wir fortan nur noch den Begriff [mise à jour] verwenden. Die Schaltfläche [Valider] (Zeile 73) löst den Aufruf von POST des Formulars unter der URL [/do/validate] (Zeile 16) aus. Wenn der Aufruf von POST fehlschlägt, wird die Ansicht [edit.jsp] mit den aufgetretenen Fehlern erneut angezeigt; andernfalls wird die Ansicht [list.jsp] angezeigt.

  • Die Ansicht [edit.jsp], die sowohl bei einem fehlgeschlagenen GET als auch bei einem fehlgeschlagenen POST angezeigt wird, erhält folgende Elemente in ihre Vorlage:
Attribut
GET
POST
id
ID der aktualisierten
aktualisiert
dito
version
die Version
wie oben
prenom
sein Vorname
Eingegebener Vorname
nom
sein Nachname
Eingegebener Nachname
datenaissance
Geburtsdatum
Eingegebenes Geburtsdatum
marie
Familienstand
Eingetragener Familienstand
nbenfants
Anzahl der Kinder
Eingegebene Anzahl der Kinder
erreurEdit
leer
eine Fehlermeldung, die einen Fehler beim Hinzufügen
oder der Änderung zum Zeitpunkt des POST, ausgelöst
durch die Schaltfläche [Envoyer]. Leer, wenn kein Fehler vorliegt.
erreurPrenom
leer
meldet einen falschen Vornamen – andernfalls leer
erreurNom
leer
zeigt einen falschen Nachnamen an – andernfalls leer
erreurDateNaissance
leer
meldet ein falsches Geburtsdatum – andernfalls leer
erreurNbEnfants
leer
meldet eine falsche Anzahl von Kindern – andernfalls leer
  • Zeilen 11–15: Wenn die Ausführung von POST im Formular fehlschlägt, wird [erreurEdit!=''] angezeigt und eine Fehlermeldung ausgegeben.
  • Zeile 16: Das Formular wird an die URL [/do/validate] gesendet
  • Zeile 20: Das Element [id] der Vorlage wird angezeigt
  • Zeile 24: Das Element [version] der Vorlage wird angezeigt
  • Zeilen 26–32: Eingabe des Vornamens der Person:
    • Bei der ersten Anzeige des Formulars (GET) zeigt ${Vorname} den aktuellen Wert des Feldes [prenom] des aktualisierten Objekts [Personne] an, und ${erreurPrenom} ist leer.
    • Im Falle eines Fehlers nach dem POST werden der eingegebene Wert ${Vorname} sowie die eventuelle Fehlermeldung ${erreurPrenom} erneut angezeigt.
  • Zeilen 33–39: Eingabe des Namens der Person
  • Zeilen 40–46: Eingabe des Geburtsdatums der Person
  • Zeilen 47–61: Eingabe des Familienstands der Person mithilfe eines Optionsfelds. Der Wert des Felds [marie] des Objekts [Personne] wird verwendet, um zu bestimmen, welches der beiden Optionsfelder ausgewählt werden muss.
  • Zeilen 62–68: Eingabe der Anzahl der Kinder der Person
  • Zeile 71: Ein verstecktes Feld HTML mit dem Namen [id], dessen Wert dem Feld [id] der gerade aktualisierten Person entspricht; -1 bei einem Hinzufügen, ein anderer Wert bei einer Änderung.
  • Zeile 72: ein verstecktes Feld HTML mit dem Namen [version], dessen Wert dem Feld [id] der gerade bearbeiteten Person entspricht.
  • Zeile 73: Die Schaltfläche [Valider] vom Typ [Submit] des Formulars
  • Zeile 74: Ein Link, über den man zur Liste der Personen zurückkehren kann. Er wurde mit „[Annuler]“ bezeichnet, da man damit das Formular verlassen kann, ohne es zu speichern.

Die Ansicht [exception.jsp]


Sie dient dazu, eine Seite anzuzeigen, die darauf hinweist, dass eine von der Anwendung nicht behandelte Ausnahme aufgetreten ist, die an den Webserver weitergeleitet wurde.

Löschen wir beispielsweise eine Person, die in der Gruppe nicht existiert:

Der Code der Ansicht [exception.jsp] lautet wie folgt:


<%@ page language="java" pageEncoding="ISO-8859-1" contentType="text/html;charset=ISO-8859-1"%>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<%@ page isErrorPage="true" %>

<%
  response.setStatus(200);
%>

<html>
    <head>
        <title>MVC - Personnes</title>
    </head>
    <body background="<c:url value="/ressources/standard.jpg"/>">
        <h2>MVC - personnes</h2>
        L'exception suivante s'est produite :
        <%= exception.getMessage()%>
        <br><br>
        <a href="<c:url value="/do/list"/>">Retour &agrave; la liste</a>
    </body>
</html>
  • Diese Ansicht erhält in ihrem Modell ein Schlüsselelement [exception], bei dem es sich um die vom Webserver abgefangene Ausnahme handelt. Damit dieses Element vom Webserver in das Modell der Seite JSP aufgenommen wird, muss die Seite das Tag in Zeile 3 definiert haben.
  • Zeile 6: Der Statuscode HTTP der Antwort wird auf 200 gesetzt. Dies ist der erste Header HTTP der Antwort. Der Code 200 signalisiert dem Client, dass seine Anfrage erfolgreich bearbeitet wurde. In der Regel wurde ein Dokument mit dem Code HTML in die Antwort des Servers integriert. Dies ist hier der Fall. Wird der Statuscode HTTP der Antwort nicht auf 200 gesetzt, hat er hier den Wert 500, was bedeutet, dass ein Fehler aufgetreten ist. Da der Webserver eine nicht behandelte Ausnahme abgefangen hat, erkennt er diese Situation als abnormal und meldet sie mit dem Code 500. Die Reaktion auf den Statuscode 500 ist je nach Browser unterschiedlich: Firefox zeigt das Dokument an, das dieser Antwort beiliegen kann, während ein anderer Browser dieses Dokument ignoriert und seine eigene Seite anzeigt. Aus diesem Grund haben wir den Code 500 durch den Code 200 ersetzt.
  • Zeile 16: Der Text der Ausnahme wird angezeigt
  • Zeile 18: Dem Benutzer wird ein Link angeboten, um zur Liste der Personen zurückzukehren

Die Ansicht [erreurs.jsp]


Sie dient dazu, eine Seite anzuzeigen, die auf Initialisierungsfehler der Anwendung c.a.d hinweist. Dabei handelt es sich um Fehler, die bei der Ausführung der Methode [init] des Controller-Servlets festgestellt wurden. Dies kann beispielsweise das Fehlen eines Parameters in der Datei [web.xml] sein, wie das folgende Beispiel zeigt:

Image

Der Code der Seite [erreurs.jsp] lautet wie folgt:


<%@ page language="java" contentType="text/html; charset=ISO-8859-1"
    pageEncoding="ISO-8859-1"%>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>

<html>
    <head>
      <title>MVC - Personnes</title>
  </head>
  <body>
      <h2>Les erreurs suivantes se sont produites</h2>
    <ul>
            <c:forEach var="erreur" items="${erreurs}">
                <li>${erreur}</li>
            </c:forEach>
    </ul>
  </body>
</html>

Die Seite erhält in ihrer Vorlage ein Element [erreurs], bei dem es sich um ein Objekt vom Typ [ArrayList] handelt, das Objekte vom Typ [String] enthält, wobei es sich bei letzteren um Fehlermeldungen handelt. Diese werden durch die Schleife in den Zeilen 13–15 angezeigt.

3.4.3.3. Der Controller der Anwendung

Der Controller [Application] ist im Paket [web] definiert:

Image


Struc tur und Initialisierung des Controllers


Das Grundgerüst des Controllers [Application] sieht wie folgt aus:


package web;

...


@SuppressWarnings("serial")
public class Application extends HttpServlet {
    // Instanzparameter
    private String urlErreurs = null;
    private ArrayList erreursInitialisation = new ArrayList<String>();
    private String[] paramètres = { "urlList", "urlEdit", "urlErreurs" };
    private Map params = new HashMap<String, String>();

    // Dienst
    private IService service = null;

    // Init
    @SuppressWarnings("unchecked")
    public void init() throws ServletException {
        // Die Initialisierungsparameter des Servlets werden abgerufen
        ServletConfig config = getServletConfig();
        // Die übrigen Initialisierungsparameter werden verarbeitet
        String valeur = null;
        for (int i = 0; i < paramètres.length; i++) {
            // Wert des Parameters
            valeur = config.getInitParameter(paramètres[i]);
            // Ist der Parameter vorhanden?
            if (valeur == null) {
                // Der Fehler wird protokolliert
                erreursInitialisation.add("Le paramètre [" + paramètres[i] + "] n'a pas été initialisé");
            } else {
                // Der Wert des Parameters wird gespeichert
                params.put(paramètres[i], valeur);
            }
        }
        // Die URL der Ansicht [erreurs] wird speziell behandelt
        urlErreurs = config.getInitParameter("urlErreurs");
        if (urlErreurs == null)
            throw new ServletException("Le paramètre [urlErreurs] n'a pas été initialisé");
        // Konfiguration der Anwendung
        ApplicationContext ctx = new ClassPathXmlApplicationContext("spring-config.xml");
        // Service-Ebene
        service = (IService) ctx.getBean("service");
        // Die Datenbank wird geleert
        clean();
        // Die Datenbank wird gefüllt
        try {
            fill();
        } catch (ParseException e) {
            throw new ServletException(e);
        }
    }

    // Tabelle füllen
    public void fill() throws ParseException {
        // Personen anlegen
        Personne p1 = new Personne("p1", "Paul", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2);
        Personne p2 = new Personne("p2", "Sylvie", new SimpleDateFormat("dd/MM/yy").parse("05/07/2001"), false, 0);
        // die gespeichert werden
        service.saveArray(new Personne[] { p1, p2 });
    }

    // Elemente aus der Tabelle löschen
    public void clean() {
        for (Personne p : service.getAll()) {
            service.deleteOne(p.getId());
        }
    }

    // GET
    @SuppressWarnings("unchecked")
    public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
...
    }

    // Anzeige der Personenliste
    private void doListPersonnes(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
...
    }

    // Person bearbeiten / hinzufügen
    private void doEditPersonne(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
...
    }

    // Löschen einer Person
    private void doDeletePersonne(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
...
    }

    // Bestätigung der Änderung / des Hinzufügens einer Person
    public void doValidatePersonne(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
...
    }

    // Anzeige des vorausgefüllten Formulars
    private void showFormulaire(HttpServletRequest request, HttpServletResponse response, String erreurEdit) throws ServletException, IOException {
    ...
    }

    // Absenden
    public void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
        // Weiterleitung an GET
        doGet(request, response);
    }

}
  • Zeilen 21–34: Hier werden die erwarteten Parameter aus der Datei [web.xml] abgerufen.
  • Zeilen 37–39: Der Parameter [urlErreurs] muss zwingend vorhanden sein, da er die URL der Ansicht [erreurs] angibt, die eventuelle Initialisierungsfehler anzeigen kann. Falls dieser nicht vorhanden ist, wird die Anwendung durch den Aufruf von [ServletException] (Zeile 39) abgebrochen. Diese Ausnahme wird an den Webserver weitergeleitet und durch das Tag <error-page> in der Datei [web.xml] behandelt. Die Ansicht [exception.jsp] wird daher angezeigt:

Image

Der obige Link [Retour à la liste] ist nicht funktionsfähig. Seine Verwendung führt zur gleichen Antwort, solange die Anwendung nicht geändert und neu geladen wurde. Er ist, wie wir bereits gesehen haben, für andere Arten von Ausnahmen nützlich.

  • Zeilen 40–43: Nutzen die Spring-Konfigurationsdatei, um eine Referenz auf die Ebene [service] abzurufen. Nach der Initialisierung des Controllers verfügen dessen Methoden über eine Referenz [service] auf die Schicht [service] (Zeile 15), die sie nutzen, um die vom Benutzer angeforderten Aktionen auszuführen. Diese werden von der Methode [doGet] abgefangen, die sie an eine bestimmte Methode des Controllers weiterleitet:
Url
Methode HTTP
Controller-Methode
/do/list
GET
doListPersonnes
/do/edit
GET
doEditPersonne
/do/validate
POST
doValidatePersonne
/do/delete
GET
doDeletePersonne

Die Methode [doGet]


Diese Methode dient dazu, die vom Benutzer angeforderten Aktionen an die richtige Methode weiterzuleiten. Ihr Code lautet wie folgt:


// GET
    @SuppressWarnings("unchecked")
    public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {

        
// Es wird überprüft, wie die Initialisierung des Servlets verlaufen ist
        if (erreursInitialisation.size() != 0) {
            // Weiterleitung zur Fehlerseite
            request.setAttribute("erreurs", erreursInitialisation);
            getServletContext().getRequestDispatcher(urlErreurs).forward(request, response);
            // Ende
            return;
        }
        // Die Methode zum Senden der Anfrage wird abgerufen
        String méthode = request.getMethod().toLowerCase();
        // Die auszuführende Aktion wird abgerufen
        String action = request.getPathInfo();
        // Aktion?
        if (action == null) {
            action = "/list";
        }
        // Aktion ausführen
        if (méthode.equals("get") && action.equals("/list")) {
            // Liste der Personen
            doListPersonnes(request, response);
            return;
        }
        if (méthode.equals("get") && action.equals("/delete")) {
            // Löschen einer Person
            doDeletePersonne(request, response);
            return;
        }
        if (méthode.equals("get") && action.equals("/edit")) {
            // Anzeige des Formulars zum Hinzufügen/Bearbeiten einer Person
            doEditPersonne(request, response);
            return;
        }
        if (méthode.equals("post") && action.equals("/validate")) {
            // Bestätigung des Formulars zum Hinzufügen/Bearbeiten einer Person
            doValidatePersonne(request, response);
            return;
        }
        // Sonstige Fälle
        doListPersonnes(request, response);
    }
  • Zeilen 7–13: Es wird überprüft, ob die Liste der Initialisierungsfehler leer ist. Ist dies nicht der Fall, wird die Ansicht [erreurs(erreurs)] angezeigt, die den oder die Fehler meldet.
  • Zeile 15: Es wird die Methode [get] oder [post] abgerufen, die der Client für seine Anfrage verwendet hat.
  • Zeile 17: Der Wert des Parameters [action] aus der Anfrage wird abgerufen.
  • Zeilen 23–27: Verarbeitung der Anfrage [GET /do/list], die die Liste der Personen anfordert.
  • Zeilen 28–32: Bearbeitung der Anfrage [GET /do/delete], die die Löschung einer Person anfordert.
  • Zeilen 33–37: Bearbeitung der Anfrage [GET /do/edit], die das Formular zur Aktualisierung einer Person anfordert.
  • Zeilen 38–42: Bearbeitung der Anfrage [POST /do/validate], die die Validierung der aktualisierten Person anfordert.
  • Zeile 44: Wenn die angeforderte Aktion nicht eine der fünf vorherigen ist, wird so verfahren, als wäre es [GET /do/list].

Die Methode [doListPersonnes]


Diese Methode verarbeitet die Anfrage [GET /do/list], die die Liste der Personen anfordert:

Image

Der Code lautet wie folgt:


    // Anzeige der Personenliste
    private void doListPersonnes(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // Das Vorlagenmodell der Ansicht [list]
        request.setAttribute("personnes", service.getAll());
        // Anzeige der Ansicht [list]
        getServletContext().getRequestDispatcher((String) params.get("urlList")).forward(request, response);
}
  • Zeile 4: Von der Ebene [service] wird die Liste der Personen der Gruppe angefordert und diese wird im Modell unter dem Schlüssel „Personen“ abgelegt.
  • Zeile 6: Die in Abschnitt 3.4.3.2 beschriebene Ansicht [list.jsp] wird angezeigt.

Die Methode [doDeletePersonne]


Diese Methode verarbeitet die Abfrage [GET /do/delete?id=XX], die das Löschen der Person mit der ID=XX anfordert. Die URL [/do/delete?id=XX] ist die der Links [Supprimer] der Ansicht [list.jsp]:

Image

dessen Code wie folgt lautet:


...
<html>
    <head>
        <title>MVC - Personnes</title>
    </head>
    <body background="<c:url value="/ressources/standard.jpg"/>">
...
            <c:forEach var="personne" items="${personnes}">
                <tr>
...
                    <td><a href="<c:url value="/do/edit?id=${personne.id}"/>">Modifier</a></td>
                    <td><a href="<c:url value="/do/delete?id=${personne.id}"/>">Supprimer</a></td>
                </tr>
            </c:forEach>
        </table>
        <br>
        <a href="<c:url value="/do/edit?id=-1"/>">Ajout</a>
    </body>
</html>

In Zeile 12 ist die URL [/do/delete?id=XX] des Links [Supprimer] zu sehen. Die Methode [doDeletePersonne], die diese URL verarbeiten soll, muss die Person mit der ID „XX“ entfernen und anschließend die neue Liste der Personen der Gruppe anzeigen. Ihr Code lautet wie folgt:


// Löschen einer Person
    private void doDeletePersonne(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // Abruf der ID der Person
        int id = Integer.parseInt(request.getParameter("id"));
        // Person löschen
        service.deleteOne(id);
        // Weiterleitung zur Personenliste
        response.sendRedirect("list");
    }
  • Zeile 4: Die verarbeitete URL hat die Form [/do/delete?id=XX]. Der Wert [XX] wird aus dem Parameter [id] abgerufen.
  • Zeile 6: Die Ebene [service] wird aufgefordert, die Person mit der erhaltenen ID zu löschen. Wir führen keinerlei Überprüfung durch. Wenn die Person, die gelöscht werden soll, nicht existiert, löst die Schicht [dao] eine Ausnahme aus, die von der Schicht [service] weitergeleitet wird. Auch hier im Controller wird diese Ausnahme nicht behandelt. Sie wird daher bis zum Webserver weitergeleitet, der entsprechend der Konfiguration die Seite [exception.jsp] anzeigt, die in Abschnitt 3.4.3.2 beschrieben ist:

Image

  • Zeile 9: Wenn die Löschung erfolgt ist (keine Ausnahme), wird der Client aufgefordert, sich zur relativen URL [list] weiterzuleiten. Da die soeben verarbeitete Seite [/do/delete] ist, lautet die Weiterleitungs-URL [/do/list]. Der Browser wird daher dazu veranlasst, die Seite [GET /do/list] aufzurufen, wodurch die Liste der Personen angezeigt wird.

Die Methode [doEditPersonne]


Diese Methode verarbeitet die Anfrage [GET /do/edit?id=XX], die das Formular zur Aktualisierung der Person mit der ID = XX anfordert. Die URL [/do/edit?id=XX] ist die der Links [Modifier] und des Links [Ajout] der Ansicht [list.jsp]:

Image

dessen Code wie folgt lautet:


...
<html>
    <head>
        <title>MVC - Personnes</title>
    </head>
    <body background="<c:url value="/ressources/standard.jpg"/>">
...
            <c:forEach var="personne" items="${personnes}">
                <tr>
...
                    <td><a href="<c:url value="/do/edit?id=${personne.id}"/>">Modifier</a></td>
                    <td><a href="<c:url value="/do/delete?id=${personne.id}"/>">Supprimer</a></td>
                </tr>
            </c:forEach>
        </table>
        <br>
        <a href="<c:url value="/do/edit?id=-1"/>">Ajout</a>
    </body>
</html>

In Zeile 11 ist die URL [/do/edit?id=XX] des Links [Modifier] zu sehen und in Zeile 17 die URL [/do/edit?id=-1] des Links [Ajout]. Die Methode [doEditPersonne] muss das Bearbeitungsformular für die Person mit der ID „XX“ anzeigen oder, falls es sich um einen Neuzugang handelt, ein leeres Formular anzeigen.

  • In [1] oben ist das Formular zum Hinzufügen und in [2] das Formular zum Ändern.

Der Code der Methode [doEditPersonne] lautet wie folgt:


// Person bearbeiten / hinzufügen
    private void doEditPersonne(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // Die ID der Person wird abgerufen
        int id = Integer.parseInt(request.getParameter("id"));
        // Hinzufügen oder Bearbeiten?
        Personne personne = null;
        if (id != -1) {
            // Änderung – die zu ändernde Person wird abgerufen
            personne = service.getOne(id);
            request.setAttribute("id", personne.getId());
            request.setAttribute("version", personne.getVersion());
        } else {
            // Hinzufügen – Es wird eine leere Person angelegt
            personne = new Personne();
            request.setAttribute("id", -1);
            request.setAttribute("version", -1);
        }
        // das Objekt [Personne] wird in die Sitzung des Benutzers aufgenommen
        request.getSession().setAttribute("personne", personne);
        // sowie in die Ansichtvorlage [edit]
        request.setAttribute("erreurEdit", "");
        request.setAttribute("prenom", personne.getPrenom());
        request.setAttribute("nom", personne.getNom());
        Date dateNaissance = personne.getDatenaissance();
        if (dateNaissance != null) {
            request.setAttribute("datenaissance", new SimpleDateFormat("dd/MM/yyyy").format(dateNaissance));
        } else {
            request.setAttribute("datenaissance", "");
        }
        request.setAttribute("marie", personne.isMarie());
        request.setAttribute("nbenfants", personne.getNbenfants());
        // Anzeige der Ansicht [edit]
        getServletContext().getRequestDispatcher((String) params.get("urlEdit")).forward(request, response);
    }
  • GET hat als Ziel eine URL vom Typ [/do/edit?id=XX]. In Zeile 4 rufen wir den Wert von [id] ab. Anschließend gibt es zwei Fälle:
    1. id ist ungleich -1. Dann handelt es sich um eine Änderung, und es muss ein Formular angezeigt werden, das mit den Daten der zu ändernden Person vorausgefüllt ist. In Zeile 9 wird diese Person von der Ebene [service] angefordert.
    2. id ist gleich -1. Dann handelt es sich um einen Neuzugang, und es muss ein leeres Formular angezeigt werden. Dazu wird in Zeile 14 eine leere Person angelegt.
    3. In beiden Fällen werden die Elemente [id, version] der Seitenvorlage [edit.jsp], die in Abschnitt 3.4.3.2 beschrieben ist, initialisiert.
  • Das resultierende Objekt [Personne] wird in die Seitenvorlage [edit.jsp] eingefügt. Diese Vorlage enthält die folgenden Elemente: [erreurEdit, id, version, prenom, erreurPrenom, nom, erreurNom, datenaissance, erreurDateNaissance, marie, nbenfants, erreurNbEnfants]. Diese Elemente werden in den Zeilen 19–31 initialisiert, mit Ausnahme derjenigen, deren Wert die leere Zeichenkette [erreurPrenom, erreurNom, erreurDateNaissance, erreurNbEnfants] ist. Es ist bekannt, dass die Bibliothek JSTL bei deren Fehlen in der Vorlage eine leere Zeichenkette als ihren Wert anzeigt. Obwohl das Element [erreurEdit] ebenfalls eine leere Zeichenkette als Wert hat, wird es dennoch initialisiert, da auf der Seite [edit.jsp] eine Überprüfung seines Werts erfolgt.
  • Sobald das Modell bereit ist, wird die Steuerung an die Seite [edit.jsp], Zeile 33, übergeben, die die Ansicht [edit] generiert.

Die Methode [doValidatePersonne]


Diese Methode verarbeitet die Anfrage [POST /do/validate], die das Aktualisierungsformular validiert. Diese Anfrage POST wird durch die Schaltfläche [Valider] ausgelöst:

Image

Zur Erinnerung: Die Eingabefelder des Formulars HTML aus der obigen Ansicht:


<form method="post" action="<c:url value="/do/validate"/>">
...
                        <input type="text" value="${nom}" name="nom" size="20">
...
                        <input type="text" value="${datenaissance}" name="datenaissance">
...
                        <c:choose>
                            <c:when test="${marie}">
                                <input type="radio" name="marie" value="true" checked>Oui
                                <input type="radio" name="marie" value="false">Non
                            </c:when>
                            <c:otherwise>
                                <input type="radio" name="marie" value="true">Oui
                                <input type="radio" name="marie" value="false" checked>Non
                            </c:otherwise>
                        </c:choose>
...
                        <input type="text" value="${nbenfants}" name="nbenfants">
...
            <input type="hidden" value="${id}" name="id">
      <input type="hidden" value="${version}" name="version">
            <input type="submit" value="Valider">
            <a href="<c:url value="/do/list"/>">Annuler</a>
        </form>

Die Anfrage POST enthält die Parameter [prenom, nom, datenaissance, marie, nbenfants, id] und wird an die URL [/do/validate] gesendet (Zeile 1). Sie wird von der folgenden Methode [doValidatePersonne] verarbeitet:


// Bestätigung der Änderung/des Hinzufügens einer Person
    public void doValidatePersonne(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // die gebuchten Elemente werden abgerufen
        boolean formulaireErroné = false;
        boolean erreur;
        // der Vorname
        String prenom = request.getParameter("prenom").trim();
        // Ist der Vorname gültig?
        if (prenom.length() == 0) {
            // Fehler vermerken
            request.setAttribute("erreurPrenom", "Le prénom est obligatoire");
            formulaireErroné = true;
        }
        // Nachname
        String nom = request.getParameter("nom").trim();
        // Ist der Vorname gültig?
        if (nom.length() == 0) {
            // Fehler wird vermerkt
            request.setAttribute("erreurNom", "Le nom est obligatoire");
            formulaireErroné = true;
        }
        // Geburtsdatum
        Date datenaissance = null;
        try {
            datenaissance = new SimpleDateFormat("dd/MM/yyyy").parse(request.getParameter("datenaissance").trim());
        } catch (ParseException e) {
            // Fehler wird vermerkt
            request.setAttribute("erreurDateNaissance", "Date incorrecte");
            formulaireErroné = true;
        }
        // Familienstand
        boolean marie = Boolean.parseBoolean(request.getParameter("marie").trim());
        // Anzahl der Kinder
        int nbenfants = 0;
        erreur = false;
        try {
            nbenfants = Integer.parseInt(request.getParameter("nbenfants").trim());
            if (nbenfants < 0) {
                erreur = true;
            }
        } catch (NumberFormatException ex) {
            // Fehler vermerken
            erreur = true;
        }
        // Falsche Anzahl der Kinder?
        if (erreur) {
            // Der Fehler wird gemeldet
            request.setAttribute("erreurNbEnfants", "Nombre d'enfants incorrect");
            formulaireErroné = true;
        }
        // Personen-ID
        int id = Integer.parseInt(request.getParameter("id"));
        // Ist das Formular fehlerhaft?
        if (formulaireErroné) {
            // Das Formular wird mit den Fehlermeldungen erneut angezeigt
            showFormulaire(request, response, "");
            // Fertig
            return;
        }
        // Das Formular ist korrekt – die in der Sitzung gespeicherte Person wird aktualisiert
        // mit den vom Client gesendeten Informationen
        Personne personne = (Personne)request.getSession().getAttribute("personne");
        personne.setDatenaissance(datenaissance);
        personne.setMarie(marie);
        personne.setNbenfants(nbenfants);
        personne.setNom(nom);
        personne.setPrenom(prenom);
        // Persistenz
        try {
            if (id == -1) {
                // Erstellung
                service.saveOne(personne);
            } else {
                // Aktualisierung
                service.updateOne(personne);
            }
        } catch (DaoException ex) {
            // Das Formular wird mit der Meldung über den aufgetretenen Fehler erneut angezeigt
            showFormulaire(request, response, ex.getMessage());
            // Fertig
            return;
        }
        // Weiterleitung zur Personenliste
        response.sendRedirect("list");
    }

    // Anzeige des vorausgefüllten Formulars
    private void showFormulaire(HttpServletRequest request, HttpServletResponse response, String erreurEdit) throws ServletException, IOException {
        // Die Vorlage der Ansicht [edit] wird vorbereitet
        request.setAttribute("erreurEdit", erreurEdit);
        request.setAttribute("id", request.getParameter("id"));
        request.setAttribute("version", request.getParameter("version"));
        request.setAttribute("prenom", request.getParameter("prenom").trim());
        request.setAttribute("nom", request.getParameter("nom").trim());
        request.setAttribute("datenaissance", request.getParameter("datenaissance").trim());
        request.setAttribute("marie", request.getParameter("marie"));
        request.setAttribute("nbenfants", request.getParameter("nbenfants").trim());
        // Anzeige der Ansicht [edit]
        getServletContext().getRequestDispatcher((String) params.get("urlEdit")).forward(request, response);
    }
  • Zeilen 7–13: Der Parameter [prenom] der Anfrage POST wird abgerufen und auf seine Gültigkeit überprüft. Sollte er sich als fehlerhaft erweisen, wird das Element [erreurPrenom] mit einer Fehlermeldung initialisiert und in die Attribute der Abfrage aufgenommen.
  • Zeilen 15–21: Für den Parameter [nom] wird in ähnlicher Weise vorgegangen
  • Zeilen 23–30: Für den Parameter [datenaissance] wird in ähnlicher Weise vorgegangen
  • Zeile 32: Der Parameter [marie] wird abgerufen. Seine Gültigkeit wird nicht überprüft, da er a priori aus dem Wert eines Optionsfelds stammt. Allerdings hindert nichts ein Programm daran, einen Parameter [POST /.../do/validate] zusammen mit einem willkürlichen Parameter [marie] zu erzeugen. Wir sollten daher die Gültigkeit dieses Parameters prüfen. Hier verlassen wir uns auf unsere Ausnahmebehandlung, die die Anzeige der Seite „[exception.jsp]“ auslöst, falls der Controller die Ausnahmen nicht selbst behandelt. Wenn also die Umwandlung des Parameters [marie] in einen booleschen Wert in Zeile 32 fehlschlägt, wird eine Ausnahme ausgelöst, die dazu führt, dass die Seite [exception.jsp] an den Client gesendet wird. Diese Funktionsweise ist für uns in Ordnung.
  • Zeilen 34–50: Der Parameter [nbenfants] wird abgerufen und sein Wert überprüft.
  • Zeile 52: Der Parameter [id] wird abgerufen, ohne seinen Wert zu überprüfen
  • Zeilen 54–59: Wenn das Formular fehlerhaft ist, wird es mit den zuvor erstellten Fehlermeldungen erneut angezeigt
  • Zeilen 62–67: Wenn die Eingabe gültig ist, wird ein neues Objekt „[Personne]“ mit den Formularelementen erstellt
  • Zeilen 69–82: Die Person wird gespeichert. Der Speichervorgang kann fehlschlagen. In einer Mehrbenutzerumgebung kann die zu ändernde Person möglicherweise gelöscht oder bereits von jemand anderem geändert worden sein. In diesem Fall löst die Schicht [dao] eine Ausnahme aus, die hier abgefangen wird.
  • Zeile 84: Wenn keine Ausnahme aufgetreten ist, wird der Client zur URL [/do/list] weitergeleitet, um ihm den neuen Status der Gruppe anzuzeigen.
  • Zeile 79: Wenn beim Speichern eine Ausnahme aufgetreten ist, wird die erneute Anzeige des ursprünglichen Formulars angefordert, wobei die Fehlermeldung der Ausnahme (3. Parameter) übergeben wird.

Die Methode [showFormulaire] (Zeilen 88–97) erstellt die für die Seite [edit.jsp] erforderliche Vorlage mit den eingegebenen Werten (request.getParameter(" ... ")). Zur Erinnerung: Die Fehlermeldungen wurden bereits durch die Methode [doValidatePersonne] in die Vorlage eingefügt. Die Seite [edit.jsp] wird in Zeile 99 angezeigt.

3.4.4. Tests der Webanwendung

In Abschnitt 3.4.1 wurden einige Tests vorgestellt. Wir laden den Leser ein, diese erneut durchzuführen. Wir zeigen hier weitere Screenshots, die Fälle von Datenzugriffskonflikten in einer Mehrbenutzerumgebung veranschaulichen:

[Firefox] ist der Browser des Benutzers U1. Dieser fordert die URL [http://localhost:8080/spring-jpa-hibernate-personnes-crud/do/list] an:

Image

[IE7] ist der Browser des Benutzers U2. Dieser fordert dieselbe URL an:

Image

Der Benutzer U1 ruft die Bearbeitungsseite der Person [p2] auf:

Image

Der Benutzer U2 tut dasselbe:

Image

Der Benutzer U1 nimmt Änderungen vor und bestätigt diese:

Der Benutzer U2 verfährt ebenso:

Der Benutzer U2 kehrt über den Link [Retour à la liste] im Formular zur Personenliste zurück:

Image

Er findet die Person [Lemarchand] so, wie U1 sie geändert hat (verheiratet, 2 Kinder). Die Versionsnummer von p2 hat sich geändert. Nun löscht U2 den Eintrag [p2]:

U1 hat weiterhin eine eigene Liste und möchte [p2] erneut ändern:

U1 nutzt den Link [Retour à la liste], um zu sehen, worum es geht:

Image

Er stellt fest, dass [p2] tatsächlich nicht mehr in der Liste enthalten ist...

3.4.5. Version 2

Wir passen die vorherige Version leicht an, um die Archive der Ebenen [service, dao, jpa] zu verwenden und nicht mehr deren Quellcodes:

  • in [1]: das neue Eclipse-Projekt. Zu beachten ist das Fehlen der Pakete [service, dao, entites]. Diese wurden in das Archiv [service-dao-jpa-personne.jar] [2] gekapselt, das sich in [WEB-INF/lib] befindet.
  • Der Projektordner befindet sich in [4]. Wir werden ihn importieren.

Mehr ist nicht zu tun. Wenn die neue Webanwendung gestartet wird und man die Liste der Personen abfragt, erhält man folgende Antwort:

 

Hibernate findet die Entität [Personne] nicht. Um dieses Problem zu beheben, müssen die verwalteten Entitäten in [persistence.xml] explizit deklariert werden:


<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.0"
    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">
    <persistence-unit name="jpa" transaction-type="RESOURCE_LOCAL">
        <class>entites.Personne</class>
    </persistence-unit>
</persistence>
  • Zeile 7: Die Entität Personne wird deklariert.

Ist dies geschehen, verschwindet die Ausnahme:

 

3.4.6. Implementierung ändern: JPA

  • in [1]: Das neue Eclipse-Projekt
  • in [2]: Die Toplink-Bibliotheken haben die Hibernate-Bibliotheken ersetzt
  • Der Projektordner lautet [4]. Dieser wird importiert.

Der Wechsel der Implementierung JPA erfordert nur einige Änderungen in der Datei [spring-config.xml]. Ansonsten ändert sich nichts. Die Änderungen in der Datei [spring-config.xml] wurden in Abschnitt 3.1.9 erläutert:


<?xml version="1.0" encoding="UTF-8"?>

<!-- JVM muss mit dem Argument -javaagent:C:\data\2006-2007\eclipse\dvp-jpa\lib\spring\spring-agent.jar 
    (à remplacer par le chemin exact de spring-agent.jar)-->

<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
...
    <bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
        <property name="dataSource" ref="dataSource" />
        <property name="jpaVendorAdapter">
            <bean class="org.springframework.orm.jpa.vendor.TopLinkJpaVendorAdapter">
...    
            <property name="databasePlatform" value="oracle.toplink.essentials.platform.database.MySQL4Platform" />
...
    </bean>
...
</beans>

Es müssen nur wenige Zeilen geändert werden, um von Hibernate auf Toplink umzustellen:

  • Zeile 11: Die Implementierung JPA erfolgt nun über Toplink
  • Zeile 13: Die Eigenschaft [databasePlatform] hat einen anderen Wert als bei Hibernate: den Namen einer Toplink-spezifischen Klasse. Wo dieser Name zu finden ist, wurde in Abschnitt 2.1.15.2 erläutert.

Das war’s. Man beachte, wie einfach es ist, mit Spring von SGBD oder der Implementierung JPA zu wechseln. Wir sind jedoch noch nicht ganz fertig. Beim Ausführen der Anwendung tritt eine Ausnahme auf:

 

Hier handelt es sich um ein Problem, das bereits in Abschnitt 3.1.9 beschrieben wurde. Es lässt sich beheben, indem man die JVM mit einem Spring-Agenten startet. Dazu ändert man die Startkonfiguration von Tomcat:

  • in [1]: Wir haben die Option [Run / Run...] gewählt, um die Tomcat-Konfiguration
  • in [2]: Wir haben die Registerkarte [Arguments] ausgewählt
  • in [3]: Der Parameter -javaagent wurde wie in Abschnitt 3.1.9 beschrieben hinzugefügt.

Anschließend kann die Liste der Personen abgefragt werden:

Image

3.5. Weitere Beispiele

Wir hätten gerne ein Web-Beispiel gezeigt, bei dem der Spring-Container durch den in Abschnitt 3.2 behandelten JBoss-EJB3-Container ersetzt wurde:

  • in [1]: das Eclipse-Projekt
  • in [3]: sein Speicherort im Beispielordner. Wir werden es importieren.

Wir haben die in Abschnitt 3.2 beschriebene Konfiguration [jboss-config.xml, persistence.xml] übernommen und anschließend die Methode [init] des Controllers [Application.java] wie folgt geändert:


// init
    @SuppressWarnings("unchecked")
    public void init() throws ServletException {
        try {
            // die Initialisierungsparameter des Servlets werden abgerufen
            ServletConfig config = getServletConfig();
            // die übrigen Initialisierungsparameter werden verarbeitet
            String valeur = null;
            for (int i = 0; i < paramètres.length; i++) {
                // Wert des Parameters
                valeur = config.getInitParameter(paramètres[i]);
                // Ist der Parameter vorhanden?
                if (valeur == null) {
                    // Der Fehler wird protokolliert
                    erreursInitialisation.add("Le paramètre [" + paramètres[i] + "] n'a pas été initialisé");
                } else {
                    // Der Wert des Parameters wird gespeichert
                    params.put(paramètres[i], valeur);
                }
            }
            // Die URL der Ansicht [erreurs] wird speziell behandelt
            urlErreurs = config.getInitParameter("urlErreurs");
            if (urlErreurs == null)
                throw new ServletException("Le paramètre [urlErreurs] n'a pas été initialisé");
            // Konfiguration der Anwendung
            // Der Container wird gestartet: EJB3 JBoss
            // Die Konfigurationsdateien ejb3-interceptors-aop.xml und embedded-jboss-beans.xml werden verarbeitet
            EJB3StandaloneBootstrap.boot(null);

            // Erstellung der anwendungsspezifischen Beans
            EJB3StandaloneBootstrap.deployXmlResource("META-INF/jboss-config.xml");

            // Alle im Klassenpfad der Anwendung gefundenen EJB-Dateien werden bereitgestellt
            //EJB3StandaloneBootstrap.scanClasspath("WEB-INF/classes".replace("/", File.separator));
            EJB3StandaloneBootstrap.scanClasspath();

            // Der Kontext JNDI wird initialisiert. Die Datei jndi.properties wird verarbeitet
            InitialContext initialContext = new InitialContext();

            // Instanziierung der Service-Schicht
            service = (IService) initialContext.lookup("Service/local");
            // Die Datenbank wird geleert
            clean();
            // Die Datenbank wird gefüllt
            fill();
        } catch (Exception e) {
            throw new ServletException(e);
        }
    }
  • Zeilen 28–38: Der EJB3-Container wird gestartet. Dieser ersetzt den Spring-Container.
  • Zeile 41: Es wird eine Referenz auf die Schicht [service] der Anwendung angefordert.

Grundsätzlich sind dies die einzigen Änderungen, die vorgenommen werden müssen. Bei der Ausführung tritt folgender Fehler auf:

 

Ich konnte nicht genau herausfinden, wo das Problem lag. Die von Tomcat gemeldete Ausnahme scheint darauf hinzudeuten, dass das Objekt mit dem Namen „TransactionManager“ vom Dienst JNDI angefordert wurde und dieser es nicht kannte. Ich überlasse es den Lesern, eine Lösung für dieses Problem zu finden. Sollte eine Lösung gefunden werden, wird sie in das Dokument aufgenommen.