Aujourd’hui au programme, découverte et test de SpringFuse, l’économiseur de temps de Jaxio. Je n’utilise pas le mot générateur de code, car bien que SpringFuse génère en effet du code, c’est plus le temps gagné qui en fait sa vraie valeur.
Le principe est le suivant : vous avez une base de données classique, Oracle, MySQL, PostgreSQL ou autre. Comment construire une application web en quelques minutes, avec l’ensemble de son code source ? Je pense à Hibernate Tools, à JBoss Seam, à Grails. Finalement c’est Seam qui se rapproche le plus, sans faire tout ce que fait SpringFuse.
SpringFuse génère le code source d’une application Java avec SpringMVC 2.5.6, Hibernate, Spring Security, bref les Usual Suspects que nous connaissons tous. Le projet généré est un projet Maven 2, avec ses tests unitaires, des services comme l’authentification, l’envoi d’email, la gestion des rôles etc. C’est ce que nous codons habituellement nous-même.
A toi lecteur qui est sceptique
Petite discussion pour toi qui se dit « … un générateur de code, je n’en n’ai pas besoin ».
En fait je pense comme toi.
Oui tu n’en n’as pas besoin. Nous vivons dans un monde parfait où chacun dispose d’un temps indéfini pour coder. Dans ce monde parfait, tu es un maître de Spring, tu maîtrises Hibernate et les subtilités du cache de second niveau. Les articles du Touilleur Express sur l’instanciation tardive ? Trop facile. Spring MVC ? Finger in the noise. Tu fais partie bien entendu d’une équipe de 5 développeurs qui tous, avec le même niveau d’expertise, maitrisent à fond Spring. D’ailleur Spring 4 ce sera toi, bref rien à faire d’un outil qui finalement… te remplace mon ami.
Ecrire souvent la même chose, en faisant attention dans mon DAO à faire ceci, à me souvenir de la configuration du cache, à m’assurer des déclarations dans Spring MVC… au bout d’un moment on a déjà vu le film. L’idée du logiciel SpringFuse n’est pas, d’après ce que j’en ai compris et mes tests, de te remplacer, toi lecteur. C’est juste ton meilleur ami qui se met à côté de toi, qui tape un peu plus vite du code, et qui est un peu meilleur que toi. Ensuite le principe c’est de te laisser le code, prêt à tourner avec Jetty, mais que tu puisses continuer à travailler dessus et à mettre par exemple du JQuery afin d’améliorer les Widgets.
Génération spontanée
Il est temps de voir en direct si toutes ses promesses sont tenues. Après m’être inscrit sur le site SpringFuse.com j’ai en fait suivi le tutorial. Celui-ci marche très bien au passage sur Mac. J’ai la version 2.0.6 de maven par défaut sur Mac, ce qui n’a pas posé de problèmes. Après avoir téléchargé le projet d’exemple afin de créer une petite base H2Database, j’ai lancé la génération du modèle SpringFuse.
macbook-pro-de-nicolas-martignole:Dev nicolas$ mkdir SpringFuseTest macbook-pro-de-nicolas-martignole:Dev nicolas$ cd SpringFuseTest/ macbook-pro-de-nicolas-martignole:SpringFuseTest nicolas$ mvn -v Maven version: 2.0.6 macbook-pro-de-nicolas-martignole:SpringFuseTest nicolas$ unzip springfuse-example.zip Archive: springfuse-example.zip inflating: pom.xml inflating: README.txt creating: src/ creating: src/main/ creating: src/main/sql/ creating: src/main/sql/h2/ inflating: src/main/sql/h2/comment.sql inflating: src/main/sql/h2/create.sql inflating: src/main/sql/h2/drop.sql inflating: src/main/sql/h2/init.sql macbook-pro-de-nicolas-martignole:SpringFuseTest nicolas$ mvn initialize [INFO] Scanning for projects... Downloading: http://maven2.springfuse.com/com/h2database/h2/1.1.105/h2-1.1.105.pom Downloading: http://repo1.maven.org/maven2/com/h2database/h2/1.1.105/h2-1.1.105.pom 654b downloaded Downloading: http://maven2.springfuse.com/com/h2database/h2/1.1.105/h2-1.1.105.jar Downloading: http://repo1.maven.org/maven2/com/h2database/h2/1.1.105/h2-1.1.105.jar 1095K downloaded [INFO] ---------------------------------------------------------------------------- [INFO] Building springfuse-example [INFO] task-segment: [initialize] [INFO] ---------------------------------------------------------------------------- Downloading: http://maven2.springfuse.com/org/codehaus/mojo/sql-maven-plugin/1.0/sql-maven-plugin-1.0.pom Downloading: http://repo1.maven.org/maven2/org/codehaus/mojo/sql-maven-plugin/1.0/sql-maven-plugin-1.0.pom ... ... [INFO] [springfuse:extract {execution: Extract Database Meta Data}] [INFO] Ready to extract the database schema [INFO] Database schema extracted [INFO] File data-model.springfuse passed reverse conversion OK [INFO] File data-model.springfuse created successfully [INFO] You are now ready to upload data-model.springfuse to http://www.springfuse.com/ and generate your project! [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESSFUL [INFO] ------------------------------------------------------------------------ [INFO] Total time: 43 seconds [INFO] Finished at: Thu Apr 02 09:08:52 CEST 2009 [INFO] Final Memory: 7M/17M [INFO] ------------------------------------------------------------------------ macbook-pro-de-nicolas-martignole:SpringFuseTest nicolas$ ls -l data-model.springfuse -rw-r--r-- 1 nicolas staff 1764 2 avr 09:08 data-model.springfuse macbook-pro-de-nicolas-martignole:SpringFuseTest nicolas$
L’exécution génère un fichier data-mode.springfuse à partir de la base de données. Le modèle est un fichier au format Google ProtocolBuffer. De retour sur l’interface web de SpringFuse, après m’être authentifé je vais dans « My Account > Generate New Projet ». Je conserve le nom conseillé par le tutorial « quickstartdb » et je génère ensuite mon code. Le générateur SpringFuse est une application hébergée sur une architecture de cloud-computing bien connue, Amazon EC2. La génération est vraiment rapide, quelques instants plus tard, je télécharge le code généré qui fait 330kb.
Il est temps de lancer Eclipse, pardon, IDEA IntelliJ 8.1. Je décide de créer un nouveau projet en important le projet eclipse. En 10 secondes, IDEA IntelliJ me génère un projet, il détecte au passage Spring et un module de type Web. Wooow cela semble bon du premier coup.
Premiers pas
Je tente un mvn -Ph2 jetty:run à la racine de mon projet quickstartdb… Spring 2.5.6 est récupéré via Maven2, le tout se passe en quelques minutes le temps de récupérer les librairies. Il est temps de lancer un navigateur et de regarder tout d’abord la partie vue, avant d’aller voir sous le capot et de regarder le moteur. Une fois le site lancé, je découvre la partie vue de l’application. Au premier abord cela semble un peu austère, mais la documentation donne tout de suite quelques pistes à tester. Je m’authentifie en tant qu’admin, et je commence à tester la modification des rôles, à télécharger un document vers le site, ensuite à tester les recherches avec les différentes fonctions de recherche. Le tout fonctionne vraiment rapidement.
L’interface propose d’effectuer une recherche par Pattern type Query Hibernate, ensuite une recherche par l’exemple et enfin une recherche utilisant les NamedQuery d’Hibernate.
Voyons maintenant le code généré. La structure du projet maven2 est vraiment claire. L’organisation du code est faite par domaine, par service. Aucunes dépendances cycliques signalés par IDEA IntelliJ entre les packages. Les tests unitaires sont générés pour chacun des éléments. Avec la partie web, le projet complet fait 133 classes Java. Il y a aussi du code pratique qui fait gagner du temps comme un service d’envoi d’email templatisé avec Velocity, la gestion de l’authentification, un service pour vous retourner un mot de passe si vous l’avez oublié, etc. Le moteur d’authentification basé sur SpringSecurity est simple, j’ai l’impression de visiter un grand appartement type loft, très clair et bien agencé. Poursuivons la visite.
La qualité du code
En regardant le modèle tout d’abord, je suis allé chercher quelques problèmes connus avec Hibernate, que nous voyons sur le terrain afin de voir comment SpringFuse les adresse. Je commence simplement par la gestion de l’identité des entités, avec les méthodes equals() et hashCode(). Comment sont surchargées les méthodes equals() et hashCode() des entités mappées par Hibernate ?
Par défaut, Java fournit une implémentation pour les méthodes hashCode() et equals(), qui permet de définir l’identité d’un objet. Chaque nouvelle instance sera donc différente, ce qui est en principe pratique. Si vous souhaitez affiner votre gestion des objets en mémoire, en surchargeant equals() et hashCode() vous pouvez alors définir votre notion de l’égalité. Hibernate se charge de passer le contenu d’un objet en mémoire vers la base de données et inversement. Il est donc tentant de se reposer sur les clés primaires d’une base de données pour dire que deux objets sont égaux. Mais est-ce suffisant ?
Chaque Session Hibernate a un cache. Lorsque vous créez un objet avec new(), puis qu’ensuite vous le sauvez, Hibernate sait ensuite sans problème le gérer. Les problèmes surviennent lorsque vous fermez ensuite votre Session. Si vous recherchez « le même objet » vous verez qu’Hibernate vous retournera une autre instance au sens Java, si vous ne surchargez pas equals/hashCode. Car même si les objets sont identiques en terme de contenu, ce ne sont pas les mêmes.
En principe cela ne pose pas de problèmes… tant que vous n’utilisez pas de Collections. Dans une classe avec une association one-to-many, un Set d’éléments utilisera donc par défaut le couple hashCode/equals afin de stocker vos objets. Pour peu que ce code ne soit pas surchargé, ou mal surchargé, les problèmes de gestion des collections surviennent… (voir http://www.hibernate.org/109.html?cmd=prntdoc)
La documentation d’Hibernate dit clairement :
The general contract is: if you want to store an object in a List [1], Map [2] or a Set [3] then it is an requirement that equals [4] and hashCode [5] are implemented so they obey the standard contract as specified in the documentation.
La bonne nouvelle est que SpringFuse génère un code particulièrement « balèze » pour ces cas difficiles, qui sont loin d’être facile à maitriser. Des tests unitaires poussés sont aussi générés afin de s’assurer que l’ensemble fonctionne comme un V8 de Mustang et pas comme un moteur de Solex.
Regardons ensemble la gestion de l’égalité (equals et hashCode) sur la classe AccountModel généré par SpringFuse:
/** * Indicates whether some other object is "equal to" this one. For information on the * current implementation you can * read the discussion about object * identity on hibernate.org * * @see java.lang.Object#equals(Object) * @see #setEqualsAndHashcodeStrategy() * @return true if the equals, false otherwise */ public boolean equals(Object account) { if (this == account) { return true; } if (account == null) { return false; } if (!(account instanceof AccountModel)) { return false; } AccountModel other = (AccountModel) account; setEqualsAndHashcodeStrategy(); if (this._useUidInEquals != other._useUidInEquals && other._freezeUseUidInEquals) { if (logger.isErrorEnabled()) { logger.error("Limit case reached in equals strategy. Developper, fix me", new Exception("stack trace")); } throw new IllegalStateException("Limit case reached in equals strategy. Developper, fix me"); } if (_useUidInEquals) { boolean eq = _uidInEquals.equals(other._uidInEquals); // ensure (during dev) that equals contract is respected if (eq && hashCode() != other.hashCode() && logger.isErrorEnabled()) { logger.error("Limit case reached in equals strategy. Developper, fix me", new Exception("stack trace")); } return eq; } else { if (other.getAccountId() == null) { return false; } boolean eq = getAccountId().equals(other.getAccountId()); // ensure (during dev) that equals contract is respected if (eq && hashCode() != other.hashCode() && logger.isErrorEnabled()) { logger.error("Limit case reached in equals strategy. Developper, fix me", new Exception("stack trace")); } return eq; } } private void setEqualsAndHashcodeStrategy() { if (!_freezeUseUidInEquals) { _freezeUseUidInEquals = true; _useUidInEquals = useUidInEquals(); if (_useUidInEquals) { _uidInEquals = new java.rmi.dgc.VMID(); } } } private boolean useUidInEquals() { return !hasPrimaryKey(); }
Un peu plus loin nous avons aussi un bout de code Java qui donne une idée de ce qu’il est possible de faire avec le code généré par SpringFuse :
if (_useUidInEquals) { _uidInEquals = new java.rmi.dgc.VMID(); }
Que ceux qui connaissent ce qu’est qu’un VMID lèvent la main…
Ok deux personnes, Cyrille et qui d’autre ?
Florent…
Bon ça fait pas beaucoup de monde non ? VMID c’est un identifiant d’objet unique par Java Virtual machine, qui est surtout utilisé pour identifier dans un cluster votre objet. Jusque là vous buvez votre lait fraise et vous vous demandez où je veux en venir.
ll se trouve que le code généré par SpringFuse fonctionne plutôt bien avec le cache de second niveau d’Hibernate. Et que ce cache de second niveau permet d’échanger entre deux java virtual machines des objets…
Bref SpringFuse vous génère du code qui sera capable de fonctionner dans un cluster. Bon courage pour le faire vous même du premier coup !
La javadoc de SpringFuse vous propose de lire la page suivante : http://www.hibernate.org/109.html. Si vous regardez le code, vous avez ici un moyen vraiment intelligent d’aller plus loin et de comprendre mieux Hibernate. Et cela ne fait que commencer…
Comment SpringFuse gère les relations one-to-many et many-to-many ?
Je suis ensuite allé regarder la gestion des associations one-to-many et many-to-many. Certains disent que les relations many-to-many sont plutôt un signe que le modèle est pourri. Ce que l’on voit sur le terrain, c’est que nos clients ont cette modélisation sur les bases de données de nos vraies applications de gestion. Et souvent il est impossible de remettre en cause le modèle relationnel, d’où l’importance alors d’avoir des best-practices. Il faut donc apprendre à gérer correctement ce type de relation.
La class AccountModel déclare une relation one-to-many avec la class Document. Un utilisateur a un ou plusieurs Document dans l’application. Géré avec un HashSet dans le modèle, le set est déclaré dans le fichier de mapping comme suit:
<set name="documents" inverse="true" cascade="all" lazy="extra" batch-size="10" embed-xml="false" node="documents"> <key column="ACCOUNT_ID"/> <one-to-many class="com.touilleur.domain.DocumentModel" /> <filter name="myDocumentFilter"/> </set>
Souvenez-vous dans un ancien article du Touilleur Express que vous avez lu en février, la gestion du chargement tardif avec la ligne « lazy=extra » permet de faire travailler la base de données correctement. Si vous souhaitez connaître le nombre de Document d’un utilisateur, Hibernate exécutera donc une simple requet select count SQL au lieu de créer la collection de Document en chargeant l’ensemble des Documents de la base, puis d’utiliser la méthode size() sur le Set…
Encore une bonne pratique offerte sur un plateau.
Je continue la visite.
Le mapping Hibernate
Prenez ces quelques lignes au début du fichier AccountModel.hbm.xml qui montre les choix d’optimisation proposés par SpringFuse.
... <hibernate-mapping> <class name="com.touilleur.domain.AccountModel" table="ACCOUNT" lazy="true" dynamic-update="true" optimistic-lock="version" batch-size="30" node="account">
L’attribut « dynamic-update » est ici à true. Il permet de générer une clause UPDATE en SQL ne contenant que le nom des colonnes modifiés, afin d’éviter de transmettre un graphe complet d’objet vers la base. La valeur du paramètre batch-size qui est de 1 par défaut, est aussi un moyen d’améliorer la stratégie de chargement par lot. Lorsque vous affichez une page avec un tableau listant le nombre d’utilisateurs, cela permet de récupérer par lot de 30 les Accounts de votre base.
Encore une fois, des optimisations simples qui donneront de meilleurs résultats.
Les tests
Les tests unitaires utilisent la nouvelle version de JUnit, basée sur des annotations. Pour certaines classes comme les DAO, SpringFuse utilise easyMock. Au passage, si vous n’avez jamais « mocké » de code, c’est un excellent tutorial sur les bonnes pratiques de l’écriture des tests. Nous sommes donc en plein TDD (Test Driven Development) et la qualité industrielle du code est là.
La partie Spring MVC
Du côté du framework web, c’est Spring MVC. A part la littérature, c’est un framework que je ne connais pas, sur lequel je n’ai pas d’expériences. C’est donc pour moi l’occasion de faire le tour du propriétaire afin de regarder comment est structurée l’application. La première bonne nouvelle c’est qu’il s’agit de la dernière version utilisant les annotations. Le code est donc vraiment simple et clair.
Exemple d’astuce offerte par SpringFuse
Une astuce trouvée dans la classe NamedQueryUtilHibernate illustre le principe de SpringFuse, qui est de vous donner des solutions gratuitement. Pour la suite de cette partie, nous allons regarder les requêtes nommées dans Hibernate (named query). Le principe est de permettre de déclarer dans un fichier XML (named-query.xml) les requêtes au format HQL afin d’éviter de les définir dans le code Java. Cela permet ensuite de les maintenir plus facilement, entre autres.
Par exemple voici comment SpringFuse propose de déclarer une requête
< ![CDATA[ from com.touilleur.domain.AccountModel ]]>
Cela permet ensuite d’appeler la query getAllAccountsQueryName directement à partir du code:
session.getNamedQuery("getAllAccountsQueryName").list();
L’une des limitations des requêtes nommées c’est qu’il n’est pas possible de spécifier un ordre de tri. Imaginez un tableau avec 4 colonnes, l’utilisateur clique sur la colonne Ville et vous souhaitez trier le tableau, et aussi paginer le résultat…
On voit dans le code XML ci-dessus que SpringFuse a déclaré un commentaire dans l’attribut « comment » avec pour valeur ce qui ressemble plus à un mot clé qu’un commentaire : « enableDynamicOrderBySupport ».
La réponse se trouve dans la classe NamedQueryUtilHibernate, regardez la méthode recreateNamedQueryRecreationWithOrderClausisIfNeeded, vous verrez que le code va recréer une nouvelle requête automatiquement avec un ordre de tri, si vous avez spécifié ce mot clé dans le champ commentaire ! Cela peut vous paraître obscur, vous n’imaginez pas la complexité pour expliquer en quelques lignes cette idée, mais elle est vraiment simple et astucieuse. Au final, tout est transparent puisque vous utilisez cette astuce via les manager générés par SpringFuse.
Cela illustre un principe de SpringFuse : ce n’est pas un logiciel open-source mais un logiciel open-minded. Ce genre de code pourrait se retrouver au fin fond d’un JAR, sans que vous n’ayez la possibilité de regarder le code. Ici c’est le contraire, l’objectif c’est de vous donner des stéroïdes afin de renforcer vos abdominaux de développeur Java.
J’apprécie la distribution de bonnes idées.
Conclusion
SpringFuse est gratuit jusqu’à 10 Tables de base de données. C’est un moyen transparent et simple de vous laisser tester la solution avec votre application.
J’imagine maintenant plusieurs usages pour SpringFuse:
– commencer un projet rapidement
– apprendre de nouvelles technologies comme Hibernate ou Spring MVC comme un livre dynamique
– reprendre un projet avec une forte dette technique afin de le dynamiser rapidement
– gagner du temps sur la partie technique pour se faire plaisir sur la partie métier
J’écris ces lignes peut-être trop emballé. Il est aussi temps de parler sur des regrets ou des questions. Pourquoi ne pas avoir tel ou tel framework de rendu encore plus joli comme JQuery par exemple ?
Je pense que finalement SpringFuse fait le choix intelligent de faire très bien ce qu’il y a de plus important dans une application : les fondations.
Libre à vous ensuite je pense de placer une librairie de rendu en plus de ce que vous donne le framework Spring MVC.
Le lien pour tester : http://www.springfuse.com
VMID vient du Distributed Garbage Collector RMI (java.rmi.dgc), on retrouve la passion de Florent pour les trucs qui distribuent mystérieusement les données et les traitements sur les serveurs comme Terracotta :-p
Mon côté néo-conservateur m’a amené à préférer les trucs plus simples et je ne connaissais que le vulgaire java.rmi.server.UID. C’est trop compliqué pour les vieux comme moi les UnlockedSharedException et autres @AutolockWrite Terracotta, je préfère la distribution explicite avec des opérations put() et get() 🙂
Cyrille