• 15 heures
  • Moyenne

Ce cours est visible gratuitement en ligne.

course.header.alt.is_video

course.header.alt.is_certifying

J'ai tout compris !

Mis à jour le 15/12/2020

Mieux utiliser Spring

Les concepts d'inversion de contrôle et d'injection de dépendances vous sont désormais familiers. Vous savez ce qu'est l'abstraction et comment mettre tout cela en oeuvre avec Spring. Dans ce chapitre, je vais aborder quelques points afin d'améliorer et étendre votre utilisation de Spring.

Organiser la configuration de Spring IOC

Compartimenter la configuration

Une des premières choses à faire maintenant que vous avez acquis les bases de Spring IoC, c'est de mieux organiser sa configuration. En effet, vous avez pu vous rendre compte que le fichier XML de configuration du contexte va vite devenir énorme car il va falloir y lister tous les beans (managers, DAO, factories...) et leurs injections de dépendances. De plus, dans notre exemple, une grande partie du fichier XML sera identique entre celui du module ticket-webapp et celui du module ticket-batch.

Sachez qu'il est possible d'importer dans le fichier XML de configuration, d'autres fichiers XML de configuration. Ainsi, ce que je vous conseillerai, c'est de répartir votre configuration du contexte sur plusieurs fichiers XML, mais pas n'importe comment !

Séparer le bootstrapping

Pour commencer, je sépare le bootstrapping du reste de la configuration. Pour le moment ça ne change pas grand-chose, mais ça ne va pas durer.

Je crée un fichier bootstrapContext.xml et dans ce fichier j'importe le fichier applicationContext.xml actuel :

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns="http://www.springframework.org/schema/beans"
       xsi:schemaLocation="
            http://www.springframework.org/schema/beans
            http://www.springframework.org/schema/beans/spring-beans.xsd">

    <!-- Inclusion d'autres fichiers de contexte Spring -->
    <import resource="classpath:/applicationContext.xml" />
</beans>

Bien sûr, il ne faut pas oublier de modifier le chargement de la configuration dans les modules ticket-batch et ticket-webapp.

package org.example.demo.ticket.batch;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class Main {

    public static void main(String[] pArgs) throws TechnicalException {
        ApplicationContext vApplicationContext
            = new ClassPathXmlApplicationContext("classpath:/bootstrapContext.xml");

        // ...
    }
}
<web-app>
    <!-- ... -->
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath:/bootstrapContext.xml</param-value>
    </context-param>
</web-app>
Séparer la configuration de chaque module

L'idée est de :

  1. Faire un fichier de contexte par module/couche applicative et de le mettre dans le module Maven concerné (businessContext.xml dans le module ticket-businessconsumerContext.xml dans le module ticket-consumer...).

  2. D'importer ces fichiers dans le fichier de bootstrapping (bootstrapContext.xml). ticket-batch et ticket-webapp.

Exemple pour le fichier consumerContext.xml :

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns="http://www.springframework.org/schema/beans"
       xsi:schemaLocation="
            http://www.springframework.org/schema/beans
            http://www.springframework.org/schema/beans/spring-beans.xsd">

    <!-- ===== Déclaration des DAO ===== -->
    <bean id="projetDao" class="org.example.demo.ticket.consumer.impl.dao.ProjetDaoImpl"/>
    <bean id="ticketDao" class="org.example.demo.ticket.consumer.impl.dao.TicketDaoImpl"/>

    <!-- ===== DaoFactory ===== -->
    <bean id="daoFactory" class="org.example.demo.ticket.business.impl.DaoFactory">
        <property name="projetDao" ref="projetDao"/>
        <property name="ticketDao" ref="ticketDao"/>
    </bean>
</beans>

