injection de dépendances

Présentation du framework Spring

Spring est un framework J2EE (Java 2 Enterprise Edition) open source basé sur les principes de conteneur léger, d'inversion de contrôle, d'injection de dépendances et de programmation orientée aspect (POA). Ce framework qui s'intègre aisément avec d'autres (Struts, Hibernate, ...) est très répandu au sein de la communauté des développeurs J2EE.

Cet article vous permettra d'appréhender cet outil ainsi que les principes sur lesquels il se base. Présentation du framework Spring

Bibliographie

Bibliographie

Pour réaliser cet article, je me suis servi des ouvrages suivants:

  1. Spring In Action, par Craig WALLS et Ryan BREIDENBACH, aux éditions Manning Publications

  2. Spring par la pratique, par Julien DUBOIS, Jean-Philippe RETAILLÉ et Thierry TEMPLIER, aux éditions Eyrolles

  3. Spring, Java / J2EE Application Framework, par Thierry TEMPLIER (présentation PowerPoint)

  4. Les conteneurs IoC du futur, par Sami JABER

  5. Introduction au framework Spring, par Erik GOLLOT

  6. Tutoriel Spring IOC, par Serge TAHÉ (document PDF)

  7. Inversion of Control Containers and the Dependency Injection pattern, par Martin FOWLER

Conclusion

Conclusion

Nous avons vu dans cet article les principes des conteneurs légers ainsi que leur implémentation dans Spring. Grâce aux mécanismes sous-jacents d'inversion de contrôle et d'injection de dépendances, l'utilisation de ce framework permet de rendre les applications basées dessus mieux structurées et plus facilement évolutives et maintenables de par la diminution flagrante du couplage entre les différents composants logiciels du projet.

Cependant, Spring dispose d'autres atouts qui en font ainsi un véritable environnement de développement. Ainsi, le module de POA permet de réduire encore plus le couplage entre ces composants. En effet, le principe est de modéliser l'utilisation de composants applicatifs transverses susceptibles d'être utilisés dans toutes les couches de l'application de manière à ce que l'exécution de ces composants soit transparente pour les autres couches. Ce sera par exemple le cas de la gestion des traces applicatives, des transactions, de la sécurité, etc ...

Une autre force de Spring est de proposer en natif le framework Hibernate ainsi que différents frameworks orienté vers le web (Spring MVC, Spring WebFlow) et le support de frameworks tiers tels que Struts, pour ne citer que le plus connu.

Le conteneur léger de Spring

4 Le conteneur léger de Spring

Maintenant que nous avons étudié les principes de base des conteneurs légers, nous allons à présent voir comment Spring les met en pratique.

4.1 La fabrique de beans

La fabrique de beans constitue l'interface de base de Spring pour accéder aux fonctionnalités de son conteneur léger. Par interface de base, nous entendons interface disposant des fonctionnalités minimales de manipulation du conteneur d'IoC.

La fabrique de beans se manipule par le biais de deux interfaces, BeanFactory et BeanDefinitionRegistry.

4.1.2 L'interface BeanFactory

Son but est de gérer l'accès aux beans définis dans le référentiel des beans. Les méthodes qu'elle propose permettent uniquement l'accès à ces beans. Le code de cette interface est reproduit ci-dessous:

package org.springframework.beans.factory;

// Imports

public interface BeanFactory
{
    public abstract Object getBean(String beanName)
        throws BeansException;

    public abstract Object getBean(String beanName,
        Class  beanType)
        throws BeansException;

    public abstract boolean containsBean(String beanName);

    public abstract boolean isSingleton(String beanName)
        throws NoSuchBeanDefinitionException;

    public abstract Class getType(String beanName)
        throws NoSuchBeanDefinitionException;

    public abstract String[] getAliases(String beanName)
        throws NoSuchBeanDefinitionException;
}

La méthode getBean(String beanName) permet de récupérer un bean à partir du nom qui lui est associé dans le référentiel.

La méthode getBean(String beanName, Class beanType) permet en plus de spécifier le type du bean à récupérer.

La méthode containsBean(String beanName) permet de savoir si le conteneur léger contient le bean correspondant au nom passé en paramètre.

La méthode isSingleton(String beanName) permet de savoir si un bean est un singleton ou un prototype.

La méthode getType(String beanName) permet de récupérer le type du bean correspondant au paramètre.

La méthode getAliases(String beanName) permet de récupérer les différents alias pour un bean, Spring permettant de spécifier plusieurs noms pour un bean.

Cette interface propose les méthodes de base pour manipuler les beans au sein du conteneur. Spring fournit plusieurs autres interfaces étendant celle-ci de manière à offrir des fonctionnalités supplémentaires. Nous pouvons par exemple citer:

  • l'interface ListableBeanFactory qui offre des fonctions avancées de manipulation des beans

  • l'interface HierarchicalBeanFactory qui propose de hiérarchiser les fabriques de beans

4.1.3 L'interface BeanDefinitionRegistry

