Skip to content

Latest commit

 

History

History
823 lines (632 loc) · 21.7 KB

File metadata and controls

823 lines (632 loc) · 21.7 KB

🧩 Séance 1 — Introduction & Hello Spring Boot (3h)

🎯 Objectifs

  • Comprendre le rôle de Spring Boot.
  • Créer et exécuter une première API REST.
  • Explorer la structure d’un projet Spring Boot.

🧑‍🏫 Slide 1 : Pourquoi un framework ?

Sans framework :
- Beaucoup de configuration manuelle
- Code répétitif
- Sécurité, logs, injections, etc. à gérer soi-même

Avec Spring Boot :
✅ Gain de temps
✅ Convention > configuration
✅ Outils intégrés (serveur web, logs, tests)

🧑‍🏫 Slide 2 : Écosystème Spring

Spring = Framework Java pour apps d’entreprise
Spring Boot = version simplifiée et automatisée de Spring

→ Crée une appli web en 2 minutes avec des dépendances prêtes à l’emploi

🧑‍🏫 Slide 3 : Architecture propre

src/
 └── main/
     └── java/
         └── com/example/demo/
             ├── DemoApplication.java
             ├── controller/      👈 contrôleurs REST (API)
             │    └── UserController.java
             ├── model/           👈 modèles ou entités (domain)
             │    └── User.java
             ├── repository/      👈 accès à la base de données
             │    └── UserRepository.java
             └── service/         👈 logique métier (règles, traitements)
                  └── UserService.java

🧑‍🏫 Slide 4 : Environnement de développement

  1. JDK 17 (java -version)
  2. VsCode

🧑‍🏫 Slide 5 : Environnement de développement

Plugin VSCode

  1. Extension Pack for Java
  2. Spring Boot Extension Pack
  3. Lombok Annotations Support for VS Code

🧑‍💻 Démo formateur : Créer un projet

  1. Aller sur https://start.spring.io

  2. Choisir :

    • Project : Maven
    • Language : Java
    • Spring Boot : 3.x
    • Java 17 : Version la plus populaire
    • Dependencies : Spring Web, Lombok, JPA, H2
  3. Télécharger le projet et l’ouvrir dans VsCode.


🧑‍💻 Démo formateur : Lancer le projet

Configuration de application.properties

spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
spring.jpa.hibernate.ddl-auto=update
spring.h2.console.enabled=true
  1. Vérifier que l'on a bien Java d'installé sur sa machine.
  2. Dans VS Code → File → Open Folder → Le dossier dézippé du projet généré
  3. VS Code détecte automatiquement le projet Java (Maven)
  4. Laisse VS Code importer les dépendances
  5. Lance ton application avec './mvnw spring-boot:run'
  6. Go sur http://localhost:8080

💻 Code démo — Hello Controller

Sur le dossier com.example.demo → New File

@RestController
public class HelloController {

    @GetMapping("/hello")
    public String sayHello() {
        return "Hello Spring Boot 🚀";
    }
}

➡️ Lancer l’app → ouvrir http://localhost:8080/hello


🧑‍🎓 Exercice 1 — “Hello You”

Créer un endpoint /hello/{name} qui retourne :

"Hello, [name]! Bienvenue dans Spring Boot."

💡 Indices :

  • Utiliser @PathVariable String name
  • Retourner une String

Corrigé :

@GetMapping("/hello/{name}")
public String sayHello(@PathVariable String name) {
    return "Hello, " + name + " ! Bienvenue dans Spring Boot.";
}

🧠 Points à faire passer

  • Spring Boot = framework opinionated → il décide pour toi.
  • Fichiers clés : Application.java (main), application.properties, pom.xml.

🧩 Séance 2 — Architecture et CRUD statique (4h)

🎯 Objectifs

  • Structurer une application en couches.
  • Créer un CRUD complet avec données en mémoire.
  • Comprendre @Service, @Repository, @Autowired.

🧑‍🏫 Slide 4 : Architecture MVC simplifiée

Controller → reçoit les requêtes
Service → logique métier
Repository → accès aux données
Model → objets manipulés (entités)