Exemple pour le fichier businessContext.xml :

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns="http://www.springframework.org/schema/beans"
       xsi:schemaLocation="
            http://www.springframework.org/schema/beans
            http://www.springframework.org/schema/beans/spring-beans.xsd">

    <!-- ===== Déclaration des Managers ===== -->
    <bean id="projetManager" class="org.example.demo.ticket.business.impl.manager.ProjetManagerImpl">
        <property name="daoFactory" ref="daoFactory" />
    </bean>
    <bean id="ticketManager" class="org.example.demo.ticket.business.impl.manager.TicketManagerImpl">
        <property name="daoFactory" ref="daoFactory" />
    </bean>

    <!-- ===== ManagerFactory ===== -->
    <bean id="managerFactory" class="org.example.demo.ticket.business.impl.ManagerFactoryImpl">
        <property name="projetManager" ref="projetManager"/>
        <property name="ticketManager" ref="ticketManager"/>
    </bean>
</beans>

Vous pouvez remarquer que dans ce fichier businessContext.xml, je fais rérérence à un bean déclaré dans le fichier consumerContext.xml : le bean daoFactory. Cela ne pose pas de problème car j'importe les deux fichiers dans le fichier de bootstrapping et Spring se charge de fusionner toute la configuration apportée par ces fichiers.

Le fichier bootstrapContext.xml :

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns="http://www.springframework.org/schema/beans"
       xsi:schemaLocation="
            http://www.springframework.org/schema/beans
            http://www.springframework.org/schema/beans/spring-beans.xsd">

    <!-- Inclusion d'autres fichiers de contexte Spring -->
    <import resource="classpath:/org/example/demo/ticket/consumer/consumerContext.xml" />
    <import resource="classpath:/org/example/demo/ticket/business/businessContext.xml" />
    <import resource="classpath:/org/example/demo/ticket/webapp/webappContext.xml" />
</beans>

Opter pour d'autres types d'injection de dépendances

Il existe plusieurs types d'injection de dépendances. Vous les avez déjà aperçus dans le chapitre précédent mais sans forcément y faire attention.

Ces types d'injections sont :

  • l'injection par constructeur

  • l'injection par setter

  • l'injection par champs ou attribut

L'injection par constructeur

Comme son nom l'indique, l'injection se fait au niveau du constructeur. Voici ce que cela donne avec la classe ManagerFactoryImpl et les annotations :

public class ManagerFactoryImpl implements ManagerFactory {

    @Inject
    public  ManagerFactoryImpl(ProjetManager pProjetManager,
                               TicketManager pTicketManager) {
        this.projetManager = pProjetManager;
        this.ticketManager = pTicketManager;
    }

    // ...
}

Sans les annotations :

public class ManagerFactoryImpl implements ManagerFactory {

    public  ManagerFactoryImpl(ProjetManager pProjetManager,
                               TicketManager pTicketManager) {
        this.projetManager = pProjetManager;
        this.ticketManager = pTicketManager;
    }

    // ...
}
<beans>
    <bean id="managerFactory" class="org.example.demo.ticket.business.impl.ManagerFactoryImpl">
        <constructor-arg ref="projetManager"/>
        <constructor-arg ref="ticketManager"/>
    </bean>
</beans>

Avec l'injection par constructeur, le bean est pleinement configuré dès le retour du constructeur. Cela évite d'avoir des dépendances à null. Vous pouvez aussi créer des beans immuables, sans setter, car ils ne sont pas nécessaires. Cependant, il est impossible d'avoir un cycle de dépendances entre les beans.

L'injection par setter

Cette fois-ci, l'injection se fait grâce aux setters. Voici ce que cela donne avec la classe ManagerFactoryImpl et les annotations :

public class ManagerFactoryImpl implements ManagerFactory {

    private ProjetManager projetManager;
    private TicketManager ticketManager;

    public ManagerFactoryImpl() {
    }

    public ProjetManager getProjetManager() {
        return projetManager;
    }

    @Inject
    public void setProjetManager(ProjetManager pProjetManager) {
        projetManager = pProjetManager;
    }

