🖨️ Version PDF
Il y a une distinction importante à faire entre une injection par constructeur et une injection par annotation (@Autowired). Il est vrai que l’utilisation de l’autowired est bien pratique, cependant Spring recommande la déclaration par constructeur.
injection par constructeur
injection par annotation
Voyons un exemple :
Spring se charge de remplir les propriétés après avoir créé l’objet du contrôleur. C’est simple,pratique mais pas idéal à long terme !
@RestController @RequestMapping("/api/public") public class PublicController { Autowired private AdherentRepository adherentRepository; @Autowired private ChienRepository chienRepository; }
Ici, Spring injecte automatiquement les dépendances via le constructeur, sans besoin de @Autowired, car depuis Spring 4.3, si une classe n’a qu’un seul constructeur, Spring l’utilise automatiquement pour l’injection !
@Autowired
depuis Spring 4.3
@RestController @RequestMapping("/api/public") public class PublicController { // déclaration private final ChienRepository chichienRepositoryens; private final AdherentRepository adherentRepository; // les 2 repositories utilisés public PublicQueryController(ChienRepository chienRepository, AdherentRepository adherentRepository) { // initialisation des repos this.ChienRepository = chienRepository; this.AdherentRepository = adherentRepository; } }
Avec l’injection par le constructeur, toutes les dépendances requises sont explicites. Si le contrôleur ne peut pas exister sans un Repository, on le sait.
Ça évite de créer des “demi-objets” avec des dépendances null !
C’est parfait pour l’injection de dépendances immuables (donc plus sûr en multi-thread)
Lombok génère automatiquement le constructeur avec les bons paramètres en utilisant les annotations comme ci-dessous @RequiredArgsConstructor. On gagne en lisibilité sans sacrifier la rigueur.
@RequiredArgsConstructor
@RequiredArgsConstructor @RestController @RequestMapping("/api/public") public class PublicController { private final ChienRepository chienRepository; private final AdherentRepository adherentRepository; }
L’injection par constructeur rend les tests unitaires plus propres et plus faciles ! Tester un contrôleur Spring Boot sans démarrer le serveur (test unitaire pur). On veut juste vérifier le comportement du contrôleur quand on mocke les repositories.
Version avec injection dans le contrôleur
Version testable sans Spring.
On va créer un test JUnit avec Mockito (aucun @SpringBootTest n’est nécessaire).
@SpringBootTest
import org.junit.jupiter.api.Test; import org.mockito.Mockito; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; class PublicControllerTest { @Test void testGetAllChiens() { // On créé un mock du repository ChienRepository mockChienRepo = Mockito.mock(ChienRepository.class); // On définit le comportement du mock Mockito.when(mockChienRepo.findAll()).thenReturn(List.of( new Chien("Rex"), new Chien("Milou") )); // on injecte les mocks dans le contrôleur PublicController controller = new PublicController(mockChienRepo); // On appelle la méthode à tester List<Chien> result = controller.getAllChiens(); // on vérifie le résultat assertThat(result).hasSize(2); assertThat(result.get(0).getNom()).isEqualTo("Rex"); // on vérifie que le repository a bien été appelé Mockito.verify(mockChienRepo).findAll(); } }
Il faut ajouter la classes et repositories pour que notre exemple compile et fonctionne :
On se limite à la classe Chien pour le moment.
public class Chien { private String nom; public Chien(String nom) { this.nom = nom; } public String getNom() { return nom; } } public interface ChienRepository { List<Chien> findAll(); }
Pourquoi c’est possible uniquement avec l’injection par constructeur ?
Dans notre exemple de tests, on ne démarre pas Spring Boot et pourtant, notre contrôleur fonctionne !
Si on avait utilisé le code ci-dessous :
@Autowired private ChienRepository chienRepository;
Nous ne pourrions pas injecter manuellement un mock sans Spring et on aurait dû écrire un test d’intégration avec @SpringBootTest qui est beaucoup plus lent et lourd. On aurait perdu la modularité et la testabilité.
final
null
En résumé
L’injection par constructeur nous permet de tester sans Spring (tests unitaires plus rapides) et aussi avec Spring (MockMvc/intégration)
Donc, cette syntaxe offre le meilleur des deux mondes :
c’est-à-dire un test qui démarre un contexte Spring minimal pour simuler un vrai appel HTTP au contrôleur via MockMvc. on va ainsi voir comment le même contrôleur peut être testé dans un environnement “réel” (mais rapide) dans lequel Spring gère tout ! (injection, routage, JSON, etc.)
On reprend notre contrôleur à tester en y mettant Concours.
@RestController @RequestMapping("/api/public") public class PublicController { private final ChienRepository chiens; private final ConcoursRepository concours; public PublicController(ChienRepository chiens, ConcoursRepository concours) { this.chiens = chiens; this.concours = concours; } @GetMapping("/chiens") public List<Chien> getAllChiens() { return chiens.findAll(); } @GetMapping("/concours") public List<Concours> getAllConcours() { return concours.findAll(); } }
Nos classes modèles Chien et Concours :
public class Chien { private String nom; public Chien(String nom) { this.nom = nom; } public String getNom() { return nom; } } public class Concours { private String nom; public Concours(String nom) { this.nom = nom; } public String getNom() { return nom; } }
import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.test.web.servlet.MockMvc; import java.util.List; import static org.mockito.Mockito.when; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; @WebMvcTest(PublicQueryController.class) // on démarre uniquement la couche web class PublicQueryControllerIntegrationTest { @Autowired private MockMvc mockMvc; // outil pour simuler des requêtes HTTP // on remplace les vrais repositories par des mocks @MockBean private ChienRepository chienRepository; @MockBean private ConcoursRepository concoursRepository; @Test void shouldReturnListOfChiens() throws Exception { // on configure le comportement du mock when(chienRepository.findAll()).thenReturn(List.of( new Chien("Rex"), new Chien("Milou") )); // on simule un appel GET /api/public/chiens mockMvc.perform(get("/api/public/chiens")) .andExpect(status().isOk()) .andExpect(jsonPath("$[0].nom").value("Rex")) .andExpect(jsonPath("$[1].nom").value("Milou")); } }
@WebMvcTest(PublicQueryController.class)
@MockBean
MockMvc
jsonPath()
MockHttpServletRequest: HTTP Method = GET Request URI = /api/public/chiens Handler: Type = PublicQueryController Method = getAllChiens() MockHttpServletResponse: Status = 200 Body = [{"nom":"Rex"},{"nom":"Milou"}]
@WebMvcTest
Pas d’injection sur champ, ni de @Autowired en vrac, il vaut mieux :
@Configuration
@Bean
@Service
@Repository
@Controller
Exemple :
@Configuration public class AppConfig { @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } }
Objectif : Séparer les responsabilités et éviter le code spaghetti !
api/ ├── controller/ │ └── AdherentController.java ├── service/ │ └── AdherentService.java ├── repository/ │ └── AdherentRepository.java └── model/ └── Adherent.java
Pour simplifier :
@RestController
C’est la meilleure façon pour :
@GetMapping("/{id}") public ResponseEntity<Adherent> getAdherent(@PathVariable Long id) { return adherentService.findById(id) .map(ResponseEntity::ok) .orElse(ResponseEntity.notFound().build()); }
Il faut prendre l’habitude de contrôler les exceptions (ne pas les laisser surcharger vos logs !).
@RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(EntityNotFoundException.class) public ResponseEntity<String> handleNotFound(EntityNotFoundException e) { return ResponseEntity.status(HttpStatus.NOT_FOUND).body(e.getMessage()); } }
Voici une autre bonne pratique pour éviter les NullPointerException et les données pourries dès la réception !
NullPointerException
@PostMapping("/adherents") public ResponseEntity<?> create(@Valid @RequestBody AdherentDTO dto) { return ResponseEntity.ok(service.save(dto)); } public class AdherentDTO { @NotBlank private String nom; @Email private String email; }
Nous verrons en détail les différents paramètres existants.
Même si on a tendance à le faire lors des TP et projets exemples pour simplifier, il est préférable d’utiliser des DTO pour les raisons ci-dessous.
public record AdherentDTO(Long id, String nom, String email) {} @Service public class AdherentService { public AdherentDTO toDTO(Adherent a) { return new AdherentDTO(a.getId(), a.getNom(), a.getEmail()); } }
On peut aussi utiliser MapStruct ou ModelMapper pour automatiser la conversion !
MapStruct
ModelMapper
Le contrôleur ne doit faire que :
Code correct :
@GetMapping public List<Chien> getChiens() { return service.findAdultes(); }
Exemple de code incorrect :
@GetMapping public List<Chien> getChiens() { return repository.findAll() .stream() .filter(c -> c.getAge() > 2) .toList(); }
SLF4J (Simple Logging Façade for Java) est une surcouche d’abstraction permettant à votre application de fonctionner avec une API de log quelconque (comme LOG4J 2, java.util.logging).
Ainsi le changement de la solution de log concrètement utilisée, n’impactera aucune ligne de code de votre application.
Pour éviter les System.out.println surtout en prod :
System.out.println
@Slf4j @Service public class ConcoursService { public void inscrire(Chien chien) { log.info("Inscription du chien {}", chien.getNom()); } }
Il ne faut pas de configuration en dur, il vaut mieux utiliser les fichiers :
Exemples :
app: nom: MonSiteAccessible mail: contact@monsiteaccessible.com
et
@ConfigurationProperties(prefix = "app") public record AppProperties(String nom, String mail) {}
# application-dev.yml spring: datasource: url: jdbc:h2:mem:testdb
Bien pratique…
# application-prod.yml spring: datasource: url: jdbc:postgresql://prod-db:5432/app
Pour sélectionner le profil avec SpringBoot :
--spring.profiles.active=dev
Il faut activer :
GET /actuator/health` {"status":"UP"}
@Valid
@NotBlank
@Email
@ControllerAdvice
@Slf4j
System.out
application.yml
application-dev.yml
application-prod.yml
et là, votre application sera propre !