💻 Code démo — modèle Task

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Task {
    private Long id;
    private String title;
    private boolean done;
}

💻 Code démo — service

@Service
public class TaskService {
    private List<Task> tasks = new ArrayList<>();

    public TaskService() {
        tasks.add(new Task(1L, "Apprendre Spring Boot", false));
    }

    public List<Task> findAll() { return tasks; }
    public Task findById(Long id) { 
        return tasks.stream()
                .filter(t -> t.getId().equals(id))
                .findFirst()
                .orElseThrow(() -> new RuntimeException("Task not found"));
    }
    public Task add(Task task) {
        task.setId((long) (tasks.size() + 1));
        tasks.add(task);
        return task;
    }
    public void delete(Long id) {
        tasks.removeIf(t -> t.getId().equals(id));
    }
}

💻 Code démo — controller

@RestController
@RequestMapping("/tasks")
public class TaskController {

    private final TaskService taskService;
    public TaskController(TaskService taskService) {
        this.taskService = taskService;
    }

    @GetMapping
    public List<Task> getAll() { return taskService.findAll(); }

    @GetMapping("/{id}")
    public Task getOne(@PathVariable Long id) { return taskService.findById(id); }

    @PostMapping
    public Task add(@RequestBody Task task) { return taskService.add(task); }

    @DeleteMapping("/{id}")
    public void delete(@PathVariable Long id) { taskService.delete(id); }
}

🧑‍🎓 Exercice 2 — Ajouter un endpoint PUT

Objectif : permettre la mise à jour d’une tâche (changer le titre ou l’état done).

Corrigé :

@PutMapping("/{id}")
public Task update(@PathVariable Long id, @RequestBody Task newTask) {
    Task t = taskService.findById(id);
    t.setTitle(newTask.getTitle());
    t.setDone(newTask.isDone());
    return t;
}

🧠 Points à souligner

  • Injection de dépendances via constructeur = meilleure pratique.
  • Le service contient la logique métier, pas le contrôleur.

🧩 Séance 3 — Persistance avec Spring Data JPA (4h)

🎯 Objectifs

  • Configurer une base de données (H2 ou MySQL).
  • Créer des entités JPA et repositories.
  • Comprendre la magie de JpaRepository.

🧑‍🏫 Slide 5 : Qu’est-ce qu’un ORM ?

ORM = Object Relational Mapping
→ traduit des objets Java en tables SQL

Spring Data JPA = simplifie l’accès à la base

💻 Code démo — Entité JPA

@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Task {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String title;
    private boolean done;
}

💻 Code démo — Repository

Dépendance à ajouter :

		<!-- Add Spring Data JPA so JpaRepository is available -->
		<dependency>
    		<groupId>org.springframework.boot</groupId>
    		<artifactId>spring-boot-starter-data-jpa</artifactId>
		</dependency>
import org.springframework.data.jpa.repository.JpaRepository;

public interface TaskRepository extends JpaRepository<Task, Long> {
    List<Task> findByDone(boolean done);
}

Commande :

.\mvnw.cmd spring-boot:run

.\mvnw.cmd clean package

💻 Code démo — Service modifié

@Service
public class TaskService {
    private final TaskRepository repo;
    public TaskService(TaskRepository repo) { this.repo = repo; }

    public List<Task> findAll() { return repo.findAll(); }
    public Task findById(Long id) { return repo.findById(id).orElseThrow(); }
    public Task add(Task t) { return repo.save(t); }
    public void delete(Long id) { repo.deleteById(id); }
}

⚙️ application.properties

spring.datasource.url=jdbc:h2:mem:testdb
spring.h2.console.enabled=true
spring.jpa.hibernate.ddl-auto=update

🧑‍🎓 Exercice 3 — Filtrer par état

Créer un endpoint /tasks/done pour retourner uniquement les tâches terminées.

Corrigé :

@GetMapping("/done")
public List<Task> getDoneTasks() {
    return taskService.findByDone(true);
}

🧠 Points à retenir

  • Spring Data JPA gère automatiquement les CRUD.
  • Pas besoin d’écrire du SQL.
  • H2 est très pratique pour tester sans serveur externe.

🧠 Tester avec un html simple

