Skip to content

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:

Image

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:

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

3.1.7. 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:

Image

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:

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

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:

Image

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:

Image

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&eacute;nom</th>
                <th>Nom</th>
                <th>Date de naissance</th>
                <th>Mari&eacute;</th>
                <th>Nombre d'enfants</th>
                <th></th>
            </tr>
            <c:forEach var="personne" items="${personnes}">
                <tr>
                    <td><c:out value="${personne.id}"/></td>
                    <td><c:out value="${personne.version}"/></td>
                    <td><c:out value="${personne.prenom}"/></td>
                    <td><c:out value="${personne.nom}"/></td>
                    <td><dt:format pattern="dd/MM/yyyy">${personne.datenaissance.time}</dt:format></td>
                    <td><c:out value="${personne.marie}"/></td>
                    <td><c:out value="${personne.nbenfants}"/></td>
                    <td><a href="<c:url value="/do/edit?id=${personne.id}"/>">Modifier</a></td>
                    <td><a href="<c:url value="/do/delete?id=${personne.id}"/>">Supprimer</a></td>
                </tr>
            </c:forEach>
        </table>
        <br>
        <a href="<c:url value="/do/edit?id=-1"/>">Ajout</a>
    </body>
</html>
  • 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]:

Image

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&eacute;nom</td>
                    <td>
                        <input type="text" value="${prenom}" name="prenom" size="20">
                    </td>
                    <td>${erreurPrenom}</td>
                </tr>
                <tr>
                    <td>Nom</td>
                    <td>
                        <input type="text" value="${nom}" name="nom" size="20">
                    </td>
                    <td>${erreurNom}</td>
                </tr>
                <tr>
                <td>Date de naissance (JJ/MM/AAAA)</td>
                    <td>
                        <input type="text" value="${datenaissance}" name="datenaissance">
                    </td>
                    <td>${erreurDateNaissance}</td>
                </tr>
                <tr>
                    <td>Mari&eacute;</td>
                    <td>
                        <c:choose>
                            <c:when test="${marie}">
                                <input type="radio" name="marie" value="true" checked>Oui
                                <input type="radio" name="marie" value="false">Non
                            </c:when>
                            <c:otherwise>
                                <input type="radio" name="marie" value="true">Oui
                                <input type="radio" name="marie" value="false" checked>Non
                            </c:otherwise>
                        </c:choose>
                    </td>
                </tr>
                <tr>
                    <td>Nombre d'enfants</td>
                    <td>
                        <input type="text" value="${nbenfants}" name="nbenfants">
                    </td>
                    <td>${erreurNbEnfants}</td>
                </tr>
            </table>
            <br>
            <input type="hidden" value="${id}" name="id">
      <input type="hidden" value="${version}" name="version">
            <input type="submit" value="Valider">
            <a href="<c:url value="/do/list"/>">Annuler</a>
        </form>
    </body>
</html>

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
ID della persona
aggiornata
idem
version
la sua versione
idem
prenom
il suo nome
nome inserito
nom
il suo cognome
cognome inserito
datenaissance
Data di nascita
data di nascita inserita
marie
stato civile
stato civile inserito
nbenfants
numero di figli
numero di figli inserito
erreurEdit
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.
erreurPrenom
vuoto
segnala un nome errato – vuoto in caso contrario
erreurNom
vuoto
segnala un cognome errato – vuoto in caso contrario
erreurDateNaissance
vuoto
segnala una data di nascita errata – vuoto in caso contrario
erreurNbEnfants
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 &agrave; 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:

Image

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]:

Image


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]:

Image

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
/do/list
GET
doListPersonnes
/do/edit
GET
doEditPersonne
/do/validate
POST
doValidatePersonne
/do/delete
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:

Image

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]:

Image

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:

Image

  • 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]:

Image

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:
    1. 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].
    2. 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.
    3. 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]:

Image

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]:

Image

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

Image

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

Image

L’utente U2 fa lo stesso:

Image

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:

Image

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:

Image

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:

Image

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.