Java
Java est un langage orienté objet, à typage statique, compilé en bytecode exécuté par la JVM (Java Virtual Machine). Sa devise historique — « write once, run anywhere » — tient au fait que le même bytecode tourne sur toute plateforme disposant d'une JVM.
Cet aide-mémoire survole rapidement les fondamentaux orientés objet, puis se concentre sur l'usage réel en web full-stack : API REST, persistance et authentification avec Spring Boot, le standard de fait de l'écosystème.
JDK, JRE, JVM
| Composant | Rôle |
|---|---|
| JVM | Machine virtuelle qui exécute le bytecode .class |
| JRE | JVM + bibliothèques standard (pour exécuter une application) |
| JDK | JRE + outils de compilation (javac, jar, javadoc…) — nécessaire pour développer |
Versions LTS
Java suit un rythme de deux versions par an, avec une version LTS (Long Term Support) tous les 2-3 ans. En 2026, les LTS de référence sont Java 21 (2023, très répandue en production) et Java 25 (2025, la plus récente). Spring Boot 4.x exige au minimum Java 17.
java --version # version d'exécution
javac --version # version du compilateur
Pour gérer plusieurs JDK sur une machine, utiliser SDKMAN! (Linux/macOS) ou des distributions comme Temurin (Eclipse Adoptium), Amazon Corretto, Azul Zulu.
Fondamentaux OOP (rapide)
La programmation orientée objet consiste à modéliser le domaine sous forme d'objets qui regroupent des données (attributs) et les comportements qui les manipulent (méthodes). En Java, tout code vit dans une classe : la classe est le plan (le modèle), l'objet en est une instance concrète créée avec new.
// Fichier Personne.java — le nom du fichier doit correspondre à la classe publique
public class Personne {
// Attributs (encapsulés : private)
private String nom;
private int age;
// Constructeur
public Personne(String nom, int age) {
this.nom = nom;
this.age = age;
}
// Getters / setters (accès contrôlé)
public String getNom() { return nom; }
public void setAge(int age) { this.age = age; }
// Méthode
public String saluer() {
return "Bonjour, je suis " + nom;
}
}
Les 4 piliers
| Pilier | Principe | Mécanisme Java |
|---|---|---|
| Encapsulation | Cacher l'état interne, exposer un contrat | private + getters/setters |
| Héritage | Réutiliser et spécialiser une classe | extends |
| Polymorphisme | Un même appel, plusieurs comportements | @Override, surcharge |
| Abstraction | Définir un contrat sans implémentation | abstract, interface |
Héritage et polymorphisme
L'héritage (extends) permet à une classe fille de réutiliser les attributs et méthodes de sa classe mère, puis de les compléter ou de les redéfinir. Le polymorphisme est la conséquence : une variable de type Personne peut référencer un Employe, et l'appel de saluer() exécutera la version la plus spécifique (celle de Employe). C'est ce qui permet d'écrire du code générique qui s'adapte au type réel de l'objet à l'exécution.
public class Employe extends Personne {
private double salaire;
public Employe(String nom, int age, double salaire) {
super(nom, age); // appel du constructeur parent
this.salaire = salaire;
}
@Override
public String saluer() { // redéfinition (polymorphisme)
return super.saluer() + ", employé";
}
}
Interfaces
Une interface définit un contrat. Une classe peut en implémenter plusieurs (là où l'héritage de classe est unique).
public interface Notifiable {
void notifier(String message); // méthode abstraite
default void notifierUrgent(String m) { // méthode par défaut (Java 8+)
notifier("[URGENT] " + m);
}
}
public class Utilisateur extends Personne implements Notifiable {
@Override
public void notifier(String message) {
System.out.println(message);
}
}
record — classes de données immuables (Java 16+)
Idéal pour les DTO : génère automatiquement constructeur, getters, equals, hashCode et toString.
public record ArticleDTO(Long id, String titre, double prix) {}
var dto = new ArticleDTO(1L, "Clavier", 49.9);
dto.titre(); // "Clavier" — accesseur généré (sans préfixe get)
enum
Une énumération définit un ensemble fermé et fini de valeurs constantes nommées. Elle remplace avantageusement les chaînes ou les entiers « magiques » : le compilateur garantit qu'on ne peut utiliser qu'une des valeurs prévues, ce qui élimine toute une classe d'erreurs.
public enum Role {
USER, ADMIN, MODERATOR;
}
Role r = Role.ADMIN;
if (r == Role.ADMIN) { /* ... */ }
Essentiel du langage moderne
Ce que l'on croise réellement dans un backend Java.
Types
Java distingue deux familles de types. Les primitifs (int, double, boolean…) stockent directement leur valeur et ne peuvent pas être null. Les objets (toute classe, dont String et les wrappers comme Integer) sont manipulés par référence et peuvent valoir null. Le typage étant statique, le type de chaque variable est fixé et vérifié à la compilation.
// Primitifs (minuscule) : stockés par valeur
int n = 42;
long grand = 10_000_000_000L;
double prix = 19.99;
boolean actif = true;
char c = 'A';
// Objets (majuscule) : références, peuvent être null
String texte = "Bonjour";
Integer boxed = 42; // wrapper autour de int
// Inférence de type (Java 10+)
var liste = new ArrayList<String>(); // type déduit
Collections
Le framework Collections fournit les structures de données courantes, chacune avec ses garanties. List conserve l'ordre d'insertion et autorise les doublons ; Set interdit les doublons ; Map associe des clés à des valeurs. On programme contre l'interface (List, Set, Map) et on choisit l'implémentation (ArrayList, HashSet, HashMap) selon les besoins de performance.
import java.util.*;
List<String> liste = new ArrayList<>(); // liste ordonnée, doublons OK
liste.add("a"); liste.add("b");
Set<String> ensemble = new HashSet<>(); // pas de doublons
Map<String, Integer> map = new HashMap<>(); // clé -> valeur
map.put("age", 30);
map.getOrDefault("taille", 0); // valeur par défaut si absente
// Collections immuables (Java 9+)
List<String> fixe = List.of("x", "y", "z");
Map<String, Integer> config = Map.of("port", 8080);
Generics
Les generics permettent de paramétrer une classe ou une méthode par un type (<T>) fixé au moment de l'utilisation. Le bénéfice : le compilateur vérifie la cohérence des types et l'on évite les transtypages (cast) manuels et leurs erreurs à l'exécution. C'est ce qui fait qu'une List<String> n'accepte que des chaînes.
public class Boite<T> {
private T contenu;
public void ranger(T item) { this.contenu = item; }
public T recuperer() { return contenu; }
}
Boite<String> b = new Boite<>();
b.ranger("secret"); // n'accepte que des String
Streams et lambdas
Une lambda est une fonction anonyme écrite de façon concise (e -> e.getNom()), que l'on passe en argument comme une valeur. L'API Stream enchaîne des opérations sur une collection de manière déclarative : on décrit quoi faire (filtrer, transformer, agréger) plutôt que comment le faire avec des boucles. Les opérations intermédiaires (filter, map, sorted) sont paresseuses et ne s'exécutent qu'au déclenchement d'une opération terminale (collect, sum, toList).
import java.util.stream.*;
List<Employe> employes = /* ... */;
// Filtrer, transformer, collecter
List<String> nomsCadres = employes.stream()
.filter(e -> e.getSalaire() > 50000) // lambda
.map(Employe::getNom) // référence de méthode
.sorted()
.collect(Collectors.toList());
// Agrégations
double masseSalariale = employes.stream()
.mapToDouble(Employe::getSalaire)
.sum();
// Regrouper
Map<Role, List<Employe>> parRole = employes.stream()
.collect(Collectors.groupingBy(Employe::getRole));
Optional — éviter les NullPointerException
Optional<T> est un conteneur qui représente explicitement « une valeur ou son absence ». Plutôt que de renvoyer null (qu'un appelant risque d'oublier de tester, provoquant une NullPointerException), une méthode renvoie un Optional : le type force alors à traiter le cas « absent » de façon visible.
Optional<Utilisateur> resultat = repository.findByEmail(email);
Utilisateur u = resultat.orElseThrow(
() -> new UtilisateurIntrouvableException(email)
);
// Ou une valeur par défaut
String nom = resultat.map(Utilisateur::getNom).orElse("Anonyme");
Gestion des exceptions
Une exception est un objet signalant une erreur qui interrompt le flux normal ; elle « remonte » la pile d'appels jusqu'à un bloc catch capable de la traiter. Java distingue deux familles : les exceptions vérifiées (checked), que le compilateur oblige à gérer ou à déclarer, et les non vérifiées (unchecked, issues de RuntimeException), qui traduisent le plus souvent un bug de programmation.
try {
var contenu = Files.readString(Path.of("config.txt"));
} catch (IOException e) {
log.error("Lecture impossible", e);
throw new RuntimeException(e);
} finally {
// toujours exécuté
}
// try-with-resources : ferme automatiquement les ressources
try (var reader = Files.newBufferedReader(path)) {
return reader.readLine();
} // reader.close() appelé automatiquement
| Type d'exception | Vérifiée à la compilation ? | Exemple |
|---|---|---|
Checked (Exception) |
Oui — throws ou try/catch obligatoire |
IOException, SQLException |
Unchecked (RuntimeException) |
Non | NullPointerException, IllegalArgumentException |
Outillage & build
Un projet Java repose sur un outil de build qui compile le code, télécharge les dépendances depuis un dépôt central, exécute les tests et produit un artefact livrable (un .jar). Les deux standards sont Maven (configuration XML déclarative) et Gradle (scripts plus concis). Tous deux suivent une convention d'arborescence identique.
Structure d'un projet Maven
mon-projet/
├── pom.xml # configuration du build
└── src/
├── main/
│ ├── java/ # code source
│ │ └── com/exemple/app/
│ │ ├── Application.java
│ │ ├── controller/
│ │ ├── service/
│ │ ├── repository/
│ │ └── model/
│ └── resources/ # config, templates
│ └── application.yml
└── test/
└── java/ # tests
Maven — pom.xml annoté
<project xmlns="http://maven.apache.org/POM/4.0.0" ...>
<modelVersion>4.0.0</modelVersion>
<!-- Hérite des versions gérées par Spring Boot -->
<!-- Ligne courante en 2026 : 4.x (baseline Java 17). 3.x reste très répandue ;
le code de cette fiche s'applique aux deux (namespace jakarta.*). -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.4.0</version>
</parent>
<!-- Coordonnées du projet -->
<groupId>com.exemple</groupId>
<artifactId>app</artifactId>
<version>1.0.0</version>
<properties>
<java.version>21</java.version>
</properties>
<dependencies>
<!-- Un "starter" tire toutes les dépendances cohérentes d'un besoin -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope> <!-- disponible en test uniquement -->
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
Commandes Maven essentielles
mvn clean # supprime le dossier target/
mvn compile # compile le code
mvn test # exécute les tests
mvn package # produit le .jar (dans target/)
mvn install # installe le .jar dans le dépôt local
mvn spring-boot:run # lance l'application (dev)
Maven vs Gradle
| Aspect | Maven (pom.xml) |
Gradle (build.gradle) |
|---|---|---|
| Format | XML déclaratif | Groovy / Kotlin DSL |
| Verbosité | Élevée | Concise |
| Performance | Correcte | Meilleure (cache, build incrémental) |
| Déclarer une dépendance | <dependency>…</dependency> |
implementation 'groupe:artefact:version' |
| Lancer | mvn spring-boot:run |
./gradlew bootRun |
| Écosystème | Majoritaire, très stable | Populaire (Android, gros projets) |
// build.gradle — équivalent Gradle du pom.xml ci-dessus
plugins {
id 'org.springframework.boot' version '3.4.0'
id 'io.spring.dependency-management' version '1.1.6'
id 'java'
}
java { sourceCompatibility = '21' }
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
Spring Boot — les bases
Spring Boot simplifie radicalement la configuration de Spring via l'auto-configuration (configure automatiquement selon les dépendances présentes) et les starters (regroupements de dépendances cohérentes).
Point d'entrée
Une application Spring Boot est une classe Java classique avec un main. Au démarrage, SpringApplication.run(...) lance le conteneur Spring (l'ApplicationContext), scanne le projet à la recherche des composants, les instancie et démarre un serveur web embarqué (Tomcat par défaut) — aucun serveur externe à installer.
package com.exemple.app;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication // = @Configuration + @EnableAutoConfiguration + @ComponentScan
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
Injection de dépendances (IoC)
Le principe d'inversion de contrôle (IoC) consiste à confier à Spring la création et l'assemblage des objets plutôt que de les instancier soi-même avec new. Chaque classe annotée devient un bean géré par le conteneur ; quand une classe déclare avoir besoin d'une autre, Spring la lui injecte automatiquement. Résultat : les composants sont faiblement couplés, remplaçables et testables isolément.
On annote les classes selon leur rôle, et on injecte les dépendances via le constructeur (méthode recommandée).
| Annotation | Couche |
|---|---|
@RestController |
Exposition HTTP (API REST) |
@Service |
Logique métier |
@Repository |
Accès aux données |
@Component |
Composant générique |
@Configuration |
Classe de configuration / beans |
@Service
public class ArticleService {
private final ArticleRepository repository; // dépendance
// Injection par constructeur : pas besoin de @Autowired si un seul constructeur
public ArticleService(ArticleRepository repository) {
this.repository = repository;
}
public List<Article> lister() {
return repository.findAll();
}
}
Pourquoi le constructeur plutôt que
@Autowiredsur le champ ? Il rend les dépendances explicites, permet de les déclarerfinal(immuables) et facilite les tests unitaires (on passe des mocks directement).
Configuration — application.yml
Toute la configuration externe de l'application (port, base de données, secrets…) se déclare dans application.yml (ou .properties), au lieu d'être codée en dur. La syntaxe ${VARIABLE} récupère une variable d'environnement : les valeurs sensibles restent ainsi hors du code source et du dépôt Git.
server:
port: 8080
spring:
datasource:
url: jdbc:postgresql://localhost:5432/madb
username: ${DB_USER} # variable d'environnement
password: ${DB_PASSWORD}
jpa:
hibernate:
ddl-auto: update # validate | update | create | create-drop
show-sql: true
app:
jwt:
secret: ${JWT_SECRET}
expiration: 900000 # 15 min en ms
Profils (environnements)
# application.yml — profil actif
spring:
profiles:
active: dev
Chaque profil a son fichier : application-dev.yml, application-prod.yml. Activation au lancement :
java -jar app.jar --spring.profiles.active=prod
// Lire une valeur de config dans le code
@Value("${app.jwt.expiration}")
private long expiration;
API REST
Une API REST expose les ressources de l'application (articles, utilisateurs…) via des URL et les verbes HTTP standard (GET, POST, PUT, DELETE). Dans Spring, un contrôleur reçoit la requête, délègue le traitement à un service, et renvoie un objet que Spring sérialise automatiquement en JSON.
Contrôleur REST
Annoté @RestController, chaque méthode est associée à une route et à un verbe. Les valeurs de retour sont converties en JSON (via Jackson), et les paramètres de la requête sont désérialisés en objets Java par les annotations @PathVariable, @RequestParam et @RequestBody.
@RestController
@RequestMapping("/api/articles") // préfixe commun à toutes les routes
public class ArticleController {
private final ArticleService service;
public ArticleController(ArticleService service) {
this.service = service;
}
@GetMapping // GET /api/articles
public List<ArticleDTO> lister() {
return service.lister();
}
@GetMapping("/{id}") // GET /api/articles/42
public ArticleDTO obtenir(@PathVariable Long id) {
return service.obtenir(id);
}
@PostMapping // POST /api/articles
@ResponseStatus(HttpStatus.CREATED) // 201
public ArticleDTO creer(@Valid @RequestBody CreerArticleDTO dto) {
return service.creer(dto);
}
@PutMapping("/{id}") // PUT /api/articles/42
public ArticleDTO modifier(@PathVariable Long id,
@Valid @RequestBody CreerArticleDTO dto) {
return service.modifier(id, dto);
}
@DeleteMapping("/{id}") // DELETE /api/articles/42
@ResponseStatus(HttpStatus.NO_CONTENT) // 204
public void supprimer(@PathVariable Long id) {
service.supprimer(id);
}
// Paramètres de requête : GET /api/articles/recherche?q=clavier&page=0
@GetMapping("/recherche")
public List<ArticleDTO> rechercher(@RequestParam String q,
@RequestParam(defaultValue = "0") int page) {
return service.rechercher(q, page);
}
}
Mapping des annotations HTTP
| Annotation | Verbe HTTP | Usage typique |
|---|---|---|
@GetMapping |
GET | Lire |
@PostMapping |
POST | Créer |
@PutMapping |
PUT | Remplacer |
@PatchMapping |
PATCH | Modifier partiellement |
@DeleteMapping |
DELETE | Supprimer |
@PathVariable |
— | Variable dans l'URL (/{id}) |
@RequestParam |
— | Paramètre de requête (?q=) |
@RequestBody |
— | Corps JSON désérialisé en objet |
Validation des entrées
Spring intègre Jakarta Bean Validation. On annote le DTO, et @Valid déclenche la vérification.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
public record CreerArticleDTO(
@NotBlank(message = "Le titre est obligatoire")
String titre,
@Positive(message = "Le prix doit être positif")
double prix,
@Email(message = "Email invalide")
String contactEmail
) {}
| Annotation | Vérifie |
|---|---|
@NotNull / @NotBlank / @NotEmpty |
Non nul / chaîne non vide / collection non vide |
@Size(min=, max=) |
Longueur d'une chaîne ou collection |
@Min / @Max / @Positive |
Bornes numériques |
@Email |
Format e-mail |
@Pattern(regexp=) |
Expression régulière |
Gestion centralisée des erreurs
Plutôt que de répéter des try/catch dans chaque contrôleur, on centralise le traitement des exceptions dans une classe @RestControllerAdvice. Elle intercepte les exceptions levées par n'importe quel contrôleur et les traduit en réponses HTTP cohérentes (bon code de statut, corps d'erreur uniforme). La logique métier peut ainsi lever une exception « métier » sans se soucier du format de la réponse.
@RestControllerAdvice
public class GestionnaireErreurs {
// Exception métier -> 404
@ExceptionHandler(RessourceIntrouvableException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public ErreurReponse introuvable(RessourceIntrouvableException e) {
return new ErreurReponse("NOT_FOUND", e.getMessage());
}
// Échec de validation -> 400 avec le détail des champs
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Map<String, String> validation(MethodArgumentNotValidException e) {
Map<String, String> erreurs = new HashMap<>();
e.getBindingResult().getFieldErrors().forEach(err ->
erreurs.put(err.getField(), err.getDefaultMessage()));
return erreurs;
}
}
public record ErreurReponse(String code, String message) {}
Codes HTTP courants
| Code | Signification | Quand |
|---|---|---|
200 OK |
Succès | GET, PUT réussis |
201 Created |
Créé | POST réussi |
204 No Content |
Succès sans corps | DELETE réussi |
400 Bad Request |
Requête invalide | Validation échouée |
401 Unauthorized |
Non authentifié | Token absent/invalide |
403 Forbidden |
Non autorisé | Droits insuffisants |
404 Not Found |
Introuvable | Ressource inexistante |
500 Internal Server Error |
Erreur serveur | Exception non gérée |
Persistance — Spring Data JPA / Hibernate
JPA (Jakarta Persistence API) est la spécification de mapping objet-relationnel (ORM) ; Hibernate en est l'implémentation par défaut ; Spring Data JPA ajoute une couche qui génère les repositories automatiquement.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
Entité
Une entité est une classe Java dont chaque instance correspond à une ligne d'une table. Les annotations décrivent le mapping : @Entity marque la classe comme persistante, @Id désigne la clé primaire, @Column règle les détails de colonne. Hibernate se charge alors de traduire les opérations sur ces objets en SQL.
import jakarta.persistence.*;
@Entity
@Table(name = "articles")
public class Article {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY) // auto-incrément
private Long id;
@Column(nullable = false, length = 200)
private String titre;
private double prix;
@Enumerated(EnumType.STRING) // stocke "DISPONIBLE" plutôt que 0
private Statut statut;
@Column(name = "cree_le", updatable = false)
private Instant creeLe = Instant.now();
// JPA exige un constructeur sans argument
protected Article() {}
// + constructeur, getters, setters
}
Relations
Les associations entre tables (clés étrangères) se traduisent par des références entre entités. Les annotations @ManyToOne, @OneToMany et @ManyToMany décrivent la cardinalité ; cascade propage les opérations aux entités liées (ex. sauvegarder une commande sauvegarde ses lignes), et orphanRemoval supprime les enfants détachés de leur parent.
@Entity
public class Commande {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// Plusieurs commandes -> un client
@ManyToOne(fetch = FetchType.LAZY) // LAZY : chargé à la demande
@JoinColumn(name = "client_id")
private Client client;
// Une commande -> plusieurs lignes
@OneToMany(mappedBy = "commande", cascade = CascadeType.ALL, orphanRemoval = true)
private List<LigneCommande> lignes = new ArrayList<>();
}
| Annotation | Cardinalité |
|---|---|
@OneToOne |
1 ↔ 1 |
@OneToMany / @ManyToOne |
1 ↔ N |
@ManyToMany |
N ↔ N (table de jointure) |
⚠️ Préférer
FetchType.LAZYpar défaut pour ne charger une relation qu'au moment où on y accède réellement. Le chargementEAGERnon maîtrisé cause le problème classique des N+1 requêtes : parcourir une liste de N entités déclenche 1 requête pour la liste, puis 1 requête par entité pour charger sa relation. On le résout avec une jointure explicite (JOIN FETCHou@EntityGraph).
Repository
Un repository est la couche d'accès aux données. Avec Spring Data JPA, il suffit de déclarer une interface étendant JpaRepository : Spring en génère l'implémentation au démarrage. Mieux, il déduit la requête du nom de la méthode (findByStatut, existsByTitre…) selon une convention de nommage — aucune ligne de SQL pour les cas courants.
public interface ArticleRepository extends JpaRepository<Article, Long> {
// Requêtes DÉRIVÉES : Spring déduit le SQL du nom de la méthode
List<Article> findByStatut(Statut statut);
List<Article> findByTitreContainingIgnoreCase(String fragment);
Optional<Article> findByTitre(String titre);
boolean existsByTitre(String titre);
long countByStatut(Statut statut);
// Requête explicite (JPQL) quand la dérivation ne suffit pas
@Query("SELECT a FROM Article a WHERE a.prix BETWEEN :min AND :max")
List<Article> trouverParFourchette(double min, double max);
// SQL natif si besoin
@Query(value = "SELECT * FROM articles WHERE prix > ?1", nativeQuery = true)
List<Article> plusChersQue(double prix);
}
JpaRepository fournit déjà : findAll(), findById(), save(), deleteById(), count(), la pagination et le tri.
DTO et mapping
Ne jamais exposer directement les entités JPA dans l'API (fuite de structure, cycles de sérialisation, couplage). On convertit vers des DTO.
@Service
public class ArticleService {
private final ArticleRepository repository;
public ArticleService(ArticleRepository repository) {
this.repository = repository;
}
@Transactional(readOnly = true)
public List<ArticleDTO> lister() {
return repository.findAll().stream()
.map(this::versDTO)
.toList();
}
@Transactional
public ArticleDTO creer(CreerArticleDTO dto) {
Article a = new Article(dto.titre(), dto.prix());
return versDTO(repository.save(a));
}
private ArticleDTO versDTO(Article a) {
return new ArticleDTO(a.getId(), a.getTitre(), a.getPrix());
}
}
@Transactionaldélimite la transaction : tout réussit, ou tout est annulé (rollback). UtiliserreadOnly = truepour les lectures (optimisation).
Authentification & sécurité
Spring Security intercepte les requêtes via une chaîne de filtres (filter chain) avant qu'elles n'atteignent les contrôleurs. On l'utilise ici pour une authentification JWT stateless, adaptée aux API et aux SPA.
Cette section applique les concepts JWT (structure du token, access/refresh, stockage) détaillés dans la fiche JWT — Authentification. On se concentre ici sur l'intégration Spring.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
Hachage des mots de passe (BCrypt)
On ne stocke jamais un mot de passe en clair : on en conserve une empreinte produite par une fonction de hachage à sens unique, impossible à inverser. BCrypt est conçu spécifiquement pour cela — il est volontairement lent (pour résister aux attaques par force brute) et intègre un sel aléatoire, de sorte que deux mots de passe identiques donnent des empreintes différentes. La vérification se fait par matches, sans jamais reconstituer le mot de passe.
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
// À l'inscription : hacher avant de sauvegarder
String hash = passwordEncoder.encode(motDePasseEnClair);
// À la connexion : comparer
boolean ok = passwordEncoder.matches(motDePasseEnClair, hashStocke);
Configuration de la sécurité
La SecurityFilterChain définit les règles appliquées à chaque requête entrante : quelles routes sont publiques, lesquelles exigent une authentification, lesquelles réclament un rôle précis. Pour une API JWT, on désactive les sessions (STATELESS) — chaque requête se ré-authentifie via son token, le serveur ne conserve aucun état — et on désactive la protection CSRF, inutile en l'absence de cookies de session.
@Configuration
@EnableWebSecurity
@EnableMethodSecurity // active @PreAuthorize sur les méthodes
public class SecurityConfig {
private final JwtAuthFilter jwtAuthFilter;
public SecurityConfig(JwtAuthFilter jwtAuthFilter) {
this.jwtAuthFilter = jwtAuthFilter;
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// API stateless : pas de session, pas besoin de CSRF
.csrf(csrf -> csrf.disable())
.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll() // login/register publics
.requestMatchers(HttpMethod.GET, "/api/articles/**").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN") // réservé aux admins
.anyRequest().authenticated() // le reste : authentifié
)
// Insérer notre filtre JWT avant le filtre d'authentification standard
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
Génération du token (service JWT)
Ce service encapsule la création et la vérification des JWT avec la bibliothèque jjwt. À la connexion, il signe un token contenant l'identité de l'utilisateur et ses claims (rôle, expiration) ; à chaque requête suivante, il vérifie la signature avec le secret pour garantir que le token n'a pas été falsifié.
@Service
public class JwtService {
@Value("${app.jwt.secret}")
private String secret;
@Value("${app.jwt.expiration}")
private long expiration;
public String genererToken(Utilisateur u) {
return Jwts.builder()
.subject(u.getEmail())
.claim("role", u.getRole().name())
.issuedAt(new Date())
.expiration(new Date(System.currentTimeMillis() + expiration))
.signWith(cle())
.compact();
}
public String extraireEmail(String token) {
return parser(token).getSubject();
}
public boolean estValide(String token) {
try {
parser(token);
return true;
} catch (JwtException e) {
return false;
}
}
private Claims parser(String token) {
return Jwts.parser().verifyWith(cle()).build()
.parseSignedClaims(token).getPayload();
}
private SecretKey cle() {
return Keys.hmacShaKeyFor(secret.getBytes());
}
}
Endpoint de connexion
La route de login est publique. Elle vérifie l'e-mail et compare le mot de passe fourni au hash stocké (via matches, sans jamais déchiffrer). Si les identifiants sont valides, elle renvoie un JWT que le client joindra à ses requêtes suivantes.
@RestController
@RequestMapping("/api/auth")
public class AuthController {
private final UtilisateurRepository repository;
private final PasswordEncoder encoder;
private final JwtService jwtService;
// + constructeur
@PostMapping("/login")
public TokenDTO login(@Valid @RequestBody LoginDTO dto) {
Utilisateur u = repository.findByEmail(dto.email())
.orElseThrow(() -> new ResponseStatusException(HttpStatus.UNAUTHORIZED));
if (!encoder.matches(dto.motDePasse(), u.getMotDePasse())) {
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Identifiants invalides");
}
return new TokenDTO(jwtService.genererToken(u), "Bearer");
}
}
public record LoginDTO(@Email String email, @NotBlank String motDePasse) {}
public record TokenDTO(String accessToken, String tokenType) {}
Filtre de validation JWT
Ce filtre s'exécute à chaque requête : il lit le header Authorization, valide le token et place l'utilisateur dans le contexte de sécurité.
@Component
public class JwtAuthFilter extends OncePerRequestFilter {
private final JwtService jwtService;
public JwtAuthFilter(JwtService jwtService) {
this.jwtService = jwtService;
}
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain)
throws ServletException, IOException {
String header = request.getHeader("Authorization");
if (header != null && header.startsWith("Bearer ")) {
String token = header.substring(7);
if (jwtService.estValide(token)) {
String email = jwtService.extraireEmail(token);
var auth = new UsernamePasswordAuthenticationToken(
email, null, List.of()); // + autorités/rôles
SecurityContextHolder.getContext().setAuthentication(auth);
}
}
chain.doFilter(request, response); // laisser passer la requête
}
}
Autorisation fine par méthode
Là où la SecurityFilterChain filtre par URL, @PreAuthorize sécurise au niveau de la méthode. Son expression (langage SpEL) est évaluée avant l'exécution : elle peut vérifier un rôle, mais aussi une condition dynamique — par exemple qu'un utilisateur n'accède qu'à sa propre ressource.
@RestController
@RequestMapping("/api/admin")
public class AdminController {
@GetMapping("/stats")
@PreAuthorize("hasRole('ADMIN')") // vérifié avant l'appel
public StatsDTO stats() { /* ... */ }
// Accès à sa propre ressource uniquement
@GetMapping("/users/{id}")
@PreAuthorize("#id == authentication.principal.id or hasRole('ADMIN')")
public UserDTO utilisateur(@PathVariable Long id) { /* ... */ }
}
CORS (appels depuis un front sur un autre domaine)
Par sécurité, un navigateur bloque par défaut les requêtes JavaScript vers une origine (domaine, port ou protocole) différente de celle de la page. Le CORS (Cross-Origin Resource Sharing) est le mécanisme par lequel le serveur autorise explicitement certaines origines — indispensable quand le front (ex. localhost:5173) et l'API (ex. localhost:8080) sont sur des origines distinctes.
@Bean
public CorsConfigurationSource corsConfigurationSource() {
var config = new CorsConfiguration();
config.setAllowedOrigins(List.of("https://mon-front.fr")); // pas "*" en prod
config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE"));
config.setAllowedHeaders(List.of("Authorization", "Content-Type"));
config.setAllowCredentials(true);
var source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return source;
}
Flux d'une requête authentifiée
Client Serveur (Spring Security)
│── POST /api/auth/login ───────→│ vérifie identifiants (BCrypt)
│←── { accessToken } ────────────│ génère le JWT
│ │
│── GET /api/articles ───────────→│ ┌─ JwtAuthFilter
│ Authorization: Bearer <jwt> │ │ valide le token
│ │ │ place l'user dans le contexte
│ │ └─ authorizeHttpRequests / @PreAuthorize
│←── 200 { données } ────────────│ contrôleur exécuté si autorisé
│ │
│── GET /api/admin/stats ────────→│ 403 si rôle insuffisant
Tests
Spring Boot fournit un starter de test complet (JUnit 5, Mockito, AssertJ, MockMvc).
Test unitaire (service isolé avec Mockito)
Un test unitaire vérifie une classe isolée de ses dépendances. Mockito fournit des mocks : de fausses implémentations dont on programme le comportement (when(...).thenReturn(...)) pour tester la logique de la classe sans base de données ni contexte Spring. Le patron given / when / then structure le test : contexte, action, vérification.
@ExtendWith(MockitoExtension.class)
class ArticleServiceTest {
@Mock
private ArticleRepository repository; // dépendance simulée
@InjectMocks
private ArticleService service; // classe testée
@Test
void lister_retourne_les_articles() {
// given
when(repository.findAll()).thenReturn(List.of(
new Article("Clavier", 49.9)
));
// when
var resultat = service.lister();
// then
assertThat(resultat).hasSize(1);
assertThat(resultat.get(0).titre()).isEqualTo("Clavier");
}
}
Test de contrôleur (couche web isolée)
@WebMvcTest ne charge que la couche web (le contrôleur ciblé, la sérialisation, la sécurité), en simulant les services sous-jacents. MockMvc envoie des requêtes HTTP simulées sans démarrer de vrai serveur, ce qui rend ces tests rapides tout en vérifiant routes, codes de statut et JSON produit.
@WebMvcTest(ArticleController.class) // charge seulement la couche web
class ArticleControllerTest {
@Autowired
private MockMvc mockMvc;
@MockitoBean // remplace le service par un mock
private ArticleService service;
@Test
void obtenir_retourne_200() throws Exception {
when(service.obtenir(1L)).thenReturn(new ArticleDTO(1L, "Clavier", 49.9));
mockMvc.perform(get("/api/articles/1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.titre").value("Clavier"));
}
}
Test d'intégration (contexte complet)
@SpringBootTest // charge tout le contexte Spring
@AutoConfigureMockMvc
class ApplicationIT {
@Autowired
private MockMvc mockMvc;
@Test
void endpoint_protege_refuse_sans_token() throws Exception {
mockMvc.perform(get("/api/admin/stats"))
.andExpect(status().isUnauthorized());
}
}
| Annotation | Portée | Vitesse |
|---|---|---|
@ExtendWith(MockitoExtension.class) |
Unitaire, sans Spring | Très rapide |
@WebMvcTest |
Couche web seule | Rapide |
@DataJpaTest |
Couche persistance seule | Rapide |
@SpringBootTest |
Application complète | Lente |
Checklist — backend Java/Spring
| # | Vérification |
|---|---|
| 1 | Injection par constructeur, dépendances final |
| 2 | Entités JPA jamais exposées dans l'API — passer par des DTO (record) |
| 3 | Validation des entrées avec @Valid + annotations Jakarta |
| 4 | Gestion des erreurs centralisée (@RestControllerAdvice) |
| 5 | Relations JPA en FetchType.LAZY par défaut — attention au N+1 |
| 6 | @Transactional(readOnly = true) sur les lectures |
| 7 | Mots de passe hachés avec BCrypt — jamais en clair |
| 8 | Secrets (JWT, BDD) dans des variables d'environnement, pas dans le code |
| 9 | API stateless : sessions désactivées, JWT dans le header Authorization |
| 10 | CORS restreint aux origines connues (pas * en production) |
| 11 | Codes HTTP corrects (201 à la création, 204 sur DELETE, 401 vs 403) |
| 12 | Tests par couche : unitaire (Mockito) + web (@WebMvcTest) + intégration |