JEE Back-end

Spring Core : Injections de dépendances (2/2)

Retour sur l'épisode précédent

Le conteneur IoC instantie/injecte les beans à partir d'une description dans un fichier XML.

Par défaut, tous les beans sont des singletons mais on peut changer ce comportement.

Tous les beans sont instantiés au moment de la création du conteneur IoC.

Comme toutes les vérifications sur l'assemblage sont faites à l'exécution, ce comportement permet détecter au démarrage les problèmes d'assemblage.

Configuration du conteneur IoC par des annotations

Nous allons voir une alternative à la configuration par fichier XML.

Pour voir comment, on reprend l'exemple du cours précédent.

@FunctionalInterface
public interface PasswordChecker {
	public boolean check(String pwd);
}

public RegularPasswordChecker implements PasswordChecker {
	public boolean check(String pwd){
 		return pwd.length>=8;	
 	}
}


Configuration avec les annotations (1/2)

Le fichier MyConfig.xml:

<?xml version="1.0" encoding="UTF-8"?>
	<beans>	 
	   <bean id="verificateur" class="fr.uge.RegularChecker"/>    
	</beans>
</xml>	

peut être remplacé par:

@Configuration
public class Config {

	@Bean
	PasswordChecker verificateur(){
		return new RegularChecker();
	}
	
}

Configuration avec les annotations (2/2)

  • @Configuration remplace la balise beans
  • @Bean remplace la balise bean
  • le nom de la méthode est l'id du bean
  • le code de la méthode est appelé par le conteneur pour créer le bean

Création du conteneur IoC

On remplace la lecture du fichier XML par la lecture de la classe de configuration.

public static void main(String[] args) {
    ApplicationContext applicationContext =
    		 new AnnotationConfigApplicationContext(Config.class);
    var checker = 
    		applicationContext.getBean("verificateur",PasswordChecker.class);
}

Injections par constructeur et par setter (1/2)

public class ConfigurablePasswordChecker implements PasswordChecker {

    private int minSize;
    private boolean requireUppercase=false;

    public ConfigurablePasswordChecker(int minSize) {
        this.minSize = minSize;
    }

    public void setRequireUppercase(boolean requireUppercase) {
        this.requireUppercase = requireUppercase;
    }

    @Override
    public boolean checkPassword(String password) {
        if (requireUppercase){
            var containsUpper=password.chars().mapToObj(c -> (char) c).anyMatch(Character::isUpperCase);
            if (!containsUpper){
                return false;
            }
        }
        return password.length()>=minSize;
    }
}

Une classe mutable c'est pas bien mais c'est pour l'exemple.

Injections par constructeur (et par setter) (2/2)

@Configuration
public class Config {

    @Bean
    PasswordChecker regularPasswordChecker(){
        return new RegularPasswordChecker();
    }

    @Bean
    PasswordChecker configurablePasswordChecker(){
        var bean = new ConfigurablePasswordChecker(12);
        bean.setRequireUppercase(true);
        return bean;
    }
} 

Essentiellement trivial !

Exemple de la librairie

@Configuration
public class Config {

    @Bean
    Book jungleBook(){
        return new Book("Le livre de la jungle","Rudyard Kipling");
    }

    @Bean
    Book montecristoBook(){
        return new Book("Le comte de Monte-Cristo","Alexandra Dumas");
    }

    @Bean
    Book globaliaBook() {
        return new Book("Globalia", "Jean-Christophe Rufin");
    }

    @Bean
    Library library(){
        return new Library(Set.of(jungleBook(), montecristoBook()));
    }

}

Par défaut, les beans sont des singletons. Donc dans cet exemple, le bean library contiendra les deux beans de type Book.

Injection par type

@Configuration
public class Config {
    @Bean
    Book jungleBook(){
        return new Book("Le livre de la jungle","Rudyard Kipling");
    }

    ...

