3. JPA in un’architettura a più livelli
Per studiare l’API JPA, abbiamo utilizzato la seguente architettura di test:
![]() |
I nostri programmi di test erano applicazioni da console che interrogavano direttamente il livello JPA. In questa occasione abbiamo scoperto i principali metodi del livello JPA. Ci trovavamo in un ambiente denominato “Java SE” (Standard Edition). JPA funziona sia in un ambiente Java SE che in uno Java EE5 (Enterprise Edition).
Ora che abbiamo acquisito una certa padronanza sia della configurazione del bridge relazionale/oggetto sia dell’utilizzo dei metodi del livello JPA, torniamo a un’architettura multistrato più classica:
![]() |
Si accederà al livello [JPA] tramite un’architettura a due livelli [metier] e [dao]. Il framework Spring [7], seguito dal contenitore EJB3 di JBoss e [8], verrà utilizzato per collegare questi livelli tra loro.
Abbiamo detto in precedenza che JPA era disponibile negli ambienti SE e EE5. L'ambiente Java EE5 fornisce numerosi servizi nell'ambito dell'accesso ai dati persistenti, in particolare i pool di connessione, i gestori di transazioni, ecc. Per uno sviluppatore può essere interessante avvalersi di questi servizi. L’ambiente Java EE5 non è ancora molto diffuso (maggio 2007). Attualmente è disponibile sul server applicativo Sun Application Server 9.x (Glassfish). Un server applicativo è essenzialmente un server di applicazioni web. Se si sviluppa un’applicazione grafica autonoma di tipo Swing, non è possibile disporre dell’ambiente EE e dei servizi che esso offre. Questo rappresenta un problema. Si stanno iniziando a vedere ambienti EE “stand-alone”, c.a.d. utilizzabili al di fuori di un server di applicazioni. È il caso di JBoss e EJB3, che utilizzeremo in questo documento.
In un ambiente EE5, i livelli sono implementati da oggetti denominati EJB (Enterprise Java Bean). Nelle versioni precedenti di EE, i EJB (EJB e 2.x) erano considerati difficili da implementare, da testare e talvolta poco performanti. Si distinguono i EJB2.x "entity" e i EJB2.x "session". In breve, un EJB2.x "entity" rappresenta una riga di una tabella di database, mentre un EJB2.x "session" è un oggetto utilizzato per implementare i livelli [metier], [dao] di un’architettura multistrato. Una delle principali critiche mosse ai livelli implementati con EJB è che sono utilizzabili solo all’interno di contenitori EJB, un servizio fornito dall’ambiente EE. Ciò rende problematici i test unitari. Pertanto, nello schema sopra riportato, i test unitari dei livelli [metier] e [dao], costruiti con EJB, richiederebbero l’installazione di un server applicativo, un’operazione piuttosto onerosa che non incoraggia particolarmente lo sviluppatore a eseguire test con frequenza.
Il framework Spring è nato in risposta alla complessità dei EJB2. Spring fornisce, in un ambiente SE, un numero significativo dei servizi solitamente forniti dagli ambienti EE. Pertanto, nell’ambito della “persistenza dei dati” che ci interessa in questa sede, Spring fornisce i pool di connessione e i gestori di transazioni necessari alle applicazioni. L’emergere di Spring ha favorito la diffusione dei test unitari, che sono diventati improvvisamente molto più facili da implementare. Spring consente l’implementazione dei livelli di un’applicazione tramite oggetti Java classici (POJO, Plain Old/Ordinary Java Object), permettendo il loro riutilizzo in un altro contesto. Infine, integra numerosi strumenti di terze parti in modo abbastanza trasparente, in particolare strumenti di persistenza come Hibernate, Ibatis, ...
Java EE5 è stato progettato per colmare le lacune della precedente specifica EE. I EJB e 2.x sono diventati EJB3. Si tratta di POJOs contrassegnati da annotazioni che li rendono oggetti particolari quando si trovano all’interno di un contenitore EJB3. All’interno di quest’ultimo, l’EJB3 potrà usufruire dei servizi del contenitore (pool di connessioni, gestore delle transazioni, ecc.). Al di fuori del contenitore EJB3, l’oggetto EJB3 diventa un normale oggetto Java. Le sue annotazioni EJB vengono ignorate.
Quanto sopra illustra Spring e JBoss EJB3 come possibile infrastruttura (framework) della nostra architettura multistrato. È questa infrastruttura che fornirà i servizi di cui abbiamo bisogno: un pool di connessioni e un gestore di transazioni.
- Con Spring, i livelli saranno implementati tramite POJOs. Questi avranno accesso ai servizi di Spring (pool di connessioni, gestore delle transazioni) tramite l’iniezione di dipendenze in tali POJOs: durante la loro creazione, Spring inietta in essi i riferimenti ai servizi di cui avranno bisogno.
- JBoss EJB3 è un contenitore EJB in grado di funzionare al di fuori di un server applicativo. Il suo principio di funzionamento (dal punto di vista dello sviluppatore) è analogo a quello descritto per Spring. Troveremo poche differenze.
Concluderemo il documento con un esempio di applicazione web a tre livelli, semplice ma comunque rappresentativo:
![]() |
3.1. Esempio 1: Spring / JPA con l’entità Persona
Prendiamo l’entità Personne esaminata nel paragrafo 2.1 e la integriamo in un’architettura a più livelli in cui l’integrazione dei livelli avviene tramite Spring e il livello di persistenza è implementato da Hibernate.
![]() |
Si presume che il lettore abbia conoscenze di base su Spring. In caso contrario, è possibile consultare il seguente documento che spiega il concetto di iniezione delle dipendenze, elemento centrale di Spring:
[ref3]: Spring IoC (Inversion Of Control) [http://tahe.developpez.com/java/springioc].
3.1.1. Il progetto Eclipse / Spring / Hibernate
Il progetto Eclipse è il seguente:
![]() |
![]() |
- in [1]: il progetto Eclipse. Si trova in [6] negli esempi del tutorial [5]. Lo importeremo.
- in [2]: i codici Java dei livelli presentati in pacchetti:
- [entites]: il pacchetto delle entità JPA
- [dao]: il livello di accesso ai dati - si basa sul livello JPA
- [service]: un livello di servizi più che di business. Qui verrà utilizzato il servizio di transazioni dei container.
- [tests]: raggruppa i programmi di test.
- in [3]: la libreria [jpa-spring] raggruppa i file JAR necessari a Spring (vedi anche [7] e [8]).
- in [4]: la cartella [conf] raccoglie i file di configurazione di Spring per ciascuno dei SGBD utilizzati in questo tutorial.
3.1.2. Le entità JPA
![]() |
Qui viene gestita una sola entità, ovvero l’entità Personne analizzata nel paragrafo 2.1, di cui riportiamo di seguito la configurazione:
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;
// costruttori
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());
}
// getters e setters
...
}
3.1.3. Il livello [dao]
![]() | ![]() |
Il livello [dao] presenta la seguente interfaccia IDao:
package dao;
import java.util.List;
import entites.Personne;
public interface IDao {
// recuperare una persona tramite il suo identificativo
public Personne getOne(Integer id);
// recupera tutte le persone
public List<Personne> getAll();
// salvare una persona
public Personne saveOne(Personne personne);
// aggiornare una persona
public Personne updateOne(Personne personne);
// eliminare una persona tramite il suo ID
public void deleteOne(Integer id);
// recuperare le persone il cui nome corrisponde a un modello
public List<Personne> getAllLike(String modele);
}
L'implementazione [Dao] di questa interfaccia è la seguente:
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;
// eliminare una persona tramite il suo identificativo
public void deleteOne(Integer id) {
Personne personne = em.find(Personne.class, id);
if (personne == null) {
throw new DaoException(2);
}
em.remove(personne);
}
@SuppressWarnings("unchecked")
// recuperare tutte le persone
public List<Personne> getAll() {
return em.createQuery("select p from Personne p").getResultList();
}
@SuppressWarnings("unchecked")
// recuperare le persone il cui nome corrisponde a un modello
public List<Personne> getAllLike(String modele) {
return em.createQuery("select p from Personne p where p.nom like :modele")
.setParameter("modele", modele).getResultList();
}
// recuperare una persona tramite il suo identificativo
public Personne getOne(Integer id) {
return em.find(Personne.class, id);
}
// salvare una persona
public Personne saveOne(Personne personne) {
em.persist(personne);
return personne;
}
// aggiornare una persona
public Personne updateOne(Personne personne) {
return em.merge(personne);
}
}
- innanzitutto, si noti la semplicità dell’implementazione [Dao]. Ciò è dovuto all’utilizzo del livello JPA, che svolge la maggior parte del lavoro di accesso ai dati.
- riga 10: la classe [Dao] implementa l’interfaccia [IDao]
- riga 13: l'oggetto di tipo [EntityManager] che verrà utilizzato per gestire il contesto di persistenza JPA. Per convenzione, a volte lo confonderemo con il contesto di persistenza stesso. Il contesto di persistenza conterrà entità Personne.
- riga 12: in nessuna parte del codice il campo [EntityManager em] viene inizializzato. Verrà inizializzato all’avvio dell’applicazione da Spring. È l’annotazione JPA @PersistenceContext alla riga 12 che richiede a Spring di iniettare in em un gestore del contesto di persistenza.
- righe 26-28: l'elenco di tutte le persone viene ottenuto tramite una query JPQL.
- righe 32-35: l'elenco di tutte le persone il cui nome corrisponde a un determinato modello viene ottenuto tramite una query JPQL.
- righe 38-40: la persona con un determinato identificativo viene recuperata tramite il metodo find di API JPA. Restituisce un puntatore null se la persona non esiste.
- righe 43-46: una persona viene resa persistente tramite il metodo `persist` di API JPA. Il metodo rende la persona persistente.
- righe 49-51: l'aggiornamento di una persona viene effettuato tramite il metodo `merge` di `API JPA`. Questo metodo ha senso solo se la persona così aggiornata era precedentemente non persistente. Il metodo rende persistente la persona così creata.
- righe 16-22: l’eliminazione della persona il cui identificativo ci viene passato come parametro avviene in due fasi:
- riga 17: viene cercata nel contesto di persistenza
- righe 18-20: se non viene trovata, viene generata un'eccezione con codice di errore 2
- riga 21: se la persona viene trovata, viene rimossa dal contesto di persistenza tramite il metodo remove di API JPA.
- Ciò che al momento non è visibile è che ogni metodo verrà eseguito all’interno di una transazione avviata dal livello [service].
L'applicazione dispone di un proprio tipo di eccezione denominato [DaoException]:
package dao;
@SuppressWarnings("serial")
public class DaoException extends RuntimeException {
// codice di errore
private int code;
public DaoException(int code) {
super();
this.code = code;
}
public DaoException(String message, int code) {
super(message);
this.code = code;
}
public DaoException(Throwable cause, int code) {
super(cause);
this.code = code;
}
public DaoException(String message, Throwable cause, int code) {
super(message, cause);
this.code = code;
}
// getter e setter
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
}
- riga 4: [DaoException] deriva da [RuntimeException]. Si tratta quindi di un tipo di eccezione che il compilatore non ci obbliga a gestire tramite un try/catch né a includere nella firma dei metodi. È per questo motivo che [DaoException] non è presente nella firma del metodo [deleteOne] dell’interfaccia [IDao]. Ciò consente a questa interfaccia di essere implementata da una classe che genera un altro tipo di eccezioni, a condizione che anch’essa derivi da [RuntimeException].
- Per distinguere gli errori che possono verificarsi, si utilizza il codice di errore della riga 7. I tre costruttori delle righe 14, 19 e 24 sono quelli della classe padre [RuntimeException], ai quali è stato aggiunto un parametro: quello del codice di errore che si desidera assegnare all’eccezione.
3.1.4. Il livello [metier / service]
![]() |
Il livello [service] presenta la seguente interfaccia [IService]:
package service;
import java.util.List;
import entites.Personne;
public interface IService {
// recuperare una persona tramite il suo identificativo
public Personne getOne(Integer id);
// recuperare tutte le persone
public List<Personne> getAll();
// salvare una persona
public Personne saveOne(Personne personne);
// aggiornare un utente
public Personne updateOne(Personne personne);
// eliminare una persona tramite il suo identificativo
public void deleteOne(Integer id);
// recuperare le persone il cui nome corrisponde a un modello
public List<Personne> getAllLike(String modele);
// eliminare più persone contemporaneamente
public void deleteArray(Personne[] personnes);
// salvare più persone contemporaneamente
public Personne[] saveArray(Personne[] personnes);
// aggiornare più persone contemporaneamente
public Personne[] updateArray(Personne[] personnes);
}
- righe 8-24: l’interfaccia [IService] riprende i metodi dell’interfaccia [IDao]
- riga 27: il metodo [deleteArray] consente di eliminare un insieme di persone all’interno di una transazione: vengono eliminate tutte le persone oppure nessuna.
- righe 30 e 33: metodi analoghi a [deleteArray] per salvare (riga 30) o aggiornare (riga 33) un insieme di persone all’interno di una transazione.
L'implementazione [Service] dell'interfaccia [IService] è la seguente:
package service;
...
// tutti i metodi della classe vengono eseguiti all’interno di una transazione
@Transactional
public class Service implements IService {
// livello [dao]
private IDao dao;
public IDao getDao() {
return dao;
}
public void setDao(IDao dao) {
this.dao = dao;
}
// eliminare più persone contemporaneamente
public void deleteArray(Personne[] personnes) {
for (Personne p : personnes) {
dao.deleteOne(p.getId());
}
}
// eliminare una persona tramite il suo identificativo
public void deleteOne(Integer id) {
dao.deleteOne(id);
}
// recuperare tutte le persone
public List<Personne> getAll() {
return dao.getAll();
}
// recuperare le persone il cui nome corrisponde a un modello
public List<Personne> getAllLike(String modele) {
return dao.getAllLike(modele);
}
// recuperare una persona tramite il suo ID
public Personne getOne(Integer id) {
return dao.getOne(id);
}
// salvare più persone contemporaneamente
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;
}
// salvare una persona
public Personne saveOne(Personne personne) {
return dao.saveOne(personne);
}
// aggiornare più persone contemporaneamente
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;
}
// aggiornare un utente
public Personne updateOne(Personne personne) {
return dao.updateOne(personne);
}
}
- riga 6: l'annotazione Spring @Transactional indica che tutti i metodi della classe devono essere eseguiti all'interno di una transazione. Una transazione verrà avviata prima dell'inizio dell'esecuzione del metodo e chiusa dopo l'esecuzione. Se durante l’esecuzione del metodo si verifica un’eccezione di tipo [RuntimeException] o derivata, un rollback automatico annulla l’intera transazione; in caso contrario, un commit automatico la convalida. È importante notare che il codice Java non deve occuparsi delle transazioni, poiché queste sono gestite da Spring.
- riga 10: un riferimento al livello [dao]. Vedremo in seguito che questo riferimento viene inizializzato da Spring all’avvio dell’applicazione.
- I metodi di [Service] si limitano a chiamare i metodi dell’interfaccia [IDao dao] della riga 10. Lasciamo al lettore il compito di esaminare il codice. Non vi sono particolari difficoltà.
- Abbiamo detto in precedenza che ogni metodo di [Service] viene eseguito all’interno di una transazione. Questa è associata al thread di esecuzione del metodo. In questo thread vengono eseguiti i metodi del livello [dao]. Questi saranno automaticamente associati alla transazione del thread di esecuzione. Il metodo [deleteArray] (riga 21), ad esempio, deve eseguire N volte il metodo [deleteOne] del livello [dao]. Queste N esecuzioni avverranno all’interno del thread di esecuzione del metodo [deleteArray], quindi all’interno della stessa transazione. Pertanto, saranno tutte confermate (commit) se tutto procede correttamente oppure tutte annullate (rollback) se si verifica un'eccezione in una delle N esecuzioni del metodo [deleteOne] del livello [dao].
3.1.5. Configurazione dei livelli
![]() | ![]() |
La configurazione dei livelli [service], [dao] e [JPA] è gestita dai due file sopra indicati: [META-INF/persistence.xml] e [spring-config.xml]. Entrambi i file devono trovarsi nella cartella classpath dell’applicazione, il che spiega perché si trovino nella cartella [src] del progetto Eclipse. Il nome del file [spring-config.xml] è a scelta.
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>
- riga 4: il file dichiara un'unità di persistenza denominata jpa che utilizza transazioni "locali", c.a.d, non fornite da un contenitore EJB3. Queste transazioni vengono create e gestite da Spring e sono oggetto di configurazioni nel file [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">
<!-- livelli applicativi -->
<bean id="dao" class="dao.Dao" />
<bean id="service" class="service.Service">
<property name="dao" ref="dao" />
</bean>
<!-- livello di persistenza 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>
<!-- la fonte dei dati 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>
<!-- il gestore delle transazioni -->
<tx:annotation-driven transaction-manager="txManager" />
<bean id="txManager"
class="org.springframework.orm.jpa.JpaTransactionManager">
<property name="entityManagerFactory"
ref="entityManagerFactory" />
</bean>
<!-- traduzione delle eccezioni -->
<bean
class="org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor" />
<!-- annotazioni di persistenza -->
<bean
class="org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor" />
</beans>
- righe 2-5: il tag radice <beans> del file di configurazione. Non commentiamo i vari attributi di questo tag. Si raccomanda di effettuare un copia/incolla perché un errore in uno di questi attributi provoca errori talvolta difficili da comprendere.
- riga 8: il bean "dao" è un riferimento a un'istanza della classe [dao.Dao]. Verrà creata un'unica istanza (singleton) che implementerà il livello [dao] dell'applicazione.
- righe 9-11: istanziazione del livello [service]. Il bean "service" è un riferimento a un'istanza della classe [service.Service]. Verrà creata un'unica istanza (singleton) che implementerà il livello [service] dell'applicazione. Abbiamo visto che la classe [service.Service] aveva un campo privato [IDao dao]. Questo campo viene inizializzato alla riga 10 dal bean "dao" definito alla riga 8.
- In definitiva, le righe 8-11 hanno configurato i livelli [dao] e [service]. Vedremo più avanti quando e come verranno istanziati.
- righe 35-42: viene definita una fonte di dati. Abbiamo già incontrato il concetto di fonte di dati durante lo studio delle entità JPA con Hibernate:
![]() |
Nell’esempio precedente, [c3p0], denominato «pool di connessioni», avrebbe potuto essere chiamato «fonte di dati». Una fonte di dati fornisce il servizio di «pool di connessioni». Con Spring, utilizzeremo una fonte di dati diversa da [c3p0]. Si tratta di [DBCP] del progetto Apache Commons DBCP [http://jakarta.apache.org/commons/dbcp/]. Gli archivi di [DBCP] sono stati inseriti nella libreria utente [jpa-spring]:
![]() |
- righe 38-41: per creare connessioni con il database di destinazione, la fonte dati deve conoscere il driver JDBC utilizzato (riga 38), l’URL del database (riga 39), l’utente della connessione e la relativa password (righe 40-41).
- righe 14-32: configurano il livello JPA
- righe 14-15: definiscono un bean di tipo [EntityManagerFactory] in grado di creare oggetti di tipo [EntityManager] per gestire i contesti di persistenza. La classe istanziata [LocalContainerEntityManagerFactoryBean] è fornita da Spring. Per essere istanziata, richiede una serie di parametri, definiti nelle righe 16-31.
- riga 16: la fonte dati da utilizzare per ottenere connessioni a SGBD. Si tratta della fonte [DBCP] definita alle righe 35-42.
- righe 17-27: l’implementazione JPA da utilizzare
- righe 18-26: definiscono Hibernate (riga 19) come implementazione JPA da utilizzare
- righe 23-24: il dialetto SQL che Hibernate deve utilizzare con il SGBD di destinazione, in questo caso MySQL5.
- riga 25: richiede che all’avvio dell’applicazione venga generato il database (drop e create).
- righe 28-31: definiscono un "caricatore di classi". Non saprei spiegare in modo chiaro il ruolo di questo bean utilizzato dal EntityManagerFactory del livello JPA. Resta il fatto che ciò comporta il passaggio al JVM, che esegue l’applicazione, del nome di un archivio il cui contenuto gestirà il caricamento delle classi all’avvio dell’applicazione. In questo caso, tale archivio è [spring-agent.jar], situato nella libreria utente [jpa-spring] (vedi sopra). Vedremo che Hibernate non necessita di questo agente, mentre Toplink ne ha bisogno.
- righe 45-50: definiscono il gestore delle transazioni da utilizzare
- riga 45: indica che le transazioni sono gestite tramite annotazioni Java (avrebbero potuto essere dichiarate anche in spring-config.xml). Si tratta in particolare dell'annotazione @Transactional presente nella classe [Service] (riga 6).
- righe 46-50: il gestore delle transazioni
- riga 47: il gestore delle transazioni è una classe fornita da Spring
- righe 48-49: il gestore delle transazioni di Spring deve conoscere la classe EntityManagerFactory che gestisce il livello JPA. Si tratta di quella definita alle righe 14-32.
- righe 57-58: definiscono la classe che gestisce le annotazioni di persistenza di Spring presenti nel codice Java, come l’annotazione @PersistenceContext della classe [dao.Dao] (riga 12).
- righe 53-54: definiscono la classe Spring che gestisce in particolare l’annotazione @Repository, la quale rende una classe così annotata idonea alla traduzione delle eccezioni native del driver Jdbc da SGBD in eccezioni generiche Spring di tipo [DataAccessException]. Questa conversione incapsula l’eccezione nativa JDBC in un tipo [DataAccessException] che presenta diverse sottoclassi:

Questa traduzione consente al programma client di gestire le eccezioni in modo generico indipendentemente dal SGBD di destinazione. Nel nostro codice Java non abbiamo utilizzato l’annotazione @Repository. Pertanto, le righe 53-54 sono superflue. Le abbiamo lasciate a scopo puramente informativo.
Abbiamo terminato con il file di configurazione di Spring. È complesso e molti aspetti rimangono poco chiari. È stato tratto dalla documentazione di Spring. Fortunatamente, il suo adattamento a diverse situazioni si riduce spesso a due modifiche:
- quella relativa al database di destinazione: righe 38-41. Forniremo un esempio relativo a Oracle.
- quella relativa all’implementazione JPA: righe 14-32. Forniremo un esempio con Toplink.
3.1.6. Programma client [InitDB]
Ci occupiamo ora della scrittura di un primo client dell’architettura descritta in precedenza:
![]() |
Il codice di [InitDB] è il seguente:
package tests;
...
public class InitDB {
// livello di servizio
private static IService service;
// costruttore
public static void main(String[] args) throws ParseException {
// configurazione dell'applicazione
ApplicationContext ctx = new ClassPathXmlApplicationContext("spring-config.xml");
// livello di servizio
service = (IService) ctx.getBean("service");
// si svuota il database
clean();
// riempimento del database
fill();
// si verifica visivamente
dumpPersonnes();
}
// visualizzazione del contenuto della tabella
private static void dumpPersonnes() {
System.out.format("[personnes]%n");
for (Personne p : service.getAll()) {
System.out.println(p);
}
}
// compilazione della tabella
public static void fill() throws ParseException {
// creazione persone
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);
// che si salva
service.saveArray(new Personne[] { p1, p2 });
}
// eliminazione elementi dalla tabella
public static void clean() {
for (Personne p : service.getAll()) {
service.deleteOne(p.getId());
}
}
}
- riga 12: il file [spring-config.xml] viene utilizzato per creare un oggetto [ApplicationContext ctx], che è un'immagine in memoria del file. I bean definiti in [spring-config.xml] vengono istanziati in questa occasione.
- riga 14: si richiede al contesto dell'applicazione ctx un riferimento al livello [service]. Si sa che quest'ultimo è rappresentato da un bean denominato "service".
- riga 16: il database viene svuotato tramite il metodo clean delle righe 41-45:
- righe 42-44: si richiede l’elenco di tutte le persone al contesto di persistenza e si esegue un ciclo su di esse per eliminarle una alla volta. Si ricorderà forse che [spring-config.xml] specifica che il database deve essere generato all’avvio dell’applicazione. Pertanto, nel nostro caso, la chiamata al metodo clean è superflua poiché si parte da un database vuoto.
- riga 18: il metodo fill popola il database. Questo è definito alle righe 32-38:
- righe 34-35: vengono create due persone
- riga 37: si richiede al livello [service] di renderle persistenti.
- riga 20: il metodo dumpPersonnes visualizza le persone persistenti. È definito alle righe 24-29
- righe 26-28: si richiede l'elenco di tutte le persone persistenti al livello [service] e le si visualizza sulla console.
L'esecuzione di [InitDB] fornisce il seguente risultato:
3.1.7. Test unitari [TestNG]
L'installazione del plugin [TestNG] è descritta nel paragrafo 5.2.4. Il codice del programma [TestNG] è il seguente:
package tests;
....
public class TestNG {
// livello di servizio
private IService service;
@BeforeClass
public void init() {
// log
log("init");
// configurazione dell'applicazione
ApplicationContext ctx = new ClassPathXmlApplicationContext("spring-config.xml");
// livello di servizio
service = (IService) ctx.getBean("service");
}
@BeforeMethod
public void setUp() throws ParseException {
// svuotamento del database
clean();
// riempimento del database
fill();
}
// log
private void log(String message) {
System.out.println("----------- " + message);
}
// visualizzazione contenuto tabella
private void dump() {
log("dump");
System.out.format("[personnes]%n");
for (Personne p : service.getAll()) {
System.out.println(p);
}
}
// riempimento tabella
public void fill() throws ParseException {
log("fill");
// creazione persone
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);
// che si salva
service.saveArray(new Personne[] { p1, p2 });
}
// eliminazione elementi della tabella
public void clean() {
log("clean");
for (Personne p : service.getAll()) {
service.deleteOne(p.getId());
}
}
@Test()
public void test01() {
...
}
...
}
- riga 9: l'annotazione @BeforeClass indica il metodo da eseguire per inizializzare la configurazione necessaria ai test. Viene eseguita prima che venga eseguito il primo test. L'annotazione @AfterClass, qui non utilizzata, indica il metodo da eseguire una volta che tutti i test sono stati completati.
- righe 10-17: il metodo init, annotato con @BeforeClass, utilizza il file di configurazione di Spring per istanziare i diversi livelli dell’applicazione e ottenere un riferimento al livello [service]. Tutti i test utilizzano poi questo riferimento.
- riga 19: l'annotazione @BeforeMethod indica il metodo da eseguire prima di ogni test. L'annotazione @AfterMethod, qui non utilizzata, indica il metodo da eseguire dopo ogni test.
- righe 20-25: il metodo setUp, annotato con @BeforeMethod, svuota il database (clean righe 52-56) e poi lo riempie con due persone (fill righe 42-49).
- riga 59: l'annotazione @Test indica un metodo di test da eseguire. Descriviamo ora questi test.
@Test()
public void test01() {
log("test1");
dump();
// elenco delle persone
List<Personne> personnes = service.getAll();
assert 2 == personnes.size();
}
@Test()
public void test02() {
log("test2");
// ricerca di persone in base al nome
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");
// creazione di una nuova persona
Personne p3 = new Personne("p3", "x", new SimpleDateFormat("dd/MM/yy").parse("05/07/2001"), false, 0);
// la si salva
service.saveOne(p3);
// richiesta del record
Personne loadedp3 = service.getOne(p3.getId());
// visualizzazione
System.out.println(loadedp3);
// verifica
assert "p3".equals(loadedp3.getNom());
}
- righe 2-8: il test 01. È importante ricordare che all’inizio di ogni test il database contiene due persone denominate rispettivamente p1 e p2.
- riga 6: si richiede l'elenco delle persone
- riga 7: si verifica che il numero di persone nell'elenco ottenuto sia 2
- riga 14: si richiede l'elenco delle persone il cui cognome inizia con p1
- si verifica che l'elenco ottenuto contenga un solo elemento (riga 15) e che il nome dell'unica persona ottenuta sia "Paul" (riga 17)
- riga 24: si crea una persona denominata p3
- riga 25: la si salva
- riga 28: la si richiede nuovamente al contesto di persistenza per verificarne la presenza
- riga 32: si verifica che la persona ottenuta abbia effettivamente il nome p3.
@Test()
public void test04() throws ParseException {
log("test4");
// caricamento della persona p1
List<Personne> personnes = service.getAllLike("p1%");
Personne p1 = personnes.get(0);
// la si visualizza
System.out.println(p1);
// si verifica
assert "p1".equals(p1.getNom());
int version1 = p1.getVersion();
// si modifica il nome
p1.setPrenom("x");
// si salva
service.updateOne(p1);
// si aggiorna
p1 = service.getOne(p1.getId());
// visualizzazione
System.out.println(p1);
// si verifica che la versione sia stata incrementata
assert (version1 + 1) == p1.getVersion();
}
- riga 5: si richiede la persona p1
- riga 10: si verifica il suo nome
- riga 11: si annota il suo numero di versione
- riga 13: si modifica il suo nome
- riga 15: si salva la modifica
- riga 17: si richiama la persona p1
- riga 21: si verifica che il numero di versione sia aumentato di 1
@Test()
public void test05() {
log("test5");
// si carica la persona p2
List<Personne> personnes = service.getAllLike("p2%");
Personne p2 = personnes.get(0);
// la si visualizza
System.out.println(p2);
// si verifica
assert "p2".equals(p2.getNom());
// si elimina la persona p2
service.deleteOne(p2.getId());
// la si ricarica
p2 = service.getOne(p2.getId());
// si verifica che sia stato ottenuto un puntatore nullo
assert null == p2;
// si visualizza la tabella
dump();
}
- riga 5: si richiede la persona p2
- riga 10: si verifica il suo nome
- riga 12: la si elimina
- riga 14: la si richiede nuovamente
- riga 16: si verifica che non sia stata trovata
@Test()
public void test06() throws ParseException {
log("test6");
// viene creato un record con due persone con lo stesso nome (violazione della regola di unicità del nome)
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)};
// si salva questa tabella - si dovrebbe ottenere un'eccezione e un rollback
boolean erreur = false;
try {
service.saveArray(personnes);
} catch (RuntimeException e) {
erreur = true;
}
// dump
dump();
// verifiche
assert erreur;
// ricerca di una persona con nome p3
List<Personne> personnesp3 = service.getAllLike("p3%");
assert 0 == personnesp3.size();
// dump
dump();
}
- riga 5: si crea un array di tre persone, due delle quali hanno lo stesso nome "p4". Ciò viola la regola di unicità del nome dell'@Entity Personne:
@Column(name = "NOM", length = 30, nullable = false, unique = true)
private String nom;
- riga 11: l'array delle tre persone viene inserito nel contesto di persistenza. L'aggiunta della seconda persona p4 dovrebbe fallire. Poiché il metodo [saveArray] si svolge all'interno di una transazione, tutti gli inserimenti effettuati in precedenza verranno annullati. Alla fine, non verrà effettuata alcuna aggiunta.
- riga 18: si verifica che [saveArray] abbia effettivamente generato un'eccezione
- righe 20-21: si verifica che la persona p3, che avrebbe potuto essere aggiunta, non lo sia stata.
@Test()
public void test07() {
log("test7");
// test del blocco ottimistico
// si carica la persona p1
List<Personne> personnes = service.getAllLike("p1%");
Personne p1 = personnes.get(0);
// la si visualizza
System.out.println(p1);
// si aumenta il numero dei suoi figli
int nbEnfants1 = p1.getNbenfants();
p1.setNbenfants(nbEnfants1 + 1);
// si salva p1
Personne newp1 = service.updateOne(p1);
assert (nbEnfants1 + 1) == newp1.getNbenfants();
System.out.println(newp1);
// si salva una seconda volta - dovrebbe verificarsi un'eccezione poiché p1 non ha più la versione corretta
// è newp1 che lo contiene
boolean erreur = false;
try {
service.updateOne(p1);
} catch (RuntimeException e) {
erreur = true;
}
// verifica
assert erreur;
// si aumenta il numero di figli di newp1
int nbEnfants2 = newp1.getNbenfants();
newp1.setNbenfants(nbEnfants2 + 1);
// si salva newp1
service.updateOne(newp1);
// si ricarica
p1 = service.getOne(p1.getId());
// si verifica
assert (nbEnfants1 + 2) == p1.getNbenfants();
System.out.println(p1);
}
- riga 6: si richiede la persona p1
- riga 12: si aumenta di 1 il numero dei suoi figli
- riga 14: si aggiorna la persona p1 nel contesto di persistenza. Il metodo [updateOne] rende persistente la nuova versione newp1 di p1. Essa differisce da p1 per il numero di versione, che deve essere stato incrementato.
- riga 15: si verifica il numero di figli di newp1.
- riga 21: si richiede nuovamente un aggiornamento della persona p1 a partire dalla versione precedente p1. Si dovrebbe verificare un'eccezione poiché p1 non è l'ultima versione della persona p1. Quest'ultima versione è newp1.
- riga 23: si verifica che l'errore si sia effettivamente verificato
- righe 27-35: si verifica che, se l’aggiornamento viene effettuato a partire dall’ultima versione newp1, tutto proceda correttamente.
@Test()
public void test08() {
log("test8");
// test di rollback su updateArray
// si carica la persona p1
List<Personne> personnes = service.getAllLike("p1%");
Personne p1 = personnes.get(0);
// visualizzazione
System.out.println(p1);
// si aumenta il numero dei suoi figli
int nbEnfants1 = p1.getNbenfants();
p1.setNbenfants(nbEnfants1 + 1);
// si salvano 2 modifiche, di cui la seconda deve fallire (persona inizializzata in modo errato)
// a causa della transazione, entrambe devono quindi essere annullate
boolean erreur = false;
try {
service.updateArray(new Personne[] { p1, new Personne() });
} catch (RuntimeException e) {
erreur = true;
}
// verifiche
assert erreur;
// si ricarica la persona p1
personnes = service.getAllLike("p1%");
p1 = personnes.get(0);
// il numero dei suoi figli non dovrebbe essere cambiato
assert nbEnfants1 == p1.getNbenfants();
}
- il test 8 è simile al test 6: verifica il rollback su un updateArray che opera su un array di due persone in cui la seconda non è stata inizializzata correttamente. Dal punto di vista di JPA, l’operazione di unione sulla seconda persona, che nonesiste già genererà un ordine SQL insert che fallirà a causa dei vincoli nullable=false presenti su alcuni campi dell’entità Personne.
@Test()
public void test09() {
log("test9");
// test di rollback su deleteArray
// dump
dump();
// si carica la persona p1
List<Personne> personnes = service.getAllLike("p1%");
Personne p1 = personnes.get(0);
// la si visualizza
System.out.println(p1);
// si effettuano 2 eliminazioni, di cui la seconda deve fallire (persona sconosciuta)
// a causa della transazione, entrambe devono quindi essere annullate
boolean erreur = false;
try {
service.deleteArray(new Personne[] { p1, new Personne() });
} catch (RuntimeException e) {
erreur = true;
}
// verifiche
assert erreur;
// si ricarica la persona p1
personnes = service.getAllLike("p1%");
// verifica
assert 1 == personnes.size();
// dump
dump();
}
- il test 9 è simile al precedente: verifica il rollback su un deleteArray che opera su un array di due persone in cui la seconda non esiste. In questo caso, però, il metodo [deleteOne] del livello [dao] genera un'eccezione.
// blocco ottimistico - accesso multithread
@Test()
public void test10() throws Exception {
// aggiunta di una persona
Personne p3 = new Personne("X", "X", new SimpleDateFormat("dd/MM/yyyy").parse("01/02/2006"), true, 0);
service.saveOne(p3);
int id3 = p3.getId();
// creazione di N thread per l'aggiornamento del numero di figli
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();
}
// si attende il completamento dei thread
for (int i = 0; i < taches.length; i++) {
taches[i].join();
}
// si recupera la persona
p3 = service.getOne(id3);
// deve avere N figli
assert N == p3.getNbenfants();
// eliminazione della persona p3
service.deleteOne(p3.getId());
// verifica
p3 = service.getOne(p3.getId());
// deve esserci un puntatore nullo
assert p3 == null;
}
- L'idea del test 10 è quella di avviare N thread (riga 9) per incrementare in parallelo il numero di figli di una persona. Si vuole verificare che il sistema del numero di versione resista correttamente a questo scenario. È stato creato proprio per questo.
- righe 5-6: viene creata una persona denominata p3, che viene poi salvata. All'inizio ha 0 figli.
- riga 7: si annota il suo identificativo.
- righe 9-14: vengono avviati N thread in parallelo, tutti incaricati di incrementare di 1 il numero di figli di p3.
- righe 16-18: si attende il completamento di tutti i thread
- riga 20: si richiede di vedere la persona p3
- riga 22: si verifica che ora abbia N figli
- riga 24: la persona p3 viene eliminata.
Il thread [ThreadMajEnfants] è il seguente:
package tests;
...
public class ThreadMajEnfants extends Thread {
// nome del thread
private String name;
// riferimento al livello [service]
private IService service;
// l'ID della persona su cui si lavorerà
private int idPersonne;
// costruttore
public ThreadMajEnfants(String name, IService service, int idPersonne) {
this.name = name;
this.service = service;
this.idPersonne = idPersonne;
}
// nucleo del thread
public void run() {
// monitoraggio
suivi("lancé");
// si ripete il ciclo finché non si riesce ad incrementare di 1
// il numero di figli della persona idPersonne
boolean fini = false;
int nbEnfants = 0;
while (!fini) {
// si recupera una copia della persona da idPersonne
Personne personne = service.getOne(idPersonne);
nbEnfants = personne.getNbenfants();
// segue
suivi("" + nbEnfants + " -> " + (nbEnfants + 1) + " pour la version " + personne.getVersion());
// il numero di figli della persona viene incrementato di 1
personne.setNbenfants(nbEnfants + 1);
// attesa di 10 ms per liberare il processore
try {
// seguito
suivi("début attente");
// si interrompe per lasciare spazio al processore
Thread.sleep(10);
// monitoraggio
suivi("fin attente");
} catch (Exception ex) {
throw new RuntimeException(ex.toString());
}
// attesa terminata - si tenta di convalidare la copia
// nel frattempo altri thread potrebbero aver modificato l'originale
try {
// si tenta di modificare l'originale
service.updateOne(personne);
// operazione completata - l'originale è stato modificato
fini = true;
} catch (javax.persistence.OptimisticLockException e) {
// versione dell'oggetto non corretta: si ignora l'eccezione per ricominciare
} catch (org.springframework.transaction.UnexpectedRollbackException e2) {
// eccezione Spring che si verifica di tanto in tanto
} catch (RuntimeException e3) {
// altro tipo di eccezione - la si segnala
throw e3;
}
}
// monitoraggio
suivi("a terminé et passé le nombre d'enfants à " + (nbEnfants + 1));
}
// monitoraggio
private void suivi(String message) {
System.out.println(name + " [" + new Date().getTime() + "] : " + message);
}
}
- righe 15-19: il costruttore memorizza le informazioni di cui ha bisogno per funzionare: il proprio nome (riga 16), il riferimento al livello [service] che deve utilizzare (riga 17) e l'identificativo della persona p di cui deve incrementare il numero di figli (riga 18).
- righe 22-66: il metodo [run] eseguito in parallelo da tutti i thread.
- riga 29: il thread tenta ripetutamente di incrementare il numero di figli della persona p. Si ferma solo quando ci riesce.
- riga 31: viene richiesta la persona p
- riga 36: il numero dei suoi figli viene incrementato in memoria
- righe 38-47: viene effettuata una pausa di 10 ms. Ciò consentirà ad altri thread di ottenere la stessa versione della persona p. Si avrà quindi, nello stesso momento, più thread in possesso della stessa versione della persona p e desiderosi di modificarla. Questo è proprio ciò che si vuole ottenere.
- riga 52: una volta terminata la pausa, il thread richiede al livello [service] di salvare la modifica. Sappiamo che di tanto in tanto si verificheranno delle eccezioni, pertanto abbiamo racchiuso l’operazione in un try/catch.
- riga 55: i test mostrano che si verificano eccezioni di tipo [javax.persistence.OptimisticLockException]. È normale: si tratta dell’eccezione generata dal livello JPA quando un thread tenta di modificare la persona p senza disporre della sua ultima versione. Questa eccezione viene ignorata per consentire al thread di riprovare l’operazione finché non riesce a portarla a termine.
- riga 57: i test mostrano che si verificano anche eccezioni di tipo [org.springframework.transaction.UnexpectedRollbackException]. È fastidioso e inaspettato. Non ho spiegazioni da fornire. Ci ritroviamo così dipendenti da Spring, mentre avremmo voluto evitarlo. Ciò significa che se si esegue la nostra applicazione in JBoss Ejb3, ad esempio, il codice del thread dovrà essere modificato. Anche in questo caso l’eccezione Spring viene ignorata per consentire al thread di riprovare l’operazione di incremento.
- riga 59: gli altri tipi di eccezione vengono segnalati all’applicazione.
Quando si esegue [TestNG] si ottengono i seguenti risultati:

