Introduction au framework Micronaut

Présentation

Micronaut est un framework moderne basé sur la JVM, nous offrant la possibilité de produire des microservices performants. Il supporte les langages de programmation Java, Groovy et Kotlin.

Il se rapproche fortement des frameworks Spring Boot et Quarkus et propose des fonctionnalités similaires telles que l’injection de dépendances, la programmation orientée aspect et l’auto-configuration.

Contrairement à d’autres frameworks, Micronaut utilise les processeurs d’annotations pour générer des métadonnées à la compilation nécessaires à l’injection de dépendances et à l’AOP. Cela évite, entre autres, l’utilisation de la réflexion et des proxies au runtime, améliorant ainsi les temps de démarrage de l’application et réduisant la consommation de mémoire. Ce point est important dans le cadre d’une architecture serverless notamment lors d’un démarrage à froid (cold start).

De plus, Micronaut intègre des fonctionnalités relatives au Cloud et aux microservices comme la découverte de services, le traçage distribué et le routage HTTP. Au niveau code, il intègre la programmation réactive par défaut et facilite la mise en œuvre de tests d’intégration. Un autre avantage est que son utilisation est très proche de celle de Spring, ce qui ne perturbera pas son apprentissage et limitera les impacts lors d'une migration.

Exemple d’utilisation

Génération du projet

Nous allons réaliser un microservice à l’aide du framework pour gérer des utilisateurs dans une base de données PostgreSQL.

Il existe plusieurs moyens de créer un projet Micronaut, dans notre cas, nous allons utiliser la CLI pour en générer un avec toutes les dépendances requises.

mn create-app --build=maven --jdk=17 --lang=java --test=junit --features=data-jdbc,jdbc-hikari,flyway,postgres com.example.demo

Cette commande permet de générer un projet vide, nous devons spécifier :

  • Le langage à utiliser ainsi que sa version (Java, Groovy ou Kotlin)
  • L’outil de build (Maven ou Gradle)
  • Le framework de test (JUnit, Kotest ou Spock)
  • Le nom du package racine et de l’application (com.example.demo)
  • Des fonctionnalités additionnelles si besoin (dans notre cas JDBC, Flyway et le driver Postgres)

À ce stade, vous pouvez voir tous les fichiers que la commande Micronaut a générés pour vous, notamment le fichier de configuration application.yml contenant les informations de votre application ainsi que la configuration de votre datasource.

micronaut:
 application:
   name: demo
datasources:
 default:
   url: jdbc:postgresql://localhost:5432/postgres
   driverClassName: org.postgresql.Driver
   username: postgres
   password: ''
   schema-generate: CREATE_DROP
   dialect: POSTGRES

Vous pouvez aussi regarder le POM Maven pour constater toutes les dépendances importées.

Micronaut Flyway

Flyway est un outil permettant de maintenir à jour un schéma de base de données. Les modifications apportées à la base sont nommées des migrations. Chaque migration est versionnée, ainsi l'outil s'appuie sur un historique de version. Micronaut nous permet de facilement l’intégrer à notre application et de le configurer en un minimum d'efforts.

Dans un premier temps, nous allons créer le dossier migrations sous le répertoire src/main/resources qui va contenir tous nos scripts de migration.

Ensuite, il faut ajouter une configuration dans le fichier application.yml pour indiquer à Flyway l’emplacement des scripts.

flyway:
 datasources:
   default:
     locations: classpath:migrations

Enfin, dans le dossier migrations, nous allons créer le script V1__create-user-schema.sql contenant l’instruction SQL pour la création de la table user.

CREATE TABLE "user"
(
   id           uuid    NOT NULL,
   first_name   varchar NOT NULL,
   last_name    varchar NOT NULL,
   birth_date   date    NOT NULL,
   address      varchar NOT NULL,
   phone_number varchar NOT NULL,
   CONSTRAINT user_pk PRIMARY KEY (id)
);

Au démarrage de l’application, Flyway exécutera le script si celui-ci ne l’a pas déjà été.

Micronaut Data

Micronaut Data nous offre toutes les fonctionnalités nécessaires pour interagir avec la base de données et est compatible avec JDBC et JPA.

Tout d’abord nous allons créer la classe d’entité User avec les mêmes champs que la table.

@MappedEntity
public class User {

 @Id
 private UUID id;

 private String firstName;

 private String lastName;

 private LocalDate birthDate;

 private String address;

 private String phoneNumber;

 // Getters, Setters, Constructors, Equals
}

  • @MappedEntity permet de spécifier à Micronaut que nous voulons mapper les données de la table user avec cette entité
  • @Id indique le champ correspondant à la clé primaire

Il faut ensuite créer l’interface repository.

@JdbcRepository(dialect = Dialect.POSTGRES)
public interface UserRepository extends CrudRepository<User, UUID> {

 List<User> find(@NotNull String firstName, @NotNull String lastName);

 List<User> saveAll(@NotEmpty List<User> users);
}

Notre interface doit être annotée avec @JdbcRepository avec le dialecte spécifié (dans notre cas Postgres) et étendre l’interface CrudRepository<User, UUID> proposant un ensemble de méthodes CRUD. Nous ajoutons deux méthodes supplémentaires pour rechercher des utilisateurs à partir de leur nom et prénom et pour sauvegarder un ensemble d'utilisateurs.