    public TicketManager getTicketManager() {
        return ticketManager;
    }

    @Inject
    public void setTicketManager(TicketManager pTicketManager) {
        ticketManager = pTicketManager;
    }

    //...
}

Sans les annotations :

public class ManagerFactoryImpl implements ManagerFactory {

    private ProjetManager projetManager;
    private TicketManager ticketManager;

    public ManagerFactoryImpl() {
    }

    public ProjetManager getProjetManager() {
        return projetManager;
    }
    public void setProjetManager(ProjetManager pProjetManager) {
        projetManager = pProjetManager;
    }
    public TicketManager getTicketManager() {
        return ticketManager;
    }
    public void setTicketManager(TicketManager pTicketManager) {
        ticketManager = pTicketManager;
    }

    //...
}
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns="http://www.springframework.org/schema/beans"
       xsi:schemaLocation="
            http://www.springframework.org/schema/beans
            http://www.springframework.org/schema/beans/spring-beans.xsd">

    <!-- ===== ManagerFactory ===== -->
    <bean id="managerFactory" class="org.example.demo.ticket.business.impl.ManagerFactoryImpl">
        <property name="projetManager" ref="projetManager"/>
        <property name="ticketManager" ref="ticketManager"/>
    </bean>
</beans>

Avec l'injection par setter, vous pouvez gérer des beans avec un cycle de dépendances. Vous pouvez aussi modifier leurs dépendances au cours de l'exécution de l'application.

Attention toutefois, lors de la construction du bean par Spring, les dépendances ne sont pas injectées tout de suite. Cela se passe en deux temps : d'abord le bean est instancié, puis les dépendances sont injectées dans l'ordre que Spring a déterminé suite à l'analyse de la hiérarchie de dépendances issue de la configuration. Ce qui est à prendre en considération si vous faites des traitements dans le constructeur.

L'injection par champs

Ici, l'injection se fait directement dans les champs (attributs) de la classe par introspection, sans passer par les setters. Voici ce que cela donne avec la classe ManagerFactoryImpl et les annotations :

public class ManagerFactoryImpl implements ManagerFactory {
    @Inject
    private ProjetManager projetManager;
    @Inject
    private TicketManager ticketManager;

    //...
}

Vous n'êtes plus obligé de créer les setters et donc avoir des beans immuables. Vous pouvez gérer des beans avec un cycle de dépendances.

Attention toutefois, lors de la construction du bean par Spring, les dépendances ne sont pas injectées tout de suite. C'est le même principe que pour l'injection par setters.

Bien entendu, vous pouvez mixer tous ces types d'injection mais comme pour l'utilisation du XML et des annotations, faites-le de manière cohérente et consistante tout au long du projet. Vous pouvez par exemple, injecter toutes les dépendances obligatoires via les constructeurs et les dépendances facultatives via des setters. Veuillez noter toutefois qu'un nombre important de paramêtres dans un constructeur est un indicateur de code potentiellement mal conçu...

Factoriser la configuration avec l'héritage de définition

Quand vous déclarez un bean et précisez sa configuration, cela s'appelle la définition d'un bean dans le jargon de Spring.

Si je reprends l'exemple des Managers, vous remarquez que j'injecte à chaque fois le bean daoFactory :

<beans>
    <bean id="projetManager" class="org.example.demo.ticket.business.impl.manager.ProjetManagerImpl">
        <property name="daoFactory" ref="daoFactory" />
    </bean>
    <bean id="ticketManager" class="org.example.demo.ticket.business.impl.manager.TicketManagerImpl">
        <property name="daoFactory" ref="daoFactory" />
    </bean>

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

Eh bien, il est possible de factoriser cette configuration en créant un bean abstrait :