I 10 test sono stati superati con successo.
Il test 10 merita ulteriori spiegazioni perché il fatto che sia andato a buon fine ha un che di magico. Torniamo innanzitutto alla configurazione del livello [dao]:
public class Dao implements IDao {
@PersistenceContext
private EntityManager em;
- riga 4: un oggetto [EntityManager] viene inserito nel campo em grazie all’annotazione JPA @PersistenceContext. Il livello [dao] viene istanziato una sola volta. Si tratta di un singleton utilizzato da tutti i thread che utilizzano il livello JPA. Pertanto, l’oggetto EntityManager `em` è comune a tutti i thread. È possibile verificarlo visualizzando il valore di em nel metodo [updateOne] utilizzato dai thread [ThreadMajEnfants]: si ottiene lo stesso valore per tutti i thread.
Di conseguenza, ci si può chiedere se gli oggetti persistenti dei diversi thread gestiti dall’em di EntityManager, che è lo stesso per tutti i thread, non finiscano per mescolarsi e creare conflitti tra loro. Un esempio di ciò che potrebbe accadere si trova in [ThreadMajEnfants]:
while (!fini) {
// si recupera una copia della persona da idPersonne
Personne personne = service.getOne(idPersonne);
nbEnfants = personne.getNbenfants();
// tracciamento
suivi("" + nbEnfants + " -> " + (nbEnfants + 1) + " pour la version " + personne.getVersion());
// incrementa di 1 il numero di figli della persona
personne.setNbenfants(nbEnfants + 1);
// attesa di 10 ms per liberare il processore
try {
// monitoraggio
suivi("début attente");
// si interrompe per lasciare spazio al processore
Thread.sleep(10);
// monitoraggio
suivi("fin attente");
} catch (Exception ex) {
throw new RuntimeException(ex.toString());
}
- riga 3: un thread T1 recupera la persona p
- riga 8: incrementa il numero di figli di p
- riga 14: il thread T1 va in pausa
Un thread T2 prende il controllo ed esegue a sua volta la riga 3: richiede la stessa persona p di T1. Se il contesto di persistenza dei thread fosse lo stesso, la persona p, essendo già presente nel contesto grazie a T1, dovrebbe essere restituita a T2. Infatti, il metodo [getOne] utilizza il metodo [EntityManager].API trova JPA e questo metodo effettua un accesso al database solo se l’oggetto richiesto non fa parte del contesto di persistenza; in caso contrario, restituisce l’oggetto dal contesto di persistenza. Se così fosse, T1 e T2 conterrebbero la stessa persona p. T2 incrementerebbe quindi nuovamente di 1 il numero di figli di p (riga 8). Se uno dei thread riuscisse ad aggiornare il dato dopo la pausa, il numero di figli di p sarebbe aumentato di 2 e non di 1 come previsto. Ci si potrebbe quindi aspettare che i N thread portino il numero di figli non a N ma a un valore superiore. Tuttavia, non è così. Si può quindi concludere che T1 e T2 non hanno lo stesso riferimento a p. Lo si verifica facendo visualizzare l’indirizzo di p dai thread: è diverso per ciascuno di essi.
Sembrerebbe quindi che i thread:
- condividano lo stesso gestore del contesto di persistenza (EntityManager)
- ma abbiano ciascuno un proprio contesto di persistenza.
Si tratta solo di ipotesi e in questo caso sarebbe utile il parere di un esperto.
3.1.8. Cambiare da SGBD
![]() |
Per passare a SGBD, è sufficiente sostituire il file [src/spring-config.xml] [2] con il file [spring-config.xml] o SGBD corrispondente nella cartella [conf] [1].
Il file [spring-config.xml] di Oracle è, ad esempio, il seguente:
<?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>
<!-- la fonte dei dati 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>
Solo alcune righe cambiano rispetto allo stesso file utilizzato in precedenza per MySQL5:
- riga 14: il dialetto SQL che Hibernate deve utilizzare
- righe 25-28: le caratteristiche della connessione JDBC con il SGBD
Si invita il lettore a ripetere i test descritti per MySQL5 con altri SGBD.
3.1.9. Cambiare l'implementazione di JPA
Torniamo all'architettura dei test precedenti:
![]() |
Sostituiamo l’implementazione JPA / Hibernate con un’implementazione JPA / Toplink. Poiché Toplink non utilizza le stesse librerie di Hibernate, utilizziamo un nuovo progetto Eclipse:
![]() |
- in [1]: il progetto Eclipse. È identico al precedente. Cambiano solo il file di configurazione [spring-config.xml] [2] e la libreria [jpa-toplink], che sostituisce la libreria [jpa-hibernate].
- In [3]: la cartella degli esempi di questo tutorial. In [4] il progetto Eclipse da importare.
Il file di configurazione [spring-config.xml] per Toplink diventa il seguente:
<?xml version="1.0" encoding="UTF-8"?>
<!-- il JVM deve essere avviato con l'argomento -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">
<!-- livelli applicativi -->
<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>
<!-- la fonte dei dati 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>
<!-- il gestore delle transazioni -->
<tx:annotation-driven transaction-manager="txManager" />
<bean id="txManager" class="org.springframework.orm.jpa.JpaTransactionManager">
<property name="entityManagerFactory" ref="entityManagerFactory" />
</bean>
<!-- traduzione delle eccezioni -->
<bean class="org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor" />
<!-- persistenza -->
<bean class="org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor" />
</beans>
Per passare da Hibernate a Toplink è necessario modificare solo poche righe:
- riga 19: l’implementazione JPA è ora gestita da Toplink
- riga 23: la proprietà [databasePlatform] ha un valore diverso rispetto a quello utilizzato con Hibernate: il nome di una classe specifica di Toplink. Dove trovare questo nome è stato spiegato nel paragrafo 2.1.15.2.
Questo è tutto. Si noti la facilità con cui è possibile modificare SGBD o l’implementazione JPA con Spring.
Non abbiamo però ancora del tutto finito. Quando si esegue [InitDB], ad esempio, si verifica un’eccezione non facile da comprendere:
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
Il messaggio di errore alla riga 1 invita a consultare la documentazione di Spring. Qui si scopre un po’ di più sul ruolo svolto da un’oscura dichiarazione nel file [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>
La riga 1 dell’eccezione fa riferimento a una classe denominata [InstrumentationLoadTimeWeaver], che si trova alla riga 13 del file di configurazione di Spring. La documentazione di Spring spiega che questa classe è necessaria in alcuni casi per caricare le classi dell’applicazione e che, affinché possa funzionare, la classe JVM deve essere avviata con un agente. Questo agente è fornito da Spring e si chiama [spring-agent]:
![]() |
- il file [spring-agent.jar] si trova nella cartella <esempi>/lib [1]. È fornito con la distribuzione Spring 2.x (cfr. paragrafo 5.11).
- In [3], si crea una configurazione di esecuzione [Run/Run...]
- in [4], si crea una configurazione di esecuzione Java (esistono diversi tipi di configurazioni di esecuzione)
![]() |
- in [5], si seleziona la scheda [Main]
- in [6], si assegna un nome alla configurazione
- in [7], si assegna un nome al progetto Eclipse a cui si riferisce questa configurazione (utilizzare il pulsante Browse)
- in [8], si assegna un nome alla classe Java che contiene il metodo [main] (utilizzare il pulsante «Browse»)
- in [9], si passa alla scheda [Arguments]. In questa scheda è possibile specificare due tipi di argomenti:
- in [9], quelli passati al metodo [main]
- in [10], quelli passati a JVM che eseguirà il codice. L'agente Spring viene definito tramite il parametro -javaagent:valore di JVM. Il valore è il percorso del file [spring-agent.jar].
- in [11]: si convalida la configurazione
- in [12]: la configurazione viene creata
- in [13]: si esegue
A questo punto, [InitDB] viene eseguito e fornisce gli stessi risultati ottenuti con Hibernate. Per [TestNG], occorre procedere allo stesso modo:
![]() |
- in [1], si crea una configurazione di esecuzione [Run/Run...]
- in [2], si crea una configurazione di esecuzione TestNG
- in [3], si seleziona la scheda [Test]
- in [4], si assegna un nome alla configurazione
- in [5], si specifica il nome del progetto Eclipse a cui si riferisce questa configurazione (utilizzare il pulsante Browse)
- in [6], si assegna un nome alla classe dei test (utilizzare il pulsante "Browse")
![]() |
- in [7], si passa alla scheda [Arguments].
- in [8]: si imposta l’argomento -javaagent di JVM.
- in [9]: si conferma la configurazione
- in [10]: la configurazione viene creata
- in [11]: viene eseguita
A questo punto, [TestNG] viene eseguito e fornisce gli stessi risultati ottenuti con Hibernate.
3.2. Esempio 2: JBoss EJB3 / JPA con l’entità Persona
Riprendiamo lo stesso esempio di prima, ma lo eseguiamo in un contenitore EJB3, quello di JBoss:
![]() |
Un contenitore Ejb3 è normalmente integrato in un server applicativo. JBoss fornisce un contenitore Ejb3 “standalone” utilizzabile al di fuori di un server applicativo. Scopriremo che fornisce servizi analoghi a quelli forniti da Spring. Cercheremo di capire quale di questi contenitori risulti più pratico.
L’installazione del contenitore JBoss EJB3 è descritta nel paragrafo 5.12.
3.2.1. Il progetto Eclipse / JBoss EJB3 / Hibernate
Il progetto Eclipse è il seguente:
![]() |
![]() |
- in [1]: il progetto Eclipse. Si trova in [6] negli esempi del tutorial [5]. Lo importeremo.
- in [2]: i codici Java dei livelli presentati nei pacchetti:
- [entites]: il pacchetto delle entità JPA
- [dao]: il livello di accesso ai dati - si basa sul livello JPA
- [service]: un livello di servizi più che di business. Qui verrà utilizzato il servizio di transazioni del contenitore EJB3.
- [tests]: raggruppa i programmi di test.
- in [3]: la libreria [jpa-jbossejb3] raggruppa i file JAR necessari per JBoss EJB3 (vedi anche [7] e [8]).
- in [4]: la cartella [conf] raccoglie i file di configurazione per ciascuno dei SGBD utilizzati in questo tutorial. Ce ne sono sempre due: [persistence.xml], che configura il livello JPA, e [jboss-config.xml], che configura il contenitore Ejb3.
3.2.2. Le entità JPA
![]() |
Qui viene gestita una sola entità, ovvero l’entità Personne già esaminata in precedenza al paragrafo 3.1.2.
3.2.3. Il livello [dao]
![]() |
Il livello [dao] presenta l'interfaccia [IDao] descritta in precedenza al paragrafo 3.1.3.
L’implementazione [Dao] di questa interfaccia è la seguente:
package dao;
...
@Stateless
public class Dao implements IDao {
@PersistenceContext
private EntityManager em;
// eliminare una persona tramite il suo ID
@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);
}
// ottenere tutte le persone
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public List<Personne> getAll() {
return em.createQuery("select p from Personne p").getResultList();
}
// recuperare gli utenti il cui nome corrisponde a un modello
@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();
}
// recuperare una persona tramite il suo identificativo
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public Personne getOne(Integer id) {
return em.find(Personne.class, id);
}
// salvare una persona
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public Personne saveOne(Personne personne) {
em.persist(personne);
return personne;
}
// aggiornare una persona
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public Personne updateOne(Personne personne) {
return em.merge(personne);
}
}
- questo codice è in tutto e per tutto identico a quello che avevamo con Spring. Cambiano solo le annotazioni Java ed è proprio su questo che ci soffermeremo.
- riga 4: l’annotazione @Stateless rende la classe [Dao] un EJB senza stato. L’annotazione @Stateful rende una classe un EJB con stato. Un EJB con stato possiede campi privati il cui valore deve essere conservato nel tempo. Un esempio classico è quello di una classe che contiene informazioni relative all’utente web di un’applicazione. Un’istanza di questa classe è associata a un utente specifico e, quando il thread di esecuzione di una richiesta di tale utente termina, l’istanza deve essere conservata per essere disponibile alla successiva richiesta dello stesso cliente. Un EJB @Stateless non ha stato. Se riprendiamo lo stesso esempio, al termine del thread di esecuzione di una richiesta di un utente, l’EJB @Stateless entrerà a far parte di un pool di EJB @Stateless e diventerà disponibile per il thread di esecuzione di una richiesta di un altro utente.
- Per lo sviluppatore, il concetto di EJB 3 @Stateless è simile a quello del singleton di Spring. Lo utilizzerà negli stessi casi.
- Riga 7: l’annotazione @PersistenceContext è la stessa di quella presente nella versione Spring del livello [dao]. Indica il campo che riceverà l’EntityManager, il quale consentirà al livello [dao] di gestire il contesto di persistenza.
- riga 11: l'annotazione @TransactionAttribute applicata a un metodo serve a configurare la transazione in cui verrà eseguito il metodo. Ecco alcuni possibili valori di questa annotazione:
- TransactionAttributeType.REQUIRED: il metodo deve essere eseguito all’interno di una transazione. Se una transazione è già stata avviata, le operazioni di persistenza del metodo avvengono all’interno di essa. In caso contrario, viene creata e avviata una transazione.
- TransactionAttributeType.REQUIRES_NEW: il metodo deve essere eseguito in una nuova transazione. Quest’ultima viene creata e avviata.
- TransactionAttributeType.MANDATORY: il metodo deve essere eseguito all'interno di una transazione esistente. Se questa non esiste, viene generata un'eccezione.
- TransactionAttributeType.NEVER: il metodo non viene mai eseguito in una transazione.
- ...
L’annotazione avrebbe potuto essere inserita nella classe stessa:
@Stateless
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public class Dao implements IDao {
L'attributo viene quindi applicato a tutti i metodi della classe.
3.2.4. Il livello [metier / service]
![]() |
Il livello [service] presenta l'interfaccia [IService] esaminata in precedenza al paragrafo 3.1.4. L’implementazione [Service] dell’interfaccia [IService] è identica all’implementazione esaminata in precedenza nel paragrafo 3.1.4, salvo tre dettagli:
@Stateless
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public class Service implements IService {
// livello [dao]
@EJB
private IDao dao;
public IDao getDao() {
return dao;
}
public void setDao(IDao dao) {
this.dao = dao;
}
- riga 2: la classe [Service] è un EJB senza stato
- riga 3: tutti i metodi della classe [Service] devono essere eseguiti all’interno di una transazione
- righe 7-8: un riferimento all'EJB del livello [dao] verrà iniettato dal contenitore EJB nel campo [IDao dao] della riga 8. È l'annotazione @EJB della riga 7 a richiedere tale iniezione. L'oggetto iniettato deve essere un EJB. Si tratta di una differenza importante rispetto a Spring, dove qualsiasi tipo di oggetto può essere iniettato in un altro oggetto.
3.2.5. Configurazione dei livelli
![]() |
La configurazione dei livelli [service], [dao] e [JPA] è gestita dai seguenti file:
- [META-INF/persistence.xml] configura il livello JPA
- [jboss-config.xml] configura il contenitore Ejb3. A sua volta, utilizza i file [default.persistence.properties, ejb3-interceptors-aop.xml, embedded-jboss-beans.xml, jndi.properties]. Questi ultimi file sono forniti con Jboss Ejb3 e garantiscono una configurazione predefinita che normalmente non viene modificata. Lo sviluppatore si interessa solo al file [jboss-config.xml]
Esaminiamo i due file di configurazione:
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">
<!-- il fornitore JPA è Hibernate -->
<provider>org.hibernate.ejb.HibernatePersistence</provider>
<!-- la DataSource JTA gestita dall'ambiente Java EE5 -->
<jta-data-source>java:/datasource</jta-data-source>
<properties>
<!-- ricerca delle entità del livello JBA -->
<property name="hibernate.archive.autodetection" value="class, hbm" />
<!-- log di Hibernate SQL
<property name="hibernate.show_sql" value="true"/>
<property name="hibernate.format_sql" value="true"/>
<property name="use_sql_comments" value="true"/>
-->
<!-- il tipo di SGBD gestito -->
<property name="hibernate.dialect" value="org.hibernate.dialect.MySQLInnoDBDialect" />
<!-- ricreazione di tutte le tabelle (drop+create) al momento della distribuzione dell'unità di persistenza -->
<property name="hibernate.hbm2ddl.auto" value="create" />
</properties>
</persistence-unit>
</persistence>
Questo file è simile a quelli che abbiamo già incontrato nell’analisi delle entità JPA. Configura un livello Hibernate JPA. Le novità sono le seguenti:
- riga 5: l'unità di persistenza jpa non presenta l'attributo transaction-type che fino ad ora era sempre presente:
<persistence-unit name="jpa" transaction-type="RESOURCE_LOCAL" />
In assenza di un valore, l’attributo transaction-type assume il valore predefinito "JTA" (per Java Transaction API), che indica che il gestore delle transazioni è fornito da un contenitore EJB3. Un gestore "JTA" può fare di più rispetto a un gestore "RESOURCE_LOCAL": può gestire transazioni che coinvolgono più connessioni. Con JTA, è possibile aprire una transazione t1 su una connessione c1 su un SGBD 1, una transazione t2 su una connessione c2 con una SGBD 2 ed essere in grado di considerare (t1,t2) come un'unica transazione in cui o tutte le operazioni vanno a buon fine (commit) oppure nessuna (rollback).
In questo caso, operiamo con il gestore JTA del contenitore JBoss EJB3.
- Riga 11: dichiara la fonte dati che deve utilizzare il gestore JTA. Questa è indicata sotto forma di un nome JNDI (Java Naming and Directory Interface). Tale fonte dati è definita in [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">
<!-- factory di DataSource -->
<bean name="datasourceFactory" class="org.jboss.resource.adapter.jdbc.local.LocalTxDataSource">
<!-- nome JNDI della DataSource -->
<property name="jndiName">java:/datasource</property>
<!-- database gestito -->
<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>
<!-- proprietà del pool di connessioni -->
<property name="minSize">0</property>
<property name="maxSize">10</property>
<property name="blockingTimeout">1000</property>
<property name="idleTimeout">100000</property>
<!-- gestore delle transazioni, in questo caso JTA -->
<property name="transactionManager">
<inject bean="TransactionManager" />
</property>
<!-- gestore della cache Hibernate -->
<property name="cachedConnectionManager">
<inject bean="CachedConnectionManager" />
</property>
<!-- proprietà di istanziazione JNDI? -->
<property name="initialContextProperties">
<inject bean="InitialContextProperties" />
</property>
</bean>
<!-- il DataSource viene richiesto a una factory -->
<bean name="datasource" class="java.lang.Object">
<constructor factoryMethod="getDatasource">
<factory bean="datasourceFactory" />
</constructor>
</bean>
</deployment>
- riga 3: il tag radice del file è <deployment>. Questo file di distribuzione ha essenzialmente lo scopo di configurare la fonte dati java:/datasource che è stata dichiarata in persistence.xml.
- La fonte dati è definita dal bean "datasource" alla riga 38. Si nota che la fonte dati viene ottenuta (riga 40) da una "factory" definita dal bean "datasourceFactory" alla riga 7. Per ottenere la fonte dati dell'applicazione, il client dovrà chiamare il metodo [getDatasource] della classe factory (riga 39).
- riga 7: la classe factory che fornisce la fonte dati è una classe JBoss.
- riga 9: il nome JNDI della fonte dati. Deve corrispondere al nome dichiarato nel tag <jta-data-source> del file persistence.xml. Infatti, il livello JPA utilizzerà questo nome JNDI per richiedere la fonte dati.
- righe 12-15: qualcosa di più classico: le proprietà JDBC della connessione a SGBD
- righe 18-21: configurazione del pool di connessioni interno del contenitore JBoss EJB3.
- righe 24-26: il gestore JTA. La classe [TransactionManager], iniettata alla riga 25, è definita nel file [embedded-jboss-beans.xml].
- righe 28-30: la cache di Hibernate, un concetto che non abbiamo ancora trattato. La classe [CachedConnectionManager], inserita alla riga 29, è definita nel file [embedded-jboss-beans.xml]. Si noti che la configurazione ora dipende da Hibernate, il che ci creerà problemi quando vorremo migrare a Toplink.
- righe 32-34: configurazione del servizio JNDI.
Abbiamo terminato con il file di configurazione di JBoss EJB3. È complesso e molti aspetti rimangono poco chiari. È stato tratto da [ref1]. Saremo tuttavia in grado di adattarlo a un altro SGBD (righe 12-15 di jboss-config.xml, riga 24 di persistence.xml). La migrazione a Toplink non è stata possibile per mancanza di esempi.
3.2.6. Programma client [InitDB]
Iniziamo a scrivere un primo client dell'architettura descritta in precedenza:
![]() |
Il codice di [InitDB] è il seguente:
package tests;
...
public class InitDB {
// livello di servizio
private static IService service;
// costruttore
public static void main(String[] args) throws ParseException, NamingException {
// si avvia il container EJB3 JBoss
// vengono utilizzati i file di configurazione ejb3-interceptors-aop.xml e embedded-jboss-beans.xml
EJB3StandaloneBootstrap.boot(null);
// Creazione dei bean specifici dell'applicazione
EJB3StandaloneBootstrap.deployXmlResource("META-INF/jboss-config.xml");
// Distribuzione di tutti i file EJBs presenti nel classpath (operazione lenta, esegue la scansione completa)
// EJB3StandaloneBootstrap.scanClasspath();
// Si distribuiscono tutti i file EJB trovati nel classpath dell'applicazione
EJB3StandaloneBootstrap.scanClasspath("bin".replace("/", File.separator));
// Si inizializza il contesto JNDI. Viene utilizzato il file jndi.properties
InitialContext initialContext = new InitialContext();
// Istanziamento del livello di servizio
service = (IService) initialContext.lookup("Service/local");
// Si svuota il database
clean();
// si riempie il database
fill();
// si verifica visivamente
dumpPersonnes();
// si arresta il contenitore EJB
EJB3StandaloneBootstrap.shutdown();
}
// visualizzazione del contenuto della tabella
private static void dumpPersonnes() {
System.out.format("[personnes]-------------------------------------------------------------------%n");
for (Personne p : service.getAll()) {
System.out.println(p);
}
}
// compilazione della tabella
public static void fill() throws ParseException {
// creazione persone
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);
// si salva
service.saveArray(new Personne[] { p1, p2 });
}
// eliminazione elementi dalla tabella
public static void clean() {
for (Personne p : service.getAll()) {
service.deleteOne(p.getId());
}
}
}
- Il modo per avviare il contenitore Jboss Ejb3 è stato trovato in [ref1].
- riga 13: il contenitore viene avviato. [EJB3StandaloneBootstrap] è una classe del contenitore.
- riga 16: l'unità di distribuzione configurata da [jboss-config.xml] viene distribuita nel contenitore: vengono configurati il gestore JTA, la fonte dati, il pool di connessioni, la cache Hibernate e il servizio JNDI.
- riga 22: si richiede al contenitore di eseguire la scansione della cartella bin del progetto Eclipse per individuare gli EJB. Gli EJB dei livelli [service] e [dao] verranno individuati e gestiti dal contenitore.
- riga 25: viene inizializzato un contesto JNDI. Ci servirà per individuare gli EJB.
- riga 28: l’EJB corrispondente alla classe [Service] del livello [service] viene richiesto al servizio JNDI. È possibile accedere a un EJB localmente (local) o tramite la rete (remote). In questo caso, il nome "Service/local" dell'EJB ricercato indica la classe [Service] del livello [service] per un accesso locale.
- Ora l’applicazione è distribuita e si dispone di un riferimento al livello [service]. Ci si trova nella stessa situazione che si verifica dopo la riga 11 riportata di seguito nel codice [InitDB] della versione Spring. Si riscontra quindi lo stesso codice in entrambe le versioni.
public class InitDB {
// livello di servizio
private static IService service;
// costruttore
public static void main(String[] args) throws ParseException {
// configurazione dell'applicazione
ApplicationContext ctx = new ClassPathXmlApplicationContext("spring-config.xml");
// livello di servizio
service = (IService) ctx.getBean("service");
// svuotamento del database
clean();
// riempimento del database
fill();
// si verifica visivamente
dumpPersonnes();
}
...
- riga 36 (JBoss EJB3): si arresta il contenitore EJB3.
L'esecuzione di [InitDB] fornisce i seguenti risultati:
Si invita il lettore a consultare questi log. Vi si trovano informazioni interessanti su ciò che fa il contenitore EJB3.
3.2.7. Test unitari [TestNG]
Il codice del programma [TestNG] è il seguente:
package tests;
...
public class TestNG {
// livello di servizio
private IService service = null;
@BeforeClass
public void init() throws NamingException, ParseException {
// log
log("init");
// si avvia il container EJB3 JBoss
// vengono utilizzati i file di configurazione ejb3-interceptors-aop.xml e embedded-jboss-beans.xml
EJB3StandaloneBootstrap.boot(null);
// Creazione dei bean specifici dell'applicazione
EJB3StandaloneBootstrap.deployXmlResource("META-INF/jboss-config.xml");
// Distribuzione di tutti i file EJBs presenti nel classpath (operazione lenta, esegue la scansione completa)
// EJB3StandaloneBootstrap.scanClasspath();
// Si distribuiscono tutti i file EJB trovati nel classpath dell'applicazione
EJB3StandaloneBootstrap.scanClasspath("bin".replace("/", File.separator));
// Si inizializza il contesto JNDI. Viene utilizzato il file jndi.properties
InitialContext initialContext = new InitialContext();
// Istanziamento del livello di servizio
service = (IService) initialContext.lookup("Service/local");
// si svuota il database
clean();
// si riempie il database
fill();
// si verifica visivamente
dumpPersonnes();
}
@AfterClass
public void terminate() {
// log
log("terminate");
// Arresto del container EJB
EJB3StandaloneBootstrap.shutdown();
}
@BeforeMethod
public void setUp() throws ParseException {
...
}
...
}
- Il metodo init (righe 10-37), che serve a configurare l'ambiente necessario per i test, riprende il codice spiegato in precedenza in [InitDB].
- Il metodo `terminate` (righe 40-45), che viene eseguito al termine dei test (in presenza dell'annotazione @AfterClass), arresta il contenitore EJB3 (riga 44).
- Tutto il resto è identico a quanto presente nella versione Spring.
I test hanno esito positivo:

3.2.8. Modifica di SGBD
![]() |
Per modificare SGBD, è sufficiente sostituire il contenuto della cartella [META-INF] [2] con quello della cartella SGBD nella cartella [conf] [1]. Prendiamo ad esempio il server SQL:
Il file [persistence.xml] è il seguente:
<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">
<!-- il provider JPA è Hibernate -->
<provider>org.hibernate.ejb.HibernatePersistence</provider>
<!-- la DataSource JTA è gestita dall'ambiente Java EE5 -->
<jta-data-source>java:/datasource</jta-data-source>
<properties>
<!-- ricerca delle entità del livello JBA -->
<property name="hibernate.archive.autodetection" value="class, hbm" />
<!-- log di Hibernate SQL
<property name="hibernate.show_sql" value="true"/>
<property name="hibernate.format_sql" value="true"/>
<property name="use_sql_comments" value="true"/>
-->
<!-- il tipo di SGBD gestito -->
<property name="hibernate.dialect" value="org.hibernate.dialect.SQLServerDialect" />
<!-- ricreazione di tutte le tabelle (drop+create) al momento della distribuzione dell'unità di persistenza -->
<property name="hibernate.hbm2ddl.auto" value="create" />
</properties>
</persistence-unit>
</persistence>
È cambiata solo una riga:
- riga 24: il dialetto SQL che Hibernate deve utilizzare
Il file [jboss-config.xml] del server SQL è invece il seguente:
<?xml version="1.0" encoding="UTF-8"?>
<deployment xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="urn:jboss:bean-deployer bean-deployer_1_0.xsd"
xmlns="urn:jboss:bean-deployer:2.0">
<!-- factory di DataSource -->
<bean name="datasourceFactory" class="org.jboss.resource.adapter.jdbc.local.LocalTxDataSource">
<!-- nome JNDI della DataSource -->
<property name="jndiName">java:/datasource</property>
<!-- database gestito -->
<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>
<!-- proprietà del pool di connessioni -->
...
</bean>
</deployment>
Sono state modificate solo le righe 12-15: esse specificano le caratteristiche della nuova connessione JDBC.
Si invita il lettore a ripetere con altri file SGBD i test descritti per MySQL5.
3.2.9. Modifica dell’implementazione JPA
Come indicato in precedenza, non abbiamo trovato alcun esempio di utilizzo del container JBoss EJB3 con TopLink. Ad oggi (giugno 2007), non so ancora se questa configurazione sia possibile.
3.3. Altri esempi
Riassumiamo ciò che è stato fatto con l’entità Personne. Abbiamo realizzato tre architetture per eseguire gli stessi test:
1 - un'implementazione Spring / Hibernate
![]() |
2 - un'implementazione Spring / Toplink
![]() |
3 - un'implementazione Jboss Ejb3 / Hibernate
![]() |
Gli esempi del tutorial riprendono queste tre architetture insieme ad altre entità analizzate nella prima parte del tutorial:
Categoria - Articolo
![]() |
- in [1]: la versione Spring / Hibernate
- in [2]: la versione Spring / Toplink
- in [3]: la versione Jboss Ejb3 / Hibernate
Persona - Indirizzo - Attività
![]() |
- in [1]: la versione Spring / Hibernate
- in [2]: la versione Spring / Toplink
- in [3]: la versione JBoss EJB3 / Hibernate
Questi esempi non introducono novità dal punto di vista dell’architettura. Si inseriscono semplicemente in un contesto in cui vi sono diverse entità da gestire con relazioni uno-a-molti o molti-a-molti tra loro, cosa che non era presente negli esempi con l’entità Personne.
3.4. Esempio 3: Spring / JPA in un’applicazione web
3.4.1. Presentazione
Riprendiamo qui un’applicazione presentata nel seguente documento:
[ref4]: Nozioni di base sullo sviluppo web MVC in Java [http://tahe.developpez.com/java/baseswebmvc/].
Questo documento illustra le basi dello sviluppo web MVC in Java. Per comprendere l’esempio che segue, il lettore deve possedere tali nozioni di base. L’applicazione web utilizzerà il server Tomcat. La sua installazione e il suo utilizzo all’interno di Eclipse sono illustrati nel paragrafo 5.3.
L’applicazione era stata sviluppata con un livello [dao] basato sullo strumento Ibatis / SqlMap [http://ibatis.apache.org/] che garantiva il ponte relazionale/oggetto. Ci limitiamo a sostituire Ibatis con JPA. L’architettura dell’applicazione sarà la seguente:
![]() |
L’applicazione web che scriveremo consentirà di gestire un gruppo di persone con quattro operazioni:
- elenco delle persone del gruppo
- aggiunta di una persona al gruppo
- modifica di una persona del gruppo
- eliminazione di una persona dal gruppo
Queste quattro operazioni di base sono quelle tipiche di una tabella di un database. Le schermate che seguono mostrano le pagine che l’applicazione visualizza all’utente.
![]() |
![]() |
![]() |
![]() |
![]() |
3.4.2. Il progetto Eclipse
Il progetto Eclipse dell'applicazione è il seguente:
![]() |
- in [1]: il progetto web. Si tratta di un progetto Eclipse di tipo [Dynamic Web Project] [2]. Lo si trova in [4] nella cartella [3] degli esempi del tutorial. Lo si importerà.
![]() |
- in [5]: i sorgenti e la configurazione dei livelli [service, dao, jpa]. Manteniamo il lavoro già svolto [dao, entites, service] del progetto Eclipse [hibernate-spring-personnes-metier-dao] analizzato nel paragrafo 3.1.1. Sviluppiamo solo il livello [web], qui rappresentato dal pacchetto [web]. Inoltre, manteniamo i file di configurazione [persistence.xml, spring-config.xml] di questo progetto, con l’unica differenza che utilizzeremo il Postgres SGBD, il che comporta le seguenti modifiche in [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>
<!-- la fonte dei dati 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>
Le righe 8 e 16-19 sono state adattate a Postgres.
- In [6]: la cartella [WebContent] contiene le pagine JSP del progetto e le librerie necessarie. Queste ultime sono riportate in [8]
- L’applicazione è utilizzabile con diversi SGBD. È sufficiente modificare il file [spring-config.xml]. La cartella [conf] [7] contiene il file [spring-config.xml] adattato a diversi SGBD.
3.4.3. Il livello [web]
La nostra applicazione presenta la seguente architettura a più livelli:
![]() |
Il livello [web] fornirà all’utente delle schermate che gli consentiranno di gestire il gruppo di persone:
- elenco delle persone del gruppo
- aggiunta di una persona al gruppo
- modifica di una persona del gruppo
- eliminazione di una persona dal gruppo
A tal fine, si avvarrà del livello [service], che a sua volta farà ricorso al livello [dao]. Abbiamo già presentato le schermate gestite dal livello [web] (paragrafo 3.4.1). Per descrivere il livello web, presenteremo in successione:
- la sua configurazione
- le sue viste
- il suo controller
- alcuni test
3.4.3.1. Configurazione dell’applicazione web
Torniamo all’architettura del progetto Eclipse:
![]() | ![]() |
- nel pacchetto [web] si trova il controller dell'applicazione web: la classe [Application].
- Le pagine JSP / JSTL dell’applicazione si trovano in [WEB-INF/vues].
- La cartella [WEB-INF/lib] contiene i file di terze parti necessari all’applicazione. Sono visibili nella cartella [Web App Libraries].
[web.xml]
Il file [web.xml] è il file utilizzato dal server web per caricare l’applicazione. Il suo contenuto è il seguente:
<?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>
<!-- Mappatura ServletPersonne-->
<servlet-mapping>
<servlet-name>personnes</servlet-name>
<url-pattern>/do/*</url-pattern>
</servlet-mapping>
<!-- file di benvenuto -->
<welcome-file-list>
<welcome-file>index.jsp</welcome-file>
</welcome-file-list>
<!-- Pagina di errore imprevisto -->
<error-page>
<exception-type>java.lang.Exception</exception-type>
<location>/WEB-INF/vues/exception.jsp</location>
</error-page>
</web-app>
- righe 23-26: gli URL [/do/*] saranno gestiti dal servlet [personnes]
- righe 7-8: il servlet [personnes] è un'istanza della classe [Application], una classe che creeremo.
- righe 9-20: definiscono tre parametri [urlList, urlEdit, urlErreurs] che identificano gli URL delle pagine JSP delle viste [list, edit, erreurs].
- righe 28-30: l’applicazione dispone di una pagina iniziale predefinita [index.jsp] che si trova nella directory principale dell’applicazione web.
- righe 32-35: l’applicazione dispone di una pagina di errore predefinita che viene visualizzata quando il server web rileva un’eccezione non gestita dall’applicazione.
- riga 37: il tag <exception-type> indica il tipo di eccezione gestita dalla direttiva <error-page>, in questo caso il tipo [java.lang.Exception] e i suoi derivati, ovvero tutte le eccezioni.
- riga 38: il tag <location> indica la pagina JSP da visualizzare quando si verifica un'eccezione del tipo definito da <exception-type>. L'eccezione verificatasi è disponibile in questa pagina in un oggetto denominato exception se la pagina contiene la direttiva:
<%@ page isErrorPage="true" %>
- (continua)
- se <exception-type> specifica un tipo T1 e un'eccezione di tipo T2 non derivata da T1 viene segnalata al server web, quest'ultimo invia al client una pagina di eccezione proprietaria, solitamente poco intuitiva. Da qui l’utilità del tag <error-page> nel file [web.xml].
[index.jsp]
Questa pagina viene visualizzata se un utente richiede direttamente il contesto dell’applicazione senza specificare un URL, c.a.d. qui [/spring-jpa-hibernate-personnes-crud]. Il suo contenuto è il seguente:
<%@ 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] reindirizza (riga 4) il client all’URL [/do/list]. Questo URL mostra l’elenco delle persone del gruppo.
3.4.3.2. Le pagine JSP / JSTL dell’applicazione
La vista [list.jsp]
Serve a visualizzare l’elenco delle persone:

Il suo codice è il seguente:
<%@ 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>
- questa vista riceve due elementi nel proprio modello:
- l'elemento [personnes] associato a un oggetto di tipo [List] contenente oggetti di tipo [Personne]: un elenco di persone.
- l'elemento facoltativo [erreurs] associato a un oggetto di tipo [List] contenente oggetti di tipo [String]: un elenco di messaggi di errore.
- righe 31-43: si scorre l’elenco ${personnes} per visualizzare una tabella HTML contenente le persone del gruppo.
- riga 40: l’URL a cui punta il link [Modifier] viene impostato dal campo [id] della persona corrente, in modo che il controller associato all’URL [/do/edit] sappia quale persona deve essere modificata.
- riga 41: lo stesso vale per il link [Supprimer].
- riga 37: per visualizzare la data di nascita della persona nel formato JJ/MM/AAAA, si utilizza il tag <dt> della libreria di tag [DateTime] del progetto Apache [Jakarta Taglibs]:

Il file di descrizione di questa libreria di tag è definito alla riga 3.
- riga 46: il link [Ajout] per l'aggiunta di una nuova persona ha come destinazione l'URL [/do/edit], proprio come il link [Modifier] della riga 40. È il valore -1 del parametro [id] che indica che si tratta di un'aggiunta piuttosto che di una modifica.
- righe 10-18: se l'elemento ${errori} è presente nel modello, vengono visualizzati i messaggi di errore in esso contenuti.
La vista [edit.jsp]
Serve a visualizzare il modulo per l'aggiunta di una nuova persona o la modifica di una persona esistente:
![]() |
Il codice della vista [edit.jsp] è il seguente:
<%@ 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>
Questa vista presenta un modulo per l’aggiunta di una nuova persona o l’aggiornamento di una persona esistente. D’ora in poi, per semplificare la scrittura, useremo il termine unico [mise à jour]. Il pulsante [Valider] (riga 73) attiva il POST del modulo all’URL [/do/validate] (riga 16). Se il POST non va a buon fine, viene nuovamente visualizzata la vista [edit.jsp] con l’errore o gli errori verificatisi; in caso contrario, viene visualizzata la vista [list.jsp].
- La vista [edit.jsp], visualizzata sia su un GET che su un POST che non supera il controllo, riceve i seguenti elementi nel proprio modello:
attributo | GET | POST |
ID della persona aggiornata | idem | |
la sua versione | idem | |
il suo nome | nome inserito | |
il suo cognome | cognome inserito | |
Data di nascita | data di nascita inserita | |
stato civile | stato civile inserito | |
numero di figli | numero di figli inserito | |
vuoto | un messaggio di errore che segnala il fallimento dell'aggiunta o della modifica al momento dell'esecuzione di POST, causata dal pulsante [Envoyer]. Vuoto se non si verificano errori. | |
vuoto | segnala un nome errato – vuoto in caso contrario | |
vuoto | segnala un cognome errato – vuoto in caso contrario | |
vuoto | segnala una data di nascita errata – vuoto in caso contrario | |
vuoto | segnala un numero di figli errato – vuoto in caso contrario |
- righe 11-15: se il codice POST del modulo non viene elaborato correttamente, si otterrà [erreurEdit!=''] e verrà visualizzato un messaggio di errore.
- riga 16: il modulo verrà inviato all’URL [/do/validate]
- riga 20: viene visualizzato l'elemento [id] del modello
- riga 24: viene visualizzato l'elemento [version] del modello
- righe 26-32: inserimento del nome della persona:
- alla visualizzazione iniziale del modulo (GET), ${prenom} visualizza il valore attuale del campo [prenom] dell’oggetto [Personne] aggiornato e ${erreurPrenom} è vuoto.
- in caso di errore dopo il campo POST, viene visualizzato nuovamente il valore inserito ${prenom} insieme all’eventuale messaggio di errore ${erreurPrenom}
- righe 33-39: inserimento del cognome della persona
- righe 40-46: inserimento della data di nascita della persona
- righe 47-61: inserimento dello stato civile della persona tramite un pulsante di opzione. Si utilizza il valore del campo [marie] dell’oggetto [Personne] per determinare quale dei due pulsanti di opzione debba essere selezionato.
- righe 62-68: inserimento del numero di figli della persona
- riga 71: un campo nascosto HTML denominato [id], il cui valore corrisponde al campo [id] della persona oggetto dell’aggiornamento; -1 in caso di aggiunta, un altro valore in caso di modifica.
- riga 72: un campo nascosto HTML denominato [version], il cui valore corrisponde al campo [id] della persona oggetto dell’aggiornamento.
- riga 73: il pulsante [Valider] di tipo [Submit] del modulo
- riga 74: un link che consente di tornare all’elenco delle persone. È stato denominato [Annuler] perché permette di uscire dal modulo senza convalidarlo.
La vista [exception.jsp]
Serve a visualizzare una pagina che segnala che si è verificata un'eccezione non gestita dall'applicazione e che è stata segnalata al server web.
Ad esempio, eliminiamo una persona che non esiste nel gruppo:
![]() |
Il codice della vista [exception.jsp] è il seguente:
<%@ 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>
- questa vista riceve nel proprio modello un elemento denominato [exception], che corrisponde all’eccezione intercettata dal server web. Affinché tale elemento venga incluso nel modello della pagina JSP dal server web, è necessario che la pagina abbia definito il tag della riga 3.
- riga 6: si imposta a 200 il codice di stato HTTP della risposta. Si tratta della prima intestazione HTTP della risposta. Il codice 200 indica al cliente che la sua richiesta è stata soddisfatta. Generalmente, nella risposta del server è stato incluso un documento HTML. È proprio questo il caso. Se il codice di stato HTTP della risposta non viene impostato a 200, in questo caso assumerà il valore 500, il che significa che si è verificato un errore. Infatti, il server web, avendo intercettato un'eccezione non gestita, rileva questa situazione anomala e la segnala con il codice 500. La reazione al codice HTTP 500 varia a seconda dei browser: Firefox visualizza il documento HTML che può accompagnare questa risposta, mentre IE ignora tale documento e visualizza la propria pagina. Per questo motivo abbiamo sostituito il codice 500 con il codice 200.
- riga 16: viene visualizzato il testo dell’eccezione
- riga 18: all’utente viene proposto un link per tornare all’elenco delle persone
La vista [erreurs.jsp]
Serve a visualizzare una pagina che segnala gli errori di inizializzazione dell’applicazione, c.a.d, e gli errori rilevati durante l’esecuzione del metodo [init] del servlet del controller. Può trattarsi, ad esempio, della mancanza di un parametro nel file [web.xml], come illustrato nell’esempio riportato di seguito:

Il codice della pagina [erreurs.jsp] è il seguente:
<%@ 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>
La pagina riceve nel proprio modello un elemento [erreurs], che è un oggetto di tipo [ArrayList] contenente oggetti [String], i quali sono messaggi di errore. Questi vengono visualizzati dal ciclo delle righe 13-15.
3.4.3.3. Il controller dell’applicazione
Il controller [Application] è definito nel pacchetto [web]:

Struc tura e inizializzazione del controller
Lo scheletro del controller [Application] è il seguente:
package web;
...
@SuppressWarnings("serial")
public class Application extends HttpServlet {
// parametri dell'istanza
private String urlErreurs = null;
private ArrayList erreursInitialisation = new ArrayList<String>();
private String[] paramètres = { "urlList", "urlEdit", "urlErreurs" };
private Map params = new HashMap<String, String>();
// servizio
private IService service = null;
// init
@SuppressWarnings("unchecked")
public void init() throws ServletException {
// si recuperano i parametri di inizializzazione del servlet
ServletConfig config = getServletConfig();
// si elaborano gli altri parametri di inizializzazione
String valeur = null;
for (int i = 0; i < paramètres.length; i++) {
// valore del parametro
valeur = config.getInitParameter(paramètres[i]);
// il parametro è presente?
if (valeur == null) {
// si registra l'errore
erreursInitialisation.add("Le paramètre [" + paramètres[i] + "] n'a pas été initialisé");
} else {
// si memorizza il valore del parametro
params.put(paramètres[i], valeur);
}
}
// l'URL della vista [erreurs] richiede un trattamento particolare
urlErreurs = config.getInitParameter("urlErreurs");
if (urlErreurs == null)
throw new ServletException("Le paramètre [urlErreurs] n'a pas été initialisé");
// configurazione dell'applicazione
ApplicationContext ctx = new ClassPathXmlApplicationContext("spring-config.xml");
// livello di servizio
service = (IService) ctx.getBean("service");
// si svuota il database
clean();
// si riempie
try {
fill();
} catch (ParseException e) {
throw new ServletException(e);
}
}
// riempimento della tabella
public void fill() throws ParseException {
// creazione persone
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);
// che si salvano
service.saveArray(new Personne[] { p1, p2 });
}
// eliminazione elementi dalla tabella
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 {
...
}
// visualizzazione elenco persone
private void doListPersonnes(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
...
}
// modifica / aggiunta di una persona
private void doEditPersonne(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
...
}
// eliminazione di una persona
private void doDeletePersonne(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
...
}
// conferma della modifica / dell'aggiunta di una persona
public void doValidatePersonne(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
...
}
// visualizzazione del modulo precompilato
private void showFormulaire(HttpServletRequest request, HttpServletResponse response, String erreurEdit) throws ServletException, IOException {
...
}
// invio
public void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
// passaggio di controllo a GET
doGet(request, response);
}
}
- righe 21-34: si recuperano i parametri previsti nel file [web.xml].
- righe 37-39: il parametro [urlErreurs] deve essere obbligatoriamente presente poiché indica l'URL della vista [erreurs] in grado di visualizzare eventuali errori di inizializzazione. Se non esiste, l'applicazione viene interrotta avviando una [ServletException] (riga 39). Questa eccezione verrà segnalata al server web e gestita dal tag <error-page> del file [web.xml]. Viene quindi visualizzata la vista [exception.jsp]:

Il link [Retour à la liste] sopra riportato non è funzionante. Utilizzandolo si ottiene la stessa risposta finché l’applicazione non viene modificata e ricaricata. È utile per altri tipi di eccezioni, come abbiamo già visto.
- righe 40-43: utilizzano il file di configurazione Spring per recuperare un riferimento al livello [service]. Dopo l’inizializzazione del controller, i suoi metodi dispongono di un riferimento [service] al livello [service] (riga 15) che utilizzeranno per eseguire le azioni richieste dall’utente. Queste saranno intercettate dal metodo [doGet], che le farà elaborare da un metodo specifico del controller:
Url | Metodo HTTP | metodo del controller |
GET | doListPersonnes | |
GET | doEditPersonne | |
POST | doValidatePersonne | |
GET | doDeletePersonne |
Il metodo [doGet]
Questo metodo ha lo scopo di indirizzare l'elaborazione delle azioni richieste dall'utente verso il metodo corretto. Il suo codice è il seguente:
// GET
@SuppressWarnings("unchecked")
public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
// si verifica l'esito dell'inizializzazione del servlet
if (erreursInitialisation.size() != 0) {
// si passa alla pagina degli errori
request.setAttribute("erreurs", erreursInitialisation);
getServletContext().getRequestDispatcher(urlErreurs).forward(request, response);
// fine
return;
}
// si recupera il metodo di invio della richiesta
String méthode = request.getMethod().toLowerCase();
// si recupera l'azione da eseguire
String action = request.getPathInfo();
// azione?
if (action == null) {
action = "/list";
}
// esecuzione dell'azione
if (méthode.equals("get") && action.equals("/list")) {
// elenco delle persone
doListPersonnes(request, response);
return;
}
if (méthode.equals("get") && action.equals("/delete")) {
// eliminazione di una persona
doDeletePersonne(request, response);
return;
}
if (méthode.equals("get") && action.equals("/edit")) {
// visualizzazione del modulo di aggiunta/modifica di una persona
doEditPersonne(request, response);
return;
}
if (méthode.equals("post") && action.equals("/validate")) {
// conferma del modulo di aggiunta/modifica di una persona
doValidatePersonne(request, response);
return;
}
// altri casi
doListPersonnes(request, response);
}
- righe 7-13: si verifica che l’elenco degli errori di inizializzazione sia vuoto. In caso contrario, viene visualizzata la vista [erreurs(erreurs)] che segnalerà l’errore o gli errori.
- riga 15: si recupera il metodo [get] o [post] che il cliente ha utilizzato per effettuare la richiesta.
- riga 17: si recupera il valore del parametro [action] della richiesta.
- righe 23-27: elaborazione della richiesta [GET /do/list] che richiede l'elenco delle persone.
- righe 28-32: elaborazione della richiesta [GET /do/delete] che richiede la cancellazione di una persona.
- righe 33-37: elaborazione della richiesta [GET /do/edit] che richiede il modulo di aggiornamento di una persona.
- righe 38-42: elaborazione della richiesta [POST /do/validate] che richiede la convalida della persona aggiornata.
- riga 44: se l'azione richiesta non è una delle cinque precedenti, allora si procede come se fosse [GET /do/list].
Il metodo [doListPersonnes]
Questo metodo elabora la richiesta [GET /do/list] che richiede l'elenco delle persone:

Il suo codice è il seguente:
// visualizzazione dell'elenco delle persone
private void doListPersonnes(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// il modello della vista [list]
request.setAttribute("personnes", service.getAll());
// visualizzazione della vista [list]
getServletContext().getRequestDispatcher((String) params.get("urlList")).forward(request, response);
}
- riga 4: si richiede al livello [service] l'elenco delle persone del gruppo e lo si inserisce nel modello sotto la chiave "persone".
- riga 6: si visualizza la vista [list.jsp] descritta al paragrafo 3.4.3.2.
Il metodo [doDeletePersonne]
Questo metodo gestisce la richiesta [GET /do/delete?id=XX] che richiede l’eliminazione della persona con id=XX. L'URL [/do/delete?id=XX] è quello dei link [Supprimer] della vista [list.jsp]:

il cui codice è il seguente:
...
<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>
Alla riga 12 si vede l'URL [/do/delete?id=XX] del link [Supprimer]. Il metodo [doDeletePersonne], che deve elaborare questo URL, deve rimuovere la persona con id=XX e quindi visualizzare il nuovo elenco delle persone del gruppo. Il suo codice è il seguente:
// eliminazione di una persona
private void doDeletePersonne(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// si recupera l'ID della persona
int id = Integer.parseInt(request.getParameter("id"));
// si elimina la persona
service.deleteOne(id);
// reindirizzamento all'elenco delle persone
response.sendRedirect("list");
}
- riga 4: l'URL elaborato è del tipo [/do/delete?id=XX]. Si recupera il valore [XX] dal parametro [id].
- riga 6: si richiede al livello [service] l’eliminazione della persona con l’ID ottenuto. Non effettuiamo alcuna verifica. Se la persona che si intende eliminare non esiste, il livello [dao] genera un’eccezione che viene propagata dal livello [service]. Non la gestiamo nemmeno qui, nel controller. Verrà quindi inoltrata al server web che, in base alla configurazione, visualizzerà la pagina [exception.jsp], descritta al paragrafo 3.4.3.2:

- riga 9: se l’eliminazione è avvenuta (nessuna eccezione), si richiede al client di reindirizzarsi all’URL relativo [list]. Poiché quella appena elaborata è [/do/delete], l’URL di reindirizzamento sarà [/do/list]. Il browser verrà quindi indirizzato a [GET /do/list], che provocherà la visualizzazione dell’elenco delle persone.
Il metodo [doEditPersonne]
Questo metodo gestisce la richiesta [GET /do/edit?id=XX] che richiede il modulo di aggiornamento della persona con id=XX. L'URL [/do/edit?id=XX] è quello dei link [Modifier] e del link [Ajout] della vista [list.jsp]:

il cui codice è il seguente:
...
<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>
Alla riga 11 si vede l’URL [/do/edit?id=XX] del link [Modifier] e alla riga 17 l’URL [/do/edit?id=-1] del link [Ajout]. Il metodo [doEditPersonne] deve visualizzare il modulo di modifica della persona con id=XX oppure, se si tratta di un'aggiunta, presentare un modulo vuoto.
![]() |
- nel metodo [1] sopra indicato, il modulo di inserimento e nel metodo [2] il modulo di modifica.
Il codice del metodo [doEditPersonne] è il seguente:
// modifica/aggiunta di una persona
private void doEditPersonne(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// si recupera l'ID della persona
int id = Integer.parseInt(request.getParameter("id"));
// Aggiunta o modifica?
Personne personne = null;
if (id != -1) {
// modifica - si recupera la persona da modificare
personne = service.getOne(id);
request.setAttribute("id", personne.getId());
request.setAttribute("version", personne.getVersion());
} else {
// aggiunta - si crea una persona vuota
personne = new Personne();
request.setAttribute("id", -1);
request.setAttribute("version", -1);
}
// si inserisce l'oggetto [Personne] nella sessione dell'utente
request.getSession().setAttribute("personne", personne);
// e nel modello della vista [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());
// visualizzazione della vista [edit]
getServletContext().getRequestDispatcher((String) params.get("urlEdit")).forward(request, response);
}
- il GET ha come destinazione un URL del tipo [/do/edit?id=XX]. Alla riga 4, recuperiamo il valore di [id]. Successivamente si presentano due casi:
- id è diverso da -1. In tal caso si tratta di una modifica ed è necessario visualizzare un modulo precompilato con le informazioni della persona da modificare. Alla riga 9, questa persona viene richiesta al livello [service].
- id è uguale a -1. In questo caso si tratta di un’aggiunta ed è necessario visualizzare un modulo vuoto. A tal fine, alla riga 14 viene creata una persona vuota.
- In entrambi i casi, vengono inizializzati gli elementi [id, version] del modello della pagina [edit.jsp] descritto al paragrafo 3.4.3.2.
- L’oggetto [Personne] ottenuto viene inserito nel modello della pagina [edit.jsp]. Questo modello comprende i seguenti elementi: [erreurEdit, id, version, prenom, erreurPrenom, nom, erreurNom, datenaissance, erreurDateNaissance, marie, nbenfants, erreurNbEnfants]. Questi elementi vengono inizializzati alle righe 19-31, ad eccezione di quelli il cui valore è la stringa vuota [erreurPrenom, erreurNom, erreurDateNaissance, erreurNbEnfants]. È noto che, in caso di loro assenza nel modello, la libreria JSTL visualizzerà una stringa vuota come loro valore. Sebbene anche l’elemento [erreurEdit] abbia come valore una stringa vuota, viene comunque inizializzato poiché nella pagina [edit.jsp] viene eseguito un test sul suo valore.
- Una volta pronto il modello, il controllo passa alla pagina [edit.jsp], riga 33, che genererà la vista [edit].
Il metodo [doValidatePersonne]
Questo metodo gestisce la richiesta [POST /do/validate] che convalida il modulo di aggiornamento. Questa richiesta POST viene attivata dal pulsante [Valider]:

Ricordiamo gli elementi di immissione dati del modulo HTML nella vista sopra riportata:
<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>
La richiesta POST contiene i parametri [prenom, nom, datenaissance, marie, nbenfants, id] e viene inviata all'URL [/do/validate] (riga 1). Viene elaborata dal seguente metodo [doValidatePersonne]:
// conferma della modifica/aggiunta di una persona
public void doValidatePersonne(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// si recuperano gli elementi inviati
boolean formulaireErroné = false;
boolean erreur;
// il nome
String prenom = request.getParameter("prenom").trim();
// nome valido?
if (prenom.length() == 0) {
// si registra l'errore
request.setAttribute("erreurPrenom", "Le prénom est obligatoire");
formulaireErroné = true;
}
// il cognome
String nom = request.getParameter("nom").trim();
// nome valido?
if (nom.length() == 0) {
// si registra l'errore
request.setAttribute("erreurNom", "Le nom est obligatoire");
formulaireErroné = true;
}
// data di nascita
Date datenaissance = null;
try {
datenaissance = new SimpleDateFormat("dd/MM/yyyy").parse(request.getParameter("datenaissance").trim());
} catch (ParseException e) {
// si rileva l'errore
request.setAttribute("erreurDateNaissance", "Date incorrecte");
formulaireErroné = true;
}
// stato civile
boolean marie = Boolean.parseBoolean(request.getParameter("marie").trim());
// numero di figli
int nbenfants = 0;
erreur = false;
try {
nbenfants = Integer.parseInt(request.getParameter("nbenfants").trim());
if (nbenfants < 0) {
erreur = true;
}
} catch (NumberFormatException ex) {
// si rileva l'errore
erreur = true;
}
// numero di figli errato?
if (erreur) {
// si segnala l'errore
request.setAttribute("erreurNbEnfants", "Nombre d'enfants incorrect");
formulaireErroné = true;
}
// ID della persona
int id = Integer.parseInt(request.getParameter("id"));
// il modulo è errato?
if (formulaireErroné) {
// si visualizza nuovamente il modulo con i messaggi di errore
showFormulaire(request, response, "");
// fine
return;
}
// il modulo è corretto - si aggiorna la persona inserita nella sessione
// con le informazioni inviate dal cliente
Personne personne = (Personne)request.getSession().getAttribute("personne");
personne.setDatenaissance(datenaissance);
personne.setMarie(marie);
personne.setNbenfants(nbenfants);
personne.setNom(nom);
personne.setPrenom(prenom);
// persistenza
try {
if (id == -1) {
// creazione
service.saveOne(personne);
} else {
// aggiornamento
service.updateOne(personne);
}
} catch (DaoException ex) {
// il modulo viene visualizzato nuovamente con il messaggio relativo all'errore verificatosi
showFormulaire(request, response, ex.getMessage());
// fine
return;
}
// si reindirizza all'elenco delle persone
response.sendRedirect("list");
}
// visualizzazione del modulo precompilato
private void showFormulaire(HttpServletRequest request, HttpServletResponse response, String erreurEdit) throws ServletException, IOException {
// si prepara il modello della vista [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());
// visualizzazione della vista [edit]
getServletContext().getRequestDispatcher((String) params.get("urlEdit")).forward(request, response);
}
- righe 7-13: viene recuperato il parametro [prenom] della richiesta POST e ne viene verificata la validità. Se risulta errato, l’elemento [erreurPrenom] viene inizializzato con un messaggio di errore e inserito negli attributi della richiesta.
- righe 15-21: si procede in modo analogo per il parametro [nom]
- righe 23-30: si procede in modo analogo per il parametro [datenaissance]
- riga 32: si recupera il parametro [marie]. Non si verifica la sua validità perché, a priori, deriva dal valore di un pulsante di opzione. Detto questo, nulla impedisce a un programma di generare un [POST /.../do/validate] accompagnato da un parametro [marie] inventato. Dovremmo quindi verificare la validità di questo parametro. In questo caso, ci affidiamo alla nostra gestione delle eccezioni, che provoca la visualizzazione della pagina [exception.jsp] se il controller non le gestisce autonomamente. Se quindi la conversione del parametro [marie] in valore booleano fallisce alla riga 32, verrà generata un’eccezione che porterà all’invio della pagina [exception.jsp] al cliente. Questo funzionamento ci soddisfa.
- righe 34-50: si recupera il parametro [nbenfants] e se ne verifica il valore.
- riga 52: si recupera il parametro [id] senza verificarne il valore
- righe 54-59: se il modulo è errato, viene visualizzato nuovamente con i messaggi di errore generati in precedenza
- righe 62-67: se è valido, si crea un nuovo oggetto [Personne] con gli elementi del modulo
- righe 69-82: il profilo viene salvato. Il salvataggio potrebbe non andare a buon fine. In un ambiente multiutente, il profilo da modificare potrebbe essere stato eliminato o già modificato da qualcun altro. In questo caso, il livello [dao] genererà un'eccezione che viene gestita qui.
- riga 84: se non si è verificata alcuna eccezione, si reindirizza il cliente all’URL [/do/list] per mostrargli il nuovo stato del gruppo.
- riga 79: se si è verificata un’eccezione durante il salvataggio, si richiede nuovamente la visualizzazione del modulo iniziale passando il messaggio di errore dell’eccezione (terzo parametro).
Il metodo [showFormulaire] (righe 88-97) costruisce il modello necessario per la pagina [edit.jsp] con i valori inseriti (request.getParameter(" ... ")). Si ricorda che i messaggi di errore sono già stati inseriti nel modello dal metodo [doValidatePersonne]. La pagina [edit.jsp] viene visualizzata alla riga 99.
3.4.4. I test dell’applicazione web
Nel paragrafo 3.4.1 sono stati presentati alcuni test. Invitiamo il lettore a ripeterli. Mostriamo qui altre schermate che illustrano i casi di conflitti di accesso ai dati in un contesto multiutente:
[Firefox] sarà il browser dell’utente U1. Quest’ultimo richiede l’URL [http://localhost:8080/spring-jpa-hibernate-personnes-crud/do/list]:

[IE7] sarà il browser dell’utente U2. Quest’ultimo richiede lo stesso URL:

L’utente U1 accede alla modifica della persona [p2]:

L’utente U2 fa lo stesso:

L'utente U1 apporta delle modifiche e le conferma:
![]() |
Anche l'utente U2 fa lo stesso:
![]() |
L'utente U2 torna all'elenco delle persone tramite il link [Retour à la liste] del modulo:

Trova la persona [Lemarchand] così come è stata modificata da U1 (sposata, 2 figli). Il numero di versione di p2 è cambiato. Ora U2 elimina [p2]:
![]() |
U1 ha ancora il proprio elenco e vuole modificare nuovamente [p2]:
![]() |
U1 utilizza il collegamento [Retour à la liste] per vedere di cosa si tratta:

Scopre che, effettivamente, [p2] non fa più parte dell’elenco...
3.4.5. Versione 2
Modifichiamo leggermente la versione precedente per utilizzare gli archivi dei livelli [service, dao, jpa] anziché i loro codici sorgente:
![]() |
- in [1]: il nuovo progetto Eclipse. Si noti la scomparsa dei pacchetti [service, dao, entites]. Questi sono stati incapsulati nell'archivio [service-dao-jpa-personne.jar] [2] inserito in [WEB-INF/lib].
- La cartella del progetto si trova in [4]. La importeremo.
Non c’è altro da fare. Quando si avvia la nuova applicazione web e si richiede l’elenco delle persone, si riceve la seguente risposta:
![]() |
Hibernate non trova l’entità [Personne]. Per risolvere questo problema, è necessario dichiarare esplicitamente in [persistence.xml] le entità gestite:
<?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>
- riga 7: l'entità Personne viene dichiarata.
Una volta fatto ciò, l’eccezione scompare:
![]() |
3.4.6. Modificare l’implementazione JPA
![]() |
- in [1]: il nuovo progetto Eclipse
- in [2]: le librerie Toplink hanno sostituito le librerie Hibernate
- la cartella del progetto è in [4]. La importeremo.
Il cambio di implementazione JPA comporta solo alcune modifiche nel file [spring-config.xml]. Nient’altro cambia. Le modifiche apportate al file [spring-config.xml] sono state spiegate al paragrafo 3.1.9:
<?xml version="1.0" encoding="UTF-8"?>
<!-- JVM deve essere avviato con l'argomento -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>
Per passare da Hibernate a Toplink è necessario modificare solo poche righe:
- riga 11: l’implementazione JPA è ora gestita da Toplink
- riga 13: la proprietà [databasePlatform] ha un valore diverso rispetto a quello utilizzato con Hibernate: il nome di una classe specifica di Toplink. Dove trovare questo nome è stato spiegato nel paragrafo 2.1.15.2.
Questo è tutto. Si noti la facilità con cui è possibile modificare SGBD o l’implementazione JPA con Spring. Tuttavia, non abbiamo ancora del tutto finito. Quando si esegue l’applicazione, si verifica un’eccezione:
![]() |
Si tratta di un problema già riscontrato e descritto nel paragrafo 3.1.9. Si risolve avviando JVM con un agente Spring. A tal fine, si modifica la configurazione di avvio di Tomcat:
![]() |
- in [1]: è stata selezionata l’opzione [Run / Run...] per modificare la configurazione di Tomcat
- in [2]: è stata selezionata la scheda [Arguments]
- in [3]: è stato aggiunto il parametro -javaagent come descritto al paragrafo 3.1.9.
Fatto ciò, è possibile richiedere l'elenco delle persone:

3.5. Altri esempi
Avremmo voluto mostrare un esempio web in cui il contenitore Spring fosse sostituito dal contenitore Jboss Ejb3 esaminato al paragrafo 3.2:
![]() |
- in [1]: il progetto Eclipse
- in [3]: la sua posizione nella cartella degli esempi. Lo importeremo.
Abbiamo ripreso la configurazione [jboss-config.xml, persistence.xml] descritta nel paragrafo 3.2, quindi abbiamo modificato il metodo [init] del controller [Application.java] nel modo seguente:
// init
@SuppressWarnings("unchecked")
public void init() throws ServletException {
try {
// si recuperano i parametri di inizializzazione del servlet
ServletConfig config = getServletConfig();
// si elaborano gli altri parametri di inizializzazione
String valeur = null;
for (int i = 0; i < paramètres.length; i++) {
// valore del parametro
valeur = config.getInitParameter(paramètres[i]);
// il parametro è presente?
if (valeur == null) {
// si registra l'errore
erreursInitialisation.add("Le paramètre [" + paramètres[i] + "] n'a pas été initialisé");
} else {
// si memorizza il valore del parametro
params.put(paramètres[i], valeur);
}
}
// l'URL della vista [erreurs] richiede un trattamento particolare
urlErreurs = config.getInitParameter("urlErreurs");
if (urlErreurs == null)
throw new ServletException("Le paramètre [urlErreurs] n'a pas été initialisé");
// configurazione dell'applicazione
// si avvia il contenitore EJB3 JBoss
// vengono utilizzati i file di configurazione ejb3-interceptors-aop.xml e embedded-jboss-beans.xml
EJB3StandaloneBootstrap.boot(null);
// Creazione dei bean specifici dell'applicazione
EJB3StandaloneBootstrap.deployXmlResource("META-INF/jboss-config.xml");
// Vengono distribuiti tutti i file EJB trovati nel classpath dell'applicazione
//EJB3StandaloneBootstrap.scanClasspath("WEB-INF/classes".replace("/", File.separator));
EJB3StandaloneBootstrap.scanClasspath();
// Si inizializza il contesto JNDI. Viene utilizzato il file jndi.properties
InitialContext initialContext = new InitialContext();
// istanziazione del livello di servizio
service = (IService) initialContext.lookup("Service/local");
// si svuota il database
clean();
// si riempie il database
fill();
} catch (Exception e) {
throw new ServletException(e);
}
}
- righe 28-38: si avvia il contenitore Ejb3, che sostituisce il contenitore Spring.
- riga 41: si richiede un riferimento al livello [service] dell’applicazione.
A prima vista, queste sono le uniche modifiche da apportare. All’esecuzione, si verifica il seguente errore:
![]() |
Non sono riuscito a capire dove fosse esattamente il problema. L’eccezione segnalata da Tomcat sembra indicare che l’oggetto denominato "TransactionManager" sia stato richiesto al servizio JNDI e che quest’ultimo non lo riconoscesse. Lascio ai lettori il compito di trovare una soluzione a questo problema. Se verrà trovata una soluzione, verrà integrata nel documento.





























































