3. JPA w architekturze wielowarstwowej
Aby zbadać API JPA, wykorzystaliśmy następującą architekturę testową:
![]() |
Nasze programy testowe były aplikacjami konsolowymi, które bezpośrednio komunikowały się z warstwą JPA. Dzięki temu poznaliśmy główne metody warstwy JPA. Pracowaliśmy w środowisku zwanym „Java SE” (Standard Edition). JPA działa zarówno w środowisku Java SE, jak i Java EE5 (Enterprise Edition).
Teraz, gdy opanowaliśmy już zarówno konfigurację mostu relacyjno-obiektowego, jak i korzystanie z metod warstwy JPA, powracamy do bardziej klasycznej architektury wielowarstwowej:
![]() |
Dostęp do warstwy [JPA] będzie realizowany poprzez architekturę dwuwarstwową [metier] i [dao]. Do połączenia tych warstw zostaną wykorzystane framework Spring [7], a następnie kontener EJB3 z JBoss i [8].
Jak wspomniano powyżej, JPA jest dostępny w środowiskach SE i EE5. Środowisko Java EE5 zapewnia wiele usług w zakresie dostępu do danych trwałych, w szczególności pule połączeń, menedżery transakcji itp. Dla programisty korzystne może być wykorzystanie tych usług. Środowisko Java EE5 nie jest jeszcze zbyt rozpowszechnione (maj 2007 r.). Obecnie można je znaleźć na serwerze aplikacji Sun Application Server 9.x (Glassfish). Serwer aplikacji jest zasadniczo serwerem aplikacji internetowych. Jeśli tworzy się samodzielną aplikację graficzną typu Swing, nie ma dostępu do środowiska EE i usług, które ono zapewnia. Stanowi to problem. Zaczynają pojawiać się środowiska „samodzielne” typu EE, c.a.d. które mogą być używane poza serwerem aplikacji. Tak jest w przypadku JBoss i EJB3, z których będziemy korzystać w niniejszym dokumencie.
W środowisku EE5 warstwy są implementowane przez obiekty zwane EJB (Enterprise Java Bean). W poprzednich wersjach EE obiekty EJB (EJB, 2.x) były uważane za trudne do wdrożenia, testowania, a czasami także za mało wydajne. Rozróżnia się „entity” EJB2.x oraz „session” EJB2.x. Krótko mówiąc, „entity” typu EJB2.x jest odwzorowaniem wiersza tabeli bazy danych, a „session” typu EJB2.x – obiektem wykorzystywanym do implementacji warstw [metier], [dao] w architekturze wielowarstwowej. Jednym z głównych zarzutów wobec warstw zaimplementowanych przy użyciu EJB jest to, że można z nich korzystać wyłącznie w ramach kontenerów EJB, usługi dostarczanej przez środowisko EE. Powoduje to problemy z testami jednostkowymi. Tak więc na powyższym schemacie testy jednostkowe warstw [metier] i [dao], zbudowanych przy użyciu EJB, wymagałyby uruchomienia serwera aplikacji, co jest dość uciążliwą operacją i nie zachęca programisty do częstego przeprowadzania testów.
Framework Spring powstał w odpowiedzi na złożoność środowisk EJB2. Spring zapewnia w środowisku SE znaczną liczbę usług, które zazwyczaj są dostarczane przez środowiska EE. Tak więc w interesującej nas tutaj części „Trwałość danych” Spring zapewnia pule połączeń i menedżery transakcji, których potrzebują aplikacje. Pojawienie się Springa sprzyjało rozwojowi kultury testów jednostkowych, których wdrożenie stało się nagle znacznie łatwiejsze. Spring umożliwia implementację warstw aplikacji za pomocą klasycznych obiektów Java (POJO, Plain Old/Ordinary Java Object), co pozwala na ich ponowne wykorzystanie w innym kontekście. Wreszcie, w dość przejrzysty sposób integruje wiele narzędzi innych producentów, w szczególności narzędzi do trwałości danych, takich jak Hibernate, Ibatis...
Java EE5 została zaprojektowana w celu uzupełnienia braków poprzedniej specyfikacji EE. EJB i 2.x stały się EJB3. Są to obiekty typu POJOs oznaczone adnotacjami, które sprawiają, że stają się one obiektami specjalnymi, gdy znajdują się w kontenerze typu EJB3. W tym kontenerze obiekt EJB3 będzie mógł korzystać z usług kontenera (pula połączeń, menedżer transakcji itp.). Poza kontenerem EJB3 obiekt EJB3 staje się zwykłym obiektem Java. Jego adnotacje EJB są ignorowane.
Powyżej przedstawiliśmy Spring oraz JBoss i EJB3 jako potencjalną infrastrukturę (framework) naszej architektury wielowarstwowej. To właśnie ta infrastruktura zapewni nam potrzebne usługi: pulę połączeń i menedżera transakcji.
- W przypadku Springa warstwy będą zaimplementowane za pomocą komponentów POJOs. Będą one miały dostęp do usług Springa (pula połączeń, menedżer transakcji) poprzez wstrzykiwanie zależności do tych komponentów POJOs: podczas ich tworzenia Spring wstrzykuje do nich odniesienia do usług, których będą potrzebować.
- JBoss EJB3 to kontener EJB, który może działać poza serwerem aplikacji. Jego zasada działania (z punktu widzenia programisty) jest analogiczna do tej opisanej dla Springa. Znajdziemy tu niewiele różnic.
Na zakończenie niniejszego dokumentu przedstawimy przykładową, prostą, ale reprezentatywną aplikację internetową o strukturze trójwarstwowej:
![]() |
3.1. Przykład 1: Spring / JPA z encją „Osoba”
Wykorzystujemy encję Personne omówioną w punkcie 2.1 i włączamy ją do architektury wielowarstwowej, w której integracja warstw odbywa się za pomocą Springa, a warstwa trwałości jest zaimplementowana przez Hibernate.
![]() |
Zakładamy, że czytelnik posiada podstawową wiedzę na temat Springa. Jeśli tak nie jest, można zapoznać się z poniższym dokumentem, który wyjaśnia pojęcie wstrzykiwania zależności, stanowiące sedno Springa:
[ref3]: Spring IoC (Inversion Of Control) [http://tahe.developpez.com/java/springioc].
3.1.1. Projekt „ ” w środowisku Eclipse / Spring / Hibernate
Projekt Eclipse wygląda następująco:
![]() |
![]() |
- w pliku [1]: projekt Eclipse. Znajduje się on w pliku [6] wśród przykładów z samouczka [5]. Zaimportujemy go.
- w pliku [2]: kody Java warstw przedstawione w pakietach:
- [entites]: pakiet encji JPA
- [dao]: warstwa dostępu do danych – opiera się na warstwie JPA
- [service]: warstwa usług, a nie warstwa biznesowa. Wykorzystuje ona usługę transakcyjną kontenerów.
- [tests]: zawiera programy testowe.
- w [3]: biblioteka [jpa-spring] zawiera pliki JAR niezbędne dla Springa (zobacz także [7] i [8]).
- w [4]: folder [conf] zawiera pliki konfiguracyjne Springa dla każdego z plików SGBD wykorzystywanych w tym samouczku.
3.1.2. Elementy: JPA
![]() |
W tym miejscu zarządzana jest tylko jedna jednostka – jednostka Personne omówiona w punkcie 2.1, której konfigurację przypominamy poniżej:
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;
// konstruktory
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());
}
// metody pobierające i ustawiające
...
}
3.1.3. Warstwa [dao]
![]() | ![]() |
Warstwa [dao] posiada następujący interfejs IDao:
package dao;
import java.util.List;
import entites.Personne;
public interface IDao {
// pobieranie osoby na podstawie jej identyfikatora
public Personne getOne(Integer id);
// pobieranie wszystkich osób
public List<Personne> getAll();
// zapisanie osoby
public Personne saveOne(Personne personne);
// aktualizacja osoby
public Personne updateOne(Personne personne);
// usunięcie osoby na podstawie jej identyfikatora
public void deleteOne(Integer id);
// pobierz osoby, których nazwisko odpowiada wzorcowi
public List<Personne> getAllLike(String modele);
}
Implementacja tego interfejsu o nazwie [Dao] wygląda następująco:
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;
// usunięcie osoby na podstawie jej identyfikatora
public void deleteOne(Integer id) {
Personne personne = em.find(Personne.class, id);
if (personne == null) {
throw new DaoException(2);
}
em.remove(personne);
}
@SuppressWarnings("unchecked")
// pobierz wszystkie osoby
public List<Personne> getAll() {
return em.createQuery("select p from Personne p").getResultList();
}
@SuppressWarnings("unchecked")
// pobranie osób, których nazwisko odpowiada wzorcowi
public List<Personne> getAllLike(String modele) {
return em.createQuery("select p from Personne p where p.nom like :modele")
.setParameter("modele", modele).getResultList();
}
// pobierz osobę na podstawie jej identyfikatora
public Personne getOne(Integer id) {
return em.find(Personne.class, id);
}
// zapisać osobę
public Personne saveOne(Personne personne) {
em.persist(personne);
return personne;
}
// zaktualizować osobę
public Personne updateOne(Personne personne) {
return em.merge(personne);
}
}
- Przede wszystkim warto zwrócić uwagę na prostotę implementacji [Dao]. Wynika to z wykorzystania warstwy JPA, która wykonuje większość pracy związanej z dostępem do danych.
- wiersz 10: klasa [Dao] implementuje interfejs [IDao]
- wiersz 13: obiekt typu [EntityManager], który będzie wykorzystywany do obsługi kontekstu trwałości JPA. W potocznym języku będziemy czasami mylić go z samym kontekstem trwałości. Kontekst trwałości będzie zawierał encje typu Personne.
- wiersz 12: nigdzie w kodzie pole [EntityManager em] nie jest inicjowane. Zostanie ono zainicjowane przez Spring przy uruchomieniu aplikacji. To właśnie adnotacja JPA @PersistenceContext w wierszu 12 nakazuje Springowi wstrzyknięcie do em menedżera kontekstu trwałości.
- wiersze 26–28: lista wszystkich osób jest uzyskiwana za pomocą zapytania JPQL.
- wiersze 32–35: lista wszystkich osób, których imię odpowiada określonemu modelowi, jest pobierana za pomocą zapytania JPQL.
- wiersze 38–40: osoba o danym identyfikatorze jest pobierana za pomocą metody find obiektu API JPA. Zwraca wskaźnik null, jeśli osoba nie istnieje.
- wiersze 43–46: osoba jest zapisywana na stałe za pomocą metody `persist` klasy `API JPA`. Metoda ta powoduje zapisanie osoby na stałe.
- wiersze 49–51: aktualizacja osoby odbywa się za pomocą metody merge klasy API JPA. Metoda ta ma sens tylko wtedy, gdy aktualizowana osoba była wcześniej odłączona. Metoda ta utrwala tak utworzoną osobę.
- wiersze 16–22: usunięcie osoby, której identyfikator przekazano nam jako parametr, odbywa się w dwóch etapach:
- wiersz 17: osoba jest wyszukiwana w kontekście trwałości
- wiersze 18–20: jeśli nie zostanie znaleziona, zgłaszany jest wyjątek z kodem błędu 2
- wiersz 21: jeśli osoba została znaleziona, usuwa się ją z kontekstu trwałości za pomocą metody remove klasy API JPA.
- To, co na razie nie jest widoczne, to fakt, że każda metoda będzie wykonywana w ramach transakcji zainicjowanej przez warstwę [service].
Aplikacja posiada własny typ wyjątku o nazwie [DaoException]:
package dao;
@SuppressWarnings("serial")
public class DaoException extends RuntimeException {
// kod błędu
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;
}
// metody getter i setter
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
}
- wiersz 4: [DaoException] wywodzi się z [RuntimeException]. Jest to zatem typ wyjątku, którego kompilator nie wymaga od nas obsługi za pomocą try/catch ani umieszczania w sygnaturze metod. Z tego powodu [DaoException] nie występuje w sygnaturze metody [deleteOne] interfejsu [IDao]. Dzięki temu interfejs ten może być zaimplementowany przez klasę rzucającą inny typ wyjątków, pod warunkiem, że klasa ta również wywodzi się z klasy [RuntimeException].
- Aby rozróżnić błędy, które mogą wystąpić, wykorzystuje się kod błędu z linii 7. Trzy konstruktory z linii 14, 19 i 24 pochodzą z klasy nadrzędnej [RuntimeException], do których dodano jeden parametr: kod błędu, który ma zostać przypisany do wyjątku.
3.1.4. Warstwa [metier / service]
![]() |
Warstwa [service] posiada następujący interfejs [IService]:
package service;
import java.util.List;
import entites.Personne;
public interface IService {
// pobranie osoby na podstawie jej identyfikatora
public Personne getOne(Integer id);
// pobranie wszystkich osób
public List<Personne> getAll();
// zapisać osobę
public Personne saveOne(Personne personne);
// aktualizować dane osoby
public Personne updateOne(Personne personne);
// usunięcie osoby na podstawie jej identyfikatora
public void deleteOne(Integer id);
// pobierz osoby, których imię odpowiada wzorcowi
public List<Personne> getAllLike(String modele);
// usunięcie wielu osób jednocześnie
public void deleteArray(Personne[] personnes);
// zapisać wiele osób jednocześnie
public Personne[] saveArray(Personne[] personnes);
// aktualizowanie wielu osób jednocześnie
public Personne[] updateArray(Personne[] personnes);
}
- wiersze 8–24: interfejs [IService] przejmuje metody z interfejsu [IDao]
- wiersz 27: metoda [deleteArray] umożliwia usunięcie grupy osób w ramach jednej transakcji: usuwane są wszystkie osoby lub żadna.
- wiersze 30 i 33: metody analogiczne do [deleteArray] służące do zapisywania (wiersz 30) lub aktualizowania (wiersz 33) grupy osób w ramach jednej transakcji.
Implementacja [Service] interfejsu [IService] wygląda następująco:
package service;
...
// wszystkie metody tej klasy są wykonywane w ramach jednej transakcji
@Transactional
public class Service implements IService {
// warstwa [dao]
private IDao dao;
public IDao getDao() {
return dao;
}
public void setDao(IDao dao) {
this.dao = dao;
}
// usuwanie wielu osób jednocześnie
public void deleteArray(Personne[] personnes) {
for (Personne p : personnes) {
dao.deleteOne(p.getId());
}
}
// usunięcie osoby na podstawie jej identyfikatora
public void deleteOne(Integer id) {
dao.deleteOne(id);
}
// pobranie wszystkich osób
public List<Personne> getAll() {
return dao.getAll();
}
// pobranie osób, których nazwisko odpowiada wzorcowi
public List<Personne> getAllLike(String modele) {
return dao.getAllLike(modele);
}
// pobranie osoby na podstawie jej identyfikatora
public Personne getOne(Integer id) {
return dao.getOne(id);
}
// zapisać kilka osób jednocześnie
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;
}
// zapisać jedną osobę
public Personne saveOne(Personne personne) {
return dao.saveOne(personne);
}
// aktualizowanie wielu osób jednocześnie
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;
}
// aktualizacja danych jednej osoby
public Personne updateOne(Personne personne) {
return dao.updateOne(personne);
}
}
- wiersz 6: adnotacja Spring @Transactional wskazuje, że wszystkie metody klasy muszą być wykonywane w ramach transakcji. Transakcja zostanie rozpoczęta przed rozpoczęciem wykonywania metody i zamknięta po jej zakończeniu. Jeśli podczas wykonywania metody wystąpi wyjątek typu [RuntimeException] lub jego pochodna, automatyczne cofnięcie (rollback) anuluje całą transakcję; w przeciwnym razie automatyczne zatwierdzenie (commit) ją zatwierdza. Należy pamiętać, że kod Java nie musi zajmować się transakcjami. Są one zarządzane przez Spring.
- wiersz 10: odwołanie do warstwy [dao]. Jak zobaczymy później, odwołanie to jest inicjowane przez Spring przy uruchomieniu aplikacji.
- Metody klasy [Service] ograniczają się do wywoływania metod interfejsu [IDao dao] z linii 10. Pozostawiamy czytelnikowi zapoznanie się z kodem. Nie ma tu żadnych szczególnych trudności.
- Wspomnieliśmy wcześniej, że każda metoda klasy [Service] jest wykonywana w ramach transakcji. Transakcja ta jest powiązana z wątkiem wykonawczym metody. W tym wątku wykonywane są metody warstwy [dao]. Metody te zostaną automatycznie powiązane z transakcją wątku wykonawczego. Na przykład metoda [deleteArray] (wiersz 21) ma wykonać N razy metodę [deleteOne] z warstwy [dao]. Te N wykonań odbędzie się w wątku wykonawczym metody [deleteArray], a więc w ramach tej samej transakcji. W związku z tym zostaną one albo wszystkie zatwierdzone (commit), jeśli wszystko przebiegnie pomyślnie, albo wszystkie cofnięte (rollback), jeśli wystąpi wyjątek w jednym z N uruchomień metody [deleteOne] z warstwy [dao].
3.1.5. Konfiguracja warstw
![]() | ![]() |
Konfiguracja warstw [service], [dao] i [JPA] jest zapewniona przez dwa powyższe pliki: [META-INF/persistence.xml] i [spring-config.xml]. Oba pliki muszą znajdować się w katalogu classpath aplikacji, co wyjaśnia, dlaczego znajdują się one w folderze [src] projektu Eclipse. Nazwa pliku [spring-config.xml] może być dowolna.
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>
- wiersz 4: plik deklaruje jednostkę trwałości o nazwie jpa, która wykorzystuje transakcje „lokalne” o nazwie c.a.d, nieudostępniane przez kontener EJB3. Transakcje te są tworzone i zarządzane przez Spring oraz konfigurowane w pliku [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">
<!-- warstwy aplikacyjne -->
<bean id="dao" class="dao.Dao" />
<bean id="service" class="service.Service">
<property name="dao" ref="dao" />
</bean>
<!-- warstwa trwałości 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>
<!-- źródło danych 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>
<!-- menedżer transakcji -->
<tx:annotation-driven transaction-manager="txManager" />
<bean id="txManager"
class="org.springframework.orm.jpa.JpaTransactionManager">
<property name="entityManagerFactory"
ref="entityManagerFactory" />
</bean>
<!-- przetwarzanie wyjątków -->
<bean
class="org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor" />
<!-- adnotacje dotyczące trwałości -->
<bean
class="org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor" />
</beans>
- wiersze 2–5: tag nadrzędny <beans> w pliku konfiguracyjnym. Nie będziemy omawiać poszczególnych atrybutów tego tagu. Należy zachować ostrożność podczas kopiowania i wklejania, ponieważ pomyłka w jednym z tych atrybutów powoduje błędy, które czasami są trudne do zrozumienia.
- wiersz 8: bean „dao” jest odwołaniem do instancji klasy [dao.Dao]. Utworzona zostanie pojedyncza instancja (singleton), która będzie implementować warstwę [dao] aplikacji.
- wiersze 9–11: instancjonowanie warstwy [service]. Bean „service” jest odwołaniem do instancji klasy [service.Service]. Utworzona zostanie pojedyncza instancja (singleton), która będzie implementować warstwę [service] aplikacji. Widzieliśmy, że klasa [service.Service] posiadała pole prywatne [IDao dao]. Pole to jest inicjowane w wierszu 10 przez bean „dao” zdefiniowany w wierszu 8.
- Ostatecznie wiersze 8–11 skonfigurowały warstwy [dao] i [service]. Później zobaczymy, kiedy i w jaki sposób zostaną one zainicjowane.
- wiersze 35–42: zdefiniowano źródło danych. Z pojęciem źródła danych zetknęliśmy się już podczas omawiania encji JPA w kontekście Hibernate:
![]() |
W powyższym przykładzie [c3p0], zwany „pulą połączeń”, mógłby być nazwany „źródłem danych”. Źródło danych zapewnia usługę „puli połączeń”. W przypadku Springa będziemy korzystać z innego źródła danych niż [c3p0]. Jest to [DBCP] z projektu Apache Commons DBCP [http://jakarta.apache.org/commons/dbcp/]. Archiwa [DBCP] zostały umieszczone w bibliotece użytkownika [jpa-spring]:
![]() |
- wiersze 38–41: aby nawiązać połączenia z docelową bazą danych, źródło danych musi znać używany sterownik JDBC (wiersz 38), adres URL bazy danych (wiersz 39) oraz nazwę użytkownika i hasło do połączenia (wiersze 40–41).
- wiersze 14–32: konfigurują warstwę JPA
- wiersze 14–15: definiują bean typu [EntityManagerFactory] zdolny do tworzenia obiektów typu [EntityManager] w celu zarządzania kontekstami trwałości. Instancja klasy [LocalContainerEntityManagerFactoryBean] jest dostarczana przez Spring. Do instancjonowania wymaga ona pewnej liczby parametrów, zdefiniowanych w wierszach 16–31.
- wiersz 16: źródło danych, które ma być wykorzystane do uzyskania połączeń z SGBD. Jest to źródło [DBCP] zdefiniowane w wierszach 35–42.
- wiersze 17–27: implementacja JPA, która ma być używana
- wiersze 18–26: definiują Hibernate (wiersz 19) jako implementację JPA, która ma być używana
- wiersze 23–24: dialekt SQL, którego Hibernate ma używać z docelowym SGBD, w tym przypadku MySQL5.
- wiersz 25: żądanie, aby podczas uruchamiania aplikacji została wygenerowana baza danych (drop i create).
- wiersze 28–31: definiują „ładowarkę klas”. Nie potrafię jasno wyjaśnić roli tego beana używanego przez EntityManagerFactory w warstwie JPA. W każdym razie wymaga to przekazania do komponentu JVM, który uruchamia aplikację, nazwy archiwum, którego zawartość będzie zarządzać ładowaniem klas podczas uruchamiania aplikacji. W tym przypadku archiwum to nosi nazwę [spring-agent.jar] i znajduje się w bibliotece użytkownika [jpa-spring] (patrz wyżej). Zobaczymy, że Hibernate nie potrzebuje tego agenta, ale Toplink go wymaga.
- wiersze 45–50: definiują menedżera transakcji, który ma być używany
- wiersz 45: wskazuje, że transakcje są zarządzane za pomocą adnotacji Java (mogłyby one również zostać zadeklarowane w pliku spring-config.xml). Chodzi w szczególności o adnotację @Transactional występującą w klasie [Service] (wiersz 6).
- wiersze 46–50: menedżer transakcji
- wiersz 47: menedżer transakcji to klasa dostarczana przez Spring
- wiersze 48–49: menedżer transakcji Springa musi znać klasę EntityManagerFactory, która zarządza warstwą JPA. Jest to klasa zdefiniowana w wierszach 14–32.
- wiersze 57–58: definiują klasę, która obsługuje adnotacje trwałości Springa występujące w kodzie Java, takie jak adnotacja @PersistenceContext w klasie [dao.Dao] (wiersz 12).
- wiersze 53–54: definiują klasę Springa, która zarządza w szczególności adnotacją @Repository, dzięki której klasa opatrzona tą adnotacją kwalifikuje się do przekształcania natywnych wyjątków sterownika JDBC klasy SGBD na generyczne wyjątki Springa typu [DataAccessException]. Konwersja ta hermetyzuje natywny wyjątek JDBC w typie [DataAccessException], który posiada różne podklasy:

Taka konwersja pozwala programowi klienckiemu na generyczne obsługiwanie wyjątków niezależnie od docelowego typu SGBD. W naszym kodzie Java nie użyliśmy adnotacji @Repository. Dlatego wiersze 53–54 są zbędne. Pozostawiliśmy je wyłącznie w celach informacyjnych.
To już wszystko, jeśli chodzi o plik konfiguracyjny Springa. Jest on złożony i wiele kwestii pozostaje niejasnych. Został on zaczerpnięty z dokumentacji Springa. Na szczęście jego dostosowanie do różnych sytuacji często sprowadza się do dwóch zmian:
- zmianę docelowej bazy danych: wiersze 38–41. Podamy przykład dotyczący Oracle.
- zmiana implementacji JPA: wiersze 14–32. Podamy przykład z wykorzystaniem Toplink.
3.1.6. Program kliencki [InitDB]
Zajmiemy się teraz pisaniem pierwszego klienta dla opisanej wcześniej architektury:
![]() |
Kod programu [InitDB] wygląda następująco:
package tests;
...
public class InitDB {
// warstwa usług
private static IService service;
// konstruktor
public static void main(String[] args) throws ParseException {
// konfiguracja aplikacji
ApplicationContext ctx = new ClassPathXmlApplicationContext("spring-config.xml");
// warstwa usług
service = (IService) ctx.getBean("service");
// opróżnianie bazy danych
clean();
// wypełnianie bazy
fill();
// sprawdzamy wizualnie
dumpPersonnes();
}
// wyświetlanie zawartości tabeli
private static void dumpPersonnes() {
System.out.format("[personnes]%n");
for (Personne p : service.getAll()) {
System.out.println(p);
}
}
// wypełnianie tabeli
public static void fill() throws ParseException {
// tworzenie osób
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);
// zapisujemy
service.saveArray(new Personne[] { p1, p2 });
}
// usuwanie elementów z tabeli
public static void clean() {
for (Personne p : service.getAll()) {
service.deleteOne(p.getId());
}
}
}
- wiersz 12: plik [spring-config.xml] jest wykorzystywany do utworzenia obiektu [ApplicationContext ctx], który stanowi obraz pamięciowy tego pliku. W tym momencie instancjonowane są komponenty typu bean zdefiniowane w pliku [spring-config.xml].
- wiersz 14: od kontekstu aplikacji ctx żąda się odwołania do warstwy [service]. Wiadomo, że jest ona reprezentowana przez bean o nazwie „service”.
- wiersz 16: baza danych jest opróżniana za pomocą metody clean z wierszy 41–45:
- wiersze 42–44: żądamy listy wszystkich osób od kontekstu trwałości i przechodzimy przez nią w pętli, aby usunąć je pojedynczo. Być może pamiętamy, że metoda [spring-config.xml] określa, że baza danych musi zostać wygenerowana podczas uruchamiania aplikacji. Dlatego w naszym przypadku wywołanie metody clean jest zbędne, ponieważ zaczynamy od pustej bazy.
- wiersz 18: metoda fill wypełnia bazę. Jest ona zdefiniowana w wierszach 32–38:
- wiersze 34–35: tworzone są dwie osoby
- wiersz 37: nakazuje się warstwie [service], aby nadała im status trwały.
- wiersz 20: metoda dumpPersonnes wyświetla osoby trwałe. Jest ona zdefiniowana w wierszach 24–29
- wiersze 26–28: żądamy listy wszystkich trwałych osób od warstwy [service] i wyświetlamy je na konsoli.
Wykonanie metody [InitDB] daje następujący wynik:
3.1.7. Testy jednostkowe [TestNG]
Instalacja wtyczki [TestNG] została opisana w punkcie 5.2.4. Kod programu [TestNG] jest następujący:
package tests;
....
public class TestNG {
// warstwa usługowa
private IService service;
@BeforeClass
public void init() {
// log
log("init");
// konfiguracja aplikacji
ApplicationContext ctx = new ClassPathXmlApplicationContext("spring-config.xml");
// warstwa usługowa
service = (IService) ctx.getBean("service");
}
@BeforeMethod
public void setUp() throws ParseException {
// opróżnianie bazy danych
clean();
// wypełnianie bazy
fill();
}
// logi
private void log(String message) {
System.out.println("----------- " + message);
}
// wyświetlanie zawartości tabeli
private void dump() {
log("dump");
System.out.format("[personnes]%n");
for (Personne p : service.getAll()) {
System.out.println(p);
}
}
// wypełnianie tabeli
public void fill() throws ParseException {
log("fill");
// tworzenie osób
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);
// które zapisujemy
service.saveArray(new Personne[] { p1, p2 });
}
// usuwanie elementów z tabeli
public void clean() {
log("clean");
for (Personne p : service.getAll()) {
service.deleteOne(p.getId());
}
}
@Test()
public void test01() {
...
}
...
}
- wiersz 9: adnotacja @BeforeClass wskazuje metodę, którą należy wykonać w celu zainicjowania konfiguracji niezbędnej do przeprowadzenia testów. Jest ona wykonywana przed uruchomieniem pierwszego testu. Adnotacja @AfterClass, która nie jest tutaj używana, wskazuje metodę, która ma zostać wykonana po zakończeniu wszystkich testów.
- wiersze 10–17: metoda init opatrzona adnotacją @BeforeClass wykorzystuje plik konfiguracyjny Springa do instancjonowania poszczególnych warstw aplikacji oraz uzyskania odwołania do warstwy [service]. Następnie wszystkie testy korzystają z tego odwołania.
- wiersz 19: adnotacja @BeforeMethod wskazuje metodę, która ma zostać wykonana przed każdym testem. Adnotacja @AfterMethod, której tutaj nie użyto, wskazuje metodę, która ma zostać wykonana po każdym teście.
- wiersze 20–25: metoda setUp, opatrzona adnotacją @BeforeMethod, opróżnia bazę danych (clean, wiersze 52–56), a następnie wypełnia ją danymi dwóch osób (fill, wiersze 42–49).
- wiersz 59: adnotacja @Test wskazuje metodę testową, która ma zostać wykonana. Teraz opiszemy te testy.
@Test()
public void test01() {
log("test1");
dump();
// lista osób
List<Personne> personnes = service.getAll();
assert 2 == personnes.size();
}
@Test()
public void test02() {
log("test2");
// wyszukiwanie osób według nazwiska
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");
// tworzenie nowej osoby
Personne p3 = new Personne("p3", "x", new SimpleDateFormat("dd/MM/yy").parse("05/07/2001"), false, 0);
// zapisanie danych osoby
service.saveOne(p3);
// ponowne wywołanie danych
Personne loadedp3 = service.getOne(p3.getId());
// wyświetlanie
System.out.println(loadedp3);
// weryfikacja
assert "p3".equals(loadedp3.getNom());
}
- wiersze 2–8: test 01. Należy pamiętać, że na początku każdego testu baza zawiera dwie osoby o nazwach odpowiednio p1 i p2.
- wiersz 6: żądamy listy osób
- wiersz 7: sprawdzamy, czy liczba osób na otrzymanej liście wynosi 2
- wiersz 14: żądamy listy osób, których nazwisko zaczyna się na p1
- sprawdzamy, czy uzyskana lista zawiera tylko jeden element (wiersz 15) oraz czy imię tej jedynej osoby to „Paul” (wiersz 17)
- wiersz 24: tworzymy osobę o nazwie p3
- wiersz 25: zapisujemy ją w bazie
- wiersz 28: ponownie pobieramy ją z kontekstu trwałego przechowywania w celu weryfikacji
- wiersz 32: sprawdzamy, czy uzyskana osoba rzeczywiście nosi nazwę p3.
@Test()
public void test04() throws ParseException {
log("test4");
// ładowanie osoby p1
List<Personne> personnes = service.getAllLike("p1%");
Personne p1 = personnes.get(0);
// wyświetla się
System.out.println(p1);
// sprawdzamy
assert "p1".equals(p1.getNom());
int version1 = p1.getVersion();
// zmiana imienia
p1.setPrenom("x");
// zapisywanie
service.updateOne(p1);
// odświeżamy
p1 = service.getOne(p1.getId());
// wyświetla się
System.out.println(p1);
// sprawdzamy, czy wersja została zwiększona
assert (version1 + 1) == p1.getVersion();
}
- wiersz 5: pobieramy osobę p1
- wiersz 10: sprawdzamy jej imię
- wiersz 11: zapisujemy jej numer wersji
- wiersz 13: zmieniamy jej imię
- wiersz 15: zapisujemy zmianę
- wiersz 17: ponownie wywołujemy osobę p1
- wiersz 21: sprawdzamy, czy numer wersji wzrósł o 1
@Test()
public void test05() {
log("test5");
// ładujemy osobę p2
List<Personne> personnes = service.getAllLike("p2%");
Personne p2 = personnes.get(0);
// wyświetla się
System.out.println(p2);
// sprawdzamy
assert "p2".equals(p2.getNom());
// usuwamy osobę p2
service.deleteOne(p2.getId());
// ponownie wczytuje się ją
p2 = service.getOne(p2.getId());
// sprawdzamy, czy uzyskano wskaźnik null
assert null == p2;
// wyświetla się tabela
dump();
}
- wiersz 5: pobieramy osobę p2
- wiersz 10: sprawdzamy jej imię
- wiersz 12: usuwa się ją
- wiersz 14: ponownie wyszukujemy tę osobę
- wiersz 16: sprawdzamy, czy nie została znaleziona
@Test()
public void test06() throws ParseException {
log("test6");
// tworzymy tablicę zawierającą 2 osoby o tym samym imieniu (narusza to zasadę unikalności imienia)
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)};
// zapisujemy tę tablicę – powinno wystąpić wyjątkiem i cofnięcie zmian
boolean erreur = false;
try {
service.saveArray(personnes);
} catch (RuntimeException e) {
erreur = true;
}
// zrzut
dump();
// weryfikacje
assert erreur;
// wyszukiwanie osoby o nazwisku p3
List<Personne> personnesp3 = service.getAllLike("p3%");
assert 0 == personnesp3.size();
// zrzut
dump();
}
- wiersz 5: tworzymy tablicę zawierającą trzy osoby, z których dwie mają tę samą nazwę „p4”. Narusza to zasadę unikalności nazwy @Entity Personne:
@Column(name = "NOM", length = 30, nullable = false, unique = true)
private String nom;
- wiersz 11: tablica trzech osób jest umieszczana w kontekście trwałości. Dodanie drugiej osoby p4 powinno zakończyć się niepowodzeniem. Ponieważ metoda [saveArray] przebiega w ramach transakcji, wszystkie wcześniejsze wstawienia zostaną cofnięte. Ostatecznie żadne dodanie nie zostanie wykonane.
- wiersz 18: sprawdzamy, czy metoda [saveArray] rzeczywiście wygenerowała wyjątek
- wiersze 20–21: sprawdzamy, czy osoba p3, która mogła zostać dodana, nie została dodana.
@Test()
public void test07() {
log("test7");
// test blokady optymistycznej
// ładujemy osobę p1
List<Personne> personnes = service.getAllLike("p1%");
Personne p1 = personnes.get(0);
// wyświetlanie
System.out.println(p1);
// zwiększamy liczbę jej dzieci
int nbEnfants1 = p1.getNbenfants();
p1.setNbenfants(nbEnfants1 + 1);
// zapisujemy p1
Personne newp1 = service.updateOne(p1);
assert (nbEnfants1 + 1) == newp1.getNbenfants();
System.out.println(newp1);
// zapisujemy po raz drugi – powinien wystąpić wyjątek, ponieważ p1 nie ma już właściwej wersji
// to newp1 go posiada
boolean erreur = false;
try {
service.updateOne(p1);
} catch (RuntimeException e) {
erreur = true;
}
// weryfikacja
assert erreur;
// zwiększamy liczbę elementów potomnych newp1
int nbEnfants2 = newp1.getNbenfants();
newp1.setNbenfants(nbEnfants2 + 1);
// zapisano newp1
service.updateOne(newp1);
// przeładowuje się
p1 = service.getOne(p1.getId());
// sprawdzanie
assert (nbEnfants1 + 2) == p1.getNbenfants();
System.out.println(p1);
}
- wiersz 6: pobieramy osobę p1
- wiersz 12: zwiększamy o 1 liczbę jej dzieci
- wiersz 14: aktualizujemy rekord p1 w kontekście trwałości. Metoda [updateOne] zapisuje nową wersję newp1 jako trwałą wersję p1. Różni się ona od p1 numerem wersji, który musiał zostać zwiększony.
- wiersz 15: sprawdzana jest liczba dzieci newp1.
- wiersz 21: ponownie żądamy aktualizacji osoby p1 na podstawie starej wersji p1. Powinien wystąpić wyjątek, ponieważ p1 nie jest najnowszą wersją osoby p1. Najnowszą wersją jest newp1.
- wiersz 23: sprawdzamy, czy błąd rzeczywiście wystąpił
- wiersze 27–35: sprawdzamy, czy w przypadku aktualizacji na podstawie najnowszej wersji newp1 wszystko przebiega prawidłowo.
@Test()
public void test08() {
log("test8");
// test cofnięcia zmian na updateArray
// ładowanie osoby p1
List<Personne> personnes = service.getAllLike("p1%");
Personne p1 = personnes.get(0);
// wyświetlanie
System.out.println(p1);
// zwiększamy liczbę jej dzieci
int nbEnfants1 = p1.getNbenfants();
p1.setNbenfants(nbEnfants1 + 1);
// zapisujemy 2 zmiany, z których druga musi zakończyć się niepowodzeniem (osoba nieprawidłowo zainicjowana)
// z powodu transakcji obie operacje muszą zostać anulowane
boolean erreur = false;
try {
service.updateArray(new Personne[] { p1, new Personne() });
} catch (RuntimeException e) {
erreur = true;
}
// sprawdzenia
assert erreur;
// ponownie ładuje się osobę p1
personnes = service.getAllLike("p1%");
p1 = personnes.get(0);
// liczba jej dzieci nie powinna ulec zmianie
assert nbEnfants1 == p1.getNbenfants();
}
- Test 8 jest podobny do testu 6: sprawdza on działanie funkcji rollback na funkcji updateArray działającej na tablicy zawierającej dwie osoby, z których druga nie została poprawnie zainicjowana. Z punktu widzenia JPA operacja merge na drugiej osobie, któraistnieje, wygeneruje polecenie SQL insert, które zakończy się niepowodzeniem z powodu ograniczeń nullable=false obowiązujących w niektórych polach encji Personne.
@Test()
public void test09() {
log("test9");
// test cofnięcia zmian na deleteArray
// zrzut danych
dump();
// ładujemy osobę p1
List<Personne> personnes = service.getAllLike("p1%");
Personne p1 = personnes.get(0);
// wyświetla się
System.out.println(p1);
// wykonuje się 2 operacje usunięcia, z których druga musi zakończyć się niepowodzeniem (nieznana osoba)
// z powodu tej transakcji obie operacje muszą zostać anulowane
boolean erreur = false;
try {
service.deleteArray(new Personne[] { p1, new Personne() });
} catch (RuntimeException e) {
erreur = true;
}
// weryfikacje
assert erreur;
// dokonuje się ponownego obciążenia osoby p1
personnes = service.getAllLike("p1%");
// weryfikacja
assert 1 == personnes.size();
// zrzut
dump();
}
- Test 9 jest podobny do poprzedniego: sprawdza rollback na deleteArray działającym na tablicy zawierającej dwie osoby, z których druga nie istnieje. Jednak w tym przypadku metoda [deleteOne] warstwy [dao] generuje wyjątek.
// blokowanie optymistyczne – dostęp wielowątkowy
@Test()
public void test10() throws Exception {
// dodanie osoby
Personne p3 = new Personne("X", "X", new SimpleDateFormat("dd/MM/yyyy").parse("01/02/2006"), true, 0);
service.saveOne(p3);
int id3 = p3.getId();
// utworzenie N wątków aktualizujących liczbę dzieci
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();
}
// oczekiwanie na zakończenie działania wątków
for (int i = 0; i < taches.length; i++) {
taches[i].join();
}
// pobieranie osoby
p3 = service.getOne(id3);
// osoba ta musi mieć N dzieci
assert N == p3.getNbenfants();
// usunięcie osoby p3
service.deleteOne(p3.getId());
// weryfikacja
p3 = service.getOne(p3.getId());
// musi występować wskaźnik null
assert p3 == null;
}
- Celem testu nr 10 jest uruchomienie N wątków (wiersz 9) w celu równoległego zwiększania liczby potomków danej osoby. Chcemy sprawdzić, czy system numeracji wersji dobrze radzi sobie w tej sytuacji. Został stworzony właśnie w tym celu.
- wiersze 5–6: tworzona jest osoba o nazwie p3, a następnie zapisywana w pamięci trwałej. Na początku ma ona 0 dzieci.
- wiersz 7: zapisujemy jej identyfikator.
- wiersze 9–14: uruchamia się równolegle N wątków, z których każdy ma za zadanie zwiększyć o 1 liczbę dzieci osoby p3.
- wiersze 16–18: czekamy na zakończenie działania wszystkich wątków
- wiersz 20: wywołuje się wyświetlenie osoby p3
- wiersz 22: sprawdzamy, czy ma ona teraz N dzieci
- wiersz 24: osoba p3 zostaje usunięta.
Wątek [ThreadMajEnfants] wygląda następująco:
package tests;
...
public class ThreadMajEnfants extends Thread {
// nazwa wątku
private String name;
// odwołanie do warstwy [service]
private IService service;
// identyfikator osoby, nad którą będziemy pracować
private int idPersonne;
// konstruktor
public ThreadMajEnfants(String name, IService service, int idPersonne) {
this.name = name;
this.service = service;
this.idPersonne = idPersonne;
}
// rdzeń wątku
public void run() {
// śledzenie
suivi("lancé");
// pętla trwa, dopóki nie uda się zwiększyć wartości o 1
// liczba dzieci osoby idPersonne
boolean fini = false;
int nbEnfants = 0;
while (!fini) {
// pobieramy kopię osoby z idPersonne
Personne personne = service.getOne(idPersonne);
nbEnfants = personne.getNbenfants();
// kontynuacja
suivi("" + nbEnfants + " -> " + (nbEnfants + 1) + " pour la version " + personne.getVersion());
// zwiększa o 1 liczbę dzieci osoby
personne.setNbenfants(nbEnfants + 1);
// oczekiwanie przez 10 ms, aby zwolnić procesor
try {
// kontynuacja
suivi("début attente");
// przerwa, aby zwolnić procesor
Thread.sleep(10);
// kontynuacja
suivi("fin attente");
} catch (Exception ex) {
throw new RuntimeException(ex.toString());
}
// oczekiwanie zakończone – próba zatwierdzenia kopii
// w międzyczasie inne wątki mogły zmodyfikować oryginał
try {
// próbujemy zmodyfikować oryginał
service.updateOne(personne);
// operacja zakończona – oryginał został zmodyfikowany
fini = true;
} catch (javax.persistence.OptimisticLockException e) {
// nieprawidłowa wersja obiektu: ignorujemy wyjątek, aby spróbować ponownie
} catch (org.springframework.transaction.UnexpectedRollbackException e2) {
// wyjątek Springa, który pojawia się od czasu do czasu
} catch (RuntimeException e3) {
// inny rodzaj wyjątku – przekazujemy go wyżej
throw e3;
}
}
// śledzenie
suivi("a terminé et passé le nombre d'enfants à " + (nbEnfants + 1));
}
// monitorowanie
private void suivi(String message) {
System.out.println(name + " [" + new Date().getTime() + "] : " + message);
}
}
- wiersze 15–19: konstruktor zapisuje informacje potrzebne do działania: swoją nazwę (wiersz 16), odwołanie do warstwy [service], z której ma korzystać (wiersz 17) oraz identyfikator osoby p, dla której ma zwiększyć liczbę dzieci (wiersz 18).
- wiersze 22–66: metoda [run] wykonywana równolegle przez wszystkie wątki.
- wiersz 29: wątek wielokrotnie próbuje zwiększyć liczbę dzieci osoby p. Zatrzymuje się dopiero po pomyślnym wykonaniu tej operacji.
- wiersz 31: pobierane są dane osoby p
- wiersz 36: liczba jej dzieci zostaje zwiększona w pamięci
- wiersze 38–47: następuje pauza trwająca 10 ms. Pozwoli to innym wątkom uzyskać tę samą wersję osoby p. W ten sposób w tym samym momencie kilka wątków będzie posiadało tę samą wersję osoby p i będzie chciało ją zmodyfikować. Taki jest zamierzony efekt.
- wiersz 52: po zakończeniu przerwy wątek zwraca się do warstwy [service] z prośbą o zapisanie zmiany. Wiemy, że od czasu do czasu będą występować wyjątki, dlatego operację tę otoczyliśmy blokiem try / catch.
- wiersz 55: testy pokazują, że występują wyjątki typu [javax.persistence.OptimisticLockException]. To normalne: jest to wyjątek zgłaszany przez warstwę JPA, gdy wątek próbuje zmodyfikować osobę p, nie dysponując jej najnowszą wersją. Wyjątek ten jest ignorowany, aby umożliwić wątkowi ponowne podjęcie próby operacji, aż do momentu jej pomyślnego wykonania.
- linia 57: testy pokazują, że pojawiają się również wyjątki typu [org.springframework.transaction.UnexpectedRollbackException]. Jest to irytujące i nieoczekiwane. Nie potrafię tego wyjaśnić. Jesteśmy teraz zależni od Springa, podczas gdy chcielibyśmy tego uniknąć. Oznacza to, że jeśli uruchomimy naszą aplikację na przykład w środowisku JBoss Ejb3, kod wątku będzie wymagał zmiany. Wyjątek Springa jest tutaj również ignorowany, aby wątek mógł ponownie podjąć próbę operacji inkrementacji.
- wiersz 59: pozostałe typy wyjątków są przekazywane do aplikacji.
Po uruchomieniu [TestNG] otrzymujemy następujące wyniki:

Wszystkie 10 testów zakończyło się powodzeniem.
Test 10 zasługuje na dodatkowe wyjaśnienia, ponieważ fakt, że zakończył się sukcesem, ma w sobie coś z magii. Wróćmy najpierw do konfiguracji warstwy [dao]:
public class Dao implements IDao {
@PersistenceContext
private EntityManager em;
- wiersz 4: obiekt [EntityManager] jest wstawiany do pola „em” za pomocą adnotacji JPA @PersistenceContext. Warstwa [dao] jest instancjonowana tylko raz. Jest to singleton wykorzystywany przez wszystkie wątki korzystające z warstwy JPA. W związku z tym obiekt EntityManager em jest wspólny dla wszystkich wątków. Można to sprawdzić, wyświetlając wartość em w metodzie [updateOne] wykorzystywanej przez wątki [ThreadMajEnfants]: dla wszystkich wątków otrzymujemy tę samą wartość.
W związku z tym można się zastanawiać, czy obiekty trwałe różnych wątków, obsługiwane przez em w metodzie EntityManager – który jest taki sam dla wszystkich wątków – nie będą się mieszać i powodować między sobą konfliktów. Przykład tego, co mogłoby się wydarzyć, znajduje się w metodzie [ThreadMajEnfants]:
while (!fini) {
// pobieramy kopię osoby z idPersonne
Personne personne = service.getOne(idPersonne);
nbEnfants = personne.getNbenfants();
// śledzenie
suivi("" + nbEnfants + " -> " + (nbEnfants + 1) + " pour la version " + personne.getVersion());
// zwiększa o 1 liczbę potomków tej osoby
personne.setNbenfants(nbEnfants + 1);
// oczekiwanie przez 10 ms, aby zwolnić procesor
try {
// kontynuacja
suivi("début attente");
// przerwa, aby zwolnić procesor
Thread.sleep(10);
// kontynuacja
suivi("fin attente");
} catch (Exception ex) {
throw new RuntimeException(ex.toString());
}
- wiersz 3: wątek T1 pobiera osobę p
- wiersz 8: zwiększa liczbę potomków p
- wiersz 14: wątek T1 wstrzymuje działanie
Wątek T2 przejmuje kontrolę i również wykonuje wiersz 3: żąda tej samej osoby p, co wątek T1. Gdyby kontekst trwałości wątków był taki sam, osoba p – będąca już w kontekście dzięki wątku T1 – powinna zostać zwrócona do wątku T2. W rzeczywistości metoda [getOne] korzysta z metody [EntityManager].Metoda ta wyszukuje obiekty w API i JPA, a dostęp do bazy danych uzyskuje tylko wtedy, gdy żądany obiekt nie znajduje się w kontekście trwałości; w przeciwnym razie zwraca obiekt z kontekstu trwałości. Gdyby tak było, T1 i T2 posiadałyby tę samą osobę p. T2 zwiększyłoby wówczas liczbę potomków p ponownie o 1 (wiersz 8). Jeśli jeden z wątków pomyślnie zaktualizuje dane po przerwie, liczba potomków p zostanie zwiększona o 2, a nie o 1, jak przewidywano. Można by zatem oczekiwać, że N wątków zwiększy liczbę potomków nie do N, ale do większej liczby. Tak jednak nie jest. Można zatem stwierdzić, że T1 i T2 nie mają tego samego odwołania do p. Sprawdzamy to, wyświetlając adres p przez wątki: dla każdego z nich jest on inny.
Wydaje się zatem, że wątki:
- współdzielą ten sam menedżer kontekstu trwałości (EntityManager)
- ale każdy z nich ma swój własny kontekst trwałości.
To tylko przypuszczenia i przydałaby się tu opinia eksperta.
3.1.8. Zmiana na SGBD
![]() |
Aby zmienić SGBD, wystarczy zastąpić plik [src/spring-config.xml] [2] plikiem [spring-config.xml] z odpowiedniego pliku SGBD w folderze [conf] [1].
Plik [spring-config.xml] firmy Oracle wygląda na przykład następująco:
<?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>
<!-- źródło danych 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>
W porównaniu z tym samym plikiem używanym wcześniej dla MySQL5 zmieniają się tylko niektóre wiersze:
- wiersz 14: dialekt SQL, którego ma używać Hibernate
- wiersze 25–28: parametry połączenia JDBC z SGBD
Zachęcamy czytelnika do powtórzenia testów opisanych dla pliku MySQL5 z innymi plikami SGBD.
3.1.9. Zmiana implementacji JPA
Wróćmy do architektury poprzednich testów:
![]() |
Zastępujemy implementację JPA / Hibernate implementacją JPA / Toplink. Ponieważ Toplink nie korzysta z tych samych bibliotek co Hibernate, używamy nowego projektu Eclipse:
![]() |
- w [1]: projekt Eclipse. Jest on identyczny z poprzednim. Zmieniają się jedynie plik konfiguracyjny [spring-config.xml] ([2]) oraz biblioteka [jpa-toplink], która zastępuje bibliotekę [jpa-hibernate].
- W pliku [3]: folder z przykładami do tego samouczka. W pliku [4]: projekt Eclipse do zaimportowania.
Plik konfiguracyjny [spring-config.xml] dla Toplinku przyjmuje następującą postać:
<?xml version="1.0" encoding="UTF-8"?>
<!-- JVM musi zostać uruchomiony z argumentem -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">
<!-- warstwy aplikacyjne -->
<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>
<!-- źródło danych 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>
<!-- menedżer transakcji -->
<tx:annotation-driven transaction-manager="txManager" />
<bean id="txManager" class="org.springframework.orm.jpa.JpaTransactionManager">
<property name="entityManagerFactory" ref="entityManagerFactory" />
</bean>
<!-- przetwarzanie wyjątków -->
<bean class="org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor" />
<!-- trwałość -->
<bean class="org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor" />
</beans>
Aby przejść z Hibernate na Toplink, należy zmienić tylko kilka wierszy:
- wiersz 19: implementacja JPA jest teraz realizowana przez Toplink
- wiersz 23: właściwość [databasePlatform] ma inną wartość niż w przypadku Hibernate: nazwę klasy specyficznej dla Toplink. Gdzie znaleźć tę nazwę, wyjaśniono w paragrafie 2.1.15.2.
To wszystko. Warto zwrócić uwagę na łatwość, z jaką można zmienić implementację SGBD lub JPA przy użyciu Springa.
Nie jesteśmy jednak jeszcze do końca gotowi. Na przykład podczas uruchamiania [InitDB] pojawia się wyjątek, który nie jest łatwy do zrozumienia:
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
Komunikat o błędzie w wierszu 1 skłania do zapoznania się z dokumentacją Springa. Dowiadujemy się wtedy nieco więcej o roli, jaką odgrywa niejasna deklaracja w pliku [spring-config.xml]:
<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>
Wiersz 1 wyjątku odnosi się do klasy o nazwie [InstrumentationLoadTimeWeaver], którą można znaleźć w wierszu 13 pliku konfiguracyjnego Springa. Dokumentacja Springa wyjaśnia, że klasa ta jest w niektórych przypadkach niezbędna do załadowania klas aplikacji i że aby mogła ona działać, klasa JVM musi zostać uruchomiona wraz z agentem. Agent ten jest dostarczany przez Springa i nosi nazwę [spring-agent]:
![]() |
- plik [spring-agent.jar] znajduje się w folderze <exemples>/lib [1]. Jest on dostarczany wraz z dystrybucją Spring 2.x (patrz paragraf 5.11).
- W pliku [3] tworzy się konfigurację uruchomienia [Run/Run...]
- w pliku [4] tworzymy konfigurację uruchomienia Java (istnieją różne rodzaje konfiguracji uruchomienia)
![]() |
- w [5] wybieramy zakładkę [Main]
- w [6] nadajemy nazwę konfiguracji
- w [7] należy podać nazwę projektu Eclipse, którego dotyczy ta konfiguracja (użyj przycisku „Browse”)
- w [8] należy nadać nazwę klasie Java zawierającej metodę [main] (użyj przycisku „Browse”)
- w polu [9] przechodzimy do zakładki [Arguments]. W tej zakładce można określić dwa rodzaje argumentów:
- w [9] – te przekazywane do metody [main]
- w [10] – te przekazywane do metody JVM, która wykona kod. Agent Spring jest definiowany za pomocą parametru -javaagent:wartość w metodzie JVM. Wartością tą jest ścieżka do pliku [spring-agent.jar].
- w [11]: zatwierdzamy konfigurację
- w pliku [12]: tworzona jest konfiguracja
- w pliku [13]: uruchamia się ją
Po wykonaniu tych czynności uruchamia się plik [InitDB] i daje takie same wyniki jak w przypadku Hibernate. W przypadku pliku [TestNG] należy postępować w ten sam sposób:
![]() |
- w [1] tworzymy konfigurację uruchomienia [Run/Run...]
- w [2] tworzymy konfigurację wykonania TestNG
- w [3] wybieramy zakładkę [Test]
- w [4] nadajemy nazwę konfiguracji
- w [5] należy podać nazwę projektu Eclipse, którego dotyczy ta konfiguracja (użyj przycisku „Browse”)
- w [6] należy nadać nazwę klasie testów (użyj przycisku „Browse”)
![]() |
- w [7] przechodzimy do zakładki [Arguments].
- w [8]: należy ustawić argument -javaagent dla JVM.
- w [9]: zatwierdź konfigurację
- w [10]: konfiguracja zostaje utworzona
- w [11]: uruchamia się ją
Po wykonaniu tych czynności uruchamia się [TestNG] i daje takie same wyniki jak w przypadku Hibernate.
3.2. Przykład 2: JBoss EJB3 / JPA z encją Personne
Powtarzamy ten sam przykład co poprzednio, ale uruchamiamy go w kontenerze EJB3, czyli tym samym, co JBoss:
![]() |
Kontener Ejb3 jest zazwyczaj zintegrowany z serwerem aplikacji. JBoss udostępnia „samodzielny” kontener Ejb3, który można wykorzystywać poza serwerem aplikacji. Przekonamy się, że zapewnia on usługi analogiczne do tych oferowanych przez Spring. Spróbujemy sprawdzić, który z tych kontenerów okazuje się bardziej praktyczny.
Instalacja kontenera EJB3 o numerze JBoss została opisana w rozdziale 5.12.
3.2.1. Projekt Eclipse / JBoss EJB3 / Hibernate
Projekt Eclipse wygląda następująco:
![]() |
![]() |
- w [1]: projekt Eclipse. Znajduje się on w [6] w przykładach samouczka [5]. Zaimportujemy go.
- w pliku [2]: kody Java warstw przedstawione w pakietach:
- [entites]: pakiet encji JPA
- [dao]: warstwa dostępu do danych – opiera się na warstwie JPA
- [service]: warstwa usług, a nie warstwa biznesowa. Wykorzystuje ona usługę transakcyjną kontenera EJB3.
- [tests]: zawiera programy testowe.
- w [3]: biblioteka [jpa-jbossejb3] zawiera pliki JAR niezbędne dla JBoss EJB3 (zobacz także [7] i [8]).
- w [4]: folder [conf] zawiera pliki konfiguracyjne dla każdego z plików SGBD wykorzystywanych w tym samouczku. W każdym przypadku są to dwa pliki: [persistence.xml], który konfiguruje warstwę JPA, oraz [jboss-config.xml], który konfiguruje kontener Ejb3.
3.2.2. Entities JPA
![]() |
W tym miejscu zarządzana jest tylko jedna jednostka – jednostka Personne, omówiona wcześniej w punkcie 3.1.2.
3.2.3. Warstwa [dao]
![]() |
Warstwa [dao] przedstawia interfejs [IDao] opisany wcześniej w paragrafie 3.1.3.
Implementacja [Dao] tego interfejsu jest następująca:
package dao;
...
@Stateless
public class Dao implements IDao {
@PersistenceContext
private EntityManager em;
// usunięcie osoby na podstawie jej identyfikatora
@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);
}
// pobieranie wszystkich osób
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public List<Personne> getAll() {
return em.createQuery("select p from Personne p").getResultList();
}
// pobieranie osób, których imię odpowiada wzorcowi
@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();
}
// pobieranie osoby na podstawie jej identyfikatora
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public Personne getOne(Integer id) {
return em.find(Personne.class, id);
}
// zapisać osobę
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public Personne saveOne(Personne personne) {
em.persist(personne);
return personne;
}
// zaktualizować osobę
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public Personne updateOne(Personne personne) {
return em.merge(personne);
}
}
- kod ten jest pod każdym względem identyczny z tym, który mieliśmy w przypadku Springa. Zmieniają się jedynie adnotacje Java i właśnie to omawiamy.
- wiersz 4: adnotacja @Stateless sprawia, że klasa [Dao] jest bezstanowym obiektem EJB. Adnotacja @Stateful sprawia, że klasa jest obiektem EJB ze stanem. EJB ze stanem posiada pola prywatne, których wartości należy zachować w czasie. Klasycznym przykładem jest klasa zawierająca informacje związane z użytkownikiem internetowym aplikacji. Instancja tej klasy jest powiązana z konkretnym użytkownikiem, a po zakończeniu wątku wykonawczego żądania tego użytkownika instancja musi zostać zachowana, aby była dostępna podczas następnego żądania tego samego klienta. EJB @Stateless nie posiada stanu. Jeśli powrócimy do tego samego przykładu, po zakończeniu wątku wykonawczego żądania użytkownika EJB @Stateless dołącza do puli EJB @Stateless i staje się dostępny dla wątku wykonawczego żądania innego użytkownika.
- Dla programisty pojęcie EJB 3 @Stateless jest zbliżone do singletonu w Spring. Będzie go używał w tych samych przypadkach.
- Wiersz 7: adnotacja @PersistenceContext jest taka sama jak ta spotykana w wersji warstwy [dao] w Spring. Oznacza ona pole, które przyjmie EntityManager, co pozwoli warstwie [dao] na obsługę kontekstu trwałości.
- wiersz 11: adnotacja @TransactionAttribute zastosowana do metody służy do skonfigurowania transakcji, w ramach której metoda ta będzie wykonywana. Oto kilka możliwych wartości tej adnotacji:
- TransactionAttributeType.REQUIRED: metoda musi być wykonywana w ramach transakcji. Jeśli transakcja została już rozpoczęta, operacje trwałościowe metody odbywają się w jej ramach. W przeciwnym razie tworzona jest i uruchamiana nowa transakcja.
- TransactionAttributeType.REQUIRES_NEW: metoda musi być wykonywana w nowej transakcji. Transakcja ta jest tworzona i uruchamiana.
- TransactionAttributeType.MANDATORY: metoda musi być wykonywana w istniejącej transakcji. Jeśli taka transakcja nie istnieje, zgłaszany jest wyjątek.
- TransactionAttributeType.NEVER: metoda nigdy nie jest wykonywana w transakcji.
- ...
Adnotację można było umieścić bezpośrednio w samej klasie:
@Stateless
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public class Dao implements IDao {
Wówczas atrybut ten zostanie zastosowany do wszystkich metod tej klasy.
3.2.4. Warstwa [metier / service]
![]() |
Warstwa [service] przedstawia interfejs [IService] omówiony wcześniej w paragrafie 3.1.4. Implementacja [Service] interfejsu [IService] jest identyczna z implementacją omówioną wcześniej w punkcie 3.1.4, z wyjątkiem trzech szczegółów:
@Stateless
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public class Service implements IService {
// warstwa [dao]
@EJB
private IDao dao;
public IDao getDao() {
return dao;
}
public void setDao(IDao dao) {
this.dao = dao;
}
- wiersz 2: klasa [Service] jest bezstanowym komponentem EJB
- wiersz 3: wszystkie metody klasy [Service] muszą być wykonywane w ramach transakcji
- wiersze 7–8: odwołanie do obiektu EJB z warstwy [dao] zostanie wstrzyknięte przez kontener EJB do pola [IDao dao] w wierszu 8. To adnotacja @EJB w wierszu 7 powoduje to wstrzyknięcie. Wstrzyknięty obiekt musi być obiektem EJB. Jest to istotna różnica w porównaniu ze Springiem, gdzie dowolny typ obiektu może zostać wstrzyknięty do innego obiektu.
3.2.5. Konfiguracja warstw
![]() |
Konfiguracja warstw [service], [dao] i [JPA] jest zapewniona przez następujące pliki:
- [META-INF/persistence.xml] konfiguruje warstwę JPA
- Plik [jboss-config.xml] konfiguruje kontener Ejb3. Sam korzysta on z plików [default.persistence.properties, ejb3-interceptors-aop.xml, embedded-jboss-beans.xml, jndi.properties]. Te ostatnie pliki są dostarczane wraz z JBoss Ejb3 i zapewniają domyślną konfigurację, której zazwyczaj nie należy zmieniać. Programista interesuje się wyłącznie plikiem [jboss-config.xml]
Przyjrzyjmy się obu plikom konfiguracyjnym:
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">
<!-- dostawcą JPA jest Hibernate -->
<provider>org.hibernate.ejb.HibernatePersistence</provider>
<!-- DataSource JTA zarządzane przez środowisko Java EE5 -->
<jta-data-source>java:/datasource</jta-data-source>
<properties>
<!-- wyszukiwanie encji warstwy JBA -->
<property name="hibernate.archive.autodetection" value="class, hbm" />
<!-- logi SQL Hibernate
<property name="hibernate.show_sql" value="true"/>
<property name="hibernate.format_sql" value="true"/>
<property name="use_sql_comments" value="true"/>
-->
<!-- typ zarządzany przez SGBD -->
<property name="hibernate.dialect" value="org.hibernate.dialect.MySQLInnoDBDialect" />
<!-- odtworzenie wszystkich tabel (drop+create) podczas wdrażania jednostki trwałości -->
<property name="hibernate.hbm2ddl.auto" value="create" />
</properties>
</persistence-unit>
</persistence>
Ten plik przypomina te, z którymi mieliśmy już do czynienia podczas analizy encji JPA. Konfiguruje on warstwę Hibernate o nazwie JPA. Nowości są następujące:
- wiersz 5: jednostka trwałości jpa nie posiada atrybutu transaction-type, który dotychczas zawsze występował:
<persistence-unit name="jpa" transaction-type="RESOURCE_LOCAL" />
W przypadku braku wartości atrybut transaction-type przyjmuje wartość domyślną „JTA” (dla Java Transaction API), co oznacza, że menedżer transakcji jest dostarczany przez kontener EJB3. Menedżer „JTA” ma szersze możliwości niż menedżer „RESOURCE_LOCAL”: może obsługiwać transakcje obejmujące wiele połączeń. Za pomocą JTA można otworzyć transakcję t1 na połączeniu c1 w ramach SGBD 1, transakcję t2 na połączeniu c2 z SGBD 2 i traktować (t1,t2) jako jedną transakcję, w której albo wszystkie operacje zakończą się powodzeniem (commit), albo żadna (rollback).
W tym przypadku korzystamy z menedżera JTA kontenera JBoss EJB3.
- Wiersz 11: deklaruje źródło danych, z którego ma korzystać menedżer JTA. Jest ono podane w postaci nazwy JNDI (Java Naming and Directory Interface). Źródło danych jest zdefiniowane w [jboss-config.xml].
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">
<!-- fabryka typu DataSource -->
<bean name="datasourceFactory" class="org.jboss.resource.adapter.jdbc.local.LocalTxDataSource">
<!-- nazwa JNDI dla DataSource -->
<property name="jndiName">java:/datasource</property>
<!-- zarządzana baza danych -->
<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>
<!-- właściwości puli połączeń -->
<property name="minSize">0</property>
<property name="maxSize">10</property>
<property name="blockingTimeout">1000</property>
<property name="idleTimeout">100000</property>
<!-- menedżer transakcji, w tym przypadku JTA -->
<property name="transactionManager">
<inject bean="TransactionManager" />
</property>
<!-- menedżer pamięci podręcznej Hibernate -->
<property name="cachedConnectionManager">
<inject bean="CachedConnectionManager" />
</property>
<!-- właściwości instancjonowania JNDI? -->
<property name="initialContextProperties">
<inject bean="InitialContextProperties" />
</property>
</bean>
<!-- DataSource jest żądane przez fabrykę -->
<bean name="datasource" class="java.lang.Object">
<constructor factoryMethod="getDatasource">
<factory bean="datasourceFactory" />
</constructor>
</bean>
</deployment>
- wiersz 3: tagiem nadrzędnym pliku jest <deployment>. Ten plik wdrożeniowy służy przede wszystkim do skonfigurowania źródła danych java:/datasource, które zostało zadeklarowane w pliku persistence.xml.
- Źródło danych jest definiowane przez bean „datasource” w wierszu 38. Widać, że źródło danych jest pobierane (wiersz 40) z „fabryki” zdefiniowanej przez bean „datasourceFactory” w wierszu 7. Aby uzyskać źródło danych aplikacji, klient musi wywołać metodę [getDatasource] klasy factory (wiersz 39).
- wiersz 7: klasa factory, która dostarcza źródło danych, jest klasą JBoss.
- wiersz 9: nazwa źródła danych JNDI. Musi to być ta sama nazwa, co ta zadeklarowana w tagu <jta-data-source> w pliku persistence.xml. W rzeczywistości warstwa JPA wykorzysta tę nazwę JNDI do wywołania źródła danych.
- wiersze 12–15: coś bardziej standardowego: parametry JDBC połączenia z SGBD
- wiersze 18–21: konfiguracja wewnętrznej puli połączeń kontenera JBoss EJB3.
- wiersze 24–26: menedżer JTA. Klasa [TransactionManager], wstrzyknięta w wierszu 25, jest zdefiniowana w pliku [embedded-jboss-beans.xml].
- wiersze 28–30: pamięć podręczna Hibernate – pojęcie, którego jeszcze nie omawialiśmy. Klasa [CachedConnectionManager], wstrzyknięta w wierszu 29, jest zdefiniowana w pliku [embedded-jboss-beans.xml]. Należy zauważyć, że konfiguracja jest teraz zależna od Hibernate, co sprawi nam problem, gdy będziemy chcieli przejść na Toplink.
- wiersze 32–34: konfiguracja serwisu JNDI.
To już koniec pracy z plikiem konfiguracyjnym JBoss EJB3. Jest on złożony, a wiele kwestii pozostaje niejasnych. Został on zaczerpnięty z pliku [ref1]. Będziemy jednak w stanie dostosować go do innego SGBD (wiersze 12–15 z jboss-config.xml, wiersz 24 z persistence.xml). Migracja do Toplink nie była możliwa z powodu braku przykładów.
3.2.6. Program kliencki [InitDB]
Zajmujemy się teraz pisaniem pierwszego klienta dla opisanej wcześniej architektury:
![]() |
Kod programu [InitDB] wygląda następująco:
package tests;
...
public class InitDB {
// warstwa usług
private static IService service;
// konstruktor
public static void main(String[] args) throws ParseException, NamingException {
// uruchamia się kontener EJB3 JBoss
// pliki konfiguracyjne ejb3-interceptors-aop.xml i embedded-jboss-beans.xml są wykorzystywane
EJB3StandaloneBootstrap.boot(null);
// Tworzenie fasoli specyficznych dla aplikacji
EJB3StandaloneBootstrap.deployXmlResource("META-INF/jboss-config.xml");
// Wdrożenie wszystkich elementów EJBs znalezionych w ścieżce klas (powolne, skanuje wszystkie)
// EJB3StandaloneBootstrap.scanClasspath();
// Wdrażane są wszystkie pliki EJB znalezione w ścieżce klas aplikacji
EJB3StandaloneBootstrap.scanClasspath("bin".replace("/", File.separator));
// Inicjowany jest kontekst JNDI. Wykorzystywany jest plik jndi.properties
InitialContext initialContext = new InitialContext();
// tworzenie instancji warstwy usług
service = (IService) initialContext.lookup("Service/local");
// Opróżnianie bazy danych
clean();
// wypełnianie bazy
fill();
// sprawdzanie wizualne
dumpPersonnes();
// zatrzymywanie kontenera EJB
EJB3StandaloneBootstrap.shutdown();
}
// wyświetlanie zawartości tabeli
private static void dumpPersonnes() {
System.out.format("[personnes]-------------------------------------------------------------------%n");
for (Personne p : service.getAll()) {
System.out.println(p);
}
}
// wypełnianie tabeli
public static void fill() throws ParseException {
// tworzenie osób
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);
// zapisywanie
service.saveArray(new Personne[] { p1, p2 });
}
// usuwanie elementów z tabeli
public static void clean() {
for (Personne p : service.getAll()) {
service.deleteOne(p.getId());
}
}
}
- Sposób uruchomienia kontenera JBoss EJB3 został opisany w pliku [ref1].
- Wiersz 13: kontener zostaje uruchomiony. [EJB3StandaloneBootstrap] jest klasą kontenera.
- wiersz 16: jednostka wdrożeniowa skonfigurowana przez [jboss-config.xml] zostaje wdrożona w kontenerze: menedżer JTA, źródło danych, pula połączeń, pamięć podręczna Hibernate oraz usługa JNDI zostają skonfigurowane.
- wiersz 22: kontener otrzymuje polecenie przeszukania folderu bin projektu Eclipse w celu znalezienia komponentów EJB. Komponenty EJB z warstw [service] i [dao] zostaną znalezione i będą zarządzane przez kontener.
- wiersz 25: zainicjowano kontekst JNDI. Posłuży on do lokalizowania komponentów EJB.
- wiersz 28: EJB odpowiadający klasie [Service] z warstwy [service] jest żądany od usługi JNDI. Dostęp do komponentu EJB można uzyskać lokalnie (local) lub przez sieć (remote). W tym przypadku nazwa „Service/local” poszukiwanego komponentu EJB odnosi się do klasy [Service] z warstwy [service] w przypadku dostępu lokalnego.
- Obecnie aplikacja została wdrożona i posiadamy odwołanie do warstwy [service]. Znajdujemy się w tej samej sytuacji, co po wierszu 11 poniżej w kodzie [InitDB] wersji Spring. W obu wersjach występuje zatem ten sam kod.
public class InitDB {
// warstwa usługowa
private static IService service;
// konstruktor
public static void main(String[] args) throws ParseException {
// konfiguracja aplikacji
ApplicationContext ctx = new ClassPathXmlApplicationContext("spring-config.xml");
// warstwa usługowa
service = (IService) ctx.getBean("service");
// opróżnianie bazy danych
clean();
// wypełnianie bazy
fill();
// sprawdzamy wizualnie
dumpPersonnes();
}
...
- wiersz 36 (JBoss EJB3): zatrzymujemy kontener EJB3.
Wykonanie kodu [InitDB] daje następujące wyniki:
Zachęcamy czytelnika do zapoznania się z tymi logami. Znajdują się w nich interesujące informacje na temat działania kontenera EJB3.
3.2.7. Testy jednostkowe [TestNG]
Kod programu [TestNG] wygląda następująco:
package tests;
...
public class TestNG {
// warstwa usługowa
private IService service = null;
@BeforeClass
public void init() throws NamingException, ParseException {
// log
log("init");
// uruchamiamy kontener EJB3 JBoss
// wykorzystywane są pliki konfiguracyjne ejb3-interceptors-aop.xml oraz embedded-jboss-beans.xml
EJB3StandaloneBootstrap.boot(null);
// Tworzenie fasoli specyficznych dla aplikacji
EJB3StandaloneBootstrap.deployXmlResource("META-INF/jboss-config.xml");
// Wdrożenie wszystkich plików EJBs znalezionych w ścieżce klas (powolne, skanuje wszystkie)
// EJB3StandaloneBootstrap.scanClasspath();
// Wdrażane są wszystkie pliki EJB znalezione w ścieżce klas aplikacji
EJB3StandaloneBootstrap.scanClasspath("bin".replace("/", File.separator));
// Inicjowany jest kontekst JNDI. Wykorzystywany jest plik jndi.properties
InitialContext initialContext = new InitialContext();
// tworzenie instancji warstwy usług
service = (IService) initialContext.lookup("Service/local");
// Opróżnianie bazy danych
clean();
// wypełnianie bazy
fill();
// sprawdzanie wizualne
dumpPersonnes();
}
@AfterClass
public void terminate() {
// log
log("terminate");
// Wyłączanie kontenera EJB
EJB3StandaloneBootstrap.shutdown();
}
@BeforeMethod
public void setUp() throws ParseException {
...
}
...
}
- Metoda init (wiersze 10–37), służąca do przygotowania środowiska niezbędnego do przeprowadzenia testów, zawiera kod wyjaśniony wcześniej w [InitDB].
- Metoda `terminate` (wiersze 40–45), która jest wykonywana po zakończeniu testów (obecność adnotacji `@AfterClass`), zatrzymuje kontener EJB3 (wiersz 44).
- Wszystko pozostałe jest identyczne jak w wersji Spring.
Testy zakończyły się powodzeniem:

3.2.8. Zmiana na SGBD
![]() |
Aby zmienić plik SGBD, wystarczy zastąpić zawartość folderu [META-INF] [2] zawartością folderu SGBD w folderze [conf] [1]. Weźmy na przykład serwer SQL:
Plik [persistence.xml] ma następującą treść:
<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">
<!-- dostawcą JPA jest Hibernate -->
<provider>org.hibernate.ejb.HibernatePersistence</provider>
<!-- DataSource JTA zarządzane przez środowisko Java EE5 -->
<jta-data-source>java:/datasource</jta-data-source>
<properties>
<!-- wyszukiwanie encji warstwy JBA -->
<property name="hibernate.archive.autodetection" value="class, hbm" />
<!-- logi SQL Hibernate
<property name="hibernate.show_sql" value="true"/>
<property name="hibernate.format_sql" value="true"/>
<property name="use_sql_comments" value="true"/>
-->
<!-- typ zarządzany przez SGBD -->
<property name="hibernate.dialect" value="org.hibernate.dialect.SQLServerDialect" />
<!-- odtworzenie wszystkich tabel (drop+create) podczas wdrażania jednostki trwałości -->
<property name="hibernate.hbm2ddl.auto" value="create" />
</properties>
</persistence-unit>
</persistence>
Zmienił się tylko jeden wiersz:
- wiersz 24: dialekt SQL, którego Hibernate ma używać
Plik [jboss-config.xml] z serwera SQL wygląda natomiast następująco:
<?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">
<!-- fabryka DataSource -->
<bean name="datasourceFactory" class="org.jboss.resource.adapter.jdbc.local.LocalTxDataSource">
<!-- nazwa JNDI dla DataSource -->
<property name="jndiName">java:/datasource</property>
<!-- zarządzana baza danych -->
<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>
<!-- właściwości puli połączeń -->
...
</bean>
</deployment>
Zmieniono jedynie wiersze 12–15: określają one parametry nowego połączenia JDBC.
Zachęcamy czytelnika do powtórzenia testów opisanych dla pliku MySQL5 na innych plikach SGBD.
3.2.9. Zmiana implementacji JPA
Jak wspomniano powyżej, nie znaleźliśmy żadnego przykładu wykorzystania kontenera JBoss EJB3 z Toplinkiem. Na dzień dzisiejszy (czerwiec 2007 r.) nadal nie wiem, czy taka konfiguracja jest możliwa.
3.3. Inne przykłady
Podsumujmy, co zrobiono z encją Personne. Stworzyliśmy trzy architektury w celu przeprowadzenia tych samych testów:
1 – implementacja oparta na Spring / Hibernate
![]() |
2 – implementacja Spring / Toplink
![]() |
3 – implementacja oparta na Jboss Ejb3 i Hibernate
![]() |
Przykłady zawarte w samouczku wykorzystują te trzy architektury wraz z innymi encjami omówionymi w pierwszej części samouczka:
Kategoria – Artykuł
![]() |
- w [1]: wersja Spring / Hibernate
- w [2]: wersja Spring / Toplink
- w [3]: wersja Jboss Ejb3 / Hibernate
Osoba – Adres – Działalność
![]() |
- w [1]: wersja Spring / Hibernate
- w [2]: wersja Spring / Toplink
- w [3]: wersja JBoss EJB3 / Hibernate
Przykłady te nie wprowadzają żadnych nowości w zakresie architektury. Odnoszą się one po prostu do sytuacji, w której istnieje wiele encji do zarządzania, powiązanych ze sobą relacjami typu „jeden do wielu” lub „wiele do wielu”, czego nie było w przykładach z encją Personne.
3.4. Przykład 3: Spring / JPA w aplikacji internetowej
3.4.1. Wprowadzenie
W tym miejscu powracamy do aplikacji przedstawionej w poniższym dokumencie:
[ref4]: Podstawy tworzenia aplikacji internetowych MVC w języku Java [http://tahe.developpez.com/java/baseswebmvc/].
Dokument ten przedstawia podstawy tworzenia aplikacji internetowych w języku Java. Aby zrozumieć poniższy przykład, czytelnik powinien posiadać tę wiedzę. Aplikacja internetowa będzie korzystać z serwera Tomcat. Jego instalacja oraz obsługa w środowisku Eclipse zostały omówione w rozdziale 5.3.
Aplikacja została pierwotnie opracowana z wykorzystaniem warstwy [dao] opartej na narzędziu Ibatis / SqlMap [http://ibatis.apache.org/], które zapewniało pomost między modelem relacyjnym a obiektowym. Zadowalamy się zastąpieniem Ibatis przez JPA. Architektura aplikacji będzie wyglądać następująco:
![]() |
Aplikacja internetowa, którą zamierzamy stworzyć, umożliwi zarządzanie grupą osób za pomocą czterech operacji:
- wyświetlenie listy osób w grupie
- dodanie osoby do grupy
- zmiana danych osoby z grupy
- usunięcie osoby z grupy
Te cztery podstawowe operacje można rozpoznać w tabeli bazy danych. Poniższe zrzuty ekr pokazują strony, które aplikacja wyświetla użytkownikowi.
![]() |
![]() |
![]() |
![]() |
![]() |
3.4.2. Projekt Eclipse
Projekt Eclipse aplikacji wygląda następująco:
![]() |
- w [1]: projekt internetowy. Jest to projekt Eclipse typu [Dynamic Web Project] [2]. Znajdziemy go w pliku [4] w folderze [3] zawierającym przykłady z samouczka. Zaimportujemy go.
![]() |
- w pliku [5]: źródła i konfiguracja warstw [service, dao, jpa]. Zachowujemy dotychczasowe wyniki [dao, entites, service] z projektu Eclipse [hibernate-spring-personnes-metier-dao] omówionego w paragrafie 3.1.1. Rozwijamy jedynie warstwę [web], reprezentowaną tutaj przez pakiet [web]. Ponadto zachowujemy pliki konfiguracyjne [persistence.xml, spring-config.xml] tego projektu, z tą różnicą, że będziemy korzystać z Postgresu SGBD, co przekłada się na następujące zmiany w [spring-config.xml]:
<?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>
<!-- źródło danych 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>
Wiersze 8 oraz 16–19 zostały dostosowane do Postgres.
- W pliku [6]: folder [WebContent] zawiera strony projektu JSP oraz niezbędne biblioteki. Biblioteki te są przedstawione w pliku [8]
- Aplikacja może być używana z różnymi wersjami SGBD. Wystarczy zmienić plik [spring-config.xml]. Folder [conf] [7] zawiera plik [spring-config.xml] dostosowany do różnych SGBD.
3.4.3. Warstwa [web]
Nasza aplikacja ma następującą architekturę wielowarstwową:
![]() |
Warstwa [web] udostępni użytkownikowi ekrany umożliwiające zarządzanie grupą osób:
- lista osób w grupie
- dodawanie osoby do grupy
- zmiana danych osoby w grupie
- usunięcie osoby z grupy
W tym celu warstwa ta będzie korzystać z warstwy [service], która z kolei będzie korzystać z warstwy [dao]. Ekrany obsługiwane przez warstwę [web] zostały już omówione (punkt 3.4.1). Aby opisać warstwę internetową, przedstawimy kolejno:
- jej konfigurację
- jej widoki
- jej kontroler
- kilka testów
3.4.3.1. Konfiguracja aplikacji internetowej
Przyjrzyjmy się architekturze projektu Eclipse:
![]() | ![]() |
- w pakiecie [web] znajduje się kontroler aplikacji internetowej: klasa [Application].
- Strony aplikacji o nazwach JSP / JSTL znajdują się w klasie [WEB-INF/vues].
- Folder [WEB-INF/lib] zawiera archiwa stron trzecich niezbędne dla działania aplikacji. Są one widoczne w folderze [Web App Libraries].
[web.xml]
Plik [web.xml] jest plikiem wykorzystywanym przez serwer WWW do ładowania aplikacji. Jego zawartość jest następująca:
<?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>
<!-- mapowanie ServletPersonne-->
<servlet-mapping>
<servlet-name>personnes</servlet-name>
<url-pattern>/do/*</url-pattern>
</servlet-mapping>
<!-- pliki strony głównej -->
<welcome-file-list>
<welcome-file>index.jsp</welcome-file>
</welcome-file-list>
<!-- Strona nieoczekiwanego błędu -->
<error-page>
<exception-type>java.lang.Exception</exception-type>
<location>/WEB-INF/vues/exception.jsp</location>
</error-page>
</web-app>
- wiersze 23–26: adresy URL [/do/*] będą obsługiwane przez serwlet [personnes]
- wiersze 7–8: serwlet [personnes] jest instancją klasy [Application], którą zamierzamy stworzyć.
- wiersze 9–20: definiują trzy parametry [urlList, urlEdit, urlErreurs] identyfikujące adresy URL stron JSP widoków [list, edit, erreurs].
- wiersze 28–30: aplikacja posiada domyślną stronę startową [index.jsp], która znajduje się w katalogu głównym aplikacji internetowej.
- wiersze 32–35: aplikacja posiada domyślną stronę błędów, która jest wyświetlana, gdy serwer WWW napotka wyjątek nieobsługiwany przez aplikację.
- wiersz 37: tag <exception-type> określa typ wyjątku obsługiwany przez dyrektywę <error-page>; w tym przypadku jest to typ [java.lang.Exception] i jego pochodne, a więc wszystkie wyjątki.
- wiersz 38: tag <location> wskazuje stronę JSP, która ma zostać wyświetlona w przypadku wystąpienia wyjątku typu zdefiniowanego przez <exception-type>. Wystąpiony wyjątek jest dostępny na tej stronie w obiekcie o nazwie exception, jeśli strona zawiera dyrektywę:
<%@ page isErrorPage="true" %>
- (ciąg dalszy)
- jeśli <exception-type> określa typ T1, a wyjątek typu T2, który nie jest pochodną typu T1, zostanie zgłoszony do serwera WWW, serwer ten wysyła do klienta własną stronę wyjątku, która zazwyczaj jest mało przyjazna dla użytkownika. Stąd znaczenie tagu <error-page> w pliku [web.xml].
[index.jsp]
Strona ta jest wyświetlana, gdy użytkownik bezpośrednio żąda kontekstu aplikacji bez podania adresu URL, c.a.d. Tutaj [/spring-jpa-hibernate-personnes-crud]. Jej zawartość jest następująca:
<%@ 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] przekierowuje (wiersz 4) klienta na adres URL [/do/list]. Ten adres URL wyświetla listę osób z grupy.
3.4.3.2. Strony JSP / JSTL aplikacji
Widok [list.jsp]
Służy do wyświetlania listy osób:

Jej kod jest następujący:
<%@ 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énom</th>
<th>Nom</th>
<th>Date de naissance</th>
<th>Marié</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>
- Ten widok otrzymuje dwa elementy w swoim modelu:
- element [personnes] powiązany z obiektem typu [List], który z kolei zawiera obiekty typu [Personne]: listę osób.
- opcjonalny element [erreurs] powiązany z obiektem typu [List], zawierającym obiekty typu [String]: listę komunikatów o błędach.
- wiersze 31–43: przeglądamy listę ${osoby}, aby wyświetlić tablicę typu HTML zawierającą osoby z grupy.
- wiersz 40: adres URL, do którego prowadzi link [Modifier], jest ustalany na podstawie pola [id] bieżącej osoby, aby kontroler powiązany z adresem URL [/do/edit] wiedział, którą osobę należy zmodyfikować.
- wiersz 41: to samo dotyczy linku [Supprimer].
- wiersz 37: aby wyświetlić datę urodzenia osoby w formacie JJ/MM/AAAA, używa się tagu <dt> z biblioteki tagów [DateTime] projektu Apache [Jakarta Taglibs]:

Plik opisu tej biblioteki tagów jest zdefiniowany w wierszu 3.
- Wiersz 46: link [Ajout] służący do dodania nowej osoby ma jako cel adres URL [/do/edit], podobnie jak link [Modifier] z wiersza 40. To właśnie wartość -1 parametru [id] wskazuje, że mamy do czynienia z dodaniem, a nie z modyfikacją.
- wiersze 10–18: jeśli element ${błędy} występuje w szablonie, wyświetlane są zawarte w nim komunikaty o błędach.
Widok [edit.jsp]
Służy do wyświetlania formularza dodawania nowej osoby lub modyfikacji istniejącej osoby:
![]() |
Kod widoku [edit.jsp] jest następujący:
<%@ 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é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é</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>
Ten widok zawiera formularz służący do dodania nowej osoby lub aktualizacji istniejącej osoby. W dalszej części, w celu uproszczenia zapisu, będziemy używać wyłącznie terminu [mise à jour]. Przycisk [Valider] (wiersz 73) powoduje wywołanie POST formularza pod adresem URL [/do/validate] (wiersz 16). Jeśli operacja POST zakończy się niepowodzeniem, ponownie wyświetlany jest widok [edit.jsp] wraz z wystąpionymi błędami; w przeciwnym razie wyświetlany jest widok [list.jsp].
- Widok [edit.jsp], wyświetlany zarówno w przypadku nieudanego widoku GET, jak i POST, otrzymuje w swoim szablonie następujące elementy:
atrybut | GET | POST |
identyfikator osoby aktualizacji | to samo | |
jej wersja | tak samo | |
imię | wpisane imię | |
nazwisko | wpisane nazwisko | |
jego data urodzenia | wprowadzona data urodzenia | |
jego stan cywilny | wprowadzony stan cywilny | |
liczba dzieci | wprowadzona liczba dzieci | |
puste | komunikat o błędzie informujący o niepowodzeniu dodania lub modyfikacji w momencie uruchomienia POST spowodowanego przez przycisk [Envoyer]. Puste, jeśli nie wystąpił błąd. | |
puste | oznacza błędne imię – w przeciwnym razie puste | |
puste | oznacza błędne nazwisko – w przeciwnym razie puste | |
puste | oznacza błędną datę urodzenia – w przeciwnym razie puste | |
puste | oznacza błędną liczbę dzieci – w przeciwnym razie pozostaw puste |
- wiersze 11–15: jeśli kod POST z formularza nie zostanie poprawnie przetworzony, pojawi się kod [erreurEdit!=''] i zostanie wyświetlony komunikat o błędzie.
- wiersz 16: formularz zostanie wysłany pod adres URL [/do/validate]
- wiersz 20: wyświetlany jest element szablonu o nazwie [id]
- wiersz 24: wyświetlany jest element szablonu o nazwie [version]
- wiersze 26–32: wprowadzenie imienia osoby:
- podczas pierwszego wyświetlenia formularza (GET), ${imię} wyświetla aktualną wartość pola [prenom] z zaktualizowanego obiektu [Personne], a ${erreurPrenom} jest puste.
- w przypadku błędu po POST ponownie wyświetlana jest wprowadzona wartość ${prenom} oraz ewentualny komunikat o błędzie ${erreurPrenom}
- wiersze 33–39: wprowadzenie nazwiska osoby
- wiersze 40–46: wprowadzanie daty urodzenia osoby
- wiersze 47–61: wprowadzanie informacji o stanie cywilnym osoby za pomocą przycisku opcji. Wykorzystuje się wartość pola [marie] obiektu [Personne], aby określić, który z dwóch przycisków opcji należy zaznaczyć.
- wiersze 62–68: wprowadzenie liczby dzieci danej osoby
- wiersz 71: ukryte pole HTML o nazwie [id], którego wartością jest pole [id] aktualizowanej osoby; -1 oznacza dodanie, inna wartość oznacza modyfikację.
- wiersz 72: ukryte pole HTML o nazwie [version], którego wartość odpowiada wartości pola [id] aktualizowanej osoby.
- wiersz 73: przycisk [Valider] typu [Submit] w formularzu
- wiersz 74: link umożliwiający powrót do listy osób. Został on oznaczony jako [Annuler], ponieważ pozwala opuścić formularz bez jego zatwierdzania.
Widok [exception.jsp]
Służy do wyświetlenia strony informującej, że wystąpił wyjątek nieobsługiwany przez aplikację, który został zgłoszony do serwera WWW.
Na przykład usuńmy osobę, która nie istnieje w grupie:
![]() |
Kod widoku [exception.jsp] jest następujący:
<%@ 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 à la liste</a>
</body>
</html>
- widok ten otrzymuje w swoim szablonie element o kluczu [exception], który stanowi wyjątek przechwycony przez serwer WWW. Aby serwer WWW uwzględnił ten element w szablonie strony JSP, strona musi zawierać tag z linii 3.
- wiersz 6: ustawia się kod stanu odpowiedzi HTTP na wartość 200. Jest to pierwszy nagłówek odpowiedzi HTTP. Kod 200 oznacza dla klienta, że jego żądanie zostało zrealizowane. Zazwyczaj do odpowiedzi serwera dołączony jest dokument o kodzie HTML. Tak jest również w tym przypadku. Jeśli kod stanu odpowiedzi HTTP nie zostanie ustawiony na 200, przyjmie on w tym przypadku wartość 500, co oznacza, że wystąpił błąd. Serwer WWW, który przechwycił nieobsługiwany wyjątek, uznaje tę sytuację za nieprawidłową i sygnalizuje ją kodem 500. Reakcja na kod HTTP 500 różni się w zależności od przeglądarki: Firefox wyświetla dokument HTML, który może towarzyszyć tej odpowiedzi, podczas gdy IE ignoruje ten dokument i wyświetla własną stronę. Z tego powodu zastąpiliśmy kod 500 kodem 200.
- wiersz 16: wyświetlany jest tekst wyjątku
- wiersz 18: użytkownikowi proponowany jest link umożliwiający powrót do listy osób
Widok [erreurs.jsp]
Służy do wyświetlania strony informującej o błędach inicjalizacji aplikacji c.a.d oraz o błędach wykrytych podczas wykonywania metody [init] serwletu kontrolera. Może to być na przykład brak parametru w pliku [web.xml], jak pokazano w poniższym przykładzie:

Kod strony [erreurs.jsp] wygląda następująco:
<%@ 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>
Strona otrzymuje w swoim szablonie element [erreurs], który jest obiektem typu [ArrayList] zawierającym obiekty [String], przy czym te ostatnie są komunikatami o błędach. Są one wyświetlane w pętli w wierszach 13–15.
3.4.3.3. Kontroler aplikacji
Kontroler [Application] jest zdefiniowany w pakiecie [web]:

Struc tura i inicjalizacja kontrolera
Szkielet kontrolera [Application] wygląda następująco:
package web;
...
@SuppressWarnings("serial")
public class Application extends HttpServlet {
// parametry instancji
private String urlErreurs = null;
private ArrayList erreursInitialisation = new ArrayList<String>();
private String[] paramètres = { "urlList", "urlEdit", "urlErreurs" };
private Map params = new HashMap<String, String>();
// usługa
private IService service = null;
// inicjalizacja
@SuppressWarnings("unchecked")
public void init() throws ServletException {
// pobieranie parametrów inicjalizacyjnych serwletu
ServletConfig config = getServletConfig();
// przetwarzanie pozostałych parametrów inicjalizacyjnych
String valeur = null;
for (int i = 0; i < paramètres.length; i++) {
// wartość parametru
valeur = config.getInitParameter(paramètres[i]);
// czy parametr istnieje?
if (valeur == null) {
// odnotowuje się błąd
erreursInitialisation.add("Le paramètre [" + paramètres[i] + "] n'a pas été initialisé");
} else {
// zapisuje się wartość parametru
params.put(paramètres[i], valeur);
}
}
// adres URL widoku [erreurs] podlega specjalnej obróbce
urlErreurs = config.getInitParameter("urlErreurs");
if (urlErreurs == null)
throw new ServletException("Le paramètre [urlErreurs] n'a pas été initialisé");
// konfiguracja aplikacji
ApplicationContext ctx = new ClassPathXmlApplicationContext("spring-config.xml");
// warstwa usługowa
service = (IService) ctx.getBean("service");
// baza danych jest opróżniana
clean();
// wypełnianie bazy
try {
fill();
} catch (ParseException e) {
throw new ServletException(e);
}
}
// wypełnianie tabeli
public void fill() throws ParseException {
// tworzenie osób
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);
// zapisywanie
service.saveArray(new Personne[] { p1, p2 });
}
// usuwanie elementów z tabeli
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 {
...
}
// wyświetlanie listy osób
private void doListPersonnes(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
...
}
// zmiana / dodanie osoby
private void doEditPersonne(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
...
}
// usunięcie osoby
private void doDeletePersonne(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
...
}
// potwierdzenie zmiany / dodania osoby
public void doValidatePersonne(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
...
}
// wyświetlenie wstępnie wypełnionego formularza
private void showFormulaire(HttpServletRequest request, HttpServletResponse response, String erreurEdit) throws ServletException, IOException {
...
}
// wysłanie
public void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
// przekazanie kontroli do GET
doGet(request, response);
}
}
- wiersze 21–34: pobierane są oczekiwane parametry z pliku [web.xml].
- wiersze 37–39: parametr [urlErreurs] musi być koniecznie obecny, ponieważ określa adres URL widoku [erreurs], który może wyświetlać ewentualne błędy inicjalizacji. Jeśli nie istnieje, aplikacja zostaje przerwana poprzez uruchomienie widoku [ServletException] (wiersz 39). Wyjątek ten zostanie przekazany do serwera WWW i obsłużony przez tag <error-page> w pliku [web.xml]. Wyświetlany jest zatem widok [exception.jsp]:

Powyższy link [Retour à la liste] nie działa. Jego użycie zwraca tę samą odpowiedź, dopóki aplikacja nie zostanie zmodyfikowana i ponownie załadowana. Jest on przydatny w przypadku innych rodzajów wyjątków, jak już widzieliśmy.
- wiersze 40–43: wykorzystują plik konfiguracyjny Springa w celu pobrania odwołania do warstwy [service]. Po zainicjowaniu kontrolera jego metody dysponują odwołaniem [service] do warstwy [service] (wiersz 15), które wykorzystają do wykonania akcji żądanych przez użytkownika. Działania te zostaną przechwycone przez metodę [doGet], która przekaże je do przetworzenia przez konkretną metodę kontrolera:
Url | Metoda HTTP | metoda kontrolera |
GET | doListPersonnes | |
GET | doEditPersonne | |
POST | doValidatePersonne | |
GET | doDeletePersonne |
Metoda [doGet]
Celem tej metody jest skierowanie przetwarzania akcji żądanych przez użytkownika do właściwej metody. Jej kod jest następujący:
// GET
@SuppressWarnings("unchecked")
public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
// sprawdzamy, jak przebiegła inicjalizacja serwletu
if (erreursInitialisation.size() != 0) {
// przekazujemy kontrolę do strony błędów
request.setAttribute("erreurs", erreursInitialisation);
getServletContext().getRequestDispatcher(urlErreurs).forward(request, response);
// koniec
return;
}
// pobieramy metodę wysyłania żądania
String méthode = request.getMethod().toLowerCase();
// pobieramy akcję do wykonania
String action = request.getPathInfo();
// akcja?
if (action == null) {
action = "/list";
}
// wykonanie akcji
if (méthode.equals("get") && action.equals("/list")) {
// lista osób
doListPersonnes(request, response);
return;
}
if (méthode.equals("get") && action.equals("/delete")) {
// usunięcie osoby
doDeletePersonne(request, response);
return;
}
if (méthode.equals("get") && action.equals("/edit")) {
// wyświetlenie formularza dodania / zmiany osoby
doEditPersonne(request, response);
return;
}
if (méthode.equals("post") && action.equals("/validate")) {
// zatwierdzenie formularza dodania / zmiany osoby
doValidatePersonne(request, response);
return;
}
// inne przypadki
doListPersonnes(request, response);
}
- wiersze 7–13: sprawdzamy, czy lista błędów inicjalizacji jest pusta. Jeśli tak nie jest, wyświetlamy widok [erreurs(erreurs)], który zgłosi błąd lub błędy.
- wiersz 15: pobieramy metodę [get] lub [post], której klient użył do wysłania żądania.
- wiersz 17: pobieramy wartość parametru [action] z zapytania.
- wiersze 23–27: przetwarzanie żądania [GET /do/list], które dotyczy listy osób.
- wiersze 28–32: przetwarzanie żądania [GET /do/delete], które dotyczy usunięcia osoby.
- wiersze 33–37: przetwarzanie zapytania [GET /do/edit], które dotyczy formularza aktualizacji danych osoby.
- wiersze 38–42: przetwarzanie żądania [POST /do/validate], które dotyczy zatwierdzenia zaktualizowanych danych osoby.
- wiersz 44: jeśli żądana akcja nie jest jedną z pięciu poprzednich, to traktuje się ją tak, jakby była to akcja [GET /do/list].
Metoda [doListPersonnes]
Metoda ta przetwarza żądanie [GET /do/list], które dotyczy listy osób:

Jej kod wygląda następująco:
// wyświetlenie listy osób
private void doListPersonnes(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// szablon widoku [list]
request.setAttribute("personnes", service.getAll());
// wyświetlanie widoku [list]
getServletContext().getRequestDispatcher((String) params.get("urlList")).forward(request, response);
}
- wiersz 4: żądamy od warstwy [service] listy osób z grupy i umieszczamy ją w modelu pod kluczem „osoby”.
- wiersz 6: wyświetla się widok [list.jsp] opisany w paragrafie 3.4.3.2.
Metoda [doDeletePersonne]
Ta metoda obsługuje zapytanie [GET /do/delete?id=XX], które żąda usunięcia osoby o identyfikatorze id=XX. Adres URL [/do/delete?id=XX] jest adresem linków [Supprimer] w widoku [list.jsp]:

którego kod wygląda następująco:
...
<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>
W wierszu 12 widoczny jest adres URL [/do/delete?id=XX] linku [Supprimer]. Metoda [doDeletePersonne], która ma przetwarzać ten adres URL, musi usunąć osobę o identyfikatorze id=XX, a następnie wyświetlić nową listę osób z grupy. Jej kod jest następujący:
// usunięcie osoby
private void doDeletePersonne(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// pobieranie identyfikatora osoby
int id = Integer.parseInt(request.getParameter("id"));
// usuwanie osoby
service.deleteOne(id);
// przekierowanie do listy osób
response.sendRedirect("list");
}
- wiersz 4: przetwarzany adres URL ma postać [/do/delete?id=XX]. Pobieramy wartość [XX] z parametru [id].
- wiersz 6: wysyłamy do warstwy [service] żądanie usunięcia osoby o uzyskanym identyfikatorze. Nie przeprowadzamy żadnej weryfikacji. Jeśli osoba, którą próbujemy usunąć, nie istnieje, warstwa [dao] zgłasza wyjątek, który jest przekazywany dalej przez warstwę [service]. Nie obsługujemy tego również tutaj, w kontrolerze. Wyjątek zostanie zatem przekazany dalej do serwera WWW, który zgodnie z konfiguracją wyświetli stronę [exception.jsp], opisaną w paragrafie 3.4.3.2:

- wiersz 9: jeśli usunięcie doszło do skutku (nie wystąpił wyjątek), klient jest przekierowywany do względnego adresu URL [list]. Ponieważ właśnie przetworzono stronę [/do/delete], adres URL przekierowania będzie brzmiał [/do/list]. Przeglądarka zostanie zatem przekierowana do strony [GET /do/list], co spowoduje wyświetlenie listy osób.
Metoda [doEditPersonne]
Ta metoda obsługuje żądanie [GET /do/edit?id=XX], które dotyczy formularza aktualizacji osoby o identyfikatorze id=XX. Adres URL [/do/edit?id=XX] jest adresem linków [Modifier] oraz linku [Ajout] w widoku [list.jsp]:

którego kod jest następujący:
...
<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>
W wierszu 11 widoczny jest adres URL [/do/edit?id=XX] linku [Modifier], a w wierszu 17 adres URL [/do/edit?id=-1] linku [Ajout]. Metoda [doEditPersonne] powinna wyświetlić formularz edycji osoby o identyfikatorze id=XX lub, w przypadku dodawania nowej osoby, wyświetlić pusty formularz.
![]() |
- W przypadku powyższej metody [1] jest to formularz dodawania, a w przypadku metody [2] – formularz edycji.
Kod metody [doEditPersonne] jest następujący:
// modyfikacja / dodanie osoby
private void doEditPersonne(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// pobieranie identyfikatora osoby
int id = Integer.parseInt(request.getParameter("id"));
// dodanie czy zmiana?
Personne personne = null;
if (id != -1) {
// modyfikacja – pobieranie osoby do modyfikacji
personne = service.getOne(id);
request.setAttribute("id", personne.getId());
request.setAttribute("version", personne.getVersion());
} else {
// dodanie – tworzy się pusty rekord osoby
personne = new Personne();
request.setAttribute("id", -1);
request.setAttribute("version", -1);
}
// umieszczamy obiekt [Personne] w sesji użytkownika
request.getSession().setAttribute("personne", personne);
// oraz do szablonu widoku [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());
// wyświetlenie widoku [edit]
getServletContext().getRequestDispatcher((String) params.get("urlEdit")).forward(request, response);
}
- GET ma jako cel adres URL typu [/do/edit?id=XX]. W wierszu 4 pobieramy wartość [id]. Następnie występują dwa przypadki:
- id jest różne od -1. W takim przypadku mamy do czynienia z modyfikacją i należy wyświetlić formularz wstępnie wypełniony danymi osoby, której dane mają zostać zmodyfikowane. W wierszu 9 dana osoba jest pobierana z warstwy [service].
- id jest równe -1. W takim przypadku mamy do czynienia z dodaniem i należy wyświetlić pusty formularz. W tym celu w wierszu 14 tworzona jest pusta osoba.
- W obu przypadkach inicjowane są elementy [id, version] szablonu strony [edit.jsp] opisanego w paragrafie 3.4.3.2.
- Uzyskany obiekt [Personne] jest umieszczany w szablonie strony [edit.jsp]. Szablon ten zawiera następujące elementy: [erreurEdit, id, version, prenom, erreurPrenom, nom, erreurNom, datenaissance, erreurDateNaissance, marie, nbenfants, erreurNbEnfants]. Elementy te są inicjowane w wierszach 19–31, z wyjątkiem tych, których wartością jest pusty ciąg znaków [erreurPrenom, erreurNom, erreurDateNaissance, erreurNbEnfants]. Wiadomo, że w przypadku ich braku w szablonie biblioteka JSTL wyświetli pusty ciąg znaków jako ich wartość. Chociaż element [erreurEdit] również ma za wartość pusty ciąg znaków, jest on jednak inicjowany, ponieważ na stronie [edit.jsp] przeprowadzany jest test jego wartości.
- Gdy szablon jest gotowy, kontrola przechodzi do strony [edit.jsp], wiersz 33, która wygeneruje widok [edit].
Metoda [doValidatePersonne]
Ta metoda obsługuje żądanie [POST /do/validate], które waliduje formularz aktualizacji. Żądanie POST jest wywoływane przez przycisk [Valider]:

Przypomnijmy elementy wprowadzania danych w formularzu HTML z powyższego widoku:
<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>
Zapytanie POST zawiera parametry [prenom, nom, datenaissance, marie, nbenfants, id] i jest wysyłane na adres URL [/do/validate] (wiersz 1). Jest ono przetwarzane przez następującą metodę [doValidatePersonne]:
// zatwierdzenie zmiany / dodania osoby
public void doValidatePersonne(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// pobieranie zapisanych elementów
boolean formulaireErroné = false;
boolean erreur;
// imię
String prenom = request.getParameter("prenom").trim();
// czy imię jest prawidłowe?
if (prenom.length() == 0) {
// odnotowuje się błąd
request.setAttribute("erreurPrenom", "Le prénom est obligatoire");
formulaireErroné = true;
}
// nazwisko
String nom = request.getParameter("nom").trim();
// czy imię jest prawidłowe?
if (nom.length() == 0) {
// odnotowano błąd
request.setAttribute("erreurNom", "Le nom est obligatoire");
formulaireErroné = true;
}
// data urodzenia
Date datenaissance = null;
try {
datenaissance = new SimpleDateFormat("dd/MM/yyyy").parse(request.getParameter("datenaissance").trim());
} catch (ParseException e) {
// odnotowano błąd
request.setAttribute("erreurDateNaissance", "Date incorrecte");
formulaireErroné = true;
}
// stan cywilny
boolean marie = Boolean.parseBoolean(request.getParameter("marie").trim());
// liczba dzieci
int nbenfants = 0;
erreur = false;
try {
nbenfants = Integer.parseInt(request.getParameter("nbenfants").trim());
if (nbenfants < 0) {
erreur = true;
}
} catch (NumberFormatException ex) {
// odnotowano błąd
erreur = true;
}
// błędna liczba dzieci?
if (erreur) {
// zgłaszamy błąd
request.setAttribute("erreurNbEnfants", "Nombre d'enfants incorrect");
formulaireErroné = true;
}
// identyfikator osoby
int id = Integer.parseInt(request.getParameter("id"));
// czy formularz zawiera błędy?
if (formulaireErroné) {
// formularz jest wyświetlany ponownie wraz z komunikatami o błędach
showFormulaire(request, response, "");
// zakończono
return;
}
// formularz jest poprawny – aktualizujemy dane osoby zapisane w sesji
// o informacje przesłane przez klienta
Personne personne = (Personne)request.getSession().getAttribute("personne");
personne.setDatenaissance(datenaissance);
personne.setMarie(marie);
personne.setNbenfants(nbenfants);
personne.setNom(nom);
personne.setPrenom(prenom);
// trwałość
try {
if (id == -1) {
// utworzenie
service.saveOne(personne);
} else {
// aktualizacja
service.updateOne(personne);
}
} catch (DaoException ex) {
// ponowne wyświetlenie formularza wraz z komunikatem o wystąpieniu błędu
showFormulaire(request, response, ex.getMessage());
// zakończono
return;
}
// przekierowanie do listy osób
response.sendRedirect("list");
}
// wyświetlenie wstępnie wypełnionego formularza
private void showFormulaire(HttpServletRequest request, HttpServletResponse response, String erreurEdit) throws ServletException, IOException {
// przygotowuje się szablon widoku [edit]
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());
// wyświetlenie widoku [edit]
getServletContext().getRequestDispatcher((String) params.get("urlEdit")).forward(request, response);
}
- wiersze 7–13: pobierany jest parametr [prenom] z żądania POST i sprawdzana jest jego poprawność. Jeśli okaże się nieprawidłowy, element [erreurPrenom] jest inicjowany komunikatem o błędzie i umieszczany w atrybutach zapytania.
- wiersze 15–21: podobną procedurę stosuje się w przypadku parametru [nom]
- wiersze 23–30: podobną procedurę stosuje się w przypadku parametru [datenaissance]
- wiersz 32: pobierany jest parametr [marie]. Nie sprawdzamy jego poprawności, ponieważ a priori pochodzi on z wartości przycisku opcji. Nie oznacza to jednak, że program nie może wygenerować parametru [POST /.../do/validate] wraz z fikcyjnym parametrem [marie]. Powinniśmy zatem sprawdzić poprawność tego parametru. W tym przypadku polegamy na naszym mechanizmie obsługi wyjątków, który powoduje wyświetlenie strony o wartości [exception.jsp], jeśli sam kontroler nie obsługuje tych wyjątków. Jeśli więc konwersja parametru [marie] na wartość logiczną zakończy się niepowodzeniem w wierszu 32, zostanie zgłoszony wyjątek, który spowoduje wysłanie strony [exception.jsp] do klienta. Takie działanie nam odpowiada.
- wiersze 34–50: pobieramy parametr [nbenfants] i sprawdzamy jego wartość.
- wiersz 52: pobieramy parametr [id] bez sprawdzania jego wartości
- wiersze 54–59: jeśli formularz zawiera błędy, jest on ponownie wyświetlany wraz z wcześniej wygenerowanymi komunikatami o błędach
- wiersze 62–67: jeśli formularz jest poprawny, tworzony jest nowy obiekt [Personne] z elementami formularza
- wiersze 69–82: dane osoby są zapisywane. Zapisanie może się nie powieść. W środowisku wieloużytkownikowym osoba, której dane mają zostać zmodyfikowane, mogła zostać usunięta lub już została zmodyfikowana przez kogoś innego. W takim przypadku warstwa [dao] zgłosi wyjątek, który jest tutaj obsługiwany.
- wiersz 84: jeśli nie wystąpił wyjątek, przekierowujemy klienta na adres URL [/do/list], aby wyświetlić mu nowy stan grupy.
- wiersz 79: jeśli podczas zapisywania wystąpił wyjątek, ponownie wyświetlamy początkowy formularz, przekazując mu komunikat o błędzie wyjątku (trzeci parametr).
Metoda [showFormulaire] (wiersze 88–97) tworzy szablon niezbędny dla strony [edit.jsp] na podstawie wprowadzonych wartości (request.getParameter(" ... ")). Należy pamiętać, że komunikaty o błędach zostały już umieszczone w szablonie przez metodę [doValidatePersonne]. Strona [edit.jsp] jest wyświetlana w wierszu 99.
3.4.4. Testy aplikacji internetowej
W punkcie 3.4.1 przedstawiono szereg testów. Zachęcamy czytelnika do ich powtórzenia. Poniżej przedstawiamy kolejne zrzuty ekranu ilustrujące przypadki konfliktów dostępu do danych w środowisku wieloużytkownikowym:
[Firefox] będzie przeglądarką użytkownika U1. Użytkownik ten wysyła żądanie na adres URL [http://localhost:8080/spring-jpa-hibernate-personnes-crud/do/list]:

[IE7] to przeglądarka użytkownika U2. Użytkownik ten wysyła żądanie na ten sam adres URL:

Użytkownik U1 przechodzi do edycji danych osoby [p2]:

Użytkownik U2 postępuje analogicznie:

Użytkownik U1 wprowadza zmiany i zatwierdza:
![]() |
Użytkownik U2 postępuje tak samo:
![]() |
Użytkownik U2 wraca do listy osób, korzystając z linku [Retour à la liste] w formularzu:

Znajduje osobę o identyfikatorze [Lemarchand] w wersji zmodyfikowanej przez użytkownika U1 (zamężna, dwoje dzieci). Numer wersji p2 uległ zmianie. Teraz U2 usuwa [p2]:
![]() |
U1 nadal posiada własną listę i chce ponownie zmodyfikować [p2]:
![]() |
U1 korzysta z linku [Retour à la liste], aby sprawdzić, o co chodzi:

Odkrywa, że rzeczywiście plik [p2] nie znajduje się już na liście...
3.4.5. Wersja 2
Nieznacznie modyfikujemy poprzednią wersję, aby wykorzystać archiwa warstw [service, dao, jpa] zamiast ich kodów źródłowych:
![]() |
- w [1]: nowy projekt Eclipse. Warto zauważyć, że zniknęły pakiety [service, dao, entites]. Zostały one zawarte w archiwum [service-dao-jpa-personne.jar] [2] umieszczonym w [WEB-INF/lib].
- Folder projektu znajduje się w [4]. Należy go zaimportować.
Nie trzeba robić nic więcej. Po uruchomieniu nowej aplikacji internetowej i wywołaniu listy osób otrzymujemy następującą odpowiedź:
![]() |
Hibernate nie znajduje encji [Personne]. Aby rozwiązać ten problem, należy jawnie zadeklarować w encji [persistence.xml] zarządzane encje:
<?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>
- wiersz 7: zadeklarowano encję Personne.
Po wykonaniu tej czynności wyjątek znika:
![]() |
3.4.6. Zmiana implementacji JPA
![]() |
- na [1]: nowy projekt Eclipse
- na [2]: biblioteki Toplink zastąpiły biblioteki Hibernate
- folder projektu to [4]. Zostanie on zaimportowany.
Zmiana implementacji JPA wiąże się jedynie z kilkoma zmianami w pliku [spring-config.xml]. Nic więcej się nie zmienia. Zmiany wprowadzone w pliku [spring-config.xml] zostały wyjaśnione w punkcie 3.1.9:
<?xml version="1.0" encoding="UTF-8"?>
<!-- JVM należy uruchomić z argumentem -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>
Aby przejść z Hibernate na Toplink, należy zmienić tylko kilka wierszy:
- wiersz 11: implementacja JPA jest teraz realizowana przez Toplink
- wiersz 13: właściwość [databasePlatform] ma inną wartość niż w przypadku Hibernate: nazwę klasy specyficznej dla Toplink. Gdzie znaleźć tę nazwę, wyjaśniono w paragrafie 2.1.15.2.
To wszystko. Warto zwrócić uwagę na łatwość, z jaką można zmienić SGBD lub implementację JPA przy użyciu Springa. Nie jesteśmy jednak jeszcze do końca gotowi. Podczas uruchamiania aplikacji pojawia się wyjątek:
![]() |
Jest to problem, który pojawił się już wcześniej i został opisany w punkcie 3.1.9. Rozwiązuje się go poprzez uruchomienie JVM za pomocą agenta Spring. W tym celu modyfikujemy konfigurację uruchamiania serwera Tomcat:
![]() |
- na [1]: wybrano opcję [Run / Run...] w celu zmiany konfiguracji serwera Tomcat
- na [2]: wybrano zakładkę [Arguments]
- w [3]: dodano parametr -javaagent zgodnie z opisem w punkcie 3.1.9.
Po wykonaniu tych czynności można wygenerować listę osób:

3.5. Inne przykłady
Chcielibyśmy pokazać przykład aplikacji internetowej, w której kontener Spring został zastąpiony kontenerem Jboss Ejb3 omówionym w punkcie 3.2:
![]() |
- w [1]: projekt Eclipse
- w [3]: jego lokalizacja w folderze z przykładami. Zaimportujemy go.
Wykorzystaliśmy konfigurację [jboss-config.xml, persistence.xml] opisaną w punkcie 3.2, a następnie zmodyfikowaliśmy metodę [init] kontrolera [Application.java] w następujący sposób:
// init
@SuppressWarnings("unchecked")
public void init() throws ServletException {
try {
// pobieramy parametry inicjalizacyjne serwletu
ServletConfig config = getServletConfig();
// przetwarzamy pozostałe parametry inicjalizacyjne
String valeur = null;
for (int i = 0; i < paramètres.length; i++) {
// wartość parametru
valeur = config.getInitParameter(paramètres[i]);
// czy parametr istnieje?
if (valeur == null) {
// odnotowuje się błąd
erreursInitialisation.add("Le paramètre [" + paramètres[i] + "] n'a pas été initialisé");
} else {
// zapisuje się wartość parametru
params.put(paramètres[i], valeur);
}
}
// adres URL widoku [erreurs] jest przetwarzany w sposób szczególny
urlErreurs = config.getInitParameter("urlErreurs");
if (urlErreurs == null)
throw new ServletException("Le paramètre [urlErreurs] n'a pas été initialisé");
// konfiguracja aplikacji
// uruchamia się kontener EJB3 JBoss
// wykorzystywane są pliki konfiguracyjne ejb3-interceptors-aop.xml oraz embedded-jboss-beans.xml
EJB3StandaloneBootstrap.boot(null);
// Tworzenie fasoli specyficznych dla aplikacji
EJB3StandaloneBootstrap.deployXmlResource("META-INF/jboss-config.xml");
// Wdrażane są wszystkie pliki EJB znalezione w ścieżce klas aplikacji
//EJB3StandaloneBootstrap.scanClasspath("WEB-INF/classes".replace("/", File.separator));
EJB3StandaloneBootstrap.scanClasspath();
// Inicjujemy kontekst JNDI. Wykorzystywany jest plik jndi.properties
InitialContext initialContext = new InitialContext();
// instancjonowanie warstwy usług
service = (IService) initialContext.lookup("Service/local");
// Opróżnianie bazy
clean();
// wypełnianie bazy
fill();
} catch (Exception e) {
throw new ServletException(e);
}
}
- wiersze 28–38: uruchamia się kontener Ejb3. Zastępuje on kontener Spring.
- wiersz 41: pobieramy odwołanie do warstwy [service] aplikacji.
W zasadzie są to jedyne zmiany, które należy wprowadzić. Podczas wykonywania pojawia się następujący błąd:
![]() |
Nie udało mi się ustalić, gdzie dokładnie leży problem. Wyjątek zgłoszony przez Tomcat wydaje się sugerować, że obiekt o nazwie „TransactionManager” został zażądany od serwisu JNDI, a ten go nie rozpoznał. Rozwiązanie tego problemu pozostawiam czytelnikom. Jeśli zostanie znalezione rozwiązanie, zostanie ono uwzględnione w dokumencie.





























































