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)

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.

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)){
		....
    }
}

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 UserChecker 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>

getBean

  • applicationContext.getBean(String name) : Object

    ☠☠☠ \`A ne pas utiliser car oblige à faire un cast ! ☠☠☠

  • applicationContext.getBean(String name,Class<T>) : T
  • applicationContext.getBean(Class<T>) : T
    Attention il ne doit y avoir qu'un seul bean de type T.

Injection de dépendances

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

  • des valeurs
  • d'autres beans

Ces injections peuvent se faire:

  • soit par constructueur,
  • soit par un champ via un setter (☠☠☠ \`A comprendre mais à ne jamais utiliser ! ☠☠☠)

Injection par le constructeur (1/2)

public class Address {

    private final int number;
    private final String street;

    public Address(int number, String street) {
        this.number = number;
        this.street = street;
    }

    public String toString() {...};
}

On crée un bean addressBob en injectant des valeurs par le constructeur.

<bean id="addressBob" class="fr.uge.jee.injections.Address">
        <constructor-arg  value="5"/>
        <constructor-arg  value="Rue des Lilas"/>
</bean>

Injection par le constructeur (2/2)

public class Person {
    private final String name;
    private final Address address;

    public Person(Address address, String name) {
        this.address = address;
        this.name = name;
    }

    public String toString() {...};
}

On crée un bean boss en injectant une valeur et le bean addressBob.

<bean id="boss" class="fr.uge.jee.injections.Person">
    <constructor-arg  value="Bob"/>
    <constructor-arg  ref="addressBob"/>
</bean>

Injection par setter (1/4)

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


    public Client(String name) {
        this.name = name;
    }

    public void setAddress(Address address) {
        this.address = address;
    }

    public String toString() {...};
}

On veut créer un bean lea pour une cliente nommée Léa et qui habite au 53 Bvd Copernic.

☠☠☠ A comprendre mais à ne pas utiliser ! ☠☠☠

Injection par setter (2/4)

On crée un bean addressLea pour son adresse.

<bean id="addressLea" class="fr.uge.jee.injections.Address">
    <constructor-arg  value="53"/>
    <constructor-arg  value="Bvd Copernic"/>
</bean>   

On injecte le nom par le constructeur et l'adresse par le setter:

<bean id="lea" class="fr.uge.jee.injections.Client">
    <constructor-arg  value="Léa"/>
    <property name="address" ref="addressLea"/>
</bean>

☠☠☠ A comprendre mais à ne pas utiliser ! ☠☠☠

Injection par setter (3/4)

    <property name="address" ref="addressLea"/>
  • name="address" est le nom du champ dans la classe Client
  • Spring va chercher une méthode setAddress et lui passer le bean addressLea

Le nom du setter est dérivé du nom du champ : s'il y a un typo dans le nom du setter, il y a aura une exception!

☠☠☠ A comprendre mais à ne pas utiliser ! ☠☠☠

Injection par setter (4/4)

L'injection par setter implique que le champ concerné est mutable. Souvent cela va à l'encontre de tout ce que l'on vous à appris !

Il faut savoir lire/comprendre du code avec de l'injection par setter mais il faut priviligier l'injection par constructeur.

☠☠☠ A comprendre mais à ne pas utiliser ! ☠☠☠

List, Set et Map

Spring fournit un moyen pour injecter des collections Java.

public class Client {
    private final String name;
    private Address address;
    private List<String> emails;

    public Client(String name) {
        this.name = name;
    }

    public void setEmails(List<String> emails) {
        this.emails = List.copyOf(emails);
    }

     ...
}

List, Set et Map

<bean id="lea" class="fr.uge.jee.injections.Client">
   <constructor-arg  value="Léa"/>
   <property name="address" ref="addressLea"/>
   <property name="emails">
       <list>
           <value>toto@gmail.com</value>
           <value>titi@gmail.com</value>
       </list>
   </property>
</bean>

On peut utiliser une syntaxe similaire pour Set et Map.

List, Set et Map

On peut injecter des collections de beans et pas seulement des valeurs.

public class Book {
    private String title;
    private long ref;

    public Book(String title,long ref){...};
}

public class Library {
    private Set<Book> books;

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

     ...
}

List, Set et Map

<bean id="book1" class="Book">
    <constructor-arg  value="Le livre de la jungle"/>
    <constructor-arg  value="55453"/>
</bean>
<bean id="book2" class="Book">
    <constructor-arg  value="Le lion"/>
    <constructor-arg  value="554545"/>
</bean>
<bean id="library" class="Library">
    <constructor-arg>
        <set>
            <ref bean="book1"/>
            <ref bean="book2"/>
        </set>
    </constructor-arg>
</bean>    

Autowire

Pour l'instant, nous dépendons des id des beans pour déterminer les beans à injecter.

Avec Autowire, Spring offre plusieurs moyens pour inférer les beans à injecter.

  • autowire="byName"
    Pour chaque champs avec un setter, le conteneur cherche un bean ayant pour id le nom du champs et il l'injecte.
  • autowire="byType"
    Pour chaque champs avec un setter, le conteneur cherche un bean ayant le même type que le champs.
    Attention si plusieurs beans ont le même type, on a une exception.
  • autowire="constructor"
    Comme pour autowire="byType" mais avec le constructeur.

Autowire byName

<bean id="addressLea" class="fr.uge.jee.injections.Address">
    <constructor-arg  value="53"/>
    <constructor-arg  value="Bvd Copernic"/>
</bean>   

<bean id="lea" class="fr.uge.jee.injections.Client">
    <property name="address" ref="addressLea"/>
</bean>

A comprendre mais à ne pas utiliser !

Autowire byName

<bean id="address" class="fr.uge.jee.injections.Address">
    <constructor-arg  value="53"/>
    <constructor-arg  value="Bvd Copernic"/>
</bean>   

<bean id="lea" class="fr.uge.jee.injections.Client">
    <property name="address" ref="address"/>
</bean>

L'id du bean est égal au nom du champ de Client.

<bean id="lea" class="fr.uge.jee.injections.Client" autowire="ByName"/>

A comprendre mais à ne pas utiliser !

Autowire byType

<bean id="addressLea" class="fr.uge.jee.injections.Address">
    <constructor-arg  value="53"/>
    <constructor-arg  value="Bvd Copernic"/>
</bean>   

<bean id="lea" class="fr.uge.jee.injections.Client">
    <property name="address" ref="addressLea"/>
</bean>

Si le bean addressLea est le seul bean du type fr.uge.jee.injections.Address alors:

<bean id="lea" class="fr.uge.jee.injections.Client" autowire="ByType"/>

A comprendre mais à ne pas utiliser !

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.
  • Par défaut, ce sont des singletons.
  • 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.

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 !