Autant l'interface BeanFactory permet d'accéder aux beans du conteneur, autant elle ne permet pas de d'indiquer à ce dernier les dépendances qu'il doit gérer. C'est le rôle de l'interface BeanDefinitionRegistry. Son code est reproduit ci-dessous:

package org.springframework.beans.factory.support;

// Imports

public interface BeanDefinitionRegistry
{
    public abstract int getBeanDefinitionCount();

    public abstract String[] getBeanDefinitionNames();

    public abstract boolean containsBeanDefinition(String beanName);

    public abstract BeanDefinition getBeanDefinition(String beanName)
        throws NoSuchBeanDefinitionException;

    public abstract void registerBeanDefinition(String beanName,
        BeanDefinition beanDef)
        throws BeansException;

    public abstract String[] getAliases(String beanName)
        throws NoSuchBeanDefinitionException;

    public abstract void registerAlias(String beanName,
        String beanAlias)
        throws BeansException;
}

La méthode getBeanDefinitionCount() permet de récupérer le nombre de beans déclarés dans le conteneur.

La méthode getBeanDefinitionNames() permet de récupérer l'ensemble des identifiants des beans gérés par le conteneur.

La méthode containsBeanDefinition(String beanName) permet de savoir si une définition existe pour le nom passé en paramètre.

La méthode getBeanDefinition(String beanName) permet de récupérer la définition d'un bean.

La méthode registerBeanDefinition(String beanName, BeanDefinition beanDef) permet d'enregistrer un bean au sein du conteneur.

La méthode getAliases(String beanName) permet de récupérer les différents alias pour un bean.

La méthode registerAlias(String beanName, String beanAlias) permet d'ajouter un alias pour un bean.

4.1.4 Implémentation de ces interfaces

Les deux interfaces précédemment décrites permettent d'interagir avec le conteneur. Spring fournit plusieurs classes implémentant ces dernières et répondant chacune à un besoin particulier.

Une implémentation simple est la classe DefaultListableBeanFactory disponible au sein du package org.springframework.beans.factory.support. Elle se contente de fournir simplement un accès au conteneur via les deux interfaces décrites ci-dessus.

La classe XmlBeanFactory du package org.springframework.beans.factory.xml est plus évoluée en ce sens qu'elle permet de définir le référentiel de beans par le biais d'un fichier XML. Pour cela, Spring fournit ses propres balises XML pour la définition des beans. Le schéma XML est disponible à l'adresse suivante:
http://www.springframework.org/schema/beans/spring-beans.xsd

4.2 Le contexte d'applications

Outre la de fabrique de beans, Spring propose la notion de contexte d'applications qui englobe la fabrique et propose des fonctionnalités supplémentaires mais sans rapport avec le concept de conteneur léger. Parmi ces fonctionnalités, on peut citer:

  • la hiérarchisation des contextes pour par exemple isoler les beans d'une couche afin qu'ils ne soient pas visibles depuis les autres couches

  • la gestion des messages de l'application et leur internationalisation

  • le chargement de ressources depuis le système de fichier, le classpath de l'application, le web, ...

  • ...

De même que les interfaces BeanFactory et BeanDefinitionRegistry possèdent leur propres implémentations, l'interface ApplicationContext a les siennes. Ainsi, le développeur a le choix de l'implémentation correspondant le mieux aux besoins de l'application.

Parmi les plus répandues, on distingue les classes FileSystemXmlApplicationContext et ClassPathXmlApplicationContext qui correspondent à la classe XmlBeanFactory évoquée ci-avant. Elles permettent de charger le référentiel du conteneur de Spring soit depuis le système de fichiers, soit depuis le classpath de l'application.

Par convention, le fichier XML contenant les définitions des beans est appelé applicationContext.xml. Si l'application doit comporter plusieurs fichiers de définition, par exemple un fichier par couche applicative, ceux-ci seront nommés applicationContext-<nom>.xml. Cette convention de nommage n'est pas impérative, il est évidemment possible d'utiliser un nom totalement arbitraire.

4.3 Définition des beans au sein du conteneur

Note: par la suite, nous allons nous concentrer sur la configuration du conteneur par fichier XML plus courante et plus commode que la manière programmatique.

Pour définir un bean au sein du conteneur, deux informations sont obligatoires:

  • le nom du bean

  • le type du bean pleinement qualifié

4.3.1 Structure du fichier XML

La balise racine du fichier de définition des beans est la balise beans.

<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd"
>


        <!-- Définitions -->

</beans>

Pour définir un bean, la syntaxe est la suivante:

<bean id="aBean"
      class="com.casteran.samples.ABean" />

où l'attribut id spécifie le nom du bean et l'attribut class le type du bean.

Pour ajouter un ou des alias pour le bean, il convient d'utiliser l'attribut name.

<bean id="aBean"
      class="com.casteran.samples.ABean"
      name="alias1,alias2" />

Les alias peuvent être séparés soit par une virgule, soit par un espace.

