La JVM fullstack avec Grails 5

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 gout 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 :

sdk install grails 5.1.3
sdk install java 11.0.12-open

Et si vous ne l’avez pas, allez l’installer ainsi :

curl -s "https://get.sdkman.io" | bash
source "$HOME/.sdkman/bin/sdkman-init.sh"

Étape 1 : Pur projet Grails

Pour commencer, on a besoin d’initialiser le projet Grails

grails create-app shorturl
cd shorturl

Allons voir ce que nous avons déjà, en lançant le mode dev avec le wrapper Grails :

./grailsw run-app

On obtient :

Running application...
Grails application running at http://localhost:8080 in environment: development
<===========--> 85% EXECUTING [44s]
> :bootRun

Le dev-mode de Grails supporte le hotreload, laissons donc l’app tourner.

Notre front-end expose la page par défaut de Grails :

welcome page 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 https://t4wan3.github.io/blog/grails/url-shortener-grails. Dans cette url, blog, grails et url-shortener-grails sont des segments.

Grails fait la persistance en base de données grâce à des “classes de domaine” équivalentes à l’association d’Entity et de JpaRepository de Spring.

Créons la classe de domaine pour stocker des ’url’ par ’segment’ :

./grailsw create-domain-class shortUrl

On l’ouvre, et on y crée les attributs :

class ShortUrl {

    String segment
    String url

    static constraints = {
    }
}

Vous voyez le champ constraints ?

Et bien on peut y ajouter des conditions de validation pour chacun des champs :

  • Le segment :
    • 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)
static constraints = {
    segment unique: true, size: 5..10, matches: "[0-9a-zA-Z]*"
    url url: true, blank: false
}

É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 :

./grailsw create-controller ShortUrl

Puis on remplace tout son contenu avec l’instruction :

class ShortUrlController {
    static scaffold = ShortUrl
}

Ouvrons le navigateur afin de voir le contenu hot-reloadé. Elle affiche la liste des contrôleurs disponibles :

page available controllers

On peut voir ici notre tout nouveau contrôleur. Ouvrons-le pour voir la page listant les urls raccourcies :

page short url list

Quand le navigateur a appelé l’endpoint du contrôleur avec une requête GET, il y avait ce header :

Accept: application/html

Le contrôleur l’interprète afin de répondre avec une page HTML listant toutes les ShorUrl stockées.

Regardons ce que fait le bouton New ShortUrl. Il ouvre une page avec un formulaire qui permet de créer de nouvelles ShortUrl. :

create short url

C’est proche de ce qu’on aimerait avoir comme page d’accueil !

Quand on crée une ShortUrl, on est redirigé vers la page show de l’objet créé.

show short url

Essayons le lien. Si je préfixe le base-path avec le segment, j’obtiens http://localhost:8080/k2m47. Mais ce lien redirige vers la page 404 :

404 page not found

Étape 4 : Configurer la redirection

Fondamentalement, on veut que http://localhost:8080/k2m47 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 redirect dans le ShortUrlController :

class UrlMappings {
    static mappings = {
        "/$controller/$action?/$id?(.$format)?"{
            constraints {
            }
        }
        
        "/$segment"(controller: 'shortUrl', action: 'redirect')

        "/"(view:"/index")
        "500"(view: '/error')
        "404"(view: '/notFound')
    }
}
class ShortUrlController {
    
    [...]
    
    def redirect(String segment) {
    redirect uri: ShortUrl.findBySegment(segment)?.url
    }
}

Ouvrons à nouveau l’url raccourcie : http://localhost:8080/k2m47

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 UrlMappings.groovy (qui existe déjà) :

class UrlMappings {
    static mappings = {
        "/$controller/$action?/$id?(.$format)?"{
            constraints {
            }
        }
        
        "/$segment"(controller: 'shortUrl', action: 'redirect')

        "/"(controller: 'shortUrl', action: "create")
        "500"(view: '/error')
        "404"(view: '/notFound')
    }
}

Quand un utilisateur accède à /, il va être redirigé vers l’action create du contrôleur nommé ShortUrlController.

De quelle action parles-tu ? Il n’y a aucune méthode dans ShortUrlController.

