API de restitution d’un CSV avec Quarkus en mode natif
Introduction #
Dans le cadre d’un projet personnel, j’ai eu besoin d’écrire une API très simple permettant de restituer le contenu d’un fichier CSV.
J’ai décidé d’écrire un court article sur le sujet car le code snippet peut être utile à d’autres mais aussi parce que c’est l’occasion d’illustrer quelques mécanismes de Quarkus, notamment en mode natif.
L’application que nous allons écrire en guise de démonstration va:
- Dès le démarrage, intégrer un CSV contenant le top 1000 des films IMDB (source datant de plus de 3 ans donc ne vous affolez pas si vous ne trouvez pas vos récents films préférés 😉)
- Proposer un endpoint qui retourne un film au hasard avec l’ensemble des informations issues du CSV
C’est parti pour la création du projet et le lancement du dev:
quarkus create app dlemontagner.movies:top1000-imdb-movies --extensions=quarkus-rest-jackson --no-code
cd top1000-imdb-movies
quarkus dev
Puis nous allons créer un répertoire data dans src/main/resources et y copier notre source de données imdb_top_1000.csv.
Enfin, nous allons indiquer le chemin de ce fichier dans notre application.properties pour nous en servir tout à l’heure:
movies.csv.src=data/imdb_top_1000.csv
Intégration et restitution du CSV #
Stockage de la donnée #
Pour commencer, nous devons construire un record qui représente la donnée du CSV à l’identique c’est à dire en s’appuyant sur les libellés d’en tête du CSV:
package dlemontagner.movies;
public record Movie(
String Poster_Link,
String Series_Title,
String Released_Year,
String Certificate,
String Runtime,
String Genre,
float IMDB_Rating,
String Overview,
int Meta_score,
String Director,
String Star1,
String Star2,
String Star3,
String Star4,
long No_of_Votes,
String Gross
) {
}
Nous allons ensuite créer une classe MoviesRepository pour stocker les records, et les retourner:
package dlemontagner.movies;
import jakarta.enterprise.context.ApplicationScoped;
import java.util.List;
import java.util.Random;
@ApplicationScoped
public class MoviesRepository {
private List<Movie> movies;
private Random rand;
public void setMovies(List<Movie> movies) {
this.movies = movies;
this.rand = new Random();
}
public List<Movie> getAll(){
return movies;
}
public Movie getRandom(){
return movies.get(rand.nextInt(movies.size()));
}
}
Nota Bene: s’agissant d’une démonstration visant davantage à partager un extrait de code qu’une architecture, je me passe exceptionnellement d’abstractions et de layers architecturaux 😉 N’hésitez pas à consulter mes articles sur l’architecture si vous avez besoin d’inspiration à ce sujet!
Parsing du CSV avec Jackson #
Pour le parsing du CSV, nous allons nous appuyer sur Jackson en ajoutant les dépendances suivantes:
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-csv</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
Et voici le code de notre intégrateur de données:
package dlemontagner.movies;
import com.fasterxml.jackson.databind.MappingIterator;
import com.fasterxml.jackson.databind.ObjectReader;
import com.fasterxml.jackson.dataformat.csv.CsvMapper;
import com.fasterxml.jackson.dataformat.csv.CsvSchema;
import io.quarkus.logging.Log;
import io.quarkus.runtime.StartupEvent;
import jakarta.enterprise.event.Observes;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
public class MoviesCSVIngestor {
private final String moviesCSVSrc;
private final MoviesRepository moviesRepository;
public MoviesCSVIngestor(@ConfigProperty(name = "movies.csv.src") String moviesCSVSrc, MoviesRepository moviesRepository) {
this.moviesCSVSrc = moviesCSVSrc;
this.moviesRepository = moviesRepository;
}
public void ingest(@Observes StartupEvent event) throws IOException {
Log.infof("Ingesting source: %s", moviesCSVSrc);
try (MappingIterator<Movie> it = this.fetchData()) {
List<Movie> movies = it.readAll();
moviesRepository.setMovies(movies);
Log.infof("Ingested %d movies", movies.size());
}
}
public MappingIterator<Movie> fetchData() throws IOException {
ObjectReader objectReader = new CsvMapper()
.readerFor(Movie.class)
.with(CsvSchema.emptySchema().withHeader());
InputStream csvStream = getClass().getClassLoader().getResourceAsStream(moviesCSVSrc);
return objectReader.readValues(csvStream);
}
}
Quelques points d’attention concernant ce code:
- L’usage de l’annotation @ConfigProperty(name = "movies.csv.src") de façon à récupérer le chemin vers la source de données qui a été indiqué dans le fichier applications.properties
- L’usage de l’annotation @Observes pour écrire une méthode qui se déclenche dès le lancement de l’application
- La mécanique de lecture du CSV s’appuie sur un CsvMapper et le mapping du header avec les composantes du record se fait grâce à CsvSchema.emptySchema().withHeader().
- Nous utilisons une source de données locale stockée dans les ressources de l’application mais il est tout à fait possible d’aller chercher le fichier CSV sur un serveur http. Pour ce faire, il faut supprimer la lecture de la resource dans la variable csvStream et remplacer le readValues(csvStream) par la ligne ci-dessous où moviesHTTPSrc est l’URL d’accès au fichier:
return objectReader.readValues(URI.create(moviesHTTPSrc).toURL());
Exposition de la donnée #
Il ne nous reste plus qu’à exposer les données disponibles avec le code ci-dessous:
package dlemontagner.movies;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import java.util.List;
@Path("/")
public class MoviesResource {
public final MoviesRepository moviesRepository;
public MoviesResource(MoviesRepository moviesRepository) {
this.moviesRepository = moviesRepository;
}
@GET
@Produces(MediaType.APPLICATION_JSON)
public List<Movie> allMovies(){
return moviesRepository.getAll();
}
@GET
@Produces(MediaType.APPLICATION_JSON)
@Path("/random")
public Movie randomMovie(){
return moviesRepository.getRandom();
}
}
Rien de particulier à ce niveau, nous sommes sur un code standard pour une API Quarkus. Encore une fois, l’enjeu de l’article n’étant pas l’architecture, je prends un gros raccourci en accédant directement au repository.
Vous pouvez désormais faire appel à l’API pour obtenir un film au hasard:
~$ http :8080/random
HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
content-length: 636
{
"Certificate": "U",
"Director": "John Landis",
"Genre": "Action, Adventure, Comedy",
"Gross": "57,229,890",
"IMDB_Rating": 7.9,
"Meta_score": 60,
"No_of_Votes": 183182,
"Overview": "Jake Blues, just released from prison, puts together his old band to save the Catholic home where he and his brother Elwood were raised.",
"Poster_Link": "https://m.media-amazon.com/images/M/MV5BYTdlMDExOGUtN2I3MS00MjY5LWE1NTAtYzc3MzIxN2M3OWY1XkEyXkFqcGdeQXVyNzkwMjQ5NzM@._V1_UX67_CR0,0,67,98_AL_.jpg",
"Released_Year": "1980",
"Runtime": "133 min",
"Series_Title": "The Blues Brothers",
"Star1": "John Belushi",
"Star2": "Dan Aykroyd",
"Star3": "Cab Calloway",
"Star4": "John Candy"
}
Particularités de Quarkus en mode natif #
Lorsque l’on travaille sur du java en mode natif, il faut être vigilant à certaines contraintes qui vont de pair avec les bénéfices du procédé.
Les ressources #
Ainsi, par défaut, quand un exécutable natif est construit par GraalVM (sur lequel s’appuie Quarkus Natif), il n’inclut pas les ressources présentes dans le classpath. Il faut donc préciser les ressources à intégrer.
Fort heureusement, Quarkus propose une configuration simple de ceci via le fichier application.properties:
quarkus.native.resources.includes=data/**
HTTPS #
Autre surprise qui peut se présenter lors de l’usage du mode natif, le HTTPS n’est pas disponible par défaut.
Ainsi, dans notre exemple, si vous cherchez à télécharger un CSV distant sur un serveur HTTPS, ceci ne fonctionnera pas en mode natif.
Encore une fois, l’activation de la fonctionnalité est simple puisqu’il suffit d’ajouter la configuration suivante dans le fichier application.properties:
quarkus.ssl.native=true
Dockeriser son application Quarkus Native #
Pour la dockerisation de l’application Quarkus en mode natif, il est important de bien lire la documentation de Quarkus à ce sujet notamment:
- Construire l’image en utilisant plusieurs étapes afin de ne pas inclure les outils de build dans l’image finale. Le code du DockerFile permettant ceci n’est pas dans le template par défaut de Quarkus mais est disponible dans la documentation
- Editer le .dockerignore intégré par défaut dans le template Quarkus afin de retirer la ligne “*” car Docker a besoin d’avoir accès à Src pour copier le code
La documentation indique de copier le DockerFile dans src/main/docker/Dockerfile.multistage mais vous constaterez que j’ai choisi de le positionner à la racine. C’est un choix de facilité de ma part, libre à vous de suivre les recommandations officielles 😉
Il ne reste plus qu’à construire l’image et la lancer:
docker build -t top1000-imdb-movies .
docker run -i --rm -p 8080:8080 top1000-imdb-movies
Code Source #
Retrouvez l'intégralité du code source dans ce repository.
Conclusion #
Il est donc simple et rapide de construire un service de restitution des données d’un CSV. Nous avons également pu voir qu’il est important de bien tester l’ensemble des fonctionnalités sur une image native lorsque l’on souhaite utiliser ce mode d’exécution. A ce propos, je vous recommande de lire les recommandations de Quarkus pour l’écriture d’une application native.
Bien évidemment, ce code n’est pas adapté lorsque le fichier CSV source devient très imposant mais ceci peut suffire pour des besoins plus modestes. Quand c’est possible, une approche KISS (Keep It Simple, Stupid) est toujours appréciable.
Et maintenant, lorsque vous voulez donner la chance au hasard tout en ayant la garantie de regarder un bon film, il ne vous reste plus qu’à faire appel à cette API 🙂