Pour spécifier que le bean défini doit être un singleton, il faut positionner l'attribut singleton à true. Par défaut, il est positionné à false, un bean est donc par défaut un prototype.

<bean id="aBean"
      class="com.casteran.samples.ABean"
      name="alias1,alias2"
      singleton="true" />

Après avoir défini les beans, l'étape suivante consiste à initialiser leurs propriétés en utilisant l'injection de dépendances, soit par constructeur, soit par modificateurs. Les deux parties suivantes décrivent ces méthodes.

4.3.1.1 L'injection par constructeur

Prenons les deux classes suivantes pour illustrer l'injection de dépendances par constructeur:

package com.casteran.samples;

public class ABean {
    // ...
}


package com.casteran.samples;

public class AnotherBean {
    private String aString;
    private ABean  aBean;
   
    // ...
   
    public AnotherBean(String aString, ABean aBean) {
        this.aString = aString;
        this.aBean   = aBean;
    }
   
    // ...
}

La classe nécessitant une dépendance, en l'occurrence AnotherBean, doit disposer d'un constructeur permettant d'initialiser la dépendance.

La définition dans le référentiel se fait de la manière suivante:

<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd"
>

    <bean id="aBean"
          class="com.casteran.samples.ABean" />

   
    <bean id="anotherBean"
          class="com.casteran.samples.AnotherBean">

        <constructor-arg>
            <value>
                Bean description
            </value>
        </constructor-arg>
        <constructor-arg>
            <ref bean="aBean" />
        </constructor-arg>
    </bean>
</beans>

Le bean anotherBean peut également être défini de la manière simplifiée suivante:

<bean id="anotherBean"
      class="com.casteran.samples.AnotherBean">

    <constructor-arg value="Bean description" />
    <constructor-arg ref="aBean" />
</bean>

Le tag ou attribut ref (suivant la forme utilisée) fait référence à un autre bean géré par Spring.

4.3.1.2 L'injection par accesseurs

Prenons les deux classes suivantes pour illustrer l'injection de dépendances par modificateurs:

package com.casteran.samples;

public class ABean {
    // ...
}


package com.casteran.samples;

public class AnotherBean {
    private String aString;
    private ABean  aBean;
   
    // ...
   
    public void setAString(String aString) {
        this.aString = aString;
    }
   
    public void setABean(ABean aBean) {
        this.aBean = aBean;
    }
   
    // ...
}

La classe nécessitant une dépendance, en l'occurrence AnotherBean, doit disposer d'un setter permettant d'initialiser la dépendance.

La définition dans le référentiel se fait de la manière suivante:

<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd"
>

    <bean id="aBean"
          class="com.casteran.samples.ABean" />

   
    <bean id="anotherBean"
          class="com.casteran.samples.AnotherBean">

        <property name="aString">
            <value>
                Bean description
            </value>
        </property>
       
        <property name="aBean">
            <ref bean="aBean" />
        </property>
    </bean>
</beans>

L'attribut name de la balise property correspond au nom de la variable de classe initialisée.

La balise property peut également être écrite sous la forme simplifiée suivante:

<property name="String" value="Bean description" />
<property name="String" ref="aBean" />

Le tag ou attribut ref (suivant la forme utilisée) fait référence à un autre bean géré par Spring.

Que ce soit dans le cas de l'injection par constructeur ou par accesseurs, un bean déclaré dans le conteneur et utilisé par d'autres beans est appelé collaborateur.

Spring permet d'injecter en tant que dépendances un grand nombre de types de données que nous allons détailler ci-dessous.

4.3.1.3 Les types simples

Les types simples que Spring gère sont les suivants, qu'ils soient scalaires ou objet:

  • les nombres: int, long, float, java.lang.Integer, java.lang.Long, ...

  • les caractères: char, java.lang.Character

  • les chaînes de caractères et tableaux de chaînes de caractères: String, String[]

  • les booléens: boolean, java.lang.Boolean

  • les tableaux de bytes: byte[]

  • les propriétés: java.util.Properties

  • les locales: java.util.Locale

  • les URLs: java.net.URL

  • les fichiers: java.io.File

  • les classes

  • la valeur null

L'exemple ci-dessous montre l'injection de tous ces types pour un bean. La forme retenue pour la définition des propriétés du bean est la version simplifiée.

package com.casteran.samples;

// Imports

public class SampleBean {
    private Integer    anInteger;
    private char       aChar;
    private Character  aCharacter;
    private String     aString;
    private String[]   aStringArray;
    private Boolean    aBoolean;
    private byte[]     aByteArray;
    private Properties someProperties;
    private Locale     aLocale;
    private URL        anURL;
    private File       aFile;
    private Class      aClass;
    private Object     aNullValue;
   
    // Setters
}