<beans>
    <bean id="abstractManager" abstract="true">
        <property name="daoFactory" ref="daoFactory" />
    </bean>

    <bean id="projetManager" class="org.example.demo.ticket.business.impl.manager.ProjetManagerImpl"
        parent="abstractManager"/>
    <bean id="ticketManager" class="org.example.demo.ticket.business.impl.manager.TicketManagerImpl"
        parent="abstractManager"
    <!-- ... -->
</beans>

Ici, j'ai créé le bean abstractManager en spécifiant l'attribut abstract="true". Ce bean ne sera pas instancié par Spring — d'ailleurs aucune classe n'est précisée, il sert simplement de "modèle". Et, dans les beans projetManager et ticketManager, j'indique utiliser le bean abstractManager comme parent grâce à l'attribut parent.

Vous pouvez également :

  • préciser une classe pour le bean parent ;

  • surcharger une propriété de la définition du bean parent dans le bean enfant en l'ajoutant à sa définition...

Vous trouverez plus de détail dans la documentation officielle de Spring.

Utiliser d'autres scopes pour les beans

Je vous disais plus tôt dans le cours que, par défaut, les beans créés par Spring sont des « singletons ». En fait, ils n'implémentent pas le design pattern Singleton mais cela veut dire que Spring ne créera qu'une seule instance de ces beans et injectera cette instance partout où vous y faites référence. Plus précisément, ces beans ont un scope singleton.

Il existe d'autres scopes, que vous pouvez utiliser en spécifiant l'attribut scope dans la définition du bean.

Vous avez notamment le scope prototype. Avec ce scope, une instance du bean est créée par Spring à chaque fois que celui-ci est utilisé, c'est-à-dire :

  • à chaque fois que vous y faites référence dans une injection (ex: <property name="x" ref="beanId" />)

  • à chaque fois que vous le demandez au contexte de Spring via ApplicationContext.getBean(...)

Les principaux scopes sont singleton et prototype. Vous trouverez plus de détails, et les autres scopes dans la documentation officielle de Spring.

Les logs

Pour produire ses logs, Spring utilise Apache Commons Logging. Vous pouvez facilement récupérer ces logs dans Apache Log4J 2.

Pour cela, il vous faut ajouter les dépendances Maven suivantes :

<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-core</artifactId>
    <version>2.6.2</version>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-jcl</artifactId>
    <version>2.6.2</version>
    <scope>runtime</scope>
</dependency>

Et vous pouvez obtenir les logs de Spring via le logger org.springframework :

<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="info">
    <ThresholdFilter level="trace"/>

    <Appenders>
        <Console name="STDOUT">
            <PatternLayout pattern="%highlight{%-5level} [%t] %c : %m%n"/>
        </Console>
    </Appenders>

    <Loggers>
        <Logger name="org.springframework" level="debug" additivity="false">
            <AppenderRef ref="STDOUT"/>
        </Logger>

        <Logger name="org.example.demo.ticket" level="debug" additivity="false">
            <AppenderRef ref="STDOUT"/>
        </Logger>

        <Root level="warn">
            <AppenderRef ref="STDOUT"/>
        </Root>
    </Loggers>
</Configuration>

Utiliser Spring pour injecter d'autres éléments

Allez, cerise sur le gâteau pour finir, ou devrais-je dire lichette de vin blanc sur la tartiflette ;) : vous pouvez utiliser Spring pour injecter d'autres éléments que des beans/dépendances — du moins des éléments différents de ce que nous avons vu jusqu'à maintenant.

Je vais vous montrer quelques exemples.

Utiliser des valeurs de fichiers properties

