Cours de Spring MVC
Support de cours sur les bases de Spring MVC
...
Une application est un ensemble d'objets que Spring appelle des beans, parce qu'ils suivent la norme JavaBean de nommage des accesseurs et initialiseurs (getters/setters) des champs privés d'un objet. Les objets qui, dans une application, ont pour rôle de rendre un service sont souvent créés en un seul exemplaire. On les appelle des singletons. Ainsi dans notre exemple d'application multi-tier étudiée ici, l'accès à la base des articles sera assuré par un unique exemplaire de la classe [ArticlesDaoPlainJdbc]. Pour une application web, ces objets de service servent plusieurs clients à la fois. On ne crée pas un objet de service par client.
Le fichier de configuration Spring ci-dessus permet de créer un objet service unique de type [ArticlesDaoPlainJdbc] dans un paquetage nommé [istia.st.articles.dao]. Les quatre informations nécessaires au constructeur de cet objet sont définies à l'intérieur d'une balise <bean>...</bean>. On aura autant de telles balises <bean> que de singletons à construire.
A quel moment va intervenir la construction des objets définis dans le fichier Spring ? L'initialisation d'une application peut être dévolue à la méthode main de cette même application si elle en a une. Pour une application web, ce peut-être la méthode [init] de la servlet principale. On trouve dans toute application, une méthode assurée d'être la première à s'exécuter. C'est généralement dans celle-ci que la construction des singletons s'opère.
Prenons un exemple. Supposons qu'on veuille tester la classe [ArticlesDaoPlainJdbc] précédente à l'aide d'un test JUnit. Une classe de test JUnit a une méthode [setUp] exécutée avant toute autre méthode. C'est là qu'on créera le singleton [ArticlesDaoPlainJdbc].
Si on suit la solution de passage des informations de configuration par constructeur, on aura la classe de test suivante :
public class TestArticlesPlainJdbc extends TestCase {
// teste la classe d'accès aux articles ArticlesDaoPlainJdbc // la source de données est définie dans sprintest
// une instance de la classe testée private IArticlesDao articlesDao;
protected void setUp() throws Exception{
// récupère une instance d'accès aux données
articlesDao =
(IArticlesDao) new ArticlesDaoPlainJdbc("org.firebirdsql.jdbc.FBDriver",
"jdbc:firebirdsql:localhost/3050:d:/databases/dbarticles.gdb","someone","somepassword");
}
La classe d'appel [TestArticlesPlainJdbc] doit connaître les quatre informations nécessaires à l'initialisation du singleton [ArticlesDaoPlainJdbc] à construire.
Si on suit la solution de passage des informations de configuration par fichier de configuration, on pourrait avoir la classe de test suivante en utilisant le fichier Spring décrit plus haut.
public class TestSpringArticlesPlainJdbc extends TestCase { // teste la classe d'accès aux articles ArticlesDaoJdbc // la source de données est définie dans sprintest
// une instance de la classe testée private IArticlesDao articlesDao;
protected void setUp() throws Exception {
// récupère une instance d'accès aux données
articlesDao = (IArticlesDao) (new XmlBeanFactory(new ClassPathResource(
"springArticlesPlainJdbc.xml"))).getBean("articlesDao");
}
Ici, la classe d'appel [TestSpringArticlesPlainJdbc] n'a pas à connaître les informations nécessaires à l'initialisation du singleton à construire. Elle a simplement besoin de connaître :
- [springArticlesPlainJdbc.xml] : le nom du fichier de configuration Spring décrit plus haut
- [articlesDao] : le nom du singleton à créer
Une modification du fichier de configuration, en-dehors de ces deux entités, n'a aucun impact sur le code Java. Cette méthode de configuration des objets d'une application est très souple. Pour se configurer, celle-ci n'a besoin de connaître que deux choses : - le nom du fichier Spring qui contient la définition des singletons à construire
- les noms de ces singletons, ceux-ci servant au code Java pour obtenir une référence sur les objets auxquels ils ont été associés grâce au fichier de configuration
2 Injection de dépendance et Inversion de contrôle
Introduisons maintenant la notion d'injection de dépendance (Dependency Injection) utilisée par Spring pour configurer les applications. On utilise également le terme inversion de contrôle (IoC, Inversion of Control). Considérons la construction du singleton [ArticlesManagerWithDataBase] de la couche métier de notre application :
Pour accéder aux données du SGBD, la couche métier doit utiliser les services d'un objet implémentant l'interface [IArticlesDao], par exemple un objet de type [ArticlesDaoPlainJdbc]. Le code de la classe [ArticlesManagerWithDataBase] pourrait ressembler à ce qui suit :
public class ArticlesManagerWithDataBase implements IArticlesManager {
// une instance d'accès aux données private IArticlesDao articlesDao;
....
public ArticlesManagerWithDataBase (String driverClassName, String url, String user, String pwd, ...) {
// création du service d'accès aux données
articlesDao =(IArticlesDao)new ArticlesDaoPlainJdbc(driverClassName,url,user,pwd);
...
}
public ... doSomething(...){ ...
}
}
La classe [ArticlesDaoPlainJdbc] est supposée ici implémenter une interface [IArticlesDao] :
public class ArticlesDaoPlainJdbc implements IArticlesDao {...}
Pour créer le singleton de type [IArticlesDao] nécessaire au fonctionnement de la classe, le constructeur de celle-ci utilise explicitement le nom de la classe d'implémentation de l'interface [IArticlesDao] :
articlesDao =(IArticlesDao) new ArticlesDaoPlainJdbc(...);
On a donc une dépendance en dur dans le code sur le nom de classe. Si la classe d'implémentation de l'interface [IArticlesDao] venait à changer, le code du constructeur précédent devrait être modifié. On a les relations suivantes entre les objets :
La classe [ArticlesManagerWithDataBase] prend elle-même l'initiative de la création de l'objet [ArticlesDaoPlainJdbc] dont elle a besoin. Pour en revenir au terme "inversion de contrôle", on dira que c'est elle qui a le "contrôle" pour créer l'objet dont elle a besoin.
Si on devait écrire une classe de test JUnit de la classe [ArticlesManagerWithDataBase], on pourrait avoir quelque chose comme suit :
public class TestArticlesManagerWithDataBase extends TestCase { // une instance de la classe métier testée
private IArticlesManager articlesManager;
protected void setUp() throws Exception {
// crée une instance de la classe métier testée
articlesManager =
(IArticlesManager) new ArticlesManagerWithDataBase("org.firebirdsql.jdbc.FBDriver", "jdbc:firebirdsql:localhost/3050:d:/databases/dbarticles.gdb","someone","somepassword");
}
La classe de test crée une instance de la classe métier [ArticlesManagerWithDataBase] qui crée à son tour, dans son constructeur, une instance de classe d'accès aux données [ArticlesDaoPlainJdbc].
La solution avec Spring va éliminer le besoin qu'a la classe métier [ArticlesManagerWithDataBase] de connaître le nom [ArticlesDaoPlainJdbc] de la classe d'accès aux données dont elle a besoin. Cela permettra d'en changer sans toucher au code java de la classe métier. Spring va permettre de créer en même temps les deux singletons, celui de la couche d'accès aux données et celui de la couche métier. Le fichier de configuration de Spring va définir un nouveau bean :
...
<beans>
<!-- la classe d'accès aux données -->
<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>
...
La nouveauté réside dans le bean définissant le singleton de la classe métier à créer :
<bean id="articlesManager" class="istia.st.articles.domain.ArticlesManagerWithDataBase">
<property name="articlesDao">
<ref bean="articlesDao"/>
</property>
</bean>
- la classe implémentant le bean [articlesManager] est définie : [ArticlesManagerWithDataBase]
- le champ [articlesDao] du bean reçoit une valeur par la balise <property name="articlesDao">. Il s'agit du champ défini dans la classe [ArticlesManagerWithDataBase] :
public class ArticlesManagerWithDataBase implements IArticlesManager {
// interface d'accès aux données private IArticlesDao articlesDao;
public IArticlesDao getArticlesDao() { return articlesDao; }
public void setArticlesDao(IArticlesDao articlesDao) { this.articlesDao = articlesDao; }
Pour que le champ [articlesDao] puisse être initialisé par Spring et sa balise <property>, il faut que le champ suive la norme JavaBean et qu'il existe une méthode [setArticlesDao] pour initialiser le champ [articlesDao]. On notera le nom de la méthode, dérivé de façon bien précise du nom du champ. De façon parallèle, il existe souvent une méthode [get...] pour obtenir la valeur du champ. Ici, c'est la méthode [getArticlesDao]. Dans cette nouvelle mouture, la classe [ArticlesManagerWithDataBase] n'a plus de constructeur. Elle n'en a plus besoin.
- la valeur qui sera affectée au champ [articlesDao] par Spring est celui du bean [articlesDao] défini dans son fichier de configuration :
<bean id="articlesManager"
class="istia.st.articles.domain.ArticlesManagerWithDataBase"> <property name="articlesDao">
<ref bean="articlesDao"/>
</property> </bean>
- lorsque Spring construira le singleton [ArticlesManagerWithDataBase], il sera amené à créer également le singleton [ArticlesDaoPlainJdbc] :
o Spring établira un graphe de dépendances des beans et verra que le bean [articlesManager] dépend du bean [articlesDao]
o il construira le bean [articlesDao], donc un objet de type [ArticlesDaoPlainJdbc]
o puis il construira le bean [articlesManager] de type [ArticlesManagerWithDataBase]
Imaginons maintenant un test JUnit pour la classe [ArticlesManagerWithDataBase]. Il pourrait ressembler à ce qui suit :
public class TestSpringArticlesManagerWithDataBase extends TestCase { // teste la classe métier [ArticlesManagerWithDataBase]
// une instance de la classe métier testée private IArticlesManager articlesManager; protected void setUp() throws Exception {
// récupère une instance d'accès aux données
articlesManager = (IArticlesManager) (new XmlBeanFactory(new ClassPathResource(
"springArticlesManagerWithDataBase.xml"))).getBean("articlesManager");
}
Suivons le déroulement de création des deux singletons définis dans le fichier Spring nommé [springArticlesManagerWithDataBase.xml].
- la méthode [setUp] ci-dessus demande une référence du bean nommé [articlesManager]
- Spring consulte son fichier de configuration, trouve le bean [articlesManager]. S'il est déjà créé, il se contente de
rendre une référence sur l'objet (singleton), sinon il le crée.
- Spring voit la dépendance du bean [articlesManager] vis à vis du bean [articlesDao]. Il crée donc le singleton [articlesDao] de type [ArticlesDaoPlainJdbc] si celui-ci n'est pas déjà créé (singleton).
- il créée le singleton [articlesManager] de type [ArticlesManagerWithDataBase]
Ce mécanisme pourrait être schématisé comme suit :
Rappelons le squelette de la classe [ArticlesManagerWithDataBase] :
public class ArticlesManagerWithDataBase implements IArticlesManager {
// interface d'accès aux données private IArticlesDao articlesDao;
public IArticlesDao getArticlesDao() { return articlesDao; }
public void setArticlesDao(IArticlesDao articlesDao) { this.articlesDao = articlesDao; }
A la fin de la construction des singletons par Spring, on a un objet de type [ArticlesManagerWithDataBase] qui a son champ [articlesDao] initialisé sans qu'il sache comment. On dit qu'on a injecté de la dépendance dans l'objet
[ArticlesManagerWithDataBase]. On dit également qu'on a inversé le contrôle : ce n'est plus l'objet [ArticlesManagerWithDataBase] qui prend l'initiative de créer lui-même l'objet implémentant l'interface [IArticlesDao] dont il a besoin, c'est l'application au plus haut niveau (lorsqu'elle s'initialise) qui prend soin de créer tous les objets dont elle a besoin en gérant les dépendances de ceux-ci entre-eux.
L'intérêt principal de la configuration du singleton [ArticlesManagerWithDataBase] par un fichier Spring, est que maintenant on peut changer la classe d'implémentation correspondant au champ [articlesDao] de la classe [ArticlesManagerWithDataBase] sans que le code de celle-ci soit modifié. Il suffit de changer le nom de la classe dans la définition au bean [articlesDao] dans le fichier Spring :
<bean id="articlesDao" class="istia.st.articles.dao.ArticlesDaoPlainJdbc"> ...
</bean>
deviendra par exemple :
<bean id="articlesDao" class="istia.st.articles.dao.ArticlesDaoIbatisSqlMap"> ...
</bean>
Le bean [ArticlesManagerWithDataBase] travaillera avec cette nouvelle classe d'accès aux données, sans même le savoir.
3 Spring IoC par la pratique
3.1 Exemple 1
Considérons la classe suivante :
package istia.st.springioc.domain;
public class Personne { private String nom; private int age;
// affichage Personne
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 présente :
- deux champs privés nom et age
- les méthodes de lecture (get) et d'écriture (set) de ces deux champs
- une méthode toString pour récupérer la valeur de l'objet [Personne] sous la forme d'une chaîne de caractères
- une méthode init qui sera appelée par Spring à la création de l'objet, une méthode close qui sera appelée à la destruction de l'objet
... ... ...
La structure du projet Eclipse de notre application est la suivante :
Commentaires :
- le dossier [src] contient les codes source. Les codes compilés iront dans un dossier [bin] non représenté ici.
- le fichier [config.xml] est à la racine du dossier [src]. La construction du projet le recopie automatiquement dans le dossier
[bin], qui fait partie du [ClassPath] de l'application. C'est là qu'il est recherché par l'objet [ClassPathResource].
- le dossier [lib] contient trois bibliothèques Java nécessaires à l'application :
n commons-logging.jar et spring-core.jar pour les classes Spring
n junit.jar pour les classes JUnit
- le dossier [lib] fait partie, lui aussi, du [ClassPath] de l'application
L'excécution de la méthode [test1] du test JUnit donne les résultats suivants :
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]
Commentaires :
- Spring logue un certain nombre d'événements grâce à la bibliothèque [commons-logging.jar]. Ces logs nous permettent de mieux comprendre le fonctionnement de Spring.
- le fichier [config.xml] a été chargé puis exploité
- l'opération
Personne personne1 = (Personne) bf.getBean("personne1");
a forcé la création du bean [personne1]. On voit le log de Spring à ce sujet. Parce que dans la définition du bean [personne1] on avait écrit [init-method="init"], la méthode [init] de l'objet [Personne] créé a été exécutée. Le message correspondant est affiché.
- l'opération
System.out.println("personne1=" + personne1.toString());
a fait afficher la valeur de l'objet [Personne] créé.
- le même phénomène se répète pour le bean de clé [personne2].
- la dernière opération
personne2 = (Personne) bf.getBean("personne2"); System.out.println("personne2=" + personne2.toString());
n'a pas provoqué la création d'un nouvel objet de type [Personne]. Si cela avait été le cas, on aurait eu l'affichage de la méthode [init], ce qui n'est pas le cas ici. C'est le principe du singleton. Spring, par défaut, ne crée qu'un seul exemplaire des beans de son fichier de configuration. C'est un service de références d'objet. Si on lui demande la référence d'un objet non encore créé, il le crée et en rend une référence. Si l'objet a déjà été créé, Spring se contente d'en donner une référence.
- on peut remarquer qu'on n'a nulle trace de la méthode [close] de l'objet [Personne] alors qu'on avait écrit dans la définition des beans [destroy-method=close]. Il est possible que cette méthode ne soit exécutée que lorsque la mémoire occupée par l'objet est récupérée par le ramasse-miettes (garbage collector). Au moment où cela se passe, l'application est déjà terminée et l'écriture à l'écran n'a aucun effet. A vérifier.
Les bases d'une configuration Spring étant maintenant acquises, nous serons désormais un peu plus rapides dans nos explications.
3.2 Exemple 2
Considérons la nouvelle classe [Voiture] suivante :
package istia.st.springioc.domain;
public class Voiture {
private String marque;
private String type;
private Personne propriétaire;
// constructeurs
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 présente :
- trois champs privés type, marque et propriétaire. Ces champs peuvent être initialisés et lus par des méthodes publiques de beans get et set. Ils peuvent être également initialisés à l'aide du constructeur Voiture(String, String, Personne). La classe possède également un constructeur sans arguments afin de suivre la norme JavaBean.
- une méthode toString pour récupérer la valeur de l'objet [Voiture] sous la forme d'une chaîne de caractères
- une méthode init qui sera appelée par Spring juste après la création de l'objet, une méthode close qui sera appelée à la destruction de l'objet
Pour créer des objets de type [Voiture], nous utiliserons le fichier Spring [config.xml] suivant :
...
<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>
Ce fichier ajoute aux définitions précédentes un bean de clé "voiture1" de type [Voiture]. Pour initialiser ce bean, on aurait pu écrire :
<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>
Plutôt que de choisir cette méthode déjà présentée, nous avons choisi ici, d'utiliser le constructeur Voiture(String, String, Personne) de la classe. Par ailleurs, le bean [voiture1] définit la méthode à appeler lors de la construction initiale de l'objet [init¬method] et celle à appeler lors de la destruction de l'objet [destroy-method].
Pour nos tests, nous utiliserons la classe de test JUnit déjà présentée, en lui ajoutant la méthode [test2] suivante :
public void test2() {
// récupération du bean [voiture1]
Voiture Voiture1 = (Voiture) bf.getBean("voiture1"); System.out.println("Voiture1=" + Voiture1.toString());
}
La méthode [test2] récupère le bean [voiture1] et l'affiche.
La structure du projet Eclipse reste celle qu'elle était dans le test précédent. L'excécution de la méthode [test2] du test JUnit donne les résultats suivants :
- 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]]
Commentaires :
- la méthode [test2] demande une référence sur le bean [voiture1]
- ligne 4 : Spring commence la création du bean [voiture1] car ce bean n'a pas encore été créé (singleton)
- ligne 6 : parce que le bean [voiture1] référence le bean [personne2], ce dernier bean est construit à son tour
- ligne 7 : le bean [personne2] a été créé. Sa méthode [init] est alors exécutée.
- ligne 9 : Spring indique qu'il va utiliser un constructeur pour créer le bean [voiture1]
- ligne 10 : le bean [voiture1] a été créé. Sa méthode [init] est alors exécutée.
- ligne 11 : la méthode [test2] fait afficher la valeur du bean [voiture1]
3.3 Exemple 3
Nous introduisons la nouvelle classe [GroupePersonnes] suivante :
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; }
// affichage
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() + "]");
}
}
Ses deux membres privés sont :
membres : un tableau de personnes membres du groupe
groupesDeTravail : un dictionnaire affectant une personne à un groupe de travail
On remarquera ici que la classe [GroupePersonnes] ne définit pas de constructeur sans argument pour suivre la norme JavaBean. On rappelle qu'en l'absence de tout constructeur, il existe un constructeur "par défaut" qui est le constructeur sans arguments et qui ne fait rien.
On cherche ici, à montrer comment Spring permet d'initialiser des objets complexes tels que des objets possédant des champs de type tableau ou dictionnaire. On ajoute un nouveau bean au fichier Spring [config.xml] précédent :
<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>
- la balise <list> permet d'initialiser un champ de type tableau ou implémentant l'interface List avec différentes valeurs.
- la balise <map> permet de faire la même chose avec un champ implémentant l'interface Map
Pour nos tests, nous utiliserons la classe de test JUnit déjà présentée, en lui ajoutant la méthode [test3] suivante :
public void test3() {
// récupération du bean [groupe1]
GroupePersonnes groupe1 = (GroupePersonnes) bf.getBean("groupe1");
System.out.println("groupe1=" + groupe1.toString());
}
La méthode [test3] récupère le bean [groupe1] et l'affiche.
La structure du projet Eclipse reste celle qu'elle était dans le test précédent. L'excécution de la méthode [test3] du test JUnit donne les résultats suivants :
- 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}
Commentaires :
o la méthode [test3] demande une référence du bean [groupe1]
o ligne 4 : Spring commence la création de ce bean
o parce que le bean [groupe1] référence les beans [personne1] et [personne2], ces deux beans sont créés (lignes 6 et 9) et leur méthode init exécutée (lignes 7 et 10)
o ligne 11 : le bean [groupe1] a été créé. Sa méthode [init] est maintenant exécutée.
o ligne 12 : affichage demandé par la méthode [test3].
4 Spring pour configurer les applications web à trois couches
4.1 Architecture générale de l'application
On souhaite construire une application 3-tier ayant la structure suivante :
- les trois couches seront rendues indépendantes grâce à l'utilisation d'interfaces Java
- l'intégration des trois couches sera réalisée par Spring
- on créera des paquetages séparés pour chacune des trois couches que l'on appellera Control, Domain et Dao. Un paquetage supplémentaire contiendra les applications de tests.
La structure de l'application sous Eclipse pourrait être la suivante :
4.2 La couche DAO d'accès aux données
La couche DAO implémentera l'interface suivante :
package istia.st.demo.dao;
public interface IDao1 {
public int doSometingInDaoLayer(int a, int b);
}
- écrire deux classes Dao1Impl1 et Dao1Impl2 implémentant l'interface IDao1. La méthode Dao1Impl1. doSomethingInDaoLayer rendra a+b et méthode Dao1Impl2. doSomethingInDaoLayer rendra a-b.
- écrire une classe de test JUnit testant les deux classes précédentes
4.3 La couche métier
La couche métier implémentera l'interface suivante :
package istia.st.demo.domain;
public interface IDomain1 {
public int doSomethingInDomainLayer(int a, int b);
}
- écrire deux classes Domain1Impl1 et Domain1Impl2 implémentant l'interface IDomain1. Ces classes auront un constructeur recevant pour paramètre de type IDao1. La méthode Domain1Impl1.doSomethingInDomainLayer incrémentera a et b d'une unité puis passera ces deux paramètres à la méthode doSomethingInDaoLayer de l'objet de type IDao1 reçu. La méthode Domain1Impl2.doSomethingInDomainLayer elle, décrémentera a et b d'une unité avant de faire la même chose.
- écrire une classe de test JUnit testant les deux classes précédentes
...
Une application est un ensemble d'objets que Spring appelle des beans, parce qu'ils suivent la norme JavaBean de nommage des accesseurs et initialiseurs (getters/setters) des champs privés d'un objet. Les objets qui, dans une application, ont pour rôle de rendre un service sont souvent créés en un seul exemplaire. On les appelle des singletons. Ainsi dans notre exemple d'application multi-tier étudiée ici, l'accès à la base des articles sera assuré par un unique exemplaire de la classe [ArticlesDaoPlainJdbc]. Pour une application web, ces objets de service servent plusieurs clients à la fois. On ne crée pas un objet de service par client.
Le fichier de configuration Spring ci-dessus permet de créer un objet service unique de type [ArticlesDaoPlainJdbc] dans un paquetage nommé [istia.st.articles.dao]. Les quatre informations nécessaires au constructeur de cet objet sont définies à l'intérieur d'une balise <bean>...</bean>. On aura autant de telles balises <bean> que de singletons à construire.
A quel moment va intervenir la construction des objets définis dans le fichier Spring ? L'initialisation d'une application peut être dévolue à la méthode main de cette même application si elle en a une. Pour une application web, ce peut-être la méthode [init] de la servlet principale. On trouve dans toute application, une méthode assurée d'être la première à s'exécuter. C'est généralement dans celle-ci que la construction des singletons s'opère.
Prenons un exemple. Supposons qu'on veuille tester la classe [ArticlesDaoPlainJdbc] précédente à l'aide d'un test JUnit. Une classe de test JUnit a une méthode [setUp] exécutée avant toute autre méthode. C'est là qu'on créera le singleton [ArticlesDaoPlainJdbc].
Si on suit la solution de passage des informations de configuration par constructeur, on aura la classe de test suivante :
public class TestArticlesPlainJdbc extends TestCase {
// teste la classe d'accès aux articles ArticlesDaoPlainJdbc // la source de données est définie dans sprintest
// une instance de la classe testée private IArticlesDao articlesDao;
protected void setUp() throws Exception{
// récupère une instance d'accès aux données
articlesDao =
(IArticlesDao) new ArticlesDaoPlainJdbc("org.firebirdsql.jdbc.FBDriver",
"jdbc:firebirdsql:localhost/3050:d:/databases/dbarticles.gdb","someone","somepassword");
}
La classe d'appel [TestArticlesPlainJdbc] doit connaître les quatre informations nécessaires à l'initialisation du singleton [ArticlesDaoPlainJdbc] à construire.
Si on suit la solution de passage des informations de configuration par fichier de configuration, on pourrait avoir la classe de test suivante en utilisant le fichier Spring décrit plus haut.
public class TestSpringArticlesPlainJdbc extends TestCase { // teste la classe d'accès aux articles ArticlesDaoJdbc // la source de données est définie dans sprintest
// une instance de la classe testée private IArticlesDao articlesDao;
protected void setUp() throws Exception {
// récupère une instance d'accès aux données
articlesDao = (IArticlesDao) (new XmlBeanFactory(new ClassPathResource(
"springArticlesPlainJdbc.xml"))).getBean("articlesDao");
}
Ici, la classe d'appel [TestSpringArticlesPlainJdbc] n'a pas à connaître les informations nécessaires à l'initialisation du singleton à construire. Elle a simplement besoin de connaître :
1. [springArticlesPlainJdbc.xml] : le nom du fichier de configuration Spring décrit plus haut
2. [articlesDao] : le nom du singleton à créer
Une modification du fichier de configuration, en-dehors de ces deux entités, n'a aucun impact sur le code Java. Cette méthode de configuration des objets d'une application est très souple. Pour se configurer, celle-ci n'a besoin de connaître que deux choses : - le nom du fichier Spring qui contient la définition des singletons à construire
- les noms de ces singletons, ceux-ci servant au code Java pour obtenir une référence sur les objets auxquels ils ont été associés grâce au fichier de configuration
2 Injection de dépendance et Inversion de contrôle
Introduisons maintenant la notion d'injection de dépendance (Dependency Injection) utilisée par Spring pour configurer les applications. On utilise également le terme inversion de contrôle (IoC, Inversion of Control). Considérons la construction du singleton [ArticlesManagerWithDataBase] de la couche métier de notre application :
Pour accéder aux données du SGBD, la couche métier doit utiliser les services d'un objet implémentant l'interface [IArticlesDao], par exemple un objet de type [ArticlesDaoPlainJdbc]. Le code de la classe [ArticlesManagerWithDataBase] pourrait ressembler à ce qui suit :
public class ArticlesManagerWithDataBase implements IArticlesManager {
// une instance d'accès aux données private IArticlesDao articlesDao;
....
public ArticlesManagerWithDataBase (String driverClassName, String url, String user, String pwd, ...) {
// création du service d'accès aux données
articlesDao =(IArticlesDao)new ArticlesDaoPlainJdbc(driverClassName,url,user,pwd);
...
}
public ... doSomething(...){ ...
}
}
La classe [ArticlesDaoPlainJdbc] est supposée ici implémenter une interface [IArticlesDao] :
public class ArticlesDaoPlainJdbc implements IArticlesDao {...}
Pour créer le singleton de type [IArticlesDao] nécessaire au fonctionnement de la classe, le constructeur de celle-ci utilise explicitement le nom de la classe d'implémentation de l'interface [IArticlesDao] :
articlesDao =(IArticlesDao) new ArticlesDaoPlainJdbc(...);
On a donc une dépendance en dur dans le code sur le nom de classe. Si la classe d'implémentation de l'interface [IArticlesDao] venait à changer, le code du constructeur précédent devrait être modifié. On a les relations suivantes entre les objets :
La classe [ArticlesManagerWithDataBase] prend elle-même l'initiative de la création de l'objet [ArticlesDaoPlainJdbc] dont elle a besoin. Pour en revenir au terme "inversion de contrôle", on dira que c'est elle qui a le "contrôle" pour créer l'objet dont elle a besoin.
Si on devait écrire une classe de test JUnit de la classe [ArticlesManagerWithDataBase], on pourrait avoir quelque chose comme suit :
public class TestArticlesManagerWithDataBase extends TestCase { // une instance de la classe métier testée
private IArticlesManager articlesManager;
protected void setUp() throws Exception {
// crée une instance de la classe métier testée
articlesManager =
(IArticlesManager) new ArticlesManagerWithDataBase("org.firebirdsql.jdbc.FBDriver", "jdbc:firebirdsql:localhost/3050:d:/databases/dbarticles.gdb","someone","somepassword");
}
La classe de test crée une instance de la classe métier [ArticlesManagerWithDataBase] qui crée à son tour, dans son constructeur, une instance de classe d'accès aux données [ArticlesDaoPlainJdbc].
La solution avec Spring va éliminer le besoin qu'a la classe métier [ArticlesManagerWithDataBase] de connaître le nom [ArticlesDaoPlainJdbc] de la classe d'accès aux données dont elle a besoin. Cela permettra d'en changer sans toucher au code java de la classe métier. Spring va permettre de créer en même temps les deux singletons, celui de la couche d'accès aux données et celui de la couche métier. Le fichier de configuration de Spring va définir un nouveau bean :
...
<beans>
<!-- la classe d'accès aux données -->
<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>
...
La nouveauté réside dans le bean définissant le singleton de la classe métier à créer :
<bean id="articlesManager" class="istia.st.articles.domain.ArticlesManagerWithDataBase">
<property name="articlesDao">
<ref bean="articlesDao"/>
</property>
</bean>
1. la classe implémentant le bean [articlesManager] est définie : [ArticlesManagerWithDataBase]
2. le champ [articlesDao] du bean reçoit une valeur par la balise <property name="articlesDao">. Il s'agit du champ défini dans la classe [ArticlesManagerWithDataBase] :
public class ArticlesManagerWithDataBase implements IArticlesManager {
// interface d'accès aux données private IArticlesDao articlesDao;
public IArticlesDao getArticlesDao() { return articlesDao; }
public void setArticlesDao(IArticlesDao articlesDao) { this.articlesDao = articlesDao; }
Pour que le champ [articlesDao] puisse être initialisé par Spring et sa balise <property>, il faut que le champ suive la norme JavaBean et qu'il existe une méthode [setArticlesDao] pour initialiser le champ [articlesDao]. On notera le nom de la méthode, dérivé de façon bien précise du nom du champ. De façon parallèle, il existe souvent une méthode [get...] pour obtenir la valeur du champ. Ici, c'est la méthode [getArticlesDao]. Dans cette nouvelle mouture, la classe [ArticlesManagerWithDataBase] n'a plus de constructeur. Elle n'en a plus besoin.
- la valeur qui sera affectée au champ [articlesDao] par Spring est celui du bean [articlesDao] défini dans son fichier de configuration :
<bean id="articlesManager"
class="istia.st.articles.domain.ArticlesManagerWithDataBase"> <property name="articlesDao">
<ref bean="articlesDao"/>
</property> </bean>
- lorsque Spring construira le singleton [ArticlesManagerWithDataBase], il sera amené à créer également le singleton [ArticlesDaoPlainJdbc] :
o Spring établira un graphe de dépendances des beans et verra que le bean [articlesManager] dépend du bean [articlesDao]
o il construira le bean [articlesDao], donc un objet de type [ArticlesDaoPlainJdbc]
o puis il construira le bean [articlesManager] de type [ArticlesManagerWithDataBase]
Imaginons maintenant un test JUnit pour la classe [ArticlesManagerWithDataBase]. Il pourrait ressembler à ce qui suit :
public class TestSpringArticlesManagerWithDataBase extends TestCase { // teste la classe métier [ArticlesManagerWithDataBase]
// une instance de la classe métier testée private IArticlesManager articlesManager; protected void setUp() throws Exception {
// récupère une instance d'accès aux données
articlesManager = (IArticlesManager) (new XmlBeanFactory(new ClassPathResource(
"springArticlesManagerWithDataBase.xml"))).getBean("articlesManager");
}
Suivons le déroulement de création des deux singletons définis dans le fichier Spring nommé [springArticlesManagerWithDataBase.xml].
- la méthode [setUp] ci-dessus demande une référence du bean nommé [articlesManager]
- Spring consulte son fichier de configuration, trouve le bean [articlesManager]. S'il est déjà créé, il se contente de
rendre une référence sur l'objet (singleton), sinon il le crée.
- Spring voit la dépendance du bean [articlesManager] vis à vis du bean [articlesDao]. Il crée donc le singleton [articlesDao] de type [ArticlesDaoPlainJdbc] si celui-ci n'est pas déjà créé (singleton).
- il créée le singleton [articlesManager] de type [ArticlesManagerWithDataBase]
Ce mécanisme pourrait être schématisé comme suit :
Rappelons le squelette de la classe [ArticlesManagerWithDataBase] :
public class ArticlesManagerWithDataBase implements IArticlesManager {
// interface d'accès aux données private IArticlesDao articlesDao;
public IArticlesDao getArticlesDao() { return articlesDao; }
public void setArticlesDao(IArticlesDao articlesDao) { this.articlesDao = articlesDao; }
A la fin de la construction des singletons par Spring, on a un objet de type [ArticlesManagerWithDataBase] qui a son champ [articlesDao] initialisé sans qu'il sache comment. On dit qu'on a injecté de la dépendance dans l'objet
[ArticlesManagerWithDataBase]. On dit également qu'on a inversé le contrôle : ce n'est plus l'objet [ArticlesManagerWithDataBase] qui prend l'initiative de créer lui-même l'objet implémentant l'interface [IArticlesDao] dont il a besoin, c'est l'application au plus haut niveau (lorsqu'elle s'initialise) qui prend soin de créer tous les objets dont elle a besoin en gérant les dépendances de ceux-ci entre-eux.
L'intérêt principal de la configuration du singleton [ArticlesManagerWithDataBase] par un fichier Spring, est que maintenant on peut changer la classe d'implémentation correspondant au champ [articlesDao] de la classe [ArticlesManagerWithDataBase] sans que le code de celle-ci soit modifié. Il suffit de changer le nom de la classe dans la définition au bean [articlesDao] dans le fichier Spring :
<bean id="articlesDao" class="istia.st.articles.dao.ArticlesDaoPlainJdbc"> ...
</bean>
deviendra par exemple :
<bean id="articlesDao" class="istia.st.articles.dao.ArticlesDaoIbatisSqlMap"> ...
</bean>
Le bean [ArticlesManagerWithDataBase] travaillera avec cette nouvelle classe d'accès aux données, sans même le savoir.
3 Spring IoC par la pratique
3.1 Exemple 1
Considérons la classe suivante :
package istia.st.springioc.domain;
public class Personne { private String nom; private int age;
// affichage Personne
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 présente :
- deux champs privés nom et age
- les méthodes de lecture (get) et d'écriture (set) de ces deux champs
- une méthode toString pour récupérer la valeur de l'objet [Personne] sous la forme d'une chaîne de caractères
- une méthode init qui sera appelée par Spring à la création de l'objet, une méthode close qui sera appelée à la destruction de l'objet
... ... ...
La structure du projet Eclipse de notre application est la suivante :
Commentaires :
- le dossier [src] contient les codes source. Les codes compilés iront dans un dossier [bin] non représenté ici.
- le fichier [config.xml] est à la racine du dossier [src]. La construction du projet le recopie automatiquement dans le dossier
[bin], qui fait partie du [ClassPath] de l'application. C'est là qu'il est recherché par l'objet [ClassPathResource].
- le dossier [lib] contient trois bibliothèques Java nécessaires à l'application :
n commons-logging.jar et spring-core.jar pour les classes Spring
n junit.jar pour les classes JUnit
- le dossier [lib] fait partie, lui aussi, du [ClassPath] de l'application
L'excécution de la méthode [test1] du test JUnit donne les résultats suivants :
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]
Commentaires :
- Spring logue un certain nombre d'événements grâce à la bibliothèque [commons-logging.jar]. Ces logs nous permettent de mieux comprendre le fonctionnement de Spring.
- le fichier [config.xml] a été chargé puis exploité
- l'opération
Personne personne1 = (Personne) bf.getBean("personne1");
a forcé la création du bean [personne1]. On voit le log de Spring à ce sujet. Parce que dans la définition du bean [personne1] on avait écrit [init-method="init"], la méthode [init] de l'objet [Personne] créé a été exécutée. Le message correspondant est affiché.
- l'opération
System.out.println("personne1=" + personne1.toString());
a fait afficher la valeur de l'objet [Personne] créé.
- le même phénomène se répète pour le bean de clé [personne2].
- la dernière opération
personne2 = (Personne) bf.getBean("personne2"); System.out.println("personne2=" + personne2.toString());
n'a pas provoqué la création d'un nouvel objet de type [Personne]. Si cela avait été le cas, on aurait eu l'affichage de la méthode [init], ce qui n'est pas le cas ici. C'est le principe du singleton. Spring, par défaut, ne crée qu'un seul exemplaire des beans de son fichier de configuration. C'est un service de références d'objet. Si on lui demande la référence d'un objet non encore créé, il le crée et en rend une référence. Si l'objet a déjà été créé, Spring se contente d'en donner une référence.
- on peut remarquer qu'on n'a nulle trace de la méthode [close] de l'objet [Personne] alors qu'on avait écrit dans la définition des beans [destroy-method=close]. Il est possible que cette méthode ne soit exécutée que lorsque la mémoire occupée par l'objet est récupérée par le ramasse-miettes (garbage collector). Au moment où cela se passe, l'application est déjà terminée et l'écriture à l'écran n'a aucun effet. A vérifier.
Les bases d'une configuration Spring étant maintenant acquises, nous serons désormais un peu plus rapides dans nos explications.
3.2 Exemple 2
Considérons la nouvelle classe [Voiture] suivante :
package istia.st.springioc.domain;
public class Voiture {
private String marque;
private String type;
private Personne propriétaire;
// constructeurs
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 présente :
- trois champs privés type, marque et propriétaire. Ces champs peuvent être initialisés et lus par des méthodes publiques de beans get et set. Ils peuvent être également initialisés à l'aide du constructeur Voiture(String, String, Personne). La classe possède également un constructeur sans arguments afin de suivre la norme JavaBean.
- une méthode toString pour récupérer la valeur de l'objet [Voiture] sous la forme d'une chaîne de caractères
- une méthode init qui sera appelée par Spring juste après la création de l'objet, une méthode close qui sera appelée à la destruction de l'objet
Pour créer des objets de type [Voiture], nous utiliserons le fichier Spring [config.xml] suivant :
...
<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>
Ce fichier ajoute aux définitions précédentes un bean de clé "voiture1" de type [Voiture]. Pour initialiser ce bean, on aurait pu écrire :
<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>
Plutôt que de choisir cette méthode déjà présentée, nous avons choisi ici, d'utiliser le constructeur Voiture(String, String, Personne) de la classe. Par ailleurs, le bean [voiture1] définit la méthode à appeler lors de la construction initiale de l'objet [init¬method] et celle à appeler lors de la destruction de l'objet [destroy-method].
Pour nos tests, nous utiliserons la classe de test JUnit déjà présentée, en lui ajoutant la méthode [test2] suivante :
public void test2() {
// récupération du bean [voiture1]
Voiture Voiture1 = (Voiture) bf.getBean("voiture1"); System.out.println("Voiture1=" + Voiture1.toString());
}
La méthode [test2] récupère le bean [voiture1] et l'affiche.
La structure du projet Eclipse reste celle qu'elle était dans le test précédent. L'excécution de la méthode [test2] du test JUnit donne les résultats suivants :
1. 18 sept. 2004 14:56:10 org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions
2. INFO: Loading XML bean definitions from class path resource [config.xml]
3. 18 sept. 2004 14:56:10 org.springframework.beans.factory.support.AbstractBeanFactory getBean
4. INFO: Creating shared instance of singleton bean 'voiture1'
5. 18 sept. 2004 14:56:10 org.springframework.beans.factory.support.AbstractBeanFactory getBean
6. INFO: Creating shared instance of singleton bean 'personne2'
7. init personne [nom=[Brigitte], age=[20]]
8. 18 sept. 2004 14:56:10 org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory autowireConstructor
9. INFO: Bean 'voiture1' instantiated via constructor [public istia.st.springioc.domain.Voiture (java.lang.String,java.lang.String,istia.st.springioc.domain.Personne)]
10. init voiture [Voiture : marque=[Peugeot] type=[307] propriétaire=[nom=[Brigitte], age=[20]]]
11. Voiture1=Voiture : marque=[Peugeot] type=[307] propriétaire=[nom=[Brigitte], age=[20]]
Commentaires :
1. la méthode [test2] demande une référence sur le bean [voiture1]
2. ligne 4 : Spring commence la création du bean [voiture1] car ce bean n'a pas encore été créé (singleton)
3. ligne 6 : parce que le bean [voiture1] référence le bean [personne2], ce dernier bean est construit à son tour
4. ligne 7 : le bean [personne2] a été créé. Sa méthode [init] est alors exécutée.
5. ligne 9 : Spring indique qu'il va utiliser un constructeur pour créer le bean [voiture1]
6. ligne 10 : le bean [voiture1] a été créé. Sa méthode [init] est alors exécutée.
7. ligne 11 : la méthode [test2] fait afficher la valeur du bean [voiture1]
3.3 Exemple 3
Nous introduisons la nouvelle classe [GroupePersonnes] suivante :
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; }
// affichage
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() + "]");
}
}
Ses deux membres privés sont :
membres : un tableau de personnes membres du groupe
groupesDeTravail : un dictionnaire affectant une personne à un groupe de travail
On remarquera ici que la classe [GroupePersonnes] ne définit pas de constructeur sans argument pour suivre la norme JavaBean. On rappelle qu'en l'absence de tout constructeur, il existe un constructeur "par défaut" qui est le constructeur sans arguments et qui ne fait rien.
On cherche ici, à montrer comment Spring permet d'initialiser des objets complexes tels que des objets possédant des champs de type tableau ou dictionnaire. On ajoute un nouveau bean au fichier Spring [config.xml] précédent :
<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. la balise <list> permet d'initialiser un champ de type tableau ou implémentant l'interface List avec différentes valeurs.
2. la balise <map> permet de faire la même chose avec un champ implémentant l'interface Map
Pour nos tests, nous utiliserons la classe de test JUnit déjà présentée, en lui ajoutant la méthode [test3] suivante :
public void test3() {
// récupération du bean [groupe1]
GroupePersonnes groupe1 = (GroupePersonnes) bf.getBean("groupe1");
System.out.println("groupe1=" + groupe1.toString());
}
La méthode [test3] récupère le bean [groupe1] et l'affiche.
La structure du projet Eclipse reste celle qu'elle était dans le test précédent. L'excécution de la méthode [test3] du test JUnit donne les résultats suivants :
· 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}
Commentaires :
o la méthode [test3] demande une référence du bean [groupe1]
o ligne 4 : Spring commence la création de ce bean
o parce que le bean [groupe1] référence les beans [personne1] et [personne2], ces deux beans sont créés (lignes 6 et 9) et leur méthode init exécutée (lignes 7 et 10)
o ligne 11 : le bean [groupe1] a été créé. Sa méthode [init] est maintenant exécutée.
o ligne 12 : affichage demandé par la méthode [test3].
4 Spring pour configurer les applications web à trois couches
4.1 Architecture générale de l'application
On souhaite construire une application 3-tier ayant la structure suivante :
· les trois couches seront rendues indépendantes grâce à l'utilisation d'interfaces Java
· l'intégration des trois couches sera réalisée par Spring
· on créera des paquetages séparés pour chacune des trois couches que l'on appellera Control, Domain et Dao. Un paquetage supplémentaire contiendra les applications de tests.
La structure de l'application sous Eclipse pourrait être la suivante :
4.2 La couche DAO d'accès aux données
La couche DAO implémentera l'interface suivante :
package istia.st.demo.dao;
public interface IDao1 {
public int doSometingInDaoLayer(int a, int b);
}
· écrire deux classes Dao1Impl1 et Dao1Impl2 implémentant l'interface IDao1. La méthode Dao1Impl1. doSomethingInDaoLayer rendra a+b et méthode Dao1Impl2. doSomethingInDaoLayer rendra a-b.
· écrire une classe de test JUnit testant les deux classes précédentes
4.3 La couche métier
La couche métier implémentera l'interface suivante :
package istia.st.demo.domain;
public interface IDomain1 {
public int doSomethingInDomainLayer(int a, int b);
}
· écrire deux classes Domain1Impl1 et Domain1Impl2 implémentant l'interface IDomain1. Ces classes auront un constructeur recevant pour paramètre de type IDao1. La méthode Domain1Impl1.doSomethingInDomainLayer incrémentera a et b d'une unité puis passera ces deux paramètres à la méthode doSomethingInDaoLayer de l'objet de type IDao1 reçu. La méthode Domain1Impl2.doSomethingInDomainLayer elle, décrémentera a et b d'une unité avant de faire la même chose.
· écrire une classe de test JUnit testant les deux classes précédentes