<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd"
>

    <bean id="sampleBean"
          class="com.casteran.samples.SampleBean">

        <property name="anInteger"
                   value="45" />


        <property name="aChar"
                  value="a" />


        <property name="aCharacter"
                  value="z" />


        <property name="aString"
                  value="qwerty" />


        <property name="aStringArray"
                  value="azerty,qwerty" />


        <property name="aBool"
                  value="true" />


        <property name="aBoolean"
                  value="false" />


        <property name="aByteArray"
                  value="a byte array" />


        <property name="someProperties"
                  value="key1=value1\nkey2=value2" />


        <property name="aLocale"
                  value="en_US" />


        <property name="anURL"
                  value="http://www.google.fr/" />


        <property name="aFile"
                  value="file:/home/reno/file.txt" />


        <property name="aClass"
                  value="java.text.SimpleDateFormat" />


        <property name="aNullValue">
            <null />
        </property>
    </bean>
</beans>

Spring va s'occuper de convertir les chaînes de caractères contenues dans le fichier XML vers le type scalaire ou Java correspondant.

4.3.1.4 Les types structurés

Les types de données structurés supportés pour l'injection de dépendances sont:

  • les listes: java.util.List

  • les sets: java.util.Set

  • les maps: java.util.Map

Le fichier XML ci-dessous montre comment initialiser ces types:

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
                           http://www.springframework.org/schema/beans/spring-beans.xsd"
>

    <bean id="sampleBean"
          class="com.casteran.samples.SampleBean">

        <property name="aList">
            <list>
                <value>item1</value>
                <value>item2</value>
            </list>
        </property>
       
        <property name="aSet">
            <set>
                <value>item1</value>
                <value>item2</value>
            </set>
        </property>
       
        <property name="aMap">
            <map>
                <entry key="key1" value="value1" />
                <entry key="key2">
                    <value>value2</value>
                </entry>
            </map>
        </property>
    </bean>
</beans>

Les valeurs contenues dans une liste, un set ou une map peuvent être des beans gérés par le conteneur léger. Dans ce cas, au lieu d'utiliser la balise <value />, nous utilisons la balise <ref/> dont l'attribut bean doit contenir le nom du bean adéquat.

4.3.1.5 Les collaborateurs

Comme nous l'avons évoqué précédemment, Spring permet d'injecter dans un bean d'autres beans, qui sont dans ce cas dénommés collaborateurs. L'injection d'un collaborateur peut se faire de manière explicite comme vu dans les exemples précédents. Mais Spring permet aussi d'injecter les collaborateurs implicitement. Cette injection est appelée en anglais autowiring ou remplissage automatique.

Il existe plusieurs types d'autowiring:

  • l'autowiring par nom: byName. Spring se base sur le nom de la variable de classe pour charger le bean correspondant.

  • l'autowiring par type: byType. Spring va chercher un bean ayant le même type que la variable de classe à initialiser. Dans le cas où plusieurs beans sont du type demandé, Spring lève une exception. Dans le cas où aucun bean n'est trouvé, Spring initialise la variable à null.

  • l'autowiring par constructeur: constructor.Spring se base sur les paramètres du constructeur pour chercher les beans correspondants.

  • l'autowiring par détection: autodetect. Spring va utiliser de lui-même la recherche par type ou par constructeur. Si la classe n'a qu'un constructeur par défaut (sans paramètres), c'est l'autowiring par type qui est choisi.

La syntaxe pour l'autowiring est présentée ci-dessous:

<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd"
>

    <bean id="sampleBean"
          class="com.casteran.samples.SampleBean"
          autowire="byName | byType | constructor | autodetect" />

    </bean>
</beans>

4.4 Cycle de vie des beans

Chaque bean de Spring a une « naissance » et une « mort ». La naissance d'un bean survient par défaut au démarrage du conteneur. Ce comportement par défaut peut être surchargé en positionnant l'attribut lazy-init du tag bean à true pour spécifier de charger le bean au runtime. La syntaxe est présentée ci-dessous:

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
                           http://www.springframework.org/schema/beans/spring-beans.xsd"
>

    <bean id="sampleBean"
          class="com.casteran.samples.SampleBean"
          lazy-init="true" />

    </bean>
</beans>

La mort du bean ne survient pas au même moment selon la nature du bean. Si c'est un prototype, le bean sera détruit quand il n'y aura plus besoin de l'injecter et que le garbage collector aura été déclenché. Spring n'a donc pas de moyen de savoir quand meurt un prototype.

Si c'est un singleton, Spring va lui-même se charger de sa destruction et ainsi pouvoir déclencher des traitements si nécessaire.

4.4.1 Traitements à la naissance d'un bean

4.4.1.1 L'attribut init

Cet attribut est défini dans le fichier XML référençant les beans de l'application. La valeur qu'il accepte est le nom d'une méthode du bean en question. Cette méthode ne doit pas avoir de paramètres et ne doit pas comporter de type de retour. Vous trouverez ci-dessous un exemple d'utilisation de cette méthode:

Classe Java

package com.casteran.samples;

public class SampleBean {
    // ...
   
    public void initializeSampleBean() {
        // processing
    }
   
    // ...
}

Référentiel de dépendances:

<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd"
>

    <bean id="sampleBean"
          class="com.casteran.samples.SampleBean"
          init="initializeSampleBean">

        <!-- ... -->
    </bean>
</beans>

4.4.1.2 L'interface InitializingBean

Cette solution implique que la classe représentant le bean implémente l'interface InitializingBean du package org.springframework.beans.factory. Cette interface impose de définir la méthode afterPropertiesSet(). Cette méthode n'accepte pas de paramètres et ne renvoie pas de valeur. Le code ci-dessous illustre ce principe:

Classe Java:

package com.casteran.samples;

// imports

public class SampleBean implements InitializingBean {
    // ...
   
    public void afterPropertiesSet() {
        // processing
    }
   
    // ...
}

Référentiel de dépendances:

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
                           http://www.springframework.org/schema/beans/spring-beans.xsd"
>

    <bean id="sampleBean"
          class="com.casteran.samples.SampleBean">

        <!-- ... -->
    </bean>
</beans>

Nous ne spécifions plus quelle méthode doit être appelée à la création du bean.

A noter que la première solution est préférable à l'utilisation de cette interface car cette dernière crée un couplage entre l'application et l'API de Spring.

4.4.2 Traitements à la mort d'un singleton

Spring ne peut appliquer de traitements à la mort d'un bean que si celui-ci est un singleton, c'est-à-dire que sa destruction est assurée par le conteneur léger. Dans le cas d'un prototype, la mort du bean est à la charge du garbage collector et aucun mécanisme n'existe pour avertir Spring des destructions d'objets effectuées.

4.4.2.1 L'attribut destroy-method

Cet attribut est défini dans le fichier XML référençant les beans de l'application. La valeur qu'il accepte est le nom d'une méthode du bean en question. Cette méthode ne doit pas avoir de paramètres et ne doit pas comporter de type de retour. Vous trouverez ci-dessous un exemple d'utilisation de cette méthode:

Classe Java:

package com.casteran.samples;

public class SampleBean {
    // ...
   
    public void destroySampleBean() {
        // processing
    }
   
    // ...
}

Référentiel de dépendances:

<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd"
>

    <bean id="sampleBean"
          class="com.casteran.samples.SampleBean"
          destroy-method="destroySampleBean">

        <!-- ... -->
    </bean>
</beans>

4.4.2.2 L'interface DisposableBean

Cette solution implique que la classe représentant le bean implémente l'interface DisposableBean du package org.springframework.beans.factory. Cette interface impose de définir la méthode destroy(). Cette méthode n'accepte pas de paramètres et ne renvoie pas de valeur. Le code ci-dessous illustre ce principe:

Classe Java:

package com.casteran.samples;

// imports

public class SampleBean implements InitializingBean {
    // ...
   
    public void destroy() {
        // processing
    }
   
    // ...
}

Référentiel de dépendances:

<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd"
>

    <bean id="sampleBean"
          class="com.casteran.samples.SampleBean">

        <!-- ... -->
    </bean>
</beans>

Notez que nous ne spécifions plus quelle méthode doit être appelée à la destruction du bean.

Pour les mêmes raisons que pour l'exécution de traitements à la naissance d'un bean, il est plus judicieux d'utiliser la première solution.

4.5 Fonctionnalités avancées du contexte d'applications

Comme nous l'avons vu, le contexte d'applications de Spring est une fabrique évoluée proposant, outre la gestion des beans, certaines fonctionnalités sans rapport avec le principe de conteneur léger. Nous allons étudier les principales.

4.5.1 L'internationalisation

Traditionnellement, la localisation d'une application Java se fait au travers de fichiers de propriétés composés de paires de clés / valeurs. Ces fichiers sont nommés sous la forme suivante:

<nom_fichier>_<code_ISO_pays>.properties

Ainsi, pour la France, le nom du fichier serait par exemple:

myApplicationMessages_FR.properties

Un fichier sans code ISO est considéré comme le fichier contenant les messages de la langue par défaut de l'application.

Le contenu d'un fichier se présente sous la forme suivante:

my_application.welcome = Bienvenue {0}

{0} correspond à une variable dont la valeur sera passée en paramètre dans la méthode de récupération du message. Il est possible d'en mettre autant que l'on veut.

Spring permet d'accéder à ces paires de clés / valeurs au sein de son conteneur léger. La classe org.springframework.context.support.ResourceBundleMessageSource permet de concaténer plusieurs fichiers de propriétés pour une langue (par exemple, un fichier pourrait contenir les messages utilisés par la couche présentation et un autre les messages d'erreur). La propriété basenames contient la liste des différents fichiers de propriétés utilisés par l'application. Pour cela, il faut définir cette classe en tant que bean de la manière suivante:

<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd"
>

    <bean id="messageSource"
          class="org.springframework.context.support.ResourceBundleMessageSource">

        <property name="basenames">
            <list>
                <value>applicationMessages</value>
                <value>errorMessages</value>
            </list>
        </property>
    </bean>
</beans>

Spring va donc charger les fichiers applicationMessages_FR.properties et errorMessages_FR.properties si l'application est en français.

