# 30 questions d'entretien Spring Boot : Guide complet pour développeurs Java > Préparez vos entretiens Spring Boot avec ces 30 questions essentielles couvrant l'auto-configuration, les starters, Spring Data JPA, la sécurité et les tests. - Published: 2026-02-07 - Updated: 2026-03-31 - Author: SharpSkill - Tags: spring boot, java, entretien, interview, backend - Reading time: 18 min --- Spring Boot est devenu le framework de référence pour le développement d'applications Java en entreprise. Les entretiens techniques évaluent la compréhension des mécanismes internes, des bonnes pratiques et de l'écosystème Spring. Ce guide couvre les 30 questions les plus fréquentes, des concepts fondamentaux aux aspects avancés. > **Conseil de préparation** > > Les recruteurs valorisent les candidats qui comprennent le "pourquoi" derrière chaque fonctionnalité. Au-delà de la syntaxe, expliquez les problèmes résolus par Spring Boot. ## Fondamentaux Spring Boot ### 1. Quelle est la différence entre Spring et Spring Boot ? Spring est un framework modulaire offrant l'injection de dépendances, la gestion transactionnelle et l'intégration avec de nombreuses technologies. Spring Boot est une couche d'abstraction au-dessus de Spring qui simplifie la configuration et le démarrage des applications. ```java // Application.java // Avec Spring Boot : une seule annotation pour démarrer @SpringBootApplication public class Application { public static void main(String[] args) { // SpringApplication configure et démarre le contexte Spring SpringApplication.run(Application.class, args); } } ``` Spring Boot apporte l'auto-configuration, les starters de dépendances, un serveur embarqué et les métriques de production via Actuator. L'objectif est de passer de l'idée au code fonctionnel en quelques minutes. ### 2. Comment fonctionne l'auto-configuration ? L'auto-configuration analyse le classpath et configure automatiquement les beans nécessaires. Le mécanisme repose sur l'annotation `@EnableAutoConfiguration` (incluse dans `@SpringBootApplication`) et les conditions définies dans les classes d'auto-configuration. ```java // CustomAutoConfiguration.java @Configuration @ConditionalOnClass(DataSource.class) // Active si DataSource est dans le classpath @ConditionalOnMissingBean(DataSource.class) // N'agit que si aucun DataSource n'existe public class CustomAutoConfiguration { @Bean @ConditionalOnProperty(name = "app.datasource.enabled", havingValue = "true") public DataSource dataSource() { // Configuration par défaut de la DataSource return DataSourceBuilder.create() .driverClassName("org.h2.Driver") .url("jdbc:h2:mem:testdb") .build(); } } ``` Spring Boot examine les conditions (`@ConditionalOn*`) pour déterminer quels beans créer. Cette approche évite les conflits et permet de surcharger facilement les configurations par défaut. ### 3. Qu'est-ce qu'un starter et comment en créer un ? Un starter est un module Maven/Gradle regroupant les dépendances et configurations nécessaires pour une fonctionnalité. Par exemple, `spring-boot-starter-web` inclut Spring MVC, Jackson, Tomcat embarqué et la validation. ```xml my-company-starter org.springframework.boot spring-boot-starter com.mycompany logging-utils ``` Pour créer un starter personnalisé, il faut définir les classes d'auto-configuration et les référencer dans `META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports`. ### 4. Expliquez le fichier application.properties vs application.yml Les deux formats servent à externaliser la configuration. YAML offre une syntaxe plus lisible pour les structures imbriquées, tandis que properties reste plus simple pour les configurations plates. ```properties # application.properties server.port=8080 spring.datasource.url=jdbc:postgresql://localhost:5432/mydb spring.datasource.username=admin spring.jpa.hibernate.ddl-auto=update ``` ```yaml # application.yml - structure hiérarchique claire server: port: 8080 spring: datasource: url: jdbc:postgresql://localhost:5432/mydb username: admin jpa: hibernate: ddl-auto: update ``` Les profils permettent d'adapter la configuration selon l'environnement : `application-dev.yml`, `application-prod.yml`. L'activation se fait via `spring.profiles.active`. ### 5. Comment fonctionne @SpringBootApplication ? Cette annotation combine trois annotations essentielles qui configurent le comportement de l'application Spring Boot. ```java // DemoApplication.java // @SpringBootApplication est équivalent à ces trois annotations : @SpringBootConfiguration // Équivalent à @Configuration @EnableAutoConfiguration // Active l'auto-configuration @ComponentScan // Scan les composants dans le package et sous-packages public class DemoApplication { public static void main(String[] args) { // Crée le contexte, exécute les configurations et démarre le serveur ConfigurableApplicationContext context = SpringApplication.run(DemoApplication.class, args); // Le contexte contient tous les beans configurés UserService userService = context.getBean(UserService.class); } } ``` Le placement de cette classe à la racine du package est crucial : le `@ComponentScan` scanne ce package et tous ses sous-packages pour trouver les composants. ## Spring Data et Persistance ### 6. Quelle est la différence entre JpaRepository, CrudRepository et PagingAndSortingRepository ? Ces interfaces forment une hiérarchie offrant des niveaux de fonctionnalités croissants pour l'accès aux données. ```java // UserRepository.java // CrudRepository : opérations CRUD de base public interface UserCrudRepository extends CrudRepository { // save(), findById(), findAll(), deleteById(), count(), existsById() } // PagingAndSortingRepository : ajoute pagination et tri public interface UserPagingRepository extends PagingAndSortingRepository { // Hérite de CrudRepository + findAll(Sort), findAll(Pageable) } // JpaRepository : fonctionnalités JPA complètes public interface UserRepository extends JpaRepository { // Hérite des précédents + flush(), saveAndFlush(), deleteInBatch() // Méthodes dérivées du nom List findByEmailContaining(String email); Optional findByUsernameAndActiveTrue(String username); } ``` Pour la plupart des cas, `JpaRepository` est le choix recommandé car il offre toutes les fonctionnalités nécessaires au développement d'applications JPA. ### 7. Comment définir des requêtes personnalisées avec @Query ? L'annotation `@Query` permet d'écrire des requêtes JPQL ou SQL natives lorsque les méthodes dérivées ne suffisent pas. ```java // OrderRepository.java public interface OrderRepository extends JpaRepository { // JPQL avec paramètres nommés @Query("SELECT o FROM Order o WHERE o.customer.id = :customerId AND o.status = :status") List findCustomerOrdersByStatus( @Param("customerId") Long customerId, @Param("status") OrderStatus status ); // Requête native SQL pour des besoins spécifiques @Query(value = "SELECT * FROM orders WHERE created_at > NOW() - INTERVAL '7 days'", nativeQuery = true) List findRecentOrders(); // Requête de modification avec @Modifying @Modifying @Transactional @Query("UPDATE Order o SET o.status = :status WHERE o.id IN :ids") int updateOrderStatuses(@Param("ids") List ids, @Param("status") OrderStatus status); } ``` Les requêtes JPQL sont portables entre bases de données, tandis que les requêtes natives offrent l'accès aux fonctionnalités spécifiques du SGBD. ### 8. Expliquez la gestion des transactions avec @Transactional Cette annotation déclare les limites transactionnelles d'une méthode ou d'une classe. Spring gère automatiquement le commit, le rollback et la propagation. ```java // PaymentService.java @Service public class PaymentService { private final AccountRepository accountRepository; private final TransactionLogRepository logRepository; // Transaction avec configuration personnalisée @Transactional( propagation = Propagation.REQUIRED, // Crée ou rejoint une transaction isolation = Isolation.READ_COMMITTED, // Niveau d'isolation timeout = 30, // Timeout en secondes rollbackFor = PaymentException.class // Rollback sur cette exception ) public void transferMoney(Long fromId, Long toId, BigDecimal amount) { Account from = accountRepository.findById(fromId) .orElseThrow(() -> new AccountNotFoundException(fromId)); Account to = accountRepository.findById(toId) .orElseThrow(() -> new AccountNotFoundException(toId)); from.debit(amount); // Exception si solde insuffisant to.credit(amount); accountRepository.save(from); accountRepository.save(to); // Le log sera rollbacké avec le reste si une exception survient logRepository.save(new TransactionLog(fromId, toId, amount)); } } ``` Les niveaux de propagation (`REQUIRED`, `REQUIRES_NEW`, `NESTED`, etc.) définissent comment les transactions s'imbriquent lors d'appels de méthodes transactionnelles. > **Piège courant** > > `@Transactional` ne fonctionne que pour les appels passant par le proxy Spring. Un appel interne à la même classe contourne le proxy et ignore l'annotation. ### 9. Comment gérer les relations Lazy vs Eager Loading ? Le mode de chargement des relations impacte directement les performances. Lazy Loading diffère le chargement jusqu'à l'accès, Eager Loading charge immédiatement. ```java // User.java @Entity public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; // Eager : les rôles sont chargés avec l'utilisateur @ManyToMany(fetch = FetchType.EAGER) private Set roles; // Lazy : les commandes ne sont chargées qu'à l'accès @OneToMany(mappedBy = "user", fetch = FetchType.LAZY) private List orders; } ``` ```java // UserService.java @Service public class UserService { @Transactional(readOnly = true) public UserDTO getUserWithOrders(Long userId) { User user = userRepository.findById(userId).orElseThrow(); // L'accès à orders déclenche une requête SQL supplémentaire // Cela fonctionne car la session est ouverte (@Transactional) List orderDTOs = user.getOrders().stream() .map(this::toDTO) .collect(Collectors.toList()); return new UserDTO(user, orderDTOs); } // Alternative : requête avec JOIN FETCH pour éviter N+1 public UserDTO getUserWithOrdersOptimized(Long userId) { User user = userRepository.findByIdWithOrders(userId); // Les orders sont déjà chargés, pas de requête supplémentaire return new UserDTO(user, user.getOrders()); } } ``` Le problème N+1 survient quand le chargement lazy génère une requête par élément de collection. `JOIN FETCH` ou les projections résolvent ce problème. ### 10. Qu'est-ce que le problème N+1 et comment le résoudre ? Le problème N+1 se produit quand une requête initiale (1) est suivie de N requêtes supplémentaires pour charger les relations de chaque entité. ```java // ArticleRepository.java public interface ArticleRepository extends JpaRepository { // PROBLÈME : cette requête génère N+1 requêtes List
findAll(); // SOLUTION 1 : JOIN FETCH dans JPQL @Query("SELECT a FROM Article a JOIN FETCH a.author JOIN FETCH a.comments") List
findAllWithDetails(); // SOLUTION 2 : Entity Graph @EntityGraph(attributePaths = {"author", "comments"}) @Query("SELECT a FROM Article a") List
findAllWithGraph(); } ``` ```java // Article.java @Entity @NamedEntityGraph( name = "Article.withDetails", attributeNodes = { @NamedAttributeNode("author"), @NamedAttributeNode("comments") } ) public class Article { @Id private Long id; @ManyToOne(fetch = FetchType.LAZY) private Author author; @OneToMany(mappedBy = "article", fetch = FetchType.LAZY) private List comments; } ``` L'utilisation de `@EntityGraph` ou `JOIN FETCH` réduit les requêtes de N+1 à une seule requête avec jointures, améliorant significativement les performances. ## API REST et Web ### 11. Comment créer un contrôleur REST avec validation ? Spring Boot combine `@RestController` avec Bean Validation pour créer des API robustes avec validation automatique des entrées. ```java // UserController.java @RestController @RequestMapping("/api/users") @Validated // Active la validation sur les paramètres de méthode public class UserController { private final UserService userService; @PostMapping @ResponseStatus(HttpStatus.CREATED) public UserResponse createUser(@Valid @RequestBody CreateUserRequest request) { // La validation est effectuée automatiquement avant l'exécution return userService.createUser(request); } @GetMapping("/{id}") public UserResponse getUser( @PathVariable @Min(1) Long id // Validation sur PathVariable ) { return userService.findById(id) .orElseThrow(() -> new UserNotFoundException(id)); } @GetMapping public Page listUsers( @RequestParam(defaultValue = "0") @Min(0) int page, @RequestParam(defaultValue = "20") @Max(100) int size ) { return userService.findAll(PageRequest.of(page, size)); } } ``` ```java // CreateUserRequest.java public record CreateUserRequest( @NotBlank(message = "Le nom est obligatoire") @Size(min = 2, max = 50, message = "Le nom doit contenir entre 2 et 50 caractères") String name, @NotBlank(message = "L'email est obligatoire") @Email(message = "Format d'email invalide") String email, @NotBlank @Pattern(regexp = "^(?=.*[A-Z])(?=.*[0-9]).{8,}$", message = "Le mot de passe doit contenir au moins 8 caractères, une majuscule et un chiffre") String password ) {} ``` Les messages de validation peuvent être externalisés dans des fichiers de messages pour l'internationalisation. ### 12. Comment gérer les exceptions globalement avec @ControllerAdvice ? Un gestionnaire d'exceptions centralisé permet de standardiser les réponses d'erreur et d'éviter la duplication de code. ```java // GlobalExceptionHandler.java @RestControllerAdvice public class GlobalExceptionHandler { private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class); // Gestion des erreurs de validation @ExceptionHandler(MethodArgumentNotValidException.class) @ResponseStatus(HttpStatus.BAD_REQUEST) public ErrorResponse handleValidationErrors(MethodArgumentNotValidException ex) { Map errors = ex.getBindingResult() .getFieldErrors() .stream() .collect(Collectors.toMap( FieldError::getField, FieldError::getDefaultMessage, (existing, replacement) -> existing )); return new ErrorResponse("VALIDATION_ERROR", "Données invalides", errors); } // Gestion des ressources non trouvées @ExceptionHandler(ResourceNotFoundException.class) @ResponseStatus(HttpStatus.NOT_FOUND) public ErrorResponse handleNotFound(ResourceNotFoundException ex) { return new ErrorResponse("NOT_FOUND", ex.getMessage(), null); } // Gestion des erreurs inattendues @ExceptionHandler(Exception.class) @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) public ErrorResponse handleUnexpectedError(Exception ex) { log.error("Erreur inattendue", ex); return new ErrorResponse("INTERNAL_ERROR", "Une erreur est survenue", null); } } ``` ```java // ErrorResponse.java public record ErrorResponse( String code, String message, Map details, Instant timestamp ) { public ErrorResponse(String code, String message, Map details) { this(code, message, details, Instant.now()); } } ``` Cette approche garantit que toutes les erreurs suivent le même format, facilitant le traitement côté client. ### 13. Quelle est la différence entre @RequestParam, @PathVariable et @RequestBody ? Ces trois annotations extraient des données de différentes parties de la requête HTTP. ```java // ProductController.java @RestController @RequestMapping("/api/products") public class ProductController { // @PathVariable : extrait des valeurs de l'URL // GET /api/products/123 @GetMapping("/{id}") public Product getProduct(@PathVariable Long id) { return productService.findById(id); } // @RequestParam : extrait des query parameters // GET /api/products?category=electronics&minPrice=100&page=0 @GetMapping public Page searchProducts( @RequestParam(required = false) String category, @RequestParam(defaultValue = "0") BigDecimal minPrice, @RequestParam(defaultValue = "0") int page ) { return productService.search(category, minPrice, PageRequest.of(page, 20)); } // @RequestBody : désérialise le corps JSON de la requête // POST /api/products avec {"name": "...", "price": ...} @PostMapping public Product createProduct(@Valid @RequestBody CreateProductRequest request) { return productService.create(request); } // Combinaison des trois dans une même méthode // PUT /api/products/123?notify=true avec corps JSON @PutMapping("/{id}") public Product updateProduct( @PathVariable Long id, @RequestParam(defaultValue = "false") boolean notify, @Valid @RequestBody UpdateProductRequest request ) { Product updated = productService.update(id, request); if (notify) { notificationService.sendUpdateNotification(updated); } return updated; } } ``` `@PathVariable` identifie une ressource spécifique, `@RequestParam` filtre ou pagine les résultats, et `@RequestBody` transporte les données complexes. ### 14. Comment implémenter le versioning d'API ? Plusieurs stratégies existent pour versionner une API REST, chacune avec ses avantages. ```java // Version par URL (recommandé pour sa clarté) @RestController @RequestMapping("/api/v1/users") public class UserControllerV1 { @GetMapping("/{id}") public UserV1Response getUser(@PathVariable Long id) { return userService.findByIdV1(id); } } @RestController @RequestMapping("/api/v2/users") public class UserControllerV2 { @GetMapping("/{id}") public UserV2Response getUser(@PathVariable Long id) { // V2 inclut des champs supplémentaires return userService.findByIdV2(id); } } ``` ```java // Version par header personnalisé @RestController @RequestMapping("/api/users") public class UserController { @GetMapping(value = "/{id}", headers = "X-API-Version=1") public UserV1Response getUserV1(@PathVariable Long id) { return userService.findByIdV1(id); } @GetMapping(value = "/{id}", headers = "X-API-Version=2") public UserV2Response getUserV2(@PathVariable Long id) { return userService.findByIdV2(id); } } ``` ```java // Version par media type (content negotiation) @RestController @RequestMapping("/api/users") public class UserMediaTypeController { @GetMapping(value = "/{id}", produces = "application/vnd.myapi.v1+json") public UserV1Response getUserV1(@PathVariable Long id) { return userService.findByIdV1(id); } @GetMapping(value = "/{id}", produces = "application/vnd.myapi.v2+json") public UserV2Response getUserV2(@PathVariable Long id) { return userService.findByIdV2(id); } } ``` Le versioning par URL reste le plus populaire pour sa simplicité et sa visibilité dans les logs et la documentation. ### 15. Comment configurer CORS dans Spring Boot ? CORS (Cross-Origin Resource Sharing) contrôle les requêtes provenant d'origines différentes. Plusieurs niveaux de configuration sont disponibles. ```java // CorsConfig.java - Configuration globale @Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/api/**") .allowedOrigins("https://frontend.example.com", "https://admin.example.com") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") .exposedHeaders("X-Total-Count", "X-Page-Count") .allowCredentials(true) .maxAge(3600); // Cache preflight pendant 1 heure } } ``` ```java // Configuration par contrôleur ou méthode @RestController @RequestMapping("/api/public") @CrossOrigin(origins = "*", maxAge = 3600) public class PublicApiController { @GetMapping("/data") public DataResponse getPublicData() { return dataService.getPublicData(); } // Override au niveau de la méthode @CrossOrigin(origins = "https://specific-client.com") @PostMapping("/submit") public SubmitResponse submitData(@RequestBody SubmitRequest request) { return dataService.submit(request); } } ``` En production, limitez les origines autorisées et utilisez HTTPS. Ne jamais utiliser `*` avec `allowCredentials(true)`. ## Sécurité Spring ### 16. Comment configurer Spring Security avec JWT ? Spring Security 6+ utilise une approche fonctionnelle pour la configuration de sécurité. ```java // SecurityConfig.java @Configuration @EnableWebSecurity public class SecurityConfig { private final JwtAuthenticationFilter jwtFilter; @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { return http .csrf(csrf -> csrf.disable()) // Désactivé pour API stateless .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS) ) .authorizeHttpRequests(auth -> auth .requestMatchers("/api/auth/**").permitAll() .requestMatchers("/api/public/**").permitAll() .requestMatchers("/api/admin/**").hasRole("ADMIN") .requestMatchers("/api/**").authenticated() ) .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class) .build(); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } } ``` ```java // JwtAuthenticationFilter.java @Component public class JwtAuthenticationFilter extends OncePerRequestFilter { private final JwtService jwtService; private final UserDetailsService userDetailsService; @Override protected void doFilterInternal( HttpServletRequest request, HttpServletResponse response, FilterChain chain ) throws ServletException, IOException { String authHeader = request.getHeader("Authorization"); if (authHeader == null || !authHeader.startsWith("Bearer ")) { chain.doFilter(request, response); return; } String jwt = authHeader.substring(7); String username = jwtService.extractUsername(jwt); if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { UserDetails userDetails = userDetailsService.loadUserByUsername(username); if (jwtService.isTokenValid(jwt, userDetails)) { UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken( userDetails, null, userDetails.getAuthorities() ); authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authToken); } } chain.doFilter(request, response); } } ``` Cette configuration crée une API stateless où chaque requête doit inclure un token JWT valide dans le header Authorization. ### 17. Comment sécuriser les méthodes avec @PreAuthorize et @PostAuthorize ? La sécurité au niveau des méthodes offre un contrôle granulaire basé sur les rôles, les permissions ou les données. ```java // UserService.java @Service @PreAuthorize("isAuthenticated()") // Toutes les méthodes requièrent l'authentification public class UserService { // Seuls les ADMIN peuvent lister tous les utilisateurs @PreAuthorize("hasRole('ADMIN')") public List findAll() { return userRepository.findAll(); } // L'utilisateur peut voir son profil OU les admins peuvent voir tout profil @PreAuthorize("hasRole('ADMIN') or #id == authentication.principal.id") public User findById(Long id) { return userRepository.findById(id) .orElseThrow(() -> new UserNotFoundException(id)); } // Vérification après exécution : l'utilisateur ne peut voir que ses propres données @PostAuthorize("returnObject.owner.id == authentication.principal.id or hasRole('ADMIN')") public Document getDocument(Long documentId) { return documentRepository.findById(documentId) .orElseThrow(() -> new DocumentNotFoundException(documentId)); } // Filtrage de collection : ne retourne que les éléments autorisés @PostFilter("filterObject.owner.id == authentication.principal.id") public List getUserProjects() { return projectRepository.findAll(); } } ``` ```java // SecurityConfig.java - Activation @Configuration @EnableMethodSecurity(prePostEnabled = true) public class MethodSecurityConfig { // Configuration additionnelle si nécessaire } ``` `@PreAuthorize` vérifie avant l'exécution, `@PostAuthorize` après. Les expressions SpEL permettent des règles complexes. ### 18. Comment protéger les endpoints contre les attaques CSRF ? CSRF (Cross-Site Request Forgery) exploite la session d'un utilisateur authentifié. La protection est activée par défaut pour les applications avec session. ```java // SecurityConfig.java @Configuration @EnableWebSecurity public class SecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { return http // Configuration CSRF pour applications avec session .csrf(csrf -> csrf .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) .csrfTokenRequestHandler(new CsrfTokenRequestAttributeHandler()) // Exclure certains endpoints de la protection CSRF .ignoringRequestMatchers("/api/webhooks/**") ) .build(); } } ``` ```java // CsrfController.java - Endpoint pour récupérer le token (SPA) @RestController public class CsrfController { @GetMapping("/api/csrf") public CsrfToken csrf(CsrfToken token) { // Retourne le token CSRF au frontend return token; } } ``` Pour les API REST stateless avec JWT, CSRF peut être désactivé car il n'y a pas de cookie de session à exploiter. La protection est essentielle pour les applications traditionnelles avec session. > **API stateless vs applications avec session** > > Les API JWT sont naturellement protégées contre CSRF car le token doit être explicitement inclus dans chaque requête. Les applications avec cookies de session nécessitent une protection CSRF active. ## Tests Spring Boot ### 19. Quelle est la différence entre @SpringBootTest et @WebMvcTest ? Ces annotations configurent le contexte de test différemment selon les besoins. ```java // UserControllerIntegrationTest.java @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) class UserControllerIntegrationTest { // Charge le contexte complet : tous les beans, base de données, etc. @Autowired private TestRestTemplate restTemplate; @Test void shouldCreateUser() { CreateUserRequest request = new CreateUserRequest("John", "john@example.com"); ResponseEntity response = restTemplate.postForEntity( "/api/users", request, User.class ); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED); assertThat(response.getBody().getName()).isEqualTo("John"); } } ``` ```java // UserControllerUnitTest.java @WebMvcTest(UserController.class) // Charge uniquement le layer web class UserControllerUnitTest { @Autowired private MockMvc mockMvc; @MockitoBean // Remplace @MockBean (déprécié) private UserService userService; @Test void shouldReturnUser() throws Exception { // Configuration du mock when(userService.findById(1L)) .thenReturn(Optional.of(new User(1L, "John", "john@example.com"))); mockMvc.perform(get("/api/users/1")) .andExpect(status().isOk()) .andExpect(jsonPath("$.name").value("John")) .andExpect(jsonPath("$.email").value("john@example.com")); } @Test void shouldReturn404WhenUserNotFound() throws Exception { when(userService.findById(999L)).thenReturn(Optional.empty()); mockMvc.perform(get("/api/users/999")) .andExpect(status().isNotFound()); } } ``` `@WebMvcTest` est plus rapide car il ne charge que le contrôleur spécifié et ses dépendances directes. `@SpringBootTest` est nécessaire pour les tests d'intégration complets. ### 20. Comment tester les repositories avec @DataJpaTest ? Cette annotation configure un contexte minimal pour tester la couche de persistance avec une base embarquée. ```java // UserRepositoryTest.java @DataJpaTest @AutoConfigureTestDatabase(replace = Replace.NONE) // Utiliser Testcontainers class UserRepositoryTest { @Autowired private UserRepository userRepository; @Autowired private TestEntityManager entityManager; @Test void shouldFindByEmail() { // Arrange : créer et persister une entité User user = new User("John", "john@example.com"); entityManager.persistAndFlush(user); entityManager.clear(); // Vider le cache L1 // Act Optional found = userRepository.findByEmail("john@example.com"); // Assert assertThat(found).isPresent(); assertThat(found.get().getName()).isEqualTo("John"); } @Test void shouldFindActiveUsersWithOrders() { // Test d'une requête complexe avec jointures User activeUser = entityManager.persistAndFlush(new User("Active", "active@test.com", true)); User inactiveUser = entityManager.persistAndFlush(new User("Inactive", "inactive@test.com", false)); entityManager.persistAndFlush(new Order(activeUser, BigDecimal.valueOf(100))); List result = userRepository.findActiveUsersWithOrders(); assertThat(result).hasSize(1); assertThat(result.get(0).getEmail()).isEqualTo("active@test.com"); } } ``` `TestEntityManager` offre des méthodes utilitaires pour les tests JPA, notamment `persistAndFlush()` qui garantit l'écriture immédiate en base. ### 21. Comment utiliser Testcontainers pour les tests d'intégration ? Testcontainers lance des conteneurs Docker pour des tests avec de vraies bases de données ou services. ```java // IntegrationTestBase.java @SpringBootTest @Testcontainers abstract class IntegrationTestBase { @Container @ServiceConnection static PostgreSQLContainer postgres = new PostgreSQLContainer<>("postgres:16") .withDatabaseName("testdb") .withUsername("test") .withPassword("test"); @Container @ServiceConnection static RedisContainer redis = new RedisContainer("redis:7"); } ``` ```java // OrderServiceIntegrationTest.java class OrderServiceIntegrationTest extends IntegrationTestBase { @Autowired private OrderService orderService; @Autowired private OrderRepository orderRepository; @Test void shouldProcessOrderWithRealDatabase() { // Créer une commande Order order = orderService.createOrder( new CreateOrderRequest(1L, List.of( new OrderItem(101L, 2), new OrderItem(102L, 1) )) ); // Vérifier en base Order saved = orderRepository.findById(order.getId()).orElseThrow(); assertThat(saved.getItems()).hasSize(2); assertThat(saved.getStatus()).isEqualTo(OrderStatus.PENDING); } @Test void shouldCacheOrderInRedis() { Order order = orderService.createOrder(createSampleOrderRequest()); // Premier appel : base de données Order fetched1 = orderService.findById(order.getId()); // Deuxième appel : cache Redis Order fetched2 = orderService.findById(order.getId()); assertThat(fetched1).isEqualTo(fetched2); // Vérifier les métriques de cache si nécessaire } } ``` L'annotation `@ServiceConnection` configure automatiquement les propriétés de connexion, éliminant le besoin de `@DynamicPropertySource`. ## Configuration et Monitoring ### 22. Comment externaliser la configuration avec @ConfigurationProperties ? Cette annotation permet de mapper des propriétés vers des objets Java typés avec validation. ```java // MailProperties.java @ConfigurationProperties(prefix = "app.mail") @Validated public record MailProperties( @NotBlank String host, @Min(1) @Max(65535) int port, @NotBlank String username, @NotBlank String password, @Valid SenderConfig sender, @Valid RetryConfig retry ) { public record SenderConfig( @NotBlank @Email String address, @NotBlank String name ) {} public record RetryConfig( @Min(1) int maxAttempts, @Min(100) long delayMs ) { public RetryConfig { // Valeurs par défaut if (maxAttempts == 0) maxAttempts = 3; if (delayMs == 0) delayMs = 1000; } } } ``` ```yaml # application.yml app: mail: host: smtp.example.com port: 587 username: ${MAIL_USERNAME} password: ${MAIL_PASSWORD} sender: address: noreply@example.com name: MyApp Notifications retry: max-attempts: 3 delay-ms: 1000 ``` ```java // MailConfig.java @Configuration @EnableConfigurationProperties(MailProperties.class) public class MailConfig { @Bean public JavaMailSender mailSender(MailProperties props) { JavaMailSenderImpl sender = new JavaMailSenderImpl(); sender.setHost(props.host()); sender.setPort(props.port()); sender.setUsername(props.username()); sender.setPassword(props.password()); return sender; } } ``` Les records Java (depuis Java 16) sont particulièrement adaptés aux `@ConfigurationProperties` car ils sont immuables et concis. ### 23. Comment utiliser les profils Spring pour différents environnements ? Les profils permettent d'adapter la configuration selon l'environnement d'exécution. ```yaml # application.yml - Configuration par défaut spring: profiles: active: ${SPRING_PROFILES_ACTIVE:dev} datasource: url: ${DATABASE_URL:jdbc:h2:mem:testdb} --- # application-dev.yml spring: config: activate: on-profile: dev datasource: url: jdbc:postgresql://localhost:5432/myapp_dev jpa: show-sql: true hibernate: ddl-auto: update logging: level: com.example: DEBUG --- # application-prod.yml spring: config: activate: on-profile: prod datasource: url: ${DATABASE_URL} hikari: maximum-pool-size: 20 jpa: show-sql: false hibernate: ddl-auto: validate logging: level: com.example: INFO ``` ```java // DevDataInitializer.java @Component @Profile("dev") // Uniquement actif en développement public class DevDataInitializer implements CommandLineRunner { private final UserRepository userRepository; @Override public void run(String... args) { // Créer des données de test userRepository.save(new User("dev@example.com", "Dev User", "password")); userRepository.save(new User("test@example.com", "Test User", "password")); } } ``` ```java // Service avec comportement conditionnel @Service public class NotificationService { @Value("${app.notifications.enabled:true}") private boolean notificationsEnabled; @Autowired(required = false) // Optionnel selon le profil private EmailService emailService; public void sendNotification(String message) { if (notificationsEnabled && emailService != null) { emailService.send(message); } else { log.info("Notification (disabled): {}", message); } } } ``` Les profils peuvent être combinés : `spring.profiles.active=prod,metrics` active les deux profils simultanément. ### 24. Comment exposer des métriques personnalisées avec Actuator et Micrometer ? Micrometer fournit une façade pour les systèmes de monitoring. Les métriques personnalisées enrichissent l'observabilité. ```java // OrderMetrics.java @Component public class OrderMetrics { private final Counter ordersCreated; private final Counter ordersFailed; private final Timer orderProcessingTime; private final AtomicInteger activeOrders; public OrderMetrics(MeterRegistry registry) { // Compteur pour les commandes créées par statut this.ordersCreated = Counter.builder("orders.created") .description("Nombre de commandes créées") .tag("application", "order-service") .register(registry); this.ordersFailed = Counter.builder("orders.failed") .description("Nombre de commandes échouées") .register(registry); // Timer pour mesurer le temps de traitement this.orderProcessingTime = Timer.builder("orders.processing.time") .description("Temps de traitement des commandes") .publishPercentiles(0.5, 0.95, 0.99) .register(registry); // Gauge pour le nombre de commandes actives this.activeOrders = new AtomicInteger(0); Gauge.builder("orders.active", activeOrders, AtomicInteger::get) .description("Nombre de commandes en cours de traitement") .register(registry); } public void recordOrderCreated() { ordersCreated.increment(); } public void recordOrderFailed() { ordersFailed.increment(); } public Timer.Sample startProcessing() { activeOrders.incrementAndGet(); return Timer.start(); } public void stopProcessing(Timer.Sample sample) { sample.stop(orderProcessingTime); activeOrders.decrementAndGet(); } } ``` ```java // OrderService.java @Service public class OrderService { private final OrderMetrics metrics; public Order processOrder(CreateOrderRequest request) { Timer.Sample sample = metrics.startProcessing(); try { Order order = createOrder(request); metrics.recordOrderCreated(); return order; } catch (Exception e) { metrics.recordOrderFailed(); throw e; } finally { metrics.stopProcessing(sample); } } } ``` Ces métriques sont exposées via `/actuator/prometheus` pour Prometheus ou `/actuator/metrics` pour l'API JSON. ### 25. Comment créer un HealthIndicator personnalisé ? Les indicateurs de santé permettent de monitorer l'état de composants critiques. ```java // ExternalApiHealthIndicator.java @Component public class ExternalApiHealthIndicator implements HealthIndicator { private final RestClient restClient; private final ExternalApiProperties properties; @Override public Health health() { try { long startTime = System.currentTimeMillis(); ResponseEntity response = restClient.get() .uri(properties.getHealthEndpoint()) .retrieve() .toBodilessEntity(); long responseTime = System.currentTimeMillis() - startTime; if (response.getStatusCode().is2xxSuccessful()) { return Health.up() .withDetail("responseTime", responseTime + "ms") .withDetail("endpoint", properties.getHealthEndpoint()) .build(); } else { return Health.down() .withDetail("status", response.getStatusCode().value()) .build(); } } catch (Exception e) { return Health.down() .withException(e) .withDetail("endpoint", properties.getHealthEndpoint()) .build(); } } } ``` ```java // DatabaseHealthContributor.java - Health composite @Component public class DatabaseHealthContributor implements CompositeHealthContributor { private final Map indicators; public DatabaseHealthContributor( DataSource primaryDataSource, @Qualifier("replicaDataSource") DataSource replicaDataSource ) { this.indicators = Map.of( "primary", new DataSourceHealthIndicator(primaryDataSource), "replica", new DataSourceHealthIndicator(replicaDataSource) ); } @Override public HealthContributor getContributor(String name) { return indicators.get(name); } @Override public Iterator> iterator() { return indicators.entrySet().stream() .map(e -> NamedContributor.of(e.getKey(), e.getValue())) .iterator(); } } ``` Le endpoint `/actuator/health` agrège tous les indicateurs. La configuration `management.endpoint.health.show-details=always` expose les détails. ## Concepts Avancés ### 26. Comment implémenter un cache avec Spring Cache et Redis ? L'abstraction Spring Cache simplifie l'intégration de caches distribués comme Redis. ```java // CacheConfig.java @Configuration @EnableCaching public class CacheConfig { @Bean public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) { RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig() .entryTtl(Duration.ofMinutes(10)) .serializeKeysWith(SerializationPair.fromSerializer(new StringRedisSerializer())) .serializeValuesWith(SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer())); // Configurations spécifiques par cache Map cacheConfigs = Map.of( "users", config.entryTtl(Duration.ofHours(1)), "products", config.entryTtl(Duration.ofMinutes(30)), "sessions", config.entryTtl(Duration.ofMinutes(5)) ); return RedisCacheManager.builder(connectionFactory) .cacheDefaults(config) .withInitialCacheConfigurations(cacheConfigs) .build(); } } ``` ```java // ProductService.java @Service public class ProductService { // Mise en cache automatique du résultat @Cacheable(value = "products", key = "#id") public Product findById(Long id) { log.info("Fetching product {} from database", id); return productRepository.findById(id) .orElseThrow(() -> new ProductNotFoundException(id)); } // Mise à jour du cache lors de la modification @CachePut(value = "products", key = "#product.id") public Product update(Product product) { return productRepository.save(product); } // Invalidation du cache @CacheEvict(value = "products", key = "#id") public void delete(Long id) { productRepository.deleteById(id); } // Invalidation de tout le cache @CacheEvict(value = "products", allEntries = true) public void clearProductCache() { log.info("Product cache cleared"); } // Clé composite pour les recherches @Cacheable(value = "productSearch", key = "#category + '-' + #minPrice + '-' + #maxPrice") public List search(String category, BigDecimal minPrice, BigDecimal maxPrice) { return productRepository.findByCategoryAndPriceBetween(category, minPrice, maxPrice); } } ``` Les annotations de cache sont transparentes : le code métier reste inchangé et le caching est déclaratif. ### 27. Comment implémenter une tâche planifiée avec @Scheduled ? Spring propose plusieurs façons de planifier des tâches récurrentes. ```java // SchedulerConfig.java @Configuration @EnableScheduling public class SchedulerConfig { @Bean public TaskScheduler taskScheduler() { ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); scheduler.setPoolSize(5); scheduler.setThreadNamePrefix("scheduled-"); scheduler.setErrorHandler(t -> log.error("Scheduled task error", t)); return scheduler; } } ``` ```java // ScheduledTasks.java @Component public class ScheduledTasks { private static final Logger log = LoggerFactory.getLogger(ScheduledTasks.class); // Exécution toutes les 5 minutes @Scheduled(fixedRate = 5 * 60 * 1000) public void syncExternalData() { log.info("Starting external data sync"); // Synchronisation avec un système externe } // Exécution 10 secondes après la fin de la précédente @Scheduled(fixedDelay = 10000, initialDelay = 5000) public void processQueue() { // Traitement de queue avec délai entre exécutions } // Expression cron : tous les jours à 2h du matin @Scheduled(cron = "0 0 2 * * *", zone = "Europe/Paris") public void dailyCleanup() { log.info("Running daily cleanup"); cleanupService.removeExpiredSessions(); cleanupService.archiveOldData(); } // Cron configurable via propriétés @Scheduled(cron = "${app.reports.cron:0 0 6 * * MON}") public void generateWeeklyReport() { reportService.generateAndSend(); } } ``` ```java // ConditionalScheduling.java - Activation conditionnelle @Component @ConditionalOnProperty(name = "app.scheduling.enabled", havingValue = "true") public class ConditionalScheduledTasks { @Scheduled(fixedRate = 60000) public void monitorSystem() { // Monitoring actif uniquement si configuré } } ``` Les tâches planifiées s'exécutent sur un thread pool séparé pour ne pas bloquer le traitement des requêtes. ### 28. Comment gérer les événements avec ApplicationEvent ? Le système d'événements permet un découplage entre les composants de l'application. ```java // UserRegisteredEvent.java public class UserRegisteredEvent extends ApplicationEvent { private final User user; private final Instant registeredAt; public UserRegisteredEvent(Object source, User user) { super(source); this.user = user; this.registeredAt = Instant.now(); } public User getUser() { return user; } public Instant getRegisteredAt() { return registeredAt; } } ``` ```java // UserService.java - Publication d'événements @Service public class UserService { private final ApplicationEventPublisher eventPublisher; @Transactional public User registerUser(CreateUserRequest request) { User user = new User(request.email(), request.name()); user = userRepository.save(user); // Publication de l'événement après la transaction eventPublisher.publishEvent(new UserRegisteredEvent(this, user)); return user; } } ``` ```java // UserEventListeners.java - Écouteurs d'événements @Component public class UserEventListeners { private static final Logger log = LoggerFactory.getLogger(UserEventListeners.class); // Listener synchrone @EventListener public void handleUserRegistered(UserRegisteredEvent event) { log.info("User registered: {}", event.getUser().getEmail()); } // Listener asynchrone pour ne pas bloquer le thread principal @Async @EventListener public void sendWelcomeEmail(UserRegisteredEvent event) { emailService.sendWelcomeEmail(event.getUser()); } // Listener transactionnel : s'exécute après le commit @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) public void notifyExternalSystem(UserRegisteredEvent event) { // Notification externe uniquement si la transaction réussit externalApiClient.notifyNewUser(event.getUser().getId()); } // Listener conditionnel avec SpEL @EventListener(condition = "#event.user.role == 'PREMIUM'") public void handlePremiumUserRegistered(UserRegisteredEvent event) { premiumService.initializePremiumFeatures(event.getUser()); } } ``` Les événements transactionnels (`@TransactionalEventListener`) garantissent la cohérence entre la base de données et les effets de bord. ### 29. Comment implémenter un client HTTP avec RestClient (Spring Boot 3+) ? `RestClient` est l'API moderne et fluide pour les appels HTTP synchrones, remplaçant `RestTemplate`. ```java // ExternalApiClient.java @Component public class ExternalApiClient { private final RestClient restClient; public ExternalApiClient(RestClient.Builder builder, ExternalApiProperties props) { this.restClient = builder .baseUrl(props.getBaseUrl()) .defaultHeader("Authorization", "Bearer " + props.getApiKey()) .defaultHeader("Accept", "application/json") .requestInterceptor((request, body, execution) -> { log.debug("Request: {} {}", request.getMethod(), request.getURI()); return execution.execute(request, body); }) .build(); } public User fetchUser(Long id) { return restClient.get() .uri("/users/{id}", id) .retrieve() .body(User.class); } public List searchProducts(String query, int page) { return restClient.get() .uri(uriBuilder -> uriBuilder .path("/products/search") .queryParam("q", query) .queryParam("page", page) .build()) .retrieve() .body(new ParameterizedTypeReference>() {}); } public Order createOrder(CreateOrderRequest request) { return restClient.post() .uri("/orders") .contentType(MediaType.APPLICATION_JSON) .body(request) .retrieve() .onStatus(HttpStatusCode::is4xxClientError, (req, res) -> { throw new OrderValidationException("Invalid order: " + res.getStatusCode()); }) .body(Order.class); } // Gestion des erreurs avec ResponseEntity public Optional findUser(Long id) { ResponseEntity response = restClient.get() .uri("/users/{id}", id) .retrieve() .toEntity(User.class); return response.getStatusCode().is2xxSuccessful() ? Optional.ofNullable(response.getBody()) : Optional.empty(); } } ``` `RestClient` offre une API similaire à `WebClient` mais pour les appels bloquants, idéale pour les applications qui n'utilisent pas la programmation réactive. ### 30. Comment gérer les migrations de base de données avec Flyway ? Flyway automatise les migrations de schéma de manière versionnée et reproductible. ```properties # application.properties spring.flyway.enabled=true spring.flyway.locations=classpath:db/migration spring.flyway.baseline-on-migrate=true spring.flyway.validate-on-migrate=true ``` ```sql -- db/migration/V1__create_users_table.sql CREATE TABLE users ( id BIGSERIAL PRIMARY KEY, email VARCHAR(255) NOT NULL UNIQUE, name VARCHAR(255) NOT NULL, password_hash VARCHAR(255) NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX idx_users_email ON users(email); ``` ```sql -- db/migration/V2__add_user_status.sql ALTER TABLE users ADD COLUMN status VARCHAR(20) DEFAULT 'ACTIVE'; CREATE INDEX idx_users_status ON users(status); ``` ```sql -- db/migration/V3__create_orders_table.sql CREATE TABLE orders ( id BIGSERIAL PRIMARY KEY, user_id BIGINT NOT NULL REFERENCES users(id), total_amount DECIMAL(10, 2) NOT NULL, status VARCHAR(20) NOT NULL DEFAULT 'PENDING', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX idx_orders_user_id ON orders(user_id); CREATE INDEX idx_orders_status ON orders(status); ``` ```java // FlywayConfig.java - Configuration avancée @Configuration public class FlywayConfig { @Bean public FlywayMigrationStrategy migrationStrategy() { return flyway -> { // Validation avant migration flyway.validate(); // Exécution des migrations flyway.migrate(); }; } // Callback pour actions post-migration @Bean public Callback flywayCallback() { return new BaseCallback() { @Override public void handle(Event event, Context context) { if (event == Event.AFTER_MIGRATE) { log.info("Migrations completed successfully"); } } }; } } ``` Chaque fichier de migration est exécuté une seule fois et son checksum est vérifié pour détecter les modifications accidentelles. ## Conclusion Ces 30 questions couvrent les aspects essentiels de Spring Boot évalués en entretien technique. La maîtrise de ces concepts démontre une compréhension approfondie du framework et des bonnes pratiques de développement Java. **Checklist de préparation :** - ✅ Comprendre le mécanisme d'auto-configuration et les conditions - ✅ Maîtriser Spring Data JPA : requêtes, transactions, problème N+1 - ✅ Savoir configurer Spring Security avec JWT et la sécurité des méthodes - ✅ Connaître les différentes annotations de test et leurs cas d'usage - ✅ Utiliser les profils et @ConfigurationProperties pour la configuration - ✅ Implémenter le caching, le scheduling et les événements - ✅ Exposer des métriques et des indicateurs de santé personnalisés - ✅ Maîtriser RestClient pour les appels HTTP modernes La pratique régulière avec des projets concrets reste le meilleur moyen de consolider ces connaissances et de répondre avec assurance aux questions techniques. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/fr/blog/spring-boot/30-questions-entretien-spring-boot