<beans>
    <!-- chargement du fichier conf/db-ticket.properties -->
    <bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
        <property name="locations">
            <list>
                <value>file:${TICKET_HOME}/conf/db-ticket.properties</value>
            </list>
        </property>
        <property name="ignoreUnresolvablePlaceholders" value="false"/>
    </bean>


    <!-- création d'un bean javax.sql.DataSource en utilisant
         des propriétés chargées depuis le fichier conf/db-ticket.properties -->
    <bean id="dataSourceTicket" destroy-method="close"
          class="org.apache.commons.dbcp2.BasicDataSource">
        <property name="driverClassName" value="${database.ticket.driverClassName}"/>
        <property name="url" value="${database.ticket.url}"/>
        <property name="username" value="${database.ticket.username}"/>
        <property name="password" value="${database.ticket.password}"/>
    </bean>

    <!-- injection de la javax.sql.DataSource "dataSourceTicket"
         dans le bean parent des DAO -->
    <bean id="abstractDao" abstract="true">
        <property name="dataSourceTicket" ref="dataSourceTicket" />
    </bean>

    <bean id="projetDao" class="org.example.demo.ticket.consumer.impl.dao.ProjetDaoImpl"
        parent="abstractDao" />
    <bean id="ticketDao" class="org.example.demo.ticket.consumer.impl.dao.TicketDaoImpl"
        parent="abstractDao" />
</beans>

Injecter une ressource JNDI

<beans>
    <!-- Récupération de la ressource JNDI :
         javax.sql.DataSource pour la base de données DB_TICKET -->
    <bean id="dataSourceTicket" class="org.springframework.jndi.JndiObjectFactoryBean">
        <property name="jndiName" value="java:comp/env/jdbc/DB_TICKET" />
    </bean>

    <!-- Injection de la "dataSourceTicket" dans le bean parent des DAO -->
    <bean id="abstractDao" abstract="true">
        <property name="dataSourceTicket" ref="dataSourceTicket" />
    </bean>

    <bean id="projetDao" class="org.example.demo.ticket.consumer.impl.dao.ProjetDaoImpl"
        parent="abstractDao" />
    <bean id="ticketDao" class="org.example.demo.ticket.consumer.impl.dao.TicketDaoImpl"
        parent="abstractDao" />
</beans>

Bien évidemment, les exemples que je vous ai montrés ci-dessus sont loin d'être exhaustifs. Je vous laisse le soin de parcourir la documentation officielle pour découvrir encore plus de possiblités.

Conclusion

Voilà, vous arrivez à la fin de cette première partie. Ouf, ça fait plaisir ! En plus, votre ventre gargouille et vous avez une folle envie de tartiflette... :p

Vous avez vu, ou revu, les principes SOLID :

  • Single responsibility principle (responsabilité unique)

  • Open/closed principle (ouvert/fermé)

  • Liskov substitution principle (substitution de Liskov)

  • Interface segregation principle (ségrégation des interfaces)

  • Dependency inversion principle (inversion des dépendances)

De ces principes découlent des patrons de conception et des approches tels que :

  • l'inversion de contrôle : le flot d'exécution n'est plus totalement géré par le développeur ou les traitements de l'application, mais il est pris en charge par des éléments annexes.

  • le design pattern Factory : une classe est responsable de la fabrication/fourniture d'instance d'autres classes.

  • l'injection de dépendances : les objets dépendant d'autres objets ne se soucient plus de comment les obtenir, ils leur sont directement injectés.

  • les abstractions : très souvent réalisées à l'aide d'interfaces, ces abstractions sont les « contrats » des interactions entre les différentes couches de l'application. Les couches basses implémentent ces abstractions. Les couches hautes dépendent de ces abstractions et non des implémentations faites dans les couches basses.

Après avoir créé un système d'injection de dépendances rustique, nous sommes passés à un niveau supérieur avec Spring. Vous avez pu constater qu'en termes de configuration, Spring vous laisse le choix (fichiers XML, annotations Spring/JSR-250/JSR-330...). Je vous ai donné quelques conseils pour mieux utiliser Spring et vous organiser.

Dans le chapitre suivant, je vous montrerai comment Spring peut vous faciliter la vie lors de vos interactions avec une base de données. Vous exécuterez toujours des requêtes SQL, mais sans avoir à tout faire à la main en JDBC !!

Exemple de certificat de réussite
Exemple de certificat de réussite