Après avoir passé quasiment 2 ans en mission avec Grails au travers de 2 produits d'assurance fullstack, j'ai acquis la certitude que les outils RAD sont toujours au goût du jour.
Ce framework exploite encore plus le principe de Spring Boot : "convention-over-configuration" (des configurations par défaut plutôt que de la configuration manuelle).
Il se positionne sur le marché des frameworks JVM comme étant fullstack, en intriquant le frontend et le backend au travers de Server-Side-Rendering, de templating html, et d'API Rest.
De plus, il offre une joint-compilation native Java/Groovy, afin de tirer le meilleur des 2 langages :
- Java : typage fort, excellentes performances
- Groovy : typage dynamique, métaprogrammation, high-order functions, nombreux opérateurs, programmation fonctionnelle
Et enfin, Grails est hautement compatible avec l'écosystème Spring puisque basé dessus.
Toutefois, j'observe dans mon réseau professionnel que Grails est encore peu connu, d'où mon envie de montrer au travers de cet article tout ce qu'il est possible de faire avec, rapidement, facilement, et simplement.
Pour ça, il me faut un sujet de démonstration.
J’ai justement regardé récemment ce live coding de la chaîne YouTube Coding Garden.
Il y code un site web de réduction d’url from scratch en 1 heure avec NodeJs.
Je me suis dit :
Waou, quelle maitrise de ses outils !
Puis, je me suis demandé :
Pourrais-je en coder un aussi vite avec Grails ?
C'est parti !
Qu’est-ce qu’un réducteur d’url ?
Très simple.
Vous avez une longue URL et pour x raisons, vous avez besoin qu’elle soit minuscule (exemples : pour s’en souvenir, l’afficher).
Vous allez donc sur un outil de raccourcissement d’url. Vous lui donnez votre longue url. Il vous donne en retour une petite url qui redirige vers la vôtre.
Quels outils composent Grails 5
La version de Grails utilisée ici est la 5.1.3, et inclut ces outils (avec quelques upgrades de version perso) :
- Gradle 7.4.1
- Java 15
- Groovy 3.0.10
- Hibernate 5.5
- Micronaut 3.2.7
- Springboot 2.6.4
- Tomcat 9.0
- Spring 5.3.16
- Groovy Server Pages 5.1.0
Java 15 est la plus haute version ayant une compatibilité totale avec Groovy 3. Pour Java 17/18, il faut attendre Groovy 4 et Grails 6.
Vous pouvez me trouver old-school, mais j’aime le templating HTML Java. Vous avez été traumatisé par les JSP/Jstl ? Pas d'inquiétude, vous allez voir que les Groovy Server Pages sont fantastiquement simples et puissantes.
Pré-requis
- Un JDK entre 8 et 15
- Grails 5.1.3
- (facultatif) Intellij => excellent support de Grails
Si vous avez sdkman, voici les commandes d’installation :
<pre><code>sdk install grails 5.1.3
sdk install java 11.0.12-open<code><pre>
Et si vous ne l’avez pas, allez l’installer ainsi :
<pre><code>curl -s "https://get.sdkman.io" | bash
source "$HOME/.sdkman/bin/sdkman-init.sh"<code><pre>
Étape 1 : Pur projet Grails
Pour commencer, on a besoin d’initialiser le projet Grails
<pre><code>grails create-app shorturl
cd shorturl<code><pre>
Allons voir ce que nous avons déjà, en lançant le mode dev avec le wrapper Grails :
<pre><code>./grailsw run-app<code><pre>
On obtient :
<pre><code>Running application...
Grails application running at http://localhost:8080 in environment: development
<===========--> 85% EXECUTING [44s]
> :bootRun<code><pre>
Le dev-mode de Grails supporte le hotreload, laissons donc l’app tourner.
Notre front-end expose la page par défaut de Grails :
Étape 2 : Créer l’entité principale
Fondamentalement, ce que nous voulons, c’est stocker des ’url’ par ’segment’. Vous savez déjà ce qu’est une url. Un segment est un morceau d’URL, entre des slashs. Prenons par exemple <span class="css-span">https://t4wan3.github.io/blog/grails/url-shortener-grails</span>. Dans cette url, <span class="css-span">blog</span>, <span class="css-span">grails</span> et <span class="css-span">url-shortener-grails</span> sont des segments.
Grails fait la persistance en base de données grâce à des "classes de domaine" équivalentes à l’association d’<span class="css-span">Entity</span> et de <span class="css-span">JpaRepository</span> de Spring.
Créons la classe de domaine pour stocker des ’url’ par ’segment’ :
<pre><code>./grailsw create-domain-class shortUrl<code><pre>
On l’ouvre, et on y crée les attributs :
<pre><code>class ShortUrl {
String segment
String url
static constraints = {
}
}<code><pre>
Vous voyez le champ <span class="css-span">constraints</span> ?
Et bien on peut y ajouter des conditions de validation pour chacun des champs :
<span class="css-span">Le segment </span>:
- Doit être unique
- Doit avoir entre 5 et 10 caractères
- Doit être en ascii
- L’url :
- Doit être une url valide
- Ne dois pas être blank (vide)
<pre><code>static constraints = {
segment unique: true, size: 5..10, matches: "[0-9a-zA-Z]*"
url url: true, blank: false
}<code><pre>
Étape 3 : Scaffolder the ShortUrl entity
La traduction littérale de "scaffold" est "échafauder". Dans notre contexte, cela signifie "générer automatiquement une hiérarchie de structures de données depuis une graine initiale".
Maintenant que notre domaine est modélisé, nous pouvons supposer que notre application est assez simple pour utiliser un CRUD.
Nous n’avons qu’une seule entité et la seule opération CRUD que nous voulons est CREATE.
Construire un formulaire frontend pour l’opération CREATE est une roue, et Grails sait que nous ne voulons pas la réinventer.
Et donc il peut la scaffolder pour nous.
Grails implémente Micronaut for Spring, et donc nous travaillons là sur un MVC.
Le scaffolding peut commencer depuis un contrôleur. On peut soit :
- Exécuter la commande de scaffolding, ce qui va alors générer les fichiers dans les sources.
- Déclarer l’instruction de scaffolding dans un contrôleur, ce qui va alors référencer les fichiers dans le build.
Essayons la 2ᵉ solution :
<pre><code>./grailsw create-controller ShortUrl<code><pre>
Puis on remplace tout son contenu avec l’instruction :
<pre><code>class ShortUrlController {
static scaffold = ShortUrl
}<code><pre>
Ouvrons le navigateur afin de voir le contenu hot-reloadé. Elle affiche la liste des contrôleurs disponibles :
On peut voir ici notre tout nouveau contrôleur. Ouvrons-le pour voir la page listant les urls raccourcies :
Quand le navigateur a appelé l’endpoint du contrôleur avec une requête GET, il y avait ce header :
<span class="css-span">Accept: application/html</span>
Le contrôleur l’interprète afin de répondre avec une page HTML listant toutes les <span class="css-span">ShorUrl</span> stockées.
Regardons ce que fait le bouton <span class="css-span">New ShortUrl</span>. Il ouvre une page avec un formulaire qui permet de créer de nouvelles <span class="css-span">ShortUrl.</span> :
C’est proche de ce qu’on aimerait avoir comme page d’accueil !
Quand on crée une <span class="css-span">ShortUrl</span>, on est redirigé vers la page show de l’objet créé.
Essayons le lien. Si je préfixe le base-path avec le segment, j’obtiens <span class="css-span">http://localhost:8080/k2m47</span>. Mais ce lien redirige vers la page 404 :
Étape 4 : Configurer la redirection
Fondamentalement, on veut que <span class="css-span">http://localhost:8080/k2m47</span> redirige vers la longue url associée stockée. On crée donc la redirection interne depuis ce pattern d’url vers une nouvelle action nommée <span class="css-span">redirect</span> dans le <span class="css-span">ShortUrlController</span> :
<pre><code>class UrlMappings {
static mappings = {
"/$controller/$action?/$id?(.$format)?"{
constraints {
}
}
"/$segment"(controller: 'shortUrl', action: 'redirect')
"/"(view:"/index")
"500"(view: '/error')
"404"(view: '/notFound')
}
}<code><pre>
<pre><code>class ShortUrlController {
[...]
def redirect(String segment) {
redirect uri: ShortUrl.findBySegment(segment)?.url
}
}<code><pre>
Ouvrons à nouveau l’url raccourcie : <span class="css-span">http://localhost:8080/k2m47</span>
Magique, l’url raccourcie apparait !
Étape 5 : Changer la page d’accueil
À présent que la redirection fonctionne, on voudrait changer la page d’accueil.
On peut y parvenir avec le fichier <span class="css-span">UrlMappings.groovy</span> (qui existe déjà) :
<pre><code>class UrlMappings {
static mappings = {
"/$controller/$action?/$id?(.$format)?"{
constraints {
}
}
"/$segment"(controller: 'shortUrl', action: 'redirect')
"/"(controller: 'shortUrl', action: "create")
"500"(view: '/error')
"404"(view: '/notFound')
}
}<code><pre>
Quand un utilisateur accède à /, il va être redirigé vers l’action create du contrôleur nommé <span class="css-span">ShortUrlController</span>.
De quelle action parles-tu ? Il n’y a aucune méthode dans ShortUrlController.
Si, il y en a. Les actions <span class="css-span">create</span>, <span class="css-span">save</span>, <span class="css-span">get</span>, <span class="css-span">update</span> sont injectées à la compile-time dans le contrôleur, grâce au scaffolding.
Étape 6 : Interdire les actions inutiles
Dans notre cas d’utilisation, on ne veut que les actions <span class="css-span">show</span>, <span class="css-span">index</span>, <span class="css-span">save</span>. Et surtout pas <span class="css-span">update</span>.
On utilise aussi la closure constraints afin de :
- Autoriser seulement le contrôleur <span class="css-span">ShortUrlController</span>
- Autoriser seulement les actions show, index, save
<pre><code>class UrlMappings {
static mappings = {
"/$controller/$action?/$id?(.$format)?"{
constraints {
controller matches: 'shortUrl'
action inList: ['show', 'index', 'save']
}
}
"/"(controller: 'shortUrl', action: "create")
"500"(view: '/error')
"404"(view: '/notFound')
}
}<code><pre>
Si la contrainte de validation échoue, alors l’utilisateur est redirigé (par convention) vers la page 404.
Étape 7 : Rendre le segment facultatif
Grails construit le formulaire à partir des attributs et des contraintes de l’entité.
Rendons le segment <span class="css-span">nullable</span> :
<pre><code>static constraints = {
segment unique: true, size: 5..10, matches: "[0-9a-zA-Z]*", nullable: true
url url: true, blank: false
}<code><pre>
Bien mieux !
Mais on doit maintenant en générer un aléatoirement si non renseigné.
Initialisons-le dans la méthode <span class="css-span">beforeValidate</span> de <span class="css-span">ShortUrl</span> (si non fourni par l’utilisateur) :
<pre><code>import static org.apache.commons.lang3.RandomStringUtils.randomAlphanumeric
class ShortUrl {
[...]
void beforeValidate() {
int segmentMinSize = constrainedProperties.segment.size.from as int
segment ?= RandomStringUtils.randomAlphanumeric(segmentMinSize)
}
}<code><pre>
<span class="css-span">beforeValidate</span> se déclenche dès qu’on tente d’ajouter/modifier une entité en base de données.
On réutilise la contrainte <span class="css-span">min size</span> en tant que taille par défaut.
Note rapide sur l’aléatoire
- Ici, le segment n’a pas à être sécurisé. On veut juste des mots de 5 caractères.
- Sur un mot de 5 caractères alphanumériques, des collisions peuvent survenir. On peut alors ajouter un peu de code de validation :
<pre><code>class ShortUrl {
[...]
protected String getRandomAlphaNumeric() {
def segmentMinSize = constrainedProperties.segment.size.from as int
RandomStringUtils.randomAlphanumeric(segmentMinSize)
}
def beforeValidate() {
if (!segment) {
do {
segment = randomAlphaNumeric
} while (hasDuplicatedSegment())
}
}
boolean hasDuplicatedSegment() {
!validate() &&
errors.fieldErrors?.find { it.field == 'segment' }?.code == 'unique'
}
}<code><pre>
À présent, un mot aléatoire est généré jusqu’à ce qu’il soit unique (Grails va vérifier dans la base de données pendant le <span class="css-span">validate()</span>).
Étape 8 : Changer la redirection sur un <span class="css-span">submit </span>de <span class="css-span">create</span>
Quand on veut une <span class="css-span">ShortUrl</span>, en soumettant le formulaire, l’action de save est exécutée, et on est redirigé par convention sur la page show montrant l’entité <span class="css-span">ShortUrl </span>créée.
La page <span class="css-span">show</span> n’a pas de valeur dans notre MVP, et donc on préfère être redirigé sur une nouvelle page de création qui contient aussi la nouvelle <span class="css-span">ShortUrl</span> dans une <span class="css-span">div</span> conditionnelle.
Pour ça, surchargeons l’implémentation scaffoldée <span class="css-span">show</span> :
<pre><code>def show(Long id) {
redirect action: 'create', params: [id: id]
}<code><pre>
On lui donne en paramètre l’id de l’entité tout juste créée dans le but d’être capable de récupérer cette entité et de l’afficher sur le page de création.
Étape 9 : La div conditionnelle sur la page de création
Quand on arrive sur la page de création, il y a deux cas d’utilisation possibles :
- On vient juste d’ouvrir la page
- On vient juste de soumettre une nouvelle <span class="css-span">ShortUrl</span> depuis une précédente page de création
Pour le second cas, on a un <span class="css-span">params.id</span> non-null, et on s’en sert alors pour récupérer en base de données l’entité associée, et on l’ajoute au model de la vue.
<pre><code>def create(String id) {
respond new ShortUrl(params), model: [created: ShortUrl.get(id)]
}<code><pre>
Maintenant, on surcharge la page de création en générant les 4 vues (index, show, edit, create) :
<pre><code>./grailsw generate-views ShortUrl<code><pre>
À la fin du body du fichier <span class="css-span">create.gsp</span>, on ajoute la div conditionnelle qui sert à afficher la potentielle <span class="css-span">ShortUrl</span> toute juste créée :
<pre><code>[...]
</div>
<g:if test="${created}">
</g:if>
</body>
</html>
<code><pre>
Le code d’affichage d’une <span class="css-span">ShortUrl</span> se trouve dans le fichier <span class="css-span">show.gsp</span>. Copions-le ici :
<pre><code>[...]
</div>
<g:if test="${created}">
<div id="show-shortUrl" class="content scaffold-show" role="main">
<h1>Shortened url :</h1>
<g:if test="${flash.message}">
<div class="message" role="status">${flash.message}</div>
</g:if>
</div>
</g:if>
</body>
</html><code><pre>
Ensuite on génère le lien de redirection avec <span class="css-span">createLink</span> et on l’affiche :
<pre><code>[...]
</div>
<g:if test="${created}">
<div id="show-shortUrl" class="content scaffold-show" role="main">
<h1>Shortened url :</h1>
<g:if test="${flash.message}">
<div class="message" role="status">${flash.message}</div>
</g:if>
<g:set var="link" value="${createLink(uri: "/${created.segment}", absolute: true)}"/>
<a href="${link}">${link}</a>
</div>
</g:if>
</body>
</html><code><pre>
Et enfin, on supprime les vues non surchargées et inutilisées :
<pre><code>rm grails-app/views/shortUrl/show.gsp
rm grails-app/views/shortUrl/index.gsp
rm grails-app/views/shortUrl/edit.gsp<code><pre>
Super ! Maintenant on peut voir la <span class="css-span">ShortUrl</span> créée sur la même page :
Étape 10 : Ajouter une meilleure page d’index
Dans Grails, la page d’index correspond à la liste (paginée) des éléments.
Par défaut, la liste de toutes les <span class="css-span">ShortUrl</span> créées ressemble à ça :
On se moque du segment et de sa page de visualisation (show).
Ce qu’on devrait plutôt montrer sur cette page d’index, c’est une table de longues urls par urls raccourcies.
Une solution simple est d’ajouter un template à la page d’index scaffoldée. Dans ce template, on indique au système GSP comment faire le rendu de la table.
Générons à nouveau les vues scaffoldées de <span class="css-span">ShortUrl</span> :
<pre><code>./grailsw generate-views ShortUrl
rm grails-app/views/shortUrl/index.gsp<code><pre>
Ensuite, on ajoute le template à utiliser sur l’élément <span class="css-span"><f:table></span> :
<pre><code>[...]
<f:table collection="${shortUrlList}" template="shortUrlList" />
[...]<code><pre>
Puis on crée le template. Il doit se trouver dans <span class="css-span">grails-app/views/templates/_fields/_shortUrlList.gsp</span>
Le contenu par défaut peut être trouvé dans le <span class="css-span">grails-fields-plugin</span>. J’ai été le chercher dans son dépôt GitHub : https://github.com/grails-fields-plugin/grails-fields/blob/master/grails-app/views/templates/_fields/_table.gsp
Et je l’ai copié dans mon template afin d’en modifier les noms de colonnes et leur contenu :
<pre><code>
<table>
[...]
<tr>
<th>Short urls</th>
<th>Shortened urls</th>
</tr>
[...]
<td>
<g:link uri="/${bean.segment}">
${g.createLink(uri: "/${bean.segment}", absolute: true)}
</g:link>
</td>
<td>
<a href="${bean.url}">
${bean.url}
</a>
</td>
[...]
</table><code><pre>
Voici le résultat :
Étape 11 - La touche finale
Notre outil ressemble toujours à un site Grails "Get Started". Alors changeons le logo (avec un qui soit libre) et le texte de footer.
On peut faire cela sur toutes les pages en éditant le fichier <span class="css-span">layouts/main.gsp</span> (rappel : nous sommes dans un framework de templating).
Pour le nouveau logo :
<pre><code><a class="navbar-brand" href="/#"><asset:image src="axe.svg" alt="Tawane’s url shortener Logo"/></a><code><pre>
Avec un peu de redimensionnement dans <span class="css-span">grails-app/assets/stylesheets/grails.css</span> :
<pre><code>a.navbar-brand img {
height: 55px;}<code><pre>
Le nouveau footer :
<pre><code><div class="footer row" role="contentinfo">
<div class="col">
<p>Url shortener by tawane</p>
</div>
<div class="col">
<p>Powered by Grails <g:meta name="info.app.grailsVersion"/></p>
</div>
<div class="col">
<p>Served by Heroku with Gradle buildpack</p>
</div>
</div><code><pre>
On obtient finale :
Maintenant, prenons un moment pour jeter un coup d’œil sur tout le code écrit. Ce n’est pas tant que ça comparé au produit obtenu, n’est-ce pas ?
Conclusion
Grâce à Grails on vient de développer une fonctionnalité fullstack complète from scratch au sein de la même JVM, avec très peu de lignes de code.
On est resté concentré sur notre MVP, mais on a malgré tout plein de bonus sympas offerts par Grails :
- La liste des <span class="css-span">ShortUrl</span> est paginée !
- La validation du formulaire html est complète, avec affichage des erreurs.
- On est redirigé vers les pages 404/500 en cas d’erreur.
- On a écrit presque zéro CSS tout en ayant un front décent.
- Les fichiers CSS existent déjà et les classes des templates sont prêtes à être éditées.
- La stack de tests (unit / integration / functional) est prête.
- Notre app est responsive, grâce au fichier mobile.css.
- Notre formulaire est SÉCURISÉ ! Grails échappe chaque saisie utilisateur.
- Les fichiers d’assets (images/css/js) sont minifiés et leurs noms sont hashés grâce au plugin asset-pipeline.
- L’internationalisation est prête : juste en valorisant les labels dans <span class="css-span">messages_ru.properties</span>, les Russes peuvent utiliser le site.
Si vous avez ressenti le pouvoir de Grails, essayez-le avec cette app ou n’importe quelle autre idée, je vous promets que vous allez adorer ce framework.
Les sources complètes sont disponibles sur github.com/t4w4n3/shorturl.
Vous pouvez essayer l’app sur https://intense-lake-67642.herokuapp.com/. Soyez patient, le serveur d’Heroku se coupe automatiquement au bout de 30 minutes d’inactivité. Son redémarrage prend environ 20 secondes si vous l’ouvrez.
Si ça parle Java chez Younup, soyez sûr qu'Antoine n'est jamais très loin !
Spécialiste de Spring Boot et fan de Groovy - pour son langage intuitif, haut niveau et qui permet la métaprogrammation - il est toujours curieux de découvrir de nouvelles technos ou outils qui pourraient booster sa productivité.
Un peu de chocolat, du café et un pc qui tient la route, c'est tout ce dont il a besoin pour coder ! Pas possible de poireauter 4h par jour devant des loaders...
Mais Antoine n'est pas seulement fan de Bob Martin ou de designs modulaires, laissez-lui un après-midi pour customiser son jardin avec un nouveau cabanon ou karchériser sa terrasse.