Ajouter cette option au controleur

''' @CrossOrigin(origins = "*") // autorise toutes les origines '''

Le html de base pour tester

<!DOCTYPE html>
<html lang="fr">
<head>
  <meta charset="UTF-8">
  <title>Test TaskController</title>
  <style>
    body {
      font-family: Arial, sans-serif;
      margin: 30px;
      background: #f5f5f5;
    }
    h1 { color: #333; }
    section {
      background: white;
      padding: 20px;
      border-radius: 10px;
      box-shadow: 0 2px 8px rgba(0,0,0,0.1);
      margin-bottom: 20px;
    }
    input, button {
      padding: 8px;
      margin: 5px;
      border-radius: 5px;
      border: 1px solid #ccc;
    }
    button {
      cursor: pointer;
      background-color: #1976d2;
      color: white;
      border: none;
    }
    button:hover {
      background-color: #0d47a1;
    }
    table {
      border-collapse: collapse;
      width: 100%;
      margin-top: 10px;
    }
    th, td {
      border: 1px solid #ddd;
      padding: 8px;
      text-align: left;
    }
    th {
      background: #1976d2;
      color: white;
    }
  </style>
</head>
<body>
  <h1>🧪 Test du TaskController</h1>

  <section>
    <h2>🔍 Récupérer les tâches</h2>
    <button onclick="getAllTasks()">Afficher toutes</button>
    <button onclick="getDoneTasks()">Afficher les 'done'</button>
    <table id="taskTable">
      <thead>
        <tr><th>ID</th><th>Title</th><th>Done</th><th>Actions</th></tr>
      </thead>
      <tbody></tbody>
    </table>
  </section>

  <section>
    <h2>➕ Ajouter une tâche</h2>
    <input id="addTitle" placeholder="Titre de la tâche">
    <label><input type="checkbox" id="addDone"> Done ?</label>
    <button onclick="addTask()">Ajouter</button>
  </section>

  <section>
    <h2>✏️ Mettre à jour une tâche</h2>
    <input id="updateId" type="number" placeholder="ID">
    <input id="updateTitle" placeholder="Nouveau titre">
    <label><input type="checkbox" id="updateDone"> Done ?</label>
    <button onclick="updateTask()">Mettre à jour</button>
  </section>

  <section>
    <h2>🗑️ Supprimer une tâche</h2>
    <input id="deleteId" type="number" placeholder="ID">
    <button onclick="deleteTask()">Supprimer</button>
  </section>

  <script>
    const BASE_URL = "http://localhost:8080/tasks";

    async function getAllTasks() {
      const res = await fetch(BASE_URL);
      const data = await res.json();
      renderTable(data);
    }

    async function getDoneTasks() {
      const res = await fetch(`${BASE_URL}/done`);
      const data = await res.json();
      renderTable(data);
    }

    async function addTask() {
      const title = document.getElementById("addTitle").value;
      const done = document.getElementById("addDone").checked;
      const res = await fetch(BASE_URL, {
        method: "POST",
        headers: {"Content-Type": "application/json"},
        body: JSON.stringify({ title, done })
      });
      if (res.ok) {
        alert("Tâche ajoutée !");
        getAllTasks();
      }
    }

    async function updateTask() {
      const id = document.getElementById("updateId").value;
      const title = document.getElementById("updateTitle").value;
      const done = document.getElementById("updateDone").checked;
      const res = await fetch(`${BASE_URL}/${id}`, {
        method: "PUT",
        headers: {"Content-Type": "application/json"},
        body: JSON.stringify({ title, done })
      });
      if (res.ok) {
        alert("Tâche mise à jour !");
        getAllTasks();
      }
    }

    async function deleteTask() {
      const id = document.getElementById("deleteId").value;
      const res = await fetch(`${BASE_URL}/${id}`, { method: "DELETE" });
      if (res.ok) {
        alert("Tâche supprimée !");
        getAllTasks();
      }
    }

    function renderTable(tasks) {
      const tbody = document.querySelector("#taskTable tbody");
      tbody.innerHTML = "";
      tasks.forEach(t => {
        const row = document.createElement("tr");
        row.innerHTML = `
          <td>${t.id}</td>
          <td>${t.title}</td>
          <td>${t.done ? "✅" : "❌"}</td>
          <td>
            <button onclick="prefillUpdate(${t.id}, '${t.title}', ${t.done})">🖋️</button>
            <button onclick="deleteTaskById(${t.id})">🗑️</button>
          </td>`;
        tbody.appendChild(row);
      });
    }

    function prefillUpdate(id, title, done) {
      document.getElementById("updateId").value = id;
      document.getElementById("updateTitle").value = title;
      document.getElementById("updateDone").checked = done;
    }

    function deleteTaskById(id) {
      document.getElementById("deleteId").value = id;
      deleteTask();
    }

    // Charger la liste au démarrage
    getAllTasks();
  </script>
</body>
</html>

🧩 Séance 4 — Tests, Swagger & Packaging (3h)

🎯 Objectifs

  • Tester les endpoints.
  • Documenter l’API avec Swagger.
  • Générer le .jar final.

💻 Code démo — Test simple

@SpringBootTest
@AutoConfigureMockMvc
class TaskControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @Test
    void shouldReturnAllTasks() throws Exception {
        mockMvc.perform(get("/tasks"))
               .andExpect(status().isOk());
    }
}

