Skip to Content
BackendSpring Boot

Spring Boot

Cheatsheet opérationnel — annotations, configuration, structure hexagonale et pièges courants.

Annotations essentielles

AnnotationRôleScope par défaut
@RestControllerEndpoint HTTP + sérialisation JSONBean singleton
@ControllerVue (thymeleaf, etc.) — pas de JSON automatiqueSingleton
@ServiceLogique métier (use-case, orchestration)Singleton
@RepositoryCouche accès données (traduit les exceptions JPA)Singleton
@ConfigurationClasse fabrique de beans (contient @Bean)Singleton
@ComponentBean générique, détecté par scan de classpathSingleton
@AutowiredInjection de dépendance (champ, constructeur, setter)
@Value("${prop}")Injection d’une propriété de configuration
@TransactionalDémarre/propage un contrôle de transactionREAD_COMMITTED
@Profile("dev")Active le bean uniquement avec le profil dev
@ConditionalOnPropertyActive le bean si la propriété existe

Règle : préférer l’injection par constructeur. Éviter @Autowired sur un champ.

Structure d’un projet

mon-service/ ├── pom.xml ├── src/ │ ├── main/ │ │ ├── java/com/monapp/ │ │ │ ├── MonAppApplication.java │ │ │ ├── domain/ # Entités, repositories (interfaces) │ │ │ ├── application/ # Use-cases, services │ │ │ ├── infrastructure/ # Implémentation JPA, clients externes │ │ │ └── web/ # Contrôleurs, DTOs, configs web │ │ └── resources/ │ │ ├── application.yml │ │ └── application-dev.yml │ └── test/ │ └── java/com/monapp/ └── mvnw

pom.xml minimal

<project> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.4.1</version> </parent> <groupId>com.monapp</groupId> <artifactId>mon-service</artifactId> <version>0.1.0</version> <properties> <java.version>21</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <!-- Développeur : rechargement à chaud --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional>true</optional> </dependency> </dependencies> </project>

Note : les versions des dépendances transitives sont gérées par spring-boot-starter-parent. Ne pas déclarer de &lt;version&gt; sur un starter fourni par le parent.

Démarrage

// src/main/java/com/monapp/MonAppApplication.java package com.monapp; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class MonAppApplication { public static void main(String[] args) { SpringApplication.run(MonAppApplication.class, args); } }

@SpringBootApplication équivaut à @Configuration + @EnableAutoConfiguration + @ComponentScan.

application.yml — Patterns de configuration

Profil actif et détection

# application.yml (valeur par défaut) spring: profiles: active: ${SPRING_PROFILES_ACTIVE:local} datasource: url: jdbc:postgresql://${DB_HOST:localhost}:${DB_PORT:5432}/${DB_NAME:app} username: ${DB_USER} password: ${DB_PASSWORD} jpa: hibernate: ddl-auto: validate show-sql: false properties: hibernate: format_sql: true default_schema: ${DB_SCHEMA:public} jackson: serialization: write-dates-as-timestamps: false indent-output: false deserialization: fail-on-unknown-properties: false servlet: multipart: max-file-size: 10MB max-request-size: 10MB

Pitfall : ne jamais committer de valeurs réelles dans application.yml. Utiliser des variables d’environnement ou un gestionnaire de secrets (Vault, AWS Secrets Manager) pour les valeurs sensibles.

Activation des profils

# Via variable d'environnement (recommandé en production) export SPRING_PROFILES_ACTIVE=prod # Via argument CLI java -jar mon-service.jar --spring.profiles.active=prod # Via fichier application-prod.yml dédié

Utilisation de variables d’environnement

app: jwt-secret: ${JWT_SECRET} feature-x-enabled: ${FEATURE_X_ENABLED:false}

Règle : les secrets ne doivent jamais être dans un fichier application.yml commité. Toujours utiliser ${VAR} ou classpath:/secrets/ monté comme volume.

Validation (Jakarta)

Activer la validation des DTO :

spring: validation: included-type-annotations: - jakarta.validation.constraints.NotBlank - jakarta.validation.constraints.NotEmpty

Les annotations @NotNull, @Size, @Email… fonctionnent sur les champs DTO dès que spring-boot-starter-validation est dans le pom.xml. Sans ce starter, elles sont ignorées silencieusement.

Sections communes

server: port: 8080 forward-headers-strategy: native spring: jpa: hibernate: ddl-auto: validate # jamais `create` en production properties: hibernate: format_sql: ${SHOW_SQL:false} dialect: org.hibernate.dialect.PostgreSQLDialect management: endpoints: web: exposure: include: health,info,metrics health: liveness: state: enabled: true # sondes Kubernetes readiness: state: enabled: true logging: level: root: INFO com.monapp: DEBUG

Starters courants

StarterCe qu’il apporte
spring-boot-starter-webTomcat intégré, Jackson, WebMvcAutoConfiguration
spring-boot-starter-data-jpaHibernate, Spring Data JPA, HikariCP
spring-boot-starter-securityFiltrage HTTP, UserDetailsService, PasswordEncoder
spring-boot-starter-validationJakarta Validation (constraints @NotNull, @Size…)
spring-boot-starter-actuator/actuator/health, /actuator/metrics
spring-boot-starter-testJUnit 5, Mockito, @SpringBootTest
spring-boot-starter-oauth2-clientSupport OAuth 2.0 / OIDC (login, resource server)

Pattern hexagonal — Exemple concret

