Le contexte d’application

Nous avons vu au chapitre précédent que le Spring framework fournit un conteneur IoC. Ce conteneur est formé à partir de un ou plusieurs contextes d’application. Un contexte d’application contient la définition des objets que le conteneur doit créer ainsi que leurs interdépendances.

L’API du Spring Framework définit l’interface ApplicationContext. Il existe plusieurs implémentations de cette interface et donc plusieurs façons de définir un contexte d’application. À l’origine, un contexte d’application devait être décrit à l’aide d’un fichier XML. Avec l’évolution des technologies, le Spring Framework s’est enrichi de nouvelles classes implémentant l’interface ApplicationContext afin d’offrir des méthodes alternatives à la création d’un contexte d’application. La méthode la plus souvent recommandée consiste à ajouter des annotations sur les classes de notre application pour indiquer au Spring Framework comment le conteneur IoC doit être initialisé. Pour cela, nous devons utiliser une instance de la classe AnnotationConfigApplicationContext.

Mise en place du projet

Pour ce chapitre, vous pouvez générer votre projet à partir du site https://start.spring.io/ sans ajouter de dépendance particulière.

Vous pouvez également télécharger le modèle de projet Maven firstapp.zip.

Après avoir décompresser l’archive sur votre disque, vous pouvez importer votre projet sous Eclipse en choisissant dans le menu File > Import…. Puis choisissez comme type d’import : Maven > Existing Maven Projects.

../_images/eclipse_import_maven_project.png

Fenêtre d’import de projet dans Eclipse

Une nouvelle fenêtre s’ouvre pour vous demander d’indiquer le répertoire dans lequel se trouve le fichier pom.xml de votre projet. Cliquez sur le bouton Browse… pour sélectionner ce répertoire. Eclipse doit remplir le reste de la fenêtre avec les informations sur votre projet. Il ne vous reste plus qu’à cliquer sur le bouton Finish.

../_images/eclipse_import_maven_project2.png

Fenêtre d’import du projet Maven dans Eclipse

Une première application avec Spring

Pour illustrer la création d’un contexte d’application, nous allons commencer par un exemple très simple. Imaginons que nous souhaitions afficher l’heure courante sur la sortie standard. Bien évidemment, une application Java aussi simple n’a pas besoin d’un quelconque framework. Mais imaginons que nous souhaitions tout de même utiliser le Spring Framework. Il nous faut créer un contexte d’application et demander à Spring d’ajouter une objet de type LocalTime dans ce contexte. Puis nous pourrons extraire cet objet du conteneur pour l’afficher à l’écran.

Voici à quoi ressemble le programme correspondant :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
package dev.gayerie.firstapp;

import java.time.LocalTime;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;

public class TimeApplication {

  @Bean
  public LocalTime maintenant() {
    return LocalTime.now();
  }

  public static void main(String[] args) {
    try(AnnotationConfigApplicationContext appCtx
                 = new AnnotationConfigApplicationContext(TimeApplication.class)) {
      LocalTime time = appCtx.getBean("maintenant", LocalTime.class);
      System.out.println(time);
    }
  }
}

On crée une méthode main à la ligne 15 comme point d’entrée de notre application. Aux lignes 16-17, on crée une instance de AnnotationConfigApplicationContext qui est le contexte Spring de notre application. Notez que la création attend en paramètre la classe à utiliser pour définir le contexte. Dans notre exemple, il s’agit directement de la classe TimeApplication. Notez également que l’on crée l’instance du contexte d’application dans un structure de type try-with-resources. Cela nous garantit que la méthode close() du contexte d’application sera appelée à la fin du bloc et donc avant la fin de l’application. Nous reviendrons sur le fait qu’un contexte d’application doit être correctement fermé afin de lui laisser la possibilité de gérer correctement le cycle de vie des objets qu’il contient.