L'accès aux messages depuis l'application se fait grâce à la méthode getMessage() comme décrit ci-dessous:

public class Sample {
    public void aVoid(User aUser)
    throws NoSuchMessageException {
        ApplicationContext appCtx =
            new ClassPathXmlApplicationContext("applicationContext.xml");
       
        String welcomeMsg =
            appCtx.getMessage("my_application.welcome",
                              new String[] {aUser.getFullName()},
                              Locale.FRANCE);
    }
}

Le premier paramètre de la méthode est le nom de la clé.

Le second est un tableau d'Object contenant les éventuels paramètres de la clé. Si la clé ne comporte pas de paramètre, cet argument doit être mis à null.

Le dernier paramètre est la locale pour laquelle on souhaite récupérer le message. Cet argument doit être du type java.util.Locale. Spring va aller chercher dans les fichiers de messages dont le code ISO correspond à la locale. Si ce paramètre est égal à null, les fichiers par défaut (dont le nom ne comporte pas de code ISO) seront utilisés.

Cette méthode lève l'exception org.springframework.context.NoSuchMessageException si aucune clé ne correpond à celle demandée.

4.5.2 Le chargement de ressources

Une ressource est un fichier externe qu'utilise l'application. Cette ressource peut être disponible sur différents supports:

  • le système de fichiers local,

  • le protocole HTTP,

  • le classpath de l'application, ...

Selon le support, la méthode d'accès diffère, Java ne proposant pas de mécanisme générique d'accès aux ressources. Ainsi, pour récupérer un fichier sur le système de fichiers, nous utiliserons un objet de type java.io.File, tandis qu'un fichier publié sur un site Internet sera manipulé par un objet de type java.net.URL.

Spring permet d'abstraire le support de stockage des ressources et utilise les notions de ressource et de chargeur de ressources, représentées chacune par une interface:

  • org.springframework.core.io.Resource définit les ressources

  • org.springframework.core.io.ResourceLoader représente les chargeurs de ressources

Plusieurs implémentations de l'interface Resource sont fournies par Spring:

  • FileSystemResource,

  • ClassPathResource,

  • UrlResource,

  • InputStreamResource,

  • ByteArrayResource, ...

Le chargeur de ressources est disponible via les contextes d'applications disposant de la méthode getResource() définie par l'interface ResourceLoader. Cette méthode accepte un paramètre contenant le chemin relatif ou absolu d'accès au fichier. Si un chemin absolu est spécifié, il doit être au format URL. Pour qu'un bean puisse accéder à des ressources, il doit implémenter l'interface org.springframework.context.ResourceLoaderAware. Celle-ci propose la méthode setResourceLoader(ResourceLoader loader) qui permet au conteneur léger d'injecter un chargeur de ressources au bean. Une fois la ressource récupérée, le fichier correspondant est disponible par le biais des méthodes getFile() et getURL() selon le support.

Voici un exemple de chargement de ressources par le contexte d'applications:

public class SampleBean implements ResourceLoaderAware {
    private ResourceLoader resourceLoader;
   
    // ...
   
    public setResourceLoader(ResourceLoader resourceloader) {
        this.resourceLoader = resourceloader;
    }
   
    // ...
   
    public void processingResources() {
        ApplicationContext appCtx =
            new ClassPathXmlApplicationContext("applicationContext.xml");
       
        Resource fsResource =
            appCtx.getResource("file://home//rcasteran//Documents//data.txt");

        Resource webResource =
            appCtx.getResource("http://www.casteran.com/data.html");

        Resource classPathResource =
            appCtx.getResource("classpath:com/casteran/samples/data.txt");
       
        // Processing resources
        File fsFile  = fsResource.getFile();
        File cpFile  = classPathResource.getFile();
        URL  webFile = webResource.getURL();
       
        // ...
    }
   
    // ...
}

Principes des conteneurs légers

3 Principes des conteneurs légers

3.1 Inversion de contrôle

Les conteneurs légers, notion relativement récente en programmation, sont régis entre autres par le principe d'inversion de contrôle (en anglais IoC pour « Inversion of Control »). Un conteneur léger est donc un conteneur d'inversion de contrôle (« IoC Container » en anglais).

La notion de contrôle concerne le contrôle du flux d'exécution du programme. En programmation classique (sans inversion de contrôle), le programmeur maîtrise ce flux de bout en bout, tandis qu'en utilisant l'IoC, certaines fonctionnalités de l'application sont déléguées à d'autres composants logiciels externes qui vont se charger de leur bonne exécution. Par exemple, en programmation événementielle, le flux d'exécution n'est plus en totalité sous le contrôle du programmeur puisque la gestion des événements est déléguée à la bibliothèque graphique employée.

L'inversion de contrôle est un concept relativement ancien qui n'est pas seulement utilisé au sein des conteneurs légers. Les conteneurs d'EJB s'en servent également pour gérer les appels aux méthodes des EJB hébergés en leur sein. De même, de nombreux frameworks sont régis par ce principe. On peut par exemple citer Struts dont le contrôleur a la charge d'appeler les actions correspondant aux requêtes qui lui sont présentées. Cependant, c'est en programmation événementielle que l'inversion de contrôle est la plus utilisée.

