Skip to content

2. Articolo 1 - Spring IoC

Obiettivi del presente documento:

  • esplorare le possibilità di configurazione e integrazione del framework Spring (http://www.springframework.org)
  • definire e utilizzare il concetto di IoC (Inversion of Control), noto anche come Dependency Injection

2.1. Configurazione di un'applicazione a 3 livelli con Spring

Consideriamo una classica applicazione a 3 livelli:

Supponiamo che l'accesso ai livelli business e DAO sia controllato da interfacce Java:

  1. l'interfaccia [IArticlesDao] per il livello di accesso ai dati
  1. l'interfaccia [IArticlesManager] per il livello business

Nel livello di accesso ai dati, o livello DAO (Data Access Object), è comune lavorare con un DBMS e quindi con un driver JDBC. Consideriamo lo scheletro di una classe che accede a una tabella di articoli in un DBMS:

public class ArticlesDaoPlainJdbc implements IArticlesDao {

     // connection to data source
    private String driverClassName=null;
    private Connection connexion=null;
    private String url = null;
    private String user = null;
    private String pwd = null;
 ....

    public List getAllArticles() {
        // the list of items is requested
        try {
             // load the JDBC driver
            Class.forName(driverClassName);
            // create a connection to BD
            connexion = DriverManager.getConnection(url, user, pwd);
            ...
        } catch (SQLException ex) {
            ...
        } finally {
            ...
        }
    }

Per eseguire un'operazione sul DBMS, ogni metodo richiede un oggetto [Connection] che rappresenta la connessione al database, attraverso la quale avverrà lo scambio di dati tra il database e il codice Java. Per creare questo oggetto, sono necessarie quattro informazioni:

String driverClassName
il nome della classe del driver JDBC del DBMS
String url
l'URL JDBC del database da utilizzare
Stringa utente
le credenziali utilizzate per stabilire la connessione
Stringa pwd
La password per questo nome utente

In che modo la nostra precedente classe [ArticlesDaoPlainJdbc] può ottenere queste informazioni? Esistono diverse possibilità:

Soluzione 1 - le informazioni sono hard-coded nella classe:

1
2
3
4
5
6
7
8
public class ArticlesDaoPlainJdbc implements IArticlesDao {

     // connection to data source
    private final String driverClassName = "org.firebirdsql.jdbc.FBDriver";
    private String url = "jdbc:firebirdsql:localhost/3050:d:/databases/dbarticles.gdb";
    private String user = "someone";
    private String pwd = "somepassword";
 ....

Lo svantaggio di questa soluzione è che è necessario modificare il codice Java ogni volta che una di queste informazioni cambia, ad esempio quando si cambia la password.

Soluzione 2 - le informazioni vengono passate all'oggetto durante la sua creazione:

public class ArticlesDaoPlainJdbc implements IArticlesDao {

     // connection to data source
    private final String driverClassName;
    private String url;
    private String user;
    private String pwd;
 ....
    public ArticlesDaoPlainJdbc(String driverClassName,String url,String user,String pwd) {
      this.driverClassName=driverClassName;
    this.url=url;
    this.user=user;
    this.pwd=pwd;
    ...
    }

In questo caso, l'oggetto riceve le informazioni necessarie al suo funzionamento al momento della sua creazione. Il problema si sposta quindi sul codice che gli ha fornito le quattro informazioni. Come le ha ottenute? La seguente classe [ArticlesManagerWithDataBase] nel livello business potrebbe creare un oggetto [ArticlesDaoPlainJdbc] dal livello di accesso ai dati:

public class ArticlesManagerWithDataBase implements IArticlesManager {

    // a data access instance
    private IArticlesDao articlesDao;
 ....
    public ArticlesManagerWithDataBase (String driverClassName, String url, String user, String pwd, ...) {
        ... 
         // creation of a data access service
        articlesDao =(IArticlesDao)new ArticlesDaoPlainJdbc(driverClassName,url,user,pwd);
    ...
    }

    public ... doSomething(...){
        ...
    }
}

Possiamo notare che, ancora una volta, le informazioni necessarie per costruire l'oggetto [ArticlesDaoPlainJdbc] vengono fornite al costruttore dell'oggetto [ArticlesManagerWithDataBase]. Possiamo immaginare che queste informazioni gli vengano passate da un livello superiore, come il livello dell'interfaccia utente. Raggiungiamo così gradualmente il livello più alto dell'applicazione. A causa della sua posizione, questo livello non viene chiamato da un livello che potrebbe passargli le informazioni di configurazione di cui ha bisogno. Dobbiamo quindi trovare un'alternativa alla configurazione basata sul costruttore. L'approccio standard per configurare un'applicazione al suo livello più alto consiste nell'utilizzare un file contenente tutte le informazioni suscettibili di cambiare nel tempo. Potrebbero esserci più file di questo tipo. All'avvio dell'applicazione, un livello di inizializzazione creerà quindi tutti o parte degli oggetti richiesti dai vari livelli dell'applicazione.

Esiste un'ampia varietà di file di configurazione. La tendenza attuale è quella di utilizzare file XML. Questo è l'approccio adottato da Spring. Il file che configura un oggetto [ArticlesDaoPlainJdbc] potrebbe apparire così:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
     <!-- data access class -->
    <bean id="articlesDao" class="istia.st.articles.dao.ArticlesDaoPlainJdbc">
        <constructor-arg index="0">
            <value>org.firebirdsql.jdbc.FBDriver</value>
        </constructor-arg>
        <constructor-arg index="1">
            <value>jdbc:firebirdsql:localhost/3050:d:/databases/dbarticles.gdb</value>
        </constructor-arg>
        <constructor-arg index="2">
            <value>someone</value>
        </constructor-arg>
        <constructor-arg index="3">
            <value>somepassword</value>
        </constructor-arg>
    </bean>
</beans>

Un'applicazione è un insieme di oggetti che Spring chiama bean, poiché seguono lo standard JavaBean per la denominazione degli accessori e degli inizializzatori (getter/setter) dei campi privati di un oggetto. Gli oggetti in un'applicazione che servono a uno scopo specifico sono spesso creati come singola istanza. Questi sono chiamati singleton. Pertanto, nel nostro esempio di applicazione multilivello qui discusso, l'accesso al database degli articoli sarà gestito da una singola istanza della classe [ArticlesDaoPlainJdbc]. Per un'applicazione web, questi oggetti di servizio servono più client contemporaneamente. Non viene creato un oggetto di servizio per ogni client.

Il file di configurazione Spring sopra riportato consente la creazione di un singolo oggetto di servizio di tipo [ArticlesDaoPlainJdbc] in un pacchetto denominato [istia.st.articles.dao]. Le quattro informazioni richieste dal costruttore di questo oggetto sono definite all'interno di un tag <bean>...</bean>. Ci saranno tanti tag <bean> quanti sono i singleton da creare.

Quando vengono creati gli oggetti definiti nel file Spring? L'inizializzazione dell'applicazione può essere gestita dal metodo main dell'applicazione, se presente. Nel caso di un'applicazione web, potrebbe trattarsi del metodo [init] del servlet principale. Ogni applicazione dispone di un metodo che viene eseguito per primo. È generalmente all'interno di questo metodo che avviene la creazione dei singleton.

Facciamo un esempio. Supponiamo di voler testare la classe [ArticlesDaoPlainJdbc] precedente utilizzando un test JUnit. Una classe di test JUnit ha un metodo [setUp] che viene eseguito prima di qualsiasi altro metodo. È qui che creeremo il singleton [ArticlesDaoPlainJdbc].

Se seguiamo l'approccio di passare le informazioni di configurazione tramite il costruttore, avremo la seguente classe di test:

public class TestArticlesPlainJdbc extends TestCase {
    // tests the ArticlesDaoPlainJdbc item access class
     // the data source is defined in sprintest

     // an instance of the class under test
    private IArticlesDao articlesDao;

    protected void setUp() throws Exception{
        // retrieves a data access instance
        articlesDao =
            (IArticlesDao) new ArticlesDaoPlainJdbc("org.firebirdsql.jdbc.FBDriver",
                "jdbc:firebirdsql:localhost/3050:d:/databases/dbarticles.gdb","someone","somepassword");
    }

La classe chiamante [TestArticlesPlainJdbc] deve conoscere le quattro informazioni necessarie per inizializzare il singleton [ArticlesDaoPlainJdbc] da costruire.

Se seguiamo l'approccio che prevede il passaggio delle informazioni di configurazione tramite un file di configurazione, potremmo avere la seguente classe di test utilizzando il file Spring descritto sopra.

public class TestSpringArticlesPlainJdbc extends TestCase {
    // tests the ArticlesDaoJdbc item access class
     // the data source is defined in sprintest

     // an instance of the class under test
    private IArticlesDao articlesDao;

    protected void setUp() throws Exception {
      // retrieves a data access instance
      articlesDao = (IArticlesDao) (new XmlBeanFactory(new ClassPathResource(
          "springArticlesPlainJdbc.xml"))).getBean("articlesDao");
    }

Qui, la classe chiamante [TestSpringArticlesPlainJdbc] non ha bisogno di conoscere le informazioni necessarie per inizializzare il singleton da costruire. Deve semplicemente sapere:

  1. [springArticlesPlainJdbc.xml]: il nome del file di configurazione Spring descritto sopra
  2. [articlesDao]: il nome del singleton da creare

Una modifica al file di configurazione, al di fuori di queste due entità, non ha alcun impatto sul codice Java. Questo metodo di configurazione degli oggetti di un'applicazione è molto flessibile. Per configurarsi, l'applicazione deve conoscere solo due cose:

  • il nome del file Spring contenente le definizioni dei singleton da creare
  • i nomi di questi singleton, che il codice Java utilizza per ottenere un riferimento agli oggetti a cui sono stati associati tramite il file di configurazione

2.2. Iniezione delle dipendenze e inversione di controllo

Introduciamo ora il concetto di iniezione delle dipendenze utilizzato da Spring per configurare le applicazioni. Viene utilizzato anche il termine inversione di controllo (IoC). Consideriamo la costruzione del singleton [ArticlesManagerWithDataBase] nel livello business della nostra applicazione:

Per accedere ai dati dal DBMS, il livello business deve utilizzare i servizi di un oggetto che implementa l'interfaccia [IArticlesDao], ad esempio un oggetto di tipo [ArticlesDaoPlainJdbc]. Il codice per la classe [ArticlesManagerWithDataBase] potrebbe essere simile al seguente:

public class ArticlesManagerWithDataBase implements IArticlesManager {

     // a data access instance
    private IArticlesDao articlesDao;
 ....
    public ArticlesManagerWithDataBase (String driverClassName, String url, String user, String pwd, ...) {
        ... 
         // creation of a data access service
        articlesDao =(IArticlesDao)new ArticlesDaoPlainJdbc(driverClassName,url,user,pwd);
    ...
    }

    public ... doSomething(...){
        ...
    }
}

La classe [ArticlesDaoPlainJdbc] dovrebbe implementare l'interfaccia [IArticlesDao] qui:

public class ArticlesDaoPlainJdbc implements IArticlesDao {...}

Per creare il singleton [IArticlesDao] necessario al funzionamento della classe, il suo costruttore utilizza esplicitamente il nome della classe che implementa l'interfaccia [IArticlesDao]:

articlesDao =(IArticlesDao) new ArticlesDaoPlainJdbc(...);

Abbiamo quindi una dipendenza hard-coded dal nome della classe nel codice. Se la classe che implementa l'interfaccia [IArticlesDao] dovesse cambiare, il codice nel costruttore precedente dovrebbe essere modificato. Abbiamo le seguenti relazioni tra gli oggetti:

La classe [ArticlesManagerWithDataBase] stessa prende l'iniziativa di creare l'oggetto [ArticlesDaoPlainJdbc] di cui ha bisogno. Tornando al termine "inversione di controllo", possiamo dire che è quella che ha il "controllo" per creare l'oggetto di cui ha bisogno.

Se dovessimo scrivere una classe di test JUnit per la classe [ArticlesManagerWithDataBase], potrebbe assomigliare a qualcosa del genere:

public class TestArticlesManagerWithDataBase extends TestCase {
    // an instance of the business class under test
    private IArticlesManager articlesManager;

    protected void setUp() throws Exception {
        // creates an instance of the business class under test
        articlesManager =
            (IArticlesManager) new ArticlesManagerWithDataBase("org.firebirdsql.jdbc.FBDriver",
                "jdbc:firebirdsql:localhost/3050:d:/databases/dbarticles.gdb","someone","somepassword");
    }

La classe di test crea un'istanza della classe di business [ArticlesManagerWithDataBase], che a sua volta crea un'istanza della classe di accesso ai dati [ArticlesDaoPlainJdbc] nel proprio costruttore.

La soluzione Spring elimina la necessità che la classe di business [ArticlesManagerWithDataBase] conosca il nome [ArticlesDaoPlainJdbc] della classe di accesso ai dati di cui ha bisogno. Ciò consente di cambiare la classe senza modificare il codice Java della classe di business. Spring consente la creazione simultanea di entrambi i singleton: uno per il livello di accesso ai dati e uno per il livello di business. Il file di configurazione Spring definirà un nuovo bean:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
     <!-- data access class -->
    <bean id="articlesDao" class="istia.st.articles.dao.ArticlesDaoPlainJdbc">
        <constructor-arg index="0">
            <value>org.firebirdsql.jdbc.FBDriver</value>
        </constructor-arg>
        <constructor-arg index="1">
            <value>jdbc:firebirdsql:localhost/3050:d:/databases/dbarticles.gdb</value>
        </constructor-arg>
        <constructor-arg index="2">
            <value>someone</value>
        </constructor-arg>
        <constructor-arg index="3">
            <value>somepassword</value>
        </constructor-arg>
    </bean>
    <bean id="articlesManager" class="istia.st.articles.domain.ArticlesManagerWithDataBase">
        <property name="articlesDao">
            <ref bean="articlesDao"/>
        </property>
    </bean>
</beans>

La novità è rappresentata dal bean che definisce il singleton della classe di business da creare:

    <bean id="articlesManager" class="istia.st.articles.domain.ArticlesManagerWithDataBase">
        <property name="articlesDao">
            <ref bean="articlesDao"/>
        </property>
    </bean>
  1. La classe che implementa il bean [articlesManager] è definita: [ArticlesManagerWithDataBase]
  2. Al campo [articlesDao] del bean viene assegnato un valore tramite il tag <property name="articlesDao">. Questo è il campo definito nella classe [ArticlesManagerWithDataBase]:
public class ArticlesManagerWithDataBase implements IArticlesManager {

  // data access interface
  private IArticlesDao articlesDao;

  public IArticlesDao getArticlesDao() {
    return articlesDao;
  }

  public void setArticlesDao(IArticlesDao articlesDao) {
    this.articlesDao = articlesDao;
  }

Affinché il campo [articlesDao] venga inizializzato da Spring e dal suo tag <property>, il campo deve seguire lo standard JavaBean e deve essere presente un metodo [setArticlesDao] per inizializzare il campo [articlesDao]. Si noti che il nome del metodo deriva proprio dal nome del campo. Analogamente, spesso è presente un metodo [get...] per recuperare il valore del campo. In questo caso, si tratta del metodo [getArticlesDao]. In questa nuova versione, la classe [ArticlesManagerWithDataBase] non ha più un costruttore. Non ne ha più bisogno.

  • Il valore che Spring assegnerà al campo [articlesDao] è quello del bean [articlesDao] definito nel suo file di configurazione:
    <bean id="articlesManager" class="istia.st.articles.domain.ArticlesManagerWithDataBase">
        <property name="articlesDao">
            <ref bean="articlesDao"/>
        </property>
    </bean>
    <bean id="articlesDao" class="istia.st.articles.dao.ArticlesDaoPlainJdbc">
        <constructor-arg index="0">
    .............
    </bean>
  • Quando Spring costruisce il singleton [ArticlesManagerWithDataBase], creerà anche il singleton [ArticlesDaoPlainJdbc]:
    • Spring stabilirà un grafico delle dipendenze dei bean e vedrà che il bean [articlesManager] dipende dal bean [articlesDao]
    • costruirà il bean [articlesDao], ovvero un oggetto di tipo [ArticlesDaoPlainJdbc]
    • quindi costruirà il bean [articlesManager] di tipo [ArticlesManagerWithDataBase]

Ora immaginiamo un test JUnit per la classe [ArticlesManagerWithDataBase]. Potrebbe apparire come segue:

public class TestSpringArticlesManagerWithDataBase extends TestCase {
    // test business class [ArticlesManagerWithDataBase]

    // an instance of the business class under test
    private IArticlesManager articlesManager;

    protected void setUp() throws Exception {
      // retrieves a data access instance
      articlesManager = (IArticlesManager) (new XmlBeanFactory(new ClassPathResource(
          "springArticlesManagerWithDataBase.xml"))).getBean("articlesManager");
    }

Seguiamo il processo di creazione dei due singleton definiti nel file Spring denominato [springArticlesManagerWithDataBase.xml].

  • Il metodo [setUp] sopra riportato richiede un riferimento al bean denominato [articlesManager]
  • Spring consulta il proprio file di configurazione e trova il bean [articlesManager]. Se è già stato creato, restituisce semplicemente un riferimento all'oggetto (singleton); altrimenti, lo crea.
  • Spring rileva la dipendenza del bean [articlesManager] dal bean [articlesDao]. Crea quindi il singleton [articlesDao] di tipo [ArticlesDaoPlainJdbc] se non è già stato creato (come singleton).
  • Crea il singleton [articlesManager] di tipo [ArticlesManagerWithDataBase]

Questo meccanismo potrebbe essere rappresentato graficamente come segue:

Ricordiamo lo scheletro della classe [ArticlesManagerWithDataBase]:

public class ArticlesManagerWithDataBase implements IArticlesManager {

  // data access interface
  private IArticlesDao articlesDao;

  public IArticlesDao getArticlesDao() {
    return articlesDao;
  }

  public void setArticlesDao(IArticlesDao articlesDao) {
    this.articlesDao = articlesDao;
  }

Una volta che Spring ha terminato la creazione dei singleton, abbiamo un oggetto di tipo [ArticlesManagerWithDataBase] il cui campo [articlesDao] è inizializzato senza che esso ne sia a conoscenza. Diciamo che abbiamo iniettato una dipendenza nell'oggetto [ArticlesManagerWithDataBase]. Si dice anche che si è invertito il controllo: non è più l'oggetto [ArticlesManagerWithDataBase] a prendere l'iniziativa di creare l'oggetto che implementa l'interfaccia [IArticlesDao] di cui ha bisogno; piuttosto, è l'applicazione di primo livello (al momento dell'inizializzazione) che si occupa di creare tutti gli oggetti di cui ha bisogno gestendone le interdipendenze.

Il vantaggio principale della configurazione del singleton [ArticlesManagerWithDataBase] tramite un file Spring è che ora possiamo cambiare la classe di implementazione corrispondente al campo [articlesDao] della classe [ArticlesManagerWithDataBase] senza modificarne il codice. Tutto ciò che dobbiamo fare è cambiare il nome della classe nella definizione del bean [articlesDao] nel file Spring:

    <bean id="articlesDao" class="istia.st.articles.dao.ArticlesDaoPlainJdbc">
...
    </bean>

diventerà, ad esempio:

    <bean id="articlesDao" class="istia.st.articles.dao.ArticlesDaoIbatisSqlMap">
...
    </bean>

Il bean [ArticlesManagerWithDataBase] funzionerà con questa nuova classe di accesso ai dati senza nemmeno rendersene conto.

2.3. Spring IoC nella pratica

2.3.1. Esempio 1

Consideriamo la seguente classe:

package istia.st.springioc.domain;

public class Personne {
  private String nom;
  private int age;

   // person display
  public String toString() {
    return "nom=[" + this.nom + "], age=[" + this.age + "]";
  }

   // init-close
  public void init() {
    System.out.println("init personne [" + this.toString() + "]");
  }

  public void close() {
    System.out.println("destroy personne [" + this.toString() + "]");
  }

   // getters-setters
  public int getAge() {
    return age;
  }

  public void setAge(int age) {
    this.age = age;
  }

  public String getNom() {
    return nom;
  }

  public void setNom(String nom) {
    this.nom = nom;
  }
}

La classe ha:

  • due campi privati, nome ed età
  • metodi getter e setter per questi due campi
  • un metodo toString per recuperare il valore dell'oggetto [Person] come stringa
  • un metodo init che verrà chiamato da Spring quando l'oggetto viene creato, e un metodo close che verrà chiamato quando l'oggetto viene distrutto

Per creare oggetti di tipo [Person], useremo il seguente file Spring:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" 
    "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
    <bean id="personne1" class="istia.st.springioc.domain.Personne" 
        init-method="init" destroy-method="close">
        <property name="nom">
            <value>Simon</value>
        </property>
        <property name="age">
            <value>40</value>
        </property>
    </bean>
    <bean id="personne2" class="istia.st.springioc.domain.Personne" 
        init-method="init" destroy-method="close">
        <property name="nom">
            <value>Brigitte</value>
        </property>
        <property name="age">
            <value>20</value>
        </property>
    </bean>
</beans>

Questo file si chiamerà config.xml.

  • Definisce due bean con le rispettive chiavi "person1" e "person2" di tipo [Person]
  • Inizializza i campi [name, age] per ciascuna persona
  • Definisce i metodi da chiamare durante la costruzione iniziale dell'oggetto [init-method] e durante la distruzione dell'oggetto [destroy-method]

Per i nostri test, useremo una singola classe di test JUnit alla quale aggiungeremo progressivamente dei metodi. La prima versione di questa classe sarà la seguente:

package istia.st.springioc.tests;

import istia.st.springioc.domain.Personne;
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.beans.factory.xml.XmlBeanFactory;
import org.springframework.core.io.ClassPathResource;
import junit.framework.TestCase;

public class Tests extends TestCase {

   // bean factory
  private ListableBeanFactory bf;

   // init tests
  public void setUp() {
    bf = new XmlBeanFactory(new ClassPathResource("config.xml"));
  }

  public void test1() {
    // retrieve [Person] bean keys from the Spring file
    Personne personne1 = (Personne) bf.getBean("personne1");
    System.out.println("personne1=" + personne1.toString());
    Personne personne2 = (Personne) bf.getBean("personne2");
    System.out.println("personne2=" + personne2.toString());
    personne2 = (Personne) bf.getBean("personne2");
    System.out.println("personne2=" + personne2.toString());
  }
}

Commenti:

  • Per recuperare i bean definiti nel file [config.xml], utilizziamo un oggetto di tipo [ListableBeanFactory]. Esistono altri tipi di oggetti che consentono l'accesso ai bean. L'oggetto [ListableBeanFactory] viene ottenuto nel metodo [setUp] della classe di test e memorizzato in una variabile privata. Sarà quindi disponibile per tutti i metodi di test.
  • Il file [config.xml] verrà collocato nel [ClassPath] dell'applicazione, ovvero in una delle directory cercate dalla Java Virtual Machine quando cerca una classe a cui fa riferimento l'applicazione. L'oggetto [ClassPathResource] viene utilizzato per cercare una risorsa nel [ClassPath] di un'applicazione, in questo caso il file [config.xml].
  • Spring può utilizzare file di configurazione in vari formati. L'oggetto [XmlBeanFactory] viene utilizzato per analizzare un file di configurazione in formato XML.
  • L'elaborazione di un file Spring restituisce un oggetto di tipo [ListableBeanFactory], in questo caso l'oggetto bf. Con questo oggetto, un bean identificato dalla chiave C viene ottenuto tramite bf.getBean(C).
  • Il metodo [test1] recupera e visualizza i valori dei bean con le chiavi "person1" e "person2".

La struttura del progetto Eclipse della nostra applicazione è la seguente:

Image

Commenti:

  • La cartella [src] contiene il codice sorgente. Il codice compilato verrà inserito in una cartella [bin] non mostrata qui.
  • Il file [config.xml] si trova nella directory principale della cartella [src]. La compilazione del progetto lo copia automaticamente nella cartella [bin], che fa parte del [ClassPath] dell'applicazione. È qui che viene cercato dall'oggetto [ClassPathResource].
  • La cartella [lib] contiene tre librerie Java richieste dall'applicazione:
      • commons-logging.jar e spring-core.jar per le classi Spring
      • junit.jar per le classi JUnit
  • Anche la cartella [lib] fa parte del [ClassPath] dell'applicazione

L'esecuzione del metodo [test1] del test JUnit produce i seguenti risultati:

18 sept. 2004 11:28:53 org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions
INFO: Loading XML bean definitions from class path resource [config.xml]
18 sept. 2004 11:28:53 org.springframework.beans.factory.support.AbstractBeanFactory getBean
INFO: Creating shared instance of singleton bean 'personne1'
init personne [nom=[Simon], age=[40]]
personne1=nom=[Simon], age=[40]
18 sept. 2004 11:28:53 org.springframework.beans.factory.support.AbstractBeanFactory getBean
INFO: Creating shared instance of singleton bean 'personne2'
init personne [nom=[Brigitte], age=[20]]
personne2=nom=[Brigitte], age=[20]
personne2=nom=[Brigitte], age=[20]

Commenti:

  • Spring registra una serie di eventi utilizzando la libreria [commons-logging.jar]. Questi log ci aiutano a comprendere meglio il funzionamento di Spring.
  • Il file [config.xml] è stato caricato e quindi elaborato
  • L'operazione*
Personne personne1 = (Personne) bf.getBean("personne1");

ha provocato la creazione del bean [person1]. Possiamo vedere il log di Spring relativo a questo evento. Poiché nella definizione del bean [person1] avevamo specificato [init-method="init"], è stato eseguito il metodo [init] dell'oggetto [Person] creato. Viene visualizzato il messaggio corrispondente.

  • L'operazione
System.out.println("personne1=" + personne1.toString());

ha visualizzato il valore dell'oggetto [Person] creato.

  • Lo stesso fenomeno si verifica per il bean chiave [person2].
  • L'ultima operazione
    personne2 = (Personne) bf.getBean("personne2");
    System.out.println("personne2=" + personne2.toString());

non ha portato alla creazione di un nuovo oggetto di tipo [Person]. Se così fosse stato, sarebbe stato visualizzato il metodo [init], cosa che qui non è avvenuta. Questo è il principio del singleton. Per impostazione predefinita, Spring crea una sola istanza dei bean nel suo file di configurazione. Si tratta di un servizio di riferimento agli oggetti. Se gli viene richiesto il riferimento a un oggetto che non è ancora stato creato, lo crea e restituisce un riferimento. Se l'oggetto è già stato creato, Spring restituisce semplicemente un riferimento ad esso.

  • Si noti che non c'è traccia del metodo [close] dell'oggetto [Person], anche se avevamo scritto [destroy-method=close] nella definizione del bean. È possibile che questo metodo venga eseguito solo quando la memoria occupata dall'oggetto viene recuperata dal garbage collector. Quando ciò accade, l'applicazione è già terminata e la scrittura sullo schermo non ha alcun effetto. Da verificare.

Ora che abbiamo trattato le basi della configurazione di Spring, potremo procedere con le nostre spiegazioni un po' più rapidamente.

2.3.2. Esempio 2

Consideriamo la seguente nuova classe [Car]:

package istia.st.springioc.domain;

public class Voiture {
  private String marque;
  private String type;
  private Personne propriétaire;

   // manufacturers

  public Voiture() {
  }

  public Voiture(String marque, String type, Personne propriétaire) {
    this.marque = marque;
    this.type = type;
    this.propriétaire = propriétaire;
  }

   // toString
  public String toString() {
    return "Voiture : marque=[" + this.marque + "] type=[" + this.type
        + "] propriétaire=[" + this.propriétaire + "]";
  }

     // getters-setters
  public String getMarque() {
    return marque;
  }

  public void setMarque(String marque) {
    this.marque = marque;
  }

  public Personne getPropriétaire() {
    return propriétaire;
  }

  public void setPropriétaire(Personne propriétaire) {
    this.propriétaire = propriétaire;
  }

  public String getType() {
    return type;
  }

  public void setType(String type) {
    this.type = type;
  }

   // init-close
  public void init() {
    System.out.println("init voiture [" + this.toString() + "]");
  }

  public void close() {
    System.out.println("destroy voiture [" + this.toString() + "]");
  }

}

La classe ha:

  • tre campi privati: type, make e owner. Questi campi possono essere inizializzati e letti utilizzando i metodi pubblici get e set. Possono anche essere inizializzati utilizzando il costruttore Car(String, String, Person). La classe dispone inoltre di un costruttore senza argomenti per conformarsi allo standard JavaBean.
  • un metodo toString per recuperare il valore dell'oggetto [Car] come stringa
  • un metodo init che verrà chiamato da Spring immediatamente dopo la creazione dell'oggetto, un metodo close che verrà chiamato quando l'oggetto viene distrutto

Per creare oggetti di tipo [Car], useremo il seguente file Spring [config.xml]:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" 
    "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
    <bean id="personne1" class="istia.st.springioc.domain.Personne" 
        init-method="init" destroy-method="close">
        <property name="nom">
            <value>Simon</value>
        </property>
        <property name="age">
            <value>40</value>
        </property>
    </bean>
    <bean id="personne2" class="istia.st.springioc.domain.Personne" 
        init-method="init" destroy-method="close">
        <property name="nom">
            <value>Brigitte</value>
        </property>
        <property name="age">
            <value>20</value>
        </property>
    </bean>
    <bean id="voiture1" class="istia.st.springioc.domain.Voiture" 
        init-method="init" destroy-method="close">
        <constructor-arg index="0">
            <value>Peugeot</value>
        </constructor-arg>
        <constructor-arg index="1">
            <value>307</value>
        </constructor-arg>
        <constructor-arg index="2">
            <ref bean="personne2"></ref>
        </constructor-arg>
    </bean>
</beans>

Questo file aggiunge un bean con la chiave "car1" di tipo [Car] alle definizioni precedenti. Per inizializzare questo bean, avremmo potuto scrivere:

    <bean id="voiture1" class="istia.st.springioc.domain.Voiture" 
        init-method="init" destroy-method="close">
        <property name="marque">
            <value>Peugeot</value>
        </property>
        <property name="type">
            <value>307</value>
        </property>
        <property name="propriétaire">
            <ref bean="personne2"/>
        </property>
    </bean>

Anziché scegliere il metodo già presentato, abbiamo scelto qui di utilizzare il costruttore Car(String, String, Person) della classe. Inoltre, il bean [car1] definisce il metodo da chiamare durante la costruzione iniziale dell'oggetto [init-method] e il metodo da chiamare durante la distruzione dell'oggetto [destroy-method].

Per i nostri test, useremo la classe di test JUnit già presentata, aggiungendovi il seguente metodo [test2]:

1
2
3
4
5
  public void test2() {
     // recovery of bean [voiture1]
    Voiture Voiture1 = (Voiture) bf.getBean("voiture1");
    System.out.println("Voiture1=" + Voiture1.toString());
  }

Il metodo [test2] recupera il bean [car1] e lo visualizza.

La struttura del progetto Eclipse rimane la stessa del test precedente. L'esecuzione del metodo [test2] del test JUnit produce i seguenti risultati:

18 sept. 2004 14:56:10 org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions
INFO: Loading XML bean definitions from class path resource [config.xml]
18 sept. 2004 14:56:10 org.springframework.beans.factory.support.AbstractBeanFactory getBean
INFO: Creating shared instance of singleton bean 'voiture1'
18 sept. 2004 14:56:10 org.springframework.beans.factory.support.AbstractBeanFactory getBean
INFO: Creating shared instance of singleton bean 'personne2'
init personne [nom=[Brigitte], age=[20]]
18 sept. 2004 14:56:10 org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory autowireConstructor
INFO: Bean 'voiture1' instantiated via constructor [public istia.st.springioc.domain.Voiture(java.lang.String,java.lang.String,istia.st.springioc.domain.Personne)]
init voiture [Voiture : marque=[Peugeot] type=[307] propriétaire=[nom=[Brigitte], age=[20]]]
Voiture1=Voiture : marque=[Peugeot] type=[307] propriétaire=[nom=[Brigitte], age=[20]]

Commenti:

  1. Il metodo [test2] richiede un riferimento al bean [car1]
  2. Riga 4: Spring inizia a creare il bean [car1] poiché questo bean non è ancora stato creato (singleton)
  3. riga 6: poiché il bean [car1] fa riferimento al bean [person2], quest'ultimo viene a sua volta costruito
  4. riga 7: il bean [person2] è stato creato. Viene quindi eseguito il suo metodo [init].
  5. Riga 9: Spring indica che utilizzerà un costruttore per creare il bean [car1]
  6. riga 10: il bean [car1] è stato creato. Viene quindi eseguito il suo metodo [init].
  7. riga 11: il metodo [test2] visualizza il valore del bean [car1]

2.3.3. Esempio 3

Introduciamo la seguente nuova classe [PersonGroup]:

package istia.st.springioc.domain;

import java.util.Map;

public class GroupePersonnes {
  private Personne[] membres;
  private Map groupesDeTravail;

   // getters - setters
  public Personne[] getMembres() {
    return membres;
  }

  public void setMembres(Personne[] membres) {
    this.membres = membres;
  }

  public Map getGroupesDeTravail() {
    return groupesDeTravail;
  }

  public void setGroupesDeTravail(Map groupesDeTravail) {
    this.groupesDeTravail = groupesDeTravail;
  }

   // display
  public String toString() {
    String liste = "membres : ";
    for (int i = 0; i < this.membres.length; i++) {
      liste += "[" + this.membres[i].toString() + "]";
    }
    return liste + ", groupes de travail = " + this.groupesDeTravail.toString();
  }

   // init-close
  public void init() {
    System.out.println("init GroupePersonnes [" + this.toString() + "]");
  }

  public void close() {
    System.out.println("destroy GroupePersonnes [" + this.toString() + "]");
  }
}

I suoi due membri privati sono:

members: un array di persone che sono membri del gruppo

workGroups: un dizionario che associa una persona a un gruppo di lavoro

Si noti qui che la classe [PeopleGroup] non definisce un costruttore senza argomenti per conformarsi allo standard JavaBean. Ricordiamo che in assenza di qualsiasi costruttore, esiste un costruttore "predefinito", ovvero il costruttore senza argomenti che non esegue alcuna operazione.

L'obiettivo qui è dimostrare come Spring consenta l'inizializzazione di oggetti complessi, come quelli con campi array o dizionario. Aggiungiamo un nuovo bean al precedente file Spring [config.xml]:

    <bean id="groupe1" class="istia.st.springioc.domain.GroupePersonnes" 
        init-method="init" destroy-method="close">
        <property name="membres">
            <list>
                <ref bean="personne1"/>
                <ref bean="personne2"/>
            </list>
        </property>
        <property name="groupesDeTravail">
            <map>
                <entry key="Brigitte">
                    <value>Marketing</value>
                </entry>
                <entry key="Simon">
                    <value>Ressources humaines</value>
                </entry>
            </map>
        </property>
    </bean>
  1. Il tag <list> consente di inizializzare un campo di tipo array o che implementa l'interfaccia List con valori diversi.
  2. Il tag <map> consente di fare la stessa cosa con un campo che implementa l'interfaccia Map

Per i nostri test, useremo la classe di test JUnit già presentata, aggiungendovi il seguente metodo [test3]:

1
2
3
4
5
  public void test3() {
    // bean retrieval [group1]]
    GroupePersonnes groupe1 = (GroupePersonnes) bf.getBean("groupe1");
    System.out.println("groupe1=" + groupe1.toString());
  }

Il metodo [test3] recupera il bean [groupe1] e lo visualizza.

La struttura del progetto Eclipse rimane la stessa del test precedente. L'esecuzione del metodo [test3] del test JUnit produce i seguenti risultati:

18 sept. 2004 15:51:45 org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions
INFO: Loading XML bean definitions from class path resource [config.xml]
18 sept. 2004 15:51:45 org.springframework.beans.factory.support.AbstractBeanFactory getBean
INFO: Creating shared instance of singleton bean 'groupe1'
18 sept. 2004 15:51:45 org.springframework.beans.factory.support.AbstractBeanFactory getBean
INFO: Creating shared instance of singleton bean 'personne1'
init personne [nom=[Simon], age=[40]]
18 sept. 2004 15:51:45 org.springframework.beans.factory.support.AbstractBeanFactory getBean
INFO: Creating shared instance of singleton bean 'personne2'
init personne [nom=[Brigitte], age=[20]]
init GroupePersonnes [membres : [nom=[Simon], age=[40]][nom=[Brigitte], age=[20]], groupes de travail = {Brigitte=Marketing, Simon=Ressources humaines}]
groupe1=membres : [nom=[Simon], age=[40]][nom=[Brigitte], age=[20]], groupes de travail = {Brigitte=Marketing, Simon=Ressources humaines}

Commenti:

  • Il metodo [test3] richiede un riferimento al bean [group1]
  • riga 4: Spring inizia a creare questo bean
  • Poiché il bean [group1] fa riferimento ai bean [person1] e [person2], questi due bean vengono creati (righe 6 e 9) e i loro metodi init vengono eseguiti (righe 7 e 10)
  • Riga 11: il bean [group1] è stato creato. Il suo metodo [init] viene ora eseguito.
  • Riga 12: Visualizzazione richiesta dal metodo [test3].

2.4. Spring per la configurazione di applicazioni web a tre livelli

2.4.1. Architettura generale dell'applicazione

Vogliamo realizzare un'applicazione a tre livelli con la seguente struttura:

  • I tre livelli saranno resi indipendenti tramite l'uso di interfacce Java
  • L'integrazione dei tre livelli sarà gestita da Spring
  • Creeremo pacchetti separati per ciascuno dei tre livelli, che chiameremo Control, Domain e Dao. Un pacchetto aggiuntivo conterrà le applicazioni di test.

La struttura dell'applicazione in Eclipse potrebbe essere la seguente:

Image

2.4.2. Il livello di accesso ai dati DAO

Il livello DAO implementerà la seguente interfaccia:

package istia.st.demo.dao;

public interface IDao1 {
  public int doSometingInDaoLayer(int a, int b);
}
  • Scrivi due classi, Dao1Impl1 e Dao1Impl2, che implementino l'interfaccia IDao1. Il metodo Dao1Impl1.doSomethingInDaoLayer restituirà a+b, mentre il metodo Dao1Impl2.doSomethingInDaoLayer restituirà a-b.
  • Scrivi una classe di test JUnit per testare le due classi precedenti

2.4.3. Il livello business

Il livello business implementerà la seguente interfaccia:

package istia.st.demo.domain;

public interface IDomain1 {
  public int doSomethingInDomainLayer(int a, int b);
}
  • Scrivi due classi, Domain1Impl1 e Domain1Impl2, che implementino l'interfaccia IDomain1. Queste classi avranno un costruttore che accetta un parametro di tipo IDao1. Il metodo Domain1Impl1.doSomethingInDomainLayer incrementerà a e b di uno, quindi passerà questi due parametri al metodo doSomethingInDaoLayer dell'oggetto IDao1 ricevuto. Il metodo Domain1Impl2.doSomethingInDomainLayer, invece, decrementerà a e b di uno prima di eseguire la stessa operazione.
  • Scrivi una classe di test JUnit per testare le due classi precedenti

2.4.4. Il livello dell'interfaccia utente

Il livello dell'interfaccia utente implementerà la seguente interfaccia:

package istia.st.demo.control;

public interface IControl1 {
  public int doSometingInControlLayer(int a, int b);
}
  • Scrivi due classi, Control1Impl1 e Control1Impl2, che implementino l'interfaccia IControl1. Queste classi avranno un costruttore che accetta un parametro di tipo IDomain1. Il metodo Control1Impl1.doSomethingInControlLayer incrementerà a e b di uno, quindi passerà questi due parametri al metodo doSomethingInDomainLayer dell'oggetto IDomain1 ricevuto. Il metodo Control1Impl2.doSomethingInControlLayer, invece, decrementerà a e b di uno prima di eseguire la stessa operazione.
  • Scrivere una classe di test JUnit per testare le due classi precedenti

2.4.5. Integrazione con Spring

  • Scrivi un file di configurazione Spring che determini quali classi debba utilizzare ciascuno dei tre livelli precedenti
  • Scrivi una classe di test JUnit utilizzando diverse configurazioni Spring per evidenziare la flessibilità dell'applicazione
  • Scrivere un'applicazione autonoma (metodo main) che passi due parametri all'interfaccia IControl1 e visualizzi il risultato restituito dall'interfaccia.

2.4.6. Una soluzione

2.4.6.1. Il progetto Eclipse

Image

Gli archivi nella cartella [lib] sono stati aggiunti al [ClassPath] del progetto.

2.4.6.2. Il pacchetto [istia.st.demo.dao]

L'interfaccia:

1
2
3
4
5
6
7
8
9
package istia.st.demo.dao;

/**
 * @author ST-ISTIA
 *  
 */
public interface IDao1 {
  public int doSometingInDaoLayer(int a, int b);
}

Una prima classe di implementazione:

package istia.st.demo.dao;

/**
 * @author ST-ISTIA
 *  
 */
public class Dao1Impl1 implements IDao1 {

     // we do something in the [dao] layer
  public int doSometingInDaoLayer(int a, int b) {
    return a+b;
  }

}

Una seconda classe di implementazione:

package istia.st.demo.dao;

/**
 * @author ST-ISTIA
 *
 */
public class Dao1Impl2 implements IDao1 {

     // we do something in the [dao] layer
  public int doSometingInDaoLayer(int a, int b) {
    return a-b;
  }
}

2.4.6.3. Il pacchetto [istia.st.demo.domain]

L'interfaccia:

package istia.st.demo.domain;

/**
 * @author ST-ISTIA
 *  
 */
public interface IDomain1 {

     // we do something in the [domain] layer
  public int doSomethingInDomainLayer(int a, int b);
}

Una prima classe di implementazione:

package istia.st.demo.domain;

import istia.st.demo.dao.IDao1;

/**
 * @author ST-ISTIA
 *  
 */
public class Domain1Impl1 implements IDomain1 {

     // the [dao] layer access service
  private IDao1 dao1;

  public Domain1Impl1() {
     // constructor with no arguments
  }

     // memorizes the [dao] layer access service
  public Domain1Impl1(IDao1 dao1) {
    this.dao1 = dao1;
  }

     // we do something in the [domain] layer
  public int doSomethingInDomainLayer(int a, int b) {
    a++;
    b++;
    return dao1.doSometingInDaoLayer(a, b);
  }
}

Una seconda classe di implementazione:

package istia.st.demo.domain;

import istia.st.demo.dao.IDao1;

/**
 * @author ST-ISTIA
 *  
 */
public class Domain1Impl2 implements IDomain1 {

     // the [dao] layer access service
  private IDao1 dao1;

  public Domain1Impl2() {
     // constructor with no arguments
  }

     // memorizes the [dao] layer access service
  public Domain1Impl2(IDao1 dao1) {
    this.dao1 = dao1;
  }

     // we do something in the [domain] layer
  public int doSomethingInDomainLayer(int a, int b) {
    a--;
    b--;
    return dao1.doSometingInDaoLayer(a, b);
  }
}

2.4.6.4. Il pacchetto [istia.st.demo.control]

L'interfaccia

1
2
3
4
5
6
7
8
9
package istia.st.demo.control;

/**
 * @author ST-ISTIA
 *  
 */
public interface IControl1 {
  public int doSometingInControlLayer(int a, int b);
}

Una prima classe di implementazione:

package istia.st.demo.control;

import istia.st.demo.domain.IDomain1;

/**
 * @author ST-ISTIA
 *  
 */
public class Control1Impl1 implements IControl1 {
   // business class in layer [domain]
    private IDomain1 domain1;

  public Control1Impl1() {
     // constructor with no arguments
  }

     // domain] layer access service enhancement
  public Control1Impl1(IDomain1 domain1) {
    this.domain1 = domain1;
  }

     // we're doing something
  public int doSometingInControlLayer(int a, int b) {
    a++;
    b++;
    return domain1.doSomethingInDomainLayer(a, b);
  }

}

Una seconda classe di implementazione:

package istia.st.demo.control;

import istia.st.demo.domain.IDomain1;

/**
 * @author ST-ISTIA
 *  
 */
public class Control1Impl2 implements IControl1 {

     // the [domain] layer access class
    private IDomain1 domain1;

  public Control1Impl2() {
     // constructor with no arguments
  }

     // stores the [domain] layer access class
  public Control1Impl2(IDomain1 domain1) {
    this.domain1 = domain1;
  }

     // we're doing something
  public int doSometingInControlLayer(int a, int b) {
    a--;
    b--;
    return domain1.doSomethingInDomainLayer(a, b);
  }

}

2.4.6.5. [Spring] File di configurazione

Un primo [springMainTest1.xml]:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
     <!-- the dao class -->
    <bean id="dao" class="istia.st.demo.dao.Dao1Impl1">
    </bean>
     <!-- the trade class -->
    <bean id="domain" class="istia.st.demo.domain.Domain1Impl1">
        <constructor-arg index="0">
            <ref bean="dao"/>
        </constructor-arg>
    </bean>
     <!-- the control class -->
    <bean id="control" class="istia.st.demo.control.Control1Impl1">
        <constructor-arg index="0">
            <ref bean="domain"/>
        </constructor-arg>
    </bean>
</beans>

Un secondo [springMainTest2.xml]:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
     <!-- the dao class -->
    <bean id="dao" class="istia.st.demo.dao.Dao1Impl2">
    </bean>
     <!-- the trade class -->
    <bean id="domain" class="istia.st.demo.domain.Domain1Impl2">
        <constructor-arg index="0">
            <ref bean="dao"/>
        </constructor-arg>
    </bean>
     <!-- the control class -->
    <bean id="control" class="istia.st.demo.control.Control1Impl2">
        <constructor-arg index="0">
            <ref bean="domain"/>
        </constructor-arg>
    </bean>
</beans>

2.4.6.6. Il pacchetto di test [istia.st.demo.tests]

Un test [main]:

package istia.st.demo.tests;

import istia.st.demo.control.IControl1;

import org.springframework.beans.factory.xml.XmlBeanFactory;
import org.springframework.core.io.ClassPathResource;

/**
 * @author ST-ISTIA
 *  
 */
public class MainTest1 {
  public static void main(String[] arguments) {
     // we retrieve an implementation of the IControl1 interface
    IControl1 control = (IControl1) (new XmlBeanFactory(new ClassPathResource(
        "springMainTest1.xml"))).getBean("control");
     // we use the
    int a = 10, b = 20;
    int res = control.doSometingInControlLayer(a, b);
     // the result is displayed
    System.out.println("control(" + a + "," + b + ")=" + res);
  }
}

Risultati visualizzati nella console di Eclipse:

11 mars 2005 11:25:14 org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions
INFO: Loading XML bean definitions from class path resource [springMainTest1.xml]
11 mars 2005 11:25:14 org.springframework.beans.factory.support.AbstractBeanFactory getBean
INFO: Creating shared instance of singleton bean 'control'
11 mars 2005 11:25:14 org.springframework.beans.factory.support.AbstractBeanFactory getBean
INFO: Creating shared instance of singleton bean 'domain'
11 mars 2005 11:25:14 org.springframework.beans.factory.support.AbstractBeanFactory getBean
INFO: Creating shared instance of singleton bean 'dao'
11 mars 2005 11:25:14 org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory autowireConstructor
INFO: Bean 'domain' instantiated via constructor [public istia.st.demo.domain.Domain1Impl1(istia.st.demo.dao.IDao1)]
11 mars 2005 11:25:14 org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory autowireConstructor
INFO: Bean 'control' instantiated via constructor [public istia.st.demo.control.Control1Impl1(istia.st.demo.domain.IDomain1)]
control(10,20)=34

Un altro test che utilizza il secondo file di configurazione [Spring]:

package istia.st.demo.tests;

import istia.st.demo.control.IControl1;
import org.springframework.beans.factory.xml.XmlBeanFactory;
import org.springframework.core.io.ClassPathResource;

/**
 * @author ST-ISTIA
 *  
 */
public class MainTest2 {
  public static void main(String[] arguments) {
     // we retrieve an implementation of the IControl1 interface
    IControl1 control = (IControl1) (new XmlBeanFactory(new ClassPathResource(
        "springMainTest2.xml"))).getBean("control");
     // we use the
    int a = 10, b = 20;
    int res = control.doSometingInControlLayer(a, b);
     // the result is displayed
    System.out.println("control(" + a + "," + b + ")=" + res);
  }
}

Risultati visualizzati nella console di Eclipse:

11 mars 2005 11:28:52 org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions
INFO: Loading XML bean definitions from class path resource [springMainTest2.xml]
11 mars 2005 11:28:52 org.springframework.beans.factory.support.AbstractBeanFactory getBean
INFO: Creating shared instance of singleton bean 'control'
11 mars 2005 11:28:52 org.springframework.beans.factory.support.AbstractBeanFactory getBean
INFO: Creating shared instance of singleton bean 'domain'
11 mars 2005 11:28:52 org.springframework.beans.factory.support.AbstractBeanFactory getBean
INFO: Creating shared instance of singleton bean 'dao'
11 mars 2005 11:28:52 org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory autowireConstructor
INFO: Bean 'domain' instantiated via constructor [public istia.st.demo.domain.Domain1Impl2(istia.st.demo.dao.IDao1)]
11 mars 2005 11:28:52 org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory autowireConstructor
INFO: Bean 'control' instantiated via constructor [public istia.st.demo.control.Control1Impl2(istia.st.demo.domain.IDomain1)]
control(10,20)=-10

Infine, un test JUnit:

package istia.st.demo.tests;

import istia.st.demo.control.IControl1;
import org.springframework.beans.factory.xml.XmlBeanFactory;
import org.springframework.core.io.ClassPathResource;
import junit.framework.TestCase;

/**
 * @author ST-ISTIA
 *  
 */
public class JunitTest2Control1 extends TestCase {
  public void testControl1() {
     // we retrieve an implementation of the IControl1 interface
    IControl1 control1 = (IControl1) (new XmlBeanFactory(new ClassPathResource(
        "springMainTest1.xml"))).getBean("control");
     // we use the
    int a1 = 10, b1 = 20;
    int res1 = control1.doSometingInControlLayer(a1, b1);
    assertEquals(34, res1);
     // we retrieve another implementation of the IControl1 interface
    IControl1 control2 = (IControl1) (new XmlBeanFactory(new ClassPathResource(
        "springMainTest2.xml"))).getBean("control");
     // we use the
    int a2 = 10, b2 = 20;
    int res2 = control2.doSometingInControlLayer(a2, b2);
    assertEquals(-10, res2);
  }
}

2.5. Conclusione

Il framework Spring offre una vera flessibilità sia nell'architettura dell'applicazione che nella configurazione. Abbiamo utilizzato il concetto di IoC, uno dei due pilastri di Spring. L'altro pilastro è l'AOP (Aspect-Oriented Programming), che non abbiamo trattato. Esso consente di aggiungere un "comportamento" a un metodo di classe tramite la configurazione senza modificare il codice del metodo. In termini semplici, l'AOP consente di filtrare le chiamate a determinati metodi:

  • il filtro può essere eseguito prima o dopo il metodo di destinazione M, oppure in entrambi i casi.
  • Il metodo M non è a conoscenza di questi filtri. Essi sono definiti nel file di configurazione di Spring.
  • Il codice del metodo M non viene modificato. I filtri sono classi Java che devono essere implementate. Spring fornisce filtri predefiniti, in particolare per la gestione delle transazioni DBMS.
  • I filtri sono bean e, in quanto tali, sono definiti nel file di configurazione di Spring come bean.

Un filtro comune è il filtro delle transazioni. Si consideri un metodo M nel livello business che esegue due operazioni inseparabili sui dati (un'unità di lavoro). Esso chiama due metodi, M1 e M2, nel livello DAO per eseguire queste due operazioni.

Poiché risiede nel livello business, il metodo M astrae l'archiviazione dei dati sottostante. Ad esempio, non deve presumere che i dati siano memorizzati in un DBMS o che debba racchiudere le chiamate ai metodi M1 e M2 all'interno di una transazione DBMS. Spetta al livello DAO gestire questi dettagli. Una soluzione al problema precedente consiste nel creare un metodo nel livello DAO che chiami a sua volta i metodi M1 e M2, racchiudendo queste chiamate all'interno di una transazione DBMS.

La soluzione di filtraggio AOP è più flessibile. Consente di definire un filtro che, prima di chiamare M, avvierà una transazione e, dopo la chiamata, eseguirà un commit o un rollback a seconda dei casi.

Questo approccio presenta diversi vantaggi:

  • una volta definito il filtro, può essere applicato a più metodi, ad esempio a tutti quelli che richiedono una transazione
  • non è necessario riscrivere i metodi filtrati
  • poiché i filtri da utilizzare sono definiti tramite configurazione, possono essere modificati

Oltre ai concetti di IoC e AOP, Spring fornisce numerose classi di supporto per le applicazioni a tre livelli:

  • per JDBC, SQLMap (iBatis), Hibernate e JDO (Java Data Object) nel livello DAO
  • per il modello MVC nel livello dell'interfaccia utente

Per ulteriori informazioni: http://www.springframework.org.