On retrouve les principes SOLID :
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.
@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.
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.
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)){
....
}
}
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.
applicationContext.getBean(Class<T>) : T applicationContext.getBean(String name,Class<T>) : TapplicationContext.getBean(String name) : Object Un bean est un objet qui est instancié, assemblé ou géré par le conteneur IoC.
<bean id="verificateur" class="fr.uge.RegularChecker"/>
Un composant logiciel est une interface + les différents beans qui fournissent cette interface.
Dans notre exemple, PasswordChecker + (RegularPasswordChecker et SecurePasswordChecker)
ConnectionHandler on a besoin d'un PasswordChecker.
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>
Le conteneur IoC peut injecter dans les beans qu'il construit:
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);
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.
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.
Les injections peuvent se faire:
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.
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));
<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.
<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.
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.
PasswordChecker.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"/>
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: