Spring Core
Noyau de Spring.
Notion de couplage fort et couplage faible
Le couplage est une métrique indiquant le niveau d'interaction entre deux ou plusieurs composants logiciels : deux composants sont dits couplés s'ils échangent de l'information.
Le couplage fort implique une dépendance forte entre deux composants : difficilement réutilisable et testable.
Le couplage faible favorise la faible dépendance entre les classes, la réduction de l'impact des changements dans une classe, la réutilisation des classes ou modules.
Injection de dépendances et inversion de contrÎle
âJe ne crĂ©e pas un objet, je demande Ă ce quâon me le fournisse.â, câest ça lâinversion de contrĂŽle.
Le design pattern Singleton est lâexemple le plus simple : une seule instance autorisĂ©e (constructeur privĂ©), qui nous est fournie.
Le design pattern Factory en est un Ă©galement : lâinstance nous est fournie par une couche diffĂ©rente (DAO).
Inversion de contrĂŽle
Inversion of Control.
Elle permet de réduire les dépendances (couplage) entre des objets dont l'implémentation peut varier.
Elle diminue la complexitĂ© de gestion du cycle de vie de ces objets (design patterns Singleton et Factory). Le contrĂŽle du flot d'exĂ©cution d'une application n'est plus gĂ©rĂ© par l'application elle-mĂȘme mais par une structure externe (conteneur).
â> Mise en place : utilisation de lâinjection de dĂ©pendances.
Injection de dépendances
Dependency Injection.
Câest le mĂ©canisme permettant d'implĂ©menter le principe de l'inversion de contrĂŽle.
Il permet d'éviter une dépendance directe entre deux classes en définissant dynamiquement la dépendance plutÎt que statiquement.
Il permet à une application de déléguer la gestion du cycle de vie de ses dépendances et leurs injections à une autre entité.
L'application ne crĂ©e pas directement les instances des objets dont elle a besoin : les dĂ©pendances d'un objet ne sont pas gĂ©rĂ©es par l'objet lui-mĂȘme mais sont gĂ©rĂ©es et injectĂ©es par une entitĂ© externe Ă l'objet.
Implémentation Spring
Spring apporte le "conteneur léger", nommé ApplicationContext, qui permet la prise en charge du cycle de vie des objets (beans) et leur mise en relation.
En client lourd, on crĂ©e un environnement Spring en modifiant la classe dâexĂ©cution :
@SpringBootApplication
public class Application {
public static void main(String[] args) {
ApplicationContext context = SpringApplication.run(Application.class, args);
}
}En web, ce nâest pas nĂ©cessaire.
Notion de Spring bean
Un bean en Spring est un objet que Spring a créé et qui est disponible dans un conteneur pour une injection.
On peut crĂ©er un bean en utilisant des annotations spĂ©cifiques grĂące auxquelles Spring crĂ©e les instances demandĂ©es. Si ce nâest pas suffisant (ex : un objet Personne pour le directeur de lâagence), on peut crĂ©er un bean par programmation.
On injecte le bean avec lâannotation @Autowired sur le constructeur ou une mĂ©thode.
@Autowired sur un attribut privé.
Câest une pratique dĂ©conseillĂ©e puisquâelle se base sur la rĂ©flexion qui charge la classe en entier et permet de modifier un attribut privĂ© alors quâil ne devrait ĂȘtre modifiable que par son setter.
Scope des beans
On définit le scope en annotant la classe qui crée le bean avec @Scope("prototype").
- singleton : scope par défaut
- prototype : une nouvelle instance Ă chaque injection
- request (uniquement en environnement web) : une nouvelle instance pour chaque requĂȘte HTTP
- session (uniquement en environnement web) : une nouvelle instance pour chaque nouvelle session
- application (uniquement en environnement web) : instance liée à un servlet context (ie une application web)
Beans par annotation
Les beans dĂ©finis par annotation doivent ĂȘtre placĂ©s au mĂȘme niveau ou sous la classe dâapplication (classe annotĂ©e avec @SpringBootApplication).
@SpringBootApplication.
Annotations principales :
@Component = annotation de base â permet Ă Spring de comprendre quâil faut prendre en compte cette classe
@Controller = annotation sur un contrĂŽleur
@Service = annotation sur un service mĂ©tier â permet la gestion des transactions sur cette couche
@Repository = annotation sur une classe DAO â permet la gestion des exceptions et des transactions de cette couche
@Autowired = annotation pour lâinjection de dĂ©pendance â permet au conteneur Spring de rechercher un bean et de lâinjecter
soit dans un attribut (déconseillé mais le plus utilisé)
soit dans une méthode
soit dans le constructeur (par défaut pour Spring Boot)
RĂ©cupĂ©ration dâun bean
Un bean est rĂ©cupĂ©rĂ© dans la classe dâexĂ©cution grĂące Ă la mĂ©thode getBean() rendue disponible par lâinstance de ApplicationContext.
Elle prend en argument le nom ou le type du bean.
- on nomme le bean dans lâannotation de la classe concernĂ©e (
@Component("unBean")) et Ă lâappel degetBean(), on caste le bean pour gĂ©rer le type de lâobjet rĂ©cupĂ©rĂ© :(MaClasseAnnotee) context.getBean("unBean")
- on donne directement le type du bean en paramĂštre :
context.getBean(MaClasseAnnotee.class)
- on peut faire les deux Ă la fois :
context.getBean("unBean", MaClasseAnnotee.class)
A lâexĂ©cution, Spring nous fournit une instance de notre classe via ce bean (dont il gĂšre le stockage).
Exemple minimaliste
Créer un nouveau projet sans starters.
On est dans un contexte de client lourd.
Modifier la classe dâexĂ©cution Application, annotĂ©e
@SpringBootApplication, avec la mĂ©thode qui charge lâenvironnement Spring :@SpringBootApplication public class Application { public static void main(String[] args) { ApplicationContext context = SpringApplication.run(Application.class, args); } }Dans le package controller, crĂ©er une classe HomeController annotĂ©e
@Controller("homeBean")avec un constructeur vide et une mĂ©thodesayHello():@Controller("homeBean") public class HomeController { @Autowired //optionnel, par defaut pour Spring Boot public void HomeController() { System.out.println("Constructeur de HomeController"); } public void sayHello() { System.out.println("MĂ©thode du contrĂŽleur qui dit bonjour"); } }Dans la classe dâexĂ©cution, rĂ©cupĂ©rer le bean du contrĂŽleur et appeler sa mĂ©thode :
- on déclare une instance de HomeController à laquelle on passe le bean grùce à la méthode
getBean()rendue disponible par lâinstance deApplicationContextâ ïžIl faut caster le bean pour gĂ©rer le type de lâobjet rĂ©cupĂ©rĂ©.
@SpringBootApplication public class Application { public static void main(String[] args) { ApplicationContext context = SpringApplication.run(Application.class, args); HomeController hc_nom = (HomeController) context.getBean("homeBean"); HomeController hc_type = context.getBean(HomeController.class); HomeController hc_both = context.getBean("homeBean", HomeController.class); hc_nom.sayHello(); //Méthode du contrÎleur qui dit bonjour hc_type.sayHello(); //Méthode du contrÎleur qui dit bonjour hc_both.sayHello(); //Méthode du contrÎleur qui dit bonjour } }- on déclare une instance de HomeController à laquelle on passe le bean grùce à la méthode
Exemple plus concret
On est en multicouches : model = bo, controller = ihm, service = bll, repository = dal (+ vue).
Suite du projet : springboot-basics.
Classe dâexĂ©cution Application, annotĂ©e
@SpringBootApplication, avec la mĂ©thode qui charge lâenvironnement Spring.Package model (bo), on crĂ©e :
âčïžUniquement des relations unidirectionnelles.- une classe Person
- une classe Genre
- une classe Movie
Dans une POJO, on crĂ©e au minimum les attributs privĂ©s, les getters/setters, un constructeur vide, un constructeur avec tous les paramĂštres. Dans la vraie vie, on ne met pas forcĂ©ment lâidentifiant dans le constructeur (souvent auto-gĂ©nĂ©rĂ©). On ne met pas non plus la mĂ©thode
toString(), qui sert Ă dĂ©boguer en console mais nâest plus utile en mode web.Dans un constructeur vide, on laisse tout par dĂ©faut sauf les listes que lâon doit toujours initialiser : âun conteneur peut ĂȘtre vide mais pas nulâ. Dâailleurs dans le setter dâun attribut conteneur, on ajoute une condition qui vĂ©rifie si lâobjet insĂ©rĂ© est nul et lâinitialise Ă vide au besoin. Dans un constructeur full, on appelle le setter pour une liste et pas le
thispour passer à travers cette vérification.Dans un constructeur, on utilise
thissauf sâil y a une vĂ©rification faite dans le setter, dans ce cas on prĂ©fĂšre appeler le setter. Mais dans la vraie vie, la trĂšs grande majoritĂ© des vĂ©rifications se font dans la partie mĂ©tier (bll, service). Il est possible de faire des vĂ©rifications âde bon sensâ dans le controller.Package service (bll), on crĂ©e :
- une interface MovieService avec les méthodes souhaitées
BP : âon ne travaille jamais directement avec des classes, mais bien avec des interfacesâ
- une classe bouchon MovieServiceMock
@Serviceavec :âčïžOn pourra ajouter un profil « dev » afin quâelle soit dĂ©sactivĂ©e lors de la mise en place de la DAL.- les attributs privĂ©s
listMovies,listGenres,listPeople
- une méthode privée qui initialise la liste de genres
- une méthode privée qui initialise la liste des personnes (réalisateurs et acteurs)
- un constructeur vide qui récupÚre les deux listes et crée les films
- les mĂ©thodes du contrat de lâinterface
- les attributs privés
Dans une classe service, on crĂ©e les attributs nĂ©cessaires, un constructeur vide, les mĂ©thodes du contrat de lâinterface implĂ©mentĂ©e, Ă©ventuellement dâautres mĂ©thodes.
Dans une classe service mock, on ajoute des données-bouchon dans le constructeur vide.
Les genres et les participants sont des tables de rĂ©fĂ©rentiel : il faut quâils existent avant de pouvoir crĂ©er un film.
BP : on externalise un maximum avec des mĂ©thodes privĂ©es explicites pour amĂ©liorer la lisibilitĂ©, âun code bien fait est un code qui nâa pas besoin de commentairesâ.
Dans le
save(),on fait les vĂ©rifications mĂ©tier et seulement Ă la fin leadd(). On utilise nos propres exceptions pour dire si tout sâest bien passĂ© ou pas.Package controller (ihm), on crĂ©e une classe MovieController
@Controlleravec- un attribut privé du service
- un constructeur
@Autowiredavec le service
- une méthode pour retourner tous les films
- une méthode pour trouver un film à partir de son identifiant
âčïžOn devrait retourner un film optional dans legetMovieById()(et dans dâautres) : ça devient la norme. On utilise le typeOptionnalpour dĂ©crire lâattribut de lâinterface (Optional<Movie>) et dans la signature de la mĂ©thode :@Override public Optional<Movie> getMovieById(long id) { Optional<Movie> optMovie = Optional.empty(); for (Movie movie : listMovies) { if (movie.getId() == id) { optMovie = Optional.of(movie); break; } } return optMovie; }Dans la classe dâexĂ©cution fournie, on met Ă jour le bean qui appelle maintenant un Optional :
- on déclare une variable
Optional<Movie>
- on fait une condition qui vérifie si la variable a un contenu (
isPresent()) et affiche un message adéquouÚte
- (+) Il ne peut pas ĂȘtre nul car
Optionalsâassure que lâobjet existe dans tous les cas, on vĂ©rifie donc sâil a un contenu.
Beans par programmation
Les beans définis par programmation doivent
- ĂȘtre placĂ©s dans un package config > dans une classe de configuration annotĂ©e
@Configuration
- avoir une méthode annotée
@Bean
@Configuration
public class EditeurConfiguration {
@Bean
public List<Editeur> avoirListeEditeurs() {
...
return listeEditeur;
}
}
Exemple
Créer un nouveau projet sans starters.
On est dans un contexte de client lourd.
Modifier la classe dâexĂ©cution Application, annotĂ©e
@SpringBootApplication, avec la mĂ©thode qui charge lâenvironnement Spring.Dans le package bo, crĂ©er la classe Editeur avec les attributs privĂ©s, les constructeurs vide et full, les getters/setters/, la mĂ©thode toString.
public class Editeur { private int id; private String nom; public Editeur() { } public Editeur(int id, String nom) { this.id = id; this.nom = nom; } public int getId() { return id; } public void setId(int id) { this.id = id; } public String getNom() { return nom; } public void setNom(String nom) { this.nom = nom; } @Override public String toString() { return "Editeur{" + "id=" + id + ", nom='" + nom + '\'' + '}'; } }Dans le package config, créer la classe EditeurConfiguration annotée
@Configurationavec la mĂ©thode avoirListeEditeurs() annotĂ©e@Bean.@Configuration public class EditeurConfiguration { @Bean public List<Editeur> avoirListeEditeurs() { // un conteneur peut ĂȘtre vide mais pas nul List<Editeur> listeEditeur = new ArrayList(); // deux editeurs Editeur poche = new Editeur(1, "Poche"); Editeur hachette = new Editeur(2, "Hachette"); // ajout des editeurs a la liste listeEditeur.add(poche); listeEditeur.add(hachette); return listeEditeur; } }Dans la classe dâexĂ©cution, rĂ©cupĂ©rer le bean avec
getBean()et lâafficher.@SpringBootApplication public class Application { public static void main(String[] args) { ApplicationContext context = SpringApplication.run(Application.class, args); EditeurConfiguration ec = context.getBean(EditeurConfiguration.class); System.out.println(ec.avoirListeEditeurs()); } }
Bonus : CommandLineRunner
= interface Spring Boot avec une méthode d'exécution.
Spring Boot appellera automatiquement la méthode run de tous les beans implémentant cette interface aprÚs le chargement du contexte de l'application.
- on modifie la classe dâexĂ©cution avec lâannotation
@SpringBootApplication, qui doit implémenterCommandLineRunner
- on lui met un attribut annoté
@Autowiredqui correspondant au bean que lâon veut rĂ©cupĂ©rer
- on garde la méthode
main()avec la mĂ©thode run qui crĂ©e lâinstance de sa classe (et charge lâenvironnement Spring)
- on surcharge la méthode
run()qui affichera notre bean Ă lâexĂ©cution de lâapplication
@SpringBootApplication
public class Application implements CommandLineRunner {
@Autowired
private List<Editeur> listeEditeurs;
public static void main(String[] args) {
ApplicationContext context = SpringApplication.run(Application.class, args);
}
@Override
public void run(String... args) throws Exception {
System.out.println(listeEditeurs);
}
}
Gérer un conflit
Sâil existe deux instances dâun mĂȘme objet dans le conteneur, Spring ne sait pas comment choisir cela crĂ©e un conflit.
Par exemple, un bean livreController demande lâinjection dâun bean de type livreService par constructeur. HĂ©las, il existe deux beans correspondant au type demandĂ© : deux classes qui implĂ©mente lâinterface LivreService, qui sont LivreServiceImpl et LivreServiceMock.
Les conflits peuvent ĂȘtre rĂ©solus par annotation :
@Primaryqui rend le bean de la classe annotée prioritaire
@Qualifier(ImplementationSouhaitee)qui transforme lâinjection en une injection par nom- si le
@Autowiredest sur le constructeur, on le met directement dans ses paramĂštres en prĂ©cisant le bean quâon veut :unconstructeur(@Qualifier(ImplementationSouhaitee) âŠ)
- si le
@Autowiredest sur lâattribut, on ajoute lâannotation au mĂȘme endroit
- si le
@Profile("unprofil")qui dĂ©finit lâassociation du bean de la classe annotĂ©e avec un ou plusieurs profils, qui sont activĂ©s dans application.properties avecspring.config.activate.on-profile="unprofil"
java-jar -D (ou bien on Ă©dite la configuration avant de lancer lâapplication).
Exemple
â ïžSeul le code concernant directement lâexemple est fourni.Dans le package service, crĂ©er les deux classes qui implĂ©mentent LivreService : LivreServiceImpl et LivreServiceMock, annotĂ©es
@Service.Par dĂ©faut, Spring Boot prend les constructeurs pour lâinjection (
@Autowired).Dans le package controller, créer la classe LivreController annotée
@Controlleravec- un attribut privé livreService annoté
@Autowired
- un constructeur qui injecte le livreService
Si on lance maintenant : conflit !
Les différentes options pour gérer le conflit :
- ajouter lâannotation
@Qualifier("LivreServiceMock")au dessus de lâattribut qui porteCa fonctionne, il sait quel bean choisir. Si on modifie lâannotation
@Qualifier("LivreServiceImpl"), il prend lâautre.
- choisir une implémentation prioritaire avec
@Primaryau dessus dâune des deux classesCa fonctionne, il sait quel bean choisir.
- définir des profils :
@Profile("dev")pour les classes quâon ne souhaite pas avoir lorsquâon est en dĂ©faut, ici la classeLivreServiceMockAttention, on doit dĂ©finir un profil partout sinon une classe sans rien est prise par dĂ©faut dans âdefaultâ.
Ca fonctionne, il choisit le profil par défaut. Si on change la configuration (
spring.profiles.active=dev), il prend bien le mock.
- un attribut privé livreService annoté
En vrac
- BP : il est conseillé de limiter les
final, il est prĂ©fĂ©rable de ne pas crĂ©er de setter pour les attributs que lâon ne veut pas pouvoir modifer.
- JavaDoc : On gĂ©nĂšre de la JavaDoc surtout pour les mĂ©thodes publiques. On Ă©crit un commentaire javadoc au dessus de la mĂ©thode concernĂ©e : on dĂ©crit ce que fait la mĂ©thode et dans quel but, sâil y a des exceptions, les paramĂštres et le retour. Dâautres annotations existent : @since, @see, @author⊠On gĂ©nĂšre la documentation avec lâoption Generate JavaDoc : un rĂ©pertoire avec la documentation, sous diffĂ©rents formats. On fournit en gĂ©nĂ©ral la documentation dans un jar Ă part.