Spring Boot
Cheatsheet opérationnel — annotations, configuration, structure hexagonale et pièges courants.
Annotations essentielles
| Annotation | Rôle | Scope par défaut |
|---|---|---|
@RestController | Endpoint HTTP + sérialisation JSON | Bean singleton |
@Controller | Vue (thymeleaf, etc.) — pas de JSON automatique | Singleton |
@Service | Logique métier (use-case, orchestration) | Singleton |
@Repository | Couche accès données (traduit les exceptions JPA) | Singleton |
@Configuration | Classe fabrique de beans (contient @Bean) | Singleton |
@Component | Bean générique, détecté par scan de classpath | Singleton |
@Autowired | Injection de dépendance (champ, constructeur, setter) | — |
@Value("${prop}") | Injection d’une propriété de configuration | — |
@Transactional | Démarre/propage un contrôle de transaction | READ_COMMITTED |
@Profile("dev") | Active le bean uniquement avec le profil dev | — |
@ConditionalOnProperty | Active le bean si la propriété existe | — |
Règle : préférer l’injection par constructeur. Éviter
@Autowiredsur 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/
└── mvnwpom.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<version>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: 10MBPitfall : 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.ymlcommité. Toujours utiliser${VAR}ouclasspath:/secrets/monté comme volume.
Validation (Jakarta)
Activer la validation des DTO :
spring:
validation:
included-type-annotations:
- jakarta.validation.constraints.NotBlank
- jakarta.validation.constraints.NotEmptyLes annotations
@NotNull,@Size,spring-boot-starter-validationest dans lepom.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: DEBUGStarters courants
| Starter | Ce qu’il apporte |
|---|---|
spring-boot-starter-web | Tomcat intégré, Jackson, WebMvcAutoConfiguration |
spring-boot-starter-data-jpa | Hibernate, Spring Data JPA, HikariCP |
spring-boot-starter-security | Filtrage HTTP, UserDetailsService, PasswordEncoder |
spring-boot-starter-validation | Jakarta Validation (constraints @NotNull, @Size…) |
spring-boot-starter-actuator | /actuator/health, /actuator/metrics |
spring-boot-starter-test | JUnit 5, Mockito, @SpringBootTest |
spring-boot-starter-oauth2-client | Support 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)
| Terme | Signification | Emplacement typique |
|---|---|---|
| Entrypoint | Point d’entrée depuis l’extérieur (HTTP, file d’attente, batch) | web/, messaging/ |
| Port | Interface d’entrée (Adapter) | infrastructure/ |
| Use-case | Cas d’utilisation spécifique | application/ |
| Adaptor | Implémentation technique (JPA, REST client, Kafka consumer) | infrastructure/ |
| Domaine | Règles métier pures | domain/ |
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 -DskipTestsTest 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 :
- Extraire la logique partagée dans un troisième bean (
FacadeService). - Injecter
Lazy:@Lazy private final FactureService factures;(usine à gaz). - 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_clairChecklist de démarrage
- Parent
spring-boot-starter-parentdéclaré - Classe
*Applicationavec@SpringBootApplication -
application.ymlde base + profils définis - Dépendances minimales (ne pas tout importer d’emblée)
- Profil
devavecddl-auto: validateet loggingDEBUG - Sondes
/actuator/healthpour Kubernetes - Variables d’environnement pour les secrets
- Structure hexagonale appliquée dès le premier service