Micronaut HTTP

Dans cette partie nous allons créer un controller permettant de créer et de rechercher des utilisateurs.

@Controller("/users")
public class UserController {

   private final UserRepository users;

   public UserController(UserRepository users) {
       this.users = users;
   }

   @Get
   public List<User> find(@QueryValue String firstName, @QueryValue String lastName) {
       return this.users.find(firstName, lastName);
   }

   @Post
   public List<User> saveAll(@Body List<User> users) {
       users.forEach(user -> user.setId(UUID.randomUUID()));
       return this.users.saveAll(users);
   }
}

La classe doit être annotée avec @Controller avec l’URI de base spécifié pour accéder à cette ressource. On injecte le bean repository en utilisant l’injection via le constructeur et on ajoute les méthodes GET et POST.

Les tests

Avec Micronaut les tests d’intégration sont simples à mettre en œuvre et requièrent un minimum de configuration. Ceci est un atout majeur pour tester nos endpoints.

Dans un premier temps, nous allons créer un script SQL pour insérer des données de test src/test/resources/migrations/afterMigrate.sql, que Flyway exécutera une fois la migration terminée.

INSERT INTO "user" (id, first_name, last_name, birth_date, address, phone_number)
VALUES('bfdf4b3d-ede1-472b-94ad-96845a5820da', 'firstName1', 'lastName1', '2021-01-01', 'address1', '0100000000');

Dans un second temps, nous allons ajouter la classe de test.

@MicronautTest
class UserControllerTest {

 @Inject
 @Client("/")
 private HttpClient httpClient;

 @Test
 void find() {
   MutableHttpRequest<?> request = HttpRequest.GET("/users");
   request.getParameters()
       .add("firstName", "firstName1")
       .add("lastName", "lastName1");

   List<User> expectedUsers = List.of(new User(UUID.fromString("bfdf4b3d-ede1-472b-94ad-96845a5820da"), "firstName1",
       "lastName1", LocalDate.parse("2021-01-01"), "address1", "0100000000"));

   List<User> actualUsers = this.httpClient.toBlocking().retrieve(request, Argument.listOf(User.class));

   assertEquals(expectedUsers, actualUsers);
 }
}

Dans cet exemple, nous voulons tester si le endpoint GET /users retourne bien une liste contenant l’utilisateur que nous avons inséré en base. Pour ce faire, il suffit d’annoter notre classe de test avec @MicronautTest et d’injecter le client HTTP en spécifiant l’id du client (“/” pour les tests). On effectue l’appel au serveur et on vérifie le résultat obtenu. À noter que le client HTTP de Micronaut est non bloquant par défaut. Néanmoins pour les tests, nous devons effectuer un appel bloquant.

Pour effectuer le même appel de manière asynchrone, il faut utiliser les fonctionnalités de Reactor pour récupérer et traiter le résultat.

Mono.from(this.httpClient.retrieve(HttpRequest.GET("/users"), Argument.listOf(User.class))).subscribe(users -> {
 // TODO
});

Compilation AOT et GraalVM

La compilation AOT (Ahead-Of-Time) est disponible depuis Java 9 et permet de compiler du code Java en code natif. Elle est à différencier de la compilation JIT (Just-In-Time) compilant à la volée du bytecode en code natif. La compilation AOT offre ainsi de meilleures performances notamment au démarrage de la JVM.

GraalVM est une machine virtuelle multi-plateforme compatible avec plusieurs langages de programmation et nous permet d’utiliser la compilation AOT grâce à la fonctionnalité Native Image. On peut utiliser cette fonctionnalité avec Micronaut pour compiler notre application en code natif et ainsi observer un gain de performance important au démarrage. Ceci est un atout majeur dans une architecture serverless où le temps de démarrage est un critère essentiel.

Pour ce faire, Micronaut nous propose de nous baser sur l’image Docker graalvm/native-image.

Il suffit de lancer une commande Maven en spécifiant le type de packaging docker-native (par défaut à jar dans le POM).

mvn package -Dpackaging=docker-native

À la fin de l’exécution de la commande, cela nous produira un container Docker prêt à démarrer.

docker run demo:latest

Enfin, nous pouvons tester notre microservice à l’aide de la commande cURL.

curl -X POST -H "Content-Type: application/json" -d '[{"firstName": "firstName1", "lastName": "lastName1", "birthDate": "2021-01-01", "address": "address1", "phoneNumber": "0100000000"}]' http://localhost:8080/users

curl "http://localhost:8080/users?firstName=firstName1&lastName=lastName1"

Conclusion

Nous avons globalement vu ce qu’était le framework Micronaut et parcouru une partie de ce qu’il proposait. On peut le considérer comme une alternative à Spring, si l’on souhaite développer des microservices légers et rapides au démarrage. Néanmoins, il ne faut pas négliger le fait que ce framework est encore récent, impliquant ainsi une difficulté probable à trouver de l’expertise, un écosystème qui n’est pas encore complètement mature et une communauté de développeurs réduite. De plus, si on souhaite utiliser la compilation AOT, il est important de prendre en compte qu'elle a un coût non négligeable (cf. https://blog.ippon.fr/2020/04/22/quarkus-est-il-lavenir-de-java/) et qu’elle n’est pas encore très utilisée à ce jour.