    @Bean
    Book globaliaBook() {
        return new Book("Globalia", "Jean-Christophe Rufin");
    }

    @Bean
    Library libraryFull(Set<Book> allBooks){
        return new Library(allBooks);
    }
}    

Les paramètres des méthodes qui définissent les beans sont injectés par leur type.

Dans notre cas, comme allBooks est une collection de Book, le conteneur va rassembler tous les beans de type Book dans un Set qu'il va passer en argument libraryFull.

@Component

Il est possible d'annoter une classe par @Component pour indiquer au conteneur IoC de créer un bean pour cette classe.

En annotant la classe de configuration par @ComponentScan, Spring va parcourir le package et les sous-package de la classe de configuration à la recherche de classe annotée par @Component pour définir les beans.

On peut spécifier le package où rechercher les @Component avec:

@Configuration
@ComponentScan("fr.uge.jee.injections")

Retour sur l'exemple Client (1/3)

Reprenons notre exemple en configurant l'assemblage en utilisant @Component

La classe de configuration indique simplement à Spring de rechercher les classes annotées par @Component.

@Configuration
@ComponentScan
class Config{
	// vide
}

Retour sur l'exemple Client (2/3)

@Component
public class Address {
	...

    public Address(@Value("5") int streetNumber,
    			   @Value("Rue des lilas") String street,
    			   @Value("France") String country) {
    	...
    }
}

L'annotation @Value permet d'injecter des valeurs. On verra dans les exercices que @Value peut injecter des valeurs prises dans des fichiers de configuration ou dans l'environmment d'exécution.

Retour sur l'exemple Client (3/3)

@Component
public class Address {
    ...

    public Address(@Value("5") int streetNumber,
                   @Value("Rue des lilas") String street,
                   @Value("France") String country) {
        ...
    }
}

@Component
public class Client {
    private final String name;
    private final Address address;

    public Client(@Value("Bob") String name, Address address) { 
        this.name = name;
        this.address = address; 
    }
}

Les paramétres manquants à la construction d'un Component sont injectés par type. En d'autres termes, le le conteneur IoC va injecter le bean Address. S'il y avait plusieurs beans avec le type Address, on aurait une erreur.

Retour sur l'exemple de la librairie

@Configuration
@ComponentScan
public class Config {
    @Bean
    Book jungleBook(){
        return new Book("Le livre de la jungle","Rudyard Kipling");
    }

    ...

    @Bean
    Book globaliaBook() {
        return new Book("Globalia", "Jean-Christophe Rufin");
    }
}

@Component
public class Library {
    private final Set<Book> books;

    public Library(Set<Book> books){
        this.books=Set.copyOf(books);
    }
}    

Dans un Component, si le constructeur prend une collection (Set,List,...), le conteneur IoC va rassembler tous les beans ayant ce type dans la bonne collection.

Injection sur les champs

@Component
public class Library {

    @Autowire
    private  Set<Book> books;

    public Library(){
    }
}    

L'injection sur les champs oblige le champ à ne pas être final !
Il faut savoir comprendre ce code mais ne jamais l'utiliser.

Variantes de @Component

Ces annotations ajoutent des fonctionnalités (cf. cours suivants) à @Component sauf pour l'instant @Service.

@Bean vs @Component

  • L'avantage de tout mettre dans la classe de configuration est d'avoir un endroit centralisé pour tout ce qui concerne l'assemblage.
  • Avec @Component, la configuration de l'assemblage est éclatée dans plusieurs fichiers.
  • La configuration par @Bean est nécessaire pour définir un bean à partir d'une classe dont on ne possède pas le code (i.e., libraire).

JSR 330

La JSR 330 introduit @Inject et @Named. C'est une annotation de la norme JEE (introduite dans la version 6).

Très grossièrement, @Inject correspond à @Autowire et @Name correspond à @Component.

Les différences sont détaillées ici.

Spring reconnait ces annotations si on rajoute la dépendance javax.inject.