La création du contexte d’application implique que Spring va créer une instance de la classe TimeApplication passée en paramètre du constructeur. Il va ensuite chercher des annotations particulières sur cet objet. Par exemple, il va chercher toutes les méthodes déclarées avec l’annotation @Bean. Ces méthodes sont traitées comme des méthodes de fabrique (factory methods). Pour Spring, elles servent à créer des objets qui doivent être gérés par le contexte d’application. Le framework va donc appeler ces méthodes une à une et récupérer les objets qu’elles retournent en leur donnant un nom correspondant au nom de la méthode.

Note

La terme de bean renvoie aux JavaBeans qui désignent les composants élémentaires en Java. Bean est à la fois une métaphore et un trait d’humour. En effet, le langage Java tient son nom de la référence au café (en provenance de l’île de Java) que les concepteurs du langage ont bu en grande quantité durant sa création. Pour faire du bon café, il faut du bon grain. Donc, les composants principaux des programmes Java doivent naturellement être des grains (beans en anglais) de café.

Si le standard JavaBeans imposait de respecter certaines convention de codage, le terme de bean s’est peu à peu généralisé pour devenir synonyme d’objet. C’est dans ce sens très général qu’il est utilisé avec le Spring Framework : un bean est un objet.

Dans ce chapitre et les suivants, nous utiliserons le terme bean pour désigner un objet Java qui est ajouté dans le contexte d’application et qui est donc géré par le conteneur IoC du Spring Framework.

Dans notre exemple, la méthode maintenant() possède l’annotation @Bean (lignes 10 à 13). Elle sera donc appelée par Spring et l’objet de type LocalTime qu’elle retourne sera placé dans le contexte d’application avec le nom maintenant.

À la ligne 18, nous utilisons la méthode getBean du contexte d’application pour récupérer un objet de type LocalTime et qui s’appelle maintenant pour pouvoir l’afficher.

Note

S’il n’existe qu’un seul objet dans le contexte d’application qui est compatible avec le type demandé, alors il n’est pas nécessaire de spécifier le nom de l’objet :

LocalTime time = appCtx.getBean(LocalTime.class);
System.out.println(time);

Si vous ne souhaitez pas utiliser le nom de la méthode comme nom pour l’objet, vous pouvez déclarer le nom de l’objet avec l’attribut name de l’annotation @Bean :

@Bean(name = "maintenant")
public LocalTime getLocalTime() {
  return LocalTime.now();
}

Vous pouvez même donner plusieurs noms à un objet en passant un tableau de valeurs à l’attribut name :

@Bean(name = {"maintenant", "now", "ahora", "jetzt"})
public LocalTime getLocalTime() {
  return LocalTime.now();
}

Notion de portée (scope)

Les beans ajoutés dans un contexte d’application ont une portée (scope). Par défaut, Spring définit deux portées :

singleton

Cette portée évoque le modèle de conception singleton. Cela signifie qu’une seule instance de ce bean existe dans le conteneur IoC. Autrement dit, si un programme appelle une méthode getBean pour récupérer ce bean, chaque appel retourne le même objet.

prototype

Cette portée est l’inverse de la portée singleton. À chaque fois qu’un programme appelle une méthode getBean pour récupérer ce bean, chaque appel retourne une nouvelle instance du bean.

Le type de la portée peut être indiquée grâce à l’annotation @Scope. La portée par défaut dans le Spring Framework est singleton.

Si nous reprenons notre programme précédent, nous pourrions le faire évoluer pour afficher le temps toutes les secondes.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package dev.gayerie.firstapp;

import java.time.LocalTime;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;

public class TimeApplication {

  @Bean
  public LocalTime maintenant() {
    return LocalTime.now();
  }

  public static void main(String[] args) throws InterruptedException {
    try(AnnotationConfigApplicationContext appCtx
                 = new AnnotationConfigApplicationContext(TimeApplication.class)) {
      while (true) {
        Thread.sleep(1000);
        LocalTime time = appCtx.getBean("maintenant", LocalTime.class);
        System.out.println(time);
      }
    }
  }
}