L'inversion de contrôle de Spring et des autres conteneurs légers en est une version spécialisée. Ces frameworks permettent au développeur de déléguer la gestion des dépendances entre les différents composants logiciels. Ainsi, nous avons une plateforme générique pour la fabrique (instanciation) et la gestion (recherche, injection) des dépendances. Le paramétrage de l'IoC pour un conteneur léger peut se faire de manière programmatique au sein du code source de l'application, ou bien grâce à des fichiers de configuration, XML par exemple. Ci-dessous, deux schémas montrant l'effet de l'utilisation d'un conteneur d'IoC dans une application architecturée en couches:

Illustration 2: application en trois couches sans IoC
Illustration 2: application en trois couches sans IoC


Illustration 3: application en trois couches avec IoC
Illustration 3: application en trois couches avec IoC

Martin FOWLER a écrit un article célèbre sur les notions d'inversion de contrôle et d'injection de dépendances, disponible à cette adresse: Inversion of Control Containers and the Dependency Injection pattern

3.2 Gestion des dépendances

On peut distinguer deux catégories de conteneurs légers, selon leur façon de gérer les dépendances entre objets:

  • les conteneurs légers fonctionnant par recherche de dépendances

  • les conteneurs légers fonctionnant par injection de dépendances

Nous allons étudier ces deux cas.

3.2.1 Recherche de dépendances

Quand la recherche de dépendance est utilisée, l'objet contenant des dépendances va interroger le conteneur léger pour que ce dernier instancie les objets correspondants et les lui fournisse. Ce type de fonctionnement est celui utilisé par les EJB. En effet, pour accéder à un EJB, nous devons interroger un annuaire JNDI.

Le schéma ci-dessous illustre le principe de recherche de dépendances par le conteneur léger:

Illustration 4: principe de la recherche de dépendances
Illustration 4: principe de la recherche de dépendances

Ci-dessous un exemple de recherche de dépendances:

Référentiel de dépendances du conteneur au format XML:

<dependencies>
   
    <!-- ... -->
   
    <dependency name="bookDAO"
                class="com.casteran.samples.dao.BookDAO" />

   
    <!-- ... -->
   
</dependencies>

Classe Java interrogeant le conteneur:

public class BookServiceImpl implements IBookService {
    private BookDAO dao;
   
    // ...
   
    public BookServiceImpl() {
       
        // ...
       
        this.dao = (BookDAO) Container.getDependency("bookDAO");
       
        // ...
       
    }
   
    // ...
}

Le constructeur de la classe BookServiceImpl va interroger explicitement le conteneur, représenté par la classe Container, pour obtenir une instance de BookDAO référencée sous le nom bookDAO dans le fichier XML de paramétrage. Le conteneur va se charger de fabriquer puis de fournir cette dépendance à la classe BookServiceImpl.

3.2.2 Injection de dépendances

Comme nous l'avons vu à la section précédente, la recherche de dépendances se fait explicitement en créant un lien fort entre les classes et le conteneur léger. L'injection de dépendances vise à rendre ce lien implicite en rendant la gestion des dépendances transparente pour les classes concernées.

Pour opérer l'injection des dépendances, le conteneur initialise les dépendances de son propre fait, déchargeant l'application de cette tâche. Les dépendances sont créées et initialisées à partir d'un référentiel. Le schéma ci-dessous décrit le principe d'injection de dépendances:

Illustration 5: principe de l'injection de dépendances
Illustration 5: principe de l'injection de dépendances

Il existe deux catégories d'injection de dépendances:

  • l'injection par constructeur

  • l'injection par modificateurs

Nous allons étudier ces types d'injection.

3.2.2.1 L'injection de dépendances par constructeur

L'idée de l'injection de dépendances par constructeur est de préciser au référentiel quels objets vont être passés au constructeur de la classe pour instancier celle-ci dans un état valide. L'exemple ci-dessous illustre ce principe:

Classe Java nécessitant une dépendance:

public class BookServiceImpl implements IBookService {
    private BookDAO dao;
   
    // ...
   
    public BookServiceImpl(BookDAO aBookDAO) {
       
        // ...
       
        this.dao = aBookDAO;
       
        // ...
       
    }
   
    // ...
}

La classe BookServiceImpl dépend de la classe BookDAO. Pour cela, nous passons au constructeur une instance de cette dernière. L'instanciation de BookServiceImpl se fera par le conteneur léger grâce à sa déclaration dans le référentiel des dépendances.

Référentiel de dépendances du conteneur au format XML:

<dependencies>

    <!-- ... -->

    <dependency name="bookDAO"
                class="com.casteran.samples.dao.BookDAO"
                isUnique="true" />


    <dependency name="bookService"
                class="com.casteran.samples.services.BookServiceImpl"
                isUnique="true">

        <constructor>
            <arg local-ref="bookDAO" />
        </constructor>
    </dependency>

    <!-- ... -->