Si, il y en a. Les actions create, save, get, update 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 show, index, save. Et surtout pas update.

On utilise aussi la closure constraints afin de :

  • Autoriser seulement le contrôleur ShortUrlController
  • Autoriser seulement les actions show, index, save
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')
    }
}

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 nullable :

static constraints = {
    segment unique: true, size: 5..10, matches: "[0-9a-zA-Z]*", nullable: true
    url url: true, blank: false
}

segment optionel

Bien mieux !

Mais on doit maintenant en générer un aléatoirement si non renseigné.

Initialisons-le dans la méthode beforeValidate de ShortUrl (si non fourni par l’utilisateur) :

import static org.apache.commons.lang3.RandomStringUtils.randomAlphanumeric

class ShortUrl {

[...]

    void beforeValidate() {
        int segmentMinSize = constrainedProperties.segment.size.from as int
        segment ?= RandomStringUtils.randomAlphanumeric(segmentMinSize)
    }
}

beforeValidate se déclenche dès qu’on tente d’ajouter/modifier une entité en base de données.

On réutilise la contrainte min size en tant que taille par défaut.


Note rapide sur l’aléatoire

  1. Ici, le segment n’a pas à être sécurisé. On veut juste des mots de 5 caractères.
  2. Sur un mot de 5 caractères alphanumériques, des collisions peuvent survenir. On peut alors ajouter un peu de code de validation :
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'
  }
}

À 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 validate()).


Étape 8 : Changer la redirection sur un submit de create

page show url

Quand on veut une ShortUrl, 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é ShortUrl créée.

La page show 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 ShortUrl dans une div conditionnelle.

Pour ça, surchargeons l’implémentation scaffoldée show :

def show(Long id) {
    redirect action: 'create', params: [id: id]
}

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 :

  1. On vient juste d’ouvrir la page
  2. On vient juste de soumettre une nouvelle ShortUrl depuis une précédente page de création

Pour le second cas, on a un params.id 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.

def create(String id) {
    respond new ShortUrl(params), model: [created: ShortUrl.get(id)]
}

Maintenant, on surcharge la page de création en générant les 4 vues (index, show, edit, create) :

./grailsw generate-views ShortUrl

À la fin du body du fichier create.gsp, on ajoute la div conditionnelle qui sert à afficher la potentielle ShortUrl toute juste créée :

[...]
</div>
<g:if test="${created}">

</g:if>
</body>
</html>

Le code d’affichage d’une ShortUrl se trouve dans le fichier show.gsp. Copions-le ici :

[...]
</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>

Ensuite on génère le lien de redirection avec createLink et on l’affiche :

[...]
</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>

Et enfin, on supprime les vues non surchargées et inutilisées :

rm grails-app/views/shortUrl/show.gsp
rm grails-app/views/shortUrl/index.gsp
rm grails-app/views/shortUrl/edit.gsp

Super ! Maintenant on peut voir la ShortUrl créée sur la même page :

page create short url et shortened url

É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 ShortUrl créées ressemble à ça :

vue par defaut

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 ShortUrl :

./grailsw generate-views ShortUrl
rm grails-app/views/shortUrl/index.gsp

Ensuite, on ajoute le template à utiliser sur l’élément <f:table> :

[...]
<f:table collection="${shortUrlList}" template="shortUrlList" />
[...]

Puis on crée le template. Il doit se trouver dans grails-app/views/templates/_fields/_shortUrlList.gsp

Le contenu par défaut peut être trouvé dans le grails-fields-plugin. 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 :

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

Voici le résultat :

vue url customisee

É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 layouts/main.gsp (rappel : nous sommes dans un framework de templating).

Pour le nouveau logo :

<a class="navbar-brand" href="/#"><asset:image src="axe.svg" alt="Tawane’s url shortener Logo"/></a>

Avec un peu de redimensionnement dans grails-app/assets/stylesheets/grails.css :

a.navbar-brand img {
    height: 55px;
}

Le nouveau footer :

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

On obtient finale :

page accueil finale

short url list

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 ShortUrl 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 messages_ru.properties, 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.