Le programme précédent ne s’arrête jamais de lui-même puisqu’il contient une boucle infinie. À chaque itération, on attend 1000 millisecondes pour ensuite demander le bean au contexte d’application pour l’afficher. Si vous lancez ce programme, vous constaterez qu’il affiche à l’infini la même heure correspondant au lancement du programme. En effet, la portée d’un bean par défaut est singleton. Cela signifie que l’objet est instancié à la création du contexte d’application et qu’il n’existera qu’une fois.

Nous pouvons utiliser l’annotation @Scope pour modifier ce comportement et spécifier une portée prototype :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package dev.gayerie.firstapp;

import java.time.LocalTime;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Scope;

public class TimeApplication {

  @Bean
  @Scope("prototype")
  public LocalTime maintenant() {
    return LocalTime.now();
  }

  public static void main(String[] args) throws InterruptedException {
    try(AnnotationConfigApplicationContext appCtx
                 = new AnnotationConfigApplicationContext(TimeApplication.class)) {
      while (true) {
        Thread.sleep(1000);
        LocalTime time = appCtx.getBean("maintenant", LocalTime.class);
        System.out.println(time);
      }
    }
  }
}

Le programme se comporte bien comme attendu et le temps s’écoule lorsqu’on affiche l’heure sur la console. Prototype signifie que l’objet créé n’est pas l’objet définitif et qu’il doit être recréé à chaque fois qu’il est demandé au contexte d’application. Dans notre exemple, Spring rappelle continuellement notre méthode maintenant() à chaque fois que le programme récupère une instance de l’objet en appelant getBean().

Il peut sembler étonnant que la portée par défaut soit singleton. En fait, nous verrons que le Spring Framework est quasi exclusivement utilisé pour construire l’ossature d’une application. Il s’agit alors de créer des objets qui n’ont besoin d’être présents qu’une seule fois en mémoire.

Prudence

Il faut garder à l’esprit que, par défaut, la portée des objets créés par le Spring Framework est singleton. Donc l’implémentation de ces objets doit être thread safe. En effet, si notre application s’exécute dans un environnement concurrent (comme une application Web), le même objet est susceptible d’être appelé simultanément par plusieurs flux de traitement (threads).

Note

Dans certains contextes d’exécution, il existe d’autres portées disponibles. Par exemple, pour une application Web, vous pouvez définir des beans avec une portée request pour indiquer que la durée de vie de ces objets ne doit pas aller au delà du traitement de la requête courante. Dans le même contexte, vous pouvez définir des beans avec une portée session pour indiquer qu’ils devront être uniques par session utilisateur sur le serveur.

L’injection de bean

En plus de pouvoir enregistrer les beans que nous créons dans un contexte d’application, nous pouvons utiliser le Spring Framework pour nous aider à initialiser les dépendances entre les objets.

Pour illustrer ce fonctionnement, nous allons prendre un exemple d’application un peu plus proche de ce que nous pourrions trouver dans la réalité tout en gardant volontairement un code simple.

Imaginons que nous souhaitions créer une application pour réaliser des traitements sur des chaînes de caractères. Notre application sera constituée de deux catégories d’objets : d’une part un service qui doit réaliser le traitement demandé et d’autres part un fournisseur de données chargé de produire la chaîne de caractères utilisée par le service. Afin de garantir un découplage maximum, nous allons matérialiser les rôles par des interfaces. Nous pourrions créer nos propres interfaces mais l’API standard Java nous fournit déjà celles dont nous avons besoin. Le service de traitement implémentera l’interface Runnable et le fournisseur de données implémentera l’interface Supplier<String>.

Comme exemple de service de traitement, nous utiliserons une classe qui se contente d’afficher la donnée produite par le fournisseur :

Une implémentation de service de traitement
package dev.gayerie.appstring;

import java.util.function.Supplier;

public class WriterService implements Runnable {

  private Supplier<String> supplier;

  public WriterService(Supplier<String> supplier) {
    this.supplier = supplier;
  }

  @Override
  public void run() {
    System.out.println(supplier.get());
  }
}

Comme exemple de fournisseur de données, nous utiliserons une classe qui produit une chaîne de caractères en dur :

Une implémentation de fournisseur de données
package dev.gayerie.appstring;

import java.util.function.Supplier;

