Micronaut
Guide opérationnel pour les microservices Micronaut — configuration, DI, contrôleurs HTTP, client HTTP, sécurité, compilation native GraalVM et comparaison rapide avec Spring Boot.
Philosophie : Micronaut améliore le temps de démarrage et l’empreinte mémoire en déplaçant le travail de réflexion (injection de dépendances, résolution de beans) de l’exécution vers la compilation. Résultat : un démarrage en quelques dizaines de millisecondes et une empreinte de quelques Mo — idéal pour le serverless et les conteneurs resserrés.
Démarrage rapide
Création du projet
# Via le CLI Micronaut
mn create-app com.monapp.monservice \
--build maven \
--lang java \
--features http-clientpom.xml minimal
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>com.monapp</groupId>
<artifactId>mon-service</artifactId>
<version>0.1.0</version>
<parent>
<groupId>io.micronaut.platform</groupId>
<artifactId>micronaut-parent</artifactId>
<version>4.7.7</version>
</parent>
<properties>
<jdk.version>21</jdk.version>
<release.version>21</release.version>
<micronaut.version>4.7.7</micronaut.version>
</properties>
<dependencies>
<dependency>
<groupId>io.micronaut</groupId>
<artifactId>micronaut-http-server-netty</artifactId>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>io.micronaut</groupId>
<artifactId>micronaut-jackson-databind</artifactId>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>io.micronaut</groupId>
<artifactId>micronaut-inject-java</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>io.micronaut</groupId>
<artifactId>micronaut-http-client</artifactId>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>io.micronaut.sql</groupId>
<artifactId>micronaut-jdbc-hikari</artifactId>
<scope>compile</scope>
</dependency>
<!-- Observabilité -->
<dependency>
<groupId>io.micronaut.micrometer</groupId>
<artifactId>micronaut-micrometer-core</artifactId>
<scope>compile</scope>
</dependency>
</dependencies>
</project>Note : le parent
micronaut-parentgère les versions transitives. Ne pas déclarer de<version>sur un starter fourni par le parent.
application.yml — Configuration
Profil actif et détection
# application.yml (valeur par défaut)
micronaut:
application:
name: mon-service
server:
port: 8080
context-path: /api
profiles:
active: ${MICRONAUT_PROFILES:local}
security:
enabled: true
datasources:
default:
url: jdbc:postgresql://${DB_HOST:localhost}/${DB_NAME:monapp}?sslmode=disable
username: ${DB_USER}
password: ${DB_PASSWORD}Variables d’environnement
# Variables typiques à définir en environnement
export DB_HOST=localhost
export DB_NAME=monapp
export DB_USER=postgres
export DB_PASSWORD=change-me
export MICRONAUT_PROFILES=prodRègle : ne jamais saisir de secrets en dur dans un fichier versionné. Utiliser des variables d’environnement ou un gestionnaire de secrets (Vault, AWS Secrets Manager, etc.).
Annotations clés
| Annotation | Rôle | Cycle de vie |
|---|---|---|
@Controller | Enregistrement d’un contrôleur | Singleton |
@Singleton | Bean singleton géré par le conteneur | Singleton |
@Context | Bean instancié par requête (stateful) | Scoped |
@Factory | Classe fabrique de beans | Singleton |
@Bean | Déclare un bean dans une classe @Factory | Configurable |
@Replaces | Remplace un bean par défaut | — |
@Client | Client HTTP typed (proxy injecté) | Singleton |
@Scheduled | Planification (cron ou intervalle) | — |
@Retryable | Tentatives automatiques en cas d’erreur | — |
@CircuitBreaker | Protection contre les services aval défaillants | — |
@Get / @Post / @Put / @Delete | Mappage d’une méthode HTTP | — |
@Body | Binding du corps de requête sur un objet | — |
@PathVariable | Extraction d’un segment d’URL | — |
@QueryValue | Extraction d’un paramètre de requête | — |
@HeaderValue | Extraction d’un header | — |
@Valid / @NotBlank / @Size | Validation Jakarta sur un champ DTO | — |
Règle : privilégier l’injection par constructeur (recommandée par défaut). L’injection par champ existe (
@Inject) mais le constructeur reste plus testable.
Contrôleurs HTTP
package com.monapp.web;
import io.micronaut.http.annotation.*;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
@Controller("/produits")
public class ProduitController {
private final ProduitService service;
public ProduitController(ProduitService service) {
this.service = service;
}
@Get
public List<ProduitDTO> lister() {
return service.lister();
}
@Get("/{id}")
public ProduitDTO obtenir(@PathVariable UUID id) {
return service.obtenir(id)
.orElseThrow(() -> new NotFoundException("Produit introuvable"));
}
@Post(consumes = "application/json", produces = "application/json")
public ProduitDTO creer(@Valid @Body CreerProduitRequest body) {
return service.creer(body.toProduit());
}
@Put("/{id}", consumes = "application/json")
public void modifier(@PathVariable UUID id, @Valid @Body ModifierProduitRequest body) {
service.modifier(id, body.toProduit());
}
}Classes de requête / réponse
package com.monapp.web;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Positive;
public class CreerProduitRequest {
@NotBlank
private String nom;
@NotBlank
private String description;
@Positive
private java.math.BigDecimal prix;
public CreerProduitRequest() {}
public String nom() { return nom; }
public String description() { return description; }
public java.math.BigDecimal prix() { return prix; }
public Produit toProduit() {
return new Produit(nom, description, prix);
}
}package com.monapp.web;
public class ProduitDTO {
private final UUID id;
private final String nom;
private final String description;
private final java.math.BigDecimal prix;
public ProduitDTO(UUID id, String nom, String description, java.math.BigDecimal prix) {
this.id = id;
this.nom = nom;
this.description = description;
this.prix = prix;
}
public UUID id() { return id; }
public String nom() { return nom; }
public String description() { return description; }
public java.math.BigDecimal prix() { return prix; }
}Convention : séparer les DTO de requête et les DTO de réponse. Un
CreerProduitRequestn’expose pas l’ID (pas encore créé). UnProduitDTOexpose l’ID mais pas les champs en écriture.
Injection de dépendances
@Singleton vs @Context
// Singleton — une seule instance, partagée (stateless)
@Singleton
public class ProduitService {
private final ProduitRepository repo;
public ProduitService(ProduitRepository repo) {
this.repo = repo;
}
public List<ProduitDTO> lister() {
return repo.lister();
}
}
// Context — nouvelle instance par requête HTTP (stateful)
@Context
public class TracingContext {
private String traceId;
public TracingContext() {
this.traceId = UUID.randomUUID().toString();
}
public String getTraceId() { return traceId; }
}@Factory — Fabrique de beans
@Factory
public class ConfigFactory {
@Bean
@Singleton
@Replaces(JdbcTemplate.class)
public DataSource dataSource(DataSourceConfiguration config) {
HikariDataSource ds = new HikariDataSource();
ds.setJdbcUrl(config.getUrl());
ds.setUsername(config.getUsername());
ds.setPassword(config.getPassword());
return ds;
}
}@Replaces — Overriding de beans
// Bean par défaut fourni par un starter
@Singleton
public class NotificationServiceStandard implements NotificationService { ... }
// Override dans le projet utilisateur
@Singleton
@Replaces(NotificationServiceStandard.class)
public class NotificationServiceSNS implements NotificationService { ... }Piège :
@Replacesdoit être déclaré dans le même contexte d’application (pas dans une classe@Factoryqui ne devient pas bean singleton du conteneur racine).
HTTP Client — Appels distants
Déclaration d’un client typed
package com.monapp.client;
import io.micronaut.http.client.annotation.Client;
import io.micronaut.http.annotation.Get;
@Client("http://localhost:8081")
public interface ServiceUtilisateursClient {
@Get("/utilisateurs/{id}")
UtilisateurDTO obtenir(@PathVariable("id") UUID id);
@Get("/utilisateurs")
List<UtilisateurDTO> lister();
}Injection et utilisation
@Singleton
public class NotificationService {
private final ServiceUtilisateursClient utilisateursClient;
public NotificationService(ServiceUtilisateursClient utilisateursClient) {
this.utilisateursClient = utilisateursClient;
}
public String notifier(UUID userId) {
UtilisateurDTO user = utilisateursClient.obtenir(userId);
return "Notif vers " + user.email();
}
}Avantage : Micronaut génère le code d’appel à la compilation. Pas de reflection, pas de proxy dynamique. Premiers millisecondes d’init.
Sécurité — JWT
Configuration de base
micronaut:
security:
enabled: true
authentication: bearer
token:
jwt:
enabled: true
generators:
idtoken:
signing-key-ref: default
validators:
idtoken:
jwt-key-retailer:
name: default
validateAudience: true
validateExpiration: trueAnnotation @Secured
@Singleton
@Controller("/admin")
public class AdminController {
@Get("/stats")
@Secured(SecurityRule.IS_AUTHENTICATED)
public Map<String, Object> statistiques() {
return Map.of("utilisateurs", 42, "revenus", 1200);
}
@Delete("/produits/{id}")
@Secured("ROLE_ADMIN")
public void supprimer(@PathVariable UUID id) {
// ...
}
}Piège : par défaut, Micronaut sécurise tous les endpoints lorsqu’aucun
@Securedn’est spécifié ET quemicronaut.security.enabled=true. Les endpoints publics doivent être explicitement ouverts avec@Secured(SecurityRule.ANONYMOUS)ou viaAuthorizationContext.
Filtre JWT manuel
@Singleton
public class JwtAuthFilter implements HttpServerFilter {
@Override
public Publisher<MutableHttpResponse> doFilter(HttpRequest request, ServerFilterChain chain) {
String bearer = request.getHeaders().getAuthorization().orElse("");
if (!bearer.startsWith("Bearer ")) {
return chain.proceed(request);
}
String token = bearer.substring(7);
try {
Claims claims = Jwts.parser()
.verifyWith(publicKey)
.build()
.parseSignedClaims(token)
.getPayload();
return chain.proceed(request);
} catch (JwtException e) {
return Publishers.just(HttpResponse.unauthorized().body("Token invalide"));
}
}
}Compilation native — GraalVM
Prérequis
# Installer GraalVM (ou sbt-native-image)
gu install native-imageCompilation
./mvnw package -PnativeConfiguration GraalVM (META-INF/native-image/...)
<!-- pom.xml dans les profiles -->
<profile>
<id>native</id>
<build>
<plugins>
<plugin>
<groupId>org.graalvm.buildtools</groupId>
<artifactId>native-maven-plugin</artifactId>
<configuration>
<buildArgs>
--no-fallback
--verbose
--report-unsupported-elements-at-runtime
</buildArgs>
</configuration>
</plugin>
</plugins>
</build>
</profile>Piège : quand des bibliothèques utilisent de la réflexion (reflexion) au runtime, la compilation native échoue ou plante à l’exécution. Ajouter les classes affectées dans le fichier de configuration native (
reflect-config.json) ou utiliser@RegisterForReflection.
Ordre d’exécution — cycle de vie
1. Parsing des annotations
2. Génération du code (générateur Java, pas de reflection)
3. Résolution de la hiérarchie des beans
4. Initialisation des beans @Singleton
5. Démarrage du serveur HTTP (Netty par défaut)
6. Application des filtres, readyDifférence clé avec Spring Boot : Spring résout les beans à l’exécution via reflection et proxy CGLIB/JDK. Micronaut le fait à la compilation via un générateur de code dédié. C’est pourquoi le démarrage est plus rapide, mais la compilation prend plus de temps.
Comparaison rapide : Spring Boot ↔ Micronaut
| Critère | Spring Boot 3 | Micronaut 4 |
|---|---|---|
| Démarrage | 2–5 s (classique) / 300 ms (Native Image) | 10–50 ms (classique) / 5–10 ms (Native Image) |
| Mémoire JRE | ~200 Mo | ~30–50 Mo |
| Mémoire Native Image | ~30–50 Mo | ~10–20 Mo |
| Mécanisme DI | Reflection + proxy (CGLIB/JDK) | Génération de code à la compilation |
| Web | Tomcat (servlet), WebFlux (reactive) | Netty (native), Tomcat optionnel |
| Configuration | application.yml | application.yml |
| Profils | spring.profiles.active | micronaut.profiles.active |
| Compilation native | GraalVM (long) | Supporté nativement (plus rapide) |
| Communauté | Énorme | Croissante, niche Cloud-native |
| IDE | IntelliJ / Eclipse | IntelliJ / VS Code |
Choisir Micronaut quand : démarrage rapide critique (serverless), empreinte mémoire resserrée, besoin de compilation native, mise à l’échelle avec de nombreux petits pods.
Choisir Spring Boot quand : écosystème mature, recrutement facile, besoins complexes (Spring Security avancé, Spring Cloud Data Flow, etc.).
Erreurs courantes
Circular dependency — refusé par défaut
Micronaut rejette tout cycle de dépendances à la compilation.
// ❌ ERREUR à la compilation : cycle entre UserService et FactureService
@Singleton
public class UserService {
public UserService(FactureService factures) { /* ... */ }
}
@Singleton
public class FactureService {
public FactureService(UserService users) { /* ... */ }
}
// ✅ Solution : bean intermédiaire
@Singleton
public class ServicesFacade {
private final UserService users;
private final FactureService factures;
public ServicesFacade(UserService users, FactureService factures) {
this.users = users;
this.factures = factures;
}
}@ConfigurationProperties — champs manquants
// ❌ ERREUR : Micronaut exige un constructeur sans-args ou un paramètre unique
@ConfigurationProperties("app.mail")
public class MailConfig {
private String host;
private int port;
// Pas de constructeur → erreur à la compilation
}
// ✅ Solution : record ou constructeur unique
@ConfigurationProperties("app.mail")
public record MailConfig(String host, int port) {}Profil activé mais fichier inexistant
micronaut.application.name=prod ne provoque aucune erreur si
application-prod.yml n’existe pas. La configuration par défaut reste utilisée.
Vérifier avec /micronaut/health ou les logs.
Secrets en dur dans un fichier versionné
# ❌ MAUVAIS — dans git !
datasources:
default:
password: mon_mot_de_passe_en_clairChecklist de démarrage
- Parent
micronaut-parentdéclaré dans lepom.xml - Classes principales annotées
@Controller,@Singleton -
application.ymlavec profillocalpour le dev - DTO de requête avec validation Jakarta (
@NotBlank,@Positive) - Client HTTP typed si service aval (
@Client) - Variables d’environnement pour les secrets (DB, clés API)
- Profil
prodavecschema-generate: NONE - Sondes de santé actives (
/health) - Construction native testée (
./mvnw package -Pnative)