</dependencies>

Nous spécifions grâce au tag constructor que le paramètre à passer au constructeur de bookService (instance de BookServiceImpl) est la dépendance bookDAO (instance de BookDAO).

3.2.2.2 L'injection de dépendance par modificateurs

Il est possible pour injecter les dépendances, de passer non plus par le constructeur de la classe mais par les accesseurs (« setters ») correspondants aux attributs à initialiser. Reprenons l'exemple précédent pour l'adapter au cas présent:

Classe Java nécessitant une dépendance:

public class BookServiceImpl implements IBookService {
    private BookDAO dao;
   
    // ...
   
    public BookServiceImpl() {
        // ...
    }
   
    // ...
   
    public void setDao(BookDAO aBookDAO) {
        this.dao = aBookDAO;
    }
   
    // ...
}

Le constructeur ne prend plus aucun argument, l'initialisation de l'attribut dao étant maintenant à la charge de l'accesseur setDao().

Référentiel de dépendances du conteneur au format XML:

<dependencies>

    <!-- ... -->

    <dependency name="bookDAO"
                class="com.casteran.samples.dao.BookDAO"
                isUnique="true" />


    <dependency name="bookService"
                class="com.casteran.samples.services.BookServiceImpl"
                isUnique="true">

        <attribute name="dao" local-ref="bookDAO" />
    </dependency>

    <!-- ... -->

</dependencies>

Le conteneur léger va lui-même faire la correspondance entre l' attribute dao et le setter correspondant dans la classe BookServiceImpl.en utilisant par exemple la réflexion sur la classe pour trouver la méthode set+upperCase(attribute).

L'inconvénient de cette méthode est de ne plus suivre la bonne pratique qui consiste à utiliser le constructeur pour instancier une classe dans un état valide.

3.3 Cycle de vie des dépendances

Que les dépendances soient recherchées ou injectées, le conteneur léger doit s'assurer de leur instanciation, de leur initialisation, de leur fourniture, ... c'est-à-dire de leur cycle de vie. Mais contrairement aux conteneurs lourds, le cycle de vie d'un objet est le plus souvent réduit à deux états qui sont la création (instanciation puis initialisation) puis la destruction de l'objet.

3.3.1 Gestion d'événements

Les conteneurs d'IoC implémentent une gestion d'événements survenant durant la vie de l'objet. Ces événements sont déclenchés par l'appel de fonctions spécifiques de l'objet traité.

Ci-dessous un exemple de gestion d'événements basique par le conteneur léger:

Classe Java correspondant à la dépendance décrite dans le référentiel:

public class BookDAO {
   
    // ...
   
    public void doOnInit() {
        // processing
    }
   
    public void doOnDestroy() {
        // processing
    }
   
    // ...
}

Référentiel de dépendances du conteneur au format XML:

<dependencies>

    <!-- ... -->
   
    <dependency name="bookDAO"
                class="com.casteran.samples.dao.BookDAO"
                onInit="doOnInit"
                onDestroy="doOnDestroy" />


    <!-- ... -->
   
</dependencies>

Nous spécifions au conteneur d'appeler la méthode doOnInit() de la dépendance bookDAO lors de son initialisation et la méthode doOnDestroy() lors de sa destruction.

3.3.2 Gestion des singletons

Une classe est dite Singleton quand elle est faite de telle sorte qu'il ne puisse exister qu'une seule de ses instances. Classiquement, le design pattern singleton s'implémente ainsi:

public class BookServiceImpl implements IBookService {
    private static BookServiceImpl instance;
   
    // ...
   
    private BookServiceImpl() {
        // ...
    }
   
    public static BookServiceImpl getInstance() {
        if(instance == null)
            instance = new BookServiceImpl();
       
        return instance;
    }
   
    // ...
}

Le constructeur a une portée privée pour que la classe ne puisse être instanciée ailleurs.

L'inconvénient de cette implémentation est que la gestion du singleton est faite directement par la classe elle-même, cela ne répond pas à une problématique métier. Le fait d'utiliser un conteneur léger pour la création des dépendances va nous permettre de lui déléguer la gestion du Singleton sans que le développeur ait à s'en préoccuper. Dans ce cas, c'est au sein du référentiel des dépendances que l'on va spécifier que la dépendance doit avoir une instance unique ou non.

Par exemple:

<dependencies>

    <!-- ... -->
   
    <dependency name="bookService"
                class="com.casteran.samples.services.BookServiceImpl"
                isUnique="true" />

   
    <!-- ... -->
   
</dependencies>

Nous indiquons ici au conteneur que la dépendance bookService ne pourra avoir qu'une et une seule instance, grâce à l'attribut isUnique positionné à true. Inversement, positionner cet attribut à false aurait pour effet que la dépendance compterait autant d'instances que de fois où elle est recherchée ou injectée au sein du code de l'application.