public class HardcodedSupplier implements Supplier<String> {
  @Override
  public String get() {
    return "Hello world";
  }
}

Pour réaliser notre application avec le Spring Framework, il faut ajouter une instance de ces objets dans le contexte d’application. Mais pour créer une instance de WriterService, nous avons besoin d’un objet de type Supplier<String>. Nous pouvons demander au Spring Framework de nous en fournir un disponible dans le contexte d’application en l’ajoutant comme paramètre de la méthode de fabrique :

L’application construite avec Spring
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package dev.gayerie.appstring;

import java.util.function.Supplier;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;

public class TaskApplication {

  @Bean
  public Supplier<String> dataSupplier() {
    return new HardcodedSupplier();
  }

  @Bean
  public Runnable task(Supplier<String> dataSupplier) {
    return new WriterService(dataSupplier);
  }

  public static void main(String[] args) throws InterruptedException {
    try (AnnotationConfigApplicationContext appCtx
                  = new AnnotationConfigApplicationContext(TaskApplication.class)) {
      appCtx.getBean(Runnable.class).run();
    }
  }
}

La méthode main de l’application crée un contexte d’application à partir de la classe courante et recherche le bean du contexte qui est compatible avec l’interface Runnable pour pouvoir lancer sa méthode run.

La partie la plus importante de notre exemple est l’implémentation de la méthode task. Elle est annotée avec @Bean pour indiquer à Spring qu’il s’agit d’une méthode qui fabrique un bean. Mais surtout, elle attend en paramètre un objet de type Supplier<String>. Pour réaliser cet appel, le Spring Framework va devoir trouver dans le contexte d’application un (et un seul) bean qui est compatible avec ce type. Il va trouver celui fournit par la méthode dataSupplier et le passer en paramètre.

Même si notre exemple reste trivial, nous pouvons utiliser un mécanisme d’injection de dépendance tout en garantissant un niveau d’abstraction important entre les différents objets de notre application.

Note

Le Spring Framework est capable de déduire l’ordre d’appel des différentes méthodes de fabrique selon les dépendances requises. Dans notre exemple, Spring doit nécessairement appeler la méthode dataSupplier avant la méthode task pour disposer d’une instance de Supplier<String>. L’ordre de déclaration des méthodes dans la classe n’a aucune importance. Spring est capable de déterminer l’ordre correct des appels en analysant le graphe des dépendances. Attention cependant à ne pas créer une dépendance cyclique (a dépend de b et b dépend de a). On comprend bien que ce type de situation ne peut pas être résolu par le Spring Framework et aboutira à une erreur à la création du conteneur IoC.

L’exécution du programme précédent affiche (entre quelques lignes de log du Spring Framework) :

Hello world

Les méthodes d’initialisation et de destruction

Le conteneur IoC du Spring Framework permet de gérer le cycle de vie des beans. Il permet notamment d’invoquer des méthodes d’initialisation et de destruction de ces beans. Par exemple, un objet peut initialiser une connexion à une base de données au lancement de l’application et libérer cette connexion à la fin de l’application. La gestion des ressources est un cas courant pour lequel nous pouvons avoir besoin de réaliser une phase d’initialisation et/ou de destruction.

Il est possible de préciser sur l’annotation @Bean le nom de la méthode d’initialisation avec l’attribut initMethod et le nom de la méthode de destruction avec l’attribut destroyMethod.

Nous pouvons compléter notre exemple avec une nouvelle implémentation d’un fournisseur de données :

Fournisseur de données à partir d’un fichier
package dev.gayerie.appstring;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.function.Supplier;

public class FileDataSupplier implements Supplier<String> {

  private String data;

  public void readData() throws IOException {
    data = Files.readString(Path.of("data.txt"));
  }

  @Override
  public String get() {
    return data;
  }

}

Cette implémentation nécessite l’appel à la méthode readData pour lire la chaîne de caractères à partir du fichier data.txt.

Dans notre classe représentant notre application, nous pouvons remplacer la méthode de fabrique :

L’application construite avec Spring
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package dev.gayerie.appstring;

