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.

30 questions d'entretien Spring Boot pour développeurs Java

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.

Application.javajava
// 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.

CustomAutoConfiguration.javajava
@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
<!-- pom.xml d'un starter personnalisé -->
<project>
    <artifactId>my-company-starter</artifactId>
    <dependencies>
        <!-- Dépendance Spring Boot de base -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <!-- Bibliothèques spécifiques à l'entreprise -->
        <dependency>
            <groupId>com.mycompany</groupId>
            <artifactId>logging-utils</artifactId>
        </dependency>
    </dependencies>
</project>

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.

DemoApplication.javajava
// @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.

UserRepository.javajava
// CrudRepository : opérations CRUD de base
public interface UserCrudRepository extends CrudRepository<User, Long> {
    // save(), findById(), findAll(), deleteById(), count(), existsById()
}

// PagingAndSortingRepository : ajoute pagination et tri
public interface UserPagingRepository extends PagingAndSortingRepository<User, Long> {
    // Hérite de CrudRepository + findAll(Sort), findAll(Pageable)
}

// JpaRepository : fonctionnalités JPA complètes
public interface UserRepository extends JpaRepository<User, Long> {
    // Hérite des précédents + flush(), saveAndFlush(), deleteInBatch()

    // Méthodes dérivées du nom
    List<User> findByEmailContaining(String email);

    Optional<User> 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.

OrderRepository.javajava
public interface OrderRepository extends JpaRepository<Order, Long> {

    // JPQL avec paramètres nommés
    @Query("SELECT o FROM Order o WHERE o.customer.id = :customerId AND o.status = :status")
    List<Order> 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<Order> 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<Long> 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.

PaymentService.javajava
@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.

User.javajava
@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<Role> roles;

    // Lazy : les commandes ne sont chargées qu'à l'accès
    @OneToMany(mappedBy = "user", fetch = FetchType.LAZY)
    private List<Order> orders;
}
UserService.javajava
@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<OrderDTO> 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é.

ArticleRepository.javajava
public interface ArticleRepository extends JpaRepository<Article, Long> {

    // PROBLÈME : cette requête génère N+1 requêtes
    List<Article> findAll();

    // SOLUTION 1 : JOIN FETCH dans JPQL
    @Query("SELECT a FROM Article a JOIN FETCH a.author JOIN FETCH a.comments")
    List<Article> findAllWithDetails();

    // SOLUTION 2 : Entity Graph
    @EntityGraph(attributePaths = {"author", "comments"})
    @Query("SELECT a FROM Article a")
    List<Article> findAllWithGraph();
}
Article.javajava
@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<Comment> 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.

Prêt à réussir tes entretiens Spring Boot ?

Entraîne-toi avec nos simulateurs interactifs, fiches express et tests techniques.

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.

UserController.javajava
@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<UserResponse> listUsers(
        @RequestParam(defaultValue = "0") @Min(0) int page,
        @RequestParam(defaultValue = "20") @Max(100) int size
    ) {
        return userService.findAll(PageRequest.of(page, size));
    }
}
CreateUserRequest.javajava
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.

GlobalExceptionHandler.javajava
@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<String, String> 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);
    }
}
ErrorResponse.javajava
public record ErrorResponse(
    String code,
    String message,
    Map<String, String> details,
    Instant timestamp
) {
    public ErrorResponse(String code, String message, Map<String, String> 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.

ProductController.javajava
@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<Product> 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.

CorsConfig.java - Configuration globalejava
@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é.

SecurityConfig.javajava
@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();
    }
}
JwtAuthenticationFilter.javajava
@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.

UserService.javajava
@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<User> 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<Project> getUserProjects() {
        return projectRepository.findAll();
    }
}
SecurityConfig.java - Activationjava
@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.

SecurityConfig.javajava
@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();
    }
}
CsrfController.java - Endpoint pour récupérer le token (SPA)java
@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.

UserControllerIntegrationTest.javajava
@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<User> response = restTemplate.postForEntity(
            "/api/users", request, User.class
        );

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED);
        assertThat(response.getBody().getName()).isEqualTo("John");
    }
}
UserControllerUnitTest.javajava
@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.

UserRepositoryTest.javajava
@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<User> 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<User> 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.

IntegrationTestBase.javajava
@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");
}
OrderServiceIntegrationTest.javajava
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.

Prêt à réussir tes entretiens Spring Boot ?

Entraîne-toi avec nos simulateurs interactifs, fiches express et tests techniques.

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.

MailProperties.javajava
@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
MailConfig.javajava
@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
DevDataInitializer.javajava
@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é.

OrderMetrics.javajava
@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();
    }
}
OrderService.javajava
@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.

ExternalApiHealthIndicator.javajava
@Component
public class ExternalApiHealthIndicator implements HealthIndicator {

    private final RestClient restClient;
    private final ExternalApiProperties properties;

    @Override
    public Health health() {
        try {
            long startTime = System.currentTimeMillis();

            ResponseEntity<Void> 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();
        }
    }
}
DatabaseHealthContributor.java - Health compositejava
@Component
public class DatabaseHealthContributor implements CompositeHealthContributor {

    private final Map<String, HealthIndicator> 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<NamedContributor<HealthContributor>> 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.

CacheConfig.javajava
@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<String, RedisCacheConfiguration> 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();
    }
}
ProductService.javajava
@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<Product> 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.

SchedulerConfig.javajava
@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;
    }
}
ScheduledTasks.javajava
@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();
    }
}
ConditionalScheduling.java - Activation conditionnellejava
@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.

UserRegisteredEvent.javajava
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; }
}
UserService.java - Publication d'événementsjava
@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;
    }
}
UserEventListeners.java - Écouteurs d'événementsjava
@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.

ExternalApiClient.javajava
@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<Product> 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<List<Product>>() {});
    }

    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<User> findUser(Long id) {
        ResponseEntity<User> 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);
FlywayConfig.java - Configuration avancéejava
@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.

Passe à la pratique !

Teste tes connaissances avec nos simulateurs d'entretien et tests techniques.

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.

Tags

#spring boot
#java
#entretien
#interview
#backend

Partager

Articles similaires