Skip to Content
BackendMicronaut

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-client

pom.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-parent gère les versions transitives. Ne pas déclarer de &lt;version&gt; 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=prod

Rè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

AnnotationRôleCycle de vie
@ControllerEnregistrement d’un contrôleurSingleton
@SingletonBean singleton géré par le conteneurSingleton
@ContextBean instancié par requête (stateful)Scoped
@FactoryClasse fabrique de beansSingleton
@BeanDéclare un bean dans une classe @FactoryConfigurable
@ReplacesRemplace un bean par défaut
@ClientClient HTTP typed (proxy injecté)Singleton
@ScheduledPlanification (cron ou intervalle)
@RetryableTentatives automatiques en cas d’erreur
@CircuitBreakerProtection contre les services aval défaillants
@Get / @Post / @Put / @DeleteMappage d’une méthode HTTP
@BodyBinding du corps de requête sur un objet
@PathVariableExtraction d’un segment d’URL
@QueryValueExtraction d’un paramètre de requête
@HeaderValueExtraction d’un header
@Valid / @NotBlank / @SizeValidation 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&lt;ProduitDTO&gt; 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 CreerProduitRequest n’expose pas l’ID (pas encore créé). Un ProduitDTO expose 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&lt;ProduitDTO&gt; 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 : @Replaces doit être déclaré dans le même contexte d’application (pas dans une classe @Factory qui 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&lt;UtilisateurDTO&gt; 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: true

Annotation @Secured

@Singleton @Controller("/admin") public class AdminController { @Get("/stats") @Secured(SecurityRule.IS_AUTHENTICATED) public Map&lt;String, Object&gt; 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 @Secured n’est spécifié ET que micronaut.security.enabled=true. Les endpoints publics doivent être explicitement ouverts avec @Secured(SecurityRule.ANONYMOUS) ou via AuthorizationContext.

Filtre JWT manuel

@Singleton public class JwtAuthFilter implements HttpServerFilter { @Override public Publisher&lt;MutableHttpResponse&gt; 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-image

Compilation

./mvnw package -Pnative

Configuration 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, ready

Diffé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èreSpring Boot 3Micronaut 4
Démarrage2–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 DIReflection + proxy (CGLIB/JDK)Génération de code à la compilation
WebTomcat (servlet), WebFlux (reactive)Netty (native), Tomcat optionnel
Configurationapplication.ymlapplication.yml
Profilsspring.profiles.activemicronaut.profiles.active
Compilation nativeGraalVM (long)Supporté nativement (plus rapide)
CommunautéÉnormeCroissante, niche Cloud-native
IDEIntelliJ / EclipseIntelliJ / 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_clair

Checklist de démarrage

  • Parent micronaut-parent déclaré dans le pom.xml
  • Classes principales annotées @Controller, @Singleton
  • application.yml avec profil local pour 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 prod avec schema-generate: NONE
  • Sondes de santé actives (/health)
  • Construction native testée (./mvnw package -Pnative)

Documentation utile