import java.util.function.Supplier;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;

public class TaskApplication {

  @Bean(initMethod = "readData")
  public Supplier<String> dataSupplier() {
    return new FileDataSupplier();
  }

  @Bean
  public Runnable task(Supplier<String> dataSupplier) {
    return new WriterService(dataSupplier);
  }

  public static void main(String[] args) throws InterruptedException {
    try (AnnotationConfigApplicationContext appCtx
                  = new AnnotationConfigApplicationContext(TaskApplication.class)) {
      appCtx.getBean(Runnable.class).run();
    }
  }
}

À la ligne 10, nous indiquons que le bean possède une méthode d’initialisation qui sera automatiquement appelée par le framework.

Note

Pourquoi ne pas appeler directement la méthode readData dans la méthode de fabrique ?

@Bean
public Supplier<String> dataSupplier() {
  FileDataSupplier supplier = new FileDataSupplier();
  supplier.readData();
  return supplier;
}

Pour notre exemple, cette implémentation est équivalente. Cependant, nous verrons au chapitre suivant qu’il est possible de réaliser automatiquement des injections de dépendances sur le bean retourné par la méthode de fabrique. Dans ce cas, l’appel à la méthode d’initialisation est effectué après la phase d’injection afin de permettre à l’objet de s’initialiser avec toutes ses dépendances disponibles.

Pour illustrer l’utilisation d’une méthode de destruction, nous allons complexifier un peu notre application. L’API Java permet l’exécution de tâches concurrentes en utilisant un Executor. La classe Executors est une classe outil qui nous permet de créer un exécuteur multi-thread. Cependant, il est nécessaire d’appeler la méthode shutdown de cet exécuteur pour attendre la fin des traitements concurrents et libérer correctement les ressources. La méthode shutdown correspond bien à une méthode de destruction à appeler sur l’exécuteur.

Voici la nouvelle implémentation :

L’application construite avec Spring
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
package dev.gayerie.appstring;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.function.Supplier;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;

public class TaskApplication {

  @Bean
  public Supplier<String> dataSupplier() {
    return new HardcodedSupplier();
  }

  @Bean(initMethod = "readData")
  public Supplier<String> fileDataSupplier() {
    return new FileDataSupplier();
  }

  @Bean
  public List<Runnable> tasks(List<Supplier<String>> dataSuppliers) {
    List<Runnable> tasks = new ArrayList<>();
    for (Supplier<String> supplier : dataSuppliers) {
      tasks.add(new WriterService(supplier));
    }
    return tasks;
  }

  @Bean(destroyMethod = "shutdown")
  public Executor executor(List<Runnable> tasks) {
    Executor executor = Executors.newCachedThreadPool();
    for (Runnable task : tasks) {
      executor.execute(task);
    }
    return executor;
  }

  public static void main(String[] args) throws InterruptedException {
    try (AnnotationConfigApplicationContext appCtx =
                  new AnnotationConfigApplicationContext(TaskApplication.class)) {
    }
  }
}

Aux lignes 33 à 40, nous déclarons une méthode de fabrique qui retourne un Executor. On déclare grâce à l’annotation @Bean qu’il existe une méthode de destruction appelée shutdown. La méthode de fabrique crée l’exécuteur et ajoute la liste des tâches reçues en paramètre. Cette liste de tâches est maintenant produite par la méthode tasks. Notez que cette méthode accepte en paramètre une liste de Supplier<String>. Spring comprend qu’il s’agit de la liste de tous les objets compatibles avec l’interface Supplier<String> présents dans le contexte d’application, c’est-à-dire l’objet retourné par l’appel à dataSupplier et l’objet retourné par l’appel à fileDataSupplier.

La méthode main se contente de créer un contexte d’application puis de le fermer à la fin du bloc try. La fermeture du contexte d’application déclenche l’appel à la méthode shutdown sur l’exécuteur. Ainsi la fermeture du contexte d’application est suspendue le temps que toutes les tâches concurrentes s’achèvent.

Note

Pour plus d’information, reportez-vous à la documentation officielle.