JEE Back-end

Spring Core : Injections de dépendances

Spring est un pur produit des design patterns (1/2)

On retrouve les principes SOLID :

  • programmer pour une interface, non pour une implémentation,
  • préférer la composition et la délégation à l'héritage,
  • code ouvert aux extensions mais fermé aux modifications. (Open-Close Principle,
  • ...

Spring est un pur produit des design patterns

L'idée clée est de pouvoir faire évoluer l'application sans modifier une seule ligne du code déjà écrit mais en rajoutant éventuellement de nouvelles classes.

  • ajouter un nouveau moyen de paiement,
  • changer la méthode de validation des mots de passe,
  • ajouter un mode de livraison.

Inversion of Control (IoC)

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

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

public SecurePasswordChecker implements PasswordChecker{
 	public boolean check(String pwd){
 		return pwd.length>=12;	
 	}
}
public class Application {
	...
	String password = ...
	PasswordChecker pc = new RegularPasswordChecker();
	if (pc.check(password)){
		....
    }
}

Le new implique que si l'on veut changer de PasswordChecker, il faudra aller modifier le code de Application.

Inversion de Control (IoC) (1/2)

Solution: on délègue la création à un objet appelé conteneur qui va lire les information dans un fichier XML ou dans une classe de configuration.

La manière moderne de faire est en utilisant une classe de configuration mais pour des raisons pédagogiques, nous allons commencer par la configuration avec un fichier XML.

Inversion de Control (IoC) (2/2)

Le fichier MyConfig.xml:

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

L'application en Spring avec IoC

public class Application {
	ApplicationContext applicationContext = 
	      new ClassPathXmlApplicationContext("MyConfig.xml");

	PasswordChecker pc = 
		applicationContext.getBean("verificateur",fr.uge.PasswordChecker.class);
	String password = ...
	if (pc.check(password)){
		....
    }
}

Inversion de Control (IoC)

Le fichier MyConfig.xml:

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

L'application en Spring avec IoC

public class Application {
	ApplicationContext applicationContext = 
	      new ClassPathXmlApplicationContext("MyConfig.xml");

//	PasswordChecker pc = 
//		applicationContext.getBean("verificateur",fr.uge.PasswordChecker.class);
	PasswordChecker pc = 
		applicationContext.getBean(fr.uge.PasswordChecker.class);
}

En pratique, on n'aura qu'un seul bean pour une interface donnée. On n'utilisera que les interfaces (au pire les classes concrètes) pour récupérer les beans et quasiment jamais le nom du bean.

getBean

  • applicationContext.getBean(Class<T>) : T
    Attention il ne doit y avoir qu'un seul bean de type T.
  • applicationContext.getBean(String name,Class<T>) : T
  • applicationContext.getBean(String name) : Object
    ☠☠☠ A ne pas utiliser car oblige à faire un cast ! ☠☠☠

Bean + Composant logiciel

Un bean est un objet qui est instancié, assemblé ou géré par le conteneur IoC.

<bean id="verificateur" class="fr.uge.RegularChecker"/>    
  • identifiant : verificateur,
  • classe : fr.uge.RegularChecker.

Un composant logiciel est une interface + les différents beans qui fournissent cette interface.
Dans notre exemple, PasswordChecker + (RegularPasswordChecker et SecurePasswordChecker)

Le coeur de Spring

  • Conteneur léger
    Gére l'instantiation de certains objets (les beans) et tout leur cycle de vie.
  • Injection de dépendances
    Gère les dépendances entre les beans. Spring injecte les beans nécessaire à la création d'un bean.
    Par exemple, on pourrait imaginer que pour faire un ConnectionHandler on a besoin d'un PasswordChecker.
  • Programmation Orientée Aspect (cf. séance prochaine)

Construction des beans

Par défaut, les beans sont, pour l'instant, crées en appelant leur constructeur sans paramètre.

public class UGEPasswordChecker implements PasswordChecker {

    private int minSize;

    public UGEPasswordChecker(){
        this.minSize=8;
    }

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

On peut fournir des paramètres pour le constructeur:

<bean id="verificateur" class="fr.uge.jee.UGEPasswordChecker">
        <constructor-arg value="12"/>
    </bean>

Injection de dépendances imbriquées (1/3)

Le conteneur IoC peut injecter dans les beans qu'il construit:

  • des valeurs
  • d'autres beans

Injection de dépendances imbriquées (2/3)

public class ConnectionHandler {

    private final PasswordChecker passwordChecker;

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

    ...
}
var passwordChecker = new UGEPasswordChecker(12);
var ConnectionHandler = new ConnectionHandler(passwordChecker);

Injection de dépendances imbriquées (3/3)

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

Le fichier MyConfig.xml:

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

<bean id="connectionHandler" class="fr.uge.jee.ConnectionHandler">
    <constructor-arg ref="passwordChecker"/>
</bean>
	</beans>
</xml>

On se base sur les id des beans pour faire l'assemblage. La philosophie prépondérante est de se baser sur les types des beans pour l'injection.

Autowire by constructor

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

Le fichier MyConfig.xml:

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

    <bean id="connectionHandler"
          class="fr.uge.jee.ConnectionHandler"
          autowire="constructor"/>

	</beans>
</xml>

L'idée est que pour tous les paramètres du constructeur, Spring cherche un bean avec un type compatible. S'il y en a plusieurs, il y a une erreur.

Différentes méthodes d'injection

Les injections peuvent se faire:

  • soit par constructueur,
  • soit par un champ via un setter (☠☠☠ à ne jamais utiliser ! ☠☠☠)

La manière recommandée est l'injection par le constructeur qui permet d'avoir des champs final.

Dans la vraie vie, vous verrez du code qui injecte via des setters.

List, Set et Map

Spring fournit un moyen pour injecter des collections Java.

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

public class DataIntegrity implements Criterium { ... };

public class EscapeChecking implements Criterium { ... };


public class Validator {

    private final List<Criterium> criteria;

    public Validator(List<Criterium> criteria){
    	this.criteria = List.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(List.of(criterium1,criterium2));

List, Set et Map


    <bean id="dataIntegrity" class="fr.uge.jee.DataIntegrity"/>
    <bean id="escapeChecking" class="fr.uge.jee.EscapeChecking"/>

    <bean id="validator" class="fr.uge.jee.Validator">
        <constructor-arg>
            <list>
                <ref bean="dataIntegrity"/>
                <ref bean="escapeChecking"/>
            </list>
        </constructor-arg>
    </bean>

On peut utiliser une syntaxe similaire pour Set et Map.

Autowire by Constructor

    <bean id="dataIntegrity" class="fr.uge.jee.DataIntegrity"/>
    <bean id="escapeChecking" class="fr.uge.jee.EscapeChecking"/>

    <bean id="validator"
          class="fr.uge.jee.Validator"
          autowire="constructor"/>

Le comportement de autowire by constructor avec les paramètres qui sont des collections est de collecter tous les beans ayant le bon type.

On est vraiment dans le principe Open-Close, on rajoute un bean implémentant l'interface et automatiquement, il est injecté dans notre validator.

En résumé et en simplifié

Le conteur IoC lit dans fichier XML la description d'un certain nombre de beans.

En utilisant la réflexion ou la réécriture de bytecode, il fait les appels à new.

To bean or not to bean ?

  • Les beans sont en général des services comme PasswordChecker.
  • On utilise des beans quand on veut profiter des services fournis par Spring ou qu'on veut pouvoir faire évoluer un service qu'on offre dans notre propre code.
  • Pour les objets standards, on ne passe pas par des beans.

Singleton

Par defaut, les beans sont des singletons. Cela veut dire que c'est le même objet qui est injecté partout.

Ce n'est pas un problème pour les services stateless (comme les lambdas). Mais potentiellement problématique pour un Builder par exemple.

Si l'on veut que chaque injection crée un nouvel objet, on peut changer le comportement:

<bean id="queryBuilder"
      class="fr.uge.jee.QueryBuilder"
      scope="prototype"/>

Mais monsieur ...

Nous sommes en train de coder du Java dans un fichier XML, est-ce que c'est vraiment mieux ?

Il existe une autre approche basée sur les annotations Java qui évite ce problème et que nous allons utiliser dans le reste du cours.

On commence avec la configuration XML car:

  • elle permet de démystifier l'injection de dépendances,
  • avec les annotations, on se retrouve avec une classe qui code un fichier XML,

  • elle est encore utilisée dans la vraie vie !