Pour lancer les tests

./mvnw test
package com.example.demo; // <-- adapte ce package à ton projet

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; // pour GET, POST, PUT, DELETE
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; // pour status, content, jsonPath

@SpringBootTest // Démarre tout le contexte Spring Boot pour tester l’application complète
@AutoConfigureMockMvc // Active MockMvc pour simuler des requêtes HTTP sans serveur réel
class TaskControllerTest {

    @Autowired
    private MockMvc mockMvc; // Objet fourni par Spring pour simuler des appels HTTP

    // --------------------------------------------------------
    // 🧪 1️⃣ TEST : Récupération de toutes les tâches
    // --------------------------------------------------------
    @Test
    void shouldReturnAllTasks() throws Exception {
        mockMvc.perform(get("/tasks")) // Simule une requête HTTP GET sur /tasks
               .andExpect(status().isOk()) // Vérifie que la réponse a le code 200 OK
               .andExpect(content().contentType(MediaType.APPLICATION_JSON)) // Vérifie que la réponse est du JSON
               .andExpect(jsonPath("$").isArray()); // Vérifie que le JSON est bien un tableau (liste de tâches)
    }

    // --------------------------------------------------------
    // 🧪 2️⃣ TEST : Ajout d’une nouvelle tâche
    // --------------------------------------------------------
    @Test
    void shouldAddNewTask() throws Exception {
        // JSON représentant une nouvelle tâche
        String newTaskJson = "{\"title\": \"Faire les courses\", \"done\": false}";

        mockMvc.perform(post("/tasks") // Envoie une requête HTTP POST sur /tasks
                        .contentType(MediaType.APPLICATION_JSON) // Spécifie qu’on envoie du JSON
                        .content(newTaskJson)) // Corps de la requête = le JSON ci-dessus
               .andExpect(status().isOk()) // Vérifie que la réponse a bien un code 200 (succès)
               .andExpect(jsonPath("$.title").value("Faire les courses")) // Vérifie que la tâche retournée contient bien le bon titre
               .andExpect(jsonPath("$.done").value(false)); // Vérifie que le champ done = false
    }

    // --------------------------------------------------------
    // 🧪 3️⃣ TEST : Récupération d’une tâche par son ID
    // --------------------------------------------------------
    @Test
    void shouldReturnOneTaskById() throws Exception {
        // D’abord, on crée une tâche pour avoir un ID existant
        String newTaskJson = "{\"title\": \"Lire un livre\", \"done\": false}";

        // On enregistre la tâche et récupère le résultat dans une variable
        String response = mockMvc.perform(post("/tasks")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(newTaskJson))
                .andExpect(status().isOk())
                .andReturn()
                .getResponse()
                .getContentAsString();

        // Exemple simple : on sait que la première tâche aura souvent l’ID 1 si la base est vide.
        // Mais dans un vrai projet, tu pourrais extraire l’ID du JSON via un parseur.
        mockMvc.perform(get("/tasks/1")) // On récupère la tâche avec l’ID 1
               .andExpect(status().isOk()) // On attend un 200 OK
               .andExpect(jsonPath("$.title").exists()); // Vérifie qu’il y a bien un titre dans la réponse
    }