Séparation claire : domain (métier pur), application (use-cases), infrastructure (détails techniques).

// --- domain --- // Entité (POJO pur, aucune dépendance framework) public record Produit( UUID id, String nom, String description, BigDecimal prix ) {} // Interface du repository (contrat du domaine) public interface ProduitRepository { Optional<Produit> findById(UUID id); List<Produit> findAll(); Produit save(Produit produit); void deleteById(UUID id); }
// --- application --- @Service @RequiredArgsConstructor public class GestionProduitService { private final ProduitRepository produitRepository; public Produit creer(Produit produit) { if (produit.prix().compareTo(BigDecimal.ZERO) <= 0) { throw new IllegalArgumentException("Le prix doit être positif"); } return produitRepository.save(produit); } public Produit get(UUID id) { return produitRepository.findById(id) .orElseThrow(() -> new RuntimeException("Produit introuvable : " + id)); } }
// --- infrastructure --- // Implémentation JPA (dépend du framework) @Repository public class ProduitJpaRepository implements ProduitRepository { private final JpaProduitEntity jpaRepository; public ProduitJpaRepository(JpaProduitEntity jpaRepository) { this.jpaRepository = jpaRepository; } @Override public Optional<Produit> findById(UUID id) { return jpaRepository.findById(id).map(EntityMapper::toDomain); } @Override public Produit save(Produit produit) { return EntityMapper.toDomain(jpaRepository.save(EntityMapper.toEntity(produit))); } } // Mapper entre l'entité JPA et le POJO domaine public final class EntityMapper { private EntityMapper() {} public static Produit toDomain(JpaProduitEntity entity) { return new Produit( entity.getId(), entity.getNom(), entity.getDescription(), entity.getPrix() ); } public static JpaProduitEntity toEntity(Produit p) { JpaProduitEntity e = new JpaProduitEntity(); e.setId(p.id()); e.setNom(p.nom()); // ... return e; } }
// --- web (couche entrée) --- @RestController @RequestMapping("/api/produits") @RequiredArgsConstructor public class ProduitController { private final GestionProduitService service; @GetMapping("/{id}") public Produit get(@PathVariable UUID id) { return service.get(id); } }

Principe : le code dans domain/ ne doit avoir aucune dépendance vers Spring, Hibernate, Jackson ou tout autre framework. C’est la testabilité qui en profite.

Ports et use-cases (vocabulary)

TermeSignificationEmplacement typique
EntrypointPoint d’entrée depuis l’extérieur (HTTP, file d’attente, batch)web/, messaging/
PortInterface d’entrée (Adapter)infrastructure/
Use-caseCas d’utilisation spécifiqueapplication/
AdaptorImplémentation technique (JPA, REST client, Kafka consumer)infrastructure/
DomaineRègles métier puresdomain/

Exécution et tests

# Lancer l'application ./mvnw spring-boot:run # Lancer avec un profil ./mvnw spring-boot:run -Dspring-boot.run.profiles=dev # Lancer les tests ./mvnw test # Build du JAR ./mvnw package -DskipTests

Test d’intégration

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) class ProduitControllerTest { @Autowired TestRestTemplate restTemplate; @Test void getProduitRetourne404() { var response = restTemplate.getForEntity( "/api/produits/{id}", String.class, UUID.randomUUID()); assertEquals(404, response.getStatusCode()); } }

Erreurs courantes

@Transactional sur une classe concrète sans interface

Hibernate utilise des proxies CGLIB par défaut. @Transactional fonctionne sur la classe elle-même, mais les appels internes (this.method()) ne passent pas par le proxy — donc la transaction n’est jamais ouverte.

// ❌ Appel interne — pas de transaction @Service public class ComportementService { public void creer() { this.persistre(); // pas de transaction ! } @Transactional public void persistre() { /* ... */ } } // ✅ Solution : extraire dans un autre bean ou utiliser AopContext @Service public class ComportementService { private final ComportementTransactionnel txService; public ComportementService(ComportementTransactionnel txService) { this.txService = txService; } public void creer() { txService.persistre(); // transaction active } }

Dépendance cyclique entre beans

UserService → FactureService → UserService (cycle !)

Spring Boot 3 refuse les cycles par défaut. Solutions :

  1. Extraire la logique partagée dans un troisième bean (FacadeService).
  2. Injecter Lazy : @Lazy private final FactureService factures; (usine à gaz).
  3. Inverser la dépendance (principe de dépendance).

Profil activé mais fichier inexistant

spring.profiles.active=prod ne provoque aucune erreur si application-prod.yml n’existe pas. La configuration par défaut reste utilisée → bugs silencieux. Toujours vérifier avec /actuator/env.

ddl-auto: update en production

Hibernate modifie la base à chaque démarrage → perte de données, colonnes en double, schéma corrompu. En production : validate uniquement. Les migrations passent par Flyway ou Liquibase.

Secrets en dur dans un fichier versionné

# ❌ MAUVAIS — dans git ! spring: datasource: password: mon_mot_de_passe_en_clair

Checklist de démarrage

  • Parent spring-boot-starter-parent déclaré
  • Classe *Application avec @SpringBootApplication
  • application.yml de base + profils définis
  • Dépendances minimales (ne pas tout importer d’emblée)
  • Profil dev avec ddl-auto: validate et logging DEBUG
  • Sondes /actuator/health pour Kubernetes
  • Variables d’environnement pour les secrets
  • Structure hexagonale appliquée dès le premier service