Jakarta EE back-end

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

Retour sur l’épisode précédent

Le conteneur IoC instancie et 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 instanciés au moment de la création du conteneur IoC.

Comme les vérifications d’assemblage sont effectuées à l’exécution, ce comportement permet de détecter les problèmes dès le démarrage.

Exemple IoC (1/3)

On reprend l'exemple du cours précédent.

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

public class UGEPasswordChecker implements PasswordChecker {
    ...
    public UGEPasswordChecker(int minSize){
        this.minSize=minSize;
    }
    ...
}

public class ConnectionHandler {

    private final PasswordChecker passwordChecker;

    public ConnectionHandler(PasswordChecker passwordChecker){
        this.passwordChecker = Objects.requireNonNull(passwordChecker);
    }
    ...
}

Exemple IoC (2/3)

Pour remplacer, la création classique:

    var passwordChecker = new UGEPasswordChecker(12);
    var connectionHandler = new ConnectionHandler(passwordCheker);

On utilise un fichier XML qui définit les beans de notre application.

<?xml version="1.0" encoding="UTF-8"?>
    <beans>  
        <bean id="checker" class="fr.uge.jee.UGEPasswordChecker">
            <constructor-arg value="12"/>
        </bean>
        <bean id="connectionHandler" class="fr.uge.jee.ConnectionHandler">
            <constructor-arg ref="checker"/>
        </bean>
    </beans>
</xml>  

Exemple IoC (3/3)

Spring privilégie l'injection basée sur les types à celle beaucoup moins robuste basée sur les identifiants des beans.

<?xml version="1.0" encoding="UTF-8"?>
    <beans>  
        <bean class="fr.uge.jee.UGEPasswordChecker">
            <constructor-arg value="12"/>
        </bean>
        <bean class="fr.uge.jee.ConnectionHandler">
            <autowire="constructor"/>
        </bean>
    </beans>
</xml>  

à la place de:

<?xml version="1.0" encoding="UTF-8"?>
    <beans>  
        <bean id="checker" class="fr.uge.jee.UGEPasswordChecker">
            <constructor-arg value="12"/>
        </bean>
        <bean id="connectionHandler" class="fr.uge.jee.ConnectionHandler">
            <constructor-arg ref="checker"/>
        </bean>
    </beans>
</xml>  

Classe de configuration

<?xml version="1.0" encoding="UTF-8"?>
    <beans>  
        <bean id="checker" class="fr.uge.jee.UGEPasswordChecker">
            <constructor-arg value="12"/>
        </bean>
    </beans>
</xml>  

Classe de configuration :

@Configuration
public class AppConfig {

    @Bean 
    UGEPasswordChecker checker() {
        return new UGEPasswordChecker(12);
    }
} 

Attention, ceci n'est pas une vraie classe:

  • La méthode donne le code pour créer le bean.
  • et l'identifiant du bean est le nom de la méthode.
  • les méthodes ne peuvent être privées mais leur visibilité n'a pas d'impact.

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(AppConfig.class);
    var checker = 
    		applicationContext.getBean(PasswordChecker.class);
}

Injection de beans dans d'autres beans (1/2)

    var passwordChecker = new UGEPasswordChecker(12);
    var connectionHandler = new ConnectionHandler(passwordCheker);
@Configuration
public class AppConfig {

    @Bean
    UGEPasswordChecker checker() {
        return new UGEPasswordChecker(12);
    }

    @Bean
    ConnectionHandler connectionHandler(PasswordChecker passwordChecker) {
        return new ConnectionHandler(passwordChecker);
    }
}    

Les paramètres des méthodes qui definissent les beans sont injectées par Spring en se basant sur les types. Ici Spring cherche un bean implémentant l'interface PasswordChecker pour le passer à la méthode connectionHandler.

Injection de beans dans d'autres beans (2/2)

    var passwordChecker = new UGEPasswordChecker(12);
    var connectionHandler = new ConnectionHandler(passwordCheker);
@Configuration
public class AppConfig {

    @Bean
    UGEPasswordChecker checker() {
        return new UGEPasswordChecker(12);
    }

    @Bean
    ConnectionHandler connectionHandler(PasswordChecker passwordChecker) {
        return new ConnectionHandler(passwordChecker);
    }
}    

L'approche recommandée est de donner le type concret des beans dans leur définition et d' utiliser l'interface pour l'injection.

Injections d'une collection de beans (1/2)

@FunctionalInterface
public interface Criterium {
    boolean satisfy(String query);
}

public class DataIntegrity implements Criterium { ... };

public class EscapeChecking implements Criterium { ... };


public class Validator {

    private final Set<Criterium> criteria;

    public Validator(Set<Criterium> criteria){
        this.criteria = Set.copyOf(criteria);
    }

    public boolean check(String query){
        Object.requireNonNull(query);
        for(var criterium : criteria){
            if (!criterium.satisfies(query)) {return false;}
        }
        return true;
    }
}
var criterium1 = new DataIntegrity();
var criterium2 = new EscapeChecking();
var validator = new Validator(Set.of(criterium1,criterium2));

Injections d'une collection de beans (2/2)

var criterium1 = new DataIntegrity();
var criterium2 = new EscapeChecking();
var validator = new Validator(List.of(criterium1,criterium2));
@Configuration
public class ValidationConfig {

    @Bean
    Criterium dataIntegrity() {
        return new DataIntegrity();
    }

    @Bean
    Criterium escapeChecking() {
        return new EscapeChecking();
    }

    @Bean
    Validator validator(Set<Criterium> criteria) {
        return new Validator(criteria);
    }
}

En résumé

La classe de configuration n'est pas une vraie classe, elle remplace un fichier XML pour la description des beans.

On privilégie autant que possible l'injection par le type et on n'utilise le moins possible (voir pas du tout) les identifiants des beans.

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

@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")
public class AppConfig {
     ...
}

Retour sur ConnectionHandler

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

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

@Configuration
@ComponentScan
public class AppConfig {

    @Bean
    UGEPasswordChecker checker() {
        return new UGEPasswordChecker(12);
    }

}
@Component
public class ConnectionHandler {

    private final PasswordChecker passwordChecker;

    public ConnectionHandler(PasswordChecker passwordChecker){
        this.passwordChecker = Objects.requireNonNull(passwordChecker);
    }
    ...
}

Les paramétres manquants à la construction d'un Component sont injectés par type.

Injection sur les champs

@Component
public class ConnectionHandler {

    @Autowire
    final PasswordChecker passwordChecker;

    public ConnectionHandler(){
    }
    ...
}

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 jakarta.inject.