JEE Back-end

Spring MVC

Spring MVC

  • Spring MVC est un framework pour écrire des applications Web
  • Il est basé sur le pattern Modèle-Vue-Controller (d'où le nom).
  • Il va simplifier l'écriture des HttpServlet:

    • la conversion des données des requêtes HTTP en objets Java;
    • la production du HTML: pour cela il utilise un moteur de templates.

Architecture de Spring

Source: Documentation de Spring MVC

La vie d'une requête

  • La requête HTTP arrive sur le Front controller (qui est un Servlet). Elle est dispatchée sur le Controlleur pour cette route.
  • Le Controlleur ne recoit pas directement la requête mais un objet Java dont les champs sont remplis à partir de la requête.
    Il est possible d'avoir la HTTPServletRequest mais ce n'est pas l'utilisation standard.
  • Le Controlleur remplit le Model (i.e. une Map<String,Object>) avec les informations qu'il veut voir afficher. Il ne renvoie pas du code HTML mais le nom d'un template HTML
  • Le Moteur de template (i.e., View) reçoit donc le nom de la vue et le Model. C'est lui qui produit le HTML qui est renvoyé au client par le Front controller.

Mon premier Controlleur

@Controller
public class HelloController {

    @GetMapping("/hello")
    public String greeting(Model model) {
        model.addAttribute("name", "Arnaud");
        return "hello";
    }
 }
  • Moralement, chaque méthode correspond à un servlet.
  • La route de greeting est /hello. Il ne traitera que les requêtes GET.
  • La méthode greeting renvoie le nom de la vue qui doit être utilisée.

Ma première vue/Mon premier template hello.html (ThymeLeaf)

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>Hello Spring-MVC</title>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
<p th:text="'Hello, ' + ${name} + '!'" />
</body>
</html>	
  • Il existe de nombreux moteurs de templates (ThymeLeaf,Faces,JSP...). Nous allons utiliser ThymeLeaf mais n'hésitez pas à changer pour le projet.
  • ThymeLeaf reçoit le nom de la vue hello et un modèle contenant l'association ["name" -> "Arnaud].
    JSP a été très populaire dans le passé mais ne devrait pas être utilisé dans un projet récent.
  • Sans la première ligne, ThymeLeaf ne reconnaitra pas votre template.

Spring Boot

Pour simplifier, la gestion des dépendances, la configuration de Spring et l'injection de tous ces composants, nous allons utiliser Spring Boot.

@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

}
  • L'annotation @SpringBootApplication est équivalente à @Configuration, @EnableAutoConfiguration et @ComponentScan.
  • Le démarrage de l'application dans Maven va lancer un serveur Tomcat et faire le déploiement.

ThymeLeaf est très puissant (1/2)

@GetMapping("/shopping")
   public String greeting(Model model) {
       model.addAttribute("groceries", List.of("Potatoes","Milk","Sugar"));
       return "shopping";
}	
<body>
<h2>My grocery list</h2>
<ul>
    <li th:each="item : ${groceries}" th:text="${item}"></li>
</ul>
</body>    

Le HTML produit contient une liste de tous les éléments de l'attribut groceries du Model.

Thymeleaf est très puissant (2/2)

ThymeLeaf peut injecter la valeur par défaut des champs d'un formulaire à partir d'un objet du Model.

@GetMapping("/rectangledefault")
    public String rectangleDefault(Model model){
        var rectangle = new Rectangle(10,15);
        model.addAttribute("rectangle",rectangle);
        return "rectangledefault";
    }	
<form th:action="@{/rectangle}" th:object="${rectangle}" method="post">
    <h2>Rectangle area calculator</h2>
    <div><label for="h">Height:</label>
        <input id="h" type="text" name="height"></div>

    <div><label for="w">Width:</label>
        <input type="text" id="w" name="width"></div>

    <div><button type="submit">Compute</button></div>
</form>	

Lisez la documentation pour voir tout ce que ThymeLeaf sait faire.

@GetMapping (1/2)

@GetMapping("/goodbye/{name}/{uid}")
    public String goodbye(@PathVariable("name") String username,
                          @PathVariable("uid") int uid, 
                          Model model) {
        model.addAttribute("name", username);
        model.addAttribute("uid",uid);
        return "goodbye";
    }	

Les paramètres name et uid sont extraits de la route:
Exemple: http://localhost:8080/goodbye/Arnaud/42

@GetMapping (2/2)