    // --------------------------------------------------------
    // 🧪 4️⃣ TEST : Mise à jour d’une tâche existante
    // --------------------------------------------------------
    @Test
    void shouldUpdateTask() throws Exception {
        // D’abord on crée une tâche à modifier
        String newTaskJson = "{\"title\": \"Faire le ménage\", \"done\": false}";

        mockMvc.perform(post("/tasks")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(newTaskJson))
                .andExpect(status().isOk());

        // Puis on prépare le JSON de mise à jour
        String updatedTaskJson = "{\"title\": \"Faire le grand ménage\", \"done\": true}";

        // Appel PUT pour mettre à jour la tâche avec l’ID 1
        mockMvc.perform(put("/tasks/1")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(updatedTaskJson))
               .andExpect(status().isOk()) // Vérifie que la requête est OK
               .andExpect(jsonPath("$.title").value("Faire le grand ménage")) // Vérifie le nouveau titre
               .andExpect(jsonPath("$.done").value(true)); // Vérifie que "done" est passé à true
    }

    // --------------------------------------------------------
    // 🧪 5️⃣ TEST : Suppression d’une tâche
    // --------------------------------------------------------
    @Test
    void shouldDeleteTask() throws Exception {
        // D’abord on crée une tâche à supprimer
        String newTaskJson = "{\"title\": \"Aller courir\", \"done\": false}";

        mockMvc.perform(post("/tasks")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(newTaskJson))
                .andExpect(status().isOk());

        // Suppression de la tâche d’ID 1
        mockMvc.perform(delete("/tasks/1")) // Envoie une requête DELETE
               .andExpect(status().isOk()); // Vérifie que la suppression renvoie 200 OK
    }

    // --------------------------------------------------------
    // 🧪 6️⃣ TEST : Récupération des tâches "done" uniquement
    // --------------------------------------------------------
    @Test
    void shouldReturnDoneTasks() throws Exception {
        // On ajoute une tâche marquée comme faite
        String doneTaskJson = "{\"title\": \"Apprendre Spring\", \"done\": true}";

        mockMvc.perform(post("/tasks")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(doneTaskJson))
                .andExpect(status().isOk());

        // On appelle GET /tasks/done pour récupérer uniquement les "done"
        mockMvc.perform(get("/tasks/done"))
               .andExpect(status().isOk()) // Vérifie que la requête réussit
               .andExpect(content().contentType(MediaType.APPLICATION_JSON)) // Réponse JSON attendue
               .andExpect(jsonPath("$[0].done").value(true)); // Vérifie que la 1ère tâche est bien done = true
    }
}

💻 Code démo — Swagger

Ajouter la dépendance dans pom.xml :

<dependency>
  <groupId>org.springdoc</groupId>
  <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
  <version>2.2.0</version>
</dependency>

Lancer l’app → http://localhost:8080/swagger-ui.html


💻 Commande finale

mvn clean package
java -jar target/demo-0.0.1-SNAPSHOT.jar

🧑‍🎓 Exercice 4 — Test de validation

Ajouter une contrainte :

@NotBlank
private String title;

Tester qu’une requête POST vide renvoie une erreur 400.


🧠 Points à souligner

  • Les tests assurent la stabilité du code.
  • Swagger facilite la communication entre développeurs.
  • Spring Boot embarque Tomcat : pas besoin de serveur externe.

🏁 Bilan de la formation

Les apprenants savent :

✅ Créer un projet Spring Boot

✅ Structurer un code propre (Controller / Service / Repository)

✅ Gérer une base de données avec JPA

✅ Tester et documenter leur API


🧠 Pistes pour aller plus loin :

  • Spring Security (authentification)
  • Déploiement sur Render / Railway
  • Variables d’environnement avec application.yml
  • Relations complexes JPA (ManyToMany, OneToMany, etc.)