@GetMapping("/square")
public String goodbye(@RequestParam(name="number", required=true) long n,
                      Model model) {
    model.addAttribute("number", n);
    model.addAttribute("numberSquared", n*n);
    return "square";
}	

Le paramètre number est extrait de la requête HTTP GET:
Exemple: http://localhost:8080/square?number=42

@PostMapping (1/5)

Une des fonctionnalités de Spring MVC est de faciliter la récupération des données venant d'un formulaire.

Spring MVC va créer automagiquement un objet Java à partir des données du formulaire.

Le lien entre les données du formulaire et les champs de l'objet Java se font par les noms.

@PostMapping (2/5)

@Controller
public class RectangleController {

    @GetMapping("/rectangle")
    public String rectangleForm(){
        return "rectangle";
    }
}

View : rectangle.html

<form th:action="@{/rectangle}" method="post">
    <h2>Rectangle area calculator</h2>
    <div><label for="h">Height:</label>
        <input id="h" type="text" name="height"></div>

    <div><label for="w">Width:</label>
        <input type="text" id="w" name="width"></div>

    <div><button type="submit">Compute</button></div>
</form>	

@PostMapping (3/5)

public record Rectangle(int width, int height) {
	public int area() { return width*height; }
}
@PostMapping("/rectangle")
public String processForm(@ModelAttribute("rectangle-model") Rectangle rectangle){
   return "rectangle-result";
}

L'annotation @ModelAttribute("rectangle-model") a pour effet de:

  • créer Rectangle rectangle à partir des informations du formulaire,
  • d'insérer cet objet dans le Model avec la clé rectangle-model.

Le lien entre les éléments du formulaire et les champs du record se font par les noms. Si l'on veut utiliser des noms différents entre le formulaire et le record, on peut utiliser l'annotation @BindParam sur les champs du record pour donner le nom correspondant dans le formulaire.

@PostMapping (4/5)

public class Rectangle {
    private final int width;
    private final int height;

	public Rectangle(int width, int height){
		this.width = width;
		this.height = height;
	}

    public int area(){return width*height;}
}
@PostMapping("/rectangle")
public String processForm(@ModelAttribute("rectangle-model") Rectangle rectangle){
	return "rectangle-result";
}

Si l'on utilise une classe, il faut privilégier l'utilisation du constructeur.

@PostMapping (5/5)

public class Rectangle {
	private int width;
	private int height;
	
	public Rectangle(){}
	
	// setters and getters

	public int area(){return width*height;}
}
@PostMapping("/rectangle")
public String processForm(@ModelAttribute("rectangle-model") Rectangle rectangle){
	return "rectangle-result";
}

Dans l'approche historique (qui est maintenant déconseillée), Spring crée un objet en appelant le constructeur sans paramètres puis le remplit en utilisant les setters. Il faut comprendre le code mais il est déconseiller de l'utiliser.

Validation des formulaires

Règle d'or de la sécurité: Never trust the user!

On valide toujours tous les champs de tous les formulaires sur le back-end.

En pratique, on rajoute de la validation aussi sur le front-end pour rendre le site plus agréable à utiliser. Typiquement du JavaScript qui va valider les champs du formulaire et griser le bouton d'envoi si ils sont invalides.

La validation sur le front-end seule ne suffit pas ! On peut toujours accéder au back-end directement sans passer par le front-end.

Validation à la main

@PostMapping("/rectanglebetter")
    public String processForm(@ModelAttribute("rectangle") Rectangle rectangle,
                              BindingResult result, 
                              Model model){
        if (result.hasErrors()){
            return "error";
        }
        if (rectangle.width()<0 || rectangle.height()<0){
            return "error";
        }
        return "parameter-result";
    }

Les erreurs de convertion sont stocker dans BindingResult result. Par exemple, si l'utilisateur entre Toto pour la largeur.

Le code peut devenir fastidieux.

Validation automatique avec Bean Validation

Spring MVC permet de valider les champs de l'objet qu'il remplit grâce à l'API Bean Validation.

public record Rectangle( @Min(value=0,message="Width must be positive") int width,
					     @Min(value=0,message="Height must be positive") int height) {
	public int area() { return width*height; }
} 
@PostMapping("/rectangle")
public String processForm(@ModelAttribute("rectangle-model") @Valid Rectangle rectangle){
	return "rectangle-result";
}

Thymeleaf permet d'incorporer directement les messages d'erreurs de validation dans le formulaire.