[{"body":"","link":"https://javaetmoi.com/categories/","section":"categories","tags":null,"title":"Categories"},{"body":" En 2025, j’ai eu l’opportunité de mettre en place Spring Modulith sur une nouvelle application web. Pour partager cette expérience avec mes collègues, j’ai préparé une démonstration live montrant comment intégrer Spring Modulith dans une application Spring Boot.\nJ’avais besoin pour cela d’une application simple et universelle. Vous commencez à me connaitre : mon choix s’est naturellement porté sur la version canonique de Spring Petclinic.\nPris au jeu, j’ai progressivement enrichi l’application afin d’illustrer plusieurs fonctionnalités clés de Spring Modulith. J’ai ensuite mis ce fork à disposition de la communauté Spring Petclinic dont le code source complet est disponible sur GitHub : spring-petclinic-modulith.\nDans ce billet, je vous propose de découvrir Spring Modulith, puis de suivre pas à pas comment l’application démo Spring Petclinic a été enrichie pour tirer parti de ses fonctionnalités.\nArchitecture modulaire L\u0026rsquo;architecture en microservices a le vent en poupe depuis une quinzaine d’années. Pourtant, force est de constater que nombre d’applications métiers restent des monolithes. Ce n\u0026rsquo;est pas nécessairement une mauvaise chose. Partir systématiquement d’un monolith avant de l’éclater (ou pas) en microservices est une approche préconisée par de nombreux architectes logiciels (cf. article Monolith First de Martin Fowler). Un monolithe bien structuré, celui qu\u0026rsquo;Oliver Drotbohm (le créateur de Spring Modulith) appelle le modulith ou que certains qualifient de modular monolith, représente souvent le meilleur compromis entre simplicité opérationnelle et maintenabilité au quotidien. Le projet Spring Modulith permet d’outiller cette approche.\nAprès plusieurs années de gestation, Spring Modulith a été rendu GA en août 2023. Relativement jeune, ce projet apporte un cadre structurant aux applications Spring Boot monolithiques en y introduisant la notion de modules applicatifs. Vérification de l\u0026rsquo;architecture au build, documentation générée automatiquement, communication inter-modules par événements, tests d\u0026rsquo;intégration ciblés… le tout sans nécessairement d’infrastructure externe.\nLes fonctionnalités de Spring Modulith Avant de plonger dans le code, prenons un peu de hauteur. Spring Modulith repose sur un principe simple : chaque sous-package direct du package de la classe principale Spring Boot(celle annotée avec @SpringBootApplication) constitue un module applicatif. Par convention, le package racine du module expose l\u0026rsquo;API publique ; tous les sous-packages sont considérés comme privés.\nÀ partir de cette convention d’organisation, Spring Modulith propose un ensemble de fonctionnalités complémentaires :\nFonctionnalité Description Vérification structurelle Lors de la construction de l’application, le test d’architecture ApplicationModules.verify() vérifie qu’aucun module n’accède aux packages internes d’un autre module et qu’il n’existe pas de dépendances cycliques. Communication par événements ApplicationEventPublisher et @ApplicationModuleListener permettent de découpler les modules sans appel direct entre beans Spring. Registre de publication des événements Persiste chaque événement en base de données (table event_publication) avant l’exécution du listener, ce qui garantit la livraison des évenements au moins une fois (le at-least-once delivery). Moments Publie automatiquement des événements temporels (DayHasPassed, HourHasPassed …) pour remplacer les @Scheduled. Tests d’intégration modulaires @ApplicationModuleTest restreint le chargement du contexte Spring Boot au module applicatif testé. L’API Scenario orchestre les tests asynchrones. Documentation L’API Documenter produit des diagrammes C4 PlantUML et des Application Module Canvas AsciiDoc décrivant l’architecture du code. Actuator La sonde /actuator/modulith expose le graphe de modules applicatifs au runtime. Observabilité L’artefact spring-modulith-observability instrumente automatiquement les beans exposés et génère des spans Micrometer pour chaque interaction inter-modules. La documentation officielle de Spring Modulith est très complète. Je vous encourage à vous y référer. Dans les paragraphes qui suivent, nous allons voir concrètement comment chaque fonctionnalité a été intégrée dans Spring Petclinic, ceci en 11 étapes. Pour rappel, cette application Spring Boot créée en 2003 met en scène une clinique vétérinaire avec ses propriétaires d\u0026rsquo;animaux, ses vétérinaires et la prise de rendez-vous.\nÉtape 1 - Ajouter les dépendances Maven L’application Spring Petclinic supporte les deux principaux systèmes de build du monde Java : Maven et Gradle. Spring Petclinic Modulith également. Dans ce billet, par simplicité, nous nous focaliserons sur le build Maven.\nToute intégration de Spring Modulith commence par l\u0026rsquo; ajout du BOM et des premières dépendances. Dans le pom.xml, nous déclarons d\u0026rsquo;abord la version sous forme de properties (bonne pratique Maven) :\n\u0026lt;spring-modulith.version\u0026gt;2.0.5\u0026lt;/spring-modulith.version\u0026gt; La version 2.x de Spring Modulith est compatible Spring Boot 4.\nPuis on importe le BOM dans \u0026lt;dependencyManagement\u0026gt;:\n\u0026lt;dependencyManagement\u0026gt; \u0026lt;dependencies\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.modulith\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-modulith-bom\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;${spring-modulith.version}\u0026lt;/version\u0026gt; \u0026lt;type\u0026gt;pom\u0026lt;/type\u0026gt; \u0026lt;scope\u0026gt;import\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;/dependencies\u0026gt; \u0026lt;/dependencyManagement\u0026gt; Et enfin les dépendances minimales :\n\u0026lt;!-- Annotations et API publique Spring Modulith --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.modulith\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-modulith-api\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!-- Support JUnit 5 pour la vérification modulaire --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.modulith\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-modulith-starter-test\u0026lt;/artifactId\u0026gt; \u0026lt;scope\u0026gt;test\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt; Deux dépendances suffisent pour démarrer. D’autres dépendances seront ajoutées au fil de l\u0026rsquo;article.\nÉtape 2 - Le test de vérification modulaire C\u0026rsquo;est le point d\u0026rsquo;entrée incontournable de Spring Modulith. En quelques lignes, on écrit un test JUnit qui analyse la structure du code et vérifie que les modules respectent leurs frontières :\npackage org.springframework.samples.petclinic; import org.junit.jupiter.api.Test; import org.springframework.modulith.core.ApplicationModules; class ModularityTests { ApplicationModules modules = ApplicationModules.of(PetClinicApplication.class); @Test void verifiesModularStructure() { modules.verify(); } } L’appel ApplicationModules.of(...) scanne les packages de l\u0026rsquo;application et construit un modèle en mémoire des modules détectés. L\u0026rsquo;appel à la méthode verify() s\u0026rsquo;assure ensuite trois aspects :\nPas de cycle entre les modules applicatifs Pas d\u0026rsquo;accès aux packages internes d\u0026rsquo;un module depuis un autre module Respect des dépendances explicites (si configurées via l’annotation @ApplicationModule) Si une de ces règles est violée, le test échoue avec un message d\u0026rsquo;erreur précis. Voici un exemple dans lequel un cycle est détecté :\norg.springframework.modulith.core.Violations: - Cycle detected: Slice owner -\u0026gt; Slice vet -\u0026gt; Slice owner La version Ultimate d’ IntelliJ IDEA est packagée avec le plugin Spring Modulith. Le support de Spring Modulith permet à IntelliJ de mettre en évidence les utilisations de beans Spring (ou de toute autre classe) qui enfreignent les règles de Spring Modulith. IntelliJ propose de refactoriser le code afin de le rendre conforme à la structure modulaire. Je vous renvoie à la documentation de cette fonctionnalité.\nÉtape 3 - Identifier les modules applicatifs Spring Modulith détecte automatiquement les modules à partir des sous-packages directs du package contenant la classe main @SpringBootApplication. Dans Spring Petclinic, la classe PetClinicApplication est localisée au niveau du package org.springframework.samples.petclinic.\nLes modules identifiés étaient à ce stade au nombre de quatre :\nModule Package racine owner org.springframework.samples.petclinic.owner vet org.springframework.samples.petclinic.vet system org.springframework.samples.petclinic.system model org.springframework.samples.petclinic.model Cette modularisation fonctionnelle de l’application Spring Petclinic avait été réalisée en 2016 par Dave Syer dans la PR #200 Modernize Spring apps structure. Oliver Drotbohm avait d’ailleurs participé à la conversation.\nLe module model mutualisait 3 classes de base JPA BaseEntity, Person, NamedEntity partagées entre les modules owner et vet. Conservé en 2016, 10 ans plus tard à l’heure du Modulith, j’ai préféré reconsidérer ce choix. En effet, en DDD, chaque Bounded Context possède intégralement son modèle du domaine métier. Les classes BaseEntity, Person, NamedEntity ne sont pas des concepts métier. Ce sont des raccourcis techniques. Inliner le contenu de ces classes techniques dans vet et owner rend chaque module prêt pour un éventuel découpage en microservices, sans aucun type partagé. Plutôt que d’être exposé sous forme de module partagé (shared module), le package model a été purement et simplement supprimé.\nÉtape 4- Séparer l\u0026rsquo;API publique des détails d\u0026rsquo;implémentation Spring Modulith attribue un rôle bien défini à chaque package :\nLe package racine du module(ex : vet/ ) expose l\u0026rsquo;API publique : les types que les autres modules ont le droit d\u0026rsquo;utiliser Les sous-packages (ex : vet/internal/) sont considérés comme internes : leur utilisation est interdite depuis les autres modules Pour chaque module de Spring Petclinic, j\u0026rsquo;ai donc commencé par déplacer les classes d\u0026rsquo;implémentation — contrôleurs, repositories, entités JPA dans un sous-package nommé internal (nom de package donné par convention, mais tout autre nommage est possible). A ma grande surprise, le package racine de chaque module était vide : les 3 modules étaient parfaitement découplés.\nLes classes de test suivent la même organisation. Trivial, ce refactoring peut paraitre déroutant. Il présente pourtant un gain immédiat : on rend explicite ce qui relève de l\u0026rsquo;API publique du module et ce qui est un détail d\u0026rsquo;implémentation. Et c\u0026rsquo;est Spring Modulith qui garantit que cette frontière est respectée via le test verify() .\nDans un module applicatif, le développeur est libre d’organiser le code comme il l’entend. Chaque module peut d’ailleurs avoir sa propre organisation : découpage en couches techniques pour l’un, architecture hexagonale pour l’autre.\nDans Petclinic, le package internal du module owner contenait 13 classes à plat. Cela fait beaucoup. On s’éloigne du SRP. J’ai ainsi fait le choix de ventiler ces classes dans 3 packages différents : ui, application et domain.\nÉtape 5 - Communication par événements entre modules La communication par évènements est une fonctionnalité phare de Spring Modulith qu’on peut utiliser en déclarant l’artefact spring-modulith-events-api :\n\u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.modulith\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-modulith-events-api\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; Pour illustrer ce mécanisme, j\u0026rsquo;ai ajouté à Spring Petclinic un nouveau cas d’utilisation métier : lorsqu\u0026rsquo;un rendez-vous est réservé, le système affecte automatiquement le vétérinaire le moins chargé.\nPlutôt que d\u0026rsquo;injecter un bean du module vet dans le module owner, on remplace l\u0026rsquo;appel direct d’une méthode par la publication d’un événement applicatif. Lors de la réservation d’un rendez-vous, la classe VisitScheduler utilise la classe ApplicationEventPublisher de Spring Framework pour émettre l’évènement VisitBooked.\n@Transactional public void bookVisit(Owner owner, Integer petId, Visit visit) { Owner managedOwner = owners.findById(owner.getId()).orElseThrow(); managedOwner.addVisit(petId, visit); owners.flush(); eventPublisher.publishEvent(new VisitBooked(visit.getId(), petId, visit.getDate())); } Le record VisitBook fait partie de l’ interface publique du module owner. On le déclare donc au niveau du package racine du module owner :\npackage org.springframework.samples.petclinic.owner; public record VisitBooked(int visitId, int petId, LocalDate date) { } La classe VetEventListener du module vet réagit à cet événement via l’annotation @ApplicationModuleListener de Spring Modulith :\n@Component class VetEventListener { private final VetRoster vetRoster; VetEventListener(VetRoster vetRoster) { this.vetRoster = vetRoster; } @ApplicationModuleListener void on(VisitBooked event) { vetRoster.assignVet(event); } } L\u0026rsquo;annotation @ApplicationModuleListener (source: spring-modulith-events-api) est un sucre syntaxique combinant trois annotations en une : @Async (source: spring-context), @Transactional (source: spring-tx) et @TransactionalEventListener (source: spring-tx). Ce listener s\u0026rsquo;exécute après le commit de la transaction émettrice, dans une nouvelle transaction, de façon asynchrone. Le module owner ne connaît pas le module vet. Le découplage est garanti par la structure des packages.\nLa mise à jour du tableau de garde des vétérinaires est assurée par le service VetRoster. L\u0026rsquo;affectation est persistée dans une nouvelle table visit_assignments qui appartient conceptuellement au module vet.\nCREATE TABLE IF NOT EXISTS visit_assignments ( visit_id INT NOT NULL PRIMARY KEY, vet_id INT NOT NULL REFERENCES vets (id), visit_date DATE NOT NULL ); Notez ici un point important : la colonne visit_id de cette table est une référence lâche, intentionnellement sans clé étrangère vers la table visits du module owner. C\u0026rsquo;est le miroir en base de données du découplage Java : le module vet ne connaît que l\u0026rsquo;identifiant publié dans l\u0026rsquo;événement VisitBooked, pas l\u0026rsquo;entité Visit elle-même. Les modules communiquent par identifiants, pas par références d\u0026rsquo;objets ni par clés étrangères croisées.\nÉtape 6 – Déclarer les dépendances autorisées Cette étape permet de donner un nom au système et de déclarer explicitement les dépendances inter-modules autorisées. Deux annotations entrent ici en jeu : @Modulithic et @ApplicationModule.\nOn commencer par annoter la classe main de l’application Petclinic avec @Modulithic:\n@Modulithic(systemName = \u0026#34;PetClinic\u0026#34;) @SpringBootApplication public class PetClinicApplication { ...} Puis, dans les fichiers package-info.java de chaque module, on utilise @ApplicationModule :\n// owner/package-info.java — aucune dépendance @ApplicationModule package org.springframework.samples.petclinic.owner; // vet/package-info.java — dépend du module owner @ApplicationModule(allowedDependencies = { \u0026#34;owner\u0026#34; }) package org.springframework.samples.petclinic.vet; // system/package-info.java — aucune dépendance @ApplicationModule package org.springframework.samples.petclinic.system; Ces garde-fous architecturaux sont ici exploités par le ModularityTests. Si un développeur (ou un agent de codage) introduit une dépendance non autorisée, le test échoue immédiatement.\nÉtape 7 - L\u0026rsquo;Event Publication Registry Sans harnais de sécurité, un événement publié mais dont le listener échoue serait perdu à jamais. L\u0026rsquo;Event Publication Registry résout ce problème en persistant chaque événement en base de données avant l\u0026rsquo;exécution du listener.\nSpring Modulith supporte 4 technologies de persistance : JDBC, JPA, MongoDB et Neo4j.\nBien que Spring Petclinic repose sur des repositories Spring Data JPA, j’ai choisi d’exploiter le support JDBC de Spring Modulith. Compatible JPA, il propose la propriété spring.modulith.events.jdbc.schema-initialization.enabled permettant de créer la table event_publication.\nLa mise en place tient en une dépendance :\n\u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.modulith\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-modulith-starter-jdbc\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; Et trois propriétés dans le fichier de configuration application.properties :\n# Crée automatiquement la table event_publication au démarrage spring.modulith.events.jdbc.schema-initialization.enabled=true # Supprime les publications complétées immédiatement spring.modulith.events.completion-mode=DELETE # Re-publie les événements non traités au redémarrage spring.modulith.events.republish-outstanding-events-on-restart=true Spring Modulith intercepte chaque appel à publishEvent() et insère une ligne dans la table event_publication au sein de la transaction initiale. Si le listener s\u0026rsquo;exécute avec succès, l\u0026rsquo;entrée est supprimée (mode DELETE). Si le listener échoue ou si l\u0026rsquo;application crashe, l\u0026rsquo;entrée reste en base et sera rejouée au redémarrage de Petclinic. Cette garantie at-least-once delivery fonctionne sans infrastructure externe : pas besoin de Kafka, de RabbitMQ ni de quelconque broker de messages. Un simple SGBD relationnel suffit.\nÉtape 8 - Moments : les événements temporels Spring Modulith propose un module spring-modulith-moment s qui publie automatiquement des événements marquant le passage du temps : HourHasPassed, DayHasPassed, WeekHasPassed, etc. C\u0026rsquo;est une alternative élégante aux classiques @Scheduled de Spring.\nSur notre application, le module vet utilise l’évènement DayHasPassed pour nettoyer quotidiennement les affectations aux vétérinaires dont la date est passée. Dans la classe VetEventListener, on déclare une seconde méthode :\n@EventListener void on(DayHasPassed event) { vetRoster.cleanupPastAssignments(event.getDate()); } Notez l\u0026rsquo;utilisation de l’annotation Spring Framework @EventListener (et non @ApplicationModuleListener) : l\u0026rsquo;événement DayHasPassed est publié par Spring Modulith lui-même en dehors de toute transaction applicative.\nÉtape 9 - Tests d\u0026rsquo;intégration modulaires La modularité apportée par Spring Modulith présente un autre avantage : sa capacité à bootstrapper un seul module en isolation. L\u0026rsquo;annotation @ApplicationModuleTest remplace ainsi @SpringBootTest et ne charge que le contexte application Spring nécessaire au module dans lequel le test se trouve. En théorie, le temps d’exécution du test devrait être amélioré.\nExemple d’utilisation sur le test VisitSchedulerTests :\n@ApplicationModuleTest class VisitSchedulerTests { @Autowired OwnerRepository owners; @Autowired VisitScheduler visitScheduler; @Test void bookVisitShouldPublishVisitBookedEvent(Scenario scenario) { // Given Owner owner = owners.findById(1).orElseThrow(); Pet pet = owner.getPets().iterator().next(); Visit visit = new Visit(); visit.setDescription(\u0026#34;Annual checkup\u0026#34;); // When / Then scenario.stimulate(() -\u0026gt; visitScheduler.bookVisit(owner, pet.getId(), visit)) .andWaitForEventOfType(VisitBooked.class) .matching(event -\u0026gt; event.petId() == pet.getId()) .toArriveAndVerify(event -\u0026gt; then(event.petId()).isEqualTo(pet.getId())); } } Notez ici l’utilisation de l\u0026rsquo; API Scenario de Spring Modulith Test. On définit un stimulus (l\u0026rsquo;appel à bookVisit), on déclare l\u0026rsquo;événement attendu (VisitBooked), on pose un critère de correspondance ( matching) et on vérifie. Le tout de manière fluide.\nLes logs d’exécution du test donnent un aperçu des beans Spring chargés par @ApplicationModuleTest :\nBootstrapping @org.springframework.modulith.test.ApplicationModuleTest for Owner in mode STANDALONE (class org.springframework.samples.petclinic.PetClinicApplication)… \\# Owner \\\u0026gt; Logical name: owner \\\u0026gt; Base package: org.springframework.samples.petclinic.owner \\\u0026gt; Excluded packages: none \\\u0026gt; Direct module dependencies: none \\\u0026gt; Spring beans: o ….application.VisitScheduler o ….domain.OwnerRepository o ….domain.PetTypeRepository o ….ui.OwnerController o ….ui.PetController o ….ui.PetTypeFormatter o ….ui.VisitController Appartenant au module system, la classe de test existante CrashControllerIntegrationTests a pu bénéficier de l’annotation @ApplicationModuleTest.\nContrairement à @SpringBootTest, @ApplicationModuleTest n’expose pas d’attribut properties. Cette limitation a pu être contournée grâce à l’annotation TestPropertySource de Spring Test.\n// Avant @SpringBootTest(webEnvironment = RANDOM_PORT, properties = { \u0026#34;spring.web.error.include-message=ALWAYS\u0026#34;, \u0026#34;management.endpoints.access.default=none\u0026#34; }) @AutoConfigureTestRestTemplate class CrashControllerIntegrationTests { // Après @ApplicationModuleTest(webEnvironment = RANDOM_PORT) @TestPropertySource( properties = { \u0026#34;spring.web.error.include-message=ALWAYS\u0026#34;, \u0026#34;management.endpoints.access.default=none\u0026#34; }) @AutoConfigureTestRestTemplate class CrashControllerIntegrationTests { Contrairement à ce dont on pouvait s’attendre, le temps d’exécution a légèrement augmenté, passant en moyenne de 500 à 520 ms. La détection des beans du module explique sans doute cet overhead. A vérifier sur d’autres testes, dans d’autres applications plus conséquentes.\nÉtape 10 - Génération de documentation Spring Modulith permet de générer automatiquement de la documentation à partir du modèle de modules. Il suffit d’ajouter l’artefact spring-modulith-docs dans le pom.xml puis d\u0026rsquo;enrichir notre classe ModularityTests :\n@Test void writeDocumentation() { new Documenter(modules).writeDocumentation(); } L\u0026rsquo;appel à writeDocumentation() produit dans le répertoire target/spring-modulith-docs/ :\nDes diagrammes C4 au format PlantUML ( .puml) représentant les relations entre modules Des « modules Canvas » au format AsciiDoc ( .adoc) listant pour chaque module : les beans Spring exposés (non visible sur Petclinic) ainsi que les événements publiés et écoutés Un document de synthèse ( all-docs.adoc) agrégeant l\u0026rsquo;ensemble des diagrammes et canvas Exemple de rendu du fichier module-vet.puml :\nExemple de rendu du fichier module-vet.adoc :\nL\u0026rsquo;intérêt de cette living documentation est double : le rendu de l’architecture du code est rendu sous nos yeux et la documentation reste synchronisée avec le code sans effort supplémentaire.\nCela dit, dans une application d’entreprise, je vous recommande de ne pas regénérer systématiquement la doc à chaque exécution du build Maven, mais à la demande lorsque vous (ou votre agent IA) avez besoin de publier ou consulter la doc.\nÉtape 11 - l\u0026rsquo;endpoint Actuator Cette dernière étape consiste exposer le graphe de modules au runtime. Deux dépendances Maven sont nécessaires :\n\u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.modulith\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-modulith-actuator\u0026lt;/artifactId\u0026gt; \u0026lt;scope\u0026gt;runtime\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.modulith\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-modulith-runtime\u0026lt;/artifactId\u0026gt; \u0026lt;scope\u0026gt;runtime\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt; Un appel GET sur l’URL http://localhost:8080/actuator/modulith renvoie le graphe complet des modules au format JSON : noms, packages et dépendances. Cette sonde est pratique pour visualiser l\u0026rsquo;architecture de l’application déployée sans avoir besoin d\u0026rsquo;aller regarder le code ou la documentation. En production, pensez néanmoins à désactiver ou sécuriser cet actuator.\n{ \u0026#34;owner\u0026#34;: { \u0026#34;displayName\u0026#34;: \u0026#34;Owner\u0026#34;, \u0026#34;basePackage\u0026#34;: \u0026#34;org.springframework.samples.petclinic.owner\u0026#34;, \u0026#34;nested\u0026#34;: [], \u0026#34;type\u0026#34;: \u0026#34;closed\u0026#34;, \u0026#34;shared\u0026#34;: false, \u0026#34;namedInterfaces\u0026#34;: { \u0026#34;\u0026lt;\u0026lt;UNNAMED\u0026gt;\u0026gt;\u0026#34;: [ \u0026#34;org.springframework.samples.petclinic.owner.VisitBooked\u0026#34; ] }, \u0026#34;initializers\u0026#34;: [], \u0026#34;dependencies\u0026#34;: [] }, \u0026#34;system\u0026#34;: { \u0026#34;displayName\u0026#34;: \u0026#34;System\u0026#34;, \u0026#34;basePackage\u0026#34;: \u0026#34;org.springframework.samples.petclinic.system\u0026#34;, \u0026#34;nested\u0026#34;: [], \u0026#34;type\u0026#34;: \u0026#34;closed\u0026#34;, \u0026#34;shared\u0026#34;: false, \u0026#34;namedInterfaces\u0026#34;: { \u0026#34;\u0026lt;\u0026lt;UNNAMED\u0026gt;\u0026gt;\u0026#34;: [] }, \u0026#34;initializers\u0026#34;: [], \u0026#34;dependencies\u0026#34;: [] }, \u0026#34;vet\u0026#34;: { \u0026#34;displayName\u0026#34;: \u0026#34;Vet\u0026#34;, \u0026#34;basePackage\u0026#34;: \u0026#34;org.springframework.samples.petclinic.vet\u0026#34;, \u0026#34;nested\u0026#34;: [], \u0026#34;type\u0026#34;: \u0026#34;closed\u0026#34;, \u0026#34;shared\u0026#34;: false, \u0026#34;namedInterfaces\u0026#34;: { \u0026#34;\u0026lt;\u0026lt;UNNAMED\u0026gt;\u0026gt;\u0026#34;: [] }, \u0026#34;initializers\u0026#34;: [], \u0026#34;allowedDependencies\u0026#34;: [ \u0026#34;owner\u0026#34; ], \u0026#34;dependencies\u0026#34;: [ { \u0026#34;target\u0026#34;: \u0026#34;owner\u0026#34;, \u0026#34;types\u0026#34;: [ \u0026#34;EVENT_LISTENER\u0026#34; ] } ] } } Conclusion Vous l’aurez vu : intégrer Spring Modulith dans Spring Petclinic s\u0026rsquo;est fait facilement et de manière très progressive. Un projet d’entreprise n’exploitera pas nécessairement toutes les fonctionnalités présentées dans cet article. Seules les étapes 1 à 5 sont obligatoires. Le fait de pouvoir ouvrir certains sous-packages à d’autres modules permet d’intégrer Spring Modulith dans des applications legacy, le temps de refactorer le code. D’expérience , le plus simple consiste néanmoins à intégrer Spring Modulith dès la mise en œuvre de l’architecture logicielle d’un nouveau monolith modulaire.\nLes 3 modules initiaux de Spring Petclinic étant isolés et indépendants, l’interface publique exposée par chaque module au travers son package racine ne présentait que peu d’intérêt. L’ajout de la fonctionnalité d’affectation automatique d’un vétérinaire à un futur rendez-vous aura permis de montrer comment faire communiquer 2 modules à l’aide d \u0026rsquo; évènements puis de montrer comment utiliser l’ Event Publication Registry. La base de données existante aura été réutilisée, facilitant son adoption (nul besoin d’infrastructure externe).\nAyant encore peu d’expérience avec Spring Modulith, je suis ouvert à toute proposition d’amélioration. Le code source du fork Spring Petclinic Modulith est disponible sur repo GitHub : spring-petclinic-modulith. Tous les changements apportés sont visibles à travers cet unique commit. N\u0026rsquo;hésitez pas à l’étudier, à expérimenter et à soumettre vos contributions à travers des issues et de Pull Requests.\nRessources\nspring-petclinic-modulith : le code source complet de la démo Documentation officielle de Spring Modulith Spring Modulith examples : les exemples fournis par l\u0026rsquo;équipe Spring Guide to Modulith with Spring Boot : article de blog de Piotr Mińkowski datant de 2023 spring-modulith-with-ddd : code source d’une application Modular Monolith basée sur Spring Modulith et le Domain Driven Design Migrating to Modular Monolith using Spring Modulith and IntelliJ IDEA Monolith First : article de Martin Fowler datant de 2015 ","link":"https://javaetmoi.com/2026/04/decouverte-de-spring-modulith/","section":"posts","tags":["spring-boot","spring-modulith","spring-petclinic"],"title":"Découverte de Spring Modulith"},{"body":"","link":"https://javaetmoi.com/","section":"","tags":null,"title":"Java \u0026 Moi"},{"body":"","link":"https://javaetmoi.com/posts/","section":"posts","tags":null,"title":"Posts"},{"body":"","link":"https://javaetmoi.com/categories/retour-dexp%C3%A9rience/","section":"categories","tags":null,"title":"Retour-D'expérience"},{"body":"","link":"https://javaetmoi.com/categories/spring/","section":"categories","tags":null,"title":"Spring"},{"body":"","link":"https://javaetmoi.com/tags/spring-boot/","section":"tags","tags":null,"title":"Spring-Boot"},{"body":"","link":"https://javaetmoi.com/tags/spring-modulith/","section":"tags","tags":null,"title":"Spring-Modulith"},{"body":"","link":"https://javaetmoi.com/tags/spring-petclinic/","section":"tags","tags":null,"title":"Spring-Petclinic"},{"body":"","link":"https://javaetmoi.com/tags/","section":"tags","tags":null,"title":"Tags"},{"body":"","link":"https://javaetmoi.com/categories/conf%C3%A9rence/","section":"categories","tags":null,"title":"Conférence"},{"body":"","link":"https://javaetmoi.com/tags/devfest/","section":"tags","tags":null,"title":"Devfest"},{"body":"","link":"https://javaetmoi.com/tags/obsidian/","section":"tags","tags":null,"title":"Obsidian"},{"body":"Lors de la conférence Devfest Nantes 2025, j\u0026rsquo;ai assisté au talk d\u0026rsquo; Hoani Cross portant sur la prise de notes. Loin d\u0026rsquo;être nouveau, ce sujet m\u0026rsquo;a particulièrement interpellé. Figurez-vous en effet qu\u0026rsquo;une partie des articles publiés sur ce blog (dont celui que vous avez sous les yeux) vient des notes rédigées lors de conférences, de projets personnels ou bien encore de ma veille techno.\nDans son talk, Hoani nous présente le logiciel Obsidian, la manière dont il l\u0026rsquo;utilise au quotidien pour noter et gérer son activité, qu\u0026rsquo;elle soit professionnelle ou personnelle.\nJe suis sorti de sa présentation quelque peu désarçonné. Hoani utilise Obsidian comme un Bullet Journal (qu\u0026rsquo;on appelle aussi « bujo ») numérique pour compiler notes, pense-bêtes, objectifs, rappels, tracking, plannings et coups de coeur. Son utilisation est vraiment avancée et très régulière. Je ne me voyais pas passer autant de temps que lui sur Obsidian.\nL\u0026rsquo;autre domaine dans lequel Obsidian semble exceller consiste en la possibilité de se créer un Second Cerveau. Les notes peuvent être reliées ensemble à l\u0026rsquo;aide de Map Of Content (MOC). Une alternative aux hashtags et l\u0026rsquo;organisation hiérarchisée en dossiers et sous-dossiers.\nUne note de type Map of Content s\u0026rsquo;assimile à une thématique, un sujet principal, auquel on rattache bidirectionnellement des notes et qui va faire office de table de matières et de tableau de bord. La création de sous-MOCs spécialisés reste possible. On se rapproche du web, des liens hypertextes et du mind mapping.\nDans ce billet, j\u0026rsquo;aimerais vous restituer la prestation d\u0026rsquo;Hoani et vous laisser découvrir son utilisation Obsidian.\nDu papier à Obsidian Senior backend architect chez Sfeir, Hoani Cross nous explique avoir utilisé de nombreux carnets manuscrits pour apprendre et structurer sa pensée. Hoani pratique le Bullet Journal et son talk se veut être un partage de ses résultats après des années de pratiques.\nHoani commence par nous rappeler les bienfaits de prendre des notes :\nRalentir pour assimiler, poser son téléphone pour se focaliser Graver dans la mémoire Transformer l’écoute en savoir, résumer avec ses propres mots Entraîner le cerveau : sport mental, reformulation, concentration Connecter des idées parfois éloignées les unes des autres Améliorer sa compréhension tout en construisant une bibliothèque personnelle (le fameux Second Cerveau) Adepte du Bullet Journal (Bujo), Hoani a longtemps utilisé un carnet numéroté, un stylo et un index en début de carnet. Son carnet comportait une page par jour contenant des idées, des notes, le suivi des variables (énergie / humeur) et des sujets spéciaux. Pour s\u0026rsquo;y retrouver, il utilise des conventions de notation appelées Bullet Journal Key. Par exemple : un carré pour une tâche.\nPremière limitation : les Bujo ne permettent pas de faire de recherche textuelle.\nHoani a essayé de basculer sur des carnets numériques avec le reMarkable et a testé différents logiciels : OneNote, EverNote, Notion, Keep, Joplin. Nul n\u0026rsquo;est arrivé à la hauteur d\u0026rsquo;Obsidian qui est presque parfait pour les raisons suivantes :\nBasé sur le langage de balisage léger Markdown, ce qui permet à l\u0026rsquo;auteur de rester propriétaire de ses données Rendu basé le moteur web Electron (comme VS Code) Logiciel gratuit, créé pendant le confinement de la Covid19, mais pas Open Source Plus de 2 500 plugins OpenSource Multiplateforme : Windows, Linux, MacOS, Android, iOS Obsidian intègre les fonctionnalités suivantes :\nCoffre avec navigateur de fichiers : toutes les notes sont stockées localement dans un dossier (appelé \u0026ldquo;coffre\u0026rdquo;), consultable via un explorateur intégré. Éditeur de Markdown intelligent : éditeur fluide combinant texte brut et mise en forme instantanée, avec gestion des liens internes, blocs de code, formules et tableaux. Canvas pour composer ses notes : espace visuel libre pour organiser ses idées sous forme de cartes reliées, idéal pour le brainstorming et la modélisation de concepts. Gestion des daily notes : fonction de journal quotidien permettant de consigner rapidement pensées, tâches ou réflexions, avec génération automatique de notes datées. Diapositives : transformation instantanée d’une note en présentation interactive, pratique pour exposer un projet ou partager ses idées sans quitter Obsidian. Enregistrement audio : capture vocale intégrée pour enregistrer des idées, réunions ou commentaires, directement stockés et liés dans le coffre. Vue graphique de l’arborescence des notes : représentation visuelle du réseau de liens entre notes, offrant une cartographie claire et dynamique de son écosystème de connaissances. L\u0026rsquo;une des forces du logiciel Obsidian réside dans le fait qu\u0026rsquo;il soit multiplafeforme : on peut commencer une note oralement sur son Smartphone puis la reprendre plus tard avec un clavier sur son laptop. Plusieurs techniques de synchronisation des coffres entre différents devices existent :\nOffre de service intégré payant : Sync (4$ par mois) Stockage Cloud avec iCloud, OneDrive, Google Drive Syncthing (OSS) : synchronisation Peer-to-Peer de fichiers. Vos données ne sont jamais stockées dans le Cloud Plugin Git (instable sur mobile) : personnellement, c\u0026rsquo;est ce dernier que j\u0026rsquo;utilise avec un repo GitHub privé. À noter que l\u0026rsquo;utilisation d\u0026rsquo;un repo Git rend possible le partage en équipe d\u0026rsquo;un coffre Obsidian.\nUtilisation d\u0026rsquo;Obsidian Le coffre d\u0026rsquo;Hoani contient toutes ses données, tant personnelles que professionnelles. Cela peut poser un problème de confidentialité : faire sortir des données pros sur son ordi perso peut être contraire aux règles de sécurité de son entreprise.\nL\u0026rsquo;organisation de son coffre comporte 8 grands dossiers :\nNotes persos Notes pros Notes de lecture (livres ou vidéo youtube) Notes non classées (temporaire) Second Brain : wiki perso Bullet Journal (Bujo) Archives Templates Chaque note .md suit la structuration suivante :\nEn-tête en front matter pour ajouter des méta-données Corps en Markdown Tags hiérarchisés Le tableau suivant présente la syntaxe Markdown supportée par Obsidian :\nSyntax Description [[Link]] Liens internes ![[Link]] Inclusion de fichiers ![[Link#^id]] Références de bloc ^id Définition d\u0026rsquo;un bloc [^id] Notes de bas de page %%Text%% Commentaires Text Barré ==Text== Surlignage ``` Bloc de code - [ ] Tâche incomplète - [x] Tâche terminée \u0026gt; [!note] Callout (note, info…) (see link) Tableaux Dans son dossier Bujo, Hoani s\u0026rsquo;appuie sur le plugin Periodic Notes pour gérer ses notes périodiques et les hiérarchise en année / trimestre / mois / jours.\nLe plugin Tasks permet de gérer les tâches dans Obsidian. La syntaxe Markdown [x] et [] permet de reconnaitre une tâche terminée d\u0026rsquo;une tâche à faire. L\u0026rsquo;utilisation d\u0026rsquo;Emojis est possible pour les méta-données.\nA noter la possibilité de créer des tableaux dynamiques à l’aide de requête, par exemple pour créer une liste de TODO à faire aujourd’hui.\nSon daily est centré autour d’une simple bullet list renseignée tout au long de la journée.\nChaque sujet commence par un tag (ex : #perso, #tech). Hoani utilise les sous-listes pour détailler le sujet.\nChaque jour, Hoani ouvre son Daily du jour et\nRegarde les tâches à réaliser Note en bullet list Crée une tâche quand nécessaire Termine les tâches accomplies Le bénéfice du numérique Comparé au manuscrit, les bénéfices du numérique sont nombreux :\nPersonnaliser son daily en ajoutant toutes les infos qui lui passent par la tête Suivre des variables quotidiennes (comme le temps passé à s\u0026rsquo;entrainer au Kendama) Afficher les tâches à réaliser et être notifié de celles en retard Automatiser certains traitements à l\u0026rsquo;aide de plugins additionnels : Templater, Tasks et Dataview Le contenu du Daily peut être très riche et comporter :\nMétadonnées avec tags Cartouche de navigation Widgets de progression mois/année Définition de la tâche ultime de la journée Rappel des tâches via un filtre de recherche et un report facilité des tâches Le journal en lui-même Entrainement au Kendama Des métriques sur livres (page en cours) et jeux vidéo (temps, winrate) Exemples de métadonnées d\u0026rsquo;un Daily permettant de suivre des variables quotidiennes :\nenergy_level_morning : 6 energy_level_evening : 7 mood_morning : insomniaque mood_evening : détendu walked : 400 working_place_morning : HOME La barre de navigation du Daily permet de passer rapidement du daily précédent au suivant, à la semaine associée …\nL\u0026rsquo;ajout de barres de progression est possible via le plugin Progressbar.\nLe numérique permet de créer d\u0026rsquo; autres types de notes périodiques que le daily. On peut, par exemple, se fixer des objectifs annuels (ex: courir un marathon), suivre visuellement leurs avancements puis, une fois la période écoulée, faire un bilan de la période et définir de nouveaux objectifs. Hoani insiste sur le fait que l\u0026rsquo;ouverture et la fermeture d\u0026rsquo;une période doit être vécue comme un rituel.\nPour automatiser la création de ces différentes notes périodiques, on peut s\u0026rsquo;aider du plugin Templater qui ajoute à Obsidian un langage de templating permettant d\u0026rsquo;exécuter des fonctions JavaScript. Ce plugin permet de pré-sélectionner un template en fonction du nom du fichier créé.\nConclusion Après la démonstration de son utilisation avancée d\u0026rsquo;Obsidian, Hoani reconnaît que le démarrage peut être long, qu\u0026rsquo;il peut être difficile de décider quoi noter et surtout comment organiser son coffre. Son conseil est de rester rigoureux, y noter ce que l\u0026rsquo;on souhaite et faire en sorte que cela reste amusant et donc pas une corvée. Ses slides sont en ligne : [Devfest Nantes 25] Révolutionnez votre prise de notes - du Bullet Journal à Obsidian. Son repo GitHub rend public ses templates Obsidian.\nPour ma part, j’ expérimente Obsidian depuis seulement deux semaines, dans un cadre purement personnel — ce billet a d’ailleurs été rédigé avec l’application. J’ai choisi de débuter sans installer le moindre plugin, histoire de me faire une idée précise de ce que le logiciel propose “out of the box”.\nPour l’instant, l’expérience est très agréable : l’ergonomie est soignée, la prise en main rapide et la rédaction des notes en Markdown particulièrement efficace.\nAu travail, pour ma prise de notes, je reste néanmoins sur OneNote, qui s’intègre parfaitement à l’écosystème Microsoft 365 et tire parti de Copilot pour la recherche et la synthèse de contenus.\n","link":"https://javaetmoi.com/2025/10/revolutionnez-votre-prise-de-notes-du-bullet-journal-a-obsidian/","section":"posts","tags":["devfest","obsidian"],"title":"Révolutionnez votre prise de notes : du Bullet Journal à Obsidian"},{"body":"","link":"https://javaetmoi.com/tags/errorprone/","section":"tags","tags":null,"title":"Errorprone"},{"body":"","link":"https://javaetmoi.com/tags/jspecify/","section":"tags","tags":null,"title":"Jspecify"},{"body":"La gestion de la nullabilité en Java a longtemps été source de bugs et de fragmentation. Contrairement à Kotlin par exemple, Java ne possède pas encore nativement de moyen d’exprimer la nullité d’un type. Qui n’aura donc jamais ragé contre une NullPointerException survenue en production ? En juin 2024, avec l’arrivée de la spécification JSpecify, soutenue par des acteurs majeurs comme Google, Microsoft, JetBrains, Oracle, Sonar ou bien encore Broadcom (Spring), l’écosystème Java dispose enfin d’une bibliothèque unifiée d’annotations de nullité. Pour bénéficier d’une détection efficace des NullPointerException dès la compilation, il est nécessaire de coupler JSpecify à des outils d’analyse statique comme NullAway (Uber) et ErrorProne (Google).\nCe court article explique comment mettre en place sur un projet d’entreprise la configuration Maven correspondante qui fera casser votre build et votre CI lorsque vous essayerez de passer une variable null en paramètre d’une méthode qui ne les accepte pas.\nDépendance Maven JSpecify Ajoutez simplement la dépendance suivante au niveau de la balise de votre pom.xml :\n\u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.jspecify\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;jspecify\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.0.0\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; A ce stade, les IDE comme IntelliJ supportant JSpecify seront à même de détecter des erreurs. Exemple extrait de Sring Petclinic dont les packages Java sont annotés avec @NullMarked :\nDans le cas où ces warnings n’apparaissent pas dans IntelliJ, vérifier que les inspections Nullability problems sont bien activées :\nConfiguration du Maven Compiler Plugin avec NullAway et ErrorProne Pour faire échouer le build Maven dans le cas où un développeur ne respecterait pas les annotations JSpecify, le compilateur Java doit être strictement configuré à l’aide des plugins Error Prone de Google et de son extension NullAway d\u0026rsquo;Uber :\n\u0026lt;plugin\u0026gt; \u0026lt;groupId\u0026gt;org.apache.maven.plugins\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;maven-compiler-plugin\u0026lt;/artifactId\u0026gt; \u0026lt;configuration\u0026gt; \u0026lt;parameters\u0026gt;true\u0026lt;/parameters\u0026gt; \u0026lt;compilerArgs\u0026gt; \u0026lt;arg\u0026gt;-XDcompilePolicy=simple\u0026lt;/arg\u0026gt; \u0026lt;arg\u0026gt;--should-stop=ifError=FLOW\u0026lt;/arg\u0026gt; \u0026lt;arg\u0026gt;-XDaddTypeAnnotationsToSymbol=true\u0026lt;/arg\u0026gt; \u0026lt;arg\u0026gt;-Xplugin:ErrorProne -XepOpt:NullAway:AnnotatedPackages=com.javaetmoi.myapp -XepOpt:NullAway:UnannotatedSubPackages=com.javaetMoi.myapp.controller.api,com.javaetMoi.myapp.controller.dto -XepOpt:NullAway:JSpecifyMode=true -XepDisableAllChecks -Xep:NullAway:ERROR -XepExcludedPaths:.*/src/test/java/.* -XepDisableWarningsInGeneratedCode \u0026lt;/arg\u0026gt; \u0026lt;annotationProcessorPaths\u0026gt; \u0026lt;path\u0026gt; \u0026lt;groupId\u0026gt;com.google.errorprone\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;error_prone_core\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;2.42.0\u0026lt;/version\u0026gt; \u0026lt;/path\u0026gt; \u0026lt;path\u0026gt; \u0026lt;groupId\u0026gt;com.uber.nullaway\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;nullaway\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;0.12.11\u0026lt;/version\u0026gt; \u0026lt;/path\u0026gt; \u0026lt;/annotationProcessorPaths\u0026gt; \u0026lt;/configuration\u0026gt; \u0026lt;/plugin\u0026gt; Explications clés :\nL\u0026rsquo;option -Xep:NullAway:ERROR fait échouer le build Maven lorsqu’un éventuel NullPointerException est détecté. Par défaut, de simples WARNING sont générés dans la console et risquent donc de passer inaperçus.\nL’option - -Xplugin:ErrorProne active le plugin ErrorProne.\nL’option -XepDisableAllChecks désactive toutes les règles de vérification de code ErrorProne. On n’utilise ici ErrorProne que pour la nullsafety. Libre à vousd’utiliser pleinement ErrorProne ou pas.\nL’option -XepOpt:NullAway:AnnotatedPackages=com.javaetmoi.myapp active NullAway sur le package Java racine de l’application métier. A noter que cette option peut être remplacer par -XepOpt:NullAway:OnlyNullMarked afin de ne scanner que les packages annotés avec @NullMarked.\nA contrario, l’option -XepOpt:NullAway:UnannotatedSubPackages=com.javaetMoi.myapp.controller.api,com.javaetMoi.myapp.controller.dto désactive NullAway sur une liste de sous-packages. Cela permet d’exclure le code généré par des plugins comme cxf-codegen-plugin ou MapStruct qui ne supportent pas encore JSpecify.\nDans le cadre d’utilisation de JSpecify dans un projet legacy, il peut-être intéressant d’exclure de l’analsyse les classes de tests avec l\u0026rsquo;option -XepExcludedPaths:.*/src/test/java/.*\nL\u0026rsquo;option -XepOpt:NullAway:JSpecifyMode=true active le support complet de JSpecify et exploite pleinement la sémantique de JSpecify, notamment au niveau des types génériques.\nL\u0026rsquo;argument javac -XDaddTypeAnnotationsToSymbol=true est requis par la version 0.12.11 de NullAway lors de l\u0026rsquo;utilisation d\u0026rsquo;une version de Java antérieure à Java 22.\nToutes les options de NullAway peuvent être retrouvées sur sa page de Configuration.\nA partir de la version 16 du langage Java, la documentation d\u0026rsquo;installation d\u0026rsquo;error prone explique comment activer des flags à la JVM via le fichier .mvn/jvm.config:\n--add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED --add-opens jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED --add-opens jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED Avec cette configuration Maven, toute tentative d’accès à une référence potentiellement nulle sera détectée… dès la compilation, que ce soit sur notre poste de dév ou notre CI Jenkins, GitHub ou GitLab ! Fini les NullPointerException surprises en production.\nExemple d’une commande mvn compile sur l’exemple précédent :\n[ERROR] COMPILATION ERROR : [INFO] ------------------------------------------------------------- [ERROR] /Users/arey/Dev/GitHub/spring-petclinic/spring-petclinic/src/main/java/org/springframework/samples/petclinic/owner/OwnerController.java:[106,82] [NullAway] passing @Nullable parameter \u0026#39;lastName\u0026#39; where @NonNull is required (see http://t.uber.com/nullaway ) Conclusion Cinq années auront été nécessaires par le groupe de travail JSpecify pour formaliser et se mettre d’accord sur quatre annotations Java. Depuis un an, nos outillages, nos IDE et nos frameworks convergent vers ce lot d’annotations. Quelques bugs de jeunesse existent encore, à l’image de ce bug Maven Build Cache Extension corrigé par mon collègue Marco et qui faisait échouer la synchronisation Maven IntelliJ avec le message \u0026ldquo;java: plug-in not found: ErrorProne\u0026rdquo;.\nA nos projets métiers de se mettre à JSpecify et d’être prêts pour Spring Framework 7 et Spring Boot 4 qui sortiront fin 2025.\nSi vos projets exploitent les annotations JSR-305 ou JetBrains, migrez vers JSpecify à l’aide de la recette OpenRewrite MigrateToJSpecify.\nSachez enfin que Dan Smith a proposé la JEP draft: Null-Restricted and Nullable Types (Preview) visant à ajouter des marqueurs syntaxiques directement au niveau du langage Java. Adopter JSpecify aujourd’hui facilitera l’adoption de cette JEP.\n","link":"https://javaetmoi.com/2025/07/jspecify-nullaway-errorprone-la-configuration-maven-ultime-pour-dire-adieu-aux-nullpointerexception/","section":"posts","tags":["errorprone","jspecify","maven","nullaway"],"title":"JSpecify + NullAway + ErrorProne : la configuration Maven ultime pour dire adieu aux NullPointerException"},{"body":"","link":"https://javaetmoi.com/categories/maven/","section":"categories","tags":null,"title":"Maven"},{"body":"","link":"https://javaetmoi.com/tags/maven/","section":"tags","tags":null,"title":"Maven"},{"body":"","link":"https://javaetmoi.com/tags/nullaway/","section":"tags","tags":null,"title":"Nullaway"},{"body":" Lors de la conférence Devoxx France 2025, j’ai participé à un hands-on lab de 2h intitulé Sortir des ORMs avec jOOQ. Acronyme de « Java Object Oriented Querying », jOOQ se présente comme une alternative à JPA permettant d’ écrire des requêtes SQL en Java via une fluent API. Animé par Sylvain Decout et Samuel Lefebvre, cet atelier visait à migrer une application Spring Boot / JPA vers jOOQ à l’aide du starter Spring Boot pour jOOQ. Pour les curieux, le repo de l’atelier est disponible sur Github : jooq-handson.\nFort de cette découverte, je me suis à mon tour prêté à l’exercice de migrer vers jOOQ la couche de persistance Spring Data JPA de l’application démo Spring Petclinic. Un nouveau fork est né : spring-petclinic-jooq. Bienvenue à ce dernier dans la communauté Spring Petclinic.\nL’usage de jOOQ se rapproche de l’utilisation de JdbcTemplate. Le développeur maitrise le nombre de requêtes envoyées à la base de données relationnelle. Ce qui les différencie, c’est la syntaxe : pas de SQL, mais une API Java fluide et type-safe spécifique à jOOQ qu’il va falloir appréhender. Rassurez-vous, cette API se rapproche du SQL : on y retrouve les mots clés select, update, insertInto, where, from, join, on, as… A ceux-ci, on ajoute des mots clés spécifiques à jOOQ : paginate, fetch, convertFrom … La documentation de jOOQ est très complète. On y apprend comment écrire des requêtes complexes à base de window function ou de Common Table Expressions (CTE) et comment utiliser des fonctionnalités avancées de SQL que peu de frameworks ORM supportent nativement : JSON functions, PIVOT, MERGE, UNION …\nCet article a pour objectif d’expliquer les étapes adoptées pour migrer l’implémentation Spring Data JPA des repository vers jOOQ. Des exemples de code avant / après y sont proposés.\nConfiguration du build Spring Boot supporte nativement l’usage des versions commerciales et Open Source de jOOQ. Dans le pom.xml ou le fichier build.gradle, commencer par déclarer le starter Spring Boot pour jOOQ spring-boot-starter-jooq :\n\u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter-jooq\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; L’étape suivante consiste à générer les classes Java à partir du schéma de la base de données. jOOQs propose différentes possibilités: à partir du script DDL de création du schéma comme sur Petclinic, de scripts Liquibase ou bien encore des méta-données d’une base existante.\nLes plugins jooq-codegen-maven ou jooq-codegen-gradle sont à configurer.\nVoici un exemple extrait de jOOQ Spring Petclinic :\n\u0026lt;plugin\u0026gt; \u0026lt;groupId\u0026gt;org.jooq\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;jooq-codegen-maven\u0026lt;/artifactId\u0026gt; \u0026lt;executions\u0026gt; \u0026lt;execution\u0026gt; \u0026lt;goals\u0026gt; \u0026lt;goal\u0026gt;generate\u0026lt;/goal\u0026gt; \u0026lt;/goals\u0026gt; \u0026lt;/execution\u0026gt; \u0026lt;/executions\u0026gt; \u0026lt;configuration\u0026gt; \u0026lt;generator\u0026gt; \u0026lt;database\u0026gt; \u0026lt;name\u0026gt;org.jooq.meta.extensions.ddl.DDLDatabase\u0026lt;/name\u0026gt; \u0026lt;properties\u0026gt; \u0026lt;property\u0026gt; \u0026lt;key\u0026gt;scripts\u0026lt;/key\u0026gt; \u0026lt;value\u0026gt;src/main/resources/db/h2/schema.sql\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;property\u0026gt; \u0026lt;key\u0026gt;sort\u0026lt;/key\u0026gt; \u0026lt;value\u0026gt;semantic\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;property\u0026gt; \u0026lt;key\u0026gt;unqualifiedSchema\u0026lt;/key\u0026gt; \u0026lt;value\u0026gt;none\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;property\u0026gt; \u0026lt;key\u0026gt;defaultNameCase\u0026lt;/key\u0026gt; \u0026lt;value\u0026gt;as_is\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;/properties\u0026gt; \u0026lt;/database\u0026gt; \u0026lt;/generator\u0026gt; \u0026lt;/configuration\u0026gt; \u0026lt;dependencies\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.jooq\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;jooq-meta-extensions\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;${jooq-meta-extensions.version}\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;/dependencies\u0026gt; \u0026lt;/plugin\u0026gt; La classe org.jooq.meta.extensions.ddl.DDLDatabase provenant de l’extension jooq-meta-extensions permet au plugin jooq-codegen-maven d’exploiter le script DDL src/main/resources/db/h2/ schema.sql utilisé par défaut lors du démarrage de l’application avec le profil Spring par défaut.\nDans le package org.jooq.generated.tables, l’exécution du plugin jOOQ génère une classe Vets héritant de la classe TableImpl et modélisant la table éponyme. La classe VetsRecord a également été générée dans le sous-package records. Elle représente une ligne de la table pets.\nNous verrons leurs usages lors de la migration de la classe PetRepository.\nMigration des repositories L’une des forces de jOOQ est qu’il sait cohabiter aux côtés de JPA. On peut donc migrer au fil de l’eau les respositories d’une application et même choisir de conserver les 2 solutions en fonction des besoins. Cette capacité a été pratique dans le travail de migration : chaque repository a été migré l’un après l’autre. L’application Petclinic est restée fonctionnelle tout du long.\nPremier changement notable : la nature des repositories qui passent d’une interface héritant de l’interface Repository de Spring Data JPA à une classe concrète qu’on annote avec @Repository et dont le constructeur accepte une instance de DSLContext.\nAvant :\npublic interface VetRepository extends Repository\u0026lt;Vet, Integer\u0026gt; { Après:\n@Repository public class VetRepository { private final DSLContext dsl; public VetRepository(DSLContext dslContext) { this.dsl = dslContext; } Continuons par changer l’implémentation d’une première requête SQL simple utilisant un SELECT.\nRequête native SQL utilisée par Spring Data JPA :\n@Query(\u0026#34;SELECT ptype FROM PetType ptype ORDER BY ptype.name\u0026#34;) List\u0026lt;PetType\u0026gt; findPetTypes(); Implémentation équivalente avec jOOQ :\npublic List\u0026lt;PetType\u0026gt; findPetTypes() { return dsl .selectFrom(TYPES) .orderBy(TYPES.NAME) .fetchInto(PetType.class); } On retrouve ici tous les éléments de la requête SQL, mais avec une syntaxe Java jOOQ équivalente. Le mot clé SQL « SELECT » est remplacé par la méthode selectFrom(), le « ORDER BY » par la méthode orderBy(). A noter que nous n’utilisons pas de chaines de caractères pour nommer les tables et les colonnes, mais les constantes générées par le plugin jOOQ. Ainsi, en cas de changement de schéma (ex : nom de colonne renommée), le code Java ne compilera plus et il faudra l’adapter. Avec cette approche, les erreurs de syntaxe ne sont plus possibles. On perçoit ici toute la sécurité apportée par la type-safety de jOOQ.\nEnfin, la méthode fetchInto() mappe les lignes retournées par la base dans une liste d’instance de PetType.\nLà où JPA et Hibernate nous facilitaient la sauvegarde de nos entités JPA, jOOQ va demander un travail nettement plus important. En effet, la méthode save() de l’interface CrudRepository de Spring Data JPA ne demandait qu’à être appelée. La magie des ORM opérait grâce aux annotations JPA apposées sur les entités. jOOQ nécessite de prévoir les 2 requêtes SQL correspondantes et d’effectuer à la main le binding des propriétés du Owner vers les colonnes. Exemple de sauvegarde d’un Owner :\npublic Integer saveOrUpdateDetails(Owner owner) { if (owner.isNew()) { return requireNonNull( dsl.insertInto(OWNERS) .set(mapOwnerToRecord(owner)) .returningResult(OWNERS.ID) .fetchOne()) .getValue(OWNERS.ID); } else { dsl.update(OWNERS) .set(mapOwnerToRecord(owner)) .where(OWNERS.ID.eq(owner.getId())) .execute(); return owner.getId(); } } private Map\u0026lt;Field\u0026lt;?\u0026gt;, Object\u0026gt; mapOwnerToRecord(Owner owner) { return Map.of(OWNERS.FIRST_NAME, owner.getFirstName(), OWNERS.LAST_NAME, owner.getLastName(), OWNERS.ADDRESS, owner.getAddress(), OWNERS.CITY, owner.getCity(), OWNERS.TELEPHONE, owner.getTelephone()); } Rien de compliqué, mais un peu plus verbeux.\nL’un des principaux avantages de jOOQ consiste à maitriser le nombre de requêtes SQL envoyées à la base. En l’occurrence, dans cet exemple, une seule et unique requête de type UPDATE est envoyée lors d’un mise à jour.\nAvec l’implémentation Spring Data JPA, dans le cadre de la mise à jour d’un Owner, comme expliqué dans l’article Skip Select Before Insert in Spring Data JPA, Spring Data JPA appelle la méthode merge() de l’entity manager JPA qui, si l’entité n’est pas en cache, va charger le Owner en exécutant autant de requêtes SQL de type SELECT que nécessaires.\nAutre différence notable : jOOQ laisse décider du ou des champs à mettre à jour. Dans notre exemple, les Pets associés à leur Owner ne seront par exemple pas mis à jour. Avec JPA, on utilisait le cascading et l’attribut cascade des annotations comme @OneToMany.\nDans la même idée, lors de requêtes de type SELECT, les jointures entre tables devront être systématiquement précisées. A titre d’exemple, charger un animal et son type nécessitera un appel à join() :\n@Transactional(readOnly = true) public Optional\u0026lt;Pet\u0026gt; findByIdWithoutVisits(Integer petId) { return dsl.select() .from(PETS) .join(PETS.types_()) .where(PETS.ID.eq(petId)) .fetchOptional(PetRepository::toPet); } Noter ici l’utilisation d’une jointure implicite basée sur la clé étrangère, évitant ainsi d’ajouter une clause ON entre la PK de PETS et la FK de TYPES.\nAvec une association 1:1, le chargement et le mapping du type d’animal ne présente aucune difficulté :\nprivate static Pet toPet(org.jooq.Record row) { return new Pet(row.get(PETS.ID), row.get(PETS.NAME), row.get(PETS.BIRTH_DATE), new PetType(row.get(PETS.TYPE_ID), row.get(TYPES.NAME))); } Le chargement des associations 1:N se complexifie. L’usage de l’ opérateur mulstiset() du SQL qui est supporté par jOOQ permet de charger les Vets et leurs Specialities en une seule requête :\npublic List\u0026lt;Vet\u0026gt; findAll(){ return dsl.select(VETS.ID, VETS.FIRST_NAME, VETS.LAST_NAME, MULTISET_SPECIALITIES) .from(VETS) .leftJoin(VETS.vetSpecialties()) .orderBy(VETS.ID) .fetch(VetRepository::toVet); } private static final Field\u0026lt;List\u0026lt;Specialty\u0026gt;\u0026gt; MULTISET_SPECIALITIES = multiset( select(VET_SPECIALTIES.specialties().ID, VET_SPECIALTIES.specialties().NAME) .from(VET_SPECIALTIES) .where(VET_SPECIALTIES.VET_ID.eq(VETS.ID))) .as(\u0026#34;specialties\u0026#34;) .convertFrom(result -\u0026gt; result.map(it -\u0026gt; new Specialty(it.get(SPECIALTIES.ID), it.get(SPECIALTIES.NAME)))); private static Vet toVet(Record4\u0026lt;Integer, String, String, List\u0026lt;Specialty\u0026gt;\u0026gt; row) { return new Vet(row.get(VETS.ID), row.get(VETS.FIRST_NAME), row.get(VETS.LAST_NAME), new HashSet\u0026lt;\u0026gt;(row.get(MULTISET_SPECIALITIES))); } jOOQ permet d’imbriquer plusieurs multiset afin de charger les visites des animaux d’un propriétaire en une seule requête. Je vous renvoie à la classe OwnerRepository.\nPour finir, les écrans « Find Owners » et « Veterinarians » affichent les résultats de manière paginée. jOOQ supporte la pagination au travers de la Seek Method (aussi appelée Keyset paging) ou du calcul des méta-données de pagination en une seule requête SQL. C’est cette dernière approche qui a été utilisée sur jOOQ Petclinic afin de garder iso-fonctionnels les écrans paginés. Les plus curieux peuvent se référer à l’implémentation de la méthode findAll(Pageable pageable) de VetRepository et à la méthode paginate() du JooqHelper. Sur le même modèle que ce que propose Spring Data, des records Pageable et Page ont été introduits dans la base de code.\nAu revoir JPA Une fois l’ensemble des Repository migrés, la dernière étape a consisté à retirer la dépendance spring-boot-starter-data-jpa ainsi que toutes les annotations JPA apposées sur les entités (@Entity, @Table, @ ManyToMany …).\nDébarrassé de JPA, nous pouvons revoir en partie le design de l’application qui avait été limité par ce dernier. En effet, les entités JPA ne peuvent pas être modélisées avec des record Java. Suite à la migration vers jOOQ, les entités du domaine métier de Spring Petclinic n’ont plus d’adhérence avec la couche de persistance. Les classes immutables ont pu être converties en record. Exemple du value object Speciality :\npublic record Specialty(Integer id, String name) { } Le refactoring de la modélisation aurait pu aller plus loin, mais ce n’était pas l’objectif de cette version de Petclinic dédiée à jOOQ et non à la Clean Architecture. Peut-être l’objet d’un prochain article ?\nConclusion A travers l’exemple de migration de l’application démo Spring Petclinic, cet article donne un aperçu des possibilités offertes par jOOQ. Cette librairie est mature, a plus de 15 ans (la version 1.0.0 de jOOQ est sortie en 2010) et est utilisée par de grands comptes comme Apple, Allianz et Mastercard.\nNotez néanmoins que jOOQ possède un système de double licence: commerciale et Open Source. Les distributions commerciales de jOOQ maintiennent un support versionné des SGBDR. A contrario, l\u0026rsquo;édition Open Source de jOOQ ne supporte que la dernière version des SGBDR Open Source. A ce titre, l’utilisation de jOOQ avec Oracle et SQL Server requière une licence commerciale.\nEn replongeant dans le SQL, je me suis aperçu que j’étais passé à côté de certaines fonctionnalités avancées comme les MULTISET. A retenir.\nEnfin, je remercie Sylvain pour sa relecture du code de Spring Petclinic jOOQ et ses conseils avisés. J’invite tous les autres experts jOOQ à venir améliorer le repository spring-petclinic-jooq en soumettant des Issues ou en proposant des Pull Requests.\n","link":"https://javaetmoi.com/2025/06/de-spring-data-jpa-a-jooq/","section":"posts","tags":["devoxx","jooq","jpa","spring-data","sql"],"title":"De Spring Data JPA à jOOQ"},{"body":"","link":"https://javaetmoi.com/tags/devoxx/","section":"tags","tags":null,"title":"Devoxx"},{"body":"","link":"https://javaetmoi.com/tags/jooq/","section":"tags","tags":null,"title":"Jooq"},{"body":"","link":"https://javaetmoi.com/tags/jpa/","section":"tags","tags":null,"title":"Jpa"},{"body":"","link":"https://javaetmoi.com/tags/spring-data/","section":"tags","tags":null,"title":"Spring-Data"},{"body":"","link":"https://javaetmoi.com/tags/sql/","section":"tags","tags":null,"title":"Sql"},{"body":"","link":"https://javaetmoi.com/tags/java/","section":"tags","tags":null,"title":"Java"},{"body":"Date : 16 avril 2025\nConférence : Devoxx France 2025\nSpeaker : José Paumard (Oracle)\nFormat : Conférence 45 mn\nSupport : slides sur Speakerdeck / replay Youtube\nJava Developer Advocate chez Oracle, José Paumard nous présente la nouvelle API Gatherers qui, depuis Java 24, vient se greffer sur l’ API Stream Java sortie il y’a 11 ans avec Java 8.\nTout comme l’API Collector, José commence par rappeler que l’API Gatherers est indépendante de l’API Stream. Cette API a été introduite dans Java via la JEP 485 Stream Gatherers conduite par Viktor Klang. Les plus curieux pourront regarder la vidéo Youtube du Deep Dive qu’a animé Viktor lors de la conférence JavaOne qui s’est tenue en mars 2025.\nL’article The Gatherer API permet également d’approfondir votre étude des Gatherers. Notez que le site dev.java permet désormais d’exécuter des snippets Java (pas directement dans le navigateur, mais sur un serveur Cloud).\nToutes les classes et interfaces de l’API Gatherers ont été ajoutées au java.util.stream.\nOpérations intermédiaires et terminales d’un Stream Pour rappel, un Stream se connecte à une source de données (collections, fichier, générateur de nombres aléatoires, regex). Un stream est composé de :\nzéro, une ou plusieurs opérations intermédiaires qui retournent un Stream une seule et unique opération terminale qui retourne un résultat et clôture le Sream. Viktor assimile l’API Stream à celle d’un Builder: on décrit un pipeline d’opérations puis on appelle l’opération terminale pour déclencher son traitement.\nExemples d’ opération terminales proposées par l’API Stream :\n`reduce() : opération de réduction findFirst() : renvoie un objet de type Optional qui encapsule le premier élément du Stream s\u0026rsquo;il existe, ne consomme pas tous les éléments du Streams. collect() : prend en paramètre un Collector toList() : méthode raccourcie disponible depuis Java 16 Les Collector permettent de créer ses propres opérations de réduction. Gatherer est le pendant des Collector pour les opérations intermédiaires. Une différence notable est qu’un Collector ne peut pas interrompre un Stream : il ne le connait pas.\nLe JDK propose de nombreuses opérations intermédiaires comme map(), filter(), dropWhile(), limit() ou bien encore mapMulti() ajoutée plus récemment. L’API Gatherers va nous permettre de créer nos propres opérations intermédiaires. Ce n’était pas possible jusque-là. Parmi ces opérations intermédiaires, il existe des opérations stateless comme filter() et des opérations statefull come sorted() qui doivent consommer tous les éléments du stream avant de produire quelque chose vers le down stream.\nIl n’y avait pas moyen de créer d’opérations intermédiaires jusqu’aux Gatherers.\nQue propose l’API Gatherer ? L’interface générique Gatherer s’appuie sur 3 paramètres :\ninterface Gatherer\u0026lt;T, A, R\u0026gt; { Integrator\u0026lt;A, T, R\u0026gt; integrator(); } T : type des éléments consommés A : type mutable utilisé en interne par les Gatherers R : type des éléments poussés dans le down stream Avec sa méthode principale integrator(), José compare l’interface Gatherer à une interface fonctionnelle de type Supplier.\nL’interface Gatherer met à disposition 3 interfaces fonctionnelles imbriquées dont nous étudierons le fonctionnement : Downstream, Greedy et Integrator.\nExemple de le l’interface Integrator :\n@FunctionalInterface interface Integrator\u0026lt;A, T, R\u0026gt; { boolean integrate(A state, T element, Downstream\u0026lt;? super R\u0026gt; downstream); } Afin de pouvoir utiliser le Gatherer, l’interface Stream de l’API Stream propose désormais depuis Java 24 la méthode gather :\nStream\u0026lt;R\u0026gt; downStream = upstream.gather(gatherer); Le JDK s’enrichit de la classe factory Gatherers (notez son pluriel) utilisées par les différentes implémentations des méthodes of() de l\u0026rsquo;interface `Gatherer.\nPublier dans le Downstream Un Downstream reçoit des données traitées par une opération intermédiaire. C’est le flux de sortie d’un Gatherer.\nVoici un exemple de Gatherer chargé de pousser un élément dans le Downstream :\nGatherer\u0026lt;T, ?, R\u0026gt; gatherer = Gatherer.of( (_, element, downStream) -\u0026gt; downStream.push(element) // returns a boolean ); Le booléen renvoyé en retour est important. Son fonctionnement est subtil : renvoyer false permet l\u0026rsquo;arrêt du traitement des éléments suivants. Il ne se passe alors plus rien lorsqu’on pousse des éléments au downStream qui n’en accepte désormais plus. Aucune exception n’est levée. Cela peut surprendre.\nDans le jargon de l’API Gatherer, lorsqu’un Integrator retourne directement la valeur du downstream.push(element), on dit qu’il est Greedy. Il traitera nécessairement tous les éléments du Stream. Son exécution est optimisée. Exemple :\nGatherer\u0026lt;T, ?, R\u0026gt; gatherer = Gatherer.of( Integrator.of((_, element, downstream) -\u0026gt; downstream.push(element)) ); Lorsqu’un Integrator n’utilise pas de coupe-circuit et consomme donc l’intégralité des éléments reçus, il est recommandé d’utiliser la méthode factory Integrator.ofGreedy() pour instancier un Integrator :\nGatherer\u0026lt;T, ?, R\u0026gt; gatherer = Gatherer.of( Integrator.ofGreedy((_, element, downstream) -\u0026gt; downstream.push(element)) ); Un Downstream possède un état nommé rejecting. La méthode isRejecting() de l’interface Downstream propose d’y accéder. Cet état a 3 propriétés :\nCommence à false Ne peut commuter que de false vers true (ne peut pas se rouvrir) L’état ne peut commuter que lors d’un push() =\u0026gt; règle spécifique aux API du JDK José nous met en garde : dans un Integrator, l’appel à la méthode isRejecting() ne sert à rien. Il s’agit d’une fausse optimisation qui s’apparente à du code mort.\n(_, element, downstream) -\u0026gt; { if (downstream.isRejecting()) { // return false; // Condition inutile } // return downstream.push(mapper.apply(element)); } José continue sa présentation en nous expliquant les bonnes pratiques à adopter lorsqu’on publie sur le Downstream :\nNe pas faire de test isRejecting() sur le Downstream Privilégiez l’usage de la méthode allMath() plus efficace que takeWhile() Fermer les ressources si nécessaire. Lorsque le Stream agit sur un fichier, il faut fermer le fichier et ne pas oublier le try with ressources Exemple exempté de bugs :\n(_, element, downstream) -\u0026gt; { try (Stream\u0026lt;R\u0026gt; elements = flatMapper.apply(element);) { return elements.allMatch(downstream::push); } } Un Downstream n’est pas un objet thread-safe. Il est donc nécessaire de ne pas générer d’effet de bord sur les données externes. Attention aux race conditions et plus particulièrement dans les parallel streams.\nA ce titre, la méthode Gatherer.oSequential() permet de créer un Gatherer séquentiel (non parallélisable).\nL’élément state est un état mutable pouvant être utilisé par le Gatherer. En complément de l\u0026rsquo;Integrator, il est nécessaire de fournir à l’API de création d\u0026rsquo;un Gatherer un Supplier chargé d’initialiser l’état du state.\nExemple d’un Gatherer limitant le nombre d’éléments et initialisant un compteur :\nclass Counter { long count = 0L; } var gatherer = Gatherer.ofSequential( Counter::new, // the initializer (state, element, downstream) -\u0026gt; { if (state.count++ \u0026lt; limit) { return downstream.push(element); } else { return false; } }); A noter que l’opérateur var retient le type des classes anonymes.\nPour agir sur l’ensemble des données du Gatherer, on peut stocker les éléments dans une collection telle qu’un HashSet dans l’exemple suivant « Distinct Gatherer »:\nvar gatherer = Gatherer.ofSequential( () -\u0026gt; new Object() { Set\u0026lt;T\u0026gt; set = new HashSet\u0026lt;\u0026gt;(); }, (state, element, downstream) -\u0026gt; { if (state.set.add(element)) { return downstream.push(element); } else { return true; } }); Pour publier l’état final d’un Gatherer, on peut ajouter après l\u0026rsquo;Initializer et l\u0026rsquo;Integrator une 3ième lambda de type BiConsumer agissant comme finisher et pouvant consommer tous les éléments du state :\nvar gatherer = Gatherer.ofSequential( () -\u0026gt; new Object() { Set\u0026lt;T\u0026gt; set = new TreeSet\u0026lt;\u0026gt;(); }, (state, element, downstream) -\u0026gt; { ... }, (state, downstream) -\u0026gt; { // finisher state.set.stream().allMatch(downstream::push); }); Les Parallel Gatherers Les développeurs Java peuvent choisir de construire un Gather supportant ou non le parallélisme et les parallel Streams. A cet effet, 2 méthodes de type fabrique sont à leur disposition :\nGatherer.of() Gatherer.ofSequential() Pour supporter le parallélisme, l’API Gatherer adopte le principe suivant : un objet state par thread. Cela permet de ne pas utiliser de collections synchronisées dégradant les performances.\nDans chaque Stream parallèle, on a donc autant de state que de threads. A la fin de l’opération intermédiaire, il est nécessaire d’utiliser un Combiner pour combiner tous les états.\nCe Combiner est un 4ième paramètre à passer à la méthode factory of() :\nvar gatherer = Gatherer.of( () -\u0026gt; new Object() { Set\u0026lt;T\u0026gt; set = new HashSet\u0026lt;\u0026gt;(); }, (state, element, downstream) -\u0026gt; { // executed in state.set.add(element); // different threads return true; }, (state1, state2) -\u0026gt; { // combiner state1.set.addAll(state2.set); return state1; }, (state, downstream) -\u0026gt; { // finisher state.set.allMatch(downstream::push); } ); Les Sequential Gatherers ne peuvent pas être appelés en même temps depuis différents thhreads. Ils ne possèdent pas de Combiner. Pour autant, José nous explique que l’API Stream est capable de séquencer les appels vers un Sequential Gatherer. Cette fonctionnalité est nouvelle et donc à utiliser avec précaution. Tester les perfs.\nPour aller plus loin, José nous invite à consulter le repo GitHub SvenWoltmann/stream-gatherers. Le JDK vient avec de nouveaux Gatherers comme scan(), fold() ou bien encore mapConcurrent().\nDes librairies tierces comme gatherers4j proposent également leur propres gatherers : reverse(), repeat(n), groupBy(fn)\u0026hellip;\nPour conclure, retenons qu’un Gatherer est construit sur 4 éléments. Tous ne sont pas obligatoires.\n","link":"https://javaetmoi.com/2025/04/api-gatherers-outil-qui-manquait-a-vos-streams/","section":"posts","tags":["devoxx","java"],"title":"L’API Gatherers : l’outil qui manquait à vos Streams"},{"body":"","link":"https://javaetmoi.com/tags/architecture/","section":"tags","tags":null,"title":"Architecture"},{"body":"Conférence : Devoxx France 2025\nDate : 17 avril 2025\nSpeakers : Cyrille Martraire (Arolla), Eric Le Merdy (QuickSign) remplaçant de Christian Sperandio (Arolla)\nFormat : Conférence (45mn) / Replay Youtube\nCette conférence a pour objectif d’ ouvrir les portes en nous donnant les clés de l’architecture. Pour seconder Cyrille, Eric a du remplacer Christian au pied levé.\nUn constat est posé. Sur les dix dernières années, les systèmes ont changé : ils sont devenus modulaires, de plus en plus distribués. La modularité permise par le Cloud permet de répartir la charge. Il y’a de plus en plus d’interconnexions entre briques applicatives.\nL’architecture bouge tout le temps, évolue constamment.\nQue doit-on savoir ? Pour commencer, on ne saura jamais tout et il faudra vivre avec. Personne ne sait tout. Même le plus capé des architectes.\nComme fil conducteur, Cyrille et Eric prennent un exemple réel issu du monde des télécommunications.\nPour cahier des charges, le client précise que le système va recevoir des fichiers chaque minute et doit les intégrer tous les 15mn. Contexte : ces fichiers viennent d’équipements télécom.\nPremière question à se poser : « est-ce possible de synchroniser la temporalité ? ».\nRéponse du client : « Non, ce n’est pas possible ».\nLa brique centrale est nommée Aggregator.\nDiagramme de contexte C4 correspondant :\nPremière clé donnée dans ce talk : commencer par identifier le problème.\nComment l’appliquer : quel est le but ? Ici c’est d’agréger les données reçues.\nCette première clé parait banal : penser problème avant de penser à la solution. Cet adage bien connu s’applique : « un problème bien posé est à moitié une solution ».\nAprès avoir cerner le problème, on continue en prenant en compte les nombreux Software Quality Attributes dont font partis le cout, la performance, la sécurité ou bien encore le sourcing des dévelopeurs. Liste complète sur arc42-templates et la FAQ C-1-2.\nExaminons à présent les* contraintes du système.\n1ière contrainte : disponibilité\nToujours Up pour recevoir les données. Calcule de données toutes les 15mn.\n2nde contrainte : performance\nLe besoin initial mentionnait la réception d’un fichier par minute. En questionnant le métier, on dénombre un fichier par équipement. Sachant qu’il y’a 50 équipements, cela ferait 50 fichiers. Pas tout à fait, puisqu’un équipement compte 40 000 capteurs. Au total, ce sont 6 milliards de données que le système devra traiter toutes les 15 minutes.\nLa formule de calcul de l’agrégation est compliquée ; ce n’est pas de simples additions.\nLe métier souhaiterait que le calcul soit instantané. Jouant sur le cout financier d’une telle exigence, Eric a réussi à négocier avec le client un temps de traitement de 2 minutes max. Cette durée est acceptable au vu du besoin : anticiper les pannes et remonter des alertes.\nSeconde clé donnée dans ce talk : négocier, étudier, éduquer les gens.\nPas nécessaire de mettre systématiquement de la cohérence transactionnelle partout.\nLes contraintes techniques nous guident pour définir l’architecture technique. Cette approche n’est pas antinomique avec le DDD. Dans notre exemple, il existe une corrélation entre les contraintes techniques et le découpage en sous-domaine.\nOn peut identifier 2 sous-domaines : le parsing lors de l’ingest et le calcul de statistiques.\nUne troisième clé nous est donnée : penser modulaire pour adresser le problème.\nVoyons à présent comment implémenter ces 2 sous-domaines.\nOn pourrait partir sur 2 services. Mais dans un premier temps, Eric propose de commencer par seul service, plus simple, plus facile à implémenter et livrer. Par contre, afin de préparer un éventuel futur découplage, on utilise l’approche pragmatique de modular monolith. Bel exercice de frugalité : une solution distribuée est remplacée par un monolith.\nL’architecture se pense à différents niveaux, à plusieurs.\nLes architectes d’entreprise ont souvent une vue d’ensemble globale. Les développeurs vont quant à eux s’intéresser davantage aux technologies.\nUn conseil, garder en tête cet objectif : bien s’entendre avec tout le monde\nLes différents types d’architectures ont leurs avantages et inconvénients. Voici celles qui auraient pu être choisies :\nMicroservices : modularité jusqu’au bout Modular monolith : facilite le découpage en microservices Function as a Service Big Ball of Mud : monolith avec archi spaghetti Parmi les contraintes techniques, le vrai risque consiste à tenir le délai de traitement d’agrégation des données en dessous des 2 minutes. La première étape consiste à lever ce risque. Il faut lever ce risque et commencer les développements.\nRéversible, l’ architecture n°2 est retenue avec une approche hexagonale. On reste pragmatique : les deux sous-domaines s’appellent dans la même JVM par appel de fonction. Cyrille rappelle que l’architecture hexagonale demande de créer un peu plus de code, mais ce n’est pas les 30 secondes que met la création d’une interface qui va les ralentir. Cela permet de prévoir des options pas chères pour être réversible et changer son architecture en cours de route. Les décisions sont réversibles.\nUne première version de l’application est déployée en production. Passent 1mn, puis 2, puis 5. On coupe tout. Trop long. Cela ne marche pas. Cyrille invite à célébrer ce constat : on sait que çà ne marche pas. Et on l’a découvert très vite.\nLa cause est rapidement identifiée : l’agrégateur du monolith est mono-thread. 3 solutions son envisagées :\n1. Solution 1 : mono instance avec du multi-threading. Plus de vCPU, worker pools.\n2. Solution 2 : multi instance avec du pub-sub. Rien à faire. On s’appuie sur un service du Cloud Provider. Clé : on reconnait les problèmes difficiles et on les délègue à du middleware en managé.\n3. Solution 3 : combine multi-thread et multi-instance : trop compliqué et trop chère. Combine tous les inconvénients. A ne pas faire.\nApproche choisie : solution 2. L’architecture est l’art du tradeoff (du compromis).\nLa solution retenue impose la fin du modular monolith. Nécessité de passer en micro-services : 2 services, 2 deployments et N services\nRéfléchissons à présent sur ce qui pourrait mal se passer avec un tuyau asynchrone : messages en double ou triple, manque de ressources, messages perdus …\nLe fournisseur de Cloud garantie une partie des problèmes évoqués.\nCyrille rappelle la nécessité d’un consumer à être idempotent pour gérer les messages en double.\nAvant de faire un choix sur l’implémentation de l’adaptateur et assurer la persistance des données (ex : PostgreSQL vs Redis), Eric propose de rester en mémoire pour tester rapidement en prod. Cela permet de gagner du temps et de vérifier les hypothèses.\nOn va livrer en prod un mock. Pas de honte. Vrai essaie sur de vraies machines avec les vraies données. On utilise la prod, le vrai environnement.\nLe calcul dure moins de 2 minutes : l’hypothèse est validée. L’adaptateur peut désormais être implémenté avec Redis.\nMessage de fond : l’architecture est évolutive. Il ne faut pas la mettre en place dès le début. L’architecture est dynamique. Tout bouge.\nL’application est composée de 2 systèmes qui doivent se parler. Un contrat JSON est définit entre dispatcher et aggregator. Le contrat est très explicite avec les unités.\nCyrille fait remarquer un problème de typo sur un champ : latency y_ms avec 2 lettres y\nUn renommage serait possible mais il est recommandé de positionner 2 champs pour respecter le contrat.\nAutre clé : on ne change pas un contrat. A partir du moment où il est publié, on doit rester dans la même version majeure pour toujours. Contracts are forever. On ne doit pas casser les clients existants.\nCyrille rappelle les utiles à l’heure de l’IA :\n- Architectural Decision Records (ADR) : template\n- ArchUnit : try architecture tests\nAutres clés proposées par Cyrille pour avoir des réunions constructives.\nCommencer par time boxer les réunions. Utiliser un tableau blanc ou numérique.\nAlterner raisonnement individuel et raisonnement en équipe :\n1. Chacun s’isole pour réfléchir de son côté au même problème\n2. Chacun vient ensuite expliquer son architecture. On essaie de dépersonnaliser sa solution. Cet exercice permet d’apprendre de ses collègues et de connaitre leurs points d’attention.\nRemember :\nThe system = the software + the people Baby steps : on apprend progressivement, par petits pas, rapidement =\u0026gt; réduit le risque dans un monde avec beaucoup d’incertitudes Rester simple Books : toutes ces attitudes nécessaires à l’Architecture restent inchangées depuis 30 ans : couplage et cohésion, contrats, modularités, API … Cet apprentissage est pérenne et en vaut donc la peine. Les livres recommandés par Cyrille resteront intemporels. ","link":"https://javaetmoi.com/2025/04/cles-de-l-architecture-pour-les-devs/","section":"posts","tags":["architecture","devoxx"],"title":"Les clés de l'architecture pour les dévs"},{"body":"Conférence : Devoxx France 2025\nDate : 17 avril 2025\nSpeaker : Sébastien Deleuze (Broadcom)\nFormat : Conférence (45 mn) / Replay Youtube\nSébastien est Core Commiter sur Spring Framework. Il intervient également sur des sujets transverses au portfolio Spring : support de Kotlin, null-safety (avec JSpecify) et les sujets d’optimisation. Dans ce talk, il a pour ambition de nous montrer comment améliorer l’efficacité de 80% des applications Spring, que ce soit de nouvelles applications ou des applications Legacy.\nLes raisons d’améliorer l’efficacité de nos applications sont multiples :\nBaisser le cout de run des applications Développement durable pour diminuer la consommation d’énergie, de mémoire et de CPU Optimiser les applications pour les containers (sur le Cloud ou OnPremise) Pour arriver à nos fins, Sébastien nous propose 3 technologies :\nCDS : techno relativement vieille mais qui s’est améliorée au fil des versions de Java AOT cache : Java 24 permet d’utiliser l’AOT cache qui est une version améliorée CDS. Sébastien prédit l’exploision de AOT Cache avec la LTS Java 25 AOT cache with profiling : technologie expérimentale et prometeuse Ces 3 technologies nécessitent un training run. Cette « exécution d’entrainement de l’application » consiste à lancer l’application pour charger les classes et créer un cache utilisé par la suite pour les déploiements en production. Le gain est triple :\nTemps de démarrage réduit Empreinte mémoire réduite Warmup de la JVM plus rapide 1. Class Data Sharing (CDS) Disponible depuis Java 9 (2017), CDS a continué à évoluer au fil des versions de Java.\nPar facilité (notamment pour la fonctionnalité d’extraction), un prérequis conseillé par Sébastien consiste à utiliser Java 17 et Spring Boot 3.3 et +.\nPour utiliser la fonctionnalité CDS, une archive CDS ( format .jsa) doit être créée pour le classpath de l\u0026rsquo;application. Spring Framework fournit un mécanisme facilitant la création de cette archive. Une fois l\u0026rsquo;archive disponible, on peut l\u0026rsquo;utiliser via un flag de la JVM.\nLa création de l’archive CDS nécessite 2 paramètres de JVM :\n-Dspring.context.exit=onRefresh : démarrage les beans singletons Spring non lazy puis arrête l’application. -XX:ArchiveClassesAtExit=spring-petclinic.jsa : création de l’archive CDS lorsque la JVM s’arrête. Les plugins Maven et Gradle de Spring Boot permettent de créer un JAR auto-exécutable. Disposer d’une seul JAR est bien pratique pour le déploiement et le téléchargement d’une application depuis le repository Maven d’entreprise, mais pas efficiente avec CDS qui ne supporte pas les JAR imbriqués. La version 3.3 de Spring Boot a facilité le support de CDS en ajoutant une fonctionnalité d’auto-extraction du JAR via le paramètre -Djarmode=tools. Son utilisation est illustrée par la commande suivante :\njava -Djarmode=tools -jar spring-petclinic.jar extract Sébastien fait une démonstration à l’aide de l’application Spring Petclinic.\nCommande permettant d’utiliser l’archive CDS au démarrage de l’application :\njava -XX :SharedArchiveFile=spring-petclinic.jsa – jar spring-petclinic.jar Le gain au démarrage est de 19%. A noter que l’utilisation de CDS peut engendrer des effets de bord si l’on ne suit pas les bonnes pratiques suivantes :\nUtiliser idéalement la même JVM pour la capture du CDS et le run de l’application Spécifier le classpath avec la liste complète des JAR et ne pas utiliser de wildcard * Le timestamp des JAR doit être préservé Les éventuels JAR additionnels doivent être ajoutés à la fin du classpath. L’étape de démarrage nécessaire à l’enregistrement de l’archive CDS est appelée le training run. Cette étape peut être intégrée dans le pipeline CI/CD de build de l’application Spring. Spring utilise une base de données H2 en mémoire. Une application d’entreprise se connectera à un PosgreSQL ou un MongoDB. Le paramétrage Spring diffère en fonction des dépendances externes de l’application. Sébastien nous recommande de consulter le repository spring-lifecycle-smoke-tests donnant différents exemples de configuration pour Spring Data, Spring Batch, Spring Coud, Kafka, Spring Security …\nPar exemple, pour éviter qu’une application Spring Data JPA ne fasse appel à la base de données, il est possible de désactiver la lecture des méta-données par Hibernate. Ne pouvant plus déterminer automatiquement le dialect, il est alors nécessaire de lui spécifier.\n# Specify explicitly the dialect (here for PostgreSQL, adapt for your database) spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect # Disable Hibernate usage of JDBC metadata spring.jpa.properties.hibernate.boot.allow_jdbc_metadata_access=false # Database initialization should typically be performed outside of Spring lifecycle spring.jpa.hibernate.ddl-auto=none spring.sql.init.mode=never Le support de Buildpack par les plugins Maven et Gradle de Spring Boo transforme une application Spring Boot en une image OCI (Docker). Commandes :\nmvn spring-boot:build-image gradle bootBuildImlage Le support de CDS est prévu dans Buildpacks via l’activation du flag BP_JVM_CDS_ENABLED. Exemple de configuration Maven :\n\u0026lt;plugin\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-maven-plugin\u0026lt;/artifactId\u0026gt; \u0026lt;configuration\u0026gt; \u0026lt;image\u0026gt; \u0026lt;env\u0026gt; \u0026lt;BP_JVM_CDS_ENABLED\u0026gt;true\u0026lt;/BP_JVM_CDS_ENABLED\u0026gt; \u0026lt;/env\u0026gt; \u0026lt;/image\u0026gt; \u0026lt;/configuration\u0026gt; \u0026lt;/plugin\u0026gt; Buildpack effectue le training run et ajoute l’archive jsa dans le container.\nPour un effort mesuré, le temps de démarrage de Spring Petclinic est réduit de 3 secondes à 1,8 secondes.\n2. AOT Cache Successeur de CDS, AOT Cache (Ahead-Of-Time Cache) est une fonctionnalité de la JVM intégrée à Java 24 qui permet d’ améliorer l’efficience de nos applications Java. Permettant de diminuer les temps de démarrage, Spring AOT est une fonctionnalité Spring obligatoire pour les images natives mais optionnelle sur la JVM. Sébastien perçoit une synergie entre AOT Cache et Spring AOT.\nL’utilisation Spring AOT impose certaines contraintes comme la pré-configuration des profiles Spring. Le repo Github sdeuleuze/demo-profile-aot montre comment activer les profils Spring dans un build Maven et Gradle.\nPensée dans le cadre du projet Leyden, La JEP 483 Ahead-of-Time Class Loading \u0026amp; Linking est disponible dans Java 24 et des améliorations prévues dans les versions suivantes de Java. L’utiliser est un bon investissement.\nLa création du cache AOT nécessite 2 étapes :\nLa 1ière étape consiste à générer le fichier .aotconf à l’aide de l’option -XX:AOTMode=record et la ligne de commande suivante :\njava -XX:AOTMode=record -XX:AOTConfiguration=spring-petclinic.aotconf \\ -jar spring-petclinic.jar Au préalable, comme avec CDS, le JAR auto-exécutable aura été extrait à l’aide de -Djarmode=tools.\nLa 2ième étape consiste à générer un fichier .aot avec l’option `-XX:AOTMode=create`` et la ligne de commande suivante :\njava -XX:AOTMode=create -XX:AOTConfiguration=spring-petclinic.aotconf -XX:AOTCache=spring-petclinic.aot -jar spring-petclinic.jar Le temps de démarrage de Spring Petclinic descend à 1,3 secondes :\n3. AOT Cache with code compilation and Spring AOT Sébastien rappelle que ce n’est que le début de l’histoire et que ces temps de démarrage à l’aide d’AOT Cache ne pourront que s’améliorer dans le futur. En effet, le projet Leyden prévoit de nouvelles améliorations, donc les 3 JEPs en draft :\nAhead-of-Time Method Profiling: amelioration du temps de chauffe Ahead-of-time Command Line Ergonomics : une seule étape au lieu de 2 étapes pour créer les fichiers .aotconf et .aot Ahead-of-time Code Compilation : récupération du code natif du JIT pour le réutiliser en prod Sébastien nous fait une démo live à partir d’une version du JDK compilée en local avec les dernières fonctionnalités du projet Leyden. Cette fois-ci, le workflow va être un peu différent : on ne fait pas de stop après le démarrage. On laisse tourner l’application. Sébastien utilise l’outil oha pour chauffer la JVM (faire le warmup). Il mesure des améliorations très significatives alors même que techno en work-in-progress. Jugez par vous-même :\nCourbe bleue : JVM classique qui prend son temps pour le warmup Courbe rouge : AOT profiling. Warmup assez rapide. Courbe jaune : warmup passé de 30 à 7 secondes. Fonctionne sur des applications Legacy Récapitulatif Le tableau récapitulatif ci-dessous compare 3 technologies d’optimisation d’une application Java :\nGraalVM: la version gratuite de GraalVM vient avec le Garbage Collector serial adapté pour les applications ayant une faible empreinte mémoire et une petite taille du de Heap. Le GC G1 n’est disponible que dans la version commerciale Oracle de GraalVM. Avantage : conso mémoire réduite au max. Pas fait pour des applications qu’on déploie / construit plusieurs fois par jour. Dépend de la taille de l’appli : ok pour un microservice mais pas un monolith. Spring fait le travail pour préconfigurer GraalVM. Mais quid des autres librairies qui ne supporte pas GraalVM et pour lesquels les développeurs doivent ajouter des méta-données. Sébastien considère que GraalVM est une niche pour 5 à 10% des applications Spring. Cela dit, des travaux sont en cours pour améliorer le support dans Spring. JVM with Project CRaC: Sébastien est assez sévère sur cette technologie qui comporte 2 énorme défauts : Linux uniquement mais surtout à cause du cycle de vie nécessitant de restaurer l’application (handles filesystems, sockets réseaux …). Quid du support des autres librairies ? L’API de ces librairies peut bloquer. Plus encore : l’image snapshot de la JVM contient les credentials. Aussi, Sébastien de recommande pas CRaC pour la production. La techno a des limites malgré l’effort de l’équipe Spring. JVM with AOT cache : utilisable sur un grand nombre de projets legacy. Temps de démarrages est réduit de 2 à 4 fois. Les effets de bord sont mesurés. La CI doit être adaptée pour lancer le warmup. Le projet Leyden et la JVM continue à évoluer. Preuve en est la Pull Request #44 datant de février 2025 du projet Leyden : 8350488: [leyden] Experimental AOT-only mode\nD’autres améliorations concernant les applications Legacy seront annoncées à la conférence Spring IO qui aura lieu du 21 au 23 mai 2025 à Barcelone.\nEnfin, pour ses benchmarks, Sébastien passe une annonce : il recherche de plus grosses applications Open Source basées sur Spring Boot et plus réalistes que l\u0026rsquo;application démo Petclinic.\nSi vous voulez creuser le sujet, je vous recommande la lecture de l’article intitulé Spring Boot CDS support and Project Leyden anticipation qu’a publié Sébastien le 29 aout 2024.\n","link":"https://javaetmoi.com/2025/04/optimisez-vos-applications-spring-boot-avec-cds-et-le-projet-leyden/","section":"posts","tags":["devoxx","spring-boot","spring-framework"],"title":"Optimisez vos applications Spring Boot avec CDS et le projet Leyden"},{"body":"","link":"https://javaetmoi.com/tags/spring-framework/","section":"tags","tags":null,"title":"Spring-Framework"},{"body":"","link":"https://javaetmoi.com/tags/quarkus/","section":"tags","tags":null,"title":"Quarkus"},{"body":"Spring et Quarkus dans le même repository Git, ou presque. Cela vous intrigue ?\nFigurez-vous qu’il y’a quelques mois, la lecture du très bon livre Understanding Quarkus 2.x d’Antonio Gongalves m’a donné envie de pratiquer ce framework alternatif à Spring Boot. Et pour apprendre une nouvelle technologie, quoi de plus stimulant que de se fixer un objectif. Je me suis donc donné comme challenge de migrer vers Quarkus l’application démo Spring Boot que je connais bien. Une fois migrée, l’application devait rester iso-fonctionnelle.\nA travers leur repo quarkus-petclinic, RedHat avait fait l’exercice avant moi. Malheureusement, l’historique Git a été écrasé, ne laissant aucune trace du chemin de migration parcouru. Pendant 3 mois, j\u0026rsquo;ai donc travaillé sur un nouveau fork que je suis fier de vous présenter : quarkus-spring-petclinic. Ajouté à la communauté Spring Petclinic, ce fork a un double objectif :\nMontrer comment migrer une application Spring Boot 3.4 vers Quarkus 3.21, avec le minium d\u0026rsquo;effort et en modifiant le moins de code possible Utiliser les extensions Spring proposées par Quarkus pour garder un lien avec le monde Spring tout en soulignant l\u0026rsquo;effort de l\u0026rsquo;équipe Quarkus pour supporter Spring, un framework incontournable de l\u0026rsquo;écosystème Java Les extensions Spring pour Quarkus utilisées sont au nombre de quatre : Spring DI, Spring Web, Spring Data JPA et Spring Cache.\nLe changement majeur aura été de porter le templating des pages HTML de Thymeleaf vers Qute.\nDébutant en Quarkus, le code proposé ne respecte peut-être pas toutes les règles de l’art prônées par l’équipe de dév Quarkus. Je m’en excuse par avance. Si vous voulez contribuer et corriger le tir : issue et Pull Request sont les bienvenues.\nLe différenciel complet entre la version Spring Boot et la version Quarkus de Petclinic peut-être visualisé sur Github.\nConfiguration du build Maven et Gradle Spring Petclinic supporte les 2 principales plateformes de build de l’ecosystème Java, à savoir Maven et Gradle. Pour chaque dépendance Spring Boot, le tableau ci-dessous dresse l’équivalent utilisé sur la version Quarkus :\nDépendances Spring Boot Dépendances Quarkus correspondantes Commentaire spring-boot-starter-actuator quarkus-smallrye-health SmallRye Health est une implementation de la MicroProfile Health. spring-boot-starter-cache cache-api\ncaffeine\nquarkus-spring-cache Extension Spring Cache pour Quarkus\nQuarkus utilise par défaut Caffeine. spring-boot-starter-data-jpa quarkus-spring-data-jpa\nquarkus-narayana-jta Extension Spring Data JPA pour Quarkus\nQuarkus s’appuie sur Hibernate ORM et Panache. Le gestionnaire de transactions JTA est à ajouter manuellement. spring-boot-starter-web quarkus-spring-web\nquarkus-rest-jackson L’extension Spring Web pour Quarkus requère quarkus-rest-jackson ou quarkus-resteasy-jackson. spring-boot-starter-validation quarkus-hibernate-validator Les versions Spring Boot et Quarkus de Petclinic s’appuient toutes 2 sur Hibernate Validator. spring-boot-starter-thymeleaf quarkus-qute Pas de correspondance directe car Quarkus utilise Qute pour le templating. spring-boot-starter-test quarkus-junit5\nquarkus-junit5-mockito\nquarkus-test-h2\nrest-assured Rest Assured remplace MockMvc pour tester les contrôleurs REST. h2 quarkus-jdbc-h2 mysql-connector-j quarkus-jdbc-mysql En plus des drivers JDBC, tire le pool de connexions Agroal qui remplace HikariCP. postgresql quarkus-jdbc-postgresql webjars-locator-lite quarkus-web-dependency-locator Utiles pour les webjars. spring-boot-devtools Pas de correspondance directe. Quarkus inclue le mode dev par défaut. spring-boot-docker-compose Utilisé par les tests d’intégration reposant sur Testcontainers.\nPas d’équivalent côté Quarkus qui sait nativement démarrer des conteneurs Docker lorsqu’aucune configuration n’est précisée. (spring-core et spring-beans) quarkus-spring-di Support des annotations Spring d’injection de dépendance, mais en tirant ArC, une implémentation light de CDI spécifique à Quarkus. quarkus-container-image-docker Création d’images Docker multi-plateformes. Les dépendances vers les 2 webjars bootstrap et font-awesome sont restés inchangées.\nLa migration a été faite avec une approche top-down : on part de la couche persistance pour remonter vers la couche de présentation.\nAdaptation de la couche Spring Data JPA L’ extension Spring Data JPA pour Quarkus présente l’avantage de pouvoir conserver les conventions de nommage des interfaces des repository Spring Data JPA. Sous le capot, l’implémentation est générée à l’aide de Panache. Les repository migrés peuvent continuer à implémenter les interfaces JpaRepository et ListCrudRepository, à utiliser les interfaces Spring Data Page et Pageable pour la pagination.\nCe portage a permis de conserver 90% du code existant de la couche de persistance de Spring Petclinic. Je l’ai personnellement trouvé plus strict que l’original. Preuve en est ce premier exemple possible avec Spring Data JPA, mais qui ne fonctionne pas sous Quakus : déclarer sur l’interface OwnerRepository la méthode findPetTypes manipulant des entités JPA de type PetType et non de type Owner.\nL’erreur suivante était générée pendant le build :\nQuery annotations may only use interfaces to map results to non-entity types. Offending query string is \u0026ldquo;SELECT ptype FROM PetType ptype ORDER BY ptype.name\u0026rdquo; on method findPetTypes of Repository org.springframework.samples.petclinic.owner.OwnerRepository\nLes messages d’erreur ne sont pas explicites. Aussi, pour debugger et trouver la cause, j’ai eu besoin d’ajouter temporairement la dépendance suivante :\n\u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;io.quarkus\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;quarkus-spring-data-jpa-deployment\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; Le moyen de contournement a consisté tout simplement à découper en deux l’interface OwnerRepository. L’interface PetTypeRepository a été ajoutée et a pour responsabilité l\u0026rsquo;accès aux PetType. On a ainsi un meilleur découplage.\nSecond cas dysfonctionnant sous Quarkus :\npublic interface VetRepository extends Repository\u0026lt;Vet, Integer\u0026gt; { Collection\u0026lt;Vet\u0026gt; findAll(); } Quarkus génère l’exception suivante :\nCaused by: io.quarkus.spring.data.deployment.UnableToParseMethodException: Method \u0026#39;findAll\u0026#39; of repository \u0026#39;org.springframework.samples.petclinic.vet.VetRepository\u0026#39; cannot be parsed as there is no proper \u0026#39;By\u0026#39; clause in the name. La classe MethodNameParser ne supporte pas le type de retour Collection. Triviale, la correction a consisté à le changer en List.\nDernier changement mineur apporté à la couche de persistance : l’exception non checkée DataAccessException n’est pas supportée par Quarkus. Elle a donc été retirée de l’interface des méthodes des Repository.\nUne fois migrée, l’interface OwnerRepository n’a aucune adhérence à Quarkus ou Panache. Elle conserve ses imports sur les classes de Spring Data Commons et Spring Data JPA :\npackage org.springframework.samples.petclinic.owner; import java.util.Optional; import jakarta.annotation.Nonnull; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; public interface OwnerRepository extends JpaRepository\u0026lt;Owner, Integer\u0026gt; { Page\u0026lt;Owner\u0026gt; findByLastNameStartingWith(String lastName, Pageable pageable); Optional\u0026lt;Owner\u0026gt; findById(@Nonnull Integer id); Page\u0026lt;Owner\u0026gt; findAll(Pageable pageable); Adaptation des scripts SQL Migrer les Repository Spring Data JPA, c’est bien. Les tester, c’est mieux. Les tests unitaires de Quarkus Spring Petclinic utilisent la base de données embarquées H2.\nL’exécution du script data.sql échouait avec l’erreur suivante :\nCaused by: org.h2.jdbc.JdbcSQLIntegrityConstraintViolationException: Intégrité référentielle violation de contrainte: \u0026#34;FK35UIBOYRPFN1BNDRR5JORCJ0M: PUBLIC.VET_SPECIALTIES FOREIGN KEY(SPECIALTY_ID) REFERENCES PUBLIC.SPECIALTIES(ID) (4)\u0026#34; Referential integrity constraint violation: \u0026#34;FK35UIBOYRPFN1BNDRR5JORCJ0M: PUBLIC.VET_SPECIALTIES FOREIGN KEY(SPECIALTY_ID) REFERENCES PUBLIC.SPECIALTIES(ID) (4)\u0026#34;; SQL statement: INSERT INTO vet_specialties VALUES (4, 2) [23506-230] Cette différence de comportement s’explique par le fait que Quarkus utilise Hibernate pour générer le script DDL de création du schéma et non pas directement le script DDL schema.sql. L’ordre des colonnes diffère entre le script DDL généré par Hibernate et le script SQL existant. Je n’ai pas trouvé la possibilité d’utiliser le script schema.sql. Je ne suis apparemment pas le seul. Si vous avez une idée, vous pouvez contribuer à l’ issue #8.\nEn attendant de trouver une solution, j’ai modifié le script SQL en précisant le nom des colonnes dans l’instruction INSERT, ce qui est une bonne pratique :\nPortage des tests AssertJ vers Hamcrest Pour les tests unitaires, Quarkus recommande l’utilisation de JUnit 5 déjà utilisé sur Spring Petclinic. Les assertions de JUnit sont limitées. Là où Spring Petclinic utilise la librairie AssertJ, Quarkus préconise l’utilisation d’ H amcrest. D’après l’ issue #38689 “Include AssertJ with Quarkus releases”, le support d’AssertJ dans Quarlus ne semble pas planifié.\nMigrer des assertions AssertJ vers les matchers Hamcrest peut être facilitée par la recette Open Rewrite MigrateHamcrestToAssertJ. L’inverse n’est pas vrai. C’est là où Github Copilot ou Codeium facilite la tâche. On migre un premier test, et l’IA vous assiste pour la suite.\nExemple avec la méthode shouldFindSingleOwnerWithPet() extrait de la classe ClinicServiceTests:\nAvant migration sous AssertJ :\n@Test void shouldFindSingleOwnerWithPet() { Optional\u0026lt;Owner\u0026gt; optionalOwner = this.owners.findById(1); assertThat(optionalOwner).isPresent(); Owner owner = optionalOwner.get(); assertThat(owner.getLastName()).startsWith(\u0026#34;Franklin\u0026#34;); assertThat(owner.getPets()).hasSize(1); assertThat(owner.getPets().get(0).getType()).isNotNull(); assertThat(owner.getPets().get(0).getType().getName()).isEqualTo(\u0026#34;cat\u0026#34;); } Après migration sous Hamcrest :\n@Test void shouldFindSingleOwnerWithPet() { Optional\u0026lt;Owner\u0026gt; optionalOwner = this.owners.findById(1); assertThat(optionalOwner.isPresent(), is(true)); Owner owner = optionalOwner.get(); assertThat(owner.getLastName(), startsWith(\u0026#34;Franklin\u0026#34;)); assertThat(owner.getPets(), hasSize(1)); assertThat(owner.getPets().get(0).getType(), notNullValue()); assertThat(owner.getPets().get(0).getType().getName(), is(equalTo(\u0026#34;cat\u0026#34;))); } Passer à l’annotation @TestTransaction Dans les classes de tests faisant appels à des Repository, l’annotation org.springframework.transaction.annotation.Transactional du module spring-tx a été remplacée par io.quarkus.test.TestTransaction du module quarkus-test-commons. Ces annotations permettent de rollbacker la transaction à la fin de l’exécution d’une méthode de test, laissant ainsi la base de données inchangée pour le prochain test.\nExemple avec la méthode shouldInsertOwner() extrait de la classe ClinicServiceTests:\n@Test @TestTransaction void shouldInsertOwner() { Page\u0026lt;Owner\u0026gt; owners = this.owners.findByLastNameStartingWith(\u0026#34;Schultz\u0026#34;, pageable); int found = (int) owners.getTotalElements(); Owner owner = new Owner(); owner.setFirstName(\u0026#34;Sam\u0026#34;); owner.setLastName(\u0026#34;Schultz\u0026#34;); owner.setAddress(\u0026#34;4, Evans Street\u0026#34;); owner.setCity(\u0026#34;Wollongong\u0026#34;); owner.setTelephone(\u0026#34;4444444444\u0026#34;); this.owners.save(owner); assertThat(owner.getId(), is(not(0))); owners = this.owners.findByLastNameStartingWith(\u0026#34;Schultz\u0026#34;, pageable); assertThat(owners.getTotalElements(), is(equalTo(found + 1L))); } De DataJpaTest à QuarkusTest Pour tester les Repository JPA, Spring Boot met à disposition l’annotation @DataJpaTest automatisant la configuration des classes de test. Elle s’occupe notamment de démarrer en mémoire une base de données embarquée H2, de créer son schéma et de charger un jeu de données de test.\nPour arriver à un résultat similaire avec Quarkus, l’annotation `@DataJpaTest a été remplacée par 2 annotations :\n@QuarkusTest @QuarkusTestResource(H2DatabaseTestResource.class) class ClinicServiceTests { L’annotation @QuarkusTestResource permet de référencer la classe H2DatabaseTestResource* (fournie par l’artefact io.quarkus:quarkus-test-h2) chargée de démarrer / arrêter un serveur H2.\nPar défaut, l’application Spring Petclinic démarre une base de données H2, la même que celle utilisée pour les tests. La propriété quarkus.hibernate-orm.sql-load-script du fichier application.properties a été positionnée sur h2 :\nquarkus.datasource.db-kind=h2 quarkus.hibernate-orm.log.sql=true quarkus.hibernate-orm.sql-load-script=db/${quarkus.datasource.db-kind}/data.sql La propriété quarkus.hibernate-orm.sql-load-script a quant à elle permis de réutiliser le script DML existant data.sql insérant quelques données de test.\nA ce stade de la migration vers Quarkus, les tests unitaires de la couche de persistance et de la couche service sont passants.\nInternationalisation Le support de l’internationalisation ( i18n pour les intimes) est incomplet dans Spring Petclinic (cf. issue #1854). Le ressource bundle messages contient différents fichiers properties de traduction. Les clés sont utilisées dans certains templates Thymeleaf (ex : welcome) et pour les messages d’erreur (ex : required, typeMismatch.birthDate). Ce ressource bundle a pu être réutilisé dans la version Quarkus.\nQute propose un mécanisme typesafe de ressource bundle basé sur l’annotation @ResourceBundle. La classe AppMessages a été ajoutée à Petclinic. En voici un extrait contenant 3 clés :\nimport io.quarkus.qute.i18n.Message; import io.quarkus.qute.i18n.MessageBundle; @MessageBundle(value = \u0026#34;messages\u0026#34;, locale = \u0026#34;en\u0026#34;) public interface AppMessages { @Message String welcome(); @Message String required(); @Message String typeMismatch_birthDate(); Le nom des clés des properties ne semble pas accepter le caractère point (ex : @Message(value = \u0026quot;typeMismatch.birthDate\u0026quot;). Certaines clés ont donc dû être renommées (ex : typeMismatch.birthDate vers typeMismatch\\_birthDate).\nAu runtime, l’usage du ressource bundle Quarkus peut-être utilisé dans un template Qute via le namespace du message bundle. Exemple :\n{#for err in errors} {#if err == \u0026#39;notFound\u0026#39;} \u0026lt;p\u0026gt;{messages:notFound}\u0026lt;/p\u0026gt; {#else} \u0026lt;p\u0026gt;{err}\u0026lt;/p\u0026gt; {/if} {/for} Ce même ressource bundle peut également être exploité depuis un contrôleur REST. La création de classe I18nHelper permet d’exploiter dynamiquement l’en-tête HTTP Accept-Language :\n@GetMapping(\u0026#34;/\u0026#34;) public TemplateInstance processFindForm(@RequestParam(defaultValue = \u0026#34;1\u0026#34;) int page, @RequestParam String lastName, @HeaderParam(\u0026#34;Accept-Language\u0026#34;) String language) { Page\u0026lt;Owner\u0026gt; ownersResults = findPaginatedForOwnersLastName(page, lastName); if (ownersResults.isEmpty()) { // no owners found String notFound = I18nHelper.lookupAppMessages(language).notFound(); return OwnerTemplates.findOwners(List.of(notFound)); } Possible que Quarkus propose nativement un mécanisme similaire. Quarkus Renarde utilise quant à lui le header Accept-Language et un cookie.\nEn passant, l’exemple précédent montre l’usage des annotations Spring @GetMapping et @RequestParam. L’annotation Spring @RequestHeader n’est pas supportée par Quarkus et a dû être substituée par l’annotation @HeaderParam de JAX-RS.\nLe debuggage de la méthode MessageBundleProcessor:: parseKeyToTemplateFromLocalizedFile aura nécessité d’ajouter temporairement au classpath la dépendance io.quarkus:quarkus-qute-deployment.\nRessources statiques Afin de se conforter aux conventions de Quarkus, les ressources statiques (fonts, css et images) ont été déplacées du répertoire static/resourcesvers le répertoire META-INF/resources.\nMigration templates Thymeleaf vers Qute Les templates Thymeleaf de Spring Petclinic utilisent le mécanisme de fragments Thymeleaf à la fois pour le gabarit des pages (layout.html) et pour les tags HTML réutilisables (inputField.html et selectField.html).\nUne première étape a donc consisté à migrer ces fragments Thymeleaf vers une équivalence Qute. La syntaxe de ces deux moteurs de templating Java diffère beaucoup. À l’aide du guide de référence de Qute, le gabarit des pages layout.html a été migré sans difficulté majeure. La gestion dynamique du menu est désormais gérée en JavaScript. Ce gabarit est référencé dans les autres templates Qute via la section {#include fragments/layout}.\nTemplate Thymeleaf de la page welcome originale :\n\u0026lt;!DOCTYPE html\u0026gt; \u0026lt;html xmlns:th=\u0026#34;https://www.thymeleaf.org\u0026#34; th:replace=\u0026#34;~{fragments/layout :: layout (~{::body},\u0026#39;home\u0026#39;)}\u0026#34;\u0026gt; \u0026lt;body\u0026gt; \u0026lt;h2 th:text=\u0026#34;#{welcome}\u0026#34;\u0026gt;Welcome\u0026lt;/h2\u0026gt; \u0026lt;div class=\u0026#34;row\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;col-md-12\u0026#34;\u0026gt; \u0026lt;img class=\u0026#34;img-responsive\u0026#34; src=\u0026#34;../static/resources/images/pets.png\u0026#34; th:src=\u0026#34;@{/resources/images/pets.png}\u0026#34;/\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; Template Qute équivalent de la page welcome :\n{#include fragments/layout} \u0026lt;body\u0026gt; \u0026lt;h2\u0026gt;{messages:welcome}\u0026lt;/h2\u0026gt; \u0026lt;div class=\u0026#34;row\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;col-md-12\u0026#34;\u0026gt; \u0026lt;img class=\u0026#34;img-responsive\u0026#34; src=\u0026#34;/images/pets.png\u0026#34; /\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/body\u0026gt; {/include} Afin d’être enregistrés automatiquement par Quarkus, les user-defined tags input et select ont été déplacés dans le répertoire src/main/resources/templates/tags. Les deux exemples de tags suivants permettent de comparer les syntaxes Thymeleaf et Qute.\nExemple du tag Thymeleaf inputField.html :\n\u0026lt;html\u0026gt; \u0026lt;body\u0026gt; \u0026lt;form\u0026gt; \u0026lt;th:block th:fragment=\u0026#34;input (label, name, type)\u0026#34;\u0026gt; \u0026lt;div th:with=\u0026#34;valid=${!#fields.hasErrors(name)}\u0026#34; th:class=\u0026#34;${\u0026#39;form-group\u0026#39; + (valid ? \u0026#39;\u0026#39; : \u0026#39; has-error\u0026#39;)}\u0026#34; class=\u0026#34;form-group\u0026#34;\u0026gt; \u0026lt;label class=\u0026#34;col-sm-2 control-label\u0026#34; th:text=\u0026#34;${label}\u0026#34;\u0026gt;Label\u0026lt;/label\u0026gt; \u0026lt;div class=\u0026#34;col-sm-10\u0026#34;\u0026gt; \u0026lt;div th:switch=\u0026#34;${type}\u0026#34;\u0026gt; \u0026lt;input th:case=\u0026#34;\u0026#39;text\u0026#39;\u0026#34; class=\u0026#34;form-control\u0026#34; type=\u0026#34;text\u0026#34; th:field=\u0026#34;*{__${name}__}\u0026#34; /\u0026gt; \u0026lt;input th:case=\u0026#34;\u0026#39;date\u0026#39;\u0026#34; class=\u0026#34;form-control\u0026#34; type=\u0026#34;date\u0026#34; th:field=\u0026#34;*{__${name}__}\u0026#34;/\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;span th:if=\u0026#34;${valid}\u0026#34; class=\u0026#34;fa fa-ok form-control-feedback\u0026#34; aria-hidden=\u0026#34;true\u0026#34;\u0026gt;\u0026lt;/span\u0026gt; \u0026lt;th:block th:if=\u0026#34;${!valid}\u0026#34;\u0026gt; \u0026lt;span class=\u0026#34;fa fa-remove form-control-feedback\u0026#34; aria-hidden=\u0026#34;true\u0026#34;\u0026gt;\u0026lt;/span\u0026gt; \u0026lt;span class=\u0026#34;help-inline\u0026#34; th:errors=\u0026#34;*{__${name}__}\u0026#34;\u0026gt;Error\u0026lt;/span\u0026gt; \u0026lt;/th:block\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/th:block\u0026gt; \u0026lt;/form\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; Exemple équivalent du tag Qute inputField.html :\n{#let invalid=result.hasErrors(name)} \u0026lt;div class=\u0026#34;form-group {#if invalid} has-error {/if}\u0026#34;\u0026gt; \u0026lt;label for=\u0026#34;{name}\u0026#34; class=\u0026#34;col-sm-2 control-label\u0026#34;\u0026gt;{it} \u0026lt;/label\u0026gt; \u0026lt;div class=\u0026#34;col-sm-10\u0026#34;\u0026gt; \u0026lt;input class=\u0026#34;form-control\u0026#34; id=\u0026#34;{name}\u0026#34; name=\u0026#34;{name}\u0026#34; type=\u0026#34;{type}\u0026#34; value=\u0026#34;{field}\u0026#34; /\u0026gt; \u0026lt;span class=\u0026#34;fa {#if invalid}fa-remove{#else}fa-ok{/if} form-control-feedback\u0026#34; aria-hidden=\u0026#34;true\u0026#34;\u0026gt;\u0026lt;/span\u0026gt; {#if invalid} \u0026lt;span class=\u0026#34;help-inline\u0026#34;\u0026gt;{result.getErrorMessage(name)}\u0026lt;/span\u0026gt; {/if} \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; {/let} Binding du modèle Une fois les templates Thymeleaf converties en Qute, des ajustements ont été nécessaire du côté des contrôleurs web, notamment au niveau du binding des champs du formulaire. Le binding est le processus par lequel les données envoyées par l’utilisateur, généralement via un formulaire, sont automatiquement associées à un objet du modèle. Spring Web MVC gère le binding en utilisant des DataBinder qui convertissent automatiquement les paramètres de requête HTTP en propriétés d’un objet Java, en s’appuyant sur les noms des champs du formulaire et les conventions de nommage. Dans l’exemple suivant, la méthode processCreationForm accepte en paramètre un objet de type Owner bindé avec les champs du formulaire createOrUpdateOwnerForm.html :\n@PostMapping(\u0026#34;/owners/new\u0026#34;) public String processCreationForm(@Valid Owner owner, BindingResult result, RedirectAttributes redirectAttributes) { Positionnée sur le paramètre owner, l’annotation @Valid permet d’exécuter la validation Bean Validation / Hibernate Validator. Je n’ai pas trouvé dans Quarkus l’équivalent des classes BindingResult et RedirectAttibutes. Ainsi, la signature de cette méthode s’allège en Quarkus :\n@PostMapping(\u0026#34;/owners/new\u0026#34;) public TemplateInstance processCreationForm(Owner owner) { On retrouve l’annotation Spring @PostMapping supportée par l’extension Quarkus. Le type de retour n’est plus une String correspondant à la vue MVC à afficher, mais une TemplateInstance.\nPour binder la classe Owner, un changement a dû être opéré au niveau de la classe Owner et de ses classes parentes Person et NamedEntity : ajouter l’annotation JAX-RS @FormParam sur les attributs bindés comme address. Extrait de la classe Owner :\npublic class Owner extends Person { @Column(name = \u0026#34;address\u0026#34;) @NotBlank @FormParam(\u0026#34;address\u0026#34;) private String address; Sans ce changement, voici le message d’erreur obtenu lors de la création d’un nouveau propriétaire d’animal de compagnie :\n2025-04-12 17:36:04,095 ERROR [org.spr.sam.pet.sys.ExceptionMappers] (executor-thread-1) Internal server error: jakarta.ws.rs.NotSupportedException: HTTP 415 Unsupported Media Type at org.jboss.resteasy.reactive.server.handlers.RequestDeserializeHandler.handle(RequestDeserializeHandler.java:75) Ce ciblage explicite des champs bindés depuis un formulaire HTML pourrait être justifié par des mesures de sécurité.\nPour terminer sur le binding du modèle, l’interface org.springframework.ui.Model est conservée dans quarkus-spring-context-api mais ne semble pas être exploitée par Quarkus.\nValidation des données Dans le paragraphe précédent, nous avons vu comment récupérer de manière typée les données saisies par l’utilisateur dans l’interface web de Petclinic. Nous allons voir à présent comment il est possible de valider les données avant de les insérer en base de données.\nLe guide Validation with Hibernate Validator explique comment mettre en place Bean Validation sur une API REST. L’annotation @jakarta.validation.Valid est supportée par Quakus. Pour autant, son usage n’a pas pu être conservé dans Petclinic. En effet, si on la laisse, Quarkus valide les données du Owner et, en cas d’erreur, ne rentre pas dans la méthode processCreationForm. Il renvoie directement un flux texte contenant le rapport d’erreur complet. Exemple de la soumission d’un formulaire vide :\nViolationReport{title=\u0026#39;Constraint Violation\u0026#39;, status=400, violations=[Violation{field=\u0026#39;processCreationForm.owner.address\u0026#39;, message=\u0026#39;ne doit pas être vide\u0026#39;}, Violation{field=\u0026#39;processCreationForm.owner.telephone\u0026#39;, message=\u0026#39;ne doit pas être vide\u0026#39;}, Violation{field=\u0026#39;processCreationForm.owner.telephone\u0026#39;, message=\u0026#39;Telephone must be a 10-digit number\u0026#39;}, Violation{field=\u0026#39;processCreationForm.owner.city\u0026#39;, message=\u0026#39;ne doit pas être vide\u0026#39;}, Violation{field=\u0026#39;processCreationForm.owner.lastName\u0026#39;, message=\u0026#39;ne doit pas être vide\u0026#39;}, Violation{field=\u0026#39;processCreationForm.owner.firstName\u0026#39;, message=\u0026#39;ne doit pas être vide\u0026#39;}]} Dans Petclinic, on souhaite renvoyer le formulaire HTML en erreur avec le message d’erreur à côté de chaque champ erroné.\nDans la documentation Quakus Qute, je n’ai pas trouvé l’équivalent de ce que propose Spring Web MVC, grâce notamment à la classe BindingResult. Pour contourner cette limitation, j’ai introduit le record Result. L’appel au Validator Bean Validation est fait de manière impérative. Son résultat (un ensemble de ConstraintViolation) permet de construire une instance de Result.\nExemple en Spring :\n@PostMapping(\u0026#34;/owners/new\u0026#34;) public String processCreationForm(@Valid Owner owner, BindingResult result, RedirectAttributes redirectAttributes) { if (result.hasErrors()) { redirectAttributes.addFlashAttribute(\u0026#34;error\u0026#34;, \u0026#34;There was an error in creating the owner.\u0026#34;); return VIEWS_OWNER_CREATE_OR_UPDATE_FORM; } this.owners.save(owner); redirectAttributes.addFlashAttribute(\u0026#34;message\u0026#34;, \u0026#34;New Owner Created\u0026#34;); return \u0026#34;redirect:/owners/\u0026#34; + owner.getId(); } Exemple équivalent en Quarkus :\n@PostMapping(\u0026#34;/new\u0026#34;) public TemplateInstance processCreationForm(Owner owner) { Result result = Result.from(validator.validate(owner)); if (result.hasErrors()) { return OwnerTemplates.createOrUpdateOwnerForm(owner, result); } this.owners.save(owner); return OwnerTemplates.ownerDetails(owner, Result.success(\u0026#34;New Owner Created\u0026#34;)); } Noter l’appel à la méthode OwnerTemplates::ownerDetails() dont nous allons étudier le fonctionnement dans le paragraphe suivant.\nÀ noter également un écart de fonctionnement entre les versions Spring Boot et Quarkus de Petclinic : lors de la soumission d’un formulaire (POST), la version Spring utilise une redirection http pour rediriger l’utilisateur sur l’URL de consultation (GET). Nativement, Quarkus et Qute ne supportent pas ce fonctionnement. Pour être iso-fonctionnel, il aurait fallu utiliser Quarkus Renarde qui supporte les redirections et le scope flash.\nEnfin, dans la version Spring, la classe PetValidator assure la validation des champs obligatoires name, type et birthDate. Dans la version Quarkus, cette classe a été supprimée au profit de l’utilisation de l\u0026rsquo;annotation @NotNull ajoutée sur classe Pet et du support de Bean Validation.\nTemplates Qute type-safe Dans la version Quarkus de Petclinic, on note l’introduction de 3 nouvelles classes annotées chacune avec @CheckedTemplate : OwnerTemplates, PetTemplates et VetTemplates. Appelées depuis les contrôleurs REST, leurs méthodes natives permettent de sélectionner le template à rendre, ceci de manière type-safe. Exemple de la classe OwnerTemplates :\n@CheckedTemplate(basePath = \u0026#34;owners\u0026#34;) public class OwnerTemplates { public static native TemplateInstance findOwners(List\u0026lt;String\u0026gt; errors); public static native TemplateInstance ownersList(List\u0026lt;Owner\u0026gt; owners, int currentPage, Page\u0026lt;Owner\u0026gt; page); public static native TemplateInstance ownerDetails(Owner owner, Result result); public static native TemplateInstance createOrUpdateOwnerForm(Owner owner, Result result); } Les paramètres des méthodes correspondent au modèle de données requis lors du rendu des templates Qute. Lors du build, la classe QuteProcessor vérifie leur concordance. C’est la magie de Quarkus. Voici un exemple explicite d’erreur remontée si l’on omet le paramètre owners à la méthode ownersList:\nio.quarkus.qute.TemplateException: owners/ownersList.html:20:36 - {owner.firstName}: Only type-safe expressions are allowed in the checked template defined via: org.springframework.samples.petclinic.owner.OwnerTemplates.ownersList(); an expression must be based on a checked template parameter [page, currentPage], or bound via a param declaration, or the requirement must be relaxed via @CheckedTemplate(requireTypeSafeExpressions = false) Au niveau de l’annotation @CheckedTemplate, l’attribut basePath permet de pointer sur le répertoire templates/owners et ne pas toucher à la localisation des fichiers te template html. Quarkus utilise le nom de la méthode pour retrouver le fichier html du même nom dans le répertoire templates/owners.\nTest des contrôleurs Le test unitaire Spring Boot de la classe OwnerController utilise l’annotation @WebMvcTest pour configurer un contexte d\u0026rsquo;application limité, ciblant uniquement les composants liés à la couche web, ceci afin de tester les endpoints HTTP sans charger l\u0026rsquo;intégralité du contexte Spring de l\u0026rsquo;application. La classe utilitaire MockMvc permet à Spring de simuler des requêtes HTTP et de tester les contrôleurs Spring MVC sans démarrer un serveur web.\nLa migration des tests des contrôleurs REST vers Quarkus demande un peu de travail. En effet, Quarkus préconise l’utilisation de la bibliothèque REST-assured. Cette dernière permet de tester les API REST en facilitant l\u0026rsquo;envoi de requêtes HTTP et la vérification des réponses de manière fluide et intuitive à l’aide d’une fluent API.\nCombinée à l’annotation @QuarkusTest, l’annotation @TestHTTPEndpoint permet de tester spécifiquement un contrôleur REST. Le support par Quarkus des annotations Spring demande quelques ajustements. En effet, la classe QuarkusTestExtension fait appel à la classe SpringWebEndpointProvider qui s’attend à ce qu’une annotation @RequestMapping annote le contrôleur REST testé. Pour être testable, le code de prod a dû être refactoré : il a été nécessaire de déclarer une annotation @RequestMapping au top niveau de chaque contrôleur REST.\nAvant la mise en place du test OwnerControllerTests :\n@RestController class OwnerController { @GetMapping(\u0026#34;/owners/new\u0026#34;) public TemplateInstance initCreationForm() { Après la mise en place du test OwnerControllerTests :\n@RestController @RequestMapping(\u0026#34;/owners\u0026#34;) class OwnerController { @GetMapping(\u0026#34;/new\u0026#34;) public TemplateInstance initCreationForm() { En prenant comme exemple la méthode testProcessCreationFormSuccess, vous pouvez comparer le code d\u0026rsquo;un test migré de Spring MockMvc vers REST-assured.\nTest avec Spring MockMvc :\n@Test void testProcessCreationFormSuccess() throws Exception { mockMvc .perform(post(\u0026#34;/owners/new\u0026#34;).param(\u0026#34;firstName\u0026#34;, \u0026#34;Joe\u0026#34;) .param(\u0026#34;lastName\u0026#34;, \u0026#34;Bloggs\u0026#34;) .param(\u0026#34;address\u0026#34;, \u0026#34;123 Caramel Street\u0026#34;) .param(\u0026#34;city\u0026#34;, \u0026#34;London\u0026#34;) .param(\u0026#34;telephone\u0026#34;, \u0026#34;1316761638\u0026#34;)) .andExpect(status().is3xxRedirection()); } Test équivalent avec REST-assured :\n@Test void testProcessCreationFormSuccess() { RestAssured .given() .param(\u0026#34;firstName\u0026#34;, \u0026#34;Joe\u0026#34;) .param(\u0026#34;lastName\u0026#34;, \u0026#34;Bloggs\u0026#34;) .param(\u0026#34;address\u0026#34;, \u0026#34;123 Caramel Street\u0026#34;) .param(\u0026#34;city\u0026#34;, \u0026#34;London\u0026#34;) .param(\u0026#34;telephone\u0026#34;, \u0026#34;1316761638\u0026#34;) .when() .post(\u0026#34;/new\u0026#34;) .then() .statusCode(200) .body(\u0026#34;html.body.div.span\u0026#34;, is(\u0026#34;New Owner Created\u0026#34;)); } Du formatter Spring au ParamConverter JAX-RS Dans la version Spring, la classe PetTypeFormatter est chargée de parser et d’afficher une instance de PetType. Elle s’appuie sur l’interface Formatter de Spring Framework supportée par Spring MVC.\nLa migration de cette classe vers Quarkus a consisté à utiliser l’interface ParamConverter de JAX-RS. Le paragraphe Parameter mapping du guide Writing REST Services with Quarkus REST explique comment implémenter une telle classe et la mettre à disposition via un provider implémentant l’interface ParamConverterProvider, ce qui a été fait à travers la classe PetclinicParamConverterProvider.\nExemple de la classe PetTypeFormatter:\n@Component public class PetTypeFormatter implements ParamConverter\u0026lt;PetType\u0026gt; { private final PetTypeRepository petTypes; public PetTypeFormatter(PetTypeRepository petTypes) { this.petTypes = petTypes; } @Override public String toString(PetType petType) { return petType.getName(); } @Override public PetType fromString(String text) { Collection\u0026lt;PetType\u0026gt; findPetTypes = this.petTypes.findAllByOrderByName(); for (PetType type : findPetTypes) { if (type.getName().equals(text)) { return type; } } throw new IllegalArgumentException(\u0026#34;type not found: \u0026#34; + text); } } Bien que le nom des méthodes ait changé, le code fonctionnel consistant à chercher un type d’animal dans les données de référence est resté inchangé.\nConversion des dates Les formulaires de l’application Petclinic permettent de saisir la date de naissance d’un animal ainsi que sa date de visite à la clinique vétérinaire. Ces champs dates peuvent être laissées vides. La validation des données saisies est faite côté serveur.\nOr, la classe org.jboss.resteasy.reactive.server.core.parameters.converters.LocalDateParamConverter ne supporte pas les chaines vides :\nCaused by: java.time.format.DateTimeParseException: Text \u0026rsquo;\u0026rsquo; could not be parsed at index 0 at java.base/java.time.format.DateTimeFormatter.parseResolved0(DateTimeFormatter.java:2108) at java.base/java.time.format.DateTimeFormatter.parse(DateTimeFormatter.java:2010) at java.base/java.time.LocalDate.parse(LocalDate.java:435) at org.jboss.resteasy.reactive.server.core.parameters.converters.LocalDateParamConverter.convert(LocalDateParamConverter.java:24) at org.jboss.resteasy.reactive.server.core.parameters.converters.LocalDateParamConverter.convert(LocalDateParamConverter.java:6) at org.jboss.resteasy.reactive.server.core.parameters.converters.TemporalParamConverter.convert(TemporalParamConverter.java:29) \u0026hellip; 14 more\nSur le même modèle que le PetTypeFormatter vu précédemment, la classe LocalDateParamConverter ) implémentant l\u0026rsquo;interface ParamConverter a été introduite puis déclarée dans le provider PetclinicParamConverterProvider.\nCache applicatif Spring Petclinic utilise Spring Cache et Caffeine pour mettre en cache la liste des vétérinaires. La version Quarkus s\u0026rsquo;appuie sur l\u0026rsquo;Extension Quarkus for Spring Cache API qui permet de conserver l\u0026rsquo;usage de l\u0026rsquo;annotation @Cacheable de Spring Cache.\nUne différence de comportement entre Quarkus et Spring Boot a été identifiée lors des tests. En effet, apposée initialement sur les méthodes du repository VetRepository, les annotations @Cacheable n\u0026rsquo;étaient prises en compte par Quarkus. Une correction a consisté à déplacer l\u0026rsquo;annotation @Cacheable au niveau du contrôleur VetController :\n@GetMapping @Cacheable(\u0026#34;vets\u0026#34;) public TemplateInstance showVetList(@RequestParam(defaultValue = \u0026#34;1\u0026#34;) int page) { Vets vets = new Vets(); Page\u0026lt;Vet\u0026gt; paginated = findPaginated(page); vets.getVetList().addAll(paginated.toList()); return VetTemplates.vetList(paginated.getContent(), page, paginated); } Devenue inutile avec Quarkus, la classe CacheConfiguration a été supprimée.\nGestion transactionnelle L\u0026rsquo;extension Narayana JTA apporte à Quarkus un gestionnaire de transaction JTA utilisable par Hibernate ORM.\nL\u0026rsquo;annotation Spring org.springframework.transaction.annotation.Transactional a été remplacée par son équivalent JTA jakarta.transaction.Transactional.\nComme pour l\u0026rsquo;annotation @Cacheable, l\u0026rsquo;annotation @Transactional n\u0026rsquo;est pas prise en compte par Quarkus lorsqu\u0026rsquo;elle est utilisée au niveau du VetRepository. Spring Petclinic n\u0026rsquo;ayant plus de couche service, l\u0026rsquo;annotation @Transactional a été déplacée au niveau du contrôleur VetController :\n@GetMapping @Cacheable(\u0026#34;vets\u0026#34;) @Transactional public TemplateInstance showVetList(@RequestParam(defaultValue = \u0026#34;1\u0026#34;) int page) { Vets vets = new Vets(); Page\u0026lt;Vet\u0026gt; paginated = findPaginated(page); vets.getVetList().addAll(paginated.toList()); return VetTemplates.vetList(paginated.getContent(), page, paginated); } Propriétés Spring Boot Déclarée le temps de la migration puis supprimée une fois celle-ci terminée, l\u0026rsquo;extension Quarkus for Spring Boot properties a permis d\u0026rsquo;identifier les clés Quarkus à convertir dans le fichier application.properties. C\u0026rsquo;est le cas par exemple de la durée du cache des ressources statiques, configurées par défaut à 24h dans Quarkus, ramenée à 12h dans Petclinic.\n# Avant spring.web.resources.cache.cachecontrol.max-age=12h # Après quarkus.http.static-resources.max-age=12h Tests d\u0026rsquo;intégration avec Testcontainers En complément des tests unitaires, Spring Petclinic utilise Testcontainers pour ses tests d\u0026rsquo;intégration avec les bases MySQL et PostgreSQL. C\u0026rsquo;est par exemple le cas du test @SpringBootTest PostgresIntegrationTests qui démarre une base PostgreSQL configurée dans le fichier docker-compose.yml, utilisant à ce titre la dépendance spring-boot-docker-compose.\nLe support par Quarkus de la bibliothèque Testcontainers est particulièrement bien abouti et presque transparent. La version @QuarkusTest de PostgresIntegrationTests ressemble à un test sans Docker :\n@QuarkusTest @TestProfile(Profiles.Postgres.class) class PostgresIntegrationTests { @Autowired private VetRepository vets; @Test void testFindAll() { vets.findAll(); } @Test void testOwnerDetails() { RestAssured.when() .get(\u0026#34;/owners/1\u0026#34;) .then() .statusCode(200) .contentType(ContentType.HTML) .body(containsString(\u0026#34;Owner Information\u0026#34;)) .body(containsString(\u0026#34;George Franklin\u0026#34;)) .body(containsString(\u0026#34;110 W. Liberty St.\u0026#34;)) .body(containsString(\u0026#34;Madison\u0026#34;)) .body(containsString(\u0026#34;6085551023\u0026#34;)) .body(containsString(\u0026#34;Leo\u0026#34;)) .body(containsString(\u0026#34;cat\u0026#34;)); } } L\u0026rsquo;annotation Quarkus @TestProfile permet de référencer l\u0026rsquo;inner-class Postgres implémentant l\u0026rsquo;interface QuarkusTestProfile.\nimport io.quarkus.test.junit.QuarkusTestProfile; public class Profiles { public static class Postgres implements QuarkusTestProfile { @Override public String getConfigProfile() { return \u0026#34;postgres-it\u0026#34;; } } public static class MySQL implements QuarkusTestProfile { @Override public String getConfigProfile() { return \u0026#34;mysql-it\u0026#34;; } } } Notez la présence de 2 profils Quarkus postgres-it et mysql-it dédiés aux tests d\u0026rsquo;intégrations.\nDans le fichier application.properties, une ligne a été ajoutée pour chacun de ces profils :\n%postgres-it.quarkus.datasource.db-kind=postgresql %mysql-it.quarkus.datasource.db-kind=mysql Ces 2 profils ont été ajoutés afin que l\u0026rsquo;URL JDBC de la base de données ne soit pas valorisée et que Quarkus utilise Dev Services pour démarrer l\u0026rsquo;image Docker PostgreSQL.\nBinaire natif GraalVM Grâce aux plugins native-maven-plugin et spring-boot-maven-plugin, la version Spring Boot de Petclinic permet de générer un binaire natif en s\u0026rsquo;appuyant sur GraalVM.\nLe guide Building a Native Executable a permis de mettre en place facilement la génération d\u0026rsquo;un exécutable natif de Quarkus Spring Petclinic. Dans le pom.xml, la configuration d\u0026rsquo;un profile maven native permet d\u0026rsquo;activer la propriété quarkus.native.enabled.\nContrairement à la version Spring Boot qui s\u0026rsquo;appuyait sur une base H2, la version Quarkus requiert le démarrage d\u0026rsquo;une base PostgreSQL ou MySQL.\nL\u0026rsquo;installation de GraalVM (ex : sdk install java 21-graal) et la déclaration de la variable d\u0026rsquo;environnement GRAALVM_HOME est nécessaire.\n./mvnw package -Dnative -Dquarkus.profile=postgres docker compose up postgres ./target/quarkus-spring-petclinic-*-runner Quarkus Spring Petclinic démarre en 126 millisecondes :\n2025-04-13 15:54:29,755 INFO [io.quarkus] (main) quarkus-spring-petclinic 3.21.0 native (powered by Quarkus 3.21.0) started in 0.126s. Listening on: http://0.0.0.0:8080 2025-04-13 15:54:29,755 INFO [io.quarkus] (main) Profile postgres activated. 2025-04-13 15:54:29,755 INFO [io.quarkus] (main) Installed features: [agroal, cache, cdi, hibernate-orm, hibernate-orm-panache, hibernate-validator, jdbc-h2, jdbc-mysql, jdbc-postgresql, narayana-jta, qute, rest, rest-jackson, rest-qute, smallrye-context-propagation, smallrye-health, spring-cache, spring-data-jpa, spring-di, spring-web, vertx, web-dependency-locator] Conclusion A travers ce billet, vous aurez entre-aperçu les différentes étapes nécessaires pour migrer vers Quarkus et Qute une application Spring Web MVC avec Thymeleaf comme moteur de templating et Spring Data JPA pour la persistance. L\u0026rsquo;usage des extensions Quarkus pour Spring facilite grandement cette migration. Les ingénieurs de chez Quarkus ont fait du très bon travail. Malgré les quelques écarts de fonctionnement soulignés dans cet article, j\u0026rsquo;en ai été assez bluffé. Bravo à eux !\nJ\u0026rsquo;ai profité de cette migration pour soumettre une dizaine de Pull Request dans la version originale de Spring Petclinic (ex : PR #1775).\nDébutant en Quarkus, je ne serais pas surpris d\u0026rsquo;apprendre par mes lecteurs des axes d\u0026rsquo;améliorations. Utilisateur et amateur de Spring depuis 20 ans, j\u0026rsquo;ai essayé de rester neutre. A vous de comparer les 2 versions de Petclinic et de vous faire votre avis. Mon ressenti personnel est que l\u0026rsquo;éco-système Java se porte bien et que la concurrence est saine et stimulante !\nRessources Repo Github Quarkus Spring Petclinic Repo Github Spring Boot Petclinic Quarkus for Spring developers: Getting started Migrating a Spring Boot microservices application to Quarkus Migrating SpringBoot PetClinic REST to Quarkus ","link":"https://javaetmoi.com/2025/04/spring-petclinic-sous-extensions-quarkus/","section":"posts","tags":["quarkus","spring-boot"],"title":"Spring Petclinic sous extensions Quarkus"},{"body":"","link":"https://javaetmoi.com/tags/genai/","section":"tags","tags":null,"title":"Genai"},{"body":" Cet article explique comment intégrer un chatbot utilisant l’ IA générative dans une application de gestion codée en Java.\nNous nous appuierons sur le framework Open Source LangChain4j, une adaptation Java de la célèbre librairie python LangChain, visant à simplifier l\u0026rsquo;intégration de grands modèles de langage ( LLM). LangChain4j permet de créer des agents conversationnels, des assistants virtuels (comme notre chatbot), ou des applications capables d\u0026rsquo;effectuer des analyses de texte et de répondre en fonction de données contextuelles, le tout sans devoir écrire de code complexe et avec un haut niveau d’abstraction. Elle facilite notamment l\u0026rsquo;utilisation des API des Large Langage Model comme OpenAI et Hugging Face, et propose différents connecteurs pour des bases de données vectorielles, incluant Elasticsearch et Qdrant. Pour accélérer son intégration, LangChain4j propose des extensions pour Quarkus et des starters pour Spring Boot.\nPour illustrer cet article, nous utiliserons l’illustre application démo Spring Petclinic et son récent fork dédié à LangChain4j : spring-petclinic-langchain4j\nPropulsé par Spring Boot, Spring Petclinic s’appuie sur Spring Data JPA pour l’accès aux données et Thymeleaf pour la couche présentation HTML / CSS / JavaScript.\nEn septembre 2024, Oded Shopen, contributeur en 2020 du fork Spring Petclinic Cloud, a proposé une intégration de Spring AI dans Spring Petclinic. De son travail, est né le projet spring-petclinic-ai. Le repository spring-petclinic-langchain4j est un portage du framework Spring AI vers LangChain4j. Y a été ajouté notamment une fonctionnalité de streaming.\nExtraits du sample, les exemples de code s’appuient sur les versions 3.3 de Spring Boot et 0.35.0 de LangChaing4j.\nDémo Avant de se plonger dans le code Java, je vous propose de voir le résultat final en visionnant ce screencast durant moins de 2 minutes et dans lequel je pose 4 questions à l’assistant :\nImpressionnant, non ? Lorsqu’on pose les mêmes questions en français, le chatbot répond en français.\nCompte développeur OpenAI A ce jour, l’application Spring Petclinic LangChain4j supporte OpenAI et son service hébergé sur Azure : Azure OpenAI. Dans cet article, nous nous focaliserons sur l’intégration OpenAI. Pour faire fonctionner ce sample, moyennant quelques euros de crédits, vous aurez besoin d’un compte développeur OpenAI et d’une clé d’API personnelle exportée en tant que variable d’environnement OPENAI_API_KEY.\nSi vous ne disposez pas de votre propre clé API OpenAI ou ne souhaitez pas dépenser le moindre centime, vous pouvez utiliser temporairement la clé de démonstration demo que OpenAI fournit gratuitement. Seul le modèle gpt-4o-mini sera alors disponible avec cette clé et le nombre de tokens sera limité à 5000.\nexport OPENAI_API_KEY=demo Déclarer les starters Spring Boot La documentation Spring Boot Integration de LangChain4j explique comment les starters Spring Boot aident à configurer l’usage des larges modèles de langages, des embedding models et des embedding stores par le biais de propriétés à déclarer dans le fichier application.properties (ou application.yaml).\nDans le pom.xml de Spring Petclinic, commençons par déclarer les deux dépendances langchain4j-spring-boot-starter et langchain4j-open-ai-spring-boot-starter :\n\u0026lt;properties\u0026gt; \u0026lt;langchain4j.version\u0026gt;0.35.0\u0026lt;/langchain4j.version\u0026gt; \u0026lt;/properties\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;dev.langchain4j\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;langchain4j-spring-boot-starter\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;${langchain4j.version}\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;dev.langchain4j\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;langchain4j-open-ai-spring-boot-starter\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;${langchain4j.version}\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; Le premier starter langchain4j-spring-boot-starter expose la classe d’auto-configuration pour Spring Boot LangChain4jAutoConfig et donne, entre autre, accès à l’annotation @AiService que nous utiliserons dans une prochaine étape.\nLe second starter langchain4j-open-ai-spring-boot-starter permet quant à lui de parser et binder les propriétés spécifiques à OpenAI du fichier de configuration application.properties (ex : langchain4j.azure-open-ai.chat-model.api-key). Par transitivité, il tire les artefacts langchain4j-open-ai et dev.ai4j:openai4j. En interne, LangChain4j s’appuie sur le client Java non officiel openai4j permettant de connecter des applications Java à l\u0026rsquo;API OpenAI.\nConfiguration OpenAI Dans une première version du chatbot ne faisant pas encore l’usage du streaming, ajouter au fichier application.properties les 4 propriétés suivantes :\nlangchain4j.open-ai.chat-model.api-key=${OPENAI_API_KEY} langchain4j.open-ai.chat-model.model-name=gpt-4o langchain4j.open-ai.chat-model.log-requests=true langchain4j.open-ai.chat-model.log-responses=true Plus compact et moins cher que le modèle gpt-4o préconisé pour la démo, le modèle gpt-4o-mini peut également être utilisé et sait répondre aux exemples de questions suggérées dans le readme.md.\nSpring Boot détermine les beans à instancier en fonction des propriétés déclarées. A titre d’exemple, la classe AutoConfig du starter LangChain4j OpenAI pour Spring Boot, déclare conditionnellement un bean de type OpenAiChatModel implémentant l’interface agnostique ChatLanguageModel lorsque la propriété langchain4j.open-ai.chat-model.api-key est déclarée. Dans la suite de cet article, nous aurons besoin d’un bean de type StreamingChatLanguageModel permettant de streamer la réponse du LLM token par token. Sur le même principe, la propriété langchain4j.open-ai.streaming-chat-model.api-key déclenchera l’instanciation d’un bean de type OpenAiStreamingChatModel implémentant l’interface StreamingChatLanguageModel.\nDéclarer un AI Service Dans la suite de cet article, le code Java dédié au chatbot est localisé dans un package dédié : org.springframework.samples.petclinic.chat.\nDans le code métier, l’interaction avec le LLM se fait au travers d’une simple interface Java nommée Assistant et annotée avec l’annotation @AiService. LangChain4j propose un mécanisme similaire à Spring Data et Square Retrofit : on définit de manière déclarative une interface respectant des conventions de nommage et, au runtime, LangChain4j fournit une implémentation de cette interface. Se référer à la documentation AI Services pour davantage d’explications.\nL\u0026rsquo;interface Assistant propose une seule et unique méthode chat. Celle-ci accepte une question de l’utilisateur et renvoie la réponse du LLM sous forme de String.\nimport dev.langchain4j.service.SystemMessage; import dev.langchain4j.service.spring.AiService; @AiService interface Assistant { @SystemMessage(fromResource = \u0026#34;/prompts/system.st\u0026#34;) String chat(String userMessage); } Le bean implémentant cette interface est mise à disposition par Spring et pourra être injecté, par exemple, dans le contrôleur REST.\nPrompter un Message Système Pour répondre à l’utilisateur, nous guidons le comportement du LLM en définissant un « system message » via l’annotation @SystemMessage. Les directives sont externalisées dans le fichier texte system.st :\nYou are a friendly AI assistant designed to help with the management of a veterinarian pet clinic called Spring Petclinic. Your job is to answer questions about and to perform actions on the user\u0026#39;s behalf, mainly around veterinarians, owners, owners\u0026#39; pets and owners\u0026#39; visits. If you need access to pet owners or pet types, list and locate them without asking the user. You are required to answer in a professional manner. If you don\u0026#39;t know the answer, politely inform the user, and then ask a follow-up question to help clarify what they are asking. If you do know the answer, provide the answer but do not provide any additional followup questions. When dealing with vets, if the user is unsure about the returned results, explain that there may be additional data that was not returned. Only if the user is asking about the total number of all vets, answer that there are a lot and ask for some additional criteria. For owners, pets or visits - provide the correct data. Comme expliqué par Oded dans son article de blog, le contexte système doit être régulièrement enrichi et optimisé afin que les réponses soient les plus précises et les plus fiables possibles.\nPar exemple, afin que le LLM prenne des initiatives sans demander l’aval de l’utilisateur, le message système a été récemment complété avec la directive suivante :\nIf you need access to pet owners or pet types, list and locate them without asking the user. Sans cette directive, le LLM demande l’autorisation de rechercher l’ID de Betty :\nDéclarer un contrôleur REST Le chabot est appelé depuis le navigateur via une API REST. Déclarer un contrôleur Rest AssistantController exposant l\u0026rsquo;endpoint /chat:\n@RestController class AssistantController { private final Assistant assistant; AssistantController(Assistant assistant) { this.assistant = assistant; } @PostMapping(\u0026#34;/chat\u0026#34;) public String chat(@RequestBody String query) { return assistant.chat(query); } } Démarrer l’application Spring Boot et vérifier le fonctionnement du chatbot via un simple appel curl :\nParamétrer la mémoire conversationnelle de l’assistant A ce stade, le chatbot n’a pas encore de mémoire. Il ne peut donc pas s’aider des précédents échanges pour générer une réponse. Voici un des exemples des plus connus :\nPour remédier à ce problème, nous déclarons un bean Spring de type ChatMemory qui conserve l’historique des 10 derniers messages.\n@Configuration class AssistantConfiguration { @Bean ChatMemory chatMemory() { return MessageWindowChatMemory.withMaxMessages(10); } } Le prénom donné lors du premier appel est désormais réutilisé par le LLM lors du deuxième appel :\nPar défaut, les messages sont sauvegardés en mémoire dans un InMemoryChatMemoryStore. En cas de redémarrage de l’application, les messages volatiles sont perdus. Avec plusieurs instances de la même application sans affinité de sessions, l’historique des messages est réparti sur différentes JVM. Cela pose également problème. Une solution consiste à implémenter l’interface ChatMemoryStore afin de persister les messages en base ou dans un cache distribué. Se référer à l’exemple ServiceWithPersistentMemoryForEachUserExample.java.\nSupporter plusieurs utilisateurs A ce stade, la même instance de ChatMemory est utilisée pour toutes les invocations du service d\u0026rsquo;IA. Cette approche a des limites et ne fonctionnera pas avec plusieurs utilisateurs. Chaque utilisateur a besoin de sa propre instance de ChatMemory pour maintenir sa conversation individuelle.\nUne solution proposée par LangChain4j consiste à utiliser un ChatMemoryProvider :\n@Configuration class AssistantConfiguration { @Bean ChatMemoryProvider chatMemoryProvider() { return memoryId -\u0026gt; MessageWindowChatMemory.withMaxMessages(10); } } Chaque utilisateur est associé à un memoryId qui lui est dédié et dispose donc de sa propre ChatMemory.\nLa signature de la méthode chat de l’interface Assistant prend désormais un second paramètre nommé memoryId, annoté avec l’annotation @MemoryId et de type UUID v4. Le paramètre userMessage est quant à lui annoté avec @UserMessage :\nimport dev.langchain4j.service.MemoryId; import dev.langchain4j.service.SystemMessage; import dev.langchain4j.service.spring.AiService; @AiService interface Assistant { @SystemMessage(fromResource = \u0026#34;/prompts/system.st\u0026#34;) String chat(@MemoryId UUID memoryId, @UserMessage String userMessage); } Le contrôleur REST est adapté en fonction :\n@PostMapping(value = \u0026#34;/chat/{user}\u0026#34;) public String chat(@PathVariable UUID user, @RequestBody String query) { return assistant.chat(user, query); } La Pull Request #8 Support multiple users with @MemoryId montre un exemple d’illustration côté frontend.\nAjouter un widget de chat L\u0026rsquo;interface web du chat a été designée par Oded. Les codes HTML, JavaScript et CSS sont respectivement localisés dans les fichiers layout.html et chat.js et chat.css\nCertaines réponses d\u0026rsquo;OpenAI sont formattées en Markdown.\nCôté front, la librairie MarkedJS permet de convertir le markdown en HTML. Elle est ajoutée dans la configuration maven en tant que webjar :\n\u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.webjars.npm\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;marked\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;${webjars-marked.version}\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; Ajouter une première fonction Afin d’interagir avec le code métier de l’application, les développeurs peuvent proposer aux LLM d’appeler des fonctions, en l’occurrence du code Java. L\u0026rsquo;appel de fonctions personnalisées renforce la capacité des LLM à fournir des réponses plus pertinentes et contextuelles. Le LLM peut, par exemple, accéder aux données de l’application.\nLe LLM n’appelle pas directement les fonctions : le modèle produit une sortie de données structurées qui spécifie le nom de la fonction à appeler ainsi que les arguments suggérés. Les fonctions sont appelées par l’application Java ayant appelée le LLM.\nÀ noter que tous les LLM ne supportent pas encore l’appel de fonctions.\nLangChain4j facilite et standardise l\u0026rsquo;appel de fonctions via les Tools. Deux niveaux d’abstraction sont proposés :\nLow-level, en utilisant la classe ToolSpecification pour décrire les fonctions au LLM : nom, description, paramètres d’entrée / sortie. High-level, à l\u0026rsquo;aide des services d’IA et des méthodes Java annotées @Tool Nous mettrons en œuvre celui de haut niveau permettant d’annoter n\u0026rsquo;importe quelle méthode Java avec l\u0026rsquo;annotation @Tool. LangChain4j génère automatiquement les ToolSpecification s à partir de la signature des méthodes annotées. Lors de l’appel du LLM, la description des fonctions qui sont mises à sa disposition lui sont transmises. Lorsque le LLM décide d’appeler une fonction, LangChain4j exécute automatiquement la méthode Java appropriée et sa valeur de retour est renvoyée au LLM. Sous la forme d’un simple bean Spring, la classe AssistantTool expose les fonctions que le LLM pourra invoquer pour récupérer des données de référence, lister les propriétaires ou bien encore ajouter en base un animal de compagnie. Commençons par déclarer une function nommée getAllOwners :\n@Component public class AssistantTool { private final OwnerRepository ownerRepository; public AssistantTool(OwnerRepository ownerRepository) { this.ownerRepository = ownerRepository; } @Tool(\u0026#34;List the owners that the pet clinic has: ownerId, name, address, phone number, pets\u0026#34;) public OwnersResponse getAllOwners() { Pageable pageable = PageRequest.of(0, 100); Page\u0026lt;Owner\u0026gt; ownerPage = ownerRepository.findAll(pageable); return new OwnersResponse(ownerPage.getContent()); } } record OwnersResponse(List\u0026lt;Owner\u0026gt; owners) { } En interne, la classe AssistantTool utilise le repository Spring Data JPA OwnerRepository utilisé par l’application.\nApposée au niveau de l’annotation @Tool, la description aide le LLM à comprendre quand appeler la fonction.\nLa fonction getAllOwners() ne prend pas de paramètre. Elle retourne le record OwnersResponse qui contient une liste de Owner. La classe Owner est une entité JPA existante et utilisée pour l’IHM. Cet exemple démontre donc les capacités de LangChain4j à réutiliser le code existant.\nUne fois la fonction appelée, LangChain4j convertit le record OwnersResponse au format JSON pour que le LLM puisse le traiter.\nÀ noter que la méthode getAllOwners n’aurait pas sa place dans une application d’entreprise. L’application démo Spring Petclinic compte seulement 10 propriétaires. Renvoyer toutes les données de la base ne pose donc pas de problème de performance. Néanmoins, dans une vraie application de gestion, proposer une méthode de recherche multi-critères serait préférable. C’est ce que propose l\u0026rsquo;issue #9.\nInterrogeons à présent le chatbot avec la question « Please list the owners that come to the clinic. » et regardons le flux d’échange entre l’application Petclinic et OpenAI.\nAu préalable, dans le fichier application.properties, nous avons activé les logs des requêtes et réponses envoyées à OpenAI :\nlangchain4j.open-ai.chat-model.log-requests=true langchain4j.open-ai.chat-model.log-responses=true Lors du 1er appel à OpenAI, à côté de la question saisie par l’utilisateur dans la fenêtre de chat, la fonction getAllOwners est proposée dans une liste de tools.\nLog partiel de la requête #1 :\n- method: POST - url: https://api.openai.com/v1/chat/completions - headers: [Authorization: Bearer xxxx], [User-Agent: langchain4j-openai] - body: { \u0026#34;model\u0026#34; : \u0026#34;gpt-4o\u0026#34;, \u0026#34;messages\u0026#34; : [ { \u0026#34;role\u0026#34; : \u0026#34;system\u0026#34;, \u0026#34;content\u0026#34; : \u0026#34;You are a friendly AI assistant designed to help with the management of a veterinarian pet clinic called Spring Petclinic…\u0026#34; }, { \u0026#34;role\u0026#34; : \u0026#34;user\u0026#34;, \u0026#34;content\u0026#34; : \u0026#34;\\\u0026#34;Please list the owners that come to the clinic.\\\u0026#34; } ], \u0026#34;temperature\u0026#34; : 0.7, \u0026#34;tools\u0026#34; : [{ \u0026#34;type\u0026#34; : \u0026#34;function\u0026#34;, \u0026#34;function\u0026#34; : { \u0026#34;name\u0026#34; : \u0026#34;getAllOwners\u0026#34;, \u0026#34;description\u0026#34; : \u0026#34;List the owners that the pet clinic has: ownerId, name, address, phone number, pets\u0026#34;, \u0026#34;parameters\u0026#34; : { \u0026#34;type\u0026#34; : \u0026#34;object\u0026#34;, \u0026#34;properties\u0026#34; : { }, \u0026#34;required\u0026#34; : [ ] } } }, … Comme attendu, OpenAI demande à l’application d’appeler la function getAllOwners.\nLog partiel de la réponse #1 :\nstatus code: 200 - headers: xxxx - body: { \u0026#34;id\u0026#34;: \u0026#34;chatcmpl-AOqizmPVZnGZ9jAB2of6NhayYi2mY\u0026#34;, \u0026#34;object\u0026#34;: \u0026#34;chat.completion\u0026#34;, \u0026#34;created\u0026#34;: 1730485909, \u0026#34;model\u0026#34;: \u0026#34;gpt-4o-2024-08-06\u0026#34;, \u0026#34;choices\u0026#34;: [ { \u0026#34;index\u0026#34;: 0, \u0026#34;message\u0026#34;: { \u0026#34;role\u0026#34;: \u0026#34;assistant\u0026#34;, \u0026#34;content\u0026#34;: null, \u0026#34;tool_calls\u0026#34;: [ { \u0026#34;id\u0026#34;: \u0026#34;call_6fe84CTFo3zwOvo10ZBgBqjl\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;function\u0026#34;, \u0026#34;function\u0026#34;: { \u0026#34;name\u0026#34;: \u0026#34;getAllOwners\u0026#34;, \u0026#34;arguments\u0026#34;: \u0026#34;{}\u0026#34; } } ], …. } LangChain4j fait aussitôt appel à la méhtode getAllOwners du bean AssistantTool. Le résultat est sérialisé en JSON et placé dans l’attribut content lors du second appel au LLM.\nLog partiel de la requête #2 :\n- method: POST - url: https://api.openai.com/v1/chat/completions - headers: [Authorization: Bearer sk-Qw...MA], [User-Agent: langchain4j-openai] - body: { \u0026#34;model\u0026#34; : \u0026#34;gpt-4o\u0026#34;, \u0026#34;messages\u0026#34; : [ { \u0026#34;role\u0026#34; : \u0026#34;system\u0026#34;, \u0026#34;content\u0026#34; : \u0026#34;You are a friendly AI …\u0026#34; }, { \u0026#34;role\u0026#34; : \u0026#34;user\u0026#34;, \u0026#34;content\u0026#34; : \u0026#34;\\\u0026#34;Please list the owners that come to the clinic\u0026#34; }, { \u0026#34;role\u0026#34; : \u0026#34;assistant\u0026#34;, \u0026#34;tool_calls\u0026#34; : [ { \u0026#34;id\u0026#34; : \u0026#34;call_6fe84CTFo3zwOvo10ZBgBqjl\u0026#34;, \u0026#34;type\u0026#34; : \u0026#34;function\u0026#34;, \u0026#34;function\u0026#34; : { \u0026#34;name\u0026#34; : \u0026#34;getAllOwners\u0026#34;, \u0026#34;arguments\u0026#34; : \u0026#34;{}\u0026#34; } } ] }, { \u0026#34;role\u0026#34; : \u0026#34;tool\u0026#34;, \u0026#34;tool_call_id\u0026#34; : \u0026#34;call_6fe84CTFo3zwOvo10ZBgBqjl\u0026#34;, \u0026#34;content\u0026#34; : \u0026#34;{\\n \\\u0026#34;owners\\\u0026#34;: [\\n {\\n \\\u0026#34;address\\\u0026#34;: \\\u0026#34;110 W. Liberty St.\\\u0026#34;,\\n \\\u0026#34;city\\\u0026#34;: \\\u0026#34;Madison\\\u0026#34;,\\n \\\u0026#34;telephone\\\u0026#34;: \\\u0026#34;6085551023\\\u0026#34;,\\n \\\u0026#34;pets\\\u0026#34;: [\\n {\\n \\\u0026#34;birthDate\\\u0026#34;: \\\u0026#34;2010-09-07\\\u0026#34;,\\n \\\u0026#34;type\\\u0026#34;: {\\n \\\u0026#34;name\\\u0026#34;: \\\u0026#34;cat\\\u0026#34;,\\n \\\u0026#34;id\\\u0026#34;: 1\\n },\\n \\\u0026#34;visits\\\u0026#34;: [],\\n \\\u0026#34;name\\\u0026#34;: \\\u0026#34;Leo\\\u0026#34;,\\n \\\u0026#34;id\\\u0026#34;: 1\\n }\\n ],\\n \\\u0026#34;firstName\\\u0026#34;: \\\u0026#34;George\\\u0026#34;,\\n \\\u0026#34;lastName\\\u0026#34;: \\\u0026#34;Franklin\\\u0026#34;,\\n \\\u0026#34;id\\\u0026#34;: 1\\n },\\n {\\n \\\u0026#34;address\\\u0026#34;: \\\u0026#34;638 Cardinal Ave.\\\u0026#34;,\\n \\\u0026#34;city\\\u0026#34;: \\\u0026#34;Sun Prairie\\\u0026#34;,\\n \\\u0026#34;telephone\\\u0026#34;: \\\u0026#34;6085551749\\\u0026#34;,\\n \\\u0026#34;pets\\\u0026#34;: [\\n {\\n \\\u0026#34;birthDate\\\u0026#34;: \\\u0026#34;2012-08-06\\\u0026#34;,\\n \\\u0026#34;type\\\u0026#34;: {\\n \\\u0026#34;name\\\u0026#34;: \\\u0026#34;hamster\\\u0026#34;,\\n \\\u0026#34;id\\\u0026#34;: 6\\n },\\n \\\u0026#34;visits\\\u0026#34;: [],\\n \\\u0026#34;name\\\u0026#34;: \\\u0026#34;Basil\\\u0026#34;,\\n \\\u0026#34;id\\\u0026#34;: 2\\n }\\n ],\\n \\\u0026#34;firstName\\\u0026#34;: \\\u0026#34;Betty\\\u0026#34;,\\n \\\u0026#34;lastName\\\u0026#34;: \\\u0026#34;Davis\\\u0026#34;,\\n \\\u0026#34;id\\\u0026#34;: 2\\n, … } ], \u0026#34;temperature\u0026#34; : 0.7, \u0026#34;tools\u0026#34; : [ { …}] OpenAI utilise le résultat de l’appel à la fonction getAllOwners pour générer une réponse présentant une liste de propriétaires d’animaux formatée en markdown :\nLog partiel de la réponse #2 :\n- status code: 200 - headers: … - body: { \u0026#34;id\u0026#34;: \u0026#34;chatcmpl-AOqj0Y9yhJjzYtzV7QMXiBU4URkJ7\u0026#34;, \u0026#34;object\u0026#34;: \u0026#34;chat.completion\u0026#34;, \u0026#34;created\u0026#34;: 1730485910, \u0026#34;model\u0026#34;: \u0026#34;gpt-4o-2024-08-06\u0026#34;, \u0026#34;choices\u0026#34;: [ { \u0026#34;index\u0026#34;: 0, \u0026#34;message\u0026#34;: { \u0026#34;role\u0026#34;: \u0026#34;assistant\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;Here is a list of the owners at the Spring Petclinic:\\n\\n1. **George Franklin**\\n - Address: 110 W. Liberty St., Madison\\n - Telephone: 6085551023\\n - Pets: \\n - Leo (Cat, born on 2010-09-07)\\n\\n2. **Betty Davis**\\n - Address: 638 Cardinal Ave., Sun Prairie\\n - Telephone: 6085551749\\n - Pets: \\n - Basil (Hamster, born on 2012-08-06)\\n\\n3. **Eduardo Rodriquez**\\n - Address: 2693 Commerce St., McFarland\\n - Telephone: 6085558763\\n - Pets: \\n - Jewel (Dog, born on 2010-03-07)\\n - Rosy (Dog, born on 2011-04-17)\\n\\n4. **Harold Davis**\\...\u0026#34;, … } ], \u0026#34;usage\u0026#34;: { … } Cette première fonction a montré comment le LLM peut récupérer des données depuis la base de données pour générer sa réponse.\nAgent conversationnel Ajoutons à présent les fonctions permettant à un vétérinaire de déclarer un nouvel animal de compagnie pour l’un de ses clients, en formulant dans le chat la requête suivante :\nAdd a dog for Betty Davis. His name is Moopsie. His birthday is on 2 October 2024.\nDans la classe AssistantTool, ajoutons une seconde fonction addPetToOwner permettant à un vétérinaire de déclarer un nouvel animal de compagnie à l’un de ses clients :\n@Tool(\u0026#34;Add a pet with the specified petTypeId, to an owner identified by the ownerId\u0026#34;) public AddedPetResponse addPetToOwner(AddPetRequest request) { Owner owner = ownerRepository.findById(request.ownerId()); owner.addPet(request.pet()); this.ownerRepository.save(owner); return new AddedPetResponse(owner); } Cette fois-ci, la méthode accepte un paramètre de type AddPetRequest :\nrecord AddPetRequest(Pet pet, Integer ownerId) { } Pour ajouter un animal de compagnie, le LLM doit connaitre l’identifiant du propriétaire (le ownerId) et les données caractérisant son compagnon. Cet identifiant peut être récupéré par le LLM via l’appel de la fonction getAllOwners.\nLe LLM doit également savoir comment valoriser les attributs de la classe Pet : name, birthDate, visits et type. Les identifiants du type PetType (ex : 1=cat, 2=dog …) peuvent être listés par le LLM via l’appel de la nouvelle fonction populatePetTypes :\n@Tool(\u0026#34;List all pairs of petTypeId and pet type name\u0026#34;) public List\u0026lt;PetType\u0026gt; populatePetTypes() { return this.ownerRepository.findPetTypes(); } Lorsque OpenAI est interrogé, dans sa première réponse, il demande à LangChain4j d’appeler 2 fonctions / tools. Optimisé, cela évitera les allers-retours :\nLog partiel de la réponse #1 :\n2024-11-02T18:14:50.532+01:00 DEBUG 10650 --- [.openai.com/...] d.a.openai4j.StreamingRequestExecutor : onEvent() {\u0026#34;id\u0026#34;:\u0026#34;chatcmpl-APC01s26BWq4QFXC1tpIgHuSml798\u0026#34;,\u0026#34;object\u0026#34;:\u0026#34;chat.completion.chunk\u0026#34;,\u0026#34;created\u0026#34;:1730567689,\u0026#34;model\u0026#34;:\u0026#34;gpt-4o-2024-08-06\u0026#34;,\u0026#34;system_fingerprint\u0026#34;:\u0026#34;fp_159d8341cc\u0026#34;,\u0026#34;usage\u0026#34;:null,\u0026#34;choices\u0026#34;:[{\u0026#34;index\u0026#34;:0,\u0026#34;delta\u0026#34;:{\u0026#34;tool_calls\u0026#34;:[{\u0026#34;index\u0026#34;:0,\u0026#34;id\u0026#34;:\u0026#34;call_T0QYuwvX9NGD6kX9KxFLKrDm\u0026#34;,\u0026#34;type\u0026#34;:\u0026#34;function\u0026#34;,\u0026#34;function\u0026#34;:{\u0026#34;name\u0026#34;:\u0026#34;getAllOwners\u0026#34;,\u0026#34;arguments\u0026#34;:\u0026#34;\u0026#34;}}]},\u0026#34;logprobs\u0026#34;:null,\u0026#34;finish_reason\u0026#34;:null}]} 2024-11-02T18:14:50.534+01:00 DEBUG 10650 --- [.openai.com/...] d.a.openai4j.StreamingRequestExecutor : onEvent() {\u0026#34;id\u0026#34;:\u0026#34;chatcmpl-APC01s26BWq4QFXC1tpIgHuSml798\u0026#34;,\u0026#34;object\u0026#34;:\u0026#34;chat.completion.chunk\u0026#34;,\u0026#34;created\u0026#34;:1730567689,\u0026#34;model\u0026#34;:\u0026#34;gpt-4o-2024-08-06\u0026#34;,\u0026#34;system_fingerprint\u0026#34;:\u0026#34;fp_159d8341cc\u0026#34;,\u0026#34;usage\u0026#34;:null,\u0026#34;choices\u0026#34;:[{\u0026#34;index\u0026#34;:0,\u0026#34;delta\u0026#34;:{\u0026#34;tool_calls\u0026#34;:[{\u0026#34;index\u0026#34;:1,\u0026#34;id\u0026#34;:\u0026#34;call_hRf3HX1yLDIU0DtAr5Sjmov5\u0026#34;,\u0026#34;type\u0026#34;:\u0026#34;function\u0026#34;,\u0026#34;function\u0026#34;:{\u0026#34;name\u0026#34;:\u0026#34;populatePetTypes\u0026#34;,\u0026#34;arguments\u0026#34;:\u0026#34;\u0026#34;}}]},\u0026#34;logprobs\u0026#34;:null,\u0026#34;finish_reason\u0026#34;:null}]} 2024-11-02T18:14:50.534+01:00 DEBUG 10650 --- [.openai.com/...] d.a.openai4j.StreamingRequestExecutor : onEvent() {\u0026#34;id\u0026#34;:\u0026#34;chatcmpl-APC01s26BWq4QFXC1tpIgHuSml798\u0026#34;,\u0026#34;object\u0026#34;:\u0026#34;chat.completion.chunk\u0026#34;,\u0026#34;created\u0026#34;:1730567689,\u0026#34;model\u0026#34;:\u0026#34;gpt-4o-2024-08-06\u0026#34;,\u0026#34;system_fingerprint\u0026#34;:\u0026#34;fp_159d8341cc\u0026#34;,\u0026#34;usage\u0026#34;:null,\u0026#34;choices\u0026#34;:[{\u0026#34;index\u0026#34;:0,\u0026#34;delta\u0026#34;:{\u0026#34;tool_calls\u0026#34;:[{\u0026#34;index\u0026#34;:1,\u0026#34;function\u0026#34;:{\u0026#34;arguments\u0026#34;:\u0026#34;{}\u0026#34;}}]},\u0026#34;logprobs\u0026#34;:null,\u0026#34;finish_reason\u0026#34;:null}]} LangChain4j appelle séquentiellement ces 2 fonctions (paralléliser ces appels serait un axe d’optimisation de notre application : issue #13) puis renvoie les résultats à OpenAI.\nLog partiel de la requête #2 :\n\u0026#34;tool_calls\u0026#34; : [ { \u0026#34;id\u0026#34; : \u0026#34;call_T0QYuwvX9NGD6kX9KxFLKrDm\u0026#34;, \u0026#34;type\u0026#34; : \u0026#34;function\u0026#34;, \u0026#34;function\u0026#34; : { \u0026#34;name\u0026#34; : \u0026#34;getAllOwners\u0026#34;, \u0026#34;arguments\u0026#34; : \u0026#34;{}\u0026#34; } }, { \u0026#34;id\u0026#34; : \u0026#34;call_hRf3HX1yLDIU0DtAr5Sjmov5\u0026#34;, \u0026#34;type\u0026#34; : \u0026#34;function\u0026#34;, \u0026#34;function\u0026#34; : { \u0026#34;name\u0026#34; : \u0026#34;populatePetTypes\u0026#34;, \u0026#34;arguments\u0026#34; : \u0026#34;{}\u0026#34; } } ] }, { \u0026#34;role\u0026#34; : \u0026#34;tool\u0026#34;, \u0026#34;tool_call_id\u0026#34; : \u0026#34;call_T0QYuwvX9NGD6kX9KxFLKrDm\u0026#34;, \u0026#34;content\u0026#34; : \u0026#34;{\\n \\\u0026#34;owners\\\u0026#34;: [\\n {\\n \\\u0026#34;address\\\u0026#34;: \\\u0026#34;638 Cardinal Ave.\\\u0026#34;,\\n \\\u0026#34;city\\\u0026#34;: \\\u0026#34;Sun Prairie\\\u0026#34;,\\n \\\u0026#34;telephone\\\u0026#34;: \\\u0026#34;6085551749\\\u0026#34;,\\n \\\u0026#34;pets\\\u0026#34;: [\\n {\\n \\\u0026#34;birthDate\\\u0026#34;: \\\u0026#34;2012-08-06\\\u0026#34;,\\n \\\u0026#34;type\\\u0026#34;: {\\n \\\u0026#34;name\\\u0026#34;: \\\u0026#34;hamster\\\u0026#34;,\\n \\\u0026#34;id\\\u0026#34;: 6\\n },\\n \\\u0026#34;visits\\\u0026#34;: [],\\n \\\u0026#34;name\\\u0026#34;: \\\u0026#34;Basil\\\u0026#34;,\\n \\\u0026#34;id\\\u0026#34;: 2\\n }\\n ],\\n \\\u0026#34;firstName\\\u0026#34;: \\\u0026#34;Betty\\\u0026#34;,\\n \\\u0026#34;lastName\\\u0026#34;: \\\u0026#34;Davis\\\u0026#34;,\\n \\\u0026#34;id\\\u0026#34;: 2\\n }, …\\]\\n}\u0026#34; }, { \u0026#34;role\u0026#34; : \u0026#34;tool\u0026#34;, \u0026#34;tool_call_id\u0026#34; : \u0026#34;call_hRf3HX1yLDIU0DtAr5Sjmov5\u0026#34;, \u0026#34;content\u0026#34; : \u0026#34;[\\n {\\n \\\u0026#34;name\\\u0026#34;: \\\u0026#34;bird\\\u0026#34;,\\n \\\u0026#34;id\\\u0026#34;: 5\\n },\\n {\\n \\\u0026#34;name\\\u0026#34;: \\\u0026#34;cat\\\u0026#34;,\\n \\\u0026#34;id\\\u0026#34;: 1\\n },\\n {\\n \\\u0026#34;name\\\u0026#34;: \\\u0026#34;dog\\\u0026#34;,\\n \\\u0026#34;id\\\u0026#34;: 2\\n },\\n {\\n \\\u0026#34;name\\\u0026#34;: \\\u0026#34;hamster\\\u0026#34;,\\n \\\u0026#34;id\\\u0026#34;: 6\\n },\\n {\\n \\\u0026#34;name\\\u0026#34;: \\\u0026#34;lizard\\\u0026#34;,\\n \\\u0026#34;id\\\u0026#34;: 3\\n },\\n {\\n \\\u0026#34;name\\\u0026#34;: \\\u0026#34;snake\\\u0026#34;,\\n \\\u0026#34;id\\\u0026#34;: 4\\n }\\n]\u0026#34; } ], De ces deux appels de fonctions, OpenAI déduit l’identifiant de Betty Davis égal à 2 ainsi que l’identifiant d’un chien lui aussi égal à 2. En réponse, il demande à LangChain4j d’appeler la fonction addPetToOwner en lui passant ces deux identifiants, ainsi que le nom et la date de naissance donné par l’utilisateur.\n2024-11-02T18:14:51.734+01:00 DEBUG 10650 --- [.openai.com/...] d.l.service.tool.DefaultToolExecutor : About to execute ToolExecutionRequest { id = \u0026#34;call_7TdLNNZPsMD4ujev8wRytdyf\u0026#34;, name = \u0026#34;addPetToOwner\u0026#34;, arguments = \u0026#34;{\u0026#34;request\u0026#34;:{\u0026#34;ownerId\u0026#34;:2,\u0026#34;pet\u0026#34;:{\u0026#34;name\u0026#34;:\u0026#34;Moopsie\u0026#34;,\u0026#34;birthDate\u0026#34;:{\u0026#34;year\u0026#34;:2024,\u0026#34;month\u0026#34;:10,\u0026#34;day\u0026#34;:2},\u0026#34;type\u0026#34;:{\u0026#34;id\u0026#34;:2}}}}\u0026#34; } for memoryId 510e5396-3c19-46c2-991c-3200a653f90f 2024-11-02T18:14:51.798+01:00 DEBUG 10650 --- [.openai.com/...] d.l.service.tool.DefaultToolExecutor : Tool execution result: { \u0026#34;owner\u0026#34;: { \u0026#34;address\u0026#34;: \u0026#34;638 Cardinal Ave.\u0026#34;, \u0026#34;city\u0026#34;: \u0026#34;Sun Prairie\u0026#34;, \u0026#34;telephone\u0026#34;: \u0026#34;6085551749\u0026#34;, \u0026#34;pets\u0026#34;: [ { \u0026#34;birthDate\u0026#34;: \u0026#34;2012-08-06\u0026#34;, \u0026#34;type\u0026#34;: { \u0026#34;name\u0026#34;: \u0026#34;hamster\u0026#34;, \u0026#34;id\u0026#34;: 6 }, \u0026#34;visits\u0026#34;: [], \u0026#34;name\u0026#34;: \u0026#34;Basil\u0026#34;, \u0026#34;id\u0026#34;: 2 }, { \u0026#34;birthDate\u0026#34;: \u0026#34;2024-10-02\u0026#34;, \u0026#34;type\u0026#34;: { \u0026#34;id\u0026#34;: 2 }, \u0026#34;visits\u0026#34;: [], \u0026#34;name\u0026#34;: \u0026#34;Moopsie\u0026#34; } ], \u0026#34;firstName\u0026#34;: \u0026#34;Betty\u0026#34;, \u0026#34;lastName\u0026#34;: \u0026#34;Davis\u0026#34;, \u0026#34;id\u0026#34;: 2 } } Cette fois-ci, LangChain4j doit passer un paramètre de type AddPetRequest lors de l’appel à la fonction addPetToOwner. La structure de donnée a préalablement été communiquée au LLM lors de la description de la fonction mise à sa disposition :\n{ \u0026#34;type\u0026#34;: \u0026#34;function\u0026#34;, \u0026#34;function\u0026#34;: { \u0026#34;name\u0026#34;: \u0026#34;addPetToOwner\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;Add a pet with the specified petTypeId, to an owner identified by the ownerId\u0026#34;, \u0026#34;parameters\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;object\u0026#34;, \u0026#34;properties\u0026#34;: { \u0026#34;request\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;object\u0026#34;, \u0026#34;properties\u0026#34;: { \u0026#34;ownerId\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;integer\u0026#34; }, \u0026#34;pet\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;object\u0026#34;, \u0026#34;properties\u0026#34;: { \u0026#34;visits\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;array\u0026#34;, \u0026#34;items\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;object\u0026#34;, \u0026#34;properties\u0026#34;: { \u0026#34;date\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;object\u0026#34;, \u0026#34;properties\u0026#34;: { \u0026#34;month\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;integer\u0026#34; }, \u0026#34;year\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;integer\u0026#34; }, \u0026#34;day\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;integer\u0026#34; } }, \u0026#34;required\u0026#34;: [] }, \u0026#34;description\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34; } }, \u0026#34;required\u0026#34;: [] } }, \u0026#34;type\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;object\u0026#34;, \u0026#34;properties\u0026#34;: {}, \u0026#34;required\u0026#34;: [] }, \u0026#34;birthDate\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;object\u0026#34;, \u0026#34;properties\u0026#34;: { \u0026#34;month\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;integer\u0026#34; }, \u0026#34;year\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;integer\u0026#34; }, \u0026#34;day\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;integer\u0026#34; } }, \u0026#34;required\u0026#34;: [] } }, \u0026#34;required\u0026#34;: [] } }, \u0026#34;required\u0026#34;: [] } }, \u0026#34;required\u0026#34;: [ \u0026#34;request\u0026#34; ] } } } Le LLM a structuré en JSON les paramètres d’appel de fonction. La classe DefaultToolExecutor de LangChain4j se charge d’unmarshaller les données JSON. En interne, elle s’appuie sur une librairie JSON (à termes, Jackson doit remplacer Google GSON).\nLes résultats des trois appels de fonction sont renvoyés à OpenAI dans une troisième et dernière requête. Ce dernier conclut que l’ajout s’est bien passé et récapitule les informations enregistrées.\nVoici un diagramme de séquences illustrant les appels que nous venons de décrire :\nResponse Streaming\nLa méthode chat() déclarée dans le @AiService renvoie une simple String. L’utilisateur doit attendre que le LLM ait généré l’intégralité de sa réponse avant de recevoir le résultat. Ceci est regrettable lorsqu’on sait qu’un LLM génère du texte un jeton à la fois.\nLa plupart des LLM propose un moyen de diffuser la réponse jeton par jeton au lieu d\u0026rsquo;attendre que l\u0026rsquo;ensemble du texte soit généré. Cette possibilité améliore l\u0026rsquo;expérience de l\u0026rsquo;utilisateur qui n\u0026rsquo;a alors pas besoin d\u0026rsquo;attendre une durée inconnue et peut commencer à lire la réponse presque immédiatement. LangChain4j supporte nativement cette fonctionnalité de Response Streaming. Il sait streamer token par token en utilisant l’interface TokenStream comme type de réponse. Le client peut s’abonner aux flux de jetons renvoyé par le LLM et ainsi être notifié lorsqu’un nouveau jeton est disponible. Modifions la signature de notre méthode :\ninterface Assistant { @SystemMessage(fromResource = \u0026#34;/prompts/system.st\u0026#34;) TokenStream chat(@MemoryId UUID memoryId, @UserMessage String userMessage); } Remarque : cette version de l’application Spring Petclinic est développée sur une stack non réactive avec Spring MVC. Si elle l’avait été avec Spring Webflux, nous aurions pu utiliser le type Flux\u0026lt;String\u0026gt; à la place de TokenStream.\nLe contrôleur REST AssistantController doit à son tour être adapté. De la même manière que sur l’application web ChatGPT, nous utilisons la technologie Server Sent Events (SSE) pour que le serveur envoie au navigateur au fil de l’eau les réponses du LLM. Spring Framework supporte nativement SSE depuis 2015 via la classe SseEmitter, se référer à sa documentation.\nChaque token est envoyé dans un message structuré en JSON. L’onglet EventStream de Google Chrome donne un aperçu du résultat :\nDans le contrôleur, l’appel à la méthode chat() est fait en asynchrone par un ExecutorService. L’appelant n’est pas bloqué. L’envoie des tokens au client (dans notre cas au navigateur) est assuré par l’appel à la classe SseEmitter.\n@RestController class AssistantController { private static final Logger LOGGER = LoggerFactory.getLogger(AssistantController.class); private final Assistant assistant; private final ExecutorService nonBlockingService = Executors.newCachedThreadPool(); AssistantController(Assistant assistant) { this.assistant = assistant; } // Using the POST method due to chat memory capabilities @PostMapping(value = \u0026#34;/chat/{user}\u0026#34;) public SseEmitter chat(@PathVariable UUID user, @RequestBody String query) { SseEmitter emitter = new SseEmitter(); nonBlockingService.execute(() -\u0026gt; assistant.chat(user, query).onNext(message -\u0026gt; { try { sendMessage(emitter, message); } catch (IOException e) { LOGGER.error(\u0026#34;Error while writing next token\u0026#34;, e); emitter.completeWithError(e); } }).onComplete(token -\u0026gt; emitter.complete()).onError(error -\u0026gt; { LOGGER.error(\u0026#34;Unexpected chat error\u0026#34;, error); try { sendMessage(emitter, error.getMessage()); } catch (IOException e) { LOGGER.error(\u0026#34;Error while writing next token\u0026#34;, e); } emitter.completeWithError(error); }).start()); return emitter; } private static void sendMessage(SseEmitter emitter, String message) throws IOException { String token = message // Hack line break problem when using Server Sent Events (SSE) .replace(\u0026#34;\\n\u0026#34;, \u0026#34;\u0026lt;br\u0026gt;\u0026#34;) // Escape JSON quotes .replace(\u0026#34;\\\u0026#34;\u0026#34;, \u0026#34;\\\\\\\u0026#34;\u0026#34;); emitter.send(\u0026#34;{\\\u0026#34;t\\\u0026#34;: \\\u0026#34;\u0026#34; + token + \u0026#34;\\\u0026#34;}\u0026#34;); } } À noter un hack (issue #12) remplaçant les sauts de ligne du LLM pour pallier au problème connu des sauts de lignes avec SSE.\nEn interne, pour streamer la réponse du LLM, LangChain4j utilise l’interface StreamingChatLanguageModel (à la place de ChatLanguageModel). Dans le fichier de configuration application.properties, les propriétés langchain4j.open-ai.chat-model.xxx sont renommées en langchain4j.open-ai.streaming-chat-model.xxx:\nlangchain4j.open-ai.streaming-chat-model.api-key=${OPENAI_API_KEY} langchain4j.open-ai.streaming-chat-model.model-name=gpt-4o langchain4j.open-ai.streaming-chat-model.log-requests=true langchain4j.open-ai.streaming-chat-model.log-responses=true Côté front, le code JavaScript du fichier chat.js a été adapté pour accepter le type MIME text/event-stream et parser les messages JSON.\nLa Pull Request #3 Response Streaming and SSE décrit tous les changements appliqués côté back et front pour passer au mode streaming.\nRetrieval Augmented Generation (RAG) L’ensemble des tools mis à disposition du LLM par Petclinic lui permettent d’accéder aux données des propriétaires, de leurs animaux et de leurs visites. Rien sur les vétérinaires officiant dans la clinique. Afin de permettre aux utilisateurs de poser des questions sur les vétérinaires, nous allons exploiter une autre fonctionnalité majeure des LLM et de LangChain4j : la génération augmentée par récupération, connue en anglais sous l’acronyme RAG pour Retrieval Augmented Generation. Un RAG permet de fournir à un LLM des informations complémentaires dont il pourrait avoir besoin pour répondre aux requêtes des utilisateurs, en particulier lorsqu\u0026rsquo;il s\u0026rsquo;agit de données plus récentes ou de contenus privés non accessibles lors de son entraînement.\nUn RAG permet d’utiliser la recherche sémantique. Par exemple, dans la question suivante, l’utilisateur utilise des synonymes des spécialités déclarées en base de données dans le référentiel : radiography (radiographie) pour radiology (radiologue) et odontology (odontologie) pour dentistry (dentiste).\nQuestion : « I\u0026rsquo;m looking for a veterinarian who specializes in both radiography and odontology for my pet »\nA l’aide du RAG, l’application Petclinic retrouve 2 vétérinaires ayant la spécialité de radiology et de dentistry. L’utilisation d’un index inversé Lucene n’aurait pas permis d’arriver à ce résultat.\nPour intégrer le RAG à Petclinic, nous devons procéder en 2 étapes : la phase d’ ingestion (indexation) des vétérinaires et la phase de requêtage (retrieval en anglais). La documentation de LangChain4j sur le support des RAG propose deux diagrammes illustrant les étapes d’ indexation et de retrieval.\nIngestion d’embeddings Afin de pouvoir être utilisées par le LLM, les données des 3 tables vets, specialties et vet_specialties doivent préalablement être ingérées et stockées dans une base de données vectorielle. PostgreSQL avec l\u0026rsquo;extension pgVector est probablement le choix le plus populaire. Greenplum et Qdrant sont 2 autres bases de données vectorielles. LangChain4j supporte plus de 25 bases vectorielles avec des niveaux plus ou moins avancés.\nLors de la phase d’ingestion, les données textuelles des vétérinaires (nom, prénom et spécialités) sont converties en vecteurs multidimensionnels appelés embedding puis stockés dans la base vectorielle. La documentation de LangChain4j parle d’Embedding Stores. Pour notre application d’exemple, par simplicité, nous allons utiliser la base vectorielle en mémoire proposée par LangChain4j. Dans la classe de configuration Spring AssistantConfiguration, commençons par déclarer le bean de type InMemoryEmbeddingStore :\n@Bean InMemoryEmbeddingStore\u0026lt;TextSegment\u0026gt; embeddingStore() { return new InMemoryEmbeddingStore\u0026lt;\u0026gt;(); } Nous devons ensuite choisir un modèle de embedding. LangChain4j en supporte plus de 19. J’ai opté pour un modèle de type in-process basé sur le runtime ONNX. Ce type de modèle présente l’avantage de pouvoir s’exécuter dans la même JVM que celle de Petclinic.\nLe repo git langchain4j-embeddings propose une douzaine d’artefact (JAR) embarquant chacun un modèle au f ormat .onnx. Parmi eux, on retrouve l’artefact langchain4j-embeddings-all-minilm-l6-v2.\nLe modèle all-MiniLM-L6-v2 est un modèle de langage basé sur la famille MiniLM conçue par Microsoft. Entrainé pour la similarité sémantique et les recherches de phrases, ce modèle de 86 Mo est compact et optimisé pour offrir des performances élevées en termes de qualité d\u0026rsquo;encodage de phrases, tout en restant léger et rapide. Il semble parfait pour notre chatbot et la recherche de similarité.\nUne fois le choix du modèle arrêté, ajoutons sa dépendance dans le pom.xml :\n\u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;dev.langchain4j\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;langchain4j-embeddings-all-minilm-l6-v2\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;${langchain4j.version}\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; Dans la classe de configuration Spring AssistantConfiguration, déclarons un bean de type EmbeddingModel :\n@Bean EmbeddingModel embeddingModel() { return new AllMiniLmL6V2EmbeddingModel(); } L’ingestion des données vétérinaires est réalisée en moins d’une seconde au démarrage de l’application Petclinic via la classe EmbeddingStoreInit :\n@Component public class EmbeddingStoreInit { private final Logger logger = LoggerFactory.getLogger(EmbeddingStoreInit.class); private final InMemoryEmbeddingStore\u0026lt;TextSegment\u0026gt; embeddingStore; private final EmbeddingModel embeddingModel; private final VetRepository vetRepository; public EmbeddingStoreInit(InMemoryEmbeddingStore\u0026lt;TextSegment\u0026gt; embeddingStore, EmbeddingModel embeddingModel, VetRepository vetRepository) { this.embeddingStore = embeddingStore; this.embeddingModel = embeddingModel; this.vetRepository = vetRepository; } @EventListener public void loadVetDataToEmbeddingStoreOnStartup(ApplicationStartedEvent event) { Pageable pageable = PageRequest.of(0, Integer.MAX_VALUE); Page\u0026lt;Vet\u0026gt; vetsPage = vetRepository.findAll(pageable); String vetsAsJson = convertListToJson(vetsPage.getContent()); EmbeddingStoreIngestor ingestor = EmbeddingStoreIngestor.builder() .documentSplitter(new DocumentByLineSplitter(1000, 200)) .embeddingModel(embeddingModel) .embeddingStore(embeddingStore) .build(); ingestor.ingest(new Document(vetsAsJson)); } public String convertListToJson(List\u0026lt;Vet\u0026gt; vets) { ObjectMapper objectMapper = new ObjectMapper(); try { // Convert List\u0026lt;Vet\u0026gt; to JSON string StringBuilder jsonArray = new StringBuilder(); for (Vet vet : vets) { String jsonElement = objectMapper.writeValueAsString(vet); jsonArray.append(jsonElement).append(\u0026#34;\\n\u0026#34;); // For use of the // DocumentByLineSplitter } return jsonArray.toString(); } catch (JsonProcessingException e) { logger.error(\u0026#34;Problems encountered when generating JSON from the vets list\u0026#34;, e); return null; } } } La classe EmbeddingStoreInit fait appel au VetRepository pour charger tous vétérinaires de la base, les marshalle en un gros Document JSON puis fait appel à la classe EmbeddingStoreIngestor de LangChain4j. Cet EmbeddingStoreIngestor est configuré avec le modèle d’embedding, la base vectorielle où les embeddings seront stockés et un DocumentByLineSplitter chargé de découper le volumineux document JSON en TextSegment censé améliorer la qualité des recherches de similarité et de réduire la taille et le coût d\u0026rsquo;une invite envoyée au LLM.\nUne fois le EmbeddingStoreIngestor construit, la méthode ingest() est appelée pour ingérer le document. Comme le montre les logs ci-dessous, ce dernier est découpé en 33 segments de texte. Les embeddings sont calculés sur les 33 segments puis stockés dans la base vectorielle :\nEmbeddingStoreIngestor : Starting to ingest 1 documents EmbeddingStoreIngestor : Documents were split into 33 text segments EmbeddingStoreIngestor : Starting to embed 33 text segments EmbeddingStoreIngestor : Finished embedding 33 text segments EmbeddingStoreIngestor : Starting to store 33 text segments into the embedding store EmbeddingStoreIngestor : Finished storing 33 text segments into the embedding store Requêtage des embeddings A présent que l’ensemble des données vétérinaires sont stockées en base vectorielle sous forme d’embeddings, configurons l’application pour que le chatbot utilise ces données lors de son dialogue avec le LLM.\nPour utiliser les fonctionnalités RAG, la classe @AiService Assistant passe par l’interface RetrievalAugmentor et son implémentation par défaut mise à disposition par LangChain4j. Cette interface est chargée d’enrichir le ChatMessage avec des contenus pertinents extraits d’une ou plusieurs sources de données, comme par exemple notre base vectorielle en mémoire. Pour avoir un aperçu des composants manipulés par le RetrievalAugmentor, je vous invite à consulter le schéma du paragraphe Advanced RAG de la documentation de LangChain4j. On y voit l’utilisation d’un ContentRetriever pour interroger une base vectorielle, un moteur de recherche, une base SQL ou bien encore un moteur de recherche.\nDans Petclinic, nous déclarons un bean ContentRetriever de type EmbeddingStoreContentRetriever chargé de récupérer des données vétérinaires dans notre base vectorielle :\n@Bean EmbeddingStoreContentRetriever contentRetriever(InMemoryEmbeddingStore\u0026lt;TextSegment\u0026gt; embeddingStore, EmbeddingModel embeddingModel) { return new EmbeddingStoreContentRetriever(embeddingStore, embeddingModel); } En redémarrant l’application Petclinic puis en posant une question au chatbot, on s’aperçoit que LangChain4j complète le prompt de l’utilisateur en concaténant à la suite de sa question la liste des vétérinaires issus de la base vectorielle et qui se rapprochent sémantiquement de sa question :\n- method: POST - url: https://api.openai.com/v1/chat/completions - headers: [Accept: text/event-stream], [Authorization: Bearer xxx], [User-Agent: langchain4j-openai] - body: { \u0026#34;model\u0026#34; : \u0026#34;gpt-4o\u0026#34;, \u0026#34;messages\u0026#34; : [ { \u0026#34;role\u0026#34; : \u0026#34;system\u0026#34;, \u0026#34;content\u0026#34; : \u0026#34;You are a friendly AI assistant …\u0026#34; }, { \u0026#34;role\u0026#34; : \u0026#34;user\u0026#34;, \u0026#34;content\u0026#34; : \u0026#34;\\\u0026#34;I\u0026#39;m looking for a veterinarian who specializes in both radiography and odontology for my pet \\\u0026#34;\\n\\ content Answer using the following information:\\n{\\\u0026#34;id\\\u0026#34;:158,\\\u0026#34;firstName\\\u0026#34;:\\\u0026#34;Lauren\\\u0026#34;,\\\u0026#34;lastName\\\u0026#34;:\\\u0026#34;Wood\\\u0026#34;,\\\u0026#34;new\\\u0026#34;:false,\\\u0026#34;specialties\\\u0026#34;:[{\\\u0026#34;id\\\u0026#34;:2,\\\u0026#34;name\\\u0026#34;:\\\u0026#34;surgery\\\u0026#34;,\\\u0026#34;new\\\u0026#34;:false}]}\\n{\\\u0026#34;id\\\u0026#34;:159,\\\u0026#34;firstName\\\u0026#34;:\\\u0026#34;Gary\\\u0026#34;,\\\u0026#34;lastName\\\u0026#34;:\\\u0026#34;Coleman\\\u0026#34;,\\\u0026#34;new\\\u0026#34;:false,\\\u0026#34;specialties\\\u0026#34;:[{\\\u0026#34;id\\\u0026#34;:1,\\\u0026#34;name\\\u0026#34;:\\\u0026#34;radiology\\\u0026#34;,\\\u0026#34;new\\\u0026#34;:false},{\\\u0026#34;id\\\u0026#34;:2,\\\u0026#34;name\\\u0026#34;:\\\u0026#34;surgery\\\u0026#34;,\\\u0026#34;new\\\u0026#34;:false}]}\\ …\u0026#34; } ], \u0026#34;temperature\u0026#34; : 0.7, … } Routage de questions Le dernier point présenté dans cet article consiste à utiliser la fonctionnalité Query Router de LangChain4j. Interroger la base vectorielle pour chaque question n’a pas nécessairement d’intérêt. Par exemple pour un simple « Hello » ou une question portant uniquement sur les propriétaires.\nComme son nom le laisse supposer, un Query Router est responsable de router une requête utilisateur vers le ou les ContentRetriever appropriés si nécessaire.\nL’implémentation de l’interface QueryRouter est à la charge du développeur. Pour déterminer si la question d’un utilisateur porte sur les vétérinaires, on aurait pu utiliser une simple recherche de la chaine de caractère « vet ». D’une part, on n’aurait pas supporté le multilingue et, d’autre part, on aurait interrogé la base vectorielle si l’utilisateur nous avait posé une question hors contexte sur, par exemples, les vétérans. Qui mieux qu’un LLM peut déterminer la sémantique de la question ?\nLa classe VetQueryRouter fait un premier appel au LLM pour répondre à la question « Is the following query related to one or more veterinarians of the pet clinic? ». On demande au LLM de répondre par oui ou par non. Sé réponse détermine si l’usage du Embedding Store est nécessaire. Nul besoin ici d’utiliser de streaming.\nclass VetQueryRouter implements QueryRouter { private static final Logger LOGGER = LoggerFactory.getLogger(VetQueryRouter.class); private static final PromptTemplate PROMPT_TEMPLATE = PromptTemplate.from(\u0026#34;\u0026#34;\u0026#34; Is the following query related to one or more veterinarians of the pet clinic? Answer only \u0026#39;yes\u0026#39; or \u0026#39;no\u0026#39;. Query: {{it}} \u0026#34;\u0026#34;\u0026#34;); private final ContentRetriever vetContentRetriever; private final ChatLanguageModel chatLanguageModel; public VetQueryRouter(ChatLanguageModel chatLanguageModel, ContentRetriever vetContentRetriever) { this.chatLanguageModel = chatLanguageModel; this.vetContentRetriever = vetContentRetriever; } @Override public Collection\u0026lt;ContentRetriever\u0026gt; route(Query query) { Prompt prompt = PROMPT_TEMPLATE.apply(query.text()); AiMessage aiMessage = chatLanguageModel.generate(prompt.toUserMessage()).content(); LOGGER.debug(\u0026#34;LLM decided: {}\u0026#34;, aiMessage.text()); if (aiMessage.text().toLowerCase().contains(\u0026#34;yes\u0026#34;)) { return singletonList(vetContentRetriever); } return emptyList(); } } La déclaration du VetQueryRouter au niveau de AssistantConfiguration passe par l’utilisation de la méthode builder de la classe DefaultRetrievalAugmentor :\n@Bean RetrievalAugmentor retrievalAugmentor(ChatLanguageModel chatLanguageModel, ContentRetriever vetContentRetriever) { return DefaultRetrievalAugmentor.builder() .queryRouter(new VetQueryRouter(chatLanguageModel, vetContentRetriever)) .build(); } Petclinic utilisant désormais le ChatLanguageModel et le StreamingChatLanguageModel, le fichier de configuration application.properties doit être complété :\nlangchain4j.open-ai.streaming-chat-model.api-key=${OPENAI_API_KEY} langchain4j.open-ai.streaming-chat-model.model-name=gpt-4o langchain4j.open-ai.streaming-chat-model.log-requests=true langchain4j.open-ai.streaming-chat-model.log-responses=true langchain4j.open-ai.chat-model.api-key=${OPENAI_API_KEY} langchain4j.open-ai.chat-model.model-name=gpt-4o-mini langchain4j.open-ai.chat-model.log-requests=true langchain4j.open-ai.chat-model.log-responses=true Dans les logs applicatifs, un premier appel est désormais envoyé au LLM avant tout autre appel :\n- method: POST - url: https://api.openai.com/v1/chat/completions - headers: [Authorization: Bearer sk-Qw...MA], [User-Agent: langchain4j-openai] - body: { \u0026#34;model\u0026#34; : \u0026#34;gpt-4o-mini\u0026#34;, \u0026#34;messages\u0026#34; : [ { \u0026#34;role\u0026#34; : \u0026#34;user\u0026#34;, \u0026#34;content\u0026#34; : \u0026#34;Is the following query related to one or more veterinarians of the pet clinic?\\nAnswer only \u0026#39;yes\u0026#39; or \u0026#39;no\u0026#39;.\\nQuery: \\\u0026#34;I\u0026#39;m looking for a veterinarian who specializes in both radiography and odontology for my pet \\\u0026#34;\\n\u0026#34; } ], \u0026#34;temperature\u0026#34; : 0.7 } Conclusion Cet article aura montré comment intégrer LangChain4j dans une application de gestion basée sur Spring Boot.\nRécapitulons les principales fonctionnalités de LangChain4j qui ont été mises en œuvre :\nAI Service: définit de manière déclarative l’interface entre notre application Java et un LLM. Memory : permet d’historiser les conversations entre l’utilisateur et le LLM, supporte le multi-utilisateurs et la persistance. System prompt : joue un rôle essentiel dans les LLM car il détermine la manière dont les modèles interprètent les requêtes des utilisateurs et y répondent. Tooling(ou appel de fonction) : permet au LLM d\u0026rsquo;appeler, si nécessaire, une ou plusieurs méthodes Java de l’application. Streaming : réponse au fil de l’eau, token par token, en utilisant côté client le Server-Sent Events. RAG: utilisation d’un embedding store en mémoire pour ingérer les données vétérinaires, faire des recherches de similarité et enrichir le prompt utilisateur en fonction d’une règle de routage. Personnellement, le développement de la version LangChain4j de Spring Petclinic m’aura permis de contribuer modestement au projet Open Source LangChain4j (PR #49, #50, #51 et # 2000).\nJe tiens à remercier mon fils Evan pour son montage de ma video Youtube. Merci également à Antonio Goncalves, Julien Dubois, Guillaume Laforge et Valentin Deleplace pour leurs workshops sur LangChain4j avec Azure OpenAI et Gemini.\nSi vous souhaitez contribuez à votre tour à Spring Petclinic LangChain4j, des issues vous attendent. L’ issue #10 vise notamment à intégrer d’autres LLM que OpenAI et Azure OpenAI. Parmi les candidats potentiels figurent Google Vertex AI Gemini, Ollama ou bien encore Mistral AI. Avis aux amatrices et aux amateurs.\nRessources :\nDocumentation officielle LangChain4j Repository Git spring-petclinic-langchain4j AI Meets Spring Petclinic: Implementing an AI Assistant with Spring AI (Oded Shopen) OpenAI Developer Quickstart) Java client library for OpenAI API Gemini en Java avec Vertex AI et LangChain4j (Google Lab) Create your own ChatGPT with Retrieval-Augmented-Generation (Microsoft) ","link":"https://javaetmoi.com/2024/11/integrer-un-chatbot-dans-une-webapp-java-avec-langchain4j/","section":"posts","tags":["genai","langchain4j","openai","spring-boot"],"title":"Intégrer un Chatbot dans une webapp Java avec LangChain4j"},{"body":"","link":"https://javaetmoi.com/tags/langchain4j/","section":"tags","tags":null,"title":"Langchain4j"},{"body":"","link":"https://javaetmoi.com/tags/openai/","section":"tags","tags":null,"title":"Openai"},{"body":"De Java EE à Jakarta EE En 2017, Oracle a fait don de la spécification Java EE (précédemment connu sous le nom de J2EE) à la fondation Eclipse. Java EE regroupe différentes API utilisées aussi bien par des serveurs d’applications, des containers de servlets et des frameworks comme Quarkus ou Spring : Servlet, JSP, JSF, JPA, JTA, JAX-WS, JAX-RS, JAXB, WebSocket, Bean Validation, CDI, EL \u0026hellip;\nSous l\u0026rsquo;égide d’Eclipse, Java EE a été rebaptisé Jakarta EE. La fondation a récupéré la base de code Java et les TCK. En 2019 est sortie une version Jakarta EE 8 pleinement compatible avec Java EE 8. Comme seul changement notable pour les dév, le groupId des artefacts Maven a été renommé de javax à jakarta. Le patch du numéro de version a été incrémenté. A titre d’exemple, l’artefact jakarta.faces:jakarta.faces-api:2.3.1 est identique à javax.faces:javax.faces-api:2.3. Pas si anodin, ce changement de GAV Maven fait que notre outil de build peut être amené, via le mécanisme de dépendances transitives, à placer dans le classpath deux mêmes artefacts ayant des groupId différents. Les exclusions maven permettent de corriger le tir.\nEn décembre 2020, la communauté Java est secouée par la sortie de Java EE 9. 20 ans de rétrocompatibilité s’écroulent. Oracle a souhaité conserver la marque Java. Les packages javax.* de la spécification Java EE ont été renommés en jakarta.*. Certains sous-packages ont également été renommés. Pour exemple, la classe Marshaller de l’API JAXB change de package : de javax.xml.bind.Marshaller vers jakarta.xml.bind.Marshaller.\nA cette occasion, le numéro de version majeur a été incrémenté. Les coordonnées Maven Jakarta EE 8 de l’API JSF jakarta.faces:jakarta.faces-api:2.3.1 changent en jakarta.faces:jakarta.faces-api:3.0.0 sous Jakarta EE 9.\nA noter que les packages javax du JDK et qui n’appartiennent donc pas à Java EE ne sont pas renommés. On peut citer : javax.sql, javax.swing, javax.naming, javax.transaction.xa et javax.naming.\nCe changement de package Java est on ne peut plus impactant :\nLe code Java non migré ne fonctionne pas avec un container/runtime plus récent Un ancien container/runtime ne fonctionne pas avec du code Java récent migré Ce changement a impacté tout l’écosystème Java : les projets Open Source, le code propriétaire / métier, les IDE, les outils de build \u0026hellip;\nQuatre ans plus tard, la grande majorité des projets Open Source actifs proposent une version de leurs artefacts compatibles jakarta. Les frameworks les plus utilisés comme Quarkus ou Spring étaient attendus par leur communauté et l’ont fait relativement rapidement. Par exemple, Spring Framework 6.0 et Spring Boot 3.0 sont tous les deux sortis en novembre 2022. Pour migrer vers Jakarta EE 9 et le package jakarta, un projet reposant lui-même sur d’autres librairies tierces doit attendre que ses dépendances soient migrées. Cela a créé une certaine inertie dans l’écosystème Java. Par exemple, le framework Apache CXF, qui offre un support pour Spring, a dû attendre la sortie de Spring Framework 6 pour sortir à son tour en décembre 2022 la version CXF 4.\nMigrer des applications legacy Prenons l’exemple d’un SI composé de dizaines d’applications Java qui, pour des questions de sécurité et d’obsolescence, doivent migrer sur un Tomcat 10.\nLes applications les plus modernes, basées sur Spring Boot, Quarkus ou Micronaut, s’appuient en général sur des stacks techniques récentes et actives. Migrer de Spring Boot 2.7 à Spring Boot 3 ne pose pas de difficultés majeures. Armé du guide de migration Spring Boot 3 et d’outils comme la recette OpenRewrite Migrate to Spring Boot 3.0, le projet Spring Boot Migrator (SBM) ou bien encore l\u0026rsquo;IntelliJ IDEA\u0026rsquo;s migration tool, les développeurs sont assistés dans leur travail et trouvent les ressources nécessaires sur le Net.\nA contrario, les applications Java les plus anciennes du SI, pouvant avoir jusqu’à 25 ans, peuvent continuer pour certaines à s’appuyer sur des frameworks et des librairies non maintenus, abandonnés depuis des années par leurs créateurs. Lorsque cela est possible, identifier puis migrer vers une alternative est recommandé. Par exemple, l’équipe projet Dozer invite à migrer vers MapStruct ou ModelMapper et propose même un plugin IntelliJ pour faciliter la tâche.\nQu’en est-il de frameworks plus structurants ? Je pense notamment à de vieux frameworks frontends sur lesquels sont conçus des centaines d’écrans d’applications de gestion.\nPar exemple, Struts 1 n’est pas compatible Jakarta EE 9 et les nouveaux packages en jakarta.* Il s\u0026rsquo;appuie sur l\u0026rsquo;API javax.servlet.http.HttpServlet du package javax.servlet. Le conteneur web Tomcat 10 manipule quant à lui la classe jakarta.servlet.http.HttpServlet. Même chose pour Richfaces abandonné par JBoss depuis 2016.\nMigrer les écrans d’une application de Struts 1vers Struts 6, React ou Angular est envisageable. Le cout en sera nettement plus élevé. Les délais aussi. L’automatisation aura ses limites. Autre solution : utiliser Struts 1 Reloaded dont la version 1.5.0 est compatible Jakarta EE 9. Maintenu par un seul et unique développeur, la base de code a divergé de l’original. Il pourrait y avoir des régressions.\nFaute de budget conséquent, ces applications seraient-elles vouées à rester ad vitam æternam sur du Spring Boot 2 ? Non, la suite de cet article explique comment automatiser la compatibilité jakarta de vieux frameworks et de vielles librairies.\nSolution technique Les développeurs du conteneur Tomcat ont adressé cette problématique lors de la sortie de Tomcat 10. En effet, Tomcat 10 sait convertir une application web existante de Java EE 8 à Jakarta EE 9 au moment du déploiement en utilisant l\u0026rsquo;outil de migration Apache Tomcat pour Jakarta EE.. Pratique, cet outil peut être utilisé en dehors de Tomcat, sous forme d\u0026rsquo;un jar auto-exécutable ou d\u0026rsquo;une tâche Ant. Contrairement à ce que son nom pourrait laisser penser, il n’est pas lié au conteneur Tomcat et pourrait être utilisé pour cibler des versions récentes de Jetty et de Wildfly.\nLe projet tomcat-jakartaee-migration effectue tous les changements nécessaires pour migrer une application de Java EE 8 vers Jakarta EE 9 en renommant chaque package Java EE 8 vers son remplaçant Jakarta EE 9. Cela inclut les références aux packages dans les classes, les constantes de type String, les fichiers de configuration, les JSP, les TLD \u0026hellip;\nTous les packages javax.* ne font pas partie de Java EE. Seuls ceux définis par Java EE sont déplacés vers l\u0026rsquo;espace de noms jakarta.*. Il n\u0026rsquo;est pas nécessaire de migrer les références aux schémas XML. Les schémas ne font pas directement référence aux packages javax et Jakarta EE 9 continuera à supporter l\u0026rsquo;utilisation des schémas de Java EE 8 et antérieurs.\nCet outil propose 2 profils: le profil partiel TOMCAT ciblant les conteneurs web comme Tomcat et Jetty et le profil EE ciblant toutes les dépendances Java EE 8.\nL\u0026rsquo;outil sait parcourir différents types d\u0026rsquo;archives : jar, zip, war \u0026hellip; Via ses converters (TextConverter, ClassConverter, ManifestConvert), il sait également manipuler plusieurs formats de fichiers : les classes compilées contenues dans les JAR comme le code source Java, les fichiers XML, JSON et properties, les pages JSP (jsp, jspxf, jspx), les tags JSP (tag, tld, tagx) \u0026hellip;\nL\u0026rsquo; outil tomcat-jakartaee-migration peut donc aussi bien travailler sur des JAR de librairies tierces que sur du code source métier qu\u0026rsquo;on souhaite migrer vers Jakarta EE 9 et même Jakarta EE 10.\nGuide d\u0026rsquo;utilisation Rendre compatible Jakarta EE 9 des librairies tierces puis les utiliser dans le code métier se fait en 2 étapes :\nEtape 1 : migrer les librairies tierces 1. Récupérer le binaire depuis la page https://tomcat.apache.org/download-migration.cgi\n2. Exécuter la ligne de commande suivante (exemple avec jsf-api-1.2\\_14.jar) :\nset MIGRATION_TOOL=C:\\dev\\jakartaee-migration\\lib\\jakartaee-migration-1.0.8.jar set M2_REPO=C:\\dev\\maven\\repository java -jar %MIGRATION_TOOL% -profile=EE %M2_REPO%\\javax\\faces\\jsf-api\\1.2_14\\jsf-api-1.2_14.jar %M2_REPO%\\javax\\faces\\jsf-api\\1.2_14-jakarta\\jsf-api-1.2_14-jakarta.jar Le fichier JAR jsf-api-1.2\\_14-jakarta.jar généré est désormais compatible Jakarta EE 9.\nExtrait de la classe FacesServlet :\njsf-api-1.2_14.jar compatible Java EE 8 jsf-api-1.2_14-jakarta.jar migré à Jakarta EE 9 package javax.faces.webapp;\nimport javax.faces.FacesException;\nimport javax.faces.FactoryFinder;\nimport javax.faces.context.FacesContext;\nimport javax.faces.context.FacesContextFactory;\nimport javax.faces.lifecycle.Lifecycle;\nimport javax.faces.lifecycle.LifecycleFactory;\nimport javax.servlet.Servlet;\nimport javax.servlet.ServletConfig;\nimport javax.servlet.ServletException;\nimport javax.servlet.ServletRequest;\nimport javax.servlet.ServletResponse;\nimport javax.servlet.UnavailableException;\nimport javax.servlet.http.HttpServletRequest;\nimport javax.servlet.http.HttpServletResponse;\nimport java.io.IOException;\nimport java.util.ResourceBundle;\nimport java.util.logging.Level;\nimport java.util.logging.Logger;\npublic final class FacesServlet implements Servlet { package jakarta.faces.webapp;\nimport jakarta.faces.FacesException;\nimport jakarta.faces.FactoryFinder;\nimport jakarta.faces.context.FacesContext;\nimport jakarta.faces.context.FacesContextFactory;\nimport jakarta.faces.lifecycle.Lifecycle;\nimport jakarta.faces.lifecycle.LifecycleFactory;\nimport jakarta.servlet.Servlet;\nimport jakarta.servlet.ServletConfig;\nimport jakarta.servlet.ServletException;\nimport jakarta.servlet.ServletRequest;\nimport jakarta.servlet.ServletResponse;\nimport jakarta.servlet.UnavailableException;\nimport jakarta.servlet.http.HttpServletRequest;\nimport jakarta.servlet.http.HttpServletResponse;\nimport java.io.IOException;\nimport java.util.ResourceBundle;\nimport java.util.logging.Level;\nimport java.util.logging.Logger;\npublic final class FacesServlet implements Servlet { 3. Renouveler l\u0026rsquo;opération pour le JAR du code source.\nExemple sur jsf-api-1.2_14-sources.jar :\njava -jar $MIGRATION_TOOL -profile=EE $M2_REPO/javax/faces/jsf-api/1.2_14/jsf-api-1.2_14-sources.jar $M2_REPO/javax/faces/jsf-api/1.2_14-jakarta/jsf-api-1.2_14-jakarta-sources.jar 4. Uploader le JAR et ses sources dans le repository binaire d’entreprise (ex : Artifactory ou Nexus Sonatype). Privilégiez l’ajout du suffixe -jakarta au numéro de version Maven à l’utilisation d’un classifier Maven.\nCette étape de migration peut être complètement automatisée par un pipeline CI Jenkins ou GitLab.\nEtape 2 : utiliser les librairies tierces migrées 1. Comme pré-requis, le code source de l\u0026rsquo;application doit avoir commencé sa migration à Jakarta EE 9 (ou supérieur).\n2. Une fois les différentes librairies et frameworks migrés et uploadés dans le repository d’entreprise, il est possible de les référencer dans les pom.xml de l\u0026rsquo;application\n3. Il est ensuite nécessaire d\u0026rsquo;adapter le code métier utilisant les classes de ces librairies qui ont changé de package, au niveau des imports du code source java, mais également dans les fichiers XML. Exemple du web.xml référençant jakarta.faces.webapp.FacesServlet :\n\u0026lt;servlet\u0026gt; \u0026lt;servlet-name\u0026gt;Faces Servlet\u0026lt;/servlet-name\u0026gt; \u0026lt;servlet-class\u0026gt;jakarta.faces.webapp.FacesServlet\u0026lt;/servlet-class\u0026gt; \u0026lt;load-on-startup\u0026gt;1\u0026lt;/load-on-startup\u0026gt; \u0026lt;/servlet\u0026gt; Pour y arriver, 4 possibilités s\u0026rsquo;offrent à nous :\nChangements manuels par search / replace Appliquer la recette OpenRewrite javaxmigrationtojakarta (ne gère pas le web.xml) Utiliser l’ IntelliJ IDEA\u0026rsquo;s migration tool Utiliser une nouvelle fois l\u0026rsquo;outil tomcat-jakartaee-migration java -jar %MIGRATION_TOOL% -logLevel=FINEST -profile=EE C:\\dev\\project\\my-webapp C:\\dev\\project\\my-webapp-jakarta Cette dernière option est à privilégier. En attente de prise en compte de la PR #60 de mon collègue Marco pour exclure le sous-répertoire .git et utiliser le répertoire source comme cible.\n4. Vérifier que tout compile\nmvn clean install Conclusion Cette solution présente plusieurs avantages :\nSimplicité Cout défiant toute concurrence Réutilisation d\u0026rsquo;un outil Open Source maintenu par l\u0026rsquo;équipe Tomcat et massivement éprouvé Automatisation possible Son principal inconvénient réside dans le fait que l’application continue à utiliser une librairie non maintenue. À moyen termes, trouver un financement pour refondre ou migrer l’application vers une technologie cible reste donc préconisé.\nEnfin, d’autres outils que celui d’Apache existe, par exemple Eclipse Transformer. Avant de vous lancer, comparez-les.\nRessources :\nApache Tomcat migration tool for Jakarta EE (GitHub) Transition from Java EE to Jakarta EE (Oracle Java Magazine) Javax to Jakarta Namespace Ecosystem Progress (Jakarta EE) Using IntelliJ IDEA\u0026rsquo;s migration tool (Jetbrains) Eclipse Transformer (GitHub) Javax to Jakarta Tales From the Crypt (Shawn McKinney) ","link":"https://javaetmoi.com/2024/08/compatibilite-jakarta-ee-9-de-vieux-frameworks/","section":"posts","tags":["javaee","spring-boot","tomcat"],"title":"Compatibilité Jakarta EE 9 de vieux frameworks"},{"body":"","link":"https://javaetmoi.com/tags/javaee/","section":"tags","tags":null,"title":"Javaee"},{"body":"","link":"https://javaetmoi.com/tags/tomcat/","section":"tags","tags":null,"title":"Tomcat"},{"body":"","link":"https://javaetmoi.com/tags/craft/","section":"tags","tags":null,"title":"Craft"},{"body":"Conférence : Devoxx France 2024\nVidéo Youtube : https://www.youtube.com/watch?v=KeM1cjKiMr4\nDate : 18 avril 2024\nSpeakerines : Pauline Rambaud et Anne-Laure de Boissieu (Bedrock Streaming)\nFormat : Conférence (45mn)\nDéjà donnée à plusieurs reprises dans différents meetups et conférences, Pauline et Anne-Laure ont repensé spécialement cette présentation pour Devoxx France. Quel honneur !\nAfin de démontrer à l’assistance qu’un commentaire laissé dans une revue de code peut amener de la confusion, nos deux speakerines commencent leur show en nous montrant une Pull Request sur le repo git de leurs slides reveal : une simple émoticône. Mal interprétée, elle entraine un biais de communication.\nC’est quoi la revue de code ? Développeuses GO, Anne-Laure et Pauline rappellent que la revue de code fait partie intégrante du métier de développeur. Elle consiste à examiner le code écrit par un autre développeur afin d\u0026rsquo;en améliorer la qualité, détecter les bugs et s\u0026rsquo;assurer du respect des normes de codage. Il existe différents types de revue. Au cours de cette présentation, elles se focaliseront sur les revues centrées sur le delta du code écrit pour corriger bug ou implémenter une feature.\nIntérêts d’une revue de code Obtenir une meilleure qualité de code Rechercher et corriger des bugs au plus tôt Partager de la connaissance Partager la responsabilité : si bug, faute de l’équipe et pas du dév Se former en continue : on apprend des autres et on fait grandir l’équipe Il existe d’autres formats que les revues asynchrones via Pull Request : le MOB programming, le Pair Programming ou même pas de revue de code. A chaque équipe de choisir.\nPas de raison apparente de souffrir ? Contrastant avec tous les bénéfices évoqués, les revues peuvent néanmoins faire souffrir auteurs et relecteurs à cause d’ incompréhensions et de maladresses. Dans certaines situations, les commentaires et les retours s’accumulent, on se retrouve submerger et on n’arrive pas à les prendre en compte.\nBonnes pratiques plus efficaces Apportant peu de valeur ajoutée, certaines catégories de commentaires pourraient être détectés et écrits par des robots. Les erreurs de formatage en sont un exemple flagrant. Utiliser un outil comme un linter et un formateur fait gagner du temps aux relecteurs humains. De la même manière, les anomalies évidentes de code et les anti-patterns communs peuvent être détectés automatiquement par des outils d’analyse statique de code comme SonarLint. Ces outils sont * intégrables* à la CI.\nAutre bonne pratique : mettre à disposition de la documentation pour les nouveaux arrivants. Utile pour l’ * onboarding*, elle doit expliquer les pratiques mises en œuvre sur le projet ou dans l’entreprise.\nTemplate de Pull Request Créer un modèle de template pour les Pull Request (PR) permet de faciliter la création d’une demande de changement et donner du contexte aux reviewers. GitHub et GitLab proposent cette fonctionnalité. Elle permet au développeur de se poser les questions essentielles à la bonne délimitation de sa PR. Dans le template, on peut retrouver les questions des Five W\u0026rsquo;s mais également la référence à la User Story implémentée (ex : lien Jira).\nDefinition of Done de la PR Avant de soumettre une Pull Request, une bonne pratique consiste à vérifier **la checklist de Definition of Done des PR **. Voici quelques exemples de points à contrôler :\nTests unitaires écrits Code testé fonctionnement à la main via la UI Code propre exempt de TODO et de code commenté … Vérifier tous ces points fera gagner du temps aux relecteurs et permet d’anticiper d’éventuels retours.\nDefinition of Reviewed Sur le même principe, un relecteur de code peut suivre une checklist :\nJe commente uniquement si besoin (et pas pour le fun) J’envoie tous mes retours en une seule fois (fonctionnalité de « batch comment ») Je vérifie que le code est testé Je teste une fois sur un tenant Pour approfondir le sujet, je vous renvoie à mon billet Check-list revue de code Java.\nConventional comments Certains commentaires sont sujets à interprétation : « Ce n’est pas clair », « Pourquoi tu as fait çà comme çà ? », « Oh my Gosh », « Poubelle », « Je n’aurais pas fait çà comme çà » …\nL’utilisation d’ emoji n’aide pas toujours à en faciliter l’interprétation. Par exemple, d’une génération à l’autre, certaines emojis n’ont pas le même sens.\nLes commentaires interrogatifs sont multi-interprétables. Dans l’exemple « Pourquoi tu as fait çà comme çà ? », on ne sait pas s’il s’agit d’une vraie question visant à progresser ou s’il s’agit d’une moquerie.\nPour se prémunir de ces incompréhensions, il parait nécessaire de standardiser les commentaires de revue de code en utilisant, par exemple, l’approche des Conventional Comments fortement inspiré des Conventional Commits.\nLes conventional comments proposent de formatter ses commentaires de commit en suivant ce formalisme :\n\u0026lt;label\u0026gt; [decorations]: \u0026lt;subject\u0026gt;[discussion] Décorations et discussions sont optionnels.\nL’utilisation de libellés (label) permet de donner son intention :\nLabel Description praise Les éloges soulignent un élément positif. Essayez de laisser au moins un de ces commentaires par revue de code. Ne laissez pas de fausses louanges (qui peuvent en fait être préjudiciables). Cherchez à être sincère. Second degré interdit. nitpick Les « nitpicks » sont des demandes de changements triviaux. Elles ne devraient pas être bloquantes. suggestion Les suggestions proposent des améliorations. Il est important d\u0026rsquo;être explicite et clair sur ce qui est suggéré et pourquoi il s\u0026rsquo;agit d\u0026rsquo;une amélioration. Envisagez d\u0026rsquo;utiliser les décorations bloquantes ou non bloquantes pour mieux communiquer votre intention. issue Les problèmes mettent en évidence des dysfonctionnements du code examiné. Bloquants par nature, ces problèmes peuvent affecter directement la fonctionnalité implémentée ou introduire une régression ailleurs. Il est fortement recommandé d\u0026rsquo;associer un problème à une suggestion de correction. Si vous n\u0026rsquo;êtes pas certain de l\u0026rsquo;existence d\u0026rsquo;un problème, posez une question pour clarifier la situation. question Les questions sont pertinentes si vous avez un doute potentiel sur le code, même s\u0026rsquo;il n\u0026rsquo;est pas clair s\u0026rsquo;il s\u0026rsquo;agit d\u0026rsquo;un vrai problème. Demander à l\u0026rsquo;auteur de clarifier certains points ou de mener une investigation plus poussée peut permettre une résolution rapide. thought Des réflexions représentent des idées qui ont émergé lors de l\u0026rsquo;examen du code. Ces commentaires ne bloquent pas l\u0026rsquo;avancement de la Pull Request. Cependant, ils peuvent être très précieux car ils peuvent mener à des initiatives plus ciblées ou à des opportunités de mentorat. chore Les corvées représentent des tâches simples qui doivent être effectuées avant que la Pull Request puisse être \u0026ldquo;officiellement\u0026rdquo; acceptée. En général, ces commentaires font référence à un processus standard. Essayez de fournir un lien vers la description du processus pour que l\u0026rsquo;auteur puisse savoir comment résoudre la corvée. Pauline et Anne-Laure nous invitent à sélectionner un sous-ensemble de ces libellés et même à les personnaliser. A titre d’exemple, elles-mêmes utilisent applause à la place de praise. Elles n’utilisent pas thought et chore mais ont introduit 2 autres notations : todo et typo. Pour standardiser les revues et faciliter la vue des nouveaux arrivants, elles mettent à disposition de leurs équipes un markdown avec les libellés et les emojis associés :\nLes commentaires conventionnels de commit permettent de moins souffrir grâce à :\nUne compréhension facilitée Une meilleure lisibilité grâce aux labels Moins de mauvaises impressions sur le ton employé Moins de perte de temps Ils permettent d’éviter ce type de commentaire inutile : « On pourrait être plus efficace »\nAfin d’éviter de spammer la Pull Request, lorsqu’une même erreur de typographie est détectée plusieurs fois, Anne-Laure et Pauline recommandent de créer un seul commentaire référençant toutes les lignes où l’erreur apparait.\nPour terminer sur les commentaires de PR, voici quelques bonnes pratiques :\nLe mentoring paie : cela fait progresser les dévs et l’équipe récoltera le fruit de ce travail Laisser des commentaires exploitables fait gagner en efficacité Combiner les commentaires similaires Utiliser le « Nous » plutôt que le « Tu » Remplacer le « Nous devons » par « Nous pourrions » Toute grande organisation dispose de ses propres règles de revue de code. C’est par exemple le cas de Google qui les publie sur GitHub : Google Engineering Practices Documentation.\nL’attitude avant tout Dans la catégorie du « commentaire difficile à exploiter », on retrouve :\n« Je t’ai déjà dit 2 fois qu’il ne fallait pas faire ça comme çà » « Ce n’est pas très élégant » « Beuh » Non aidant, ces commentaires questionnent sur l’attitude qu’un relecteur doit adopter.\nUne attitude bienveillante et constructive est un pré-requis à la communication asynchrone.\nThéorisée en 1971 par Gerald Weinbert, la Programmation sans ego (ergoless programming) reste d’actualités. Elle vise à ce que notre égo ait le moins d’impact. On minimise les facteurs personnels et on émet des critiques respectueuses.\nQuelques principes clés de Gerald Weinbert :\nComprenez et acceptez que vous faites des erreurs. C’est formateur. Traitez les autres comme vous aimeriez qu’on vous traite. Vous n’êtes pas votre code. Se détacher de son code permet d’accepter des critiques sur le code. Ne réécrivez pas le code de l’autre sans consultation : vous n’aiderez pas le dév qui l’a écrit, et cela est très frustrant de voir son code réécrit. Traiter les personnes avec respect, considération et patience. Le risque est que les personnes ne fassent pas de retour et n’osent pas s’exprimer. Battez-vous pour ce en quoi vous croyez, mais acceptez la défaite avec grâce. Lorsque votre solution / proposition est rejetée par l’équipe, ne pas prendre la mouche. Se dire que sa solution était bonne, mais que l’équipe a décidé d’en prendre une autre. Egoless ne veut pas dire sans égo. Critiquez le code, pas les personnes Que retenir ? Tenter de moins laisser notre ego nous contrôler Faisons preuve d’empathie C’est en groupe qu’on produit les meilleures choses Dans certaines équipes, personne ne fait de revue de code. La revue de code est un muscle. Il faut s’entrainer et le faire travailler. Alors lancez-vous et entrainez-vous !\nLes débutants ne savent peut-être pas toujours comment s’y prendre, d’autant plus lorsqu’ils sont amenés à revoir du code de développeurs plus senior. Pour les aider, il faut absolument éviter de réécrire le code à leur place, laisser des commentaires plus constructifs et faire part d’empathie (on a tous été junior).\nPratiques à essayer :\nOrganiser un vis ma vie : permet de voir comment le tech lead fait ses retours de PR Animer un MOB programming autour des revues Proposer une méthode et découper le travail ensemble Expliquer et discuter ensemble Accepter le temps passé à relire Après toutes ces recommandations, arriverez-vous à la conclusion qu’on ne peut plus rien dire dans une revue ? Et bien non ! Les feedbacks négatifs sont nécessaires pour :\nAméliorer la qualité du code Garantir sa maintenabilité Prévenir les risques de bug Lorsque aucun label n’est associé à un commentaire, on peut se demander si ce commentaire va être exploitable ? Peut-être que ce commentaire n’a pas lieu d’être, du moins à l’écrit. En cas de conflits, parlons-en ailleurs que dans une PR. La PR ne doit pas être le lieu de gestion des conflits.\nNe restons pas en souffrance. Lorsqu’un dév souffre, il n’est souvent pas le seul. Il est nécessaire d’en discuter avec d’autres dévs. Améliorer la revue de code, c’est avant tout se poser des questions et améliorer l’équipe\n","link":"https://javaetmoi.com/2024/05/revue-de-code-on-nest-pas-venu-ici-pour-souffrir/","section":"posts","tags":["craft","devoxx"],"title":"Revue de code, on n’est pas venu-e-s ici pour souffrir !"},{"body":"","link":"https://javaetmoi.com/tags/arc42/","section":"tags","tags":null,"title":"Arc42"},{"body":"","link":"https://javaetmoi.com/tags/asciidoc/","section":"tags","tags":null,"title":"Asciidoc"},{"body":"","link":"https://javaetmoi.com/tags/c4/","section":"tags","tags":null,"title":"C4"},{"body":"","link":"https://javaetmoi.com/tags/compodoc/","section":"tags","tags":null,"title":"Compodoc"},{"body":"","link":"https://javaetmoi.com/tags/diataxis/","section":"tags","tags":null,"title":"Diataxis"},{"body":"","link":"https://javaetmoi.com/tags/k8sviz/","section":"tags","tags":null,"title":"K8sviz"},{"body":"Conférence : Devoxx France 2024\nDate : 17 avril 2024\nSpeakers : Damien Lucas (OnePoint)\nFormat : Conférence (45 mn)\nSlides : https://dlucasd.github.io/la-doc-va-bien-ne-t-en-fais-pas/devoxx/#/\nVidéo Youtube : https://www.youtube.com/watch?v=zQ0A75HqFuA\nRepo GitHub : https://github.com/dlucasd/la-doc-va-bien-ne-t-en-fais-pas\nLa documentation, sujet atemporel. Travaillant sur des projets en TMA, Damien faisait le constat suivant : d’un projet à l’autre, la structure, l’organisation et le niveau d’informations de la documentation diffèrent. De temps à autres, Damien assistait à des réunions visant à restructurer la documentation. Chaque participant a sa vision. Trouver un consensus n’est pas facile.\nDamien s’est ainsi demandé s’il n’existait pas clé en main un template de rédaction de la documentation, si possible Open Source et reconnu par la communauté des dévs et architectes.\nAu cours de ses recherches, il est tombé sur le framework arc42 créé en 2005 par 2 allemands : Gernot Starke et Peter Hruschka. Ce template se focalise sur l’ architecture des logiciels et des systèmes. Plusieurs formats sources sont possibles en téléchargement depuis la page https://arc42.org/download : asciidoc, markdown, latex, Word, Confluence, html, Doxygen, IBM Rhapsody … Voici par exemple le template arc42 pour Word : arc42-template-FR-withhelp-docx.zip\nDamien a une préférence pour l’asciidoc qui permet d’avoir une approche docs-as-code : on peut le commiter dans un repository git puis générer un document au format souhaité (ex : PDF)\nLes templates arc42 au format asciidoc (extension .adoc) sont disponibles sur le repo GitHub arc42-template : une dizaine de langues est supportée dont le français.\nCe template nous guide et nous pose les bonnes questions :\nContenu : que faut-il documenter ? Motivation : pourquoi documenter et pour qui ? Représentation : comment documenter ? Faut-il préférer un diagramme ou une liste à puce ? Arc42 propose de documenter une application en 12 chapitres. Chaque chapitre est lui-même généralement composé de 3 sous-parties.\nDans de ce talk, Damien s’appuie sur un projet fictif pour illustrer chacun des 12 chapitres. Ce projet consiste à développer une application de billetterie pour les JO. Il en profitera pour nous présenter des outils de génération de diagrammes (PlantUML et Mermaid), des outils de modélisation (C4 et Structurizr) et des outils de génération de documentation (avec CLI et donc intégrable à la CI).\n1. Introduction et objectifs Vue haut niveau du projet décrivant les grandes fonctionnalités. Exemple : permet acheter place + télécharger billet Il est recommandé d’ajouter des liens vers la documentation existante. Exemple : vers les maquettes, les spécifications fonctionnelles\nDans le projet fictif, OpenAPI est utilisé pour documenter les API REST. L’outil OpenAPI Generator permet de générer la documentation AsciiDoc. L’équipe aurait pu également faire le choix de générer un site statique.\nPour documenter une API, Julien préfère l’outil ReDoc: plus moderne, l’interface est plus pratique. Preuve en est, elle est utilisée nativement dans IntelliJ lors de l’édition d’une spécification OpenAPI.\nCes 2 outils exploitent les informations du contrat d’interface. Julien rappelle la nécessité de documenter au maximum le contrat d’interface.\nLorsque l’approche API First n’est pas utilisée sur un projet, il reste possible de générer le contrat à partir du code avec des outils comme Springdoc.\n2. Contraintes d’architecture Lors de la création d’un système, les décisions et la liberté des architectes sont guidées par les contraintes qu’ils doivent respecter : contraintes techniques, contraintes organisationnelles, contraintes politiques et conventions (de code, de nommage, de comit).\nExemples : la webapp doit être compatible Firefox, Edge et Chrome\nL’application doit être déployée sur un serveur Tomcat OnPremise\n3. Contexte et périmètre Le contexte et le périmètre délimitent le système de tous les systèmes connexes voisins. Les interfaces fonctionnelles et techniques avec les partenaires sont décrites dans ce paragraphe. Un diagramme de contexte a toute sa place. Traditionnellement, les outils Visio ou draw.io peuvent être utilisés pour réaliser de tels diagrammes. Vous vous en doutez, Damien recommande de privilégier les diagrammes as code afin de faciliter les revues et préconise C4 model, créé par Simon Brown entre 2006 et 2011 et inspiré de UML.\nLa modélisation C4 documente l\u0026rsquo;architecture d\u0026rsquo;un système logiciel en utilisant plusieurs points de vue et avec 4 niveaux d’abstraction. Nous y reviendrons.\nExemple de diagramme de contexte conçu à l’aide de C4-PlantUML :\n4. Stratégie de solution Résumé et explication des décisions fondamentales et des stratégies de solution qui régissent l’architecture du système.\nExemple : un fort trafic sur le site lors de l’ouverture de la billetterie a suscité la mise en œuvre d’une solution de file d’attente à l’achat. On ajoute dans la documentation d’un lien vers l’étude menée.\nOn peut utiliser ici un simple tableau :\n5. Vue en boites Décomposition du système en boîte avec différents niveaux d’abstraction. On retrouve ici les 4 niveaux du modèle C4 :\nLevel 1 : Context Level 2 : Containers Level 3 : Components Level 4 : Code (diagramme de classes) Depuis la vue Context, on peut zoomer dans l’une des vues Container, et ainsi de suite.\nPar expérience, Damien nous explique que le niveau 4 est peu utilisé.\nLa problématique n°1 de C4 est la redondance d’informations. On peut retrouver le même système externe dans chacun des fichiers représentants les 4 niveaux.\nLa problématique n°2 de C4 est provoquée par l’auto-layout des outils lorsque le nombre de boites devient conséquent.\nCréé par Simon Brown comme C4, l’outil Structurizr permet de créer des diagrammes clairs et informatifs qui illustrent la structure d\u0026rsquo;un système logiciel. Il s’appuie sur le domain specific language Structurizr DSL supporté dans VS Code et IntelliJ :\nLes diagrammes Structurizr sont dynamiques et permettent de zoomer. Les avantages :\nplus de redondance d’information car, dans l’exemple ci-dessus extrait su site officiel, le softwareSystem est réutilisée permet de générer tous les flux entrants sur la base de données export vers de nombres formats comme plantuml, marmaid et c4 Pour documenter les applications Angular, l’outil Compodoc créé par Vincent Ogloblinsky nous est conseillé. Compodoc permet de zoomer sur des modules Angular, de représenter les routes et s’appuie sur la JSDoc.\nPour React et Vue.js, Damien n’a pas trouvé d’autres outils que Storybook.\n6. Vue exécution La vue exécution décrit le comportement concret des briques du système à travers des scénarios.\nOn y retrouve différents types de diagrammes UML : activités, flux, séquences …\nExemple d’un diagramme de séquence généré avec Mermaid:\nMairmaid est très bien intégré dans GitLab et GitHub qui savent nativement afficher le rendu graphique des diagrammes, contrairement à PlantUML qui nécessite l’installation de GraphViz.\n7. Vue déploiement La vue déploiement décrit l’infrastructure technique utilisée pour exécuter le système ainsi que la correspondance entre les briques logicielles et les éléments d’infrastructure.\nOn y retrouve bien évidemment des diagrammes de déploiement sur les environnements avec des informations de type infrastructure : VM, type de base de données, version de RHEL …\nLorsque l’application est déployée dans Kubernetes, il est possible d’utiliser k8sviz et KubeView.\nPour Docker Compose, il existe des outils comme docker-compose-diagram.\n8. Concepts transversaux Ce chapitre est généralement le plus conséquent. Il décrit les règles principales et les idées de solutions pertinentes du système. On y retrouve des informations sur le packaging, le mécanisme de déploiement, le pattern d’architecture employé (ex : n-tiers vs hexagonale), observabilité, le modèl de données …\nL’outil SchemaSpy permet de générer un MPD à partir d’une URL de connexion. Il s’appuie notamment sur les commentaires SQL ajoutées sur les tables, les colonnes et les contraintes.\nUne de ses fonctionnalités pratiques est de pouvoir zoomer sur une table en affichant ses relations de niveau 1 ou 2 puis d’en fait un export en images.\nExemple : zoom sur la table Track et ses relations au 2ième degré\n9. Décisions d’architecture Ce chapitre consigne les décisions d’architectures significatives. On retrouve une certaine similarité avec les Architecture Decision Records (ADR).\nCela permet de retracer les décisions prises au cours du temps. Exemple : pourquoi avoir utilisé telle base de données NoSQL ? Y sont décrits le contexte et le problème constatés, la décision et le statut (proposé, accepté, rejeté, obsolète …).\n10. Critères de qualité Ce paragraphe contient toutes les exigences de qualité sous la forme d’un arbre de qualité avec des scénarios et des cas d’utilisation concret.\nExemple sur l’exigence de performance : gestion d’au moins 10 000 transactions simultanées lors de la vente de billets tout en répondant aux utilisateurs en moins d’une seconde.\nD\u0026rsquo;expérience, travaillant sur des projets en TMA, Damien utilise peu cette partie.\n11. Risques et dettes techniques Cette section liste les risques et les dettes techniques identifiés, classés par ordre de priorité.\nExemple de risque : mise à l’échelle insuffisante pour gérer le trafic.\nExemple de dette technique : absence de tests automatisés pour le processus d’achat.\n12. Glossaire Tableau listant les termes techniques et métier les plus importants.\nDans le cadre du Domain-Driven Design (DDD) et de l\u0026rsquo; Ubiquitous Language, un glossaire joue un rôle crucial en centralisant et en formalisant la terminologie propre au domaine métier. Un glossaire permet également de maintenir une traduction de référence (ex : anglais \u0026lt;-\u0026gt; français).\nDamien cite les annotations Java de Cyrille Martraire permettant d’extraire le glossaire et dont je vous avais déjà parlé en 2016.\nBilan de arc42 Sur les 12 chapitres, le contenu de la moitié peut été généré. Cette documentation permet de comprendre l’application sans lire la moindre ligne de code. A priori, se lancer dans la documentation peut faire peur, mais Damien insiste qu’en une semaine on peut avoir un squelette de documentation tout à fait satisfait et qu’il sera possible de l’enrichir par incrément. Le format asciidoc est très flexible puisqu’il permet de générer un site statique, un PDF et/ou une publication dans Confluence.\nDocumentation utilisateur La documentation utilisateur n’est pas abordé dans arc42.\nDamien nous recommande le framework Diataxis qui encourage à découper la documentation en 4 parties :\nTutoriel avec le Hello World How to: listes d’étapes avec but bien précis Concepts : comment fonctionne l\u0026rsquo;outil Documentation de référence La documentation de Quarkus s’appuie désormais sur le concept Diataxis.\nConclusion Pour conclure, voici les bénénéfices d’utilisation du template :\nHomogénéité d’un projet à l’autre, ce qui permet de s’y retrouver Le template sert de guide. Il est très riche Moins de charge mentale lors de la rédaction de la documentation : on se laisse guider ","link":"https://javaetmoi.com/2024/04/la-doc-va-bien-ne-ten-fais-pas/","section":"posts","tags":["arc42","asciidoc","c4","compodoc","devoxx","diataxis","k8sviz","mermaid","openapi","plantuml","redoc","spingdoc","structurizr"],"title":"La doc va bien, ne t’en fais pas"},{"body":"","link":"https://javaetmoi.com/tags/mermaid/","section":"tags","tags":null,"title":"Mermaid"},{"body":"","link":"https://javaetmoi.com/tags/openapi/","section":"tags","tags":null,"title":"Openapi"},{"body":"","link":"https://javaetmoi.com/tags/plantuml/","section":"tags","tags":null,"title":"Plantuml"},{"body":"","link":"https://javaetmoi.com/tags/redoc/","section":"tags","tags":null,"title":"Redoc"},{"body":"","link":"https://javaetmoi.com/tags/spingdoc/","section":"tags","tags":null,"title":"Spingdoc"},{"body":"","link":"https://javaetmoi.com/tags/structurizr/","section":"tags","tags":null,"title":"Structurizr"},{"body":"","link":"https://javaetmoi.com/tags/ddd/","section":"tags","tags":null,"title":"Ddd"},{"body":"","link":"https://javaetmoi.com/tags/hexagonal/","section":"tags","tags":null,"title":"Hexagonal"},{"body":"Le live coding qui rendra vos applications plus pérennes Conférence : Devoxx France 2024\nDate : 19 avril 2024\nSpeakers : Julien Topçu (Shodo)\nFormat : Conférence (45mn)\nRepo GitLab : https://gitlab.com/beyondxscratch/hexagonal-architecture-java-springboot\nVidéo Youtube : https://www.youtube.com/watch?v=-dXN8wkN0yk\nCette session de live coding se déroule dans l’univers de Starwars et commence par une citation de Maitre Yoda :\nEn 45mn, Julien doit développer le système Rebels Rescue visant à reconstituer des flottes de sauvetage. N’en déplaise à l’Empire, les technos seront Spring Boot et Java 21.\nA cet effet, il s’appuiera sur l’API publique SWAPI permettant d’accéder à un référentiel de vaisseaux à disposition. L’application sélectionne les vaisseaux qui permettent d’effectuer la mission de sauvetage. Le code source est disponible dans le repo GitLab de Julien : hexagonal-architecture-java-springboot\nJulien commence par rappeler les inconvénients d’une architecture 3-tiers basée sur le triptyque Contrôleur -\u0026gt; Service -\u0026gt; Persistance\nTrès utile, cette architecture n-tiers vieillit mal.\nEn théorie, la logique métier doit être centralisée dans la couche service. Mais en pratique, on la voit diluée partout, jusque dans les procédures stockées \u0026hellip; Autre problème de taille : les responsabilités techniques leaks de tous les côtés. Julien prend un exemple de code GitHub avec une classe Exercice mélangeant annotations JPA (ex: @Column) liée à la persistance de données et des annotations Jackson (ex : @JsonProperty) liées à la couche de présentation. Le couplage est évident. Le code métier casse si on migre d’une base relationnelle à MongoDB ou bien d’une API REST à GraphQL. Un upgrade de la version de Spring Boot ne devrait pas casser le code fonctionnel. C’est trop malheureusement le cas avec ce type d’architecture.\nLes applications legacy sont souvent construites sur des stacks très vieilles. Opérationnelles, elles font tourner le business. Leur couplage à de vieux frameworks comme Servlets et EJB les rend particulièrement difficile à migrer vers Spring Boot ou Quarkus.\nLe couplage amène de la fragilité.\nA contrario, l\u0026rsquo;architecture hexagonale sacralise ce qui apporte de la valeur métier. Elle permet de faire des tests sur le métier. Le code métier est placé dans le Domain. À première vue, le Domain ressemble un peu au Service d’avant. A ceci près que le Domain doit être agnostique. On inverse la dépendance pour que la Persistence dépende du Domain et non l’inverse.\nLe Domain ne doit pas dépendre de frameworks. Pour ne pas réinventer la roue, on peut toutefois y mettre quelques librairies.\nA titre d’exemple, le Domain de Rebels Rescue ne contient que 2 dépendances de test : junit-jupiter et assertj-core. Pour éviter qu’un développeur ne vienne enfreindre cette règle, Julien s’appuie sur le plugin maven-enforcer-plugin:\n\u0026lt;plugin\u0026gt; \u0026lt;artifactId\u0026gt;maven-enforcer-plugin\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;3.0.0\u0026lt;/version\u0026gt; \u0026lt;executions\u0026gt; \u0026lt;execution\u0026gt; \u0026lt;goals\u0026gt; \u0026lt;goal\u0026gt;enforce\u0026lt;/goal\u0026gt; \u0026lt;/goals\u0026gt; \u0026lt;configuration\u0026gt; \u0026lt;rules\u0026gt; \u0026lt;bannedDependencies\u0026gt; \u0026lt;excludes\u0026gt; \u0026lt;!-- forbids non domain dependencies --\u0026gt; \u0026lt;exclude\u0026gt;*\u0026lt;/exclude\u0026gt; \u0026lt;/excludes\u0026gt; \u0026lt;includes\u0026gt; \u0026lt;include\u0026gt;*:*:*:*:test\u0026lt;/include\u0026gt; \u0026lt;/includes\u0026gt; \u0026lt;/bannedDependencies\u0026gt; \u0026lt;/rules\u0026gt; \u0026lt;/configuration\u0026gt; \u0026lt;/execution\u0026gt; \u0026lt;/executions\u0026gt; \u0026lt;/plugin\u0026gt; Le maven-enforcer-plugin bannit toutes les librairies qui ne sont pas en scope test. L’usage d\u0026rsquo;ArchUnit pourrait s’envisager avec, j\u0026rsquo;imagine, des règles sur les imports de packages (JDK et domain).\nDans une architecture hexagonale, l’extérieur du Domain est appelé l’infrastructure.\nLes interfaces d’entrée et de sorties du domaine sont rangées au niveau des frontières nommées API et SPI : - Application Programming Interface Java à ne pas confondre avec API REST. - Service Providing Interface : le SPI ne dépend que des objets du domaine. De cette manière, les annotations ORM ne polluent pas le Domaine. La structuration de la base de données n’a pas d’impact sur la modélisation du domain métier. Dans la littérature, l’archi hexagonale est également appelée Ports and Adapters.\nLorsqu’on développe sur une application architecturée en hexagone, on commence par implémenter le Domain. Dans notre exemple, on retrouve les deux classes : StarShip et Fleet.\npublic record Fleet (UUID id, List\u0026lt;StarShip\u0026gt; starships){ public Fleet(List\u0026lt;StarShip\u0026gt; starships) { this(UUID.randomUUID(),starships); } }public record StarShip(String name, int passengersCapacity, BigDecimal cargoCapacity) { } Ce n’était pas une pratique courante il y’a 10 ans, mais en 2024, Julien recommande de créer un test fonctionnel permettant de vérifier le contrat d’entrée dans le domaine. À cet effet, la méthode should_assemble_a_fleet_for_1050_passengers de la classe AssembleAFleetFunctionalTest assemble une flotte de 1050 passagers.\n// When Fleet fleet = assembleAFleet.forPassengers(numberOfPassengers); L’implémentation du test nécessite de créer l’interface AssembleAFleet :\npublic interface AssembleAFleet { Fleet forPassengers(int numberOfPassengers); } Point d’entrée dans le Domain, cette interface doit être rangée dans le package api. Si le nommage vous gêne, le package api peut être nommé features. La classe FleetAssembler implémente l’interface AssembleAFleet. À noter : l’interface prend le nom d’une commande (verbe au présent), l’implémentation un nom commun.\nclass FleetAssembler implements AssembleAFleet {@Override public Fleet forPassengers(int numberOfPassengers) { List\u0026lt;StarShip\u0026gt; starShips = getStarShipsHavingPassengersCapacity(); List\u0026lt;StarShip\u0026gt; rescueStarShips = selectStarShips(numberOfPassengers, starShips); return fleets.save(new Fleet(rescueStarShips)); } L’implémentation de la méthode getStarShipsHavingPassengersCapacity() demande d’introduire l’interface StartSheepInventory contenant l’unique méthode starShips(). Cette interface est ajoutée au package spi pour aller chercher l’inventaire par appel d’une API externe.\nprivate List\u0026lt;StarShip\u0026gt; getStarShipsHavingPassengersCapacity() { return starshipsInventory.starShips().stream() .filter(starShip -\u0026gt; starShip.passengersCapacity() \u0026gt; 0) .sorted(comparingInt(StarShip::passengersCapacity)) .collect(Collectors.toCollection(ArrayList::new)); }public interface StarShipInventory { List\u0026lt;StarShip\u0026gt; starShips(); } Dans la classe FleetAssembler, l\u0026rsquo;inventory StarShipInventory est injecté par constructeur :\nprivate final StarShipInventory starshipsInventory;public FleetAssembler(StarShipInventory starShipsInventory) { this.starshipsInventory = starShipsInventory; } L’écriture du TU nécessite une instance de FleetAssembler. Pour simuler l’extérieur, on un stub. Technique intéressante : Julien se passe ici de Mockito et crée le stub à l’aide d’une fonction lambda :\n//Given var starShips = asList( new StarShip(\u0026#34;no-passenger-ship\u0026#34;, 0, ZERO), new StarShip(\u0026#34;xs\u0026#34;, 10, new BigDecimal(\u0026#34;1000\u0026#34;)), new StarShip(\u0026#34;s\u0026#34;, 50, new BigDecimal(\u0026#34;50000\u0026#34;)), new StarShip(\u0026#34;m\u0026#34;, 200, new BigDecimal(\u0026#34;70000\u0026#34;)), new StarShip(\u0026#34;l\u0026#34;, 800, new BigDecimal(\u0026#34;150000\u0026#34;)), new StarShip(\u0026#34;xl\u0026#34;, 2000, new BigDecimal(\u0026#34;500000\u0026#34;)));StarShipInventory starShipsInventory = () -\u0026gt; starShips; Le Domain de Rebels Rescue est prêt.\nJulien poursuit par l’implémentation d’un contrôleur REST exposant cette logique sur le réseau. Un module Maven dédié à l’infrastructure est créé. Ce module infrastructure dépend du module domain.\nOn retrouve un contrôleur Rest Spring MVC RescueFleetController exposant un endpoint POST /rescueFleets :\n@PostMapping public ResponseEntity\u0026lt;FleetResource\u0026gt; assembleAFleet(@RequestBody RescueFleetRequest rescueFleetRequest){ var fleet = assembleAFleet.forPassengers(rescueFleetRequest.numberOfPassengers); return created(fromMethodCall(on(this.getClass()).getFleetById(fleet.id())).build().toUri()) .body(new FleetResource(fleet)); } L’IDE ne trouve pas d’instance de bean AssembleAFleet. Ce qui est normal car le Domain n’a pas de dépendance vers Spring et la classe FleetAssembler ne peut donc pas être annotée par l’annotation @Component de Spring. Pour résoudre cette problématique, certains développeurs utilisent des fabriques de bean. Julien n’est pas fan et préfère l’usage du component scan. Il fait en sorte que ce soit Spring qui connaisse notre domaine et non l’inverse. Pour cela, il introduit les DEUX annotations customs @DomainService et @Stub qu’il place dans un package ddd.\n/** * \u0026lt;p\u0026gt; * A Domain Service, i.e. a feature that belongs to the domain and the * ubiquitous language. * \u0026lt;/p\u0026gt; * * @see \u0026lt;a href= * \u0026#34;https://www.domainlanguage.comwp-content/uploads/2016/05/DDD_Reference_2015-03.pdf\u0026#34;\u0026gt;Domain-Driven Design Reference\u0026lt;/a\u0026gt; */ @Retention(RetentionPolicy.RUNTIME) public @interface DomainService {} Cette annotation permet de documenter les classes en faisant référence au document Domain-Driven Design Reference d’Eric Evans, le père du DDD.\nDans la couche d’infrastructure, la classe DomainConfiguration configure Spring pour scanner les beans annotés par @DomainService et @Stub\n@Configuration @ComponentScan( basePackageClasses = {Fleet.class}, includeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION, classes = {DomainService.class, Stub.class})}, excludeFilters = {@ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = {StarShipInventoryStub.class})}) public class DomainConfiguration {} Cette approche est intéressante. Une autre approche consiste à intégrer le jeu d’annotations Dependency Injection de la JSR-330 via l’artefact jakarta.inject-api.\nDans un premier temps, pour faire fonctionner l’application, on peut stubber l’implémentation de StarShipInventory en réutilisant le stub du test StarShipInventoryStub qui est donc placé dans le code de prod et annoté avec l’annotation @Stub. L’ajout de ce stub permet de déployer l’application en prod ou en préprod. Julien a exploité cette technique chez Expedia pour gagner du temps avec l’équipe mobile.\nPour terminer l’application, il reste à développer le client Swapi à l’aide de l\u0026rsquo;API StarWars https://swapi.dev/, référentiel sur l’univers de Starwars utilisable par tous.\nLa classe SwapiClient implémente l’interface StarShipInventory. A noter que dans l’infrastructure, on peut utiliser l’annotation Spring @Component. L’API REST de Swapi est paginée. Le domaine ne sera pas pollué par le choix technique de Swapi. De la même manière, les données inutilisées renvoyées Swapi n’auront pas leur place dans le Domain. L’usage de Swapi n’aura pas d’impact sur le Domain : type String de passengers alors que Julien veut un integer dans le Domain, champs nommés différemment \u0026hellip;\nLa classe SwapiClient modélise le modèle de Swapi à l’aide des deux records SwapiReponse et SwapiStarship. Elle utilise le RestTemplate de Spring et convertit le modèle Swapi vers domaine (notion d\u0026rsquo;adaptateurs). Elle traite également les cas particuliers comme les chaines n/a et unknnown renvoyées par l’API Swapi dans le nombre de passengers mais aussi le séparateur de millier. Ces traitements sont gérés dans l’adaptateur.\nL’adaptateur agit comme une anticorruption layer et retire les n/a et unknnown . Il nettoie les données pour avoir un Domain propre.\nPour simuler de vraies réponses de SWAPI dans les TU, la classe de test StarwarsRebelsRescueApplicationTests utilise un serveur de mock basé sur wiremock\nJulien termine sa présentation en introduisant volontairement une régression. Suite à l’ajout du champ cargoCapacity, on renomme capacity en passengersCapacity. On casse les consommateurs de notre API REST. A ce stade de la démo, le contrôleur REST est couplé avec le domaine métier. Les ressources REST sont les objets du domaine. Il y\u0026rsquo;a nécessité de créer une représentation de ce qu’est une ressource REST. Julien introduit le record FleetResource et ajoute un champ deprecation permettant de prévenir les consommateurs du renommage.\nArchitecture finale de l’application Rebels Rescue :\n","link":"https://javaetmoi.com/2024/04/larchitecture-hexagonale-par-la-pratique/","section":"posts","tags":["architecture","ddd","devoxx","hexagonal","spring-boot"],"title":"L’Architecture Hexagonale par la pratique"},{"body":"Conférence : Devoxx France 2024\nDate : 19 avril 2024\nSpeakers : Lucian Precup (Adelean)\nFormat : Tools in action (30 mn)\nLors de cette 12ième édition de Devoxx France, j’ai eu l’agréable surprise de voir 4 anciens collègues animer un talk : Guillaume Darmont sur Java Flight Recorder et Java Mission Control, Florian Boulay sur emacs, Stéphane Landelle sur Netty et Lucian Precup sur Lucene. Avec ce dernier, nous avons mis en œuvre Elasticsearch sur une application métier au cœur du SI d’une grande entreprise. C’était il y’a plus de 10 ans. Expert en moteur de recherche, je me souviens encore Lucian m’expliquer ce qu’est un index inversé.\nLucian commence par sonder son public. Fait notable, dans l’assistance, personne n’utilise un moteur de recherche qui ne serait pas basé sur Apache Lucene, technologie à la base de milliers de moteur de recherche et dont Lucian va nous retracer l’histoire.\nCréé en 2001, Apache Lucene a aujourd’hui plus de 22 ans. C’est un projet Open Source de la fondation Apache toujours maintenu et même très actif. Chose assez rare dans le monde OSS qui voit passer de nombreux projets.\nDéveloppé en Java, Lucian nous explique les raisons de la longévité de Lucene :\nFédère une communauté de passionnés Permet la réalisation de nombreux cas d’utilisation métiers Sur X / Twitter, Lucian suit les contributeurs de Lucene depuis 2010 et voit régulièrement annoncer des améliorations très impressionnantes.\nPendant longtemps, le projet Lucene fut le projet OSS qui a découvert le plus de bug dans la JVM.\nLa dernière amélioration notable de Lucene consiste à bénéficier de l’ API Vector pour améliorer les performances en utilisant les instructions SIMD des processeurs.\nLucian assiste régulièrement à la conférence mondiale sur le search : la Berlin Buzzworlds\nChaque année, Uwe Schindler ou l’un de ses collègues annonce les nouveautés de Lucene\nEn 2031 Lucian parie qu’il y’a aura encore un talk sur Apache Lucene. Rendez-vous tenu !\nLucene a permis de motoriser :\nApache Nutch : a posé les fondations de Hadoop Apache Solr : serveur de recherche entreprise Elasticsearch : moteur de recherche MongoDB Atlas Search : moteur de recherche d’entreprise cloud native basée sur MongoDB OpenSearch : fork d’Elasticsearch 7 créé par Amazon Les solutions Adelean a2 et all. site Lucien continue par présenter les notions fondamentales de Lucene.\nIndex inversé L’index inversé est le concept fondateur de Lucene.\nLucian prend l’exemple de 3 documents à indexer :\nD1 : « tous les secteurs économiques, du commerce au secteur automobile en passant âr me secteur énergétique sont concernés. » D2 « Les immatriculations de voitures neuves en France ont augmenté de 0,9% en données brutes en juillet. » D3 « Ils s’appuient sur des technologies que la France maitrise sur le plan industriel et économique. » Ces phrases sont tokenisées en termes puis stockées dans un index dit inversés. Cet index ressemble à une Map ayant pour clé les termes et pour valeur l’ensemble des éléments référençant le token.\nDes raffinements sont possibles : suppression des majuscules, stemming (gestion du singulier et du pluriel), stopwords (comme les articles) …\nUn index inversé est extrêmement performant, les recherches booléennes très rapides.\nCette toute première version de Lucene serait un bon exercice pour un candidat, un étudiant ou même Chat GPT\nLucene a continué d’évoluer pour supporter l’ auto-complétion avec nGram. Un index est spécialement créé avec toutes les étapes d’une auto-complétion (cet index pourrait être gros et il est donc compressé).\nChose amusante, les premières versions de Lucene ne supportaient pas les nombres.\nIl fallait utiliser des pseudo nombres et utiliser du padding et comparer des chaines de caractère. Exemple : \u0026ldquo;00001\u0026rdquo; \u0026lt; \u0026ldquo;00004\u0026rdquo;\nCette fonctionnalité a été ajoutée suite aux besoins de sites e-commerces.\nS’ensuit de nouvelles fonctionnalités comme les facettes, les facettes imbriquées, les facettes avec intervalles …\nLucene 4 : introduction un stockage en colonne avec les DocValues\nCela permet d’excellentes performances pour les tris ou bien encore les agrégations. Cas d’utilisation très populaires dans les outils de monitoring et d’observabilité.\nLucene sait donc stocker les données en colonne comme le fait la base NoSQL Cassandra.\nVient ensuite le besoin de faire des recherches sur des données spatiales\nCe sujet est complexe si on veut être rapide. Nécessité de structures particulières : Type Geo Point, Geo Shape …\nLucene est devenue une base de données spatiales, peut être l’une des plus performantes d’après Lucian ?\nCes dernières années, l’ IA Générative est arrivée.\nCela a poussé Lucene à introduire de la recherche vectorielle. La vectorisation permet de représenter des documents sous forme de nombres dans un espace mathématique, ce qui facilite leur analyse et leur traitement par des algorithmes. Chaque élément dans ce vecteur représente une caractéristique du document.\nLucene permet de classer des documents (textes ou images) dans une base vectorielle.\nLucene supporte plusieurs centaines de dimensions. Le Vector Search est désormais possible grâce à la représentation vectorielle d’un texte. Les mots « clémentine » et « mandarine » peuvent alors être positionnés au même endroit (plus besoin d’utiliser les synonymes)\nOn retrouve 2 types de vecteur : creux et dense\nLes vecteurs creux sont moins couteux.\nLe vecteur va être ajouté au document transformé, et un algorithme de similarité va être utilisé pour retrouver le document le plus pertinent.\nExemple de « Term expansion » :\nRetrieval Augmented Generation (RAG) Le RAG est la réponse de Lucene à ChatGPT. Lucian rappelle que ChatGPT n’a pas de connaissance en temps réel. Par exemple, la connaissance de Chat GPT 3.5 s’arrête en janvier 2022 ; il ne connait pas Langchain4j qui est trop récent.\nCes LLM sont programmés pour générer des mots, d’où les hallucinations. Ils se forcent à répondre\nUne solution consiste à générer du texte augmenté par récupération.\nLe RAG mélange moteur de recherche et LLM.\nAvec les User Query, on n’utilise plus du fulltext search.\nAtelier RAG time Le Hands-On Lab RAGtime : Discuter avec vos propres données était consacré au buzzword de 2024.\nRessources Pour terminer sur Lucene, voici quelques ressources conseillées par Lucian :\nLe livre AI Powered Search, Trey Grainger, Doug Turnbull, Max Irwin (Manning) Articles du blog de la société Adelean Diving into NLP with the Elastic Stack, Pietro Mele From voice to text, the power of the Open Source ecosystem - return on the OSXP conference, Lucian Precup Understanding the differences between sparse and dense semantic vectors, Pietro Mele NLP in OpenSearch, Pietro Mele ","link":"https://javaetmoi.com/2024/04/apache-lucene-de-l-indexation-textuelle-a-l-ia/","section":"posts","tags":["devoxx","ia","lucene","rag"],"title":"Apache Lucene : de l’indexation textuelle à l’IA"},{"body":"","link":"https://javaetmoi.com/tags/ia/","section":"tags","tags":null,"title":"Ia"},{"body":"","link":"https://javaetmoi.com/tags/lucene/","section":"tags","tags":null,"title":"Lucene"},{"body":"","link":"https://javaetmoi.com/tags/rag/","section":"tags","tags":null,"title":"Rag"},{"body":"","link":"https://javaetmoi.com/tags/cassandra/","section":"tags","tags":null,"title":"Cassandra"},{"body":"","link":"https://javaetmoi.com/tags/embedding/","section":"tags","tags":null,"title":"Embedding"},{"body":"","link":"https://javaetmoi.com/tags/gemini/","section":"tags","tags":null,"title":"Gemini"},{"body":"","link":"https://javaetmoi.com/tags/gemma/","section":"tags","tags":null,"title":"Gemma"},{"body":"","link":"https://javaetmoi.com/tags/llm/","section":"tags","tags":null,"title":"Llm"},{"body":"","link":"https://javaetmoi.com/tags/ollama/","section":"tags","tags":null,"title":"Ollama"},{"body":"","link":"https://javaetmoi.com/tags/spring-ai/","section":"tags","tags":null,"title":"Spring-Ai"},{"body":"Au-delà des simples chatbots Conférence : Devoxx France 2024\nDate : 17 avril 2024\nSpeakers : Abdellfetah Sghiouar (Google), Cédrick Lunven (DataStax)\nFormat : Deep Dive (3h)\nSlides : https://github.com/datastaxdevs/conference-2024-devoxx-france/blob/main/slides.pdf\nVidéo Youtube : https://www.youtube.com/watch?v=6n8JysFyVA8\nRepo GitHub : https://github.com/datastaxdevs/conference-2024-devoxx-france\nDans ce Deep Dive de 3h (anciennement nommé Université à Devoxx France), Abdellfetah Sghiouar et Cédrick Lunven nous expliquent comment intégrer l\u0026rsquo;intelligence artificielle générative (la fameuse GenAI) dans nos applications Java, et ceci sans expertise en machine learning ou en Python (ce qui tombe bien). Après nous avoir initié aux Large Language Models (LLMs) et aux techniques de prompting, ils nous apprennent à coder en Java avec LangChain4J et Spring AI pour utiliser le LLM Gemini de Google dans nos projets Java.\nL\u0026rsquo;approche Retrieval Augmented Generation (RAG) est illustrée par son intégration avec des bases de données vectorielle comme Apache Cassandra, ceci pour générer des réponses avec nos propres données. Les Developer Advocates de Google et de DataStax nous donnent des stratégies pour minimiser les erreurs et les hallucinations des LLMs. Les modèles multimodaux (LMM) plus avancés seront également introduits.\nCédrick est Developer Advocate chez DataStax\nIl y’a 10 ans, il s’est fait connaitre par la communauté en créant le projet ff4j.\nCes dernières années, il a énormément travaillé sur Cassandra. Cédrick contribue aux projets OpenSource Langchain4j et Spring AI. Je l’ai personnellement rencontré dans le cadre du projet Spring Petclinic Reactive.\nAbdel est Developer Advocate chez Google\nExpert en Kubernetes, il travaille notamment sur le déploiement des solutions d’IA sur k8s. Introduction Abdel souligne le rôle prépondérant de Google dans le domaine de l’IA Generative.\nLa GenAI a émergé en 2017 avec l’invention du modèle Transformer par Google En 2018, Google a créé le modèle de langage BERT qui a permis d’améliorer les performances en traitement automatique des langues (NLP en anglais). En 2019, création de AlphaFold permettant de prédire comment une protéine se développe à partir de leur séquence en acides aminés. Là où un doctorant passé toutes ses études à prédire la structure d’une protéine, AlphaFold l’a fait pour toutes les protéines sur la terre 2019 : Google ouvre en open source le modèle text-to-text Transfer Transformer. 2021 : Google introduit LaMDA, un modèle de langage conçu spécifiquement pour améliorer les interactions conversationnelles entre les humains et les systèmes d\u0026rsquo;IA 2022 : sorti du modèle de langage PaLM 2023 : sorti de PalM 2 2024 : lancement du chatbot Gemini (anciennement connu sous le nom de Bard) Abdel poursuit sa présentation par un rappel des termes utilisés dans l’IA.\nL’IA englobe les différentes techniques permettant de reproduire le raisonnement humain.\nLe Machine Learning (ML) inclue le Deep Learning qui inclue à son tour l’ IA Générative.\nLe ML nécessite des données d’entrainement et fait appel à la Data Science.\nLe GenAI inclue Image Gen et LLM.\nLes LLM sont des réseaux de neurones basés sur l’architecture Transformers. Ils sont capables de reconnaitre, prédire et générer du langage.\nUn LLM génère un mot à la fois. Il calcule la probabilité du prochain mot en se basant sur une approche statistique.\nLe fonctionnement d’un LLM et de l’IA générative est particulièrement bien illustré sur le site du Financial Times ig.ft.com/generative-ai\nLe site https://lifearchitect.ai/models/ référence la taille de nombreux LLM :\nPaLM 2 a été entrainé sur 340 milliards de paramètres et, en ce mois d’avril 2024, bat tous les records.\nSe référer à l’article Pathways Language Model (PaLM): Scaling to 540 Billion Parameters for Breakthrough Performance\nD’après Abdel, les LLMs se sont démocratisés grâce à la disponibilité de cartes graphiques moins onéreuses et à la possibilité d’entrainer son modèle dans le Cloud.\nAutre hypothèse formulée : les investisseurs croient désormais en l’IA !\nCas d’utilisation des LLM en 2024 :\nLangage : écrire du texte, faire des résumé, extraction de texte, créer un chat, classification, recherche, idéation Code : génération de code (boilerplate), complétion de code (compagnon/assistant), chat, conversion de code Speech : speech-to-speech (traduction), text-to-speech Vision : génération d’images, édition, captioning, image Q\u0026amp;A, image search, génération des transcripts de vidéos (comme à Devoxxx France 2024 ;-) Google propose de nombreuses offres regroupées dans le portfolio Vertex AI :\nLa première démo en Java de ce Deep Dive commence par l’utilisation de Vertex AI en ajoutant la dépendance maven com.google.cloud:google-vertexai\nLa classe Demo01_VertexClientChat fait appel à Gemini Pro pour répondre à quelques questions :\n@Test void testChat() throws Exception { try (VertexAI vertexAI = new VertexAI(GCP_PROJECT_ID, GCP_PROJECT_LOCATION)) { GenerateContentResponse response;GenerativeModel model = new GenerativeModel(\u0026#34;gemini-pro\u0026#34;, vertexAI); ChatSession chatSession = new ChatSession(model);response = chatSession.sendMessage(\u0026#34;Hello.\u0026#34;); System.out.println(ResponseHandler.getText(response));response = chatSession.sendMessage(\u0026#34;What are all the colors in a rainbow?\u0026#34;); System.out.println(ResponseHandler.getText(response));response = chatSession.sendMessage(\u0026#34;Why does it appear when it rains?\u0026#34;); System.out.println(ResponseHandler.getText(response)); } } Une seconde démo demande au LLM multimodal gemini-vision-pro de décrire la photo d’un coucher de soleil. Le code envoie simultanément au LLM l’image et la question. Le code Java dépend du client Java Gemini et donc de Vertex AI.\nExtrait de la classe Demo02_VertexClientVisionPro :\n@Test void testVision() throws Exception { // Load the image byte[] imageBytes;// = Files.readAllBytes(Paths.get(imageName)); String resourcePath = \u0026#34;/img1.png\u0026#34;; // Resource path in the classpathtry (InputStream is = Demo02_VertexClientVisionPro.class.getResourceAsStream(resourcePath)) { assertThat(is).isNotNull(); imageBytes = is.readAllBytes(); System.out.println(\u0026#34;Image bytes read successfully. Length: \u0026#34; + imageBytes.length); try (VertexAI vertexAI = new VertexAI(GCP_PROJECT_ID, GCP_PROJECT_LOCATION)) { GenerativeModel model = new GenerativeModel(\u0026#34;gemini-pro-vision\u0026#34;, vertexAI); GenerateContentResponse response = model.generateContent( ContentMaker.fromMultiModalData( \u0026#34;What is this image about?\u0026#34;, PartMaker.fromMimeTypeAndData(\u0026#34;image/jpg\u0026#34;, imageBytes) ));System.out.println(ResponseHandler.getText(response)); } } catch (IOException e) { System.out.println(\u0026#34;Error reading the image file.\u0026#34;); } } Gemini Gemini est le modèle d\u0026rsquo;IA le plus performant de Google Deep Mind. C’est un modèle multimodale pouvant traiter à la fois du texte, des images et de la vidéo.\nUne version Nano est en cours d’incorporation dans Flutter afin d’utiliser la carte graphique du téléphone.\nGemini 1.5 accepte en entrée un livre d’un million de mots. On est loin des premiers prompts limités à quelques centaines de mots.\n3 adresses permettent de tester Gemini :\nhttps://console.cloud.google.com/vertex-ai/model-garden : https://ai.google.dev/ : nécessite une clé https://ai.google.dev/examples Démo possible sur https://gemini.google.com/app avec un simple compte Google. Exemple « Quelle est la hauteur de la tour Eiffel ? »\nGemma Gemma est un modèle OpenSource mis à disposition par Google. Basé sur Gemini, Gemma est téléchargeable depuis HuggingFace. On peut le déployer n’importe où et l’utiliser en Java.\nAbdel poursuit ce talk par une démo de la webapp bed-time-stories.web.app créée par Guillaume Laforge et qui permet de générer une histoire pour les enfants. La démo utilise Vertex AI. Abdel utilise Gemma avec quelques commandes curl pour générer une histoire.\nCedrick reprend la main.\nLangchain4j Le projet ollama permet de faire tourner des LLM en local sur son poste de dév.\nOn peut installer ollama sur Mac, Linux et Windows : https://ollama.com/download\nOllama vient avec une CLI permettant de récupérer un modèle de LLM comme gemma:2b et gemma:7b\nCommande permettant de faire tourner un modèle :\nollama run gemma:7b\nNul besoin de compte (mais peut-être d’une carte Nvidia ?)\nUne fois le modèle démarré, on peut l’interroger à base d’une simple commande curl :\ncurl http://localhost:11434/api/generate -d \u0026#39;{ \u0026#34;model\u0026#34;: \u0026#34;gemma:7b\u0026#34;, \u0026#34;prompt\u0026#34;: \u0026#34;Pourquoi le ciel est bleu ?\u0026#34; }\u0026#39; Plutôt que de passer par curl ou d’utiliser en Java un RestTemplate, Cédric propose d’utiliser la librairie Langchain4j pour faire cette demande.\nLangchain4j est une implémentation de Langchain, bibliothèque populaire du monde Python et JavaScript.\nLe code suivant est extrait de la classe _21_GemmaChat:\n@Test void talkWithGemma() { ChatLanguageModel gemma = OllamaChatModel.builder() .baseUrl(\u0026#34;http://localhost:11434/api/\u0026#34;) .modelName(\u0026#34;gemma:7b\u0026#34;) .build();System.out.println(gemma.generate(\u0026#34;Present yourself , the name of the model, who trained you ?\u0026#34;)); } L\u0026rsquo;interface ChatLanguageModel vient de langchain4j-core et la classe OllamaChatModel vient de langchain4j-ollama.\nLangchain4j sait manipuler des images. La classe _14_ImageModel_GenerateTest montre comment faire générer une photo d’un coucher de soleil sur une plage de Malibu au LLM Vertex AI. Cette fois ci, on utilise la librairie langchain4j-vertexai avec le builder VertexAiImageModel\nD’autres classes d’abstraction de langchain4j existent : LanguageModel, ImageModel\nLangchain4j est le leader fournissant le modèle théorique. Tous les fournisseurs de LLM implémentent le langage model, créent une Pull Request et la soumettent à la communauté. Particulièrement doués en IA, les chinois contribuent également.\nVoici les LLM supportés par Lanchain4j :\nCédric continue le talk par une démo utilisant langchain4j-gemini avec le modèle StreamingChatLanguageModel et l’appel au builder VertexAiGeminiStreamingChatModel\nSe référer au test _10_LanguageModelSayHello\nSpring AI Plus jeune et développé par l’équipe Spring, le framework Spring AI est un concurrent de Langchain4j.\nCédrick nous montre un HelloWorld en Spring Boot basé sur Gemini:\n@SpringBootTest class _01_LanguageModel_SayHelloTest {@Autowired private VertexAiGeminiChatClient client;@Value(\u0026#34;classpath:/prompts/system-message.st\u0026#34;) private Resource systemResource;@Test void roleTest() { String request = \u0026#34;Tell me about 3 famous pirates from the Golden Age of Piracy and why they did.\u0026#34;; String name = \u0026#34;Bob\u0026#34;; String voice = \u0026#34;pirate\u0026#34;; UserMessage userMessage = new UserMessage(request); SystemPromptTemplate systemPromptTemplate = new SystemPromptTemplate(systemResource); Message systemMessage = systemPromptTemplate.createMessage(Map.of(\u0026#34;name\u0026#34;, name, \u0026#34;voice\u0026#34;, voice)); Prompt prompt = new Prompt(List.of(userMessage, systemMessage)); ChatResponse response = client.call(prompt); System.out.println(response.getResult().getOutput().getContent()); }} La classe _01_LanguageModel_SayHelloTest montre l’utilisation de classes d’abstraction comme UserMessage, Message, Prompt ou bien encore ChatResponse.\nLa configuration du LLM est centralisée dans le fichier application.properties :\nspring.ai.vertex.ai.gemini.projectId=devoxxfrance spring.ai.vertex.ai.gemini.location=us-central1 spring.ai.vertex.ai.gemini.chat.options.model=gemini-pro spring.ai.vertex.ai.gemini.chat.options.temperature=0.8 spring.ai.vertex.ai.gemini.chat.options.topK=2 spring.ai.vertex.ai.gemini.chat.options.topP=0.9 spring.ai.vertex.ai.gemini.chat.options.maxTokens=100 #spring.ai.vertex.ai.gemini.credentialsUri= Prompt Engineering La seule commande envoyée à un LLM est une ligne de texte, le fameux prompt.\nQuelques bonnes pratiques permettant d’interagir avec le LLM nous sont données :\nComme contraintes, il peut être intéressant de demander au LLM de ne pas répondre aux questions dont il ne connait pas la réponse. Cela permet de limiter les hallucinations. Exemple de contexte : « Si l’on te pose des questions qui sort des objectifs qu’on t’a donné, répond que tu ne sais pas. »\nAvec un prompt assez long, on peut souvent contourner les frontières du LLM.\nEn complément de cette phrase, on envoie au LLM différents paramètres.\nTempérature comprise entre 0 et 1 : niveau d’expression qu’on donne au LLM\n0 : plus précis, robotique\n1 : créatif, hallucination possible\nLe choix de la température dépend des uses cases. Dans le cas d’une recherche documentaire par exemple, on emploie une température basse.\nTop P favorisant la diversité et la créativité.\nJusqu’à combien de mots tu peux mettre des réponses avec un degré de probabilité\nSpécifiez une valeur faible pour les réponses moins aléatoires et une valeur plus élevée pour les réponses plus aléatoires\nTop K similaire au Top P, mais en moins dynamique\nTokens : taille de la réponse\nPlus la réponse est longue, plus le cout augmente et sa précision baisse.\nDes techniques avancées de prompt engineering permettent d’améliorer la rédaction des prompts.\n**Few-Shot Learning\n**\nOn envoie au LLM quelques exemples afin qu’ils comprennent mieux ce que l’on souhaite lui demander.\nChain of Thoughts\nChaine de pensée : donner un raisonnement pour que le LLM trouve le résultat.\n**CoT + Self consistency\n**\nLe LLM utilise plusieurs raisonnements et sélectionne la réponse finale en fonction du plus grand nombre de réponses similaires.\nReAct Les LLM savent désormais faire appel à des systèmes externes, comme par exemple se brancher sur Bing ou appeler un service météo.\nPrompt Best Practices Nous sont données 10 bonnes pratiques de prompt engineering :\nUn exemple vaut 100 instructions DARE Determine Appropriate Response Rôles, Personas, Public : votre vision Objectifs : votre mission Portée : si tu ne le sais pas, dis-le-moi ! Adapter la température au cas d’utilisation Utilisez un langage naturel détaillé pour dévoiler une chaîne d\u0026rsquo;invites Structurer les prompts. L’ordre est important. Cédrick recommande de spécifier au prompt quel est l’objectif Responsible AI and filters : lorsqu’on enlève les filtres d’un LLM OpenSource on peut avoir de grosses surprises (ex : débrayer la procédure de fabrication d’une bombe). Test, Measure, Improve, Repeat : revue par le même LLM ou même d’autres LLM Be specific, no open questions. Se limiter (ex: 500 caractères). Sinon le contexte dilue la question Review from multiple people Detailed algorithms and reasoning problems Prompt Template Java permet d’utiliser des templates de prompt. Cédrick compare un template à une boite avec des trous, du publipostage Word, un template Velocity ou encore un template Mustache.\nA cet effet, Langchain4j et Spring IA fournissent tous les 2 une classe PromptTemplate\nLa classe _16_PromptTemplateTest donne un exemple d’utilisation :\n@Test void prompt() { PromptTemplate promptTemplate = PromptTemplate.from(\u0026#34;\u0026#34;\u0026#34; Explain me why a {{profile}} should attend conference {{conference}}. The conference is on {{current_date}} at {{current_time}} with {{current_date_time}} \u0026#34;\u0026#34;\u0026#34;);Map\u0026lt;String, Object\u0026gt; variables = new HashMap\u0026lt;\u0026gt;(); variables.put(\u0026#34;profile\u0026#34;, \u0026#34;Java Developer\u0026#34;); variables.put(\u0026#34;conference\u0026#34;, \u0026#34;Devoxx France\u0026#34;);Prompt prompt = promptTemplate.apply(variables); Response\u0026lt;String\u0026gt; response = getLanguageModel(\u0026#34;text-bison\u0026#34;).generate(prompt); System.out.println(response.content()); } protected LanguageModel getLanguageModel(final String modelName) { return VertexAiLanguageModel.builder() .project(GCP_PROJECT_ID) .endpoint(GCP_PROJECT_ENDPOINT) .location(GCP_PROJECT_LOCATION) .publisher(GCP_PROJECT_PUBLISHER) .modelName(modelName) .build(); } Limitations des LLM Un LLM :\na une date de fraicheur des informations n’a pas accès à la base documentaire de l’entreprise peut halluciner si il n’est pas bien prompté accepte un nombre limité de tokens en entrée est lent à répondre Pour utiliser ses propres connaissances, il est nécessaire d’utiliser un Retrieval-Augmented Generation (RAG)\nVector Search Pour enrichir les données d’un LLM, il faut les stocker dans une base vectorielle.\nAvant cela, il faut les convertir en vecteur. C’est le moment où vous devez faire appel à vos souvenirs de cours de maths.\nUn vecteur possède une direction et une longueur.\nIl est représenté dans un espace à plusieurs dimensions.\nEn fonction du modèle (vidéo, texte, image), on n’utilise pas la même dimension.\nLa longueur d’un vecteur s’appelle la norme et se calcule par la racine carrée de la somme des coefficients au carré.\nDans un espace en 3D, la sphère a pour norme |v|=1\nLes composantes d’un vecteur sont appelés Embeddings.\nLe Prompt est également transformé en vecteur.\nOn recherche des vecteurs qui sont proches les uns des autres. Cela nécessite de calculer une similarité entre 2 vecteurs.\nEn maths, il y’a plusieurs possibilités de calculer une distance. Un slide est préférable à un long discours :\nLa plus connue est la distance euclidienne. Elle s’appelle L2 et nécessite beaucoup de calculs.\nCédrick nous fait remarquer que la dimension des vecteurs d’une base vectorielle est multiple de 284.\nLes bases vectorielles utilisent plus couramment la distance nommée « Angular distance » ou « cosine similarity »\nChaque base de données implémente sa propre formule. Le plus important consiste à trouver les vecteurs les plus proches. Le plus simple consiste à travailler sur la sphère unité. Je vous laisse apprécier :\nCes calculs amènent beaucoup de zéro après la virgule. En Java, on arrive facilement aux limites de la précision du type double et il est nécessaire d’utiliser des BigDecimal.\nLa métrique à utiliser dépend du cas d’utilisation.\nLe k-Nearest Neighbors (kNN) est une technique fondamentale dans le domaine du Vector Search. Elle permet de trouver les k vecteurs les plus proches d\u0026rsquo;un vecteur requête (query vector) dans un espace vectoriel. Les présentateurs nous invitent à lire l’article K Nearest Neighbor Classification – Animated Explanation for Beginners\nLes calculs de similarité permettant de trouver quels sont les vecteurs les plus proches sont longs, d’où la nécessité d’approximer. L’ Approximate Nearest Neighbors(ANN) est un ensemble de techniques qui cherchent à identifier les k voisins les plus proches d\u0026rsquo;un vecteur requête (query vector) dans un espace vectoriel de haute dimension, mais en introduisant une approximation pour gagner en efficacité.\nLe partitionnement des datasets se fait dans un graphe de proximité.\nChaque point du graphe est un vecteur. Le edge est la distance (calculée lorsqu’on sauve le vecteur).\nNaturellement, de nombreuses bases vectorielles comme qdrant ou milvus se sont lancées dans le secteur des embeddings. Les bases NoSQL et relationnelles existantes se sont également mises à supporter les vecteurs. On peut citer pgvector sur PostgreSQL, Elasticsearch, Neo4j (qui faisait déjà du graphe) mais encore Apache Cassandra.\nCassandra Cassandra est une base OSS gouvernée par la fondation Apache.\nC’est une base tabulaire, ce qui signifie qu’on ne peut pas faire de jointure lors d’un select et qu’il faut penser au requêtage dès la sauvegarde en dénormalisant les données.\nCassandra ferait une parfaite matrice d’adjacence.\nLes nœuds sont distribués et contiennent entre 2 à 4 To de données. L’ajout d’un nœud nécessite de répartir les données. Cassandra scale très bien. Les données sont redondées.\nLa version 5 de Cassandra introduit le nouveau type Vector :\nCREATE TABLE IF NOT EXISTS vsearch.products ( id int PRIMARY KEY, name TEXT, description TEXT, item_vector VECTOR\u0026lt;FLOAT, 5\u0026gt; //5-dimensional embedding ); Ainsi qu’un nouvel index de stockage attaché appelé StorageAttachedIndex :\nCREATE CUSTOM INDEX IF NOT EXISTS ann_index ON vsearch.products(item_vector) USING \u0026#39;StorageAttachedIndex\u0026#39;; ANN est une famille d’algorithme de recherche approximative.\nL’opérateur ANN OF permet d\u0026rsquo;effectuer efficacement des recherches ANN sur leurs données lors d’une recherche CQL :\nSELECT * FROM vsearch.products ORDER BY item_vector ANN OF [0.15, 0.1, 0.1, 0.35, 0.55] LIMIT 1; Le site ann-benchmarks.com compare les performances brutes des algorithmes de recherche approximative des plus proches voisins.\nAujourd’hui, presque toutes les bases utilisent la technique du HNSW (pour Hierarchical Navigable Small World) avec des Vector Index stockés dans plusieurs couches : Lucene (Elastic, Solr, OpenSearch, MongoDB), Weaviate, Qdrant, PGVector.\nLucene avait déjà implémenté l’algo HNSW. Cassandra l’a utilisé dans Cassandra. Un problème majeur à son utilisation est que Java utilise beaucoup de mémoire, notamment lorsque dataset et sharding augmentent. Les ingénieurs de Cassandra ont donc planché sur une autre solution.\nDans un premier temps, Cassandra a testé l’algo DiskANN (papier implémenté par Microsoft en C). lls l’ont recodé en Java et l’ont baptisé jVector. Le projet jVector utilise Java 21 et la nouvelle API Vector du projet Panama permettant d’exploiter les instructions SIMD.\nVanama stocke le graphe sur un seul layer et crée plus de liens que nécessaires. Les distances sont précalculées lors de l’indexation. En mémoire, on ne monte plus les vecteurs en entier, mais des données plus petites, les centroids, et ceci par quantification. Au lieu de travailler avec un Vecteur, on utilise des centroids.\nLe plus souvent, dans des cas métiers, une recherche par vecteur n’est pas suffisante. Il est nécessaire d’exploiter des métadonnées : date d’indexation, auteur, prise en compte des habilitations …\nLecture conseillé : 5 Hard Problems in Vector Search, and How Cassandra Solves Them\nPour choisir sa base de données vectorielles, Cédrick conseille aux architectes d’utiliser le comparateur Vector DB Comparison\nRepartons dans notre IDE préféré pour montrer l’utilisation de Langchain4j et de Cassandra.\nCédrick utilise la nouvelle classe d’abstraction : EmbeddingModel\nOn lui donne du texte et il sort un vecteur.\nCode extrait de la classe _51_EmbeddingModel:\n@Test void testEmbeddingModel() { Response\u0026lt;Embedding\u0026gt; embedding = getEmbeddingModelGecko().embed(\u0026#34;Hello, World!\u0026#34;); log.info(\u0026#34;Dimension: {}\u0026#34;, embedding.content().dimension()); log.info(\u0026#34;Vector: {}\u0026#34;, embedding.content().vectorAsList()); }protected EmbeddingModel getEmbeddingModelGecko() { return VertexAiEmbeddingModel.builder() .project(GCP_PROJECT_ID) .endpoint(GCP_PROJECT_ENDPOINT) .location(GCP_PROJECT_LOCATION) .publisher(GCP_PROJECT_PUBLISHER) .modelName(\u0026#34;textembedding-gecko@001\u0026#34;) .build(); } La modélisation des données sous Cassandra est complexe. Aussi, pour faciliter la modélisation, l’usage de Cassio est recommandé.\nS’en suit la démo _54_CassandraVectorStore montrant l’usage de tags pour filtrer des requêtes. La classe EmbeddingStore est mise à l’honneur.\n@Test void langchain4jEmbeddingStore() { // I have to create a EmbeddingModel EmbeddingModel embeddingModel = getEmbeddingModelGecko();// Embed the question Embedding questionEmbedding = embeddingModel .embed(\u0026#34;We struggle all our life for nothing\u0026#34;) .content();// We need the store EmbeddingStore\u0026lt;TextSegment\u0026gt; embeddingStore = new CassandraCassioEmbeddingStore( getCassandraSession(), TABLE_NAME, EMBEDDING_DIMENSION);// Query (1) log.info(\u0026#34;Querying the store\u0026#34;); embeddingStore .findRelevant(questionEmbedding, 3, 0.8d) .stream().map(r -\u0026gt; r.embedded().text()) .forEach(System.out::println);// Query with a filter(2) log.info(\u0026#34;Querying with filter\u0026#34;); embeddingStore.search(EmbeddingSearchRequest.builder() .queryEmbedding(questionEmbedding) .filter(metadataKey(\u0026#34;author\u0026#34;).isEqualTo(\u0026#34;nietzsche\u0026#34;)) .maxResults(3).minScore(0.8d).build()) .matches() .stream().map(r -\u0026gt; r.embedded().text()) .forEach(System.out::println);} En production, il manque encore des fonctionnalités pour, par exemple, rafraichir les données de la base vectorielle tout en optimisant le cout.\nA ce jour, toutes les bases de données vectorielles ne savent pas encore implémenter tous les filtres de Langchain4j. La distribution Cloud de Cassandra nommée AstraDB ajoute les filtres manquants.\nRetrieval Augmented Generation (RAG) Le processus Retrieval Augmented Generation (RAG) consiste à optimiser le résultat d\u0026rsquo;un LLM. 2 types de RAG existent : naive et advanced.\nUn vecteur de dimension 1000 ne peut pas stocker l’intégralité d’un document. On découpe donc ce document en morceau (chunk). Il existe plusieurs techniques pour faire du chunking.\nDes préprocesseurs commencent par lire les documents (avec par exemple Apache Tika et Apache PDFBox), retirent les balises, normalisent l’encodage (UTF-8). Ceci pour ne conserver que le texte brut. Chaque extension de fichier (ex : markdown) demande un parseur.\nSur OpenAI, on peut mettre entre 256 et 512 tokens par vecteur de dimension 1536.\nPour garder le contexte, les segments doivent se superposer.\nAstuce pour sauvegarder le segment : ajouter un hash dans les métadonnées du document, ce qui évite de le réindexer pour rien.\nNouvelle démo basée sur le test _62_NaiveRag_RetrievalTest et montrant l’usage des interfaces ContentRetriever et Assistant de langchain4j :\n@Test void shouldRetrieveContent2() {ContentRetriever contentRetriever = EmbeddingStoreContentRetriever.builder() .embeddingStore(new AstraDbEmbeddingStore(getCollectionRAG())) .embeddingModel(getEmbeddingModelGecko()) .maxResults(2) .minScore(0.5) .build();// configuring it to use the components we\u0026#39;ve created above. Assistant ai = AiServices.builder(Assistant.class) .contentRetriever(contentRetriever) .chatLanguageModel(getChatLanguageModelChatBison()) .chatMemory(MessageWindowChatMemory.withMaxMessages(10)) .build();String response = ai.answer(\u0026#34;Who is Johnny?\u0026#34;); System.out.println(response); } Les advanced RAG consistent à mettre à disposition un query router utile, par exemple, pour séparer les requêtes des tenants, exploiter les niveaux de confidentialité des documents …\nLors de longues discussions, on peut atteindre les limites du LLM. Il est alors possible de faire un résumer de l’historique de la conversation. Usage des Query Transformer et de la classe CompressingQueryTransformer de langchain4j.\nLorsqu’il y’a plusieurs résultats, il faut les agréger via des Query Aggregator. Les algorithmes de reranking peuvent utiliser d’autres algorithmes. La star du reranking s’appelle Cohere.\nLa classe de test _66_AdvancedRag_QueryReranking en donne un exemple d’utilisation :\n@Test void shouldRerankResult() {// Re Ranking ScoringModel scoringModel = CohereScoringModel.withApiKey(System.getenv(\u0026#34;COHERE_API_KEY\u0026#34;));ContentAggregator contentAggregator = ReRankingContentAggregator.builder() .scoringModel(scoringModel) .minScore(0.8) .build();RetrievalAugmentor retrievalAugmentor = DefaultRetrievalAugmentor.builder() .contentRetriever(createRetriever(\u0026#34;/johnny.txt\u0026#34;)) .contentAggregator(contentAggregator) .build();Assistant assistant = AiServices.builder(Assistant.class) .chatLanguageModel(getChatLanguageModelChatBison()) .retrievalAugmentor(retrievalAugmentor) .chatMemory(MessageWindowChatMemory.withMaxMessages(10)) .build();System.out.println( assistant.answer(\u0026#34;Tell me 10 things about Johnny\u0026#34;)); } A noter l’utilisation des interfaces RetrievalAugmentor et ScoringModel de langchain4j.\nFunctions calling \u0026amp; Semantic Search Ce talk se termine par une démonstration de la notion de Tool de langchain4j.\nPour récupérer la météo à Paris, Gemini sait qu’il existe une API permettant de récupérer le temps.\nEn java, on annote une méthode avec @Tool, ce qui permet au LLM d’appeler ce tool lorsqu’on lui pose la question d’additionner 2 nombres.\nExtrait de la classe de test _71_CallFunctionTest:\nstatic ChatLanguageModel model= VertexAiGeminiChatModel.builder() .project(GCP_PROJECT_ID) .location(GCP_PROJECT_LOCATION) .modelName(\u0026#34;gemini-pro\u0026#34;) .build();static class Calculator { @Tool(\u0026#34;Adds two given numbers\u0026#34;) double add(double a, double b) { System.out.printf(\u0026#34;Called add(%s, %s)%n\u0026#34;, a, b); return a + b; } }interface Assistant { String chat(String userMessage); }@Test void testFunctionCalling1() {Calculator calculator = new Calculator();Assistant assistant = AiServices.builder(Assistant.class) .chatLanguageModel(model) .chatMemory(MessageWindowChatMemory.withMaxMessages(10)) .tools(calculator) .build();String answer = assistant.chat(\u0026#34;How much is 754 + 926?\u0026#34;); System.out.println(answer); } ","link":"https://javaetmoi.com/2024/04/utiliser-les-ia-generatives-avec-java/","section":"posts","tags":["cassandra","devoxx","embedding","gemini","gemma","genai","java","langchain4j","llm","ollama","rag","spring-ai","vector"],"title":"Utiliser les IA Génératives avec Java"},{"body":"","link":"https://javaetmoi.com/tags/vector/","section":"tags","tags":null,"title":"Vector"},{"body":"En attendant de pouvoir tester les 2 étages du Palais des Congrès du 17 au 19 avril 2024, je consigne dans ce billet 16 notes prises au cours de ces 3 jours toujours aussi riches.\nD\u0026rsquo;ici quelques jours / semaines, après un repos bien mérité des organisateurs, l’intégralité des vidéos des keynotes, conférences et universités présentées lors de Devoxx France 2023 seront disponibles sur Youtube sur la chaîne Devoxx FR. Mes notes pourrons vous aider à vous faire rapidement un aperçu de leur contenu avant de les visionner.\nTout comme l\u0026rsquo;édition précédente, je n\u0026rsquo;y aurais pas été découvrir les dernières technos hypes de 2023. Paradoxalement, il m\u0026rsquo;a semblé y avoir plus de conférences sur le SQL que le NoSQL. Kubernetes, les applications natives, le Craft et Docker auront été au rendez-vous de cette 11ième édition.\nMes notes classées par ordre de préférence :\nValue Types et Pattern Matching - José Paumard et Rémi Forax Loi de Conway : lorsque les bonnes pratiques ne suffisent plus - Julien Topçu Gestion de la dette d’architecture dans le contexte d’hypercroissance - Cyril Beslay CRAC vs GraalVM, pour un démarrage plus rapide- Lilian Benoit Voyage au centre de la Veille - Fabien Hiegel et David Franck Avoir un journal de codeur / codeuse - Sandrine Banas Bootiful Spring Boot 3 - Josh Long Le Craft : des concepts au déploiement à l’échelle - Matthieu Vincent et Guillaume Le Dain A la découverte d’Accelerate - Geoffrey Graveaud Container Builders : Which is the best image builder ? - Christian Nader From Dallas to Happy days : Tips to positively hack your life - Emmanuel Bernard Docker au service du DevSecOps - Carmen Piciorus Le Cache HTTP - Hubert Sablonnière Le Guide du Maitre du Donjon : Maitriser la cybersécurité en créant des challenges CTF- Adam Bertrand Comment être condamné par la CNIL ? - Juliette Audema Clean as You Code your projects - Nolwenn Cadic et Marco Comi ","link":"https://javaetmoi.com/2023/04/16-prises-de-notes-a-devoxx-france-2023/","section":"posts","tags":["accelerate","architecture","cache","clean-code","crac","craft","devoxx","docker","graalvm","java","spring-boot","sécurité"],"title":"16 prises de notes à Devoxx France 2023"},{"body":"","link":"https://javaetmoi.com/tags/accelerate/","section":"tags","tags":null,"title":"Accelerate"},{"body":"","link":"https://javaetmoi.com/tags/cache/","section":"tags","tags":null,"title":"Cache"},{"body":"","link":"https://javaetmoi.com/tags/clean-code/","section":"tags","tags":null,"title":"Clean-Code"},{"body":"","link":"https://javaetmoi.com/tags/crac/","section":"tags","tags":null,"title":"Crac"},{"body":"","link":"https://javaetmoi.com/tags/docker/","section":"tags","tags":null,"title":"Docker"},{"body":"","link":"https://javaetmoi.com/tags/graalvm/","section":"tags","tags":null,"title":"Graalvm"},{"body":"","link":"https://javaetmoi.com/tags/s%C3%A9curit%C3%A9/","section":"tags","tags":null,"title":"Sécurité"},{"body":"Dans une grande entreprise, le développement d’applications métiers doit respecter les règles en vigueur : normes de développement, normes de sécurité, barrière qualité, socle technique borné, intégration à l’usine de dév …\nLe démarrage d’une nouvelle application Java peut être accélérée de bien des manières : usage d’outils Low Code comme Palmyra, générateur de squelettes d’application comme JHipster, utilisation d’applications blanches déclinées par catégorie d’appli (ex : batch, web), copier/coller/élagage d’une application de référence, guide de démarrage sous forme wiki … Chaque technique présente ses avantages et ses inconvénients. Mais certaines ne couvrent pas toutes les règles évoquées précédemment.\nAfin d’ accélérer le développement d’une nouvelle application, mon objectif était de générer un squelette d’application minimaliste dont le code généré est parfaitement maitrisé et avec des dépendances choisies à la carte par le tech lead. Libre à lui ensuite de retravailler le code généré pour mettre en place l’architecture cible de l’application, en choisissant par exemple de partir sur une architecture hexagonale.\nBien connu des développeurs Spring Boot, je me suis appuyé sur le code backend faisant tourner le site https://start.spring.io/, à savoir le projet Spring Initializr conçu et maintenu majoritairement par Stéphane Nicoll. Léger, codé en Java, reposant sur Spring Boot et documenté, ce projet a été conçu pour être personnalisé et extensible. Cela en a fait un excellent candidat.\nLa première mouture de ce générateur développé en quelques jours m’aura permis de générer :\nla configuration du socle Spring Boot d’entreprise la configuration du logger permettant de standardiser les logs au format JSON la sécurisation des API REST avec Spring Security, OpenID Connect et le SSO d’entreprise les contrôleurs et DTO d’une API REST à partir d’une spécification OpenAPI 3 le Dockerfile et la configuration du pipeline CI/CD API REST Le projet Spring Initializr permet de développer un « initializr » d’applications maisons exposant une API REST iso fonctionnelle à ce que fait Spring Initializr. Cet initialzr propriétaire peut donc être utilisé tel quel par différents outils s’interfaçant déjà ave l’API REST de Spring Initializr :\ndepuis les principaux IDE Java du marché supportant Spring Initializr, nativement ou après installation d’un plugin : IntelliJ, Eclipse, STS, VSCode, NetBeans. en ligne de commande cUrl avec Spring Boot CLI et/ou une IHM web (adaptée ou non à partir du repo spring-io/start.spring.io) Un appel GET à l’API REST de l’initializr permet de renvoyer le paramétrage utilisés par les outils cités : choix du Langage (Java/Kotlin/Groovy), version de Java et de Spring Boot, outil de build (Maven/Gradle) ou bien encore les dépendances.\nExtrait d’une réponse à un appel GET :\n{ \u0026#34;javaVersion\u0026#34;:{ \u0026#34;type\u0026#34;:\u0026#34;single-select\u0026#34;, \u0026#34;default\u0026#34;:\u0026#34;11\u0026#34;, \u0026#34;values\u0026#34;:[ { \u0026#34;id\u0026#34;:\u0026#34;11\u0026#34;, \u0026#34;name\u0026#34;:\u0026#34;11\u0026#34; }, { \u0026#34;id\u0026#34;:\u0026#34;17\u0026#34;, \u0026#34;name\u0026#34;:\u0026#34;17\u0026#34; } ] }, \u0026#34;bootVersion\u0026#34;:{ \u0026#34;type\u0026#34;:\u0026#34;single-select\u0026#34;, \u0026#34;default\u0026#34;:\u0026#34;2.7.0.RELEASE\u0026#34;, \u0026#34;values\u0026#34;:[ { \u0026#34;id\u0026#34;:\u0026#34;2.7.0.RELEASE\u0026#34;, \u0026#34;name\u0026#34;:\u0026#34;2.7.0\u0026#34; } ] }, \u0026#34;dependencies\u0026#34;:{ \u0026#34;type\u0026#34;:\u0026#34;hierarchical-multi-select\u0026#34;, \u0026#34;values\u0026#34;:[ { \u0026#34;name\u0026#34;:\u0026#34;Java\u0026amp;Moi\u0026#34;, \u0026#34;values\u0026#34;:[ { \u0026#34;id\u0026#34;:\u0026#34;openapi\u0026#34;, \u0026#34;name\u0026#34;:\u0026#34;OpenAPI\u0026#34;, \u0026#34;description\u0026#34;:\u0026#34;Configure the web application to expose a REST API designed with a contact-first OpenAPI Specification (OAS): \\n - The openapi-generator-maven-plugin\\n - SwaggerUI with Springdoc\u0026#34; } ] }, { \u0026#34;name\u0026#34;:\u0026#34;Spring\u0026#34;, \u0026#34;values\u0026#34;:[ { \u0026#34;id\u0026#34;:\u0026#34;web\u0026#34;, \u0026#34;name\u0026#34;:\u0026#34;Web\u0026#34;, \u0026#34;description\u0026#34;:\u0026#34;Build web, including RESTful, applications using Spring MVC. Uses Apache Tomcat as the default embedded container.\u0026#34; } ] } ] }, ... } Fil conducteur Comme fil conducteur de ce billet, je vous propose de construire l’initializr javaetmoi-initializr chargé de générer la configuration d’une application web Spring MVC exposant une API REST en contract first au format OpenAPI 3. Le générateur configure le plugin openapi-generator-maven-plugin ainsi que Springdoc pour avoir accès à Swagger UI. C’est précisément le type d’architecture mis en œuvre sur le sample spring-petclinic-rest (avec pour le moment Springfox à la place de Springdoc).\nDepuis IntelliJ IDEA, voici les dépendances proposées :\nLe code source de javaetmoi-initializr est disponible sur le repository : arey/javaetmoi-initializr.\nDémarrage Pour développer un initializr, commencez par créer une application Spring Boot à l’aide de https://start.spring.io/ tout en ajoutant la dépendance Spring Web (ce qui déclarera l’artefact spring-boot-starter-web). Je recommande ensuite de suivre le paragraphe Creating your own instance du manuel de référence de Spring Intializr. Une fois le Bill of Materials initializr-bom ajouté au , déclarerez les dépendances suivantes :\n\u0026lt;dependencies\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter-web\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;io.spring.initializr\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;initializr-web\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;io.spring.initializr\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;initializr-generator-spring\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;/dependencies\u0026gt; Personnellement, j’ai utilisé la version 0.12.0 du initializr-bom datant du 24 janvier 2022 et compatible avec Spring Boot 2.7 et Java 17. Comme conseillé dans la documentation, remplacez le fichier application.properties par un fichier application.yaml plus enclin à accueillir une structure hiérarchique.\nIl sera ensuite nécessaire de configurer la propriété initializr du fichier application.yaml avec :\nles versions de java supportées : 8, 11 et/ou 17 les versions de Spring Boot proposées les langages supportés : Java, Kotlin et/ou Groovy le système de build supporté : Maven et/ou Gradle le nom de package par défaut Le type de packaging supporté : war ou jar le groupId par défaut la version par défaut de l’application les dépendances Maven Voici un extrait configurant les 2 dépendances OpenAPI et Web aperçues dans la capture d’écran IntelliJ :\ninitializr: dependencies: - name: Java\u0026amp;Moi content: - id: openapi name: OpenAPI starter: false description: \u0026#34;Configure the web application to expose a REST API designed with a contact-first OpenAPI Specification (OAS): \\n - The openapi-generator-maven-plugin\\n - SwaggerUI with Springdoc\u0026#34; - name: Spring content: - name: Web id: web description: Build web, including RESTful, applications using Spring MVC. Uses Apache Tomcat as the default embedded container. Pour une configuration complète, référez-vous au fichier application.yml du repo git.\nDans cet exemple, la version de Spring Boot à utiliser est codée en dur. Il est recommandé d’aller chercher dynamiquement la ou les versions proposées à l’utilisateur en déclarant un bean Spring implémentant l’interface InitializrMetadataUpdateStrategy ou en utilisant la classe SaganInitializrMetadataUpdateStrategy clé en main.\nNon présent dans l’exemple, en attaquant l’API REST des différents composants de l’Usine de Dév (ex : Nexus, GitLab, ACR), on peut aller chercher la dernière version :\ndu POM Parent d’entreprise du BOM Spring Boot d’entreprise des librairies maisons des images Docker privées et validées par les Ops Personnaliser la génération L’ajout des dépendances initializr-web et initializr-generator-spring fait que votre initializr reproduit le fonctionnement de Spring Boot initializr et permet de facto de générer une classe main, sa classe de test, un fichier application.properties ….\nPour adapter ce comportement à votre besoin, il est possible de déclarer des beans Spring en les regroupant dans des classes de configuration annotées par @ProjectGenerationConfiguration\nCes classes doivent être enregistrées dans le fichier META-INF/spring.factories. Voici un exemple enregistrant 2 classes de configuration : une première transverse et une seconde ne s’activant que lorsque la dépendance OpenAPI a été sélectionnée :\nio.spring.initializr.generator.project.ProjectGenerationConfiguration=\\ com.javaetmoi.initializr.generator.common.CommonSpringBootConfiguration,\\ com.javaetmoi.initializr.generator.openapi.OpenAPIConfiguration La classe CommonSpringBootConfiguration a pour objectif de remplacer le fichier application.properties par un fichier application.yml plus propice à accueillir la configuration générée par les autres générateurs. On y retrouve 2 beans Spring : un premier chargé de créer le fichier application.yml à partir d’un template et le second chargé de supprimer le fichier application.properties créé par la classe ApplicationPropertiesContributor du module initializr-generator-spring. Le plus simple aurait été de réussir à désactiver ce dernier.\n@ProjectGenerationConfiguration class CommonSpringBootConfiguration { @Bean ApplicationYamlContributor applicationYamlContributor() { return new ApplicationYamlContributor(); } @Bean DeleteAplicationPropertiesContributor deleteAplicationPropertiesContributor() { return new DeleteAplicationPropertiesContributor(); } } La classe ApplicationYamlContributor hérite du contributeur SingleResourceProjectContributor facilitant la création d’un inique fichier. A noter la redéfinition de la méthode getOrder pour que ce bean soit appelé prioritairement, ceci afin que le fichier application.yml existe pour les autres générateurs.\nclass ApplicationYamlContributor extends SingleResourceProjectContributor { @Override public int getOrder() { return Ordered.HIGHEST_PRECEDENCE; } ApplicationYamlContributor() { this(\u0026#34;classpath:configuration/application.yml\u0026#34;); } ApplicationYamlContributor(String resourcePattern) { super(\u0026#34;src/main/resources/application.yml\u0026#34;, resourcePattern); } } Dépendance OpenAPI Intéressons-nous à présent à la mise en place d’une API REST mettant en œuvre le plugin Maven openapi-generator-maven-plugin. A partir d’une spécification OpenAPI décrite dans un fichier openapi.yaml, ce plugin génère l’interface des contrôleurs REST et les classes du modèle représentant les ressources REST. A titre d’exemple, le fichier openapi.yaml généré contient une API Hello World. Une implémentation basique de cette API est également générée.\nLa classe de configuration OpenAPIConfiguration annotée avec @ProjectGenerationConfiguration déclare pas moins de 7 beans. Notez l’usage de l’annotation @ConditionalOnRequestedDependency qui permet de n’activer cette classe de configuration Spring que si la dépendance OpenAPI a été sélectionnée. Quatre autres annotations du même genre existent : ConditionalOnPackaging, ConditionalOnLanguage, ConditionalOnBuildSystem et ConditionalOnPlatformVersion.\n@ProjectGenerationConfiguration @ConditionalOnRequestedDependency(DEPENDENCY_OPENAPI) @AutoConfigureAfter({InitializrAutoConfiguration.class}) class OpenAPIConfiguration { @Bean OpenAPIPluginCustomizer openAPIPluginCustomizer(ProjectDescription projectDescription) { return new OpenAPIPluginCustomizer(projectDescription); } @Bean OpenApiDependenciesCustomizer openApiDependenciesCustomizer() { return new OpenApiDependenciesCustomizer(); } @Bean SpecOpenApiContributor specOpenApiContributor() { return new SpecOpenApiContributor(); } @Bean HelloControllerContributor helloControllerContributor(ProjectDescription projectDescription, MustacheTemplateRenderer mustacheTemplateRenderer) { return new HelloControllerContributor(mustacheTemplateRenderer, projectDescription); } @Bean SwaggerControllerContributor swaggerControllerContributor(ProjectDescription projectDescription, MustacheTemplateRenderer mustacheTemplateRenderer) { return new SwaggerControllerContributor(mustacheTemplateRenderer, projectDescription); } @Bean RemoveOpenAPIDependencyCustomizer removeOpenAPIDependencyCustomizer() { return new RemoveOpenAPIDependencyCustomizer(); } @Bean TestOpenApiContributor testOpenApiContributor() { return new TestOpenApiContributor(); } } 1. Implémentant l’interface BuildCustomizer, la classe OpenAPIPluginCustomizer est chargée de configurer le plugin Maven openapi-generator-maven-plugin :\nclass OpenAPIPluginCustomizer implements BuildCustomizer\u0026lt;MavenBuild\u0026gt; { private final ProjectDescription projectDescription; OpenAPIPluginCustomizer(ProjectDescription projectDescription) { this.projectDescription = projectDescription; } @Override public void customize(MavenBuild build) { build.plugins().add(\u0026#34;org.openapitools\u0026#34;, \u0026#34;openapi-generator-maven-plugin\u0026#34;, c -\u0026gt; { c.version(\u0026#34;5.4.0\u0026#34;); c.execution(\u0026#34;generate\u0026#34;, e -\u0026gt; e.goal(\u0026#34;generate\u0026#34;)); c.configuration(configuration -\u0026gt; { configuration.add(\u0026#34;inputSpec\u0026#34;, \u0026#34;${project.basedir}/src/main/resources/openapi/openapi.yaml\u0026#34;); configuration.add(\u0026#34;generatorName\u0026#34;, \u0026#34;spring\u0026#34;); configuration.add(\u0026#34;library\u0026#34;, \u0026#34;spring-boot\u0026#34;); configuration.add(\u0026#34;modelNameSuffix\u0026#34;, \u0026#34;Resource\u0026#34;); configuration.add(\u0026#34;apiPackage\u0026#34;, projectDescription.getPackageName() + \u0026#34;.rest.controller\u0026#34;); configuration.add(\u0026#34;modelPackage\u0026#34;, projectDescription.getPackageName() + \u0026#34;.rest.model\u0026#34;); configuration.configure(\u0026#34;configOptions\u0026#34;, configOptions -\u0026gt; { configOptions.add(\u0026#34;interfaceOnly\u0026#34;, \u0026#34;true\u0026#34;); configOptions.add(\u0026#34;openApiNullable\u0026#34;, \u0026#34;false\u0026#34;); }); }); }); } } A noter l’usage de lambda de type Consumer dans l’API de Spring Intializr.\nLa configuration Maven générée est la suivante :\n\u0026lt;plugin\u0026gt; \u0026lt;groupId\u0026gt;org.openapitools\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;openapi-generator-maven-plugin\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;5.4.0\u0026lt;/version\u0026gt; \u0026lt;configuration\u0026gt; \u0026lt;inputSpec\u0026gt;${project.basedir}/src/main/resources/openapi/openapi.yaml\u0026lt;/inputSpec\u0026gt; \u0026lt;generatorName\u0026gt;spring\u0026lt;/generatorName\u0026gt; \u0026lt;library\u0026gt;spring-boot\u0026lt;/library\u0026gt; \u0026lt;modelNameSuffix\u0026gt;Api\u0026lt;/modelNameSuffix\u0026gt; \u0026lt;apiPackage\u0026gt;com.javaetmoi.myapp.demo.rest.controller\u0026lt;/apiPackage\u0026gt; \u0026lt;modelPackage\u0026gt;com.javaetmoi.myapp.demo.rest.model\u0026lt;/modelPackage\u0026gt; \u0026lt;configOptions\u0026gt; \u0026lt;interfaceOnly\u0026gt;true\u0026lt;/interfaceOnly\u0026gt; \u0026lt;openApiNullable\u0026gt;false\u0026lt;/openApiNullable\u0026gt; \u0026lt;/configOptions\u0026gt; \u0026lt;/configuration\u0026gt; \u0026lt;executions\u0026gt; \u0026lt;execution\u0026gt; \u0026lt;id\u0026gt;generate\u0026lt;/id\u0026gt; \u0026lt;goals\u0026gt; \u0026lt;goal\u0026gt;generate\u0026lt;/goal\u0026gt; \u0026lt;/goals\u0026gt; \u0026lt;/execution\u0026gt; \u0026lt;/executions\u0026gt; \u0026lt;/plugin\u0026gt; 2. La dépendance OpenAPI n’est pas une vraie dépendance au sens Maven. Non seulement elle déclare puis configure le plugin openapi-generator-maven-plugin, mais elle déclare également la dépendance Maven pour Springdoc (Swagger UI) et le starter spring-boot-starter-validation activant la validation des annotations Bean Validation positionnée par le plugin openapi. Pour se faire, la classe OpenApiDependenciesCustomizer implémente également l’interface BuildCustomizer :\nclass OpenApiDependenciesCustomizer implements BuildCustomizer\u0026lt;MavenBuild\u0026gt; { @Override public void customize(MavenBuild build) { build.dependencies().add(\u0026#34;springdoc\u0026#34;, Dependency .withCoordinates(\u0026#34;org.springdoc\u0026#34;, \u0026#34;springdoc-openapi-ui\u0026#34;) .version(VersionReference.ofValue(\u0026#34;1.6.9\u0026#34;)) .build()); build.dependencies().add(\u0026#34;spring-boot-starter-validation\u0026#34;, \u0026#34;org.springframework.boot\u0026#34;, \u0026#34;spring-boot-starter-validation\u0026#34;, DependencyScope.COMPILE); } } La configuration Maven générée est ici évidente :\n\u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter-validation\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springdoc\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;springdoc-openapi-ui\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.6.9\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; 3. Sur le même principe que ApplicationYamlContributor, la classe SpecOpenApiContributor ajoute le fichier openapi.yaml au projet généré.\n4. A partir du template Mustache HelloController.mustache, la classe HelloControllerContributor génère un @RestController implémentant l’interface HelloApi généré par le plugin maven.\nVoici le template HelloController.mustache :\npackage {{package}}.controller; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import {{package}}.model.MessageResource; @RestController @RequestMapping(\u0026#34;/api/v1\u0026#34;) public class HelloController implements HelloApi { @Override public ResponseEntity\u0026lt;MessageResource\u0026gt; hello(String name) { return ResponseEntity.ok(new MessageResource().message(name)); } } 5. La classe SwaggerControllerContributor génère un contrôleur Spring MVC redirigeant l’utilisateur sur l’IHM de Swagger UI lorsqu’il navigue sur http://localhost:8080\n@Controller public class SwaggerController { @RequestMapping(value = \u0026#34;/\u0026#34;) public String index() { return \u0026#34;redirect:swagger-ui/index.html\u0026#34;; } } 6. La classe TestOpenApiContributor ajoute un fichier hello.http facilitant le test de l’API depuis IntelliJ :\n\u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter-openapi\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; Tester javaetmoi-initializr revient à suivre les instructions données dans son README.md.\nTests unitaires Tester unitairement les différents générateurs de code est facilité par l’artefact initializr-generator-test que vous pouvez ajouter en scope test à votre projet :\n\u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;io.spring.initializr\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;initializr-generator-test\u0026lt;/artifactId\u0026gt; \u0026lt;scope\u0026gt;test\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt; Cet artefact propose un ensemble de classes permettant de générer des projets. Je pense par exemple à la classe ProjectGeneratorTester. Le résultat de la génération est alors disponible dans la classe ProjectStructure qui permet de récupérer le chemin vers le code généré mais propose également tout un jeu d’assertions AssertJ facilitant les tests sur le pom Maven, l’arborescence des fichiers et le contenu des fichiers/classes générées.\nExtrait de la classe de teste OpenApiTest, la méthode suivante vérifie que les dépendances Maven springdoc-openapi-ui et spring-boot-starter-validation ont été ajoutées au pom.xml :\n@Test void should_openapi_dependency_generate_pom_with_springdoc_and_spring_boot_starter_validation() { // Given var metadata = InitializrMetadataTestBuilder.withDefaults().build(); // When var project = generateProject(DESCRIPTION, metadata); // Then assertThat(project).mavenBuild() .hasDependency(\u0026#34;org.springdoc\u0026#34;, \u0026#34;springdoc-openapi-ui\u0026#34;) .hasDependency(\u0026#34;org.springframework.boot\u0026#34;, \u0026#34;spring-boot-starter-validation\u0026#34;); } La méthode suivante vérifie quant à elle le contenu de la classe HelloController.java générée :\n@Test void should_openapi_dependency_generate_HelloController() { // Given var metadata = InitializrMetadataTestBuilder.withDefaults().build(); // When var project = generateProject(DESCRIPTION, metadata); // Then // @formatter:off assertThat(project).textFile(\u0026#34;src/main/java/com/javaetmoi/demo/rest/controller/HelloController.java\u0026#34;).containsExactly( \u0026#34;package com.javaetmoi.demo.rest.controller;\u0026#34;, \u0026#34;\u0026#34;, \u0026#34;import org.springframework.http.ResponseEntity;\u0026#34;, \u0026#34;import org.springframework.web.bind.annotation.RequestMapping;\u0026#34;, \u0026#34;import org.springframework.web.bind.annotation.RestController;\u0026#34;, \u0026#34;\u0026#34;, \u0026#34;import com.javaetmoi.demo.rest.model.MessageResource;\u0026#34;, \u0026#34;\u0026#34;, \u0026#34;@RestController\u0026#34;, \u0026#34;@RequestMapping(\\\u0026#34;/api/v1\\\u0026#34;)\u0026#34;, \u0026#34;public class HelloController implements HelloApi {\u0026#34;, \u0026#34;\u0026#34;, \u0026#34; @Override\u0026#34;, \u0026#34; public ResponseEntity\u0026lt;MessageResource\u0026gt; hello(String name) {\u0026#34;, \u0026#34; return ResponseEntity.ok(new MessageResource().message(name));\u0026#34;, \u0026#34; }\u0026#34;, \u0026#34;}\u0026#34; ); // @formatter:on Conclusion Au travers de ce billet, nous aurons vu comment personnaliser Spring Initializr à partir d’un exemple concret. Si vous êtes familiers à l’écosystème Spring, la prise en main de l’API Java de cet outil devrait se faire relativement rapidement.\nAu cours de mes développements, je me suis aperçu certaines limitations de l’API. J’avais par exemple besoin de générer dynamiquement de la configuration Java de Spring Security. L’ajout de l’annotation @Override ou d’un throws Exception n’était pas proposée par la classe JavaMethodDeclaration :\n@Override protected void configure(HttpSecurity http) throws Exception { Comme suggéré dans l’ issue #1043, n’ayant besoin que du support de Java, je me suis tourné vers l’usage de JavaPoet que j’avais déjà utilisé sur le projet javabean-marshaller. Son intégration dans une implémentation de ProjectContributor n’a pas posé de difficulté, preuve que Spring Initializr est extensible. Pour un support de Kotlin, j’aurais pu utiliser KotlinPoet.\nN’ayant pas regardé en détails ce que proposait JHipster, je ne saurais pas départager les 2 solutions. Mais je serais curieux de vos retours d’expérience.\nRessources Manuel de reference Spring Initializr How to customize the Spring Initializr Start the Spring Initializr personalization journey Repository GitHub javaetmoi-initializr ","link":"https://javaetmoi.com/2022/07/generateur-de-squelette-dapplication-base-sur-spring-initializr/","section":"posts","tags":["openapi","spring-boot","spring-initializr"],"title":"Générateur de squelette d’application basé sur Spring Initializr"},{"body":"","link":"https://javaetmoi.com/tags/spring-initializr/","section":"tags","tags":null,"title":"Spring-Initializr"},{"body":"Ce fut ma 9ième participation à Devoxx France (et oui, j’ai malheureusement loupé l’édition 9 ¾). Et je dois vous avouer que ma conférence préférée m’avait manqué. Une bonne bulle d’oxygène au détour d’un projet réglementaire en Java. Les 10 ans de Devoxx France furent un grand cru. Le nombre de stands / partenaires occupent de plus en plus d\u0026rsquo;espace au Palais des Congrés et les speakers se dépassent d’année en année. Un grand bravo aux organisateurs, gilets rouges, orateurs et aux Cast Codeurs qui clôturent chaque édition en beauté.\nD\u0026rsquo;ici quelques jours, l’intégralité des vidéos des conférences et universités présentées lors de Devoxx France 2022 sont disponibles sur la chaîne Devoxx FR de Youtube.\nSi vous souhaitez rapidement vous faire un avis sur leur contenu avant de les visionner ou si vous souhaitez garder une trace écrite de ce que vous y avez appris, je mets librement à disposition l’ensemble de mes 13 notes prises au cours de ces 3 jours riches en contenus et en découvertes. Entre les retards SNCF et mon Macbook vieillissant qui fait des siennes, le nombre est moindre que les années précédentes. Mais promis, j’essaierai de me rattraper en 2023 :-)\nFait marquant, cette édition 2022 n’aura pas fait place à de nouvelles technos hypes. On peut se souvenir de Quarkus en 2019, Kafka en 2016 ou bien encore Angular.JS en 2013. Cette 10ième édition aura été celle de la maturité : retours d’expérience, architecture, état de l’art, sécurisation du code et approfondissement du fonctionnement de la plateforme Java étaient au rendez-vous.\nMes notes classées par ordre de préférence :\nLoom nous protègera-t-il du Braquage Tempor el ?- José Paumard et Rémi Forax OAuth 2.1 expliqué simplement (même si tu n’es pas dev) ! - Julien Topçu Mieux maitriser TLS, OpenSSL et les certificats - Mathieu Humbert Architecture microservices et coherence des données : mais on fait comment dans la vraie vie ?- Jean-François James Valhalla, to the hell and back- Rémi Forax Major migration made easy - Tim te Beek Cybersécurité et générateur de nombres aléatoires - Mathis Hammel Comprendre GraphQL - Guillaume Scheibel et Geoffroy Couprie Connaissez-vous vraiment JWT ? - Karim Pinchon A la découverte des Docker Dev Environments- Djordje Lukic et Guillaume Lours Pourquoi DevOps ne tient pas ses promesses ?- Gérôme Egron et Guillaume Mathieu Dans les coulisses du Cloud- Cécile Morange Construire et déployer son application avec Argo dans Kubernetes - Paul-Henry Perrissel et Nicolas Mpacko Tongo ","link":"https://javaetmoi.com/2022/04/13-prises-de-notes-a-devoxx-france-2022/","section":"posts","tags":["argo","cloud","devoxx","docker","graphql","java","jvm","jwt","kubernetes","oauth-2.0","tls"],"title":"13 prises de notes à Devoxx France 2022"},{"body":"","link":"https://javaetmoi.com/tags/argo/","section":"tags","tags":null,"title":"Argo"},{"body":"","link":"https://javaetmoi.com/tags/cloud/","section":"tags","tags":null,"title":"Cloud"},{"body":"","link":"https://javaetmoi.com/tags/graphql/","section":"tags","tags":null,"title":"Graphql"},{"body":"","link":"https://javaetmoi.com/tags/jvm/","section":"tags","tags":null,"title":"Jvm"},{"body":"","link":"https://javaetmoi.com/tags/jwt/","section":"tags","tags":null,"title":"Jwt"},{"body":"","link":"https://javaetmoi.com/tags/kubernetes/","section":"tags","tags":null,"title":"Kubernetes"},{"body":"","link":"https://javaetmoi.com/tags/oauth-2.0/","section":"tags","tags":null,"title":"Oauth-2.0"},{"body":"","link":"https://javaetmoi.com/tags/tls/","section":"tags","tags":null,"title":"Tls"},{"body":"Contexte De nos jours, il est courant de devoir consommer une API REST sécurisée à l’aide du standard OAuth 2.0 ou de sa surcouche OpenID Connect (OIDC).\nSchématiquement, le consommateur génère un jeton (token) opaque ou JWT en appelant un serveur d’autorisation (Authorization server) puis, à chaque appel d’API REST, le transmet en tant que bearer via l’ en-tête HTTP Authorization. Ce token a souvent une durée de vie transmise par le serveur d’autorisation via la propriété expires_in.\nOAuth 2.0 propose quatre cinématiques (flows), la plus commune étant l’Authorization Code Flow. Lorsque l’API REST est appelée depuis une application web, il est courant de voir utiliser le Client Credentials Flow ou le Resource Owner Password Credentials Flow.\nRécemment, j’ai été amené à consommer l’API REST du CRM Salesforce depuis une application Spring Boot. Cette API était sécurisée avec le Resource Owner password Credentials Flow. Salesforce joue à la fois le rôle de l’Authorization Server et du Resource Owner. Le client (l’application Spring Boot) transmet ses credentials (login et mot de passe) à l’Authorization Server pour obtenir un Access Token.\nCet article a pour objectif de vous présenter la configuration Spring Security mise en œuvre pour appeler cette API. Les extraits de code proviennent du repository GitHub arey/spring-security-oauth2-salesforce-sample.\nStack technique L’appel d’une API sécurisée avec OAuth 2.0 depuis une application Java reposant sur Spring Boot peut être implémenté de plusieurs façons : une authentification maison à l’aide d’un bon vieux Apache HttpClient ou de Spring RestTemplate, l’utilisation d’une librairie tierce (ex : MITREid ou Nimbus) ou bien encore de Spring Security. L’utilisation de Spring Security s’est tout naturellement imposée car elle s’intègre parfaitement à la stack technique existante et était déjà utilisée dans l’application pour sécuriser ses propres API.\nDepuis sa version 5, Spring Security permet d’intégrer des services sécurisés avec OAuth 2.0. Il n’est plus nécessaire d’utiliser le projet Spring Security OAuth qui a été déprécié.\nLe module spring-security-oauth2-client contient le code client supportant OAuth 2.0 et OIDC. Son package racine est org.springframework.security.oauth2.client.\nSpring Boot vient avec un starter facilitant l’intégration de ce module : spring-boot-starter-oauth2-client.\nEn résumé, cet exemple s’appuie sur Spring Boot 2.5 et Spring Security 5.5.\nDépendances Maven Pour appeler une API REST sécurisée avec OAuth 2.0, la documentation de référence de Spring Security encourage l’utilisation de Spring WebClient provenant du module spring-webflux. De nombreux exemples trouvés sur le Net vont dans ce sens. L’usage de RestTemplate passe sous silence. D’après Stackoverflow, moyennant la création de l’intercepteur OAuthClientCredentialsRestTemplateInterceptor, pour celles et ceux qui préfèrent, il semble néanmoins possible de continuer à utiliser RestTemplate.\nSur une application Spring MVC (non réactive) qui utilise WebClient avec des appels bloquants, voici les modules Spring Boot à déclarer dans le pom.xml de Maven :\n\u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter-web\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter-webflux\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter-security\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter-oauth2-client\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; Remarque : le token Salesforce étant opaque, nul besoin d’ajouter la dépendance spring-security-oauth2-jose.\nConfiguration Spring Boot Dans le fichier de configuration Spring Boot application.yml, on déclare un client nommé salesforce et un provider du même nom (rappelez-vous, Salesforce joue le rôle d’Authorization Server et du Resource Owner). L’ authorization-grant-type est de type password (se référer à la classe AuthorizationGrantType pour une liste exhaustive des constantes) et le client-authentification-method est valorisé avec client_secret_post (se référer à la classe ClientAuthenticationMethod).\n# Configuration of the Salesforce CRM myapp: salesforce: host: https://${SALESFORCE_SUBDOMAIN}.salesforce.com # Change vXX.X version if required base-path: ${myapp.salesforce.host}/services/data/v53.0 # Replace the your-resource-path placeholder by your own resource path resource-path: /sobjects/${SALESFORCE_RESOURCE_PATH}/{id} spring: security: oauth2: client: registration: salesforce: provider: salesforce client-authentication-method: client_secret_post authorization-grant-type: password client-id: ${CLIENT_ID} client-secret: ${CLIENT_SECRET} username: ${USERNAME} password: ${PASSWORD} provider: salesforce: token-uri: ${myapp.salesforce.host}/services/oauth2/token Les variables en majuscule peuvent être changées / hardcodées à votre guise ou bien passées sous forme de variables d’environnements. Elles dépendent pour la plupart de l’environnement dans lequel l’application est déployée.\nLa classe OAuth2ClientProperties charge tous les clients définis dans le fichier application.yml ou application.properties. Les clients doivent être préfixés par le préfixe : spring.security.oauth2.client.\nLa cinématique Resource Owner Password n’est pas (encore ?) pleinement supporté par Spring Boot 2.5 dans la mesure où les propriétés username et password ne sont pas directement mappées dans la classe OAuth2ClientProperties de Spring Boot. Dans notre exemple, on réutilise le préfixe spring.security.oauth2.client pour les déclarer au même niveau que le client-id et le client-secret.\nDéclaration du bean salesforceWebClient La classe de configuration Spring OAuth2ClientConfig déclare le bean salesforceWebClient de type WebClient. Si besoin, d’autres clients pourraient y être ajoutés.\nLe filtre ServletOAuth2AuthorizedClientExchangeFilterFunction est utilisé lors de la construction de WebClient via le WebClient.Builder.\nLe bean authorizedClientManager construit un OAuth2AuthorizedClientProvider supportant le grant_type=password. Une spécificité consiste à tester le nom du client (ici salesforce) pour ajouter dynamiquement username et password au contexte d’autorisation OAuth2AuthorizationContext.\n@Configuration public class OAuth2ClientConfig { private static final String SALESFORCE_CLIENT_NAME = \u0026#34;salesforce\u0026#34;; private static final String CLIENT_PROPERTY_KEY = \u0026#34;spring.security.oauth2.client.registration.\u0026#34;; @Autowired private Environment env; @Bean public OAuth2AuthorizedClientManager authorizedClientManager( ClientRegistrationRepository clientRegistrationRepository, OAuth2AuthorizedClientService authorizedClientService) { OAuth2AuthorizedClientProvider authorizedClientProvider = OAuth2AuthorizedClientProviderBuilder.builder() .password() .build(); // Using AuthorizedClientServiceOAuth2AuthorizedClientManager instead of the DefaultOAuth2AuthorizedClientManager // to support asynchrone execution through the @Async annotation AuthorizedClientServiceOAuth2AuthorizedClientManager authorizedClientManager = new AuthorizedClientServiceOAuth2AuthorizedClientManager( clientRegistrationRepository, authorizedClientService); authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider); authorizedClientManager.setContextAttributesMapper(oAuth2AuthorizeRequest -\u0026gt; { if (SALESFORCE_CLIENT_NAME.equals(oAuth2AuthorizeRequest.getClientRegistrationId())) { HashMap\u0026lt;String, Object\u0026gt; map = new HashMap\u0026lt;\u0026gt;(); map.put(OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME, getProperty(SALESFORCE_CLIENT_NAME, \u0026#34;username\u0026#34;)); map.put(OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME, getProperty(SALESFORCE_CLIENT_NAME, \u0026#34;password\u0026#34;)); return map; } return null; } ); return authorizedClientManager; } @Bean public WebClient salesforceWebClient(OAuth2AuthorizedClientManager authorizedClientManager) { // May use a ServerAuth2AuthorizedClientExchangeFilterFunction in a reactive stack ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2Client = new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager); oauth2Client.setDefaultClientRegistrationId(SALESFORCE_CLIENT_NAME); return WebClient.builder() .baseUrl(env.getProperty(\u0026#34;myapp.salesforce.base-path\u0026#34;)) .apply(oauth2Client.oauth2Configuration()) .build(); } private String getProperty(String client, String property) { return env.getProperty(CLIENT_PROPERTY_KEY + client + \u0026#34;.\u0026#34; + property); } } Utilisation du bean salesforceWebClient Une fois configuré, le bean salesforceWebClient peut être utilisé comme tout WebClient, sans se soucier du mécanisme d’authentification.\nExemple :\n@Component public class SalesforceClient { private static final Logger LOG = LoggerFactory.getLogger(SalesforceClient.class); @Autowired private WebClient salesforceWebClient; @Value(\u0026#34;${myapp.salesforce.resource-path}\u0026#34;) private String resourcePath; public String upsertResource(String resourceId, String jsonRequest) { Mono\u0026lt;String\u0026gt; response = salesforceWebClient .patch() .uri(uriBuilder -\u0026gt; uriBuilder .path(resourcePath) .build(resourceId)) .contentType(MediaType.APPLICATION_JSON) .bodyValue(jsonRequest) .retrieve() .onStatus(HttpStatus::isError, errorResponse -\u0026gt; { logErrorBody(errorResponse); return Mono.error(new RuntimeException(String.format(\u0026#34;Salesforce request on error status=%s, headers=%s\u0026#34;, errorResponse.statusCode(), errorResponse.headers().asHttpHeaders()))); }) .bodyToMono(String.class); return response.block(); } public static void logErrorBody(ClientResponse response) { if (LOG.isErrorEnabled()) { response.bodyToMono(String.class) .publishOn(Schedulers.boundedElastic()) .subscribe(body -\u0026gt; LOG.error(\u0026#34;Body of the #Salesforce error response: {}\u0026#34;, body)); } } } Lors de l’appel à response.block(), la méthode ServletOAuth2AuthorizedClientExchangeFilterFunction::filter est appelée. Lors du premier appel, elle délègue l’authentification OAuth 2.0 à la classe DefaultPasswordTokenResponseClient. En coulisse, un RestTemplate est utilisé pour réaliser l’appel POST HTTP et récupérer l’access token :\nPOST https://\u0026lt;your_subdomain\u0026gt;.salesforce.com/services/oauth2/token? grant_type=password \u0026amp;username=... \u0026amp;password=... \u0026amp;client_id=… \u0026amp;client_secret=… Le test d’intégration SalesforceClientIntegrationTest permet de vérifier que tout fonctionne.\nPour aller plus loin Dans cet article, au travers d’exemples de code extraits du repo GitHub spring-security-oauth2-salesforce-sample, nous avons vu comment implémenter la cinématique OAuth 2.0 Resource Owner Password Credentials dans une application Spring Boot 2.5 avec Spring Security 5.5.\nCette implémentation n’est pas parfaite dans le sens où Salesforce ne renvoie malheureusement pas l’en-tête expires_in recommandée par la RFC-6749 et précisant la durée de validation de l’access token (en général de 2h, mais sans garantie). En son absence, la méthode getExpiresAt() de la classe OAuth2AccessTokenResponse de Spring Security le calcule en ajoutant une seconde au timestamp issued_at. Vous l’aurez compris, l’authentification OAuth 2.0 se fera presque à chaque requête. Dans mon cas métier, ce n’était pas trop préoccupant car les appels Salesforce étaient peu fréquents et en asynchrone. Je vous laisse réfléchir à ce qu’il en est pour vous ? Selon le site d’Xkit, il semblerait que Salesforce expose une API OAuth 2.0 d’introspection permettant de connaître l’état du jeton et donc sa validité. Resterait à trouver comment brancher cet appel dans Spring Security.\nRessources :\nOAuth 2 Resource Owner Password Credentials Flow (Marmicode) Getting Started with OAuth 2.0 by Ryan Boyd - Chapter 4. Resource Owner Password Flow (Oreilly library) Using Spring Security 5 to integrate with OAuth 2-secured services such as Facebook and GitHub (Spring Blog) Spring Security OAuth 5.2 Migration Sample (GitHub) Spring Security 5 Replacement for OAuth2RestTemplate (StackOverflow) Spring Security OAuth 2.0 client configuration for Salesforce (GitHub) OAuth 2.0 Username-Password Flow for Special Scenarios(Salesforce documentation) REST API Developer Guide (Salesforce documentation) When do Salesforce access tokens expire? (Xkit) ","link":"https://javaetmoi.com/2021/11/configuration-spring-security-dun-client-de-lapi-rest-salesforce-securisee-avec-oauth-2-0/","section":"posts","tags":["oauth-2.0","salesforce","spring-security"],"title":"Configuration Spring Security d’un client de l’API REST Salesforce sécurisée avec OAuth 2.0"},{"body":"","link":"https://javaetmoi.com/tags/salesforce/","section":"tags","tags":null,"title":"Salesforce"},{"body":"","link":"https://javaetmoi.com/tags/spring-security/","section":"tags","tags":null,"title":"Spring-Security"},{"body":" Publier en 2021 un article sur les logs n’est pas très novateur ; je vous l’accorde. Le logging est une pratique vieille comme l’informatique, ou presque. C’est une pratique universelle qu’on retrouve quel que soit le langage de programmation et quel que soit le type d’application. Pour autant, elle est survolée en fac et en école d’ingénieur. Les dévs apprennent bien souvent à logger sur le tas, en fonction de leurs besoins et de ce qui est déjà mis en place sur leur application. Rares sont également les entreprises mettant à disposition des normes et des bonnes pratiques en termes de traces applicatives.\nDans cet article, je ne vous expliquerai pas comment utiliser SLF4J, Logback, Log4j 2 ou la controversée API de Logging du langage Java. C’est un prérequis que bon nombre d’entre vous connaissent déjà. Beaucoup de ressources existent à ce sujet, en commençant par leurs documentations officielles.\nNon, je vous y exposerai plutôt les bonnes pratiques que je préconise, tant au niveau d’une application que d’une organisation. Je répondrai également aux questions les plus courantes : quand utiliser tel ou tel niveau de log ? que mettre dans les messages de logs ?\nNom de mon blog oblige, j\u0026rsquo;utiliserai des exemples venant du monde Java. Mais vous pourrez aisément transposer ces bonnes pratiques à d’autres technologies. Et bien entendu, elles sont à adapter en fonction de votre contexte et de vos besoins.\nObjectifs des logs Les logs permettent d’ historiser les évènements normaux et anormaux survenus au cours du fonctionnement de logiciels, d’équipements réseaux ou bien encore d’applications. Dans la suite de cet article, je me focaliserai sur les logs applicatifs qui sont les logs produits par les applications métiers que vous développez.\nLes logs applicatifs sont utiles à divers moments du cycle de vie d\u0026rsquo;une application :\nEn phase de développement, les logs sont complémentaires au debugger et permettent de comprendre le fonctionnement d’une application en suivant pas à pas le fil d\u0026rsquo;exécution de différentes fonctions critiques. En phase d\u0026rsquo; intégration et de recette, ils permettent de faciliter l\u0026rsquo;analyse des anomalies remontées par la QA. Enfin, en phase d\u0026rsquo; exploitation, les logs peuvent permettre de diagnostiquer des problèmes de prod remontés par le service utilisateur ou l’équipe MCO. De manière proactive, il est également possible de configurer des alertes sur des patterns d’erreur détectés dans les logs. Centraliser les logs Sur le poste de dév, logger dans la console ou dans un fichier est courant. En un coup d’œil, le développeur peut s’y référer.\nSur les autres environnements, il est recommandé d’utiliser un système centralisé de collecte de logs, chargé de les indexer et proposant une IHM et une API REST de recherche. En cas d’incident, cela évite de se connecter en SSH sur les 15 nœuds de votre cluster pour trouver le fichier qui vous intéresse.\nDepuis une dizaine d’années, ont émergés de nombreux systèmes de collecte appelés SIEM (Security Information and Event Management). Splunk est un système propriétaire. La stack Elasticsearch / Logstash - Beats / Kibana ( ELK) est Open Source.\nLes logs générés par les applicatifs sont soit directement envoyés à ces systèmes par le réseau (risque de perte de logs en cas d’indisponilité du SIEM), soit générés temporairement sur le système de fichier puis ingérés via un collecteur préalablement installé sur l’hôte.\nLa collecte des logs n’est pas l’apanage des applications back. Une application front basée sur Angular peut par exemple envoyer ses logs au SIEM ou à son Backend for Frontend via un framework comme NGX Logger.\nNormaliser les données de logs A l’échelle d’une entreprise, l’utilisation d’un SIEM est recommandée. La collecte des logs applicatifs dans un SIEM est facilitée par la normalisation des données de logs et de leur format.\nParmi les données courantes, on retrouve couramment la date et l’heure de l’évènement, le niveau de log, le message de log, le nom du logger et la stacktrace en cas d’erreur.\nD’autres données peuvent enrichir ses logs et faciliter les recherches ultérieures : login de l’utilisateur authentifié, nom de l’ application, nom du serveur, identifiant de corrélation, nom du thread …\nD’une application à une autre, il est intéressant d’utiliser le même pattern de log afin de simplifier l’ingestion des lignes de logs par un collecteur Splunk ou un Logstash.\nPour aller un cran plus loin, je vous recommande de normaliser le format de sortie. Utiliser du JSON et des noms de champs standardisés (ex : \u0026ldquo;level\u0026rdquo; pour le niveau de log) permet en effet d’indexer directement vos logs dans des index Splunk ou Elasticsearch, sans prétraitement et ceci, si vous le souhaitez, dans le même index. L’enrichissement des logs reste possible.\nApplications Java, Python, Node ou bien encore .NET peuvent adopter les mêmes conventions.\nNormaliser le nom des champs dans un document accessible aux développeurs et aux Ops (ex : un wiki ou un sharepoint).\nEn Java, si vous utilisez Logback, je vous recommande d’utiliser le projet Logstash Logback Encoder pour formater des logs en JSON. Bien que conçu initialement pour Logstash, son encoder JSON est compatible avec Splunk. Ce projet vient avec un certain nombre de Standard Fields (ex : @timestamp, @version, message); il est possible de les renommer et d’en ajouter.\nVoici un exemple de log au format JSON :\n{ \u0026#34;@timestamp\u0026#34;: \u0026#34;2021-01-10T12:47:41.234+01:00\u0026#34;, \u0026#34;@version\u0026#34;: 1, \u0026#34;message\u0026#34;: \u0026#34;Contract created id=3d2b6902-d25d-4d98-b260-3733b880c2ea\u0026#34;, \u0026#34;loggerName\u0026#34;: \u0026#34;com.mycompagny.myapp.web.ContractController\u0026#34;, \u0026#34;threadName\u0026#34;: \u0026#34;ajp-/127.0.0.14:8009-1\u0026#34;, \u0026#34;level\u0026#34;: \u0026#34;INFO\u0026#34;, \u0026#34;levelValue\u0026#34;: 20000, \u0026#34;httpSessionID\u0026#34;: \u0026#34;yYOvS0RKBM5y7NOrJQYAmGwA\u0026#34;, \u0026#34;req\u0026#34;: { \u0026#34;method\u0026#34;: \u0026#34;POST\u0026#34;, \u0026#34;remoteHost\u0026#34;: \u0026#34;10.152.34.13\u0026#34;, \u0026#34;requestURI\u0026#34;: \u0026#34;/api/v1/contract\u0026#34;, }, \u0026#34;user\u0026#34;: \u0026#34;jdoe\u0026#34;, \u0026#34;transactionID\u0026#34;: \u0026#34; a94fb8af-6587-4283-a8bf-69ba98d0da72\u0026#34;, \u0026#34;app\u0026#34;: { \u0026#34;code\u0026#34;: \u0026#34;MYAPP\u0026#34;, \u0026#34;env\u0026#34;: \u0026#34;Production\u0026#34; } } De l\u0026rsquo;importance du niveau de log Générer des logs a un impact sur les performances de l\u0026rsquo;application, le trafic réseau si un SIEM est utilisé, et l\u0026rsquo; espace disque nécessaire à leur rétention.\nIl y\u0026rsquo;a un compromis à trouver entre verbosité des logs et espace disque : trop de logs noient les logs importants et saturent le système (leur durée de rétention est alors plus faible), trop peu de logs nuit à l\u0026rsquo;exploitabilité de l\u0026rsquo;application.\nLes niveaux de logs permettent d\u0026rsquo;adresser cette problématique. En effet, en fonction de l\u0026rsquo;environnement, la charge est différente : sur l\u0026rsquo;environnement de dév, le développeur est seul alors qu\u0026rsquo;en prod il peut y avoir des milliers/millions d\u0026rsquo;utilisateurs. Une bonne pratique consiste à configurer les niveaux de logs différemment d\u0026rsquo;un environnement à l\u0026rsquo;autre.\nDans le monde Java, la façade de logging SLF4J propose 5 niveaux de logs : TRACE, DEBUG, INFO, WARN et ERROR.\nLe logger affiche toutes les traces niveaux supérieurs ou égal au niveau sélectionné. Par exemple si sur l\u0026rsquo;environnement d\u0026rsquo;intégration, le logger est configuré en niveau DEBUG, les traces de niveau DEBUG/INFO/WARN/ERROR seront affichées ; les traces de niveau TRACE sont ignorées.\nA noter que le niveau de log peut être configuré au niveau de chaque logger. Un niveau par défaut est positionné au niveau du logger racine (root).\nLe niveau de log applicatif correspond au niveau de log du package Java racine de votre application (ex: com.mycompany.myapp).\nNiveau de log applicatif en fonction de l\u0026rsquo;environnement En production et pré-production, il est recommandé de positionner le niveau de log applicatif à INFO. Ne tracer que les WARN et ERROR masquerait les logs précédents pouvant être utiles à l’interprétation de l\u0026rsquo;erreur. Les logs INFO trop verbeux sont à abaisser en DEBUG. Le niveau de log comme Spring et Hibernate peut également être positionné en INFO. Le niveau de logs des frameworks très verbeux est relevé à WARN ou ERROR (ex : Atomikos).\nEn intégration et en recette, le niveau DEBUG est souvent pertinent. Il évite de devoir redémarrer le serveur d’application (ex : un JBoss) pour baisser le niveau de log ou faire appel au runtime à l’actuator /actuator/loggers d’une application Spring Boot.\nSur le poste de dév, vous pouvez alterner entre INFO, DEBUG ou TRACE en fonction de vos tâches.\nTypes de log par niveau Le tableau ci-dessous précise le niveau de gravité à utiliser en fonction de la nature de l’évènement que l\u0026rsquo;on souhaite tracer :\nNiveauUsageExemples****TRACEUtilisé pour le débogage fin en mode verbeux de l\u0026rsquo;application.\nPar expérience, ce niveau est très peu utilisé par les développeurs, mais pourrait l’être davantage.- Nouvelle valeur d\u0026rsquo;une variable\n- Résultat d\u0026rsquo;une évaluation conditionnelle\n- Trace d\u0026rsquo;exécution d\u0026rsquo;une boucle\n- Entrée / sortie d\u0026rsquo;une méthodeDEBUGUtilisé pour le débogage courant de l\u0026rsquo;application. Les informations loguées avec ce niveau intéressent plus particulièrement le développeur.- Flux entrées / sorties (ex: requêtes SQL / requêtes HTTP)\n- Données saisies invalidesINFOTraces fournissant des informations contextualisées sur le fonctionnement général de l\u0026rsquo;application et son utilisation.\nCes traces à destination du métier et de la MCO.\nLes logs d\u0026rsquo;audit sont également logués avec le niveau INFO.- Opérations en écriture qui ont un sens fonctionnel (ex: validation d’une commande, émission de contrat, génération de courrier)\n- Opération en lecture au cas par cas\n- Début et fin des grandes étapes d\u0026rsquo;un batch ou d\u0026rsquo;un traitement long\n- Sauvegarde et fermeture de fichiers\n- Connexion / déconnexion d\u0026rsquo;un utilisateurWARNTrace d\u0026rsquo;anomalies ne déstabilisant pas l\u0026rsquo;application et ne demandant pas une intervention immédiate (erreur d\u0026rsquo;ordre fonctionnel par exemple).\nProblème non bloquant ne faisant pas échouer la transaction métier. Permet de ne pas spammer les logs avec des logs de niveau ERROR.- Un paramétrage applicatif est manquant mais ce cas est prévu par le système (une valeur par défaut est appliquée, par exemple) et son fonctionnement n\u0026rsquo;est pas remis en cause\n- Erreur de loginERRORTrace d\u0026rsquo;anomalie signalant une erreur importante mais ne remettant pas en cause le fonctionnement général de l’application. Ce niveau de log signifie un arrêt de la requête/service en cours et fait généralement suite à une exception de type RuntimeException remontant au plus haut de la pile d’appel. Le niveau FATAL n\u0026rsquo;existant pas dans SLF4J, les erreurs critiques empêchant tout fonctionnement ultérieur de l\u0026rsquo;application sont également tracées avec le niveau ERROR.- Adhérence momentanément indisponible (ex: erreur 503 remontée lors de l’appel une API REST)\n- Erreur JDBC liée à une contrainte d\u0026rsquo;intégrité\n- Arrêt inattendu d\u0026rsquo;un batch (ex : filesystem saturé)\nContenu des messages des logs Un message de log se doit d\u0026rsquo;être lisible, explicite et comporter suffisamment d’informations pour être exploité. Il doit fournir des informations contextuelles que ne permet pas de donner par exemple une stacktrace (ex: numéro de contrat).\nLog sans intérêt :\nEchec du traitement Log exploitable :\nEchec du traitement numéro=123: checksum=23 invalide Lorsque vous ne pouvez pas structurer vos logs (en utilisant du JSON), il est intéressant d’utiliser des paires clé=valeur. Une plateforme comme Splunk peut automatiquement détecter ce pattern et extraire la clé et sa valeur.\nDonnées à ne pas logger En accord avec la RGPD et la CNIL, les données sensibles listées dans la catégorie A3:2017 Sensitive Data Exposure de l’OWAP ne doivent pas apparaître dans les logs.\nLes mots de passe Informations nominatives : nom, prénom, nom de naissance, numéro de sécurité sociale Coordonnées bancaires : IBAN, RIB, numéro de carte bancaire Informations de localisation : adresse postale, adresse IP, e-mail Données de santé, génétiques et biométriques Vous avez le choix entre ne pas les concaténer aux messages de logs ou bien de les masquer avec, par exemple, des wildcards ****. Cette seconde option a un cout sur les performances car elles utilisent souvent des regex.\nLe projet Logstash Logback Encoder propose un mécanisme basé sur le décorateur MaskingJsonGeneratorDecorator. Avec Logback, vous trouverez facilement différents exemples comme celui proposé par Dhaval Kolapkar.\nL’enregistrement systématique des flux REST et SOAP en production pose problème car il faut identifier les flux pouvant véhiculer certaines de ces informations et les masquer.\nÉtiqueter les logs Afin d\u0026rsquo;en simplifier la recherche et de leur donner un sens fonctionnel, il est possible d’étiqueter les messages de log à l\u0026rsquo;aide de hashtags comme sur Twitter.\nPar exemple, les pistes d’audit demandées par les PO dans vos User Story peuvent être étiquetées avec le hashtag #audit :\nLOG.info(\u0026#34;Envoi de la commande numero=\u0026#34; + order.getNumber() + \u0026#34; #audit\u0026#34;); Un autre exemple consiste à tagger les logs remontant des problèmes de performance (appels ou requêts longues) avec le hashtag #perf.\nAutres bonnes pratiques Pour terminer ce billet, voici quelques autres bonnes pratiques que je vous recommande d’adopter :\nNe pas logger 2x la même erreur : d\u0026rsquo;une manière générale, ne pas logger l\u0026rsquo;erreur lorsque l’exception interceptée est propagée ( throw dans un catch). L\u0026rsquo;exception doit être loggée en haut de la pile d\u0026rsquo;appel, en général dans un handler d\u0026rsquo;exceptions générique. Pertinence des messages des logs : un log de niveau info sans information contextuelle ne peut pas être exploité en production (ex: \u0026ldquo;Contrat émis\u0026rdquo;). Le message doit être contextualisé (ex: \u0026ldquo;Contrat émis id=123\u0026rdquo;). Pertinence des messages des erreurs : lorsqu\u0026rsquo;on encapsule une exception, le message doit apporter des informations supplémentaires sur le contexte d\u0026rsquo;appel (ex: numéro de client, étape de traitement \u0026hellip;) Activer le logging des enveloppes SOAP / body REST jusqu\u0026rsquo;en recette est pratique pour rejouer les flux posant soucis. Pensez à exclure les appels contenant des flux binaires (ex : téléchargement de fichiers). Login utilisateur : le login de l\u0026rsquo;utilisation authentifié avec un framework de sécurité (ex: Spring Security) doit systématiquement être ajouté en tête / métadonnées de log. Cela nécessite généralement un développement spécifique (ex : utilisation de filtre de servlet, du MDC de SLF4J et d’un PatternLayoutEncoder Logback). Dans l’exemple de log JSON précédent, on retrouve le login au niveau de la propriété user. Utiliser un identifiant de corrélation : pouvoir lier des lignes de logs entre elles est une fonctionnalité très appréciable. Pour la mettre en œuvre, créer un identifiant de corrélation de type UUID au début d’une transaction métier et ajouter systématiquement cet UUID aux métadonnées de logs (comme le login). Pour aller plus loin, vous pouvez passer cet identifiant de système en système via une en-tête http ou JMS (ex : X-Request-ID). Dans l’exemple de log JSON précédent, on retrouve cet identifiant au niveau de la propriété transactionID. Changement à chaud du niveau de logs : lors d’un incident de prod, il est parfois nécessaire d’abaisser temporairement le niveau de logs afin de qualifier le problème. Changer à chaud ce niveau de logs sans redémarrer l’application est souvent nécessaire. Certaines stacks techniques comme Spring Boot et son actuator levels le permettent facilement. L’utilisation de Spring Boot Admin en facilite l’usage. Pensez à sécuriser ces endpoints. Se prémunir du log forging: la vulnérabilité d’ injection de logs par ajout de caractères CRLF (\\r\\n) dans des paramètres HTTP loggés figure dans le Top 10 du classement 2017 des failles de sécurité de l’OSWAP. La configuration de votre framework de logging permet de substituer ces caractères et ainsi de se prémunir de cette vulnérabilité : à l’aide d’un Pattern Layout avec Log4J 2 ou d’un conversionRule avec Logback. Maitriser la volumétrie des logs : lorsque les logs sont consignés dans des fichiers de log, il est nécessaire de dimensionner le filesystem en regard de la volumétrie maximum des logs, sans quoi vous pourriez perdre des logs (sans espace disque, plus de logs). Pour diminuer la taille des logs, ces derniers peuvent être historisés tout en étant compressés (une archive d\u0026rsquo;un fichier de 10 Mo occupe généralement moins de 500 Ko). Lorsque les logs sont indexés dans un SIEM, les fichiers servent alors de tampons. Leur dimensionnement permet de palier à l\u0026rsquo;indisponibilité du SIEM. C\u0026rsquo;est la taille de l\u0026rsquo;index Elasticsearch ou Splunk qui conditionne la durée de rétention des logs. Configurer son IDE pour déclarer rapidement le logger d’une classe, par exemple sous IntelliJ avec un Live Template. Conclusion Dans cet article, je vous aurais présenté des bonnes pratiques de logs à appliquer ou adapter au niveau de votre application et de votre organisation.\nDisposer de logs pertinentes et utiles nécessite un travail de fond. Il est rarement possible de viser juste du premier coup. Les logs se retravaillent et s’affinent sur plusieurs itérations. Analyser les logs de recette et de prod permet d’améliorer les logs existants en les complétant ou en rajoutant du contexte. Les incidents de prod sont également un moyen de vérifier si vos logs sont exploitables et de corriger le tir si besoin est.\nRessources :\nLe logging (Jean-Michel Doudoux) Comparatif de solutions SIEM : Splunk et ELK (Maxime Piazzola) Log tagging creates smarter application logs #awesomelogs (Alexandra Altaver) Logging best practices in an app or add-on for Splunk Enterprise Changing de log Logging Level at the Runtime for a Spring Boot Application (Baeldung) Log Forging by CRLF Injection (Abhijit Ghosh) Mask sensitive data in logs (Dhaval Kolapkar) ","link":"https://javaetmoi.com/2021/01/bonnes-pratiques-de-logging/","section":"posts","tags":["elasticsearch","logback","logstash","slf4j","splunk"],"title":"Bonnes pratiques de logging"},{"body":"","link":"https://javaetmoi.com/tags/elasticsearch/","section":"tags","tags":null,"title":"Elasticsearch"},{"body":"","link":"https://javaetmoi.com/tags/logback/","section":"tags","tags":null,"title":"Logback"},{"body":"","link":"https://javaetmoi.com/tags/logstash/","section":"tags","tags":null,"title":"Logstash"},{"body":"","link":"https://javaetmoi.com/tags/slf4j/","section":"tags","tags":null,"title":"Slf4j"},{"body":"","link":"https://javaetmoi.com/tags/splunk/","section":"tags","tags":null,"title":"Splunk"},{"body":" Lors de l’excellente conférence DevFest Paris 2020 qui s’est tenue le 14 février au Palais des Congrès d’Issy-les-Moulineaux, j’ai découvert une typologie de tests dont je n’avais jamais entendu parler : les tests de propriétés(property based tests en anglais).\nPendant 2h, Thomas Haessle (CTO de Cutii) et Julien Debon (Tech Lead chez Décathlon) nous ont fait travailler sur un Code lab disponible en pas moins de 5 langages de programmation : JavaScript, Java, OCaml, Haskell et Rust. Comme vous vous en doutez, j’ai suivi le lab Java.\nLes quelques slides de leur introduction sont disponibles sur Google Docs. Le projet GitHub du lab Troll of Fame contenant les 5 repos se trouve quant à lui ici : trollaklass\nAu travers de ce cours billet, je tenais à mettre en avant leur travail et à partager mon enthousiasme. De chez vous, n’hésitez pas à suivre ce Lab pour vous familiariser avec les tests de propriétés. Le README.md contient l’énoncé des 6 étapes et l’explication des concepts associés. Comme son nom l’indique, la branche solution contient l’ensemble des solutions.\nLe concept Les tests de propriétés nous viennent des langages fonctionnels comme Haskell. Certains ne disposent d’ailleurs que de ce type de test.\nPour nous faire comprendre la différence entre nos tests unitaires habituels et les tests de propriétés (même si de mon point de vue, les tests de propriétés peuvent être considérés comme des tests unitaires), Julien prend l’exemple de la saint Sylvestre.\nPour tester si un jour du calendrier correspond à la St-Sylvestre, un test unitaire comporterait plusieurs scénarios de test avec des dates différentes : 31/12/2020, 31/01/2019, 30/11/2019 … Un test de propriétés accepterait n’importe quelle date et vérifierait le jour et le mois. En effet, quel que soit la date donnée, la St-Sylvestre tombe toujours le 31 décembre, peu importe l’année.\nUn autre exemple emprunté sur le site de JUnit Quickcheck consiste à tester un algorithme de chiffrement / déchiffrement à l’aide d’une clé symétrique : quel que soit la clé et le texte à chiffrer, le chiffrement du texte puis le déchiffrement du texte chiffré doit retourner le texte initial.\nLes tests de propriétés ne fixent pas les données de tests. Ces derniers sont générés aléatoirement.\nVoici une synthèse des différences : Tests unitaires****Tests de propriétésJeu de données fixeJeu de données aléatoireUne seule exécutionBeaucoup d’exécutionsRègles d’assertion (ex : true, 42, « toto »)Règles d’assertion ou comportement\nLe Lab Troll of Fame L’objectif du Lab consiste à ajouter des tests de propriétés sur le logiciel Troll of Fame, sachant que tous les TU sont au vert. Certains tests de propriétés vont révéler des bugs d’implémentation qu’il faudra corriger.\nCommencez par repo Git https://github.com/trollaklass/troll-of-fame-java puis suivez les instructions du README.md.\nLes dépendances tirées par le build Gradle build.gradle.kts dévoilent la stack technique utilisée :\nJUnit pour les TU AssertJ pour les assertions JUnit Quickcheck pour les tests de propriétés Lombok pour diminuer le code technique Vavr pour utiliser des structures immuables Google Auto Service pour détecter les générateurs de paramètres (annotés avec @AutoService) et les mettre à disposition de JUnit Quickcheck Les 2 classes ElfGen et TrollGen seront utilisés par Quickcheck pour générer des jeux de données aléatoires.\nPar défaut, les tests par propriétés annotés avec l’annotation @com.pholser.junit.quickcheck.Property seront exécutés avec 100 jeux de données différents.\nLorsque le test échoue, un nombre aléatoire d’amorce (radom seed) est généré afin de pouvoir reproduire le cas de test :\nTrollProp \u0026gt; invariance FAILED java.lang.AssertionError: Property named \u0026#39;invariance\u0026#39; failed ( Expecting: \u0026lt;0\u0026gt; to be greater than or equal to: \u0026lt;1\u0026gt; ) With arguments: [Troll(name=abc, killList=HashMap())] Seeds for reproduction: [-8851778975433212269] Caused by: java.lang.AssertionError: Expecting: \u0026lt;0\u0026gt; to be greater than or equal to: \u0026lt;1\u0026gt; Lorsqu’un build Jenkins casse, on peut récupérer le seed. Lors de TDD, on peut fixer la seed afin d’utiliser dans un premier temps le même jeu de données.\nConclusion Les différents exercices du Lab permettent d’implémenter différents tests de propriétés : invariance, inversion, analogie, idempotence, métamorphisme et injection.\nL’utilisation de jeux de données aléatoires permet de couvrir davantage de cas de tests. Le mutation testing perd de l’intérêt. De l’aveu de Julien, l’usage de TU reste néanmoins nécessaire pour tester les cas limites (ex : division par zéro).\nEnfin, l’utilisation d’ objets immutables(via Vavr) prend tout son sens avec les tests par propriétés car on compare souvent les objets entre eux, ce qui nécessite de ne pas modifier le jeu de données passé en paramètre.\n","link":"https://javaetmoi.com/2020/02/tests-de-proprietes-ecrire-moins-de-tests-trouver-plus-de-bugs/","section":"posts","tags":null,"title":"Tests de propriétés : écrire moins de tests, trouver plus de bugs"},{"body":"","link":"https://javaetmoi.com/tags/azure/","section":"tags","tags":null,"title":"Azure"},{"body":" Assurée par un formateur Microsoft, la formation « Microsoft Azure Fondamentals » se déroule sur une journée et permet de se préparer à la certification AZ-900 « Microsoft Azure Fondamentals ». A l’issue de la formation, un voucher est donné à chaque participant. Ces derniers sont invités à passer leur certification dans la foulée.\nLa formation est découpée en 4 modules. Les notes ci-dessous m’auront permis d’obtenir ma certification du premier coup. J’espère qu’elles vous aideront à vous préparer. En rouge, sont notés les mots clés, concepts et noms de produits à retenir par cœur.\nCertification AZ-900 Certification AZ-900 : passage obligatoire pour toutes les autres certifications Azure (ex : AZ-203 : Developing Solutions for Microsoft Azure) Durée : 1h Score nécessaire : \u0026gt;= 700 / 1000 Entre 40 et 44 questions Les 5 à 7 premières questions : on ne peut pas revenir sur ses réponses L’examen est en anglais, chinois ou japonais. Pour vous y préparer, vous pouvez suivre le parcours d’apprentissage « Principes de base d’Azur ».\nModule 1 – Cloud Concepts Pourquoi : agilité, scalability / élasticité, sécurité, haute disponibilité / high availability (SLA), fault tolerance, on demand self-service, accessiblité / global reach (accessible de n’importe où), disaster recovery, customer latency capabilities, maitrise des coûts / predictive cost considerations\nPassage du modèle CapEx vers OpEx\nCapEx : Capidatal Expanditure: modèle basé sur le capital / les serveurs, un ensemble de serveurs et d’Ops =\u0026gt; On-premise OpEx : Operational Expanditure modèle du cloud computing. Le client n’achète plus les machines, mais les services Consumption-based model On ne paie que ce qu’on utilise : pay-as-you-go : on paie la consommation des services. Lorsqu’on arrête la VM, on paie quand même le stockage. Lors de la scalabilité, on ne paie plus les ressources qu’on n’utilise plus. Dans tous les modèles, la responsabilité est partagée par les 2 : Microsoft et le client Accès au portail Azure : [portal.azure.com](https://portal.azure.com/\" \\l \u0026ldquo;home) Comparaison des modèles Cloud\nPublic Cloud : Propriété du fournisseur Accessible depuis Internet Cloud Privé de Microsoft : Azure Stack Fourni par le Cloud Provider La sécurité physique des ressources est gérée par le client Non accessible sur Internet Cas d’usage : besoin accrue en termes de sécurité (ex : ministère de la Défense) Cloud Hybrid : Combinaisons des clouds publics et privés Ex : site web sur Cloud public et base de données sur Cloud privé Les clients gèrent la sécurité D’après l’expérience de la formatrice, 90% des clients combinent On-Promise et Cloud avec plusieurs fournisseurs.\nTypes of services\nResponsabilités :\nIaaS (VM) : Microsoft gère le stockage, le réseau et le matériel. Le client gère l’OS, les applications PaaS (Azure Function ou Azure Webapp) : Microsoft gère la maj de l’OS, le client gère les données, les accès (AD en local ou Azure) et l’application SaaS (Office 365) : applications hébergées dans Azure que le client a besoin de configurer : l’admin doit configurer les accès à Outlook et Teams Module 2 – Core Azure Services Regions\nUne région = une collection de Data Center 54 régions représentant 140 pays\nUn Data Center et une Région existe en France (pas le cas en Belgique et en Allemagne)\nRecommandation : utiliser les régions les plus proches de l’utilisateur à cause de la latence réseau et du coût Region Pairs\nChaque région est associée à une autre région pour assurer la bascule en cas de panne (sauf le Brésil).\nGéographies\n5 zones géographiques\nAvailability Options\nSLA 99,9% : Single VM avec stockage Prenium (géré par Microsoft) SAL de 99,95% : créer des availability sets VML SLA : 99,99% : créer des availability zones Availabity Sets : lorsqu’on crée une VM, une bonne pratique consiste à le créer dans un Availability Sets dans le même datacenter pour avoir :\nUpdate Domains (UD) : mise à jour logiciel Fault domains (FD) : pb d’alimentation, gère les problèmes physiques Availability zones : permet de remédier un pb arrivant sur un Data Center. On reste dans la même région\nResource groups\nPermet d’organiser les projets. N’a pas de lien avec la scalabilité Conteneurs de ressources (ex : web app, VM, stockage) Nécessaire lors de la création d’une ressource Une ressource n’existe que dans un groupe de ressource. On peut la déplacer d’un groupe de ressources à un autre Les ressources d’un groupe peuvent être créées sur des régions différentes. Utile lorsqu’un service n’existe pas sur la région privilégiée Permet de définir des droits sur un groupe de ressources : Role Based Access Control (RBAC). On définit les droits à l’aide de rôles. Une personne a les mêmes droits sur toutes les ressources du groupe Azure Resource Manager\nPermet de déployer simultanément plusieurs ressources identiques en utilisant un template ARM (au format JSON) : pour déployer 50 VM, on déploie le template 50x. Terraform permet de déployer les templates en multi-Cloud Permet d’automatiser la création des ressources Souscriptions : pour travailler sur Azure, il faut avoir un compte Azure. Ce compte est soit rattaché à un compte pro, soit à une adresse perso. Sous le compte Azure, on retrouve 1 ou plusieurs souscriptions. La souscription gère la partie contractuelle. Certains clients ont une souscription par département (ex : finance, IT). Permet de gérer la partie facturation. Chaque souscription a sa propre facturation. L’administrateur gère les souscriptions. Management groups : services Azure donnant les droits différents sur des souscriptions différentes Azure Compute\nVirtual machines Virtual machines scale sets : scalabilité des VM, permet d’augmenter le nombre d’instances de VM. On définit les conditions d’augmentation du nombre d’instances en fonction du taux d’utilisation du CPU et/ou de la RAM. On peut le configurer dans le template ARM Function App : server less, bout de code exécuté sans serveur derrière, un bout de code par fonctionnalité (ex : inscription, réservation, paiement, envoi de mail de confirmation). Le déclenchement d’une fonction est déclenchée en fonction d’un événement (trigger). Exemple : ajout d’une ligne en base de données. Architecture servless basée sur les évènements. Gain de coût : un client se trouvant sur la page d’inscription ne va pas solliciter la fonctionnalité de réservation. App Services : PaaS permet d’héberger une webapp dans Azure Kubernetes Services Availabity Sets : disponibilité, Update Domaine, Fault Domain Disks Container services\n2 services PaaS :\nAzure Container Instances Azure Kubernetes Services (AKS) Azure Network Services\nAzure Virtual Network (VNet) : obligatoire, permet de communiquer entre VM, que ce soit en local ou dans Azure. Lorsqu’on crée une VM, on doit le placer dans un VNet. Dans un VNet, on retrouve des sous-réseaux (Subnet). Par défaut, un VNet est créé avec un Subnet. La sécurisation est assurée par NSG Azure Load Balancer : permet de répartir la charge et d’avoir de la HA VPN Gateway : permet de communiquer avec le OnPrem Azure Application Gateway : permet de gérer le trafic dans une application web Content Delivery Network : redirige les utilisateurs sur le point de stockage le plus proche afin de leur desservir des ressources statiques (images, vidéo). Système de cache Azure data categories\nStructured data : schema, SGBD Semi-structured data : NoSQL, données clé/valeur Unstructured data : photos, videos Azure storage services\nIaaS : pour les VM on a 2 possibilités : Premium Storage (Microsoft gère) ou autre si migration de VM (.vhd)\nDisks : disque persistant pour les VM IaaS. Options : SSD ou pas, Lift and shift operations Files : fichiers partagés avec SMB et REST. Azure File Share. Par défaut, 3 copies du fichier dans le même datacenter PaaS\nContainers : données non structurées placées dans des blobs d’un espace de stockage. Plusieurs types de stockage. La taille d’un compte de stockage est limitée (5 TB). Le nombre de compte de stockage est limitée par la souscription. On peut demander au Support Azure d’augmenter le nombre de stockage permis pour une souscription. Block blobs : petite taille de stockage (ex : image, pdf) Page blobs : toutes les autres tailles Append blobs : va être supprimé de la roadmap Azure Tables : pour les données NoSQL, va être supprimé dans la roadmap Azure et remplacé par Azure Cosmos DB Queues : gestion asynchrone des messages Le cout d’un Storage est calculé en fonction de la capacité de stockage, le niveau de réplication (4 niveaux), le niveau d’accès (hot, cold, archive)\nAzure database services\nAzure Cosmos DB : base de données globalement distribuée. Le temps de latence est géré par Cosmos. Azure SQL Database : la latence est gérée manuellement par duplication/synchronisation des données. Base compatible avec Microsoft SQL Server 2012 Azure Database migration : permet de migrer le schéma et les données (ex : SQL Server ou Oracle) vers Azure SQL Database Azure Marketplace\nApplications développées par les partenaires Microsoft ou d’autres sociétés Les applis sur le marketplace sont validées par Microsoft. Les Internet of Things\nAzure IoT Cental : solution SaaS pour faire des démos autour de l’IoT : récupération, monitoring, restitution. Azure IoT Hub : permet d’ingérer les données. Utiliser une communication bidirectionnelle entre l’application IoT et les devices. Big data and analytics\nAzure SQL Data Warehouse : service PaaS permet de faire des traitements en // sur une grosse volumétrie de données (To et Péta-octets). Basé sur SQL Server. SLA de 99,99%. Déployé sur des serveur HP7 ? Azure HDinsight : service PaaS big-data d’Azure utilisant des frameworks Open Source comme Hadoop et Spark. Facturé au niveau du compute et du stockage. Azure Data Lake Analytics : service PaaS développé par Microsoft pour le big data. Langage propre à Microsoft. Artificial Intelligence\nAzure Machine Learning service : environnement PaaS pour faire de l’IA : développer des algorithmes, entrainer, tester, déployer, manager Azure Machine Learning studio : environnement collaboratif permettant de faire du drag and drop de fonctionnalités déjà développées Data Bricks : service permettant de préparer des données en vue du machine learning Serverless computing\nAzure functions : code s’exécutant sur des infras basées sur des évènements. Gère lui mêmes les évènements. Azure Logic Apps : service permettant d’automatiser et d’orchestrer des tâches, des process BPM, les workflows. Exemple : la fonctionnalité d’envoi de mail est courante. Bout de code utilisée en boite noire. Azure Event Grid : fonctionne de pair avec Azure Logic Apps pour router les évènements entre logic apps. DevOps\nAzure DevOps services : solution Microsoft pour collaborer sur le template JSON de déploiement. Anciennement TFS. Outils incluant repository Git, boards Kanban et cloud-based load testing. Azure DevTest Labs : permet de provisionner rapidement des environnements, généralement pour les développeurs Azure App Service\nService permettant d’héberger une application web Supporte plusieurs langages et frameworks : PHP, .NET, Ruby, Python ou bien encore Java DevOps optimization Global scale with high availability Connection aux plateformes SaaS et On Prem Applications templates : Wordpress, API and Mobile features Serverless code Azure Management tools\nAzure portal Azure PowerShell : shell Windows permettant de créer des ressources Azure Command-Line Interface (CLI): language different d’Azure Command-Line. Créé en 2015 car PowerShell n’était pas compatible avec MacOS. En 2020, les 2 outils fonctionnent avec tous les OS : Windows, MacOS … Attention, les syntaxes des 2 langages sont différents. Azure Cloud Shell : installé directement dans le portal. On a le choix du langage : PowerShell ou Bash. Sur iPhone, on peut utiliser le portal Azure ou Cloud Shell Azure mobile app : application iPhone ou Android Azure REST API : pour les développeurs Azure advisor\nDonne des recommandations sur le HA, la sécurité, les performances et le coût. Identifie les opportunités de diminuer les coûts en arrêtant / supprimant des VM non utilisées Module 3 – Security, privacy Shared security\nSécurité partagée par Microsoft et le client\nAzure Firewall\nPermet de filtrer le trafic entrant et sortant au niveau du VNet Network Security Group (NSGs)\nPermet de sécuriser les VM Composant permettant de déclarer des règles de sécurité. On définit des règles au niveau NSG. Le NSG peut être rattaché au Subnet. Toutes les VM du Subnet sont protégées. Recommandation : un NSG par Subnet. Azure Distributed Denial of Service (DDoS) protection\nService activé par défaut dans Azure en mode basic Optionnel, le mode « Standard service » permet d’avoir un rapport d’audit Authentication and authorization\nAuthentification : on vérifie que la personne fait partie de l’annuaire Authorization : on vérifie les droits Azure Active Directory (AD)\nFonctionnalité de base d’authentification Single sign-on (SSO) Application management : derrière Office 365, il y’a un Azure AD. L’administrateur nous a donné les droits d’utiliser Teams. B2B : on peut faire communiquer 2 AD : Generali et partenaire B2C : permet à l’utilisateur d’une application mobile de s’enregistrer dans un AD spécifique à l’appli Device management : permet d’utiliser un AD en BYOD L’outil AD Connect permet de synchroniser l’AD Azure et l’AD local Azure Multi-Factor Authentication (MFA)\nPermet de renforcer l’authentification login / password Azure : envoi de code, appel téléphonique ou application Possible sur le LDAP gratuit Azure Security Center\nPortail permettant d’avoir des recommandations sur la sécurité des ressources (ex : configuration du NSG) Azure Key Vault\nPermet de stocker les certificats, les clés de chiffrage, les mots de passe Azure Advances Threat Protection (Azure ATP)\nPortail détectant les failles de sécu d’un site web hébergé sur Azure Utiliser des sensors Envoi de reporting hebdo par mail Azure policy\nApplication de la stratégie d’une entreprise L’administrateur met en place des policies pour restreindre des possibilités : Ex1 : stratégie de mettre les données que sur des Data Center français Ex2 : les développeurs ne travaillent qu’avec les services PaaS Ex3 : limiter les droits de l’utilisateur Implementing Azure Policy\nCreate a policy definition -\u0026gt; assign the definition to resources -\u0026gt; review the evaluation effects Permet de détecter les ressources qui ne sont pas alignées avec la stratégie Resource Locks\n2 types de Locks : CanNotDelete : Read, Update (pas de suppression) ReadOnly : Read Un verrou peut être appliquer sur une souscription, un resource group ou une ressource. Le CanNotDelete est systématiquement mis en place sur souscription. Permet de protéger les resources Azure d’une suppression accidentelle.\nPossibilité dans Azure de déléguer ou transférer le rôle Admin\nAzure Blueprints\nPermet de créer un template d’Azure policies en vue de réutilisation sur d’autres environnement Subscription Governance\nLa souscription facilite les tâches administratives :\nBilling : une facturation générée par souscription Access Control : role-based access control On peut déplacer les ressources d’une souscription à une autre Tags\nMéta-données positionnées sur les ressources et les groupes de ressources Permet de s’y retrouver et de gérer la facturation Consiste en une paire clé/valeur Exemples : owner:joe, environment:production, department:marketing Azure Monitor\nService gratuit permettant de collecter et d’analyser l’actualité de toutes les ressources (VM ou autre) Chaque ressource créée possède un Azure Monitor Activity Logs : composant Azure Monitor permettant d’avoir des logs sur les actions réalisées sur la machine : création, mise à jour, changement de configuration … Conserve les métriques de la machine : performance, consommation … Log Analytics\nUtilisé en environnement multi-cloud : Azure, AWS et OnPrem. Cet outil s’appuie sur les données remontées par Azure Monitor. Dans les autres cloud, il faut ajouter des agents Log Analytics\nAzure Service Health\nService permettant d’avoir l’état de santé des services Azure. Problème venant de Microsoft et non des ressources du client. Plusieurs composants : Azure Status : global overview Azure Service Health : dashboard personnalisé Azure Resource health : permet de diagnostiquer des problèmes Azure Monitoring applications and services\nAzure Application Insights : similaire à Google Analytics, destiné aux applications web Azure Alerts : bonne pratique à mettre en place systématiquement. Exemple : en cas de pb sur une VM, on peut envoyer un mail, arrêt une VM … : pour les actions, on peut passer par Logic Apps, Azure Functions ou PowerShell Visualisation : on peut utiliser Power BI (outil de DataViz) Trust Center\nPage indépendante d’Azure permet de connaître la gestion de la confidentialité Compliance Termes and Requirements\nGDPR : européren ISO : international Microsoft privacy statement\nDocument signé entre le client et Microsoft\nCompliance Manager\nOutil permettant de donner un score pour évaluer la compliance S’appuie sur les Azure policies pour calculer les scores Azure Government services\nPartie dans Azure dédiée aux personnes et entreprises américaines Azure China 21Vianet\nLes contrats passent entre 21Vianet et le client. Instance d’Azure cloud localisée en Chine Azure Information Protection\nPermet de chiffrer les emails Module 4 – Azure pricing and support Azure subscriptions\nExemple d’une Azure subscription par environnement : dev, test, production Facilite la facturation et les autres tâches administratives Subscription offers\nPlusieurs offres : Free, Pay-as-you-go, Entreprise Agreement, Student Offre free : Les souscriptions gratuites n’ont pas de SLA Limitée : 170 euros par mois pendant 12 mois, tous les services ne sont pas dispos Lorsque on dépense ce quota, la VM est bloquée Offre EA Generali est un client tiers 1 Les clients tiers 2 passent par des partenaires (ex: Cap Gemini) Managment groups\nGérer les droits des personnes à des souscriptions différentes Purchasing Azure products and services\nPartenaire ou Microsoft directement Les adresses IP publiques sont payantes Factors affecting costs\nResource Type : le storage est le service le moins cher Services : les partenaires fixent eux-mêmes leurs tarifs Location Zones for Billing Purposes\n90% du trafic entrant sont gratuits (ex : VM local -\u0026gt; VM dans Azure) Les trafics sortants sont tous payants et dépendent des zones Zone 1 : West US, East US, West Europe and others =\u0026gt; zone la moins chère Zone 2 : Zone 3 : DE Zone 1 : Germany =\u0026gt; le plus cher Princing calculator\nCalculette permettant d’estimer le coût d’un projet Le coût de la VM est impacté par plusieurs éléments (ex : OS, localisation …) Total cost of ownership calculator\nOutil permettant d’estimer la migration vers Azure de gros workloads Minimizing costs\nRecommandations :\nUtiliser Azure Advisor qui nous dit comment diminuer les couts Utiliser des spending limits : ex 200$ pour le Free, fixe Utiliser Azure reservation : permet de réserver une VM pendant 1 an ou 3 ans, ce qui permet de bénéficier d’une réduction de 35%. Offre réservée aux clients tiers 1 Choisir les low-cost locations and regions Utiliser des tags pour identifier les ressources low-cost Azure Cost Managment\nOutil intégré dans Azure permettant de suivre les couts Disponible pour les clients en tiers 1 Support plan options\nSupport basic Support developer : trial and non-production environnements : possible via email depuis le portail azure aux heures de bureau. Réponse en 8h Support standard : pour les environnements de production, contact par email et téléphone 24/7. Réponse en 2h. Professional Direct : applications directes. Support 24/7, Réponse en 15mn via mail ou téléphone Support Premier : comme le Professional Direct avec un CSA (architecte) Alternative support channels\nMSDN Azure forums Stack Overflow … Knowledge center : base de connaissance Service Level Agreement\n99,99% Remboursement sur compte bancaire en cas de SLA non atteint Composite SLAs\nOn multiplie tous les SLA SLA du projet : .9995 (webapp) x .9999 (database) = 99,94% Public and private preview features\nPrivate preview : resource disponible pour une partie des clients et/ou une communauté (ex : MVP), testeurs Microsoft … Public preview : disponible pour tout le monde, non recommandé en prod, pas de SLA. Version beta General Availability : version finale Azure updates permet de savoir quand une ressource passe en GA\n","link":"https://javaetmoi.com/2020/02/certification-microsoft-azure-fondamentals/","section":"posts","tags":["azure","cloud"],"title":"Certification Microsoft Azure Fondamentals"},{"body":" Le projet Spring Cloud Netflix facilite l’intégration de différents projets de la suite Netflix OSS dans des applications Spring Boot / Spring Cloud : Eureka, Zuul 1, Ribbon, Hystrix, Archaius, Feign. Jusqu’en 2018, le projet Spring Petclinic Microservices dont j’assure la maintenance utilisait ces 4 premiers projets.\nOr, certains des projets historiques de Netflix OSS ne sont plus activement développés. Ils sont rentrés en mode maintenance. C’est notamment le cas d’ Hystrix, de Zuul 1 et de Ribbon. En décembre 2018, lors de l’annonce de la sortie de Spring Cloud Greenwich RC1, Pivotal recommande de migrer vers des projets tiers et de nouveaux modules Spring Cloud :\nAnciennement****Solutions cibles Hystrix Resilience4jHystrix Dashboard / Turbine Micrometer + Monitoring System Ribbon Spring Cloud Loadbalancer Zuul 1 Spring Cloud Gateway Archaius 1 Spring Boot external config + Spring Cloud Config\nDans le cadre de Spring Petclinic Microservices, seul Eureka est épargné et continue de jouer son rôle d’annuaire de service. Un désendettement vers Resilience4j, Micrometer, Spring Cloud Loadbalancer et Spring Cloud Gateway s’est naturellement imposé (issue #117).\nCet article retrace les différentes étapes de migration. J’espère qu’il vous sera utile si vous avez le même chemin à parcourir.\nZuul 1 vers Spring Cloud Gateway Comme le souligne l’article Rate\nLimiting In Spring Cloud Gateway With Redis, avec 2028 étoiles (au 22/11/2019), Spring Cloud Gateway est le 2ième projet le plus populaire de la galaxy Spring Cloud derrière Spring Cloud Netflix. Il a été conçu pour succéder au proxy Zuul et fait office d’API Gateway pour les architectures microservices. Bâti autour d’une architecture réactive, Spring\nCloud Gateway requière l’utilisation de Spring WebFlux, de Netty et du projet\nReactor.\nLa migration de Spring Petclinic Microservices de Zuul 1 vers Spring Cloud Gateway s’est concrétisée par la Pull\nRequest #125.\nDans le pom.xml du module spring-petclinic-api-gateway, la première étape a consisté à changer de starter : de spring-cloud-starter-netflix-zuul vers spring-cloud-starter-gateway La seconde étape a nécessité de migrer de Spring Web vers Spring Webflux. Étape extrêmement simple puisqu’elle consiste à supprimer le starter spring-boot-starter-web du pom.xml. La dépendance vers Spring Webflux est tirée transitivement par spring-cloud-starter-gateway L’annotation @EnableZuulProxy a été retirée de la classe principale Suite à la migration vers Spring Webflux, la page d’accueil /static/index.html n’était plus mappée vers /\nUne solution de contournement a nécessité de déclarer un Router. Se référer à l’ issue Spring Boot #9785 Enfin, les règles de routage Zuul déclarées dans le fichier application.yml ont dû être transposées vers Spring Cloud Gateway Configuration de depart Zuul :\nzuul: prefix: /api ignoredServices: \u0026#39;*\u0026#39; routes: vets-service: /vet/** visits-service: /visit/** customers-service: /customer/** api-gateway: /gateway/** Configuration Spring Cloud Gateway :\nspring: cloud: gateway: routes: - id: vets-service uri: lb://vets-service predicates: - Path=/api/vet/** filters: - StripPrefix=2 - id: visits-service uri: lb://visits-service predicates: - Path=/api/visit/** filters: - StripPrefix=2 - id: customers-service uri: lb://customers-service predicates: - Path=/api/customer/** filters: - StripPrefix=2 Le filtre SripPrefix accepte pour paramètre le nombre de parties du chemin à retirer de la requête HTTP avant d’être redirigée vers le microservice cible.\nExemple : lorsqu’une requête arrive sur la gateway avec l’URL http://localhost::8080/api/customer/owners, la requête /owners est transmise au microservice customers-service (sans le /api/customer).\nA noter que la route /api/gateway servie par le contrôleur Rest ApiGatewayController n’a plus besoin d’être déclarée.\nRibbon vers Spring Cloud Reactive LoadBalancer Spring Cloud LoadBalancer a été incubé avant d\u0026rsquo;avoir été intégré au projet Spring Cloud Commons. Il abstrait l’utilisation d’un répartiteur de charge (load-balancer) côté client et fonctionne avec Spring RestTemplate, Spring WebClient et Spring WebFlux WebCient. Sous le capot, Spring Cloud LoadBalancer s’appuie sur Ribbon lorsque ce dernier est détecté dans le classpath et n’est pas désactivé. Dans le cas contraire, il dispose de sa propre implémentation de répartiteur de charge.\nLors de la montée de version vers Spring Cloud Hoxton, un avertissement au démarrage de l’application invite à ne plus utiliser Ribbon pour orchestrer la répartition des appels, car Spring Cloud Ribbon est désormais en mode maintenance :\n2019-11-13 18:32:41.670 WARN [api-gateway,,,] 79015 --- [ restartedMain] BockingLoadBalancerClientRibbonWarnLogger : You already have RibbonLoadBalancerClient on your classpath. It will be used by default. As Spring Cloud Ribbon is in maintenance mode. We recommend switching to BlockingLoadBalancerClient instead. In order to use it, set the value of `spring.cloud.loadbalancer.ribbon.enabled` to `false` or remove spring-cloud-starter-netflix-ribbon from your project.\nL’utilisation du répartiteur de charge natif à Spring Cloud LoadBalancer a demandé l’ exclusion de plusieurs dépendances liées à Ribbon :\nspring-cloud-starter-netflix-ribbon spring-cloud-netflix-ribbon ribbon-eureka \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.cloud\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-cloud-starter-netflix-eureka-client\u0026lt;/artifactId\u0026gt; \u0026lt;exclusions\u0026gt; \u0026lt;exclusion\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.cloud\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-cloud-starter-netflix-ribbon\u0026lt;/artifactId\u0026gt; \u0026lt;/exclusion\u0026gt; \u0026lt;exclusion\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.cloud\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-cloud-netflix-ribbon\u0026lt;/artifactId\u0026gt; \u0026lt;/exclusion\u0026gt; \u0026lt;exclusion\u0026gt; \u0026lt;groupId\u0026gt;com.netflix.ribbon\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;ribbon-eureka\u0026lt;/artifactId\u0026gt; \u0026lt;/exclusion\u0026gt; \u0026lt;/exclusions\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.cloud\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-cloud-starter-netflix-hystrix\u0026lt;/artifactId\u0026gt; \u0026lt;exclusions\u0026gt; \u0026lt;exclusion\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.cloud\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-cloud-netflix-ribbon\u0026lt;/artifactId\u0026gt; \u0026lt;/exclusion\u0026gt; \u0026lt;/exclusions\u0026gt; \u0026lt;/dependency\u0026gt; L’ajout du starter spring-cloud-starter-loadbalancer n’a pas été nécessaire car ce dernier est tiré transitivement par le starter spring-cloud-starter-netflix-eureka-client.\nDans mon cas, l’utilisation du BlockingLoadBalancerClient n’était pas possible car Spring Cloud Gateway nécessite d’utiliser des APIs non bloquantes. Preuve en stacktrace :\njava.lang.IllegalStateException: block()/blockFirst()/blockLast() are blocking, which is not supported in thread reactor-http-nio-2 |_ checkpoint ⇢ HTTP GET \u0026#34;/api/gateway/owners/6\u0026#34;[ExceptionHandlingWebHandler] Stack trace: at reactor.core.publisher.BlockingSingleSubscriber.blockingGet(BlockingSingleSubscriber.java:77) at reactor.core.publisher.Mono.block(Mono.java:1663) at org.springframework.cloud.loadbalancer.blocking.client.BlockingLoadBalancerClient.choose(BlockingLoadBalancerClient.java:86) at org.springframework.cloud.loadbalancer.blocking.client.BlockingLoadBalancerClient.execute(BlockingLoadBalancerClient.java:51) at org.springframework.cloud.client.loadbalancer.LoadBalancerInterceptor.intercept(LoadBalancerInterceptor.java:58) at org.springframework.http.client.InterceptingClientHttpRequest$InterceptingRequestExecution.execute(InterceptingClientHttpRequest.java:93) at brave.spring.web.TracingClientHttpRequestInterceptor.intercept(TracingClientHttpRequestInterceptor.java:51) at org.springframework.cloud.sleuth.instrument.web.client.LazyTracingClientHttpRequestInterceptor.intercept(TraceWebClientAutoConfiguration.java:308) at org.springframework.http.client.InterceptingClientHttpRequest$InterceptingRequestExecution.execute(InterceptingClientHttpRequest.java:93) at org.springframework.http.client.InterceptingClientHttpRequest.executeInternal(InterceptingClientHttpRequest.java:77) at org.springframework.http.client.AbstractBufferingClientHttpRequest.executeInternal(AbstractBufferingClientHttpRequest.java:48) at org.springframework.http.client.AbstractClientHttpRequest.execute(AbstractClientHttpRequest.java:53) at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:742) at org.springframework.web.client.RestTemplate.execute(RestTemplate.java:677) at org.springframework.web.client.RestTemplate.getForObject(RestTemplate.java:318) at org.springframework.samples.petclinic.api.application.CustomersServiceClient.getOwner(CustomersServiceClient.java:33) L’utilisation du RestTemplate pour appeler puis agréger le résultat de 2 microservices n’était plus possible (à noter que le client Zipkin l’utilise encore, mais en asynchrone dans un thread différent de ceux gérés par Reactor). J’ai donc été contraint de migrer vers la version réactive de WebClient.\nDu Spring RestTemplate au Spring WebFlux WebClient A l’instar du RestTemplate, WebClient peut être automatiquement configuré pour utiliser un LoadBalancerClient via l’utilisation de l’annotation @LoadBalanced :\n@Bean @LoadBalanced public WebClient.Builder loadBalancedWebClientBuilder() { return WebClient.builder(); } Le @RestController ApiGatewayController exposant l’API REST /owners/{ownerId} change de signature : il renvoie désormais un Mono à la place d’un OwnerDetails.\nL’utilisation du WebClient.Builder déclaré plus haut nécessite un peu d’entrainement lorsqu’il s’agit de chaîner les appels distants et d’enrichir une première réponse avec une seconde.\nVoici un exemple d’appel simplifié :\n@Autowired private WebClient.Builder webClientBuilder; @GetMapping(value = \u0026#34;owners/{ownerId}\u0026#34;) public Mono\u0026lt;OwnerDetails\u0026gt; getOwnerDetails(final @PathVariable int ownerId) { return webClientBuilder.build().get() .uri(\u0026#34;http://customers-service/owners/{ownerId}\u0026#34;, ownerId) .retrieve() .bodyToMono(OwnerDetails.class) .flatMap(owner -\u0026gt; webClientBuilder.build() .get() .uri(\u0026#34;http://visits-service/ pets/visits?petId={petId}\u0026#34;, joinIds(owner.getPetIds())) .retrieve() .bodyToMono(Visits.class) .map(addVisitsToOwner(owner)) ); } private String joinIds(List\u0026lt;Integer\u0026gt; petIds) { return petIds.stream().map(Object::toString).collect(joining(\u0026#34;,\u0026#34;)); } private Function\u0026lt;Visits, OwnerDetails\u0026gt; addVisitsToOwner(OwnerDetails owner) { return visits -\u0026gt; { owner.getPets() .forEach(pet -\u0026gt; pet.getVisits() .addAll(visits.getItems().stream() .filter(v -\u0026gt; v.getPetId() == pet.getId()) .collect(Collectors.toList())) ); return owner; }; } Migrer de RestTemplate à WebClient, c’est bien. Mais mettre à jour les tests unitaires, c\u0026rsquo;est mieux. La classe MockRestServiceServer utilisée jusque-là n’a pas d’équivalent pour WebClient. La documentation du framework Spring invite à utiliser OkHttp MockWebServer. Si cela vous intéresse, vous pouvez regarder sa mise en œuvre dans la classe de test VisitsServiceClientIntegrationTest.\nDe Netflix Hystrix à Spring Cloud Circuit Breaker et Resilience 4J A l’instar de Spring Cloud LoadBalancer, Spring Cloud Circuit Breaker fait partie du projet Spring Cloud Commons. Initié suite à la retraite d’Hystrix, Spring Cloud Circuit Breaker permet de s’abstraire de l’implémentation d’un coupe circuit. Il supporte 4 implémentations : Hystrix, Resilience4J, Sentinel et Spring Retry.\nResilience4J est l’implémentation préconisée par Spring et c’est elle que j’ai donc mise en œuvre. Mais basculer sur une autre librairie est censée ne nécessiter qu’un peu de configuration.\nLa migration vers Spring Cloud Circuit Breaker commence par de la configuration Maven :\nAjouter 2 dépendances Maven :\norg.springframework.cloud:spring-cloud-starter-circuitbreaker-reactor-resilience4j\nio.github.resilience4j:resilience4j-micrometer (cette dépendance permet de remonter des métriques dans Prometheus)\nExclure spring-cloud-netflix-hystrix de l’artefact spring-cloud-starter-netflix-eureka-client\nSuppression de la dépedance org.springframework.cloud:spring-cloud-starter-netflix-hystrix\nL’annotation EnableCircuitBreaker n’est pas nécessaire pour Resilience4j. Elle a été supprimée de la classe principale.\nLa déclaration du bean defaultCustomizer permet de spécifier la configuration par défaut de l’ensemble des circuit breaker Resilience4j utilisés dans l’application :\n/** * Default Resilience4j circuit breaker configuration */ @Bean public Customizer\u0026lt;ReactiveResilience4JCircuitBreakerFactory\u0026gt; defaultCustomizer() { return factory -\u0026gt; factory.configureDefault(id -\u0026gt; new Resilience4JConfigBuilder(id) .circuitBreakerConfig(CircuitBreakerConfig.ofDefaults()) .timeLimiterConfig(TimeLimiterConfig.custom().timeoutDuration(Duration.ofSeconds(4)).build()) .build()); } En déclarant d’autres beans Spring, il est possible de configurer spécifiquement certains circuit breaker par leur ID.\nL’utilisation d’un circuit breaker passe par l’utilisation de la fabrique ReactiveCircuitBreakerFactory créée par Spring Boot. La classe ApiGatewayController a été adaptée : le Mono renvoyé par le service getVisitsForPets subit une opération de transformation. C’est le ReactiveCircuitBreaker getOwnerDetails qui est maintenant chargé d’exécuter la suite du flux et de gérer les exceptions applicatives ou le timeout, et de router alors sur une méthode de contournement ( fallback method). Dans notre cas, lorsque le service des visites est inaccessible ou trop lent, on considère que l’animal de compagnie n’a pas eu de visite.\nprivate final ReactiveCircuitBreakerFactory cbFactory;@GetMapping(value = \u0026#34;owners/{ownerId}\u0026#34;) public Mono\u0026lt;OwnerDetails\u0026gt; getOwnerDetails(final @PathVariable int ownerId) { return customersServiceClient.getOwner(ownerId) .flatMap(owner -\u0026gt; visitsServiceClient.getVisitsForPets(owner.getPetIds()) .transform(it -\u0026gt; { ReactiveCircuitBreaker cb = cbFactory.create(\u0026#34;getOwnerDetails\u0026#34;); return cb.run(it, throwable -\u0026gt; emptyVisitsForPets()); }) .map(addVisitsToOwner(owner)) ); } private Mono\u0026lt;Visits\u0026gt; emptyVisitsForPets() { return Mono.just(new Visits()); } Le test unitaire ApiGatewayControllerTest s’assure que le coupe-circuit est opérationnel :\n@ExtendWith(SpringExtension.class) @WebFluxTest(controllers = ApiGatewayController.class) @Import(ReactiveResilience4JAutoConfiguration.class) class ApiGatewayControllerTest { @MockBean private CustomersServiceClient customersServiceClient; @MockBean private VisitsServiceClient visitsServiceClient; @Autowired private WebTestClient client; /** * Test Resilience4j fallback method */ @Test void getOwnerDetails_withServiceError() { OwnerDetails owner = new OwnerDetails(); PetDetails cat = new PetDetails(); cat.setId(20); cat.setName(\u0026#34;Garfield\u0026#34;); owner.getPets().add(cat); Mockito .when(customersServiceClient.getOwner(1)) .thenReturn(Mono.just(owner)); Mockito .when(visitsServiceClient.getVisitsForPets(Collections.singletonList(cat.getId()))) .thenReturn(Mono.error(new ConnectException(\u0026#34;Simulate error\u0026#34;))); client.get() .uri(\u0026#34;/api/gateway/owners/1\u0026#34;) .exchange() .expectStatus().isOk() .expectBody() .jsonPath(\u0026#34;$.pets[0].name\u0026#34;).isEqualTo(\u0026#34;Garfield\u0026#34;) .jsonPath(\u0026#34;$.pets[0].visits\u0026#34;).isEmpty(); } } Suite à cette migration, le microservice exposant le Dashboard Hystrix a tout bonnement été supprimé. L’intégration de Micrometer / Prometheus / Grafana avait déjà été réalisée et expliquée dans un précédent billet : Dashboard Grafana dockerizé. Il restera à modifier le tableau de bord Grafana pour afficher les métriques remontées par Resilience4j.\nConclusion Commencée sous Greenwich, le désendettement de Zuul, Ribbon et Hystrix a été achevé dans Spring Petclinic Microservices un an plus tard suite à la sortie de Spring Cloud Hoxton RC2. Fonctionnellement, rien n’a changé pour l’utilisateur. Mais techniquement, nous nous appuyons désormais sur une stack à jour et, je l’espère, promis à un bel avenir. Le code source de l’application est disponible pour tous les développeurs désirant s’inspirer d’une mise en œuvre concrète de Spring Cloud Gateway, Spring Cloud Loadbalancer et Spring Cloud Circuit Breaker.\nRéférences :\nSpring\nCloud Gateway - Reference manual (Pivotal) Spring\nCloud Gateway – Configuring a simple route (DZone) Spring\nCloud LoadBalancer – Reference manual (Pivotal) Spring\nCloud Circuit Breaker – Reference manual (Pivotal) Introducing\nSpring Cloud Circuit Breaker (Pivotal) ","link":"https://javaetmoi.com/2019/11/desendettement-de-spring-cloud-netflix/","section":"posts","tags":["hystrix","resilience4j","ribbon","spring-cloud","zuul"],"title":"Désendettement de Spring Cloud Netflix"},{"body":"","link":"https://javaetmoi.com/tags/hystrix/","section":"tags","tags":null,"title":"Hystrix"},{"body":"","link":"https://javaetmoi.com/tags/resilience4j/","section":"tags","tags":null,"title":"Resilience4j"},{"body":"","link":"https://javaetmoi.com/tags/ribbon/","section":"tags","tags":null,"title":"Ribbon"},{"body":"","link":"https://javaetmoi.com/tags/spring-cloud/","section":"tags","tags":null,"title":"Spring-Cloud"},{"body":"","link":"https://javaetmoi.com/tags/zuul/","section":"tags","tags":null,"title":"Zuul"},{"body":"Ce fut ma 8ième participation à Devoxx France. Les années passent et je suis toujours aussi friand de cette bulle d’oxygène dans mon quotidien encore bien trop souvent parsemé de Struts, JSF et MagicDraw. Un grand bravo aux organisateurs, bénévoles et aux speakers.\nD\u0026rsquo;ici quelques jours, l’intégralité des vidéos des conférences et universités présentées lors de Devoxx France 2019 sont disponibles sur la chaîne Devoxx FR de Youtube.\nSi vous souhaitez rapidement vous faire un avis sur leur contenu avant de les visionner ou si vous souhaitez garder une trace écrite de ce que vous y avez appris, je mets librement à disposition l’ensemble de mes 18 notes prises au cours de ces 3 jours riches en contenus et en découvertes.\nLors de cette édition 2019, les 2 frameworks hypes du moment Quarkus et Micronaut étaient sur le devant de la scène en permettant de développer des applications Java modernes et natives grâce à GraalVM. Poussée par l’essor des microservices, l’intégration de Java à Docker et son orchestrateur Kubernetes est de plus en plus poussée. Les indémodables étaient également de la partie : design d’ API REST, montée de version de Java, qualimétrie, JavaEE(oups, pardon, JakartaEE) et sécurité.\nMes notes classées par ordre de préférence :\nQuarkus : pourquoi et comment faire une appli Java Cloud Native avec Graal VM (Emmanuel Bernard et Clément Escoffier) D’architecte à Métarchitecte : une évolution nécessaire (Rémi Cocula) Construire son JDK en 10 étapes (José Paumard) Observabilité, Mythes, réalité et Chaos (Benjamin Gakic) Cycle de vie des applications dans Kubernetes (Charles Sabourdin et Jean-Christophe Sirot) Micronaut, le framework JVM ultra-light du futur (Olivier Revial) La gestion de l’authentification et de l’autorisation dans une architecture microservices ? Pas de soucis ! (Vivien Maleze et Florian Garcia) Du monolithe aux microservices chez leboncoin (Eric Lefevre-Ardant) Un turbo dans ton workflow GitHub (Alain Hélaïli) Sonar Smash (Helen Wallace et James Mac Mahon) De Java 8 à Java 11 sur un gros projet : les pièges à éviter (Alexis Dmytryk et Thomas Collignon) Oubliez Java EE, voilà Jakarta EE ! (Jean-François James et Sébastien Blanc) La JVM et Docker, vers une symbiose parfaite (Guillaume Scheibel) Migrer ses APIs vers GraphQL : pourquoi ? comment ! (Guillaume Scheibel) Au secours, mon projet BigData est en production! (Vincent Devillers) SpringBoot avec Kotlin, Kofu et les Coroutines (Sébastien Deleuze) 50 points de contrôle d\u0026rsquo;une API REST (François-Guillaume Ribreau) Dev environments: use the nix, Luke! (Clément Delafargue et Hussein Ait-Lahcen) ","link":"https://javaetmoi.com/2019/05/18-prises-de-notes-a-devoxx-france-2019/","section":"posts","tags":["bigdata","devoxx","github","graalvm","graphql","jakartaee","java","jdk","kubernetes","micronaut","quarkus","sonarqube"],"title":"18 prises de notes à Devoxx France 2019"},{"body":"","link":"https://javaetmoi.com/tags/bigdata/","section":"tags","tags":null,"title":"Bigdata"},{"body":"","link":"https://javaetmoi.com/tags/github/","section":"tags","tags":null,"title":"Github"},{"body":"","link":"https://javaetmoi.com/tags/jakartaee/","section":"tags","tags":null,"title":"Jakartaee"},{"body":"","link":"https://javaetmoi.com/tags/jdk/","section":"tags","tags":null,"title":"Jdk"},{"body":"","link":"https://javaetmoi.com/tags/micronaut/","section":"tags","tags":null,"title":"Micronaut"},{"body":"","link":"https://javaetmoi.com/tags/sonarqube/","section":"tags","tags":null,"title":"Sonarqube"},{"body":"A l’instar de SLF4J pour les logs, Micrometer est la façade d’export de métriques utilisée par Spring Boot et ses Actuators. Micrometer supporte une douzaine de systèmes de monitoring : Datalog, Netflix Atlas, New Relic, JMX, CloudWatch, InfluxDB ou bien encore Prometheus.\nRécemment, j’ai poursuivi le travail initié par Kevin Crawley pour intégrer Prometheus et Grafana dans la version microservices de Spring Petclinic. Proposée par Maciej Szarliński, l’idée consistait à remplacer les compteurs Micrometer de typeregistry.counter(\u0026ldquo;create.visit\u0026rdquo;).increment() par l’ annotation @Timed.\nJ’ai profité de ce changement pour améliorer le packaging Docker de Grafana et en simplifier l’accès. Pour accéder au dashboard personnalisé exposant l’évolution du nombre d’animaux et de propriétaires, un docker-compose up suivi d’un clic sur l’ URL du dashboard sont désormais suffisant.\nCe billet présente les configurations Docker et Grafana mises en oeuvre.\nDocker compose Le docker-compose.yml de Spring Petclinic Microservices est relativement simple : il pointe sur 2 Dockerfile Grafana et Promotheus personnalisés.\nL’usage de volume Docker n’est pas nécessaire.\nA noter que le port 9090 de Prometheus est mappé sur le port 9091 car le port 9090 était déjà occupé par Spring Boot Admin.\ngrafana-server: build: ./docker/grafana container_name: grafana-server mem_limit: 256M ports: - 3000:3000 prometheus-server: build: ./docker/prometheus container_name: prometheus-server mem_limit: 256M ports: - 9091:9090 Dockerfile Prometheus Prometheus est un outil de supervision chargé de collecter et de stocker les métriques collectées (dans notre étude de cas depuis des actuators Spring Boot). Les métriques sont stockées dans une base de données de type Time Series. Prometheus propose bien évidemment une API HTTP pour consulter ces métriques et créer de jolis tableaux de bord dans des outils comme Grafana ou WaveFront.\nMise en place par Kevin Crawley, l’image Docker de Prometheus docker/prometheus/Dockerfile personnalise l’image officielle de Prometheus 2.4.2 en ajoutant le fichier prometheus.yml dans le répertoire de configuration /etc/prometheus :\nFROM prom/prometheus:v2.4.2 ADD prometheus.yml /etc/prometheus/ Le fichier de configuration prometheus.yml précise quels sont les actuators Spring Boot que Prometheus doit interroger périodiquement pour récupérer et historiser les métriques. Chose amusante : Prometheus se monitore lui-même.\n# my global config global: scrape_interval: 15s # Set the scrape interval to every 15 seconds. Default is every 1 minute. evaluation_interval: 15s # Evaluate rules every 15 seconds. The default is every 1 minute. # scrape_timeout is set to the global default (10s). # A scrape configuration containing exactly one endpoint to scrape: # Here it\u0026#39;s Prometheus itself. scrape_configs: - job_name: prometheus static_configs: - targets: [\u0026#39;localhost:9090\u0026#39;] - job_name: api-gateway metrics_path: /actuator/prometheus static_configs: - targets: [\u0026#39;api-gateway:8080\u0026#39;] - job_name: customers-service metrics_path: /actuator/prometheus static_configs: - targets: [\u0026#39;customers-service:8081\u0026#39;] - job_name: visits-service metrics_path: /actuator/prometheus static_configs: - targets: [\u0026#39;visits-service:8082\u0026#39;] - job_name: vets-service metrics_path: /actuator/prometheus static_configs: - targets: [\u0026#39;vets-service:8083\u0026#39;] Les métriques préfixées par petclinic_ sont accessibles depuis l’interface web de Prometheus http://localhost:9091/ :\nDans l’exemple ci-dessus, la métrique petclinic_pet_seconds_count comptabilise le nombre d’appels différents au contrôleur REST PetResource :\nLe GET sur /petTypes pour lister les types d’animaux Le POST sur /owners/{ownerId}/pets pour ajouter un animal à un propriétaire Dockerfile Grafana Pratique, l’IHM de consultation des métriques proposée par Prometheus ne permet pas de construire de jolis tableaux de bord. C’est pourquoi les auteurs de Prometheus préconisent l’utilisation de Grafana.\nLe Dockerfile de Grafana repose sur l’image officielle 5.2.4 de Grafana, y ajoute 2 répertoires /provisioning et /dashboard et le fichier de configuration grafana.ini :\nFROM grafana/grafana:5.2.4 ADD ./provisioning /etc/grafana/provisioning ADD ./grafana.ini /etc/grafana/grafana.ini ADD ./dashboards /var/lib/grafana/dashboards L’image Grafana est livrée avec un fichier grafana.ini dont toutes les options sont commentées avec les valeurs par défaut. Pour le personnaliser, je me suis référé à la documentation :\nSpécifier le répertoire contenant les fichiers permettant de pré-configurer Grafana avec la source de données Prometheus et le dashboard pour Petclinic Désactiver l’authentification et permettre ainsi à un utilisateur non authentifié de consulter le dashboard Spring Petclinic Metrics Au final, voici à quoi ressemble le fichier de configuration grafana.ini :\n##################### Spring Petclinic Microservices Grafana Configuration ##################### #################################### Paths #################################### [paths] # folder that contains provisioning config files that grafana will apply on startup and while running. provisioning = /etc/grafana/provisioning #################################### Anonymous Auth ########################## # Anonymous authentication has been enabled in the Petclinic sample with Admin role # Do not do that in Production environment [auth.anonymous] # enable anonymous access enabled = true # specify organization name that should be used for unauthenticated users org_name = Main Org. # specify role for unauthenticated users org_role = Admin Le fichier de pré-configuration de la source de données Prometheus provisionning/datasources/all.yml référence l’URL de Prometheus avec son port interne à Docker :\napiVersion: 1 datasources: - name: Prometheus type: prometheus access: proxy org_id: 1 url: http://prometheus-server:9090 is_default: true version: 1 editable: true Le fichier de pré-configuration des dashboards provisioning/dashboards/all.yml référence le répertoire / var / lib / grafana / dashboards dans lequel a été copié le fichier de configuration du dashboard Spring Petclinic Metrics grafana-petclinic-dashboard.json (cf. Dockerfile) :\napiVersion: 1 providers: - name: \u0026#39;default\u0026#39; orgId: 1 folder: \u0026#39;\u0026#39; type: file disableDeletion: false updateIntervalSeconds: 10 options: path: /var/lib/grafana/dashboards Le fichier de configuration du dashboard Spring Petclinic Metrics grafana-petclinic-dashboard.json s’appuie à la fois sur les métriques personnalisées préfixées par petclinic_ et sur les métriques générées nativement par Spring Boot.\nExtrait d’utilisation de la métrique http_server_requests_seconds_sum :\n\u0026#34;expr\u0026#34;: \u0026#34;sum(rate(http_server_requests_seconds_sum{status!~\\\u0026#34;5..\\\u0026#34;}[1m]))/sum(rate(http_server_requests_seconds_count{ status!~\\\u0026#34;5..\\\u0026#34;}[1m]))\u0026#34;, \u0026#34;format\u0026#34;: \u0026#34;time_series\u0026#34;, \u0026#34;intervalFactor\u0026#34;: 1, \u0026#34;legendFormat\u0026#34;: \u0026#34;HTTP - AVG\u0026#34;, \u0026#34;refId\u0026#34;: \u0026#34;A\u0026#34; Extrait d’utilisation de la métrique petclinic_owner_seconds_count :\n\u0026#34;expr\u0026#34;: \u0026#34;sum(petclinic_owner_seconds_count{method=\\\u0026#34;POST\\\u0026#34;, status=\\\u0026#34;201\\\u0026#34;})\u0026#34;, \u0026#34;format\u0026#34;: \u0026#34;time_series\u0026#34;, \u0026#34;instant\u0026#34;: false, \u0026#34;intervalFactor\u0026#34;: 1, \u0026#34;legendFormat\u0026#34;: \u0026#34;owner create\u0026#34;, \u0026#34;refId\u0026#34;: \u0026#34;A\u0026#34; Conclusion Cet article aura montré comment construire une image docker de Grafana pour vos démos.\nAu démarrage de Grafana, la source de données vers Prometheus est pré-configurée. Le tableau de bord Spring Petclinic Metrics utilise cette source de données pour récupérer les métriques Spring Boot et Petclinic historisées par Prometheus.\nL’accès au tableau de bord est public. L’URL est connue : http://localhost:3000/d/69JXeR0iw/spring-petclinic-metrics\nLes instructions de démarrage des images Docker peuvent être retrouvées dans le README.md du repo GitHub spring-petclinic/spring-petclinic-microservices.\nRessources :\nMicrometer concepts (Micrometer website) Grafana support for Prometheus (official Prometheus documentation) Découverte de l’outil de supervision Prometheus (Linuxfr.org) Monitoring using Spring Boot 2, Prometheus and Grafana (DZone) ","link":"https://javaetmoi.com/2019/03/dashboard-grafana-docker/","section":"posts","tags":["docker","grafana","microservices","prometheus"],"title":"Dashboard Grafana dockerizé"},{"body":"","link":"https://javaetmoi.com/tags/grafana/","section":"tags","tags":null,"title":"Grafana"},{"body":"","link":"https://javaetmoi.com/tags/microservices/","section":"tags","tags":null,"title":"Microservices"},{"body":"","link":"https://javaetmoi.com/tags/prometheus/","section":"tags","tags":null,"title":"Prometheus"},{"body":"","link":"https://javaetmoi.com/tags/assembleur/","section":"tags","tags":null,"title":"Assembleur"},{"body":" Pour ce premier article de 2019, je vous propose un voyage dans le temps, au contenu plus personnel. J’ai récemment dîné avec mon vieil ami Nicolas Lidzborski qui travaille chez Google aux US et que je n’avais pas revu depuis une dizaine d’années. Comme toute retrouvaille, nous nous sommes remémorés des souvenirs du Lycée, ses profs et son club IF. De sa chambre d’ado, Nicolas a retrouvé une illustration que nous avions réalisé pendant le concours Soft la Nuit de 1996, un marathon de 24h pendant lequel 20 équipes de 4 jeunes devaient développer un logiciel. Et oui, mon premier hackaton commence à dater. Afin de pouvoir se qualifier, nous avions du présenter des projets personnels. Mon jeu vidéo Black Hell en faisait partie. Par curiosité, et avec un brin de nostalgie, je suis parti à la recherche d’un backup de disquette. J’en ai ressorti code source et binaire. Et surprise : avec l’émulateur DOSBox, j’ai réussi à le faire tourner, à la fois sous Windows 10 et MacOS.\nPrésentation du jeu Blach Hell est un shoot’em up, ou plutôt 2 shoot’em up : le mode solo et le mode 2 joueurs (sur le même écran) n’ont rien à voir.\nDans la lignée de Xenon 2, la version solo vous propose de prendre les commandes d’un avion de chasse nommé « Silicium ». Vous aurez à éliminer des vaisseaux et à éviter des mines avant de pouvoir détruire le Big Boss final. Un travelling vertical fait avancer le terrain en arrière-plan. 5 touches sont nécessaires : les flèches multidirectionnelles pour se déplacer et Ctrl pour tirer.\n\u0026amp;t=2s\nPartie en mode solo\nDans le mode 2 joueurs, chaque joueur choisit un vaisseau parmi les 3 proposés. Ils rentrent ensuite dans une arène pour un combat à mort. Afin de se déplacer sur tout l’écran et viser leur adversaire, les vaisseaux peuvent tourner à 360 °. Des options à récupérer viennent pimenter les parties : vitesse doublée, soin, invincibilité emporaire, double canon, bombe …\nPour celles et ceux qui souhaitent voir à quoi ce mode ressemble sans avoir à installer DOSBox ni à dépoussiérer leur vieux 486, j’ai publié une vidéo d’une partie 2 joueurs sur Youtube.\n\u0026amp;t=18s\nPartie en mode 2 joueurs\nCaractéristiques techniques Ce jeu a été développé en Turbo Pascal 6. De nombreuses routines de manipulation d’interrupteurs et d’optimisation de code sont codées en Assembleur x86.\nLe jeu pèse 2,7 Mo. Il peut être compressé en une archive ZIP de moins d’1 Mo, ce qui lui permettait d’être distribué sur une disque de 1,44 Mo.\nA elle seule, la vidéo de décollage N1PLAIN.FLX pèse 1,2 Mo.\nPour faire fonctionner le jeu, j’utilise DOSBox.\nLes instructions sont données dans le README.MD.\nUnivers graphique Pour créer un jeu, il faut du code, mais également des ressources graphiques.\nA l’époque, point d’Internet pour trouver une banque d’images prête à l’emploi.\nLes vaisseaux ont été modélisés en 3D avec le logiciel 3D Studio. Les textures et certains sprites ont été dessinés pixels par pixels avec Deluxe Paint Animation. Les paysages ont quant à eux été créés à l’aide de VistaPro.\nLa résolution VGA de 320x200 pixels avec 256 couleurs imposait que tous les fonds d’écran et sprites partagent la même palette de couleurs. C’est pourquoi, dans les binaires du jeu, les fichiers d’images (.IMA) et de palettes (.PAL et .COD) sont séparés.\nL’écran d’options est le seul écran du jeu supportant le SVGA (640x480). Pour les personnes n’ayant pas de carte vidéo compatible, une option en ligne de commande (BH.EXE -VGA) permettait de le rétrograder en VGA.\nChaque partie commence par une animation en image de synthèse montrant le Silicium décoller de sa base. Cette animation est encodée dans le fichier N1PLAIN.FLX.\nUn peu de code En parcourant les 4 319 lignes de code source de Black Hell, je suis tombé sur de bons souvenirs. En voici une sélection.\nBasculer la fenêtre ASCII de Ms DOS vers le mode VGA 320x200 avec 256 couleurs demande à déclencher l’interruption 10h du BIOS avec 13h dans le registre AX.\nCes 2 lignes de code assembleur se retrouvent à plusieurs fois :\nASM MOV AX,13h INT 10h END; A noter au passage l’interopérabilité très simple du Pascal avec l’Assembleur à l’aide du bloc de code ASM.\nLe mode VGA est relativement simple à programmer : on accède directement à la mémoire de la carte vidéo par un pointeur localisé à l’ adresse $A000:0000h(segment:offset avec segment*16 + offset = adresse physique). Un pixel est représenté par un seul octet (256 couleurs). La taille mémoire est de 320 x 200 x 1 = 64 000 octets.\nDans le code Pascal, on retrouve ce tableau de bytes (type Virtual) ainsi qu’un pointeur vers l’adresse de la mémoire vidéo (variable Screen) :\nTYPE VirtualPtr=^Virtual; Virtual=ARRAY[0..63999] of BYTE; VAR Player1,Player2 : VirtualPtr; Spr,Decors : VirtualPtr; Screen : Virtual ABSOLUTE $A000:0; La boucle principale du jeu consiste à gérer les touches, calculer la position des vaisseaux, détecter les collisions, scroller le fond puis afficher le tout à l’écran. L’image affichée à l’écran est calculée dans l’espace mémoire référencé par le pointeur Spr.\nAvant de recopier Spr vers la carte vidéo, on doit attendre que le moniteur ait fini d’afficher l’image précédente, ceci afin d’éviter de désagréables effets d’images coupées.\nLes écrans CRT utilisent des canons à électron pour afficher les pixels. Le faisceaux d’électron se déplace de gauche à droite et de haut en bas.\nLe signal VBL indique que le faisceau a atteint le bas de l’écran et qu’il retourne en haut. Le signal HBL indique que le faisceau a atteint la fin de la ligne et qu’il revient au début de la ligne suivante.\nLe code du jeu s’aligne sur le balayage de l’écran entre 2 recopies d’image:\nPROCEDURE Synchro;ASSEMBLER; LABEL Attend_VBL; LABEL Attend_fin_HBL; ASM mov DX,$3DA Attend_VBL: IN AL,DX AND AL,8 jz Attend_VBL Mov DX,$3DA Attend_fin_HBL: IN AL,DX AND AL,1 jnz Attend_fin_HBL END; Synchro; MOVE(Sp^,Screen,64000); La palette de couleurs tient sur 768 octets (3 x 256). Une couleur est représenté par 3 octets correspondant aux primitives RGB).\nLe changement de palette de la carte vidéo nécessite davantage d’instructions :\nVAR Palette: ARRAY[0..767] of BYTE; DecodPal(s,Palette); ASM MOV AX,1012h XOR BX,BX MOV CX,256 MOV DX,Seg Palette MOV ES,DX MOV DX,Offset Palette INT 10h END; Pour afficher l’écran d’accueil, des menus ou de l’aide, le mode VGA convient. Par contre, pour créer des animations, de nombreux jeux vidéo utilisaient un mode non documenté du VGA et popularisé par Michael Abrash : le fameux Mode X. Ce mode permet de gérer le double-buffering. Black Hell ne déroge pas à la règle :\nPROCEDURE INITMODX;ASSEMBLER; ASM MOV AX,13h INT 10h MOV DX,3C4h { TS } MOV AL,4 OUT DX,AL INC DX IN AL,DX AND AL,0F7h OR AL,4h { Désensenclenche le mode Chain 4 } OUT DX,AL DEC DX { Efface les 4 pages graphiques } MOV AX,0F02h OUT DX,AX MOV AX,0A000h MOV ES,AX XOR DI,DI XOR AX,AX MOV CX,0FFFFh CLD REP STOSW MOV DX,3D4h { CRTC } MOV AL,14h OUT DX,AL INC DX IN AL,DX AND AL,0BFh { bit 6 du doubleword à 0 } OUT DX,AL DEC DX MOV AL,17h OUT DX,AL INC DX IN AL,DX OR AL,40h { bit 6 à 1 } OUT DX,AL MOV FPAGE,0; END; Le DOS n’a pas été conçu pour exploiter plus d’1 Mo de RAM. Et seuls les premiers 640 Ko de mémoire conventionnelle pouvaient servir aux applications.\nPour charger ses images, Black Hell requière 1 Mo mémoire. Il fait donc appel à la mémoire étendue connue sous le nom de XMS (Extended Memory Specification) :\nVAR XMSDrv : POINTER; XRegs : XREGISTERS; PROCEDURE XMS_INIT; { Vérifie que l\u0026#39;XMS est installée } BEGIN XMSExists:=false; WITH Regs DO BEGIN AX:=$4300; Intr($2F,regs); if Regs.al = $80 then Begin { Copie l\u0026#39;adresse d\u0026#39;accès au pilote XMS } AX:=$4310; Intr($2F,regs); XMSDrv:=Ptr(ES,BX); XMSExists:=true; { Détermine la taille de mémoire étendue disponible } Xregs.AX:=$0800; XmsCall(XRegs); XMF:=Xregs.AX; XML:=Xregs.DX; End; END; If (not XmsExists) or (XML\u0026lt;1024) then BEGIN ClrScr; If not XmsExists then Write(\u0026#39;Xms indisponible ...\u0026#39;) Else Write(XmF,\u0026#39; Ko d\u0026#39;\u0026#39;XMS libre et il en faut au moins 1024 ...\u0026#39;); Halt; END; END; La détection des frappes de touche passe par l’usage du PORT 60 et des codes du contrôleur clavier :\nCASE PORT[$60] Of 72 : Haut:=True; 80 : Bas:=True; 75 : Gauche:=True; 77 : Droite:=True; END On retrouve même des cheat-code pour être invulnérable et debugger plus facilement le jeu (je vous laisse retrouver avec quelle touche) :\nCONST TI = 23; If Inv and alt then If not Invulnerable then Invulnerable:=True Else Invulnerable:=False; L’effet de fade-out de fin de jeu est implémenté à l’aide de rechargements successifs de la palette graphiques. Entre chaque rechargement, les composantes RVB sont décrémentées de 1, ceci afin d’atteindre progressivement le 0 du noir :\nPROCEDURE FADE_OUT; Var CoulMax : Byte; BEGIN CoulMax:=63; REPEAT For i:=0 to 767 do BEGIN j:=Palette[i]; If j\u0026gt;0 then Dec(j); Palette[i]:=j; END; Synchro; SetPal(\u0026#39;\u0026#39;,3); Dec(CoulMAx); UNTIL CoulMax=0; END; Le Turbo Pascal vient sans API de manipulation ni de chargement d’images. Pour lire des fichiers au format GIF, JPG ou PCX, il était nécessaire d’implémenter à la main l’algorithme de décompression. Ne disposant pas de leurs spécifications, j’ai utilisé un algorithme de compression / décompression très naïf, peu efficient mais qui a du faire gagner au total quelques centaines de Ko. Je vous laisse en juger :\nDecompact(\u0026#39;N1Alien.ima\u0026#39;,1); PROCEDURE DECOMPACT(Ne:String;Num:Word); VAR Nb : WORD; Color : BYTE; BEGIN Total:=0; Assign(f,ne); Reset(f); BEGIN REPEAT Read(f,Ch); Read(f,ch2); Nb:=Ord(ch); Color:=Ord(ch2); i:=0; REPEAT CASE Num OF 0 : Spr^[Total+i]:=Color; 1 : Player1^[Total+i]:=Color; 2 : Player2^[Total+i]:=Color; 3 : Decors^[Total+i]:=color; END; if nb\u0026gt;i then inc(i); UNTIL (i=nb); Inc(Total,nb); UNTIL Total\u0026gt;=64000; Close(f); END; end; A l’époque, pas de Direct3D ni d’OpenGL. La génération de l’ ombre portée sous les vaisseaux est réalisée à la main par la routine suivante :\nPROCEDURE Shadow; VAR Plus : WORD; D0001 : BYTE; OmbX,OmbY : INTEGER; BEGIN If (SprP1\u0026gt;=0) and (SprP1\u0026lt;=4) then BEGIN Plus:=0;D0001:=0; END Else If (SprP1\u0026gt;=5) and (SprP1\u0026lt;=9) then BEGIN Plus:=11200;D0001:=5; END Else BEGIN Plus:=22400;D0001:=10; END; For u:=1 to 27 do For v:=1 to 17 do If Decors^[320*(v shl 1)+(u shl 1)+((SprP1-D0001)*54)+Plus]\u0026lt;\u0026gt;0 then BEGIN If Yp1+17\u0026gt;=100 then OmbY:=((100-Yp1+48) shr 2); If Yp1+17\u0026lt;100 then OmbY:=((200-Yp1-48) shr 2); If Xp1+27\u0026gt;=160 then OmbX:=48-(Xp1 shr 2); If Xp1+27\u0026lt;160 then OmbX:=((200-Xp1) shr 2); READPIXEL(Xp1+OmbX+u,Yp1+OmbY+v); PUTPIXEL(Xp1+OmbX+u,Yp1+OmbY+v,ShadNiv1[CoulRead]); END; END; Au début du jeu, à des fins d’optimisation, sont pré-calculés les tables de sinus et de cosinus :\nFor i:=1 to 360 do Sinus[i]:=Trunc(Round(Sin(i*Pi/180)*1024)); For i:=1 to 360 do Cosinus[i]:=Trunc(Round(Cos(i*Pi/180)*1024)); Pour calculer les trajectoires des vaisseaux, j’avais dû demander à mon prof de Maths de Première S quelle était la formule de matrice de rotation en 2 dimensions, formule qu’on n’apprenait en principe qu’en terminale :\n{ Rotation autour des Z } RotX:=((Play1.Circle[1]*Cosinus[Play1.Circle[3]*23+1]) shr 10) - ((Play1.Circle[2]*Sinus[Play1.Circle[3]*23+1]) shr 10); RotY:=((Play1.Circle[1]*Sinus[Play1.Circle[3]*23+1]) shr 10 ) + ((Play1.Circle[2]*Cosinus[Play1.Circle[3]*23+1]) shr 10); ScrY:=(RotY shl 6) div 45; ScrX:=(RotX shl 6) div 45; ScrY:=Play1.Cy+ScrY+Play1.Circle[5]; ScrX:=Play1.Cx+ScrX+Play1.Circle[4]; If (ScrX\u0026gt;0) and (ScrX\u0026lt;314) and (ScrY\u0026gt;0) and (ScrY\u0026lt;149) then PutSprAt(10,122,14,126,ScrX+3,ScrY+3,3); Jouer à 2 sur le même écran, c’est sympa. Avoir les commandes sur le même clavier, un peu moins. Du coup, j’avais pris en charge la gestion du Joystick pour le second joueur :\nVAR j1b1,j1b2 : BYTE; joyX,JoyY : WORD; PROCEDURE JOYBOUTCOOR;ASSEMBLER; ASM MOV AH,84h MOV DX,1 INT 15h MOV JoyX,AX MOV JoyY,BX END; PROCEDURE JOYKEYB; BEGIN CASE JoyX OF 5,4 : Play2.Gauche:=True; 152,153 : Play2.Droite:=True; 78 : BEGIN Play2.Gauche:=False;Play2.Droite:=False; END; END; CASE JoyY OF 4 : Play2.Haut:=True; 166,168 : Play2.Bas:=True; 85 : BEGIN Play2.Haut:=False;Play2.Bas:=False; END; END; END; Conclusion Beaucoup de personnes découvrent la programmation au travers le développement de jeux vidéo. J’en fais partie. Et ce n’est pas sans fierté que j’ai redécouvert mon premier jeu grand public. A noter qu’à l’époque, je le distribuais déjà avec son code source.\nEn 1995, Internet était balbutiant. Pour se former, il y’avait bien quelques chats et forums accessibles via BBS et sites minitels. En 2nde, j’ai eu la chance d’avoir un prof d’informatique qui m’enseigna le Turbo Pascal (une pensée pour Monsieur Canal). J’ai appris l’assembleur dans un livre de poche et la programmation système dans la fameuse Bible PC de Michael Tischer. Mes amis codeurs Patrick et Nicolas m’ont également beaucoup apportés.\nC’est quelque peu étrange de publier dans GitHub du code écrit il y’a 24 ans, mais pourquoi pas ? Je serai a peu près sûr de le retrouver dans 20 ans pour mes petits-enfants.\nPour conclure ce billet, ma plus grande fierté a été quand mon plus jeune fils de 5 ans me demanda d’échanger sa partie de Mario Kart contre une de Black Hell. Vive le rétrogaming !\nPour le plaisir, voici quelques souvenirs d’un autre siècle :\n","link":"https://javaetmoi.com/2019/02/black-hell-mon-premier-jeu-video/","section":"posts","tags":["assembleur","turbo-pascal"],"title":"Black Hell : mon premier jeu vidéo"},{"body":"","link":"https://javaetmoi.com/tags/turbo-pascal/","section":"tags","tags":null,"title":"Turbo-Pascal"},{"body":"Dans ce billet, j’aimerais vous présenter les différentes briques techniques permettant de mettre en œuvre une architecture microservices reposant sur Spring Boot, Spring Cloud, Netflix OSS et Docker. Pour m’y aider, je m’appuierai sur l’application démo Spring Petclinic Microservices que je vous avais déjà brièvement présenté en 2016 et que j’ai récemment migrée vers Spring Cloud Finchley et Spring Boot 2.\nCe fork a été construit à partir de l’application monolithique spring-petclinic-angularjs. Cette dernière a été découpée en plusieurs services, chacun responsable d’un domaine métier de la clinique vétérinaire : les animaux et leurs propriétaires, leurs visites à la clinique et les vétérinaires.\nAu final, Spring Petclinic Microservices est construit autour de petits services indépendants (quelques centaines de ligne de code), s’exécutant dans leur propre JVM et communiquant sur HTTP via une API REST. Ces microservices sont tous écrits en Java. Mais on aurait pu utiliser Kotlin pour développer certains d’entre eux. Le front est quant à lui codé en JavaScript.\nArchitecture technique Pour fonctionner, les différents microservices composant l’application Petclinic reposent sur différentes briques techniques matérialisées sur le diagramme d’architecture ci-dessous :\nRemarque : pour simplifier le diagramme, les flèches partant de customers-service et visits-service peuvent être transposées aux « xxx-services » homologues.\nVue d’ensemble des briques techniques :\nEn bleu, les 3 microservices back exposent le fonctionnel de l’application au travers d’une API REST. Pour assurer la scalabilité horizontale et la tolérance aux pannes, plusieurs instances d’un même microservice peuvent être démarrées simultanément. Afin d’être autonome, chaque microservice dispose de sa propre base de donnée. Cependant, toutes les instances d’un même microservice partagent la même base de données (qui pourrait être clusterisé). Le front-end JavaScript n’attaque pas directement ces 3 microservices : il passe par le microservice front API Gateway qui s’occupe également de desservir les ressources statiques (code JavaScript, pages HTML et CSS).\nEn vert, on retrouve les microservices d’infrastructures:\nL’ annuaire de service Eureka va permettre aux microservices de s’enregistrer puis de communiquer sans connaître par avance leurs adresses IP. Les microservices (ainsi que les autres serveurs) vont charger leur configuration applicative depuis le Serveur de Config. Les fichiers de configuration sont versionnés sur le dépôt de code GitHub spring-petclinic-microservices-config. Optionnel, les traces des différents appels peuvent être remontées dans le serveur de logs distribués Zipkin. Enfin, pour superviser et administrer les différents microservices, les Ops peuvent compter sur le serveur Spring Boot Admin.\nSpring Boot taillé pour les Microservices Bien que ne faisant pas partie des microframeworks Java, Spring Boot se prête parfaitement au développement de Microservices par ses nombreux atouts :\nGénération d’un microservice packagé sous forme d’un JAR. Ce JAR inclue le serveur web (conteneur de Servlets ou Netty). Le microservice ne nécessite qu’une simple JVM pour s’exécuter. Diminution de la base de code par un allègement important de la configuration des différents frameworks (dans Petclinic : Spring MVC, Spring Data JPA, EhCache). Spring Boot détecte les frameworks présents dans le classpath et les configure automatiquement. Des starters additionnels viennent étendre cette fonctionnalité. Monitoring et gestion des microservices en Production au travers d’endpoint REST exposés à l’aide des Actuator. Chacun des 4 microservices métiers customers, vets, visits et api-gateway est une application au sens Spring Boot. Chacun dispose de son propre module Maven contenant quelques classes Java et fichiers de configuration.\nPar exemple, le module spring-petclinic-visits-service comporte 4 classes :\nVisitsServiceApplication: la classe main du microservice annotée avec l’annotation @SpringBootApplication ainsi que l’annotation Spring Cloud @ EnableDiscoveryClient dont nous verrons l’intérêt par la suite. Visit: entité JPA représentant une visite et référençant l’animal par son ID (et non son type Java) dans un soucis de découplage des microservices. VisitRepository: interface Spring Data JPA implémentant le pattern Repository et permettant d’accéder aux visites stockées dans une base relationnelle (HSQLDB ou MySQL). VisitResource: contrôleur REST exposant une API pour créer une visite et lister les visites d’un animal. L’usage d’annotations Lombok permet d’alléger le code, mais n’a rien d’obligatoire. Comme vous le constatez, mise à part l’annotation Spring Cloud @EnableDiscoveryClient, le code Java de ce microservice ressemble à une application REST Spring Boot des plus classique. Une différence significative se trouve au niveau de leur configuration Maven (pom.xml) et de leur configuration applicative (.yml).\nIntégration de Spring Cloud Pour fonctionner de concert dans un environnement distribué, ces microservices vont s’appuyer sur un ensemble d’outils proposés par Spring Cloud : une gestion centralisée da la configuration, la découverte automatisée des autres microservices, la répartition de charge et le routage d’API.\nIntégrer ces différentes fonctionnalités Spring Cloud dans une application Spring Boot commence par la déclaration de starters Spring Cloud au niveau du pom.xml. Pour utiliser ces starters sans se soucier de leur version, un prérequis consiste à importer le BOM spring-cloud-dependencies (ex : le pom parent de Spring Petclinic Microservices) :\n\u0026lt;dependencyManagement\u0026gt; \u0026lt;dependencies\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.cloud\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-cloud-dependencies\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;${spring-cloud.version}\u0026lt;/version\u0026gt; \u0026lt;type\u0026gt;pom\u0026lt;/type\u0026gt; \u0026lt;scope\u0026gt;import\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;/dependencyManagement\u0026gt; Dans tous les différents microservices de Petclinic, on a commencé par ajouter le starter spring-cloud-starter-config qui permet d’aller récupérer la configuration applicative auprès du serveur de configuration :\n\u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.cloud\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-cloud-starter-config\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; Configuration Spring Cloud Par convention, la configuration d’une application Spring Boot est centralisée dans le fichier de configuration application.properties (ou application.yml). Via le mécanisme de hiérarchie de contextes, une application Spring Cloud initie un contexte de bootstrap qui charge sa configuration depuis le fichier bootstrap.yml.\nLe fichier bootstrap.yml est minimaliste. On y retrouve le nom du microservice et l’URL du serveur de configuration. Exemple issu de vets-service :\nspring: cloud: config: uri: http://localhost:8888 application: name: vets-service Pendant les développements, l’exécution de vets-service (et des autres microservices) demande à ce qu’un serveur de configuration soit démarré en local sur le port 8888. Au démarrage, le microservice ira récupérer le reste de sa configuration (complétant application.properties/yml et bootstrap.yml) auprès du le serveur de configuration. Veuillez noter ici que l’URL du serveur de configuration doit être connue et ne peut pas être découverte au runtime.\nServeur de configuration Le serveur de configuration utilisée par Petclinic est un serveur développé par les ingénieurs de Pivotal. Il fait partie intégrante de l’offre Spring Cloud. Ce serveur peut être mutualisé pour l’ensemble des microservices d’un SI. Toute la configuration applicative est versionnée dans un dépôt Git. Changer la configuration ne nécessite plus de rebuilder les applications ou de les redéployer. Un simple redémarrage est suffisant. Au travers de l’annotation @RefreshScope ou de l’événement EnvironmentChangeEvent, lorsque l’application a été designée pour, il est même possible de changer à chaud la configuration des beans Spring.\nLe serveur de config est packagé sous forme d’un JAR Spring Cloud. Pour créer le module spring-petclinic-config-server, un peu de dév a été nécessaire :\nGénérer une application minimaliste Spring Boot (par exemple via https://start.spring.io) Inclure une dépendance vers l’artefact spring-cloud-config-server: \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.cloud\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-cloud-config-server\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; Ajouter l’annotation @EnableConfigServer sur la classe main. Dans son fichier de configuration bootstrap.yml, on retrouve le port 8888 utilisé précédemment, mais surtout l’URL du repo Git hébergeant les fichiers de configuration :\nserver.port: 8888 spring: cloud: config: server: git: uri: https://github.com/spring-petclinic/spring-petclinic-microservices-config --- spring: profiles: local cloud: config: server: git: uri: file:///${GIT_REPO} Pendant la phase de développement, pour tester ses changements de configuration, il n’est pas nécessaire de les pousser sur le dépôt Git distant. Le profile Spring « local » permet d’aller chercher les fichiers dans un dépôt Git local au poste de dév en passant ces 2 paramètres à la JVM :\n-Dspring.profiles.active=local -DGIT_REPO=/projects/spring-petclinic-microservices-config\nPar simplicité, ni l’accès au dépôt, ni l’accès au serveur de configuration n’ont été sécurisés. C’est bien entendu nécessaire en entreprise. Le contenu des fichiers de configuration (comme les mots de passe) peut également être chiffré. Je vous renvoie à la documentation pour consulter toutes les options possibles.\nUne fois démarré, le serveur de configuration met à disposition la configuration au travers d’une API REST exposant plusieurs endpoints :\n/{application}/{profile}[/{label}] /{application}-{profile}.yml /{label}/{application}-{profile}.yml /{application}-{profile}.properties /{label}/{application}-{profile}.properties Pour en revenir avec notre exemple, lors de son démarrage depuis un poste de dév (avec le profile « default » de Spring activé), l’application vets-service récupère sa configuration via un GET sur l’URL http://localhost:8888/vets-service-default.yml Un navigateur, une commande curl ou Postman renverrait la réponse suivante :\neureka: instance: instance-id: ${spring.application.name}:${random.uuid} logging: level: org: springframework: INFO management: security: enabled: false petclinic: database: hsqldb server: port: 0 spring: cloud: refresh: refreshable: false datasource: data: classpath*:db/hsqldb/data.sql schema: classpath*:db/hsqldb/schema.sql jpa: hibernate: ddl-auto: none sleuth: sampler: percentage: 1.0 vets: cache: heap-size: 100 ttl: 60 Lors du traitement de la requête, le serveur de configuration fusionne le contenu de 2 fichiers du dépôt Git :\napplication.yml: la configuration transverse à tous les microservices vets-service.yml: la configuration spécifique à l’application vets-service Le serveur prend également en considération le ou les profiles Spring actifs côté appelant (mais pas celui déclaré dans le application.properties).\nLes logs de démarrage de vets-service confirment ce comportement :\nc.c.c.ConfigServicePropertySourceLocator : Fetching config from server at : http://localhost:8888 c.c.c.ConfigServicePropertySourceLocator : Located environment: name=vets-service, profiles=[default], label=null, version=0361c87037425c4d4ee4614fe0df06640de479a2, state=null b.c.PropertySourceBootstrapConfiguration : Located property source: CompositePropertySource {name=\u0026#39;configService\u0026#39;, propertySources=[MapPropertySource {name=\u0026#39;configClient\u0026#39;}, MapPropertySource {name=\u0026#39;https://github.com/spring-petclinic/spring-petclinic-microservices-config/vets-service.yml (document #1)\u0026#39;}, MapPropertySource {name=\u0026#39;https://github.com/spring-petclinic/spring-petclinic-microservices-config/vets-service.yml (document #0)\u0026#39;}, MapPropertySource {name=\u0026#39;https://github.com/spring-petclinic/spring-petclinic-microservices-config/application.yml (document #0)\u0026#39;}]} Le démarrage de vets-service échoue quelques millisecondes plus tard. Sa configuration est bien chargée, mais il n’arrive pas à s’enregistrer auprès de l’annuaire de service.\nAnnuaire de service Eureka Pour communiquer, les microservices doivent savoir se co-localiser. Dans une architecture microservices hébergée dans le Cloud, nous pouvons difficilement anticiper le nombre d’instances d’un même microservice (dépendant de la charge) ni même où elles seront déployées (et donc sur quelle IP et quel port elles seront accessibles). C’est là où le serveur Eureka rentre en jeu : il va mettre en relation les microservices. Chaque microservice va :\nS’enregistrer au démarrage puis donner périodiquement signe de vie (heartbeat toutes les 30 secondes) Récupérer l’adresse de leurs adhérences à partir d’un identifiant, en l’occurrence le nom de l’application déclaré via la propriété application.name (ex : vets-service) du boostrap.yml (chargé avant le application.properties) Eureka fait partie des projets OSS de Netflix supportés par Spring Cloud. A l’instar de ce qui a été fait pour le serveur de configuration, il est nécessaire de mettre en œuvre un serveur Eureka (module spring-petclinic-discovery-server). Cela se fait très simplement :\nPartir d’une application vierge Spring Boot Déclarer l’artefact spring-cloud-starter-netflix-eureka-server Ajouter l’annotation @EnableEurekaServer sur la classe main Déclarer l’artefact spring-cloud-starter-config et configurer l’adresse du serveur de configuration (idem pour tous les microservices) Configurer le serveur Eureka par l’intermédiaire du fichier discovery-server.xml (le nom de fichier correspond au nom de l’application spring.application.name) :\neureka: instance: hostname: localhost client: registerWithEureka: false fetchRegistry: false serviceUrl: defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/ Nous indiquons au serveur d’opérer dans la zone géographique par défaut et de ne pas s’enregistrer auprès d’autres instances d’Eureka. En production, redonder les instances d’Eureka renforcerait la tolérance aux pannes et lui éviterait d’être un Single Point of Failure (SPOF). A ce stade, le serveur Eureka peut être démarré.\nChaque microservice doit ensuite intégrer un client Eureka chargé de dialoguer avec le serveur Eureka :\nCommencer par déclarer le starter spring-cloud-starter-netflix-eureka-client Sur la classe main du microservice, ajouter l’annotation @EnableDiscoveryClient entraperçu sur la classe VisitsServiceApplication. L’annotation @EnableDiscoveryClient active l’implémentation Eureka de l’interface Spring Cloud DiscoveryClient chargée d’enregistrer le microservice et de localiser ses pairs. A noter que Spring Cloud supporte d’autres annuaires de service : Consul de Hashicorp et Apache Zookeeper.\nDans les logs de démarrage du microservice vets-service, la phase d’enregistrement Eureka intervient en dernier :\no.s.c.n.eureka.InstanceInfoFactory : Setting initial instance status as: STARTING com.netflix.discovery.DiscoveryClient : Initializing Eureka in region us-east-1 c.n.d.s.r.aws.ConfigClusterResolver : Resolving eureka endpoints via configuration com.netflix.discovery.DiscoveryClient : Disable delta property : false com.netflix.discovery.DiscoveryClient : Single vip registry refresh property : null com.netflix.discovery.DiscoveryClient : Force full registry fetch : false com.netflix.discovery.DiscoveryClient : Application is null : false com.netflix.discovery.DiscoveryClient : Registered Applications size is zero : true com.netflix.discovery.DiscoveryClient : Application version is -1: true com.netflix.discovery.DiscoveryClient : Getting all instance registry info from the eureka server com.netflix.discovery.DiscoveryClient : The response status is 200 com.netflix.discovery.DiscoveryClient : Starting heartbeat executor: renew interval is: 30 c.n.discovery.InstanceInfoReplicator : InstanceInfoReplicator onDemand update allowed rate per min is 4 com.netflix.discovery.DiscoveryClient : Discovery Client initialized at timestamp 1537461549479 with initial instances count: 7 o.s.c.n.e.s.EurekaServiceRegistry : Registering application vets-service with eureka with status UP com.netflix.discovery.DiscoveryClient : Saw local status change event StatusChangeEvent [timestamp=1537461549493, current=UP, previous=STARTING] com.netflix.discovery.DiscoveryClient : DiscoveryClient_VETS-SERVICE/vets-service:3e3fe9e1-071c-447f-94ca-2050dee7af2a: registering service... o.s.s.p.vets.VetsServiceApplication : Started VetsServiceApplication in 10.272 seconds (JVM running for 10.821) com.netflix.discovery.DiscoveryClient : DiscoveryClient_VETS-SERVICE/vets-service:3e3fe9e1-071c-447f-94ca-2050dee7af2a - registration status: 204 Le serveur Eureka vient avec une petite interface de supervision accessible en local à l’adresse http://localhost:8761/. Le statut des différents microservices et le nombre d’instances y sont visibles : A ce stade, nous avons vu comment faire pour enregistrer un microservice auprès de l’annuaire Eureka, mais pas encore comment fait appel à un microservice depuis un autre microservice.\nAppeler un microservice Dans Petclinic, le microservice front API Gateway centralise les appels aux API REST des 3 microservices back. On peut l’assimiler à un Backend for Frontend. Il permet de gérer les problématiques de CORS tout en assurant l’équilibrage de charge.\nPar exemple, lorsque l’utilisateur souhaite consulter l’écran de consultation d’un propriétaire, le code JavaScript du navigateur fait appel à l’URL : http://localhost:8080/api/gateway/owners/1\nLe contrôleur REST Spring MVC ApiGatewayController a la responsabilité de traiter cette requête HTTP. Il délègue son traitement au service CustomersServiceClient qui fait à son tour un appel REST au microservice customers-service:\npublic class CustomersServiceClient { private final RestTemplate loadBalancedRestTemplate; public OwnerDetails getOwner(final int ownerId) { return loadBalancedRestTemplate.getForObject(\u0026#34;http://customers-service/owners/{ownerId}\u0026#34;, OwnerDetails.class, ownerId); } } Le host de l’URL a une particularité : ce n’est ni un nom de domaine, ni un nom de serveur, ni même une adresse IP. Ici, on utilise l’ID du microservice, celui utilisé pour s’enregistrer auprès du serveur Eureka. L’autre particularité concerne le nom donné à l’instance du bean implémentant l’interface Spring MVC RestTemplate : loadBalanced RestTemplate.\nDans la configuration Spring du microservice ApiGatewayApplication, le bean RestTemplate est annoté avec l’annotation Spring Cloud @LoadBalanced :\n@Bean @LoadBalanced RestTemplate loadBalancedRestTemplate() { return new RestTemplate(); } De manière transparente pour le développeur, l’annotation @LoadBalanced configure le RestTemplate pour utiliser un répartiteur de charge (load-balancer) côté client. L’implémentation par défaut du LoadBalancerClient est Netflix Ribbon. La classe de configuration LoadBalancerAutoConfiguration se charge de positionner l’intercepteur LoadBalancerInterceptor sur le RestTemplate.\nCet intercepteur va faire appel au service Eureka pour localiser les différentes instances de customers-service disponibles. Il va ensuite appliquer l’ algorithme round-robin pour appeler successivement chaque instance et ainsi répartir la charge. D’autres algorithmes sont bien entendu disponible dans Ribbon (ils implémentent tous l’interface IRule).\nRemarque : par programmation, grâce à l’annotation @EnableDiscoveryClient, il est possible d’interroger le service Eureka pour récupérer manuellement la liste des instances disponibles et d’exploiter le tuple host/port de SeviceInstance :\n@Autowired private DiscoveryClient discoveryClient; … List\u0026lt;ServiceInstance\u0026gt; instances = discoveryClient.getInstances(\u0026#34;customers-service\u0026#34;); Router les appels Le microservice front API Gateway centralise les appels du navigateur. Bien qu’il puisse jouer un rôle d’agrégateur, la plupart des appels sont directement destinés aux microservices back : on a du 1 pour 1. Développer des contrôleurs REST chargés d’aiguiller la requête aux back n’a que peu d’intérêt.\nPour éviter tout boilerplate code, Spring Cloud Netflix propose d’utiliser le proxy Zuul. Activable via l’annotation @EnableZuulProxy, Zull va permettre de forwarder les requêtes reçues par l’API Gateway vers les microservices back. Il fait office de reverse proxy(comme le ferait un Apache ou un Nginx).\nPour bénéficier de Zuul, il est nécessaire d’ajouter au module spring-petclinic-api-gateway le starter spring-cloud-starter-netflix-zuul :\n\u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.cloud\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-cloud-starter-netflix-zuul\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; La configuration des routes est fait dans le fichier api-gateway.yml dont voici un extrait :\nzuul: prefix: /api ignoredServices: \u0026#39;*\u0026#39; routes: vets-service: /vet/** visits-service: /visit/** customers-service: /customer/** api-gateway: /gateway/** La requête http://localhost:8080/api/vet/vets est automatiquement routée par Zuul vers http://vets-service/vets. En interne, le proxy utilise Eureka pour localiser les instances de vets-setvice.\nConsole d’administration Le serveur Eureka propose une interface permettant de consulter la liste des microservices enregistrés et disponibles. C’est bien, mais insuffisant pour administrer toutes les applications d’un SI. Développé par codecentric AG, Spring Boot Admin est un projet communautaire permettant de monitorer et d’ administrer des applications Spring Boot déployées en production. Développée en Vue.js, l’IHM de Spring Boot Admin fait appel aux Actuators de Spring Boot pour connaître l’état des applications. Supportant Spring Cloud, il s’interface directement à Eureka pour récupérer la liste des différentes applications Spring Boot.\nParmi les fonctionnalités proposées par Spring Boot Admin, on peut lister :\nLa consultation du statut de chaque application La récupération de différentes métriques: JVM, mémoire, Micrometer.io, pool de connections, cache … La consultation des informations sur le build: date de création, sha1 du commit Git, GAV Maven … La gestion des logs: téléchargement des fichiers de logs, modification à chaud du niveau de logs La gestion des heapdump: consultation et téléchargement En cas de changement d’état d’une application, des notifications par mail, Slack, Hipchat, PageDuty … Petclinic intègre Spring Boot Admin dans le module spring-petclinic-admin-server.\nParti d’une application Spring Cloud rudimentaire, 2 dépendances Maven ont été ajoutées :\n\u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;de.codecentric\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-admin-starter-server\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;${spring-boot-admin.version}\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;de.codecentric\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-admin-server-ui\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;${spring-boot-admin.version}\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; L’annotation @EnableAdminServer a été ajoutée sur la classe main :\n@Configuration @EnableAutoConfiguration @EnableAdminServer @EnableDiscoveryClient public class SpringBootAdminApplication { public static void main(String[] args) { SpringApplication.run(SpringBootAdminApplication.class, args); } } Côté client, une dépendance vers Jolokia a été ajoutée dans les pom.xml. Jolokia permet d’exposer sur HTTP les beans JMX.\n\u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.jolokia\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;jolokia-core\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; Spring Boot Admin s’appuie sur les différents Actuators proposés par Spring Boot : heapdump, threadump, loggers, scheduledtasks \u0026hellip; Depuis Spring Boot 2, pour des raisons de sécurité, seuls les Actuators health et info sont exposés par défaut. Il est nécessaire d’activer explicitement les autres actuators. Dans le fichier de configuration application.yml, a été ajoutée la ligne suivante : management.endpoints.web.exposure.include: \u0026ldquo;*\u0026rdquo;\nUne fois démarré, Spring Boot Admin est accessible sur l’URL http://localhost:9090/ :\nEn sélectionnant une des 2 instances de customers-service, on accède aux différents outils d’administration, dont par exemple ici le suivi de la consommation de ressources (mémoire, thread, CPU) :\nSpring Boot Admin n’est pas limité à l’affichage d’informations dans de joli graphes. Un administrateur peut aller changer le niveau de log d’un logger Logback. Le changement de niveau est immédiat. Aucun redémarrage n’est nécessaire.\nTraces distribuées Afin de pouvoir tracer et debugger les appels HTTP entre nos microservices, un mécanisme de traces distribuées a été mis en œuvre à l’aide du client Spring Cloud Sleuth et du serveur Zipkin. L’interface graphique du serveur Zipkin permet de les consulter les piles d’appel et les adhérences entre microservices.\nEn pratique, le serveur Zipkin se déploie dans une image Docker. Sa personnalisation n’est plus supportée par l’équipe de Dév. Dans Petclinic, par simplicité, son intégration a été réalisée dans le module spring-petclinic-tracing-server sous forme d’une application Spring Boot configurée avec l’annotation dépréciée @EnableZipkinServer.\nL’interface est disponible sur l’URL : http://localhost:9411/zipkin/ Sur une période de temps, Zipkin sait générer un diagramme de dépendances entre microservices :\nContainerisation L’architecture de Petclinic repose sur un ensemble de 8 microservices, tous basés sur Spring Boot. Livrables sous forme d’un simple JAR, leur déploiement ne nécessite qu’un simple JRE Java 8. Pour déployer Petclinic chez un fournisseur Cloud proposant une offre de type Container as a Service (CaaS), les microservices doivent être packagés sous forme d’images Docker. Petclinic vient avec un exemple de packaging Docker pour un déploiement local (sur le poste de dév) à l’aide de Docker Compose.\nDans le POM parent, le profile Maven buildDocker permet de construire les images Docker à l’aide du plugin Maven de Spotify : ./mvnw clean install -PbuildDocker\nUne fois les images construites, on peut toutes les démarrer en une seule commande : docker-compose up\nLes images Docker reposent toutes sur le même Dockerfile (à noter que l’ENTRYPOINT est redéfini dans le docker-compose.yml):\nFROM openjdk:8 VOLUME /tmp ADD https://raw.githubusercontent.com/vishnubob/wait-for-it/master/wait-for-it.sh wait-for-it.sh RUN bash -c \u0026#39;chmod +x wait-for-it.sh\u0026#39; ARG ARTIFACT_NAME ARG EXPOSED_PORT ADD ${ARTIFACT_NAME}.jar /app.jar ENV SPRING_PROFILES_ACTIVE docker RUN bash -c \u0026#39;touch /app.jar\u0026#39; EXPOSE ${EXPOSED_PORT} ENTRYPOINT [\u0026#34;java\u0026#34;, \u0026#34;-XX:+UnlockExperimentalVMOptions\u0026#34;, \u0026#34;-XX:+UseCGroupMemoryLimitForHeap\u0026#34;, \u0026#34;-Djava.security.egd=file:/dev/./urandom\u0026#34;,\u0026#34;-jar\u0026#34;,\u0026#34;/app.jar\u0026#34;] Introduits depuis Java 8 update 131, les flags UnlockExperimentalVMOptions et UseCGroupMemoryLimitForHeap ordonnent à la JVM d’ utiliser ¼ de la mémoire allouée à l’OS (si Xmx non spécifié). Ils fonctionnent de pair avec le paramètre mem_limit spécifié dans le docker-compose.yml pour chaque image Docker.\nUne autre spécificité du Dockerfile concerne l’utilisation du script wait-for-it.sh. En effet, l’ordre de démarrage des microservices est important : le serveur de Configuration doit être démarré en premier, suivi de l’annuaire de Services et du serveur Zipkin. Les autres microservices peuvent ensuite être démarrés simultanément. Le script wait-for-it.sh permet de se mettre en attente de la disponibilité d’une application web. Dans le docker-compose.yml, l’entrypoint du container discovery-server attend que le config-server soit démarré avant de démarrer sa JVM : entrypoint: [\u0026quot;./wait-for-it.sh\u0026quot;,\u0026ldquo;discovery-server:8761\u0026rdquo;,\u0026quot;\u0026ndash;timeout=60\u0026quot;,\u0026quot;\u0026ndash;\u0026quot;,\u0026ldquo;java\u0026rdquo;, …]\nLes applications Spring Boot démarrent avec le profile Spring docker. Dans le fichier de configuration Spring Cloud de chaque microservice, ce profile écrase des valeurs par défaut utilisées pour un déploiement hors container. Si l’on prend comme exemple un extrait du fichier de configuration customers-service.yml :\n--- spring: profiles: docker zipkin: baseUrl: http://tracing-server:9411 server: port: 8081 eureka: client: serviceUrl: defaultZone: http://discovery-server:8761/eureka/ On remarque que :\nLe port HTTP est hard-codé et fixé à 8081. En effet, le Docker Compose ne démarre qu’une seule instance de customers-service. Son numéro de port n’a pas besoin d’être alloué dynamiquement par Spring Boot. L’URL du serveur Eureka et du serveur Zipkin référencent les paramètres container_name et ports du docker-compose.yml. Conclusion Ce long billet nous aura permis de voir comment mettre en place une architecture microservices à l’aide de Spring Boot, Spring Cloud et Netflix OSS. Comme support, nous nous serons appuyés sur l’application démo « Spring Petclinic Microservices » créée par Maciej Szarliński. De nombreuses améliorations sont d’ores et déjà prévues :\nRécupération des métriques à l’aide de Micrometer et Prometheus Support des JDK 10 et 11 Utilisation du client Feign à la place de RestTemplate Ce projet est communautaire : vos contributions sont les bienvenues. Enfin, pour celles et ceux que cela intéresse, sachez que d’autres fork de Spring Petclinic existent. Je vous renvoie vers cet ancien billet pour une présentation générale.\nRessources :\nRepo Git spring-petclinic/spring-petclinic-microservices de l’application Spring Petclinic Microservices Repo Git spring-petclinic/spring-petclinic-microservices-config de la configuration de Spring Petclinic Microservices Microservices a definition of this new architectural term (Martin Fowler) The rise of Java microframeworks (E4developer) Spring Cloud Config official documentation Appréhendez l’architecture Microservices (OpenClassrooms) ","link":"https://javaetmoi.com/2018/10/architecture-microservices-avec-spring-cloud/","section":"posts","tags":["eureka","netflix-oss","ribbon","spring-boot","spring-cloud","zipkin"],"title":"Architecture Microservices avec Spring Cloud"},{"body":"","link":"https://javaetmoi.com/tags/eureka/","section":"tags","tags":null,"title":"Eureka"},{"body":"","link":"https://javaetmoi.com/tags/netflix-oss/","section":"tags","tags":null,"title":"Netflix-Oss"},{"body":"","link":"https://javaetmoi.com/tags/zipkin/","section":"tags","tags":null,"title":"Zipkin"},{"body":"L’intégralité des vidéos des conférences et universités présentées lors de Devoxx France 2018 sont disponibles sur la chaîne Devoxx FR de Youtube.\nSi vous souhaitez rapidement vous faire un avis sur leur contenu avant de les visionner ou si vous souhaitez garder une trace écrite de ce que vous y avez appris, je mets librement à disposition mes notes prises au cours de ces 3 jours.\nLes sujets sont variés : le langage Java bien évidemment, des frameworks comme Spring et RxJS, de l’ outillage pour vos test tests et vos builds, de l’ infrastructure avec Docker et Kubernetes, de l’ architecture avec DDD et OpenAPI, sans oublier des sujets plus connexes tels la place du développeur en entreprise ou bien l’apprentissage du code aux enfants.\nDéveloppeur, reprends le digital en main (Alain Hélaïli) Après Java 8, Java 9 et 10 (Jean-Michel Doudoux) Les 12 factors Kubernetes (Etienne Coutaud) Un Loof çà va, de Loof, bonjour les dégâts (Nicolas et Thomas De Loof) Java dans Docker(Charles Sabourdin et Jean-Christophe Sirot) Migrer à Spring Boot 2 lorsqu’on a une « vraie » application (Julien Dubois) Spring Framework 5 : Feature Highlights \u0026amp; Hidden Gems (Juergen Hoeller) DDD \u0026amp; Event Sourcing à la rescousse pour implémenter la GDPR (Jérôme Avoustin et Guillaume Lours) En finir avec les problèmes de gestion de dépendance grâce à Gradle 5 (Cédric Champeau) Architecture hexagonale pour les nuls (Youen Chéné) Effective Java, Third Edition : Keepin’ it Effective (Joshua Bloch) Les Cookies HTTP #RetourAuxSources (Hubert Sablonnière) Nouvelle génération de tests pour projets Java (Vincent Massol) RxJS : les clefs pour comprendre les observables (Thierry Chatel) Swagger 2 est mort, vive OpenAPI 3 (Sébastien Lecacheur et Grégory Bloquel) ","link":"https://javaetmoi.com/2018/04/15-prises-de-notes-a-devoxx-france-2018/","section":"posts","tags":["ddd","devoxx","docker","java","kubernetes","rxjs","spring-boot","spring-framework"],"title":"15 prises de notes à Devoxx France 2018"},{"body":"","link":"https://javaetmoi.com/tags/rxjs/","section":"tags","tags":null,"title":"Rxjs"},{"body":"Lors de Devoxx France 2018, j’ai participé au Hands-on Lab d’initiation à Apache Cassandra. Animé par Alexander Dejanovski (The LastPickle) et Maxence Lecointe (Ippon), ce Lab m’aura enfin permis de découvrir cette base de donnée NoSQL, d’appréhender ses concepts fondamentaux, de jouer avec un cluster en local et d’écrire quelques requêtes CQL par le biais de son client Java.\nLe Lab était construit autour d’un support de présentation et de 5 exercices pratiques. Les slides Devoxx France – Initiation àApache Cassandra - Avril 2018.pdf et les exercices sont disponibles sur le dépôt GitHub thelastpickle/devoxxfr2018.\nCe billet a pour objectif de permettre aux développeurs n’ayant pas eu la chance de suivre ce Lab de profiter du travail préparatif des 2 speakers (un grand merci à eux) en lui donnant de la visibilité. Vous pourrez ainsi vous former par vous-même à Cassandra. Les explications données dans ce billet complètent les slides mais ne remplacent pas leur lecture.\nInstallation de CCM Slides : 4 à 5\nUne partie du Lab repose sur l’utilisation du Cassandra Cluster Manager (CCM). Cet outil est particulièrement pratique pour développer en local avec Cassandra. En effet, il permet de créer des clusters multi-nœuds et multi Data Centers (DC). Un pré-requis de ce Lab consiste donc à installer CCM en suivant son guide d’installation.\nCassandra n’est pas encore compatible avec Java 9 (cf. CASSANDRA-9608). Il est donc nécessaire d’utiliser un JDK 8 pour le démarrer.\nPour tester son installation, vous pouvez simuler la création d’un cluster de 3 nœuds sur 3 DC via la commande suivante :\nccm create my_cluster_3016 -v binary:3.0.16 -n 3:3 D’autres cas d’usage de CCM sont :\nle teste d’un programme sur différentes versions de Cassandra, et réaliser ainsi des tests d’upgrade / downgrade, l’exécution des tests d’intégration sur un cluster éphémère. Concepts fondamentaux Slides : 6 à 48\nAvant de pouvoir commencer le Lab, il est nécessaire d’acquérir quelques connaissances sur Cassandra et le Cassandra Query Language ( CQL). Ce dernier ressemble à du SQL.\nDans Cassandra, l’unité de stockage et de réplication est le Keyspace. Il s’apparente au schéma du monde des bases de données relationnelles. Lors de la création du Keyspace, on spécifie le facteur de réplication sur les nœuds du cluster. De ce paramétrage, va dépendre la montée en charge et la tolérance aux pannes de l’application. A noter que la réplication multi-datacenter est native dans Cassandra.\nLa création d’une Table requière la déclaration d’une clé primaire composée d’une partition key(id_flux dans la suite de l’article) et d’un clustering key. Comme en SQL, la clé primaire est unique. Le système de partition est basé sur des hashs. Chaque clé de partition est hachée. L’algorithme de hashing Murmur3 permet d’utiliser toute la plage des Long en Java. Avant Cassandra 1.2 et l’apparition des VNodes, un cluster de 4 nœuds se répartissait les tokens (hash) par plage (range). Le nœud A était responsable d’une des 4 plages. Lorsqu’on ajoutait des nœuds, le nœud A devenait responsable d’une plus petite plage. Problématique : on devait doubler le nombre de nœuds du cluster pour scaler proprement le cluster (chaque plage est alors de même taille). Avec les VNodes, chaque nœud s’attribue un range de tokens de manière aléatoire.\nComment déterminer la partition auquel appartient un enregistrement ? En appliquant la formule : Hash(id_flux) = token Chaque nœud dispose d’un répertoire interne contenant la répartition des tokens.\nLa réplication des données est gérée lors de leur enregistrement. L’id_flux est à la main du développeur.\nLe Storage Engine est inspiré de celui d’InnoDB. Il n’est pas maitre/esclave. Il existe une notion de range primaire, mais elle est purement logique.\nChose très importante : Cassandra est optimisée pour récupérer des données d’une seule partition. Pour être performante, les requêtes doivent donc requêter une seule partition.\nLa clé de clustering sert à ordonner les données. Clé de partition et clé de clustering peuvent être composites.\nLes types CQL sont nombreux : counter, inet, les collections (pour dénormaliser car pas de jointure), les tuples, uuid … (se référer au slide 17 pour une liste plus exhaustive)\nLimites du CQL par rapport au SQL (slide 18) :\nPas de jointures entre tables Pas de OR dans les clauses WHERE (que des AND) Limitations sur les champs du WHERE Pas d’INSERT/SELECT. Cette limitation implique l’utilisation d’un programme pour transvaser les données d’une table à une autre. Il est nécessaire de bien prévoir le modèle de données. Pas de vues. L’utilisation de Vues Dématérialisées est fortement déconseillée par Alexander Index peu performants Le GROUP BY introduit en 3.10 ne fonctionne que sur une partition. Comment requêter une table ?\nLes requêtes sont conditionnées par les colonnes définissant la clé primaire. Par exemple, avec la PRIMARY KEY (id_flux, id_etape, start_time), il est possible de réaliser une requête dont la clause WHERE porte sur\nid_flux id_flux et id_etape id_flux, id_etape et start_time L’ordre des colonnes est très important. Il est en effet interdit de requêter Cassandra sur :\nid_etape id_flux et start_time, car il manquerait la clause sur id_etape qui est avant start_time dans la clé primaire Cette limitation est très restrictive. Il est en effet nécessaire de maintenir autant de tables qu’il y’aura de requêtes. La modélisation des tables dépend donc directement de leur usage en lecture.\nEn résumé, l’usage dans Cassandra est 1 table par requête. Cela augmente le volume de données. Mais les écritures dans Cassandra sont peu chères (écriture en mémoire puis écriture séquentielle sur disque). Les données sont compressées dans Cassandra. La version 3.0 a grandement amélioré l’occupation de l’espace disque. Cassandra assure-t-elle la cohérence entre les 2 tables ? Oui, si tout se passe bien.\nLes slides 25 et 26 abordent le caractère d’ idempotence de Cassandra. Deux même INSERT (avec la même clé primaire) vont s’exécuter sans erreur. La dernière écriture prévaut sur la 1ière. La 1ière ligne est donc écrasée. Il n’existe pas de contraintes d’intégrité comme en SQL. Un INSERT et équivalent à un UPDATE. Avant d’insérer une ligne, il n’est plus besoin de savoir si l’enregistrement existe ou non. A noter qu’on travaille plus souvent avec des Sets que des Lists car les Sets garantissent l’idempotence. Différenciation syntaxique : utilisation des {} au lieu des []\nLa notion de Tombstone est très importante, car à l’origine de nombreux problèmes de performance. Un DELETE place une donnée spéciale appelée TOMBSTONE. Il s’agit d’un marqueur logique de suppression. Cassandra est codée en Java. Lire en mémoire des TOMBSTONE génère non seulement beaucoup d’I/O pour rien, mais exerce également beaucoup de pression sur le GC. Les enregistrements peuvent avoir une durée de vie (un TTL). Une fois la durée de vie passée, ils passent en TOMBSTONE. Pour garder un historique de 3 mois, fixer le TTL à ~7776000. Les conditions de purge des Tombstones sont complexes. La durée de vie minimale d’un Tombstone est de 10 jours. A partir de 100 000 Tombstones, Cassandra va killer la requête, se protégeant ainsi envers le GC.\nUne colonne de type Counter est mise à jour par incréments. Elle n’est pas idempotente. C’est parfait pour créer des statistiques approximatives en temps réels (ex : un compteur par heure, jour et mois). Spotify utilise les counters pour calculer en journée le nombre approximatif d’écoute. La nuit, ils utilisent un batch pour calculer le nombre exact.\nPour garantir des performances optimales, une règle de base est d’essayer d’avoir des partitions de moins de 100 Mo à cause du Heap. Pour s’en prémunir, on peut ajouter le jour dans la clé de partition. Exemple : PRIMARY KEY ((id_flux, jour), id_etape, start_time). Il devient alors nécessaire de lancer plusieurs requêtes en // pour requêter sur plusieurs jours. Bien que Cassandra offre la possibilité de créer des Index Secondaires, il est recommandé de les éviter. Les temps de réponse peuvent être considérablement dégradés. A noter que le timeout par défaut d’une requête est de 10 secondes.\nA présent que les présentations sont faites, le Lab va se dérouler en 2 parties :\nUne 1ière partie dédiée à l’administration d’un cluster et à son requêtage Une 2nde partie où vous allez écrire un programme Java chargée de lire et d’écrire dans une base Cassandra Lab – Part 1 Slides : 49 à 99\nVous allez commencer par créer un cluster Cassandra 3.0.16, puis apprendre à le démarrer, à consulter son statut et à savoir comment accéder à la configuration de chaque nœud.\nLa connexion à un cluster Cassandra repose sur le principe des Seed Nodes. C’est le même principe que sur les réseaux Peer-to-Peer type eMule. Pour accéder au cluster, il est nécessaire de connaître au moins un Seed Node. En règle général, on configure 3 Seeds nodes par Data Center. Les Seeds Nodes sont uniquement utilisés au démarrage d’un client pour prendre connaissance de la typologie du cluster. Un nœud est identifié par son Host ID (et non son IP). On peut donc changer l’IP d’un nœud. Un Rack peut être assimilé à une zone de disponibilité AWS ou bien encore à un rack électrique. Cassandra place une réplique par rack. Il est recommandé d’avoir 3 racks au minimum.\nLa suite du Lab requière l’utilisation d’un Cassandra Dataset Manager (CDM). Cet outil va être utilisé pour importer des données de films dans votre cluster Cassandra. Le Keyspace movielens est composé de 5 tables. La table principale movies utilise la colonne ID de type UUID comme clé primaire et donc clé de répartition. Il y’a donc un 1 enregistrement (1 film) par partition. Les 2 tables ratings_by_user et ratings_by_movie permettent de répondre à des requêtes différentes : par utilisateur ou par film. A noter qu’une base Cassandra n’est pas sécurisée de base. Il sera nécessaire d’activer l’authentification. Le Lab met en exergue qu’il est impossible de rechercher en l’état un film par titre partiel ou par sa première lettre. Vous allez être amenés à créer la table movies_by_first_letter qui permet une recherche par 1ière lettre ou 1er mot. Pour se faire, la 1ière lettre et le 1er mot du titre d’un film doivent avoir leur propre colonne. Cette table ne permet pas de faire une recherche de type like et encore moins une recherche approximative. Cassandra n’est pas fait pour de la recherche. Il est préférable de privilégier un moteur de recherche type Elasticsearch. Alexander précise qu’il existe une distribution commerciale alignant le sharding d’Elasticsearch avec celui de Cassandra. Pour combler ce vide en termes de recherche, Apple a contribué à l’amélioration de l’Index Secondaire avec SASSI. Cette fonctionnalité est à utiliser avec précaution.\nLors du Lab, la commande ccm node2 nodetool decommission permet de streamer les données sur les autres nœuds avant la décommision du nœud 2.\nCassandra tolère la perte de données en fonction du niveau de cohérence(Consistency Level) configuré :\nCohérence in fine : niveau ONE / LOCAL_ONE : lecture pas forcément à jour si mise à jour non terminée (1 seule réplique) Cohérence forte : Cassandra utilise le timestamp pour renvoyer la donnée à jour en fonction du quorum Lorsqu’une opération d’écriture nécessite d’avoir le Quorum, Cassandra attend d’avoir une majorité de répliques avant de valider l’écriture. Le nombre de réplique impacte le quorum, pas le nombre de nœuds. Lorsqu’on n’a que 2 Data Centers, on est obligé de faire du Local Quorum à cause du problème du split-brain. Lors de la resynchro des 2 DS, c’est la dernière écriture qui gagne. La lecture en Quorum ne requête pas toutes les répliques. Elle interroge un nombre suffisant de répliques (quel que soit le Data Center) Conseil d’Alexander : partez du principe que les applications ont besoin de cohérence et faites du Quorum. Lorsqu’on vient du Relationnel, on est habitué à voir de la cohérence. Démarrez avec du Local Quorum qui est plus simple que le Quorum.\nIntéressons-nous à présent au Client d’une base Cassandra. Une seule connexion est nécessaire pour l’ensemble des requêtes CQL. Il est toutefois possible de créer une connexion par cœur d’un CPU. Créer une connexion coute cher car elle se connecte à tous les nœuds du cluster. La connexion connaît le schéma et la typologie des nœuds (distribution des tokens). Cela permet au drivers de savoir si des nœuds tombent ou sont ajoutés. Une grande partie de l’intelligence se trouve dans le cluster.\nLors de la création d’une connexion, peuvent être spécifiées plusieurs stratégies :\nLoad Balancing policies: permet de choisir le nœud qui va traiter la requête CQL. Cassandra permet de créer sa propre policy. Retry policies : dans un système distribué, il peut y avoir des problèmes passager de réseau. Attention à bien positionner un setIdempotent(true) sur la requête lors d’un Retry. Certains drivers sont un peu plus complet que d’autres : le SpeculativeRetry n’est disponible que sur le driver Java. Les slides 84 à 99 expliquent comment coder un client Cassandra en Java :\nAjout de la dépendance Maven cassandra-driver-core Création et configuration de l’objet Cluster Création de l’objet Session: une fois l’objet Cluster créé, il faut créer un objet Session qui va permettre d’exécuter des requêtes CQL. Exécution d’une requête CQL puis récupération des données renvoyées Utilisation d’un PreparedStatement(recommandée) Exécution de requêtes asynchrones: le executeAsync() renvoie la main après avoir d’envoyer des écritures dans le cluster. On boucle ensuite sur la liste de Futures pour attendre la fin de l’écriture. Guava permet de simplifier l’écriture : Futures.successfulAsList(futures) Lab – Part 2 Slides : 100 à 140\nDans cette seconde partie, vous allez coder en Java 2 classes main :\nUne classe Writer chargée d’écrire des messages dans la table messages Une classe Reader chargée de lire les messages de la table messages, de les recopier dans une seconde table devoxx.messages_ack puis de les supprimer dans la 1ière table. Pour vous y aider, vous pourrez vous référer aux exemples de code des slides précédents. La branche part2-first-design-squelette met à votre disposition un projet Maven ainsi que des squelettes de classes qui sont à compléter.\nLe correctif est disponible dans la branche part2-first-design. Pour le tester en local, pensez à changer le ContactPoint.\nEn l’état, cette implémentation pose 2 problèmes :\nUne dégradation des performances au cours du temps. A force des suppressions, on lit de plus en plus de Tombstones. De la concurrence de lecture: les messages peuvent être traités par plusieurs participants. La suite du Lab consiste à coder une deuxième implémentation corrigeant ces 2 problèmes. Partez du squelette proposé dans la branche part2-second-design-squelette. Pour corriger le problème de performance, une solution consiste à designer la base autour des Tombstones. On est contraint de supprimer les enregistrements, mais on limite la durée de vie des partitions en utilisant un bucketing temporel (à la minute). Du coup, on aura moins de tombstone par partition/lecture. Pour résoudre le problème de concurrence de traitement, un système de verrou est mis en œuvre via l’introduction d’une colonne processed_by. Ce sont les LightWeights Transactions effectuées sur le champ processed_by qui vont nous permettre de verrouiller les enregistrements.\nLa solution est disponible sur la branche part2-second-design.\nLe Lab se termine par une présentation du mécanisme de compaction. La compaction permet de merger les données afin d’avoir des données à jour. Elle permet également de supprimer la Tombstone. Lors d’opération en écriture, Cassandra écrit dans le Heap puis flushe sur disque dans un SSTable (fichier immuable). Lors d’une opération de lecture, Cassandra va essayer d’adresser le moins de SSTable possible.\n","link":"https://javaetmoi.com/2018/04/lab-initiation-apache-cassandra/","section":"posts","tags":["cassandra","devoxx","nosql"],"title":"Initiation à Apache Cassandra"},{"body":"","link":"https://javaetmoi.com/tags/nosql/","section":"tags","tags":null,"title":"Nosql"},{"body":" Afin de normaliser la configuration Logback des applications web sur lesquelles j’interviens, j’ai récemment eu besoin de programmer Logback via son API en Java et non en utilisant la syntaxe XML Joran. Moins courant que le traditionnel logback.xml, cette possibilité de configurer Logback par le code offre davantage de possibilités, ne serait-ce que par son caractère dynamique.\nPar le passé, j’avais déjà eu l’occasion de manipuler l’API Logback dans des tests unitaires afin de changer dynamiquement le niveau de log des loggers. Cette fois-ci, je l’ai utilisé pour déclarer les différents appenders et configurer toute l’ infrastructure applicative de logs:\nActiver l’appender Console uniquement sur le poste de dév (afin qu’en prod, les logs ne se retrouvent pas en double dans le fichier server.log de JBoss) Factoriser la stratégie de journalisation des différents appenders fichiers (troubleshooting, overview, soap …) Récupérer différentes informations applicatives (ex : nom de la JVM, application, nom de l’environnement, login de l’utilisateur authentifié) à destination du collecteur de logs (Logstash ou Splunk) Exposer l’accès aux loggers au travers d’une API REST La configuration des logs reste paramétrable via un fichier de properties externe. En effet, le paramétrage peut différer d’un environnement de déploiement à l’autre (ex : chemin du répertoire des fichiers logs). La configuration Logback reste extensible par l’inclusion d’un fichier XML au format Joran.\nDans cet article, je vais vous présenter quelques bouts de code simplifié manipulant l’API Java de Logback.\nOù brancher l’initialisation ? Notre étude de cas porte sur une application web démarrée dans un conteneur de servlets (ex : Tomcat) ou un serveur d’application (ex : JBoss). Pour logger, les développeurs utilisent l’API SLF4J. Logback a été retenue comme implémentation de SLF4J.\nPour brancher la configuration Java, le moyen le plus courant est d’utiliser un ServletContextListener dédié. Afin d’être démarré en premier, ce listener est déclaré tout en haut du web.xml, en tête des autres listeners (avant le listener Spring).\nOutre la configuration Logback, on retrouve également dans ce listener la configuration SLF4J. Par exemple, pour initialiser le bridge JUL SLF4JBridgeHandler.\nExemple de Listener :\n@WebListener public class LogbackListener extends ContextAwareBase implements ServletContextListener { @Override public void contextInitialized(ServletContextEvent servletContextEvent) { setContext(getLobackContext()); // TODO : initialisation de Logback } A noter l’utilisation de la classe helper ContextAwareBase dont nous verrons l’usage par la suite.\nAccèder au LoggerContext Afin de pouvoir programmer Logback, il est nécessaire de récupérer l’instance de LoggerContext. Classe centrale de Logback, LoggerContext implémente l’interface ILoggerFactory de SLF4J. Pour la récupérer, on demande à SLF4J de nous retourner l’interface en passant par la méthode statique getILoggerFactory de la classe utilitaire LoggerFactory de SLF4J.\nUn bout de code est bien plus parlant. Pour s’y retrouver entre SLF4J et Logback, le nom qualifié des classes est utilisé :\nprivate LoggerContext getLogbackContext() { return (ch.qos.logback.classic.LoggerContext) org.slf4j.LoggerFactory.getILoggerFactory(); } Usage du ContextAwareBase La classe ContextAwareBase héritée par notre LogbackListener est une classe utilitaire proposée par Logback. Elle implémente l’interface ContextAware. Elle apporte 2 fonctionnalités :\nConserver en mémoire le contexte Logback (accessible via la méthode getContext) pour un accès ultérieur plus rapide. Fournir des méthodes de logs internes : addInfo, addWarn et addError. A priori, cela peut paraître étrange, mais vous allez bientôt connaître la raison. Dans le code du framework Logback comme dans le code applicatif chargé d’initialiser Logback, nous ne pouvons pas encore utiliser le système de logs de SLF4J/Logback pour tracer des erreurs ou tout simplement des informations utiles. A ce stade SLF4J/Logback n’est en effet pas encore prêt.\nPour pallier à cette problématique, Logback propose un mécanisme de statuts matérialisé par l’interface ch.qos.logback.core.status.Status. Les méthodes addInfo, addWarn et addError ajoutent un statut dans une liste de statuts interne à Logback et gérée par le StatusManager. Nous verrons par la suite comment exploiter ces statuts.\nDans notre Listener, nous pouvons ainsi tracer des informations importantes :\naddInfo(\u0026#34;Suppression du handler JUL : \u0026#34;+ handler.getClass().getName()); Installation du bridge SLF4J Afin de rediriger les logs de Java Util Logging (JUL) vers Logback, il est nécessaire d’ajouter au classpath le JAR jul-to-slf4j.jar puis d’installer le bridge SLF4JBridgeHandler. Ce bridge route tous les logs JUL vers l’API SLF4J qui redirige à son tour les logs vers Logback.\nVoici un exemple d’installation du SLF4JBridgeHandler. A noter qu’il met en application l’usage des méthodes addInfo et addError.\nprivate void configureJdkLoggingBridgeHandler() { try { if (isBridgeHandlerAvailable()) { java.util.logging.Logger rootLogger = LogManager.getLogManager().getLogger(\u0026#34;\u0026#34;); Handler[] handlers = rootLogger.getHandlers(); for (Handler handler : handlers) { if (handler instanceof ConsoleHandler) { addInfo(\u0026#34;Suppression du handler JUL : \u0026#34; + handler.getClass().getName()); rootLogger.removeHandler(handler); } } SLF4JBridgeHandler.install(); } } catch (RuntimeException ex) { addError(\u0026#34;Le bridge SLF4J pour JUL n\u0026#39;a pas été installé\u0026#34;, ex); } } Bien plus safe que l’appel à la méthode SLF4JBridgeHandler:: removeHandlersForRootLogger, le code ci-dessus retire le ConsoleHandler mais laisse ceux éventuellement ajoutés par le serveur d’application.\nLa méthode isBridgeHandlerAvailable() teste l’existence du bridge dans le classpath :\nprivate boolean isBridgeHandlerAvailable() { return ClassUtils.isPresent(\u0026#34;org.slf4j.bridge.SLF4JBridgeHandler\u0026#34;, ClassUtils.getDefaultClassLoader()); } Synchronisation Lorsque Logback est configuré à partir d’un fichier XML, Logback se prémunit de toute demande de configuration concurrente en utilisant le verrou LogbackLock (se référer à la méthode GenericConfigurator:: doConfigure(List)).\nPar précaution, toute la configuration du LogbackContext est réalisée dans un bloc synchronisé sur ce verrou :\nsynchronized (getLogbackContext().getConfigurationLock()) { stopAndReset(); enableDebugMode(); // ... } Réinitialisation du contexte Avant de pouvoir configurer par programmation les appenders et le niveau des loggers, il est nécessaire de réinitialiser Logback. En apparence, cela ne semble pas nécessaire puisque l’application vient tout juste de démarrer. C’est oublier qu’avant d’arriver dans le code du listener, la JVM aura déjà instancié de nombreuses classes. Or, la toute 1ière instanciation d’un logger déclenche automatiquement l’initialisation de SLF4J (méthode LoggerFactory::performInitialization) qui appelle à son tour l’initialisation de Logback (instanciation du LoggerContext par la classe StaticLoggerBinder). Logback recherche alors sa configuration dans le classpath en testant la présence des fichiers logback-test.xml, logback.groovy et logback.xml.\nVous avez compris : lorsqu’on arrive dans le listener, Logback s’est déjà initialisé. Il est donc préférable de faire le ménage en réinitialisant sa configuration par défaut. Pour se faire, on appelle successivement les méthodes stop() et reset() du LoggerContext. Et si le SLF4JBridgeHandler est installé, on propage à JUL les réinitialisations des niveaux des loggers :\nprivate void stopAndReset(LoggerContext loggerContext) { loggerContext.stop(); loggerContext.reset(); if (isBridgeHandlerAvailable()) { LevelChangePropagator levelChangePropagator = new LevelChangePropagator(); levelChangePropagator.setResetJUL(true); levelChangePropagator.setContext(loggerContext); loggerContext.addListener(levelChangePropagator); } } Activation du mode debug Nous avons vu que Logback conserve en mémoire une liste de Status permettant d’historiser certains évènements. Lorsque le mode debug de Logback est activé, les statuts sont affichés dans la sortie console standard (qui dans un serveur d’application comme JBoss est redirigé vers le fichier server.log).\nEn XML, l’activation du mode debug se fait dans au niveau de la balise racine : En Java, on enregistre le listener OnConsoleStatusListener auprès du StatusManager du contexte Logback :\nStatusListenerConfigHelper.addOnConsoleListenerInstance(loggerContext, new OnConsoleStatusListener()); Déclaration des appenders Logback délègue la tâche d’écriture des logs à des composants appelés « appenders». Implémentant l’interface Appender, les appenders permettent d’afficher les logs sur la console, de les archiver dans des fichiers ou bien encore de les stocker en base de données.\nLa déclaration des appenders communs à tous les loggers non exclusifs passe par la configuration du logger racine (le « root logger »). La méthode utilitaire root permet de spécifies son niveau et la liste des appenders :\nprivate void root(Level level, List\u0026lt;Appender\u0026lt;ILoggingEvent\u0026gt;\u0026gt; rootAppenders) { ch.qos.logback.classic.Logger logger = this.getLogbackContext().getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME); rootAppenders.forEach(logger::addAppender); } La déclaration de l’ appender Console permettant d’afficher les logs dans la sortie console est relativement simple :\nprivate ConsoleAppender\u0026lt;ILoggingEvent\u0026gt; consoleAppender() { ConsoleAppender\u0026lt;ILoggingEvent\u0026gt; appender = new ConsoleAppender\u0026lt;ILoggingEvent\u0026gt;(); PatternLayoutEncoder encoder = new PatternLayoutEncoder(); encoder.setPattern(CONSOLE_LOG_PATTERN); start(encoder); appender.setEncoder(encoder); appender(\u0026#34;console\u0026#34;, appender); return appender; } Comme son nom l’indique, la classe PatternLayoutEncoder permet d’indiquer à Logback le format d’affichage des logs. Voici un exemple de pattern :\nprivate static final String CONSOLE_LOG_PATTERN =\u2028\u0026#34;%d{HH:mm:ss.SSS} | %highlight(%-5level) | %cyan(%-50.50logger{49}) | %-200m %C.%M\\\\(%F:%L\\\\)%n\u0026#34;; Notez l’utilisation des sous-patterns de couleur%highlight et %cyan\nL’appel à la méthode start de l’encoder est nécessaire :\nprivate void start(LifeCycle lifeCycle) { if (lifeCycle instanceof ContextAware) { ((ContextAware) lifeCycle).setContext(this.context); } lifeCycle.start(); } De la même manière, il est nécessaire de démarrer l’appender. On passe par l’appel de la méthode appender:\nprivate void appender(String name, Appender\u0026lt;?\u0026gt; appender) { appender.setName(name); start(appender); } La déclaration d’un appender fichier est bien plus verbeuse :\nprivate RollingFileAppender\u0026lt;ILoggingEvent\u0026gt; troubleshootingAppender() { RollingFileAppender\u0026lt;ILoggingEvent\u0026gt; appender = new RollingFileAppender\u0026lt;\u0026gt;(); PatternLayoutEncoder encoder = new PatternLayoutEncoder(); // Do not rely on the OS default charset encoder.setCharset(APPENDER_CHARSET); encoder.setPattern(FILE_LOG_PATTERN); appender.setEncoder(encoder); start(encoder); appender.setFile(\u0026#34;/log/MyApp-troubleshooting.log\u0026#34;); SizeBasedTriggeringPolicy\u0026lt;ILoggingEvent\u0026gt; triggeringPolicy = new SizeBasedTriggeringPolicy\u0026lt;ILoggingEvent\u0026gt;(); triggeringPolicy.setMaxFileSize(FileSize.valueOf(\u0026#34;10MB\u0026#34;)); triggeringPolicy.start(); appender.setTriggeringPolicy(triggeringPolicy); FixedWindowRollingPolicy rollingPolicy = new FixedWindowRollingPolicy(); rollingPolicy.setMinIndex(1); rollingPolicy.setMaxIndex(10); String rollingName = \u0026#34;/log/backup/MyApp-troubleshooting_%i.log.zip\u0026#34;; rollingPolicy.setFileNamePattern(rollingName); appender.setRollingPolicy(rollingPolicy); rollingPolicy.setParent(appender); start(rollingPolicy); appender(\u0026#34;troubleshooting-file\u0026#34;, appender); return appender; } En complément du pattern d’affichage, la stratégie de journalisation doit être spécifiée. Dans notre exemple, lorsque le fichier de logs atteint 10 Mo, il est compressé en une archive zip qui est déplacée dans le sous-répertoire backup. Un total de 10 archives est conservé. La compression d’un fichier de log est généralement très efficiente et sera bien antérieure à 10% de la taille initiale. On garantit ainsi que, sur le filesystem, la taille totale des logs n’excèdera pas 20 Mo.\nLe pattern utilisé par cet appender diffère légèrement de celui de l’appender console en ajoutant le jour et le nom du thread et en n’utilisant pas les sous-patterns de couleur :\nprivate static final String FILE_LOG_PATTERN =\u2028\u0026#34;%d{yyyy/MM/dd HH:mm:ss.SSS} | %-50.50logger{49} | %-200m | %-5thread |%C.%M\\\\(%F:%L\\\\) %n\u0026#34;; Une fois déclarés les appenders racines, on appelle la méthode root présentée ci-dessus :\nroot(Level.INFO, appenders); Niveau des loggers Le niveau de logs des principaux frameworks est simple à configurer :\nprivate void base() { logger(\u0026#34;com.atomikos\u0026#34;, Level.WARN); logger(\u0026#34;org.apache.cxf\u0026#34;, Level.INFO); logger(\u0026#34;org.hibernate\u0026#34;, Level.INFO); logger(\u0026#34;org.springframework\u0026#34;, Level.INFO); } private void logger(String name, Level level) { Logger logger = this.getLogbackContext().getLogger(name); logger.setLevel(level); } Activation de JMX Afin de pouvoir pour exemple activer les niveaux des logs à chaud, il peut être intéressant d’exposer les MBean de Logback via JMX.\nprivate void enableJMX(ServletContext servletContext) { LoggerContext loggerContext = this.getLogbackContext(); String servletContextName = servletContext.getServletContextName(); if (servletContextName != null) { loggerContext.setName(servletContextName); } JMXConfiguratorAction jmxConfiguratorAction = new JMXConfiguratorAction(); jmxConfiguratorAction.setContext(loggerContext); try { jmxConfiguratorAction.begin(null, null, new AttributesImpl()); } catch (ActionException e) { addError(\u0026#34;The Logback JMX configuration failed\u0026#34;, e); } } Lorsque plusieurs WAR sont déploés dans une JVM, il est préférable de nommer le LoggerContext de chaque web app à partir du nom déclaré dans le web.xml.\nL’exposition des MBean sur HTTP est possible à l’aide de librairies tierces tels Jolokia.\nPour aller plus loin La configuration Logback présentée dans cet article pourrait être complétée par :\nLa configuration d’un appender LogstashEncoder permettant de générer les logs au format JSON et de les indexer les logs dans Elasticsearch La déclaration conditionnelle de l’appender Console sur le poste de dév (utilisation d’un fichier properties ou d’un paramètre de JVM) L’utilisation du contexte MDC pour afficher systématiquement dans les logs le login de l’utilisateur authentifié et l’identifiant de la session Un mécanisme d’extension de la configuration Logback par l’inclusion d’un fichier XML ou d’une classe Java externe. Conclusion Au cours de cet article, nous aurons vu comment configurer Logback à travers son API Java. Les classes manipulées sont les mêmes que celles utilisées par la syntaxe Groovy. Le code complet des snippets est disponible dans ce Gist.\nLe choix de privilégier la configuration Java a été fait par l’équipe de Sp r ing Boot. Je vous recommande d’aller jeter un coup d’œil aux classes DefaultLogbackConfiguration et LogbackConfigurator. Etant donné qu’il n’y a plus de fichier XML à parser, le temps de démarrage de l’application est sensiblement amélioré (100 ms d’après la documentation Logback).\nRessources :\nManuel d’utilisation de Logback Spring Boot - LoggingSystem abstraction and logging configuration properties Code source LogbackListener.java ","link":"https://javaetmoi.com/2018/03/configurez-logback-en-java/","section":"posts","tags":["logback"],"title":"Configurez Logback en Java"},{"body":" Spring WebFlux est une fonctionnalit é majeure de Spring Framework 5. Disposant de son propre module Maven (spring-weblux), ce nouveau framework web se positionne comme une alternative à Spring Web MVC. Ce dernier a été conçu par-dessus l’API Servlet. Spring WebFlux l’a été pour les applications r éactives, avec I/O non bloquantes, asynchrones, à faible latence, basées sur des serveurs comme Netty, Undertow ou compatibles Servlets 3.1 et +. Spring WebFlux s’éloigne du modèle d’un thread par requête HTTP et se base désormais sur le projet Reactor pour orchestrer le traitement des requêtes. Conçu avant tout pour exposer des API REST attaquant des bases NoSQL non bloquantes dans des architecture micro-services, Spring WebFlux peut être utilisé sur des applications web dont les IHM sont rendues côté serveur (ex : avec Thymeleaf ou Freemarker).\nJ’ai récemment migré vers Spring WebFlux la version Kotlin et Spring Boot de l’application démo Spring Petclinic. Dans ce court billet, je voulais vous lister les adaptations mises en œuvre dans le commit 279b2e7.\nChangement de dépendances Le build Gradle a été modifié en 2 points :\nLe starter Spring Boot spring-boot-starter-web est remplacé par spring-boot-starter-webflux La dépendance vers Expression Language (org.glassfish:javax.el) a été ajoutée pour les tests qui requièrent le support Bean Validation offert par Spring (classe LocalValidatorFactoryBean). Après résolution des dépendances, le changement le plus notable est que le JAR spring-webmvc a été remplacé par spring-weblux. Spring Web MVC s’appuie sur l’API Servlet. Pour preuve, toutes les classes de ce module appartiennent au package org.springframework.web.servlet. On y retrouvait par exemples les classes DispatcherServlet et ModelAndView. Spring WebFlux ne les utilise plus.\nUne migration quasi-transparente Spring WebFlux réutilisent les classes et annotations bien connues des développeurs Spring MVC : @Controller, @RequestMapping, @ModelAttribute, Model ou bien encore @InitBinder. La migration vers Spring WebFlux du code de production est donc relativement simple.\nLes contrôleurs doivent être ajustés afin de ne plus utiliser les classes du module spring-webmvc. Ces changements sont identifiés dès la phase de compilation. Dans l’exemple ci-dessous, la classe ModelAndView a été remplacée par la classe Model:\nUtilisation de ModelAndView avec Spring Web MVC :\n@GetMapping(\u0026#34;/owners/{ownerId}\u0026#34;) fun showOwner(@PathVariable(\u0026#34;ownerId\u0026#34;) ownerId: Int): ModelAndView { val mav = ModelAndView(\u0026#34;owners/ownerDetails\u0026#34;) mav.addObject(this.owners.findById(ownerId)) return mav } Code migré vers Spring WebFlux en utilisant la classe Model :\n@GetMapping(\u0026#34;/owners/{ownerId}\u0026#34;) fun showOwner(@PathVariable(\u0026#34;ownerId\u0026#34;) ownerId: Int, model: Model): String { model.addAttribute(this.owners.findById(ownerId)) return \u0026#34;owners/ownerDetails\u0026#34; } Au cours de la migration, des incompatibilités ont été détectées au runtime et lors de l’exécution des tests unitaires. Ce fut notamment le cas de la classe ModelMap qui provoquait une erreur lors de la résolution des paramètres :\njava.lang.IllegalStateException: Failed to invoke handler method with resolved arguments: [0][type=java.lang.Integer][value=1],[1][type=org.springframework.validation.support.BindingAwareConcurrentModel][value={owner=org.springframework.samples.petclinic.owner.Owner@373c0f9b, types=[bird, cat, dog, hamster, lizard, snake]}] on public java.lang.String org.springframework.samples.petclinic.owner.PetController.initUpdateForm(int,org.springframework.ui.ModelMap) at org.springframework.web.reactive.result.method.InvocableHandlerMethod.lambda$invoke$0(InvocableHandlerMethod.java:160) ~[spring-webflux-5.0.1.RELEASE.jar:5.0.1.RELEASE] at reactor.core.publisher.MonoFlatMap$FlatMapMain.onNext(MonoFlatMap.java:118) [reactor-core-3.1.1.RELEASE.jar:3.1.1.RELEASE]\nPour corriger ce problème, la classe ModelMap a été remplacée par Model dans les handlers de requêtes.\nUtilisation de ModelMap avec Spring Web MVC :\n@GetMapping(value = \u0026#34;/pets/{petId}/edit\u0026#34;) fun initUpdateForm(@PathVariable petId: Int, model: ModelMap): String { val pet = pets.findById(petId) model.put(\u0026#34;pet\u0026#34;, pet) return VIEWS_PETS_CREATE_OR_UPDATE_FORM } Remplacée par la classe Model avec Spring WebFlux :\n@GetMapping(value = \u0026#34;/pets/{petId}/edit\u0026#34;) fun initUpdateForm(@PathVariable petId: Int, model: Model): String { val pet = pets.findById(petId) model.addAttribute(\u0026#34;pet\u0026#34;, pet) return VIEWS_PETS_CREATE_OR_UPDATE_FORM } Refactoring des tests unitaires La migration a demandé davantage d’effort pour les tests unitaires de la couche web. Il a en effet été nécessaire de complètement les refactorer.\nSpring WebFlux ne permet plus de tester en boîte blanche les contrôleurs. La classe MockMvc n’existe plus. Et il est désormais impossible de vérifier l’état du model ou le nom de la vue rendue par le contrôleur.\nPour tester les contrôleurs, Spring WebFlux propose d’utiliser la classe WebTestClient, le pendant de la classe WebClient pour les tests. WebTestClient a été pensé avant tout pour tester les retours au format JSON. Tester du HTML est moins simple. Il est nécessaire d’évaluer les templates Thymeleaf, ce qui présente néanmoins l’avantage de les tester. Lorsque l’on souhaite effectuer des assertions XPath, il est nécessaire de normaliser le HTML au format XHTML (fermer les balises).\nSi l’on prend exemple sur la classe de test OwnerControllerTest, son en-tête a dû être modifiée en 3 points :\nL’annotation @WebMvcTest est remplacée par @WebFluxTest La classe de configuration ThymeleafAutoConfiguration a été ajoutée Injecté, le bean WebTestClient remplace MockMvc En-tête d’une classe de test d’un contrôleur Spring Web MVC :\n@RunWith(SpringRunner::class) @WebMvcTest(OwnerController::class) class OwnerControllerTest { @Autowired lateinit private var mockMvc: MockMvc En-tête d’une classe de test migrée à Spring WebFlux :\n@RunWith(SpringRunner::class) @WebFluxTest(OwnerController::class) @Import(ThymeleafAutoConfiguration::class) class OwnerControllerTest { @Autowired lateinit private var client: WebTestClient; Attardons-nous à présent sur l’une des méthodes de test. Par exemple, celle qui teste la soumission d’un formulaire invalide. Avec Spring Web MVC, les assertions s’appuient sur les méthodes attributeHasErrors et attributeHasFieldErrors de l’objet renvoyait par la méthode model() :\n@Test fun testProcessCreationFormHasErrors() { mockMvc.perform(post(\u0026#34;/owners/new\u0026#34;) .param(\u0026#34;firstName\u0026#34;, \u0026#34;Joe\u0026#34;) .param(\u0026#34;lastName\u0026#34;, \u0026#34;Bloggs\u0026#34;) .param(\u0026#34;city\u0026#34;, \u0026#34;London\u0026#34;) ) .andExpect(status().isOk) .andExpect(model().attributeHasErrors(\u0026#34;owner\u0026#34;)) .andExpect(model().attributeHasFieldErrors(\u0026#34;owner\u0026#34;, \u0026#34;address\u0026#34;)) .andExpect(model().attributeHasFieldErrors(\u0026#34;owner\u0026#34;, \u0026#34;telephone\u0026#34;)) .andExpect(view().name(\u0026#34;owners/createOrUpdateOwnerForm\u0026#34;)) } Avec Spring WebFlux, on analyse le contenu du HTML généré. Les assertions sont ici moins précises car les messages d’erreur ne sont pas reliés au champs de saisie :\n@Test fun testProcessCreationFormHasErrors() { val formData = LinkedMultiValueMap\u0026lt;String, String\u0026gt;(3) formData.put(\u0026#34;firstName\u0026#34;, Arrays.asList(\u0026#34;Joe\u0026#34;)) formData.put(\u0026#34;lastName\u0026#34;, Arrays.asList(\u0026#34;Bloggs\u0026#34;)) formData.put(\u0026#34;city\u0026#34;, Arrays.asList(\u0026#34;London\u0026#34;)) val res = client.post().uri(\u0026#34;/owners/new\u0026#34;) .header(\u0026#34;Accept-Language\u0026#34;, \u0026#34;en-US\u0026#34;) .body(BodyInserters.fromFormData(formData)) .exchange() .expectStatus().isOk .expectBody(String::class.java).returnResult() Assertions.assertThat(res.responseBody).contains(\u0026#34;numeric value out of bounds (\u0026amp;lt;10 digits\u0026amp;gt;.\u0026amp;lt;0 digits\u0026amp;gt; expected\u0026#34;) Assertions.assertThat(res.responseBody).contains(\u0026#34;must not be empty\u0026#34;) } Lors de la migration du test CrashControllerTest chargé de vérifier que la levée d’une exception technique renvoie sur une page d’erreur générique, après avoir importé la classe de configuration ErrorWebFluxAutoConfiguration, le template error.html n’était pas retrouvé. Un palliatif (temporaire ?) a été de le renommer en 5xx.html.\nDémarrage Une fois que le code compile et que les TU sont au vert, il reste à démarrer l’application. Dans les logs, on note un changement d’importance :\nNetty started on port(s): 8080 Ce n’est plus Jetty, mais Netty qui apparaît dans les logs de démarrage. Netty étant le serveur par défaut choisi par Pivotal dans Spring Boot pour faire exécuter les applications Spring WebFlux.\nPour aller plus loin Lorsqu’une application reactive utilise Spring WebFlux, 2 modèles de programmation sont proposés pour configurer la couche web :\nUtiliser les annotations de Spring MVC. Choix qui a été fait pour spring-petclinic-kotlin car le plus transparent lors d’une migration. Utiliser la programmation fonctionnelle via les Functional Endpoints pour déclarer les routes et les handlers de requêtes HTTP. Spring Framework 5 vient avec une nouvelle fonctionnalité : le Kotlin routing DSL. L’utilisation de ce DSL pourrait avoir du sens sur spring-petclinic-kotlin, au moins pour la partie REST. Peut-être la prochaine évolution ? Post-scriptum Comme précisé par Sébastien Deleuze dans la Pull Request #9, utiliser Spring WebFlux avec JPA peut entrainer des problèmes de scalabilité. En effet, JPA est une API bloquante. Un rollback vers Spring MVC a été réalisé. Une migration complète vers WebFlux passerait donc par une migration vers une base NoSQL supportant les appels non bloquants. L\u0026rsquo;UI devrait également être retravaillée pour profiter du streaming des données. Fonctionnellement, Petclinic n\u0026rsquo;en a pas réellement besoin et ne se prête donc pas bien au use case d\u0026rsquo;utilisation de WebFlux.\nRéférences :\nSpring WebFlux (manuel de référence de Spring Framework) Projet Reactor (site web oficiel) Commit GitHub montrant les différences entre l’utilisation de Spring MVC et Spring WebFlux Spring 5 WebClient (Baeldung blog) WebFlux Fonctionnal DSL (manuel de référence de Spring Framework) ","link":"https://javaetmoi.com/2017/12/migration-spring-web-mvc-vers-spring-webflux/","section":"posts","tags":["spring-mvc","spring-webflux"],"title":"Migration Spring MVC vers Spring WebFlux"},{"body":"","link":"https://javaetmoi.com/tags/spring-mvc/","section":"tags","tags":null,"title":"Spring-Mvc"},{"body":"","link":"https://javaetmoi.com/tags/spring-webflux/","section":"tags","tags":null,"title":"Spring-Webflux"},{"body":" En guise de conclusion de mon précédent billet, je proposais de migrer le build Maven d ’une application web Spring Boot 2 en un build Gradle bas é sur le langage Kotlin. C’est désormais chose faite. Mais bien que Gradle privil égie aujourd’hui l’usage du DSL Kotlin au détriment de Groovy, son guide d’utilisation n’a pas encore été actualisé et il est difficile de trouver de la documentation. Il faut passer par le projet GitHub kotlin-dsl pour accéder à quelques tutoriaux et des exemples. Heureusement, GitHub fourmille d’autres d’exemples, notamment du côté des projets soutenus par les contributeurs Pivotal sur Spring Boot.\nSans plus tarder, voici le fichier de conf build.gradle.kts de la version Kotlin de Spring Petclinic.\nLe fichier build.gradle.kts import org.jetbrains.kotlin.gradle.tasks.KotlinCompile buildscript { extra[\u0026#34;kotlinVersion\u0026#34;] = \u0026#34;1.1.60\u0026#34; extra[\u0026#34;springBootVersion\u0026#34;] = \u0026#34;2.0.0.M6\u0026#34; extra[\u0026#34;jUnitVersion\u0026#34;] = \u0026#34;5.0.0\u0026#34; extra[\u0026#34;boostrapVersion\u0026#34;] = \u0026#34;3.3.6\u0026#34; extra[\u0026#34;jQueryVersion\u0026#34;] = \u0026#34;2.2.4\u0026#34; extra[\u0026#34;jQueryUIVersion\u0026#34;] = \u0026#34;1.11.4\u0026#34; val springBootVersion: String by extra repositories { mavenCentral() maven(\u0026#34;https://repo.spring.io/milestone\u0026#34;) } dependencies { classpath(\u0026#34;org.springframework.boot:spring-boot-gradle-plugin:$springBootVersion\u0026#34;) classpath(\u0026#34;org.junit.platform:junit-platform-gradle-plugin:1.0.2\u0026#34;) } } plugins { val kotlinVersion = \u0026#34;1.1.60\u0026#34; id(\u0026#34;org.jetbrains.kotlin.jvm\u0026#34;) version kotlinVersion id(\u0026#34;org.jetbrains.kotlin.plugin.spring\u0026#34;) version kotlinVersion id(\u0026#34;io.spring.dependency-management\u0026#34;) version \u0026#34;1.0.3.RELEASE\u0026#34; } apply { plugin(\u0026#34;org.springframework.boot\u0026#34;) } val kotlinVersion: String by extra val springBootVersion: String by extra val jUnitVersion: String by extra val boostrapVersion: String by extra val jQueryVersion: String by extra val jQueryUIVersion: String by extra version = springBootVersion tasks { withType\u0026lt;KotlinCompile\u0026gt; { kotlinOptions { jvmTarget = \u0026#34;1.8\u0026#34; freeCompilerArgs = listOf(\u0026#34;-Xjsr305=strict\u0026#34;) } } } repositories { mavenCentral() maven(\u0026#34;https://repo.spring.io/milestone\u0026#34;) } dependencies { compile(\u0026#34;org.springframework.boot:spring-boot-starter-actuator\u0026#34;) compile(\u0026#34;org.springframework.boot:spring-boot-starter-cache\u0026#34;) compile(\u0026#34;org.springframework.boot:spring-boot-starter-data-jpa\u0026#34;) compile(\u0026#34;org.springframework.boot:spring-boot-starter-web\u0026#34;) compile(\u0026#34;org.springframework.boot:spring-boot-starter-thymeleaf\u0026#34;) compile(\u0026#34;javax.cache:cache-api\u0026#34;) compile(\u0026#34;org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion\u0026#34;) compile(\u0026#34;org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion\u0026#34;) compile(\u0026#34;org.webjars:webjars-locator\u0026#34;) compile(\u0026#34;org.webjars:jquery:$jQueryVersion\u0026#34;) compile(\u0026#34;org.webjars:jquery-ui:$jQueryUIVersion\u0026#34;) compile(\u0026#34;org.webjars:bootstrap:$boostrapVersion\u0026#34;) testCompile(\u0026#34;org.springframework.boot:spring-boot-starter-test\u0026#34;) testCompile(\u0026#34;org.junit.jupiter:junit-jupiter-api:$jUnitVersion\u0026#34;) testRuntime(\u0026#34;org.junit.jupiter:junit-jupiter-engine:$jUnitVersion\u0026#34;) runtime(\u0026#34;org.hsqldb:hsqldb\u0026#34;) runtime(\u0026#34;mysql:mysql-connector-java\u0026#34;) } Quelques explications Le DSL Kotlin pour Gradle a facilité la déclaration des plugins en introduisant le bloc plugins {}. Par exemple, le plugin de compilation du code source Kotlin de l’application est déclaré dans ce bloc. Suite à un bug JUnit 5, le plugin junit-platform-gradle-plugin a dû rester déclaré dans le bloc buildscript {}. Un contournement existe, mais il ne fonctionne pas correctement lorsque les plugins sont tirés de plusieurs repos Maven, ce qui est le cas du plugin spring-boot-gradle-plugin en version Milestone 2.0.0.M6 qui n’est pas encore publié dans Maven Central.\nLe plugin Spring Boot pour Gradle org.springframework.boot joue un triple rôle :\nLa construction le uber-JAR exécutable L’ exécution de la webapp via la ligne de commande gradle bootRun La gestion des dépendances Attardons-nous un moment sur la gestion des dépendances. Fonctionnant de concert avec le plugin Dependency Management io.spring.dependency-management, le plugin Spring Boot permet d’éviter de déclarer la version des dépendances déclarées dans le bloc dependencies {} ; du moins, celles déclarées dans le BOM Maven spring-boot-starter-parent qu’il importe.\nLa centralisation des versions non déclarées dans le BOM passe par l’usage de la propriété extra déclarée sous forme d’extension Kotlin. Il est toutefois regrettable qu’ extra ne soit pas visible du bloc plugins {}.\nConclusion Par rapport au pom.xml d’origine, le build Gradle n’intègre pas encore la génération du CSS à partir de LESS (issue #4). Novice en Kotlin et en Gradle, toute suggestion d’amélioration du build.gradle.kts est la bienvenue. J’attends vos pull requests :-)\nRéférences :\nBetter dependency management for Gradle (Andy Wilkinson) Spring Boot Gradle plugin (Manuel de référence de Spring Boot) Projet GitHub sdeleuze/pring-kotlin-functional (Sébastien Deleuze) Projet GitHub mixitconf/mixit (MiXiT website) ","link":"https://javaetmoi.com/2017/11/build-gradle-dsl-kotlin-webapp-spring-boot/","section":"posts","tags":["gradle","kotlin","spring-boot"],"title":"Build Gradle en Kotlin d’une webapp Spring Boot"},{"body":"","link":"https://javaetmoi.com/tags/gradle/","section":"tags","tags":null,"title":"Gradle"},{"body":"","link":"https://javaetmoi.com/tags/kotlin/","section":"tags","tags":null,"title":"Kotlin"},{"body":" Lors la dernière conférence Google I/O qui s’est tenue en mai 2017, Google a officialisé le support de Kotlin sur Android. Google n’est pas le seul acteur de l’IT à miser sur ce nouveau langage créé par JetBrains (l’éditeur de l’IDE IntelliJ) et s’exécutant sur la JVM (mais pas que). En effet, dès février 2016, Pivotal proposait de développer des applications Spring Boot avec Kotlin. En janvier 2017, ils annonçaient que la version 5 du framework Spring proposerait des fonctionnalités exclusives à Kotlin. Chez Gradle, le langage Kotlin est désormais privilégié au détriment de Groovy.\nPour découvrir ce nouveau venu dans la galaxie des langages de programmation, je me suis intéressé à migrer vers Kotlin l’application démo Spring Petclinic développée en Java et Spring Boot. Je souhaitais ici partager son code source : spring-petclinic-kotlin et énumérer les différences notables avec sa version Java.\nUne migration en souplesse En m’appuyant sur le manuel de référence de Kotlin, j’ai pu migrer l’application sans trop de difficulté et en quelques heures. IntelliJ m’a grandement facilité la tâche puisqu’un copier/coller d’une classe Java dans un fichier Kotlin (extension .kt) lançait le plugin de conversion automatique. Quelques adaptations manuelles restaient néanmoins nécessaires.\nGrâce à l’interopérabilité de Kotlin avec Java, j’ai pu faire cohabiter classes Kotlin et classes Java dans le même projet IntelliJ. Au cours de la migration, cela m’a permis de vérifier régulièrement le bon fonctionnement l’application.\nDes conventions qui changent Kotlin changent certaines conventions du langage Java :\n1. Les classes et les méthodes sont par défaut finales et ne peuvent être héritées / redéfinies sans l’utilisation du mot clé open Dans Petclinic, la classe BaseEntity parente de toutes les entités JPA est déclarée ainsi :\n@MappedSuperclass open class BaseEntity L’omission du paramètre open déclenche une erreur de compilation des classes filles : « This type is final, so it cannot be inherited from ».\nCe changement de comportement impacte le fonctionnement de certaines librairies tierces. En effet, lors de l’utilisation d’annotations tels que @Cacheable ou @Configuration, le framework Spring utilise l’héritage pour instrumenter le code. La configuration du plugin Spring pour le compilateur Kotlin dans le pom.xml permet de s’affranchir de l’ajout du mot clé open sur les beans Spring de type @Component.\n2. La visibilité des méthodes et des classes est par défaut publique Appartenant au package visit, la classe Visit est référencée par la classe Pet du package de même niveau owner:\nclass Visit : BaseEntity() 3. Les types primitifs de Java disparaissent. Plus besoin de choisir entre un int et un Integer : vous utiliserez un Int.\n4. Le type des variables et de retour de méthode n’est plus déclaré à gauche mais à droite. Extrait de l’interface OwnerRepository :\nfun findById(@Param(\u0026ldquo;id\u0026rdquo;) id: Int): Owner\nIl faut s’y faire et retrouver ses habitudes du bon vieux Turbo Pascal.\n5. Par défaut, aucune variable ne peut être null. Le compilateur vous rappellera à l’ordre. Lorsqu’une variable peut prendre la valeur null, il est nécessaire de le préciser explicitement en faisant suivre son type par le caractère ?\nvar name: String? = null 6. Les getter/setter (mutateurs) des propriétés d’une classe sont générés automatiquement par Kotlin. Dans le code, on accède directement à une propriété sans passer par les mutateurs. Kotlin ajoute automatiquement l’appel au mutateur correspondant. Là ou en Java on passait par un setter :\njames.setLastName(\u0026#34;Carter\u0026#34;); en Kotlin, on affecte directement la valeur à la propriété :\njames.lastName = \u0026#34;Carter\u0026#34; Bien entendu, Kotlin offre la possibilité de ne générer qu’un des 2 mutateurs et/ou de redéfinir leur implémentation. Par exemple, dans la BaseEntity.kt, la propriété isNew est évaluée à partie de l’ID de l’entité :\nval isNew: Boolean get() = this.id == null Une syntaxe allégée Par rapport à Java, Kotlin se veut apporter de la concision sans perdre en lisibilité, et ceci par le biais de léger changements syntaxiques.\n1. Le signe point-virgule ; en fin d’instruction devient facultatif. Et lorsqu’une méthode ne comporte qu’une seule instruction, l’utilisation d’ accolades et du mot clé return ne sont plus nécessaires. Extrait du PetController Java :\n@ModelAttribute(\u0026#34;types\u0026#34;) public Collection\u0026lt;PetType\u0026gt; populatePetTypes() { return this.pets.findPetTypes(); } Extrait du PetController Kotlin :\n@ModelAttribute(\u0026#34;types\u0026#34;) fun populatePetTypes(): Collection\u0026lt;PetType\u0026gt; = this.pets.findPetTypes() 2. Concernant l’héritage et l’implémentation d’une interface, les mots clés extends et implements sont remplacés par le symbole :\nCode Java :\npublic interface VetRepository extends Repository\u0026lt;Vet, Integer\u0026gt; { Code Kotlin :\ninterface VetRepository : Repository\u0026lt;Vet, Int\u0026gt; { 3. Le compilateur Kotlin sait inférer le type des variables. Lorsque vous déclarez une variable en lui affectant une valeur (autre que null), il n’est plus nécessaire de spécifier son type. Exemple issu de Owner.kt :\n@Column(name = \u0026#34;city\u0026#34;) @NotEmpty var city = \u0026#34;\u0026#34; 4. L’instruction for each permettant d’itérer sur les éléments d’une collection change de syntaxe. Kotlin passe du : au in. A noter que le type de variable n’est plus exigé.\nVersion Java :\nfor (Pet pet : getPetsInternal()) { Version Kotlin :\nfor (pet in pets) { Des améliorations intéressantes La plus-value de Kotlin par rapport à Java dépasse les conventions et les changements syntaxiques évoqués dans les 2 paragraphes précédents.\n1. Kotlin propose de créer automatiquement des POJO avec getters, setters, méthodes equals(), hashCode(), toString() et copy() (cette dernière étant propre à Kotlin) via un mécanisme appelé data class. La classe Vets profite de cette simplification :\n@XmlRootElement data class Vets(var vetList: Collection\u0026lt;Vet\u0026gt;? = null) 2. Dans les contrôleurs Spring MVC écrits en Java, il est courant d’avoir une suite de conditions if else dont chaque bloc renvoie sur une page différente. Extrait de la méthode processFindForm de la classe Java OwnerController:\nif (results.isEmpty()) { result.rejectValue(\u0026#34;lastName\u0026#34;, \u0026#34;notFound\u0026#34;, \u0026#34;not found\u0026#34;); return \u0026#34;owners/findOwners\u0026#34;; } else if (results.size() == 1) { owner = results.iterator().next(); return \u0026#34;redirect:/owners/\u0026#34; + owner.getId(); } else { model.put(\u0026#34;selections\u0026#34;, results); return \u0026#34;owners/ownersList\u0026#34;; } Pour réduire le nombre de return, Kotlin permet d’utiliser le if comme expression et non plus comme instruction. Lorsqu’une branche contient plusieurs instructions, la dernière est assignée au if ; dans l’exemple ci-dessous, c’est le nom de la page :\nreturn if (results.isEmpty()) { result.rejectValue(\u0026#34;lastName\u0026#34;, \u0026#34;notFound\u0026#34;, \u0026#34;not found\u0026#34;) \u0026#34;owners/findOwners\u0026#34; } else if (results.size == 1) { val foundOwner = results.iterator().next(); \u0026#34;redirect:/owners/\u0026#34; + foundOwner.id } else { model.put(\u0026#34;selections\u0026#34;, results) \u0026#34;owners/ownersList\u0026#34; } Autant dire que Kotlin sait faire plaisir à SonarQube en limitant l’usage de l’instruction return.\nUne autre façon d’écrire ce code consiste à utiliser l’expression when qui est une sorte de super switch case. Dans la classe OwnerController Kotlin, les if / else disparaissent au profit de lambdas :\nreturn when { results.isEmpty() -\u0026gt; { result.rejectValue(\u0026#34;lastName\u0026#34;, \u0026#34;notFound\u0026#34;, \u0026#34;not found\u0026#34;) \u0026#34;owners/findOwners\u0026#34; } results.size == 1 -\u0026gt; { \u0026#34;redirect:/owners/\u0026#34; + results.first().id } else -\u0026gt; { model.put(\u0026#34;selections\u0026#34;, results) \u0026#34;owners/ownersList\u0026#34; } } 3. Compatible avec Java 6, Kotlin avait introduit les lambda avant Java 8. Les collections ont été enrichies de méthodes permettant d’itérer, de filtrer, de trier, trouver un élément, récupérer le dernier … On peut y accéder directement, à savoir passer par un stream.\nExtrait de la classe Owner codée en Java 6 :\npublic List\u0026lt;Pet\u0026gt; getPets() { List\u0026lt;Pet\u0026gt; sortedPets = new ArrayList\u0026lt;\u0026gt;(getPetsInternal()); PropertyComparator.sort(sortedPets, new MutableSortDefinition(\u0026#34;name\u0026#34;, true, true)); return Collections.unmodifiableList(sortedPets); } Pendant en Kotlin :\nfun getPets(): List\u0026lt;Pet\u0026gt; = pets.sortedWith(compareBy({ it.name })) La méthode find() permet de rechercher un élément dans une collection. A noter l’utilisation de l’opérateur ?: qui permet de lever une exception si find renvoie null. Extrait de la classe PetTypeFormatter.kt:\nfindPetTypes.find { it.name == text } ?: throw ParseException(\u0026#34;type not found: \u0026#34; + text, 0) 4. Bien que par défaut les variables ne puissent être null, nous avons vu qu’il était possible de les rendre nullable. L’ opérateur elvis ?. permet d’accéder à des propriétés sans craindre des NullPointerException :\nval compName = pet.name?.toLowerCase() Kotlin proposent d’autres fonctionnalités fortes intéressantes que je n’ai pas eu l’occasion de mettre en œuvre dans Spring Petclinic. Je pense notamment aux extension function s qui permettent d’ajouter dynamiquement des méthodes à une classe.\nDes changements plus discutables 1. La déclaration de constantes ne passe plus par l’usage des mots clés static final devant la propriété d’une classe. A la place, Kotlin propose de passer par des constantes de portée globale ou par des objets companion.\nConstantes globales (extrait de PetControllerTest.kt) :\nconst val TEST_OWNER_ID = 1 const val TEST_PET_ID = 1 Constantes internes à une classe (extrait de PetValidator.kt) :\ncompanion object { const val TEST_PET_ID = 1 } 2. Un développeur Spring et JPA utilise massivement les annotations. Or, lorsque la propriété est multi-valuée (tableau), Kotlin requière l’utilisation du mot clé arrayOf Exemple d’un mapping @OneToMany JPA extrait de Owner.kt :\n@OneToMany(cascade = arrayOf(CascadeType.ALL), mappedBy = \u0026#34;owner\u0026#34;) var pets: MutableSet\u0026lt;Pet\u0026gt; = HashSet() Pour le coup, on perd en lisibilité par rapport à Java. Heureusement, ce désagrément devrait être corrigé dans une prochaine version de Kotlin : KT-11235\nConclusion Pour un développeur Java, l’apprentissage de Kotlin se fera sans trop d’effort. Sans révolutionner Java, Kotlin permet de moderniser sa syntaxe. Il apporte quelques nouveautés fortes appréciables une fois qu’on y a goûté.\nDébutant sur Kotlin, je suis preneur de toute suggestion d’amélioration (et il doit y en avoir !!). Tout contributeur est le bienvenu.\nPour aller jusqu’au bout de l’exercice, il serait intéressant de migrer le build Maven vers un build Gradle écrit en Kotlin (réf. #2). Là encore, avis aux amateurs.\nRessources :\nKotlin version of Spring Petclinic Documentation de référence du language Kotlin Developing Spring Boot applications with Kotlin Introducing Kotlin support in Spring Framework 5.0 Java vs Kotlin : le comparatif des langages natifs Android ","link":"https://javaetmoi.com/2017/09/migrez-application-java-spring-boot-vers-kotlin/","section":"posts","tags":["kotlin","spring-boot"],"title":"Découvrir Kotlin en migrant une webapp Spring Boot"},{"body":"","link":"https://javaetmoi.com/tags/algorithme/","section":"tags","tags":null,"title":"Algorithme"},{"body":" Faisant partie des algorithmes de la théorie des graphes, l\u0026rsquo;algorithme de Kruskal permet de rechercher un arbre recouvrant de poids minimum.\nUne application pratique de l\u0026rsquo;algorithme de Kruskal consiste à relier tous les ordinateurs d\u0026rsquo;un même réseau local avec une longueur optimale de fibre optique.\nDans ce billet, vous trouverez une implémentation Java de cet algorithme. Il m\u0026rsquo;aura permis de résoudre le problème Fibre Optique donné en finale du concours du Meilleur Dev de France 2017.\nimport java.io.FileNotFoundException; import java.util.ArrayList; import java.util.Comparator; import java.util.List; public class KruskalAlgo { /** * Détermine l\u0026#39;arbre couvrant de poids minimum (ARPM) à partir d\u0026#39;un graphe connexe non-orienté et pondéré. * \u0026lt;p\u0026gt; * Utilise l\u0026#39;algorithme de Kruskal * @see https://fr.wikipedia.org/wiki/Algorithme_de_Kruskal * * @param vertices graphe constitué d\u0026#39;un ensemble de points dans un plan à 2 dimensions. * @return arrêtes de l\u0026#39;arbre couvrant de poids minimum dans ce graphe. */ static List\u0026lt;Edge\u0026gt; compute(List\u0026lt;Vertex\u0026gt; vertices) { // Calcule les arêtes et leur poids List\u0026lt;Edge\u0026gt; allEdges = new ArrayList\u0026lt;\u0026gt;(); for (int i = 0; i \u0026lt; vertices.size(); i++) { for (int j = i + 1; j \u0026lt; vertices.size(); j++) { allEdges.add(new Edge(vertices.get(i), vertices.get(j))); } } // Tri par poids ascendant allEdges.sort(Comparator.comparingDouble(Edge::getWeight)); // Applique l\u0026#39;algo de Kruskal List\u0026lt;Edge\u0026gt; graph = new ArrayList\u0026lt;\u0026gt;(); int i = 0; while (graph.size() \u0026lt; vertices.size() - 1) { Edge edge = allEdges.get(i++); int id1 = edge.u.clusterId; int id2 = edge.v.clusterId; // L\u0026#39;arête est ajouté au compute si ses 2 sommets n\u0026#39;appartiennent pas au même réseau if (id1 != id2) { graph.add(edge); // Regroupe les sommets des 2 réseaux venant d\u0026#39;être reliés for (Vertex v : vertices) if (v.clusterId == id2) { v.clusterId = id1; } } } return graph; } static class Vertex { static int NEX_ID = 0; private final int x; private final int y; private int clusterId = NEX_ID++; Vertex(int x, int y) { this.x = x; this.y = y; } } static class Edge { private final Vertex u; private final Vertex v; private final double weight; Edge(Vertex v1, Vertex v2) { this.u = v1; this.v = v2; this.weight = Math.hypot(Math.abs(v1.x - v2.x), Math.abs(v1.y - v2.y)); } double getWeight() { return weight; } } public static void main(String args[]) throws FileNotFoundException { List\u0026lt;Vertex\u0026gt; vertices = new ArrayList\u0026lt;\u0026gt;(); vertices.add(new Vertex(0, 2)); vertices.add(new Vertex(0, 0)); vertices.add(new Vertex(1, 1)); vertices.add(new Vertex(2, 1)); vertices.add(new Vertex(3, 2)); vertices.add(new Vertex(4, 2)); vertices.add(new Vertex(3, 0)); List\u0026lt;Edge\u0026gt; graph = KruskalAlgo.compute(vertices); System.out.println(graph.stream().mapToDouble(Edge::getWeight).sum()); // 7.656854249492381 } } ","link":"https://javaetmoi.com/2017/09/algo-java-kruskal-recherche-arbre-couvrant-poids-minium/","section":"posts","tags":["algorithme","java"],"title":"Implémentation Java de l'algorithme de Kruskal"},{"body":" Dans ce billet, j’ai eu l’envie de vous partager mon implémentation Java du très célèbre problème du rendu de monnaie dont voici l’énoncé : étant donné un système de monnaie, comment rendre de façon optimale une somme donnée, c\u0026rsquo;est-à-dire avec le nombre minimal de pièces et de billets ? Par exemple, dans le système monétaire de l’Euro, la manière la plus optimale de rendre 6 euros consiste à rendre un billet de 5 € et une pièce de 1 €, même si d’autres combinaisons existent (ex : 3 x 2 € ou 6 x 1 €).\nDans le cas d’un système monétaire non canonique, utiliser un algorithme glouton ne donnera pas nécessairement un résultat optimal. Il est nécessaire de passer par la méthode algorithmique dite de programmation dynamique.\nVoici l’ implémentation Java récursive par programmation dynamique de rendu de monnaie:\npackage com.javaetmoi.algo; import java.util.HashMap; import java.util.Map; /** * Résolution par programmation dynamique et récursivité du calcul de rendu de monnaie. */ public class AlgoRenduMonnaie { /** * Système de monnaie (exemple de l\u0026#39;Euro : [1, 2, 5, 10, 20, 50, 100, 200, 500]) */ private long[] pieces; /** * Cache des résultats intermédiaires. * \u0026lt;p\u0026gt; * Format : Map\u0026lt;Montant, Map\u0026lt;IndexPiece, RenduMonnaie\u0026gt;\u0026gt; */ private Map\u0026lt;Long, Map\u0026lt;Integer, RenduMonnaie\u0026gt;\u0026gt; resultatsIntermediaires = new HashMap\u0026lt;\u0026gt;(); /** * Constructeur * * @param pieces système de monnaire utilisé par l\u0026#39;algorithme. */ public AlgoRenduMonnaie(long[] pieces) { this.pieces = pieces; } /** * Méthode principale exécutant l\u0026#39;algorithme de rendu de monnaie. * \u0026lt;p\u0026gt; * Cette méthode peut-être appelée plusieurs fois. * Elle réutilisera les résultats des précédents appels. * * @param montant somme à rendre * @return résultat optimal ou \u0026lt;code\u0026gt;null\u0026lt;/code\u0026gt; si rendu de monnaie impossible */ public RenduMonnaie calculerRenduMonnaieOptimal(long montant) { initResultatsIntermediaires(); return calculeMonnaie(montant); } /** * Structure de données renvoyée par l\u0026#39;algorithme et également utilisée pour les calculs intermédiaires. */ public class RenduMonnaie { private final long montant; /** * Pour chaque piece du systeme monétaire, conserve le nombre minimal de pieces à rendre pour le montant donné. */ private final int[] nbPiecesARendre; /** * Constructeur pour un montant à 0. */ RenduMonnaie() { this.montant = 0; this.nbPiecesARendre = new int[pieces.length]; } RenduMonnaie(long montant, RenduMonnaie precedent, int indexPiece) { this.montant = montant; int length = precedent.nbPiecesARendre.length; nbPiecesARendre = new int[length]; System.arraycopy(precedent.nbPiecesARendre, 0, nbPiecesARendre, 0, length); nbPiecesARendre[indexPiece]++; } public int[] getNbPiecesARendre() { return nbPiecesARendre; } public long getMontant() { return montant; } public int nbPieces() { int nbPieces = 0; for (int i = 0; i \u0026lt; pieces.length; i++) { nbPieces += nbPiecesARendre[i]; } return nbPieces; } public String toString() { StringBuilder str = new StringBuilder(); for (int i = 0; i \u0026lt; nbPiecesARendre.length; i++) { if (nbPiecesARendre[i] != 0) { str.append(nbPiecesARendre[i]).append(\u0026#34;x\u0026#34;).append(pieces[i]).append(\u0026#34;€ \u0026#34;); } } return str.toString(); } } /** * Renvoie l\u0026#39;index de la pièce la plus proche d\u0026#39;un montant. */ private Integer getIndexPieceMax(long montant) { Integer pieceMax = null; for (int i = 0; i \u0026lt; pieces.length; i++) { if (pieces[i] \u0026lt;= montant) { pieceMax = i; } } return pieceMax; } /** * Initialise le cache avec le 1er résultat : rendre un montant de zéro consiste à ne rendre aucune pièce. */ private void initResultatsIntermediaires() { RenduMonnaie renduZero = new RenduMonnaie(); Map\u0026lt;Integer, RenduMonnaie\u0026gt; zeroMap = new HashMap\u0026lt;\u0026gt;(); for (int i = 0; i \u0026lt; pieces.length; i++) { zeroMap.put(i, renduZero); } resultatsIntermediaires.put(0L, zeroMap); } /** * Méthode appelée récursivement. * * @param montant somme à rendre * @return résultat optimal ou \u0026lt;code\u0026gt;null\u0026lt;/code\u0026gt; si rendu de monnaie impossible */ private RenduMonnaie calculeMonnaie(long montant) { Integer indexPieceMax = getIndexPieceMax(montant); if (indexPieceMax == null) { return null; } resultatsIntermediaires.putIfAbsent(montant, new HashMap\u0026lt;\u0026gt;()); RenduMonnaie meilleurRendu = null; int meilleurePiece = -1; for (int indexPiece = indexPieceMax; indexPiece \u0026gt;= 0; indexPiece--) { long nouveauMontant = montant - pieces[indexPiece]; resultatsIntermediaires.putIfAbsent(nouveauMontant, new HashMap\u0026lt;\u0026gt;()); RenduMonnaie renduOptimal = resultatsIntermediaires.get(nouveauMontant).get(indexPiece); if (renduOptimal == null) { renduOptimal = calculeMonnaie(nouveauMontant); } if (renduOptimal != null) { if ((meilleurRendu == null) || (meilleurRendu.nbPieces() \u0026gt; renduOptimal.nbPieces())) { meilleurRendu = renduOptimal; meilleurePiece = indexPiece; } } } if (meilleurRendu != null) { meilleurRendu = new RenduMonnaie(montant, meilleurRendu, meilleurePiece); resultatsIntermediaires.get(montant).put(meilleurePiece, meilleurRendu); } return meilleurRendu; } } Le test unitaire JUnit associé valide différents cas de tests et documente son utilisation :\npackage com.javaetmoi.algo; import org.junit.Ignore; import org.junit.Test; import static org.junit.Assert.*; public class MonnaieTest { @Test public void monnaie_2_5_10_montant_1() { calculerMonnaie(new long[]{2, 5, 10}, 1L, null); } @Test public void monnaie_2_5_10_montant_5() { calculerMonnaie(new long[]{2, 5, 10}, 5L, 1); } @Test public void monnaie_2_5_10_montant_6() { calculerMonnaie(new long[]{2, 5, 10}, 6L, 3); } @Test public void monnaie_2_5_10_montant_10() { calculerMonnaie(new long[]{2, 5, 10}, 10L, 1); } @Test public void monnaie_2_5_10_montant_37() { calculerMonnaie(new long[]{2, 5, 10}, 37L, 5); } @Test public void monnaie_1_3_4_montant_6() { calculerMonnaie(new long[]{1, 3, 4}, 6L, 2); } @Test @Ignore public void monnaie_1_2_5_10_20_50_100_200_500_montant_1989() { calculerMonnaie(new long[]{1, 2, 5, 10, 20, 50, 100, 200, 500}, 1989L, 11); } private void calculerMonnaie(long[] pieces, long montant, Integer nbPiecesAttendues) { AlgoRenduMonnaie algo = new AlgoRenduMonnaie(pieces); AlgoRenduMonnaie.RenduMonnaie rendu = algo.calculerRenduMonnaieOptimal(montant); if (nbPiecesAttendues == null) { assertNull(rendu); return; } assertNotNull(rendu); for (int i = 0; i \u0026lt; pieces.length; i++) { if (rendu.getNbPiecesARendre()[i] != 0) { System.out.println(\u0026#34;Nombre de pièces de \u0026#34; + pieces[i] + \u0026#34; € : \u0026#34; + rendu.getNbPiecesARendre()[i]); } } assertEquals(montant, rendu.getMontant()); assertEquals(nbPiecesAttendues.intValue(), rendu.nbPieces()); } } Plus le système monétaire est dense et plus le montant à rendre est élevé, plus il y’a de chance que la récursivité provoque des débordements de pile d’appel (je vous invite à tester le test annoté avec @Ignore). Passer par une impléemntation itérative permettrait de résoudre ce problème.\nPour complexifier le problème, on pourrait tenir compte du nombre de pièces disponibles dans la caisse et adapter le rendu de monnaie en conséquences. La mise en cache des résultats intermédiaires ne serait alors plus possible.\nRéférences :\nProblème de rendu de monnaie, Wikipedia Rendu de monnaie, bases de programmation dynamique, Jill-Jênn Vie \u0026amp; Clémence Corrigé du contrôle des connaissances Algorithmique \u0026amp; Programmation (INF431) ","link":"https://javaetmoi.com/2017/07/algo-rendu-monnaie-programmation-dynamique-java/","section":"posts","tags":["algorithme","java"],"title":"Implémentation Java de l'algorithme de rendu de monnaie par programmation dynamique"},{"body":"Dans ce billet, nous allons mettre en pratique l’initiation à Vue.js reçue le mois dernier. Je vous propose de coder un pseudo Google Form avec l’aide de Vue.js, de Bootsrap et du framework de validation VeeValidate. Le formulaire HTML est généré automatiquement à partir d’un paramétrage JSON récupéré par une API REST. Nous n’aborderons pas ici la partie serveur. Un utilisateur peut sauvegarder son formulaire à l’état de brouillon afin de poursuivre ultérieurement sa saisie. Le formulaire à afficher peut donc être pré-saisi. La validation est dynamique: elle se fait au fur et à mesure de la saisie du formulaire. Voici un exemple de formulaire :\nDémo live Avant de passer aux explications, mettons en action ce formulaire. HTML, code JavaScript et rendu graphique sont accessibles dans ce snippet JSFiddle codé avec Vue 2.2, VeeValidate 2.0 et Bootstrap 3.3 :\nLe modèle objet du formulaire Vue.js implémentant le pattern MVC, intéressons-nous au modèle objet sous-jacent à notre formulaire :\nUn formulaire est composé d’une liste de questions. Chaque question comporte un libellé suivi d’un champ de saisi. Le champ de saisi peut différer en fonction du type question : zone de saisie sur une ligne, radio bouton, liste déroulante, zone de texte multi-lignes … Le paramétrage du formulaire (et son état courant) est décrit sous forme d’un tableau de questions en notation littérale JavaScript :\nvar formParameters = [ { id: 1, label: \u0026#39;First Name\u0026#39;, type: \u0026#39;input\u0026#39;, answer: \u0026#39;Antoine\u0026#39; }, { id: 2, label: \u0026#39;Last Name\u0026#39;, type: \u0026#39;input\u0026#39; }, { id: 3, label: \u0026#39;Email\u0026#39;, type: \u0026#39;input\u0026#39;}, { id: 4, label: \u0026#39;Job\u0026#39;, type: \u0026#39;select\u0026#39;, options: [\u0026#39;...\u0026#39;, \u0026#39;Developer\u0026#39;, \u0026#39;Ops\u0026#39;, \u0026#39;Project Manager\u0026#39;], answer: \u0026#39;Developer\u0026#39; }, { id: 5, label: \u0026#39;Gender\u0026#39;, type: \u0026#39;radio\u0026#39;, options: [\u0026#39;Male\u0026#39;, \u0026#39;Female\u0026#39;], answer: \u0026#39;Male\u0026#39;}, { id: 6, label: \u0026#39;Address\u0026#39;, type: \u0026#39;textarea\u0026#39;, placeholder: \u0026#39;Your zip code and city\u0026#39;} ]; En pratique, le paramétrage du formulaire sera récupéré par API REST au chargement de la page. Afin de rendre autonome notre exemple, il y est hard-codé. Voici à quoi ressemble le point d’entrée de notre application Vue.js :\nvar app = new Vue({ el: \u0026#39;#dynform\u0026#39;, data: { questions: [] }, created: function () { // Dynamic Form could be load from a REST API this.questions.push(formParameters); } }); Le tableau de questions (notre modèle) est stocké dans l’objet data de l’instance Vue.\nArbre de composants Orienté composants, Vue.js permet de structurer la génération du formulaire à l’aide de plusieurs composants. Le composant générique est responsable d’afficher le libellé de la question puis de sélectionner le sous-composant approprié pour la zone de saisie. Exemple : un lorsque la question est de type radio. Il gère également l’affichage du caractère wildcard * lorsque la question est obligatoire.\nLa page HTML La majorité du code HTML est localisé dans les templates Vue.js des sous-composants. Le code HTML de la page du formulaire est réduit à un simple générant autant de balises que de questions paramétrées dans le modèle du formulaire :\n\u0026lt;form id=\u0026#34;dynform\u0026#34; class=\u0026#34;panel-body form-horizontal\u0026#34; v-on:submit.prevent=\u0026#34;displayForm\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;row\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;col-md-12 \u0026#34;\u0026gt; \u0026lt;form-question v-for=\u0026#34;question in questions\u0026#34; :question=\u0026#34;question\u0026#34; :key=\u0026#34;question.id\u0026#34;\u0026gt;\u0026lt;/form-question\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;button type=\u0026#34;submit\u0026#34; class=\u0026#34;btn btn-primary\u0026#34;\u0026gt;Submit\u0026lt;/button\u0026gt; \u0026lt;/form\u0026gt; La soumission du formulaire déclenche la fonction displayForm dont nous verrons l’implémentation par la suite.\nLe composant Le composant accepte en paramètre l’une des questions de notre formulaire. Conditionnel, son template est généré dynamiquement en JavaScript par concaténation de String. Ne pouvant utiliser la propriété template, il est initialisé dans la fonction created au travers de la propriété this.$options.template.\nVue.component(\u0026#39;form-question\u0026#39;, { props: [\u0026#39;question\u0026#39;], created: function () { this.$options.template = \u0026#39;\u0026lt;div class=\u0026#34;form-group\u0026#34;\u0026gt; \u0026#39; + \u0026#39;\u0026lt;label :for=\u0026#34;question.id\u0026#34; class=\u0026#34;col-sm-3 col-lg-2 control-label\u0026#34;\u0026gt;\u0026#39; + \u0026#39;{{question.label}}\u0026#39;; if (this.question.required || ((this.question.validate !== undefined) \u0026amp;\u0026amp; this.question.validate.match(\u0026#34;required\u0026#34;))) { this.$options.template += \u0026#39;\u0026lt;em\u0026gt;*\u0026lt;/em\u0026gt;\u0026#39; } this.$options.template += \u0026#39;\u0026lt;/label\u0026gt;\u0026#39; + \u0026#39;\u0026lt;div class=\u0026#34;col-sm-9 col-lg-10\u0026#34;\u0026gt;\u0026#39;; switch (this.question.type) { case \u0026#39;input\u0026#39; : this.$options.template += \u0026#39;\u0026lt;form-input :question=\u0026#34;question\u0026#34;\u0026gt;\u0026lt;/form-input\u0026gt;\u0026#39;; break; case \u0026#39;select\u0026#39; : this.$options.template += \u0026#39;\u0026lt;form-select :question=\u0026#34;question\u0026#34;\u0026gt;\u0026lt;/form-select\u0026gt;\u0026#39;; break; ... } this.$options.template += \u0026#39;\u0026lt;/div\u0026gt;\u0026#39; + \u0026#39;\u0026lt;/div\u0026gt;\u0026#39;; } }); Le switch case permet de sélectionner le sous-composant Vue à afficher : , , et . Chacun d’eux accepte un seul paramètre : la question courante à afficher.\nForm Input Au travers du , regardons de plus près à quoi ressemble un sous-composant. Voici une version dénudée de validation :\nVue.component(\u0026#39;form-input\u0026#39;, { props: [\u0026#39;question\u0026#39;], template: \u0026#39;\u0026lt;div class=\u0026#34;form-group\u0026#34;\u0026gt;\u0026#39; + \u0026#39;\u0026lt;input :name=\u0026#34;question.label\u0026#34; :id=\u0026#34;question.id\u0026#34; type=\u0026#34;text\u0026#34; class=\u0026#34;form-control\u0026#34;\u0026#39; + \u0026#39; v-model=\u0026#34;question.answer\u0026#34; :placeholder=\u0026#34;question.placeholder\u0026#34;/\u0026gt;\u0026#39; + \u0026#39;\u0026lt;/div\u0026gt;\u0026#39; }); Les attributs id, name et placeholder sont attribués par binding en utilisant la syntaxe raccourcie de v-bind:name=\u0026ldquo;question.label\u0026rdquo;. La valeur du champs de saisie référence le modèle question.answer.\nLes autres sous-composants sont conçus sur le même modèle.\nValidation du formulaire La validation du formulaire est implémentée à l’aide de la librairie VeeValidate. Chaque question du modèle se voit ajouter un attribut validate spécifiant les contraintes de validation à l’aide de la syntaxe VeeValidate. Exemple sur le nom de famille qui est requis, ne doit comporter que des caractères alphabétiques et au minimum 2 caractères :\n{id: 2, label: \u0026#39;Last Name\u0026#39;, type: \u0026#39;input\u0026#39;, validate: \u0026#34;required|alpha|min:2\u0026#34;} Le template de chaque sous-composant est agrémenté avec un attribut v-validate bindé sur le modèle validate. En cas d’erreur de validation, le message d’erreur est affiché dans un et la classe CSS has-error de Bootstrap et ajouté au englobant de type form-group. Complétons ainsi notre exemple du sous-composant :\nVue.component(\u0026#39;form-input\u0026#39;, { props: [\u0026#39;question\u0026#39;], template: \u0026#39;\u0026lt;div class=\u0026#34;form-group\u0026#34; :class=\u0026#34;{\\\u0026#39;input\\\u0026#39;: true, \\\u0026#39;has-error\\\u0026#39;: errors.has(question.label) }\u0026#34;\u0026gt;\u0026#39; + \u0026#39;\u0026lt;input type=\u0026#34;text\u0026#34; v-validate=\u0026#34;question.validate\u0026#34; :id=\u0026#34;question.id\u0026#34; :name=\u0026#34;question.label\u0026#34;\u0026#39;+ \u0026#39;class=\u0026#34;form-control\u0026#34; v-model=\u0026#34;question.answer\u0026#34;:placeholder=\u0026#34;question.placeholder\u0026#34;/\u0026gt;\u0026#39; + \u0026#39; \u0026lt;span v-show=\u0026#34;errors.has(question.label)\u0026#34; class=\u0026#34;help-block\u0026#34;\u0026gt;{{ errors.first(question.label) }}\u0026lt;/span\u0026gt;\u0026#39; + \u0026#39;\u0026lt;/div\u0026gt;\u0026#39; }); Factorisation du template de gestion des erreurs La gestion des erreurs de validation est identique sur chaque sous-composant. Le se voit ajouter la classe CSS Boostrap has-error lorsque VeeValidate détecte une ou plusieurs erreurs. Le affiche le 1er message d’erreur détecté. Ayant toutes 2 besoins d’accéder à la propriété errors locale au sous-composant, ces balises HTML ne peuvent être remontées dans le composant . Pour éviter la duplication de code HTML dans les template, il est néanmoins possible de factoriser le code dans une fonction questionTemplate :\nfunction questionTemplate(customField) { return \u0026#39;\u0026lt;div class=\u0026#34;form-group\u0026#34; :class=\u0026#34;{\\\u0026#39;input\\\u0026#39;: true, \\\u0026#39;has-error\\\u0026#39;: errors.has(question.label) }\u0026#34;\u0026gt;\u0026#39; + customField + \u0026#39;\u0026lt;span v-show=\u0026#34;errors.has(question.label)\u0026#34; class=\u0026#34;help-block\u0026#34;\u0026gt;{{ errors.first(question.label) }}\u0026lt;/span\u0026gt;\u0026#39; + \u0026#39;\u0026lt;/div\u0026gt;\u0026#39; } Vue.component(\u0026#39;form-input\u0026#39;, { props: [\u0026#39;question\u0026#39;], template: questionTemplate(\u0026#39;\u0026lt;input v-validate=\u0026#34;question.validate\u0026#34; :name=\u0026#34;question.label\u0026#34; :id=\u0026#34;question.id\u0026#34; type=\u0026#34;text\u0026#34; class=\u0026#34;form-control\u0026#34; v-model=\u0026#34;question.answer\u0026#34; :placeholder=\u0026#34;question.placeholder\u0026#34;/\u0026gt;\u0026#39;) }); A noter que cette factorisation n’a pas été mise en œuvre dans le snippet JSFiddle.\nValidation globale Avant de soumettre au serveur le formulaire, une validation globale est réalisée côté client. En cas de succès, le snippet affiche au format JSON les données à transmettre. En cas d’erreur, il affiche leur nombre et les messages d’erreur à côté de chaque champ en erreur.\nLa validation d’un formulaire composé de plusieurs sous-composants n’est pas native avec VeeValidate, preuve en est l’issue Can\u0026rsquo;t validate form with multiple child components. Plutôt que de passer par un composant faisant office de bus de messages, j’ai choisi d’utiliser l’ API de validation. L’instance $validator de l’application Vue est recyclée. Les contraintes de validation de chaque champ lui sont rattachées (méthode attach). L’objet data référence les données du formulaire à valider. Cet objet est passé à la méthode de validation validateAll qui accepte 2 fonctions de callback :\nEn cas de succès (méthode then), un tableau contenant les données à soumettre au serveur est construit puis, dans le cadre de la démo, affiché simplement dans une popup. Lorsqu’un ou plusieurs champs sont invalides (méthode catch), un artifice consistant à itérer sur l’ensemble des sous-composants et à déclencher leur validation individuelle permet d’afficher le message d’erreur local et d’activer le style CSS approprié. Le nombre de champs invalide est affiché dans une popup. methods: { displayForm: function(event) { var $this = this; var $validator = this.$validator; var data = {}; this.questions.forEach(function(question) { if (question.validate !== undefined) { $validator.attach(question.label, question.validate); data[question.label] = question.answer; } }); var $questions = this.questions; $validator.validateAll(data).then(function() { var form = []; $questions.forEach(function(question) { form.push({ id: question.id, label: question.label, answer: question.answer }); }); alert(\u0026#34;Valid form: \u0026#34;+JSON.stringify(form)); }).catch(function(error) { $this.$children.forEach(function(child) { child.$children.forEach(function(child) { child.$validator.validateAll().then(function() {}).catch(function() {}); }); }); alert(\u0026#34;Invalid form. Error count: \u0026#34; + $validator.getErrors().count()); }) } } Conclusion En une centaine de lignes de code JavaScript, nous disposons d’une application web capable d’afficher n’importe quel formulaire décrit en JSON. Pour l’instant limité, le nombre de champs de saisie ne demande qu’à être étendu : sélection multiple, date avec calendrier, upload de fichiers …\nPour des questions de sécurité et d’intégrité des données, la validation effectuée côté client devra être redondée côté serveur.\n","link":"https://javaetmoi.com/2017/05/formulaire-dynamique-en-vue-js/","section":"posts","tags":["javascript","vue.js"],"title":"Formulaire dynamique en Vue.Js"},{"body":"","link":"https://javaetmoi.com/tags/javascript/","section":"tags","tags":null,"title":"Javascript"},{"body":"","link":"https://javaetmoi.com/tags/vue.js/","section":"tags","tags":null,"title":"Vue.js"},{"body":"Les vidéos des présentations données lors de l’édition 2017 de la conférence Devoxx France sont d’ores et déjà disponibles sur la chaîne Devoxx FR de Youtube. Si vous n’avez pas le temps de toutes les visionner, si vous souhaitez vous faire un avis avant de les regarder ou si vous souhaitez garder une trace écrite de ce que vous y avez appris, je mets librement à disposition quelques-unes de mes notes. Il y’en a pour tous les goûts : du Java pur et dur, du framework avec Spring, du front avec Vue.js, des conteneurs avec Docker Swarm mode, des nouvelles approches de développement avec la programmation réactive, des patterns d’architecture avec les microservices, CQRS et l’Event-Sourcing, du legacy tendance avec les logs, et du Big Data avec Elasticsearch.\nPréparez-vous à la modularité selon Java 9 Java 9 modulo les modules Spring Framework 5.0 Spring Reactive Kit d\u0026rsquo;orchestration avec Docker Swarm mode Docker sous Windows Cloudifie ton monolithe Event-Sourcing et CQRS par la pratique Problèmes rencontrés avec les microservices 10 méthodes pour rendre heureux un développeur en entreprise Tech Lead dans pizza team XXL Log me tender Java 10 et le Pattern-Matching Ingest node Elasticsearch ","link":"https://javaetmoi.com/2017/04/14-prises-de-notes-a-devoxx-france-2017/","section":"posts","tags":["cloud","cqrs","devoxx","docker","elasticsearch","java","microservices","spring-framework"],"title":"14 prises de notes à Devoxx France 2017"},{"body":"","link":"https://javaetmoi.com/tags/cqrs/","section":"tags","tags":null,"title":"Cqrs"},{"body":"Voici la présentation qui m’a permis de partager avec mes collègues les différents sujets qui m’auront marqué lors de cette édition 2017 de Devoxx France. Au programme : Java 9 et 10 (les java modules, mais pas que), les Microservices, Docker et les orchestrateurs, Spring Framework 5, la programmation réactive, Vue.js et enfin ces bons vieux logs.\n[slideshare id=75354376\u0026amp;doc=13-14-devoxx-fr-home-170424152217]\n","link":"https://javaetmoi.com/2017/04/quoi-de-neuf-a-devoxx-france-2017/","section":"posts","tags":["devoxx","docker","java","microservices","spring-framework","vue.js"],"title":"Quoi de neuf à Devoxx France 2017 ?"},{"body":"Au cours des précédentes éditions de Devoxx France, je me suis familiarisé avec les frameworks JavaScript du moment : AngularJS en 2013 puis Angular 2 et ReactJS en 2016. Cette année, ce fut au tour d’un nouveau venu, à savoir Vue.js. Je l’ai testé au travers du Hands-on Lab animé par Emmanuel Demey et Aurélien Loyer. Si vous n’avez pas eu la chance d’y participer, cet article a pour humble objectif de vous aider à réaliser ce Lab par vous-même, tel un tutoriel. Il complète le code disponible sur le dépôt GitHub du Lab ainsi que les slides consultables en ligne. Vous pouvez également l’utiliser pour étudier à quoi ressemble une application Vue.js et découvrir ses principaux concepts.\nEmmanuel et Aurélien sont consultants web chez Zenika Lille. Familiarisés avec Angular, ils ont découvert VueJS au travers d’un projet personnel. VueJS nous est présenté comme une librairie (et non un framework) dédiée à la création d’interfaces web HTML. Il se veut simple et efficace, idéal pour créer rapidement une application web. Ses concepts principaux sont les Vues(il ne s’appelle pas Vue.js pour rien), les Directives, les Composants et le Binding. Tels les Web Components, Vue.js utilise le Shadow DOM pour scoper le style CSS des composants. Après cette courte introduction, place au CodeLab.\nL’application Zenika Ecommerce Au cours de ce CodelLab, vous allez développer une petite application de e-commerce dédiée à la vente de bières (nos speakers ne sont pas Lillois pour rien). La page est décomposée en 2 parties :\nun menu supérieur permettant d’accéder au panier et donnant quelques informations sur le contenu de ce dernier, une liste de bières en stock que vous pouvez acheter. Le template HTML et les ressources statiques de cette page nous sont fournies dans la branche step0. L’objectif du Lab sera de les dynamiser en les intégrant dans des composants Vue.js. Les données (à savoir les bières) seront tout d’abord hard-codées à la main dans le JS avant d’être récupérées d’un serveur Node.JS via une API REST.\nCe Lab est développé en ECMAScript 6 (alias JavaScript 2015). L’utilisation de la syntaxe raccourcie de déclaration de méthodes est encouragée.\nPré-requis Les instructions des différents exercices du Lab sont données dans le fichier index.md. Chaque exercice est reconnaissable au pattern PW \u0026lt;Numéro\u0026gt; (pour Project Work ?).\nAvant de commencer à implémenter un exercice, vous devrez tout d’abord vous référer à la partie théorique des slides. Avant d’aller plus loin, les pré-requis suivants sont nécessaires :\nUn client Git Un Node.JS 7 ou + Votre IDE favori L’ extension Chrome Vue-devtools (facultatif) L’installation du cli Vue et le boostrap de l’application feront partis de l’exercice PW1.\nPW0 – Ressources mises à dispositions Cette étape se résume à cloner le repo https://github.com/Gillespie59/devoxx-vuejs et à checkouter la branche step0.\nVoici les fichiers / répertoires qui vous intéresseront :\ndocs/index.md: instructions permettant de réaliser le Lab server/: serveur Express / Node.JS utilisé à partir de l’exercice 4 pour exposer la liste de bières sous forme d’API REST. A noter que le fichier server/beers.json sera utilisé dès l’étape 2 pour hard-codé sous forme d’objet JavaScript le tableau de bières à afficher. static/: ressources statiques (CSS, fonts et images) html: template statique HTML de la page d’accueil utilisé dès l’étape 2. html : non utilisé lors du lab par faute de temps, ce template permet d’initier la page affichant le panier utilisateur (PW7) PW1 – Application blanche Vue.JS Avant de créer votre première application, un peu de théorie est nécessaire pour vous familiariser avec les principaux concepts de Vue.js. Pour se faire, parcourez les 5 slides suivants :\nInterpolation 2-3 Bindings 2-4 et 2-5 Events Handlers 2-6 et 2-7 Lors de la conception du binding de Vue.js, son concepteur n’a pas souhaité utiliser le préfixe data- afin d’être conforme au W3C. Son point de vue est que le code généré par Vue.js est lui conforme W3C.\nPour créer une application Vue.JS, à l’instar de ember-cli et angular-cli, on peut utiliser l’interface en ligne de commande (command-line interface) vue-cli. L’utilitaire vue-cli propose différents types de squelette : du plus basique à celui basé sur webpack. Dans le Lab, nous utiliserons le squelette webpack.\nA la fin du PW1, la d’accueil du template Vue.js doit s’ouvrir dans votre navigateur : Le code source de l’application que vous allez compléter au cours du Lab se trouve dans le répertoire src/. Le fichier main.js est le point d’entrée de l’application. On y retrouve la déclaration de la vue racine de l’application :\nnew Vue({ el: \u0026#39;#app\u0026#39;, template: \u0026#39;\u0026lt;App/\u0026gt;\u0026#39;, components: { App } }) L’objet Vue est l’ objet principal de la librairie . Son constructeur prend en paramètre un objet JS dont les propriétés sont normalisées. Ici, notre vue racine en définie 3 :\nel: associe la vue avec un élément du DOM ayant l’identifiant app template: l’élément du DOM sera remplacé par le code HTML du template, ici une balise personnalisée components: composants Vue.js nécessaires au rendu de la vue. Ici, le composant App est référencé. C’est lui qui va être chargé d’interpréter la balise Dans le fichier index.html, nous retrouvons le portant l’identifiant \u0026ldquo;app\u0026rdquo; et qui sera donc associé à la vue racine :\n\u0026lt;body\u0026gt; \u0026lt;div id=\u0026#34;app\u0026#34;\u0026gt;\u0026lt;/div\u0026gt; \u0026lt;/body\u0026gt; Particulièrement simple, cette vue racine ne comporte ni données ni gestionnaire d’évènements.\nLe code source du composant App est localisé dans le fichier src/App.vue. L’approche composant de Vue.js s’inspire très fortement du standard Web Components dont Polymer est une implémentation. L’objectif d’un composant est d’ encapsuler du code(HTML + JS + CSS) afin de pouvoir le réutiliser. Un composant est associé à une balise HMTL. Ici, à la balise . Vous l’aurez remarqué, c’est le nom du fichier .vue qui détermine le nom de la balise HTML associée. Un composant peut être déclaré par programmation via la méthode Vue.component() ou bien décrit dans un fichier dédié portant l’ extension .vue. Le fichier App.vue est scindé en 3 parties :\n: code HTML templatisé à l’aide de la syntaxe Mustache. : code JavaScript du composant : nom, données, comportement, méthode callback appelée lors des différentes étapes du cycle de vie du composant … ","link":"https://javaetmoi.com/2017/04/codez-lab-vue-js-devoxx-france-2017/","section":"posts","tags":["devoxx","javascript","vue.js"],"title":"Codez le lab Vue.js de Devoxx France 2017"},{"body":"","link":"https://javaetmoi.com/tags/aop/","section":"tags","tags":null,"title":"Aop"},{"body":"","link":"https://javaetmoi.com/tags/bootstrap/","section":"tags","tags":null,"title":"Bootstrap"},{"body":"","link":"https://javaetmoi.com/tags/ehcache/","section":"tags","tags":null,"title":"Ehcache"},{"body":"","link":"https://javaetmoi.com/tags/hibernate/","section":"tags","tags":null,"title":"Hibernate"},{"body":"","link":"https://javaetmoi.com/tags/junit/","section":"tags","tags":null,"title":"Junit"},{"body":"","link":"https://javaetmoi.com/tags/less/","section":"tags","tags":null,"title":"Less"},{"body":"","link":"https://javaetmoi.com/tags/transaction/","section":"tags","tags":null,"title":"Transaction"},{"body":"Spring Petclinic is a sample application that has been designed to show how the Spring Framework can be used to build simple but powerful database-­oriented applications. The « canonical » version of Spring Petclinic is based on Spring Boot and Thymeleaf. But many forks exists: distributed version (microservices) built with Spring Cloud, React, AngularJS. The fork we are talking about is named Spring Framework Petclinic. It maintains a Petclinic version both with a plain old Spring Framework configuration and a 3-layer architecture (i.e. presentation \u0026ndash;\u0026gt; service \u0026ndash;\u0026gt; repository).\nThose 3 last years, the community has commited a lot of improvements: Bootstrap 3 migration, PostgreSQL support, alternative Java configuration, LESS, switch to the Spring IO Platform, stateless architecture, SQL optimizations, unit tests, templating with JSP tags.\nThe slides Michael Isvy shared 4 years ago have just been updated with all those new features. You may have a look. It covers a lot of interesting topics: software architecture, domain model presentation, data access, Spring profiles, caching, transaction management, exception handling, AOP, exception handler, validation, webjars, Java configuration, unit testing and so on.\n[slideshare id=71978076\u0026amp;doc=2017-01-springframeworkpetclinic-170209204315]\n","link":"https://javaetmoi.com/2017/02/spring-framework-petclinic-presentation/","section":"posts","tags":["aop","bootstrap","ehcache","hibernate","jpa","junit","less","spring-framework","spring-mvc","transaction","webjar"],"title":"Up-to-date Spring Framework Petclinic presentation"},{"body":"","link":"https://javaetmoi.com/tags/webjar/","section":"tags","tags":null,"title":"Webjar"},{"body":"","link":"https://javaetmoi.com/tags/angularjs/","section":"tags","tags":null,"title":"Angularjs"},{"body":"L’ application démo Spring Petclinic a été conçue pour montrer comment le framework Spring peut être utilisé pour développer une application web secondée par une base de données relationnelle. En somme, rien de révolutionnaire. Mais c’est ce qui fait tout son intérêt : présenter une architecture logicielle respectant l’état de l’art d’une application conçue avec Spring.\nAvec plus de 2000 forks sur GitHub, la communauté a créé de nombreux forks de Spring Petclinic : Angular, React, REST, Spring Cloud … Afin de fédérer cet engouement, l’ organisation GitHub Spring Petclinic a été créé sur GitHub en novembre 2016. La version de référence de Spring Petclinic reste sur https://github.com/spring-projects/spring-petclinic. Les branches et les forks ont basculé sur https://github.com/spring-petclinic.\nCe billet a pour objectif de vous présenter cette récente initiative puis de vous présenter les différents forks d’ores et déjà disponible dans l’organisation Spring Petclinic. Mais avant cela, remontons le temps.\nLes origines D’après une vieille documentation encore en ligne, Spring Petclinic a été initialement développé par Ken Krebs en 2003. A cette époque, la version 1.0 de Spring Framework n’était pas encore releasée (il a fallu attendre mars 2004). La Javadoc @author démontre que le co-fondateur du framework Spring, Juergen Hoeller en personne, a activement contribué à Petclinic. Les années passèrent. L’application bénéficia des montées de version du framework Spring. En 2007, Spring Petclinic était distribué avec Spring Framework 2.5 en tant qu’ application d’exemple. Ensuite, pendant 5 ans, l’application n’a plus évolué.\nEn 2013, Michael Isvy, ex-responsable formation Spring chez Pivotal, Keith Donald et Costin Leau ont fait revivre l’application en la déplaçant sur GitHub et en la migrant vers Spring 3.\nA partir de juin 2015, j’ai eu l’honneur de reprendre la coordination technique du projet. Mes contributions principales auront été de proposer une configuration full Java, une version Spring Boot et de migrer l’IHM vers le thème Bootstrap 3 de Pivotal.\nLe mois dernier, j’ai passé la main à Dave Syer, qui n’est autre que le papa de Spring Batch, Spring Cloud et de Spring Boot.\nL’application Petclinic de référence Reprenant les rennes, Dave Syer a tout de suite mis sa griffe sur le repo spring-projects/spring-petclinic :\nLa version Spring Boot est désormais celle de référence. La version Spring Framework est « archivée » dans un fork présenté plus loin. Java 8 minimum La couche présentation en JSP est réécrite en Thymeleaf. Le WAR auto-exécutable devient un JAR. L’ architecture applicative est modernisée. La Pull Request #200 « Modularize and migrate to aggregate-oriented domain » présente les changements. La couche service est retirée. Les Controllers dialoguent directement avec la couche Repository qui assure désormais la gestion des transactions. L’organisation des packages passe d’un découpage technique (model, repository, service et web) à un découpage métier (owner, vet, visit). Ce dernier changement d’architecture est le fait le plus marquant. Quelle rupture avec 15 ans de découpage Contrôleur -\u0026gt; Service -\u0026gt; DAO. Afin d’éviter des dépendances circulaires entre packages Java, la conception objet est en quelque sorte dénormalisée. La classe Visit ne référence plus la classe Pet, mais seulement son identifiant.\nMaintenue par l’équipe Pivotal, cette version « canonique » de Spring Petclinic est celle à partir desquels les forks pourront être créés. Notons enfin que c’est la version Spring Boot qui est mise en avant. Cela implique qu’une nouvelle application Spring doit donc partir dans la majorité des cas sur du Spring Boot.\nSpring Framework Petclinic L’application spring-petclinic/spring-framework-petclinic a pour objectif de maintenir une version de Spring Petclinic sans Spring Boot, à l’ancienne, avec de la configuration Spring, de bonnes vielles pages JSP et une architecture 3-tiers.\nComparée à son aînée, cette version présente de nombreux points d’intérêts :\nLa configuration Spring en XML (branche master) ou en full Java (branch javaconfig) de l’ensemble des couches d’une application web : présentation (Spring MVC, ressources statiques, dépendances JavaScript récupérées avec webjar), service (cache et transaction) et persistance. 3 implémentations de la couche de persistance : Spring JDBC, Hibernate et Spring Data JPA. Le choix se fait au démarrage de l’application web par l’usage d’un profile Spring. Des templates de pages et des composants graphiques avec JSP. L’usage de l’ AOP avec l’aspect CallMonitoringAspect Un support de PostreSQL en plus de MySQL et HSQLDB. Le fichier README.MD donne les points d’entrée vers les fichiers de configurer et les classes Java les plus intéressantes.\nSpring Petclinic AngularJS Le fork spring-petclinic/spring-petclinic-angular1 a été créé à partir de la branche angular de l’application de référence, juste avant que celle-ci ne soit supprimée. Liu Dapeng, Michael Isvy et moi-même en sont les principaux contributeurs.\nL’intérêt principal de ce fork est de disposer d’un front-end full JavaScript. Le code Angular JS 1.5 dialogue avec le backend à l’aide d’une API REST propulsée par Spring MVC. L’intérêt secondaire est de prouver que les mondes JavaScript et Java peuvent parfaitement cohabiter. Le téléchargement et l’exécution des outils front-end gulp, bower, npm et node sont pilotés par Maven à l’aide du frontend-maven-plugin.\nL’application est décomposée en 2 modules Maven, un client front-end et une partie serveur Spring Boot :\nspring-petclinic-client: ressources statiques (fichiers JavaScript Angular, images, fonts, css) packagées sous forme d’un webjar spring-petclinic-server: API REST de Spring Petclinic et la page index.html (template Thymeleaf) permettant de référencer les ressources statiques du webjar Côté serveur, afin d’exposer une API REST au front-end Angular, les contrôleurs Spring MVC ont été convertis en @RestController, renommés pour les besoins de l\u0026rsquo;API REST et simplifiés car une partie de la logique est désormais traitée dans le navigateur (ex : OwnerResource.java).\nLa partie front n’a rien à voir avec l’originale en JSP / Java. Elle bascule complètement dans le monde JavaScript (à l’exception d’un peu de Maven). Le spring-petclinic-client/pom.xml est configuré pour installer Node JS et NPM, récupérer les librairies tierces JS avec bower (cf. bower.json) puis lancer la phase de build avec Gulp (cf. gulpfile.js). Le répertoire target/dist construit par Gulp et les librairies JS sont enfin packagés par Maven sous forme de webjar. A noter que Gulp est configuré pour générer des CSS à partir des fichiers LESS de Petclinic et à minifier JS et CSS. Le code AngularJS est centralisé dans le répertoire spring-petclinic-client/src/scripts/. L’application Angular est bootstrapée dans le fichier app.js. Le module externe ui-router est chargé de la navigation entre vues. L’organisation de chaque vue se fait sur le même modèle. Voici en exemple celle listant les vétérinaires :\nvet-list.component.js: déclaration du composant vetList, du template vet-list.template.html et du contrôleur VetListController vet-list.controller.js: définition du contrôleur VetListController chargé de faire un appel REST pour récupérer l’objet JSON représentant la liste des vétérinaires. vet-list.js: configuration ui-router faisant le lien entre l’URL /vets et le template vet-list vet-list.template.js: template HTML comportant des directives Angular Spring Petclinic AngularJS montre également l’usage des DevTools. Introduits dans Spring Boot 1.3, ils peuvent remplacer l’usage d’outils comme JRebel ou Spring Loaded. Le module spring-boot-devtools a été configuré de manière à ce que :\nla recompilation d’une classe Java déclenche le rechargement du contexte applicatif Spring (qui dure à peine 2 secondes sur mon macbook) une modification de ressources statiques déclenche un rafraichissement de la page dans le navigateur (le plugin LiveReload doit préalablement être installé) Cette configuration n’est active que pendant la phase de développement. Elle est localisée dans le fichier application-dev.properties qui n’est chargé par Spring Boot que lorsque le profile Spring dev est actif. Dans votre IDE, ajouter l’option ci-dessous au démarrage de la JVM : -Dspring.profiles.active=dev\nSpring Petclinic ReactJS Le projet spring-petclinic/spring-petclinic-reactjs est le 2nd portage de l’application Spring Petclinic vers un front-end full JavaScript de type SPA (Single Page Application), en l’occurrence basé ici sur ReactJS (un framework MVC JavaScript développé par Facebook) et TypeScript (un sur-ensemble de ES6 développé par Microsoft). Ce fork a été développé par Nils Hartmann, co-auteur d’un livre en allemand sur React et pro Spring Boot. Nils est parti de la version Spring Boot de Spring Petclinic. Pour designer l’API REST, il a récupéré certaines classes de la version AngularJS.\nComparé au fork AngularJS, front-end et backend disposent de leur propre serveur : l’un tournant sur Node.JS et l’autre sous Spring Boot.\nLa partie front-end est localisée dans le sous-répertoire client. Outre le code TypeScript (TS) et les ressources statiques, on retrouve la configuration d’un certain nombres d’outils JavaScript :\nNPM tire les dépendances Webpack permet de modulariser le code JavaScript Pendant le développement, Babel transpile à chaud le code TS en JS TSlint est utilisé pour vérifier la qualité du code TS Téléchargement de définitions TS avec Typings Chaque page de l’application web a été décomposée en composants et sous-composants React. A titre d’exemple, la page OwnersPage.tsx affichant le détail d’un propriétaire est découpée en 2 composants : OwnerInformation.tsx et PetsTable.tsx. La majeure partie du code applicatif se retrouve ainsi dans le répertoire client/src/components dédié aux composants.\nLa partie serveur se rapproche de celle d’AngularJS. Seule différence majeure : les données envoyées par le client sont validées. Se référer aux classes InvalidRequestException, ApiExceptionHandler, ErrorResource et FieldErrorResource. Bien conçue, cette couche de validation pourra être reportée sur la version AngularJS (cf. issue 7).\nSpring Petclinic Microservices Fondée par Maciej Szarliński, la version microservices de Spring Petclinic est mon coup de cœur du moment : spring-petclinic/spring-petclinic-microservices. Un grand nombre de modules de la stack Spring Cloud y sont mis en œuvre.\nCe fork de la version AngularJS de Spring Petclinic a été décomposée en 3 micro-services fonctionnels: customers, vets et visits. Autonomes, ces micro-services ne communiquent pas ensemble. Au démarrage, ils vont chercher leur configuration auprès du serveur de config (module spring-petclinic-config-server). Par défaut, le serveur de config récupère la configuration depuis le repo GitHub spring-petclinic-microservices-config. Il est possible d’utiliser un repo Git local : -Dspring.profiles.active=local -DGIT_REPO=/projects/spring-petclinic-microservices-config\nLes 3 micro-services vont s’enregistrer auprès de l’ annuaire de Service(module spring-petclinic-discovery-server) basé sur Eureka. Ils peuvent ainsi être accédés à partir de leur nom de service (ex : http://customers-service/owners/{ownerId})). Leur nom est paramétré dans le fichier bootstrap.yml, au côté de l’URL du serveur de config.\nLe front-end Angular n’attaque pas directement les 3 micro-services. Il passe par une API Gateway dont le mécanisme de routage est assuré par Zuul (module spring-petclinic-api-gateway). Cette gateway n’est pas réduite à un simple passe-plat. Elle s’occupe de :\nAgréger les réponses renvoyées par plusieurs micro-services avant de les retourner au client (se référer à la classe ApiGatewayController) Load-balancer les requêtes entre plusieurs instances du même micro-services (annotation @LoadBalanced dans la classe ApiGatewayApplication). Afin de pouvoir suivre les requêtes HTTP entre plusieurs microservices, un mécanisme de traces distribuées a été mis en œuvre avec Spring Cloud Sleuth. L’interface graphique du serveur Zipkin permet de les consulter.\nEnfin, avec pour objectif de simplifier le démarrage de l’ensemble de ces applications (3 microservices + 4 composants d’infra), un fichier docker-compose.yml sera bientôt mis à disposition.\nTableau de synthèse Le tableau ci-dessous dresse une liste des différentes versions de Spring Petclinic présentant à mes yeux un intérêt majeur : **AppellationDescriptionSpring PetclinicVersion de référence de Spring Petclinic. Implémentée avec Spring Boot et Thymeleaf.Spring Framework PetclinicConfiguration XML et Java de Spring Framework. Front-end implémenté en JSP. 3 technologies de persistance : JDBC, JPA et Spring Data JPA.Spring Petclinic AngularJSFront-end Angular 1 embarqué dans un webjar. Usage de DevTools.Spring Petclinic ReactJS**Front-end ReactJS délivré par un serveur NodeJS et attaquant l’API REST du back-end implémenté en Spring Boot.Spring Petclinic MicroservicesVersion distribuée de Spring Petclinic implémentée à l’aide de Spring Cloud : serveur Spring Config, annuaire de services avec Eureka, gestion des logs avec Zipkin et Sleuth, API Gateway avec Zuul, Docker compose …\nConclusion Dans cet article, j’ai commencé par retracer l’historique de l’application de référence Spring Petclinic qui a fêté son 13ième anniversaire et qui comptabilise plus de 2000 forks. Parmi ces forks, une poignée a intégré la nouvelle organisation Spring Petclinic. On y retrouve des versions front-end basées sur AngularJS et ReactJS, une version distribuée avec des micro-services et du Spring Cloud, une version plus legacy n’utilisant pas Spring Boot mais de la configuration XML ou Java (au choix).\nL’organisation Spring Petclinic demande à s’élargir, soit en proposant un nouveau fork (ex : sur Angular 2) soit en contribuant à ceux existants. Toute personne intéressée peut en faire la demande via l’issue Spring Petclinic Organization. Alors : à vos claviers !!\nResources :\nSpring Petclinic community (organisation GitHub) DevTools in Spring Boot 1.3 par Phill Webb (Pivotal) Distributed Tracing with Spring Cloud Sleuth and Spring Cloud Zipkin par Josh Long (Pivotal) Spring Boot 1.3 pour le web par Biran Clozel et Stéphane Nicoll (Pivotal) ","link":"https://javaetmoi.com/2016/12/les-forks-de-spring-petclinic/","section":"posts","tags":["angularjs","devtools","microservices","react","rest","spring-boot","spring-cloud","spring-framework"],"title":"Découvrez les forks de Spring Petclinic"},{"body":"","link":"https://javaetmoi.com/tags/devtools/","section":"tags","tags":null,"title":"Devtools"},{"body":"","link":"https://javaetmoi.com/tags/react/","section":"tags","tags":null,"title":"React"},{"body":"","link":"https://javaetmoi.com/tags/rest/","section":"tags","tags":null,"title":"Rest"},{"body":" Par le passé, j’ai publié 2 images Docker sur le registre Docker Hub, l’équivalent du Maven Central Repository pour Docker : un client MySQL et une base PostgreSQL MusicBrainz. Ces images étaient construites puis publiées automatiquement à partir d’un dépôt GitHub contenant un Dockerfile et, éventuellement, un script Shell.\nPlus récemment, j’ai souhaité mettre à disposition une image Docker de l’ application Spring Petclinic basée sur Angular 1 et Spring Boot. Ce billet explique :\nComment l’image Docker a été construire Et comment l’utiliser pour tester Petclinic Automatisation de la construction Pour utiliser le mécanisme de construction automatique proposé par Docker Hub, une 1ière solution aurait consisté à télécharger le JAR de Petclinic depuis un repo Maven public. Or, ce n’est pas (encore ?) le cas. Une 2nde solution aurait été de faire construire le JAR par Docker. L’image aurait nécessité Git et Maven. Afin de garder une taille d’image raisonnable, le repo Maven local et le repo Git auraient dû être effacé après construction du JAR. Ce n’était pas la solution la plus optimale.\nLa solution que j’ai finalement retenue n’est pas basée sur le mécanisme proposé par Docker Hub mais sur l’utilisation du plugin pour Maven docker-maven-plugin développé par l’équipe de Spotify.\nConfiguration du docker-maven-plugin Disponible sur la plateforme spring.io, le guide Starting Guide Spring Boot With Docker explique pas à pas comment créer une image Docker d’une application Spring Boot.\nSur Petclinic, la configuration du docker-maven-plugin a été adaptée afin de faciliter la publication de l’image sur Docker Hub. Voici un extrait du pom.xml du module springboot-petclinic-server :\n\u0026lt;plugin\u0026gt; \u0026lt;groupId\u0026gt;com.spotify\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;docker-maven-plugin\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;0.4.13\u0026lt;/version\u0026gt; \u0026lt;configuration\u0026gt; \u0026lt;imageName\u0026gt;${docker.image.prefix}/springboot-petclinic\u0026lt;/imageName\u0026gt; \u0026lt;dockerDirectory\u0026gt;src/main/docker\u0026lt;/dockerDirectory\u0026gt; \u0026lt;resources\u0026gt; \u0026lt;resource\u0026gt; \u0026lt;targetPath\u0026gt;/\u0026lt;/targetPath\u0026gt; \u0026lt;directory\u0026gt;${project.build.directory}\u0026lt;/directory\u0026gt; \u0026lt;include\u0026gt;${project.build.finalName}.jar\u0026lt;/include\u0026gt; \u0026lt;/resource\u0026gt; \u0026lt;/resources\u0026gt; \u0026lt;forceTags\u0026gt;true\u0026lt;/forceTags\u0026gt; \u0026lt;imageTags\u0026gt; \u0026lt;imageTag\u0026gt;${project.version}\u0026lt;/imageTag\u0026gt; \u0026lt;imageTag\u0026gt;latest\u0026lt;/imageTag\u0026gt; \u0026lt;/imageTags\u0026gt; \u0026lt;useConfigFile\u0026gt;true\u0026lt;/useConfigFile\u0026gt; \u0026lt;/configuration\u0026gt; \u0026lt;/plugin\u0026gt; Le plugin est configuré pour utiliser la version Maven pour tagger l’image Docker.\nLa balise useConfigFile précise au plugin d’aller rechercher les paramètres d’authentification au registre Docker dans le fichier de configuration Docker. Sur mon mac, ce fichier de configuration config.json se situe dans le répertoire ~/.docker :\n{ \u0026#34;auths\u0026#34;: { \u0026#34;https://index.docker.io/v1/\u0026#34;: { \u0026#34;auth\u0026#34;: \u0026#34;xxxxxxxxxx==\u0026#34; } } } Enfin, La propriété docker.image. prefix pointe sur mon compte personnel Docker Hub :\n\u0026lt;properties\u0026gt; \u0026lt;docker.image.prefix\u0026gt;arey\u0026lt;/docker.image.prefix\u0026gt; \u0026lt;/properties\u0026gt; Dockerfile Pour des images simples, le plugin docker-maven-plugin permet de se passer complètement de Dockerfile : image de base, nom de l’image et point d’entrée sont directement configurés dans le pom.xml. Charge au plugin de générer le Dockerfile.\nPour Petclinic, l’usage d’un Dockerfile été préféré. Facultatives, quelques directives spécifiques ont été ajoutées. Par ailleurs, l’utilisation d’un Dockerfile présente l’avantage de pouvoir être utilisé en dehors de Maven. La propriété dockerDirectory référence le répertoire contenant le Dockerfile.\nPour être opérationnelle, l’image Docker de SpringBoot Petclinic nécessite :\nune image Linux, une JVM Java 7 ou 8 et le JAR de Spring Petclinic. L’ image Docker basée sur OpenJDK couvre les 2 premiers besoins. La version basée sur le projet Alpine Linux permet d’utiliser une image de base très réduite (environ 5 Mo).\nAu final, voici le Dockerfile de SpringBoot Petclinic :\nFROM openjdk:alpine MAINTAINER Antoine Rey \u0026lt;antoine.rey@free.fr\u0026gt; # Spring Boot application creates working directories for Tomcat by default VOLUME /tmp ADD petclinic.jar petclinic.jar RUN sh -c \u0026#39;touch /petclinic.jar\u0026#39; # To reduce Tomcat startup time we added a system property pointing to \u0026#34;/dev/urandom\u0026#34; as a source of entropy. ENTRYPOINT [\u0026#34;java\u0026#34;,\u0026#34;-Djava.security.egd=file:/dev/./urandom\u0026#34;,\u0026#34;-jar\u0026#34;,\u0026#34;/petclinic.jar\u0026#34;] Pointue, l’utilisation du répertoire /tmp, du touch et du urandom sont détaillées dans le Starting Guide Spring Boot With Docker. Je vous laisse vous y référer.\nConstruire l’image Docker La construction de l’image Docker passer par une unique commande Maven :\nmvn docker:build Au préalable, il est nécessaire de démarrer Docker. Dans le cas contraire, vous obtiendrez un message d’erreur similaire :\n[INFO] Building image arey/springboot-petclinic nov. 04, 2016 8:47:21 AM org.apache.http.impl.execchain.RetryExec execute INFOS: I/O exception (java.io.IOException) caught when processing request to {}-\u0026gt;unix://localhost:80: No such file or directory … ERROR] Failed to execute goal com.spotify:docker-maven-plugin:0.4.13:build (default-cli) on project springboot-petclinic-server: Exception caught: java.util.concurrent.ExecutionException: com.spotify.docker.client.shaded.javax.ws.rs.ProcessingException: java.io.IOException: No such file or directory Techniquement, le plugin maven dialogue avec Docker par l’intermédiaire d’un client Java Docker également développé par Spotify. Les échanges se font en REST / JSON.\nUne fois la commande mvn docker:build exécutée, les étapes de construction de l’image apparaissent dans les logs Maven :\n[INFO] --- docker-maven-plugin:0.4.13:build (default-cli) @ springboot-petclinic-server --- [INFO] Copying /Users/arey/dev/github/spring-petclinic /springboot-petclinic-server/target/petclinic.jar -\u0026gt; /Users/arey/dev/github/spring-petclinic/springboot-petclinic-server/target/docker/petclinic.jar [INFO] Copying src/main/docker/Dockerfile -\u0026gt; /Users/arey/dev/ github/spring-petclinic /springboot-petclinic-server/target/docker/Dockerfile [INFO] Building image arey/springboot-petclinic Step 1 : FROM openjdk:alpine ---\u0026gt; f1da3c7976d0 Step 2 : MAINTAINER Antoine Rey \u0026lt;antoine.rey@free.fr\u0026gt; ---\u0026gt; Using cache ---\u0026gt; 321262ba62a5 Step 3 : VOLUME /tmp ---\u0026gt; Using cache ---\u0026gt; 7de28ccfef3f Step 4 : ADD petclinic.jar petclinic.jar ---\u0026gt; 2af817fea936 Removing intermediate container 939a70038030 Step 5 : RUN sh -c \u0026#39;touch /petclinic.jar\u0026#39; ---\u0026gt; Running in e90cff88f24c ---\u0026gt; e95aaed6515b Removing intermediate container e90cff88f24c Step 6 : ENTRYPOINT java -Djava.security.egd=file:/dev/./urandom -jar /petclinic.jar ---\u0026gt; Running in 7d03e1941841 ---\u0026gt; 3b9b33ffb62b Removing intermediate container 7d03e1941841 Successfully built 3b9b33ffb62b [INFO] Built arey/springboot-petclinic [INFO] Tagging arey/springboot-petclinic with 1.4.1 [INFO] Tagging arey/springboot-petclinic with latest L’ image Docker arey/springboot-petclinic est construite et est disponible localement.\nPour tester par vous-même la création de l’image Docker de Spring Boot Petclinic, exécuter les commandes suivantes :\ngit clone https://github.com/spring-projects/spring-petclinic cd spring-petclinic git checkout angularjs mvn clean install cd spring-petclinic-server mvn docker:build Publier l’image Docker Publier l’image Docker construite avec Maven dans le registre public Docker Hub est enfantin. Après avoir paramétré la propriété docker.image.prefix du pom.xml et le fichier de configuration ~/.docker/config.json , exécuter la ligne de commande Maven suivante :\nmvn docker:build -DpushImageTag Démarrer Spring Boot Petclinic Démarrer l’image Docker arey/springboot-petclinic disponible sur Docker Hub ou construite localement se fait en ligne de commande :\ndocker run -e \u0026#34;SPRING_PROFILES_ACTIVE=prod\u0026#34; -p 8080:8080 -t --name springboot-petclinic arey/springboot-petclinic L’application web est alors disponible sur l’URL http://DOCKER_HOST:8080/\nLe profile Spring de prod permet d’activer la mise en cache et le versionning des ressources statiques (cf. application-prod.properties).\nPour arrêter le conteneur, utiliser la commande :\ndocker stop springboot-petclinic Conclusion Le packaging d’une application Spring Boot sous forme d’image Docker peut entièrement être automatisé avec Maven (mais également avec Gradle). Publier ensuite cette image sur Docker Hub et tout Internaute pourra tester votre application sans avoir à installer le moindre outil (mis à part Docker).\nConcernant Petclinic, la prochaine étape pourrait consister à déployer cette image sur un Cloud public. A suivre \u0026hellip;\nRessources :\nDocker container for the Spring Boot Petclinic application Starting Guide Spring Boot With Docker Image Docker officielle pour Java basée sur OpenJDK Documentation du plugin docker-maven-plugin Version Angular JS de l’application SpringBoot Petclinic Client Java pour Docker ","link":"https://javaetmoi.com/2016/11/image-docker-pour-spring-boot-petclinic/","section":"posts","tags":["docker","maven","spring-boot"],"title":"Image Docker pour Spring Boot Petclinic"},{"body":"Dans mon précédent article sur Spring Boot, je vous détaillais le chemin de migration de l’application démo Spring Petclinic vers Spring Boot. Intéressons-nous aujourd’hui aux fondamentaux : qu’est-ce qu’est Spring Boot ? Et comment fonctionne-t-il ? Vous trouverez des éléments de réponse dans la présentation suivante. J’y décris les grands principes de Spring Boot. Puis j’essaie de démystifier le fonctionnement de l’auto-configuration. Enfin, je montre comment Spring Boot permet de simplifier encore davantage vos tests.\n[slideshare id=66730766\u0026amp;doc=2016-10-04-introductionaspringboot-161004173204]\nSommaire :\nLes principes de Spring Boot Le traditionnel Hello World Configuration applicative Des tests simplifiés Démo : développement d’une application Hello World Etudes de cas : migration de l’application Spring Petclinic Aperçu des autres fonctionnalités ","link":"https://javaetmoi.com/2016/10/introduction-a-spring-boot/","section":"posts","tags":["spring-boot"],"title":"Introduction à Spring Boot"},{"body":"L’utilisation conjointe de Maven pour réaliser des release et de git-flow peut s’avérer laborieuse. En effet, lorsque vous travaillez avec des branches (quel que soit le SCM), une bonne pratique veut que chaque branche possède son propre numéro de version. Afin d’éviter des collisions de nommage, cette pratique devient indispensable lorsque vous utilisez un serveur d’intégration continue pour publier les artefacts construits dans un repo Maven. Une fois une branche crée à partir d’une autre, chaque branche vit sa vie. Des releases Maven peuvent être réalisées de part et d’autre. Là où cela devient tendu, c’est lorsque vous devez reporter les commits d’une branche vers une autre. Des conflits de merge sur le numéro de version Maven apparaissent alors inévitablement. Lorsque votre application multi-modules comporte 15 pom.xml, c’est 15 conflits qu’il va falloir gérer manuellement. Il est effectivement risqué de conserver aveuglément la version du pom.xml local ou distant, car d’autres changements (et vrais conflits) peuvent se produire dans d’autres sections du pom.xml.\nComme cas d’études, prenons l’exemple du repo Git helloworld : Une application HelloWorld construite avec Maven a été releasée avec Maven en version 1.0.0 sur la branche develop. La prochaine version renseignée sur develop est 1.1.0-SNAPSHOT. Une branche de maintenance release/1.0.x a ensuite été créée à partir du tag pointant sur le commit « [maven-release-plugin] prepare release helloworld-1.0.0 ». Une faute d’orthographe a été corrigée. Afin de la livrer en production rapidement, une version 1.0.1 a été réalisée avec Maven. Cette correction doit désormais être reportée sur la branche develop. Entre temps, 2 commits ont été réalisés sur celle-ci, dont l’un touchant au pom.xml. Quelle solution adopter pour effectuer ce report ?\nUne première solution consiste à utiliser un cherry-pick du commit « Fix spelling ». Pour rappel, cherry-pick permet de reporter commit par commit les différences entre branche. Le risque est d’oublier un commit. Et cette solution peut devenir laborieuse si beaucoup de commits séparent les 2 branches.\nUne seconde solution consiste à merger la branche release/1.0.x dans la branche develop. Apparaît alors le conflit sur le numéro de version du pom.xml évoqué en introduction :\ngit merge release/1.0.x Auto-merging src/main/java/com/compagny/HelloWorld.java Auto-merging pom.xml CONFLICT (content): Merge conflict in pom.xml Automatic merge failed; fix conflicts and then commit the result. git diff diff --cc pom.xml index b43c671,01257f6..0000000 --- a/pom.xml +++ b/pom.xml @@@ -6,9 -6,9 +6,13 @@@ \u0026lt;groupId\u0026gt;com.mycompagny\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;helloworld\u0026lt;/artifactId\u0026gt; ++\u0026lt;\u0026lt;\u0026lt;\u0026lt;\u0026lt;\u0026lt;\u0026lt; HEAD + \u0026lt;version\u0026gt;1.1.0-SNAPSHOT\u0026lt;/version\u0026gt; ++======= + \u0026lt;version\u0026gt;1.0.2-SNAPSHOT\u0026lt;/version\u0026gt; ++\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt; release/1.0.x - \u0026lt;description\u0026gt;Hello World application\u0026lt;/description\u0026gt; + \u0026lt;description\u0026gt;Git merge demonstration\u0026lt;/description\u0026gt; Le conflit doit être résolu manuellement : la version 1.1.0-SNAPSHOT de la branche develop est à conserver.\nUne solution permettant d’éviter de résoudre ce genre de conflits consiste à utiliser un script que va utiliser Git pour merger 2 fichiers pom.xml. C’est précisément l’objectif du driver de merge mergepom.py écrit en Python et que vous pouvez récupérer sur le repo GitHub pom-merge-driver. L’utilisation de ce script sous Linux et Mac est décrite dans le README.md. Sur Windows, il est nécessaire de faire quelques adaptations :\nTélécharger et installer Python (la version portable est suffisante) Dans le script mergepom.py, supprimer la ligne d’en-tête #! /usr/bin/env python Dans le fichier .gitconfig, ajouter le chemin vers python.exe : [merge \u0026#34;pommerge\u0026#34;] name = A custom merge driver for Maven\u0026#39;s pom.xml driver = \u0026#39;/C/dev/python/python.exe\u0026#39; C:/dev/git/mergepom.py %O %A %B Afin de laisser la liberté aux autres développeurs d’utiliser ce script ou non, j’ai ajouté le fichier attributes dans le répertoire .git/info de mon repo local :\npom.xml merge=pommerge Remarque : alternativement, on peut également ajouter cette ligne dans le fichier .gitattributes situé dans le répertoire racine du repo Git. Tous les développeurs de l\u0026rsquo;application en profitent alors.\nOn relance la commande de merge. Cette fois-ci, le script détecte un conflit sur le numéro de version du pom.xml et décide de garder celui de la branche courante, à savoir 1.1.0-SNAPHSOT :\ngit merge release/1.0.x Merging pom version 1.0.2-SNAPSHOT into develop. Keeping version 1.1.0-SNAPSHOT Auto-merging src/main/java/com/compagny/HelloWorld.java Auto-merging pom.xml Merge made by the \u0026#39;recursive\u0026#39; strategy. pom.xml | 2 +- src/main/java/com/compagny/HelloWorld.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) Le script Python respecte le workflow git-flow. De ce fait, les merge dans le master des branches de release et de hotfix conservent le numéro de version de ces dernières.\nComme moi, j’espère que l’existence de ce driver de merge Git vous simplifiera vos merges. Ne prenant en compte que les fichiers pom.xml encodé en UTF-8, j’ai fait une demande d’évolution. En attendant, vous pouvez changer l’encodage en dur dans le script ou bien, encore mieux, soumettre une pull request.\nEnfin, sachez que d\u0026rsquo;autres drivers de merge Git existent. pomutils est écrit en Java. Si vous en avez testé, n\u0026rsquo;hésitez pas à laisser un feedback.\n","link":"https://javaetmoi.com/2016/09/merge-git-et-release-maven/","section":"posts","tags":["git","maven"],"title":"Faire cohabiter merge Git et release Maven"},{"body":"","link":"https://javaetmoi.com/tags/git/","section":"tags","tags":null,"title":"Git"},{"body":"Cela fait un an que je contribue activement à la maintenance de l’application Spring Petclinic. Développée initialement par les créateurs du framework Spring, Juergen Hoeller et Rob Harrop, cette application démo a évolué au fur et à mesure des montées de version du framework. Elle est passée d’une approche full XML, à une approche mixte annotations + XML. Une branche est également disponible pour la configuration Java. Récemment, nous avons mis à disposition une branche basée sur Spring Boot 1.4.0. L’objectif de ce billet est de vous expliquer quels ont été les impacts d’une telle migration.\nStack technique existante Techniquement, la branche master à partir de laquelle a été créée la branche springboot est relativement à jour. Elle s’appuie sur le BOM 2.0.7 de la plateforme Spring.IO. Spring MVC 4.2, JSP, Dandelion, jQuery 2 et Bootstrap 3.3 sont utilisés pour la couche de présentation. La couche de persistance propose 3 implémentations différentes : Spring Data JPA 1.9, JPA/Hibernate 4.3 et JDBC. Un profile Spring permet de choisir quelle implémentation utiliser au démarrage de l’application.\nLe build est principalement construit autour de Maven. Bower est utilisé pour télécharger les frameworks JavaScript / CSS. Les feuilles de styles sont écrites en LESS. Un plugin maven les convertit en CSS.\nL’application est compatible Java 7 et 8. Sur mon poste de dév, je la déploie sur Tomcat 7, et Tomcat 8.. A noter que sous Jetty, les pages JSP ne s’affichent pas suite à un bug de Dandelion.\nCible Le but de la migration est de conserver l’application iso-fonctionnelle et de garder dans la mesure du possible les mêmes frameworks. Par contre, nous avons fait le choix de ne retenir qu’une seule des 3 implémentations de la couche de persistance, à savoir Spring Data JPA. Garder les 3 technologies aurait complexifié inutilement la configuration Spring.\nAfin de coller davantage à l’esprit Spring Boot, nous sommes revenus aux webjars et avons délaissé l’usage de Bower. Ce billet n’abordera pas ce changement.\nNon pris en compte par Spring Data JPA, l’aspect CallMonitoringAspect n’a pas été conservé.\nConfiguration Maven La migration vers Spring Boot a simplifié le pom.xml qui est passé de 461 à 357 lignes XML. Voici les changements apportés :\nAjout d’un POM Parent : org.springframework.boot:spring-boot-starter-parent Suppression du Bill Of Materials io.spring.platform:platform-bom Remplacement des dépendances de frameworks par les « Spring Boot Starter » équivalents : spring-boot-starter-actuator, spring-boot-starter-cache, spring-boot-starter-data-jpa, spring-boot-starter-test et spring-boot-starter-web Déclaration du plugin Maven spring-boot-maven-plugin Suppression de la déclaration des plugins maven-war-plugin, maven-assembly-plugin et tomcat7-maven-plugin Configuration Spring L’automatisation de la configuration Spring est mise en avant par les concepteurs de Spring Boot. Nous nous attendions donc à obtenir une configuration la plus minimaliste possible. Le passage à Spring Boot nous a contraint à délaisser le web.xml au profit de l’interface ServletContainerInitializer introduite par l’API Servlet 3.0.\nLa configuration Spring Boot est écrite en Java : 76 lignes de code Java sont venues remplacer 248 lignes de configuration Spring XML et 99 lignes du web.xml.\nVoici le détail des changements :\nCréation d’une classe principale PetClinicApplication implémentant la classe abstraite SpringBootServletInitializer et annotée avec l’annotation @SpringBootApplication Centralisation de la configuration de la servlet et des filtres Dandelion dans la classe de configuration DandelionConfig Introduction de la classe de configuration CacheConfig permettant de n’activer ehcache qu’en production Suppression du web.xml Suppression de 5 fichiers de configuration Spring Paramétrage Initialement répartie dans différents fichiers, le paramétrage applicatif a été centralisé dans le fichier application.properties. Les fichiers data-access.properties et logback.xml ont été supprimés. Par convention, le fichier de configuration ehcache.xml a été déplacé à la racine du classpath.\nSimplification des tests Au niveau des dépendances, le starter spring-boot-starter-test tire tous les frameworks de tests utilisés par Petclinic : JUnit, Spring Test, AssertJ, Mockito, Json Path et Hamcrest. Un peu comme Unitils en son temps, Spring Boot facilite l’utilisation conjointe de ces différents frameworks. Et la version 1.4.0 de Spring Boot améliore encore leur intégration.\nAinsi, l’annotation @MockBean permet de créer un mock avec Mockito, de l’enregistrer au sein du contexte applicatif Spring et de l’injecter dans votre classe de tests unitaires. Nul besoin désormais de faire appel explicitement à la méthode Mockito::mock().\nEn fonction de la couche applicative à laquelle la classe testée appartient, la configuration Spring du test associé peut être auto-détectée. Ainsi, pour la couche web Spring MVC, l’annotation @WebMvcTest détecte tous les beans annotés avec @Controller, @ControllerAdvice et @JsonComponent puis configure l’instance de MockMvc. La classe de test OwnerControllerTests la montre en action.\nAutres changements D’autres changements mineurs ont été nécessaires :\nLe contrôleur Spring MVC WelcomeController a été ajouté. Il a pour rôle d’afficher la page d’accueil de l’application. Le PetTypeFormatter s’est vu transformé en bean Spring (ajout de l’annotation @Component). Le fichier txt a été ajouté afin de personnaliser le logo Ascii-art affiché lors du démarrage de l’application Suite à un problème avec Dandelion, les inclusions de JSP ont été remplacées par des tags JSP. Conclusion Le portage vers Spring Boot n’aura demandé que quelques heures de développement. Au final, la configuration Spring est simplifiée à l’extrême. Et mis à part la configuration Maven, Petclinic ne contient a plus une seule ligne de XML. En bonus, l’application s’est automatiquement vu enrichie d’une API de management accessible à la fois en REST et en JMX. Petclinic est packagé sous forme de war auto-exécutable. Une limitation du support des pages JSP par Spring Boot fait qu’il n’est pas (encore ?) possible de le jarjariser.\n","link":"https://javaetmoi.com/2016/08/migrer-vers-spring-boot/","section":"posts","tags":["spring-boot"],"title":"Migrer vers Spring Boot"},{"body":"","link":"https://javaetmoi.com/tags/google/","section":"tags","tags":null,"title":"Google"},{"body":"J’ai eu l’opportunité d’assister à une journée de découverte de la plateforme Cloud de Google. Dispensée dans les locaux parisiens de Google, cette formation d’une journée était animée par Didier Girard, Google Developer Expert et Directeur Général Délégué de Sfeir. Ce fut l’occasion de découvrir la diversité des offres proposées par la Google Cloud Platform et de pouvoir les comparer à celles, plus médiatisées, d’autres géants du web tels Amazon (AWS) et Microsoft (Azure).\nLarge, la gamme de services Google Cloud Platform est répartie en 4 offres :\nCompute : App Engine, Container Engine, Compute Engine Storage : Bigtable, Cloud Storage, Cloud SQL, Cloud Datastore Big Data: BigQuery, Pub/Sub, Dataflow, Dataproc, Datalab Machine Learning: Vision API, Machine Leargning, Speech API, Translate API Cet article se focalisera sur l’ offre Compute. Mais avant d’aller plus loin, arrêtons-nous un moment sur ce qui est l’une des forces de la plateforme Cloud de Google : son infrastructure.\nL’Infrastructure de Google Bien que la technologie de conteneurisation ait été placée sur le devant de la scène il y’a 2 ans par Docker, Google a une très longue expérience dans ce domaine. A titre d’exemple, Google App Engine est basée sur des containers depuis son début. Google n’utilise pas de VM. Google a également une expertise indéniable dans la gestion à grande échelle de containers. Dans ses Data Centers, chaque semaine, 2 milliards de containers sont arrêtés et démarrés. Connu en interne sous le nom de Borg, Google a d’ailleurs open sourcé son système d’orchestration de containers Kubernetes.\nL’infrastructure technique de Google est l’une des plus puissantes au monde. Elle repose sur 3 piliers :\nData Center: parmi les Cloud Providers, Google n’a pas le plus de Data Center. Mais il va drastiquement augmenter leur création. En 2016, Google va en construire dans 12 nouveaux pays. Un Data Center est prévu en France pour 2018. Depuis 2015, les 2 infrastructures internes et externes convergent. Ainsi, votre application déployée sur le Cloud de Google s’exécutera sur les mêmes machines que, par exemple, Google Docs. Sauf Gmail continuera à fonctionner sur une infra interne. Backbone: depuis 2008, Google cofinance le déploiement du réseau Internet sur la planète. Tous les Data Centers de Google sont reliés par fibre optique. Edge Caching: pour ses services, Google a besoin de nombreux « points de présence ». Des systèmes de cache sont mis en place sur les points de présence (il s’agit de CDN). Ce cache est mis à disposition gratuitement sur App Engine et Compute Engine. Enfin, l’infrastructure Cloud de Google est découpée en zones (un data center) et en régions (plusieurs zones).\nL’offre Compute de Google regroupe 3 services vous permettant de déployer vos applications sur le Cloud de Google :\nApp Engine Container Engine Compute Engine Google App Engine Google App Engine (GAE) est une plateforme hautement scalable dédiée aux applications web, aux jeux et aux backends d’applications mobile. Didier a commencé à la béta-tester en 2008. Il nous cite plusieurs références :\nA bon entendeur: développée par Didier, cette application Androïd de détection de zones de danger a atteint 1 millions de téléchargements et des pics de 10 000 utilisateurs simultanés. En forte charge, son coût mensuel n’est que de 2$. Snapchat: son backend est sous App Engine. En 2015, sur 20 personnes, ils n’ont aucun Ops. Techniquement, ce sont les Ops de Google. Aujourd’hui, Snapchat dépasse Twiter en nombre d’utilisateurs. Conférence des évêques de France: chaque année, le 24 décembre entre midi et 16h, le trafic de leur site permettant de consulter les horaires de messe est multiplié par 100. Economiquement, migrer vers GAE a été bien plus rentable (30$ mensuel) que de louer 100 machines. Google Street Art: référencé sur la page d’accueil de Google Search, le site web doit supporter 50 millions de visiteurs en 24h. Pour des applications à fort trafic, Didier précise qu’il est nécessaire de vérifier les quotas de toutes les API utilisées (ex : nécessité d’une dérogation pour Google Maps qui est limité à 1) Client présent dans le monde entier avec plein de micro-app (ex : note de frais). Pour des questions de temps de réponse, ces applications doivent être déployées dans plusieurs data centers. Le coût de développement de la migration vers Google Apps est prévu pour être amorti en 2 ans. Le ROI est très élevé car 28h de CPU par jour sont gratuits. Didier précise qu’il est difficile de porter une application existante dans App Engine. Si l’application n’est pas pensée pour ce type d’architecture, le coût sera prohibitif. A la question « Pourquoi GAE n’est-il pas plus utilisé ? », Didier répond que Google App Engine est trop en avance et souffre d’un problème de vocabulaire (ex : instance vs container).\nNativement, Google App Engine propose 4 runtimes managés: Python 2.7, Java 7, Go et PHP. Didier nous fait un retour d’expérience sur les 3 premiers :\nPython: rapide au démarrage mais lent à l’exécution. Bien pour les applications web avec du cache. Java: démarre lentement, mais rapide à l’exécution. Possibilité de pré-chauffer des instances pour encaisser le trafic. Go: le meilleur des 2 mondes : démarre et s’exécute vite. Le service Uber de géolocalisation a été recodé en Go. Pour avoir un ordre d’idée, Go peut consommer 10x moins que du Python Le fait d’utiliser l’un de ces 4 runtimes permet de bénéficier de runtimes pré-chauffés permettant d’absorber rapidement la charge. Lors d’une démo, une application Go a été mise en ligne en quelques ms. L’ historisation des versions est l’une des fonctionnalités phares de GA : jusqu’à 10 versions d’une même application sont historisées. Toutes les versions sont utilisables via des URL dédiées. GAE offre la possibilité de faire du split trafic. Par exemple, 3% du trafic passent sur une nouvelle version. Cela permet de mesurer le business. Bien entendu, les logs sont splittés par version.\nDidier nous sensibilise sur le fait que GAE a une approche très puriste du Cloud. De ce fait, il est interdit d’écrire sur le filesystem. Et il n’existe pas de sessions web. L’utilisation de solution de stockage est nécessaire. Par ailleurs, il est interdit d’installer de librairies tierces sur l’OS. Pour pallier à ces limitations, il est possible de passer par des Flexible Machines. Elles remplacent les Managed VM et permettent de créer ses propres containers avec un Dockerfile. Ainsi, il est possible de faire exécuter une application Java 8 dans GAE. Cette personnalisation a un coût : le tarif d’une Flexible Machine est celui d’une instance Compute Engine.\nGoogle Container Engine A mi-chemin entre le IaaS et le PaaS, Google Container Engine permet d’exécuter ses propres containers Docker sur l’infrastructure de Google. Container Engine est, en quelque sort, la version managée de Kubnernetes.\nL’utilisation de Container Enfine demande pour pré-requis de connaître les concepts d’architecture suivant :\nContainer: unité de base Pod: ensemble cohérent de conteneurs. C’est ce qu’on manipule avec Kubernetes. Un pod n’est pas public. On peut l’exposer via un service (association d’un port à un pod). Node : instance Google Compute Engine Master: supervise le fonctionnement des nœuds Replication Controler: à partir d’un template de pod, ce contrôleur peut créer des pods pour absorber la charge. A noter que Google permet d’héberger des images Docker privées via son Google Container Registry.\nGoogle Compute Google Compute est la solution IaaS (bare-metal) de Google. La création d’une machine se fait sur mesure :\nCPU Image de Boot avec un image préinstallée (Ubuntu, Windows …) ou personnalisée Préemptible ou non : Google peut vous la reprendre à tout moment en échange d’un meilleur tarif. Taille du disque (max de 64 To) et type : Standard, SSD, local SSD Suppression ou non du disque au boot. Cette fonctionnalité a été utilisée au cours d’une démo pour créer une image du disque avec un serveur Apache installé Disque partageable entre plusieurs machines Les fonctionnalités sont nombreuses :\nSnapshots (versionning de disque) Resizing de disque à chaud Startup scripts (fichier hébergé sur Cloud Storage) Auto-scaling Facturation à la minute (à partir de la 10ième minute). Niveau réseau, on peut créer/manager VPN, DNS, Load Balancing (HTTP, UDP, TCP), CDN, interconnexion en fibre avec le data center Google. Tout est prévu pour recréer son infrastructure réseau dans les Data Center de Google.\nEnfin, Compute Engine vient avec des outils :\nCloud Functions: petit bout de code JS (s’exécute sous node.JS) Stackdriver: tableaux de bord de monitoring du Cloud Google et Amazon Source Repositories: repository Git, debugging en prod pour récupérer des valeurs de variables. Conclusion Moi qui avait déjà testé et apprécié Google App Engine en 2012, cette formation m’aura plus particulièrement permis de découvrir les autres services de l’offre Compute. Basés sur Docker, Flexible Machine et Container Engine sont mes coups de cœur.\nSujet que je n’ai pas abordé mais qui est primordial pour les décideurs : les tarifs d’accès à la plateforme. Sachez qu’il n’y a pas de contrat spécifique. Les tarifs sont publics. Les dégressivités s’appliquent automatiquement. Bien entendu, plus on on descend vers du sur-mesure et du dédié, plus les tarifs s’élèvent. Et à l’inverse, plus on utilise les services managés de Google, plus la facture s’allège, l’utilisation de la plateforme se simplifie (moins d’Ops) et la scalabilité est facilitée.\nEnfin, des produits comme App Engine et Big Query permettent à une entreprise de devenir Server Less. Nul besoin de se préoccuper des serveurs.\n","link":"https://javaetmoi.com/2016/07/offre-compute-google-cloud-platform/","section":"posts","tags":["cloud","docker","google"],"title":"L’offre Compute de Google Cloud Platform"},{"body":"Bien que Java 8 soit sorti il y’a 2 ans, tous les développeurs n’ont pas eu encore la chance de pouvoir utiliser, en entreprise, tous les concepts issus de la programmation fonctionnelle et qui ont été introduits dans cette version majeure : expressions lambda, interfaces fonctionnelles, méthodes par défaut, Optional, références de méthode, Streams … Pourtant, Java 8 est à nos portes : des projets de migration de serveur d’application se terminent, les socles d’entreprise se mettent à jour, des frameworks exploitent ces nouveautés (ex : JUnit 5) \u0026hellip; Et on va enfin pouvoir exploiter à bon escient toutes ces nouvelles fonctionnalités. Mais avant cela, une mise à niveau est indispensable. Et c’est dans cet objectif que j’ai récemment initié mes collègues aux Streams. A partir d’un jeux de données réduit (une liste de 3 clients), j’ai implémenté quelques règles de gestion à la fois en Java 7 avec des boucles et en Java 8 avec des Streams, histoire de leur montrer la différence.\n[slideshare id=62288571\u0026amp;doc=13-14-lesstreamsjava8-160523061803]\n","link":"https://javaetmoi.com/2016/06/les-streams-java-8-par-lexemple/","section":"posts","tags":["java"],"title":"Les Streams Java 8 par l'exemple"},{"body":"","link":"https://javaetmoi.com/tags/angular-2/","section":"tags","tags":null,"title":"Angular-2"},{"body":"Voici la présentation que j\u0026rsquo;ai animée auprès de mes collègues afin de leur faire un retour suite à ma participation à Devoxx France 2016. Le leitmotiv était \u0026ldquo;1 conférence appréciée =\u0026gt; 1 slide\u0026rdquo;.Au menu : Angular 2, ECMASript 2015, Kakfa, Spring Cloud, architecture StackOverflow, Jenkins pipeline, React, revues de code et documentation.\n[slideshare id=61995666\u0026amp;doc=2016-05-13-retoursdevoxxfrance2016-160513175637]\n","link":"https://javaetmoi.com/2016/05/devoxx-france-2016-un-slide-par-conf/","section":"posts","tags":["angular-2","devoxx","java","javascript","kafka","spring-cloud"],"title":"Devoxx France 2016 : une conf appréciée, un slide"},{"body":"","link":"https://javaetmoi.com/tags/kafka/","section":"tags","tags":null,"title":"Kafka"},{"body":"Pour vous aider à choisir quelle conférence visionner sur la chaîne Devoxx FR 2016 de Youtube ou pour vous remémorer certaines chose, je mets librement à votre disposition les différentes notes que j’ai pu prendre sur mon laptop.\nLes sujets sont variés : des Microservices avec Spring Boot et Spring Cloud, du Big Data avec Kafka et Elasticsearch, du Front End avec ECMAScript 2015 et React, du Java 8 et 9 ou bien encore de la méthodologie avec les revues de code et de la living documentation.\nCertaines notes pourront être lues de manière autonome ; je pense par exemple au quickie Comment rater ses revues de code ? et à la conférence Stack Overflow behind the scenes. Pour être exploitables en l’état, d’autres notes demanderont à ce que vous ayez assisté à la conférence ou que vous ayez pu récupérer les supports de présentation.\nSans plus attendre, voici donc mes 16 notes triées par ordre alphabétique :\nBootiful Microservice Comment faire tourner une JVM de 16 To ? Comment rater ses revues de code ? DDD : et si on reprenait tout depuis le bon bout ? E6+ maintenant ! Elasticsearch et Hibernate sont sur un bateau Hand\u0026rsquo;s On Lab Kafka Hibernate tu connais \u0026hellip; mais en fait tu connais pas High Performance Hibernate De Jenkins Maven/ Freestyle à Pipeline Le design d\u0026rsquo;API REST, un débat sans fin ? Let\u0026rsquo;s React Live documentation Retour d\u0026rsquo;expérience sur Java 8 Stack Overflow behind the scenes. How it\u0026rsquo;s made String Concaténation de 1 à 9 ","link":"https://javaetmoi.com/2016/05/16-prises-de-notes-a-devoxx-france-2016/","section":"posts","tags":["ddd","devoxx","elasticsearch","hibernate","java","javascript","jenkins","microservices","react","rest","spring-boot","spring-cloud"],"title":"16 prises de notes à Devoxx France 2016"},{"body":"","link":"https://javaetmoi.com/tags/jenkins/","section":"tags","tags":null,"title":"Jenkins"},{"body":"","link":"https://javaetmoi.com/tags/angular2/","section":"tags","tags":null,"title":"Angular2"},{"body":"Lors de l’édition 2013 de Devoxx France, je découvrais la simplicité de coder une application full JavaScript avec Angular JS. Lors de l’édition 2014, je recodais from scratch sa fonctionnalité phare de binding directionnel. Deux ans ont passé. Depuis son annonce, la version 2 d’Angular déchaine les passions au sein de la communauté front. Animé par Wassim Chegham , Emmanuel Demey et Cyril Balit, le Hand’s On Lab sur Angular 2 fut pour moi l’occasion de découvrir les nouveautés, mais surtout, de découvrir si ce nouveau cru est aussi séduisant que le premier.\nCe billet s’adresse à celles et ceux qui n’ont pas pu assister à ce Lab et qui ont envie de découvrir Angular 2. Il s’appuie sur les ressources mises à disposition par les speakers. En7 étapes, vous développerez une application de Quizz avec la beta 11 d’Angular 2.\nInstallation du post de dév Le repository GitHub angular2-codelab met à disposition l’application finale sur le master ainsi que 2 branches par étape (step) :\nUne branche de départ (ex : step1) Une branche mettant à disposition la correction de l’étape (ex : step1-solution) Point d’attention : le contenu de la branche step2 ne correspond pas à celui de la branche step1-solution. Lors du passage à l’étape suivante, un checkout de la branche correspondante sera donc nécessaire.\nLe fichier README.md donne les instructions pour :\nForker puis récupérer le repo Construire l’application (modules JavaScript téléchargés via npm) Démarrer l’application via la commande ng serve La commande ng fait appel au client en ligne de commande fournit par Angular 2. Ce dernier remplace des outils de build comme gulp et permet également de générer les composants. A termes, il est prévu qu’il soit capable de packager l’application et de la déployer sur le serveur.\nPour IDE, un simple éditeur de texte peut suffire. Proposant complétion et compilation, l’utilisation de Visual Studio, Atom ou d’IntelliJ IDEA est toutefois préférable. Le serveur web démarré par ng supporte le live reload : à chaque changement de code, la page est rafraichie.\nBien que les slides montrent à quoi ressemble l’application finale, je vous conseille de faire un checkout du master et de naviguer sur http://localhost:4200/. Vous aurez ainsi une idée plus précise de l’objectif à atteindre lors de la partie pratique.\nUn mot sur TypeScript Avant de commencer à jouer avec Angular 2, il est nécessaire de s’arrêter sur un détail d’importance. Bien qu’étant un framework front, le langage first d’Angular 2 n’est pas JavaScript/ECMAScript mais TypeScript. Inventé par Microsoft, TypeScript est un sur-ensemble de ES5, ES2015 (ES6), ES2016 (ES7) et ceux à venir. Dans le navigateur ou via un outil de build, TypeScript est transpilé en JavaScript. Angular 2 n’impose pas l’utilisation de TypeScript. On peut utiliser ES5, ES6 et même Dart. Mais l’utilisation de TypeScript simplifie l’utilisation d’Angular2. A minima, Wassim conseille d’utiliser ES6. Pour faire tourner du ES6 sur les navigateurs ne le supportant pas, il est nécessaire de passer par un transpileur comme Babel. Utiliser TypeScript pour vos développements Angular 2 n’impose pas de rester sur ce dernier. Repasser à du full JavaScript restera possible.\nCe lab est codé en TypeScript. Une connaissance minimale de ce dernier ou, à défaut, d’ES6 est recommandé. J’ai toutefois pu m’en sortir sans : baladez-vous dans le code source du step1 pour apprendre la syntaxe des imports, la définition d’une classe, l’usage des annotations …\nSi vous vous posez la question, les fichiers suffixés par .d.ts et présents dans le workspace du Lab permettent d’utiliser des librairies tierces JavaScript depuis TypeScript (ex : Jasmine et Selenium).\nLes apports d’Angular 2 Angular 2 est présenté comme une plateforme de développement (et non un framework). Il couvre bien plus de fonctionnalités qu’Angular 1 (NG1).\nChose intéressante : on peut faire tourner une application Angular 2 côté serveur avec Node. Comme React, Angular 2 peut fonctionner sans DOM (avec le shadow DOM), ce qui ouvre son utilisation aux applications natives.\nStep 1 – Les composants\nCette première étape va vous permettre de créer votre premier composant et d’initier l’application web.\nUne application Angular 2 est un arbre de composants (au sens Web Components), là où Angular 1 était un arbre cyclique (dirty checking). Il s’agit du concept principal d’Angular 2. Un composant Angular 2 combine directive, contrôleur et scope d’Angular 1.\nL’application Quizz est constituée des 7 composants suivants :\nNg2CodelabApp: une application Angular 2 est elle même un composant. C’est le composant parent de tous les autres composants. Toolbar: barre de navigation située en haut de l’écran Home: page d’accueil affichant une liste de thèmes (TypeScript, ECMAScript 6, Angular 2 et React). Le détail de chaque thème est géré par le composant ThemeCard. ThemeCard: affiche le nom du thème, une image, son descriptif et propose un bouton permettant de commencer le quizz associé. Technology: page affichant une question (composant QuestionCard) et permettant de naviguer vers les questions précédentes / suivantes. QuestionCard: affiche la question et reçoit les réponses de l’utilisateur Summary: page listant les résultats des questions d’un quizz Ces composants sont tous localisés dans le répertoire src/app/components de votre projet. Chaque composant dispose d’un répertoire dédié. Techniquement, un composant est une classe JavaScript décorée avec l’annotation @Component et explicitement exportée (mot clé export d’ES6).\nUn composant peut disposer d’une vue. Cette dernière est déclarée en tant que propriété template ou templateUrl du @Component. L’attribut templateUrl référence un template HTML externe (comme sous Angular 1) et template permet d’utiliser les backquotes TS pour déclarer un template inline.\nAu travers de la propriété selector de @Component, un composant peut également être associé à une balise HTML, et même plus largement à un selector CSS 3.\nPour démarrer une application Bootstrap, il faut appeler la fonction bootstrap en lui passant la classe du composant parent, en l’occurrence Ng2CodelabApp.\nLes informations précédentes devraient vous aider à implémenter le step-1 en suivant les instructions données dans le fichier STEPS.md. A vous de jouer. Avant de commencer le step-2, voici quelques explications sur la solution.\nDans le fichier ng2-codelab.js, la déclaration du selector: \u0026lsquo;app\u0026rsquo;, permet à Angular 2 de reconnaître la balise \u0026lt; app\u0026gt; que vous avez ajoutée dans le fichier index.html:\n\u0026lt;app\u0026gt; \u0026lt;div class=\u0026#34;mdl-grid\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;mdl-cell mdl-cell--12-col\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;mdl-spinner mdl-js-spinner is-active\u0026#34;\u0026gt;\u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/app\u0026gt; Toujours dans le index.html, on indique à Angular 2 quel fichier JS utiliser pour bootstraper l’application via l’appel à System.import(\u0026ldquo;app.js\u0026rdquo;). Le fichier app.js est la version transpilée de app.ts.\n\u0026lt;script\u0026gt; System.config({ packages: { app: { format: \u0026#39;register\u0026#39;, defaultExtension: \u0026#39;js\u0026#39; } } }); System.import(\u0026#39;app.js\u0026#39;).then(null, console.error.bind(console)); \u0026lt;/script\u0026gt; Dans le jargon d’Angular 2, le composant Ng2CodelabApp initié dans cette étape s’appelle un « dumb » component.\nEnfin, dans app.js, la classe Ng2CodelabApp exportée précédemment est importée : import { Ng2CodelabApp } from \u0026lsquo;./app/ng2-codelab\u0026rsquo;;\nA la fin de cette étape, la page d’accueil n’affiche qu’un simple sablier. Step 2 – Templates et cycle de vie Dans le step 1, les fichiers HTML manipulés ne comportaient aucune syntaxe particulière à Angular. Le step 2 vise à utiliser quelques notations syntaxiques propres à Angular 2 et qui vont sont présentées dans le slide Template Syntaxe et dont voici quelques explications. Syntaxe raccourcie d’Angular 2 :\nDouble accolade {{: affiche une propriété du composant (même syntaxe qu’Angular 1) Crochet []: binding unidirectionnel d’une propriété du DOM (ex : classList), d’une propriété d’une classe, d’une classe CSS, d’un style CSS, d’un attribut du DOM Parenthèse (): binding sur un événement du navigateur (ex : (click)) ou d’un événement custom, et même de futurs évènements HTML (en Angular 1 la liste des évènements était hard-codée) Crochet + parenthèse [()]: binding bidirectionnel. Contrairement à Angular 1, le binding bidirectionnel n’est plus la norme dans Angular 2. Etoile *: directives qui viennent modifier le DOM. Au premier abord, cette nouvelle syntaxe peut dérouter. Les speakers ne s’en cachent pas. Mais à l’usage, ils nous ont assuré qu’elle permettait de gagner en lisibilité.\nLes composants ont un cycle de vie. Angular 2 permet aux développeurs d’interagir à l’aide de fonction callback : ngOnInit, ngDoCheck, ngOnChanges … Ces fonctions sont à déclarer dans les composants. Angular 2 les appellera au moment approprié. Détail de 3 fonctions sur les 8 disponibles :\nngOnInit: déclenchée lorsque la directive a été compilée, que les meta-données sont chargées et que la directive démarre ngOnChanges(records): écoute les changements d’état réalisés par un composant parent ngAfterViewInit: appelée lorsque la vue a été chargée Chaque composant gère son état. C’est l’une des raisons qui fait qu’Angular 2 est plus performant que son prédécesseur.\nEn pratique, le Parent passe une information à son Enfant (propriété @Input). C’est l’Enfant qui met à jour son état. L’Enfant peut communiquer avec son Parent via l’event binding (propriété @Output). Le slide State Managment schématise ce comportement. Des ressemblances existent avec l’ architecture Flux.\nDans le slide d’exemple, le composant ThemeCard implémente l’interface AfterViewInit. Facultative, l’utilisation de cette interface TypeScript permet à l’IDE de mettre en garde le développeur si il omet d’implémenter la fonction ngAfterViewInit().\nLa convention de nommage suivante est à respecter pour le double data-binding :\n@Input() \u0026lt;propertyName\u0026gt; @Output() \u0026lt;propertyName\u0026gt;Change La partie théorie s’arrête ici. En pratique, le step 2 vous demande de créer le nouveau composant ThemeCard puis de l’utiliser dans le composant Home. En entrée, ThemeCard acceptera un paramètre de type ITechnology. Le template de ThemeCard affichera le titre, la description et le logo de cette technologie. Le composant Home sera responsable de charger la liste des technologies à afficher. Elle s’appuiera sur le TechnologiesStore mis à votre disposition. Les notions de module et d’injection de dépendances seront abordés par la suite. Pour le moment, contentez vous d’ajouter la propriété themeCards et le constructeur suivant à la classe Home :\nprivate themeCards: any[]; constructor(technologiesStore: TechnologiesStore){ technologiesStore.fetch().then((themes) =\u0026gt; this.themeCards = themes); } Remarque : pour utiliser le TechnologiesStore, il faut le déclarer en tant que providers au niveau de l’annotation @Component. Munis de ces informations, vous pouvez commencer le step 2. Avant de passer au step 3, arrêtons-nous un moment sur la solution.\nLe template de Home est intéressant :\n\u0026lt;theme-card *ngFor=\u0026#34;#card of themeCards\u0026#34; [theme]=\u0026#34;card\u0026#34; class=\u0026#34;mdl-cell mdl-cell--6-col\u0026#34; \u0026gt;\u0026lt;/theme-card\u0026gt; Il boucle sur la collection de themeCards chargée par le constructeur du composant Home. La syntaxe dièse #card permet de référencer le this du composant.\nLe template theme-card.html référence quant à lui les propriétés du composant :\n\u0026lt;h2 class=\u0026#34;mdl-card__title-text\u0026#34;\u0026gt;{{ theme.title }}\u0026lt;/h2\u0026gt; … \u0026lt;img [src]=\u0026#34;theme.logo\u0026#34; alt=\u0026#34;\u0026#34; width=\u0026#34;200\u0026#34; height=\u0026#34;200\u0026#34;\u0026gt; \u0026lt;p\u0026gt;{{ theme.description }}\u0026lt;/p\u0026gt; Step 3 – Le routage Cette étape a pour objectif de vous faire prendre en main le component angular2/router. Bien que s’agissant d’un module externe à Angular 2, il s’agit du module officiel de gestion de la navigation. Ce qu’il est important de comprendre, c’est que chaque composant peut être routable. Dans Angular 2, les routes se font entre composants et non entre vues. Bien qu’il existe des règles globales à l’application, chaque composant gère ses propres règles de routage.\nLe slide Component Router illustre la configuration des routes et leur utilisation dans un template inline. Utilisé dans le template, l’attribut [routerLink] crée un lien hypertexte vers un autre composant. L’élément équivaut au ng-view d’Angular 1. L’annotation @RouteConfig permet de déclarer les routes. La syntaxe des points de suspensions du path (ex : \u0026lsquo;/details/ …\u0026rsquo;) permet de déléguer le routage à un autre composant. Dans le constructeur d’un composant, il est possible de récupérer le paramètre d’URL via la classe RouteParams.\nLe slide Bootstraping the Router illustre l’initialisation du routeur qui a lieu dans le fichier principal (app.ts). 3 composants sont à importer :\nROUTER_PROVIDERS: permet de récupérer les services du routeur PathLocationStragegy ou HashLocationStrategie: type de navigation, par hash ou URL LocationStrategy: permet de spécifier la stratégie de navigation à utiliser dans l’application D’après les speakers, l’ambition de ce module router est d’arriver au même niveau que UI Router avec des routes imbriquées (routes abstraites). Il est d\u0026rsquo;ailleurs dors et déjà utilisable dans Angular 1. L’objectif du step3 est d’initier le composant QuestionCard puis de mettre en place la navigation entre le Home, ThemeCard et QuestionCard. A vous de jouer.\nDans la solution, 2 routes sont déclarées au niveau du composant parent Ng2CodelabApp :\nune route principale / gérée par le composant Home et configurée pour être la route par défaut, et une route /question gérée par le composant QuestionCard. @Component({ selector: \u0026#39;app\u0026#39;, templateUrl: \u0026#39;app/ng2codelab.html\u0026#39;, directives : [Home, ROUTER_DIRECTIVES] }) @RouteConfig([ { path: \u0026#39;/\u0026#39;, component: Home, name: \u0026#39;Home\u0026#39;, useAsDefault: true }, { path: \u0026#39;/question\u0026#39;, component: QuestionCard, name: \u0026#39;QuestionCard\u0026#39; } ]) export class Ng2CodelabApp { Dans le template theme-card.html, la directive [routerLink] génère le lien hypertexte /question.\n\u0026lt;a [routerLink]=\u0026#34;[ \u0026#39;/QuestionCard\u0026#39; ]\u0026#34; class=\u0026#34;mdl-button mdl-button--colored mdl-js-button mdl-js-ripple-effect\u0026#34;\u0026gt; Start test \u0026lt;/a\u0026gt; Un clic sur le lien déclenche le routage vers le composant QuestionCard. Dans le DOM, la balise est remplacée par .\nStep 4 – Les providers Entraperçu dans l’étape 2 lors de l’utilisation du TechnologiesStore, le provider est le thème central de cette 4ième étape. Un provider est chargé de mettre une classe JavaScript à disposition d’un composant. Typiquement, un composant va avoir besoin d’un service qui interagit avec le backend pour récupérer / mettre à jour des données. La mise en relation est basée sur l’ injection de dépendance(IoC). Pour rappel, Angular 1 est le premier framework front à avoir utilisé l’IoC. Son implémentation était assez rudimentaire. Angular 2 améliore ce premier coup d’essai en donnant davantage la main aux développeurs. Ainsi, un composant enfant pourra, par exemple, redéfinir les providers de son parent.\nAngular 2 propose 2 types de providers :\nLocal : permet d’utiliser un provider dans un composant. En ES6, il est nécessaire d’utiliser l’annotation @Inject Global: injection à la racine de l’application. Mises-en garde : comme en Angular 1, tout provider est un singleton. La fonction provide() d’angular-core permet de configurer les providers. Le slide Providers Configuration montre 3 syntaxes différentes.\nLe step-4 consiste à afficher les questions d’une technologie, à maintenir les choix de l’utilisateur et à naviguer entre les questions. Pour vous y aider, le service QuestionStore est mis à votre disposition :\n@Injectable() export class QuestionsStore { private questions: IQuestion[]; constructor(questions: IQuestion[] = QUESTIONS){ this.questions = questions.map( (question: IQuestion) =\u0026gt; new Question(question)); }… } Ce service est injecté dans le composant Technology qui vous ait également fournit.\nUne fois le step-4 réalisé, le composant QuestionCard est relativement concis :\n@Component({ selector: \u0026#39;question-card\u0026#39;, encapsulation: ViewEncapsulation.None, templateUrl: \u0026#39;./app/components/question-card/question-card.html\u0026#39;, directives: [ROUTER_DIRECTIVES] }) export class QuestionCard implements AfterViewInit { @Input() question: IQuestion; @Output() checked: EventEmitter\u0026lt;IChoice\u0026gt;; constructor() { this.checked = new EventEmitter(); } onCheckedChange($event, choice: IChoice) { this.checked.emit(choice); } } La question à afficher lui est passée en paramètre @Input. Et à chaque fois que l’utilisateur sélectionne / désélectionne une réponse, l’événement checked est envoyé au composant parent, à savoir Technology. Le template de ce dernier binde l’événement sur la fonction toggle :\ntemplate: ` \u0026lt;question-card (checked)=\u0026#34;toggle($event)\u0026#34;[question]=\u0026#34;currentQuestion\u0026#34; class=\u0026#34;mdl-cell mdl-cell--4-col\u0026#34; \u0026gt;\u0026lt;/question-card\u0026gt; private toggle(choice: IChoice) { this.questions[this.currentQuestionId].toggle(choice); } Step 5 – Smart components Aucun slide « Break time » ne précède l’étape n°5. Cette dernière ne requière pas de nouvelles notions. Par contre, vous allez mettre en œuvre un autre type de composant : le « smart » component Summary. Les smarts components n’échangent pas seulement des données avec leur composant parent, mais lisent / écrivent des données via des services. Dans cette étape, vous allez accéder au service QuestionStore depuis le composant Summary. Le QuestionStore sera mis à disposition par un Provider de type Factory.\nA vous d’implémenter le step-5.\nLa solution ne comporte pas de difficultés particulières. Le QuestionStore est injecté par constructeur. Et les questions sont récupérées en asynchrone via une promesse.\n@Component({ providers: [ new Provider(QuestionsStore, { useFactory: () =\u0026gt; new QuestionsStore(SessionStore.read()) }) ], selector: \u0026#39;summary\u0026#39;, template: ` \u0026lt;div\u0026gt; \u0026lt;div class=\u0026#34;mdl-cell mdl-cell--9-col-desktop mdl-cell--6-col-tablet mdl-cell--4-col-phone\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;mdl-card__supporting-text\u0026#34;\u0026gt; \u0026lt;h4\u0026gt;Your score is {{ score }}/{{ total }}\u0026lt;/h4\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;question-card [question]=\u0026#34;question\u0026#34; *ngFor=\u0026#34;#question of questions\u0026#34;\u0026gt;\u0026lt;/question-card\u0026gt; `, directives: [QuestionCard], encapsulation: ViewEncapsulation.None }) export class Summary implements OnInit{ private questions: IQuestion[]; private score: number; private total: number; private questionsStore: IQuestionsStore; constructor(questionsStore: QuestionsStore) { this.questionsStore = questionsStore; this.questionsStore .fetch() .then( (questions) =\u0026gt; this.questions = questions ); } … } Step 6 – Les pipes Les filtres d’Angular 1 ont été renommées en pipes. Le concept et la syntaxe restent inchangés. Leur fonctionnement s’inspire des pipes Linux. Ils s’utilisent dans les templates pour transformer ou formater une donnée. Un pipe peut accepter des paramètres. Angular 2 vient avec un certains nombres de built-ins pipes: DatePipe, UpperCase … L’étape 6 consiste à créer son propre Pipe. Le slide Custom Pipes montre comment utiliser l’annotation @Pipe pour déclarer un Pipe. Appuyez-vous sur cette exemple pour implémenter le step-6.\nComme vous pouvez le constater, l’implémentation du MarkPipe est concise :\nimport { Pipe } from \u0026#39;angular2/core\u0026#39;; import { IChoice } from \u0026#39;../../services/question-store/question-store\u0026#39;; @Pipe({ name: \u0026#39;mark\u0026#39; }) export class MarkPipe { transform(choice: IChoice) { return choice.isCorrect() ? \u0026#39;✔\u0026#39; : \u0026#39;✘\u0026#39;; } } Voici comment ce pipe est utilisé dans le template du composant QuestionCard :\n\u0026lt;span *ngIf=\u0026#34;preview\u0026#34; class=\u0026#34;answer\u0026#34; \u0026gt;{{ choice | mark }}\u0026lt;/span\u0026gt; Step 7 – Les directives Cette dernière étape du Lab vous montre comment créer votre propre Directive. Le concept de directive structurelle n’a pas changé depuis Angular 1 : une directive permet d’attacher des comportements à des éléments customs du DOM. Un exemple sera bien plus parlant : MyHighlightDirective. L’usage de l’interface Renderer à la place d’un accès direct au DOM permet de rendre l’application portable (ex : rendu côté serveur).\nJe vous laisse implémenter le step-7. Et voici un extrait de la solution :\nimport { Directive, ElementRef, Renderer, Input, AfterViewInit } from \u0026#39;angular2/core\u0026#39;; @Directive({ selector: \u0026#39;[status]\u0026#39; }) export class StatusDirective implements AfterViewInit { @Input(\u0026#39;status\u0026#39;) status: boolean; constructor( private el: ElementRef, private renderer: Renderer ) {} ngAfterViewInit() { let color = this.status ? \u0026#39;green\u0026#39; : \u0026#39;red\u0026#39;; this.renderer.setElementStyle(this.el.nativeElement, \u0026#39;color\u0026#39;, color); } } Conclusion Ce Lab vous aura permis de construire pas à pas une application de quizz tout en découvrant les fonctionnalités phares d’Angular 2.\nPersonnellement, j’ai trouvé le niveau de ce Lab élevé. Pour se débloquer, il est fréquent d’aller regarder la correction. Et le nombre de lignes de code à produire pour chaque étape est relativement conséquent. Le fait de ne connaître ni TypeScript ni ES6 aura été une difficulté supplémentaire. La prise en main d’Angular 2 m’a demandé un investissement bien plus important que pour Angular 1. L’approche par composants n’est pas habituelle lorsqu’on a l’habitude de raisonner sur des pages. De mon point de vue, il est nécessaire de bien maîtriser ce framework avant de pouvoir l’utiliser efficacement.\nReste à savoir si Angular 2 réussira à s’imposer face à des frameworks plus légers tels ReactJS ? J’ai ouïe dire lors de Devoxx France que les Evangélistes Google le considéraient comme déjà mort … S’agit-il d’un troll ? Seul l’avenir nous le dira !\n","link":"https://javaetmoi.com/2016/04/angular-2-hands-on-lab-devoxx-france/","section":"posts","tags":["angular2","devoxx","typescript"],"title":"Lab Angular 2 à Devoxx France 2016"},{"body":"","link":"https://javaetmoi.com/tags/typescript/","section":"tags","tags":null,"title":"Typescript"},{"body":"Les jeux de données font partie intégrante des tests. Elaborer un jeu de données demande une connaissance fonctionnelle, aussi bien sur la nature des données que sur le scénario de test envisagé. Utiliser des jeux de données réalistes participe à la compréhension du scénario de test et, donc, à sa documentation. S’il vous était possible de générer ces fameux jeux de données, seriez-vous intéressés ?C’est précisément l’objet de ce billet et d’un modeste outil baptisé JavaBean Marshaller.\nContexte Au cours de mes missions, j’ai rencontré plusieurs outils maisons de génération de jeux de données(souvent abusivement appelés mocks). Leur fonctionnement consiste à pouvoir sauvegarder des grappe d’objets au format XML ou JSON, puis à les recharger. Ces jeux de données sont utilisés dans des tests ou pour bouchonner des adhérences indisponibles. Le déclenchement de la sauvegarde des jeux de données peut-être réalisé en AOP.\nForts pratiques, ces outils souffrent malgré tout de certaines limitations:\nRelative lenteur de rechargement des fichier XML et JSON, en particulier pour des tests unitaires dont l’exécution doit rester rapide Impossibilité de factoriser des morceaux de jeux de données entre plusieurs tests Maintenance et refactoring rendus difficiles Complexité des graphes d’objets cycliques (IDREF en XML ou XStream JSON) Complexité des relations bidirectionnelles Format des jeux de données couplé à une technologie de marshalling (ex : JAXB, Jackson) Partant de ce constat, je me suis demandé s’il était possible de remédier à ces limitations. La solution qui m’est instantanément venue à l’esprit fut d’utiliser un autre format que le XML ou le JSON, à savoir le Java. Après tout, lorsqu’on écrit manuellement des jeux de données, c’est en Java qu’on le fait (sauf si on passe par des outils tels DbUnit). Le langage Java reste ce qu’il y’a de plus naturel pour des développeurs Java. Qui plus est, vérifiés à la compilation, les jeux de données seront simples à maintenir.\nJavaBean Marshaller Disponible en Open Source sur github, le projet JavaBean Marshaller fournit la classe utilitaire JavaBeanMarshaller. En paramètre de la méthode generateJavaCode, vous passez l’objet racine de votre grappe d’objets Java. En sortie, une classe Java permettant de réinstancier votre grappe sera créée.\nUn exemple sera bien plus parlant. Prenons le diagramme de classes ci-dessous. Non représentés sur ce diagramme, les classes Album et Artiste possèdent des getter / setter et constructeur sans argument.\nImaginons l’instance d’une classe Artist référençant un seul et unique Album. Voici le bout de code correspondant et volontairement très compact :\nArtist u2 = new Artist(1, \u0026#34;U2\u0026#34;, ArtistType.GROUP); Album joshuaTree = new Album(\u0026#34;The Joshua Tree\u0026#34;, LocalDate.of(1987, Month.MARCH, 9), u2); u2.getAlbums().add(joshuaTree); Ajoutons la dépendance maven :\n\u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.javaetmoi.util\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;javaetmoi-javabean-marshaller\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.0.3\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; Appelons ensuite la méthode JavaBeanMarshaller.generateJavaCode(u2);\nUne classe ArtistFactory contenant le code suivant est généré :\nimport com.javaetmoi.javabean.domain.Album; import com.javaetmoi.javabean.domain.Artist; import com.javaetmoi.javabean.domain.ArtistType; import java.time.LocalDate; import java.time.Month; import java.util.ArrayList; import java.util.List; public class ArtistFactory { public static Artist newArtist() { Artist artist1 = new Artist(); List\u0026lt;Album\u0026gt; albums1 = new ArrayList\u0026lt;\u0026gt;(); Album album1 = new Album(); album1.setArtist(artist1); album1.setReleaseDate(LocalDate.of(1987, Month.MARCH, 9)); album1.setName(\u0026#34;The Joshua Tree\u0026#34;); albums1.add(album1); artist1.setAlbums(albums1); artist1.setName(\u0026#34;U2\u0026#34;); artist1.setId(1L); artist1.setType(ArtistType.GROUP); return artist1; } } Cette classe compile. Plus verbeuse que le code original, elle a le mérite de structurer la création d’une instance et présente un code lisible par tout développeur Java. En fonction de vos cas de test, libre à vous de modifier les valeurs, ajouter d’autres albums …\nFonctionnalités Dans sa version 1.0.0, le projet JavaBean marshaller supporte :\nJDK 7 et 8 Collections Maps Tableaux à 1 et 2 dimensions Relations unidirectionnelles et bidirectionnelles Graphe cyclique Nombreux types du JDK : Types primitifs : boolean, short, float … Types wrappers : Boolean, String, BigDecimal Enumérations Date : util.Date, java.sql.Date, Calendar, XMLGregorianCalendar Java 8 Date \u0026amp; Time API : LocalDate, Period, Instant … Librairies tierces : JodaTime : DateTime, Period, Instant … Extensible Ouvert aux extensions, le générateur JavaBean marshaller permet d’enrichir les types supportés :\nAutres classes du JDK Classes de frameworks tiers Classes d’une application métier Le mécanisme d’extension repose sur l’interface CodeGenerator. Implémenter cette interface puis enregistrer l’instance auprès du JavaBeanMarshaller suffisent. La classe abstraite DefaultCodeGenerator allège l’implémentation.\nRepartons de l’exemple précédent. Essayons de modifier le code généré. Au lieu de faire appel au constructeur sans argument de la classe Album, on souhaite utiliser le constructeur prenant en paramètres ses propriétés. Pour cela, on crée une classe AlbumGenerator implémentant la DefaultCodeGenerator.\npublic class AlbumGenerator extends DefaultCodeGenerator\u0026lt;Album\u0026gt; { @Override public void generateSetter(MethodSpec.Builder method, SetterParam param) { Album album = getValue(param); Item releaseDate = param.getMarshaller().buildItem(album.getReleaseDate()); String artistVarName = param.getMarshaller().getVariableName(album.getArtist()); method.addStatement(\u0026#34;$L.add(new $T(\\\u0026#34;$L\\\u0026#34;, \u0026#34;+releaseDate.getPattern()+\u0026#34;, $L))\u0026#34;, param.getVarName(), param.getValueClass(), album.getName(), releaseDate.getVal(), artistVarName); } } Comme vous pouvez le constater, l’implémentation de la méthode generateSetter demande à manier l’API du générateur. Les tokens $T et $L et la classe MethodSpec.Builder viennent du framework Javapoet sur lequel le générateur s’appuie. Nous en reparlerons dans le chapitre suivant. Les nombreuses implémentations existantes peuvent servir de documentation.\nEn reprenant la même grappe et en utilisant l’ AlbumGenerator,\nArtist u2 = new Artist(1, \u0026#34;U2\u0026#34;, ArtistType.GROUP); Album joshuaTree = new Album(\u0026#34;The Joshua Tree\u0026#34;, LocalDate.of(1987, Month.MARCH, 9), u2); u2.getAlbums().add(joshuaTree); JavaBeanMarshaller.register(new AlbumGenerator()); JavaBeanMarshaller.generateJavaCode(u2); La méthode newArtist gagne en concision :\npublic static Artist newArtist() { Artist artist1 = new Artist(); List\u0026lt;Album\u0026gt; albums1 = new ArrayList\u0026lt;\u0026gt;(); albums1.add(new Album(\u0026#34;The Joshua Tree\u0026#34;, LocalDate.of(1987, 3, 9), artist1)); artist1.setAlbums(albums1); artist1.setName(\u0026#34;U2\u0026#34;); artist1.setId(1L); artist1.setType(ArtistType.GROUP); return artist1; } Le fonctionnement En interne, la classe JavaBeanMarshaller effectue un parcours de graphe. Pour y arriver, elle s’appuie sur la classe PropertyUtils de Commons BeanUtils. Les propriétés déjà traitées sont mémorisées dans un Set.\nLa génération de code Java s’appuie sur la librairie Javapoet créée par Square. Bien que le code généré soit simple et aurait pu être fait sans librairie tierce, Javapoet apporte :\nLa gestion de l’import des packages L’ajout des ; à la fin de chaque instruction Une syntaxe inspirée de String.format() permettant d’éviter la concaténation de chaînes de caractères (tokens $T, $L et $N) Un outil de génération de jeux de données facilitant les tests avait le devoir d’être testé. C’est chose faite. Sa couverture de test est accessible sur Coveralls.io.\nLes tests unitaires reposent tous sur la même stratégie :\nCréation d’une grappe d’objets Appel à la méthode generateJavaCode chargée de générer la classe Java Compilation de la classe (via la méthode getSystemJavaCompiler du JDK) Appel à la méthode statique permettant de recréer la grappe d’objets Comparaison des 2 grappes d’objets (méthode assertReflectionEqual de Unitils) Conclusion Dans ce billet, vous avez fait la connaissance avec un tout jeune générateur de Dataset Java (sa 1ière release date du 19 mars 2016). Je compte sur vous pour me confirmer (ou non) son utilité. N’hésitez pas non plus à me soumettre les cas que je n’ai pas prévu. Et si vous souhaitez y contribuer, vous êtes les bienvenus.\nUne prochaine étape consistera à rendre ce générateur moins intrusif. En effet, une fois le dataset généré, il faut bien penser à retirer du code de prod l’appel au JavaBeanMarshaller ainsi que la dépendance maven. L’utilisation d’un agent et d’une annotation serait une solution. A suivre …\n","link":"https://javaetmoi.com/2016/03/generation-jeux-donnees-java/","section":"posts","tags":["javapoet","test"],"title":"Génération de jeux de données Java"},{"body":"","link":"https://javaetmoi.com/tags/javapoet/","section":"tags","tags":null,"title":"Javapoet"},{"body":"","link":"https://javaetmoi.com/categories/test/","section":"categories","tags":null,"title":"Test"},{"body":"","link":"https://javaetmoi.com/tags/test/","section":"tags","tags":null,"title":"Test"},{"body":"Au quotidien, tout développeur Java utilise un IDE, un JDK, un outil build et un navigateur. Ce sont des standards. A côté, chaque développeur utilise un ou plusieurs petits outils permettant d’améliorer son quotidien. Par outil, j’entends aussi bien un plugin, un logiciel ou une fonctionnalité avancée de son IDE. Dans cette présentation, j’ai eu envie de partager des outils fonctionnant sous Windows(mais pas que). J’utilise certains depuis des années, d’autres depuis seulement quelques semaines suite aux recommandations de collègues. A vous de voir si vous souhaitez les tester puis les adopter, ou non.\n[slideshare id=59714114\u0026amp;doc=2016-03-17-cesoutilsquivousfontgagnerdutemps-160318071738]\nLes outils présentés dans le support de présentation sont les suivants :\nLe rechargement à chaud : JRebel, Spring Loaded, HotSwap Agent / DCEVM Expressions régulières : 2 applications web en ligne Console et ligne de commande : Cmdr, le terminal IntelliJ et le plugin EasyShell pour Eclipse Décompilateur Java : JD-GUI Presse papiers : Ditto Client REST : Postman Client SSH : Putty Connection Manager ","link":"https://javaetmoi.com/2016/03/outils-pour-developpeurs-java/","section":"posts","tags":["cmdr","dcevm","ditto","easyshell","eclipse","hotswap-agent","intellij","jd-gui","jrebel","postman","putty","spring-loaded"],"title":"Ces outils qui nous font gagner du temps"},{"body":"","link":"https://javaetmoi.com/tags/cmdr/","section":"tags","tags":null,"title":"Cmdr"},{"body":"","link":"https://javaetmoi.com/tags/dcevm/","section":"tags","tags":null,"title":"Dcevm"},{"body":"","link":"https://javaetmoi.com/tags/ditto/","section":"tags","tags":null,"title":"Ditto"},{"body":"","link":"https://javaetmoi.com/tags/easyshell/","section":"tags","tags":null,"title":"Easyshell"},{"body":"","link":"https://javaetmoi.com/tags/eclipse/","section":"tags","tags":null,"title":"Eclipse"},{"body":"","link":"https://javaetmoi.com/tags/hotswap-agent/","section":"tags","tags":null,"title":"Hotswap-Agent"},{"body":"","link":"https://javaetmoi.com/tags/intellij/","section":"tags","tags":null,"title":"Intellij"},{"body":"","link":"https://javaetmoi.com/tags/jd-gui/","section":"tags","tags":null,"title":"Jd-Gui"},{"body":"","link":"https://javaetmoi.com/tags/jrebel/","section":"tags","tags":null,"title":"Jrebel"},{"body":"","link":"https://javaetmoi.com/tags/postman/","section":"tags","tags":null,"title":"Postman"},{"body":"","link":"https://javaetmoi.com/tags/putty/","section":"tags","tags":null,"title":"Putty"},{"body":"","link":"https://javaetmoi.com/tags/spring-loaded/","section":"tags","tags":null,"title":"Spring-Loaded"},{"body":"","link":"https://javaetmoi.com/tags/dbunit/","section":"tags","tags":null,"title":"Dbunit"},{"body":"","link":"https://javaetmoi.com/tags/mockito/","section":"tags","tags":null,"title":"Mockito"},{"body":"","link":"https://javaetmoi.com/tags/spring-test/","section":"tags","tags":null,"title":"Spring-Test"},{"body":"","link":"https://javaetmoi.com/tags/tdd/","section":"tags","tags":null,"title":"Tdd"},{"body":"Un récent 13-14 m’a donné l’occasion de partager ma vision des tests unitaires avec mes collègues, qu’ils soient développeurs Java ou chefs de projet.\nAu cours de cette présentation, j’ai essayé de répondre à des questions qui font régulièrement débat : « Qu’est-ce qu’un test unitaire ? A quoi çà sert ? Que dois-je tester ? ». Tester n’est pas facile. Cela demande un apprentissage. Heureusement, il existe des bonnes pratiques et des outils. J’y ai notamment présenté ceux faisant parti de notre stack technique : JUnit, Mockito, DbUnit et Spring Test.\nDes exemples de code ont illustré cette présentation dont voici librement le support :\n[slideshare id=58163908\u0026amp;doc=20160211-tester-unitairement-une-application-java-160211185545]\n","link":"https://javaetmoi.com/2016/02/tester-unitairement-application-java/","section":"posts","tags":["dbunit","junit","mockito","spring-test","tdd","test","unitils"],"title":"Tester unitairement une application Java"},{"body":"","link":"https://javaetmoi.com/tags/unitils/","section":"tags","tags":null,"title":"Unitils"},{"body":"","link":"https://javaetmoi.com/tags/certification/","section":"tags","tags":null,"title":"Certification"},{"body":"","link":"https://javaetmoi.com/tags/jdbc/","section":"tags","tags":null,"title":"Jdbc"},{"body":" Four years ago, I’ve published a first mock exam for the Spring Core 3.0 Certification. Encouraged by Michael and Alan, I’ve updated this free mock exam for the Spring Professional certification based on the Spring Core 4.2 course.\nAccording to the Core Spring 4.2 Certification Study Guide, 3 new topics have been added to the Spring Core 4.2 mock exam: REST, Microservices and Spring Cloud. They replace older topics: JMX, JMS and Remoting.\nTest Details\nThis mock exam is as close as possible to the real Spring Professional certification. You have 90 minutes to answer 50 multiple-choice questions. You must answer 38 questions correctly (76%) in order to pass the exam.\nRepartition of the questions by topics:\nContainer (13) Test (5) AOP (10) Data Access (5) Transactions (5) Spring MVC (3) Spring Security (3) Microservices (2) REST (2) Spring Cloud(2) Test Availability You have the choice between two formats:\nA PDF version of the Spring Core 4.2 mock exam: spring-certification-4_2-mock-exam-antoine.pdf Online test on GoConqr: www.goconqr.com/en-US/p/4316687-Core-Spring-4-2-Certification-Mock-Exam-quizzes Please take the test and give me your feedback by blog comments.\n","link":"https://javaetmoi.com/2016/01/spring-core-4-2-certification-mock-exam/","section":"posts","tags":["aop","certification","hibernate","jdbc","jpa","microservices","rest","spring-boot","spring-cloud","spring-framework","spring-mvc","spring-security","test","transaction"],"title":"Spring Core 4.2 Certification Mock Exam"},{"body":"Chaque jour, de nombreux développeurs utilisent le framework Spring pour l’injection de dépendances et la gestion des transactions. Majeures, ces 2 fonctionnalités ne nécessitent pas un gros effort d’apprentissage. Pour autant, leurs mises en œuvre par le framework est complexe. Par curiosité intellectuelle, mais également afin d’éviter certains pièges et de profiter pleinement des capacités de Spring, il est intéressant de comprendre les mécanismes internes du framework qu’on utilise au quotidien : cycle de vie d’un bean, proxy, intercepteur, post-processeur, fabrique de beans, hiérarchie de contextes, portée … Les slides de cette présentation ont pour objectif de vous les introduire.\nLes dessous du framework spring par Antoine Rey\nSujets abordés :\nPost-processeurs et fabriques de beans Spring Intercepteur transactionnel et pièges de l’annotation @Transactional Lever des ambiguïtés lors de l’injection de beans Injection de beans de portées différentes Hiérarchie de contextes Spring Créer sa propre annotation Architecture pluggable Accès au contexte Spring ","link":"https://javaetmoi.com/2015/12/les-dessous-du-framework-spring/","section":"posts","tags":["spring-framework"],"title":"Les dessous du Framework Spring"},{"body":"","link":"https://javaetmoi.com/tags/editorconfig/","section":"tags","tags":null,"title":"Editorconfig"},{"body":" Lors du démarrage d’un projet sur lequel plusieurs développeurs vont être amenés à travailler, se pose très tôt la question des styles et règles de formatage à appliquer au code. En effet, afin de pouvoir comparer l’historique des révisions d’un fichier, une bonne pratique veut que l’on ne change pas les règles de formatage au cours de route. Si tel était le cas, les changements importants seraient noyés par les changements d’indentations et autres retours à la ligne. Parmi les normes de développements d’une entreprise ou d’un projet Open Source, un chapitre couvre généralement les règles de formatage. C’est par exemple le cas du guide de style de code du projet Spring Framework. Ces règles peuvent également être définies au sein d’un outil de qualimétrie comme SonarQube. Chaque violation de règle entraine alors un défaut. Ce billet propose 2 solutions permettant à des développeurs IntelliJ, Spring Tools Suite (STS) et Eclipse de travailler ensemble.\nLe formateur Eclipse Nativement, Eclipse embarque un formateur de code source Java bien connu des développeurs. Afin de pouvoir partager des règles de formatage, il propose des fonctionnalités d’import / export. Au format XML, le fichier décrivant les règles de formatage est très répandu. Par exemple, le projet Spring AMQP fournit le fichier eclipse-code-formatter.xml. Ces règles peuvent être importés dans STS mais également dans IntelliJ IDEA via le plugin Eclipse Code Formater. Afin qu’aucune différence ne puisse être constaté entre du code formaté sous Eclipse et sous IntelliJ, ce plugin embarque directement le code du formateur d’Eclipse. Il existe un plugin similaire pour Netbeans, mais je ne l’ai jamais testé.\nLa principale limite du plugin Eclipse Code Formater réside dans le nombre de langages supportés : Java, GWT et JavaScript. Autrement dit, vos fichiers XML, pages JSP, HTML, et autres feuilles de styles CSS ne seront pas pris en compte. Le plugin est dépendant des fonctionnalités d’export d’Eclipse. Par exemple, il n’est actuellement pas possible d’exporter les règles de formatage du XML.\nEditorConfig J’ai découvert l’outil EditorConfig suite à une pull request réalisée sur le projet Spring Petclinic. Cet outil a fait ses preuves puisque de nombreux projets Open Source l’ont adopté : AngularJS, Jenkins, Bootstrap, Wordpress …\nLa configuration d’EditorConfig est bien plus restreinte que celle du formateur d’Eclipse : seulement 8 paramètres dont le style d’indentation (espace ou tabulation), le nombre d’indentations, ou bien la suppression des espaces superflus en fin de ligne. Là où il se distingue, c’est de pouvoir fixer l’encodage des fichiers et le type de retour à la ligne (Unix, Windows). Sa configuration est simple. Voici le fichier .editorconfig mis en place sur Petclinic :\n# Configuration racine pouvant être affinée pour chaque sous-répertoire root = true # Section de configuration valable pour tous les types de fichiers [*] # Encodage de des fichiers charset = utf-8 # Fin de ligne à la Linux end_of_line = lf # Insère une ligne à la fin des fichiers insert_final_newline = true # Caractère espace pour indentater indent_style = space # Section de configuration spécifique aux classes Java et aux fichiers XML [*.{java,xml}] # Nombre d’espaces d’indentation indent_size = 4 # Suppression des espaces de fin trim_trailing_whitespace = true Le principal avantage d’EditorConfig est le nombre d’éditeur de code supporté. Pas moins de 27. Certains nativement comme IntelliJ, WebStorm ou GitHub. Pour Eclipse, Netbeans ou encore Notepadd++, l’installation d’un plugin sera nécessaire.\nConclusion Ce modeste billet vous aura montré 2 moyens de partager la configuration de vos formateurs, et ceci quel que soit votre IDE Java. Pour des besoins simples, sur des projets multi-technos, EditorConfig est un bon candidat. Il convient également aux développeurs détestant que leur code soit formaté. Pour une application d’Entreprise centrée sur Java, l’utilisation du formateur Eclipse reste à mon avis le meilleur choix.\n","link":"https://javaetmoi.com/2015/12/formatez-votre-code-editorconfig/","section":"posts","tags":["eclipse","editorconfig","intellij"],"title":"Formatez votre code"},{"body":"","link":"https://javaetmoi.com/tags/kibana/","section":"tags","tags":null,"title":"Kibana"},{"body":"","link":"https://javaetmoi.com/tags/spark/","section":"tags","tags":null,"title":"Spark"},{"body":"Sur les 12 représentations mondiales, la 3ième date de la tournée européenne de l a conférence Elastic{ON} a eu lieu le 5 novembre 2015 à Paris.\nInvité par la société Adelean, j’ai pu y participé. Pour toutes celles et ceux qui n’ont pas eu cette chance, ce billet me permet de vous faire partager cette journée.\nPlongée dans le produit et la roadmap Keynote de Shay Banon Créateur du moteur de recherche Elasticsearch, Shay Banon a tout naturellement ouvert cette journée. Ce fut pour lui l’occasion de retracer la genèse de son bébé. L’histoire d’Elasticsearch a commencé il y’a 15 ans par une application de cuisine baptisée iCook et que Shay avait développé pour sa femme. Basée sur Spring, Hibernate et Eclipse RCP, la fonctionnalité centrale était la barre de recherche positionnée sur la page d’accueil. Shay a très vite compris que le SQL n’était pas adapté à la recherche full text. Il a donc adapté l’architecture pour utiliser Apache Lucene. Voyant que son API de haut niveau pouvait adresser d’autres cas d’utilisation que la cuisine, il l’a open sourcé sous le nom de Compass. Pour un utilisateur, Shay rappelle que le search doit être rapide. C’est d’ailleurs la fierté de Google qui affiche le temps d’exécution de ses requêtes. Le temps a passé. Il y’a 5 ou 6 ans, le volume de données à indexer a considérablement augmenté. Shay a été confronté à de nouvelles problématiques pour distribuer les données et gérer les pannes. Il a alors initié un nouveau projet. Après 5 mois de développement, Shay l’a open-sourcé sous le nom d’ Elasticsearch (ES). Très vite, l’adoption d’ES a dépassé ses espérances. Sa popularité a explosé. Un éco-système s’est construit autour. Accompagné de Steven Schuurman et Simon Willnauer, Shay décide alors de monter la société Elasticsearch qui a récemment été rebaptisé en Elastic.\nS’ensuit alors quelques chiffres :\nCommunauté de 35 000 membres 120 groupes utilisateurs répartis dans 80 villes 32 000 commits réalisés sur la stack Elasticsearch-Logstash-Kibana (ELK) 35 000 000 téléchargements En France, David Pilato a grandement contribué au succès d’ES. Shay a été impressionné par tout ce qu’il a fait sur ES et l’a rapidement embauché. Aujourd’hui, ES est composé d’ingénieurs venant de tout horizon, dont quelques français (j’ai d’ailleurs recroisé à Elastic{On} Tanguy Leroux). Des besoins ont très vites émergés autour Elasticsearch. La visualisation des données a été permise par Kibana. L’indexation de données a été facilitée Logstash. Et, plus récemment, Packedbeats a rendu possible la capture de trafic réseau. Ces 3 projets ont intégré Elastic Aujourd’hui, la Core Elastic Stack est composée des 4 produits suivants :\nUI: Kibana Store, Index, Analyze: Elasticsearch Ingest: Logstah, Beats A cette stack s’ajoute des extensions, parmi lesquels des produits commerciaux comme Marvel.\nLa keynote se termine par une liste de sociétés utilisatrices de solutions Elastic et dont la notoriété n’est plus à démonter :\nWikimedia: Elasticsearch est la colonne vertébrale de Wikipedia Mozilla: afin de détecter des menaces en termes de sécurité, la stack ELK permet d’analyser en temps réel 300 millions d’évènements par jour. NASA: 30 000 messages et 100 000 documents envoyés 4 fois par jour par la sonde Mars Rover afin d’optimiser sa mission Verizon: 500 bilions de documents pour analyse temps réel des logs Bien connus des développeurs, des acteurs comme GitHub ou Stackoverflow n’ont pas été cités.\nElasticsearch 2 by Climton Gormley Team Leader d’Elasticsearch, Climton Gormley a un background de développeur Perl. C’est l’un des tout premiers utilisateur d’Elasticsearch qui a contribué au développement de la communauté sur IRC et la mailing list.\nCimton commence son intervention par rappeler ce qu’est ES. C’est avant tout une couche d’abstraction permettant de distribuer les indexations et les requêtes sur plusieurs index Lucene. ES doit détecter les pannes. L’idée est de pouvoir utiliser ES de la même façon, que ce soit en local sur un poste de développement ou sur un cluster de 500 nœuds. Dès le départ, un gros effort a été réalisé sur l’API, ceci afin qu’elle soit facile à utiliser pour les développeurs, quel que soit leur langage de programmation. Un peu plus tard dans la journée, un développeur Java me confirmait qu’il donnait 5 étoiles à leur API. La recherche et l’analyse temps-réel est également une fonctionnalité centrale d’ES. Les agrégations ont été créées dans ce sens. Climton enchaine sur les nouveautés apportées par Elasticsearch 2.0 sorti la semaine précédente :\nAmélioration de la gestion des nœuds défaillants et du temps de restauration. Ecritures durables: lors de l’indexation d’un document, ES garantie dorénavant qu’un fsync est réalisé sur le disque. Utilisation du Java Security Manager pour empêcher un hacker d’exploiter des failles de sécurité. Afin de diminuer le trafic réseau sur de gros clusters, les états des shards sont envoyés sous forme de deltas. Meilleure compression des index par utilisation des nouveautés apportées par Lucene 5.0. Réduction de l’ usage de la Heap au profit de l’accès direct à la mémoire (off-Heap). Introduction des doc-values. Leur mise en cache est géré par le cache du filesytem (bien mieux que ne peut le faire la JVM). Gains en performance pour une Heap réduite. La mise en cache automatique et le merge des segments sont plus intelligents. Simplification du Query DSL en supprimant les filter au profit des query. Le pipeline d’agrégation est une fonctionnalité majeure apportée par Elasticsearch 2.0. Adrien Grand est monté sur scène nous en parler. Successeurs des facettes, Elasticsearch 1.0 avait introduit les agrégations. Les pipelines d’agrégations d’Elasticsearch 2.0 permettent d’agréger des données agrégées. Autrement dit, ils permettent de combiner le résultat de plusieurs agrégations (ex : comparer 2 moyennes). Plusieurs opérations sont possibles : dérivé, somme cumulative, moyenne mouvante, min/mx/agv/sum …\nClimton termine son talk par la roadmap d’Elasticsearch :\nRéécriture complète de la géo-localisation : moins d’espace sur disque et amélioration de la vitesse. Query Profiling: permet de mieux diagnostiquer la cause d’une requête lente. Graphiquement, un camembert permet de localiser les lenteurs. Administration : API pour réindexer : besoin récurrent de réindexer les données. Change le mappings, reindex les données … en tâche de fond. API de gestion de tâches: permet de suivre et d’interagir avec les traitements réalisés par ES en arrière plan (ex : réindexation) Enrichissements \u0026amp; Computations Simplifier le pipeline alimentation de l’index Nouveau langage de script. Propriétaire, il a été pensé pour fiabiliser le cluster. Kibana 4 par Boaz Leskes Software Developer chez Elastic, Boaz précise que Kibana 4 est une réécriture complète de Kibana 3. Regroupées par thèmes, en voici les nouveautés :\nPowefull Analytics Ajout du support des agrégations Nouvelles fonctions d’analyse Robust Deployment Kibana est packagé avec un server backend (node.JS). Cela permet de ne plus avoir à exposer son cluster Elasticsearch sur Internet. Niveau de sécurité affiné Flexible Architecture Design modulaire Framework front-end moderne Diagrammes rendus avec le framework D3 Personnalisation Carte personnalisable Formateurs de champs Field formatters Administration Page de status du serveur Kibana Niveau de logs configurable Ingest par Shay Banon Par le terme « Ingest», on entend extraire des données et les pousser dans ES.\nLa force de Logstash 1 venait du nombre de plugins existants, permettant de gérer de nombreux use cases. En effet, plus de 200 plugins existent. Et de nouveaux sortent régulièrement : Kafka, JDBC, HTTP et Salesforce.\nSorti le 28 octobre 2015, Logstash 2.0 apporte les nouveautés suivantes :\nPerformance et résilience Consistance de shutdown entre les plugins Performances multipliées par 3 sur les plugins grok, useragent, geoip et JSON. Logstash 1 n’utilisait qu’un seul cœur. La version 2 profite désormais de toute la puissance du serveur. Quelques annonces ont ensuite été réalisées au sujet des futures fonctionnalités de Logstash :\nUtilisation de files (queue) Persistantes A taille variables Avec gestion d’une Dead Letter Queue Gestion et securité Health monitoring Gestion centralisée Installation de plugins en offline Shay rappelle la dualité de Logstash : être à la fois très léger en tant qu’agent (prendre le moins de CPU) et consommer un maximum de ressources en tant que serveur. Désormais, Logstash sera cantonné à la partie serveur. Son remplaçant côté client est Beats, une API très légère permettant de collecter des données.\nPlusieurs projets bâtis au dessus de Beats existent dans le GitHub elastic :\nPacketbeat : un analyseur réseau supportant les protocoles HTTP, MySQL, PostgreSQL (et bientôt ICMP et AMQP). Topbeat : index l’utilisation processeur des processus d’un OS (Windows, Mac OSX, Linux sont supportés). Filebeat : envoi de fichiers (ex : logs) remplaçant logstash-forwarder. A l’instar de Metricsbeat, de nouveaux projets basés sur Beats verront le jour.\nExtensions par Steve Mayzak Steve se présente comme Solutions Architect Team Lead. Au cours de sa session, il nous a présenté différentes extensions d’Elasticsearch. Produits commerciaux, ils sont offerts lors de la souscription d’un support ES.\nUn besoin récurrent des clients étaient de sécuriser leurs données. C’est chose faite avec Shield :\nProtection par login / mot de passe Restrictions au niveau des documents et des champs S’interface aux solutions SSO des entreprises ES accepte désormais le login des utilisateurs Prochainement, Shield améliorera son intégration avec une API de configuration et le support de Kibana.\nWatcher est une extension permettant de gérer des alertes et dres notifications :\nMise en place d’alertes liées à vos données Notifications flexibles Marvel permet de monitorer, diagnostiquer et optimiser un cluster ES. Changements de Marvel 2.0 :\nRedesigné afin de profiter de Kibana 4 Plus simple d’utilisation Future de Marvel : monitoring de Logstash, Kibana, Beats\nDe prochaines extensions sont dors et déjà inscrites à la roadmap d’Elastic :\nAPI et IHM de graphe Réplications de cluster sur plusieurs data center (ex d’usage : Disaster Recovery) Utilisation d’ES pour du Machine learning Found : Elasticsearch as a Service par Morten Ingrebrigsten Found est présenté comme le seul service complet hébergeant les produits Elastic. Gage de qualité pour les DSI, son support est assuré par l’équipe de développement ELK. Basé sur Docker, 2 offres sont disponibles : Standard et Premium. L’offre Found on Premise package toute la stack ELPK pour un déploiement sur un Cloud privé ou public.\nElasticsearch for Apache Hadoop Ingénieur Elastic, Costin Lea est venu nous parler du produit Elasticsearch for Apache Hadoop. Costin commence par nous rappeler que l’écosystème Hadoop est très vaste: Hive, Storm, HDFS, Cloudera, Hortonworks.\nL’intégration d’Hadoop et d’ES a été pensée dans les 2 sens :\nES =\u0026gt; Hadoop : backup des données sur HDSF + requête ES depuis Hadoop Hadoop =\u0026gt; ES : indexer les données directement dans ES Depuis ses prémices, Hadoop a beaucoup évolué.\nHadoop 0.20.x/1.x :\nstorage (HSFS) et framework Map / Reduce pour extraire et traiter les données de manière distribuée Hadoop est alors un produit difficile à prendre en main. Cascading et Hive ont été crées pour en simplifier l’usage.\nHadoop 2.x:\nEntre HDFS et Map / Reduce, l’introduction de la couche YARN a permis d’ouvrir Hadoop à d’autres traitements que le Map / Reduce.\nES-Hadoop est certifié pour fonctionner avec les principales distribution d’Hadoop : Cloudera, Hortonworks, Mapr, Concurrent, Databriks.\nCostin continue son talk par un slide présentant un exemple d’intégration de Spark et d’ES avec Scala. Une initiation à Spark était nécessaire pour le comprendre. Ce que j’en ai retenu est que la classe SparkContext (package org.elasticsearch.spark) d’ES-Spark apporte 2 méthodes :\nesRDD pour charger un RDD depuis ES saveToEs pour écrire un RDD dans ES Le RDD ES supporte Spark SQL. Au runtime, le Spark SQK est converti en Query DSL ES. Enfin, aucun setup n’est nécessaire pour faire fonctionner le code donné en exemple (pas de settings particulier d’ES).\nLa Roadmap d’ES-Hadoop est la suivante :\nSupport des agrégations Intégration avec Marvel Machine Learning : utilisation dans Spark d’informations connues. Les workers Hadoop ne communiquant pas ensemble, le partage de données permettra l’alléger la charge globale de travail Retours d’expérience L’après-midi a été consacrée à différents retours d’expérience de mise en place d’Elasticsearch dans de grandes entreprises : Orange, ERDF, Natixis, PSA et AXA.\nHow Orange is moving its French web search engine to Elastic products Depuis 1996, Orange propose sur son portail un moteur de recherche destiné aux Internautes francophones. Le moteur propose des recherches thématiques. Derrière chaque recherche thématique, il y’a une technologie propriétaire différente. Depuis 2013, Orange migre peu à peu tous ces moteurs thématiques vers Elasticsearch. Le 1er moteur thématique a été mis en prod en 2014. Début 2016, les 1,2 milliards d’URL analysées par Orange le seront par Elasticsearch.\nDès le crawl des pages web, un indicateur de qualité est calculé à partir de l’URL. Techniquement, Orange maintient un fichier contenant des expressions régulières (regex) permettant de détecter si une page est en français, contient des spams … Les regex permettent de tagger des URL avec des labels. A chaque label correspond un score. Les URL sont consultables dans Kibana. La pertinence du résultat ne se limite pas au TF/IDF d’ES. Orange effectue des calculs de graphes.\nL’architecture technique retenue par Orange est la suivante :\nArchitecture dockerisée 6 clusters ES gérant chacun 200 millions d’URL Chaque cluster est formé de 13 machines Répartition des nœuds d’un cluster : 3 nœuds clients et 10 nœuds data Les performances sur ES 1.5 ont été obtenues de manière empirique en effectuant plusieurs essais avec des paramétrages différents. Voici les valeurs remarquables retenues :\nFilter cache size : 20%, 30%, 60% Nombre de machines : 10 (mieux que 20). L’ajout de machines engendrait de l’overhead pour agréger les résultats. Nombre d’ processor: pas réglé (ne sait pas si ce paramétrage est lié à l’usage de Docker ou non ?) Parser JSON : librairie RapidJSON (C++) Nombre de shards : 20. Deux shards par machines ont permis une utilisation efficiente du CPU. Nombre de réplica : 0 (ne sert à rien de répliquer dans leur cas) Volume de RAM : 48 Go. Le passage de 8 à 48 a augmenté les performances, sans les doubler. Possibilité de redescendre à 8 Go si besoin de libérer des ressources matérielles. Agrégation en 2 requêtes : la 1ière pour remonter les ID des documents, et la 2ième pour récupérer les champs souhaités Les 3 nœuds clients permettent de déterminer sur quel nœud data les documents doivent être indexés. Jean-Pierre Paris explique que la montée de version vers Elasticsearch 2 nécessitera de revoir ce paramétrage et de refaire des benchs. Afin d’être exploitable, la collecte des résultats des tests demande de la rigueur. Orange a spécifiquement développé un outil pour ses benchs. Ils comptent obtenir un Licence Agreement auprès Elastic afin de pouvoir commiter sur GitHub (CCLA).\nPour finir, voici quelques chiffres sur l’indexation :\n1,2 milliards de documents à indexer 4,2 To d’index Durée d’indexation de 2h45 (avec leur ancien système, c’était de 10 ou 12h) Centralisation de grands volumes de logs chez ErDF Architecte chez ErDF, Vladislav Pernin présente l’architecture basée sur la stack ELK, Kafka et Ansible retenue pour gérer ses logs. Ses slides sont disponibles sur Speaker Deck.\nContexte :\nBeaucoup de serveurs Architecture distribuée \u0026amp; asynchrone Accès aux logs limités Cas d’utilisation :\nSurveiller les environnements : de dév à la prod Bouton pour créer des anomalies sur Jira avec tout le contexte du log Pattern de recherche de bug. Exemple : depuis quand cette exception est-elle apparue ? Audit pro-actif (enchainement de logs) Tracer le chemin d’un utilisateur Statistiques métiers : taux d’utilisation des fonctionnalités Surveillance des 35 millions de compteurs électriques communiquant en cours de déploiement Quelques chiffres :\nEn production depuis 4 ans. 8 projets, 50 environnements, 900 serveurs 65 types de log 1,1 milliards de doc \u0026amp; 290 Go d’espace disque 250 logs / secondes Vladislav poursuit son intervention par un retour d’expérience de chacune des solutions techniques mises en œuvre.\nLogstash\nEn 2011, le choix d’utiliser Logstash a été guidé par :\nSon tail intelligent Sa richesse fonctionnelle Son installation facile La facilité de créer un patch en attendant sa prise en compte par l’équipe de dév Vladislav donne 3 conseils sur groks :\nTester unitairement Tests de performances afin d’affiner les regex Multiplier les logs pour la scalabilité horizontale Problèmes rencontrés :\nEncore jeune et mouvant Perte de données possibles (queue non persistance) 40 secondes de démarrage Ligne de logs partielles lors de la journalisation des logs (sur Apache) et liée a problème des inodes Linux Kafka\nLes logs ont d’abord été transportés via RabbitMQ. Au dessus de 5000 logs/s, RabbitMQ ne supportait plus la charge. Le passage à Apache Kafka a permis d’atteindre le débit à 200 000 logs/s sans tuning. Malgré quelques défauts de jeunesse, Kafka est robuste, persistant et scalable. Désormais, les applications Java envoient directement leurs logs au format JSON (appender Kafka pour Logback. Kafka shadé pour éviter les conflits de version). Ainsi, elles n’ont plus besoin de logger sur le filesystem.\nElasticsearch\nSes points fort selon Vladislav :\nOutil mature Grande communauté Documentation de bonne qualité Installation facile Release fréquente, upgrade facilitée Lors de son choix en 2011, ES était balbutiant.\nQuelques problèmes\nSur versions anciennes : tri, indexes corrompus, OutOfMemory Recovery trop long River RabbitMQ remplacée par Lohstash en push direct Volumétrie fluctuante mais sharding fixe Aucun support natif de la Sécurité (Shield comme produit commercial) Prises-en compte du nouveau mapping lors de la rotation d’index API query :\nPuissante mais compliquée Jointures impossibles pour les développeurs venant du monde SQL. Un document ES doit en effet être auto-suffisant. Mapping :\nNécessite de connaître le type de recherche pour définir le mapping Bonnes performances sur SAN et VM.\nEn 2011, l’IHM de Logstash ne fonctionnait pas et celle de Kibana était non utilisable. ErDF a donc développé sa propre IHM propriétaire. Aujourd’hui, ils partiraient sur du Kibana\nTravail à réaliser sur la qualité des logs :\nUniformisation de la structure Utilisation d’un ID de corrélation : permet de suivre les appels d’un système à l’autre Enrichissement : environnement, projet, application Leur déploiement est automatisé avec Ansible. ErDF a fait le choix de groker sur le serveur plutôt que sur les clients.\nPour superviser cette architecture, ils génèrent un log toutes les 5 secondes sur chaque machine puis vérifient sa présence dans ES.\nA l’avenir, la volumétrie va être x 100.\nComment Natixis Financement a enrichi sa vision client avec Elasticsearch Natixis est la banque de financements du Groupe Banque Populaire Caisse d’Epargne. Elle vend des prêts personnels et du crédit revolving. Cette entité compte aujourd’hui 6 millions de clients.\nPendant le développement d’un poste de travail pour les chargés de clientèle, ils ont eu besoin d’agréger des données (interactions avec les clients) issues d’une dizaine d’applications. Les données sont reçues depuis différents canaux : SMS, site Internet, appels téléphoniques … Et leur collecte est réalisée à la fois par batch (intégration de fichiers CSV) et en temps réel (WS REST). Leur exigence fonctionnelle était de pouvoir y accéder à la fois par Web Service et par une IHM de recherche multi-critères intégrée au poste de travail. Le projet a été réalisé en 4 mois (de mars à juin 2015).\nQuelques chiffres :\n350 utilisateurs 10 critères de recherche : identifiant client, agence, date, canal de contact, motif de l’interaction, nom du client … Temps de réponse \u0026lt; 3 secondes Volume de données croissant : plusieurs dizaines de millions à l’horizon 2017 Aucune donnée confidentielle n’est stockée dans ES.\nKibana l’efficience à PSA Architecte Big Data à PSA Peugeot Citroën, Alexandre Fricker débute son intervention par nous confier que PSA n’autorise l’utilisation de composants Open Source que depuis 2006. En 2015, leur stack de développement a été open sourcé. De manière confidentielle, Elasticsearch est utilisé depuis 2011 pour la gestion des logs. En 2013, il a réalisé une démo d’Elasticsearch à 4 BU. Suite à sa démo, 20 projets ES été développés en 2014. L’utilisation d’Es couvre plusieurs domaines : logs centralisés, mesure de la qualité de service, géolocalisation par IP (identifier des tentatives de déni de services). En 2015, pas moins de 75 projets sont menés avec ES. Son usage se diversifie encore davantage : maintenance de la chaine de production de l’usine de Poissy, analyse des pannes. Une présentation d’Elasticsearch est faite au DSI et à la Direction Industrielle. En 2016-2017, est prévue la systématisation des usages démontrés en 2015. De nouveaux usages sont encore possibles :\nTraitements des logs de sécurité Indexation et recherche documentaire Big Data : données venant du véhicule connecté Métrologie davantage plus réelle GTC, monde industriel et logistique L’architecture technique est simple. Tous les projets sont hébergés sur un cluster 8 nœuds. 1 index est créé par projet.\nPanel de discussion avec Axa Fabien Janssens, IT Solution Design chez Axa, conclue cette journée par une intervention sous forme de questions / réponses. Il rappelle que la donnée est au cœur du business d’Axa. Leurs données centrales sont leurs clients et leurs contrats. Le SI d’AXA souffre d’un gros problème de technologies legacy liées aux fusions / acquisitions successives. En 2013, Axa crée un Data Innovation Lab. Son premier projet a été la création d’un Data Lake. Toutes les données du SI y sont conservées à tout jamais, sans traitement. Ce lac peut être vu comme un disque géant. L’historique des données est conservé.\nApache Spark est utilisé pour réaliser des traitements et faire parler des données. La détection de fraude et le marketing sont 2 cas d’usage parmi d’autres. Un Data Lake est crée pour chaque catégorie de données. Leur premier Data Lake fut consacré aux clients. Axa a modélisé un document JSON par client. Ce document est enrichi avec les nouvelles sources de données connectées au Data Lake. Au total, 3 millions de clients sont référencés. Et tous les champs sont indexés. Le scoring réalisé par Spark est remonté dans ES. Techniquement, le cluster ES tourne avec 3 nœuds. Le prochain Data Lake sera dédié aux produits.\n","link":"https://javaetmoi.com/2015/11/tout-sur-elastic-on-tour-paris-2015/","section":"posts","tags":["bigdata","elasticsearch","kibana","logstash","spark"],"title":"Tout sur le Elastic{ON} Tour Paris 2015"},{"body":" Lorsqu’on développe dans son coin une démo basée sur une nouvelle techno, il est fréquent d’avoir besoin de données de tests. Soit on se les construit à la main, soit on en récupère sur Internet. Le mouvement Open Data et les API mises à disposition par les grands du Web permettent de récupérer des données en temps réel. Dans les conférences, nombre de démos live utilisent les API de Twitter ou de Github. Ces données sont généralement formatées en JSON. Une connexion réseau est alors nécessaire.\nDans le cadre d’une série d’articles sur Elasticsearch et AngularJS, j’ai eu le besoin d’indexer des données de manière offline. Cherchant une source de donnée musicale, j’ai opté pour MusicBrainz qui, à l’instar d’IMDb pour le cinéma, est une plateforme ouverte collectant des méta-données sur les artistes, leurs albums et leurs chansons puis les mettant à disposition du publique. Cette plateforme est composée d’une base de données relationnelles et d’une interface web permettant d’effectuer des recherches, de consulter les données et de participer à l’enrichissement de la base. Last.fm, The Guardian ou bien encore la BBC s’interfacent avec MusicBrainz.\nDans l’article Elastifiez la base MusicBrainz sur OpenShift, je proposais 2 méthodes pour installer la base de données : récupérer une VM ou un dump de la base PostgreSQL. Dans les 2 cas, la procédure d’installation demandait une intervention humaine. Ce billet vous en propose une 3ième : automatiser l’installation de base de données à l’aide de Docker. Après quelques lignes de commande et un peu de patience le temps de l’import du dump PostgreSQL, vous pourrez vous connecter localement à la base musicale contenant des données à jour.\nL’image arey/musicbrainz-database Basée sur l’ image officielle de postgres, l’image Docker arey/musicbrainz-database installe la base de données PostgreSQL 9.4 ainsi que toutes les librairies nécessaires au fonctionnement de la base de données MusicBrainz (ex : postgresql-server-dev-9.4, postgresql-musicbrainz-unaccent).\nCette image vient avec le script shell create-database.sh utilisé pour créer la structure de données et importer le dump de la base de données MusicBrainz. Les étapes le décomposant sont les suivantes :\nCrée le schéma musicbrainz Crée les tables à partir des scripts DDL présent sur le GitHub de MusicBrainz Télécharge le dump de la base de données Importe le dump après l’avoir téléchargé par FTP Ajoute les index et les clés primaires Le code source de l’image arey/musicbrainz est disponible sur GitHub.\nLignes de commande Deux lignes de commandes sont nécessaires pour obtenir une base de données alimentées et accessibles depuis n’importe quel client SQL :\ndocker run -t -d -p 5432:5432 --name musicbrainz-database -e POSTGRES_USER=musicbrainz -e POSTGRES_PASSWORD=musicbrainz arey/musicbrainz-database docker run -it --link musicbrainz-database:postgresql -e POSTGRES_USER=musicbrainz -e POSTGRES_PASSWORD=musicbrainz --rm arey/musicbrainz-database /create-database.sh Pour tester l’installation de la base, exécuter la requête SQL suivante comptant le nombre d’artistes référencés :\ndocker run -it --link musicbrainz-database:postgresql --rm arey/musicbrainz-database sh -c \u0026#39;exec psql -h postgresql -d musicbrainz -U musicbrainz -a -c \u0026#34;SELECT COUNT(*) FROM artist\u0026#34;\u0026#39; Lorsque le client psql demande de saisir un mot de passe, sairi ‘musicbrainz’. Vous obtiendrez la sortie suivante :\nPassword for user musicbrainz: SELECT COUNT(*) FROM artist count -------- 995899 (1 row) Depuis une application Java, la chaine de connexion JDBC à utiliser est la suivante : jdbc:postgresql://localhost:5432/musicbrainz Login et mot de passe sont identiques : musicbrainz / musicbrainz Pour les utilisateurs Windows ou MacOSX utilisant boot2docker, il est nécessaire de remplacer localhost par l’IP donnée par la commande boot2docker ip.\nSous le capot Avant de construite ma propre image Docker, j’ai étudié les images Docker existantes telles que rickatnight11/docker_musicbrainz et jsturgis/musicbrainz-docker. Elles installent un serveur MusicBrainz complet avec sa partie front. Je souhaitais une image plus légère centrée sur la base PostgreSQL. Cela dit, je mentirais en disant que je ne m’en suis pas inspiré. Vous trouverez ci-dessous le fichier Dockerfile et le script de création de la base.\nDockerfile\nFROM postgres:9.4 RUN apt-get update RUN DEBIAN_FRONTEND=noninteractive apt-get -y -q install git-core build-essential libxml2-dev libpq-dev libexpat1-dev libdb-dev libicu-dev postgresql-server-dev-9.4 wget RUN git clone https://github.com/metabrainz/postgresql-musicbrainz-unaccent.git \u0026amp;\u0026amp; git clone https://github.com/metabrainz/postgresql-musicbrainz-collate.git RUN cd postgresql-musicbrainz-unaccent \u0026amp;\u0026amp; make \u0026amp;\u0026amp; make install \u0026amp;\u0026amp; cd ../postgresql-musicbrainz-collate \u0026amp;\u0026amp; make \u0026amp;\u0026amp; make install \u0026amp;\u0026amp; cd ../ RUN echo \u0026#34;listen_addresses=\u0026#39;*\u0026#39;\u0026#34; \u0026gt;\u0026gt; /var/lib/postgresql/data/postgresql.conf ADD create-database.sh /create-database.sh Script shell createdatabase.sh\n#!/bin/bash cd /tmp echo \u0026#34;Creating Musicbrainz database structure\u0026#34; echo \u0026#34;postgresql:5432:musicbrainz:$POSTGRES_USER:$POSTGRES_PASSWORD\u0026#34; \u0026gt; ~/.pgpass chmod 0600 ~/.pgpass psql -h postgresql -d musicbrainz -U $POSTGRES_USER -a -c \u0026#34;CREATE SCHEMA musicbrainz\u0026#34; wget https://raw.githubusercontent.com/metabrainz/musicbrainz-server/master/admin/sql/Extensions.sql psql -h postgresql -d musicbrainz -U $POSTGRES_USER -a -f Extensions.sql rm Extensions.sql wget https://raw.githubusercontent.com/metabrainz/musicbrainz-server/master/admin/sql/CreateTables.sql psql -h postgresql -d musicbrainz -U $POSTGRES_USER -a -f CreateTables.sql rm CreateTables.sql echo \u0026#34;Downloading last Musicbrainz dump\u0026#34; wget -nd -nH -P /tmp http://ftp.musicbrainz.org/pub/musicbrainz/data/fullexport/LATEST LATEST=\u0026#34;$(cat /tmp/LATEST)\u0026#34; wget -nd -nH -P /tmp http://ftp.musicbrainz.org/pub/musicbrainz/data/fullexport/$LATEST/mbdump-derived.tar.bz2 wget -nd -nH -P /tmp http://ftp.musicbrainz.org/pub/musicbrainz/data/fullexport/$LATEST/mbdump.tar.bz2 echo \u0026#34;Uncompressing Musicbrainz dump\u0026#34; tar xjf /tmp/mbdump-derived.tar.bz2 rm mbdump-derived.tar.bz2 tar xjf /tmp/mbdump.tar.bz2 rm mbdump.tar.bz2 for f in mbdump/* do tablename=\u0026#34;${f:7}\u0026#34; echo \u0026#34;Importing $tablename table\u0026#34; echo \u0026#34;psql -h postgresql -d musicbrainz -U $POSTGRES_USER -a -c COPY $tablename FROM \u0026#39;/tmp/$f\u0026#39;\u0026#34; chmod a+rX /tmp/$f psql -h postgresql -d musicbrainz -U $POSTGRES_USER -a -c \u0026#34;\\COPY $tablename FROM \u0026#39;/tmp/$f\u0026#39;\u0026#34; done rm -rf mbdump echo \u0026#34;Creating Indexes and Primary Keys\u0026#34; wget https://raw.githubusercontent.com/metabrainz/musicbrainz-server/master/admin/sql/CreatePrimaryKeys.sql psql -h postgresql -d musicbrainz -U $POSTGRES_USER -a -f CreatePrimaryKeys.sql rm CreatePrimaryKeys.sql wget https://raw.githubusercontent.com/metabrainz/musicbrainz-server/master/admin/sql/CreateIndexes.sql psql -h postgresql -d musicbrainz -U $POSTGRES_USER -a -f CreateIndexes.sql rm CreateIndexes.sql Conclusion Le script shell de création de la base m’aura demandé plus d’efforts que le Dockerfile. Indépendant de tout script externe, il devrait être stable dans le temps. L’image devra par contre évoluer au fil du temps, par exemple lors de montée de version de PostgreSQL.\nL’image arey/musicbrainz-database peut être utilisée avec docker-compose. C’est désormais le cas sur le projet musicbrainz-elasticsearch qui l’utilise pour démarrer la base de données MusicBrainz et un cluster Elasticsearch.\n","link":"https://javaetmoi.com/2015/11/docker-file-database-musicbrainz/","section":"posts","tags":["docker","postresql"],"title":"Docker file de la database MusicBrainz"},{"body":"","link":"https://javaetmoi.com/tags/postresql/","section":"tags","tags":null,"title":"Postresql"},{"body":"","link":"https://javaetmoi.com/tags/el/","section":"tags","tags":null,"title":"El"},{"body":"Implémentation de référence de Bean Validation 1.1, Hibernate Validator 5.x requière une implémentation d\u0026rsquo;Unified Expression Language respectant la JSR-341 (correspond aux EL 2.2). EL 2.2 étant apparue avec Java EE 6, il n’est donc pas possible d’utiliser Hibernate Validator 5 dans un serveur d’application Java EE 5 et un conteneur de servlets 2.5. C’est pourquoi Hibernate Validator 5 ne fonctionne pas avec Tomcat 6.\nEssayer et vous tomberez au runtime sur l’exception suivante : NoSuchMethodError: javax.el.ExpressionFactory.newInstance()Ljavax/el/ExpressionFactory)\nComme indiqué dans la documentation, embarquer EL 2.2 dans votre WAR ne résout pas le problème et génère ce type d’erreur au runtime :\njava.lang.LinkageError: loader constraint violation: when resolving interface method \u0026#34;javax.servlet.jsp.JspApplicationContext.getExpressionFactory()Ljavax/el/ExpressionFactory;\u0026#34; the class loader (instance of org/apache/jasper/servlet/JasperLoader) of the current class, org/apache/jsp/index_jsp, and the class loader (instance of org/apache/catalina/loader/StandardClassLoader) for resolved class, javax/servlet/jsp/JspApplicationContext, have different Class objects for the type javax/el/ExpressionFactory used in the signature Pour profiter des avancées apportées par Bean Validation 1.1, comme par exemple l’ amélioration du formatage des messages d\u0026rsquo;erreurs des contraintes, vous avez le choix entre:\nEffectuer une montée de version de Tomcat ou de JBoss Utiliser une autre implémentation de Bean Validation 1.1 (personnellement je n’en connais pas) Patcher Hibernate Validator. C’est cette 3ième solution que je compte vous présenter.\nMise en oeuvre Pour être tout à fait exact, il ne faudra pas patcher qu’Hibernate Validator. L’API EL 2.2 et son implémentation devront également être patchées. En effet, afin de faire fonctionner de pair EL 2.1 (pour le conteneur de Servlet) et EL 2.2 (pour Hibernate Validator), le package des classes d’EL 2.2 doit être renommé. A cet effet, le package javax.el pourra par exemple être changé en com.javaetmoi.fork.javax.el. Récupérer le code source de javax.el 2.2.6 et javaxx.el-api 2.2.6 depuis le SVN de java.net ne pose guère de difficulté, construire le projet avec Maven non plus. Une fois les projets importés dans votre IDE, la fonction de refactoring de ce dernier s’occupera de modifier automatiquement les imports. Vous aurez à éditer les pom.xml et à changer le groupId ou l’artefactId afin de pouvoir dépendre des 2 versions d’EL. Builder les projets et déployer les dans votre repos local ou votre repo d’entreprise.\nPatcher Hibernate Validator n’est guère plus compliqué. Commencer par forker ou cloner son repo Github. Ensuite, renommer en masse de tous les imports vers le package javax.el.\nLe contenu de 2 classes devra être changé manuellement :\nResourceBundleMessageInterpolator\nResourceBundleMessageInterpolator.class.getClassLoader().loadClass( \u0026#34;com.javaetmoi.fork.javax.el.ExpressionFactory\u0026#34; ); ELIgnoringClassLoader\nprivate final String EL_PACKAGE_PREFIX = \u0026#34;com.javaetmoi.fork.javax.el\u0026#34;; Editer le pom.xml. Afin de ne pas confondre ce fork avec l’originalhanger le groupId, l’artefactId et/ou le numéro de version du module hibernate-validator. Ajouter les dépendances vers l’API et l’implémentation de EL 2.1. Construire le JAR et le déployer. Le tour est joué.\nConclusion En une petite heure, vous aurez réussi à faire en sorte qu’Hibernate Validator 5.x soit compatible avec votre conteneur de servlet 2.5 (ou votre serveur d’application JEE 5). En soit, les adaptations ne sont ni compliquées ni risquées. Se posera la question de devoir les réappliquer lorsque vous voulez monter de version Hibernate Validator ou EL. Un fichier patch ou un fork sous Github permettront de faciliter les mises à jour. Lors d’un passage à JEE 6 ou à un conteneur de servlet compatible Servlet 3.x, ces versions personnalisés d’EL et d’Hibernate Validator pourront être remplacées peur l’original.\n","link":"https://javaetmoi.com/2015/10/hibernate-validator-5-sur-conteneur-servlet-2-5/","section":"posts","tags":["el","hibernate-validator"],"title":"Hibernate Validator 5 sur un conteneur de Servlet 2.5"},{"body":"","link":"https://javaetmoi.com/tags/hibernate-validator/","section":"tags","tags":null,"title":"Hibernate-Validator"},{"body":"","link":"https://javaetmoi.com/tags/benchmark/","section":"tags","tags":null,"title":"Benchmark"},{"body":" Ce billet a pour origine un commentaire posté dans mon précédent billet et dans lequel Laurent demandait un retour d’expérience sur l’utilisation de frameworks Java de mapping objet vers objet tels Dozer ou ModelMapper.\nDans l’architecture d’une applicative n-tiers, une couche de mapping objet / objet peut intervenir à plusieurs niveaux :\nEn entrée ou en sortie d’un web service SOAP afin de convertir en objet métier les DTO générés à partir du WSDL, ou inversement. Entre la couche de présentation et la couche de services métiers lorsque la première expose des DTO et la seconde travaille avec des objets métiers. Entre la couche de services métiers et la couche d’accès aux données afin de mapper les entités persistances en objets métiers. Dans le premier exemple, le développeur n’a guère le choix. Dans les 2 autres, il s’agit d’un choix d’architecture. L’introduction d’une couche de mapping n’est pas un choix à prendre à la légère : ayant pour objectif de découpler les couches, elle complexifie l’application et peut détériorer ses performances. Le choix d’en introduire une et d’utiliser un framework pour faciliter sa mise en œuvre n’est pas non plus évident.\nCe billet est découpé en 2 parties :\nUne première dressant les avantages et les inconvénients d’utiliser Dozer par rapport à une approche manuelle, et une seconde présentant les résultats d’un micro-benchmark comparant plusieurs frameworks : Dozer, Orika, Selma, MapStruct et ModelMapper. Tableau comparatif Dozer vs mapping manuel en Java Extrait d’un retour d’expérience, le tableau ci-dessous dresse les avantages et les inconvénients de Dozer par rapport à une approche manuelle. A vous de pondérer chaque avantage / inconvénient en fonction de vos exigences.\nDozer Java Avantages - Lisibilité du XML pour mapper les champs : profondeur du chemin de la propriété, découplage entre la correspondance source/destination et la règle de transformation, conversion implicite en fonction des types source et destination\n- Réutilisation du code : transformations réutilisables\n- Structure le développement de mappings\n- Le mapping sert à la fois pour créer un nouvel objet et compléter un objet existant\n- Mapping bi-directionnel offert - Simplicité\n- Pile d’appel claire lors du debug\n- Type safe Inconvénients - Faibles performances\n- Mapping non compilé : pas de complétion dans l’IDE, refactoring nécessitant des recherches dans le XML\n- Utilisation de converter pour gérer les cas compliqués (et ne pas faire appel à du code Java après le mapping Dozer).\n- Apprentissage du framework et des bonnes pratiques - Verbosité du code Java pour les tests de nullité et le code d’instanciation En fonction de votre expertise, ce tableau pourrait être adapter avec d’autres frameworks.\nQuel que soit l’approche choisie (framework ou code manuel), seuls des tests unitaires permettront de valider le mapping. Ne pouvant être automatisés, ces tests s’avèrent malheureusement longs et fastidieux.\nMicro-benchmark Ne trouvant aucun comparatif récent sur les performances des frameworks de mapping, j’ai créé sur GitHub le projet java-object-mapper-benchmark. Ce dernier utilise JMH (Java Microbenchmarking Harness) pour réaliser un micro-benchmark entre Dozer, Selma, ModelMapper, Orika, MapStruct et un mapping écrit manuellement.\nLe diagramme ci-dessous présente résultats obtenus avec la configuration suivante :\nOS: MacOSX CPU: Core i7 2.8GHz 6MB cache × 4 cores RAM: 16GB JVM: Oracle 1.8.0_25 64 bits Comme on pouvait s’y attendre, les performances du code écrit à la main sont les meilleures. Selma et MapStruct se rapprochent le plus des performances d’un code écrit manuellement. Ce résultat s’explique par le fait qu’ils génèrent le code source à l’aide de l’ Annotation Processor introduit par Java 6 (JSR-269). Basés sur l’ introspection de code, Dozer et ModelMapper sont peu performants. Entre ces 2 catégories, on retrouve Orika qui utilise au runtime l’API Java Compiler pour générer le code du mapping.\nPour exécuter vous même le benchmark, Maven, un JDK et 3 lignes de commandes suffisent :\ngit clone git://github.com/arey/java-object-mapper-benchmark.git mvn clean install java -jar target/benchmarks.jar Conclusion En 2015, l’utilisation d’un framework de mapping objet / objet basé sur la génération de code plutôt que sur l’introspection semble préférable. Non seulement les performances sont bien meilleures, mais le couplage avec le framework est faible puisqu’il est possible de le supprimer et de conserver dans votre SCM le code généré. Selma et MapStruct sont les 2 gagnants du benchmark.\nEncore une fois, avant de partir sur une telle approche, prenez un temps de réflexion. Des entités métiers annotées avec Bean Validation et traversant l’ensemble des couches restent l’architecture la plus simple à mettre en œuvre. Je suis déjà intervenu sur une application où une couche de mapping avait été mise en œuvre dès le départ pour des raisons de découplage, puis retirée au fur et à mesure car sa plus value était trop faible.\nRéférences :\nDozer vs Orika vs Manual (2013) Java Bean Mapper Performance Tests (2007) Selma, le mapping Java à la compilation (2014) Using JMH for Java Microbenchmarking (2013) ","link":"https://javaetmoi.com/2015/09/benchmark-frameworks-java-mapping-objet/","section":"posts","tags":["benchmark","dozer","mapstruct","modelmapper","orika","selma"],"title":"Benchmark de frameworks de mapping objet"},{"body":"","link":"https://javaetmoi.com/tags/dozer/","section":"tags","tags":null,"title":"Dozer"},{"body":"","link":"https://javaetmoi.com/tags/mapstruct/","section":"tags","tags":null,"title":"Mapstruct"},{"body":"","link":"https://javaetmoi.com/tags/modelmapper/","section":"tags","tags":null,"title":"Modelmapper"},{"body":"","link":"https://javaetmoi.com/tags/orika/","section":"tags","tags":null,"title":"Orika"},{"body":"","link":"https://javaetmoi.com/tags/selma/","section":"tags","tags":null,"title":"Selma"},{"body":" Réaliser des revues de code e st une activité que je pratique régulièrement sur les projets que j’encadre techniquement. De manière général, elles se déroulent sur le poste du développeur. Ce dernier me présente ses derniers changements, justifie ses choix et m’explique ses difficultés. En fonction de son expérience sur le projet, la périodicité des revues varie d’1 fois par jour à 2 fois par mois. Les améliorations à apporter sont réalisées en séance en pair programming ou bien consignées directement dans le code à l’aide d’un TODO. Je profite de ces moments privilégiés pour expliquer et/ou échanger autour des règles de coding et d’architecture logicielle.\nDans le cadre de l’ externalisation du développement d’une application, les revues de code se pratiquent différemment. En effet, la livraison du code intervient souvent après un long effet tunnel. Dès le début des développements, les développeurs doivent connaître mes attentes. Ce billet a pour objectif de les formaliser de manière synthétique.\nSonar, bien mais pas suffisant Dans ces 2 types de revues, la qualité du code est mesurée automatiquement par l’outil SonarQube. Des seuils sont fixés pour chaque métrique : taux de couverture de tests, nombre de défauts \u0026hellip; Accessible en ligne et documenté, le profil SonarQube fait office de référence sur les règles à respecter : de la simple règle de formatage du code à la gestion des exceptions.\nPour autant, toutes les bonnes pratiques en termes de qualité du code et d’architecture ne peuvent pas être contrôlées par cet outil. Qui plus est, il est parfois possible de leurrer SonarQube. Par exemple, en utilisant des outils de génération automatique de tests unitaires (TU) ou en développement des TU sans assertions, on augmente artificiellement la couverture de code. Une revue « manuelle » reste donc obligatoire.\nLes points qui sont surveillés lors de cette revue sont référencés dans une check-list. Ces points ont été répartis en 5 catégories :\nCode Design Architecture logicielle Performance Tests Sécurité Certains points sont spécifiques à l’architecture technique de l’application. Dans notre exemple, l’application web est décomposée en 3 couches et repose sur les technologies Spring MVC, CXF et JDBC. Sans surprise, de nombreuses de règles sont tirées de l’ouvrage de référence des développeurs « Clean Code» d\u0026rsquo;Uncle Bobo. Les autres proviennent de bonnes pratiques, d’état de l’art et d’expériences.\nCode Design RubriqueDescriptionQualitéRespecter les règles du profil \u0026ldquo;Sonar way \u0026quot; de SonarQubeLisibilité du codeCode élégant et facile à lire. Les classes et les méthodes doivent être relativement petites (classes \u0026lt; 300 lignes et méthodes \u0026lt; 20). Les conditions doivent être compréhensibles. Homogénéité du code : règles de nommage, découpage en package \u0026hellip; Pas de code mort ni de code commenté. Noms appropriés (doit révéler l\u0026rsquo;intention, ne pas les tronquer ou le abréger)SimplicitéPrincipe KISS. Pas d\u0026rsquo;over-design Utilisation de Design Pattern justifiée Dépendances vers d\u0026rsquo;autres classes minimalesRéutilisationFramework open source à privilégier sur du code maison. Code factorisé. Code dupliqué à éviter. Utilisation appropriée de l\u0026rsquo;héritage et de la compositionCommentairesLa javadoc apporte une plus-value et doit aider à la maintenance de l\u0026rsquo;application. Interfaces, Classes, propriétés et méthodes publiques doivent comporter une Javadoc. Les commentaires dans le code doivent se limiter à des explications (le pourquoi).ConfigurationLes variables susceptibles de changer d\u0026rsquo;un environnement à l\u0026rsquo;autre doivent être externalisées et variabilisées par environnement. Le texte et les libellés des pages JSP sont externalisés dans des message bundles (.properties)\nArchitecture logicielle RubriqueDescriptionGestion des transactionsLes transactions base de données sont gérées au niveau de la couche des services métier.Configuration SpringPrivilégier les annotations Spring à la configuration XML (moins verbeux). La configuration XML ou Java est réservée aux beans d\u0026rsquo;infrastructure et à la configuration de l\u0026rsquo;architecture applicative.DécouplageUtiliser l\u0026rsquo;inversion de dépendances (Spring IoC). Utiliser des types abstraits ou des interfaces. Eviter les appels de méthodes statiques.Thread-safeLes ressources partagées entre 2 requêtes HTTP doivent être synchronisées. Attention aux beans Spring de portées singleton et prototype avec états.Gestion des exceptionsUtiliser des exceptions vérifiées pour les erreurs fonctionnelles récupérables. Utiliser les exceptions non vérifiées ( RuntimeException) pour les erreurs techniques non récupérables. Lever des exceptions dès que nécessaire (au plus tôt). Programmation défensive, par assertions. Traiter les exceptions au niveau le plus haut. Traiter les exceptions dans les niveaux intermédiaires que si nécessaire.FrameworksUtiliser les bibliothèques et frameworks référencés dans le catalogue des normes et standards l’EntrepriseL\u0026rsquo;ajout de dépendances tierces est soumise à dérogation et se devra d’être justifié.SingletonNe pas utiliser le pattern Singleton. Laisser Spring gérer le cycle de vie des objets (beans de portée singleton)Logs applicatifsMessages de logs pertinents et contextualisés. L’encapsulation d’une exception apporte des informations complémentaires sur le contexte d’appel. Login de l’utilisateur affiché systématiquement grâce au MDC de SLF4J.Ne pas logger 2x la même erreur.Seules les erreurs techniques sont loggés avec le ERROR.Découpage en couchesRespect du découpage en 3 couches : Contrôleur =\u0026gt; Services métiers =\u0026gt; DAO/Repository. Les services métiers et les DAO sont déclarés dans le contexte root. Les contrôleurs Spring MVC sont quant à eux déclarés dans un contexte enfant.\nPerformances RubriqueDescriptionWeb ServiceLimiter le nombre d\u0026rsquo;appel de WS.Echouer rapidement (timeout faible).Base de donnéesMaitriser le nombre de requêtes SQL exécutés par un DAO. Utiliser des index. Eviter les recherches de type like commençant par un %.Lorsque la pagination n’est pas utilisée, toujours limiter le nombre de résultats remontés par une requête.CacheL\u0026rsquo;utilisation du cache Hibernate ou d\u0026rsquo;un cache applicatif doit être motivée. Les gains doivent pouvoir être mesurés. L\u0026rsquo;application doit fonctionner lorsque le cache est désactivé.Mapping Objet-ObjetLes mappings entre DTO doivent être réalisés manuellement en Java. L\u0026rsquo;utilisation de frameworks comme Dozer ou Orika est proscrite.\nTests unitaires RubriqueDescriptionRègles d’orAussi important que du code de production. Permet de documenter le code.Utiliser des noms de méthode qui documentent le TU.Un scénario par méthode de test.PérimètreTester les cas limites. Tester les exceptions. Tester les requêtes SQL. Un test sans assertion ne vaut (presque) rien.DAO / RepositoryDbUnit (ou DbSetup) et une base de données embarquées peuvent être utilisée pour tester les DAO.\nSécurité RubriqueDescriptionSQLUtiliser des PreparedStatement avec JDBC.LogsNe pas logger des données sensibles.WebLes applications web sont sécurisées avec Spring Security. Valider systématiquement les données saisies par l\u0026rsquo;utilisateur. Un utilisateur ne doit pas pouvoir escalader ses propres privilèges en forgeant sa propre requête HTTP.\nConclusion Cette check-list n’est sans doute pas exhaustive. Qui plus est, vous ne serez sans doute pas d’accord avec tous ces principes. Libre à vous de la personnaliser en fonction de vos contraintes techniques et de vos règles d’architecture. N’hésitez pas non plus à l aisser vos commentaires afin d’en débattre. En fonction de vos retours, je complèterai/amenderai ce billet.\n","link":"https://javaetmoi.com/2015/08/check-list-revue-code-et-architecture-java/","section":"posts","tags":null,"title":"Check-list revue de code Java"},{"body":"","link":"https://javaetmoi.com/tags/apache/","section":"tags","tags":null,"title":"Apache"},{"body":"","link":"https://javaetmoi.com/tags/lamp/","section":"tags","tags":null,"title":"Lamp"},{"body":"","link":"https://javaetmoi.com/tags/mysql/","section":"tags","tags":null,"title":"Mysql"},{"body":"","link":"https://javaetmoi.com/tags/php/","section":"tags","tags":null,"title":"Php"},{"body":" Afin de préparer la migration technique d’un site web, j’ai eu besoin de reconstruire un environnement à l’identique de la production.\nHébergé sur un serveur Linux, ce site est propulsé par Apache 2.2, PHP 5.6 et MySQL 5.5. C’était l’occasion parfaite pour découvrir Docker. Une première étape consiste à décomposer cette plateforme LAMP en conteneurs Docker ayant chacun leur responsabilité. Voici les 3 conteneurs identifiés :\nsite: conteneur Apache et PHP sur lequel les pages PHP du site sont déployées database: conteneur MySQL hébergeant la base de données utilisée par les pages PHP phpmyadmin: conteneur dédié à l’outil d’administration de base de données phpMyAdmin Pour orchestrer le démarrage des conteneurs, gérer leur configuration et définir leurs interactions, l’utilisation de l’outil Docker Compose paraissait évidente.\nDans ce billet, vous trouverez le fichier docker-compose correspondant, un Dockerfile personnalisant l’image officielle php:5.6-apache, les lignes de commandes démarrant les conteneurs sous MacOSX et alimentant la base à partir d’un script SQL.\nConfiguration Docker Compose Le fichier docker-compose.yml décrit en YAML les 3 conteneurs présentés en introduction :\nsite: build: site ports : - \u0026#34;80:80\u0026#34; volumes: - /Users/arey/dev/mysite/www:/var/www/html/ links: - database phpmyadmin: image: corbinu/docker-phpmyadmin ports : - \u0026#34;8080:80\u0026#34; environment: - MYSQL_USERNAME=root - MYSQL_PASSWORD=password links: - database:mysql database: image: mysql:5.5 ports: - \u0026#34;3306:3306\u0026#34; environment: - MYSQL_ROOT_PASSWORD=password - MYSQL_DATABASE=mysite - MYSQL_USER=mysite - MYSQL_PASSWORD=password Voici quelques explications :\nLe conteneur site est créé à partir d’un Dockerfile que nous étudierons par la suite. Le port 80 d’Apache est exposé à l’hôte sur le port 80. Le répertoire /Users/arey/dev/mysite/ www contenant les pages PHP est monté dans le répertoire /var/www/html/ correspondant au répertoire home d’Apache. Enfin, ce conteneur dépend du conteneur database.\nLe conteneur database utilise l’image officielle mysql :5.5. Le port par défaut 3306 de MySQL est exposé aux autres conteneurs et à l’hôte. La base de données mysite est crée au démarrage du conteneur. Les credentials de l’administrateur et d’un utilisateur sont paramétrés.\nLe conteneur phpmyadmin utilise l’image corbinu/docker-phpmyadmin. Il se connecte au conteneur database en utilisant les credential définis dans le conteneur database. L’IHM de phpMyAdmin est accessible depuis un navigateur sur le port 8080.\nDockerfile Créé dans le sous-répertoire site, le Dockerfile suivant permet de construire une image personnalisée Docker à partir de l’image officielle php:5.6-apache:\nFROM php:5.6-apache # Install PDO MySQL driver # See https://github.com/docker-library/php/issues/62 RUN docker-php-ext-install pdo mysql RUN docker-php-ext-install pdo mysqli # Workaround for write permission on write to MacOS X volumes # See https://github.com/boot2docker/boot2docker/pull/534 RUN usermod -u 1000 www-data # Enable Apache mod_rewrite RUN a2enmod rewrite Les commentaires sont suffisamment explicites. Si besoin, la commande RUN a2emod permet d’activer d’autres modules Apache.\nDémarrer les conteneurs Sur MacOSX, l’utilisation de Docker passe par la VM Boot2Docker. L’installation de VirtualBox est un pré-requis à l’utilisation de boot2docker.\nLe gestionnaire de package Homebrew permet d’installer Docker et boot2docker en quelques lignes :\nbrew install boot2docker brew install docker La commande boot2docker init permet de télécharger et d’installer la VM boot2docker-vm. Et les commandes ci-dessous permettent de démarrer la VM et d’obtenir son adresse IP.\nboot2docker start $(boot2docker shellinit 2\u0026gt; /dev/null) boot2docker ip Le téléchargement des images, la construction de l’image site et le démarrage des 3 conteneurs ne demandent que 2 lignes de commande :\ndocker-compose build docker-compose up Une fois démarrés, phpMyAdmin est accessible depuis votre hôte sur l’adresse http://192.168.59.104:8080/index.php (renseigner l’adresse IP obtenue précédemment)\nChargement de la base de données Le site web ne peut pas fonctionner avec une base vide. La dernière étape consiste donc à charger la base MySQL à partir d’un export de la base de données de production. Une première solution est d’utiliser phpMyAdmin. Une seconde solution consiste à installer un client MySQL. Une 3ième est d’utiliser un conteneur docker doté d’un client MySQL :\ndocker run -v /Users/arey/dev/mysite/sql:/sql --link mysite_database_1:mysql -it arey/mysql-client -h mysql -ppassword -D mysite -e \u0026#34;source /sql/export.sql\u0026#34; Voici quelques explications :\nLe répertoire /Users/arey/dev/mysite/sql contient le script sql mysite_database_1 correspond au nom de l’image docker attribué par docker-compose L’image arey/mysql-client est publiée sur Docker Hub. Je l’ai créé spécifiquement pour ce type de besoin. Son code source est sur GitHub. Conclusion En 2 fichiers textes et une dizaine de lignes de commandes, vous disposerez d’une plateforme LAMP opérationnelle. Cela s’avère très pratique pour tester des montées de version de base de données, de CMS ou bien encore de PHP. Elle peut également être utilisée lors de la phase de développement.\nUne fois mon projet de migration terminé, j’ai archivé ces 2 fichiers dans un repo git privé sur BitBucket puis j’ai supprimé les images téléchargées, libérant ainsi autant d’espace sur mon SSD.\nRéférences :\nConteneurisation d’un LAMP avec Docker Documentation officielle de Docker Compose ","link":"https://javaetmoi.com/2015/07/plateforme-lamp-docker-compose/","section":"posts","tags":["apache","docker","lamp","mysql","php"],"title":"Plateforme LAMP avec Docker Compose"},{"body":" Pour rappel, Spring Batch Admin est une console de supervision des traitements par lots implémentés avec Spring Batch. En plus d\u0026rsquo;un frontal web, elle offre une API JSON et expose des métriques via JMX. Bien que dépendant du projet Spring Batch, Spring Batch Admin dispose de son propre repo GitHub et de son propre cycle de vie. Cet article se base sur la version 2.0.0.M1 sortie en janvier 2015. Développé en Spring MVC et composé de 3 JARs, Spring Batch Admin peut aussi bien être intégrée dans une application existante que déployée dans son propre WAR.\nOuvert aux extensions, Spring Batch Admin a tout pour devenir un véritable serveur de batchs : monitoring, chargement et mise à jour à chaud de la configuration des jobs, ordonnancement, exécution de jobs sur réception de fichiers … En 3 ans, c\u0026rsquo;est la seconde fois que je suis amené à personnaliser Spring Batch Admin. Le manuel de référence fourmille d\u0026rsquo;explications. Ce billet recense quelques informations complémentaires qui, je l\u0026rsquo;espère, pourront vous être utiles :\nTransformer Spring Batch Admin en une application auto-exécutable embarquant sa propre base de données et son propre conteneur de servlet Personnaliser l’interface d’admin Adapter les templates FreeMarker au besoin métier Exécuter un job suite à la réception d’un fichier Router un message en fonction du résultat de l’exécution d’un job Ajouter un contrôleur REST Pré-requis Certaines classes utilisées dans ce billet sont issues du projet spring-batch-toolkit hébergé sur GitHub. Disponible sur Maven Central, n’hésitez pas à l’utiliser sur vos projets.\nCréer sa propre application Pour créer from scratch une application Spring Batch Admin, le plus simple consiste à s\u0026rsquo;inspirer de l\u0026rsquo;application web d\u0026rsquo;exemple spring-batch-admin-sample : pom.xml maven, web.xml, index.jsp, fichiers de configuration XML et properties pourront être repris puis adaptés.\nPour stocker l’historique de l’exécution des jobs dans une base de données HSQLDB, la recopie des fichiers batch-hsql.properties et business-schema-hsqldb.sql s’avère nécessaire. Remplacer hsql par le nom d’une autre base supportée.\nA noter que l’IHM devrait être retirée de la version finale de Spring Batch Admin 2.0.0 et déplacée dans un projet sample séparé. Il sera donc alors nécessaire de reprendre les templates FreeMarker, les ressources statiques et le code Java lié à la UI.\nPersonnaliser le nom de l\u0026rsquo;application et de la société Les différents libellés affichés dans l\u0026rsquo;en-tête et le pied page de l\u0026rsquo;application Spring Batch Admin peuvent être chargés depuis un ressource bundle messages.\nPour se faire, créer un fichier messsages.properties dans le répertoire src/main/resources de votre projet. Puis ajouter et personnaliser les propriétés suivantes :\nsite.name=Java \u0026amp; Moi Blog company.url=/ company.name=Java \u0026amp; Moi product.url=/tags/spring-batch product.name=Batch Server copyright=2015 Java \u0026amp; Moi company.contact.url=/about/ company.contact=Contact Créer ensuite le fichier de configuration Spring src/main/resources/META-INF/spring/batch/servlet/override/ manager-context.xml et déclarer le bean messageSource :\n\u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;beans xmlns=\u0026#34;http://www.springframework.org/schema/beans\u0026#34; xmlns:xsi=\u0026#34;http://www.w3.org/2001/XMLSchema-instance\u0026#34; xsi:schemaLocation=\u0026#34;http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd\u0026#34;\u0026gt; \u0026lt;!-- Override messageSource bean in order to provide custom text content --\u0026gt; \u0026lt;bean id=\u0026#34;messageSource\u0026#34; class=\u0026#34;org.springframework.context.support.ResourceBundleMessageSource\u0026#34;\u0026gt; \u0026lt;property name=\u0026#34;basename\u0026#34; value=\u0026#34;messages\u0026#34; /\u0026gt; \u0026lt;/bean\u0026gt; \u0026lt;/beans\u0026gt; Personnaliser le logo Déployer Spring Batch Admin avec le logo de SpringSource, c\u0026rsquo;est bien. Le déployer avec le logo de votre société ou de votre client, c\u0026rsquo;est mieux. Pour changer de logo :\nCopier l\u0026rsquo;image spring-batch-admin-resources-2.0.0.M1.jar!/META-INF/images/header-right.png dans le répertoire webapp/images/header-right.png Ouvrir ce fichier avec votre éditeur préféré (Gimp, Paint.NET …) et remplacer le logo SpringSource par celui de votre choix. Attention aux bords arrondis. Paramétrer le nombre de jobs exécutés en parallèle Pour exécuter les jobs, Spring Batch Admin s\u0026rsquo;appuie sur la classe SimpleJobLauncher de Spring Batch. Son pool de threads est dimensionné à 6 threads. De ce fait, un maximum de 6 jobs peuvent être exécutés simultanément. Pour augmenter ou diminuer le nombre de thread, il est nécessaire de redéfinir le bean jobLauncherTaskExecutor\nAjouter un fichier META-INF/spring/batch/override/execution-context.xml contenant la définition de bean :\n\u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;beans xmlns=\u0026#34;http://www.springframework.org/schema/beans\u0026#34; xmlns:xsi=\u0026#34;http://www.w3.org/2001/XMLSchema-instance\u0026#34; xmlns:task=\u0026#34;http://www.springframework.org/schema/task\u0026#34; xsi:schemaLocation=\u0026#34;http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task.xsd\u0026#34;\u0026gt; \u0026lt;!-- Override jobLauncherTaskExecutor bean in order to customize the pool-size --\u0026gt; \u0026lt;task:executor id=\u0026#34;jobLauncherTaskExecutor\u0026#34; pool-size=\u0026#34;${batch.job.threadpool.size}\u0026#34; rejection-policy=\u0026#34;ABORT\u0026#34; /\u0026gt; \u0026lt;/beans\u0026gt; Puis ajouter la propriété batch.job.threadpool.size dans le fichier batch-.properties :\n## Maximum jobs that could be launched in parallel batch.job.threadpool.size=10 Remarque : un autre moyen de contrôler le nombre de traitements réalisés en parallèle est d\u0026rsquo;utiliser le poolTaskExecutor déclaré par Spring Batch Admin (mais non utilisé par ce dernier). C\u0026rsquo;est particulièrement vrai si vos Jobs utilisent des techniques de parallélisation tels le partitionnement ou la parallélisation de steps. Mutualiser le pool de threads sur plusieurs jobs permet un dimensionnement optimal : les ressources serveur seront ainsi réparties en fonction de la charge globale. Lorsqu\u0026rsquo;un seul job est exécuté, ce dernier pourra profiter de l\u0026rsquo;intégralité des threads mis à disposition du serveur de batch (600 par défaut).\nUne base de données auto-installable Pour fonctionner, Spring Batch Admin nécessite une base de données. C\u0026rsquo;est la base qui lui permet de suivre l\u0026rsquo;exécution des batchs. Tous les jobs à monitorer, qu\u0026rsquo;ils soient exécutés dans Spring Batch Admin ou depuis un autre serveur, doivent utiliser un JobRepository persistant. Et ceci, même si vos jobs ne font que de la manipulation de fichiers.\nSi vos batchs n\u0026rsquo;ont pas besoin de base de données pour fonctionner, la création de la base peut être confiée à Spring Batch Admin lors de son démarrage. Nativement, Spring Batch Admin ne sait pas automatiquement détecter si la base existe. L’utilisateur doit lui spécifier ou non de (re)créer la base via la propriété batch.data.source.init exploitée dans le fichier /META-INF/spring/batch/bootstrap/manager/data-source-context.xml de spring- batch-admin-manager-2.0.0.M1.jar\nEn redéfinissant le bean initialize-database, Spring Batch Admin peut être configuré pour ne créer le schéma que s’il n’existe pas. L’exécution du script de destruction du schéma est retirée et on précise à Spring d’ignorer les erreurs. Ainsi, si une table existe, l’exécution du CREATE TABLE ne fera pas échouer l’exécution du script.\nEn pratique, créer dans votre web app un fichier META-INF/spring/batch/ override/data-source-context.xml contenant le bean suivant :\n\u0026lt;jdbc:initialize-database data-source=\u0026#34;dataSource\u0026#34; enabled=\u0026#34;true\u0026#34; ignore-failures=\u0026#34;ALL\u0026#34;\u0026gt; \u0026lt;jdbc:script location=\u0026#34;${batch.schema.script}\u0026#34;/\u0026gt; \u0026lt;jdbc:script location=\u0026#34;${batch.business.schema.script}\u0026#34;/\u0026gt; \u0026lt;/jdbc:initialize-database\u0026gt; Pour supprimer une base, supprimer le répertoire racine contenant ses fichiers.\nPackager Spring Batch Admin Au début de ce billet, nous avons vu comment distribuer Spring Batch Admin dans son propre WAR ou comment l’inclure dans une application web existante. Se posera ensuite la question de l’installation. L’installation de la base de données a déjà été abordée dans un paragraphe précédent. Concentrons nous à présent sur le conteneur de servlet. Au lieu de devoir installer préalablement sur le serveur un conteneur web comme Jetty ou Tomcat, je vous propose de l’embarquer directement dans le binaire de Spring Batch Admin. A la manière de Spring Boot, il est possible de démarrer Jetty depuis un simple main. Pour y arriver, je vous invite à suivre le tutoriel Embarquer Jetty dans une web app récemment publié sur ce blog.\nExécuter un job suite à la réception d’un fichier Spring Batch Admin offre une intégration poussée de Spring Integration avec Spring Batch. Le chargement à chaud de la configuration XML d’un nouveau job utilise précisément un adaptateur de type file ( file:inbound-channel-adapter) pour détecter la mise à disposition d’un nouveau fichier. Pour davantage de détails, je vous invite à consulter le fichier META-INF/spring/batch/bootstrap/integration/configuration-context.xml du module spring-batch-admin-manager.\nLa UI et les endpoints REST de Spring Batch Admin offrent la possibilité d’uploader un fichier qui sera déposé dans le pusblish-subscribe-channel nommé input-file et déclaré dans le fichier META-INF/spring/batch/bootstrap/integration/file-context.xml du module spring-batch-admin-manager. Charge au développeur de s’abonner au channel pour, par exemple, déclencher un job.\nEn combinant ces 2 fonctionnalités, il est possible de déclencher l’exécution d’un job Spring Batch lors de la réception d’un fichier dans un répertoire donné. Ce cas d’utilisation est particulièrement intéressant lorsque le job exécuté prend en entrée le fichier reçu. Pour exemple, on peut imaginer un batch chargé de prendre un fichier CSV et d’insérer chaque ligne dans une base de données NoSQL. Le chemin complet du fichier est passé au batch à l’aide du paramètre input.file. Le chemin du fichier est préfixé par file:// Le nom du job à déclencher est déduit du nom du fichier à partir, par exemple, d’une convention de nommage.\nLa première étape consiste à créer le fichier META-INF/spring/batch/override/admin-context.xml et à déclarer toute une série d’espaces de nom :\n\u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;beans xmlns=\u0026#34;http://www.springframework.org/schema/beans\u0026#34; xmlns:xsi=\u0026#34;http://www.w3.org/2001/XMLSchema-instance\u0026#34; xmlns:context=\u0026#34;http://www.springframework.org/schema/context\u0026#34; xmlns:file=\u0026#34;http://www.springframework.org/schema/integration/file\u0026#34; xmlns:int=\u0026#34;http://www.springframework.org/schema/integration\u0026#34; xmlns:int-mail=\u0026#34;http://www.springframework.org/schema/integration/mail\u0026#34; xmlns:bean=\u0026#34;http://www.springframework.org/schema/beans\u0026#34; xsi:schemaLocation=\u0026#34;http://www.springframework.org/schema/integration http://www.springframework.org/schema/integration/spring-integration.xsd http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/integration http://www.springframework.org/schema/integration/spring-integration.xsd http://www.springframework.org/schema/integration/file http://www.springframework.org/schema/integration/file/spring-integration-file.xsd http://www.springframework.org/schema/integration/mail http://www.springframework.org/schema/integration/mail/spring-integration-mail.xsd\u0026#34;\u0026gt; La seconde étape consiste à brancher un file:inbound-channel-adapter sur le channel input-files existant. Ainsi, que ce soit par un upload de fichier via HTTP ou un transfert de fichier par SFTP, CFT ou rsync, la suite du traitement du fichier est identique.\n\u0026lt;file:inbound-channel-adapter directory=\u0026#34;/data/sas-in\u0026#34; channel=\u0026#34;input-files\u0026#34; filename-pattern=\u0026#34;*.csv\u0026#34; prevent-duplicates=\u0026#34;true\u0026#34;\u0026gt; \u0026lt;int:poller max-messages-per-poll=\u0026#34;10\u0026#34; fixed-rate=\u0026#34;1000\u0026#34;/\u0026gt; \u0026lt;/file:inbound-channel-adapter\u0026gt; Le corps du message déposé dans le channel input-files est de type File.\nUne 3ième étape consiste à transformer ce fichier en une demande d’exécution de job, à savoir un objet de type JobLaunchRequest (appartenant au module spring-batch-integration).\nUne chaîne de 2 endpoints est nécessaire :\n\u0026lt;context:annotation-config/\u0026gt; \u0026lt;int:chain input-channel=\u0026#34;input-files\u0026#34; output-channel=\u0026#34;job-requests\u0026#34;\u0026gt; \u0026lt;int:service-activator\u0026gt; \u0026lt;bean class=\u0026#34;com.javaetmoi.core.batch.integration.DynamicFileToJobLaunchRequestAdapter \u0026#34;/\u0026gt; \u0026lt;/int:service-activator\u0026gt; \u0026lt;int:transformer\u0026gt; \u0026lt;bean class=\u0026#34;org.springframework.batch.admin.integration.LastJobParametersJobLaunchRequestEnhancer\u0026#34;\u0026gt; \u0026lt;property name=\u0026#34;jobService\u0026#34; ref=\u0026#34;jobService\u0026#34;/\u0026gt; \u0026lt;/bean\u0026gt; \u0026lt;/int:transformer\u0026gt; \u0026lt;/int:chain\u0026gt; L’auto-wiring est activé pour faciliter l’injection de beans dans la classe FilenameToJobLaunchRequestAdapter. En interne, cet adaptateur fait appel à un bean implémentant l’interface FileToJobNameConverter qui est capable de déduire le nom du job à exécuter en fonction du nom du fichier.\nVoici un exemple d’implémentation :\n@Service public class CsvFileToJobConverter implements FileToJobNameConverter { private final static String FILE_NAME_PATTERN = \u0026#34;(\\\\w*)_(.*)\\\\.csv\u0026#34;; private static final String JOB_SUFFIX = \u0026#34;Job\u0026#34;; @Override public String getJobNameFromFile(File file) throws NoSuchJobException { String filename = file.getName().trim().toLowerCase(Locale.FRANCE); if (!filename.matches(FILE_NAME_PATTERN)) { throw new NoSuchJobException(\u0026#34;Filename in wrong format: \u0026#34;+filename); } return filename.replaceAll(FILE_NAME_PATTERN, \u0026#34;$1\u0026#34;) + JOB_SUFFIX; } } A l’issu de l’exécution du FilenameToJobLaunchRequestAdapter, une instance de JobLaunchRequest est créée et envoyée sur le channel. Fourni par Spring Batch Admin, le transformeur LastJobParametersJobLaunchRequestEnhancer complète les paramètres de lancement du job en reprenant ceux utilisés lors de la dernière exécution du job. L’infrastructure de Spring Batch Admin prend ensuite la relève : récupérant le JobLaunchRequest depuis le channel job-requests, elle fait appel à un SimpleJobLauncher pour exécuter immédiatement le job. Une instance de JobExecution est alors déposée dans le channel job-operator.\nAttendre la fin de l’exécution d’un batch La classe SimpleJobLauncher délègue l’exécution des jobs à un pool de threads. Elle rend donc la main avant la fin de l’exécution du job. Dans la milestone 2.0.0-M1 de Spring Batch Admin, les messages déposés dans le channel job-operator sont simplement loggés. Un TODO présage que, dans une prochaine version, Spring Batch Admin proposera de réaliser des traitements en fonction de l’exécution du batch. Extrait de la configuration META-INF/spring/batch/bootstrap/integration/launcher-context.xml:\n\u0026lt;!-- TODO: filter into success and failure channels --\u0026gt; \u0026lt;publish-subscribe-channel id=\u0026#34;job-operator\u0026#34; /\u0026gt; \u0026lt;logging-channel-adapter channel=\u0026#34;job-operator\u0026#34; /\u0026gt; En attendant, la classe JobExitStatusRouter de spring-batch-toolkit permet de router le message dans 2 channels en fonction du code de retour du job ( ExitStatus) :\n\u0026lt;int:router input-channel=\u0026#34;job-operator\u0026#34;\u0026gt; \u0026lt;bean class=\u0026#34;com.javaetmoi.core.batch.integration.JobExitStatusRouter\u0026#34;/\u0026gt; \u0026lt;/int:router\u0026gt; \u0026lt;int:publish-subscribe-channel id=\u0026#34;job-success\u0026#34;/\u0026gt; \u0026lt;int:publish-subscribe-channel id=\u0026#34;job-error\u0026#34;/\u0026gt; Pour accéder au code de retour du job, la classe JobExitStatusRouter attend la fin de son exécution. L’implémentation est très sommaire puisqu’elle utilise la technique du pooling pour interroger à intervalle réguler le statut du job. Un mécanisme de notification aurait été préférable. Mais à ma connaissance, Spring Bach n’offre pas nativement une telle possibilité.\nEnvoi d’un mail en cas d’erreur Lorsque le batch tombe en erreur, si ce dernier ne propose pas déjà un système d’alertes, il est possible d’envoyer un mail à l’équipe en charge de sa supervision. Disponible dans le projet spring-batch-toolkit, la classe JobExecutionToMailOutTransformer permet de construire le corps du mail à partir du JobExecution récupérée dans le channel job-error. Est ensuite utilisé les endpoints du module spring-integration-mail pour compléter le mail puis l’envoyer :\n\u0026lt;chain input-channel=\u0026#34;job-error\u0026#34; xmlns=\u0026#34;http://www.springframework.org/schema/integration\u0026#34;\u0026gt; \u0026lt;filter expression=\u0026#34;${batch.mail.error.alert}\u0026#34;/\u0026gt; \u0026lt;transformer\u0026gt; \u0026lt;bean:bean class=\u0026#34;com.javaetmoi.core.batch.integration.JobExecutionToMailOutTransformer\u0026#34;/\u0026gt; \u0026lt;/transformer\u0026gt; \u0026lt;int-mail:header-enricher\u0026gt; \u0026lt;int-mail:subject value=\u0026#34;${batch.mail.error.subject}\u0026#34;/\u0026gt; \u0026lt;int-mail:to value=\u0026#34;${batch.mail.error.to}\u0026#34;/\u0026gt; \u0026lt;int-mail:cc value=\u0026#34;${batch.mail.error.cc}\u0026#34;/\u0026gt; \u0026lt;int-mail:from value=\u0026#34;${batch.mail.error.from}\u0026#34;/\u0026gt; \u0026lt;/int-mail:header-enricher\u0026gt; \u0026lt;int-mail:outbound-channel-adapter host=\u0026#34;${batch.mail.server.host}\u0026#34; username=\u0026#34;${batch.mail.server.username}\u0026#34; password=\u0026#34;${batch.mail.server.password}\u0026#34;/\u0026gt; \u0026lt;/chain\u0026gt; Personnaliser un template JSON En fonction des besoins métiers, il est parfois nécessaire de devoir modifier ou compléter la réponse d’un service REST de Spring Batch Admin. Qu’elles soient en RSS, XML ou JSON, les réponses sont templatisées avec FreeMarker. En attendant la prise en compte du ticket BATCHADM-223, j’ai par exemple été contraint de transformer une map en un array. Issu du JAR spring-batch-admin-manager-2.0.0-M1.jar, le fichier org/springframework/batch/admin/web/manager/jobs/json/ executions.ftl a été dupliqué puis renommé en executions-custom.ftl. Il a été placé dans un package identique.\nUne fois le template modifié, la redéfinition du bean jobs/executions.json a été réalisé dans le fichier /META-INF/spring/batch/servlet/override/ manager-context.xml:\n\u0026lt;!-- Override provided beans in order to use our custom FreeMarker template --\u0026gt; \u0026lt;bean name=\u0026#34;jobs/executions.json\u0026#34; parent=\u0026#34;standard.json\u0026#34;\u0026gt; \u0026lt;property name=\u0026#34;attributes\u0026#34;\u0026gt; \u0026lt;props merge=\u0026#34;true\u0026#34;\u0026gt; \u0026lt;prop key=\u0026#34;body\u0026#34;\u0026gt;/manager/jobs/json/executions-custom.ftl\u0026lt;/prop\u0026gt; \u0026lt;/props\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;/bean\u0026gt; Empêcher l’exécution simultanée d’un même job Précédemment, nous avons vu comment la réception d’un fichier peut déclencher l’exécution d’un job. Mais que se passe-t-il lorsque 2 fichiers sont reçus et que ces 2 fichiers déclenchent le même job ? Et bien 2 instances du job sont créés puis exécutées en parallèle. Ce comportement peut introduire des effets de bord. Il peut alors être nécessaire de sérialiser le traitements de ces fichiers.\nComme son nom l’indique, la classe AcceptOnceFilePerJobListFilter du projet spring-batch-toolkit permet de n’exécuter à la fois qu’une seule instance du même job. Elle s’appuie sur l’interface FileToJobNameConverter utilisée précédemment. Le nom du job que le fichier va déclencher est conservé en mémoire.\nL’attribut filter du file:inbound-channel-adapter doit alors être paramétré de la manière suivante :\n\u0026lt;file:inbound-channel-adapter directory=\u0026#34;/data/sas-in\u0026#34; channel=\u0026#34;input-files\u0026#34; filter=\u0026#34;receivedFileListFilter\u0026#34;\u0026gt; \u0026lt;int:poller max-messages-per-poll=\u0026#34;10\u0026#34; fixed-rate=\u0026#34;1000\u0026#34; task-executor=\u0026#34;receivedFileTaskExecutor\u0026#34;/\u0026gt; \u0026lt;/file:inbound-channel-adapter\u0026gt; \u0026lt;bean id=\u0026#34;receivedFileListFilter\u0026#34; class=\u0026#34;org.springframework.integration.file.filters.CompositeFileListFilter\u0026#34;\u0026gt; \u0026lt;constructor-arg\u0026gt; \u0026lt;list\u0026gt; \u0026lt;bean class=\u0026#34;org.springframework.integration.file.filters.SimplePatternFileListFilter\u0026#34;\u0026gt; \u0026lt;constructor-arg value=\u0026#34;*.csv\u0026#34;/\u0026gt; \u0026lt;/bean\u0026gt; \u0026lt;bean ref=\u0026#34;acceptOnceFilePerJobListFilter\u0026#34;/\u0026gt; \u0026lt;/list\u0026gt; \u0026lt;/constructor-arg\u0026gt; \u0026lt;/bean\u0026gt; \u0026lt;bean id=\u0026#34;acceptOnceFilePerJobListFilter\u0026#34; class=\u0026#34;com.javaetmoi.core.batch.integration.AcceptOnceFilePerJobListFilter\u0026#34;\u0026gt; \u0026lt;property name=\u0026#34;fileToJobNameConverter\u0026#34; ref=\u0026#34;fileToJobNameConverter\u0026#34;/\u0026gt; \u0026lt;/bean\u0026gt; \u0026lt;bean id=\u0026#34;rollbackProcessedCatalogServiceActivator\u0026#34; class=\u0026#34;com.sparkow.batch.admin.endpoint.RollbackProcessedFileServiceActivator\u0026#34;\u0026gt; \u0026lt;property name=\u0026#34;acceptOnceCatalogListFilter\u0026#34; ref=\u0026#34;acceptOnceCatalogListFilter\u0026#34;/\u0026gt; \u0026lt;/bean\u0026gt; Une fois l’exécution du job terminée, il est nécessaire de notifier le bean acceptOnceCatalogListFilter afin qu’il puisse de nouveau laisser passer les fichiers traités par ce job. C’est le rôle de la classe RollbackProcessedFileServiceActivator.\nAttendre la fin du chargement de la configuration XML des Jobs Lorsque Spring Batch Admin démarre, les fichiers préalablement déposés dans le répertoire /data/sas-in sont analysés par l’ inbound-channel-adapter alors que la configuration XML du job chargé de les traiter n’est pas encore chargé. Le fichier tombe alors en erreur et est déplacé dans le répertoire /data/sas-error\nPour remédier à ce problème, une solution consiste à démarrer manuellement le bean de type inbound-channel-adapter du « Root WebApplicationContext » initié par le ContextLoaderListener . Pour se faire, la propriété auto-startup doit être positionnée à false et un id doit être renseigné :\n\u0026lt;file:inbound-channel-adapter id=\u0026#34;fileInboundChannelAdapter\u0026#34; directory=\u0026#34;/data/sas-in\u0026#34; channel=\u0026#34;input-files\u0026#34; filter=\u0026#34;receivedFileListFilter\u0026#34; auto-startup=\u0026#34;false\u0026#34;\u0026gt; Pour chaque job, Spring Batch Admin crée un contexte Spring. Qui plus est, le DispatcherServlet de Spring MVC déclaré dans le web.xml crée également un contexte applicatif enfant du « Root WebApplicationContext » Au total, N+2 contextes Spring sont créés.\nOn démarre le bean inbound-channel-adapter une fois l’ensemble des contextes initialisés. Le bean ServerStartEventHandler s’abonne aux évènements de type ContextRefreshedEvent émis par le conteneur Spring à chaque fois qu’un contexte applicatif est initialisé ou rafraichit :\n/** * Start the adapter that read CSV files once jobs xml configuration files are loaded. */ public class ServerStartEventHandler implements ApplicationListener\u0026lt;ContextRefreshedEvent\u0026gt; { @Override public void onApplicationEvent(ContextRefreshedEvent event) { ApplicationContext applicationContext = event.getApplicationContext(); if (applicationContext.getDisplayName().contains(\u0026#34;Batch Servlet-servlet\u0026#34;)) { SourcePollingChannelAdapter adapter = (SourcePollingChannelAdapter) applicationContext.getBean(\u0026#34;fileInboundChannelAdapter\u0026#34;); adapter.start(); } } } Au cours du démarrage, la méthode onApplicationEvent est appelée autant de fois que de contextes. Le nom du contexte Spring MVC qui est le dernier chargé contient le nom du servlet « Batch Servlet ».\nAjouter un contrôleur REST Spring Batch Admin propose un frontal REST permettant d’accéder à des ressources au format HTML, RSS et JSON. Par exemple, un GET sur le chemin /jobs/{jobName}/executions.json listera l’historique des exécutions d’un job. De par l’extension, les données échangées sont au format JSON. Ouvert aux extensions, Spring Batch Admin permet d’ajouter ses propres ressources REST.\nLa première étape consiste à ajouter un contrôleur Spring MVC respectant les propriétés suivantes :\nHériter de la classe abstraite AbstractBatchJobsController Etre déclaré en tant que contrôleur REST via l’annotation @RestController Définir un chemin d’accès racine par l’annotation @RequestMapping(\u0026quot;/\u0026quot;) Ajouter autant de handlers de requêtes HTTP que souhaité Bien que tous les contrôleurs REST de Spring Batch Admin les utilisent, l’utilisation de Spring Data et Spring HATEOS est optionnelle.\nAfin que ces nouvelles API soient connues des utilisateurs et apparaissent sur la page d’accueil, une seconde étape consiste à les déclarer dans un fichier properties normalisé. La clé contient le verbe HTTP et l’URI de la ressource. La valeur correspond au commentaire affiché sur la page d’accueil. Voici un exemple de fichier mycustom-json-resources.properties:\nPOST/myresource/{id}.json=Update an existing resource GET/myresource.json=List all the resources, in order of the most recent to least. La 3ième et dernière étape consiste à déclarer le contrôleur et le fichier properties dans le fichier META-INF/spring/batch/servlet/override /controller-context.xml :\n\u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;beans xmlns=\u0026#34;http://www.springframework.org/schema/beans\u0026#34; xmlns:xsi=\u0026#34;http://www.w3.org/2001/XMLSchema-instance\u0026#34; xmlns:util=\u0026#34;http://www.springframework.org/schema/util\u0026#34; xsi:schemaLocation=\u0026#34;http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd\u0026#34;\u0026gt; \u0026lt;bean class=\u0026#34;com.javaetmoi.batch.admin.web.MyResourceController\u0026#34;/\u0026gt; \u0026lt;util:properties id=\u0026#34;jsonResources\u0026#34; location=\u0026#34;classpath:/org/springframework/batch/admin/web/manager/json-resources.properties, classpath:com/javaetmoi/batch/admin/web/myresources-json-resources.properties\u0026#34;/\u0026gt; \u0026lt;/beans\u0026gt; Le bean jsonResources fournit par Spring Batch Admin est ici redéfini afin de prendre en compte notre fichier properties personnalisé. Le nom du répertoire META-INF/spring/batch/servlet/override est prédéfini par Spring Batch Admin. Ce dernier assure que les fichiers de configuration Spring s’y trouvant seront chargés après les siens, permettant ainsi au développeur de redéfinir des beans et/ou d’en ajouter.\nConclusion Afin de pouvoir personnaliser de manière avancée Spring Batch Admin, une appropriation de son code source est nécessaire. Il est en effet fréquent de devoir identifier les beans qui devront être redéfinis.\nPour la version 2.0.0, l’équipe de Spring Batch Admin ambitionne de déplacer le frontal web dans une application démo. Une fois réalisée, certaines explications données dans ce billet seront obsolètes. En contre partie, ce changement d’architecture devrait simplifier la personnalisation de l’interface utilisateur et permettre, par exemple, l\u0026rsquo;internationalisation des IHM.\nDepuis janvier 2015 et la release de la 2.0.0-M1, le repo github Spring Batch recense peu d’activités. Le chemin vers la version 2.0.0 semble donc encore loin. Au cours de mes développements, j’ai soumis 7 pull request et 8 tikets Jira. Certaines ont dors et déjà acceptées pour la version 2.0.0. D’autres restent à valider et à planifier. J’ai hâte de les retrouver et de pouvoir ainsi simplifier mon code.\n","link":"https://javaetmoi.com/2015/06/personnaliser-spring-batch-admin/","section":"posts","tags":["spring-batch","spring-integration"],"title":"Personnaliser Spring Batch Admin"},{"body":"","link":"https://javaetmoi.com/tags/spring-batch/","section":"tags","tags":null,"title":"Spring-Batch"},{"body":"","link":"https://javaetmoi.com/tags/spring-integration/","section":"tags","tags":null,"title":"Spring-Integration"},{"body":"","link":"https://javaetmoi.com/tags/devops/","section":"tags","tags":null,"title":"Devops"},{"body":" Une fois le développement d’une application web terminé, vient le moment (douloureux ou non) de son installation sur un serveur. En général, plusieurs pré-requis sont nécessaires : JRE, serveur d’application, base de données … Aujourd’hui, Docker et/ou des outils comme Ansible et Puppet facilitent le provisionning du middleware. Néanmoins, il est possible de simplifier encore davantage cette phase d’installation. Des applications comme Sonar et Jenkins le font depuis des années : packager l’application avec son propre conteneur de Servlets et sa propre base de données. Afin de pouvoir déployer des applications les plus légères possibles, les architectures micro-services poussent dans ce sens. Et c’est d’ailleurs ce que proposent des frameworks comme Play Framework et Spring Boot. Ce dernier permet en effet de créer un JAR exécutable démarrant au choix un Tomcat ou un Jetty.\nCe billet explique pas à pas comment embarquer un conteneur Jetty dans sa propre application. Nul besoin d’utiliser Spring ou Scala.\nPour distribuer votre web app, vous aurez le choix entre :\nune archive ZIP contenant JARs, scripts shells et fichiers de configuration. ou un unique JAR auto-exécutable Le packaging est assuré par différents plugins Maven.\nDisposer d’une JVM et le seul pré-requis. Sachant qu’OpenJDK est installé sur la plupart des distributions Linux, ce n’est pas nécessairement une contrainte. Seule la version de Java devra être vérifiée avec soin.\nCode source Le code source utilisé pour illustrer ce billet provient du projet embedded-jetty-webapp hébergé sur GitHub. Pour des raisons de lisibilité, certaines parties ont été simplifiées. Si vous souhaitez rendre autonome votre propre application, je vous conseille de vous inspirer directement du code disponible sur GitHub (pom.xml maven et classes Java).\nDépendances Maven Avant de pouvoir utiliser l’API de Jetty pour démarrer / arrêter un serveur, il faut tout d’abord tirer toutes les dépendances nécessaires au fonctionnement d’une web app. Voici la configuration Maven :\n\u0026lt;properties\u0026gt; \u0026lt;version.javax-servlet\u0026gt;3.1.0\u0026lt;/version.javax-servlet\u0026gt; \u0026lt;version.jetty\u0026gt;9.2.7.v20150116\u0026lt;/version.jetty\u0026gt; \u0026lt;/properties\u0026gt; \u0026lt;!-- Jetty --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.eclipse.jetty\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;jetty-server\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;${version.jetty}\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.eclipse.jetty\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;jetty-webapp\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;${version.jetty}\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.eclipse.jetty\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;jetty-servlet\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;${version.jetty}\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.eclipse.jetty\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;jetty-util\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;${version.jetty}\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.eclipse.jetty\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;jetty-servlets\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;${version.jetty}\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.eclipse.jetty\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;jetty-jsp\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;${version.jetty}\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.eclipse.jetty\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;jetty-http\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;${version.jetty}\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!-- Servlet API --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;javax.servlet\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;javax.servlet-api\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;${version.javax-servlet}\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; Comme vous pouvez le constater, Jetty est particulièrement modulaire. Si vous utilisez JSP comme technologie de rendu, il faudra ajouter l’artefact jetty-jsp sous peine du message d’erreur « JSP support not configured ».\nDémarrer un Jetty Manipuler l’API Jetty pour démarrer un conteneur de servlet depuis une classe Main ne présente pas de difficulté :\npublic static void main(String[] args) throws Exception { Server server = new Server(8080); WebAppContext root = new WebAppContext(); root.setContextPath(\u0026#34;/\u0026#34;); root.setDescriptor(\u0026#34;webapp/WEB-INF/web.xml\u0026#34;); URL webAppDir = Thread.currentThread().getContextClassLoader().getResource(\u0026#34;webapp\u0026#34;); if (webAppDir == null) { throw new RuntimeException(\u0026#34;No webapp directory was found into the JAR file\u0026#34;); } root.setResourceBase(webAppDir.toURI().toString()); root.setParentLoaderPriority(true); server.setHandler(root); server.start(); } Une 1ière subtilité réside dans l’utilisation du ClassLoader du thread courant. Sans quoi, en dehors d’un IDE, le répertoire webapp ne sera pas trouvé.\nLa 2nde subtilité vient du fait que l’artefact construit est de type JAR et non un WAR. Bien qu’elle y ressemble, l’arborescence du projet n’est donc pas celle d’un WAR. Le répertoire webapp ne se trouve pas dans le répertoire src/main/webapp mais dans src/main/resources/webapp. Ainsi, lors de la construction du JAR, le répertoire webapp sera copié à la racine du JAR sans configuration maven particulière.\nDans notre exemple, la web app utilise un descripteur de déploiement web.xml. Optionnel depuis Servlet 3.0, l’appel à la méthode setDescriptor est facultatif.\nEnfin, le port HTTP utilisé dans notre exemple est le 8080. Ce dernier aurait pu être passé en paramètre du main() ou bien chargé depuis un fichier de configuration.\nLors de l’appel à la méthode start(), le conteneur Jetty démarre. L’application web est ensuite aussitôt démarrée. Il n’y a pas réellement de phase de déploiement.\nArrêter proprement Jetty Pour arrêter le serveur, une solution peu recommandée est d’utiliser un kill -9 sur le PID du process Java. Les traitements en cours s’arrêtent brutalement et les ressources ne sont pas correctement libérées. Une solution plus élégante est de demander au serveur Jetty de s’arrêter proprement. Le contexte de servlets est alors fermé par Jetty. Les listeners JEE implémentant l’interface ServletContextListener en sont notifiés.\nPour communiquer avec Jetty, une solution possible est d’utiliser un socket TCP. Je me suis grandement inspiré du code Java utilisé par le plugin Jetty pour maven.\nLe principe est simple, un thread Monitor est démarré à la suite du serveur Jetty, et ceci dans la même JVM :\nserver.start(); Monitor monitor = new Monitor(8090, new Server[] {server}); monitor.start(); server.join(); Ce thread démarre un SocketServer écoutant sur le port 8090. Il attend que l’instruction stop lui soit envoyée. Pour davantage de détails, vous pouvez vous reportez à la méthode statique stop de la classe JettyServer ainsi qu’à la classe Monitor.\nUne autre technique serait d’utiliser JMX pour communiquer avec Jetty. L’ajout du module jetty-jmx est alors nécessaire.\nCréation du package Comme je vous l’indiquais en introduction, je vous propose de packager votre application web de 2 manières différentes.\n1. Appassembler\nLe plugin Appassembler pour maven permet de créer un répertoire target/appass embler qu’il suffit de copier/coller pour installer l’application. Ce dernier contient 3 sous-répertoires :\nbin: scripts start.sh, start.bat, stop.sh et stop.bat permettant de démarrer / arrêter la webapp. Ces scripts se chargent de trouver le JRE, sont compatibles avec cygwin et positionnent le classpath. conf: facultatif, ce répertoire contient la configuration de l’application (fichiers properties ou YAML, logback.xml …) lib: tous les JARs nécessaires au fonctionnement de l’application Activé par défaut, le profile maven appassembler regroupe la configuration nécessaire :\n\u0026lt;profile\u0026gt; \u0026lt;id\u0026gt;appassembler\u0026lt;/id\u0026gt; \u0026lt;activation\u0026gt; \u0026lt;activeByDefault\u0026gt;true\u0026lt;/activeByDefault\u0026gt; \u0026lt;/activation\u0026gt; \u0026lt;build\u0026gt; \u0026lt;plugins\u0026gt; \u0026lt;!-- Generate both Windows and Linux bash shell execution scripts --\u0026gt; \u0026lt;plugin\u0026gt; \u0026lt;groupId\u0026gt;org.codehaus.mojo\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;appassembler-maven-plugin\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;${version.plugin.appassembler-maven-plugin}\u0026lt;/version\u0026gt; \u0026lt;executions\u0026gt; \u0026lt;execution\u0026gt; \u0026lt;goals\u0026gt; \u0026lt;goal\u0026gt;assemble\u0026lt;/goal\u0026gt; \u0026lt;/goals\u0026gt; \u0026lt;/execution\u0026gt; \u0026lt;/executions\u0026gt; \u0026lt;configuration\u0026gt; \u0026lt;repositoryLayout\u0026gt;flat\u0026lt;/repositoryLayout\u0026gt; \u0026lt;useWildcardClassPath\u0026gt;true\u0026lt;/useWildcardClassPath\u0026gt; \u0026lt;!-- Set the target configuration directory to be used in the bin scripts --\u0026gt; \u0026lt;configurationDirectory\u0026gt;conf\u0026lt;/configurationDirectory\u0026gt; \u0026lt;!-- Copy the contents from \u0026#34;/src/main/config\u0026#34; to the target configuration directory in the assembled application --\u0026gt; \u0026lt;copyConfigurationDirectory\u0026gt;true\u0026lt;/copyConfigurationDirectory\u0026gt; \u0026lt;!-- Include the target configuration directory in the beginning of the classpath declaration in the bin scripts --\u0026gt; \u0026lt;includeConfigurationDirectoryInClasspath\u0026gt;true\u0026lt;/includeConfigurationDirectoryInClasspath\u0026gt; \u0026lt;!-- Extra JVM arguments that will be included in the bin scripts --\u0026gt; \u0026lt;extraJvmArguments\u0026gt;-Xmx1024m\u0026lt;/extraJvmArguments\u0026gt; \u0026lt;programs\u0026gt; \u0026lt;program\u0026gt; \u0026lt;id\u0026gt;start\u0026lt;/id\u0026gt; \u0026lt;mainClass\u0026gt;com.javaetmoi.jetty.JettyServer\u0026lt;/mainClass\u0026gt; \u0026lt;name\u0026gt;start\u0026lt;/name\u0026gt; \u0026lt;/program\u0026gt; \u0026lt;program\u0026gt; \u0026lt;id\u0026gt;stop\u0026lt;/id\u0026gt; \u0026lt;mainClass\u0026gt;com.javaetmoi.jetty.Stop\u0026lt;/mainClass\u0026gt; \u0026lt;name\u0026gt;stop\u0026lt;/name\u0026gt; \u0026lt;/program\u0026gt; \u0026lt;/programs\u0026gt; \u0026lt;binFileExtensions\u0026gt; \u0026lt;unix\u0026gt;.sh\u0026lt;/unix\u0026gt; \u0026lt;/binFileExtensions\u0026gt; \u0026lt;/configuration\u0026gt; \u0026lt;/plugin\u0026gt; \u0026lt;/plugins\u0026gt; \u0026lt;/build\u0026gt; \u0026lt;/profile\u0026gt; Voici les commandes à exécuter pour tester ce type de packaging :\ngit clone git://github.com/arey/embedded-jetty-webapp.git cd embedded-jetty-webapp mvn clean install target/appassembler/bin/start.sh \u0026amp; curl http://localhost:8080/HelloWorld target/appassembler/bin/stop.sh Assembly L’une des fonctionnalités offertes par le plugin Assembly pour Maven est de rassembler tous les JAR d’une application en un seul gros JAR couramment suffixé par jar-with-dependencies (exemple : jetty-webapp-1.0.0-SNAPSHOT-jar-with-dependencies.jar). Afin de rendre ce JAR auto-exécutable, sa class main doit être spécifier dans son manifeste.\nVoici la configuration du profile maven flatjar:\n\u0026lt;profile\u0026gt; \u0026lt;id\u0026gt;fatjar\u0026lt;/id\u0026gt; \u0026lt;build\u0026gt; \u0026lt;plugins\u0026gt; \u0026lt;plugin\u0026gt; \u0026lt;artifactId\u0026gt;maven-assembly-plugin\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;${version.plugin.maven-assembly-plugin}\u0026lt;/version\u0026gt; \u0026lt;configuration\u0026gt; \u0026lt;descriptorRefs\u0026gt; \u0026lt;descriptorRef\u0026gt;jar-with-dependencies\u0026lt;/descriptorRef\u0026gt; \u0026lt;/descriptorRefs\u0026gt; \u0026lt;archive\u0026gt; \u0026lt;manifest\u0026gt; \u0026lt;mainClass\u0026gt;com.javaetmoi.jetty.JettyServer\u0026lt;/mainClass\u0026gt; \u0026lt;/manifest\u0026gt; \u0026lt;/archive\u0026gt; \u0026lt;/configuration\u0026gt; \u0026lt;executions\u0026gt; \u0026lt;execution\u0026gt; \u0026lt;id\u0026gt;make-assembly\u0026lt;/id\u0026gt; \u0026lt;phase\u0026gt;package\u0026lt;/phase\u0026gt; \u0026lt;goals\u0026gt; \u0026lt;goal\u0026gt;single\u0026lt;/goal\u0026gt; \u0026lt;/goals\u0026gt; \u0026lt;/execution\u0026gt; \u0026lt;/executions\u0026gt; \u0026lt;/plugin\u0026gt; \u0026lt;/plugins\u0026gt; \u0026lt;/build\u0026gt; \u0026lt;/profile\u0026gt; Voici les commandes à exécuter pour tester ce type de packaging :\ngit clone git://github.com/arey/embedded-jetty-webapp.git cd embedded-jetty-webapp mvn clean install -Pflatjar java -jar target/jetty-webapp-1.0.0-SNAPSHOT-jar-with-dependencies.jar \u0026amp; curl http://localhost:8080/HelloWorld java -cp target/jetty-webapp-1.0.0-SNAPSHOT-jar-with-dependencies.jar com.javaetmoi.jetty.Stop Conclusion Par cet article, j’espère vous avoir convaincu de la facilité d’embarquer Jetty dans n\u0026rsquo;importe quelle web app. Tomcat s’intègre d’une manière similaire. Avec cette approche, la mise à jour de Jetty ne nécessite qu’une simple montée de version de Jetty dans le pom.xml\nAutre atout : l’exécution d’un Jetty au démarrage de son application est profitable lors du développement. En effet, il n’est plus nécessaire d’installer et/ou d’utiliser le moindre plugin dans son IDE. L’application web est démarrée par un simple Run ou Debug sur la classe main.\nRéférences :\nAppassembler maven plugin Eclipse Maven Jetty Plugin Apache Assembly Maven Plugin WAR-less Java Web Apps de James Ward ","link":"https://javaetmoi.com/2015/06/web-app-jetty-standalone/","section":"posts","tags":["devops","jetty","maven"],"title":"Embarquer Jetty dans une web app"},{"body":"","link":"https://javaetmoi.com/tags/jetty/","section":"tags","tags":null,"title":"Jetty"},{"body":"Si vous pensez encore que le data-binding, l’ inversion de dépendances, le pattern MVC ou bien encore la gestion de la navigation sont réservés au code Java des applications web modernes, courrez visionner cette présentation. AngularJS, le dernier framework JavaScript de chez Google, devrait vous surprendre.\nBasée sur l’université AngularJS, ou le futur du développement Web présentée lors de Devoxx France 2013, cette présentation a pour objectif de vous initier à AngularJS. Les concepts fondamentaux seront mis en action au travers de l\u0026rsquo;application Game Store. Côté industrialisation, nous verrons que tests unitaires, tests fonctionnels et infrastructure de build ne sont pas non plus délaissés.\nA l’heure où la couche présentation des applications web est de plus en plus déportée côté client, ce workshop a pour objectif de démystifier ce type de framework permettant de structurer une application JavaScript.\n","link":"https://javaetmoi.com/2015/05/introduction-a-angularjs/","section":"posts","tags":["angularjs","devoxx","javascript","présentation"],"title":"Introduction à Angular JS"},{"body":"","link":"https://javaetmoi.com/tags/pr%C3%A9sentation/","section":"tags","tags":null,"title":"Présentation"},{"body":"En attendant que les vidéos des différentes conférences de l’édition 2015 de Devoxx France soient mises en ligne sur Parleys et en complément de certains supports déjà mis en ligne par certains Speakers, je mets librement à votre disposition les différentes notes que j’ai pu prendre sur mon laptop.\nLes sujets sont variés : du Machine Learning avec Watson, Spark et MMLib, du Reactive Programming avec RxJava et Vert.x, du Java 9, du Spring 4.1 ou bien encore du Docker.\nCertaines notes pourront être lues de manière autonome ; je pense par exemple au quickie Stratégie de mise en place de revues de code et à la conférence Livrer chaque jour ce qui est prêt !. Pour être exploitables en l’état, d’autres notes demanderont à ce que vous ayez assisté à la conférence ou que vous ayez pu récupérer les supports de présentation.\nSans plus attendre, voici donc mes 18 notes triées par ordre alphabétique :\nTout ce que vous avez toujours voulu savoir sur les clients Java http concurrents, asynchrones, sans oser demander ! Concurrency in Enterprise Java Créer des applications cognitives avec IBM Watson Engagement Explorer et Bluemix Elles ressemblent à quoi mes données ? avec la stack Elasticsearch, Logstash et Kibana JDBC + JPA + Hibernate – Sans maitrise la puissance n’est rien ! Les monoïdes démystifiées Livrer chaque jour ce qui est prêt Machine Learning avec Spark MMLIB et D3.js Migration d\u0026rsquo;une web app sous Tomcat vers Vert.x Modern Entreprise Java Architecture with Spring 4.1 Modular Java Platform dans Java 9 Refactoring to Functional avec Kotlin RxJava les mains dans le code Sortez couverts avec Hystrix Stratégie de mise en place de revues de code Uniformisez vos postes de dev avec Docker Compose Unit testing concurrent code with ThreadWeaver Vos Managers ne veulent pas entendre parler de la dette technique, tant mieux Bonne lecture, découverte ou redécouverte et, je l’espère, à l’année prochaine pour Devoxx France 2016 !!\n","link":"https://javaetmoi.com/2015/04/18-prises-de-notes-a-devoxx-france-2015/","section":"posts","tags":["devoxx","docker","elasticsearch","hibernate","hystrix","java","jpa","rxjava","spark","spring-framework","tomcat","vert.x"],"title":"18 prises de notes à Devoxx France 2015"},{"body":"","link":"https://javaetmoi.com/tags/rxjava/","section":"tags","tags":null,"title":"Rxjava"},{"body":"","link":"https://javaetmoi.com/tags/vert.x/","section":"tags","tags":null,"title":"Vert.x"},{"body":"","link":"https://javaetmoi.com/tags/hadoop/","section":"tags","tags":null,"title":"Hadoop"},{"body":" En cette édition 2015 de Devoxx France, Apache Spark est l’une des technologies qui se démarque, comme le furent Docker et Java 8 en 2014 ou AngularJS en 2013. Connu pour être le digne successeur d’Hadoop, le framework Spark fait partie des outils Big-Data que j’ai découvert lors de la conférence NoSQL Matters 2015.\nPrésenté par Hayssam Saleh et Olivier Girardot, le Hands-on-Lab « Initiation à Spark avec Java 8 et Scala » était donc l’occasion idéale pour m’initier en pratique aux fonctionnalités proposées par Spark et découvrir l’univers du Machine Learning.\nSi vous n’avez pas eu la chance de pouvoir assister à ce Lab, toutes les ressources utilisées lors du Lab ont été mises en lignes pour le suivre en offline (ou le terminer à la maison).\nUn gitbook Initiation à Spark avec Java 8 et Scala. Avec ses 33 pages, ce livre contient à la fois la présentation réalisée en séance par les speakers ainsi que les intitulés des exercices. Les jeu de données au format CSV et JSON nécessaires pour le Lab La configuration maven pour Java ou sbt pour Scala. Bien qu’ayant particulièrement bien préparés leur présentation, Hayssam et Olivier ont surestimé la vélocité de leur auditoire. Nous n’avons en effet eu le temps que de coder 3 des 9 workshops prévus initialement. Les présentateurs ont donné aux participants le choix d’utiliser Java 8 ou Scala. La grande majorité de l’assistance a choisi Java ; ce fut également mon cas. Dans ce billet, je compte vous restituer ce que j’ai appris au cours de ces 3h de Lab. Je vous relaierai les bonnes pratiques dispensées par Hayssam qui a récemment passé la certification Spark. Pour vous aider, j’ai publié le code Java sur le projet github initiation-spark-java.\nHello World avec Spark Le pré-requis à l’utilisation de Spark est de disposer d’un JDK 8. La distribution Apache Spark 1.3 est multi-OS. Une fois l’archive dézippée, on peut vérifier son fonctionnement en utilisant son shell en ligne de commande bin/spark-shell.sh puis en exécutant une première commande Scala : sc.parallelize(1 to 1000).foreach(println)\nCette commande affiche 1000 nombres de manière non ordonnée. La fonction parallelize construit une collection distribuée. La liste peut être répartie sur plusieurs machines et/ou plusieurs cœurs. Ici, elle est répartie sur un cluster local. Sur un Macbook disposant de 8 cœurs, 8 partitions sont créées. Chaque nombre est affiché sur la console. L’ordre d’affichage n’est pas prédictif.\nSpark, mais pourquoi faire ? Apache Spark est un framework de calcul distribué s’inscrivant dans la mouvance BigData. Il s’adresse aussi bien aux datascientists qu’aux développeurs.\nApparu en 2010, Spark est le digne successeur du pattern d’architecture Map/Reduce mis en œuvre dès 2002 chez Google. Map/Reduce est conçu comme un batch distribué : pas d’interaction utilisateur, pas de temps réel … Spark permet quant à lui de réaliser des traitements au fil de l’eau. Il permet de réaliser des micro-batchs. Quitte à perdre des données, Spark met tout en œuvre pour aller le plus rapidement possible en profitant de leur co-localité. Spark inclue la librairie d’algorithmes de Machin Learning MMLib.\nEcrit en Scala, Spark est pensé pour être utilisé Scala. Néanmoins, Spark propose une API Java. Par apport à l’API Scala, l’API Java souffre souvent de petits retards. D’après l’expérience d’Hayssam Saleh, la taille des programmes Spark tourne autour des 700 lignes de code Scala.\nLes concepts de Spark Le RDD Le concept fondamental de Spark est le RDD, pour Resilient Distributed Dataset. Il s’agit d’une structure de données, immuable, itérable et complètement lazy.\nCette structure représente un graphe acyclique ordonnée (de la même manière que les commits Git) des différentes opérations à appliquer aux données chargées par Spark. Il s’agit en quelque sorte d’ un plan d’exécution.\nTout traitement Spark commence par le chargement d’un RDD. Spark permet de charger les données depuis plusieurs sources : HDFS, un fichier texte, une structure en mémoire, des données sérialisées, des données ou des SequenceFile Hadoop … Transformations et actions\nLes 2 concepts que sont les transformations et les actions s’appliquent au RDD. Les transformations sont empilables sur les RDD. Les RDD étant immutables, une transformation crée donc un nouveau RDD. Les transformations sont paresseuses. Cela signifie qu’elles ne sont pas évaluées tout de suite.\nPour véritablement lancer le traitement sur un cluster ou les CPU locaux, on passe par une action. Les actions sont terminales. Lorsqu’on lancer une action, on retourne à la JVM (appelant). Une seule action peut être réalisée par RDD.\nDans l’API Spark, le nom des méthodes ne permettent pas de déterminer si l’on a affaire à une opération ou d’une action. Pour cela, il faut regarder le type de retour : s’il s’agit d’un RDD, la méthode est une transformation, sinon c’est une action.\nLe Spark Context Le SparkContext est la couche d’abstraction permettant à Spark de savoir où il va exécuter les traitements. Dans le code, il est générallement matérialisé par la variable sc. Une fois les développements terminés, le SparkContext est redéfini lors du déploiement du binaire sur un cluster de 50 machines. En Java, on privilégie l’utilisation du JavaSparkContext : il retourne des JavaRDD et sait manipuler les collections Java.\nChargement d’un premier RDD en Java Comme nous l’avons vu, tout programme Spark commence par le chargement d’un RDD. Afin de boostraper notre IDE, nous allons créer un projet Java chargeant un RDD depuis un fichier CSV. On commence par écrire un pom.xml contenant la dépendance vers spark-core:\n\u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.apache.spark\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spark-core_2.11\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.3.1\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; A noter que lors du Lab, la version 1.3.1 de Spark n’était qu’en Release Candidate et qu’il était donc nécessaire d’ajouter un repository pour les early adopters. La version 1.3.1 permet de pleinement utiliser Spark 1.3 en Java.\nUn mvn dependency:tree permet de lister toutes les librairies sur lesquelles se base Spark : Hadoop, Jackson, Metrics et bien entendu Scala. Le suffixe 2.11 de l’artefact spark-core correspond à la version de Scala utilisée. Ainsi, pour Scala 2.10, il existe l’artefact spark-core_2.10.\nLe scope maven par défaut compile est utilisé ici afin de pouvoir exécuter le code Spark depuis un IDE. Afin de pouvoir déployer le code sur un cluster Spark, il sera nécessaire de le positionner à provided et de construire un unique JAR à l’aide du plugin maven assembly.\nVoici le code source de la classe FirstRDD affichant le nombre de lignes contenues dans le fichier rating.txt.\npublic class FirstRDD { public static void main(String[] args) { SparkConf conf = new SparkConf().setAppName(\u0026#34;Workshop\u0026#34;).setMaster(\u0026#34;local[*]\u0026#34;); JavaSparkContext sc = new JavaSparkContext(conf); String path = Paths.get(FirstRDD.class.getResource(\u0026#34;/ratings.txt\u0026#34;).getPath()); JavaRDD\u0026lt;String\u0026gt; lines = sc.textFile(path.toString()); System.out.println(\u0026#34;Lines count: \u0026#34; + lines.count()); } } Le paramètre \u0026ldquo;local[*]\u0026rdquo; précise à Spark d’exécuter les traitements sur un cluster local et en profitant de tous les cœurs disponibles.\nLa lecture du fichier texte à la mode Java 7 renvoie un RDD de String. L’appel à la méthode count() déclenche une action. 100 000 lignes comptabilisées.\nLes fichiers de type CSV sont particulièrement bien adaptés à Spark : chaque ligne correspond à un élément. Néanmoins, Sparl offre la possibilité de charger du JSON à l’aide du parseur Jackson. On utilise alors un RDD de tuple RDD[(String, String)] : le path est la clé, la valeur est le contenu entier du fichier JSON.\nSparl permet également de charger le RDD à partir d’une source de données JDBC. 3 éléments sont nécessaires : une connexion JDBC, la requête JDBC et ses paramètres et une classe chargée de lire une ligne du ResultSet.\nA des fins de test, comme nous l’avons fait dans l’exemple du Hello World, on peut utiliser une collection via la fonction parallelize. Exemple en Scala : val rdd1 : RDD[1] = sc.parallelize(1 until 100000)\nUtilisation des RDD A présent que nous savons comment charger un RDD, apprenons à l’utiliser.\nPlusieurs transformations sont disponibles sur un RDD. En voici un extrait :\nMap: change le type des objets contenus dans une collection Filter: sélectionne un sous-ensemble d’une liste à partir d’un prédicat Union: regroupe plusieurs RDD Pour rappel, Spark ne réalise aucun traitement tant qu’on n’exécute pas l’action terminale. Il reste sur le driver. Spark construit en mémoire une structure de données reliant les transformations les unes et aux autres. Il prépare le graphe acyclique dirigé.\nLes actions s’exécutent de manière distribuée sur des workers. Certaines actions ne produisent aucun résultat (ex : println), d’autres renvoient un objet ou une collection. Les données issues du calcul sont retournées au drivers. Par exemple, lors d’une action de réduction (qui consiste à réduire une liste, par exemple en sommant ses éléments), un objet Java ou Scala est retourné sur le driver.\nLes speakers mettent en garde l’auditoire sur le fait que, lorsque l’action renvoie trop de données Java, un OutOfMemoryException a des chances d’être levé par la JVM. Attention donc aux volumes de données importants.\nEn fonction des opérations de transformation appliquées au RDD, Spark va optimiser les traitements. Le réseau est l’ennemi du distribué car très lent. Spark limite le shuffling et privilégie la colocalisation.\nWorkshop 1 : première action Le premier workshop de ce Lab consiste à calculer la moyenne, le min, le max et le nombre de votes de l\u0026rsquo;utilisateur ayant l\u0026rsquo;identifiant 200.\nPour source de données, nous repartons du fichier ratings.txt dont voici les 3 premières lignes :\n196 242 3 881250949 186 302 3 891717742 22 377 1 878887116 L’initiation du JavaSparkContext et la récupération du chemin vers le fichier ratings.txt s’effectuent de la même manière que dans la classe FirstRDD. 3 transformations sont ensuite enchaînées :\nJavaRDD\u0026lt;Rating\u0026gt; ratings = sc.textFile(ratingsPath) .map(line -\u0026gt; line.split(\u0026#34;\\\\t\u0026#34;)) .map(row -\u0026gt; new Rating( Long.parseLong(row[0]), Long.parseLong(row[1]), Integer.parseInt(row[2]), LocalDateTime.ofInstant(Instant.ofEpochSecond(Long.parseLong(row[3]) * 1000), ZoneId.systemDefault()) )); La 1ière transformation consiste à lire le fichier texte dans un tableau de String La 2nde transformation consiste à séparer chaque ligne en tokens séparés par le caractère La 3ième transformation permet de mapper les 4 tokens dans le POJO Rating. Le résultat de ses transformations est l’obtention d’un RDD de Rating. A noter que Spark ne permet malheureusement pas d’utiliser les Streams de Java 8 et que les méthodes map font ici parties de l’API Spark.\nLe calcul de la moyenne des votes repart du RDD ratings et lui applique 2 transformations et une action :\ndouble mean = ratings .filter(rating -\u0026gt; rating.user == 200) .mapToDouble(rating -\u0026gt; rating.rating) .mean(); Les transformations de filtre et de mapping utilisent les lambdas de Java 8. L’action mean() est terminale. Elle déclenche la distribution du calcul sur les workers.\nEn interne, des acteurs Akka discutent ensemble. Les données sont échangées via de la sérialisation Java. Pour gagner en performance, il est possible d’utiliser Kryo pour sérialiser les données. L’obtention de la note maximale attribuée par l’utilisateur n°200 ressemble au calcul de la moyenne. Sauf l’action finale diffère :\ndouble max = ratings .filter(rating -\u0026gt; rating.user == 200) .mapToDouble(rating -\u0026gt; rating.rating) .max(Comparator.\u0026lt;Double\u0026gt;naturalOrder()); L’implémentation en Scala de ces quelques lignes aurait gagnée en concision. En effet, Scala aurait réussi à déterminer le comparateur par défaut des POJO et l’utilisation du Comparator.naturalOrder() aurait été superflue. Qui plus est, il aurait été inutile d’appeler la transformation mapToDouble. Son appel aurait été explicite.\nJe ne rentrais pas ici dans le détail du calcul du min et du count. Le code source complet de la classe Workshop1 est disponible sur GitHub.\nWorkshop 2 : le cache Dans le Workshop 1, le fichier est lu 4 fois. De même, le filtrage sur l’utilisateur n°200 est opéré 4 fois. Vous vous en doutez, 4 aller/retours entre le driver et le cluster a un coût. C’est pourquoi, lorsque des transformations sont communes à plusieurs opérations, Spark propose un mécanisme de cache. Et l’objectif du workshop n°2 est précisément d’utiliser le cache.\nPour se faire, il faut indiquer à Spark que le RDD ne doit pas être déchargé suite à une action. Spark laisse alors les données sur le cluster. Libre au développeur de décharger le cache lorsqu’il n’en a plus besoin. Spark propose 5 stratégies de caching (exemple : StorageLevel.MEMORY_AND_DISK).\nDans la classe Workshop2, nous repartons du RDD ratings sur lequel nous appliquons une transformation de filtrage sur l’utilisateur 200. Ces transformations sont mises en cache :\nJavaRDD\u0026lt;Rating\u0026gt; cachedRatingsForUser = ratings .filter(rating -\u0026gt; rating.user == 200) .cache(); double max = cachedRatingsForUser .mapToDouble(rating -\u0026gt; rating.user) .max(Comparator.\u0026lt;Double\u0026gt;naturalOrder()); double count = cachedRatingsForUser .count(); cachedRatingsForUser.unpersist(false); Lors de l’appel à l’action max, les logs montent que les 2 partitions rdd_4_0 et rdd_4_1 sont mises en cache :\n15/04/12 14:03:43 INFO MemoryStore: ensureFreeSpace(4192) called with curMem=160926, maxMem=2061647216 15/04/12 14:03:43 INFO MemoryStore: Block rdd_4_1 stored as values in memory (estimated size 4.1 KB, free 1966.0 MB) 15/04/12 14:03:43 INFO BlockManagerInfo: Added rdd_4_1 in memory on localhost:65117 (size: 4.1 KB, free: 1966.1 MB) 15/04/12 14:03:43 INFO BlockManagerMaster: Updated info of block rdd_4_1 15/04/12 14:03:44 INFO MemoryStore: ensureFreeSpace(20704) called with curMem=165118, maxMem=2061647216 15/04/12 14:03:44 INFO MemoryStore: Block rdd_4_0 stored as values in memory (estimated size 20.2 KB, free 1966.0 MB) 15/04/12 14:03:44 INFO BlockManagerInfo: Added rdd_4_0 in memory on localhost:65117 (size: 20.2 KB, free: 1966.1 MB) 15/04/12 14:03:44 INFO BlockManagerMaster: Updated info of block rdd_4_0 L’appel à unpersist libère la mémoire :\n15/04/12 14:03:44 INFO MapPartitionsRDD: Removing RDD 4 from persistence list 15/04/12 14:03:44 INFO BlockManager: Removing RDD 4 15/04/12 14:03:44 INFO BlockManager: Removing block rdd_4_0 15/04/12 14:03:44 INFO MemoryStore: Block rdd_4_0 of size 20704 dropped from memory (free 2061474527) 15/04/12 14:03:44 INFO BlockManager: Removing block rdd_4_1 15/04/12 14:03:44 INFO MemoryStore: Block rdd_4_1 of size 4192 dropped from memory (free 2061478719) Workshop 3 : Spark SQL Spark SQL permet d’exécuter de bonnes vielles requêtes SQL 92 sur un RDD structuré. Via JDBC, il est ainsi possible de brancher des outils de BI tels Business Object ou Crystal Report sur un RDD.\nDepuis la version 1.3, la notion de SchemaRDD a été remplacée par celle de DataFrame.\nLe 3ième et dernier Workshop réalisé au cours de ce Lab consiste à charger un fichier JSON contenant un tableau de produits dans un DataFrame puis à l’interroger en SQL.\nLa méthode sql() est une simple transformation que l\u0026rsquo;on peut chainer avec tout autre transformation, et en particulier d’autres requêtes SQL.\nAvant de pouvoir d’utiliser le SQL il faut ajouter dans le pom.xml la dépendance vers le module spark-sql:\n\u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.apache.spark\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spark-sql_2.11\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.3.1\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; La classe Workshop3 commence par créer un SQLContext à partir du SparkContext. Une liste de produits est ensuite chargée sous forme de DataFrame depuis un fichier JSON. Le premier produit de la liste est affiché :\nSQLContext sqlContext = new SQLContext(sc); String path = Workshop3.class.getResource(\u0026#34;/products.json\u0026#34;).getPath(); DataFrame products = sqlContext.load(path, \u0026#34;json\u0026#34;); System.out.println(products.first()); Le DataFrame est enregistré en tant que table temporaire portant le nom de products. Une requête SQL peut ensuite être exécutée sur cette table :\nsqlContext.registerDataFrameAsTable(products, \u0026#34;products\u0026#34;); DataFrame frame = sqlContext.sql(\u0026#34;SELECT count(*) FROM products where id \u0026gt; 999\u0026#34;); System.out.println(frame.first()); Conclusion En complément des 3 workshops réalisés en séance, les speakers auront eu l’occasion de nous initier aux aspects avancés de Spark, tels les pairs, les accumulateurs, le broadcast, Spark Streaming ou bien encore le repartionnement.\nJ’ai particulièrement apprécié la prestation d’Hayssam Saleh qui est très pédagogique. J’en ai eu de nouveau la confirmation en assistant à sa session Machine Learning avec Spark, MMLIB et D3.js. Je vous invite à regarder sa présentation sur Youtube lorsqu’elle sera disponible d’ici 4 à 8 semaines et, en attendant, à consulter son compte-rendu que je mettrais prochainement en ligne. Stay tuned.\n","link":"https://javaetmoi.com/2015/04/initiation-apache-spark-en-java-devoxx/","section":"posts","tags":["bigdata","devoxx","hadoop","java","scala","spark"],"title":"Initiation à Apache Spark avec Java"},{"body":"","link":"https://javaetmoi.com/tags/scala/","section":"tags","tags":null,"title":"Scala"},{"body":" Ayant gagné une place par le groupe utilisateurs Elasticsearch (que je remercie une nouvelle fois), j’ai eu l’opportunité d’assister pour la première fois à une conférence dédiée au NoSQL. Parmi la centaine de participants, je devais sans nul doute être le plus néophyte. Certes, je connais relativement bien Elasticsearch pour l’avoir mis en œuvre, mais l’écosystème d’Hadoop et des bases de données NoSQL restait pour moi encore très vague. Ce fut donc l’occasion rêvée d’approfondir mes connaissances sur le sujet et de m’aérer l’esprit. Cerise sur le gâteau, cette conférence m’aura permis d’apprécier la qualité de speaker de deux anciens collègues, Bruno Guedes et Lucian Precup.\nKeynote La keynote d’ouverture « NoSQL : The Good, the Bad and the Ugly» fut brillamment animée par Rob Harrop que je connaissais pour être le co-fondateur de SpringSource. Après avoir donné sa propre définition du NoSQL, Rob s’est attaché à expliquer ce qu’il y’avait de bon et de moins bon dans ce mouvement. Voici les idées clés que j’ai retenues :\nLa diversité du choix et la sophistication des solutions peu à la fois être une force et une faiblesse. Une fois n’est pas coutume, l’innovation est venue de l’industrie et non dans la recherche fondamentale / académique. Pour rester compétitives, les bases de données relationnelles cherchent à s’approprier des caractéristiques apportées par les bases NoSQL. Pour un débutant, il est difficile de faire la part des choses entre le discours marketing des éditeurs de solution et les différents trolls que l’on peut trouver sur Internet. Pour s’y retrouver, Rob nous invite à se référer aux articles de Kyle Kingsbury / Jepsen qui torture et pousse à bout de nombreuses solutions NoSQL. Pour s’initier progressivement au monde du NoSQL, Rob nous conseille de commencer par quelque chose de modeste, comme du cache distribué ou de la recherche fulltext. Par sécurité, le dual run peut alors être envisagé. Rob insiste également sur le fait de d’abord essayer de tirer partie au maximum de sa base de données relationnelle. Et pour cela, je ne peux que vous conseiller l’excellent ouvrage SQL Performance Explained.\nSe sont ensuite enchaînées différentes conférences portant sur Hadoop, Spark, Elasticsearch, MongoDB, Druid ou bien encore Cassandra.\nLe retour du SQL Un des thèmes récurrent de cette conférence fut paradoxalement le SQL. En effet, contrairement au monde des bases de données relationnelles, aucun standard de requêtage n’a émergé pour les bases NoSQL. Certes, des frameworks tels qu’ Hibernate OGM ou Spring Data permettent d’avoir un cadre commun. Mais encore faut-il appréhender leurs API. Lorsqu’ils souhaitent faire parler ces données, le Data Scientist a besoin d’un langage expressif. Souvent venu de la BI, il est familiarisé avec le SQL. Relativement récents, HAdoop With Queries (HAWQ) et Spark SQL offrent désormais la possibilité d’interroger des péta-octets de données en utilisant le SQL.\nPour nous convaincre de son efficacité, Duy Hai Doan, évangéliste chez Datastax, a réalisé une démo dans laquelle il indexait des tweets en JSON dans une base Cassandra puis les requêtait avec Spark SQL.\nBurno Guedes, CTO de Zenika, a quant à lui réalisé une démo similaire avec HAWQ. HAdoop With Query est une implémentation de PostgreSQL qui stocke nativement les données dans HDFS. HAWQ n’est donc pas une technologie de map/reduce. Il peut être vu une alternative à Hive supportant la norme ANSI SQL-92. Associé à Hortonworks , Pivotal est en train de rendre HAWQ Open Source au sein du consortium Open Data Plaform (ODP). Au cours de sa présentation « Back to the future : SQL 92 for Elasticsearch ? », Lucian Precup, CTO d’ Adelean, nous explique que de nombreux utilisateurs d’Elasticsearch se demandent comment réécrire telle ou telle requête SQL avec Elasticsearch. Le cas classique est un développeur qui cherche à migrer ses services métiers de recherche implémentés jusqu’alors en SQL ou en HQL / Hibernate Criteria.\nOr, à ce jour, Elasticsearch ne supporte pas (encore ?) la syntaxe SQL. Pour interroger Elasticsearch, le développeur doit passer par une de ses API : REST / JSON, Java, JavaScript \u0026hellip;\nDe la même manière qu’en SQL, il existe plusieurs moyens de requêter le moteur de recherche. Lucian prend l’exemple de 2 requêtes ES renvoyant le même résultat mais dont la 2ième est 100x plus rapide que la 1ière car le filtre est appliqué avant la recherche. A partir d’exemples, Lucian montre ensuite les requêtes ES équivalentes aux agrégations (sum, avg, count), aux clauses group by et having et aux jointures du SQL. Pour une requête SQL, jusqu’à 4 opérations ES sont nécessaires afin d’obtenir le même résultat.\nMigrer ou réécrire ? La session « From SQL to NoSQL in less than 40mn» animée par Tugdual Grall, évangéliste MongoDB, a particulièrement retenu mon attention. En effet, elle abordait les raisons qui poussent à migrer vers une base NoSQL : haute performance, disponibilité de 99,999% (\u0026lt;10 mn / an) mais surtout, l’effort et les difficultés qu’il fallait consentir pour passer d’une base de données relationnelles à une base NoSQL. Migrer d’une base Oracle vers Mongo n’a rien à voir avec une migration Oracle vers MySQL. Une telle migration impact en effet toutes les couches d’une architecture :\nStorage: les bases de données relationnelles sont généralement stockées sur un équipement de type SAN alors que les bases de données NoSQL utilisent des disques durs locaux, espace de stockage peu cher. JDBC: drivers différents, API propriétaire, pool de connections non fournis par le serveur d’application. Transactions: pas de transactions sur plusieurs documents avec Mongo. Pas forcément gênant car là où il faut 4 requêtes SQL pour insérer un Produit et 3 Caractéristiques, il ne sera nécessaire d’insérer qu’un seul document JSON. SQL / ResultSet: pas de SQL avec Mongo, pas de données retournées sous forme d’un tableau et pas de JOIN. ORM: possible avec Hibernate OGM, mais pas nécessaire avec Mongo POJO: attention au code technique qui pourrait s’y trouver. Une architecture logicielle bien pensée, avec des couches de service et d’accès aux données atténue l’effort de migration. Le passage au NoSQL impacte également les différentes professions d’un SI : Solution Architects, Data Architectes, System Administrators, Developers, DBA et plus particulièrement ces 3 dernières. Tug nous fait ensuite réfléchir à la question suivante : faut-il migrer une application ou tout réécrire ? Il n’y a pas de réponse type. Les 2 scénarios doivent être envisagés et chiffrés. Une migration sera privilégiée pour une application bien architecturée et comportant peu de tables. Une réécriture donne l’opportunité de changer d’architecture (ex : API REST API). Pour nous aider à prendre une décision, Tug nous montre quelques outils sur lesquels nous appuyer :\nBilan En conclusion, cette 8ième édition de NoSQL matters m’aura permis de rencontrer et d’échanger avec des experts du NoSQL et du BigData. A ma grande surprise, je n’y ai croisé aucun DBA. A croire que le monde du NoSQL n’intéresse majoritairement que les développeurs et quelques sys admin ?\nDe cette journée, mon seul regret est ne pas avoir pu participer à la session pratique qui se déroulait la veille dans les locaux de Zenika. L’année prochaine, qui sait ?\n","link":"https://javaetmoi.com/2015/03/nosql-matters-paris-2015/","section":"posts","tags":["elasticsearch","nosql"],"title":"NoSQL Matters Paris 2015"},{"body":"Vous avez déjà entendu parler d’ Apache Camel ? Les Design Pattern n’ont plus de secrets pour vous ? L’Event Driven Architecture vous attire ? Les Enterprise Integration Pattern vous interpellent ? Vos applications reposent sur TCP, JMS, FTP, SFTP, les fichiers, les mails, XML, les web services SOAP, REST, RSS, JPA, JDBC ou même Twitter ?\nN’attendez plus : venez découvrir l’utilisation des patterns comme le Channel Adapter, le Router ou l’ Aggregator au travers d’un workshop sur Spring Integration.\nVoici le support de présentation d’un workshop d’1h s’appuyant sur le cas d’étude suivant : un moteur chargé d’indexer des données clients dans le moteur de recherche Elasticsearch.\nUne connaissance minimaliste de Spring Framework est pré-requise.\nSpring Integration est à votre portée de main. EAI et ESB n’ont qu’à bien se tenir !!\n","link":"https://javaetmoi.com/2015/03/etude-de-cas-spring-integration-elasticsearch/","section":"posts","tags":["elasticsearch","spring-integration"],"title":"Etude de cas Spring Integration"},{"body":"Pour créer vos IHM web en Java, vous n’avez que l’embarras du choix : Vaadin, JSF, GWT, Spring MVC, Tapestry …\nPour accéder aux données, à chacun ses préférences : Hibernate, JPA 2, iBatis, Spring JDBC, Spring Data …\nEn matière de web services, il n’y a qu’à choisir : CXF, JAX-WS, JAX-RS, Spring WS, Restlet …\nMais pour écrire vos traitements par lot ? java.io ? Soyons fou : commons-io. Pas très sexy … la JSR-352 Java Batch de JEE 7 ? Optez pour l’original.\nAlors franchissez le pas et venez découvrir Spring Batch au cours d’un workshop basé sur un cas d’utilisation concret.\nSommaire de la présentation :\nIntroduction Présentation de l’étude de cas Périmètre fonctionnel Origine du projet de migration Objectifs du projet Mise en œuvre Décomposition du batch en une seule étape Vocable Spring Batch Diagramme de séquence de traitement d’un chunk Configuration d\u0026rsquo;un Job et d’un Step Reader Hibernate Quelques implémentations de reader disponibles Déclaration et implémentation d’un Item Processor Configuration des writers Quelques implémentations de Writers disponibles Extrait du diagramme de dépendance des beans Spring Gestion des transactions Gestion des erreurs Exécution du batch Démo Pour aller plus loin Conclusion Retours sur la migration vers Spring Batch Spring Batch en 3 mots ","link":"https://javaetmoi.com/2015/02/worskshop-etudes-de-cas-spring-batch/","section":"posts","tags":["spring-batch"],"title":"Etudes de cas Spring Batch"},{"body":" Désormais ancrées dans le quotidien des développeurs, les plateformes d’ intégration continue permettent de détecter rapidement tout problème de compilation, de tests en erreur ou même d’ ajout de défauts remontés par SonarQube. L’objectif fixé par le team leader est de ne pas faire échouer le build et, si c’est malheureusement le cas, tout arrêter pour le réparer. Sur certains projets, le gage donné au développeur ayant cassé le build est de ramener les viennoiseries le lendemain. Pour être certain de ne pas faire chauffer sa carte de paiement, une bonne pratique consiste à exécuter une ligne de commande maven (ou gradle) avant chaque commit dans le gestionnaire de code source. Cependant, sur certains changements que l’on juge mineur, il peut être tentant de passer outre. Aujourd’hui, les PC ou les Mac multi-coeurs avec SSD permettent de lancer un build sans freezer le poste de développement. C’est donc davantage par excès de confiance qu’à cause du temps d’attente qu’il arrive de casser Jenkins, Bamboo ou bien encore TeamCity.\nPour contrer tout oubli, il est possible de systématiser l’exécution du build Maven avant de commiter. Les outils de gestion de configuration SVN et Git offrent un mécanisme de hook. Lors de la phase de pre-commit, on va demander au SCM d’exécuter un script de hook chargé de vérifier le code source. En cas d’erreur, le commit est refusé. Ecrire de tels scripts n’est pas compliqué sous Linux car beaucoup d’exemples existent. Par contre, sous Windows, c’est plus rare. L’objet d e cet article est donc de vous donner des exemples de scripts de hook de pre-commit et de vous expliquer comment les configurer dans Tortoise SVN et Git.\nHook SVN Deux types de hook existent dans Subversion : des hooks clients et des hooks serveurs. Pour ne pas transformer le serveur SVN en serveur d’intégration continue, ce sont les hooks clients qui vont ici nous intéresser. Le script appelé par le hook n’est rien d’autre qu’un .bat. L’exemple de script pre-commit.bat ci-dessous est localisé dans le même répertoire que le POM reactor du projet. Un pré-requis est que Maven et Java sont dans le PATH de Windows.\n@echo on call mvn clean –f myapp-parent\\pom.xml \u0026gt; target\\pre-commit.log if not exist target mkdir target call mvn install sonar:sonar -Dsonar.analysis.mode=preview -Dsonar.issuesReport.html.enable=true -Dsonar.buildbreaker.skip=false –f myapp-parent\\pom.xml \u0026gt; target\\pre-commit.log echo Maven error code: %ERROR_CODE% cmd /C exit /B %ERROR_CODE% Afin de pouvoir être consultée en cas d’échec du build, la sortie console est redirigée dans un fichier de logs. Le code d’erreur de maven est retourné à SVN qui sait l’interpréter. Tout code différent de 0 fait échouer le commit. Exécuté dans à la racine de l’arborescence, le paramètre –f myapp-parent\\pom.xml permet de spécifier à maven où se trouve le POM parent (structure de type flat module). Nul besoin d’ajouter ce paramètre lorsque le POM parent se situe à la racine du projet.\nTortoiseSVN permet de configurer un hook client à 2 niveaux :\nDe manière globale pour tous les repos SVN du poste de dév. Chaque développeur doit individuellement configurer TortoiseSVN. Cette configuration peut être problématique lorsqu’un développeur est amené à travailler sur plusieurs repos et que le script de hook n’est pas assez générique. Le script précédent devra être généralisé pour fonctionner avec l’ensemble des projets. De manière unitaire pour chaque repo SVN. A l’instar d’une propriété svn:ignore, TortoiseSVN ajoute récursivement une propriété tsn:precommithook au niveau du repository SVN. Tous les développeurs bénéficient alors de ce hook. Etapes de configuration du mode global :\nDepuis n’importe quel répertoire, sélectionner le menu contextuel T ortoiseSVN \u0026gt; Settings Se rendre dans le menu Hook Scripts Renseigner les champs suivants : Hook type : pre-commit Woking copy path : chemin vers le pom parent de l\u0026rsquo;application Command line to execute : pre-commit.bat Etapes de configuration du mode local :\nSélectionner le répertoire racine du projet SVN Ouvrir le menu contextuel et sélectionner le menu TortoiseSVN \u0026gt; Properties Cliquer sur le bouton New \u0026gt; Local Hooks (création d\u0026rsquo;une property tsvn:precommithook) Choisir les paramètres suivants : Hook Type : Start Commit Hook Command Line to Execute : %REPOROOT+%/myproject-parent/pre-commit.bat Cocher les options suivantes : Wait for the script to finish Always execute the script Apply property recursively Commiter ces modifications de manière récursive sur l\u0026rsquo;ensemble des sous répertoires du projet SVN Remarque : les hooks clients sont une fonctionnalité propre à TortoiseSVN. De ce fait, le client en ligne de commande svn.exe ou bien encore le plugin Subversive d’Eclipse ne reconnaissent pas la propriété tsvn:precommithook.\nHook Git Git appartenant à la catégorie des DVCS, il n’existe pas de hook server. La configuration s’effectue donc au niveau du repository Git local. Les scripts de hook sont à positionner dans le sous-répertoire .git\\hooks. Par défaut, ce répertoire contient des exemples post-fixés par l’extension .sample. Le nom des scripts est conventionné et correspond au nom de la phase à laquelle il est exécuté. Ainsi, le script de hook exécuté avant le commit se nomme pre-commit. Sous Windows, Msysgit est le client Git le plus populaire. Basé sur les utilitaires Msys, le script ne doitpas être écrit en script batch comme c’est le cas avec SVN, mais en bash Linux.\nVoici un exemple de sript shell pre-commit :\n#!/bin/sh echo \u0026#34;Executing pre-commit\u0026#34; # Set Java and Maven: export JAVA_HOME=\u0026#34;C:/dev/jdk/1.7.0_45\u0026#34; export MAVEN_OPTS=\u0026#34;-Xmx1024m -XX:MaxPermSize=256m\u0026#34; export MAVEN_HOME=\u0026#34;C:/dev/maven/apache-maven-3.2.3\u0026#34; echo \u0026#34;Running Maven clean install for errors and Sonar for testing if the project fails its quality gate.\u0026#34; # Retrieving current working directory CWD=`pwd` MAIN_DIR=\u0026#34;$( cd \u0026#34;$( dirname \u0026#34;${BASH_SOURCE[0]}\u0026#34; )\u0026#34; \u0026amp;\u0026amp; pwd )\u0026#34; # Go to main project dir cd $MAIN_DIR/../../myapp-parent # Running Maven clean install and Sonar $MAVEN_HOME/bin/mvn clean install -U sonar:sonar -Dsonar.analysis.mode=preview -Dsonar.issuesReport.html.enable=true -Dsonar.buildbreaker.skip=false if [ $? -ne 0 ]; then echo \u0026#34;Error while compiling or testing the code\u0026#34; # Go back to current working dir cd $CWD exit 1 fi # Go back to current working dir cd $CWD exit 0 La commande cd $MAIN_DIR/../../myapp-parent permet de positionner dans le répertoire contenant le POM parent. Ce chemin est à adapter selon la structure du projet.\nConclusion Que ce soit avec Git ou avec TortoiseSVN, il est possible de systématiser l’appel à commande mvn clean install(ou à tout autre script) avant de commiter. Cette bonne pratique fait en sorte que le code historisé dans le gestionnaire de code source est toujours stable. L’intérêt de maintenir un serveur d’intégration continue peut donc se poser. Dès 2009, David Gageot montrait d’ailleurs qu’il était possible de monter une intégration continue sans serveur. Pour autant, les serveurs d’intégration continue tels que Jenkins ont aujourd’hui davantage de responsabilités que par le passé : exécution des tests d’intégration, des tests Selenium et des tests de montée en charge, packaging, déploiement d’applications sur les différents environnements … Leurs jours ne sont donc pas comptés !\n","link":"https://javaetmoi.com/2015/01/hook-svn-git-maven-windows/","section":"posts","tags":["git","maven","svn"],"title":"Hook SVN et Git pour Maven sous Windows"},{"body":"","link":"https://javaetmoi.com/tags/svn/","section":"tags","tags":null,"title":"Svn"},{"body":"L\u0026rsquo;écosystème des technologies supportées par Spring vous est familier. L\u0026rsquo;inversion de contrôle et l\u0026rsquo;injection de dépendances n\u0026rsquo;ont plus de secret pour vous. Les supports de JDBC, de JPA et d\u0026rsquo;Hibernate sont pour vous une réalité. Vous êtes conquis par la magie de Spring Data JPA. Vous avez testé QueryDSL. Alors n\u0026rsquo;attendez plus !!\nLes secrets du conteneur Spring vous attendent pour ce 3ième workshop sur le framework Spring : module spring-test, injection de beans de portées différentes, support des JSR 250 et 330, post-processeurs de beans, fichiers de configuration et abstraction de l’accès aux ressources.\nConçu en décembre 2011, et faisant suite aux 2 billets précédents, le support de présentation de ce workshop reste d’actualités avec Spring 4.x. Seule la syntaxe XML pourrait de nos jours être réécrites en Java.\nVoici le sommaire complet de la présentation :\nTests unitaires et d\u0026rsquo;intégration Les apports du module Spring test avec JUnit, DbUnit et Mockito Zoom sur le composant test d’un projet d’entreprise Techniques avancées du conteneur Spring Injection de beans de portées différentes Support des JSR 250 et 330 Usage des post-processeurs de bean Spring Externalisation de la configuration Accès aux ressources externes ","link":"https://javaetmoi.com/2014/12/tests-et-techniques-avancees-conteneur-spring/","section":"posts","tags":null,"title":"Tests et techniques avancées du conteneur Spring"},{"body":"EhCache est sans nul doute le framework open source de gestion de cache applicatif le plus populaire parmi les développeurs Java. Polyvalent, EhCache peut être mis en œuvre dans les différentes couches d’une application web :\nPersistance : utilisé comme cache de niveau 2 de JPA / Hibernate pour stocker des entités et le résultat des requêtes en base. Service métier : mise en cache du résultat d’un service métier ou d’un appel de web service Présentation : cache de pages ou de fragments de page HTML Habitués à gérer la sécurité et les transactions de manière déclarative à l’aide d’annotations, le projet open source ehcache-spring-annotations a fait le bonheur des développeurs Spring en 2010 en introduisant l’ annotation @Cacheable Hébergé sur Google Code, ce projet n’est aujourd’hui plus maintenu. Il ne supporte pas Spring 4.x. Rattrapant son retard, la version 3.1 du framework Spring a été enrichi de sa propre annotation @Cacheable. Comme à son habitude, Spring permet de s’abstraire de la solution de cache sous-jacente (ex : ConcurrentMap, EhCache, Guava …) en proposant une API générique. Les débuts de cette API ont été difficiles (cf. SPR-10237 à laquelle j’ai participé). Aujourd’hui mature, implémentant la JSR-107 JCache, il n’y a aucune raison pour ne pas migrer dessus. Relativement court , ce billet explique pas à pas comment migrer de ehcache-spring-annotations vers le support de cache du framework Spring.\nErreur rencontrée L’utilisation de la directive \u0026lt;ehcache:annotation-driven /\u0026gt; avec Spring 4.1 fait échouer le chargement du contexte Spring :\n2014-11-27 20:01:04,948 ERROR org.springframework.web.context.ContextLoader [319] - Context initialization failed org.springframework.beans.factory.BeanDefinitionStoreException: Unexpected exception parsing XML document from class path resource [com/javaetmoi/demo/services/applicationContext-cache.xml]; nested exception is java.lang.IllegalArgumentException: \u0026#39;beanName\u0026#39; must not be empty Migration Ce paragraphe dresse les étapes à suivre pour de désendetter du projet ehcache-spring-annotations et utiliser le support offert par les versions 3.1 et supérieures du framework Spring.\nDépendances Dans le pom.xml , supprimer la dépendance vers ehcache-spring-annotations :\n\u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.googlecode.ehcache-spring-annotations\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;ehcache-spring-annotations\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.2.0\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; Si vous ne l’utilisez pas encore, ajouter la dépendance suivante. Elle contient la fabrique EhCacheManagerFactoryBean que nous utiliserons par la suite.\n\u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt; spring-context-support \u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;4.1.2.RELEASE \u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; Si nécessaire, mettez également à jour votre version d’EhCache. En effet, Spring 4.1 requière EhCache 2.5 ou supérieur.\nConfiguration Spring Plusieurs changements sont à réaliser dans le fichier de configuration XML où est déclaré le cache applicatif.\n1. Commencer par remplacer le namespace XML ehcache :\nSource :\nxmlns:ehcache=\u0026#34;http://ehcache-spring-annotations.googlecode.com/svn/schema/ehcache-spring\u0026#34; Cible :\nxmlns:cache=\u0026#34;http://www.springframework.org/schema/cache\u0026#34; 2. Changer ensuite le xsi:schemaLocation du namespace\nSource :\nhttp://ehcache-spring-annotations.googlecode.com/svn/schema/ehcache-spring http://ehcache-spring-annotations.googlecode.com/svn/schema/ehcache-spring/ehcache-spring-1.0.xsd Cible :\nhttp://www.springframework.org/schema/cache http://www.springframework.org/schema/cache/spring-cache-4.1.xsd 3. Modifier l’activation des annotations\nSource : \u0026lt;ehcache:annotation-driven /\u0026gt;Cible : cache:annotation-driven/\nSi vous utilisez la configuration Java, vous pouvez utiliser à la place l’annotation @EnableCaching. Par défaut, cacheManager est le nom du bean gestionnaire de cache associé aux annotations.\nAjouter le bean cacheManager référençant la fabrique de gestionnaire de cache : \u0026lt;bean id=\u0026#34;cacheManager\u0026#34; class=\u0026#34;org.springframework.cache.ehcache.EhCacheCacheManager\u0026#34;\u0026gt; \u0026lt;property name=\u0026#34;cacheManager\u0026#34; ref=\u0026#34;ehcacheManagerFactory\u0026#34;/\u0026gt; \u0026lt;/bean\u0026gt; La déclaration de fabrique ehcacheManagerFactory ne bouge pas :\n\u0026lt;bean id=\u0026#34;ehcacheManagerFactory\u0026#34; class=\u0026#34;org.springframework.cache.ehcache.EhCacheManagerFactoryBean\u0026#34;\u0026gt; \u0026lt;property name=\u0026#34;configLocation\u0026#34; value=\u0026#34;classpath:com/javaetmoi/demo/services/ ehcache.xml\u0026#34;/\u0026gt; \u0026lt;property name=\u0026#34;shared\u0026#34; value=\u0026#34;true\u0026#34;/\u0026gt; \u0026lt;/bean\u0026gt; Refactoring Java La dernière étape consiste à refactorer les classes Java utilisant les annotations du projet ehcache-spring-annotations :\nModifier le package de l\u0026rsquo;annotation @Cacheable de com.googlecode.ehcache.annotations pàar org.springframework.cache.annotation. L\u0026rsquo;attribut cacheName de la précédente annotation devient value. L’annotation standardisée javax.cache.annotation.CacheResult peut également être employée Remplacer l\u0026rsquo;annotation @com.googlecode.ehcache.annotations.TriggersRemove par @org.springframework.cache.annotation.CacheEvict. L\u0026rsquo;attribut cacheName devient value. Et l\u0026rsquo;attribut removeAll devient allEntries. L’annotation standardisée javax.cache.annotation.CacheRemove peut également être employée Conclusion La migration ne pose aucune difficulté technique. L’équivalence des annotations nécessite de consulter la documentation des 2 projets. Une fois configuré, vous pourrez alors changer de solution de cache sans toucher au code Java. A vous le cache distribué et élastique de GemFire !\nRéférences :\nChapitre Cache Abstraction du manuel de reference du framework Spring Comparaison Spring Cache vs ehcache-spring-annotations Tutorial Spring Cache ","link":"https://javaetmoi.com/2014/12/migration-projet-ehcache-spring-annotations/","section":"posts","tags":["ehcache","spring-framework"],"title":"Désendettement du projet ehcache-spring-annotations"},{"body":"","link":"https://javaetmoi.com/tags/jdo/","section":"tags","tags":null,"title":"Jdo"},{"body":"Dans le 1er workshop, je vous présentais l\u0026rsquo;écosystème de Spring et les bases du conteneur léger. Ce second workshop se focalise sur une couche centrale de la majorité des applications d\u0026rsquo;entreprise : la persistance de données. Vous verrez que Spring facilite la gestion des exceptions et des transactions. En supprimant le boilerplate code, Spring facilite également l\u0026rsquo;accès aux différentes technologies de persistance : que ce soit JDBC, Hibernate ou bien encore JPA. Enfin, nous verrons comment les projets Spring Data JPA et QueryDSL simplifient encore davantage l\u0026rsquo;écriture de la couche d\u0026rsquo;accès aux données.\nVoici le support de présentation du workshop sur Spring que j’ai animé en décembre 2011 au sein de ma société et que j’ai réactualisé avant sa diffusion sur Slideshare :\nAu sommaire du workshop :\nLa persistance des données facilitée Gestion des exceptions Déclarer une source de données Design pattern Template Method Support pour JDBC Support pour Hibernate et JPA Encore plus loin avec : Spring Data JPA et QueryDSL Gestion des transactions Support des transactions Déclaration avec l\u0026rsquo;annotation @Transactional Annotations versus AOP ","link":"https://javaetmoi.com/2014/11/persistance-spring-jdbc-hibernate-jpa/","section":"posts","tags":["hibernate","jdbc","jdo","jpa","querydsl","spring-data","transaction"],"title":"Persistance de données avec Spring"},{"body":"","link":"https://javaetmoi.com/tags/querydsl/","section":"tags","tags":null,"title":"Querydsl"},{"body":"","link":"https://javaetmoi.com/tags/jruby/","section":"tags","tags":null,"title":"Jruby"},{"body":"","link":"https://javaetmoi.com/tags/plugin/","section":"tags","tags":null,"title":"Plugin"},{"body":" Ce billet a pour objectif de vous présenter un cas d’usage du plugin exit pour LogStash. Une utilisation répandue de LogStash consiste à alimenter Elasticsearch à partir de fichiers de logs. Extensible via un mécanisme de plugins, LogStash sait gérer plusieurs types de source et plusieurs types de destinations. Une utilisation alternative de LogStash consiste à l’utiliser comme batch d’indexation. Le fichier à indexer a une fin. Et l’utilisateur souhaite que LogStash s’arrête une fois les données importées.\nSolution possible A l’instar de la commande tail, LogStash est conçu pour attendre la suite d’un fichier. Il n’a pas connaissance de la fin de fichier. Une première étape consiste donc à informer LogStash que la dernière ligne du fichier a été atteinte et qu’il peut s’arrêter. Une solution possible est donnée comme réponse à la la question « How to automatically kill a logstash agent when tests are done » posée sur Stackoverfow. Elle consiste à ajouter un tag endfile lorsqu’un pattern est détecté dans le message (la ligne) qui vient d’être lue, ici la chaîne END FILE :\nfilter { if [message] =~ \u0026#34;^END FILE\u0026#34; { mutate { add_tag =\u0026gt; [\u0026#34;endfile\u0026#34;] } } } L’ajout du tag est réalisé dans la section filter. Cette solution nécessite d’avoir la main sur le fichier : soit en le générant, soit en ayant la possibilité de lui concaténer une ligne END FILE.\nDans la section output, avant d’écrire le message, on vérifie si le tag endfile existe. Si c’est le cas, on quitte l’agent LogStash en faisant appel au plugin exit :\noutput { if \u0026#34;endfile\u0026#34; in [tags] { exit { } } stdout { } } Le plugin exit Le plugin exit pour LogStash est écrit en JRuby. Il a été testé sous LogStash 1.4.2. Son code source est disponible dans le repo https://github.com/arey/logstash-exit-plugin\nLes commandes suivantes permettent de le récupérer et de le tester :\ngit clone git://github.com/arey/logstash-exit-plugin.git cd logstash-exit-plugin set LOGSTASH_HOME= %LOGSTASH_HOME%\\bin\\logstash agent \u0026ndash;pluginpath .\\plugins -f exit-example.conf Un effet de bord lié à l\u0026rsquo;implémentation de ce plugin est que la JVM s’arrête brutalement. Et ceci, alors que les plugins chargés d’écrire les messages ont peut-être encore des données à flusher. Le plugin permet de contourner ce problème en prévoyant une pause avant l’arrêt subite de la JVM (paramètre pause_second). Sans le garantir à 100%, une pause de 10 secondes devrait donner suffisamment de temps aux writers pour terminer leur job.\nUn autre usage de ce plugin consiste à sortir de LogStash dès qu’une erreur est rencontrée. Le paramètre exit_code permet de spécifier le code d’erreur retournée par la JVM à l’appelant.\nPour les plus curieux, le code du plugin est des plus trivial :\nExtrait de la classe ruby Exit Conclusion Par faute d’avoir trouvé mieux, le plugin exit sort brutalement de la JVM (équivalent à un System.exit()). Une solution plus élégante serait de demander à LogStash de s’arrêter et de pouvoir propager l’évènement LogStash::ShutdownSignal. Je fais appel à la communauté LogStash pour m’indiquer comment procéder.\nEnfin, rattaché à la section output, un plugin similaire pourrait également être codé au niveau de la section filter. A vous de jouer !\n","link":"https://javaetmoi.com/2014/11/plugin-exit-pour-logstash/","section":"posts","tags":["elasticsearch","jruby","logstash","plugin"],"title":"Plugin exit pour LogStash"},{"body":"Rejoignez les millions de développeurs Spring. De par sa forte pénétration dans les entreprises, tout développeur Java /JEE a ou aura à travailler sur une application s’appuyant sur Spring. Or Spring dépasse le cadre du simple framework open source. Cette série de 5 workshops a pour objectif de faire un tour d’horizon de l’écosystème des technologies supportées par Spring avant de se focaliser plus spécifiquement sur certaines d’entre elles. Retours d’expérience, bonnes pratiques, techniques avancées seront de partie.\nPropulsée dans Java EE 6 avec CDI et plus récemment au sein de JavaScript avec Google Gin, l’injection de dépendance sera au cœur du premier workshop. Voici le support de présentation du workshop sur Spring que j\u0026rsquo;ai animé en novembre 2011 au sein de ma SSII et que j\u0026rsquo;ai réactualisé avant sa diffusion sur Slideshare.\nWorkshop Spring - Session 1 - L\u0026rsquo;offre Spring et les bases from Antoine Rey\nAu sommaire du workshop :\nZoom sur le portfolio Spring Source / Pivotal Le cœur du framework Spring : IoC, AOP et support Le support proposé par Spring : persistance, présentation, communication, test, outils … Les fondamentaux 1. Fonctionnement du conteneur léger 2. Les beans Spring 3. Les design patterns rencontrés dans Spring ","link":"https://javaetmoi.com/2014/11/workshop-spring-1-offre-spring-et-bases/","section":"posts","tags":["présentation","spring-framework","workshop"],"title":"L'offre Spring et les bases"},{"body":"","link":"https://javaetmoi.com/tags/workshop/","section":"tags","tags":null,"title":"Workshop"},{"body":" Le développement d’applications web requière une vigilance toute particulière quant à l’utilisation de la session web. Spring MVC offre les mécanismes permettant aux développeurs de ne plus manipuler directement l’objet HttpSession mis à disposition par le conteneur web. Les 2 annotations @Scope(\u0026ldquo;session\u0026rdquo;) et @SessionAttributes en font parties. Dans ce billet, je vous expliquerai le fonctionnement de l’annotation @SessionAttributes qu’il est essentiel de maitriser avant d’utiliser. Nous verrons qu’elle fonctionne de pair avec l’annotation @ModelAttribute et qu’elle permet de simuler une portée conversation. Nous commencerons cet article par rappeler ce qu’est un modèle et nous le terminerons en testant unitairement du code qui utilise @SessionAttributes.\nLe modèle de Spring MVC Comme son nom l’indique, Spring MVC est un framework de présentation basé sur le pattern M odel V iew C ontroller. Un modèle est mis à disposition de la vue par le contrôleur, par exemple pour alimenter les listes déroulantes lors du rendu de la page HTML. Un modèle peut également être soumis au contrôleur par la vue (post de formulaire) ; on parle alors de « command object ». La conversion de données (ou binding) entre des chaînes de caractères du protocole HTTP et la représentation Java du modèle est assuré par les Converter et les Formatter configurés au démarrage du contexte Spring ou via l’annotation @InitBinder pour du sur-mesure. Un binding bi-directionnel est mis en œuvre sur un modèle utilisé conjointement pour le rendu de la page et la soumission de données (ex : formulaire d’édition). Spring MVC représente le modèle comme un ensemble de clé-valeur (tableau associatif). La clé est une chaine de caractère. La valeur peut-être de n’importe quel type. La classe ModelMap implémente cette représentation. Elle étend la classe java.util.LinkedHashMap.\nDans les contrôleurs Spring MVC, il est possible de manipuler l’interface Model pour ajouter manuellement des données au modèle soit directement, soit par l’utilisation de la classe ModelAndView. Voici un exemple tiré du manuel de référence de Spring Framework :\n@ModelAttribute public void populateModel(@RequestParam String number, Model model) { model.addAttribute(accountManager.findAccount(number)); } Pour implémentation de l’interface Model, Spring MVC utilise la classe BindingAwareModelMap qui étend indirectement ModelMap. Dans cet exemple, l’instance renvoyée par l’appel à la méthode findAccount est de type Account. La clé est calculée par convention de nommage via la classe org.springframework.core.Conventions. Il est bien entendu possible d’utiliser la méthode model.addAttribute(\u0026ldquo;account\u0026rdquo;, accountManager.findAccount(number)); pour spécifier une clé.\nL’enrichissement du modèle peut également être réalisé sans manipulation de l’interface Model :\n@ModelAttribute public Account addAccount(@RequestParam String number) { return accountManager.findAccount(number); } Sur mes applications, je privilégie cette seconde syntaxe qui est moins verbeuse et permet de découper le code en autant de méthodes que d’objets à ajouter dans le modèle.\nD’un point de vue technique, les 2 exemples présentés ci-dessus sont équivalents. Concentrons-nous à présent sur le rôle de L’annotation @ModelAttribute.\nAnnotation @ModelAttribute sur les méthodes Le comportement de l’annotation @ModelAttribute diffère en fonction de là où elle est apposée :\nsur les méthodes des contrôleurs sur les paramètres des méthodes des contrôleurs. Dans les exemples précédents, l’annotation @ModelAttribute annote une méthode d’un contrôleur. Elle indique à Spring MVC que la méthode est responsable de préparer le modèle. A noter que plusieurs méthodes d’un même contrôleur peuvent être annotés avec @ModelAttribute. Spring MVC appelle toutes les méthodes @ModelAttribute avant d’appeler la méthode @RequestMapping (également appelé handler) chargée de traiter la requête HTTP en appelant les services métiers.\nLes données ajoutées au modèle dans les méthodes @ModelAttributes sont ensuite accessibles à la méthode @RequestMapping.\nDans le second exemple, la méthode addAccount renvoie un Account sans manipuler l’interface Model. Spring MVC sait implicitement que l’objet retourné par une méthode @ModelAttribute doit être ajouté au modèle. Pour la clé, il utilise les mêmes conventions de nommage que la méthode addAttribute(Object attributeValue) . Il est possible de spécifier la clé en utilisant la syntaxe @ModelAttribute(\u0026ldquo;account\u0026rdquo;) .\nUne fois l’appel à la méthode @RequestMapping réalisé, et avant le rendu de la vue, Spring MVC doit mettre à disposition de la vue le modèle. Par défaut, Spring MVC utilise les attributs de la requête. Tout se joue dans la méthode exposeModelAsRequestAttributes de la classe AbstractView. Les objets du modèle sont ajoutés aux attributs de la requête comme on pourrait le faire en manipulant l’API Servlet : request.setAttribute(modelName, modelValue);\nLorsque le mode debug est activée, la trace suivante est généré dans les logs :\n18:42:41.702 [qtp20079748-21] DEBUG o.s.web.servlet.view.JstlView - Added model object \u0026#39;account\u0026#39; of type [com.javaetmoi.core.mvc.demo.model.Account] to request in view with name \u0026#39;accountdetail\u0026#39; Ici, la vue est une JSP utilisant les tags JSTL.\nDans le corps de la page JSP, il est alors possible d’utiliser une Expression Language (EL) évaluant les propriétés du modèle : \u0026lt;c:out value=\u0026quot;${account.number}\u0026quot; /\u0026gt;\nAttention aux performances\nL’annotation @ModelAttribute peut causer des problèmes de performance si l’on ne maîtrise pas son cycle d’appel dans les contrôleurs de Spring MVC.\nEn effet, l\u0026rsquo;appel systématique aux méthodes @ModelAttribute à chaque rafraichissement de page peut détériorer les performances d’une application lorsqu’un appel à un ou plusieurs web services et/ou DAO est nécessaire pour construire le modèle.\nL\u0026rsquo;utilisation de l\u0026rsquo;annotation @SessionAttributes ou d\u0026rsquo;un cache applicatif permet d\u0026rsquo;enrayer ce type de déconvenue.\nL’annotation @SessionAttributes Les handlers des contrôleurs Spring MVC (annotés avec @RequestMapping) acceptent en paramètre de nombreux types de paramètres ; les interfaces HttpSession et HttpServletRequest en font partie. Un développeur peut donc directement manipuler HttpSession pour ajouter en session des données du modèle qu’ils voudraient voir conserver sur plusieurs requêtes HTTP.\nAfin de simplifier le code d’accès à la session web, et toujours dans l’idée d’éviter de manipuler directement la session, Spring MVC propose l’annotation @SessionAttributes. Cette annotation se déclare au niveau de la classe de type @Controller. Ses 2 propriétés value et type permettent de lister respectivement le nom des modèles (le nom des clés) et/ou le type de modèle à sauvegarder de manière transparente dans la session HTTP.\nAvant le rendu de la vue, Spring MVC copie par référence les attributs du modèle référencés par @SessionAttributes dans la session. Les attributs du modèle seront alors à la fois disponible en tant qu’attribut de la requête ( HttpServletRequest) et de la session ( HttpSession). Pour persister les données du modèle en session, Spring MVC utilise l’abstraction SessionAttributeStore. L’implémentation par défaut repose sur la session HTTP. Mais on pourrait très bien imaginer une implémentation utilisant un cache de données distribué (type Redis ou GemFire) ou une base NoSQL. Gains escomptés de cette approche :\nAffinité de session plus nécessaire Tolérance aux pannes renforcées Livraisons sans interruption de service Cette ouverture sera peut-être prochainement exploitée par le nouveau projet spring-session.\nUne autre facilité apportée par l’annotation @SessionAttributes est d’ éviter au développeur de tester si un objet existe déjà en session avant de l’instancier/ou de le récupérer puis de l’ajouter à la session. En effet, avant d’invoquer la méthode @RequestMapping cible, Spring MVC commence par initialiser le modèle du contrôleur (méthode RequestMappingHandlerAdapter#invokeHandleMethod). Dans un premier temps, il restaure les attributs du modèle qui sont en session(méthode ModelFactory# initModel). Dans un second temps, il itère sur les méthodes annotées par @ModelAttributes(méthode ModelFactory#invokeModelAttributeMethods). Avant d’appeler chaque méthode @ModelAttributes, il vérifie si l’attribut retourné n’existe pas déjà dans le modèle (et donc préalablement en session).\nLe diagramme d’activités ci-dessous illustre le mécanisme complet :\nLibérer la mémoire A présent que nous avons vu comment ajouter des données en session, apprenons à les retirer, et ceci toujours sans manipuler l’interface HttpSession. Pour se faire, Spring MVC met à disposition l’ interface SessionStatus. La méthode setComplete() permet de supprimer de la session tous les attributs référencés par l’annotation @ModelAttributes du contrôleur où elle est appelée.\nComme le montre l’exemple de code tiré du projet spring-mvc-toolkit, Spring MVC sait passer au handler une instance de SessionStatus :\n@RequestMapping(\u0026#34;/endsession\u0026#34;) public String endSessionHandlingMethod(SessionStatus status){ status.setComplete(); return \u0026#34;sessionsattributepage\u0026#34;; } Lorsqu’un attribut a été retiré de la session (\u0026quot; myBean1\u0026quot; dans l’exemple ci-dessous) et que l’on cherche à initier le modèle à partir des données en session @SessionAttributes(\u0026ldquo;myBean1\u0026rdquo;), Spring MVC lève une HttpSessionRequiredException :\norg.springframework.web.HttpSessionRequiredException: Expected session attribute \u0026#39;myBean1\u0026#39; at org.springframework.web.method.annotation.ModelFactory.initModel(ModelFactory.java:103) at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandleMethod(RequestMappingHandlerAdapter.java:726) Démonstration Les explications données dans ce blog s’appuient sur des tests réalisés dans la branche SessionAttributes du projet spring-mvc-toolkit. Reprenant l’idée présentée dans le billet Understanding Spring MVC Model and SessionAttributes, les 2 contrôleurs MyController et OtherController tracent l’appel de méthodes et affichent le contenu du modèle, de la requête et de la session. La page sessionsattributepage.jsp affiche quant à elle le contenu de la requête et de la session.\nExtrait de la classe MyController:\nExtrait de la classe MyController.java Extrait de la classe OtherController:\nExtrait de la classe OtherController.java Voici les étapes à suivre pour exécuter l’application démo. Les prérequis sont d’avoir installé sur son post Git, Java 6 ou + et maven 3 ou + :\ngit clone git://github.com/arey/spring-mvc-toolkit.git git checkout SessionAttributes mvn clean install cd spring-mvc-toolkit-demo mvn jetty:run-war Naviguer sur http://localhost:8080/dosomething Nous allons décrire à présent les traces affichées et le contenu des pages observé lors de la navigation sur les liens.\nAppel à dosomething Traces observées lors de l’appel à http://localhost:8080/dosomething :\nInside of addMyBean1ToSessionScope Inside of addMyBean2ToRequestScope Inside of addMyOtherBeanAToSessionScope Inside of addMyOtherBeanBToSessionScope Inside of dosomething handler method --- Model data --- myBean1 -- MyBean [name=My Bean 1] myBean2 -- MyBean [name=My Bean 2] myOtherBeanA -- MyOtherBean [name=My Other Bean A] myOtherBeanB -- MyOtherBean [name=My Other Bean B] === Request data === *** Session data *** Page affichée dans le navigateur :\nAnalyse :\nLes 4 méthodes annotées par @ModelAttribute sont appelées avant la méthode @RequestMapping. Les beans créés par chacune de ces méthodes sont disponibles dans le modèle dès l’appel à la méthode @RequestMapping. Lors de l’appel à la méthode @RequestMapping, la requête et la session HTTP ne contiennent encore aucun attribut. Lors du rendu de la page, les 4 beans sont présents au niveau de la requête. Par contre, seul les 3 beans référencés par l’annotation @SessionAttributes( value=\u0026ldquo;myBean1\u0026rdquo;, types={MyOtherBean.class} ) sont présents en session. Premier appel à other Traces observées lors du clic sur le lien \u0026ldquo;/other\u0026rdquo; :\nInside of addMyBean3ToSessionScope Inside of other handler method MyBean [name=My Bean 1] --- Model data --- myBean3 -- MyBean [name=My Bean 3] myBean1 -- MyBean [name=My Bean 1] === Request data === *** Session data *** myOtherBeanA -- MyOtherBean [name=My Other Bean A] myOtherBeanB -- MyOtherBean [name=My Other Bean B] myBean1 -- MyBean [name=My Bean 1] Page affichée dans le navigateur :\nAnalyse :\nLors de l’appel à la méthode @RequestMapping:\nles 2 beans référencés par l’annotation @SessionAttributes({\u0026ldquo;myBean1\u0026rdquo;, \u0026ldquo;myBean3\u0026rdquo;}) sont disponibles dans le modèle, les beans myOtherBeanA et myOtherBeanB sont présents en session mais pas recopiées dans le modèle Lors du rendu de la page JSP : Le bean myBean3 créé par le contrôleur est ajouté à la session qui compte désormais 4 beans\nAppel à endsession Trace observée lors du clic sur le lien \u0026ldquo;/endession\u0026rdquo; :\nInside of addMyBean2ToRequestScope --- Model data --- myOtherBeanA -- MyOtherBean [name=My Other Bean A] myOtherBeanB -- MyOtherBean [name=My Other Bean B] myBean1 -- MyBean [name=My Bean 1] myBean2 -- MyBean [name=My Bean 2] === Request data === *** Session data *** myOtherBeanA -- MyOtherBean [name=My Other Bean A] myOtherBeanB -- MyOtherBean [name=My Other Bean B] myBean1 -- MyBean [name=My Bean 1] myBean3 -- MyBean [name=My Bean 3] Page affichée dans le navigateur :\nAnalyse :\nL’URL /endession est mappée sur le contrôleur MyController déjà utilisé lors du 1er accès à l’URL /dosomething Seule l’une des 4 méthodes annotées avec @ModelAttribute est appelée : addMyBean2ToRequestScope. Les 3 autres méthodes ne sont pas appelées car les beans qu’elles créent sont déjà présent en session. L’appel à la méthode setComplete(); ne retire pas instantanément les beans de la session mais joue le rôle de marqueur. Les beans référencés par MyController sont supprimés de la session avant le rendu de la page JSP. Bien que supprimés de la session, ils sont disponibles dans le scope request. Second appel à other Injecté dans le handler, le bean myBean1 n\u0026rsquo;est plus disponible en session. public String otherHandlingMethod(Model model, HttpServletRequest request, HttpSession session, @ModelAttribute(\u0026ldquo;myBean1\u0026rdquo;) MyBean myBean) {\nTests unitaires La mise au point de tests unitaires ou de tests d’intégration mettant en jeu ou plusieurs contrôleurs annotés avec @SessionAttributes nécessite un travail supplémentaire. En effet, lorsque le handler d’un contrôleur s’appuie sur une donnée qui devrait être présente en session, il est nécessaire d’utiliser la méthode sessionAttr pour passer au contrôleur la donnée attendue. Par ailleurs, entre 2 appels de handler, Spring Test ne conserve pas les données sauvegardées en session. Lors du 2ième appel, il est donc sorte nécessaire de réinjecter la donnée créée lors du premier appel. La classe MvcResult permet d’accéder au résultat du 1er appel. Le test unitaire SessionAttributesTest monte un exemple d’utilisation :\nExtrait de la classe SessionAttributesTest.java Conclusion Introduite depuis Spring 2.5, l’ annotation @SessionAttributes n’a pas d’équivalent dans d’autres frameworks MVC. Je pense par exemple à Struts. Son utilisation demande de comprendre son fonctionnement et la « magie » qu’on peut lui prêter. J’espère que cet article vous aura permis de démystifier ces mécanismes. La prochaine fois que vous l’utiliserez, je vous invite à vous référer au diagramme présenté au milieu de ce billet. N’hésitez pas non plus à cloner le projet spring-mvc-toolkit et à jouer avec la branche SessionAttributes.\nRéférences :\nManuel de référence du framework Spring MVC Understanding Spring MVC Model and SessionAttributes Projet Spring Session Power of Spring\u0026rsquo;s @ModelAttribute and @SessionAttributes Spring MVC - Session Attributes handling ","link":"https://javaetmoi.com/2014/10/annotation-sessionattributes-modelattribute-spring-mvc/","section":"posts","tags":["mvc","spring-mvc","test"],"title":"Démystifier l’annotation @SessionAttributes de Spring MVC"},{"body":"","link":"https://javaetmoi.com/tags/mvc/","section":"tags","tags":null,"title":"Mvc"},{"body":"","link":"https://javaetmoi.com/tags/bean-validation/","section":"tags","tags":null,"title":"Bean-Validation"},{"body":"","link":"https://javaetmoi.com/tags/html-5/","section":"tags","tags":null,"title":"Html-5"},{"body":"","link":"https://javaetmoi.com/tags/jsp/","section":"tags","tags":null,"title":"Jsp"},{"body":"","link":"https://javaetmoi.com/tags/tag/","section":"tags","tags":null,"title":"Tag"},{"body":" Cet article explique comment étendre Spring MVC pour générer le code HTML 5 des champs de saisie (input fields) à partir des annotations Bean Validation (JSR 330) apposées sur des Entités ou de simples DTO.\nDans une application web , valider les écrans de saisie côté client permet de donner un retour rapide à l’utilisateur. Avant HTML 5, le développeur web était bien démuni pour implémenter ces contrôles de surface sur le Navigateur. Certes, HTML 4 permettait de spécifier la taille max des champs de saisie (balise maxLength) et leur caractère obligatoire ou non (balise required). Les autres contrôles effectués côté serveur étaient alors bien souvent recodés en JavaScript à l’aide de jQuery, de CSS et de quelques plugins. Aujourd’hui, HTML 5 se démocratise et le code JavaScript de validation devrait bientôt s’alléger drastiquement. En effet, cette spécification permet de standardiser la validation des champs de saisie côté client. Le développeur a désormais la possibilité de spécifier le type de champs (ex : nombre, date, URL …), des valeurs min et max ou bien encore un pattern de validation à l’aide d’une expression régulière.\nValidation HTML 5 Dans l’exemple ci-dessous exploitant les capacités du HTML 5, Google Chrome gère nativement la validation du formulaire et l’affichage du message d’erreur. Les icones sont obtenus à l’aide d’un style CSS utilisant les pseudo-classes input:required:invalid, input:focus:invalid et input:required:valid.\nVoici la représentation HTML de ce formulaire :\n\u0026lt;form id=\u0026#34;customer\u0026#34; action=\u0026#34;/htmlvalidation\u0026#34; method=\u0026#34;post\u0026#34;\u0026gt; \u0026lt;div\u0026gt; \u0026lt;label\u0026gt;First Name\u0026lt;/label\u0026gt; \u0026lt;input id=\u0026#34;firstName\u0026#34; type=\u0026#34;text\u0026#34; required=\u0026#34;required\u0026#34; /\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;div\u0026gt; \u0026lt;label\u0026gt;Last Name\u0026lt;/label\u0026gt; \u0026lt;input id=\u0026#34;lastName\u0026#34; type=\u0026#34;text\u0026#34; required=\u0026#34;required\u0026#34; /\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;div\u0026gt; \u0026lt;label\u0026gt;Address\u0026lt;/label\u0026gt; \u0026lt;input id=\u0026#34;address\u0026#34; type=\u0026#34;text\u0026#34; maxlength=\u0026#34;20\u0026#34; /\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;div\u0026gt; \u0026lt;label\u0026gt;City\u0026lt;/label\u0026gt; \u0026lt;input id=\u0026#34;city\u0026#34; type=\u0026#34;text\u0026#34; required=\u0026#34;required\u0026#34; /\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;div\u0026gt; \u0026lt;label\u0026gt;Telephone\u0026lt;/label\u0026gt; \u0026lt;input id=\u0026#34;telephone\u0026#34;type=\u0026#34;text\u0026#34; maxlength=\u0026#34;10\u0026#34; /\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;div\u0026gt; \u0026lt;label\u0026gt;Email\u0026lt;/label\u0026gt; \u0026lt;input id=\u0026#34;email\u0026#34; type=\u0026#34;email\u0026#34; /\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;div\u0026gt; \u0026lt;label\u0026gt;Website URL\u0026lt;/label\u0026gt; \u0026lt;input id=\u0026#34;website\u0026#34; type=\u0026#34;url\u0026#34; /\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;div\u0026gt; \u0026lt;label\u0026gt;Age\u0026lt;/label\u0026gt; \u0026lt;input id=\u0026#34;age\u0026#34; type=\u0026#34;number\u0026#34; max=\u0026#34;99\u0026#34; min=\u0026#34;18\u0026#34; /\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;div\u0026gt; \u0026lt;button type=\u0026#34;submit\u0026#34;\u0026gt;Add Customer\u0026lt;/button\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/form\u0026gt; Dans la suite de cet article, nous verrons comment Spring MVC peut générer ce code HTML 5.\nAttention toutefois, chaque navigateur implémente différemment cette norme. Par exemple, sous Google Chrome 36, les champs de type date sont particulièrement aboutis, avec masque de saisie et calendrier ; voir ci-dessous la représentation de la ligne HTML Birthdate: . Par contre, ni Internet Explorer 11 ni Firefox 31 ne fournissent un tel confort de saisie.\nBean Validation Dans le monde Java, la spécification Bean Validation (JSR-303 et JSR-349) n’a plus à faire ses preuves. Elle s’est tout d’abord imposée au niveau de la couche de persistance. Ses annotations sont en effet utilisées par JPA pour générer la structure de la base de données et pour valider les données avant d’exécuter les ordres SQL d’insertion et de mise à jour. Bean Validation a ensuite fait son entrée au niveau de la couche de présentation : avec JSF 2 puis dans Spring MVC via l’annotation @Valid. Enfin, Bean Validation peut également être utilisé en dehors de tout framework, par exemple pour valider les données en entrée d’un web service.\nCode HTML 5 généré Spring MVC permet de binder bi-directionnellement un champ de saisie avec la propriété d’une classe. Cette classe peut être aussi bien une entité métier qu’un simple DTO. En supposant que Bean Validation est utilisé pour valider les données de cette classe côté server, nous allons demander à Spring d’exploiter ces annotations lors de la génération des attributs HTML de la balise , et ceci via le tag JSP personnalisé \u0026lt;jem:input /\u0026gt; que nous allons développer.\nVoici le code HTML 5 que nous aimerions que Spring MVC génère : Code JavaPage JSPHTML 5 généré@NotEmpty String firstName;\u0026lt;jem:input path=\u0026ldquo;firstName\u0026rdquo; /\u0026gt;@NotNull String city;\u0026lt;jem:input path=\u0026ldquo;city\u0026rdquo; /\u0026gt;@Size(max=40) String address;\u0026lt;jem:input path=\u0026ldquo;address\u0026rdquo; /\u0026gt;@Size(max=40) String address;\u0026lt;jem:input path=\u0026ldquo;address\u0026rdquo; maxlength=\u0026ldquo;20\u0026rdquo;/\u0026gt;@Min(value = 18) @Max(value=99) Integer age;\u0026lt;jem:input path=\u0026ldquo;age\u0026rdquo; /\u0026gt;@Email String email;\u0026lt;jem:input path=\u0026ldquo;email\u0026rdquo; /\u0026gt;@URL String website;\u0026lt;jem:input path=\u0026ldquo;website\u0026rdquo; /\u0026gt;Integer birthYear;\u0026lt;jem:input path=\u0026ldquo;birthYear\u0026rdquo; /\u0026gt;Remarques :\nLes attributs font parties de la classe Customer. Côté contrôleur web, une instance est ajoutée au modèle Spring MVC de la vue. Le préfixe \u0026lt;jem: permet de distinguer notre balise personnalisée avec la balise input de Spring MVC (\u0026lt;form:input /\u0026gt;) Les tags \u0026lt;jem:input /\u0026gt; sont disposés dans le formulaire \u0026lt;form:form modelAttribute=\u0026ldquo;customer\u0026rdquo;\u0026gt; Si besoin est, l’attribut maxlength peut être redéfini manuellement via le tag \u0026lt;jem:input /\u0026gt; .\nMise en œuvre L’implémentation du tag Html5InputTag interprétant les contraintes Bean Validation demande un peu moins de 200 lignes de code. Elle spécialise la classe org.springframework.web.servlet.tags.form.InputTag de Spring MVC. Trois méthodes y sont redéfinies :\nAvant d’appeler la méthode parent, la méthode writeTagContent analyse la propriété à binder à la recherche de contraintes matérialisées par des annotations Bean Validation. Le résultat est stocké dans une Map et sera utilisé dans les 2 autres méthodes. En complément des attributs type et value, la méthode writeValue est chargée d’écrire les attributs maxLength, min, max et required à partir des contraintes portées par la propriété à binder. Enfin, la méthode getType détermine la valeur de l’attribut type en fonction du type de la propriété à binder (ex : Integer) ou des contraintes qu’elle porte. Pour davantage de détails, voici le code source complet de la classe Html5InputTag:\nExtrait de la classe Html5InputTag.java Cette classe peut être reprise et adaptée en fonction de vos besoins.\nTests unitaires La classe TestHtml5InputTag teste unitairement chacune des annotations Bean Validation supportés par le tag. A titre d’exemple, voici la méthode testant le HTML généré à partir de l’annotation @Size :\nExtrait de la classe TestHtml5InputTag.java Intégration manuelle du tag Pour utiliser cette classe dans une application Spring MVC, il est nécessaire de déclarer le tag correspondant dans un fichier TLD qui sera analysé par le conteneur web à son démarrage. Ce descripteur doit se situer dans le répertoire WEB-INF\\tld (pour un WAR) ou META-INF\\tld (pour un JAR).\nLa description du tag reprend exactement celle du tag input de Spring MVC déclaré dans le descripteur META_INF/spring-form.tld du module spring-webmvc. Seule l’implémentation change.\nTaglib prête à l’emploi Pour faciliter l’intégration de ce tag, le projet open source spring-mvc-toolkit propose un taglib. L’URI du taglib est /core/spring-mvc\nAfin de pouvoir l’utiliser sur une application existante, l’ajout de la dépendance maven suivante est nécessaire :\n\u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.javaetmoi.core\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-mvc-toolkit\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;0.1\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; Lors d’un mvn clean install , le JAR sera téléchargé depuis Maven Central.\nDémo Le projet spring-mvc-toolkit vient avec une application démo mettant en œuvre les différentes fonctionnalités offertes par le projet La page htmlvalidation.jsp montre comment utiliser le tag Html5InputTag. Remarquez qu’aucun code JavaScript n’est utilisé. Afin d’uniformiser le comportement sur l’ensemble des navigateurs, 2 styles CSS sont appliqués aux pseudo-classes :valid et :invalid pour afficher des icônes à droite du champ de saisie.\nDans le pom.xml de cette application web de démo, le plugin Jetty pour maven est préconfiguré.\nVoici la démarche à suivre pour tester la page :\nRécupérer le code hébergé sur GitHub : git clone git://github.com/arey/spring-mvc-toolkit.git Construire le projet avec maven : cd spring-mvc-tookit mvn clean install Démarrer Jetty cd spring-mvc-toolkit-demomvn jetty:run-war Accéder à la page de test du tag depuis votre Navigateur : http://localhost:8080/htmlvalidation Conclusion Cet article aura montré comment étendre les tags JSP de Spring MVC pour ajouter la validation côté client apportée par HTML 5. L’enrichissement du HTML généré par les tags se base sur les contraintes Bean Validation.\nA ce jour, la classe Html5InputTag supporte 4 annotations Bean Validation ( @Min, @Max, @NotNull et @Size) et 3 annotations spécifiques à Hibernate Validator ( @Email, @NotEmpty et @URL). Le support d’autres annotations pourraient être ajouté. L’annotation @Pattern pourrait par exemple générer l’attribut pattern qui accepte une expression régulière. La difficulté réside dans l’adaptation d’une regex Java en regex JavaScript, ce qui a été fait dans le sens inverse par l’équipe GWT. Le support des groups Bean Validation pourrait également être ajouté. Enfin, ce qui a ici été appliqué pour la classe InputTag peut l’être à moindre échelle sur la classe TextAreaTag.\nRéférences :\nHTML5 Form Validation Examples Formulaires HTML5 : placeholder, required, pattern et validation Classe RegExp.java de GWT ","link":"https://javaetmoi.com/2014/09/validation-html-5-avec-spring-mvc-et-bean-validation/","section":"posts","tags":["bean-validation","hibernate","html-5","jsp","spring-mvc","tag"],"title":"Validation HTML 5 avec Spring MVC et Bean Validation"},{"body":"Lorsque vous rendez open-source un projet, même le plus modeste soit-il, quoi de plus naturel que de vouloir faciliter son utilisation par la communauté de développeurs intéressés ? Dans le monde Java, le dépôt de binaires Open Source le plus connu est le Central Repository, également connu sous le nom de Maven Central. En effet, lors de la construction d’un projet Java, Apache Maven essaie par défaut de localiser ses dépendances depuis Maven Central. D’autres outils de build comme Gradle et Ant/Ivy y font également référence. Géré par Sonatype et reposant sur Nexus, Maven Central est accessible en écriture par les développeurs Open-Source en faisant la demande. Ayant récemment publié des artefacts sur ce repo, ce billet présente les différentes étapes qui m’ont permis d’y arriver.\nLes limites d’un repository personnel Il y’a 2 ans, j’expliquais comment monter sa propre usine de développement Java dans le Cloud à l’aide de GitHub et de Cloudbees. Pour Repository Maven, j’utilisais le Webdav mis gracieusement à disposition par Cloudbees. L’inconvénient principal de cet hébergement est que les artefacts publiés sur ce repository ne sont pas nativement accessibles par Maven. Il est en effet nécessaire de configurer la balise distributionManagement du pom.xml nécessitant ces artefacts. En entreprise, lorsque les builds maven passent systématiquement par un proxy maven, cela se complique pour le développeur. Plusieurs choix s’offrent à lui :\nRéférencer le repository Cloudbees dans le proxy maven de son Entreprise Déployer les artefacts maven dans le repository maven de son Entreprise Copier/coller le code qui l’intéresse Renoncer à l’utilisation du code convoité Sans débat, la disponibilité d’artefacts dans le repository Maven Central facilite leur adoption. C’est un critère qui m\u0026rsquo;avait d\u0026rsquo;ailleurs convaincu d\u0026rsquo;adopter DbSetup.\nAutre argument et pas des moindres : contrairement à un repository personnel, la pérennité du repository Maven Central est garantie.\nDemander à l’accès au service OSSRH Le manuel utilisateur de maven explique les différentes solutions permettant de déployer un artefact dans Maven Central. Le moyen le plus simple est de passer par l’intermédiaire du service Open-Source Software Repository Hosting ( OSSRH) offert par Sonatype. Ce service offre un espace de staging dans lequel des artefacts « releasés » peuvent être publiés avant d’être promus puis synchronisés vers Maven Central.\nLe guide du service OSSRH explique quelles sont les démarches nécessaires à l’obtention des habilitations permettant de publier un artefact dans le repository Nexus OSSRH. L’accès au Jira de Sonatype est un pré-requis. Il suffit de renseigner un formulaire d’inscription. Les login / mot de passe de ce Jira serviront à se connecter sur le Nexus de Sonatype et à déployer les artefacts dans OSSRH.\nL’accès au Jira permet également d’ ouvrir un ticket Jira de demande de création d’un projet. L’information principale à communiquer concerne le groupId maven des artefacts que vous souhaitez déployer. La demande est examinée par un employé de Sonatype . Pour des raisons de sécurité et de confiance, le groupId est vérifié.\nDans la demande OSSRH-11327, le groupId com.javaetmoi m’a été accordé. Cela me permet de publier des artefacts avec des « sous » groupId comme com.javaetmoi.core. Moins de 24h après, Joel Orlina avait traité ma demandé.\nPréparer sa signature PGP Un pré-requis nécessaire à la publication d’un artefact dans Maven Central consiste à signer son artefact à l’aide du standard OpenPGP. L’installation de GnuPG, la génération de clés privés / publiques et la diffusion de la clé publique sont décrits sur la page Working with PGP signatures. Les instructions sont claires et une installation sous Windows 7 ne m’a posé aucune difficulté.\nDans le settings.xml local à l\u0026rsquo;utilisateur, pense à indiquer le chemin complet du binaire gpg2.exe :\n\u0026lt;profiles\u0026gt; \u0026lt;profile\u0026gt; \u0026lt;id\u0026gt;ossrh\u0026lt;/id\u0026gt; \u0026lt;activation\u0026gt; \u0026lt;activeByDefault\u0026gt;true\u0026lt;/activeByDefault\u0026gt; \u0026lt;/activation\u0026gt; \u0026lt;properties\u0026gt; \u0026lt;gpg.executable\u0026gt;C:/Software/Dev/GnuPG/gpg2.exe\u0026lt;/gpg.executable\u0026gt; \u0026lt;gpg.passphrase\u0026gt;xxxxxx\u0026lt;/gpg.passphrase\u0026gt; \u0026lt;/properties\u0026gt; \u0026lt;/profile \u0026lt;/profiles\u0026gt; Configuration maven La dernière étape avant de pouvoir déployer les artefacts de son projet dans Maven Central, consiste à configurer le POM de votre projet (le POM parent pour un projet multi-modules). La page Apache Maven du Central Repository explique pas à pas quels sont les plugins maven à configurer et les coordonnées du repository à déclarer. Pour exemple, je vous invite à vous référer au pom.xml du projet spring-batch-toolkit.\nNe pas oublier de déclarer le server ossrh dans le settings.xml local de l\u0026rsquo;utilisateur.\nPublication dans Maven Central Déployer une version release de son projet dans Maven Central peut se faire de plusieurs manières :\nL’utilisation du Maven Release Plugin La commande deploy de maven Une publication manuelle à l’aide du plugin maven nexus-staging La page Apache Maven du Central Repository explique les commandes maven à exécuter.\nPour ma part, j’ai souhaité publié dans Maven Central des artefacts que j’avais déjà releasé sur mon repo personnel Cloudbees. J’ai créé une branche à partir du tag créé lors de la release maven. J’ai ensuite reconfiguré le pom.xml comme expliqué ci-dessus. La commande maven suivante a fait le reste : mvn clean deploy -P release\nVoici un exemple de sortie console que vous obtiendrez en cas de succès :\n.... Uploading: https://oss.sonatype.org:443/service/local/staging/deployByRepositoryId/comjavaetmoi-1004/com/javaetmoi/core/javaetmoi-hibernate4-hydrate/2.0/javaetoi-hibernate4-hydrate-2.0-sources.jar.asc Uploaded: https://oss.sonatype.org:443/service/local/staging/deployByRepositoryId/comjavaetmoi-1004/com/javaetmoi/core/javaetmoi-hibernate4-hydrate/2.0/javaetmi-hibernate4-hydrate-2.0-sources.jar.asc (499 B at 0.1 KB/sec) [INFO] * Upload of locally staged artifacts finished. [INFO] * Closing staging repository with ID \u0026#34;comjavaetmoi-1004\u0026#34;. Waiting for operation to complete....... [INFO] Remote staged 1 repositories, finished with success. [INFO] Remote staging repositories are being released... Waiting for operation to complete........ [INFO] Remote staging repositories released. [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 1:47.675s [INFO] Finished at: Thu Sep 04 20:41:33 CEST 2014 [INFO] Final Memory: 15M/247M [INFO] ------------------------------------------------------------------------ Avant publication dans Maven Central, le nexus-staging-maven-plugin effectue des vérifications. Ainsi, ma première publication a été rejetée car aucun développeur n’était mentionné dans le pom.xml. Le nom de la licence Open Source choisie pour le projet est également indispensable.\nLors de futures releases, j’utiliserais directement le Maven Release Plugin comme je le faisais jusque-là sur CloudBees.\nRésultat A l’issu de la publication, vous recevrez successivement 2 mails :\nNexus: Staging Completed Nexus: Promotion Completed Voici un extrait du second mail :\nMessage from: https://oss.sonatype.org Description: com.javaetmoi.core:javaetmoi-hibernate-hydrate:1.0 Deployer properties: •\t\u0026#34;userAgent\u0026#34; = \u0026#34;Nexus-Client/2.7.2-01\u0026#34; •\t\u0026#34;userId\u0026#34; = xxxx •\t\u0026#34;ip\u0026#34; = xxxx Details: The following artifacts have been promoted to the \u0026#34;Releases\u0026#34;[id=releases] repository /com/javaetmoi/core/javaetmoi-hibernate-hydrate/1.0/javaetmoi-hibernate-hydrate-1.0.jar (SHA1: cd0d87951fc2c5ac5c8bef1ca10cd5c40a5a3e3c) /com/javaetmoi/core/javaetmoi-hibernate-hydrate/1.0/javaetmoi-hibernate-hydrate-1.0-javadoc.jar (SHA1: d43dc47450d0acbed4ce21a2e8ff69de06f03fd3) /com/javaetmoi/core/javaetmoi-hibernate-hydrate/1.0/javaetmoi-hibernate-hydrate-1.0.pom.asc (SHA1: cbfdc56ff91926164dacd6954782a2b1ade8eef2) /com/javaetmoi/core/javaetmoi-hibernate-hydrate/1.0/javaetmoi-hibernate-hydrate-1.0.pom (SHA1: 7336a648dc1deb3f6b223eb121ac2a1c0ef6053b) /com/javaetmoi/core/javaetmoi-hibernate-hydrate/1.0/javaetmoi-hibernate-hydrate-1.0-javadoc.jar.asc (SHA1: a91a7f1f2e5603346c43325c7d009008a9d0de18) /com/javaetmoi/core/javaetmoi-hibernate-hydrate/1.0/javaetmoi-hibernate-hydrate-1.0-sources.jar.asc (SHA1: 7e014101817772f1b90e5835b2b6b4e45713dcf6) /com/javaetmoi/core/javaetmoi-hibernate-hydrate/1.0/javaetmoi-hibernate-hydrate-1.0-sources.jar (SHA1: 45dbc2f47bcd690a00604f9c38be54383a99cc46) /com/javaetmoi/core/javaetmoi-hibernate-hydrate/1.0/javaetmoi-hibernate-hydrate-1.0.jar.asc (SHA1: 267be3c43664fa096fd38b77df205c4477c51ad0) Action performed by Antoine Rey (mon email) Dès réception de ce mail, l’artefact peut alors être télécharg dans votre repo local maven. Pour se faire, ajouter la dépendance dans le pom.xml :\n\u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.javaetmoi.core\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;javaetmoi-hibernate4-hydrate\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;2.0\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; Puis lancer un build : mvn clean install\nLes artefacts sont directement téléchargés depuis Maven Central :\nDownloading: http://repo.maven.apache.org/maven2/com/javaetmoi/core/javaetmoi-hibernate4-hydrate/2.0/javaetmoi-hibernate4-hydrate-2.0.pom Downloaded: http://repo.maven.apache.org/maven2/com/javaetmoi/core/javaetmoi-hibernate4-hydrate/2.0/javaetmoi-hibernate4-hydrate-2.0.pom (12 KB at 9.3 KB/sec) Downloading: http://repo.maven.apache.org/maven2/com/javaetmoi/core/javaetmoi-hibernate4-hydrate/2.0/javaetmoi-hibernate4-hydrate-2.0.jar Downloaded: http://repo.maven.apache.org/maven2/com/javaetmoi/core/javaetmoi-hibernate4-hydrate/2.0/javaetmoi-hibernate4-hydrate-2.0.jar (11 KB at 25.0 KB/sec) Conclusion La publication d’un JAR dans Maven Central n’est pas si compliqué. Elle n’est pas non plus réservée aux projets les plus connus. Votre groupId doit être choisi rigoureusement. Si vous disposez d’un nom de domaine, utilisez-le.\nPour automatiser encore davantage la publication de me releases dans Maven Central, il me reste une dernière étape : le faire depuis mon instance Jenkins sur Dev@Cloud. Les plugins Jenkins M2 Release et Promoted Builds devraient m’y aider. Le plus compliqué sera sans doute d’installer et de faire perdurer une clé PGP privé.\nRéférences :\nOSSRH Guide Repository Nexus OSSRH de Sonatype Jira de Sonatype Page Apache Maven du Central Repository PGP signatures Apache Maven GPG plugin Guide pour uploader des artefacts dans le Maven Central Repository ","link":"https://javaetmoi.com/2014/09/publier-deployer-releaser-artefact-sur-maven-central/","section":"posts","tags":["maven"],"title":"Publiez vos artefacts sur Maven Central"},{"body":"","link":"https://javaetmoi.com/tags/jboss/","section":"tags","tags":null,"title":"Jboss"},{"body":"","link":"https://javaetmoi.com/tags/jsf/","section":"tags","tags":null,"title":"Jsf"},{"body":" Début 2014, j’ai étudié la faisabilité technique d’une migration de JSF 1.2 + Richfaces 3.3 vers JSF 2.1 + Richfaces 4.3 sans changer de serveur d’application. Notre serveur JBoss 5.1 EAP étant certifié JavaEE 5, la première difficulté consistait à désinstaller l’implémentation Mojarra de JSF 1.2 embarquée dans JBoss. Cette opération est le pré-requis à l’installation de la version de JSF de son choix. Cette dernière aura alors pour unique contrainte d’être compatible avec le moteur de Servlet 2.5 sur lequel repose JBoss Web. Plus classique, la seconde difficulté consistait à monter les versions de JSF et de Richfaces d’une application existante. J’ai arrêté mon étude après avoir migré le premier écran de cette application. Ayant conservé quelques notes, je me suis dit qu’elles pourraient intéresser certains ou certaines d’entre vous. Ce billet commence par expliquer comment désinstaller JSF 1.2, se poursuit par le déploiement du Showcase de Richfaces 4.3.5 dans JBoss 5.1 EAP et se termine par la mise à disposition de mes notes de migration.\nDésinstaller JSF 1.2 La base de connaissance de RedHat indique la procédure à suivre. Elle met en garde sur le fait que le support RedHat n’applique pas sur les versions de JSF ajoutées en dehors d’une distribution EAP.\nSupprimer les JAR de JSF Supprimer les JAR jsf-impl.jar, jsf-api.jar et jboss-faces.jar du répertoire deploy/jbossweb.sar/jsf-libs.\nSupprimer la configuration JSF de JBoss La désactivation de JSF dans JBoss passe par la suppression des 4 blocs de lignes XML suivantes dans le fichier deployers/jbossweb.deployer/web.xml :\n\u0026lt;context-param\u0026gt; \u0026lt;param-name\u0026gt;com.sun.faces.injectionProvider\u0026lt;/param-name\u0026gt; \u0026lt;param-value\u0026gt;org.jboss.web.jsf.integration.injection.JBossDelegatingInjectionProvider\u0026lt;/param-value\u0026gt; \u0026lt;/context-param\u0026gt; \u0026lt;listener\u0026gt; \u0026lt;listener-class\u0026gt;org.jboss.web.jsf.integration.config.JBossJSFConfigureListener\u0026lt;/listener-class\u0026gt; \u0026lt;/listener\u0026gt; \u0026lt;listener\u0026gt; \u0026lt;listener-class\u0026gt;com.sun.faces.application.WebappLifecycleListener\u0026lt;/listener-class\u0026gt; \u0026lt;/listener\u0026gt; \u0026lt;init-param\u0026gt; \u0026lt;description\u0026gt;JSF standard tlds\u0026lt;/description\u0026gt; \u0026lt;param-name\u0026gt;tagLibJar0\u0026lt;/param-name\u0026gt; \u0026lt;param-value\u0026gt;jsf-libs/jsf-impl.jar\u0026lt;/param-value\u0026gt; \u0026lt;/init-param\u0026gt; La dernière manipulation consiste à supprimer la ligne suivante dans le fichier deploy/jbossweb.sar/META-INF/jboss-structure.xml : Pour se prémunir de toute erreur de syntaxe, démarrer JBoss à vide.\nDéployer une application JSF 2 Afin de valider le déploiement d’une application JSF 2 / Richfaces 4, le candidat retenu a naturellement été l’ application démo Richfaces Showcase de JBoss. Seuls les quelques composants utilisant les fonctionnalités asynchrones de Servlet 3.0 ne seront pas disponibles. Sortie début 2014, la version utilisée est la 4.3.5. Disponible librement sur le site de JBoss, l’archive richfaces-4.3.5.Final.zip donne accès au code source de Richfaces et de son Showcase. Accessible dans le sous-répertoire _examples\\richfaces-showcase_, le WAR de l’application web peut être construit avec maven. Plusieurs profiles maven sont configurés dans le pom.xml. Certains permettent de choisir l’implémentation de JSF (Myfaces ou Sun RI/Mojarra). D’autres permettent de construire le WAR en fonction du serveur d’application cible : Tomcat 6, JBoss AS 7 et 7.1 ou bien encore un serveur certifié JavaEE 6. Les derniers permettent de le déployer sur les PAAS OpenShift et Google App Engine. Aucun profil n’existe pour JBoss 5.1 EAP. Le profil se rapprochant le plus de notre JBoss 5.1 EAP est celui pour Tomcat 6. Ce profil est celui activé par défaut lors d’un mvn clean install.\nQuelques adaptations du pom.xml sont nécessaires afin que le WAR généré par maven puisse être déployé dans JBoss 5.1 EAP :\n1. Supprimer les dépendances vers Hibernate\nLes 4 dépendances suivantes peuvent être supprimées car JBoss embarque ses propres versions d’Hibernate 3.3. A noter que le showcase est rétro-compatible avec Hibernate 3.3 et JPA 1:\nhibernate-commons-annotations-4.0.1.Final.jar hibernate-core-4.0.0.Final.jar hibernate-entitymanager-4.0.0.Final.jar hibernate-jpa-2.0-api-1.0.1.Final.jar Dans le fichier src/main/resources-tomcat/META-INF/persistence.xml, la déclaration suivante doit être supprimée : org.hibernate.ejb.HibernatePersistence 2. Adapter les descripteurs de déploiement à JBoss 5\nLes descripteurs de déploiement de tomcat6 ont été adaptés, en particulier le fichier src/main/webapp-tomcat/WEB-INF/web.xml. Premier changement, son namespace référence l’API Servlet 2.5 :\n\u0026lt;web-app xmlns:xsi=\u0026#34;http://www.w3.org/2001/XMLSchema-instance\u0026#34; xmlns=\u0026#34;http://java.sun.com/xml/ns/javaee\u0026#34; xmlns:web=\u0026#34;http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd\u0026#34; xsi:schemaLocation=\u0026#34;http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd\u0026#34; id=\u0026#34;richfaces-showcase\u0026#34; version=\u0026#34;2.5\u0026#34;\u0026gt; La fonctionnalité de requête asynchrone, la balise \u0026lt; async-supported\u0026gt; du filtre PushFilter doit être supprimée.\nJSF étant désormais embarqué dans le répertoire lib du war, l’ajout du ConfigureListener est nécessaire :\n\u0026lt;listener\u0026gt; \u0026lt;listener-class\u0026gt;com.sun.faces.config.ConfigureListener\u0026lt;/listener-class\u0026gt; \u0026lt;/listener\u0026gt; Enfin, CDI n’étant pas géré par le conteneur JEE, la ressource BeanManager doit être supprimée.\nUne fois déployé dans JBoss, le showcase RichFaces est disponible à cette URL : http://localhost:8080/richfaces-showcase-tomcat6/ Les logs de démarrage du serveur JBoss et de l’application montrent clairement que JSF 2 et Richfaces 4 sont utilisés :\n20:51:53,781 INFO [ServerImpl] Starting JBoss (Microcontainer)... 20:51:53,782 INFO [ServerImpl] Release ID: JBoss [EAP] 5.1.1 (build: SVNTag=JBPAPP_5_1_1 date=201105171607) 20:51:53,782 INFO [ServerImpl] Home Dir: D:\\jboss-eap-5.1\\jboss-as 20:51:56,080 INFO [ServerInfo] Java version: 1.6.0_31,Sun Microsystems Inc. 20:51:56,081 INFO [ServerInfo] Java Runtime: Java(TM) SE Runtime Environment (build 1.6.0_31-b05) 20:51:56,081 INFO [ServerInfo] Java VM: Java HotSpot(TM) 64-Bit Server VM 20.6-b01,Sun Microsystems Inc. 20:51:56,081 INFO [ServerInfo] VM arguments: -Dprogram.name=JBossTools: JBoss EAP 5.x Runtime -Xms256m -Xmx768m -XX:MaxPermSize=256m -Dsun.rmi.dgc.client.gcInterval=3600000 -Dsun.rmi.dgc.server.gcInterval=3600000 -Djava.endorsed.dirs=D:\\jboss-eap-5.1\\jboss-as\\lib\\endorsed -Dfile.encoding=ISO-8859-1 20:52:15,315 INFO [AjpProtocol] Initializing Coyote AJP/1.3 on ajp-localhost%2F127.0.0.1-8009 20:52:15,332 INFO [StandardService] Démarrage du service jboss.web 20:52:15,334 INFO [StandardEngine] Starting Servlet Engine: JBoss Web/2.1.11.GA 20:52:15,377 INFO [Catalina] Server startup in 62 ms 20:52:18,375 INFO [PersistenceUnitDeployment] Starting persistence unit persistence.unit:unitName=#richfaces-showcase 20:52:18,455 INFO [Version] Hibernate Annotations 3.4.0.GA_CP01 20:52:18,466 INFO [Environment] Hibernate 3.3.2.GA_CP04 20:52:18,593 INFO [Version] Hibernate EntityManager [WORKING] 20:52:18,620 INFO [Ejb3Configuration] Processing PersistenceUnitInfo [name: richfaces-showcase ...] 20:52:18,688 WARN [Ejb3Configuration] Persistence provider caller does not implement the EJB3 spec correctly. PersistenceUnitInfo.getNewTempClassLoader() is null. 20:52:18,755 INFO [AnnotationBinder] Binding entity from annotated class: org.richfaces.demo.arrangeablemodel.Person 20:52:18,950 INFO [SettingsFactory] JDBC driver: HSQL Database Engine Driver, version: 1.8.0 20:52:18,979 INFO [Dialect] Using dialect: org.hibernate.dialect.HSQLDialect 20:52:19,030 INFO [Version] Hibernate Validator 3.1.0.GA 20:52:19,270 INFO [TomcatDeployment] deploy, ctxPath=/richfaces-showcase 20:52:19,440 INFO [Version] WELD-000900 1.1.4 (Final) 20:52:19,534 INFO [config] Initialisation de Mojarra 2.1.19 ( 20130213-1512 https://svn.java.net/svn/mojarra~svn/tags/2.1.19@11614) pour le contexte «/richfaces-showcase» 20:52:24,347 INFO [application] JSF1048 : Présence d?annotations PostConstruct/PreDestroy Les méthodes de beans gérés marquées avec ces annotations auront des annotations dites traitées. 20:52:30,174 INFO [Application] RichFaces Core Implementation by JBoss by Red Hat, version 4.3.5.Final 20:52:30,217 INFO [Application] Startup initialization of PushContext 20:52:30,232 WARNING [Application] JMS API was found on the classpath; if you want to enable RichFaces Push JMS integration, set context-param \u0026#39;org.richfaces.push.jms.enabled\u0026#39; in web.xml 20:52:30,292 INFO [AnnotationBinder] Binding entity from annotated class: org.richfaces.demo.arrangeablemodel.Person 20:52:30,292 INFO [EntityBinder] Bind entity org.richfaces.demo.arrangeablemodel.Person on table Person 20:52:30,298 INFO [HibernateSearchEventListenerRegister] Unable to find org.hibernate.search.event.FullTextIndexEventListener on the classpath. Hibernate Search is not enabled. 20:52:30,299 INFO [NamingHelper] JNDI InitialContext properties:{} 20:52:30,517 INFO [Bootstrap] WELD-000101 Transactional services not available. Injection of @Inject UserTransaction not available. Transactional observers will be invoked synchronously. 20:52:30,855 INFO [Tomcat6Container] Tomcat 6 detected, CDI injection will be available in Servlets and Filters. Injection into Listeners is not supported 20:52:31,392 WARNING [Webapp] PushFilter has been deprecated, you should use PushServlet instead 20:52:31,446 INFO [ProfileServiceBootstrap] Loading profile: ProfileKey@1b83ee9a[domain=default, server=default, name=jsf2] 20:52:31,454 INFO [Http11Protocol] D?marrage de Coyote HTTP/1.1 sur http-localhost%2F127.0.0.1-8080 20:52:31,478 INFO [AjpProtocol] Starting Coyote AJP/1.3 on ajp-localhost%2F127.0.0.1-8009 20:52:31,487 INFO [ServerImpl] JBoss (Microcontainer) [5.1.1 (build: SVNTag=JBPAPP_5_1_1 date=201105171607)] Started in 37s:701ms Migrer vers JSF 2 et Richfaces 4 Migrer une application développée en Richfaces 3 vers du Richfaces 4 est loin d’être indolore. Outre la montée de version de JSF, c’est le passage à Richfaces 4 qui demande le plus d’efforts. En effet, cette version majeure n’est pas rétro-compatible avec la précédente. De nombreux changements ont été apportés :\nRéduction du code d’ Ajax4Jsf au profit du support Ajax introduit dans JSF 2 Renommage de classes, packages, tags et paramètres Suppression de tags Richfaces Pour faciliter le travail du développeur, JBoss met à disposition un Guide de migration de Richfaces 3 vers Richfaces 4. Et pour le compléter, voici mes notes de travail :\nMigration Richfaces\nRetirer le context parameter org.ajax4jsf.VIEW_HANDLERS nécessite de réimplémenter la classe ParameterizedFaceletViewHandler La classe org.richfaces.renderkit.html.HtmlRichMessageRenderer est renommée en HtmlMessageRenderer La classe AjaxContext d\u0026rsquo;A4JSF a disparu. Nécessiter d’utiliser l\u0026rsquo;API JSF2 context.getPartialViewContext().isAjaxRequest() pour savoir si la requête http est une requête Ajax. Le code ajaxContext.getAjaxSingleClientId() est migré en context.getPartialViewContext().getExecuteIds().size() == 1 Supprimer la déclaration du filtre org.ajax4jsf.Filter dans le web.xml qui n\u0026rsquo;est plus nécessaire dans RichFaces 4. Classes InternetResourceBuilder et InternetResource non trouvées : constante DEFAULT_EXPIRE redéfinie à 1 jour. Les composants et ont été supprimés de RichFaces 4. Les tags natifs de JSF 2 doivent être utilisés h:outputStylesheet and h:outputScript Remplacer le tag a4j:loadBundle par \u0026lt;f:loadBundle\u0026gt; Sous peine d’une javax.faces.FacesException: No enum const class org.richfaces.component.JQueryTiming.onload, la déclaration \u0026lt;rich:jQuery timing=\u0026ldquo;onload\u0026rdquo; selector=\u0026ldquo;document\u0026rdquo; query=\u0026ldquo;loadMyCombo()\u0026rdquo;/\u0026gt; doit être corrigée avec l’attribut timing=\u0026ldquo;domready\u0026rdquo;. Renommer l’objet JavaScript Richfaces en RichFaces. Migration des composants RichFaces\nPrendre connaissance du guide de migration des composants. Migrer rich:modalPanel vers \u0026lt;rich: popupPanel\u0026gt; Tag rich:spacer supprimé. Pas d\u0026rsquo;équivalent. Création nécessaire d\u0026rsquo;un custom tag. Migrer le tag rich:simpleTogglePanel en rich:collapsiblePanel Tag a4j:include remplacé par \u0026lt;ui:include \u0026gt; + paramètre viewId changé en src Tag aj4:form remplacé par \u0026lt;h:form\u0026gt; Tag a4j:support remplacé par a4j:ajax Tag rich:toolTip remplacé par rich:tooltip Tag rich:suggestionbox des snippets à migrer vers l’autocomplete. Noms d’évènements JavaScript à renommer. Exemple de message d’erreur si oubli : a4j:ajax onclickevent is not supported for the HtmlSelectOneRadio HtmlSelectOneRadio : event=\u0026ldquo;onclick\u0026rdquo; à changer en \u0026ldquo;select\u0026rdquo; HtmlSelectOneMenu : event=\u0026ldquo;onchange\u0026rdquo; à changer en \u0026ldquo;select\u0026rdquo; HtmlInputText : event=\u0026ldquo;onchange\u0026rdquo; à changer en \u0026ldquo;change\u0026rdquo; UICalendar : event=\u0026ldquo;onchanged\u0026rdquo; à changer en \u0026ldquo;change\u0026rdquo; Migration Facelets\nFacelets est désormais inclu dans l\u0026rsquo;API JSF 2 et l\u0026rsquo;implémentation JSF RI Mojara 2 ( jsf-impl-2.1.19-jbossorg-1.jar et jboss-jsf-api_2.1_spec-2.1.19.1.Final.jar)\nPackage com.sun.facelets scindé en 2 : javax.faces.view.facelets pour l’API et com.sun.faces.facelets pour l’implémentation\nClasse com.sun.facelets.impl.ResourceResolver relocalisée dans javax.faces.view.facelets.ResourceResolver\nSupprimer le parameter org.ajax4jsf.VIEW_HANDLERS du web.xml\nRenommer les variables suivantes dans le web.xml:\nfacelets.SKIP_COMMENTS en javax.faces.FACELETS_SKIP_COMMENTS facelets.LIBRARIES en javax.faces.FACELETS_LIBRARIES Migration Apache Tomahawk\nUtiliser la version MyFaces Tomahawk 1.1.14 for JSF 2.0. L\u0026rsquo;artefact tomahawk change en tomahawk20 org.apache.myfaces.tomahawk tomahawk20 1.1.14 Migration JSF 2\nL’extension .jspx des pages ne fonctionne plus =\u0026gt; géré par le moteur JSP de Tomcat. Passage à xhtml + suppression conf JSF\nClasse com.sun.faces.application.ConfigNavigationCase de JSF 1 à remplacer :\nUtilisation de la classe javax.faces.application.NavigationCase introduite dans JSF 2. Le constructeur prend 3 nouveaux paramètres : condition, includeViewParams et parameters La méthode getToViewId prend désormais un FacesContext en paramètre =\u0026gt; nécessite de faire appel à FacesContext.getCurrentInstance() Remplacer du HTML par \u0026lt;h:head\u0026gt; de JSF 2 sous peine d’obtenir l’erreur jSf précisant qu’une ou plusieurs ressources partagent la cible «body», mais qu’aucun composant «body» n’a été défini dans la vue.\nConclusion En l’espace 2 jours, j’aurais réussi à démarrer mon application puis à migrer partiellement un premier écran, et ceci sans changer de serveur d’application. J’ai chiffré à 30 jours de développement le temps nécessaire pour migrer l’intégralité d’une application comportant une vingtaine d\u0026rsquo;écrans. Pour les applications comportant de nombreux écrans, afin de réduire les coûts et les erreurs humaines, l’automatisation des tâches récurrentes de migration pourrait être intéressante. Dommage que JBoss ne propose pas un tel outil.\nRéférences :\nRichFaces Migration Guide. 3.3.x - 4.x Migration RichFaces 3.3 - \u0026gt; 4.x migration guide. Unleashed Write a JSF 2 custom EL resolver For JSF 2.0, How to Enable EL 2.2 on Tomcat 6 JSF 2 Configuration parameters What’s New in JSF 2? Richfaces 4.X Sandbox Components ","link":"https://javaetmoi.com/2014/06/notes-migration-jsf2-richfaces4-jboss5-eap/","section":"posts","tags":["jboss","jsf","richfaces"],"title":"Notes de migration vers JSF 2 et Richfaces 4"},{"body":"","link":"https://javaetmoi.com/tags/richfaces/","section":"tags","tags":null,"title":"Richfaces"},{"body":" Dans ce billet, nous verrons comment configurer en Java le contexte Spring d’une application basée sur Spring MVC, Spring Security, Spring Data JPA et Hibernate, et cela sans utiliser la moindre ligne de XML.\nPersonnellement, je n’ai rien contre la syntaxe XML à laquelle j’étais habitué. D’autant la verbosité de la configuration avait considérablement diminué grâce à l’introduction des namespaces XML et des annotations. Avant d’utiliser la syntaxe Java sur une application d’entreprise, j’étais même sceptique quant aux gains qu’elle pouvait apporter. Aujourd’hui, je comprends mieux son intérêt et pourquoi les projets du portfolio Spring tels Spring Integration 4.0, Spring Web Service 2.2 ou bien Spring Security 3.2 proposent dans leur dernière version un niveau de configuration Java iso-fonctionnel avec leur équivalent XML. Sans compter que le support de la configuration Java leur ouvre la porte d’une intégration plus poussée à Spring Boot, le nouveau fer de lance de Pivotal.\nPréambule Avant de faire partie intégrante du framework Spring, la configuration Java était proposée aux développeurs Spring dans un projet externe : Spring JavaConfig. Depuis la version 3.0 du framework Spring, des fonctionnalités équivalentes ont été introduites au fil des versions. La version 4.0 voit l’aboutissement de ce travail.\nTous les extraits de code suivants sont issus d’une application web disponible sur Github : spring-javaconfig-sample.\nLes dépendances maven requises sont déclarées dans le pom.xml. La toute dernière version des frameworks ont été exploitées :\nSpring Framework 4.0 Spring Data JPA 1.6 Spring Security 3.2 Hibernate 4.3 Afin que cette application puisse être déployée dans un conteneur de Servlet 2.5, l’initialisation de la configuration Spring repose intégralement sur les déclarations réalisées en XML dans le web.xml. A cet effet, lors de la commande mvn clean install , un serveur Jetty 6 compatible servlet 2.5 démarre puis arrête l’application.\nDans un conteneur de Servlet 3.x, la déclaration du DispatcherServlet pourrait être réalisée en Java via la classe AbstractAnnotationConfigDispatcherServletInitializer introduite dans Spring 3.2.\nCe billet n’a pas pour vocation de se substituer au manuel de référence du framework Spring. Il ne vous montrera pas non plus l’équivalent XML de la configuration Java. Son objectif est de vous donner un exemple de configuration afin que vous puissiez monter rapidement une application utilisant la syntaxe Java.\nContextes applicatifs Spring L’application web donnée en exemple reprend l’architecture type d’une application Spring MVC dans laquelle un WebApplicationContext parent (ou root) est chargé avec le listener ContextLoaderListener et un WebApplicationContext enfant est chargé via la DispatcherServlet.\nLe contexte parent met à disposition les beans Spring de la couche service métier et de la couche de persistance ( infrastructure comprise). La sécurité est également définie au niveau du contexte parent car les services métiers peuvent être sécurisés ( @Secured). Chacune de ces couches étant configurée dans une classe qui lui est dédiée, la classe MainConfig a pour rôle de toutes les référencer :\n@Configuration @Import(value = { DataSourceConfig.class, InfrastructureConfig.class, RepositoryConfig.class, ServiceConfig.class, SecurityConfig.class } ) public class MainConfig { L’ annotation @Configuration joue un rôle central. Les classes où elle est apposée se substituent en effet aux traditionnels fichiers de configuration XML. Nous y retrouvons la déclaration de beans Spring, l’ import de fichiers ou de classes de configuration, la détection automatique de beans annotés par analyse de classpath ou bien encore l’ activation de fonctionnalités avancées et des annotations associées ( @Transactional, @Cacheable, @Scheduled, @Async …)\nRemarque : la déclaration de beans Spring peut se faire en dehors d’une classe de @Configuration. C’est ce qu’on appelle le mode lite Beans. Non recommandé, le manuel de référence de Spring explique quels en sont les limitations et les dangers.\nLa classe MainConfig est également annotée avec l’annotation @Import permettant d’importer d’autres classes de configuration. Via l’annotation @ImportResource, Spring offre la possibilité d’importer des fichiers de configuration XML. Très pratique lorsqu’on dépend de librairies tierces n’offrant qu’une configuration XML.\nPour Spring, les classes annotées avec @Configuration sont des beans Spring:\n08:02:13.246 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Creating shared instance of singleton bean \u0026#39;mainConfig\u0026#39; 08:02:13.246 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Creating instance of bean \u0026#39;mainConfig\u0026#39; Cette spécificité infère aux beans de configuration la possibilité d’utiliser l’ injection de dépendance via les annotations @Autowired et @Inject. Leur cycle de vie permet également d’utiliser les annotations @PostConstruct et @PreDestroy.\nLa classe MainConfig exploite cette possibilité pour injecter l’ Environment modélisant l’environnement d’exécution de l’application. Cette interface permet notamment d’accéder aux profils Spring activés. Lors de l’initialisation de la configuration spécifiée par MainConfig, la méthode initApp() annotée avec @PostConstruct génère une trace listant les profils actifs.\n@Autowired private Environment env; @PostConstruct public void initApp() { LOG.debug(\u0026#34;Looking for Spring profiles...\u0026#34;); if (env.getActiveProfiles().length == 0) { LOG.info(\u0026#34;No Spring profile configured, running with default configuration.\u0026#34;); } else { for (String profile : env.getActiveProfiles()) { LOG.info(\u0026#34;Detected Spring profile: {}\u0026#34;, profile); } } } Le contexte enfant est quant à lui définie au travers d’une seule classe WebMvcConfig que nous détaillerons par la suite. Cette hiérarchie de contexte permet aux beans de type @Service déclarés dans le contexte parent d’être visibles par les beans de type @Controller, l’inverse n’étant pas vrai.\nTester la configuration Spring De par le fait qu’elle est vérifiée à la compilation, la configuration codée en Java permet d’éviter de facto les erreurs que l’on retrouvait couramment en XML : fautes de frappe, déplacement de classes, JAR non présent dans le classpath \u0026hellip;\nCe premier garde-fou n’empêche pas d’autres erreurs. Par exemple, l’annotation @EnableCaching génère une exception lorsqu’aucun bean de type CacheManager n’a pas été déclaré :\nCaused by: java.lang.IllegalStateException: No bean of type CacheManager could be found. Register a CacheManager bean or remove the @EnableCaching annotation from your configuration. Pour se prémunir de genre d’exceptions découvertes au démarrage de votre application, le module Spring Test propose tout un jeu d’annotations : @WebAppConfiguration, @ContextConfiguration, @ActiveProfiles ou bien encore @ContextHierarchy. Introduite avec la version 3.2.2 de Spring, cette dernière permet de reproduire une hiérarchie de contextes Spring et de tester ainsi le chargement de contextes en conditions réelles.\nLa classe SpringConfigTest donne un exemple de test d’intégration de configuration Spring :\n@RunWith(SpringJUnit4ClassRunner.class) @WebAppConfiguration @ContextHierarchy({ @ContextConfiguration(classes = MainConfig.class), @ContextConfiguration(classes = WebMvcConfig.class) }) @ActiveProfiles(\u0026#34;test\u0026#34;) public class SpringConfigTest { @Autowired private WebApplicationContext wac; @Test public void springConfiguration() { assertNotNull(wac); } } DataSource La couche de persistance reposant sur JPA, une connexion à une base de données relationnelle est nécessaire. Selon le contexte dans lequel s’exécute l’application, cette DataSource peut être récupérée de 2 manières :\nPar lookup JNDI lorsque l’application est déployée dans un serveur d’application JavaEE ou un conteneur web gérant ses propres ressources. Initialisée au démarrage de l’application par création d’une base de données en mémoire. Utile pour les tests d’intégration ou pour faciliter le déploiement de l’application. Déclaré dans le bean de configuration DataSourceConfig, le bean dataSource sera par la suite injecté dans le bean de configuration InfrastructureConfig.\nL’instanciation, la configuration et l’initialisation d’un bean Spring est réalisé dans une méthode annotée par @Bean. Par défaut, le nom du bean Spring est déduit du nom de sa méthode. Voici un exemple de déclaration de beans :\n@Configuration @PropertySource({ \u0026#34;classpath:com/javaetmoi/sample/config/datasource.properties\u0026#34; }) public class DataSourceConfig { @Autowired private Environment env; @Bean @Profile(\u0026#34;javaee\u0026#34;) public JndiObjectFactoryBean dataSource() throws IllegalArgumentException { JndiObjectFactoryBean dataSource = new JndiObjectFactoryBean(); dataSource.setExpectedType(DataSource.class); dataSource.setJndiName(env.getProperty(\u0026#34;jdbc.jndiDataSource\u0026#34;)); return dataSource; } @Bean @Profile(\u0026#34;test\u0026#34;) public DataSource testDataSource() { return new EmbeddedDatabaseBuilder().setType(EmbeddedDatabaseType.H2).build(); } } On retrouve une méthode de déclaration de DataSource pour chacun des 2 contextes présentés ci-dessus. Chaque méthode et annotée avec un @Profile Spring différent : javaee et test.\nLe profile javaee est activé dans le web.xml:\n\u0026lt;context-param\u0026gt; \u0026lt;param-name\u0026gt;spring.profiles.active\u0026lt;/param-name\u0026gt; \u0026lt;param-value\u0026gt;javaee\u0026lt;/param-value\u0026gt; \u0026lt;/context-param\u0026gt; Le profile test est quant à lui activé par l’annotation @ActiveProfiles(\u0026ldquo;test\u0026rdquo;) apposée sur la classe de test SpringConfigTest.\nLa méthode dataSource() utilise la fabrique de beans JndiObjectFactoryBean pour récupérer la DataSource à partir de son nom JNDI.\nLe bean Environment est de nouveau utilisé. Cette fois-ci, pour récupérer la valeur de la propriété \u0026ldquo;jdbc.jndiDataSource\u0026rdquo; définie dans le fichier datasource.properties chargée à l’aide de l’annotation @PropertySource.\nA noter que le type de retour de cette méthode est un JndiObjectFactoryBean. Pour retourner directement un DataSource, il aurait été nécessaire de se substituer au conteneur Spring en invoquant la méthode afterPropertiesSet() de la fabrique de beans :\ndataSource.afterPropertiesSet(); return (DataSource) dataSource.getObject(); En effet, appelée pendant la phase d’initialisation du bean, afterPropertiesSet() effectue le lookup JNDI. Sans cet appel, la dataSource serait null :\nCaused by: org.springframework.beans.factory.BeanDefinitionStoreException: Factory method [public javax.persistence.EntityManagerFactory com.javaetmoi.sample.config.InfrastructureConfig.entityManagerFactoryBean()] threw exception; nested exception is java.lang.IllegalArgumentException: DataSource must not be null Couche de persistance Déjà amorcée par la déclaration de la source de données, la mise en œuvre de la couche de persistance est complétée par 2 autres beans de configuration :\nInfrastructureConfig : infrastructure JPA composée d’une fabrique du gestionnaire d’entités JPA et du gestionnaire de transactions associé RepositoryConfig : activation de Spring Data JPA et détection des beans de type Repository (DAOs) L’annotation @EnableTransactionManagement portée par le bean InfrastructureConfig permet d’activer l’utilisation des annotations @Transactional chargées de délimiter les transactions base de données :\n@Configuration @EnableTransactionManagement @PropertySource({ \u0026#34;classpath:com/javaetmoi/sample/config/infrastructure.properties\u0026#34; }) public class InfrastructureConfig { @Autowired Environment env; @Autowired private DataSource dataSource; Récupérée par JNDI ou instanciée en mémoire, la DataSource est injectée sur le même modèle que n’importe quel bean Spring.\nViennent ensuite la déclaration des beans transactionManager et transactionTemplate :\n@Bean public JpaTransactionManager transactionManager() { JpaTransactionManager jpaTransactionManager = new JpaTransactionManager(); jpaTransactionManager.setEntityManagerFactory(entityManagerFactory()); return jpaTransactionManager; } @Bean public TransactionTemplate transactionTemplate() { TransactionTemplate transactionTemplate = new TransactionTemplate(); transactionTemplate.setTransactionManager(transactionManager()); return transactionTemplate; } Mettons de côté la méthode entityManagerFactory() sur laquelle nous reviendrons plus loin. Cette configuration montre comment mettre en relations 2 beans Spring : le bean transactionTemplate utilise en effet le bean transactionManager. En XML, cette mise en relation est habituellement réalisée à l’aide de la balise ref et de l’identifiant du bean. En Java, l’ injection d’un bean dans un autre se fait en utilisant la méthode de déclaration du bean à injecter, en l’occurrence ici transactionManager(). Ce qui ressemble à un appel de méthode est trompeur. En effet, Spring interprète ce type d’appel afin de gérer le cycle de vie des Beans. Par exemple, lorsqu’un bean est de portée singleton, la méthode de création du bean n’est invoquée qu’une seule et unique fois même si le bean est injecté dans plusieurs beans. Techniquement, Spring instrumente les classes annotées avec @Configuration au démarrage du contexte applicatif.\nLe bean entityManagerFactory est déclaré de la façon suivante :\nEx @Bean public EntityManagerFactory entityManagerFactory() { LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean(); em.setDataSource(dataSource); em.setPersistenceUnitName(\u0026#34;javaconfigSamplePersistenceUnit\u0026#34;); em.setPackagesToScan(\u0026#34;com.javaetmoi.sample.domain\u0026#34;); em.setJpaVendorAdapter(jpaVendorAdaper()); em.setJpaPropertyMap(additionalProperties()); em.afterPropertiesSet(); return em.getObject(); } Il utilise le bean dataSource injecté plus haut dans la classe de configuration. Derrière la méthode jpaVendorAdaper() se cache un autre bean. Quant à additionalProperties(), il s’agit d’un vrai appel de méthode. De la même manière que pour la DataSource, l’appel aux méthodes afterPropertiesSet() et getObject() sont nécessaires pour retourner un bean de type EntityManagerFactory.\nUne autre possibilité aurait consisté à renvoyer un LocalContainerEntityManagerFactoryBean puis à utiliser l’annotation @Autowired sur la méthode transactionManager. Charge à Spring de demander à la fabrique de beans de lui retourner un EntityManagerFactory:\n@Bean public LocalContainerEntityManagerFactoryBean entityManagerFactoryBean() { LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean(); // … return em; } @Bean @Autowired public JpaTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) { JpaTransactionManager jpaTransactionManager = new JpaTransactionManager(); jpaTransactionManager.setEntityManagerFactory(entityManagerFactory); return jpaTransactionManager; } L’injection du bean dataSource dans le LocalContainerEntityManagerFactoryBean aurait pu être réalisée d’une autre manière. En effet, en tant que beans Spring, les beans de configuration peuvent être injectés dans d’autres bean de configuration. Ainsi, le bean DataSourceConfig aurait pu être injecté dans InfrastructureConfig. Il devient alors possible d’appeler dataSourceConfig.dataSource() pour récupérer la dataSource :\n@Autowired private DataSourceConfig dataSourceConfig; … @Bean public EntityManagerFactory entityManagerFactory() { LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean(); em.setDataSource(dataSourceConfig.dataSource()); … } Bien que facilitant la navigation dans la configuration Spring, cette technique induit un couplage fort entre les 2 beans de configuration.\nUne fois l’infrastructure Hibernate / JPA mis en place, le bean de configuration RepositoryConfig initie la couche d’accès aux données. Par commodité, son implémentation est réalisée à l’aide du projet Spring Data JPA. L’annotation @EnableJpaRepositories active ce dernier : toutes les interfaces contenues dans un package donné et étendant l’interface Repository de Spring Data sont détectées puis interprétées. Par défaut, les requêtes JPA sont générées à partir du nom des méthodes exposées dans ces interfaces. Les implémentations personnalisées des Repository sont recherchées à l’aide du suffixe Impl.\n@Configuration @EnableJpaRepositories(\u0026#34;com.javaetmoi.sample.repository\u0026#34;) public class RepositoryConfig { } La couche de persistance est sans doute celle nécessitant le plus de configuration. A côté, la couche services métiers est particulièrement simple.\nCouche services L’objectif premier de ServiceConfig consiste à détecter tous les beans Spring annotés par l’annotation @Service. L’annotation @ComponentScan joue ce rôle en analysant récursivement le package Java dédié aux services métiers, dans notre exemple le package com.javaetmoi.sample.service :\n@Configuration @EnableAsync @EnableScheduling @EnableAspectJAutoProxy @EnableCaching @ComponentScan(basePackages = { \u0026#34;com.javaetmoi.sample.service\u0026#34; }) public class ServiceConfig implements AsyncConfigurer { C’est dans cette couche que je vous propose d’activer des fonctionnalités avancées de Spring :\n@EnableAsync: appel de méthode asynchrone via l’annotation @Async @EnableScheduling: planification d’appel de méthode via l’annotation @Scheduled @EnableAspectJAutoProxy: proxyfication de beans Spring ne possédant pas d’interface. @EnableCaching: résultat de l’appel de méthode mis en cache avec @Cacheable, évincer avec @CacheEvict ou bien rafraichit avec @CachePut. Si vous n’ pas encore passés à Java 8, l’annotation @Caching permet d’utiliser plusieurs fois l’une des 3 annotations précédentes sur la même méthode. Comme indiqué plus haut, l’annotation @EnableCaching nécessite un CacheManager. La définition d’un bean cacheManager et d’un defaultCache sont donnés ici à titre indicatif :\n@Bean public CacheManager cacheManager() { SimpleCacheManager cacheManager = new SimpleCacheManager(); List\u0026lt;Cache\u0026gt; caches = new ArrayList\u0026lt;Cache\u0026gt;(); // to customize caches.add(defaultCache()); cacheManager.setCaches(caches); return cacheManager; } @Bean public Cache defaultCache() { ConcurrentMapCacheFactoryBean cacheFactoryBean = new ConcurrentMapCacheFactoryBean(); cacheFactoryBean.setName(\u0026#34;default\u0026#34;); cacheFactoryBean.afterPropertiesSet(); return cacheFactoryBean.getObject(); } Afin de pouvoir personnaliser le pool de threads utilisé lors de traitements asynchrone, l’annotation @EnableAsync encourage à définir son propre pool en implémentant l’interface AsyncConfigurer et sa méthode public Executor getAsyncExecutor() :\n@Override public Executor getAsyncExecutor() { log.debug(\u0026#34;Creating Async Task Executor\u0026#34;); ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); // to customize with your requirements executor.setCorePoolSize(5); executor.setMaxPoolSize(40); executor.setQueueCapacity(100); executor.setThreadNamePrefix(\u0026#34;MyExecutor-\u0026#34;); executor.initialize(); return executor; } Avant de passer à la configuration de la couche présentation, terminons l’initialisation du contexte racine par la mise en place de la sécurité.\nSécurité Reposant sur projet Spring Security, la configuration de la sécurité applicative est centralisée dans le fichier SecurityConfig. L’annotation @EnableWebMvcSecurity s’utilise de pair avec la classe abstraite WebSecurityConfigurerAdapter. Cette dernière permet d’activer la configuration par défaut de Spring Security. La redéfinition de certaines méthodes permet de personnaliser la configuration via les classes AuthenticationManagerBuilder , HttpSecurity et WebSecurity.\n@Configuration @EnableWebMvcSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { Publié sur le blog de Spring, le billet Spring Security Java Config Preview: Web Security explique comment utiliser la syntaxe Java.\nDans notre exemple, la déclaration du filtre springSecurityFilterChain dans le web.xml permet de rester compatible avec les conteneurs de Servlet 2.5.@\nL’un des apports du fichier SecurityConfig réside dans la mise à disposition du bean authenticatedUserDetails. Implémentant l’interface UserDetails de Spring Security, ce bean permet d’accéder aux données utilisateurs (ex : login, nom, habilitations) de l’utilisateur authentifié. De portée session (spécifiée avec l’annotation @Scope), ce bean peut être injecté dans des beans de scopes différents, par exemple dans les contrôleurs ou les services métiers de portée singleton. Très pratique pour les logs et/ou les données de traçabilité.\n@Bean @Scope(value = \u0026#34;session\u0026#34;, proxyMode = ScopedProxyMode.TARGET_CLASS) public UserDetails authenticatedUserDetails() { SecurityContextHolder.getContext().getAuthentication(); Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if (authentication != null) { if (authentication instanceof UsernamePasswordAuthenticationToken) { return (UserDetails) authentication.getPrincipal(); } if (authentication instanceof RememberMeAuthenticationToken) { return (UserDetails) authentication.getPrincipal(); } } return null; } Remarque : le système d’authentification sur lequel est branchée votre application peut renvoyer des données supplémentaires. Il est alors fréquent d’avoir à spécialiser la classe User implémentant UserDetails. Si vous ne définissez pas d’interface sur votre classe MyUser, l’utilisation du ScopedProxyMode.TARGET_CLASS est requise par Spring pour proxyfier votre classe.\nCouche présentation La configuration Java de Spring MVC ressemble à celle de Spring Security. L’annotation @EnableWebMvc s’utilise conjointement à la classe abstraite WebMvcConfigurerAdapter. L’application bénéficie d’une configuration par défaut qu’il est possible de personnaliser par redéfinition de méthodes. La classe WebMvcConfig joue ce rôle :\n@Configuration @EnableWebMvc @ComponentScan(basePackages = { \u0026#34;com.javaetmoi.sample.web\u0026#34; }) public class WebMvcConfig extends WebMvcConfigurerAdapter { private static final int CACHE_PERIOD = 31556926; // one year @Autowired private RequestMappingHandlerAdapter requestMappingHandlerAdapter; @PostConstruct public void init() { requestMappingHandlerAdapter.setIgnoreDefaultModelOnRedirect(true); } @Bean public ViewResolver viewResolver() { // Example: the \u0026#39;info\u0026#39; view logical name is mapped to the file \u0026#39;/WEB-INF/jsp/info.jsp\u0026#39; InternalResourceViewResolver bean = new InternalResourceViewResolver(); bean.setViewClass(JstlView.class); bean.setPrefix(\u0026#34;/WEB-INF/jsp/\u0026#34;); bean.setSuffix(\u0026#34;.jsp\u0026#34;); return bean; } @Bean(name = \u0026#34;messageSource\u0026#34;) public ReloadableResourceBundleMessageSource reloadableResourceBundleMessageSource() { ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource(); messageSource.setBasenames(\u0026#34;classpath:com/javaetmoi/sample/web/messages\u0026#34;); messageSource.setDefaultEncoding(\u0026#34;UTF-8\u0026#34;); return messageSource; } @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { // Static ressources from both WEB-INF and webjars registry .addResourceHandler(\u0026#34;/resources/**\u0026#34;) .addResourceLocations(\u0026#34;/resources/\u0026#34;) .setCachePeriod(CACHE_PERIOD); registry .addResourceHandler(\u0026#34;/webjars/**\u0026#34;) .addResourceLocations(\u0026#34;classpath:/META-INF/resources/webjars/\u0026#34;) .setCachePeriod(CACHE_PERIOD); } @Override public void addViewControllers(ViewControllerRegistry registry) { registry.addViewController(\u0026#34;/about\u0026#34;); // the about page did not required any controller } @Override public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) { // Serving static files using the Servlet container\u0026#39;s default Servlet. configurer.enable(); } @Override public void addFormatters(FormatterRegistry formatterRegistry) { // add your custom formatters } } En complément de la redéfinition de méthode, il est possible de configurer Spring MVC en paramétrant les beans créés automatiquement par l’annotation @EnableWebMvc et la classe DelegatingWebMvcConfiguration qu’elle référence.\nC’est précisément le cas du bean RequestMappingHandlerAdapter qui est paramétré dans init() appelée au chargement du contexte applicatif Spring.\nPour les nouvelles applications Spring MVC, il est recommandé par Spring de forcer à true le flag ignoreDefaultModelOnRedirect. Lors de redirection, l\u0026rsquo;utilisation de l\u0026rsquo;interface RedirectAttributes devient alors nécessaire pour spécifier quelle donnée doit être passée à la RedirectView.\nLa configuration donnée en exemple ne donne qu’un aperçu de l’éventail des possibilités de configuration offertes par Spring MVC : choix de la technologie de rendu (ici JSP), accès aux ressources statiques, définition du cache, internationalisation, déclaration des formateurs globaux \u0026hellip;\nConclusion En guise de conclusion, voici un tableau récapitulant les avantages et les inconvénients qu’a un développeur Java à passer sur une syntaxe full Java :\nAvantages****Inconvénients\nType safe : syntaxe vérifiée à la compilation\nComplétion dans l’IDE sans plugin\nImport Java mettant en évidence les dépendances non présentes\nRefactoring facilité de la configuration\nAjout de code possible lors de la déclaration de beans (ex : logs)\nAccélère le chargement du contexte applicatif\nTraite la configuration comme du code\nNavigation dans la conf facilitée\nMixité possible avec la configuration XML\nPlus besoin de connaitre les namespaces XML\nNouvelle syntaxe à appréhender\nSubtilités à maîtriser : afterPropertiesSet(), appels de méthodes annotées avec @Bean qui n’en sont pas\nRéférences :\nManuel de référence Spring Framework 4 (Pivotal) Spring Java Based Configuration (Tutorials Point) Spring Data, une autre façon d’accéder aux données (So@t) Spring Security Java Config Preview: Web Security (Pivotal) Spring Java Config 101 (DZone) Spring Cache (Zenika) ","link":"https://javaetmoi.com/2014/06/spring-framework-java-configuration/","section":"posts","tags":["hibernate","java","jpa","spring-framework","spring-mvc","spring-security"],"title":"Configurez Spring en Java"},{"body":"En attendant que les vidéos des différentes conférences de l’édition 2014 de Devoxx France soient mises en ligne sur Parleys et en complément des supports déjà mis en ligne par certains Speakers, je mets librement à votre disposition les différentes notes que j’ai pu prendre sur mon laptop. Les sujets sont variés : de Docker à Angular JS, en passant par Java 8. Certaines pourront être lues de manière autonome ; je pense par exemple au quickie Outils pour manager une équipe et à la conférence 33 things your want to do better. Pour être exploitables en l’état, d’autres notes demanderont à ce que vous ayez assisté à la conférence ou que vous ayez pu récupérer les supports de présentation.\nSans plus attendre, voici donc mes 14 notes triées par ordre chronologique :\nJava 8 - Streams et Collectors Créons un web component avec Polymer JBoss Forge in Action Un PaaS Java Docker en 30 minutes Les Applications Réactives Au secours mon code Angular est pourri Spring4TW et Boot Do you really get Classloaders Outils pour manager une équipe (de développement) RxJava et Java 8 Google App Engine, çà bouge beaucoup Deux années de Continuous Delivery au pays des Traders Bootstrap your productivity with Spring Boot 33 things your want to do better Bonne lecture, découverte ou redécouverte et, je l’espère, à l’année prochaine pour Devoxx France 2015 !!\n","link":"https://javaetmoi.com/2014/04/14-prises-de-notes-a-devoxx-france-2014/","section":"posts","tags":["agile","angularjs","classloader","cloud","devops","devoxx","docker","java","jboss","polymer","rxjava","spring-boot","spring-framework"],"title":"14 prises de notes à Devoxx France 2014"},{"body":"","link":"https://javaetmoi.com/tags/agile/","section":"tags","tags":null,"title":"Agile"},{"body":"","link":"https://javaetmoi.com/tags/classloader/","section":"tags","tags":null,"title":"Classloader"},{"body":"","link":"https://javaetmoi.com/tags/polymer/","section":"tags","tags":null,"title":"Polymer"},{"body":"Lors de Devoxx France 2013, je découvrais AngularJS lors de l’Université sur AngularJS animée par Thierry Chatel. Enthousiasmé par ce framework, je vous faisais ici même une restitution de cette Université. Depuis un an, j’ai poursuivi mon initiation en codant un front-end pour Elasticsearch avec Angular. Lorsque j’ai découvert que Matthieu Lux et Olivier Huber proposaient le Hand’s-on-Lab « Angular JS from scratch : comprendre Angular en le refaisant de zéro » à Devoxx France 2014, j’y ai vu l’occasion ou jamais d’approfondir mes connaissances et de découvrir les mécanismes se cachant derrière la magie d’Angular.\nCe workshop a eu un beau succès : une salle comble 10 minutes avant son début et une place sur le podium des meilleures sessions de la matinée. Pour coder les différents exercices sans avoir à se tourner régulièrement vers les solutions, de solides connaissances en JavaScript étaient nécessaires : héritage par prototype, constructeur, portée du this, couteau suisse underscore (each, clone, isEqual) … Par ailleurs, pour apprécier la démarche, une connaissance minimaliste d’Angular me paraissait également indispensable. Durant les 3 heures du Lab, nous avons pu implémenter 11 des 12 étapes prévues initialement (la dernière étant en bonus). Timing parfaitement respecté. Si vous n’avez pas eu la chance d’assister à cette présentation et si vous disposez de 3 heures devant vous, je vous conseille de tenter de le réaliser chez vous. Les slides du workshop, le code source de départ, les solutions et les tests unitaires sous Jasmine sont disponibles dans le repo Github angular-from-scratch de Zenika.\nL’objectif de ce billet est de vous accompagner dans la réalisation du Lab. Je me focaliserai sur les mécanismes qui permettent de recoder le traditionnel « Hello Word » d’Angular. Vous y trouverez donc une version édulcorée du code du Lab. Garde-fous contre les boucles infinies et comparaisons par valeur n’y seront pas abordés. Afin de mieux comprendre où s’inscrivent les différentes étapes qui permettent de réimplémenter Angular, je m’appuierai régulièrement sur le schéma présenté par Olivier au début du Lab :\nLe code complet de ce billet est disponible dans jsfiddle et également sous forme de gist. Afin de pouvoir plus facilement se référer au code source d’Angular, le nom des méthodes et des objets utilisés dans ce Lab reprend volontairement ceux d’Angular.\nEtape 1 : le $scope Connu de tout développeur Angular, le scope est l’objet central du framework. Il permet de mettre à disposition des vues et des contrôleurs le modèle de données de l’application. Contrairement à d’autres frameworks comme Backbone, vous pouvez y placer des objets JavaScript standard (POJSO). La particularité du scope est de pouvoir être observé. A l’instar du pattern Observer, des watchers peuvent s’enregistrer et être à l’écoute de tout changement sur le modèle, dans sa globalité ou sur une partie donnée. Les watchers sont tout simplement matérialisés par un tableau JavaScript :\nfunction Scope() { this.$$watchers = []; } En général, Angular instancie pour vous le scope des différentes vues composant une page. Pour les besoins du Lab, nous l’instancions manuellement :\nvar scope = new Scope(); Nous y définissons un objet labs comportant 2 propriétés :\nscope.labs = { titre: \u0026#34;AngularJS from scratch\u0026#34;, date: new Date() } La page HTML référence le titre objet de l’objet labs :\n\u0026lt;h1 class=\u0026#34;page-header\u0026#34; ng-bind=\u0026#34;labs.titre\u0026#34;\u0026gt;AngularJS from scratch\u0026lt;/h1\u0026gt; \u0026lt;input type=\u0026#34;text\u0026#34; ng-model=\u0026#34;labs.titre\u0026#34;/\u0026gt; Nous reviendrons sur les directives ng-bind et ng-model lors des étapes 10 et 11.\nEtape 2 : Scope.$watch Comme vu dans l’étape précédente, le scope contient un tableau de watchers. Le but de cette étape est d’implémenter une fonction $watch permettant d’ajouter un watcher dans le tableau $$watchers du scope. Quiconque le souhaite pourra alors surveiller une donnée du scope.\nScope.prototype.$watch = function (watcherFn, listenerFn) { var watcher = { watcherFn: watcherFn, listenerFn: listenerFn, last: undefined }; this.$$watchers.push(watcher); } La fonction $watch est ajoutée dans le prototype du Scope. Toute instance de Scope hérite ainsi de cette fonction. La ligne this.$$watchers.push(watcher); ne pose aucune difficulté.\nUn watcher est caractérisé par 3 éléments :\nune fonction watcherFn indiquant quelle donnée du modèle l’appelant souhaite observer, une fonction de rappel listenerFn appelée lorsqu’un changement sera détecté une variable interne last permettant de sauvegarder la précédente valeur du modèle et de réaliser le dirty checking. Voici un exemple d’appel à la fonction $watch :\nscope.$watch(function (scope) { return scope.labs.titre; }, function (newValue, oldValue, scope) { console.log(\u0026#34;La titre a changé de\u0026#34;, oldValue, \u0026#34;à\u0026#34;, newValue); }); En pratique, un développeur Angular fait rarement appel explicitement à cette méthode.\nEtape 3 et 5 : Scope.$digest et digest loop La fonction $digest est au cœur d’Angular. Sur le schéma ci-dessus, elle représente la digest loop. Comme son nom l’indique, son algorithme principal consiste à boucler sur le tableau de watchers jusqu’à ce que tous les évènements aient été traités. Par évènement, on entend un changement dans le modèle. Voici un exemple d’implémentation :\nScope.prototype.$digest = function () { var dirty; do { dirty = false; _.each(this.$$watchers, function (watcher) { var newValue = watcher.watcherFn(this); if (watcher.last !== newValue) { watcher.listenerFn(newValue, watcher.last, this); watcher.last = newValue; dirty = true; } }.bind(this)); } while (dirty); } Quelques explications peuvent être nécessaires à la compréhension de ce code :\nL’itération sur le tableau $$watchers est réalisée par la méthode each d’Underscore La méthode watcherFn accepte comme argument le scope à observer. Ici, un this est passé en paramètre. Sans l’utilisation du bind(this), ce serait le this de l’inner fonction qui aurait été passé à watcherFn et non le scope sur lequel la méthode $digest est appelée. bind(this) est une technique native JavaScript que je ne connaissais pas. Elle permet de forcer le this. Une technique plus repandue est l’utilisation d’un var self=this; avant la déclaration de l’inner fonction. Underscore aurait également pu être utilisé pour gérer cette problématique récurrente en JavaScript. Lorsqu’un changement est détecté, la fonction de rappel listenerFn est appelée avec la nouvelle valeur, l’ancienne valeur et le scope. A chaque fois qu’un $digest est appelé, la fonction watcherFn de tous les watchers est appelée. Cela a un coût. Et c’est pourquoi les auteurs d’Angular encouragent à garder cette fonction la plus légère possible. Appels réseaux et algorithmes complexes y sont à proscrire.\nLors du Lab, nous avons ajouté 3 améliorations :\nUn premier garde-fou permettant d’éviter un appel infini en levant une erreur après 10 itérations Un second garde-fou permettant d’éviter des appels récursifs à la méthode $digest (étape 7). La possibilité d’effectuer des comparaisons par valeur et non pas uniquement par référence. La comparaison de tableaux ou de grappes d’objets devient alors possible (étape 6). Etape 4 : Scope.$apply La fonction $apply exécute une expression passée en argument puis lance quoi qu’il arrive un $digest :\nScope.prototype.$apply = function (exprFn) { try { exprFn(); } finally { this.$digest(); } } Cette méthode est appelée en interne par Angular lorsqu’il a besoin de binder une donnée, par exemple lors de l’utilisation de la directive ng-bind dans les templates. Tous les composants Angular y font appels.\nEn dehors d’un contexte Angular, cette méthode doit explicitement être appelée par le développeur. C’est typiquement le cas depuis une callback jQuery. L’étape 5 ayant été traitée en même temps que l’étape 3 et les étapes 6 et 7 étant facultatives pour l’objectif fixé initialement, nous enchaînons directement à l’étape 8.\nEtape 8 : place aux directives Dans le fragment HTML présenté au début du billet, 2 directives viennent enrichir le HTML sous forme d’attributs : ng-model et ng-bind. Dans Angular, une directive n’est rien d’autre qu’une fonction ou un objet ayant des propriétés bien définies. Pour les besoins du Lab, nous resterons sur le cas simple : la fonction. L’ objet $$directives doit permettre d’enregistrer les fonctions associées à ces directives. Pour rappel, un objet JavaScript peut être utilisé de la même manière qu’un tableau associatif (une Map en Java) : à partir de la clé (chaine ‘ng-bind’) on récupère la valeur (fonction ng-bind). La fonction $directive permet quant à elle d’ajouter une directive et de lire une directive depuis l’objet $$directives.\nvar $$directives = {}; var $directive = function (name, directiveFn) { if (directiveFn) { $$directives[name] = directiveFn; } return $$directives[name]; } La fonction $directive fait à la fois getter et setter pour les $$directives. L’implémentation utilise une technique répandue en JavaScript : lorsque seul le nom d’une directive est passé en paramètre, la fonction agit comme un getter. Lorsque le nom et le code d’une directive sont passés en paramètre, la fonction enregistre la fonction avant de la retourner.\nA noter que les développeurs Angular ne manipulent jamais directement ces 2 objets. Ils sont utilisés par le moteur d’injection de dépendance d’Angular.\nEtape 9 : $compile le DOM Cette étape consiste à écrire la fonction $compile chargée de parcourir récursivement les éléments du DOM. Les attributs de chaque élément sont également parcourus. Lorsqu’un attribut correspond au nom d’une directive, la fonction implémentant la directive est exécutée. Le code est compréhensif :\nvar $compile = function(element, scope) { _.each(element.children, function (child) { $compile(child, scope); }); _.each(element.attributes, function(attribute) { var directiveFn = $directive(attribute.name); if (directiveFn) { directiveFn(scope, element, element.attributes); } }); } Deux remarques à propos du code :\nContrairement à ce que l’on pouvait s’attendre, tous les attributs de l’élément sur lequel est apposée la directive sont passés en paramètre de la fonction directiveFn. La récursion sur les éléments enfants est lancée avant le parcours des attributs de l’élément. Angular offre le choix avec les propriétés prelink et postlink. De manière générale, postlink est à privilégier. Pour demander à notre framework maison de parcourir l’intégralité du DOM, une unique ligne de code est nécessaire :\n$compile(document.body, scope); A présent que le framework sait découvrir des directives dans le DOM et appeler la fonction correspondante, il est temps d’implémenter une première directive.\nEtape 10 : ng-bind La directive ng-bind se met à l’écoute sur la donnée pointée par la valeur de l’attribut ng-bind. Lors d’un changement de valeur, l’élément du DOM (dans notre exemple ) est modifié avec la nouvelle valeur.\n$directive(\u0026#39;ng-bind\u0026#39;, function (scope, element, attributes) { scope.$watch(function(scope) { return eval(\u0026#39;scope.\u0026#39; + attributes[\u0026#39;ng-bind\u0026#39;].value); // \u0026#39;scope. labs.titre\u0026#39; }, function(newValue) { element.innerHTML = newValue; }); }); Bien que controversée, l’utilisation de la fonction eval simplifie ici le code. Au cours du Lab, Matthieu nous a donné son équivalent fonctionnel. Basé sur les fonctions split et reduce, le code devient illisible pour les développeurs ne pratiquant pas ce paradigme.\nEtape 11 : ng-model ng-model est une directive gérant le data-binding bidirectionnel. Appliquée à la balise , elle permet d’y afficher le contenu du modèle même lorsque celui-ci change et de mettre à jour le modèle lorsque l’utilisateur saisit des données dans le champ de saisie. S’agissant d’une version ++ de la directive ng-bind, le début de leur implémentation correspondant au premier sens de binding se ressemble :\n$directive(\u0026#39;ng-model\u0026#39;, function(scope, element, attributes) { scope.$watch(function() { return eval(\u0026#39;scope.\u0026#39; + attributes[\u0026#39;ng-model\u0026#39;].value); }, function(newValue) { element.value = newValue; }); element.addEventListener(\u0026#39;keyup\u0026#39;, function() { scope.$apply(function() { eval(\u0026#39;scope.\u0026#39; + attributes[\u0026#39;ng-model\u0026#39;].value + \u0026#39; = \\\u0026#34;\u0026#39; + element.value + \u0026#39;\\\u0026#34;\u0026#39;); }); }); }); La directive ng-model ajoute un listener d’évènements JavaScript. Lorsque l’évènement \u0026lsquo;keyup\u0026rsquo; survient, le modèle est mis à jour à l’intérieur de la fonction $apply. Cette dernière déclenche la digest loop qui notifie la balise ng-bind. C’est par ce mécanisme que lorsque l’utilisateur saisi du texte dans l’input, le titre est mis à jour en conséquence. La fonction eval est là encore utilisée. Angular n’y fait pas appel car il possède son propre parseur.\nHello World Une fois ces 2 directives enregistrées, un changement du titre du labs met simultanément à jour le titre et le champs de saisie :\nscope.$apply(function () { scope.labs.titre = \u0026#34;Hello World\u0026#34;; }) Conclusion 92 lignes de JavaScript auront été nécessaires pour ré-implémenter une version minimaliste du cœur d’AngularJS. Vous pouvez tester : le code fonctionne sous IE 11, Firefox 28 et Chrome 34. Underscore aura permis de gagner en clarté ainsi que quelques lignes de code. N’ayant que quelques dizaines d’heures d’Angular à mon actif, j’espère que ce que je vous aurais restitué sera exempt d’erreurs. Dans le cas contraire, je compte sur les speakers et vous pour me rectifier.\n","link":"https://javaetmoi.com/2014/04/lab-angularjs-from-scratch-devoxx-france-2014/","section":"posts","tags":["angularjs","devoxx"],"title":"Comprendre AngularJS en le recodant à Devoxx France 2014"},{"body":"","link":"https://javaetmoi.com/tags/bug/","section":"tags","tags":null,"title":"Bug"},{"body":"","link":"https://javaetmoi.com/categories/orm/","section":"categories","tags":null,"title":"Orm"},{"body":" Dans mon précédent billet, je vous expliquais comment réintroduire le support de VFS 2 abandonné dans Spring Framework 4.0. Et ceci, dans l’optique de pouvoir déployer mon application dans le serveur d’application JBoss 5.1 EAP.\nOutre ce problème survenant lors de la détection de beans Spring dans le classpath, la montée de version de Spring 3.2 à Spring 4 a révélé un autre problème lié au format VFS de JBoss. Cette fois-ci, c’est le framework Hibernate 4.3 qui n’arrive pas à détecter les entités JPA du classpath.\nCertifié conforme à Java EE 5, JBoss 5.1 EAP utilise Hibernate 3.3 comme implémentation de JPA 1. Dans mon cas, Hibernate 4.3 est utilisé en mode standalone et est donc directement embarqué dans les librairies de mon EAR. La configuration JPA 2.1 est assurée par le support JPA offert par Spring, et plus particulièrement par la classe LocalContainerEntityManagerFactoryBean.\nProblème rencontré Au démarrage de l’application, Hibernate s’appuie sur l’interface Scanner pour détecter les classes, les packages et les ressources d’une Unité de Persistance JPA. L’implémentation StandardScanner délègue le parcours du WAR à la classe StandardArchiveDescriptorFactory. Ligne 72 de cette dernière, l’appel à la méthode file.exists() échoue sur le chemin « vfszip:/C:/jboss-eap-5.1/server/default/deploy/myapp-ear-1.0.0-SNAPSHOT.ear/myapp-war-1.0.0-SNAPSHOT.war/WEB-INF/classes/ ». Une IllegalArgumentException est levée :\n21:51:24,472 INFO [STDOUT] 21:51:24.472 | INFO | | Building JPA container EntityManagerFactory for persistence unit \u0026#39;persistenceUnit\u0026#39;| org.springframework.orm.jpa.LocalContainerEntityManagerFactoryB ean.createNativeEntityManagerFactory(LocalContainerEntityManagerFactoryBean.java:332) 21:51:24,550 INFO [STDOUT] 21:51:24.550 | INFO | | HHH000204: Processing PersistenceUnitInfo [ name: persistenceUnit ...] | org.hibernate.jpa.internal.util.LogHelper.logPersistenceUnitInformation(LogHelper.java:46) 21:51:24,754 INFO [STDOUT] 21:51:24.754 | INFO | | HHH000412: Hibernate Core {4.3.4.Final} | org.hibernate.Version.logVersion(Version.java:54) 21:51:24,754 INFO [STDOUT] 21:51:24.754 | INFO | | HHH000206: hibernate.properties not found | org.hibernate.cfg.Environment.\u0026lt;clinit\u0026gt;(Environment.java:239) 21:51:24,769 INFO [STDOUT] 21:51:24.754 | INFO | | HHH000021: Bytecode provider name : javassist | org.hibernate.cfg.Environment.buildBytecodeProvider(Environment.java:346) 21:51:33,848 INFO [STDOUT] 21:51:33.848 | ERROR | | Context initialization failed | org.springframework.web.context.ContextLoader.initWebApplicationContext(ContextLoader.java:331) org.springframework.beans.factory.BeanCreationException: Error creating bean with name \u0026#39;entityManagerFactoryBean\u0026#39; defined in class com.javaetmoi.sample.config.InfrastructureConfig: Invocation of init method failed; nested exception is java.lang.IllegalArgumentException: File [/C:/jboss-eap-5.1/server/default/deploy/myapp-ear-1.0.0-SNAPSHOT.ear/myapp-war-1.0.0-SNAPSHOT.war/WEB-INF/classes/] referenced by given URL [vfszip:/C:/jboss-eap-5.1/server/default/deploy/myapp-ear-1.0.0-SNAPSHOT.ear/myapp-war-1.0.0-SNAPSHOT.war/WEB-INF/classes/] does not exist at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1553) ~[spring-beans-4.0.2.RELEASE.jar:4.0.2.RELEASE] at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:539) ~[spring-beans-4.0.2.RELEASE.jar:4.0.2.RELEASE] at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:475) ~[spring-beans-4.0.2.RELEASE.jar:4.0.2.RELEASE] at org.springframework.beans.factory.support.AbstractBeanFactory$1.getObject(AbstractBeanFactory.java:304) ~[spring-beans-4.0.2.RELEASE.jar:4.0.2.RELEASE] at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:228) ~[spring-beans-4.0.2.RELEASE.jar:4.0.2.RELEASE] at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:300) ~[spring-beans-4.0.2.RELEASE.jar:4.0.2.RELEASE] at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:195) ~[spring-beans-4.0.2.RELEASE.jar:4.0.2.RELEASE] at org.springframework.context.support.AbstractApplicationContext.getBean(AbstractApplicationContext.java:973) ~[spring-context-4.0.2.RELEASE.jar:4.0.2.RELEASE] at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:750) ~[spring-context-4.0.2.RELEASE.jar:4.0.2.RELEASE] at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:482) ~[spring-context-4.0.2.RELEASE.jar:4.0.2.RELEASE] at org.springframework.web.context.ContextLoader.configureAndRefreshWebApplicationContext(ContextLoader.java:403) ~[spring-web-4.0.2.RELEASE.jar:4.0.2.RELEASE] at org.springframework.web.context.ContextLoader.initWebApplicationContext(ContextLoader.java:306) ~[spring-web-4.0.2.RELEASE.jar:4.0.2.RELEASE] at org.springframework.web.context.ContextLoaderListener.contextInitialized(ContextLoaderListener.java:106) [spring-web-4.0.2.RELEASE.jar:4.0.2.RELEASE] at org.apache.catalina.core.StandardContext.listenerStart(StandardContext.java:3910) [jbossweb.jar!/:na] at org.apache.catalina.core.StandardContext.start(StandardContext.java:4389) [jbossweb.jar!/:na] at org.jboss.web.tomcat.service.deployers.TomcatDeployment.performDeployInternal(TomcatDeployment.java:321) [jboss-web-service.jar!/:5.1.1 (build: SVNTag=JBPAPP_5_1_1 date=201105171607)] ... at org.jboss.Main$1.run(Main.java:556) [run.jar:5.1.1 (build: SVNTag=JBPAPP_5_1_1 date=201105171607)] at java.lang.Thread.run(Thread.java:662) [na:1.6.0_26] Caused by: java.lang.IllegalArgumentException: File [/C:/jboss-eap-5.1/server/default/deploy/myapp-ear-1.0.0-SNAPSHOT.ear/myapp-war-1.0.0-SNAPSHOT.war/WEB-INF/classes/] referenced by given URL [vfszip:/C:/jboss-eap-5.1/server/default/deploy/myapp-ear-1.0.0-SNAPSHOT.ear/myapp-war-1.0.0-SNAPSHOT.war/WEB-INF/classes/] does not exist at org.hibernate.jpa.boot.archive.internal.StandardArchiveDescriptorFactory.buildArchiveDescriptor(StandardArchiveDescriptorFactory.java:73) ~[hibernate-entitymanager-4.3.4.Final.jar:4.3.4.Final] at org.hibernate.jpa.boot.archive.internal.StandardArchiveDescriptorFactory.buildArchiveDescriptor(StandardArchiveDescriptorFactory.java:48) ~[hibernate-entitymanager-4.3.4.Final.jar:4.3.4.Final] at org.hibernate.jpa.boot.scan.spi.AbstractScannerImpl.buildArchiveDescriptor(AbstractScannerImpl.java:95) ~[hibernate-entitymanager-4.3.4.Final.jar:4.3.4.Final] at org.hibernate.jpa.boot.scan.spi.AbstractScannerImpl.scan(AbstractScannerImpl.java:70) ~[hibernate-entitymanager-4.3.4.Final.jar:4.3.4.Final] at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.scan(EntityManagerFactoryBuilderImpl.java:723) ~[hibernate-entitymanager-4.3.4.Final.jar:4.3.4.Final] at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.\u0026lt;init\u0026gt;(EntityManagerFactoryBuilderImpl.java:219) ~[hibernate-entitymanager-4.3.4.Finl.jar:4.3.4.Final] at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.\u0026lt;init\u0026gt;(EntityManagerFactoryBuilderImpl.java:186) ~[hibernate-entitymanager-4.3.4.Final.jar:4.3.4.Final] at org.hibernate.jpa.boot.spi.Bootstrap.getEntityManagerFactoryBuilder(Bootstrap.java:45) ~[hibernate-entitymanager-4.3.4.Final.jar:4.3.4.Final] at org.hibernate.jpa.boot.spi.Bootstrap.getEntityManagerFactoryBuilder(Bootstrap.java:57) ~[hibernate-entitymanager-4.3.4.Final.jar:4.3.4.Final] at org.hibernate.jpa.HibernatePersistenceProvider.createContainerEntityManagerFactory(HibernatePersistenceProvider.java:150) ~[hibernate-entitymanager-4.3.4.Final.jar:4.3.4.Final] at org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean.createNativeEntityManagerFactory(LocalContainerEntityManagerFactoryBean.java:336)~[spring-orm-4.0.2.RELEASE.jar:4.0.2.RELEASE] at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.afterPropertiesSet(AbstractEntityManagerFactoryBean.java:318) ~[spring-orm-4.0.2.RELEASE.jar:4.0.2.RELEASE] at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1612) ~[spring-beans-4.0.2.RELEASE.jar:4.0.2.RELEASE] at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1549) ~[spring-beans-4.0.2.RELEASE.jar:4.0.2.RELEASE] ... 82 common frames omitted Solution Comme précisé en introduction, notre application utilise la fabrique de beans LocalContainerEntityManagerFactoryBean pour créer l’ EntityManagerFactory de JPA. Lors de la configuration de ce bean, la méthode setPackagesToScan permet d’indiquer à Spring quel package Java il doit scanner au démarrage de l’application pour détecter les entités JPA. Spring utilise alors le même mécanisme d’auto-détection que pour les beans Spring et scanne tous les JAR du classpath.\nDans l’exemple ci-desssous, le package com.javaetmoi.demo.model ainsi que tous ses sous-packages sont scannés :\n@Bean public LocalContainerEntityManagerFactoryBean entityManagerFactoryBean() { LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean(); em.setDataSource(dataSource()); em.setPackagesToScan(\u0026#34;com.javaetmoi.persistencedemo.model\u0026#34;); em.setJpaVendorAdapter(new HibernateJpaVendorAdapter();); em.setJpaProperties(additionalProperties()); return em; } Le support JPA proposé par Spring fait que le fichier pesistence.xml n’a plus de raison d’être. Et vous l’aurez compris, le scan d’Hibernate ait redondant à celui de Spring. Il peut donc être désactivé.\nLa classe NopScanner disponible sous forme de Gist permet de court-circuiter le scan d’Hibernate :\nNo-operation Hibernate Scanner Indiquer à Hibernate d’utiliser la classe NopScanner revient à déclarer la propriété Hibernate suivante :\nhibernate.ejb.resource_scanner=com.javaetmoi.core.persistence.hibernate.NopScanner\nEn utilisant la syntaxe Java, cette propriété peut être ajoutée dans la méthode additionalProperties() utilisée lors de la déclaration du bean LocalContainerEntityManagerFactoryBean dans l’exemple précédent :\nprivate Properties additionalProperties() { return new Properties() { { setProperty(\u0026#34;hibernate.ejb.resource_scanner\u0026#34;, \u0026#34;com.javaetmoi.core.persistence.hibernate.NopScanner\u0026#34;); setProperty(\u0026#34;hibernate.dialect\u0026#34;,env.getProperty(\u0026#34;hibernate.dialect\u0026#34;)); } }; } Conclusion La possibilité offerte par JPA de pouvoir être utilisée en dehors d’un conteneur Java EE ainsi que la souplesse du support JPA proposé par le framework Spring font qu’il est possible d’utiliser JPA 2.1 (introduit ans JEE 7) dans un serveur JEE 5.\nAyant utilisé cette approche dans ce billet, je vous donnerai prochainement un exemple complet de configuration Spring en Java d’une application basée sur Spring MVC et JPA. Restez connectés !!\n","link":"https://javaetmoi.com/2014/04/hibernate4-sous-jboss5-avec-spring4/","section":"posts","tags":["bug","hibernate","jpa","spring-framework"],"title":"Utilisez Hibernate 4.3 sous JBoss 5 avec Spring 4"},{"body":" Ce billet solutionne un problème rencontré lors de la montée de version du famework Spring de la version 3.2 à la version 4.0. En effet, le déploiement d’une application sous JBoss 5.1 EAP échouait dès l’initialisation du contexte Spring. Plus précisément, une exception était levée lorsque Spring scanne le classpath à la recherche de beans Spring annotés par les annotations @Repository, @Service, @Controller …\nComme le montre la pile d’appel complète ci-dessous, l’exception java.lang.ClassNotFoundException: org.jboss.vfs.VFS est encapsulée dans l’exception java.lang.IllegalStateException: Could not detect JBoss VFS infrastructure\nCe problème ne m’était initialement pas apparu lors des développements sous Eclipse avec le plugin JBoss Tools pour WTP : Spring n’a aucun mai à trouver les beans d’un WAR ou d’un EAR explosé. Cette erreur s’est manifestée lors du déploiement manuel de l’EAR dans le répertoire deploy de JBoss puis du démarrage du serveur par la commande run.bat.\n14:01:54,811 INFO [STDOUT] 14:01:54.811 | ERROR | Context initialization failed | org.springframework.web.context.ContextLoader.initWebApplicationContext(ContextLoader.java:336) java.lang.ExceptionInInitializerError: null at org.springframework.core.io.support.PathMatchingResourcePatternResolver$VfsResourceMatchingDelegate.findMatchingResources(PathMatchingResourcePatternResolver.java:652) ~[spring-core-4.0.2.RELEASE.jar:4.0.2.RELEASE] at org.springframework.core.io.support.PathMatchingResourcePatternResolver.findPathMatchingResources(PathMatchingResourcePatternResolver.java:347) ~[spring-core-4.0.2.RELEASE.jar:4.0.2.RELEASE] at org.springframework.core.io.support.PathMatchingResourcePatternResolver.getResources(PathMatchingResourcePatternResolver.java:269) ~[spring-core-4.0.2.RELEASE.jar:4.0.2.RELEASE] at org.springframework.context.support.AbstractApplicationContext.getResources(AbstractApplicationContext.java:1170) ~[spring-context-4.0.2.RELEASE.jar:4.0.2.RELEASE] at org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider.findCandidateComponents(ClassPathScanningCandidateComponentProvider.java:268) ~[spring-context-4.0.2.RELEASE.jar:4.0.2.RELEASE] at org.springframework.context.annotation.ClassPathBeanDefinitionScanner.doScan(ClassPathBeanDefinitionScanner.java:242) ~[spring-context-4.0.2.RELEASE.jar:4.0.2.RELEASE] at org.springframework.context.annotation.ComponentScanAnnotationParser.parse(ComponentScanAnnotationParser.java:134) ~[spring-context-4.0.2.RELEASE.jar:4.0.2.RELEASE] at org.springframework.context.annotation.ConfigurationClassParser.doProcessConfigurationClass(ConfigurationClassParser.java:236) ~[spring-context-4.0.2.RELEASE.jar:4.0.2.RELEASE] at org.springframework.context.annotation.ConfigurationClassParser.processConfigurationClass(ConfigurationClassParser.java:205) ~[spring-context-4.0.2.RELEASE.jar:4.0.2.RELEASE] at org.springframework.context.annotation.ConfigurationClassParser.processImports(ConfigurationClassParser.java:426) ~[spring-context-4.0.2.RELEASE.jar:4.0.2.RELEASE] at org.springframework.context.annotation.ConfigurationClassParser.doProcessConfigurationClass(ConfigurationClassParser.java:248) ~[spring-context-4.0.2.RELEASE.jar:4.0.2.RELEASE] at org.springframework.context.annotation.ConfigurationClassParser.processConfigurationClass(ConfigurationClassParser.java:205) ~[spring-context-4.0.2.RELEASE.jar:4.0.2.RELEASE] at org.springframework.context.annotation.ConfigurationClassParser.parse(ConfigurationClassParser.java:182) ~[spring-context-4.0.2.RELEASE.jar:4.0.2.RELEASE] at org.springframework.context.annotation.ConfigurationClassParser.parse(ConfigurationClassParser.java:152) ~[spring-context-4.0.2.RELEASE.jar:4.0.2.RELEASE] at org.springframework.context.annotation.ConfigurationClassPostProcessor.processConfigBeanDefinitions(ConfigurationClassPostProcessor.java:299) ~[spring-context-4.0.2.RELEASE.jar:4.0.2.RELEASE] at org.springframework.context.annotation.ConfigurationClassPostProcessor.postProcessBeanDefinitionRegistry(ConfigurationClassPostProcessor.java:243) ~[spring-context-4.0.2.RELEASE.jar:4.0.2.RELEASE] at org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanDefinitionRegistryPostProcessors(PostProcessorRegistrationDelegate.java:254) ~[spring-context-4.0.2.RELEASE.jar:4.0.2.RELEASE] at org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(PostProcessorRegistrationDelegate.java:94) ~[spring-context-4.0.2.RELEASE.jar:4.0.2.RELEASE] at org.springframework.context.support.AbstractApplicationContext.invokeBeanFactoryPostProcessors(AbstractApplicationContext.java:609) ~[spring-context-4.0.2.RELEASE.jar:4.0.2.RELEASE] at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:464) ~[spring-context-4.0.2.RELEASE.jar:4.0.2.RELEASE] at org.springframework.web.context.ContextLoader.configureAndRefreshWebApplicationContext(ContextLoader.java:403) ~[spring-web-4.0.2.RELEASE.jar:4.0.2.RELEASE] at org.springframework.web.context.ContextLoader.initWebApplicationContext(ContextLoader.java:306) ~[spring-web-4.0.2.RELEASE.jar:4.0.2.RELEASE] at org.springframework.web.context.ContextLoaderListener.contextInitialized(ContextLoaderListener.java:106) [spring-web-4.0.2.RELEASE.jar:4.0.2.RELEASE] ... X common frames omitted at java.lang.Thread.run(Thread.java:662) [na:1.6.0_26] Caused by: java.lang.IllegalStateException: Could not detect JBoss VFS infrastructure at org.springframework.core.io.VfsUtils.\u0026lt;clinit\u0026gt;(VfsUtils.java:92) ~[spring-core-4.0.2.RELEASE.jar:4.0.2.RELEASE] ... 93 common frames omitted Caused by: java.lang.ClassNotFoundException: org.jboss.vfs.VFS from BaseClassLoader@1269ac3{vfszip:/C:/dev/servers/jboss-eap-5.1/server/default/deploy/myapp-ear-1.0.0-SNAPSHOT.ear/} at org.jboss.classloader.spi.base.BaseClassLoader.loadClass(BaseClassLoader.java:477) ~[jboss-classloader.jar:2.0.9.GA] at java.lang.ClassLoader.loadClass(ClassLoader.java:247) ~[na:1.6.0_26] at org.springframework.core.io.VfsUtils.\u0026lt;clinit\u0026gt;(VfsUtils.java:69) ~[spring-core-4.0.2.RELEASE.jar:4.0.2.RELEASE] ... 93 common frames omitted 14:01:55,014 ERROR [StandardContext] Error listenerStart Diagnostic Les versions communautaires JBoss AS 5 et commerciales JBoss 5 EAP s’appuient toutes les deux sur la version 2 du Virtual File System. Or, comme l’atteste le commit de Juergen Hoeller dans GitHub, le support de VFS 2 a été volontairement retiré de Spring 4.\nComme je m’y attendais, je ne suis pas le seul développeur à regretter cet abandon. Le forum de Spring en donne une idée. Qui plus est, la documentation de Spring n’est pas tout à fait à jour à ce sujet. J’ai soumis la pull request concernant la JavaDoc de la classe VfsResource.\nRéactivation de VFS2 Rétablir le support de VFS 2 dans Spring 4 n’a pas été très compliqué. J’ai tout simplement dupliqué le code de la classe VfsUtils de Spring 3.2 dans la classe Vfs2Utils. Il a ensuite été nécessaire d’ implémenter l’interface ResourcePatternResolver et de câbler cette implémentation dans les classes responsables du chargement du contexte applicatif Spring (qui héritent de la classe AbstractApplicationContext).\nPour celles et ceux que cela intéresserait, j’ai publié ces quelques classes dans le projet spring4-vfs2-support sous GitHub. Les 2 classes JBoss5XmlWebApplicationContext et JBoss5AnnotationConfigWebApplicationContext en sont les points d’entrée.\nUtilisation Pour utiliser Spring 4 avec JBoss 5, vous pouvez copier/coller le code du repo spring4-vfs2-support ou bien tirer la dépendance maven suivante :\n\u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.javaetmoi.core\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;javaetmoi-spring4-vfs2-support\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.1.0\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; L’artefact javaetmoi-spring4-vfs2-support est publié sur un repository maven hébergé par CloudBees. Ce repo doit être référencé dans votre proxy Maven d’Entreprise (ex : Nexus) ou bien déclaré dans votre pom.xml au niveau de la balise :\n\u0026lt;repository\u0026gt; \u0026lt;id\u0026gt;javaetmoi-maven-release\u0026lt;/id\u0026gt; \u0026lt;releases\u0026gt; \u0026lt;enabled\u0026gt;true\u0026lt;/enabled\u0026gt; \u0026lt;/releases\u0026gt; \u0026lt;name\u0026gt;Java et Moi Maven RELEASE Repository\u0026lt;/name\u0026gt; \u0026lt;url\u0026gt;http://repository-javaetmoi.forge.cloudbees.com/release/\u0026lt;/url\u0026gt; \u0026lt;/repository\u0026gt; Une fois la configuration Maven terminée, il reste à indiquer à Spring quelle classe il doit utiliser pour charger sa configuration. Exemple de configuration du listener ContextLoaderListener dans le web.xml :\n\u0026lt;context-param\u0026gt; \u0026lt;param-name\u0026gt;contextClass\u0026lt;/param-name\u0026gt; \u0026lt;param-value\u0026gt; com.javaetmoi.core.spring.JBoss5XmlWebApplicationContext\u0026lt;/param-value\u0026gt; \u0026lt;/context-param\u0026gt; \u0026lt;listener\u0026gt; \u0026lt;listener-class\u0026gt;org.springframework.web.context.ContextLoaderListener\u0026lt;/listener-class\u0026gt; \u0026lt;/listener\u0026gt; Conclusion A l’exception de celles basées sur la spécification Servlet 3.0, vous pouvez souhaiter la bienvenues à toutes les nouveautés de Spring 4.\n","link":"https://javaetmoi.com/2014/04/support-vfs2-jboss5-spring4/","section":"posts","tags":["bug","jboss","spring-framework"],"title":"Support du VFS 2 de JBoss 5 dans Spring 4"},{"body":"","link":"https://javaetmoi.com/tags/jms/","section":"tags","tags":null,"title":"Jms"},{"body":"","link":"https://javaetmoi.com/tags/oracle/","section":"tags","tags":null,"title":"Oracle"},{"body":"Ce billet ne devrait intéresser que les développeurs Java ou administrateurs JBoss en charge de la configuration de JBoss Messaging, le broker JMS intégré aux versions 4.3 et 5.x du serveur d’application JBoss EAP. Pour fil conducteur, prenons l’exemple d’une application Java EE déployée dans un pseudo cluster JBoss où, par choix d’architecture technique, chaque serveur JBoss est autonome. A ce titre, les sessions HTTP ne sont pas partagées entre les différents serveurs JBoss ; le répartiteur de charge fonctionne en affinité de session De plus, chaque serveur dispose de ses propres files JMS (clustering JBoss Messaging non mis en œuvre). Les messages JMS sont persistés dans une base de données, Oracle dans notre cas . La persistance des messages peut se faire de 2 manières :\nUtiliser un schéma Oracle différent pour chaque serveur JBoss du cluster Utiliser le même schéma pour tous les serveurs JBoss du cluster JBoss Messaging supportant le multi-tenancy, cet article explique comment mettre en œuvre la 2ième solution.\nSolution Le manuel d’administration de JBoss Messaging explique clairement comment configurer JBoss Messaging en cluster ; les files JMS sont alors partagées pour tous les serveurs JBoss du même cluster. Par contre, elle reste évasive sur l’utilisation d’une même base de données pour plusieurs serveurs qui ne seraient pas en cluster.\nTechniquement, les files JMS et leurs messages sont sauvegardées dans un ensemble de 11 tables, préfixées par le trigramme JBM_. Notre objectif est que la source de données dédiée à JBoss Messaging soit la même pour tous les serveurs. Ces tables sont ainsi partagées par l’ensemble des serveurs JBoss.\nSans paramétrage particulier, l’émission simultanée de plusieurs messages JMS à partir de serveurs différents génère les warnings suivants :\n2014-03-20 13:02:41,464 WARN [org.jboss.messaging.core.impl.JDBCSupport] (ajp-172.40.24.152-8009-20) SQLException caught, java.sql.SQLIntegrityConstraintViolationException: ORA-00001: violation de contrainte unique (MYAPPUSER.SYS_C0018809) at oracle.jdbc.driver.T4CTTIoer.processError(T4CTTIoer.java:439) ... at org.jboss.messaging.core.impl.JDBCPersistenceManager.cacheID(JDBCPersistenceManager.java:1967) La contrainte d’unicité concerne la table JBM_ID_CACHE : les serveurs réservent le même identifiant de message. Pour résoudre ce problème, chaque serveur doit posséder son propre identifiant ServerPeerID. Cet identifiant est utilisé dans la colonne NODE_ID des différentes tables de JBoss Messaging.\nAinsi, la table JBM_POSTOFFICE sera alimentée avec autant de files du même nom qu’il y’a de ServerPeerID (ici 2): POSTOFFICE_NAMENODE_IDQUEUE_NAMECONDSELECTORCHANNEL_IDCLUSTEREDALL_NODESJMS post office\n1\nMyAppQueuequeue. MyAppQueue\n3\nN\nN\nJMS post office\n2\nMyAppQueuequeue. MyAppQueue\n7383\nN\nN\nMise en œuvre Au démarrage du JBoss, nous passons en paramètre l’identifiant du serveur (seul un nombre entier est accepté) :\n-Djboss.messaging.ServerPeerID=\nSous peine de retomber sur l’exception mentionnée ci-dessus, chaque serveur doit avoir un numéro distinct.\nUne solution alternative à l’utilisation de la propriété système jboss.messaging.ServerPeerID est de paramétrer le fichier messaging-service.xml de chaque nœud avec le numéro de nœud adéquat.\nConclusion En guise de conclusion, voici les avantages de l’architecture présentée dans ce billet :\nRéduction des couts d’administration de base de données : pas de schéma ou d’instance à créer pour chaque nœud du cluster. Même configuration pour tous les serveurs : Facilite le déploiement d’un nouveau nœud Réduit les risques de mauvaises configuration Enfin, avec pour condition préalable que votre base de données soit correctement dimensionnée, le multi-tenancy de JBoss Messaging permet d’aller plus loin en utilisant la même base pour persister des files JMS de serveurs qui font tourner des applications différentes.\n","link":"https://javaetmoi.com/2014/03/plusieurs-jboss-messaging-meme-base/","section":"posts","tags":["bug","jboss","jms","oracle"],"title":"Plusieurs JBoss Messaging pour une même base"},{"body":"","link":"https://javaetmoi.com/tags/htmlunit/","section":"tags","tags":null,"title":"Htmlunit"},{"body":"","link":"https://javaetmoi.com/tags/jasmine/","section":"tags","tags":null,"title":"Jasmine"},{"body":"","link":"https://javaetmoi.com/tags/phantomjs/","section":"tags","tags":null,"title":"Phantomjs"},{"body":"","link":"https://javaetmoi.com/tags/saga/","section":"tags","tags":null,"title":"Saga"},{"body":"","link":"https://javaetmoi.com/tags/selenium/","section":"tags","tags":null,"title":"Selenium"},{"body":" Vous développez une application web en Java. Le couche présentation est assurée typiquement par un framework MVC situé côté serveur : Spring MVC, Struts 2, Tapestry ou bien encore JSF. Votre projet est parfaitement industrialisé : infrastructure de build sous maven, intégration continue, tests unitaires, tests Selenium, analyse qualimétrique via Sonar.\nA priori, vous n’avez rien à envier à la richesse grandissante de l’écosystème JavaScript, de l’outillage et des frameworks MV* côté clients. Et pourtant, quelque chose vous manque cruellement. En effet, depuis que RIA et Ajax se sont imposés, votre application Java contient davantage de code JavaScript qu’il y’a 10 ans. S’appuyant sur des librairies telles que jQuery ou Underscore, ce code JavaScript est typiquement embarqué dans votre WAR. Pour le valider, les développeurs doivent démarrer leur conteneur web et accéder à l’écran sur lequel le code est utilisé. Firebug ou Chrome sont alors vos meilleurs amis pour la mise au point du script.\nCe code JavaScript n’est généralement pas documenté. Le tester manuellement demande du temps. Les modifications sont sources d’erreur. Tout changement est donc périlleux. Si, à l’instar de vos tests JUnit pour vous classes Java, vous disposiez de tests JavaScript, vous en seriez comblés. Or, c’est précisément ce qu’il vous manque. Et c’est là où Jasmine et son plugin maven viennent à votre rescousse.\nPrésentation de Jasmine Les développeurs AngularJS le connaissent déjà. Mis au point par Pivotal (ex SpringSource), Jasmine est un framework de tests unitaires pour JavaScript et CoffeeScript. Contrairement à QUnit qui est a été initialement créé pour tester le projet jQuery, Jasmine est indépendant de tout framework JavaScript. Son principal avantage réside dans le fait qu’il ne nécessite pas de navigateur pour exécuter les tests : un simple moteur JavaScript suffit.\nComplet, Jasmine offre tout l’outillage nécessaire à l’écriture de tests : FonctionnalitéDescriptionFrameworks Java équivalent****Structuration des testsSuite de tests (describe), fonctions de tests (it), setUp et tearDown, ignoreJUnit, TestNGMatchersFonctions utilisées pour les assertions : expect, toEqual, toBe, not, toBeDefined …Feist Assert, JUnit, HamcrestBouchonsCréation d’espions et de simulacres: createSpy, andReturn, andCallFake, toHaveBeenCalled …Mockito Vous expliquez ici comment écrire des tests avec Jasmine dépasse le cadre de cet article. Je vous renvoie à la documentation officielle et au tutorial Testing JavaScript using the Jasmine framework.\nDans la suite de cet article, les fichiers Player.js, Song.js, PlayerSpec.js et SpecHelper.js issus de la version standalone de Jasmine sont utilisés comme jeu d’exemple. Pour les adeptes de jQuery, le projet jasmine-jquery étend Jasmine de 2 manières :\nAjout de matchers liés au DOM : toBeSelected, toContainText, toHaveClass, toContainHtml, toBeVisible \u0026hellip; Initialisation du DOM à partir d’un fichier HTML lors de l’étape de fixture. Le fichier PasswordSpec.js en montre un exemple d’utilisation.\nIntégration de Jasmine à maven Le plugin pour maven jasmine-maven-plugin permet d’ exécuter vos tests Jasmine(aussi appelés specs) lors de la phase de test de votre build maven.\nLe pom.xml du projet jasmine-test-webapp donne un exemple de configuration du plugin. Pour mieux comprendre sa configuration, vous présenter l’organisation du projet est nécessaire.\nL’arborescence du projet suit les conventions maven d’un war. Pages dynamiques et ressources statiques se trouvent dans le répertoire src/main/webapp.\nPar choix, Le code JavaScript spécifique à l’application est localisé dans le sous-répertoire static/js/app du répertoire src/main/webapp. C’est ce code qui doit être testé. Les librairies tierces comme ici jQuery sont placées dans un sous-répertoire static/js/lib.\nLes fichiers de tests JavaScript sont quant à eux placés dans le répertoire src/test/javascript. Pour utiliser l’extension jasmine-jquery dans les specs, la seule présence du fichier jasmine-jquery.js suffit.\nLa configuration associée du jasmine-maven-plugin est la suivante :\nConfiguration du jasmine-maven-plugin Remarque : afin que l’objet $ soit défini lors du chargement du script Password.js, la librairie jQuery est chargée en premier.\nVoici le résultat de la sortie console de la commande mvn test :\n[INFO] ------------------------------------------------------------------------ [INFO] Building JavaEtMoi Samples :: jasmine-test-webapp - war 1.0-SNAPSHOT [INFO] ------------------------------------------------------------------------ ... [INFO] [INFO] --- maven-surefire-plugin:2.10:test (default-test) @ jasmine-test-webapp --- [INFO] No tests to run. ... [INFO] --- jasmine-maven-plugin:1.3.1.4:test (default) @ jasmine-test-webapp --- [INFO] Executing Jasmine Specs [INFO] ------------------------------------------------------- J A S M I N E S P E C S ------------------------------------------------------- [INFO] password validation label should become red when password is too short should be green when password length is more then 6 symbols Player should be able to play a Song when song has been paused should indicate that the song is currently paused should be possible to resume tells the current song if the user has made it a favorite #resume should throw an exception if song is already playing Results: 7 specs, 0 failures ... [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 14.035s [INFO] Finished at: Sat Mar 01 19:26:03 CET 2014 [INFO] Final Memory: 10M/247M [INFO] ------------------------------------------------------------------------ Mise à part l’installation de Java et de Maven, aucun prérequis n’est nécessaire. Magique, vous ne trouvez pas ? Mais alors, comment est interprété le code JavaScript ? Et qui met à disposition le DOM manipulé dans le JavaScript ? En plus de Jasmine, le jasmine-maven-plugin embarque HmtlUnitDriver, une implémentation de Selenium WebDriver. En interne, HmtlUnitDriver s’appuie sur HtmlUnit pour le DOM et sur Rhino pour le JavaScript. Tous les deux sont écrits en Java et sont Open Source. Initié par la fondation Mozilla, le moteur JavaScript Rhino a été intégré à Java 6. Respectant le standard HTML, HmtlUnitDriver permet d’émuler des spécificités de certains navigateurs. Jasmine-maven-plugin permet de tirer parti cette fonctionnalité. A sa configuration, il est possible d’ajouter la balise suivante : FIREFOX_17\nBehavior-Driven Development Le plugin jasmine-maven-plugin permet de développer en TDD sans avoir à lancer un clean test à chaque changement de code. La commande mvn jasmine:bdd lance un serveur web qui scrute tout changement dans le répertoire du code JavaScript et des tests JavaScript. Rafraichir la fenêtre de son navigateur permet de réexécuter les tests.\nCouverture de tests A présent que jasmine-maven-plugin est en place sur votre projet, vous pouvez en profiter pour générer à moindre coût la couverture du code JavaScript testé. En effet, ce plugin s’interface avec le saga-maven-plugin. Par la ligne de configuration suivante, on indique au jasmine-maven-plugin de ne pas arrêter le serveur web une fois les tests unitaires exécutés :\ntrue\nLa configuration du saga-maven-plugin est triviale et très bien documentée :\nConfiguration du saga-maven-plugin Des rapports Cobertura et HTML sont générés dans le sous-répertoire target/coverage pendant la phase verify de maven : Utilisation de PhantomJS Le principal inconvénient de la solution présentée jusqu’ici est que ni HtmlUnit ni Rhino ne sont utilisés par un quelconque navigateur du marché. Comment être certain que votre code s’exécute sur un Chrome ou un Safari ? C’est là que PhantomJS rentre en jeu. En effet, PhantomJS est un navigateur headless (sans interface graphique) basé sur WebKit (le moteur HTML de Safari, d’Opera et du fork de Chrome). Le plugin Jasmine pour maven permet d’ utiliser PhantomJS à la place de HtmlUnit.\nPour utiliser PhantomJS, il est tout d’abord nécessaire de l’installer. Une archive existe pour chaque OS supporté : Windows, MacOSX, Linux 32 bits et 64 bits. Afin que maven puisse installer l’archive PhantomJS de l’OS sur lequel le build est exécuté, il est possible d’uploader ces archives dans votre repo d’entreprise (ex : Nexus, Artifactory). Voici un exemple de commande maven :\nmvn deploy:deploy-file -DgroupId=org.phantomjs -DartifactId=phantomjs -Dversion=1.9.7 -Dpackaging=zip -Dclassifier=windows -Dfile=phantomjs-1.9.7-windows.zip -DrepositoryId=javaetmoi-cloudbees-release -Durl=https://repository-javaetmoi.forge.cloudbees.com/release/ Une fois déployées, les archives sont disponibles dans le répertoire org/phantomjs/phantomjs/1.9.7/\nLe goal unpack du plugin maven-dependency-plugin permet d’installer PhantomJS dans le répertoire target/ pendant la phase initialize de la commande mvn test :\n\u0026lt;plugin\u0026gt; \u0026lt;groupId\u0026gt;org.apache.maven.plugins\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;maven-dependency-plugin\u0026lt;/artifactId\u0026gt; \u0026lt;executions\u0026gt; \u0026lt;execution\u0026gt; \u0026lt;phase\u0026gt;initialize\u0026lt;/phase\u0026gt; \u0026lt;goals\u0026gt; \u0026lt;goal\u0026gt;unpack\u0026lt;/goal\u0026gt; \u0026lt;/goals\u0026gt; \u0026lt;configuration\u0026gt; \u0026lt;artifactItems\u0026gt; \u0026lt;artifactItem\u0026gt; \u0026lt;groupId\u0026gt;org.phantomjs\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;phantomjs\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.9.7\u0026lt;/version\u0026gt; \u0026lt;type\u0026gt;zip\u0026lt;/type\u0026gt; \u0026lt;classifier\u0026gt;windows\u0026lt;/classifier\u0026gt; \u0026lt;outputDirectory\u0026gt;${project.build.directory}\u0026lt;/outputDirectory\u0026gt; \u0026lt;/artifactItem\u0026gt; \u0026lt;/artifactItems\u0026gt; \u0026lt;/configuration\u0026gt; \u0026lt;/execution\u0026gt; \u0026lt;/executions\u0026gt; \u0026lt;/plugin\u0026gt; Une autre solution permettant d’installer automatiquement PhantomjS consiste à utiliser le plugin PhantomJS pour maven.\nEnfin, pour substituer PhantomJS à HtmlUnit, le jasmine-maven-plugin doit être configuré de la manière suivante :\n\u0026lt;webDriverClassName\u0026gt;org.openqa.selenium.phantomjs.PhantomJSDriver\u0026lt;/webDriverClassName\u0026gt; \u0026lt;webDriverCapabilities\u0026gt; \u0026lt;capability\u0026gt; \u0026lt;name\u0026gt;phantomjs.binary.path\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;${project.build.directory}/phantomjs-1.9.7-windows/phantomjs.exe\u0026lt;/value\u0026gt; \u0026lt;/capability\u0026gt; \u0026lt;/webDriverCapabilities\u0026gt; Lorsque des tests échouent, les erreurs remontées par PhantomJS sont, de manière générale, plus compréhensibles que leurs homologues HtmlUnit. Voici un exemple de message d’erreur lorsqu’on oubli de rajouter la librairie underscore. js et que la variable globale _ n’est pas définie :\nAvec HtmlUnit :\n[ERROR] java.lang.RuntimeException: org.openqa.selenium.WebDriverException: com.gargoylesoftware.htmlunit.ScriptException: TypeError: Impossible dappeler la méthode \u0026#34;{1}\u0026#34; de {0} (script in http://localhost:3213/ from (6, 34) to (15, 12) Avec PhantomJS :\n\\[WARNING\\] JavaScript Console Errors: \u0026amp;nbsp; * TypeError: \u0026#39;undefined\u0026#39; is not an object (evaluating \u0026#39;_.each\u0026#39;) Le pom.xml utilise les profiles maven pour utiliser à la demande PhantomJS. Il installe la version de PhantomJS correspondant à l’OS sur lequel la commande maven est exécutée. Le profile phantomJS permet d’exécuter les tests avec PhantomJS : mvn test -PphantomJS\nConclusion Au travers de ce billet, nous avons vu comment intégrer l’exécution de tests unitaires JavaScript dans une infrastructure de build basée sur maven. L’intégration de Jasmine avec maven est telle qu’il n’est nullement nécessaire de mettre en place des outils du monde JavaScript basés sur Node.JS tel Grunt ou Gulp. Avec le plugin Jasmine pour maven, vos tests unitaires peuvent aussi bien tester des fonctions métiers que des fonctions manipulant le DOM du navigateur. En outre, l’intégration de PhantomJS permet de se garantir que le code JavaScript testé fonctionnera sur les navigateurs basés sur WebKit.\nVos tests unitaires JavaScript peuvent dès à présent intégrer votre plateforme d’intégration continue. Preuve en est, les tests unitaires du projet jasmine-test-webapp ont été exécutés par Travis CI et par le Jenkins de CloudBees.\n","link":"https://javaetmoi.com/2014/03/tester-code-javascript-webapp-java/","section":"posts","tags":["htmlunit","jasmine","javascript","maven","phantomjs","saga","selenium","test"],"title":"Tester le code JavaScript de vos webapp Java"},{"body":"","link":"https://javaetmoi.com/tags/cxf/","section":"tags","tags":null,"title":"Cxf"},{"body":"","link":"https://javaetmoi.com/tags/jmeter/","section":"tags","tags":null,"title":"Jmeter"},{"body":"Les tests de charge d’une nouvelle fonctionnalité m’a récemment permis de détecter un comportement inattendu de CXF s’apparentant à une fuite mémoire. Fusion de Celtix et de XFire, le framework CXF propose une implémentation cliente et serveur de web services SOAP et REST. Le comportement suspect concerne la partie cliente d’un web service SOAP avec pièce-jointes.\nLes symptômes ont été observés dans les conditions suivantes. Un tir de charge avec JMeter simule l’upload de fichiers de 4 Mo. Trente utilisateurs connectés simultanément uploadent des fichiers PDF. D’une durée de 5mn, le scénario fonctionnel mettant en jeu l’upload de fichiers est réitéré pendant 3h. A l’issu du tir, aucune erreur technique ou fonctionnelle n’est remontée. Par contre, l’analyse de l’empreinte mémoire est suspecte : non seulement cette nouvelle fonctionnalité a nécessité davantage de mémoire que lors des tirs précédents, mais surtout : la mémoire n’est jamais libérée, même après l’expiration des sessions utilisateurs.\nOrigine du problème Un Heap Dump de la JVM a permis de déterminer le type d’objets résidents en mémoire et également de quelles classes ces objets ont été alloués. Dans mon cas, près de 300 Mo de tableaux de bytes occupaient la Heap. La plupart de ces tableaux occupaient 5 Mo. La pile d’appel ci-dessous montre que ces tableaux sont créés par le client CXF, et plus précisément par la classe AttachmentSerializer chargée de sérialiser en XML le SoapMessage émis par le client CXF.\nReproductible sur un poste de développement, le debugger d’Eclipse permet de diagnostique que les 5 Mo de tableau de bytes correspondent au message SOAP et sa pièce-jointe encodée en base 64 :\nEn interne, la classe ClientImpl de CXF maintient une Map requestcontext associant un thread à un message SOAP. Les derniers messages émis par le client CXF sont stockés dans cette Map. Au final, plus le nombre de threads faisant appel au client CXF est élevé, plus CXF demandera de mémoire.\nCorrection La correction mise en œuvre a été d’implémenter un intercepteur CXF chargé de déréférencer l’instance de AttachmentSerializer une fois le message SOAP envoyé. Le Garbage Collector peut alors libérer la mémoire. L’intercepteur est positionné sur la toute dernière phase CXF : SETUP_ENDING.\nExtrait de la classe ClearAttachmentsOutInterceptor.java Lors de la déclaration Spring d’un client CXF, l’intercepteur ClearAttachmentsOutInterceptor doit être positionné dans la balise jaxws:outInterceptors :\n\u0026lt;jaxws:client id=\u0026#34;myWebServiceClient\u0026#34; serviceClass=\u0026#34;MyWebService\u0026#34; address=\u0026#34;http://localhost:8080/ws/MyWebService\u0026#34;\u0026gt; \u0026lt;jaxws:outInterceptors\u0026gt; \u0026lt;bean class=\u0026#34;ClearAttachmentsOutInterceptor\u0026#34;/\u0026gt; \u0026lt;/jaxws:outInterceptors\u0026gt; \u0026lt;/jaxws:client\u0026gt; Conclusion Surpris de découvrir une telle problématique de mémoire dans un framework aussi populaire, je ne comprends pas le choix pris par les développeurs de CXF de conserver trace d’un message une fois celui-ci émis et sa réponse reçue. Une hypothèse : éviter de réallouer une grappe d’objets à chaque appel de web service ? Chose rassurante, je ne suis pas le seul à avoir rencontré ces soucis de mémoire lors de tirs de charge. Ce problème est en effet décrit sur les Jira des ESB Apache Service Mix (SMXCOMP-855) et Mule ESB (MULE-6434). Chacun d’eux a apporté sa propre solution de contournement.\n","link":"https://javaetmoi.com/2014/02/memory-leak-client-cxf-attachment/","section":"posts","tags":["cxf","jmeter","soap","spring-framework"],"title":"Memory Leak du client CXF"},{"body":"","link":"https://javaetmoi.com/tags/soap/","section":"tags","tags":null,"title":"Soap"},{"body":"","link":"https://javaetmoi.com/tags/bower/","section":"tags","tags":null,"title":"Bower"},{"body":"Au travers du billet Elastifiez la base MusicBrainz sur OpenShift, je vous ai expliqué comment indexer dans Elasticsearch et avec Spring Batch l’encyclopédie musicale MusicBrainz. L’index avait ensuite été déployé sur le Cloud OpenShift de RedHat.\nUne application HTML 5 était mise à disposition pour consulter les albums de musique ainsi indexés. Pour m’y aider, Lucian Precup m’avait autorisé à adapter l’application qu’il avait mise au point pour l’atelier Construisons un moteur de recherche de la conférence Scrum Day 2013.\nAfin d’approfondir mes connaissances de l’ écosystème JavaScript, je me suis amusé à recoder cette application front-end en partant de zéro. Ce fut l’occasion d’adopter les meilleures pratiques en vigueur : framework JavaScript MV*, outils de builds, tests, qualité du code, packaging …\nAu travers de ce article, je vous présenterai comment :\nMettre en place un projet Anguler à l’aise d’ Angular Seed, Node.js et Bower Développer en full AngularJS et Angular UI Bootstrap Utiliser le framework elasticsearch-js Internationaliser une application Angular Tester unitairement et fonctionnellement une application JS avec Jasmine et Karma Analyser du code source JavaScript avec jshint Packager avec Grunt le livrable à déployer Utiliser l’ usine de développement JavaScript disponible sur le Cloud : Travis CI, Coversall.io et David Le code source de l’application est bien entendu disponible sur GitHub et testable en ligne.\nDémarrer un projet avec Angular Seed Hébergé sur GitHub et maintenu par les auteurs d’Angular, le projet angular-seed permet de démarrer rapidement une application Angular. Outre le squelette applicatif, ce projet propose :\ndes exemples de tests unitaires et de tests dits end-to-end, des scripts .sh ou .bat permettant d’ exécuter ces différents types de tests un script JS permettant de démarrer un serveur web sous NodeJS Le README.MD explique de manière approfondie l’organisation du projet et la nature de chaque fichier.\nUne fois ce repository cloné sous GitHub ou bien dézippé en local, il est possible de le personnaliser à sa guise.\nUne alternative à angular-seed serait d’utiliser Yo pour générer le squelette de l’application, sur un principe similaire aux archetypes maven.\nExécuter l’application L’application blanche fournie dans Angular Seed étant une application full HTML, il n’est pas nécessaire de la déployer dans un serveur d’application JEE ou un conteneur web. Un simple serveur web comme Apache ou Nginx est nécessaire.\nLes utilisateurs de Firefox pourront même se passer de serveur web et ouvrir directement le fichier app/index.html à partir de leur disque.\nChrome n’ayant pas cette faculté (les requêtes Ajax chargées de récupérer un fichier sur disque sont bloquées), vous pouvez utiliser le serveur web installé sur votre poste de développement.\nEn guise de serveur web, vous pourrez utiliser le script scripts\\web-server.js pour en démarrer un en full JavaScript. Le seul pré-requis est l’ installation de NodeJS qui intègre le moteur JavaScript V8 de Google. A noter que NodeJS et son gestionnaire de paquets npm seront nécessaires dans la suite de cet article pour installer les outils, construire l’application, monter de version les dépendances ou bien encore exécuter les tests.\nUne fois NodeJS installé et ajouté au PATH du système, exécuter la commande suivante pour démarrer le serveur:\nD:\\Dev\\angular-musicbrainz\u0026gt;node scripts\\web-server.js Http Server running at http://localhost:8000/ Saisir l’URL http://localhost:8000/app/index.html dans le navigateur de votre choix. Les requêtes HTTP apparaissent sur la console :\nGET /app/index.html Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.63 Safari/537.36 GET /app/lib/bootstrap/dist/css/bootstrap.css Mozilla/5.0d (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.63 Safari/537.36 GET /app/lib/bootstrap/dist/css/bootstrap-theme.css Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.63 Safari/53 GET /app/lib/angular-resource/angular-resource.js Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.63 Safari/537. … Structuration du projet La structure des répertoires du projet reprend celle d’Angular Seed. Le tableau ci-dessous liste les librairies tierces utilisées par l’application.\nRépertoires / fichiersDescriptionapp/Code source et ressources de l’applicationcss/Feuilles de styles CSSi18n/Fichiers JSON de traduction de l’applicationimg/Images et icôneindex.htmlPage principale de la Single Page Applicationjs/Fichiers JavaScript spécifiques à l’applicationapp.jsDéclaration des modules et démarrage de l’applicationcontrollers.jsContrôleurs Angular spécifiques à l’applicationdirectives.jsDirectives Angular spécifiques à l’applicationfilters.jsFiltres / formateurs Angular spécifiques à l’applicationroutes.jsConfiguration des routesservices.jsServices spécifiques à l’application**lib/Librairies tierces déclarées dans bower.jsonangular/Module principal d’Angularangular-i18n/Fichiers de traductions fournis par Angularangular-mocks/Mocks permettant de bouchonner des services Angularangular-resource/Accès REST aux ressources serveurangular-route/Routage des vues en fonction de l’URLangular-sanitize/Filtres standardsangular-scenario/DSL des scénarios de tests end-to-endangular-ui-boostrap-bower/Widgets Angular basés sur Bootstrapbootstrap/Mise en page et charte graphiqueelasticsearch-js/Client JavaScript pour Elasticsearchjquery/Manipulation de DOMpartials/Template HTML des vues de l’applicationdirectives/Template HTML des directives de l’applicationconf/Configuration Karma des tests unitaires et e2edist/Répertoire de destination du livrable de productionnode_modules/Modules NodeJS utilisées par Gruntscripts/Scripts shell, JS et batch permettant de lancer les tests et de démarrer un serveur webtest/Code source des testse2e/Tests end-to-endunit/**Tests unitaires\nAutomatisation avec Grunt Grunt peut être comparé au Gradle du monde JavaScript. Il permet d’exécuter des tâches sous NodeJS et est particulièrement utile pour automatiser certaines tâches de développement. Son installation nécessite une seule commande :\nnpm install -g grunt-cli\nLe gestionnaire npm permet ensuite de télécharger et d’installer les modules NodeJS nécessaires au fonctionnement du script Grunt. Commande à exécuter à la racine du répertoire, au même niveau que le fichier package.json :\nnpm install\nLe sous-répertoire node_modules est alimenté par les modules déclarés dans le fichier package.json.\nAngular Seed ne configurant pas Grunt, je me suis inspiré de différents exemples pour mettre au point le script Gruntfile.js.\nVoici quelques commandes utiles :\ngrunt test: lance successivement les tests unitaires et les tests end-to-end grunt server: démarre un serveur web, ouvre la page dans l’application et, à l’instar de JRebel, recharge à chaud le code modifié depuis votre IDE. grunt jshint: vérifie que la qualité du code de production JavaScript grunt build: construit le livrable à installer sur le serveur web. Les fichiers générés sont mis à disposition dans le sous-répertoire dist. grunt karma:coverage: génère le taux de couverture des tests unitaires. Au format HTML, le rapport est accessible depuis le sous-répertoire coverage\\PhantomJS 1.9.2 (Windows 7)\\lcov-report Pendant le développement de l’application web, à des fins de débogage, le code JavaScript est non minifié et séparé dans plusieurs fichiers, de même pour les feuilles de style CSS.\nSur un principe similaire à ce que propose Jawr dans le monde Java, l’étape de build va permettre d’obtenir un livrable le plus léger possible. Voici les opérations effectuées :\nLe code JavaScript et les feuilles de styles CSS sont concaténées puis minifiés. Exemple de directive à placer dans le code HTML :\nDirective build:js sur la page index.html Les images bitmaps et vectorielles sont réduites.\nLes pages HTML référençant ces ressources statiques sont mises à jour en conséquences.\nDépendances Bower Comme vu au paragraphe précédent, les modules nécessaires au fonctionnement de l’infrastructure de build basé sur Grunt sont récupérés à l’aide de npm et du fichier package.json.\nLes librairies nécessaires au développement de l’application (Angular, Bootstrap) et de ses tests sont gérées par un second système de dépendances, à savoir Bower. Cet outil permet de rechercher des librairies via la commande bower search . L’instruction bower install –save(-dev) permet de la télécharger et de la référencer dans le fichier bower.json. Les librairies sont installées dans le répertoire app/lib configuré dans le fichier .bowerrc.\nS\u0026rsquo;appuyant sur le fichier bower.json, la commande bower install permet de récupérer toutes les dépendances utilisées par l’application. Elle permet également de mettre à jour les dépendances lors d’une montée de version.\nEn théorie, il n’est pas nécessaire d’archiver ces dépendances dans le gestionnaire de code source (ici GitHub). En pratique, c’est utile de pouvoir exécuter l’application sans avoir à installer ni NodeJS ni Bower.\nArchitecture applicative En avril 2013, je découvrais Angular à Devoxx France. Je vous en présentais ici même les mécanismes et les fonctionnalités clés. Après m’être auto-formé et avoir animé un workshop sur ce framework, j’étais impatient de le mettre en œuvre sur un projet personnel (en attendant un futur projet pro). Et autant vous l’annoncer dès à présent, je n’ai pas été déçu : j’ai pris un réel plaisir à développer le front end de mon application de recherche angular-musicbrainz.\nD’un point de vue de l’architecture applicative, cette single page application est composée de 3 fichiers HTML principaux :\nindex.html : bootstrape l’application en initiant le gabarit HTML (essentiellement le menu) et en téléchargeant les fichiers JavaScript, HTML et CSS nécessaires à son fonctionnement partials\\search.html : vue de recherche et de restitution des résultats. C’est l’implémentation de cette vue que nous allons présenter dans la suite de se billet. partials\\info.html : vue référençant quelques URLs utiles. Le code JavaScript de l’application est découpé techniquement. A une couche technique (contrôleurs, services, filtres et directives) correspond un fichier JavaScript et un module Angular.\nSimple, cette organisation est pratique pour de petites applications. Sur des applications plus conséquences, un découpage fonctionnel est à privilégier.\nServices L’application repose sur 4 services implémentés dans le fichier services.js:\nNom du serviceFonctionnalitésesCrée et configure un client Elasticsearch. S’appuie sur la fabrique mise à disposition par la librairie elasticsearch-js.searchServiceCode métier regroupant les requêtes JSON de recherche Elasticsearch. 2 méthodes sont disponibles : l’une pour la recherche full text et l’autre pour l’auto-complétion.userLanguagePermet d’accéder à la locale de l’utilisateur.translationRécupère le fichier de traduction de l’application.\nFiltres Les templates HTML reposent sur 5 filtres personnalisés\nNom du filtreUtilisationRendu****interpolate{ \u0026lsquo;v%VERSION%\u0026rsquo; | interpolate }V1.0joinBy{{hit._source.tags | joinBy:\u0026rsquo; - \u0026lsquo;}}pop - rock - bluesreverseng-repeat=\u0026ldquo;rating in facets.rating.entries | reverse\u0026rdquo; artistTypeLabel{{type.term | artistTypeLabel}}Artiste (fr) ou Artist (en)yearFormat{{range | yearFormat}}Avant 1970 (fr)\nPour les détails d’implémentation, se référer au code source filters.js et aux tests unitaires qui les documentent filterSpecs.js.\nDirectives 2 widgets graphiques ont été réalisés à l’aide de directives:\nDirectiveUtilisationRendu****Cover Rank A noter que la directive rating d’Angular Bootsrap offre une alternative à la directive rank. Cette dernière reprend la CSS de MusicBrainz permettant d’ajuster le dégradé des étoiles au pixel près.\nAfin de rendre le code des directives plus lisible et maintenable, les templates HTML de ces 2 directives ont été externalisés dans des fichiers html dédiés.\nExemple rating.xml :\n\u0026lt;span class=\u0026#34;inline-rating\u0026#34;\u0026gt; \u0026lt;span class=\u0026#34;star-rating small-star\u0026#34;\u0026gt; \u0026lt;span style=\u0026#34;width:{{score+ceil}}%;\u0026#34; class=\u0026#34;current-rating\u0026#34;\u0026gt;{{score}}\u0026lt;/span\u0026gt; \u0026lt;/span\u0026gt; \u0026lt;/span\u0026gt; La configuration des tests unitaires Karma a dû être ajustée en conséquences :\n// generate js files from html templates to expose them during testing. preprocessors : { \u0026#39;app/partials/directives/**/*.html\u0026#39;: [\u0026#39;ng-html2js\u0026#39;] }, Contrôleurs A chaque vue de l’application, correspond un contrôleur. Le fichier controller.js en défini donc deux : SearchCtrl et InfoCtrl.\nTrivial, le contrôleur InfoCtrl met à disposition dans le scope de le vue info les 2 URLs affichées et formatées à l’aide de la directive Angular ngLinky\nContrôleur :\n$scope.demoUrl = \u0026#39;http://angular-musicbrainz.javaetmoi.com/\u0026#39;; Template :\n\u0026lt;li\u0026gt;Online Demo: \u0026lt;span ng-bind-html=\u0026#34;demoUrl | linky\u0026#34;/\u0026gt;\u0026lt;/li\u0026gt; Le contrôleur SearchCtrl embarque toute la logique applicative de l’application. Il offre à la fois des fonctions réagissant aux actions utilisateurs et les données utilisées par Angular lors du rendu de la vue search. En voici les principales :\nPropriétéTypeDescription****fullTextSearchfonctionExécute une recherche full text lors du clic sur le bouton « Recherche MusicBrainz».autocompletefonctionExécute une requête d’auto-complétion à chaque frappe de l’utilisateur dans le zone de recherche.selectPageFonctionPermet à l’utilisateur de sélectionner une plage de résultats. Exécute une recherche Elasticsearch sur la plage indiquée.searchRespDonnéeRésultats Elasticsearch d’une recherche fulltext.pageSizeDonnéeNombre de résultats à afficher à l’écran.currentPageDonnéePlage de résultats actuellement affichée.pageSizesDonnéeTailles de plages que l’utilisateur peut choisir.\nRoutes Par rapport au template angular-seed, la configuration des routes a été externalisée dans un fichier dédié routes.js.\n**Application **\nLe 6ième et dernier module Angular correspond au module applicatif musicAlbumApp déclaré dans le fichier app.js. Outre la déclaration des modules Angular nécessaires au fonctionnement de l\u0026rsquo;application, ce module est chargé de déterminer la langue dans laquelle l\u0026rsquo;interface doit s\u0026rsquo;afficher puis charger les données adéquates. Nous y reviendrons dans la suite de cet article.\nUtilisation d’elasticsearch-js Pour interroger le cluster Elasticsearch depuis le navigateur, j’ai étudié 3 possibilités : le service natif $http d’Angular, la librairie elastic.js et la librairie elasticsearch-js. Sortie en décembre 2013 et mise en avant par Elasticsearch.org, j’ai choisi d’utiliser cette dernière. Via la création de tickets GitHub, j’ai eu la chance de pouvoir contribuer à l’amélioration de cette jeune librairie déjà très mature.\nLe module esFactory permet de déclarer en quelques lignes un client JavaScript Elasticsearch. Voici les paramètres renseignés :\nangular.module(\u0026#39;musicAlbumApp.services\u0026#39;, [\u0026#39;ngResource\u0026#39;]) .value(\u0026#39;version\u0026#39;, \u0026#39;1.0\u0026#39;) // elasticsearch.angular.js creates an elasticsearch // module, which provides an esFactory .service(\u0026#39;es\u0026#39;, [\u0026#39;esFactory\u0026#39;, function (esFactory) { return esFactory({ hosts: [ // you may use localhost:9200 with a local Elasticsearch cluster \u0026#39;es.javaetmoi.com:80\u0026#39; ], log: \u0026#39;trace\u0026#39;, sniffOnStart: false }); }]) J’ai volontairement désactivé la fonctionnalité de sniffOnStart. En effet, j’ai configuré le reverse proxy Nginx pour ne laisser passer que les requêtes de _search. Les requêtes HTTP de type HEAD envoyées par le client pour déterminer la disponibilité des différents nœuds du cluster étaient donc rejetées.\nL’appel au service de recherche Elasticsearch est également très simple. Dans l’attribut body de la fonction search proposée par l’API, est utilisé le formalisme standard de déclaration des requêtes au format JSON. En complément, les attributs index et type permettent respectivement d’indiquer sur quel index Elasticsearch et sur quel type de document lancer la recherche. Voici un exemple d’appel :\nExtrait méthode fullTextSearch\n.factory(\u0026#39;searchService\u0026#39;, [\u0026#39;es\u0026#39;, function (es) { return { \u0026#39;fullTextSearch\u0026#39;: function (from, size, text) { return es.search({ index: \u0026#39;musicalbum\u0026#39;, type: \u0026#39;album\u0026#39;, body: { \u0026#39;from\u0026#39;: from, \u0026#39;size\u0026#39;: size, \u0026#39;query\u0026#39;: { \u0026#39;bool\u0026#39;: { \u0026#39;must\u0026#39;: [ { \u0026#39;fuzzy_like_this\u0026#39;: { \u0026#39;fields\u0026#39;: [ \u0026#39;name\u0026#39;, \u0026#39;artist.name\u0026#39;, \u0026#39;year.string\u0026#39; ], \u0026#39;like_text\u0026#39;: text, \u0026#39;min_similarity\u0026#39;: 0.7, \u0026#39;prefix_length\u0026#39;: 1 } } ] } }, \u0026#39;facets\u0026#39;: { \u0026#39;artist_type\u0026#39;: { \u0026#39;terms\u0026#39;: { \u0026#39;field\u0026#39;: \u0026#39;artist.type_id\u0026#39; } }, \u0026#39;album_rating\u0026#39;: { \u0026#39;histogram\u0026#39;: { \u0026#39;key_field\u0026#39;: \u0026#39;rating.score\u0026#39;, \u0026#39;interval\u0026#39;: 21 } }, \u0026#39;album_year\u0026#39;: { \u0026#39;range\u0026#39;: { \u0026#39;field\u0026#39;: \u0026#39;year\u0026#39;, \u0026#39;ranges\u0026#39;: [ { \u0026#39;to\u0026#39;: 1970}, { \u0026#39;from\u0026#39;: 1970, \u0026#39;to\u0026#39;: 1980}, { \u0026#39;from\u0026#39;: 1980, \u0026#39;to\u0026#39;: 1990}, { \u0026#39;from\u0026#39;: 1990, \u0026#39;to\u0026#39;: 2000}, { \u0026#39;from\u0026#39;: 2000, \u0026#39;to\u0026#39;: 2010}, { \u0026#39;from\u0026#39;: 2010 } ] } } } } }); }, La fonction search renvoie une promesse de réponse. Pour récupérer la réponse retournée par Elasticsearch, la méthode then peut être utilisée :\nsearchService.fullTextSearch(from, $scope.pageSize.count, text).then( function (resp) { $scope.searchResp = resp; $scope.totalItems = resp.hits.total; searchService.fullTextSearch(from, $scope.pageSize.count, text).then( function (resp) { $scope.searchResp = resp; $scope.totalItems = resp.hits.total; } ); Localisation Le service $locale d’Angular permet de formater les nombres et les dates en fonction des préférences linguistiques de l’utilisateur. Il existe autant de fichiers JavaScript que de combinaisons langue / pays (exemples : angular-locale_fr-fr.js, angular-locale_en-us.js).\nPour charger le fichier adéquat, l’application doit détecter le langage défini par l’utilisateur dans son Navigateur. A première vue, les variables du DOM window.navigator.userLanguage et window.navigator.language auraient dû apporter cette information. Il en aurait été trop simple. L’article Detecting a Browser’s Language in Javascript explique précisément pourquoi.\nLe header HTPP Accept-Language ne peut être lue que côté serveur web. Or, l’application était jusque-là full JavaScript. Convertir la page index.html en une page PHP ou JSP aurait été simple. Néanmoins, j’ai préféré m’affranchir de toute installation côté serveur. J’ai donc utilisé le service http://ajaxhttpheaders.appspot.com mis à disposition sur Google App Engine et dont voici un exemple d’utilisation :\n$http.jsonp(\u0026#39;http://ajaxhttpheaders.appspot.com?callback=JSON_CALLBACK\u0026#39;). success(function (data) { var acceptLang = data[\u0026#39;Accept-Language\u0026#39;]; langRange = userLanguage.getFirstLanguageRange(acceptLang); language = userLanguage.getLanguage(langRange); if (sessionStorage) { sessionStorage.setItem(\u0026#39;userLanguageRange\u0026#39;, langRange); } }). finally(function () { loadI18nResources(); }); Une fois la langue de l’utilisateur connue, la fonction $.getScript de JQuery permet de charger dynamiquement le fichier JavaScript Angular correspondant.\nAfin d’éviter des appels intempestifs au service http://ajaxhttpheaders.appspot.com, la langue est conservée dans le sessionStorage du navigateur.\nDans un souci d’internationalisation, l’application angular-musicbrainz a été traduite en 2 langues : le français et l’anglais. Les libellés affichés à l’écran dépendent donc des préférences utilisateurs. Le système mis en œuvre s’inspire de ce que proposent les articles Creating multilingual support using AngularJS et Traduction des libellés dans les vues AngularJS. Un objet translation contenant la traduction de tous les libellés d’une langue est chargé à partir d’un fichier JSON puis ai mis dans le scope parent ($rootScope). Cet objet peut être accédé à la fois dans les templates HTML que côté JavaScript :\n\u0026lt;label id=\u0026#34;search-input-label\u0026#34; class=\u0026#34;col-sm-3 control-label\u0026#34; ng-bind=\u0026#34;translation.SEARCH_LABEL\u0026#34;\u0026gt;Searching a music album\u0026lt;/label\u0026gt; $scope.pageSizes = [ {count: 5, label: \u0026#39;5 \u0026#39; + $scope.translation.SEARCH_PAGE_RESULT}, Widgets graphiques Le projet UI Bootstrap propose une douzaine de directives Angular basées sur Boostrap : sélection de date avec calendrier, accordéon, onglets, barres de progression, fenêtre popup, collapse, carrousel d’images …\nNotre web app de recherche utilise 3 de ses directives:\nDirectiveVisuelExemple d’utilisation dans les templates HTMLtypeahead \u0026lt;input type=\u0026#34;text\u0026#34; class=\u0026#34;form-control\u0026#34; ng-model=\u0026#34;searchText\u0026#34; typeahead=\u0026#34;album for album in autocomplete($viewValue) | filter:$viewValue\u0026#34; /\u0026gt; pagination \u0026lt;pagination total-items=\u0026#34;totalItems\u0026#34; page=\u0026#34;currentPage\u0026#34; max-size=\u0026#34;maxSize\u0026#34; num-pages=\u0026#34;numPages\u0026#34; items-per-page=\u0026#34;pageSize.count\u0026#34; on-select-page=\u0026#34;selectPage(page)\u0026#34;\u0026gt; \u0026lt;/pagination\u0026gt; pager \u0026lt;pager total-items=\u0026#34;totalItems\u0026#34; page=\u0026#34;currentPage\u0026#34; on-select-page=\u0026#34;selectPage(page)\u0026#34;\u0026gt; \u0026lt;/pager\u0026gt; Comme le montre les exemples ci-dessus, les directives UI Boostrap permettent d’étendre le HTML, soit par de nouveaux tags (ex: , soit par des attributs enrichissants des tags standards (ex : typeahead sur ).\nTests unitaires Avec son découpage en modules, la possibilité de créer des mocks et l’indépendance du code JavaScript au regard du DOM, Angular permet de tester unitairement chaque contrôleur, service, filtre, route et directive. Qui plus est, l’application blanche angular-seed vient avec toute l’infrastructure de tests : spécifications Jasmine à compléter, configuration Karma, scripts batch et shell permettant d’exécuter les tests. Autant dire, le développeur n’a aucune excuse pour ne pas tester unitairement son application.\nL’application angular-musicbrainz comptabilise 36 tests unitaires, couvrant ainsi 65% du code source. Avant de pouvoir exécuter les tests unitaires, il est nécessaire d’installer les quatre modules Karma suivants :\nnpm install -g karma karma-junit-reporter karma-ng-html2js-preprocessor karma-coverage\nLes tests unitaires peuvent être exécutés de 2 manières :\nPar la commande grunt : grunt karma Ou par un script : scripts\\test.bat Karma exécute les tests puis se met en attente de changements. En effet, tel infinitest, Karma relance les tests à chaque modification du code source ou des tests. Cela s’avère très pratique pour lever au plus tôt toute régression ou bien travailler en TDD.\nAutre aspect de Karma : il permet de faire tourner les tests simultanément dans un ou plusieurs navigateurs. Dans le fichier de configuration karma.conf.js, Google Chrome et le navigateur headless PhantomJS ont été retenus.\nUne fois la structuration d’un cas de test prise en main (mots clés describe, beforeEach et it), l’écriture du code de tests est plus ou moins simple. La difficulté principale vient de la lourdeur de la configuration nécessaire à mettre en place pour bouchonner les adhérences. Voici par exemple comment tester la fonction fullTextSearch du contrôleur SearchCtrl :\nit(\u0026#39;fullTextSearch should put the searchResp variable into the scope\u0026#39;, function () { expect(scope.searchResp).toBeUndefined(); expect(scope.isAvailableResults()).toBeFalsy(); expect(scope.isAtLeastOneResult()).toBeFalsy(); scope.fullTextSearch(\u0026#39;U2\u0026#39;, 1); it(\u0026#39;fullTextSearch should put the searchResp variable into the scope\u0026#39;, function () { expect(scope.searchResp).toBeUndefined(); expect(scope.isAvailableResults()).toBeFalsy(); expect(scope.isAtLeastOneResult()).toBeFalsy(); scope.fullTextSearch(\u0026#39;U2\u0026#39;, 1); // scope.$digest() will fire watchers on current scope, // in short will run the callback function in the controller that will call anotherService.doSomething scope.$digest(); expect(scope.searchResp).toBeDefined(); expect(scope.totalItems).toBeDefined(); expect(scope.isAvailableResults()).toBeTruthy(); expect(scope.isAtLeastOneResult()).toBeTruthy(); }); Le contrôleur SearchCtrl s’appuie sur le service searchService dont la fonction fullTextSearch a dû être bouchonnée. Au final, le développeur écrit plus de code de test que de code testé.\nEspérons que le duo Karma / Jasmine gagnera en maturité avec le temps. En Java, l’utilisation des annotations @Mock et @InjectInto permet en effet de réduire drastiquement ce type code.\nNon des moindre, le dernier point à connaître lors de l’écriture des tests concerne les assertions. Venant avec un nombre de matchers clés en mains, Jasmine permet d’écrire ses propres matchers.\nTests end-to-end A l’instar de ce que propose Selenium dans le monde Java, Karma permet d’écrire et d’exécuter des scénarios fonctionnels. Un prérequis à leur exécution est que l’application web doit être démarrée.\nLà encore, l’application blanche angular-seed vient avec toute l’infrastructure de tests e2e nécessaire. Comme prérequis, le module karma-ng-scenario doit être installé via npm :\nnpm install -g karma-ng-scenario\nLes tests e2e peuvent être exécutés en ligne de commande: scripts\\e2e-test.bat\nPour l’écriture des scénarios de tests, le module angular-scenario fournit un DSL permettant de sélectionner des éléments du DOM et de simuler des évènements utilisateurs. A noter que le framework Protactor doit remplacer à termes ce module.\nComme le montre l’extrait de code ci-dessous, le code reste lisible :\ndescribe(\u0026#39;search\u0026#39;, function () { beforeEach(function () { browser().navigateTo(\u0026#39;#/search\u0026#39;); }); it(\u0026#39;should render search when user navigates to /search\u0026#39;, function () { expect(element(\u0026#39;#search-input-label\u0026#39;).text()). toContain(\u0026#39;music\u0026#39;); }); it(\u0026#39;U2 album search\u0026#39;, function () { input(\u0026#39;searchText\u0026#39;).enter(\u0026#39;U2\u0026#39;); element(\u0026#39;:button\u0026#39;).click(); expect(element(\u0026#39;#result-number\u0026#39;).text()). toContain(\u0026#39;22\u0026#39;); }); }); Créé par l’un des développeurs d’Angular, Karma a l’avantage de connaître le fonctionnement interne d’Angular. Cette faculté lui permet de résoudre les problèmes de requêtes Ajax souvent rencontrés dans les tests Selenium. Adieux les tempos ou autre waitForElement.\nExécution des tests end-to-end dans Chrome :\nContrôle qualité avec JSHint Fork actif de jslint, JSHint s’apparente au Checkstyle du monde Java. Cet outil Open Source effectue plusieurs types de vérifications sur les fichiers JavaScript :\nConventions de nommage Règles de formatage Bonnes pratiques permettant d’éviter de potentiels bugs Le fichier de configuration .jshintrc permet d’activer chacune des dizaines de règles proposées par JSHint. Activée sur notre projet, la règle curly vérifie par exemple s’il ne manque pas des accolades dans les boucles et les conditions.\nLa vérification des fichiers JavaScript peut ensuite se faire, soit en ligne de commande :\nD:\\Dev\\angular-musicbrainz\u0026gt;grunt jshint Running \u0026#34;jshint:all\u0026#34; (jshint) task Linting app/js/services.js ...ERROR [L140:C17] W116: Expected \u0026#39;{\u0026#39; and instead saw \u0026#39;return\u0026#39;. return undefined; Soit directement depuis IntelliJ IDEA après configuration :\nJSHint a toute sa place sur un projet de grande taille sur lesquels de nombreux développeurs travaillent puis se relaieront pour sa maintenance. Sur de plus modestes applications comme angular-musicbainz , il a le mérite de former et de mettre en garde des développeurs JavaScript Junior.\nUsine de dév JavaScript Dans le billet Ma petite usine logicielle, je vous expliquais comment utiliser CloudBees et GitHub pour industrialiser vos projets Java. Vous l’aviez compris, l’intégration continue et l’automatisation des tâches me tiennent à cœur. J’ai donc naturellement regardé ce qui existait dans le monde JavaScript. Ce dernier n’est pas en reste. Voici ce que j’ai mis en place sur angular-musicbrainz.\nDéjà mis en place sur mes projets Java avec Maven, Travis CI est une plateforme d’intégration continue mise à disposition gratuitement pour les projets Open Source. Cette plateforme présente l’avantage de supporter NodeJS et peut donc intégrer des applications JavaScript.\nLa configuration du build Travis se trouve dans le fichier .travis.yml:\nlanguage: node_js node_js: - 0.10 before_script: - npm install -g grunt-cli script: - grunt karma:ci after_success: - grunt coverage On demande à Travis d’installer le client Grunt avant d’exécuter les tests unitaires et de publier la couverture de code. A chaque commit dans le repo GitHub, le build est lancé. La sortie console s’affiche en temps réel :\nEn cas d’échec du build, vous pouvez être notifiés par email , IRC, webhook \u0026hellip;\nOutre la génération d’un rapport de couverture de code testé, la commande grunt coverage envoie ce rapport au service Coveralls. Ce dernier historise le taux de couverture et offre une IHM permettant de naviguer parmi les fichiers analysés.\nL’ historique des builds du projet angular-musicbrainz est accessible en ligne :\nLe taux de couverture du build n°494581 est également consultable en ligne :\nAutre service en ligne intéressant : pouvoir vérifier rapidement que les dépendances d’une application sont à jour. C’est ce que propose David. Voici visuellement la synthèse proposée par David pour les dépendances de dev d’angular-musicbainz :\nA noter qu’un service similaire pour les dépendances utilisées par Bower serait intéressant.\nChacun de ces services propose un badge dynamique. Pratique, ces badges peuvent être affichés dans le README.MD :\nConclusion\nCe long billet m’aura permis de vous faire découvrir les différentes facettes du monde JavaScript dont j’ai fait connaissance tout au long du développement de cette petite application web de recherche.\nL’utilisation d’Angular est plaisante et me réconcilie avec le développement JavaScript que je ne trouvais jusque-là pas assez industrialisé. Diminuant le nombre de lignes de code JavaScript au profit du HTML, ce framework permet de structurer proprement le code JavaScript. Ceux qui ont connus des applications où chaque page comporte des centaines de lignes jQuery non organisées apprécieront sans aucun doute.\nEn quelques années, je constate avec plaisir que l’écosystème JavaScript a rattrapé son retard sur celui de Java : intégration continue, outils de builds, tests unitaires, tests fonctionnels, qualimétrie, gestion des dépendances, MVC, data-binding, POJO, templating, injection de dépendances, modularisation, nombre grandissant de frameworks, moteur d’exécution optimisé, support IDE … Chapeau bas.\n","link":"https://javaetmoi.com/2014/02/developper-industrialiser-web-app-recherche-angularjs/","section":"posts","tags":["angularjs","bootstrap","bower","elasticsearch","grunt","jasmine","javascript","jshint","karma","nodejs","npm","test","travis"],"title":"Développer et industrialiser une web app avec AngularJS"},{"body":"","link":"https://javaetmoi.com/tags/grunt/","section":"tags","tags":null,"title":"Grunt"},{"body":"","link":"https://javaetmoi.com/tags/jshint/","section":"tags","tags":null,"title":"Jshint"},{"body":"","link":"https://javaetmoi.com/tags/karma/","section":"tags","tags":null,"title":"Karma"},{"body":"","link":"https://javaetmoi.com/tags/nodejs/","section":"tags","tags":null,"title":"Nodejs"},{"body":"","link":"https://javaetmoi.com/tags/npm/","section":"tags","tags":null,"title":"Npm"},{"body":"","link":"https://javaetmoi.com/tags/travis/","section":"tags","tags":null,"title":"Travis"},{"body":" « Près de 2 ans passés chez un client en tant que référent technique d’un middle de recherche basé sur le moteur de recherche Elasticsearch, il me paraît aujourd’hui opportun de vous faire part des différentes problématiques rencontrées au cours des développements et de son exploitation. »\nEn 2 versions majeures et une montée de version d’Elasticsearch, les problématiques abordées ont été nombreuses : occupation mémoire, ré-indexation sans interruption de service, Split Brain, IDF et partitionnement. Prêts pour ce retour d’expérience ? »\nRÉINDEXATION SANS INTERRUPTION DE SERVICE Dans notre application, l’index Elasticsearch utilisé pour la recherche est construit à l’aide d’un batch Java. Les données indexées proviennent d’une base de données relationnelle. Une fois construit, l’index est mis à jour en temps réel par un système sophistiqué de notifications.\nMalgré ce dispositif, il est parfois nécessaire ou préférable de reconstruire totalement l’index. Voici quelques exemples : exécution de scripts SQL de rattrapage sur la base de données source, chargement en masse de nouvelles données, pertes de notification, évolution du mapping de l’index…\nEn fonction de la sollicitation de la base de données source et du cluster Elasticsearch, l’alimentation d’un index de plusieurs dizaines de millions de documents peut prendre jusqu’à 2h. Or, les services de recherche ont une haute disponibilité avec un SLA de 24/7. Hors de question de les interrompre pendant cette plage horaire de maintenance.\nLa solution à ce problème est illustrée sur le schéma suivant :\nVoici quelques précisions concernant chacune de ces 3 étapes :\nAu lieu d’interroger un index directement à partir de son nom physique (ici produits1), l’application cliente d’Elasticsearch (ex : HTML ou Java) utilise un nom logique ( produits) matérialisé dans Elasticsearch par un alias. Le batch commence par déterminer quel est le nom de l’index qu’il doit construire. Il regarde à quel index est lié l’alias produits. Dans l’exemple ci-dessus, il s’agit de produits1. Le batch en déduit qu’il doit construire un nouvel index produits2. A la fin de l’indexation, le batch utilise l’API d’Elasticsearch pour faire pointer l’alias clients vers le nouvel index clients2. L’index clients1 est supprimé. Ce changement est transparent pour l’application cliente. Voici le code Java permettant de changer l’alias en une seule requête Elasticsearch :\n// Bascule l\u0026#39;alias de l\u0026#39;ancien index vers le nouvel index IndicesAliasesRequestBuilder aliasesReqBuilder = esClient.admin().indices().prepareAliases(); aliasesReqBuilder.addAliasAction(new AliasAction(Type.ADD, \u0026#34;produits2\u0026#34;, \u0026#34;produits\u0026#34;)); aliasesReqBuilder.addAliasAction(new AliasAction(Type.REMOVE, \u0026#34;produits1\u0026#34;, \u0026#34;produits\u0026#34;)); aliasesReqBuilder.execute().actionGet(); Pour aller plus loin, l’article Changing mapping with zero downtime de Clinton Gormley explique quelles sont les opérations nécessitant une reconstruction de l’index. L’auteur confirme que l’utilisation d’un alias est la solution à privilégier pour garantir la haute disponibilité d’une application.\nOUTOFMEMORYERROR Au cours des développements et des tests de montée en charge du middle de recherche, nous sommes tombés à plusieurs reprises sur la tant redoutée OutOfMemoryError. A chaque fois, les raisons différaient. Etudions en quelques-unes.\nTRI DES RÉSULTATS Une attente récurrente des utilisateurs est de pouvoir trier leurs résultats en fonction de critères prédéfinis. Or, nativement, les résultats sont ordonnés en fonction d’un score calculé par chaque instance Lucene et agrégé par Elasticsearch. En fonction des règles de mapping et de recherche définies par les développeurs , les résultats les plus pertinents remontent en tête.\nPour ordonner les résultats différemment, Elasticsearch propose des fonctionnalités de tri. Ces fonctionnalités étaient disponibles dans la version 0.19.2 que nous utilisions. Nous nous sommes donc naturellement appuyés dessus pour implémenter des tris ressemblants aux cas présentés dans la capture d’écran ci-jointe.\nMis en place sans grande difficulté, la recette de l’application a confirmé que leur mise en œuvre satisfaisait les attentes du métier. Malheureusement, nos tests de charge ont mis en évidence des problèmes de mémoire. Après quelques minutes, les logs d’Elasticsearch remontaient des OutOfMemoryError et les nœuds incriminés passaient en mode dégradé.\nEn désactivant les ordres de tris des scénarios de recherche, les tests de charge passaient haut la main.\nAprès investigation, nous nous sommes rendu compte qu’ Elasticsearch met en cache toutes les valeurs des champs triés. La première fois qu’un tri est demandé sur un champ, Elasticsearch construit son cache et le temps de réponse en est fortement impacté. La mise en place de warmup est possible pour éviter ce désagrément au premier utilisateur.\nLes APIs d’administration et de supervision d’Elasticsearch permettent de consulter la taille du cache des tris. A partir de la volumétrie de production, nous avons pu mesurer qu’il nous faudrait 2,4 Go de mémoire rien que pour activer l’ensemble des tris. Les 2 Go de Xmx alloués par l’exploitation ne suffisaient donc pas.\nLes APIs de la version 0.19.2 d’Elasticsearch ne permettent pas de voir la répartition de la mémoire par champ. Nous avons tout de même pu les mesurer à coup d’arrêt / relance / tri unitaire. Pour vous donner une idée, mettre en cache les codes postaux des adresses demandait 94 Mo. Or, le nombre de codes postaux est fini. Tous uniques, la taille demandée par des identifiants n’en est que décuplée.\nElasticsearch étant installé sur une JVM 32 bits et un Linux, la limite des 3 Go de Xmx et les contraintes projets ont dû nous contraindre à désactiver les tris dans la V1 de notre application.\nNous n’avons réactivé les tris que récemment, profitant d’une montée de version d’Elasticsearch, et plus précisément la 0.90.5. Entre les 2 versions, l’occupation mémoire des données dans le cache a été grandement optimisée. A titre d’exemple, avec le même nombre de code postaux, le cache est passé de 94 Mo à 28 Mo. Suite à cette montée de version et à quelques améliorations apportées dans le mapping de notre index, la taille du cache a été calculée à 773 Mo (à comparer aux 2,4 Go précédents). Une fois la JVM configurée pour utiliser 3 Go de Xmx, les tests de charge ont été concluants.\nExemple d’utilisation de l’API stats demandant la taille des caches de l’ensemble des nœuds du cluster :\nRequête : GET http://localhost:9200/_nodes/stats/indices/fielddata/*?pretty\nRéponse :\n\u0026#34;nodes\u0026#34; : { \u0026#34;BZa34kwxRPqsxumXHIvCXa\u0026#34; : { \u0026#34;timestamp\u0026#34; : 1385115057137, \u0026#34;name\u0026#34; : \u0026#34;Noeud2\u0026#34;, \u0026#34;transport_address\u0026#34; : \u0026#34;inet[/172.30.31.12:9300]\u0026#34;, \u0026#34;hostname\u0026#34; : \u0026#34;localhost\u0026#34;, \u0026#34;attributes\u0026#34; : { \u0026#34;max_local_storage_nodes\u0026#34; : \u0026#34;3\u0026#34;, \u0026#34;zone\u0026#34; : \u0026#34;zoneB\u0026#34; }, \u0026#34;indices\u0026#34; : { \u0026#34;fielddata\u0026#34; : { \u0026#34;memory_size\u0026#34; : \u0026#34;773.2mb\u0026#34;, \u0026#34;memory_size_in_bytes\u0026#34; : 810758963, \u0026#34;evictions\u0026#34; : 0, \u0026#34;fields\u0026#34; : { \u0026#34;codepostal\u0026#34; : { \u0026#34;memory_size\u0026#34; : \u0026#34;28.1mb\u0026#34;, \u0026#34;memory_size_in_bytes\u0026#34; : 29464985 }, \u0026#34;tarif\u0026#34; : { \u0026#34;memory_size\u0026#34; : \u0026#34;65.9mb\u0026#34;, \u0026#34;memory_size_in_bytes\u0026#34; : 69101158 }, ... LUCENE ET LES JVM 32 BITS Au cours de la migration du middle de recherche de la version 0.19.2 d’Elasticsearch à une version 0.90.x, nous sommes tombés de manière inopinée sur un OutOfMemoryError remonté par l’API Java Elasticserarch de recherche. Comme le montre la pile d’appel ci-dessous, le message d’erreur référence le bug Lucene LUCENE-1566 Large Lucene index can hit false OOM due to Sun JRE issue :\n[2013-11-25 21:26:14,080][DEBUG][action.search.type] [elasticSeachNode] [client12][0], node[mflXc6jJS9CaJiYaCrKe2g], [P], s[STARTED]: Failed to execute [org.elasticsearch.action.search.SearchRequest@1ea6e24]java.lang.OutOfMemoryError: OutOfMemoryError likely caused by the Sun VM Bug described in https://issues.apache.org/jira/browse/LUCENE-1566; try calling FSDirectory.setReadChunkSize with a value smaller than the current chunk size (104857600) at org.apache.lucene.store.NIOFSDirectory$NIOFSIndexInput.readInternal(NIOFSDirectory.java:184) at org.apache.lucene.store.BufferedIndexInput.readBytes(BufferedIndexInput.java:158) ... at org.elasticsearch.search.query.QueryPhase.execute(QueryPhase.java:127) ... Le ticket Lucene référence à son tour un bug de la JVM Hotspot : JDK-6478546 : FileInputStream.read() throws OutOfMemoryError when there is plenty available. Sur des JVM 32 bits, la lecture de fichiers de plusieurs centaines de mega-octets provoque à tort des OutOfMemoryError sur les JVM ayant une heap conséquence.\nBien que ce bug soit marqué comme corrigé depuis la version 2.9 de Lucene, tous les critères de ce bug étaient réunis :\nJVM 32 bits Java 6 Sur le disque, la taille des index Lucene occupe entre quelques méga-octets et 2 Go Heap de 2 Go Pour résoudre ce problème, nous sommes simplement passés à une JVM Java 7 64 bits. Ce changement a été rendu possible grâce à une demande réalisée un an plus tôt auprès de l’équipe d’exploitation, ceci en prévision d’une éventuelle augmentation de mémoire au-delà des 3 Go de Heap, le maximum sous Linux des JVM 32 bits.\nBatch d’indexation en erreur\nLors de la montée de version d’Elasticsearch de la version 0.90.2 à 0.90.3, le batch d’indexation est tombé en OutOfMemoryError. Configuré avec 512 mo de Xmx, le batch n’avait jusque-là jamais posé de problème particulier.\nMulti-threadé, le batch a été paramétré pour traiter simultanément 5000 documents par thread. L’empreinte mémoire des documents indexés est relativement petite, de l’ordre de 1 ou 2 ko.\nEntre les 2 versions, notre profiler Java nous a montré qu’à volumétrie égale (25 000 requêtes), l’empreinte mémoire des instances de la classe IndexRequest était passée de 29 Mo à 823 Mo:\nAyant identifié le problème, j’ai remonté le bug 3624 dans le GitHub d’Elasticsearch. Dans l’heure qui a suivi, Shay Banon, le créateur d’Elasticsearch en personne, a pris en main le sujet. Il a identifié que la taille minimale du buffer des requêtes était passé de 1 Ko à 32 Ko entre les 2 versions. Le lendemain, il publiait un patch faisant repasser le buffer à 2 Ko. Dix jours plus tard, le patch était disponible dans la version 0.90.4 d’Elasticsearch. Entre temps, nous avons utilisé un contournement indiqué par Shay permettant de fixer manuellement la taille du buffer.\nSPLIT BRAIN Durant les premiers mois d’exploitation de notre cluster Elasticsearch, nous sommes tombés sur le phénomène du Split Brain.\nTechniquement, notre cluster Elasticsearch comporte 2 nœuds. Chaque nœud est réparti sur site géographique distinct. Chaque nœud héberge un seul index Elasticsearch formé d’un shard et d’un réplica.\nLe Split Brain Elasticsearh est survenue après une coupure réseau d’1 mn 30. Le schéma suivant montre l’état du cluster avant, pendant et après la coupure :\nPour fonctionner, un cluster Elasticsearch nécessite d’avoir un nœud maître. Le nœud 1 joue ce rôle. Il héberge la partition primaire (shard 0). Le nœud 2 est esclave et contient le réplica de la partition du nœud 1. Le réplica est une copie de la partition primaire et peut faire office de backup en cas de défaillance du nœud 1. Une coupure réseau intervient entre les 2 nœuds. Lorsqu’il essaie de communiquer avec son maître, le nœud 2 se prend une ConnectTransportException. A 3 reprises, toutes les 30 secondes, le nœud 2 essaie de se reconnecter au cluster. En vain. Possédant un réplica complet de l’index, sans nœud voisin, le nœud 2 va alors s’autoproclamer maître. Deux « sous-clusters » existent sur le réseau, chacun étant capable de répondre à des requêtes de recherche et d’indexation. Une fois le réseau rétabli, les 2 nœuds ne se réunissent pas en un seul et unique cluster comme on pourrait s’y attendre. Deux clusters indépendants coexistent. C’est ce que l’on appelle le Split Brain. Cette situation entraîne une divergence progressive des données indexées sur chacun des 2 sous-clusters, tantôt les requêtes d’indexation arrivant sur le nœud 1, tant sur le nœud 2. Ce problème est difficile à mettre en évidence car toutes les requêtes de recherche comme d’indexation répondent. Nous nous en sommes aperçus par hasard à quelques jours d’une nouvelle mise en production du middle de recherche, en vérifiant les logs Elasticsearch. Après redémarrage d’un des nœuds, une réindexation totale des données a été réalisée. En attendant de trouver une solution plus pérenne, le dossier de supervision a été complété afin de nous alerter en cas de récidive.\nLa solution à ce problème est connue et documentée : passer le cluster à 3 nœuds et fixer le paramètre discovery.zen.minimum_master_node à N/2 + 1, soit 2 dans notre cas.\nCe paramètre permet de spécifier que l’élection d’un nouveau maître nécessite la majorité absolue. Scindé en deux, un cluster de 2 nœuds n’aurait pas pu élire de maître. Jusque-là maître, le Nœud 1 se serait mis en attente de la reconnexion réseau. Le cluster aurait perdu sa haute-disponibilité : aucun des nœuds n’aurait pu desservir les requêtes de recherche.\nAvec 3 nœuds, l’isolement d’un nœud vis-à-vis de ses 2 compères aurait permis de conserver un cluster actif. A noter que le 3ième nœud peut être configuré pour ne pas héberger de données (paramètre node.data à false). Il joue alors uniquement le rôle d’ arbitre.\nIDF Pour calculer le score d’un document, Elasticsearch se base notamment sur l’ Inverse Document Frequency (IDF). La formule statistique sous-jacente à l’IDF peut se traduire en une phrase : la fréquence d’un terme influe sur son score. Et plus précisément : plus le terme est rare dans les documents indexés, plus il est pertinent et son score est élevé. En soit, ce critère parait être du bon sens. Mais dans des applications de gestion nécessitant une précision accrue, cela n’est pas toujours le résultat souhaité.\nPrenons le cas d’une recherche par nom et prénom. Plus rares, les personnes avec le nom Antoine sortent avant ceux qui ont le prénom Antoine. A l’inverse, les personnes ayant le prénom Martin sortent avant ceux qui ont le nom Martin. Or, fonctionnellement, il nous a été demandé de privilégier le nom de famille. Le besoin métier nécessite donc d’augmenter le poids du nom par rapport au prénom. C’est précisément ce que permettent les boosts. Le plus difficile est de trouver le bon ratio. Pour se faire, des données et une volumétrie de production sont indispensables. Le boost à donner à chaque champ va être déterminé de manière empirique par exécution successive de requêtes de recherche.\nL’IDF peut également poser des problèmes de précision lorsqu’un index est partitionné en plusieurs shard s. Techniquement, un shard correspond à un index physique Lucene. Chaque shard dispose donc de sa propre répartition des fréquences des termes.\nEn fonction des autres documents indexés dans le même shard, le même document peut donc avoir un score différent.\nPour pallier à ce problème, Elasticsearch permet de réaliser des recherches de type DFS Query Then Fetch. Est ajoutée une phase initiale consistant à demander à chaque shard la fréquence des termes et documents à rechercher. Une fréquence globale à tout l’index peut alors être calculée.\nBien entendu, ce calcul n’est pas neutre en termes de performance.\nPARTITIONNEMENT Le partitionnement (ou sharding) est une fonctionnalité phare d’Elasticsearch. C’est ce qui permet à un cluster d’être tolérant aux pannes et de devenir hautement scalable.\nPour autant, utiliser le partitionnement apporte un certain nombre d’ inconvénients spécifiques aux systèmes distribués. En plus d’éventuelles pertes de performance ou de dégradations de l’occupation mémoire, une requête de recherche distribuée sur plusieurs shards peut souffrir des symptômes suivants :\nNombre d’éléments des facettes inexacts (bien que la précision peut être améliorée à partir de la version 0.90.6) Ordre incorrect des éléments d’une facette Scoring potentiellement différents sur deux partitions Par ailleurs, il est connu par l’équipe en charge du développement d’Elasticsearch que partitionnement et pagination ne font pas bon ménage. En effet, sans routage efficace, le coût nécessaire pour trier les documents croît exponentiellement en fonction du numéro de page demandé. Le diagramme ci-dessous met en évidence cette complexité :\nMalgré toutes les problématiques abordées dans ce billet,j’aimerais conclure ce retour d’expérience en précisant qu’ Elasticsearch nous a donné entière satisfaction. Les temps de réponse sont au rendez-vous, de l’ordre de quelques millisecondes. Et les utilisateurs en sont grandement satisfaits.\nAvant de se lancer dans sa mise en œuvre, il faut garder à l’esprit que chaque utilisation d’Elasticsearch est singulière. En effet, les techniques employées pour indexer des articles de presse ou des logs ne sont pas les mêmes que celles utilisées pour indexer des données métier. Aussi, s’entourer d’un expert en moteurs de recherche est un réel atout pour assurer la réussite d’un projet.\nRéférences :\nHow sharding in elasticsearch makes scoring a little less accurate and what to do about it Understanding “query then fetch” vs “dfs query then fetch” Terms facet gives wrong count with n_shards \u0026gt; 1 Partenaires de la société Elasticsearch The Kagillion shards problem Elasticsearch in production ","link":"https://javaetmoi.com/2013/12/retour-experience-problematiques-elasticsearch/","section":"posts","tags":["elasticsearch","nosql"],"title":"Retour d’expérience sur les problématiques Elasticsearch"},{"body":"","link":"https://javaetmoi.com/tags/dbsetup/","section":"tags","tags":null,"title":"Dbsetup"},{"body":" Pour les besoins d’un workshop sur Elasticsearch, je me suis amusé à indexer une encyclopédie musicale et à mettre en ligne une petite application HTML 5 permettant de réaliser des recherches.\nComme source de données musicale, j’ai opté pour MusicBrainz qui est une plateforme ouverte collectant des méta-données sur les artistes, leurs albums et leurs chansons puis les mettant à disposition du publique.\nPour indexer les données depuis une base PostgreSQL, j’ai privilégié Spring Batch au détriment d\u0026rsquo;une river. Pour l’IHM, j’ai adapté un prototype basé sur AngularJS, jQuery et Bootstrap qu’avait réalisé Lucian Precup pour la Scrum Day 2013. La mise en ligne de l’index Elasticsearch m’aura permis de tester la plateforme Cloud OpenShift de Redhat.\nCet article a pour objectif de décrire les différentes étapes qui m’ont été nécessaires pour réaliser ma démo et d’expliquer ce que j’ai librement rendu accessible sur GitHub et Internet.\nVue d’ensemble Le diagramme suivant présente l’architecture mise en place.\nUn batch d’indexation se connecte via JDBC à la base de données de MusicBrainz et indexe les albums de musique dans Elasticsearch. Une application HTML 5 permet d’interroger l’index Elasticsearch.\nBase de données MusicBrainz A l’instar d’IMDb pour le cinéma, MusicBrainz est une base de données dédiée à la musique. Artistes, groupes de musiques, albums, pochettes et chansons issus du monde entier y sont référencés. Outre la base de données musicale, MusicBrainz propose également une interface graphique permettant d’effectuer des recherches, de consulter les données et de participer à l’enrichissement de la base. Last.fm, The Guardian ou bien encore la BBC s’interfacent avec MusicBrainz.\nParce que la base PostgreSQL du sites MusicBrainz.org n’est pas accessible depuis Internet mais également dans le souci de pouvoir réaliser ma démo déconnecté du réseau, j’ai cherché à pouvoir installer la base de données en locale. MusicBrainz propose 2 solutions :\nTélécharger l’ image d’une machine virtuelle du serveur MusicBrainz ou Télécharger la dernière archive de la base PostgreSQL est l’installer en suivant les instructions du INSTALL.md Pour ma part, j’ai opté pour la solution la plus simple : installer une VM. Disponible au format OVA, elle peut être déployée aussi bien dans VirtualBox ou que dans VMWare. Le guide d’installation de la VM terminé, 2 étapes seront ensuite nécessaires pour que le host puisse accéder à la base PostgreSQL :\nConfigurer la redirection de port : VirtualBox permet de rediriger les connexions TCP établies sur un port de l’host vers un autre port de la VM. La base PostgreSQL écoutant sur le port 5432, la règle suivante peut être ajoutée via l’interface de VirtualBox : PostgreSQL database - TCP - host : 5432 / guest : 5432 Configurer PostgreSQL: par mesure de sécurité, la base PostgreSQL ne permet pas d’accès distant. Pour que le batch exécuté depuis l’OS hôte puisse s’y connecter, ces instructions doivent être suivies. Démarrer la VM, s’y connecter (login : vm / musicbrainz) et éditer les 2 fichiers de configuration ph_hba.conf et postgresql.conf. Depuis l’hôte, il est à présent possible de se connecter à la base à partir de n’importe quel client SQL (SQuireL, pgAdmin …). Utiliser les paramètres de connexion suivants :\nURL : jdbc:postgresql://localhost:5432/musicbrainz Login : musicbrainz / musicbrainz Le batch Java est désormais capable de récupérer les données à indexer.\nServeur Elasticsearch en local Le batch se connecte à un cluster Elasticsearch. L’installation d’un cluster est donc nécessaire, que ce soit sur votre poste de développement ou sur une autre machine. Installer un serveur Elasticsearch est on ne peut plus simple. Quelques lignes de commandes suffisent. Pour davantage d’explications, je vous renvoie à l’article Premiers pas avec ElasticSearch de Tanguy Leroux. Au vu de la volumétrie des données et de la faible charge, un seul nœud suffit amplement.\nLe batch d’indexation Le batch n’indexe pas toute la base de données MusicBrainz. Il se cantonne aux albums de musique qui sont un sous ensemble des release groups. Seuls les albums « principaux » sont indexés. Single, EP, Compilation, Live ou autre Remix ne sont pas indexés.\nLe batch d’indexation est composé d’un seul job Spring Batch. La configuration des beans d’infrastructure sur lesquels s’appuie le batch est répartie dans les fichiers applicationContext-datasource.xml, applicationContext-elasticsearch.xml et applicationContext-batch.xml. Y sont déclarés :\nla source de données MusicBrainz et son gestionnaire de transaction, un client Elasticsearch déclaré via la fabrique de beans Spring mise à disposition par David Pilato dans le projet spring-elasticsearch, un JobRepository en mémoire et un JobLauncher Spring Batch. Déclaré dans le fichier applicationContext-job.xml, le job musicAlbumJob est décomposé en 4 étapes successives :\nSuppression d’un éventuel précédent index Création de l’ index musicalbum Définition du type de document album Indexation dans Elasticsearch La définition du job ne comporte aucune difficulté :\n\u0026lt;job id=\u0026#34;musicAlbumJob\u0026#34; xmlns=\u0026#34;http://www.springframework.org/schema/batch\u0026#34;\u0026gt; \u0026lt;step id=\u0026#34;deleteIndexIfExists\u0026#34; next=\u0026#34;createIndexSettings\u0026#34;\u0026gt; \u0026lt;tasklet ref=\u0026#34;deleteIndexTasklet\u0026#34; /\u0026gt; \u0026lt;/step\u0026gt; \u0026lt;step id=\u0026#34;createIndexSettings\u0026#34; next=\u0026#34;createIndexMapping\u0026#34;\u0026gt; \u0026lt;tasklet ref=\u0026#34;createIndexSettingsTasklet\u0026#34; /\u0026gt; \u0026lt;/step\u0026gt; \u0026lt;step id=\u0026#34;createIndexMapping\u0026#34; next=\u0026#34;indexMusicAlbum\u0026#34;\u0026gt; \u0026lt;tasklet ref=\u0026#34;createIndexMappingTasklet\u0026#34; /\u0026gt; \u0026lt;/step\u0026gt; \u0026lt;step id=\u0026#34;indexMusicAlbum\u0026#34;\u0026gt; \u0026lt;!-- Executes partition steps locally in separate threads of execution --\u0026gt; \u0026lt;batch:partition step=\u0026#34;indexMusicAlbumPartition\u0026#34; partitioner=\u0026#34;partitionerMusicAlbum\u0026#34;\u0026gt; \u0026lt;batch:handler grid-size=\u0026#34;${batch.partition}\u0026#34; task-executor=\u0026#34;batchTaskExecutor\u0026#34; /\u0026gt; \u0026lt;/batch:partition\u0026gt; \u0026lt;/step\u0026gt; \u0026lt;/job\u0026gt; A noter ligne 31 que le batch profite du mécanisme de partitionnement présenté dans le précédent billet Parallélisation de traitements batchs. Chacun des beans référencés par les steps sont définis dans le même fichier de configuration Spring. Les 3 premières étapes sont implémentés à l’aide de tasklets :\n\u0026lt;bean id=\u0026#34;deleteIndexTasklet\u0026#34; class=\u0026#34;com.javaetmoi.core.batch.tasklet.DeleteElasticIndexTasklet\u0026#34; p:esClient-ref=\u0026#34;esClient\u0026#34; p:indexName=\u0026#34;${es.index}\u0026#34; /\u0026gt; \u0026lt;bean id=\u0026#34;createIndexSettingsTasklet\u0026#34; class=\u0026#34;com.javaetmoi.core.batch.tasklet.CreateElasticIndexSettingsTasklet\u0026#34; p:esClient-ref=\u0026#34;esClient\u0026#34; p:indexName=\u0026#34;${es.index}\u0026#34; p:indexSettings=\u0026#34;${es.settings.filename}\u0026#34; /\u0026gt; \u0026lt;bean id=\u0026#34;createIndexMappingTasklet\u0026#34; class=\u0026#34;com.javaetmoi.core.batch.tasklet.CreateElasticIndexMappingTasklet\u0026#34; p:esClient-ref=\u0026#34;esClient\u0026#34; p:indexName=\u0026#34;${es.index}\u0026#34; p:indexMapping=\u0026#34;${es.mapping.filename}\u0026#34; p:mappingType=\u0026#34;${es.mapping.type}\u0026#34; /\u0026gt; Utilisant l’API Java d’Elasticsearch, ces tasklets sont assez génériques pour être réutilisées sur d’autres projets. En attendant d’apporter qui sait ma contribution au projet spring-batch-elasticsearch d’Olivier Bazoud, je les ai mis à disposition dans la version 0.2 du projet spring-batch-toolkit.\nA titre d\u0026rsquo;exemple, voici un extrait de la tasklet CreateElasticIndexSettingsTasklet:\npublic class CreateElasticIndexSettingsTasklet implements Tasklet { private static final Logger LOG = LoggerFactory.getLogger(CreateElasticIndexSettingsTasklet.class); private Client esClient; private String indexName; private Resource indexSettings; @PostConstruct public void afterPropertiesSet() { Assert.notNull(esClient, \u0026#34;esClient must not be null\u0026#34;); Assert.notNull(indexName, \u0026#34;indexName must not be null\u0026#34;); Assert.notNull(indexSettings, \u0026#34;indexSettings must not be null\u0026#34;); } @Override public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception { LOG.debug(\u0026#34;Creating the index {} settings\u0026#34;, indexName); String source = IOUtils.toString(indexSettings.getInputStream(), \u0026#34;UTF-8\u0026#34;); CreateIndexRequestBuilder createIndexReq = esClient.admin().indices().prepareCreate(indexName); createIndexReq.setSettings(source); CreateIndexResponse response = createIndexReq.execute().actionGet(); if (!response.isAcknowledged()) { throw new RuntimeException(\u0026#34;The index settings has not been acknowledged\u0026#34;); } esClient.admin().indices().refresh(new RefreshRequest(indexName)).actionGet(); LOG.info(\u0026#34;Index {} settings created\u0026#34;, indexName); return RepeatStatus.FINISHED; } /** * Sets the Elasticsearch client used to defined index settings. * * @param esClient * Elasticsearch client */ public void setEsClient(Client esClient) { this.esClient = esClient; } /** * Sets the name of the index where documents will be stored * * @param indexName * name of the Elasticsearch index */ public void setIndexName(String indexName) { this.indexName = indexName; } /** * Sets the JSON resource defining index settings. * * @param indexSettings * Spring resource descriptor, such as a file or class path resource. */ public void setIndexSettings(Resource indexSettings) { this.indexSettings = indexSettings; } } Le bean de partition indexMusicAlbumPartition s’appuie quant à lui sur un chunk Spring Batch composé d’un reader, d’un writer et d’un processor composite :\n\u0026lt;!-- Read music albums from database then index them into ElasticSearch --\u0026gt; \u0026lt;batch:step id=\u0026#34;indexMusicAlbumPartition\u0026#34;\u0026gt; \u0026lt;tasklet transaction-manager=\u0026#34;musicBrainzTransactionManager\u0026#34;\u0026gt; \u0026lt;chunk reader=\u0026#34;musicAlbumReader\u0026#34; processor=\u0026#34;musicAlbumProcessor\u0026#34; writer=\u0026#34;musicAlbumWriter\u0026#34; commit-interval=\u0026#34;${batch.commit.interval}\u0026#34; retry-limit=\u0026#34;3\u0026#34;\u0026gt; \u0026lt;retryable-exception-classes\u0026gt; \u0026lt;include class=\u0026#34;org.elasticsearch.client.transport.NoNodeAvailableException\u0026#34; /\u0026gt; \u0026lt;include class=\u0026#34;org.elasticsearch.transport.ReceiveTimeoutTransportException\u0026#34; /\u0026gt; \u0026lt;/retryable-exception-classes\u0026gt; \u0026lt;/chunk\u0026gt; \u0026lt;listeners\u0026gt; \u0026lt;listener\u0026gt; \u0026lt;bean class=\u0026#34;com.javaetmoi.core.batch.listener.LogStepListener\u0026#34; scope=\u0026#34;step\u0026#34; p:commitInterval=\u0026#34;${batch.commit.interval}\u0026#34;/\u0026gt; \u0026lt;/listener\u0026gt; \u0026lt;/listeners\u0026gt; \u0026lt;/tasklet\u0026gt; \u0026lt;/batch:step\u0026gt; Dans le fichier properties de configuration du batch, la taille des lots ( commit-interval) est fixé à 5000 albums.\nLe bean musicAlbumReader utilise la classe JdbcCursorItemReader de Spring Batch pour exécuter la requête SQL chargée de lire les albums. Cette requête effectue une jointure entre 10 tables et filtre sur des critères permettant de ramener un ResultSet dans lequel un album ne correspond qu’à une seule ligne. Aucune agrégation de lignes n’est donc à réaliser par le reader. L’enrichissement de l’album avec des données multi-valuées (ex : tags) est réalisé dans la phase de traitement.\nPour comprendre la requête, le modèle physique de données de MusicBrainz est consultable en ligne.\n\u0026lt;bean id=\u0026#34;musicAlbumReader\u0026#34; class=\u0026#34;org.springframework.batch.item.database.JdbcCursorItemReader\u0026#34; scope=\u0026#34;step\u0026#34; p:dataSource-ref=\u0026#34;musicBrainzDataSource\u0026#34; p:rowMapper-ref=\u0026#34;albumRowMapper\u0026#34;\u0026gt; \u0026lt;property name=\u0026#34;sql\u0026#34;\u0026gt; \u0026lt;value\u0026gt;\u0026lt;![CDATA[ SELECT release_group.id AS albumId, release_group.gid AS albumGid, release_group.type AS albumPrimaryTypeId, release_name.name AS albumName, artist_name.name AS artistName, artist.gid AS artistGid, artist.type as artistTypeId, artist.begin_date_year artistBeginDateYear, artist.gender as artistGenderId, area.name as artistCountryName, artist_meta.rating artistRatingScore, artist_meta.rating_count artistRatingCount, release_group_meta.first_release_date_year albumYear, release_group_meta.rating albumRatingScore, release_group_meta.rating_count albumRatingCount FROM artist INNER JOIN artist_credit_name ON artist_credit_name.artist = artist.id INNER JOIN artist_credit ON artist_credit.id = artist_credit_name.artist_credit INNER JOIN release_group ON release_group.artist_credit = artist_credit.id INNER JOIN release_name ON release_name.id = release_group.name INNER JOIN artist_name ON artist.name = artist_name.id INNER JOIN area ON artist.area = area.id LEFT OUTER JOIN release_group_secondary_type_join ON release_group_secondary_type_join.release_group = release_group.id LEFT OUTER JOIN artist_meta ON artist.id = artist_meta.id LEFT OUTER JOIN release_group_meta ON release_group_meta.id = release_group.id WHERE release_group.type = \u0026#39;1\u0026#39; AND artist_credit.artist_count = 1 AND release_group_secondary_type_join.secondary_type IS NULL AND release_group.id \u0026gt;= ? and release_group.id \u0026lt;= ? ]]\u0026gt;\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;property name=\u0026#34;preparedStatementSetter\u0026#34;\u0026gt; \u0026lt;bean class=\u0026#34;org.springframework.batch.core.resource.ListPreparedStatementSetter\u0026#34;\u0026gt; \u0026lt;property name=\u0026#34;parameters\u0026#34;\u0026gt; \u0026lt;list\u0026gt; \u0026lt;!-- SPeL parameters order is important because it referes to \u0026#34;where album_id \u0026gt;= ? and album_id \u0026lt;= ?\u0026#34; --\u0026gt; \u0026lt;value\u0026gt;#{stepExecutionContext[minValue]}\u0026lt;/value\u0026gt; \u0026lt;value\u0026gt;#{stepExecutionContext[maxValue]}\u0026lt;/value\u0026gt; \u0026lt;/list\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;/bean\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;/bean\u0026gt; Le ResultSet est mappé à l’aide de la classe AlbumRowMapper implémentant l’interface RowMapper de Spring JDBC. Une instance de a classe Album est retournée en sortie du reader.\npublic class Album { private Integer id; private String gid; private String name; private ReleaseGroupPrimaryType type; private Integer year; private Rating rating = new Rating(); private Artist artist = new Artist(); private List\u0026lt;String\u0026gt; tags; A ce stade, la liste des tags utilisés dans MusicBrainz pour qualifier le genre musical d’un album est vide.\nLe bean musicAlbumProcessor est composé de 2 traitements successifs matérialisés par 2 classes : EnhanceAlbumProcessor et MusicAlbumDocumentProcessor. La première exécute une requête JDBC pour charger les tags de l’album. Le 2nd transforme la classe Album en un document indexable dans Elasticsearch.\n\u0026lt;bean id=\u0026#34;musicAlbumProcessor\u0026#34; class=\u0026#34;org.springframework.batch.item.support.CompositeItemProcessor\u0026#34;\u0026gt; \u0026lt;property name=\u0026#34;delegates\u0026#34;\u0026gt; \u0026lt;list\u0026gt; \u0026lt;bean class=\u0026#34;com.javaetmoi.elasticsearch.musicbrainz.batch.item.EnhanceAlbumProcessor\u0026#34; /\u0026gt; \u0026lt;bean class=\u0026#34;com.javaetmoi.elasticsearch.musicbrainz.batch.item.MusicAlbumDocumentProcessor\u0026#34; p:documentType=\u0026#34;${es.mapping.type}\u0026#34; /\u0026gt; \u0026lt;/list\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;/bean\u0026gt; La classe MusicAlbumDocumentProcessor implémente indirectement l’interface ItemProcessor de Spring Batch. Elle prend en entrée un Album et le transforme EsDocument. La classe EsDocument modélise un document indexable dans Elasticsearch. Elle comporte un identifiant, un type, un contenu et éventuellement une version. Cette classe est suffisamment générique pour avoir été factorisé dans le projet spring-batch-toolkit.\npublic class EsDocument { private String id; private String type; private Long version; private XContentBuilder contentBuilder; /** * EsDocument constructor. * * @param type * type of the Elasticsearch document * @param contentBuilder * Elasticsearch helper to generate JSON content. */ public EsDocument(String type, XContentBuilder contentBuilder) { this.type = type; this.contentBuilder = contentBuilder; } protected String getId() { return id; } /** * Set the ID of a document which identifies a document. * * @param id * ID of a document (may be \u0026lt;code\u0026gt;null\u0026lt;/code\u0026gt;) */ public void setId(String id) { this.id = id; } protected XContentBuilder getContentBuilder() { return contentBuilder; } protected String getType() { return type; } /** * Sets the version, which will cause the index operation to only be performed if a matching * version exists and no changes happened on the doc since then. * * @param version * version of a document * @see http://www.elasticsearch.org/blog/versioning/ */ protected void setVersion(Long version) { this.version = version; } protected boolean isVersioned() { return version !=null; } public Long getVersion() { return version; } } Le type XContentBuilder fait partie de l’API Java d’Elasticsearch. Il permet de construire en mémoire la représentation d’un objet JSON. La classe abstraite EsDocumentProcessor dont hérite MusicAlbumDocumentProcessor implémente le pattern template method et pilote la création du EsDocument. La construction de l’objet JSON a été réalisée manuellement en utilisant les méthodes startObject, field, array et endObject exposées par le XContentBuilder. Comme alternative, Jackson aurait pu être utilisé pour sérialiser la classe Album en JSON.\nLe bean musicAlbumWriter termine le traitement batch. Il utilise la fonctionnalité de requêtes en masse ( bulk request) d’Elasticsearch pour indexer simultanément tous les documents lus dans un chunk (soit ici 5000). Factorisée elle aussi dans le projet spring-batch-toolkit, la classe EsDocumentWriter concentre le code :\n/** * Index several documents in a single bulk request. */ public class EsDocumentWriter implements ItemWriter\u0026lt;EsDocument\u0026gt; { private static final Logger LOG = LoggerFactory.getLogger(EsDocumentWriter.class); private Client esClient; private String indexName; private Long timeout; @PostConstruct public void afterPropertiesSet() { Assert.notNull(esClient, \u0026#34;esClient must not be null\u0026#34;); Assert.notNull(indexName, \u0026#34;indexName must not be null\u0026#34;); } @Override public final void write(List\u0026lt;? extends EsDocument\u0026gt; documents) throws Exception { BulkRequestBuilder bulkRequest = esClient.prepareBulk(); for (EsDocument doc : documents) { IndexRequestBuilder request = esClient.prepareIndex(indexName, doc.getType()).setSource( doc.getContentBuilder()); request.setId(doc.getId()); if (doc.isVersioned()) { request.setVersion(doc.getVersion()); } bulkRequest.add(request); } BulkResponse response; if (timeout != null) { response = bulkRequest.execute().actionGet(timeout); } else { response = bulkRequest.execute().actionGet(); } processResponse(response); } private void processResponse(BulkResponse response) { if (response.hasFailures()) { String failureMessage = response.getItems()[0].getFailureMessage(); throw new ElasticSearchException(\u0026#34;Bulk request failed. First failure message: \u0026#34; + failureMessage); } LOG.info(\u0026#34;{} documents indexed into ElasticSearch in {} ms\u0026#34;, response.getItems().length, response.getTookInMillis()); } /** * Sets the Elasticsearch client used for bulk request. * * @param esClient * Elasticsearch client */ public void setEsClient(Client esClient) { this.esClient = esClient; } /** * Sets the name of the index where documents will be stored. * * @param indexName * name of the Elasticsearch index */ public void setIndexName(String indexName) { this.indexName = indexName; } /** * Waits if necessary for at most the given time for the computation to complete, and then * retrieves its result, if available. * * @param timeout * the maximum time in milliseconds to wait */ public void setTimeout(Long timeout) { this.timeout = timeout; } } En sortie, voici un exemple du document JSON représentant l’album “Achtung Baby” du groupe U2 :\nMapping Elasticsearch Comme expliqué précédemment, le batch est chargé de créer l’ index musicalbum. Outre le nombre de shards et de réplicas, le fichier es-index-settings.json déclare les filtres et les analyseurs utilisés pour indexer puis rechercher des albums.\nLe filtre myEdgeNGram et l’analyseur myPartialNameAnalyzer sont par exemple utilisés par l’ autosuggestion des résultats de recherche :\n\u0026#34;analysis\u0026#34;: { \u0026#34;filter\u0026#34;: { \u0026#34;myEdgeNGram\u0026#34;: { \u0026#34;side\u0026#34;: \u0026#34;front\u0026#34;, \u0026#34;max_gram\u0026#34;: 10, \u0026#34;min_gram\u0026#34;: 1, \u0026#34;type\u0026#34;: \u0026#34;edgeNGram\u0026#34; } }, \u0026#34;analyzer\u0026#34;: { \u0026#34;myStandardAnalyzer\u0026#34;: { \u0026#34;filter\u0026#34;: [ \u0026#34;standard\u0026#34;, \u0026#34;lowercase\u0026#34;, \u0026#34;asciifolding\u0026#34; ], \u0026#34;type\u0026#34;: \u0026#34;custom\u0026#34;, \u0026#34;tokenizer\u0026#34;: \u0026#34;standard\u0026#34; }, \u0026#34;myPartialNameAnalyzer\u0026#34;: { \u0026#34;filter\u0026#34;: [ \u0026#34;standard\u0026#34;, \u0026#34;lowercase\u0026#34;, \u0026#34;asciifolding\u0026#34;, \u0026#34;myEdgeNGram\u0026#34; ], \u0026#34;type\u0026#34;: \u0026#34;custom\u0026#34;, \u0026#34;tokenizer\u0026#34;: \u0026#34;standard\u0026#34; }, Le fichier es-index-mappings.json précise à Elasticsearch comment indexer les différents champs de l’ EsDocument construit à partir d’un Album. Ce sont les usages de recherche qui guident la réalisation du fichier de mapping. Par exemple, le nom d’un album sera indexé de 2 manières à l’aide d’une propriété de type multi_field : l’une pour la recherche fulltext et l’autre pour l’autosuggestion.\n{ \u0026#34;_source\u0026#34;: { \u0026#34;enabled\u0026#34;: \u0026#34;true\u0026#34;, \u0026#34;compress\u0026#34;: \u0026#34;true\u0026#34; }, \u0026#34;properties\u0026#34;: { \u0026#34;id\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;analyzer\u0026#34;: \u0026#34;myIdAnalyzer\u0026#34; }, \u0026#34;name\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;multi_field\u0026#34;, \u0026#34;fields\u0026#34;: { \u0026#34;name\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;analyzer\u0026#34;: \u0026#34;myStandardAnalyzer\u0026#34; }, \u0026#34;start\u0026#34;: { \u0026#34;search_analyzer\u0026#34;: \u0026#34;myStandardAnalyzer\u0026#34;, \u0026#34;index_analyzer\u0026#34;: \u0026#34;myPartialNameAnalyzer\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34; } } }, \u0026#34;year\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;multi_field\u0026#34;, \u0026#34;fields\u0026#34;: { \u0026#34;year\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;date\u0026#34;, \u0026#34;format\u0026#34;: \u0026#34;year\u0026#34; }, \u0026#34;string\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;analyzer\u0026#34;: \u0026#34;myBasicAnalyzer\u0026#34; } } }, \u0026#34;rating\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;object\u0026#34;, \u0026#34;properties\u0026#34;: { \u0026#34;score\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;integer\u0026#34; }, \u0026#34;count\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;integer\u0026#34; } } }, \u0026#34;tags\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;index_name\u0026#34;: \u0026#34;tag\u0026#34; }, \u0026#34;artist\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;object\u0026#34;, \u0026#34;properties\u0026#34;: { \u0026#34;name\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;multi_field\u0026#34;, \u0026#34;fields\u0026#34;: { \u0026#34;name\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;analyzer\u0026#34;: \u0026#34;myStandardAnalyzer\u0026#34; }, \u0026#34;start\u0026#34;: { \u0026#34;search_analyzer\u0026#34;: \u0026#34;myStandardAnalyzer\u0026#34;, \u0026#34;index_analyzer\u0026#34;: \u0026#34;myPartialNameAnalyzer\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34; } } }, \u0026#34;type_id\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;integer\u0026#34; }, \u0026#34;type_name\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34; }, \u0026#34;begin_date_year\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;date\u0026#34;, \u0026#34;format\u0026#34;: \u0026#34;year\u0026#34; }, \u0026#34;country_name\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;analyzer\u0026#34;: \u0026#34;myStandardAnalyzer\u0026#34; }, \u0026#34;gender\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;analyzer\u0026#34;: \u0026#34;myBasicAnalyzer\u0026#34; }, \u0026#34;rating\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;object\u0026#34;, \u0026#34;properties\u0026#34;: { \u0026#34;score\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;integer\u0026#34; }, \u0026#34;count\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;integer\u0026#34; } } } } } } } Tests unitaires Avant d’exécuter le batch sur la base de données MusicBrainz, le test unitaire TestMusicAlbumJob m’aura permis d’éprouver le code. La structure du schéma de la base MusicBrainz est reproduite dans une base de données en mémoire H2. Elle est alimentée avec la discographie de U2. Pour se faire, la librairie open source DbSetup a été mise une nouvelle fois à contribution. Une instance Elasticsearch embarquée est démarrée par le test. Le batch est exécuté. Le test vérifie simplement que le nombre de documents indexés correspond au nombre d’albums de U2. En complément, l’exécution d’une requête de recherche aurait permis de valider le mapping.\nExécution du batch Comme son nom l’indique, la classe IndexBatchMain fournit la méthode main permettant d’exécuter le batch en ligne de commande. Quelques étapes suffisent :\nDémarrer un serveur Elasticsearch Démarrer la base de données MusicBrainz database ou la VM l’hébergeant git clone https://github.com/arey/musicbrainz-elasticsearch.git Personnaliser si besoin le fichier es-musicbrainz-batch.properties mvn install mvn exec:java Quelques minutes plus tard, quelques 265 169 albums sont indexés.\nDémo Pour exploiter l’index nouvellement créé, rien de tel qu’une petite interface en HTML 5. Pour se faire, Lucian Precup m’a autorisé à adapter une page qu’il avait mis au point dans le cadre de l’atelier Construisons un moteur de recherche tenu lors de la Scrum Day 2013. Réalisée en AngularJS, jQuery et Boostrap, cette page propose une zone de recherche full-text, offre de l’autosuggestion et affiche le résultat de recherche de manière paginée. Quelques filtres et directives Angular ont été ajoutés pour, par exemple, gérer les appréciations des mélomanes. La capture d’écran ci-dessous donne un aperçu du rendu graphique :\nDéployée sur OVH, l’application Angular est accessible à l’adresse http://musicsearch.javaetmoi.com/\nRequêtes de recherche La recherche utilisée pour l’ autosuggestion repose sur une query_string analysant le nom de l’album, le nom de l’artiste et la date de sortie de l’album. Pour les noms, elle utilise 2 champs : celui pour la recherche exacte (ex: artist.name) et celui pour la recherche de type « commence par » (ex : artist.name.start). La surbrillance est activée sur les 3 critères.\nLe gist 7436834 propose la commande curl équivalente :\ncurl -XPOST \u0026#39;http://es.javaetmoi.com/musicalbum/album/_search?pretty\u0026#39; -d \u0026#39; { \u0026#34;fields\u0026#34;: [ \u0026#34;artist.name\u0026#34;, \u0026#34;id\u0026#34;, \u0026#34;name\u0026#34;, \u0026#34;year.string\u0026#34; ], \u0026#34;query\u0026#34;: { \u0026#34;query_string\u0026#34;: { \u0026#34;fields\u0026#34;: [ \u0026#34;name\u0026#34;, \u0026#34;name.start\u0026#34;, \u0026#34;year.string\u0026#34;, \u0026#34;artist.name\u0026#34;, \u0026#34;artist.name.start\u0026#34; ], \u0026#34;query\u0026#34;: \u0026#34;U2\u0026#34;, \u0026#34;use_dis_max\u0026#34;: false, \u0026#34;auto_generate_phrase_queries\u0026#34;: true, \u0026#34;default_operator\u0026#34;: \u0026#34;OR\u0026#34; } }, \u0026#34;highlight\u0026#34;: { \u0026#34;number_of_fragments\u0026#34;: 0, \u0026#34;pre_tags\u0026#34;: [ \u0026#34;\u0026lt;b\u0026gt;\u0026#34; ], \u0026#34;post_tags\u0026#34;: [ \u0026#34;\u0026lt;/b\u0026gt;\u0026#34; ], \u0026#34;fields\u0026#34;: { \u0026#34;artist.name\u0026#34;: {}, \u0026#34;name\u0026#34;: {}, \u0026#34;year.string\u0026#34;: {} } } }\u0026#39; Voici un extrait du résultat retourné par Elasticsearch:\n{ \u0026#34;took\u0026#34; : 13, \u0026#34;timed_out\u0026#34; : false, \u0026#34;_shards\u0026#34; : { \u0026#34;total\u0026#34; : 1, \u0026#34;successful\u0026#34; : 1, \u0026#34;failed\u0026#34; : 0 }, \u0026#34;hits\u0026#34; : { \u0026#34;total\u0026#34; : 22, \u0026#34;max_score\u0026#34; : 9.11103, \u0026#34;hits\u0026#34; : [ { \u0026#34;_index\u0026#34; : \u0026#34;musicalbum\u0026#34;, \u0026#34;_type\u0026#34; : \u0026#34;album\u0026#34;, \u0026#34;_id\u0026#34; : \u0026#34;c6b36664-7e60-3b3e-a24d-d096c67a11e9\u0026#34;, \u0026#34;_score\u0026#34; : 9.11103, \u0026#34;fields\u0026#34; : { \u0026#34;id\u0026#34; : \u0026#34;c6b36664-7e60-3b3e-a24d-d096c67a11e9\u0026#34;, \u0026#34;artist.name\u0026#34; : \u0026#34;U2\u0026#34;, \u0026#34;name\u0026#34; : \u0026#34;War\u0026#34; }, \u0026#34;highlight\u0026#34; : { \u0026#34;artist.name\u0026#34; : [ \u0026#34;U2\u0026#34; ] } }, … La recherche fulltext utilise quant à elle le type de recherche f uzzy_like_this permettant une recherche approximative sur le nom de l’album, le nom de l’artiste et la date de sortie de l’album. Trois facettes de types différents permettent d’afficher la répartition du nombre de résultats en fonction du type d’artiste ( terms facet), des appréciations ( histogram facet) et de la décennie ( range facet).\nLe gist 7436893 présente la commande curl équivalente :\ncurl -XPOST \u0026#39;http://es.javaetmoi.com/musicalbum/album/_search?pretty\u0026#39; -d \u0026#39; { \u0026#34;from\u0026#34;: 0, \u0026#34;size\u0026#34;: 10, \u0026#34;query\u0026#34;: { \u0026#34;bool\u0026#34;: { \u0026#34;must\u0026#34;: [ { \u0026#34;fuzzy_like_this\u0026#34;: { \u0026#34;fields\u0026#34;: [ \u0026#34;name\u0026#34;, \u0026#34;artist.name\u0026#34;, \u0026#34;year.string\u0026#34; ], \u0026#34;like_text\u0026#34;: \u0026#34;U2 war\u0026#34;, \u0026#34;min_similarity\u0026#34;: 0.7, \u0026#34;prefix_length\u0026#34;: 1 } } ] } }, \u0026#34;facets\u0026#34;: { \u0026#34;artist_type\u0026#34;: { \u0026#34;terms\u0026#34;: { \u0026#34;field\u0026#34;: \u0026#34;artist.type_id\u0026#34; } }, \u0026#34;album_rating\u0026#34;: { \u0026#34;histogram\u0026#34;: { \u0026#34;key_field\u0026#34;: \u0026#34;rating.score\u0026#34;, \u0026#34;interval\u0026#34;: 20 } }, \u0026#34;album_year\u0026#34;: { \u0026#34;range\u0026#34;: { \u0026#34;field\u0026#34;: \u0026#34;year\u0026#34;, \u0026#34;ranges\u0026#34;: [ { \u0026#34;to\u0026#34;: 1970}, { \u0026#34;from\u0026#34;: 1970, \u0026#34;to\u0026#34;: 1980}, { \u0026#34;from\u0026#34;: 1980, \u0026#34;to\u0026#34;: 1990}, { \u0026#34;from\u0026#34;: 1990, \u0026#34;to\u0026#34;: 2000}, { \u0026#34;from\u0026#34;: 2000, \u0026#34;to\u0026#34;: 2010}, { \u0026#34;from\u0026#34;: 2010 } ] } } } }\u0026#39; Voici un extrait du résultat retourné par Elasticsearch :\n{ \u0026#34;took\u0026#34; : 57, \u0026#34;timed_out\u0026#34; : false, \u0026#34;_shards\u0026#34; : { \u0026#34;total\u0026#34; : 1, \u0026#34;successful\u0026#34; : 1, \u0026#34;failed\u0026#34; : 0 }, \u0026#34;hits\u0026#34; : { \u0026#34;total\u0026#34; : 539, \u0026#34;max_score\u0026#34; : 5.985128, \u0026#34;hits\u0026#34; : [ { \u0026#34;_index\u0026#34; : \u0026#34;musicalbum\u0026#34;, \u0026#34;_type\u0026#34; : \u0026#34;album\u0026#34;, \u0026#34;_id\u0026#34; : \u0026#34;c6b36664-7e60-3b3e-a24d-d096c67a11e9\u0026#34;, \u0026#34;_score\u0026#34; : 5.985128, \u0026#34;_source\u0026#34; : {\u0026#34;id\u0026#34;:\u0026#34;c6b36664-7e60-3b3e-a24d-d096c67a11e9\u0026#34;,\u0026#34;name\u0026#34;:\u0026#34;War\u0026#34;,\u0026#34;year\u0026#34;:1983,\u0026#34;tags\u0026#34;:[\u0026#34;rock\u0026#34;,\u0026#34;album rock\u0026#34;,\u0026#34;alternative pop/rock\u0026#34;,\u0026#34;classic pop and rock\u0026#34;,\u0026#34;pop/rock\u0026#34;],\u0026#34;rating\u0026#34;:{\u0026#34;score\u0026#34;:79,\u0026#34;count\u0026#34;:9},\u0026#34;artist\u0026#34;:{\u0026#34;name\u0026#34;:\u0026#34;U2\u0026#34;,\u0026#34;id\u0026#34;:\u0026#34;a3cb23fc-acd3-4ce0-8f36-1e5aa6a18432\u0026#34;,\u0026#34;type_id\u0026#34;:2,\u0026#34;type_name\u0026#34;:\u0026#34;Group\u0026#34;,\u0026#34;begin_date_year\u0026#34;:\u0026#34;1976\u0026#34;,\u0026#34;country_name\u0026#34;:\u0026#34;Ireland\u0026#34;,\u0026#34;rating\u0026#34;:{\u0026#34;score\u0026#34;:87,\u0026#34;count\u0026#34;:21}}} }, ... ] }, \u0026#34;facets\u0026#34; : { \u0026#34;artist_type\u0026#34; : { \u0026#34;_type\u0026#34; : \u0026#34;terms\u0026#34;, \u0026#34;missing\u0026#34; : 6, \u0026#34;total\u0026#34; : 533, \u0026#34;other\u0026#34; : 0, \u0026#34;terms\u0026#34; : [ { \u0026#34;term\u0026#34; : 2, \u0026#34;count\u0026#34; : 407 }, { \u0026#34;term\u0026#34; : 1, \u0026#34;count\u0026#34; : 120 }, { \u0026#34;term\u0026#34; : 3, \u0026#34;count\u0026#34; : 6 } ] },... ] } } } Dans le Cloud avec OpenShift A la recherche d’un hébergeur me permettant d’installer mon index en ligne, je suis tombé sur le billet Searching with ElasticSearch on OpenShift de Marek Jelen, évangéliste OpenShift. C’était l’occasion de découvrir l’offre Cloud de RedHat, et cela sans sortir ma carte bancaire. En effet, OpenShift offre 3 Gems limitées à 512 Mo de RAM et de 1 Go d’espace disque. Avec un index de 160 Mo, c’était amplement suffisant.\nLes explications du billet sont claires. Parti du cartouche Do-It-Yourself 0.1 contenant une simple distribution Linux, l’installation d’Elasticsearch se fait classiquement. Des variables systèmes prédéfinies doivent être utilisées pour spécifier l’adresse IP (OPENSHIFT_DIY_IP), le port HTTP (OPENSHIFT_DIY_PORT) et le répertoire d’installation (OPENSHIFT_DATA_DIR).\nSi vous le souhaitez, l’installation des plugins eshead et bigdesk est possible.\nAfin de résoudre l’exception BindException[Address already in use] au démarrage d’Elasticsearch, j’ai suivi les préconisations postées dans un commentaire par ewindsor.\nUne fois Elasticsearch démarré, seul le port HTTP est accessible depuis Internet. C’est le port utilisé par l’IHM de recherche. Le port utilisé par le client TCP Elastisearch n’est quant à lui pas accessible. Le Batch d’indexation s’exécutant en local ne peut donc pas alimenter directement le cluster Elasticsearch. Par facilité, je me suis contenté d’uploader par SFTP mon index local (répertoire data\\musicbrainz) sur le serveur OpenShift.\nUn redémarrage d’Elasticsearch et l’index est visible via Eshead :\nLe plugin Jetty pour Elasticsearch et le cartouche Nginx pour OpenShift permettent de sécuriser l’accès au serveur Elasticsearch, rendant possible la configuration d’un reverse proxy avec authentification basic HTTP.\nPour terminer, OpenShift permet d’associer un nom de domaine à une Gem. Ainsi, le nom de domaine es.javaetmoi.com pointe sur le serveur Nginx.\nL’application Angular http://musicsearch.javaetmoi.com/ fait appel à l’API REST d’Angular exposée sur l’URL http://es.javaetmoi.com/musicalbum/album/_search\nConclusion Ce billet nous aura permis d’aborder de bout en bout la mise en ligne d’une application basée sur Elasticsearch : de l’indexation des données par batch à leur consultation dans votre navigateur.\nEn moins d’une heure, l’index Elasticsearch aura été mis en ligne sur OpenShift, le PaaS / IaaS de Redhat. La disponibilité d’un cartdrige OpenShift pour Elasticsearch permettrait d’accélérer son déploiement. A noter que mon cluster Elasticsearch n’est formé que d’un seul nœud. Je n’ai pas vérifié s’il était possible d’installer un cluster Elasticsearch sur plusieurs serveurs.\nVous l’aurez remarqué, l’index musicalbum créé par le batch est figé. Pour aller plus loin, il aurait été intéressant d’automatiser sa mise à jour régulière. La base de données Musicbrainz est capable de se synchroniser toutes les heures avec la base principale. Il serait donc possible de reconstruire périodiquement l’index en utilisant, par exemple, un mécanisme d’alias pour ne pas interrompre le service de recherche. La base répliquée et le batch aurait pu être installés sur une 3ième Gem OpenShift. Resterait alors à régler la communication entre le batch et le serveur Elasticsearch. RedHat a dû prévoir la possibilité d’ouvrir un port entre 2 Gems. Dans le cas contraire, un client Java utilisant l’API REST d’indexation permettrait de contourner le blocage du port utilisé pour la communication TCP d’Elasticsearch.\n","link":"https://javaetmoi.com/2013/11/musicbrainz-elasticsearch-angularjs-openshift/","section":"posts","tags":["angularjs","cloud","dbsetup","elasticsearch","spring-batch"],"title":"Elastifiez la base MusicBrainz sur OpenShift"},{"body":"","link":"https://javaetmoi.com/tags/database/","section":"tags","tags":null,"title":"Database"},{"body":" Lors du développement de tests d’intégration, j’ai récemment eu besoin de charger une base de données à l’aide de jeux de données. Pour écrire mon premier test, j’ai simplement commencé par écrire un fichier SQL. En un appel de méthode (JdbcTestUtils::executeSqlScript) ou une ligne de déclaration XML (\u0026lt;jdbc:script location=\u0026quot;\u0026quot; /\u0026gt;), Spring m’aidait à charger mes données.\nPour tous ceux qui se sont déjà prêtés à l’exercice, maintenir des jeux de données est relativement fastidieux, qui plus en SQL. Cette solution n’était donc pas pérenne.\nDepuis une dizaine d’années, j’utilise régulièrement DbUnit pour tester la couche de persistance des applications Java sur lesquelles j’interviens, qu’elle soit développée avec JDBC, Hibernate ou bien encore JPA. Cette librairie open source est également très appréciable pour tester unitairement des procédures stockées manipulant des données par lot. Pour mon besoin, j’aurais donc pu naturellement me tourner vers cet outil qui a fait ses preuves et dont je suis familier.\nMais voilà, commençant à apprécier les avantages de la configuration en Java offerte par Spring et les APIs fluides des frameworks FestAssert ou ElasticSearch utilisés sur l’application, l’idée d’ écrire des jeux de données en Java me plaisait bien. Et justement, il y’a quelques temps, l’argumentaire de l’article Why use DbSetup? ne m’avait pas laissé indifférent. C’était donc l’occasion d’utiliser cette jeune librairie développée par les français de Ninja Squad et qui mérite de se faire connaitre, j’ai nommé DbSetup.\nLe guide utilisateur de DbSetup étant particulièrement bien conçu, l\u0026rsquo;objectif de cet article n’est pas de vous en faire une simple traduction, mais de vous donner envie de l’essayer et de vous présenter la manière dont je l’ai mis en oeuvre. Celle-ci s’éloigne en effet quelque peu de celle présentée dans la documentation, la faute à mes vieux réflexes d’utilisateur de DbUnit et au bienheureux rollback pattern de Spring.\nIntégration de DbSetup avec Spring Test Comme énoncé en introduction, le framework Spring et DbSetup peuvent fonctionner de concert, chacun ayant des rôles bien précis :\nSpring :\nDémarrage de la base de données embarquée H2 Création du schéma de la base de données de l’application par exécution d’un script SQL contenant des ordres DDL Gestion de la source de données et des transactions Assertions facilitées par l’exécution de requêtes SQL via JdbcTemplate Db Setup\nInsertion des données de test dans les tables de la base de données Remarque : dans mon contexte projet, chaque méthode de test a besoin de son propre jeu de données. Les données insérées ou modifiées par la précédente méthode testée doivent donc être purgées. Par choix, ce ménage n’est pas assuré par la classe DbSetupTracker proposée par DbSetup, mais par le support transactionnel offert par Spring Test. Une transaction base de données est ouverte par Spring avant l’appel de la méthode de test puis est annulée une fois la fin de la méthode atteinte. Par contre, les données sont insérées par DbSetup au début de la méthode de test. Parce qu’il est parfois utile de pouvoir consulter l’état de la base après un test en échec, l’annotation @Rollback(false) peut être apposée temporairement sur la méthode incriminée afin que Spring valide la transaction.\nPour que Spring Test puisse charger le contexte applicatif et initier le contexte transactionnel, les 3 annotations suivantes décorent la classe de test :\n@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration @Transactional public class TestSpringDbSetup { … } Le code source complet de la classe TestSpringDbSetup.java peut être consulté sur GitHub .\nLa configuration du contexte Spring est déclarée en java, dans une nested class du test. A noter que cette classe de configuration pourrait être externalisée afin d’être utilisée par toutes les classes de tests unitaires. Y sont déclarés : une source de données, un gestionnaire de transaction, une instance de JdbcTemplate pouvant être utilisées par les assertions et une Destination DbSetup.\n@Configuration static class Config { @Bean public DataSource dataSource() { return new EmbeddedDatabaseBuilder() .setType(EmbeddedDatabaseType.H2) .addScript(\u0026#34;schema.sql\u0026#34;) .build(); } @Bean public PlatformTransactionManager transactionManager() { return new DataSourceTransactionManager(dataSource()); } @Bean public JdbcTemplate jdbcTemplate() { return new JdbcTemplate(dataSource()); } @Bean Destination destination() { return new TransactionAwareDestination(dataSource(), transactionManager()); } } L’interface Destination fournie par DbSetup permet à ce dernier d’accéder à une connexion JDBC. De base, DbSetup vient avec 2 implémentations : l’une pour récupérer une connexion depuis une DataSource et une autre pour la récupérer directement depuis le DriverManager.\nNotre exemple utilise l’implémentation spécifique TransactionAwareDestination dont le code source est disponible sous forme de Gist. Basé sur le proxy TransactionAwareDataSourceProxy proposé par Spring, cette destination permet à DbSetup d’utiliser le contexte transactionnel géré par Sring pour récupérer et clôturer une connexion, commiter ou bien rollbacker. La classe TransactionAwareDestination apporte une amélioration reportée dans la Jira SPR-6441.\nAfin de pouvoir être utilisés dans les méthodes de test, les beans JdbcTemplate et Destination sont injectés en tant que propriétés de la classe TestSpringDbSetup :\n@Autowired private JdbcTemplate jdbcTemplate; @Autowired private Destination destination; Du plus simple effet, le script schema.sql contient la création d’une table :\ncreate table customer(id number primary key, name varchar not null); La configuration de notre classe de test est terminée. Mis à part une référence sur l’interface Destination, le code ne fait pour l’instant aucun usage de DbSetup. C’est précisément l’objectif du paragraphe suivant.\nUtilisation de DbSetup Au travers de 2 méthodes de tests, nous allons voir quelles sont les facilitées proposées par DbSetup pour écrire des jeux de données. Attention, les scénarios de test proposés n’ont aucune valeur métier et permettent uniquement d’illustrer cet article.\nNotre première méthode de test insère en base de données 4 clients dont les prénoms ont une racine commune ou une casse différente :\n@Test public void indexCustomerWithSimilarName() throws SQLException { // Prepare Operation operation = insertInto(\u0026#34;CUSTOMER\u0026#34;) .withGeneratedValue(\u0026#34;ID\u0026#34;, ValueGenerators.sequence()) .columns(\u0026#34;NAME\u0026#34;) .values(\u0026#34;Antoine\u0026#34;) .values(\u0026#34;ANTOINE\u0026#34;) .values(\u0026#34;Antoinette\u0026#34;) .values(\u0026#34;Pierre-Antoine\u0026#34;) .build(); new DbSetup(destination, operation).launch(); // Execute test // ... // Assertions assertEquals(4, jdbcTemplate.queryForObject(\u0026#34;select count(*) from customer\u0026#34;, Integer.class); assertEquals(4, count); } Sans même connaitre l\u0026rsquo;API de DbSetup, la premier constat est que le code est lisible et reste donc maintenable par tout développeur Java.\nL’identifiant de chaque client n’ayant pas d’importance dans notre scénario de test, un générateur de valeurs séquentielles est utilisé à l’aide de la classe utilitaire ValueGenerators. Cette possibilité offerte par DbSetup est fort appréciable dans le cas où des colonnes non null doivent être valorisées alors que leurs valeurs n’ont pas d’impact sur le test.\nSi elle avait été plus longue, la liste de noms à insérer aurait pu être construite à partir d’une boucle parcourant une liste de valeurs. Merci Java.\nUne fois la méthode de test terminé, Spring Test rollback l’insertion de ces 4 clients.\nNotre seconde méthode de test utilise 2 autres fonctionnalités offertes par DbSetup et qui font défaut à DbUnit :\nprivate static final int CUSTOMER_1 = 1; @Test public void indexSingleCustomer() { Operation operation = insertInto(\u0026#34;CUSTOMER\u0026#34;) .columns(\u0026#34;ID\u0026#34;, \u0026#34;NAME\u0026#34;) .values(CUSTOMER_1, \u0026#34;James\u0026#34;) .build(); new DbSetup(destination, operation).launch(); int id = jdbcTemplate.queryForObject( \u0026#34;select id from customer where id=?\u0026#34;, Integer.class, CUSTOMER_1); assertEquals(CUSTOMER_1, id) ; } En effet, là où les fichiers XML de DbUnit sont limités à de simples chaînes de caractères, DbSetup permet d’ utiliser des valeurs typées pour écrire des jeux de données. Dans l’exemple précédent, un int est utilisé pour renseigner la valeur de la colonne \u0026ldquo;ID\u0026rdquo;. DbSetup supporte nativement de nombreux types Java. Le framework de test se charge de binder les types Java en type SQL (java.sql.Types). Il laisse néanmoins la possibilité d’étendre cette liste en implémentant l’interface BinderConfiguration et/ou en créant ses propres Binder.\nDéclarer ses jeux de données en Java permet d’ utiliser des constantes, ici CUSTOMER_1. La même constante peut être réutilisée pour plusieurs jeux de données, utilisée lors des assertions ou lors de l’exécution du test. Le code en devient plus lisible. Les constantes sont particulièrement intéressantes avec les clés étrangères.\nCes 2 exemples ne mettent en pratique qu’une partie des fonctionnalités de DbSetup. Valeurs par défaut, exécution de requêtes SQL, chainage d’opération en sont d’autres.\nAvantages et inconvénients Après avoir utilisé DbSetup pendant quelques semaines, j’en suis particulièrement satisfait. Voici les avantages et inconvénients que j’en retire :\nAvantages****Faiblesses\nUn DbUnit like dopé aux bonnes pratiques contemporaines\nIntégration possible à Spring Test\nPrise en main rapide\nFactorisation des jeux de données rendue possible\nQualité de la documentation, de la javadoc et du code source\nDisponible sur le repo maven central\nLégèreté, aucune dépendance tierce\nEquipe ouverte aux améliorations\nFonctionnalité restreinte au chargement\nAucun log pour le debug\nQui de la pérennité ?\nPour les intéressés qui souhaiteraient migrer de DbUnit vers DbSetup, voici les fonctionnalités proposées par DbUnit mais non couvertes par DbSetup :\nChargement de jeux de données depuis les formats XML et CSV (à nuancer car DbSetup propose une alternative en Java, ce qui en fait son principal intérêt) Création d’une XSD à partir du schéma de la base de données pour une validation stricte des jeux de données Export de données depuis une base de données source Assertion des données présentes en base après l’exécution d’un test Récupération du graphe de dépendances des tables (TablesDependencyHelper) Conclusion Retrouver du plaisir dans l’écriture de jeux de données n’était pas gagné d’avance. DbSetup remporte haut la main ce challenge. Est-ce l’attrait pour la nouveauté ? Il est trop tôt pour me prononcer.\nSi vous souhaitez contribuer à ce projet open source, ses auteurs sont particulièrement ouverts à toute suggestion. Les 2 tickets que j’ai ouverts sur Github ont été traités dans les heures qui suivent, et de manière très professionnelle.\nPour terminer, je me demande s’il ne serait pas possible d’étendre DbUnit afin lui apporter les facilités d’écriture de DbSetup. A quand une implémentation JavaDataSet de l’interface IDataSet?\n","link":"https://javaetmoi.com/2013/09/dbsetup-spring-test-vs-dbunit/","section":"posts","tags":["database","dbsetup","dbunit","spring-framework","test"],"title":"DbSetup, une alternative à DbUnit"},{"body":" Dans le cycle de vie d’une application, il arrive parfois qu’ une branche prenne le pas sur une autre branche et qu’il soit nécessaire d’écraser la seconde par la première. Prenons l’exemple d’une application où, par convention, le master (ou le trunk sous SVN) est considéré comme la branche de développement (axée vers le futur) et que l’utilisation du système de branches soit habituellement consacrée aux branches de maintenance. Dans certaines circonstances (ex : nouveaux développements à commencer pour la version N+2, migration technique à réaliser …), une branche peut prendre le dessus du master. Afin de retrouver la convention d’origine, une recopie de la branche sur le master va, à termes, être nécessaire. Que ce soit avec Git ou git-svn, nous allons voir comment Git peut nous y aider en quelques lignes de commande.\nMise en scène L’ historique de commits ci-dessous illustre les explications qui suivront : Cet historique des commits commence par la branche master sur laquelle les fonctionnalités A et B ont été commitées. La branche maBranche est alors créée à partir du commit de la fonctionnalité B. Un premier merge no fast-forward est créé pour récupérer la fonctionnalité E de master dans maBranche : le commit de merge « Merge branch ‘master’ into maBranche » est créé.\nA partir de là, les nouvelles fonctionnalités F, G et H sont développées sur maBranche. Ne pouvant attendre la fin des développements de la fonctionnalité G qui résoudrait proprement le problème rencontré par les utilisateurs, est créé sur le master un contournement permettant d’y palier temporairement. Ce Hotfix est déployé en production. N’ayant aucun intérêt dans les prochaines versions de l’application, ce Hotfix ne doit être en aucun cas mergé sur maBranche. Une fois les développements de la fonctionnalité H terminés et déployés en production, l’équipe décide de faire revenir sur le master les développements de maBranche. Les 2 derniers commits de l’historique mettent en œuvre cette opération.\nCommandes Voici les 4 lignes de commandes à exécuter pour réaliser cet écrasement de branche :\nSe placer sur la branche à conserver git checkout maBranche Demander une fusion avec la stratégie ours (attention, ceci est différent d\u0026rsquo;un merge avec stratégie recursive et l’ option ours) qui va uniquement conserver le contenu de la branche actuelle. En effet, l\u0026rsquo;option -s ours indique à Git de fusionner la branche source dans la branche cible mais sans aucunement modifier la branche cible. Cette stratégie est habituellement utilisée pour ne pas reporter un commit d’une branche de maintenance sur le master. git merge -s ours master -m \u0026ldquo;Merge avec stratégie ours\u0026rdquo; Repasser sur la branche master git checkout master Demander une fusion de la branche maBranche vers le master tout en conservant 2 branches distinctes (un fast-forward aurait été réalisé puisque que le dernier commit n’est autre que notre fusion de la commande n°2). git merge \u0026ndash;no-ff maBranche Sortie console sur notre exemple de la commande Git n°4 : Comme attendu, le fichier Hotfix.txt ayant été ajouté lors du commit Hotfix est supprimé du master. Si, dans un autre exemple, le commit Hotfix avait modifié une ligne du fichier FeatureE.txt, un revert de cette modification aurait alors été effectué.\nConclusion Cet article explique comment Git permet d’écraser le contenu de la branche A par une branche B lorsque la branche B a pris le pas sur la branche A et qu’aucune des modifications présente dans le branche A n\u0026rsquo;a besoin d\u0026rsquo;être conservée (tout a été préalablement fusionné, reporté par cherry-pick, copié à la main ou volontairement ignoré car à ne pas reporter). Cerise sur le gâteau : ces opérations sont applicables entre 2 branches SVN par le biais du bridge git-svn.\n","link":"https://javaetmoi.com/2013/08/ecraser-une-branche-par-une-autre-avec-git/","section":"posts","tags":["git"],"title":"Ecraser une branche par une autre avec Git"},{"body":" A Devoxx France, lorsqu’Axel Fontaine vante les mérites du Continuous Delivery et que Ludovic Cinquin affirme que Facebook est mis en production 2 fois par jour, avouez que cela a de quoi vous laisser rêveur ? En attendant de travailler un jour dans des structures ayant compris que des livraisons fréquentes et automatisées permettent de gagner en fiabilité, en agilité et de développer leur business, vous n’avez d’autres choix que de vous approprier les processus établis où vous intervenez et de les améliorer à votre niveau.\nDans les grands comptes où je suis intervenu, la mouvance Devops prônant de tels processus n’avait pas encore percé. Quelques outils sont bien mis en place. Mais pour autant, MOE et exploitation sont 2 équipes bien distinctes. L’exploitation peut elle-même être scindée en 2 : production et intégration (hors-prod). C’est précisément dans ce contexte que s’inscrit cet article. Il montre comment utiliser un serveur d’intégration continue pour releaser puis déployer une application sur les environnements autorisés.\nCONTEXTE DE MISE EN ŒUVRE Prenons un exemple concret qui servira de trame à ce billet. Supposons que l’exploitation met à disposition de la MOE des scripts shell permettant de livrer leur application sur le serveur d’intégration et de préparer un package qui sera utilisé lors des livraisons sur les environnements suivants. Le package de livraison comprend :\nLe ou les binaires de l’application : EAR, WAR ou même JAR pour les batchs La configuration applicative par environnement Ces fichiers constituent les livrables. On pourrait imaginer y ajouter des scripts SQL de mise à jour du schéma de la base et/ou des données.\nLes scripts shell permettent de copier les binaires et la configuration sur le serveur d’application, de redémarrer le serveur d’application puis de contrôler l’état de l’application une fois celle-ci démarrée. Au format XML ou .properties, les fichiers de configuration sont déposés dans le classpath du serveur afin d’être accessibles par l’application.\nDans notre exemple, la MOE a la main mise sur l’environnement d’intégration. Elle peut y déployer son application à volonté. A tout moment (ex : à la fin d’une itération ou d’un sprint), elle peut décider de créer le package qui permettra à la MOA de recetter l’application. Une fois déposé dans le SAS de recette, le package de livraison est déployé sur l’environnement de recette, et cela automatiquement 2 fois par jour. Leur installation en pré-production et en production est réalisée à la demande de la MOE par les équipes d’intégration puis de production.\nAu quotidien, les développeurs Java utilisent Jenkins comme plateforme d’intégration continue. Ils l’utilisent pour récupérer le code depuis SVN, le compiler, exécuter les tests unitaires, lancer l’analyse qualimétrique ou bien encore dérouler les tests fonctionnels. Est configuré un job Jenkins de build par branche de l’application.\nLes livraisons en intégration sont réalisées manuellement par commandes SFTP et SSH. Les binaires sont récupérés du repository maven d’Entreprise Nexus. Au préalable, le plugin release pour maven leur aura permis de construire les livrables et de le publier dans Nexus via la commande suivante : mvn perform:prepare perform:release\nVoyons à présent comment Jenkins va permettre d’améliorer ce process et de fluidifier les livraisons.\nRELEASE DEPUIS JENKINS Afin de garantir le périmètre d’une version d’un livrable et la répétabilité de sa construction, le processus de release est encouragé dès lors qu’un livrable est susceptible de pouvoir être déployé jusqu’en production. Ce processus vérifie entre autre que l’application ne dépend que d’artefacts immuables, dont la version est figée. Simple mais fastidieuse, gourmande en ressources matérielles, cette action peut être facilitée par le plugin M2 Release de Jenkins.\nSon installation ne pose aucune difficulté. Un compte générique permettant d’accéder au SCM de l’entreprise peut y être configuré afin que tous les projets puissent par la suite en bénéficier. Le plugin maven release utilise ce compte pour récupérer le code source, commiter des changements de version dans les pom.xml et créer un tag.\nPour qu’un plan de build puisse être releasé depuis Jenkins, la case Maven release build doit être cochée : Au besoin, les commandes maven peuvent être complétées avec un goal ou un profil maven particulier, pour par exemple publier les sources et la javadoc dans le repo maven d’entreprise ou bien encore exécuter des tests d’intégration en complément des tests unitaires.\nA la suite d’un build, Jenkins offre la possibilité d’ archiver les artefacts construits et qui se trouvent encore dans son espace de travail. Cette fonctionnalité est particulièrement utile lors d’une release. Les livrables peuvent ainsi être archivés et rattachés au build pour un usage ultérieur :\nLe pattern de sélection des fichiers à archiver est basé sur la syntaxe ant (ex : **/*.ear pour archiver tous les fichiers EAR du workspace). Dans l’exemple précédent, l’EAR de l’application web, l’archive zip contenant les binaires du batch de l’application ainsi que tous les fichiers de configuration (.xml, .properties …) sont archivés à chaque fin de build.\nUne fois configuré, le processus de release se fait à la demande sur simple clic. Cette action peut être réalisée par le chef de projet ou le responsable de l’intégration, une fois que ce dernier a vérifié que toutes les fiches Jira de la version à releaser sont clôturées et que le code est commité dans SVN.\nVoici la procédure à suivre pour déclencher manuellement la release :\nSélectionner le build Jenkins correspondant à la branche de la version de l’application à releaser (exemple : MyBank-2.0.x ou MyBank-trunk) Cliquer sur le menu Peform Maven Release Renseigner le formulaire de release en suivant les règles de versionning définies sur votre application Cliquer sur le bouton Schedule Maven Release Build Le plugin release de maven prend la main et effectue les tâches suivantes : Création du tag de la release dans SVN Déploiement dans Nexus les artefacts construits (EAR) Commits SVN mettant à jour la version des pom.xml En cas de succès, dans l’historique des builds, l’icône doit apparaître à côté du build releasé. La version de la release (ex : 3.0.5) est accessible en tant qu’info-bulle : Une sélection du build permet de consulter les livrables archivés Cette version être prête à être déployée sur un serveur d’application DÉPLOIEMENT DES BUILDS PAR PROMOTION Ainsi, la version de l’application web releasée précédemment est désormais prête à être déployée sur l’ environnement d’intégration pour des tests plus poussés côté MOE. Une fois le procès-verbal (PV) d’intégration validé, cette version pourra être ensuite déployée sur l’environnement de recette afin que la MOA lance sa campagne de tests d’acceptation. Le PV de recette signé, cette même version sera prête à être livrée en production. Ce workflow décrit le scénario le plus optimiste dans lequel la première version de l’application ne contient aucune anomalie. En pratique, plusieurs allers retours avec l’équipe de développement auront très certainement lieu, ce qui entraînera plusieurs cycles de release et de re-livraison de l’application. Voici un scénario déjà plus réaliste :\nFin des développements de la V2 de l’application à release version 2.0.0 à déploiement en intégration à bugs remontés par la MOE Corrections à Release version 2.0.1 à déploiement en intégration à PV d’intégration à déploiement en recette à bugs remontés par la MOA Corrections à Release version 2.0.2 à déploiement en intégration à déploiement en recette à PV de recette fonctionnelle à déploiement en pré-production à PV de recette technique à déploiement en production La promotion d’une version de l’application vers un environnement en amont de la chaîne porte un nom : la promotion de builds. Et au même titre que la gestion des release, le serveur d’intégration continue Jenkins permet de la piloter. Pour se faire, l’installation du plugin Promoted Builds est pré-requise. Le build de l’application doit ensuite être configuré en sélectionnant autant de fois la case Promote builds when que de promotions seront possibles. Dans notre exemple, la promotion vers l’environnement d’intégration sera appelée « Deploy Integration » et la promotion vers l’environnement de recette sera nommée « Deploy UAT ». Afin de pouvoir les différencier visuellement, une icône de couleur différente peut être associée à chacune des promotions (exemple : pour l’intégration et pour la recette). Jenkins est paramétré pour que la promotion soit activée manuellement par l’utilisateur. Une alternative aurait par exemple pu consister à promouvoir automatiquement en intégration la version d’une application suite au succès de ses build nocturnes des tests (ex : Fitness, Selenium).\nLorsqu’un build est promu, des actions peuvent être déclenchées séquentiellement. Plusieurs actions sont mises à disposition nativement par Jenkins. Des plugins permettent de les enrichir. Voici quelques exemples d’actions possibles :\nExécuter une ligne de commande batch Windows ou Linux Envoyer une notification par mail ou par XMPP Créer une fiche Jira Lancer une commande maven Copier des artefacts sur un serveur via SSH Supprimer le workspace Jenkins courant Dans notre contexte, la copie par SSH des artefacts sur le serveur de déploiement de l’entreprise nous intéresse particulièrement. Apportée par le plugin Publish Over SSH, cette action permet en complément d’exécuter une série de commandes sur le serveur distant une fois les artefacts copiés. La figure suivante illustre la configuration de la recopie d’un EAR dans un répertoire dédié du serveur de déploiement puis l’exécution de 3 commandes permettant d’installer l’EAR, d’arrêter le serveur d’application puis de le redémarrer. En amont de cette action, le transfert de la configuration peut être configuré de la même manière. Dans la configuration proposée, l’EAR et les fichiers de configuration sont récupérés depuis l’archive des artefacts du build promu. Une alternative consisterait à récupérer le numéro de version de la release puis à utiliser l’ API REST du serveur Nexus et la commande wget pour récupérer le livrable dont les groupId, artefactId et packaging sont connus. Cette configuration pourrait s’appuyer sur le plugin Jenkins Remote SSH.\nUne fois configuré, il est possible de promouvoir un build et de déployer l’application sur l’environnement de son choix, qu’il s’agisse du build d’une version SNAPSHOT ou d’une RELEASE. D’après l’historique des builds ci-dessous, le build #2451 correspond à une version releasée de l’application qui a été déployée en intégration. Le build #2455 correspond à une version releasée qui a été déployée en intégration et en recette.\nParticulièrement souple, la promotion de build permet de livrer sur le même environnement des versions d’applications hébergées sur des branches différentes (trunk/master vs branche de maintenance). Elle permet également de livrer en un clic une version précédente pour, par exemple, vérifier si l’anomalie constatée est ou non une régression.\nCONCLUSION Cet article nous aura permis de découvrir une utilisation moins courante d’un serveur d’intégration continue. Ne se limitant plus à l’étape de développements et de tests, Jenkins permet de gérer le cycle de vie des releases d’une application ainsi que leur déploiement.\nLes possibilités offertes par Jenkins sont variées. Dans un autre contexte, il pourrait tout à fait être envisagé de substituer la promotion manuelle par une promotion automatiquement déclenchée par un autre build ou par un commit dans votre SCM (Git, SVN). Nous aurions très bien pu également découper notre build monolithique en plusieurs builds spécialisés : job de build, job de packaging, job de tests et job de déploiement. Cette chaîne de builds est rendue possible par la configuration d’un pipeline de jobs.\nEnfin, le déploiement sur le serveur d’applications fut assuré ici par de simples scripts shell pilotant les scripts natifs du serveur d’application. Lorsque l’entreprise dispose d’un outil dédié à cette tâche tel Serena Release Manager, l’utilisation du plugin Jenkins Serena Deploy permet de pousser l’EAR et la configuration dans le repository SRA. Sur le même principe, tout fournisseur de PAAS dispose généralement d’un plugin Jenkins permettant de déployer une application dans le Cloud (CloudBees, OpenShift, Heroku, Cloud Foundry…).\n","link":"https://javaetmoi.com/2013/07/jenkins-release-deploy-plugins/","section":"posts","tags":["devops","jenkins","maven","plugin"],"title":"Orchestrez vos déploiements avec Jenkins"},{"body":"De nos jours, l’utilisation d’un serveur d’ intégration continue pour déployer son application puis exécuter ses tests Selenium s’est relativement démocratisée. Néanmoins, l’investissement réalisé pour l’écriture de ces tests peut rapidement être mis à mal par le coût associé à leur maintenance. En effet, les tests d’IHM sont de nature plus instables que de simples tests unitaires. Outre des problématiques de rendu et de transversalité des fonctionnalités testées, l’une des principales difficultés réside dans la répétabilité des tests. Les données de test y jouent pour beaucoup. Cette difficulté est décuplée lorsque votre application repose sur une architecture SOA dont les services SOAP, XML ou bien REST sont hébergés par des tiers : vous n’avez aucune maitrise sur les données de l’environnement testé, ni sur sa stabilité. Des tests qui échouent régulièrement à cause de données ayant été modifiées rendent laborieuse la détection de véritables régressions. Cet article propose une solution appliquée depuis 2 ans sur une application de taille modeste (35 000 LOC pour 20 écrans). Constitution d’un jeu de données\nLa 1ière idée permettant de résoudre ce type de problématique consiste à essayer d’échantillonner manuellement des données de test satisfaisant les critères de chaque test Selenium. Par exemple, pour exécuter le scénario fonctionnel permettant d’effectuer un avenant, est dressée une liste de contrat en cours. Mais que se passe-t-il lorsqu’un développeur ou qu’un autre test Selenium clôture l’un des contrats référencés ou qu’une recopie de la base de production supprime le contrat du système ? L’échantillonnage doit être recommencé.\nUne solution plus avancée consiste à échantillonner dynamiquement les données de test. Pour se faire, les applications tierces doivent soit mettre à disposition des services spécifiques de recherche, soit donner accès à leur base de données. Les premiers demandent des coûts de développement et de maintenance, ce qui les rend difficilement envisageables. La seconde nécessite d’appréhendez le modèle des différents systèmes tiers ou qu’un membre de l’équipe apporte son aide dans l’élaboration des requêtes SQ L nécessaires à la récupération des échantillons. Moins coûteuse, cette solution n’est pas toujours bien vue. Qui plus est, tout changement ultérieur du schéma risque de faire échouer les tests.\nL’indépendance est votre salut Pour rendre les tests exécutables à l’infini sans avoir à se préoccuper de l’état des partenaires (Back Offices, référentiels métiers, serveur SSO, éditique, GED …), la solution que nous avons mise en place consiste à bouchonner tous leurs appels. En pratique, nous nous sommes appuyés sur le composant Mock Switcher développé par mon illustre prédécesseur. Ce composant permet d’ enregistrer les réponses des partenaires en fonction des paramètres d’appel. Sauvegardés au format XML ou JSON, ces réponses peuvent être retouchées pour les besoins du test. Mais surtout, elles simuleront les réponses des partenaires lors de l’exécution des tests. Techniquement, le Mock Switcher repose sur Spring AOP et XStream/ Jackson. Il peut être assimilé à un système de cache applicatif persistant les données sur disque et utilisé à des fins de test. Initialement, le Mock Switcher avait été développé pour bouchonner temporairement des partenaires indisponibles et ne pas bloquer ainsi les développeurs. Son usage a ici été quelque peu détourné.\nSur le même principe, s’affranchir des éléments d’infrastructure permet de gagner en stabilité et de s’assurer davantage de la répétabilité des tests. De nos jours, cette virtualisation est possible à l’aide de bases de données embarquées (H2, HSQLDB), de providers JMS en mémoire (ActiveMQ) ou bien encore de bouchons de serveurs mails SMTP (Dumbster ou Wiser SubEtha SMTP) …\nAvec ces 2 approches combinées, l’application peut s’exécuter en autonomie complète, déconnectée de tout réseau.\nEn pratique, les tests sont exécutés par une commande maven qui construit l’application, la démarre avec un profil Spring standalone (utilisation de l’infrastructure de test et activation des mocks) puis exécute les tests Selenium. La base de données embarquée est initiée avec des données du référentiel. Si besoin est, des modifications spécifiques au scénario de tests peuvent être effectuées avec le démarrage d’un test. Le schéma suivant illustre l’application de ces différentes étapes : Cette typologie de test présente des avantages comme des inconvénients : Avantages****Inconvénients\nLes mêmes tests peuvent être exécutés en parallèle (par des développeurs et/ou builds différents)\nDiminution des faux négatifs\nExécution plus rapide des tests\nFeedback immédiat lors d’une migration technique (ex : montée de version de frameworks)\nMaitrise des jeux de données associés aux scénarios de tests\nLorsqu’un test échoue, une vérification du jeu de données est nécessaire\nMaintenance des bouchons XML / JSON\nMaintenance du script DDL et des jeux de données SQL\nDébogage nécessitant de démarrer l’application avec le profile standalone\nAdhérences non testées\nConclusion Plus rapide à exécuter que des tests réellement connectés, ces tests peuvent être déclenchés après chaque commit effectué par un développeur dans le gestionnaire de code source. Une pré-qualification de l’application est ainsi effectuée au plus tôt.\nComplémentaire aux tests unitaires et aux tests d’intégration automatisés, ces tests orientés IHM couvrent les principales couches de l’application : de la page HTML au code Java des contrôleurs, des services métiers et de la couche de persistance, en passant par le code JavaScript et les appels Ajax. Seule la partie échange est émulée. Pour combler ce vide, sur notre application, d’autres tests Selenium sans bouchon sont joués en complément par un build nocturne ; ils permettent de vérifier l’intégration de l’application avec ses partenaires et toutes ses adhérences. Pour ne pas retomber sur les problématiques de répétabilité évoquées en début d’article, ces tests essaient d’être le plus indépendants des données et couvrent ainsi d’autres fonctionnalités.\n","link":"https://javaetmoi.com/2013/06/selenium-robuste-car-autonome/","section":"posts","tags":["selenium","soa","spring-aop","test"],"title":"Rendez autonomes vos Selenium"},{"body":"","link":"https://javaetmoi.com/tags/soa/","section":"tags","tags":null,"title":"Soa"},{"body":"","link":"https://javaetmoi.com/tags/spring-aop/","section":"tags","tags":null,"title":"Spring-Aop"},{"body":"Au cours de la migration d’une cinquantaine d’applications web de Websphere vers JBoss 5.1 EAP, nous avons été confrontés à un problème de sécurité mis en évidence par l’infrastructure de pré-production : le firewall bloquait systématiquement toute requête comportant un jsessionid dans l’ URL. Modifier les règles du firewall pour laisser passer ce type de requêtes aurait introduit une faille de sécurité exploitable par appropriation de session web. Cette faille nous a d’ailleurs été révélée en parallèle par l’outil d’audit de sécurité IBM AppScan. Ce billet rappelle l’origine du problème et précise quelle solution a été employée pour le résoudre le plus rapidement possible.\nDescription du problème Les requêtes HTTP bloquées par le firewall à l’entrée de la DMZ ont le format suivant :\nGET http://www.monapli.com/index.faces;jsessionid=4A3D4C11876FC7B22F98B0E14287BC8E.monappliServer01 Ces requêtes sont renvoyées par le navigateur lors de la première tentative d’accès d’un utilisateur à l’une des pages d’une application web, à savoir lorsque qu’aucune session web n’était encore créée.\nUne analyse plus fine montre que ces requêtes sont émises par le navigateur suite à une demande de redirection 302 effectuée par le framework de sécurité mis en œuvre côté applicatif. La requête initiale ayant déclenchée la redirection a la forme suivante :\nPOST http://www.monapli.com/index.faces + token d’authentification à usage unique Et voici un exemple de réponse du serveur web :\nHTTP/1.1 302 Found Date: Wed, 22 May 2013 19:32:24 GMT X-Powered-By: Servlet 2.5; JBoss-5.0/JBossWeb-2.1 Location: http://www.monappli.com/ index.faces;jsessionid=4A3D4C11876FC7B22F98B0E14287BC8E.monappliServer01 Content-Type: text/plain Les origines L’ajout du ;jsessionid=xxx est pris à l’initiative du conteneur web lors de l’appel à la méthode HttpServletResponse:: encodeRedirectURL. Bien que pouvant s’apparenter à un bug, ce comportement fait partie intégrante de l’ API Servlet. En effet, lorsqu’une nouvelle session est créée, le serveur ne sait pas si le client supporte les cookies et les a activé. Il génère alors un cookie JSESSIONID spécifiant le jsessionid et réécrit ce jsessionid dans l’URL. Lorsque le client émet une seconde requête, le serveur vérifie la présence du cookie. Deux scénarios sont alors possibles :\nLorsque le serveur détecte le cookie JSESSIONID dans la requête, il désactive la réécriture d’URL avec les jsessionid. Lorsque le cookie JSESSIONID n’est pas transmis mais que l’URL de la requête comporte un jsessionid, le serveur continue à réécrire toute URL en concaténant le jsessionid. Ce comportement s’applique tout au long de la navigation. Initialement parti d’une bonne attention à l’époque où les navigateurs ne supportaient pas tous les cookies, ce mécanisme de suivi de session pose aujourd’hui plus de problèmes qu’il n’en résout : référencement dans les moteurs de recherche, stockage des URL dans le cache des proxys, déséquilibrage de clusters … Devenu inutile, IBM l’a par défaut désactivé depuis au moins la version 5 de son serveur d’application Websphere. C’est pourquoi nous n’avions jusque-là jamais rencontré ce problème. Redhat JBoss a mis davantage de temps à se rendre compte de cette problématique de jsessionid référencée sur son bugtracker.\nSolutions Pour contrer ce problème, plusieurs solutions existent. Certaines ne s’appliquent pas à nos contraintes d’environnements :\nJBossWeb 2.1 étant basé sur la version 6.0.15 de Tomcat, il n’est pas possible d’utiliser l’ attribut disableURLRewriting du context.xml introduit dans Tomcat 6.0.30 Forcer le SessionTrackingMode à COOKIE dans le web.xml n’est possible que sur un conteneur de servlet 3.0 (ex : JBoss 6 EAP). D’autres solutions nécessitent de modifier, releaser puis relivrer l’ensemble des applications web :\nLa solution proposée par RedHat consistant à écrire un filtre de servlet JEE ressemblant au JsessionIdRemoveFilter du framework Seam et dont le code source est proposé page 44 du guide d’administration de JBoss EAP 5. La mise en place dans le web.xml du filtre Tuckey UrlRewriterFilter et la configuration du fichier urlrewrite.xml associé La solution finalement retenue consiste à exploiter le module mod_headers du serveur Apache 2.2 situé en frontal des JBoss. Alors que le mod_rewrite ne peut agir que sur les requêtes HTTP, le mod_headers peut modifier les en-têtes des réponses HTTP. Toutes les en-têtes Location comportant un ;jsessionid= sont réécrites sans le ;jsessionid=\nPour se faire, le module mod_headers a été activé dans le fichier de configuration Apache htppd.conf :\nLoadModule headers_module modules/mod_headers.so La ligne suivante a ensuite été ajoutée dans le fichier de configuration de l‘hôte virtuel vhost.conf :\nHeader edit Location ^(.*);jsessionid=(.*)$ $1 La réponse précédente réencodée par le mod_headers donne :\nHTTP/1.1 302 Found Date: Wed, 22 May 2013 19:35:38 GMT X-Powered-By: Servlet 2.5; JBoss-5.0/JBossWeb-2.1 Location: http://www.monappli.com/index.faces Content-Type: text/plain La redirection GET http://www.monapli.com/index.faces n’est plus bloquée par le firewall.\nConclusion Rapide à déployer, ce palliatif a permis d’adresser le plus grand nombre des applications. Pour une poignée d’entre elles, l’ajout du filtre de servlet a été nécessaire car aucune redirection n’était effectuée lors du premier accès à l’application. La page HTML rendue côté serveur comportait directement des liens comprenant le fameux jsessionid, la méthode HttpServletResponse::encodeUrl étant appelée par les différents tags positionnés sur la page.\n","link":"https://javaetmoi.com/2013/05/mod_headers-rescousse-jsessionid-jboss/","section":"posts","tags":["apache","jboss","servlet","tomcat"],"title":"Mod_headers à la rescousse du jsessionid"},{"body":"","link":"https://javaetmoi.com/tags/servlet/","section":"tags","tags":null,"title":"Servlet"},{"body":" A deux semaines de sa première formation en entreprise sur AngularJS, répétition générale pour Thierry Chatel devant 200 développeurs avides d’en apprendre un peu plus sur le dernier né des frameworks JavaScript de Google. Développeur Java / Swing chez IBM au début des années 2000, Thierry s’est ensuite dirigé vers du conseil en architecture avant de découvrir AngularJS durant l’été 2011. Depuis, il y consacre beaucoup d’énergie et anime notamment le site FrAngular.com, premier blog francophone dédié à ce framework. Assez parlé de sa personne, lui-même n’en serait que trop gêné.\nComme de nombreux développeurs venus assister à cette conférence, j’étais curieux de découvrir à mon tour le framework qui avait fait autant parlé de lui lors de Devoxx World 2012. Et autant vous l’annoncer dès maintenant : je n’ai pas été déçu.\nD’une durée de 3h, cette Université intitulée AngularJS, ou le futur du développement Web s’est décomposée en une première partie théorique suivie d’une seconde plus pratique basée sur différents types d’applications : Last Tweets, directive Google Maps et Game Store. Live coding et démos furent au rendez-vous. Pour la petite anecdote, les slides de la présentation sont écrits avec la syntaxe Markdown et sont interprétés par l’outil Angular Showoff reposant, vous l’aurez deviné, sur Angular. L’intérêt majeur est qu’ils peuvent embarquer du code Angular, pratique pour les démos in-slides telles que :\nYour name: Hello {{me}}! La fonctionnalité clé La fonctionnalité phare que recherchait notre speaker dans un framework JavaScript était le data binding bi-directionnel entre le modèle et les vues. C’est donc ce qu’il a particulièrement apprécié dans Angular et mis en avant dès le début de sa présentation. Par data binding bi-directionnel, on entend qu’une mise à jour du modèle JavaScript soit répercutée sur l’IHM et, inversement, qu’une interaction de l’utilisateur (ex : saisie dans un champ texte) soit aussitôt reflétée dans le modèle. C’est le comportement que l’on retrouve typiquement côté serveur avec des technologies telles JSF entre la vue xhtml et les beans managés. La majorité de ses concurrents JavaScript n’assurent le binding que dans un seul sens : des données du modèle vers la vue HTML. En général, l’opération inverse doit être implémentée manuellement en JavaScript en s’abonnement aux évènements du DOM, par exemple à l’aide de JQuery. Angular permet de gérer nativement ce binding qui, jusque-là, faisait défaut au HTML. Concrètement, cela se traduit par drastiquement moins de code JavaScript de manipulation du DOM et, implicitement, une compréhension du code et une maintenance facilitée. Je confirme que, dans les applications présentées lors de ce show, nous n’avons pas vu une ligne de ce type de code. Autre aspect en faveur d’Angular : le modèle JavaScript ainsi que les différents services JS écrits par le développeur ne requièrent aucune adhérence avec Angular. On retrouve le côté POJO de Java et des beans Spring. C’est ce qui donne son petit côté magique au framework.\nLe mécanisme Le mécanisme sous-jacent au data binding est introduit par Thierry au travers du slogan d’Angular « HTML enhanced for webapps ». En effet, le parti pris d’AngularJS est d’étendre le HTML, de l’enrichir avec des directives Angular, à savoir des tags et des attributs supplémentaires ainsi que des expressions entre {{double accolades}}. La page HTML fait elle-même office de Template. Au démarrage de l’application, lors d’une phase de compilation, le framework parcourt le DOM à la recherche de ces balises puis instrumente le code HTML afin d’assurer le binding. A partir de ce point, modèles et vues sont synchronisés en continue. La force d’Angular réside dans le fait qu’il n’est nul besoin d’utiliser JavaScript pour déclarer un template. C’est la page HTML qui est étendue pour jouer ce rôle.\nSous le capot d’Angular, la détection des changements côtés vue s’appuie sur des évènements du DOM (ex : onclick, onkeyup) ou extérieurs (ex : timeout, réponse Ajax). Par contre, les modifications du modèle sont détectées par un mécanisme bien spécifique : le dirty checking. A ce que j’en ai compris, il permet de surveiller tout objet JavaScript, de détecter qu’une valeur a changé puis d’exécuter du code JS. J’entrevoie une similitude avec le fonctionnement d’Hibernate qui surveille les changements opérés sur les objets attachés à sa session, ceci pour les refléter en base de données.\nLe dirty checking s’appuie sur des watches, des listeners à l’écoute des changements. Lors de la compilation du template, Angular positionne automatiquement des watches sur les expressions et les tags HTML bindés avec un modèle. Pour des problématiques de performance, Thierry nous alerte sur le fait que l’expression surveillée ne doit pas être trop couteuse à évaluer. Pour les objets « non managés » par Angular (issus par exemple d’API tierces), le développeur peut déclarer des watches de manière impérative. Cette possibilité a été illustrée lors du développement d’une directive Angular utilisant l’API Google Maps. Le but était de surveiller le déplacement du centre de la carte via la souris de l’utilisateur et, le cas échéants, de mettre à jour les 2 champs de saisie correspondant à la latitude et la longitude, champs par ailleurs modifiables au clavier.\nSyntaxe utilisée pour ajouter un watcher :\nscope.$watches(\u0026#39;center\u0026#39;, function () { map.setCenter(scope.center); }, true); L’objet center possédant 2 propriétés, le paramètre booléen true précise à Angular qu’il doit surveiller l’objet en profondeur.\nPour terminer sur ce sujet, notre orateur nous laisse rêveur en nous annonçant que ce mécanisme de dirty-checking est à l’étude au sein du W3C sous le nom de Object.observe() et que certaines versions avant-gardistes de navigateurs telles Chrome Canari ou Chronium l’implémentent déjà nativement, multipliant ainsi par 20 les performances actuelles du dirty-checking d’Angular.\nLes autres concepts Bien entendu, la force d’Angular ne se limite pas au seul data binding bi-directionnel. Le framework met à disposition de nombreux concepts bien connus des développeurs Java :\nMVC : le pattern Modèle Vue Contrôleur fait partie intégrante d’Angular. Le HTML enrichi représente la vue. Des actions positionnées sur la vue permettent de faire appel à des contrôleurs JavaScript. Ces derniers exécutent le code applicatif puis mettent les données à disposition de la vue par l’intermédiaire d’un contexte. Thierry nous explique que les contrôleurs peuvent fonctionner sans la vue, ce qui est intéressant pour les tests. Une mauvaise pratique est donc de manipuler le DOM dans le contrôleur. Il nous rappelle aussi que le pattern MVC permet d’avoir plusieurs vues pour les mêmes contrôleurs / données ; une vue pouvant ainsi être dédiée aux appareils mobiles. Injection de dépendance : les amateurs de Spring, de Guice ou de CDI retrouveront avec plaisir ce pattern dans Angular. Lors de l’appel d’une méthode d’un contrôleur, Angular lui injecte en paramètre les services dont il a besoin. Ces services peuvent être fournis par Angular (ex : $http pour des appels REST) ou par le développer qui les aura préalablement configurés à l’aide d’un provider. Pour les curieux, Thierry rentre dans le détail de l’implémentation en expliquant qu’Angular se base sur la signature des méthodes pour déterminer le service à injecter. Le nom des paramètres est interprété comme le nom du service à injecter. Cette approche pose problème lorsque le code JS est minifié. Il devient alors nécessaire de déclarer explicitement le nom des services à injecter en passant par un tableau de chaînes de caractères. Service : tout objet JS peut être mis à disposition du reste de l’application en tant que service. Outre le fait de pouvoir mutualiser du code faisant, par exemple, appel à des services REST du backend, les services permettent de conserver des données entre 2 contrôleurs. Thierry prend l’exemple d’un panier d’achat issu de l’application Game Store. Navigation: la navigation entre les différentes vues s’appuie sur le mécanisme de routes bien connu des développeurs PlayFramework. Boutons précédents, suivants et marque-pages sont gérés nativement. Scope: en Java, que ce soit en JSF ou avec Spring, les beans ont une portée (un scope). Dans Angular, cette notion prend un autre sens. Il faut le voir comme un contexte d’exécution qui peut être propre à une directive Angular. Les scopes sont utilisés lors de l’évaluation des {{ expressions }}. Lorsque l’objet référencé n’est pas présent dans le contexte courant, Angular va le chercher dans le contexte englobant, et ainsi de suite, en remontant la hiérarchie dans le DOM. Cela me fait penser aux EL de l’API Servlet qui utilisent dans l’ordre les portées page, request, session et application. Filtre: dans Angular, les filtres ne sont pas à rapprocher des filtres JavaEE ou Spring Security, mais aux pipes Unix. Thierry a utilisé un filtre pour parser le texte d’un tweet et y ajouter une balise lorsqu’une URL est détectée. Angular semble disposer d’un ensemble de filtres prêts à l’emploi. Les tests Lors de cette présentation, l’outillage concernant les tests n’a pas été oublié. Pour les tests fonctionnels, Karma (ex-Testacular) permet d’exécuter des tests dans les navigateurs connectés à l’outil. Une démo a été réalisée sous nos yeux avec Chrome, Firefox, IE et depuis un Smartphone. Karma présente l’avantage d’utiliser le moteur JavaScript des différents navigateurs connectés. Tel infinitest, Karma relance les tests à chaque sauvegarde du code. Créé par Vojta Jína, développeur principal d’Angular, Karma connait le fonctionnement d’AngularJS. Cela lui permet de résoudre de manière transparente le problème des requêtes Ajax souvent rencontré dans les tests Selenium.\nDu point de vue des tests unitaires, Angular permet de tester des contrôleurs et des services JS nécessitant de faire appel à un serveur. Pour se faire, Angular fourni le mock object _$httpBackend_ qui permet de simuler un appel au serveur et de retourner le jeu de données nécessaire au test (ex : au format JSON).\nEnfin, dans le cadre d’un débogage, le plugin Batarang pour Chrome facilite l’inspection du DOM et des scopes.\nLa cible D’après notre speaker, Angular cible avant tout les applications de gestion qui font habituellement recourt à de multiples champs de saisie. Dédié aux applications mono-pages (SPA), il ne le recommande pas pour les sites web, d’autant plus qu’ils ne seraient pas indexables dans Google. Un paradoxe. A noter qu’Angular est un framework qui s’utilise uniquement côté client, à savoir dans le navigateur, et qui n’a donc aucun sens à être embarqué côté serveur (ex : dans Node.JS).\nEncore jeune, très peu d’applications visibles sur le Net reposent sur Angular. Thierry nous cite tout de même quelques exemples :\nle nouveau manager d’OVH, le site de Google « Doubleclick for publisher » dédié aux professionnels de la publicité, et le site Youtube de la PS3. Les limites Thierry nous parle de la limite des 2000 watches. Limite que l’on peut facilement dépasser si l’on n’y prête pas attention et que l’on dispose d’une machine puissante. Ce seuil est d’autant plus valable sur les mobiles. Une astuce consiste à ne surveiller que les données visibles par l’utilisateur et à désactiver les watches sur les expressions qui ne varieront jamais.\nLe framework Angular ne dispose pas de composants UI. Il permet de réutiliser des composants JQuery UI, mais un adaptateur est nécessaire. Des librairies externes telles qu’ AngularUI ou AngularStrap commencent à voir le jour.\nPar ailleurs, en tant que framework, Angular est particulièrement structurant. Simple librairie, JQuery l’est beaucoup moins. Une application basée sur Angular doit donc adhérer complètement à sa philosophie ; il n’est pas intéressant de n’utiliser que certaines briques. A noter tout de même qu’il est possible de n’utiliser Angular que sur une partie de son application web et de le positionner, par exemple, que sur un simple div.\nLors de la série des questions/réponses, un développeur de l’assistance fait remarquer qu’Angular demande une syntaxe spécifique pour fonctionner sous IE 7. Son fonctionnement sur d’anciens navigateurs semble donc nécessiter quelques adaptations. Lors de cette session, certaines fonctionnalités indispensables à toute IHM n’ont pas été abordées. C’est par exemple le cas de la validation des données saisies dans les formulaires.\nConclusion Malgré sa jeunesse, AngularJS semble chambouler le foisonnant écosystème des frameworks JavaScript (Backbone.js, Knockout, Ember.js). Certains le voient déjà comme l’inspirateur d’ HTML 6. Avec DART, GWT et Angular, Google possède plusieurs poulains. Reste à savoir lequel sera le plus endurant ?\nPour ma part, outre l’omniprésent JQuery, je n’avais encore jamais pris le temps d’étudier l’un de ces frameworks JavaScript qui détrônent progressivement les technologies côté serveur. Cette université m’aura conforté dans mon choix de regarder de plus près ce que propose AngularJS. D’après Thierry, apprendre correctement le JavaScript est un pré-requis. L’apprécier en est un second. Avant d’aborder les tutoriaux sur Angular, je vais donc commencer pas ressortir le livre JavaScript, gardez le meilleur de Douglas Crockford.\nRéférences :\nLes slides de l’Université sur AngularJS à Devoxx France 2013 : http://tchatel.github.com/slides-angularjs/ Blog francophone sur le framework AngularJS : http://www.frangular.com/ Tutorial du site d’AngularJS : http://docs.angularjs.org/tutorial Concepts du framework du site officiel : http://docs.angularjs.org/guide/concepts Séries de courtes vidéos réalisées par John Lindquist : http://egghead.io/ Application Last Tweets de Thierry Chatel : http://jsfiddle.net/tchatel/4FNeB/ Exemple de directive Google Maps de Thierry Chatel : http://plnkr.co/edit/gn1jVW?p=preview Application Game Store de Thierry Chatel : https://github.com/tchatel/angular-gamestore Outil Angular Showoff utilisé pour la réalisation des slides : https://github.com/tchatel/angular-showoff Communauté Google+ Angular JS France : https://plus.google.com/communities/109984348857296908402 ","link":"https://javaetmoi.com/2013/04/angularjs-devoxx-france-2013/","section":"posts","tags":["angularjs","devoxx","javascript"],"title":"AngularJS à Devoxx France 2013"},{"body":"Dans le cadre d’un important chantier de migration technique d’une application, j’ai eu l’occasion de pratiquer ce que j’appellerais la promotion de code en continue.\nPour resituer le contexte, ce chantier dura plus de 6 mois. Entre le début et la fin de la migration, l’application a été livrée plusieurs fois en production, embarquant à chaque fois de nombreuses évolutions fonctionnelles. Nous avons donc dû nous organiser pour migrer l’application sans pénaliser l’avancement du reste de l’équipe. Les changements techniques étant bien trop transverses à l’application, la stratégie de Feature Toggle ne pouvait s’appliquer. Nous nous sommes donc dirigés vers une technique assimilable au Feature Branch ; notre migration technique n’étant rien d’autre qu’une feature comme une autre. Logiquement, une branche dédiée à la migration a été créée.\nNotre stratégie fut de merger régulièrement dans cette branche le code issu de la branche de développement. Une fois la migration terminée, la branche de migration a été à son tour mergée dans la branche de développement.\nGit svn à la rescousse Comme c’est encore le cas dans de nombreuses entreprises, SVN est l’unique outil de versioning proposé dans l’usine de développement. La plateforme d’intégration continue s’appuie dessus et, installés sur une infrastructure de production, les repos SVN présentent l’intérêt d’être archivés quotidiennement. C’est là que la passerelle git-svn intervient en mettant à la portée des utilisateurs SVN la puissance et la facilité des merges apportées par Git. Git-svn permet en effet de maintenir à jour la branche svn avec le trunk. Cette facilité a un prix : une vigilance accrue et une grande rigueur lors des opérations en ligne de commande. En effet, une erreur d’inattention et vous pouvez vous retrouver à commiter le code de la branche de migration sur le trunk SVN. C’est pourquoi, au cours de la migration, le mode opératoire présenté ci-dessus a été scrupuleusement suivi.\nMode opératoire Les instructions suivantes permettent de récupérer dans la branche de migration SVN, les développements commités sur le trunk SVN. L’exemple illustrant ces instructions concerne une application Java. Maven est l’outil de build utilisé pour compiler, exécuter les tests unitaires et vérifier ainsi l’état du merge. Les 2 branches Git manipulées sont associées aux branches SVN suivantes :\nmaster (git) =\u0026gt; trunk (svn) local-feature-migration (branche locale git)=\u0026gt; feature-migration (branche distance svn) Depuis la précédente opération de merge, nous partons du principe que des commits ont pu ou non avoir eu lieu dans le trunk. De la même manière, des modifications ont pu ou non avoir été commitées et poussées dans la branche de migration.\nAvant de commencer, s’assurer que toutes les modifications sont commitées dans le repo SVN et l’intégration continue est au vert, à la fois sur le trunk et la branche.\nEnfin, cette opération de merge est à réaliser de préférence depuis le même repo git local. Ce dernier possède l’historique des merges réalisés précédemment. Il faut en effet garder à l’esprit que, une fois commité dans SVN, le commit ancêtre entre le trunk et la branche est perdu. Même avec git-svn, un autre développeur ne pourrait le récupérer.\n1. Faire pointer le master au niveau du trunk\ngit checkout master git svn rebase 2. Effectuer un checkout de la branche locale (et non distante)\ngit checkout local-feature-migration Avec le pont git-svn, les merges ne fonctionnent que dans un sens. Il faut donc veiller à se placer sur la branche sur laquelle on veut créer le commit de merge.\n3. Merger le master vers la branche locale\ngit merge --no-ff master Remarque : l\u0026rsquo;option \u0026ndash;no-ff (no fast-forward) est très importante : elle force git à créer un commit dit de \u0026ldquo;merge\u0026rdquo;. Lorsque la branche ou le master n\u0026rsquo;ont pas bougé, cela évite que la référence HEAD de la branche local-feature-migration soit déplacée pour pointer sur le dernier commit de la branche master. En résumé, cela permet de conserver 2 branches distinctes, comme dans SVN. La conséquence désastreuse de cet oubli est qu\u0026rsquo;un svn dcommit de la branche local-feature-migration serait pourrait être réalisé sur le trunk.\n4. Régler les éventuels conflits\ngit mergetool git rebase --continue Utiliser son outil de merge préféré, par exemple kdiff3 ou TortoiseMerge.\n5. Exécuter le build maven\nmvn clean install Permet de s’assurer que le code compile et que les tests unitaires passent.\n6. Commiter dans SVN\ngit svn dcommit Sous peine de corrompre votre repo git, attention à ne pas effectuer de git svn rebase entre le merge et le svn dcommit.\nClonage d’un repo SVN existant, création d’une branche Git et liaison avec une branche SVN distante sont expliqués dans la documentation de git-svn.\nBénéfices Voici une liste non exhaustive des bénéfices apportés par cette méthode de travail :\nLe report des commits au fil de l\u0026rsquo;eau permet de régler au plus tôt d\u0026rsquo;éventuels problèmes de merge. L’effet tunnel est évité. Les conflits sont résolus plus rapidement car les modifications sont encore toutes fraiches dans la tête des développeurs (du trunk comme de la branche). Cette stratégie permet de déployer en dual-run les 2 versions de l\u0026rsquo;application, sans perte de fonctionnalités puisqu’elles sont iso-fonctionnelles (à la migration technique prêt). La comparaison de comportements et les bascules en sont facilitées. Conclusion Après quelques essais, la fréquence retenue pour réaliser ces opérations de merge fut d’une fois par semaine. Au final, ce n’est pas une application, mais dix qui ont été migrées à l’aide de cette stratégie. Au total, 2 millions de ligne de code ont ainsi été promues. Bien entendu, ce procédé peut être appliqué avec d’autres technologies que maven et Java.\n","link":"https://javaetmoi.com/2013/03/promotion-code-continue-merge-git-svn/","section":"posts","tags":["git","svn"],"title":"Promotion de code en continue avec git-svn"},{"body":"Dans un précédent billet, je vous ai présenté les solutions mises en œuvre sur un projet pour paralléliser un batch d’indexation alimentant un moteur de recherche d’entreprise. Utilisée pour initialiser l’index de recherche puis le resynchroniser quotidiennement, la technique d’intégration par batch ne permet cependant pas d’indexer les données au fil de l’eau. Ce billet aborde précisément cet aspect. En effet, le fil de l’eau ou le quasi temps réel fut dès le départ une exigence forte du métier. Recherches instantanées et auto-complétion révolutionnent le traditionnel formulaire de recherche mettant plusieurs secondes à renvoyer les résultats. Mais au prix de faire des recherches sur des données pouvant dater de J-1 ? Ce n’était pas acceptable ! Un middle d’indexation fut la réponse apportée.\nArchitecture technique L’intégration d’un moteur de recherche dans un SI doit se fondre dans l’architecture technique en place. Voici quelques patterns d’architectures appliqués dans de nombreuses entreprises :\nHaute-disponibilité : applications déployées en cluster actif / actif, réparties sur N nœuds, robustes aux pannes et aux indisponibilités Données sécurisées : données non publiques, règles de visibilité assujetties aux habilitations, DMZ Architecture SOA : nombreuses couches (Front Office / Middle Office / Back Office / Référentiels), couplage lâche, exposition des services en SOAP ou en REST, communications asynchrones passant par un EAI Partant de ces règles, voici les exigences supplémentaires qui nous ont été formulées :\nMoteur de recherche installé comme infrastructure transverse au SI APIs du moteur de recherche non accessibles aux applications sources et utilisatrices Overhead minimal sur les transactions métiers déclenchant une indexation Pour les respecter, a été développé un moteur d’indexation faisant office de middle avec le moteur de recherche ElasticSearch. Implémenté sous forme d’application web Java, l’utilisation d’une River ou d’un plugin auraient pu être des alternatives. Quelques avantages à la solution mise en œuvre :\nIndustrialisation des livraisons sur un serveur d’application JEE Monitoring de l’application normalisé Livraisons sans interruption des services de recherche Dialoguant avec les applications métiers et ElasticSearch, ce middle embarque la logique d’indexation et gère les montées de version du moteur de recherche avec leurs changements d’API.\nPour que les différents systèmes puissent notifier au middle d’indexation qu’une donnée a été créée / modifiée / supprimée, un composant de notification a été proposé aux applications JEE. Ce composant inclue un mécanisme d’interception des mises à jour des objets métiers.\nLe diagramme ci-dessous démontre l’orchestration du processus d’indexation au fil de l’eau.\nLe processus d’indexation se déroule en 4 étapes :\nAction de mise à jour : vise à créer, mettre à jour ou supprimer un (ou plusieurs) objet du domaine métier ; cette action peut être réalisée aussi bien par un utilisateur (post d’un formulaire Struts, requête Ajax) que par un appel de web services (SOAP ou REST).\nNotifications: information de mise à jour chargée de notifier le middle d’indexation qu’il est probablement nécessaire de (ré)indexer un document ou de le supprimer de l’index. Cette notification peut-être de 2 types :\nAutoportante : contient les données nécessaires à l’indexation Minimale : contient le type de la données, son identifiant et la nature de l’action. Dans la suite de ce billet, c’est de ce type de notification dont nous parlerons. Lecture des données : dans le cas d’une création ou d’une modification d’objet métier, les informations véhiculées par la notification autoportante permettent de déterminer quelles données sont nécessaires pour construire le document Lucene. En fonction du Back Office ou du Référentiel interrogé, ces données peuvent être récupérées de différentes manières : SOAP, REST, JDBC (vue base de données), JCA / CICS … Plusieurs appels peuvent parfois être nécessaires pour agréger l’ensemble des données à indexer.\nIndexation: construction du document Lucene à indexer, gestion du versionning et utilisation de l’API Java ElasticSearch.\nComposant de notification Utilisé lors de la 1ière étape du processus d’indexation, un composant de notification est mis à disposition des applications développées en Java et dont le socle technique s’appuie sur le framework Spring. Conçu pour une intégration peu intrusive, il met à disposition une poignée d’annotations Java et se configure en quelques lignes de XML ou de Java.\nCe composant est lui-même décomposé en 3 phases :\nInterception des modifications Construction de la notification Publication vers le middle Techniquement, il s’appuie sur différents modules du Framework Spring :\nAOP: interception et interprétation des annotations SpEL: expressions acceptées par les annotations Task: parallélisation des phases de construction et de publication avec @Async OXM : marshalling XML des notifications JMS: publication de la notification vers l’EAI par le biais du JmsTemplate Le niveau d’interception est généralement positionné sur les services métiers transactionnels de mise à jour unitaire ou massive des objets métiers. Une donnée n’est ainsi indexée que lorsque ses modifications sont effectives dans l’application source. En cas de crash, toute perte de notification est rattrapée par le batch de nuit ; l’utilisation d’un commit à 2 phases ou d’un mécanisme de rejeu ne sont donc pas nécessaires.\nVoici un exemple d’application des annotations fournies par la brique d’interception sur le service métier CustomerService :\n@Transactional @DataOperation public void addPostalAddress(@DataRef(type=\u0026#34;Person\u0026#34;, id=\u0026#34;#{id}\u0026#34;) Customer customer, Address address) @Transactional @DataOperation(action = DataAction.DELETE) public void deleteCustomer(@DataId(type=\u0026#34;Customer\u0026#34;) int id) L’annotation @DataRef permet de spécifier le type d’objet métier et offre la possibilité d’utiliser une Spring Expression Language pour récupérer dynamiquement l’identifiant de l’objet.\nTechnique d’interception A lui seul, le mécanisme d’interception pourrait faire office d’un second billet. Succinctement, il s’appuie sur un post-processeur de beans Spring qui analyse chaque bean Spring créé par le conteneur léger. Son implémentation s’inspire de celle des post-processeurs du Framework Spring tels AsyncAnnotationBeanPostProcessor et TransactionProxyFactoryBean, tous deux responsables de respectivement traiter les annotations @Async et @Transactional.\nUne fois le bean Spring initialisé, le post-processeur de bean intercale un proxy AOP héritant de la classe AdvisedSupport. Cette étape est facultative si un proxy a déjà été placé par un précédent post-processeur. Un aspect héritant de la classe AbstractPointcutAdvisor est ensuite inséré en début de la chaîne d’aspects du proxy.\nCet aspect DataOperationAnnotationAdvisor possède 2 composantes :\nUn point de coupe ciblant les méthodes annotées avec @DataOperation Un greffon implémentant l’interface MethodInterceptor et en charge de parcourir la signature des méthodes à la recherche des éventuelles annotations @DataId et @DataRef, de gérer les appels réentrants et les imbrications, de se synchroniser si besoin est avec le gestionnaire des transactions et, bien entendu, de tracker tout changement opéré sur les objets métiers. Le diagramme ci-dessous illustre la mise en place du greffon DataOperationInterceptor devant le bean customerService :\nToutes les modifications d’objet métier identifiées au cours d’une transaction sont ensuite assemblées au sein d’une notification qui est émise via JMS vers le middle d’indexation.\nBien qu’introduisant une adhérence supplémentaire, l’utilisation du support AspectJ de Spring aurait probablement proposée une implémentation plus légère. Attention toutefois à bien conserver l’ordre des intercepteurs : le DataOperationInterceptor doit être placé avant le TransactionalInterceptor.\nMiddle d’indexation Le middle d’indexation n’est ni plus ni moins qu’un moteur de messages chargé de lire les notifications, de les traiter et de gérer les cas d’erreur.\nSon implémentation peut être réalisée à l’aide de MDB, de Spring JMS, d’Apache Camel ou bien encore de Spring Integration. Ces 2 derniers présentent un intérêt tout particulier pour ce type d’architecture orientée messages et faisant appel à de nombreuses techniques d’intégration : JMS, SOAP, JDBC, REST, JCA, ElasticSearch …\nQuel que soit la technologie, le middle est décomposée en 3 parties ayant chacune leur responsabilité :\n1. Réception des notifications\nLe middle d’indexation traite les notifications en parallèle. L’utilisation de files JMS permet de lisser les pics de charge. La scalabilité horizontale du middle est assurée par un simple ajout de nœud (une Nième JVM du serveur d’application). Pour dimensionner le système en fonction, par exemple, de la volumétrie d’un type d’objet métier, différents pools de threads peuvent être configurés.\nUne notification comportant plusieurs informations de mises à jour peut être découpée ; cela permet de segmenter le traitement d’une notification composite. Certains utiliseront l’EIP Message Splitter. Les informations de mises à jour portant sur le même type d’objet métier sont ensuite acheminées vers le service dédié à la construction du document. L’usage de l’EIP Message Router est ici possible.\n\u0026lt; !-- Lecture des notifications reçues sur la file JMS à l’aide du pattern EIP Channel Adapter \u0026ndash;\u0026gt;\n\u0026lt;int-jms:message-driven-channel-adapter \u0026#34; destination=\u0026#34; inNotificationQueue\u0026#34; channel=\u0026#34;notificationChannel\u0026#34; /\u0026gt; 2. Construction du document\nL’étape de construction du document à indexer demande tout d’abord de récupérer les données appropriées. Un ou plusieurs appels à des systèmes tiers sont nécessaires. Dans notre contexte, l’usage de web services SOAP était préconisé. La construction du document Lucene est liée au mapping de l’index préalablement défini dans ElasticSearch, mapping lui-même créé en fonction du besoin métier en termes de recherche. Une connaissance fonctionnelle et une expertise sur les moteurs de recherche sont ici indispensables et indissociables. Les compétences de Lucian Precup en ce domaine ne sont plus à démonter.\nUne partie du code java peut être mutualisée avec le batch.\n3. Indexation du document dans ElasticSearch\nL’indexation du document Lucene se fait à l’aide de l’API Java ElasticSearch. Afin de pas perdre de notifications en cas d’erreurs volatiles, l’écriture est réalisée en synchrone. Le versionning ElasticSarch permet de faire cohabiter simultanément batch quotidien et indexation fil de l’eau.\nCôté code, le client ElasticSearch org.elasticsearch.client.Client est injecté dans le service chargé d’indexer les documents à l’aide d’une fabrique de beans Spring créée pour l’occasion ou de celle mise à disposition par Dadoonet dans le projet spring-elasticsearch hébergé sur Github. Une extension Spring Integration pour ElasticSearch pourrait également avoir son intérêt.\nConclusion Si je ne vous ai pas perdus en route au fil de ces longues explications, vous vous dites probablement que le travail accompli est relativement conséquent alors qu’il parait plus simple et plus efficace d’indexer les données directement depuis l’application qui en est maitre. C’est oublier que dans les grandes entreprises, séparation des préoccupations, découplage et sécurité sont de rigueur. Pour garantir les performances, le middle d’indexation est déployé dans la même bulle réseau que les nœuds du cluster ElasticSearch. Niveau sécurité, le firewall garanti que seul le middle d’indexation a le droit d’indexer des données dans ElasticSearch. Enfin, la mise en œuvre du composant de notification est facilitée sur les applications convenablement architecturées.\n","link":"https://javaetmoi.com/2013/02/architecture-middle-indexation-elasticsearch/","section":"posts","tags":["architecture","elasticsearch","jms","soa","spring-framework","spring-integration"],"title":"Architecture d’un middle d’indexation"},{"body":"Après un précédent billet relatant un bug lié à la version du driver Oracle utilisé, voici un nouveau billet portant sur bug lié, cette fois ci, à la version de la JVM utilisée. Ce bug nous a été révélé très tardivement dans le cycle de développement de l’application Java incriminée. En effet, PV de recette en poche, les tests de charge menés avec JMeter sur l’environnement de pré-production ne nous avaient rien révélé. Seuls les tests de robustesse nous ont alertés d’une mystérieuse java.net.UnknownHostException survenant 4 à 5 minutes après l’arrêt volontaire d’une application tierce.\nLes symptômes Techniquement, l’application testée consomme différents web services SOAP. Le framework Apache CXF est utilisé comme client SOAP. Développée en Java 6, l’application est déployée sur un serveur d’application JBoss. Lors de l’arrêt de l’application tierce exposant les web services, le client n’arrive plus à s’y connecter. Comme attendu, une java.net. SocketException est levée. Jusque-là, tout est normal. Par contre, après plusieurs minutes d’arrêt, le SocketException se transforme en UnknownHostException. Et c’est là que commence nos interrogations. En effet, une UnknownHostException est levée en principe lorsque l’adresse IP d’un hôte n’a pas pu être résolue. Or, jusqu’à cette erreur, l’adresse IP avait pu être résolue. Voici la pile d’appel observée :\nCaused by: java.net.UnknownHostException: www.monappli.fr at java.net.PlainSocketImpl.connect(PlainSocketImpl.java:177) at java.net.SocksSocketImpl.connect(SocksSocketImpl.java:366) at java.net.Socket.connect(Socket.java:525) at sun.net.NetworkClient.doConnect(NetworkClient.java:158) at sun.net.www.http.HttpClient.openServer(HttpClient.java:394) at sun.net.www.http.HttpClient.openServer(HttpClient.java:529) at sun.net.www.http.HttpClient.\u0026lt;init\u0026gt;(HttpClient.java:233) at sun.net.www.http.HttpClient.New(HttpClient.java:306) at sun.net.www.http.HttpClient.New(HttpClient.java:323) at sun.net.www.protocol.http.HttpURLConnection.getNewHttpClient(HttpURLConnection.java:860) at sun.net.www.protocol.http.HttpURLConnection.plainConnect(HttpURLConnection.java:801) at sun.net.www.protocol.http.HttpURLConnection.connect(HttpURLConnection.java:726) at sun.net.www.protocol.http.HttpURLConnection.getOutputStream(HttpURLConnection.java:904) at org.apache.cxf.transport.http.HTTPConduit$WrappedOutputStream.handleHeadersTrustCaching(HTTPConduit.java:1375) at org.apache.cxf.transport.http.HTTPConduit$WrappedOutputStream.onFirstWrite(HTTPConduit.java:1317)at org.apache.cxf.io.AbstractWrappedOutputStream.write(AbstractWrappedOutputStream.java:42)at org.apache.cxf.io.AbstractThresholdOutputStream.write(AbstractThresholdOutputStream.java:69)at org.apache.cxf.transport.http.HTTPConduit$WrappedOutputStream.close(HTTPConduit.java:1395) ... 34 more D\u0026rsquo;après le code source, l’exception est levée par la classe PlainSocketImpl lors d’un appel natif.\nBien qu’incompris, ce problème aurait pu être acceptable s’il s’était limité à un fonctionnement en mode dégradé. Malheureusement, plusieurs semaines après une mise en production en avance de phase, ce problème est réapparu de manière intempestive et, plus grave encore, sans aucune interruption de service de l’application tierce.\nL’investigation Plusieurs pistes ont alors été envisagées : coupures réseaux, utilisation d’IP V6, problème d’accès aux DNS, excès de zèle du firewall, problème de cache DNS côté serveur comme côté JVM, bug applicatif … D’éventuels problèmes de DNS ou de firewall ont rapidement été écartés par l’ingénierie système.\nEcartant le problème d’un bug applicatif, la mise au point du programme java ci-dessous nous a permis de reproduire systématiquement le UnknownHostException. Basique, ce programme lit en boucle la 1ière ligne d’un des WSDL exposés par l’application tierce. Deux minutes à peine après son exécution, des UnknownHostException fleurissaient.\nimport java.io.BufferedReader; import java.io.InputStreamReader; import java.net.URL; import java.net.URLConnection; import java.net.UnknownHostException; public class TestUrlConnection { public static void main(String... args) throws Exception { final URL url = new URL(\u0026#34;http://www.monappli.fr/ws/MonWebService/v1_0?wsdl\u0026#34;); for (long i = 0; i \u0026lt; 10000000000L; i++) { try { URLConnection conn = url.openConnection(); BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream())); in.readLine(); in.close(); } catch (UnknownHostException e) { e.printStackTrace(); } } } } L’ajout des adresses IP dans le fichier /etc/hosts du serveur Linux fut le premier contournement trouvé.\nSolution Le contournement définitif consista à utiliser une JVM Sun / Oracle 1.6.0_25 à la place de la version 1.6.0_16 initialement installée. Aucun risque de régression fonctionnelle n’était à craindre puisque la version 1.6.0_25 équipait déjà les environnements d’intégration de recette. De nouveaux tests de charge ont validé techniquement cette montée de version.\nMalheureusement, les release notes de la JVM ne nous ont pas permis de trouver le problème corrigé entre ces 2 versions de JVM. Notre hypothèse est que, datant d’août 2009, la version 1.6.0_16 du JRE ne supportait pas encore la Red Hat 6 de novembre 2011 sur laquelle s’exécute notre application. Quant au fait que l’exception soit apparue plus systématiquement lors de tests de robustesse, une explication plausible est que, lorsque le web service est indisponible, sa fréquence d’appel est plus grande, ce qui accroit la sollicitation aux appels système de résolution de nom de domaine.\n","link":"https://javaetmoi.com/2013/01/unknownhostexception-jvm-version/","section":"posts","tags":["bug","jvm"],"title":"Une bien mystérieuse UnknownHostException"},{"body":"Récemment, je suis tombé sur un bug lié à l’utilisation d’une version de driver JDBC pour Oracle plus récente que la version de la base Oracle attaquée en SQL via JDBC.\nLes symptômes Dans notre contexte applicatif, la date et l’heure des données lues en base sont utilisées pour détecter des conflits de version, d’une manière similaire au versioning Hibernate. Concrètement, cela nous permet d’éviter qu’une donnée traitée par batch quotidien écrase une donnée plus fraiche provenant d’un système tiers. Ce mécanisme permet notamment d’exécuter un batch sans interruption de service de l’application web associée. Le bug que je vais vous décrire nous a été révélé tardivement. Sous certaines conditions, nous avons en effet constaté que le batch ne rattrapait jamais des données. C’est comme si l’heure n’était jamais prise en compte dans le code Java.\nLecture de la colonne DATE Dans la base de données Oracle 9i interrogées, date et heure des données sont stockées dans une colonne de type DATE. Aucun doute sur un éventuel problème d’insertion des données, Toad nous confirme que l’heure est bel et bien présente. L’hypothèse d’un problème de lecture de l’heure a été confirmée en debuggant le batch, et plus particulièrement le code Java chargé de parcourir le ResultSet ramenée par une requête SQL. Voici le résultat des différents tests effectués :\nString type = resultSet.getMetaData().getColumnTypeName(1); // \u0026#34;DATE\u0026#34; Object res = resultSet.getObject(\u0026#34;lastupdate\u0026#34;); // 2013-01-18 res = resultSet.getDate(\u0026#34;lastupdate\u0026#34;); // 2013-01-18 res = resultSet.getTimestamp(\u0026#34;lastupdate\u0026#34;); // 2013-01-18 19:35:20.0 La norme SQL précise que le type temporel DATE ne contient pas d’informations sur l’heure. La classe java.sql.Date nous le rappelle. Le type temporel TIMESTAMP matérialisé par la classe java.sql.Timestamp permet quant à lui de stocker date, heure et nanosecondes. En forçant l’appel à la méthode getTimestanp(), on obtient le résultat escompté. L’heure stockée en base est bien remontée lors de l’exécution de la requête. Je me serais donc attendu à ce que les meta-données JDBC soit de type TIMESTAMP à la place de DATE et que la méthode getObject() utilisée dans le code applicatif manipule des java.sql.Timestamp.\nLe driver JDBC en cause Sur le site d’Oracle, la FAQ « What is going on with DATE and TIMESTAMP? » décrit précisément le problème rencontré et donne plusieurs pistes pour le résoudre. Pour résumer, jusqu’à la version 9.2 d’Oracle, cette dernière ne distinguait pas les types temporels SQL DATE et TIMESTAMP. Le type DATE Oracle combinait à la fois dates et heures. Jusque-là, le driver JDBC Oracle associé le type DATE à un java.sql.Timestamp. L’implémentation du type SQL TIMESTAMP est arrivée avec la version 9.2 de la base Oracle. Pour se conformer à la norme SQL, Oracle préconisa de migrer les colonnes de type DATE contenant des heures dans une colonne de type TIMESTAMP. Logiquement, le driver JDBC de la 9.2 associait désormais le type DATE dans un java.sql.Date et le type TIMESTAMP dans un java.sql.Timestamp. C’était oublier les bases antérieures à la version 9.2 ou celles qui n’avaient pas suivi les préconisations par difficultés techniques ou coûts. Ce changement de comportement du driver a perduré jusqu’à sa version 10.2. Oracle fit marche arrière avec la version 11.1 de son driver JDBC. Les types SQL DATE furent de nouveau associés à la classe java.sql.Timestamp. Notre code applicatif utilisait la version 10.2.0.3 du driver Oracle. La base de données Oracle interrogée est quant à elle une 9.2.0.8.0. Elle pourrait donc théoriquement utiliser le type TIMESTAMP ; mais ce n’est pas le cas.\nCorrections possibles Plusieurs solutions permettent de contourner ce problème :\nMigrer le schéma pour utiliser le type TIMESTAMP à la place d’une DATE. Dans notre contexte, la base ne nous appartenant pas, cette solution ne peut s’appliquer. Utiliser la méthode defineColumnType de la classe OracleStatement afin de redéfinir en Timestamp les colonnes de type DATE. De par la verbosité du code et l’adhérence à la classe OracleStatement du driver Oracle, cette solution fut mise de côté. En outre, nos tests étant basés sur la base de données embarquée H2, cette solution nous imposerait de supporter les 2 bases de données. Forcer l’appel à la méthode getTimestamp du ResultSet à la place d’un getObject. Sans doute la solution la moins risquée. Les impacts sont identifiés et maitrisés. Côté code, le code générique faisant massivement appel à la méthode getObject devrait être retouché pour tester par le biais des méta-données JDBC si la colonne lue est de type DATE. Utiliser le mode de compatibilité Oracle 8 en passant à true la propriété oracle.jdbc.V8Compatible de la connexion JDBC. Ne maitrisant pas les effets de bord et ce mode de compatibilité n’étant plus supporté à partir de la version 11 du driver Oracle, cette solution a été écartée. Utiliser la version 11 du driver JDBC Oracle corrigeant le problème. C’est la solution qui a été retenue. Une migration vers Oracle 11 des bases de données de l’entreprise étant prévue à moyen termes, cette solution parait la plus pérenne, d’autant que nos tests n’ont pas décelé d’autres changements induits par cette montée de version de driver. Version du driver JDBC Jusqu’à ce problème, je n’avais jamais prêté attention à la version du driver JDBC pour Oracle utilisée chez mon client. En effet, son choix est aux mains de l’équipe d’exploitation qui assure leur installation et leurs montées de version sur les serveurs d’applications. Le socle applicatif de l’entreprise est naturellement basé sur la même version. Par ailleurs, les driver Oracle sont unifiés. A savoir qu’un driver sait communiquer avec des bases de données de versions inférieures, voir même supérieure : « Which JDBC drivers support which versions of Oracle Database? »\nConclusion En conclusion, comme je vous l’ai montré, une montée de version de drivers JDBC n’est pas anodine, surtout avec Oracle. Tout comme la montée de version d’un framework, une analyse d’impacts doit être menée à partir des notes de livraisons. Par ailleurs, nos tests unitaires sur une base embarquée n’ont pas pu déceler ce problème. Moralité, même avec un taux de couverture maximum, des tests d’intégration ne sont jamais à exclure. Seul petit réconfort : les ingénieurs Java de chez Oracle sont tombés sur le même bug. Preuve en est, l’exécution d’une requête SQL depuis l\u0026rsquo;outil Oracle SQL Developer installé sur mon poste de dév et qui ne renvoie pas l\u0026rsquo;heure des dates.\n","link":"https://javaetmoi.com/2013/01/bug-date-heure-drivers-oracle-10g-9i/","section":"posts","tags":["bug","jdbc","oracle"],"title":"Oracle : dis-moi quelle heure est-il ?"},{"body":"Lors de la migration d’une application d’un serveur d’application vers un autre, il est fréquent d’être confronté à des problématiques de conflits de librairies. C’est par exemple le cas lorsqu’une application initialement déployée sur un Websphere Application Server 6.1 doit migrer sur JBoss 5.1 EAP (version commerciale de JBoss AS). Pour rappel, WAS 6.1 implémente J2EE 1.4 et s’exécute donc sur Java 5. Quant à JBoss 5.1 EAP, il implémente la norme Java EE 5, embarque donc de nombreuses implémentations des standards tels que JPA 1, JSF 1.2 et JAX-WS 1, et tourne sur Java 6.\nPour illustration, prenons une application s’appuyant sur Hibernate 3.6 pour sa couche de persistance et JAXB 2.2 pour le marshalling lors d’appel de web services. Ces 2 librairies sont embarquées dans le répertoire lib de son EAR et ne posent pas de problèmes particuliers à WAS 6.1. Par contre, sur JBoss 5.1 EAP, c’est un tout autre problème. En effet, son implémentation JPA repose sur la version 3.3 d\u0026rsquo;Hibernate. Qui plus est, JAXB 2.1 a été intégrée dans Java 6. Si vous tentez de déployer une telle application sur un JBoss installé avec la configuration par défaut, il y’a de fortes chances que vous tombiez sur l’une ou l’autre des exceptions suivantes : ClassCastException, NoSuchMethodException, IllegalAccessErrors, VerifyError. A ce que j’ai compris en parcourant la documentation mais également déduis de mes tests, différents mécanismes permettent d’expliquer ces comportements :\nPar défaut, lors du chargement d’une classe, le classloader de l’EAR va commencer par demander à son classloader parent (en l’occurrence celui de JBoss) de trouver la classe. Ainsi, c’est par exemple la classe Session d’Hibernate 3.3 qui sera chargée et non celle de la version 3.6 comme attendu. Il s’agit du comportement standard d’un classloader. Et c’est ce qu’on appelle communément le « j2se classloading compliance ». Sous WAS, cette stratégie de chargement peut être changée en paramétrant le classloader en PARENT_LAST. Les classes chargées par d’autres applications déployées sur la même instance de JBoss peuvent être partagées par votre application. Par exemple, la console d’admin JBoss admin-console.war embarque sa propre version de Richfaces et de Seam et peut, malgré elle, vous en faire bénéficier. Solutions étudiées Pour mener à bien la migration, plusieurs pistes ont été étudiées : SolutionsInconvénientsAvantages1Downgrader les versions des frameworks pour utiliser celles embarquées dans JBoss 5.1Important travail de refactoring pour combler les fonctionnalités manquantes. Bugs existants récupérés.Respect de la norme Java EE 5. Support éditeur maximal.2Configurer sur mesure le répertoire d’installation de JBoss (ex : supprimer le support des EJB 3 et de JPA)Mutualisation du répertoire d’installation rendue caduque. Main sur la production _._Un JBoss qui démarre plus vite. Pas d’impact sur le code.3Isoler le déploiement de l’applicationLire la documentation JBoss. Augmentation possible de la PermGen.Risque nul. Simple configuration. Configuration embarquée dans l’EAR.\nConfigurer le classloader de l’application Pour mettre en œuvre la solution n°3 concernant à « scoper » l’application, il est nécessaire de configurer le chargement des classes de JBoss . Une description détaillée de son fonctionnement est disponible sur la page JBossClassLoadingUseCases du wiki de JBoss. Dans notre cas, La configuration des classes loaders nécessaire est deployment scoped et Java2ParentDelegation désactivé. Cette configuration est représentée par la figure ci-contre.\nCette configuration présente les 2 avantages suivants :\nLes JARs embarqués dans l\u0026rsquo;EAR priment sur celles fournies par JBoss 5.1 et le JRE. Chaque application déployée sur le même serveur possède son propre UnifiedLoaderRepository (ULR). Le chargement des classes est isolé et n\u0026rsquo;interfère pas. Elles sont également isolées du chargement des applications tierces (ex: jmx-console et admin-console). La configuration du fichier jboss-app.xml à déposer dans le répertoire META-INF de l’EAR est décrite sur la page ClassLoadingConfiguration du wiki JBoss. En voici un exemple : Lors de la migration d’une application d’un serveur d’application vers un autre, il est fréquent d’être confronté à des problématiques de conflits de librairies. C’est par exemple le cas lorsqu’une application initialement déployée sur un Websphere Application Server 6.1 doit migrer sur JBoss 5.1 EAP (version commerciale de JBoss AS). Pour rappel, WAS 6.1 implémente J2EE 1.4 et s’exécute donc sur Java 5. Quant à JBoss 5.1 EAP, il implémente la norme Java EE 5, embarque donc de nombreuses implémentations des standards tels que JPA 1, JSF 1.2 et JAX-WS 1, et tourne sur Java 6.\nPour illustration, prenons une application s’appuyant sur Hibernate 3.6 pour sa couche de persistance et JAXB 2.2 pour le marshalling lors d’appel de web services. Ces 2 librairies sont embarquées dans le répertoire lib de son EAR et ne posent pas de problèmes particuliers à WAS 6.1. Par contre, sur JBoss 5.1 EAP, c’est un tout autre problème. En effet, son implémentation JPA repose sur la version 3.3 d\u0026rsquo;Hibernate. Qui plus est, JAXB 2.1 a été intégrée dans Java 6. Si vous tentez de déployer une telle application sur un JBoss installé avec la configuration par défaut, il y’a de fortes chances que vous tombiez sur l’une ou l’autre des exceptions suivantes : ClassCastException, NoSuchMethodException, IllegalAccessErrors, VerifyError. A ce que j’ai compris en parcourant la documentation mais également déduis de mes tests, différents mécanismes permettent d’expliquer ces comportements :\nPar défaut, lors du chargement d’une classe, le classloader de l’EAR va commencer par demander à son classloader parent (en l’occurrence celui de JBoss) de trouver la classe. Ainsi, c’est par exemple la classe Session d’Hibernate 3.3 qui sera chargée et non celle de la version 3.6 comme attendu. Il s’agit du comportement standard d’un classloader. Et c’est ce qu’on appelle communément le « j2se classloading compliance ». Sous WAS, cette stratégie de chargement peut être changée en paramétrant le classloader en PARENT_LAST. Les classes chargées par d’autres applications déployées sur la même instance de JBoss peuvent être partagées par votre application. Par exemple, la console d’admin JBoss admin-console.war embarque sa propre version de Richfaces et de Seam et peut, malgré elle, vous en faire bénéficier. Solutions étudiées Pour mener à bien la migration, plusieurs pistes ont été étudiées : SolutionsInconvénientsAvantages1Downgrader les versions des frameworks pour utiliser celles embarquées dans JBoss 5.1Important travail de refactoring pour combler les fonctionnalités manquantes. Bugs existants récupérés.Respect de la norme Java EE 5. Support éditeur maximal.2Configurer sur mesure le répertoire d’installation de JBoss (ex : supprimer le support des EJB 3 et de JPA)Mutualisation du répertoire d’installation rendue caduque. Main sur la production _._Un JBoss qui démarre plus vite. Pas d’impact sur le code.3Isoler le déploiement de l’applicationLire la documentation JBoss. Augmentation possible de la PermGen.Risque nul. Simple configuration. Configuration embarquée dans l’EAR.\nConfigurer le classloader de l’application Pour mettre en œuvre la solution n°3 concernant à « scoper » l’application, il est nécessaire de configurer le chargement des classes de JBoss . Une description détaillée de son fonctionnement est disponible sur la page JBossClassLoadingUseCases du wiki de JBoss. Dans notre cas, La configuration des classes loaders nécessaire est deployment scoped et Java2ParentDelegation désactivé. Cette configuration est représentée par la figure ci-contre.\nCette configuration présente les 2 avantages suivants :\nLes JARs embarqués dans l\u0026rsquo;EAR priment sur celles fournies par JBoss 5.1 et le JRE. Chaque application déployée sur le même serveur possède son propre UnifiedLoaderRepository (ULR). Le chargement des classes est isolé et n\u0026rsquo;interfère pas. Elles sont également isolées du chargement des applications tierces (ex: jmx-console et admin-console). La configuration du fichier jboss-app.xml à déposer dans le répertoire META-INF de l’EAR est décrite sur la page ClassLoadingConfiguration du wiki JBoss. En voici un exemple : [gist id=\u0026ldquo;4451751\u0026rdquo;]\nConfiguration maven Le plugin maven-ear-plugin permet de générer ce fichier jboss-app.xml : [gist id=\u0026ldquo;4451788\u0026rdquo;]\nConclusion Scoper l’EAR déployé sur JBoss vous laisse la possibilité de choisir la version des frameworks que vous souhaitez et de ne pas vous le laisser imposer. Vous pourrez ainsi utiliser Hibernate comme implémentation de JPA 2 (en mode embarqué avec le support offert par Spring), Hibernate Validator 4 comme implémentation de la JSR 303 Bean Validation ou même SLF4J 1.6 et Logback 1.0.9 pour gérer vos traces. A des fins de debug, tout client JMX permet de consulter les classes disponibles dans l’UnifiedLoaderRepository. Enfin, pour un réglage plus fin du classloader, et bien que je n’ai pas eu besoin d‘y recourir, une configuration avancée du jboss-classloading.xml est a priori possible.\nRéférences :\nJBossClassLoadingUseCases : https://community.jboss.org/wiki/JBossClassLoadingUseCases ClassLoadingConfiguration : https://community.jboss.org/wiki/ClassLoadingConfiguration A Look Inside JBoss Microcontainer\u0026rsquo;s ClassLoading Layer : http://java.dzone.com/articles/jboss-microcontainer-classloading Demystifying the JBoss5 jboss-classloading.xml file : http://phytodata.wordpress.com/2010/10/21/demystifying-the-jboss5-jboss-classloading-xml-file/ Configuration maven Le plugin maven-ear-plugin permet de générer ce fichier jboss-app.xml : Lors de la migration d’une application d’un serveur d’application vers un autre, il est fréquent d’être confronté à des problématiques de conflits de librairies. C’est par exemple le cas lorsqu’une application initialement déployée sur un Websphere Application Server 6.1 doit migrer sur JBoss 5.1 EAP (version commerciale de JBoss AS). Pour rappel, WAS 6.1 implémente J2EE 1.4 et s’exécute donc sur Java 5. Quant à JBoss 5.1 EAP, il implémente la norme Java EE 5, embarque donc de nombreuses implémentations des standards tels que JPA 1, JSF 1.2 et JAX-WS 1, et tourne sur Java 6.\nPour illustration, prenons une application s’appuyant sur Hibernate 3.6 pour sa couche de persistance et JAXB 2.2 pour le marshalling lors d’appel de web services. Ces 2 librairies sont embarquées dans le répertoire lib de son EAR et ne posent pas de problèmes particuliers à WAS 6.1. Par contre, sur JBoss 5.1 EAP, c’est un tout autre problème. En effet, son implémentation JPA repose sur la version 3.3 d\u0026rsquo;Hibernate. Qui plus est, JAXB 2.1 a été intégrée dans Java 6. Si vous tentez de déployer une telle application sur un JBoss installé avec la configuration par défaut, il y’a de fortes chances que vous tombiez sur l’une ou l’autre des exceptions suivantes : ClassCastException, NoSuchMethodException, IllegalAccessErrors, VerifyError. A ce que j’ai compris en parcourant la documentation mais également déduis de mes tests, différents mécanismes permettent d’expliquer ces comportements :\nPar défaut, lors du chargement d’une classe, le classloader de l’EAR va commencer par demander à son classloader parent (en l’occurrence celui de JBoss) de trouver la classe. Ainsi, c’est par exemple la classe Session d’Hibernate 3.3 qui sera chargée et non celle de la version 3.6 comme attendu. Il s’agit du comportement standard d’un classloader. Et c’est ce qu’on appelle communément le « j2se classloading compliance ». Sous WAS, cette stratégie de chargement peut être changée en paramétrant le classloader en PARENT_LAST. Les classes chargées par d’autres applications déployées sur la même instance de JBoss peuvent être partagées par votre application. Par exemple, la console d’admin JBoss admin-console.war embarque sa propre version de Richfaces et de Seam et peut, malgré elle, vous en faire bénéficier. Solutions étudiées Pour mener à bien la migration, plusieurs pistes ont été étudiées : SolutionsInconvénientsAvantages1Downgrader les versions des frameworks pour utiliser celles embarquées dans JBoss 5.1Important travail de refactoring pour combler les fonctionnalités manquantes. Bugs existants récupérés.Respect de la norme Java EE 5. Support éditeur maximal.2Configurer sur mesure le répertoire d’installation de JBoss (ex : supprimer le support des EJB 3 et de JPA)Mutualisation du répertoire d’installation rendue caduque. Main sur la production _._Un JBoss qui démarre plus vite. Pas d’impact sur le code.3Isoler le déploiement de l’applicationLire la documentation JBoss. Augmentation possible de la PermGen.Risque nul. Simple configuration. Configuration embarquée dans l’EAR.\nConfigurer le classloader de l’application Pour mettre en œuvre la solution n°3 concernant à « scoper » l’application, il est nécessaire de configurer le chargement des classes de JBoss . Une description détaillée de son fonctionnement est disponible sur la page JBossClassLoadingUseCases du wiki de JBoss. Dans notre cas, La configuration des classes loaders nécessaire est deployment scoped et Java2ParentDelegation désactivé. Cette configuration est représentée par la figure ci-contre.\nCette configuration présente les 2 avantages suivants :\nLes JARs embarqués dans l\u0026rsquo;EAR priment sur celles fournies par JBoss 5.1 et le JRE. Chaque application déployée sur le même serveur possède son propre UnifiedLoaderRepository (ULR). Le chargement des classes est isolé et n\u0026rsquo;interfère pas. Elles sont également isolées du chargement des applications tierces (ex: jmx-console et admin-console). La configuration du fichier jboss-app.xml à déposer dans le répertoire META-INF de l’EAR est décrite sur la page ClassLoadingConfiguration du wiki JBoss. En voici un exemple : [gist id=\u0026ldquo;4451751\u0026rdquo;]\nConfiguration maven Le plugin maven-ear-plugin permet de générer ce fichier jboss-app.xml : [gist id=\u0026ldquo;4451788\u0026rdquo;]\nConclusion Scoper l’EAR déployé sur JBoss vous laisse la possibilité de choisir la version des frameworks que vous souhaitez et de ne pas vous le laisser imposer. Vous pourrez ainsi utiliser Hibernate comme implémentation de JPA 2 (en mode embarqué avec le support offert par Spring), Hibernate Validator 4 comme implémentation de la JSR 303 Bean Validation ou même SLF4J 1.6 et Logback 1.0.9 pour gérer vos traces. A des fins de debug, tout client JMX permet de consulter les classes disponibles dans l’UnifiedLoaderRepository. Enfin, pour un réglage plus fin du classloader, et bien que je n’ai pas eu besoin d‘y recourir, une configuration avancée du jboss-classloading.xml est a priori possible.\nRéférences :\nJBossClassLoadingUseCases : https://community.jboss.org/wiki/JBossClassLoadingUseCases ClassLoadingConfiguration : https://community.jboss.org/wiki/ClassLoadingConfiguration A Look Inside JBoss Microcontainer\u0026rsquo;s ClassLoading Layer : http://java.dzone.com/articles/jboss-microcontainer-classloading Demystifying the JBoss5 jboss-classloading.xml file : http://phytodata.wordpress.com/2010/10/21/demystifying-the-jboss5-jboss-classloading-xml-file/ Conclusion Scoper l’EAR déployé sur JBoss vous laisse la possibilité de choisir la version des frameworks que vous souhaitez et de ne pas vous le laisser imposer. Vous pourrez ainsi utiliser Hibernate comme implémentation de JPA 2 (en mode embarqué avec le support offert par Spring), Hibernate Validator 4 comme implémentation de la JSR 303 Bean Validation ou même SLF4J 1.6 et Logback 1.0.9 pour gérer vos traces. A des fins de debug, tout client JMX permet de consulter les classes disponibles dans l’UnifiedLoaderRepository. Enfin, pour un réglage plus fin du classloader, et bien que je n’ai pas eu besoin d‘y recourir, une configuration avancée du jboss-classloading.xml est a priori possible.\nRéférences :\nJBossClassLoadingUseCases : https://community.jboss.org/wiki/JBossClassLoadingUseCases ClassLoadingConfiguration : https://community.jboss.org/wiki/ClassLoadingConfiguration A Look Inside JBoss Microcontainer\u0026rsquo;s ClassLoading Layer : http://java.dzone.com/articles/jboss-microcontainer-classloading Demystifying the JBoss5 jboss-classloading.xml file : http://phytodata.wordpress.com/2010/10/21/demystifying-the-jboss5-jboss-classloading-xml-file/ ","link":"https://javaetmoi.com/2013/01/isoler-classloader-ear-jboss/","section":"posts","tags":["classloader","hibernate","javaee","jboss","jpa","maven","was"],"title":"Isoler le classloader de son EAR sous JBoss"},{"body":"","link":"https://javaetmoi.com/tags/was/","section":"tags","tags":null,"title":"Was"},{"body":"","link":"https://javaetmoi.com/pages/","section":"pages","tags":null,"title":"Pages"},{"body":"Développant des applications basées sur le framework Spring depuis 2006 et ayant à mon actif 2 certifications Spring, cette page recense les articles les plus pertinents postés sur ce blog.\nCertifications Spring Core Spring 3.0 Certification Mock Exam : examen blanc de 50 questions, en ligne ou au format PDF, permettant de préparer la Certification Core Spring 3.0. Enterprise Spring Integration Certification Mock Exam : examen blanc permettant de préparer la certification Enterprise Integration with Spring (EIwS 1.x). Au programme, 25 questions réparties sur les thématiques Web Services, REST, Remoting, JMS, Transaction, Spring Batch et Spring Integration. Certified Spring Enterprise Integration Specialist Study Notes: guide de révision permettant de préparer la certification Spring Enterprise Integration Specialist. Les sujets couverts sont Spring Batch, Spring Integration, Spring Remoting, Spring WS, Srping REST, Spring JMS et les transactions XA. Spring Batch Parallélisation de traitements batchs : partant de l’expérience acquise sur un batch indexant des données dans le moteur de recherche Elasticsearch , cet article explique pas à pas comment mettre en œuvre 2 des techniques de parallélisationset de partitionnement proposées nativement par Spring Batch.\nSpring Batch s’auto-nettoie : tasklet permettant de nettoyer l\u0026rsquo;historique Spring Batch.\nIndexation Elasticsearch avec Spring Batch : l’indexation de la base de données musicale MusicBrainz illustre l’utilisation de tasklets de suppression, de création et de configuration d’un index Elasticsearch. Reader JDBC, processor et writer Elasticsearch sont également mis à l’épreuve dans une tasklet de type chunk.\nEtude de cas Spring Batch: support de présentation d\u0026rsquo;un retour d\u0026rsquo;expérience sur la migration d\u0026rsquo;un batch existant vers Spring Batch.\nSpring Boot Introduction à Spring Boot : slides présentant les grands principes de Spring Boot., démystifiant le fonctionnement de l’auto-configuration puis montrant comment Spring Boot permet de simplifier encore davantage les tests. Migrer vers Spring Boot : présente les différentes étapes qui ont été nécessaires pour migrer l\u0026rsquo;application démo Spring Petclinic de Spring Framework vers Spring Boot. Migration Spring MVC vers Spring WebFlux : étude de cas de la migration d\u0026rsquo;une application démo basée sur Spring Boot 2 Générateur de squelette d’application basé sur Spring Initializr : explique comment créer une version spécialisée de Spring Initializr en prenant pour exemple la configuration du openapi-generator-maven-plugin Spring MVC Validation HTML 5 avec Spring MVC et Bean Validation : explique comment étendre le tag JSP InputTag de Spring MVC pour lui faire générer du code HTML 5 de validation de formulaires côté navigateur à partir des contraintes Bean Validation (JSR 330) Démystifier l’annotation @SessionAttributes de Spring MVC : couplée à l\u0026rsquo;annotation @ModelAttribute, @SessionAttributes permet de simuler une portée conversation. Définition d\u0026rsquo;un modèle, tests unitaires et libération de la mémoire sont expliqués. Spring Cloud Architecture Microservices avec Spring Cloud : découvrez comment bâtir une architecture microservices avec Spring Cloud, Netflix OSS, Zipkin et Docker. Désendettement de Spring Cloud Netflix : retour d\u0026rsquo;expérience sur le désendettement de Spring Cloud Netflix : migration de Zuul 1 vers Spring Cloud Gateway, de Ribbon vers Spring Cloud Loadbalancer et de Hystrix vers Spring Cloud Circuit Breaker / Resilience4j. Spring Framework Configuration de Spring en Java : explique comment configurer le contexte applicatif d\u0026rsquo;une application Spring avec le langage Java, sans XML. Les annotations @Configuration, @Bean, @Import, @ComponentScan, @Scope et @EnableXXX y sont décrites. Une application démo et un test unitaire basés sur Spring MVC, Spring Security, Spring Data JPA et Hibernate illustre l\u0026rsquo;article.\nArchitecture d\u0026rsquo;un moteur d\u0026rsquo;indexation : décrit l’architecture mise en œuvre pour indexer des données dans Elasticsearch met à l’épreuve Spring AOP pour intercepter les mises à jour des données et Spring Integration pour les indexer en temps réel.\nDbSetup, une alternative à DbUnit : présente quelles sont les facilités qu\u0026rsquo;offre le framework DbSetup pour alimenter une base de données et montre comment l\u0026rsquo;intégrer avec Spring Test, notamment en utilisant le rollback pattern.\nSupport du VFS 2 de JBoss 5 dans Spring 4 : présentation du projet spring4-vfs2-suppor t permettant de déployer une application basée sur Spring Framework 4.0 dans un JBoss AS 5 ou un JBoss 5.x EAP.\nModern Entreprise Java Architecture with Spring 4.1 : prise de note de la conférence donnée par Juergen Hoeler lors de Devoxx France 2015 sur l\u0026rsquo;état de l\u0026rsquo;art des applications basées sur Spring et les nouveautés de Spring Framework 4.0 et 4.1.\nDésendettement du projet ehcache-spring-annotations : guide de migration du projet ehcache-spring-annotations vers le support de cache du framework Spring.\nL\u0026rsquo;offre Spring et les bases : support de présentation d\u0026rsquo;un workshop zoomant sur la richesse du portfolio Spring et introduisant aux fondamentaux de Spring Framework.\nSpring Petclinic Découvrez les forks de Spring Petclinic : cet article commence par présenter techniquement l\u0026rsquo;application démo Spring Petclinic puis dresse un panorama des forks regroupés dans l\u0026rsquo;organisation GitHub Spring Petclinic : React, AngularJS, Microservices avec Spring Boot, plain old Spring Framework \u0026hellip; Image Docker pour Spring Boot Petclinic: explique comment construire une image Docker de Spring Petclinic à l\u0026rsquo;aide du plugin pour Maven docker-maven-plugin développé par l’équipe de Spotify. Découvrir Kotlin en migrant une webapp Spring Boot : guide de migration de l\u0026rsquo;application démo Spring Petclinic de Java vers Kotlin. Intégrer un Chatbot dans une webapp Java avec LangChain4j : guide d\u0026rsquo;intégration étape par étape d\u0026rsquo;un assistant virtuel dans une application de gestion Java à l\u0026rsquo;aide du framework LangChain4j, de son starter Spring Boot et des LLM OpenAI et Azure OpenAI. ","link":"https://javaetmoi.com/spring/","section":"pages","tags":null,"title":"Spring"},{"body":"","link":"https://javaetmoi.com/tags/buildhive/","section":"tags","tags":null,"title":"Buildhive"},{"body":"","link":"https://javaetmoi.com/tags/cloudbees/","section":"tags","tags":null,"title":"Cloudbees"},{"body":"Suite à une question qui m’a récemment été posée sur Github, j’ai réalisé que ce que j’avais mis en place pour des besoins personnels pouvait intéresser d’autres développeurs.\nDans ce billet, je vais donc vous expliquer comment créer votre propre usine logicielle. Déployée à cheval sur GitHub et l’offre DEV@Cloud de CloudBees, vous y retrouverez les briques les plus classiques : SCM, intégration continue, dépôt de binaires, bug tracker, wiki … Le gain : à chaque commit poussé dans GitHub, votre code est compilé, testé unitairement puis déployé dans un repository maven public dédié aux Snapshots. Par ailleurs, vous pourrez effectuer des releases maven en local depuis votre poste de développement ; les artefacts construits seront mis à disposition dans un repository maven dédié. Tout développeur pourra librement référencer l’un ou l’autre de ces repository et utiliser votre code.\nEn bonus, si vous développez des projets open source, vous n’aurez même pas à sortir votre carte bancaire. Composants de l’usine de développement Le tableau ci-dessous liste les différentes briques de l’usine de développement ainsi que les motivations qui m’ont poussé à les choisir. Brique de l’usine logicielleOutilPlateforme****RaisonsGestionnaire de Code Source (SCM)GitGitHubPour utiliser la pleine puissance de Git, bridé jusque-là par l’utilisation en entreprise du bridge git-svn.Outil de buildMavenPoste de Dev + CloudbeesL’incontournable maven. Mais cela aurait pu être l’occasion de tester Gradle.Intégration ContinueJenkinsCloudBeesUn comble : probablement celui que je connaissais le moins par rapport à Continium, Bamboo et TeamCity.Dépôt de binairesRepository MavenCloudbeesOffre de base de CloudBees suffisante. Accès par webdavEspace documentaireWikiGitHubPages versionnées avec Git Syntaxte MarkDown Le XWiki de CloudBees aurait pu être une alternativeBugTrackerNavigateur WebGitHubProjets OSS personnels pas suffisamment actifs pour bénéficier d’un Jira (ni même d’une licence JRebel) Afin de vous donner une idée du résultat, je vous invite à jeter un coup d’œil aux différentes URLs :\nJenkins DEV@Cloud : https://javaetmoi.ci.cloudbees.com Repository maven : https://repository-javaetmoi.forge.cloudbees.com/ Compte github : https://github.com/arey Pré-requis 2 prérequis sont nécessaires au déploiement d’une telle usine de développement :\nDisposer d’un compte GitHub et d’un repository contenant un projet java déjà mavenisé Avoir accès à la plateforme de build de CloudBees, soit en souscrivant à l’une des offres gratuites ou payantes, soit en souscrivant au Free FOSS Programm Configuration Maven Afin de pouvoir intégrer un projet mavenisé dans l’usine de développement, il est préalablement nécessaire de compléter sa configuration maven pour prendre en compte :\nLe gestionnaire de code source pour que maven ait accès en lecture / écriture au repository git distant (hébergé ici sur GitHub), ce qui est par exemple nécessaire pour tagger et faire des releases maven. Les repository maven des releases et des snapshots, ce qui est utile à Jenkins ou au plugin release de maven pour déployer un artefact, et par maven pour télécharger des artefacts. La configuration de l’ extension maven wagon-webdav, utile lors du déploiement d’un artefact sur le repo maven CloudBees utilisant le protocole webdav. Les credentials d’accès en écriture au webdav, là encore utile pendant la phase de déploiement d’un artefact. Toute cette configuration est détaillée dans un précédent billet intitulé Release Maven sous Windows d’un projet GitHub déployé sur CloudBees. Vous y trouverez notamment comment configurer les différentes balises maven au travers 2 fichiers :\npom.xml : , , et settings.xml : Gage de son intérêt, le projet github maven-config-github-cloudbees à l’origine de l’article a été forké par Ryan Cambell et est désormais proposé dans la Cloudbees Community de GitHub.\nUne fois le pom.xml commité dans GitHub avec le reste du code source, le build Jenkins correspondant peut être configuré.\nConfiguration Jenkins Depuis la console d’administration de Jenkins, vérifier que le Jenkins GIT plugin soit installé puis installer le GitHub plugin.\nDans la section CloudBees DEV@Cloud Authorization, configurer l’URL du chemin d’accès au repository Github qui sera utilisée par le plugin GitHub:\nDans la section Gestion de code source du build Jenkins, sélectionner l’option Git Repositories puis renseigner le Repository URL. La syntaxe à utiliser est la suivante : https://github.com//.gitExemple : https://github.com/arey/hibernate-hydrate.git Afin que Jenkins lance le build lors de la réception d’un hook en provenance de GitHub, sélectionner la case Build when a change is pushed to GitHub dans le panneau ci-dessous : La version de maven, le chemin vers le pom.xml racine ainsi que le goal à exécuter peuvent être configurés dans la section Build : Lorsqu’aucun goal n’est précisé, Jenkins exécute un install.\nA la fin du build, on indique à Jenkins de déployer les artefacts dans le repository CloudBees des Snapshots : Afin d’exploiter au mieux le plugin GitHub de Jenkins et laisser Jenkins configurer les hooks dans GitHub, il est possible de renseigner votre login / mot de passe dans l’encart GitHub Web Hook accessible depuis le menu Administration Jenkins \u0026gt; Configurer le Système. Dernière étape de la mise en place de notre usine de développement : la configuration de GitHub.\nConfiguration GitHub Pour que Jenkins soit notifié à chaque push dans GitHub et relancer ainsi le build maven configuré précédemment, il est nécessaire de configurer un Hook web dans GitHub. La WebHook URL doit référencer votre forge logicielle CloudBees. Syntaxe : https:// .ci.cloudbees.com/github-webhook/ Exemple : https://javaetmoi.ci.cloudbees.com/github-webhook/ Cette configuration n’est a priori pas nécessaire si vous utilisez le plugin GitHub Jenkins. Ce dernier se charge en effet d’ajouter les WebHooks pour vous.\nPour que CloudBees ait les habilitations nécessaires pour accéder à l’ensemble de vos repository GitHub, sa clé publique doit être ajoutée dans la partie SSH Keys accessible via le menu d’administration de GitHub : En principe, si je n’ai rien omis de mentionner dans ce guide, tout est prêt. Et pour vérifier que votre usine de développement est opérationnelle, vous avez le choix entre :\npousser une modification dans votre repository GitHub ou simuler un hook depuis GitHub. Conclusion Suivant CloudBees depuis son lancement il y’a plus de 2 ans, j’ai eu la chance de pouvoir bénéficier début 2012 de l’offre gratuite Free and Open-Source Software. Après avoir passé un peu de temps au départ pour mettre en place mon usine, j’en suis aujourd’hui pleinement satisfait et je serais prêt à l’expérimenter en entreprise.\nN’ayant utilisé qu’une infime partie des services proposés par CloudBees, de nombreuses découvertes s’offrent encore à moi : utiliser le plugin release de Jenkins, tester SauceLabs ou bien encore déployer une application web sur la plateforme RUN@CloudBees.\nApparu quelques mois après mes débuts sur DEV@cloud, CloudBees propose le produit BuildHive aux développeurs utilisant GitHub et qui souhaitent mettre en place de l’intégration continue sur leur projet. Non seulement ce produit est gratuit, mais il simplifie considérablement la configuration de votre build, à la fois côté Jenkins que côté GitHub grâce au protocole OAuth. Tout est automatisé. Je me suis inscrit et j’ai créé mon premier build en à peine 2 minutes. Un hook sur les pull request permet même de lancer un build afin de valider le code soumis. Néanmoins, il y’a tout de même quelques limitations par rapport à la solution que vous ai proposée : pas de repository maven, impossibilité d\u0026rsquo;installer des plugins Jenkins \u0026hellip; A vous de décider lequel vous convient !\n","link":"https://javaetmoi.com/2012/12/ma-petite-usine-logicielle-github-cloudbees/","section":"posts","tags":["buildhive","cloudbees","git","github","jenkins","maven"],"title":"Ma petite usine logicielle"},{"body":"Contexte Récemment, j’ai participé au développement d’un batch capable d’indexer dans le moteur de recherche Elasticsearch des données provenant d’une base de données tierce. Développé en Java, ce batch s’appuie sur Spring Batch, le plus célèbre framework de traitements par lot de l’écosystème Java\nPlus précisément, ce batch est décomposé en 2 jobs Spring Batch, très proches l’un de l’autre :\nle premier est capable d’initialiser à partir de zéro le moteur de recherche et le second traite uniquement les mouvements quotidiens de données. Problématique Au cours du traitement batch, l’exécution de la requête par Oracle pour préparer son curseur a été identifiée comme l’opération la plus couteuse, loin devant la lecture des enregistrements en streaming à travers le réseau, leur traitement chargé de construire les documents Lucene à indexer ou leur écriture en mode bulk dans ElasticSearch. A titre d’exemple, sur des volumétries de production, la préparation côté serveur Oracle d’une requête SQL ramenant 10 millions d’enregistrement peut mettre jusqu’à 1h30.\nAvec pour objectif que le batch passe sous le seuil de 2h à moindre coût, 2 axes d’optimisations ont été étudiés : diminuer le temps d’exécution par Oracle et diminuer le temps de traitement.\nSolutions étudiées Les optimisations d’un DBA consistant à utiliser des tables temporaires et des procédures stockées n’ont pas été concluantes : trop peu de gains (10 à 20%) pour une réécriture partielle de notre batch, et avec le risque d’engendrer des régressions.\nAprès mesures et calculs, l’utilisation de la pagination sur des plages de 100, de 1 000 ou même de 10 000 enregistrements a également été écartée. Dans notre contexte, cela aurait dégradé les performances. Le choix de rester sur l’utilisation d’un curseur JDBC a été maintenu.\nA cette occasion, nous avons remarqué que les temps de mise en place d’un curseur Oracle pour préparer 1 millions ou 10 millions d’enregistrements étaient du même ordre de grandeur.\nUtilisant déjà l’une des techniques proposées par Spring Batch pour paralléliser notre traitement batch, pourquoi ne pas refaire appel à ses loyaux services ?\nSpring Batch et ses techniques de parallélisation Comme indiqué dans son manuel de référence, Spring Batch propose nativement 4 techniques pour paralléliser les traitements :\nMulti-threaded Step (single process) Parallel Steps (single process) Remote Chunking of Step (multi process) Partitioning a Step (single or multi process) Pour optimiser le batch, 2 de ces techniques ont été utilisées.\nLe Remote Chunking of Step a été écarté d’office. Dans le contexte client, installer un batch en production est déjà forte affaire. Alors en installer plusieurs interconnectés, je n\u0026rsquo;ose pas me l’imaginer : à étudier en dernier recours.\nLe Multi-threaded Step est sans doute la technique la plus simple à mettre en œuvre. Seule un peu de configuration est suffisante : l’ajout d’un taskExecutor sur le tasklet à paralléliser. La conséquence majeure est que les items peuvent être traités dans le désordre.\nUn prérequis à cette technique est que les ItemReader et ItemWiter soient stateless ou thread-safe. La classe de JdbcCursorItemReader de Spring Batch hérite de la classe AbstractItemCountingItemStreamItemReader qui n\u0026rsquo;est pas thread-safe. L’utilisation d’un wrapper synchronized aurait pu être envisagée si la classe fille de JdbcCursorItemReader développée pour les besoins du batch ne s’appuyait pas elle-même sur un RowMapper avec état reposant sur l’ordre de lecture des éléments.\nLes Parallel Steps ont été mises en œuvre dès le début du batch pour traiter en parallèle des données de types différents (ex : Musique et Film). De par leurs jointures, les requêtes SQL de chacun différaient. Avant optimisation, 9 steps étaient déjà exécutés en parallèle par ce biais.\nQuatrième et dernière technique, celle du Partitioning a Step est la piste que nous avons étudiée pour diminuer le temps d’exécuter des 3 steps les plus longs. Elle consiste à partitionner les données selon un critère pertinemment choisi (ex : identifiant, année, catégorie), le but étant d’obtenir des partitions de taille équivalente et donc de même temps de traitement.\nBien qu’il ne fut pas parfaitement linéairement réparti, le discriminant retenu pour le batch a été l’identifiant fonctionnel des données à indexer. Les données ont été découpées en 3 partitions. Comme attendu, bien que le volume de données soit divisé par trois, le temps de mise en place du curseur Oracle ne diminua pas. Par contre, le temps de traitements fut divisé par 3, faisant ainsi passer le temps d’exécution du batch de 3h à 2h.\nMalgré une augmentation du nombre de requêtes exécutées simultanément, la base Oracle n’a pas montré de faiblesse. Une surcharge aurait en effet pu ternir ce résultat.\nExemple de mise en œuvre Après ce long discours, rien de tel qu’un peu d’exercice. Pour les besoins de ce billet, et afin de capitaliser sur l’expérience acquise sur la configuration Spring Batch, j’ai mis à jour le projet spring-batch-toolkit hébergé sur GitHub. Le fichier blog-parallelisation.zip contient l’ensemble du code source mavenisé.\nJe suis parti d’un cas d’exemple des plus simples : un batch chargé de lire en base de données des chefs-d’œuvre puis de les afficher sur la console.\nEn base, il existe 2 types de chefs-d’œuvre : les films et les albums de musique. Comme le montre le diagramme du modèle physique de données ci-contre, chaque type de chef-d\u0026rsquo;oeuvre dispose de sa propre table : respectivement MOVIE et MUSIC_ALBUM. Les données communes sont normalisées dans la table MAESTERPIECE.\nEn ce qui concerne le design du batch, le job peut être décomposé en 2 steps exécutés en parallèle, l’un chargé de traiter les albums de musique, l’autre les films. Une fois les 2 steps terminés, un dernier step affiche le nombre total de chefs-d’œuvre traités.\nAvec une volumétrie de film supérieure à celle des albums, le step des films est décomposé en 2 partitions exécutées en parallèle.\nLe besoin est simple. Partons d’une démarche TDD et commençons par l’écriture d’un test d’intégration.\nDans un premier temps, attaquons-nous aux données de test, sans doute ce qu’il y’a de plus fastidieux. Exécuté au moment de la création de la base de données embarquée, le script SQL TestParallelAndPartitioning.sql contient les ordres DDL du schéma ci-dessous ainsi que des requêtes INSERT permettant de l’alimenter.\nVoici un exemple de données de tests :\nInsert \u0026lt;strong\u0026gt;into\u0026lt;/strong\u0026gt; MASTERPIECE (MASTERPIECE_ID, NAME, YEAR, GENRE) \u0026lt;strong\u0026gt;values\u0026lt;/strong\u0026gt; (2, \u0026#39;Star Wars: Episode IV - A New Hope!\u0026#39;, 1977, \u0026#39;Movie\u0026#39;); Insert \u0026lt;strong\u0026gt;into\u0026lt;/strong\u0026gt; MOVIE (MOVIE_ID, MASTERPIECE_ID, REALISATOR, ACTORS) \u0026lt;strong\u0026gt;values\u0026lt;/strong\u0026gt; (1, 2, \u0026#39;George Lucas\u0026#39;, \u0026#39;Mark Hamill, Harrison Ford, Carrie Fisher\u0026#39;); … Insert \u0026lt;strong\u0026gt;into\u0026lt;/strong\u0026gt; MASTERPIECE (MASTERPIECE_ID, NAME, YEAR, GENRE) \u0026lt;strong\u0026gt;values\u0026lt;/strong\u0026gt; (4, \u0026#39;The Wall\u0026#39;, 1979, \u0026#39;Music\u0026#39;); Insert \u0026lt;strong\u0026gt;into\u0026lt;/strong\u0026gt; MUSIC_ALBUM (ALBUM_ID, MASTERPIECE_ID, BAND) \u0026lt;strong\u0026gt;values\u0026lt;/strong\u0026gt; (3, 4, \u0026#39;Pink Floyd\u0026#39;); Au total, 11 albums et 8 films sont référencés.\nLa classe de tests TestParallelAndPartitioning repose sur Spring Test, Spring Batch Test et JUnit.\nComme le montre l’extrait de code suivant, la classe JobLauncherTestUtils issue de Spring Batch Test permet d’exécuter notre unique job sans avoir à lui passer de paramètres ainsi que d’attendre la fin de son traitement.\n@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration public class TestParallelAndPartitioning extends AbstractSpringBatchTest { @Autowired private JobLauncherTestUtils testUtils; @Test public void launchJob() throws Exception { // Launch the parallelAndPartitioningJob JobExecution execution = testUtils.launchJob(); // Batch Status assertEquals(ExitStatus.COMPLETED, execution.getExitStatus()); // Movies assertEquals(\u0026#34;8 movies\u0026#34;, 8, getStepExecution(execution, \u0026#34;stepLogMovie\u0026#34;).getWriteCount()); // Music Albums StepExecution stepExecutionMusic = getStepExecution(execution, \u0026#34;stepLogMusicAlbum\u0026#34;); assertEquals(\u0026#34;11 music albums\u0026#34;, 11, stepExecutionMusic.getWriteCount()); Object gridSize = ExecutionContextTestUtils.getValueFromStep(stepExecutionMusic, \u0026#34;SimpleStepExecutionSplitter.GRID_SIZE\u0026#34;); assertEquals(\u0026#34;stepLogMusicAlbum divided into 2 partitions\u0026#34;, 2L, gridSize); StepExecution stepExecPart0 = getStepExecution(execution, \u0026#34;stepLogMusicAlbumPartition:partition0\u0026#34;); assertEquals(\u0026#34;First partition processed 6 music albums\u0026#34;, 6, stepExecPart0.getWriteCount()); StepExecution stepExecPart1 = getStepExecution(execution, \u0026#34;stepLogMusicAlbumPartition:partition1\u0026#34;); assertEquals(\u0026#34;Second partition processed 5 music albums\u0026#34;, 5, stepExecPart1.getWriteCount()); } L’exécution du job est suivie d’assertions :\nLe job s’est terminé avec succès Le step des films stepLogMovie a traité les 8 films attendus Le step des albums de musiques stepLogMusicAlbum a traité les 11 films attendus Et en y regardant de plus près, le step des albums a été décomposé en deux « sous-steps », stepLogMusicAlbumPartition:partition0 et stepLogMusicAlbumPartition:partition1 qui correspondent, comme leur nom l’indique, à chacune des 2 partitions. Les 11 films ont été séparés en 2 lots de capacités avoisinantes, à savoir de 6 et 5 films. Avec 3 partitions, on aurait pu s’attendre à un découpage de 4-4-3. La configuration du batch commence par la déclaration de beans d’infrastructure Spring relativement génériques pour des tests :\nUne base de données en mémoire H2 initialisée avec le schéma des 6 tables de Spring Batch Le gestionnaire de transactions utilisé par Spring Batch pour gérer ses chunk Le JobRespository dans lequel seront persistés l’historique et le contexte d’exécution des batchs Les beans SimpleJobLauncher et JobLauncherTestUtils permettant d’exécuter le job testé Ces beans sont déclarés dans le fichier AbstractSpringBatchTest-context.xml :\n\u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;beans xmlns=\u0026#34;http://www.springframework.org/schema/beans\u0026#34; xmlns:jdbc=\u0026#34;http://www.springframework.org/schema/jdbc\u0026#34; xmlns:p=\u0026#34;http://www.springframework.org/schema/p\u0026#34; xmlns:xsi=\u0026#34;http://www.w3.org/2001/XMLSchema-instance\u0026#34; xmlns:batch=\u0026#34;http://www.springframework.org/schema/batch\u0026#34; xmlns:c=\u0026#34;http://www.springframework.org/schema/c\u0026#34; xsi:schemaLocation=\u0026#34; http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.2.xsd http://www.springframework.org/schema/batch http://www.springframework.org/schema/batch/spring-batch-2.2.xsd http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc-3.2.xsd \u0026#34;\u0026gt; \u0026lt;!-- Create an in-memory Spring Batch database from the schema included into the spring-batch-core module --\u0026gt; \u0026lt;jdbc:embedded-database id=\u0026#34;dataSource\u0026#34; type=\u0026#34;H2\u0026#34;\u0026gt; \u0026lt;jdbc:script location=\u0026#34;classpath:org/springframework/batch/core/schema-drop-h2.sql\u0026#34; /\u0026gt; \u0026lt;jdbc:script location=\u0026#34;classpath:org/springframework/batch/core/schema-h2.sql\u0026#34; /\u0026gt; \u0026lt;/jdbc:embedded-database\u0026gt; \u0026lt;!-- Datasource transaction manager used for the Spring Batch Repository and batch processing --\u0026gt; \u0026lt;bean id=\u0026#34;transactionManager\u0026#34; class=\u0026#34;org.springframework.jdbc.datasource.DataSourceTransactionManager\u0026#34; p:dataSource-ref=\u0026#34;dataSource\u0026#34; /\u0026gt; \u0026lt;!-- Helps with testing (autowired, injected in the test instance) --\u0026gt; \u0026lt;bean class=\u0026#34;org.springframework.batch.test.JobLauncherTestUtils\u0026#34; lazy-init=\u0026#34;true\u0026#34; /\u0026gt; \u0026lt;!-- Starts a job execution --\u0026gt; \u0026lt;bean id=\u0026#34;jobLauncher\u0026#34; class=\u0026#34;org.springframework.batch.core.launch.support.SimpleJobLauncher\u0026#34;\u0026gt; \u0026lt;property name=\u0026#34;jobRepository\u0026#34; ref=\u0026#34;jobRepository\u0026#34; /\u0026gt; \u0026lt;/bean\u0026gt; \u0026lt;!-- In-memory database repository --\u0026gt; \u0026lt;batch:job-repository id=\u0026#34;jobRepository\u0026#34; /\u0026gt; \u0026lt;/beans\u0026gt; La majeure partie de la configuration Spring est définie dans le fichier TestParallelAndPartitioning-context.xml d’où sont tirés les extraits suivants.\nEn plus du schéma nécessaire par le JobRepository persistant de Spring Batch, les 3 tables de notre exemple sont créées puis alimentées avec notre jeu de données comportant 19 chefs-d’œuvre :\n\u0026lt;!-- Initialize database with 8 movies and 11 music albums --\u0026gt; \u0026lt;jdbc:initialize-database\u0026gt; \u0026lt;jdbc:script location=\u0026#34;classpath:com/javaetmoi/core/batch/test/TestParallelAndPartitioning.sql\u0026#34; /\u0026gt; \u0026lt;/jdbc:initialize-database\u0026gt; Un pool de threads sera utilisé pour paralléliser le job . Ce pool est dimensionné à 4 threads : un thread pour chacun des 2 parallel steps + un thread pour chacun des 2 « sous-steps » correspondants aux 2 partitions.\n\u0026lt;!-- Thread pools : 1 thread for stepLogMovie and 3 threads for stepLogMusicAlbum --\u0026gt; \u0026lt;task:executor id=\u0026#34;batchTaskExecutor\u0026#34; pool-size=\u0026#34;4\u0026#34; /\u0026gt; Vient ensuite la déclaration du job Spring Batch. L’utilisation des balises split et flow permet de mettre en œuvre les Parallel Steps. Couplée avec l’attribut task-executor, l’enchainement des Steps référencés par les flows n’est alors plus linéaire.\nLes 2 flows flowMovie et flowMusicAlbum sont exécutés en parallèle. Une fois ces 2 flows terminés, le step stepEnd terminera le job.\n\u0026lt;!-- Job combining both parallel steps and an local partitions --\u0026gt; \u0026lt;batch:job id=\u0026#34;parallelAndPartitioningJob\u0026#34;\u0026gt; \u0026lt;batch:split id=\u0026#34;splitIndexDelta\u0026#34; task-executor=\u0026#34;batchTaskExecutor\u0026#34; next=\u0026#34;stepEnd\u0026#34;\u0026gt; \u0026lt;!-- 2 parall steps. The first one will be partitioned --\u0026gt; \u0026lt;batch:flow parent=\u0026#34;flowMusicAlbum\u0026#34; /\u0026gt; \u0026lt;batch:flow parent=\u0026#34;flowMovie\u0026#34; /\u0026gt; \u0026lt;/batch:split\u0026gt; \u0026lt;!-- The stepEnd will be executed after the 2 flows flowMusicAlbum and flowMovie --\u0026gt; \u0026lt;batch:step id=\u0026#34;stepEnd\u0026#34;\u0026gt; \u0026lt;batch:tasklet\u0026gt; \u0026lt;bean class=\u0026#34;com.javaetmoi.core.batch.test.EndTasklet\u0026#34; /\u0026gt; \u0026lt;/batch:tasklet\u0026gt; \u0026lt;/batch:step\u0026gt; \u0026lt;/batch:job\u0026gt; Composé d’un seul step (sans partition), la déclaration du flow flowMusicAlbum chargée de logger les films est la plus simple. De type chunk, le step a un reader utilisant un curseur JDBC pour itérer sur la liste des films. La classe BeanPropertyRowMapper permet d’effectuer le mapping entre les colonnes du ResultSet de la requête SQL et le bean java Movie ; il se base sur le nom des colonnes et le nom des propriétés du bean.\nLe writer affiche les propriétés du bean Movie à l’aide de la méthode ToStringBuilder.reflectionToString() d’Apache Commons Lang.\nL’attribut commit-interval du chunk est fixé volontairement à 2. Ainsi, le writer est appelé tous les 2 films. Cela permet de voir plus facilement l’enchevêtrement des différents threads.\n\u0026lt;!-- The movie flow is composed of a single step that reads all movies then log them --\u0026gt; \u0026lt;batch:flow id=\u0026#34;flowMovie\u0026#34;\u0026gt; \u0026lt;batch:step id=\u0026#34;stepLogMovie\u0026#34;\u0026gt; \u0026lt;batch:tasklet\u0026gt; \u0026lt;batch:chunk writer=\u0026#34;anyObjectWriter\u0026#34; commit-interval=\u0026#34;2\u0026#34;\u0026gt; \u0026lt;batch:reader\u0026gt; \u0026lt;bean class=\u0026#34;org.springframework.batch.item.database.JdbcCursorItemReader\u0026#34; p:dataSource-ref=\u0026#34;dataSource\u0026#34;\u0026gt; \u0026lt;property name=\u0026#34;rowMapper\u0026#34;\u0026gt; \u0026lt;bean class=\u0026#34;org.springframework.jdbc.core.BeanPropertyRowMapper\u0026#34; c:mappedClass=\u0026#34;com.javaetmoi.core.batch.test.Movie\u0026#34; /\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;property name=\u0026#34;sql\u0026#34;\u0026gt; \u0026lt;value\u0026gt;\u0026lt;![CDATA[ select a.masterpiece_id as id, name, year, realisator, actors from masterpiece a inner join movie b on a.masterpiece_id=b.masterpiece_id where genre=\u0026#39;Movie\u0026#39; ]]\u0026gt;\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;/bean\u0026gt; \u0026lt;/batch:reader\u0026gt; \u0026lt;/batch:chunk\u0026gt; \u0026lt;/batch:tasklet\u0026gt; \u0026lt;/batch:step\u0026gt; \u0026lt;/batch:flow\u0026gt; Le flow chargé de traiter les films est lui aussi composé d’un seul step : stepLogMusicAlbum. Ce dernier est partitionné en 2 (propriété grid-size=\u0026ldquo;2\u0026rdquo; du handler). Le même pool de threads est utilisé pour traiter les 2 partitions. Le bean chargé de partitionner les données est référencé : partitionerMusicAlbum. Le traitement des « sous-steps » partitionnés est confié au bean stepLogMusicAlbumPartition.\n\u0026lt;!-- The music flow is composed of a single step which is partitioned --\u0026gt; \u0026lt;batch:flow id=\u0026#34;flowMusicAlbum\u0026#34;\u0026gt; \u0026lt;batch:step id=\u0026#34;stepLogMusicAlbum\u0026#34;\u0026gt; \u0026lt;!-- Executes partition steps locally in separate threads of execution --\u0026gt; \u0026lt;batch:partition step=\u0026#34;stepLogMusicAlbumPartition\u0026#34; partitioner=\u0026#34;partitionerMusicAlbum\u0026#34;\u0026gt; \u0026lt;batch:handler grid-size=\u0026#34;2\u0026#34; task-executor=\u0026#34;batchTaskExecutor\u0026#34; /\u0026gt; \u0026lt;/batch:partition\u0026gt; \u0026lt;/batch:step\u0026gt; \u0026lt;/batch:flow\u0026gt; Le bean partitionerMusicAlbum repose sur la classe ColumnRangePartitioner reprise des samples Spring Batch La clé de partition doit lui être précisé sous forme du couple nom de table / nom de colonne.\nTechniquement, cette classe utilise ces données pour récupérer les valeurs minimales et maximales de la clé. Pour se faire, 2 requêtes SQL sont exécutées. A partir, du min et du max, connaissant le nombre de partitions à créer (grid-size), elle calcule des intervalles de données de grandeur équivalente. Afin que les partitions soient de taille équivalente en termes de données, les valeurs des clés doivent être uniformément distribuées. C’est par exemple le cas avec un identifiant technique généré par une séquence base de données et pour lesquelles aucune donnée n’est supprimée (pas de trou). Les clés minValue et maxValue de chaque intervalle sont mises à disposition dans le contexte d\u0026rsquo;exécution de chaque « sous-step ».\n\u0026lt;!-- The partitioner finds the minimum and maximum primary keys in the music album table to obtain a count of rows and then calculates the number of rows in the partition --\u0026gt; \u0026lt;bean id=\u0026#34;partitionerMusicAlbum\u0026#34; class=\u0026#34;com.javaetmoi.core.batch.partition.ColumnRangePartitioner\u0026#34;\u0026gt; \u0026lt;property name=\u0026#34;dataSource\u0026#34; ref=\u0026#34;dataSource\u0026#34; /\u0026gt; \u0026lt;property name=\u0026#34;table\u0026#34; value=\u0026#34;music_album\u0026#34; /\u0026gt; \u0026lt;property name=\u0026#34;column\u0026#34; value=\u0026#34;album_id\u0026#34; /\u0026gt; \u0026lt;/bean\u0026gt; De la même manière que son cousin stepLogMovie, le bean stepLogMusicAlbumPartition est composé d’un chunk tasklet. Celui-ci référence 2 beans définis dans la suite du fichier de configuration : readerMusicAlbum et anyObjectWriter, ce dernier étant déjà utilisé par le bean stepLogMovie.\n\u0026lt;!-- Read music albums from database then write them into logs --\u0026gt; \u0026lt;batch:step id=\u0026#34;stepLogMusicAlbumPartition\u0026#34;\u0026gt; \u0026lt;batch:tasklet\u0026gt; \u0026lt;batch:chunk reader=\u0026#34;readerMusicAlbum\u0026#34; writer=\u0026#34;anyObjectWriter\u0026#34; commit-interval=\u0026#34;2\u0026#34; /\u0026gt; \u0026lt;/batch:tasklet\u0026gt; \u0026lt;/batch:step\u0026gt; Par rapport à celui en charge de la lecture des films, le bean readerMusicAlbum se démarque en 2 points :\nLa requête SQL filtre non seulement les chefs-d’œuvre par leur genre ( where genre=\u0026lsquo;Music\u0026rsquo;), mais également sur une plage d’identifiants ( and b.album_id \u0026gt;= ? and b.album_id \u0026lt;= ?) relatifs à la clé de partitionnement. Cette requête est donc dynamique. Basé sur un PreparedStatement JDBC, elle est exécutée autant de fois qu’il y’a de partitions à traiter. Les 2 paramètres de la requête (symbolisés par un ?) sont évalués dynamiquement à partir du contexte d’exécution du step. Une Spring Expression Language (SPeL) est utilisée dans la définition du bean anonyme basé sur la classe ListPreparedStatementSetter. Ceci est permis grâce à la portée du bean reader qui est de type step (scope=\u0026ldquo;step\u0026rdquo;).\n\u0026lt;!-- JdbcCursorItemReader in charge of selecting music albums by id range --\u0026gt; \u0026lt;bean id=\u0026#34;readerMusicAlbum\u0026#34; class=\u0026#34;org.springframework.batch.item.database.JdbcCursorItemReader\u0026#34; scope=\u0026#34;step\u0026#34; p:dataSource-ref=\u0026#34;dataSource\u0026#34;\u0026gt; \u0026lt;property name=\u0026#34;rowMapper\u0026#34;\u0026gt; \u0026lt;bean class=\u0026#34;org.springframework.jdbc.core.BeanPropertyRowMapper\u0026#34; c:mappedClass=\u0026#34;com.javaetmoi.core.batch.test.MusicAlbum\u0026#34; /\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;property name=\u0026#34;sql\u0026#34;\u0026gt; \u0026lt;value\u0026gt;\u0026lt;![CDATA[ select a.masterpiece_id as id, name, year, band from masterpiece a inner join music_album b on a.masterpiece_id=b.masterpiece_id where genre=\u0026#39;Music\u0026#39; and b.album_id \u0026gt;= ? and b.album_id \u0026lt;= ? ]]\u0026gt;\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;property name=\u0026#34;preparedStatementSetter\u0026#34;\u0026gt; \u0026lt;bean class=\u0026#34;org.springframework.batch.core.resource.ListPreparedStatementSetter\u0026#34;\u0026gt; \u0026lt;property name=\u0026#34;parameters\u0026#34;\u0026gt; \u0026lt;list\u0026gt; \u0026lt;!-- SPeL parameters order is important because it referes to \u0026#34;where album_id \u0026gt;= ? and album_id \u0026lt;= ?\u0026#34; --\u0026gt; \u0026lt;value\u0026gt;#{stepExecutionContext[minValue]}\u0026lt;/value\u0026gt; \u0026lt;value\u0026gt;#{stepExecutionContext[maxValue]}\u0026lt;/value\u0026gt; \u0026lt;/list\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;/bean\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;/bean\u0026gt; Après épuration des logs et ajout d’un Thread.sleep(50) dans la classe ConsoleItemWriter dans le but, voici le résultat de l’exécution du batch :\nJob: [FlowJob: [name=parallelAndPartitioningJob]] launched with the following parameters: [{timestamp=1354297881856}] Executing step: [stepLogMusicAlbum] Executing step: [stepLogMovie] Movie[realisator=George Lucas,actors=Mark Hamill, Harrison Ford, Carrie Fisher,id=2,name=Star Wars: Episode IV - A New Hope!,year=1977] Movie[realisator=Richard Marquand,actors=Mark Hamill, Harrison Ford, Carrie Fisher,id=6,name=Star Wars : Episode VI - Return of the Jedi,year=1983] Movie[realisator=Paul Verhoeven,actors=Arnold Schwarzenegger, Sharon Stone,id=7,name=Total Recal,year=1990] Movie[realisator=James Cameron,actors=Arnold Schwarzenegger,id=11,name=Terminator 2 : Judgement Day,year=1991] MusicAlbum[band=The Beatles,id=1,name=Help!,year=1965] MusicAlbum[band=The Police,id=3,name=Outlandos d\u0026#39;Amour!,year=1978] MusicAlbum[band=Metallica,id=10,name=Black Album,year=1991] MusicAlbum[band=Radiohead,id=13,name=OK Computer,year=1997] Movie[realisator=Quentin Tarantino,actors=John Travolta, Samuel L. Jackson, Uma Thurman,id=12,name=Pulp Fiction,year=1994] Movie[realisator=Peter Jackson,actors=Elijah Wood, Sean Astin,id=15,name=The Lord of the Rings: The Return of the King,year=2003] MusicAlbum[band=Pink Floyd,id=4,name=The Wall,year=1979] MusicAlbum[band=U2,id=5,name=War,year=1983] MusicAlbum[band=Muse,id=14,name=Showbiz,year=1999] MusicAlbum[band=Muse,id=16,name=The Resistance,year=2009] Movie[realisator=Christopher Nolan,actors=Leonardo DiCaprio, Marion Cotillard,id=17,name=Inception,year=2010] Movie[realisator=Christopher Nolan,actors=Christian Bale, Gary Oldman,id=18,name=The Dark Knight Rises,year=2012] MusicAlbum[band=U2,id=8,name=Achtung Baby,year=1991] MusicAlbum[band=Nirvana,id=9,name=Nevermind,year=1991] MusicAlbum[band=Saez,id=19,name=Messina,year=2012] Executing step: [stepEnd] 19 masterpiece(s) have been processed Job: [FlowJob: [name=parallelAndPartitioningJob]] completed with the following parameters: [{timestamp=1354297881856}] and the following status: [COMPLETED] Ces traces confirment que le traitement des chefs-d’œuvre est équitablement réparti dans le temps et entre les différents threads, avec une alternance de films et d\u0026rsquo;albums de musique, et des albums des 2 partitions traités en parallèle.\nConclusion Pour un effort minime, à peine quelques heures de développement, la durée d’exécution du batch a baissé de 33%, avec un débit avoisinant les 5 000 documents par secondes indexés dans ElasticSearch. Pourquoi donc s’en priver ?\nLa documentation Spring Batch doit être attentivement suivie pour ne pas tomber dans certains pièges liés à la parallélisassion. La documentation officielle, le livre Spring Batch in Action et maintenant ce billet devraient être des sources suffisantes pour comprendre et mettre en œuvre aux moins 2 des techniques proposées nativement par Spring Batch : Parallel Steps et Partitioning a Step.\n","link":"https://javaetmoi.com/2012/12/parallelisation-de-traitements-batchs-spring-batch/","section":"posts","tags":["elasticsearch","spring-batch","test"],"title":"Parallélisation de traitements batchs"},{"body":"Last month, I passed the Enterprise Integration with Spring exam (EIwS 1.x) with a score of 90%. This test is also known as Certified Enterprise Integration Specialist exam. Before passing this exam, you have to attend Enterprise Integration with Spring training from SpringSource or a Certified Partner.\nIn my last blog entry, I have published a french study guide / notes to the exam. Since, I received a few emails asking me some materials in English.\nOpposed to the Spring Core Certification, I didn’t find any mock exams for the . So I decided to create a mock exam like I did in my Core Spring 3.0 Certification Mock Exam blog entry. The questions are close to the real Enterprise Integration with Spring exam and I hope will help you in practicing for the test or to test your Spring Integration proficiently. I have tried to keep my exam accurate, based on my real exam-experience.\nTest Details Twice smaller than the real one, this mock exam is composed of 25 questions. You have 44 minutes to complete it and a minimal score of 76% to reach (19 right questions).\nRepartition of the questions by category:\nWeb Services (3) REST (3) Remoting (2) JMS (2) Transaction (2) Spring Batch (5) Spring Integration (8) Test Availability You have the choice between two formats:\nA PDF version of the Enterprise Integration with Spring mock exam : enterprise-spring-integration-certification-mock-exam.pdf Online test on Skill-guru : http://www.skill-guru.com/test/465/spring-integration-certification-mock-exam Please take the test and give me your feedback by blog comments.\n","link":"https://javaetmoi.com/2012/10/enterprise-spring-integration-certification-mock-exam/","section":"posts","tags":["certification","exam","jms","mock","remoting","rest","spring-batch","spring-framework","spring-integration","springsource","transaction","web-services"],"title":"Enterprise Spring Integration Certification Mock Exam"},{"body":"","link":"https://javaetmoi.com/tags/exam/","section":"tags","tags":null,"title":"Exam"},{"body":"","link":"https://javaetmoi.com/tags/mock/","section":"tags","tags":null,"title":"Mock"},{"body":"","link":"https://javaetmoi.com/tags/remoting/","section":"tags","tags":null,"title":"Remoting"},{"body":"","link":"https://javaetmoi.com/tags/springsource/","section":"tags","tags":null,"title":"Springsource"},{"body":"","link":"https://javaetmoi.com/tags/web-services/","section":"tags","tags":null,"title":"Web-Services"},{"body":"En l’espace de 8 mois, me voici doté d’une deuxième certification Spring. Après la certification Spring Core dont je vous ai fait écho dans mon tout premier billet, j’ai eu l’opportunité de préparer la certification Spring Integration Specialist.\nComme à l’accoutumée avec les certifications Spring, la formation officielle Spring Enterprise Integration est pré-requise. Elaborée par SpringSource et dispensée par Zenika, cette formation couvre de nombreux sujets basés sur Spring Framework 3 et différents projets du Portfolio Spring :\nMulti-Threading et Scheduling Spring Remoting Spring Web Service 2.0 (Security en annexe) REST avec Spring MVC Spring JMS Transactions locales et distribuées (JTA et XA) Spring Batch 2.1 Spring Integration 2.0 A la fin de ces 4 jours, je suis reparti avec le livre Spring Batch in Action (généreusement offert par notre formateur Arnaud, co-auteur du livre) et quelques devoirs.\nPour me préparer, à l’instar du Jeanne Boyarsky\u0026rsquo;s Spring 3.X Certification Study Notes, j’ai rédigé quelques fiches de révisions, bien plus pratiques à transporter que le livret reçu en formation. Aujourd’hui, je vous propose de vous en faire profiter.\nPrésentation des fiches de révision Ces fiches reprennent une à une toutes les questions abordées dans l’Enterprise Integration with Spring 1.x Certification Study Guide mis à disposition par SpringSource. J’ai essayé d’y répondre en m’appuyant sur le support de formation, les différents manuels de référence et le code source.\nAu regard de la formation, lorsqu’il m’a semblé que des points importants n’avaient pas été abordés, j’ai ajouté des questions/réponses repérables par leur police en italique. Vous trouverez ces fiches sous 2 formes :\nun fichier au format PDF: spring-integration-specialist-certification-study-notes-antoine.pdf une version HTML en ligne publiée à la suite de ce billet : Remoting Généralités Les concepts proposés par Spring Remoting, côté serveur comme client\nRemoting =\u0026gt; appel synchrone de méthodes distantes\nCôté serveur : concept d’Exporter permettant d’exposer à distance un bean Spring (POJO).\nCôté client : un ProxyFactoryBean chargé de créer dynamiquement un proxy masquant les appels distants et gérant la plomberie technique (connexion, exceptions …)Les bénéfices de Spring Remoting par rapport aux techniques traditionnelles d’appel de méthodes distantes\nFaible couplage avec la technologie d’accès à distance : les exceptions vérifiées sont encapsulées par Spring, Il n’est plus nécessaire d’étendre d’interfaces techniques (ex : Remote), Les services métiers peuvent directement être exposés sans modification du code, Spring s’occupe de l’enregistrement dans le registre (RMI) ou l’exposition du service en tant qu’endpoint (http). Aucun couplage avec la technologie de remoting utilisée. Changement de protocole possible par simple changement de configuration Les protocoles de Remoting supportés par Spring\nRMI(-IIOP) HTTPInvoker : protocole d’échange propriétaire à Spring permettant de sérialiser des objets java sur HTTP Hessian : protocole binaire basé sur HTTP conçu par Caucho Burlap : alternative XML à Hessian) JAX-RPC : remplacé par JAX-RS depuis Java EE 5 / Java 6 EJB sans état RMI-based Spring Remoting En quoi le remoting RMI avec Spring est moins invasif que le RMI natif ?Côté serveur :\nExposition de services POJO (RmiServiceExporter) Les interfaces des services exposés n’ont plus à étendre java.rmi.Remote le binding dans le registre RMI est effectué automatiquement par Spring Côté client :\nSpring convertit les exceptions vérifiées java.rmi.RemoteException en exceptions non vérifiées (runtime) de type RemoteAccessException Plus besoin de stub RMI : Spring génère dynamiquement le proxy (RmiProxyFactoryBean). Attention : les classes échangées doivent toujours implémenter l’interface Serializable\nSpring HTTP Invoker Comment le client et le serveur interagisse l’un avec l’autre ?Protocole propriétaire utilisant la sérialisation Java pour les paramètres en entrée et en sortie.\nL’invocation de méthodes est réalisée à l’aide d’un POST HTTP.\nUtilisation au choix de l’API du JDK ou d’Apache Commons HttpClient.Comment exposer un service en HTTP ?\nDéclarer le bean spring du service à exposer\nExporter le bean en utilisant le HttpInvokerServiceExporter\n\u0026lt;bean name=\u0026#34;/monservice\u0026#34; class=\u0026#34;o.s.r.h. HttpInvokerServiceExporter …/\u0026gt; Utiliser la DispatcherServlet de Spring MVC ou déclarer dans le web.xml une servlet par service exporté\nSpring Web Service Généralités Différences entre les Web Services et le Remoting ou le MessagingCouplage technologique lâche. On définit le contrat de service Document-Oriented entre les consommateurs et le fournisseur de service.\nBasés sur du XML, les web services sont interopérables avec d’autres plateformes que Java : .NET, C++, Ruby, PHP …\nSpring Web Services L’approche supportée par Spring-WS pour construire des web servicesSpring WS permet d’utiliser uniquement l’approche contract-first.\nNécessite de commencer par écrire la XSD ou le WSDL (plutôt que d’annoter des méthodes). Le schéma décrit les messages échangés dans le corps de la requête SOAP.\nPossibilité d’utiliser des outils pour générer une XSD à partir de messages XML d’exemple, XSD qu’il est souvent nécessaire de retoucher, là encore à l’aide d’outils (Trang, XML Spy)\nSpring WS est à même de générer dynamiquement le WSDL à partir de la XSD.Les frameworks Object-to-XML supportés par Spring OXMPour manipuler les requêtes SOAP, Spring WS propose plusieurs techniques :\nBas niveau en parsant le XML à l’aide d’API XML : JDOM, XOM, Dom4J, TrAX, W3C DOM) En utilisant le marshalling Object / XML (OXM) Par binding XPath Spring OXM supporte : JAXB 1 et 2 (standard Java), Castor XML, XMLBeans, XStream, JiBX.\nLe marshaller peut être déclaré manuellement :\n\u0026lt;oxm:jaxb2-marshaller id=\u0026#34;marshaller\u0026#34; contextPath=\u0026#34;com.myapp.ws.dto\u0026#34;/\u0026gt; Spring WS permet de déclarer tous les beans d’infrastructure (y compris le marshaller JAXB2) par la balise :\n\u0026lt;ws:annotation-driven /\u0026gt; Les différentes stratégies supportées pour mapper une requête à un EndpointDans le jargon Spring WS, un Endpoint correspond au code métier traitant les messages SOAP.\nCôté serveur, le point d’entrée d’une requête SOAP est le MessageDispatcher. Celui-ci fait appel au EndpointMapping pour déterminer quel Endpoint doit être invoqué. Indirect, l’appel au Endpoint passe par un Endpoint Adapter qui assure, par exemple, l’unmarshalling XML.\nPour déterminer quelle méthode de quel Endpoint invoquer, Spring WS peut utiliser plusieurs stratégies :\nNom de la balise racine du corps du message SOAP (Payload) Balise action définie dans l’en-tête SOAP WS-Adressing (basé sur les en-têtes SOAP Action, ReplyTo et To) X-Path De ces stratégies, comme fonctionne précisément le @PayloadRoot ?L’annotation @PayloadRoot permet de mapper la balise racine du corps de la requête SOAP (le Payload) sur une méthode d’un bean annoté avec @Endpoint. Le nom de la balise racine et son namespace doivent être précisés.\nCouplée aux annotations @ResponsePayload et @RequestPayload , elle assure l’unmarshalling des paramètres d’entrée et le marshalling XML du paramètre de sortie.\nNécessite que le bean PayloadRootAnnotationMethodEndpointMapping soit enregistré.\n@PayloadRoot(localPart=\u0026#34;helloRequest\u0026#34;, namespace=\u0026#34;http://myapp.com/schemas/hello\u0026#34;) public @ResponsePayload Hello sayHello(@RequestPayload Personne personne) Les fonctionnalités proposées par le WebServiceTemplateSimplifie l’appel aux web services\nFacilite l’envoie de requêtes et la réception des réponses\nTravaille directement avec le payload des messages SOAP\nSupporte le marshaling / unmarshalling\nMécanisme de méthodes de rappel (callback) pour les appels bas niveau (ex : accès aux headers SOAP).\nExtensible par ajout d’intercepteurs permettant par exemple de valider la réponse SOAP au regard de la XSD\nGestion des exceptions assurée par le SoapFautMessageResolver qui encapsule les erreurs dans une SoapFaultClientException. Possibilité de fournir son propre resolver.\nPermet d’utiliser plusieurs protocoles : HTTP, Mail, JMS, XMPP\nExemple d’utilisation :\nHello hello = (Hello) webServiceTemplate.marshallSendAndReceive(personne) Web Services Security Les implémentations sous-jacentes à WS-Security supportés par Spring-WSLa sécurisation des web services en termes de signature, d’authentification et de chiffrement est implémentée à l’aide d’intercepteurs.\nXwsSecurityInterceptor basé sur le package de Sun XML and Web Services Security (XWSS) de Sun. Pré-requis : JDK de Sun/Oracle et l’implémentation de référence de SAAJ de Sun. Nécessite un security policy file pour opérer. Wss4jSecurityInterceptor pour l’intégration d’Apache WSS4J implémentant les standards : SOAP Message Security 1.0 (OASIS) Username Token profile 1.0 X.509 Token Profile 1.0 Comment les key stores sont supportés par Spring WS pour être utilisés par WS-Security ?La plupart des opérations de cryptographie nécessite un java.security.KeyStore standard. Le Keystore stocke 3 types d’éléments :\nClés privés : utilisée par WS-Security pour signer et déchiffrer Clés symétriques (ou clé secrète) : client et serveur stockent la même clé. Cette dernière est utilisée à la fois pour chiffrer et déchiffrer. Certificats de confiance (X509). WS-Security les utilise pour valider les certifications, vérifier la signature et le chiffrage. Les différentes classes XWSS de Spring WS référencent un bean keystore pouvant être créé à l’aide de la fabrique KeyStoreFactoryBean. Cette dernière a 2 propriétés : le chemin vers le keystore (ex : classphath:truststore.jks ou keystore.jks) et le mot de passe du keystore.\nXwss repose sur un KeyStoreCallbackHandler référençant le bean d’un des keystores (le keystore dépend du type d’opérations).\nPour gérer les certificats, WSS4J utilise un keystore dont le fichier est référencé par la classe CryptoFactoryBean.\nRESTful services with Spring-MVC Généralités Les principes de RESTStyle d’architecture basé sur http\nHTTP est utilisé comme protocole applicatif et non comme simple couche de transport comme avec SOAP.\n5 concepts principaux :\nDes ressources identifiables par leur URI (tout est ressource, ex : une entité métier) Une interface d’accès aux ressources unifiée : seulement quelques verbes (opérations) : GET : permet d’accéder en lecture seule à la représentation d’une ressource. Pas d’effets de bord. Mise en cache possible côté client (en-têtes E-Tag ou Last-Modified =\u0026gt; code 304 Not Modified) HEAD : similaire à un GET, sans corps ni mise en cache possible. POST : crée une nouvelle ressource. Opération non idempotente. PUT : met à jour une ressource ou créée une ressource identifiée par son URI (ex : PUT /personne/123). Idempotent. DELETE : supprime une ressource. Idempotent. Une ressource peut avoir plusieurs représentations (text/html, image/png). Utilisation des en-têtes Accept et Content-Type pour indiquer au serveur quelle représentation le client sait interpréter. Une conversation est sans état : le serveur ne maintient pas d’état. Le client peut conserver un état à l’aide de liens http. Architecture scalable. Hypermedia : une ressource contient des liens Idempotence : opération maintenant le même état après une ou plusieurs invocations\nSécurité possible avec HTTP Basic ou Digest + SSL. Utilisation possible de XML-DSIG et XML-Encryption\nREST doit être écarté pour des transactions longues.\nSupport de REST dans Spring-MVC Spring-MVC est une alternative à JAX-RS, non une implémentationJAX-RS est un standard : Java API for RESTful Web Services (JSR-311).\nJersey et CXF en sont des implémentations. Ces frameworks supportent Spring.\nAnnotations JAX-RS : @Path, @GET, @POST, @Produces, @PathParam\nJAX-RS 1.0 est cantonné à la partie serveur.\nSpring MVC apporte un support pour REST différent de JAX-RS :\nUtilise les annotations Spring MVC : @RequestMapping, @PathVariable Permet de définir des templates d’URI : \u0026ldquo;/client/{id}\u0026rdquo; comme JAX-RS Permet de déclarer par annotation le code des réponses HTTP. Exemple d’un code 204 : @ResponseStatus(HttpStatus.NO_CONTENT) Gère la négociation de contenu Permet d’interroger côté client des services REST à l’aide du RestTemplate L’annotation @RequestMapping, incluant le support des URI templateL’annotation @RequestMapping permet de mapper une requête HTTP sur une classe et une méthode.\nQuelques propriétés :\nvalue : chemin supportant le style ant (ex: \u0026ldquo;/myPath/*.do\u0026rdquo;) et pouvant être templatisé avec des {}. La valeur extraite est convertie et passée en paramètre de la méthode (utilisation de @PathVariable). La regex par défaut du template [^\\.]* peut être redéfinie (ex : /hotels/{hotel:\\d+}) method : verbe HTTP à mapper params : paramètres de la requête HTTP à mapper. Exemples : myParam=myValue, myParam=!myValue, myParam ou !myParam headers : en-têtes HTTP à mapper sur cette classe / méthode. Les annotations @RequestBody et @ResponseBodyUtilisée avec des POST ou des PUT, @RequestBody annote une méthode. Spring MVC convertit le corps de la requête vers le type du paramètre. Il s’aide de l’en-tête Content-Type.\nPositionnée sur une méthode, @ResponseBody indique à Spring MVC de convertir le paramètre de sortie en fonction de l’en-tête Accept de la requête (HTML, XML, JSON)Les fonctionnalités proposées par le RestTemplateLe RestTemplate permet de faire appel à des services RESTful.\nSupporte les templates d’URI Détection automatique des frameworks disponibles dans le classpath ROME (Atom, RSS), Jackson, JAXB2 pour enregistrer les converters Accès direct au corps des requêtes et des réponses Permet d’utiliser une HttpEntity modélisant une requête / réponse http avec son corps et ses en-têtes Quelques méthodes :\ngetForObject(String url, ClassresponseType, String urlVariables) delete(String url, String… urlVariables) postForLocation(String url, Object request, String… urlVariables) put(String url, Object request, String… urlVariables) Le RestTemplate peut être configuré pour s’appuyer sur Apache Commons HTTP.\nPour l’utiliser, il suffit de l’instancier : new RestTemplate() ou de le déclarer en tant que bean Spring.\nJMS avec Spring Généralités De quelle manière les applications basées sur Spring JMS peuvent-elles récupérer leurs ressources JMS ?2 cas de figures : le serveur d’application fournie les ressources JMS (ConnectionFactory et Queue) ou l’application s’interface directement avec le provider JMS.\nSpring peut donc accéder aux ressources JMS de 2 manières :\nEn les récupérant auprès du conteneur Java EE via un lookup JNDI\n\u0026lt;jee:jndi-lookup id=\u0026#34;connectionFactory” jndi-name=”jms/QueueCF\u0026#34;/\u0026gt; Par déclaration d’un provider JMS standalone :\n\u0026lt;bean id=\u0026#34;connectionFactory” class=”o.a.a.ActiveMQConnectionFactory\u0026#34;\u0026gt; \u0026lt;property name=\u0026#34;brokerURL\u0026#34; value=\u0026#34;tcp://localhost:4444\u0026#34;/\u0026gt; \u0026lt;/bean\u0026gt; Les fonctionnalités offertes par le conteneur de listeners JMS de Spring, incluant l’utilisation du MessageListenerAdapter à travers l’attribut ‘method’ de l’élément jms:listener/Alternative aux MDB nécessitant un conteneur EJB, les conteneurs de listeners JMS de Spring permettent de recevoir des messages de manière asynchrone.\n2 conteneurs sont proposés :\nSimpleMessageListenerContainer : approche bas niveau car utilisation requise de l’API JMS. Nombre fixe de sessions JMS DefaultMessageListenerContainter : support des transactions, scaling dynamique, support des workmanagers Le namespace jms simplifie la déclaration d’un conteneur contenant une liste de listeners JMS :\n\u0026lt;jms:listener-container connection-fatory=\u0026#34;jmsCF\u0026#34;\u0026gt; \u0026lt;jms:listener destination=\u0026#34;queue.order\u0026#34; ref=\u0026#34;orderListener\u0026#34;/\u0026gt; \u0026lt;/jms:listener-container\u0026gt; Dans cet exemple, le bean orderListener doit implémenter MessageListener ou SessionAwareMessageListener. Il est possible de filtrer les messages en utilisant un sélecteur JMS. L’élément jms:listener-container est hautement paramétrable : task-executor, message-converter, concurrency, transaction-manager, acknowledge, cache …\nTransparente lors de l’utilisation du namespace jms, la classe MessageListenerAdapter permet de déclarer n’importe qu’elle classe en Message Driven POJO (MDP) :\n\u0026lt;jms:listener ref=\u0026#34;orderService\u0026#34; method=\u0026#34;placeOrder”/\u0026gt; Un MessageConverter est chargé de convertir le message JMS en paramètre d’entrée de la méthode placeOrder.\nLorsque la méthode du MDP retourne un paramètre, il est possible de configurer le listener avec l’attribut response-destination pour le convertir en un message JMS et le déposer dans une file de réponse.Les fonctionnalités proposées par le JmsTemplateLe JmsTemplate simplifie l’utilisation de l’API JMS 1 :\nDiminution du code technique redondant\nGestion robuste des erreurs\nEncapsulation des JMSException explicites dans des exceptions non vérifiées (runtime)\nGestion transparente des ressources JMS\nRéutilisation des sessions et des connexions JMS à l’aide du CachingConnectionFactory faisant office de pool (à configurer)\nConversion implicite d’objets en message (ex : String en TextMessage) et possibilité de définir son propre MessageConverter, par exemple pour assurer le marshalling XML\nSélection dynamique de la destination sur laquelle émettre le message. Utilisation par défaut du DynamicDestinationResolver, et possibilité d’implémenter JndiDestinationResolver\nRéception synchrone de message (méthodes receiveXXX()). Appel bloquant avec possibilité de spécifier un timeout.\nEnvoi simplifié de messages (méthodes convertAndSend(destination, message)). Utilisation de callbacks pour une utilisation avancée nécessitant de manipuler l’API JMS : MessagePostProcessor, MessageCreator, ProducerCallback, SessionCallback.\njmsTemplate.execute(new SessionCallback() { public Object doInJms(Session session) throws JMSException { ... } } Transactions Transactions JMS Locales avec Spring Comment activer les transactions JMS locale lors de l’utilisation du conteneur de listeners JMS de Spring ?Le conteneur de listener JMS de Spring permet d’utiliser l’un des 3 modes d’acquittement de JMS ou bien une transaction locale.\n\u0026lt;jms:listener-container acknowledge=\u0026#34;transacted\u0026#34;\u0026gt; transacted : transaction s’appliquant uniquement à la ressource JMS. La transaction démarre lorsque le message est reçu. auto : dès la réception du message JMS, ce dernier est acquitté (retiré) de la file. En cas d’erreur de traitement, le broker JMS est donc dans l’incapacité redélivrer le message. Opération unitaire pouvant être plus lente que d’autres modes. client : l’application cliente est responsable de l’acquittement du message JMS. L’appel de la méthode Message.acknowledge() peut être effectué après le traitement dans la même Session JMS de plusieurs messages. En cas d’erreur, on demande au broker de redélivrer les messages par l’appel de Message.recover(). Le client peut donc être amené à traiter des doublons. dups_ok : le drivers JMS assure l’acquittement des messages en mode lazy, ce qui lui permet des optimisations. Le système doit être à même de savoir traiter des messages JMS dupliqués. Comment une transaction JMS locale est-elle rendue disponible au JmsTemplate ?Utilisée dans un listener en mode \u0026ldquo;transacted\u0026rdquo;, le JmsTemplate utilise la même session JMS et participe donc à la transaction initiée par le listener.\nLors de la déclaration d’un JmsTemplate, il est possible de spécifier le mode transactionnel par défaut via les propriétés sessionTransacted et sessionAcnowledgeMode. Ces paramètres sont ignorés lorsqu’une Session JMS active est déjà en cours. En interne, JmsTemplate fait appel à ma méthode ConnectionFactoryUtils.doGetTransactionalSession(…) pour réutiliser la session en cours.Comment Spring cherche à synchroniser une transaction JMS locale et une transaction base de données locale ?Spring applique la stratégie dite du “best effort\u0026quot; :\nLes commits base de données précèdent les commits JMS Permet de ne perdre aucun message Provoque des messages dupliqués lorsque le commit JMS échoue Système de synchronisation rapprochant le plus possible les commits bases de données et JMS Seules les transactions distribuées XA assurent une synchronisation 100% garantie.La fonctionnalité offerte par le JmsTransactionManagerLe JmsTransactionManager attache au thread courant une paire de Connection/Session JMS récupérée de la ConnectionFactory.\nLe JmsTemplate auto-détecte une Session attachée au thread et y participe automatiquement.\nLe JmsTransationManager permet d’utiliser une CachingConnectionFactory qui utilise une unique Connection JMS pour tous ses accès (gains en performance). Toutes les Sessions appartiennent à la même Connection.\nL’usage avec Spring de JTA et des commit à 2 phases Que garantie JTA contrairement aux transactions locales ?Plus que JTA, c’est l’utilisation de XA qui :\nGarantie l’ACIDité des transactions distribuées / globales sur plusieurs ressources Coordonne les commits sur plusieurs ressources Ecarte tout traitement de messages dupliqués : les messages sont délivrés une et une seule fois. Comment basculer d’une transaction locale à une transaction JTA globale ?La bascule se fait par re-configuration. Le code ne change pas.\nNécessite de remplacer le JmsTransactionManager par le JtaTransactionManager (ou l’une de ses classes filles spécifiques aux serveurs d’applis). Toutes les 2 héritent de PlatformTransationManager.\nJtaTransactionManager n’implémente pas JTA mais permet d’intégrer un gestionnaire de transaction JTA tiers.\nL’utilisation du tag simplifie encore la déclaration du gestionnaire de transaction JTA.\nLe conteneur de listeners JMS doit être configuré avec un transaction manager JTA :\n\u0026lt;jms:listener-container transaction-manager=\u0026#34;jtaTransactionManager\u0026#34;/\u0026gt; Des frameworks tiers comme Hibernate doivent être configurés spécifiquement pour JTA.D’où peut-on récupérer un gestionnaire de transaction JTA ?Lorsque l’application est déployée dans un serveur Java EE, Spring récupère le gestionnaire de transaction JTA du serveur par un lookup JNDI.\nLa déclaration indique à Spring de détecter le serveur d’application et de créer le bean spring transationManager avec le meilleur gestionnaire de transaction (le plus spécifique au serveur d’application).\nChaque ressource transactionnelle XA (dataSource, connectionFactory JMS) peut être récupérée par un \u0026lt;jee:jndi-lookup … /\u0026gt;\nPour les applications stand-alone, nécessité de définir manuellement un bean transactionManager et de spécifier ses 2 propriétés transactionManager et userTransaction à l’aide d’implémentation JTA comme Atomikos.\nTraitements par lots avec Spring Batch Généralités Les principaux concepts : Job, Step, Job Instance, Job Execution, Step Execution …\nJob : entité encapsulant l’ensemble des traitements d’un batch Step : un batch est composé d’étapes successives Job Instance = Job + Job Parameters : exécution logique d’un Job. Peut être redémarré après une erreur Job Execution : tentative physique d’exécution d’un Job Instance Step Execution : tentative physique d’exécution d’une étape JobLauncher : interface permettant de lancer un Job avec un ensemble de Job Parameters. Les interfaces typiquement utilisées pour implémenter des Step chunk-orientedGénéralement, un traitement par morceaux (chunk) s’appuie sur un ItemReader, un ItemProcessor (optionnel) et un ItemWriter.\nSpring Batch met à disposition plusieurs implémentations prêtes à l’emploi par simple configuration XML :\nJDBC (curseur et pagination), JPA, iBatis, Hibernate, Procédure Stockée Fichiers plats (CSV ou à taille fixe) Fichiers XML (basé sur StAX) JMS Comment et où les états peuvent-ils être persistés ?L’interface JobRepository offre un mécanisme de persistance proposant des opérations CRUD pour le JobLauncher, les Job et les Step. Persiste le statut de l’exécution des jobs.\nSpring Batch propose 2 implémentations : mémoire (Map) ou base relationnelle.\nLe namespace batch permet de déclarer un repository :\n\u0026lt;batch:job-repository id=\u0026#34;jobRepository\u0026#34; /\u0026gt; Le code applicatif et les readers / writers statefull peuvent persister des données à l’aide de l’ExecutionContext. Utile pour le monitoring, la reprise sur erreur et le passage d’états d’une étape à l’autre.\nLe Job ExecutionContext est commité à la fin de chaque Step.\nLe Step ExecutionContext est commité à la fin de chaque chunk.\nLe listener StepExecutionListener et l’annotation @BeforeStep permettent d’accéder au context d’exécution et de lire / écrire des données :\nint position = executionContext.getInt(\u0026#34;position\u0026#34;, 0) ; executionContext.put(\u0026#34;position\u0026#34;, position) ; Destinée aux ItemReader et ItemWriter, l’interface ItemStream définie un contrat permettant de sauvegarder / restaurer des états en cas d’erreur.\nLes méhodes open() et update() prennent en paramètre un ExecutionContextQu’est-ce qu’un paramètre de job et comment sont-ils utilisés ?Paramètres passés pour exécuter un job (ex : nom d’un fichier, date du jour). Uniques pour chaque Job Instance (exception JobInstanceAlreadyCompletedException)\nPour que chaque Job Instance soit unique, possibilité d’utiliser le JobParametersIncrementer.\nLors de l’exécution d’un batch à l’aide du CommandLineJobRunner, il est possible de passer les paramètres en utilisant la syntaxe key(type)=value avec type = string, date ou long (ex : schedule.date(date)=2012/07/26\nAu sein de bean de portée scope, une SpEL peut être utilisée pour accéder à la valeur d’un paramètre (ex : #{jobParameters[\u0026lsquo;input.file.name\u0026rsquo;]})Qu’est-ce qu’un FieldSetMapper et à quoi servent-ils ?Le FieldSetMapper est utilisé par le FlatFileItemReader : il permet de mapper une ligne d’un fichier plat dans un objet du domaine.\nLe reader commence par décomposer une ligne en token, puis il fait appel à la méthode T mapFieldSet(FieldSet fieldSet) throws BindException; de FieldSetMapper pour parser les données.\nUn FieldSet est l’équivalant du ResultSet JDBC. Il permet d’accéder aux champs d’une ligne d’un fichier plat par leurs noms ou leurs indexes, et cela de manière fortement typé (ex : int readInt(String name))\nSpring Batch fournie 2 implémentations de FieldSetMapper :\nPassThroughFieldSetMapper : renvoie tel quel le FieldSet BeanWrapperFieldSetMapper : utilise l’introspection en se basant sur le nom des propriétés du bean et le nom des champs du FieldSet. Spring Integration (SI) Généralités Les principaux concepts (Messages, Channels, Endpoint types)\nFaites particulièrement attention aux différents types de Endpoints et de quelle manière ils sont utilisés.Permet la mise en œuvre d’une architecture orientée évènement (API Message)\nEncourage le faible couplage et la séparation des préoccupations (ex : parsing vs traitement métier)\nCaractéristiques principales d’un Message :\npossède un corps (payload) et des en-têtes (MessageHeaders) optionnelles est immuable possède un identifiant unique Les Endpoints connectent le code applicatif au système de messages de Spring Integration, et cela de manière non invasive.\nLes Channels connectent les Endpoints. Ils participent au faible couplage. Par défaut, un Channel est en mémoire (simple bean), mais possibilité de les faire persister via JMS ou JDBC.\nDifférents types de Endpoints :\nFilter : décide de faire passer ou non un message vers le output channel. Par défaut, un message filtré est supprimé. Autre configuration possible : levée d’une exception (attribut throw-exception-on-rejection) ou message routé dans un discard-channel. Exemple d’utilisation : lorsque plusieurs consommateurs sont abonnés à un pub-sub channel, ce endpoint permet de filtrer les messages à traiter en fonction de critères bien précis.\n\u0026lt;filter input-channel=\u0026#34;input\u0026#34; output-channel=\u0026#34;output\u0026#34; ref=\u0026#34;filterBean\u0026#34; method=\u0026#34;filter\u0026#34; /\u0026gt; Router : décide dynamiquement vers quel(s) channel(s) un message doit être envoyé. La décision est généralement fonction des en-têtes ou du contenu. Une SpEL peut également être utilisée. Quelques implémentations sont disponibles : RecipientListRouter, HeaderValueRouter\n\u0026lt;router input-channel=\u0026#34;input\u0026#34; ref=\u0026#34;routerBean\u0026#34; method=\u0026#34;route\u0026#34; /\u0026gt; Splitter : découpe un message en plusieurs messages. Typiquement, cela permet de segmenter le traitement d’un payload « composite ». Afin de pouvoir être regroupés, le slipper spécifie dans les en-têtes : CORRELATION_ID (par défaut à partir du MESSAGE_ID), SEQUENCE_SIZE et SEQUENCE_NUMBER\n\u0026lt;splitter input-channel=\u0026#34;input\u0026#34; output-channel=\u0026#34;output\u0026#34; ref=\u0026#34;splitterBean\u0026#34; method=\u0026#34;split\u0026#34; /\u0026gt; Agregator : recompose en un seul message plusieurs messages. Un agrégateur doit être capable d’identifier les messages d’un même lot (Correlation strategy) et de savoir s’il a reçu tous les messages du même lot (Release strategy). Cet endpoint avec état repose sur un MessageStore. Par défaut, Agregator et Splitter fonctionnent de concert.\n\u0026lt;agregator input-channel=\u0026#34;input\u0026#34; output-channel=\u0026#34;output\u0026#34; ref=\u0026#34;agregatorBean\u0026#34; correlation-strategy=\u0026#34;correlationBean\u0026#34; release-strategy-expression=\u0026#34;#this.size() gt 10\u0026#34;/\u0026gt; Service Activator : endpoint générique permettant de connecter un service (métier) au système de messagerie de SI. L’opération d’un service est invoquée pour traiter le message reçu sur l’input-channel. La réponse du service est encapsulée dans un message émis sur le output-channel ou le replyChannel. Aucun message n’est retourné en réponse de méthodes retournant void ou null. Equivaut à utiliser un . Le flag requires-reply=\u0026ldquo;true\u0026rdquo; permet de lever une exception. Lors de la déclaration d’un Service Activator, il n’est pas nécessaire de spécifier la méthode à appeler si le service ne contient qu’une seule méthode publique ou si une méthode est annotée avec @ServiceActivator.\n\u0026lt;service-activator input-channel=\u0026#34;input\u0026#34; ref=\u0026#34;someService\u0026#34; method=\u0026#34;someMethod\u0026#34;/\u0026gt; Transformer : déclinaison du Service Activator dédiée à la conversion du payload et/ou à l’enrichissement du payload ou de l’en-ête. Exemples d’utilisation : Object -\u0026gt; JSON, XML ? Object, Map ? Object\n\u0026lt;transformer input-channel=\u0026#34;input\u0026#34; output-channel=\u0026#34;output\u0026#34; ref=\u0026#34;transformerBean\u0026#34; method=\u0026#34;transform\u0026#34; /\u0026gt; Channel Adapter: connecte un Message Channel avec des systèmes ou des services de transports externes. Il y’a 2 types de Channel Adapter : inbound pour les messages entrant dans l’application (mails, soap, fichier =\u0026gt; messages) et outbound pour les messages sortant (messages =\u0026gt; mail, jms, rest). Un Channel Adapter est uni-directionnel (one-way).\n\u0026lt;int-file:inboud-channel-adapter id=\u0026#34;filesIn\u0026#34; channel=\u0026#34;incomingFiles\u0026#34; directory=\u0026#34;file:C:/inputFiles\u0026#34; /\u0026gt; \u0026lt;int-jdbc:outbound-channel-adapter query=\u0026#34;insert into event (id, name) values (:headers[id], :payload[name])\u0026#34; data-source=\u0026#34;dataSource\u0026#34; \u0026#34; channel=\u0026#34;input\u0026#34; /\u0026gt; _Quel usage fait-on des Gateway ?_Le principal but d’une Gateway est de masquer l’API de messaging fournie par SI. Le code applicatif travaille alors uniquement avec des interfaces. La Gateway fait office de proxy.\nUne inbound Gateway fait rentrer des messages dans l’application et attend la réponse. Une outbound Gateway fait appel à un système externe et renvoie la réponse dans l’application (sous forme de Message).\n\u0026lt;int:gateway id=\u0026#34;cafeService\u0026#34; service-interface=\u0026#34;org.cafetaria.ICafeService\u0026#34; default-request-channel=\u0026#34;request\u0026#34; default-reply-channel=\u0026#34;reply\u0026#34; /\u0026gt; L’attribut default-reply-channel est facultatif. SI crée alors un Channel temporaire à usage unique.Comment créer de nouveaux Messages par programmation ?La classe MessageBuilder peut être utilisée pour créer des messages par programmation :\nMessage\u0026lt;String\u0026gt; msg = MessageBuilder.withPayload(\u0026#34;test\u0026#34;) .setHeader(\u0026#34;foo\u0026#34;, \u0026#34;bar\u0026#34;).build(); On peut également faire appel au constructeur du message par un new GenericMessage(payload, headers);\nUne fois instancié, un message est immuable.\nChaque message possède un identifiant unique (UUID.randomUUID())\nL’en-tête est une simple Map\u0026lt;String, Object\u0026gt;Utilisation des Chains et des BridgesLes Chains permettent d’alléger la configuration d’endpoints travaillant les uns à la suite des autres (message en sortie de l’un = message en entrée du suivant).\nLe Chain spécifie un input-channel et un output-channel (optionnel) et tous les endpoints déclarés à l’intérieur n’ont plus besoin de se soucier sur quel Channel travailler.\n\u0026lt;chain input-channel=\u0026#34;input\u0026#34; output-channel=\u0026#34;output\u0026#34;\u0026gt; \u0026lt;filter ref=\u0026#34;someSelector\u0026#34;/\u0026gt; \u0026lt;header-enricher\u0026gt; \u0026lt;header name=\u0026#34;foo\u0026#34; value=\u0026#34;bar\u0026#34;/\u0026gt; \u0026lt;/header-enricher\u0026gt; \u0026lt;service-activator ref=\u0026#34;someService\u0026#34; method=\u0026#34;someMethod\u0026#34;/\u0026gt; \u0026lt;/chain\u0026gt; Tous les Endpoints chainés le sont avec des DirectChannels. Lorsque le dernier Endpoint retourne une valeur, un output-channel ou un replyChannel doivent être spécifiés._Les intercepteurs de Channel et le pattern Wire Tap comme exemple d’utilisation_Des intercepteurs peuvent être positionnés individuellement sur chaque Channel ou de manière globale (utilisation possible d’un pattern pour sélectionner les channels sur lesquels il s’applique).\nImplémenté à l’aide d’un intercepteur, le pattern EIP Wire Tap permet de recopier dans un autre Channel les messages déposés dans le Channel intercepté. Particulièrement utile pour le debuggage et le monitoring, il est souvent utilisé conjointement avec le logging channel adapter.\nSI permet de configurer un Wire Tap de manière transverse (globale).\nTraitement des messages synchrones vs asynchrones Les différents types de Channel et comment chacun doit être utiliséDeux grandes familles de Channel :\nPoint-to-Point : un seul consommateur DirectChannel : envoi bloquant, synchrone, réception dans le même thread. Possibilité d’avoir plusieurs abonnés en mode failover ou load-balancer (configuration d’un dispatcher). ExecutorChannel : envoi non bloquant, asynchrone, les messages sont consommés par un seul thread QueueChannel : envoi non bloquant, asynchrone, file FIFO, réception dans un (ou plusieurs) thread(s) séparé(s) à base d’un mécanisme de polling (implémente PollableChannel). File d’attentes spécialisées : PriorityChannel (header priority), Rendezvous Channel Publish-Subscribe : plusieurs consommateurs (similitudes aux topics JMS) Appels séquentiels dans le même thread (synchrone) Ou utilisation possible d’un TaskExecutor pour paralléliser les notifications (asynchrone) Les effets bords possibles, par exemple sur les transactions et la sécuritéL’ajout d’un Executor sur un DirectChannel ou un PublishSubscribeChannel ajoute de l’asynchronisme. Synchrone =\u0026gt; assimilé à des appels de méthodes :\nContextes transactionnels et de sécurité disponibles (ThreadLocal) Les exceptions sont retournées naturellement à l’appelant (mais wrappées) Faible overhead Pas scalable Asynchrone :\nLes récepteurs reçoivent le message dans un autre thread Les contextes de sécurité et de transaction sont perdus Les exceptions ne sont pas systématiquement propagées à l’appelant Délai de traitement du message inconnu. Permet néanmoins de se mettre en attente du message de réponse Une ligne de configuration permet de passer du mode synchrone au mode asynchrone :\nsur les channel Référence à un task-executor sur les publish-subscribe-channel Le besoin de polling actif et comment le configurerLe polling est nécessaire pour activer la consommation de messages déposés dans un PollableChannel (les Channels sont passifs)\nPar défaut, un unique thread assure le polling, mais il est possible d’utiliser un TaskExecutor.\nOn peut définir un poller qui sera utilisé par défaut pour lire les messages déposés dans les PollableChannel:\n\u0026lt;poller default=\u0026#34;true\u0026#34; task-executor=\u0026#34;pool\u0026#34; fixed-delay=\u0026#34;200\u0026#34; /\u0026gt; Chaque Endpoint peut redéfinir un poller :\n\u0026lt;service-activator …\u0026gt; \u0026lt;poller task-executor=\u0026#34;otherpool\u0026#34; fixed-rate=\u0026#34;500\u0026#34; /\u0026gt; \u0026lt;/service-activator\u0026gt; Les pollers peuvent être déclarés comme transactionnel afin que le traitement d’un message soit atomique. 2 pré-conditions :\nLe traitement doit être géré par un seul thread La transaction englobe l’appel à la méthode receive() du PollableChannel \u0026lt;service-activator …\u0026gt; \u0026lt;poller fixed-rate=\u0026#34;500\u0026#34;\u0026gt; \u0026lt;transactional /\u0026gt; \u0026lt;/poller\u0026gt; \u0026lt;/service-activator\u0026gt; ","link":"https://javaetmoi.com/2012/09/certified-spring-enterprise-integration-specialist-study-notes/","section":"posts","tags":["certification","jms","jta","rest","rmi","spring-batch","spring-framework","spring-integration","springsource","transaction","web-services","xml"],"title":"Certified Spring Enterprise Integration Specialist Study Notes"},{"body":"","link":"https://javaetmoi.com/tags/jta/","section":"tags","tags":null,"title":"Jta"},{"body":"","link":"https://javaetmoi.com/tags/rmi/","section":"tags","tags":null,"title":"Rmi"},{"body":"","link":"https://javaetmoi.com/tags/xml/","section":"tags","tags":null,"title":"Xml"},{"body":"Contexte Chez mon client, les fichiers de configuration sont variabilisés (ex : fichiers de configuration logback, hosts des différents référentiels et back office, paramétrage applicatif, configuration ehcache …). Cette technique permet d’avoir le même gabarit quel que soit l’environnement sur lequel est déployée l’application (ex : intégration, recette, production). Charge à l’outil de déploiement de générer le fichier de configuration final à partir du gabarit et du fichier de variables spécifiques à l’environnement cible sur lequel le déploiement s’effectue.\nPeu souple, l’ outil de déploiement délimite les variables du gabarit par un double caractère @ qu’il n’est pas possible de redéfinir. Exemple : @@PROJECT_LOG_DIRECTORY@@ En développement, sur un environnement Windows, la variable PROJECT_LOG_DIRECTORY peut prendre par exemple la valeur c:\\temp\\log\\myapp alors qu’en production, sur un serveur Linux, elle aura la valeur /app/log/myapp\nComme vous pouvez vous en douter, l’outil de déploiement n’est pas utilisé en développement pour déployer son application depuis Eclipse. Cependant, les gabarits des fichiers de configuration sont réutilisés (et donc testés). Utilisé pour les tâches de build, c’est tout naturellement maven qui a la responsabilité de substituer les variables par leurs valeurs. Le répertoire contenant les fichiers de configuration générés est ajouté au classpath de l’application.\nLe maven resources plugin en action Nativement, maven est capable de filtrer des ressources. La balise spécifiée dans la XSD 4.0.0 de maven permet de déclarer une liste de répertoire contenant les ressources (dont les fichiers de configuration font parties). La balise permet quant à elle de lister les fichiers filtres qui seront utilisés pendant la phase process-resources de maven, cette dernière étant chargée de copier et filtrer les fichiers de ressources vers le répertoire cible.\nEn interne, ce mécanisme repose sur le plugin maven-resources-plugin. Deux délimiteurs sont positionnés par défaut :\n\u0026lt;delimiters\u0026gt; \u0026lt;delimiter\u0026gt;${*}\u0026lt;/delimiter\u0026gt; \u0026lt;delimiter\u0026gt;@\u0026lt;/delimiter\u0026gt; \u0026lt;!-- equivalent à @*@ --\u0026gt; \u0026lt;/delimiters\u0026gt; Depuis la version 2.4 du plugin, il est possible de redéfinir les délimiteurs. La syntaxe à utiliser est la suivante : \u0026lsquo;beginToken*endToken\u0026rsquo;. Lorsque les tokens de début et de fin sont identiques, la syntaxe est simplifiée : \u0026rsquo;token\u0026rsquo;.\nCertain d’être sur la bonne piste, j’ai tenté de redéfinir le délimiteur pour coller à mon besoin. Hélas, la déclaration d’un délimiteur @@*@@ ne fonctionne pas. Le plugin semble limité à la définition d’un seul caractère pour le token de fin.\nQu’à cela ne tienne, un svn checkout http://svn.apache.org/viewvc/maven/shared/tags/maven-filtering-1.0 et me voilà rapidement entrainé dans les méandres de la classe org.apache.maven.shared.filtering.MultiDelimiterInterpolatorFilterReaderLineEnding\nSi j’avais eu plus de temps devant moi, j’aurais pu tenter d’améliorer son fonctionnement, voire proposer un patch à la communauté. Seulement, il m’aurait été difficile de déployer une version de maven patchée sur l’ensemble des postes de développement et encore moins sur la plateforme d’intégration continue. Il me fallait une solution plus facile à mettre en œuvre et bien plus rapide à déployer.\nLe plugin maven replacer à la rescousse Changeons de stratégie : au lieu de demander au maven-resources-plugin d’accepter le délimiteur @@, autant lui fournir directement un fichier qu’il saura nativement consommer avec un simple @ pour délimiteur. C’est dans ces moments que l’on est heureux d’utiliser un outil ayant un écosystème aussi riche. L’opération recherchée est précisément la fonction du maven-replacer-plugin hébergé sur Google Code.\nAfin d’être exécuté avant l’opération de filtrage, le goal replace du maven-replacer-plugin est associé à la phase generate-resources de maven. Le plugin est configuré pour substituer les caractères @@ en @. Les fichiers sont générés dans le répertoire target/generated-resources. Ce dernier sera utilisé en entrée du maven-resources-plugin pour filtrer et copier les ressources dans le répertoire target/classes lors de la phase process-resources.\nVoici un exemple concret de configuration maven :\nContexte Chez mon client, les fichiers de configuration sont variabilisés (ex : fichiers de configuration logback, hosts des différents référentiels et back office, paramétrage applicatif, configuration ehcache …). Cette technique permet d’avoir le même gabarit quel que soit l’environnement sur lequel est déployée l’application (ex : intégration, recette, production). Charge à l’outil de déploiement de générer le fichier de configuration final à partir du gabarit et du fichier de variables spécifiques à l’environnement cible sur lequel le déploiement s’effectue.\nPeu souple, l’ outil de déploiement délimite les variables du gabarit par un double caractère @ qu’il n’est pas possible de redéfinir. Exemple : @@PROJECT_LOG_DIRECTORY@@ En développement, sur un environnement Windows, la variable PROJECT_LOG_DIRECTORY peut prendre par exemple la valeur c:\\temp\\log\\myapp alors qu’en production, sur un serveur Linux, elle aura la valeur /app/log/myapp\nComme vous pouvez vous en douter, l’outil de déploiement n’est pas utilisé en développement pour déployer son application depuis Eclipse. Cependant, les gabarits des fichiers de configuration sont réutilisés (et donc testés). Utilisé pour les tâches de build, c’est tout naturellement maven qui a la responsabilité de substituer les variables par leurs valeurs. Le répertoire contenant les fichiers de configuration générés est ajouté au classpath de l’application.\nLe maven resources plugin en action Nativement, maven est capable de filtrer des ressources. La balise spécifiée dans la XSD 4.0.0 de maven permet de déclarer une liste de répertoire contenant les ressources (dont les fichiers de configuration font parties). La balise permet quant à elle de lister les fichiers filtres qui seront utilisés pendant la phase process-resources de maven, cette dernière étant chargée de copier et filtrer les fichiers de ressources vers le répertoire cible.\nEn interne, ce mécanisme repose sur le plugin maven-resources-plugin. Deux délimiteurs sont positionnés par défaut :\n\u0026lt;delimiters\u0026gt; \u0026lt;delimiter\u0026gt;${*}\u0026lt;/delimiter\u0026gt; \u0026lt;delimiter\u0026gt;@\u0026lt;/delimiter\u0026gt; \u0026lt;!-- equivalent à @*@ --\u0026gt; \u0026lt;/delimiters\u0026gt; Depuis la version 2.4 du plugin, il est possible de redéfinir les délimiteurs. La syntaxe à utiliser est la suivante : \u0026lsquo;beginToken*endToken\u0026rsquo;. Lorsque les tokens de début et de fin sont identiques, la syntaxe est simplifiée : \u0026rsquo;token\u0026rsquo;.\nCertain d’être sur la bonne piste, j’ai tenté de redéfinir le délimiteur pour coller à mon besoin. Hélas, la déclaration d’un délimiteur @@*@@ ne fonctionne pas. Le plugin semble limité à la définition d’un seul caractère pour le token de fin.\nQu’à cela ne tienne, un svn checkout http://svn.apache.org/viewvc/maven/shared/tags/maven-filtering-1.0 et me voilà rapidement entrainé dans les méandres de la classe org.apache.maven.shared.filtering.MultiDelimiterInterpolatorFilterReaderLineEnding\nSi j’avais eu plus de temps devant moi, j’aurais pu tenter d’améliorer son fonctionnement, voire proposer un patch à la communauté. Seulement, il m’aurait été difficile de déployer une version de maven patchée sur l’ensemble des postes de développement et encore moins sur la plateforme d’intégration continue. Il me fallait une solution plus facile à mettre en œuvre et bien plus rapide à déployer.\nLe plugin maven replacer à la rescousse Changeons de stratégie : au lieu de demander au maven-resources-plugin d’accepter le délimiteur @@, autant lui fournir directement un fichier qu’il saura nativement consommer avec un simple @ pour délimiteur. C’est dans ces moments que l’on est heureux d’utiliser un outil ayant un écosystème aussi riche. L’opération recherchée est précisément la fonction du maven-replacer-plugin hébergé sur Google Code.\nAfin d’être exécuté avant l’opération de filtrage, le goal replace du maven-replacer-plugin est associé à la phase generate-resources de maven. Le plugin est configuré pour substituer les caractères @@ en @. Les fichiers sont générés dans le répertoire target/generated-resources. Ce dernier sera utilisé en entrée du maven-resources-plugin pour filtrer et copier les ressources dans le répertoire target/classes lors de la phase process-resources.\nVoici un exemple concret de configuration maven : [gist id=3124220]\nDans le répertoire src/main/resources/configuration, on peut retrouver par exemple un fichier application.properties ressemblant à :\nlog.directory=@@APPLICATION_LOG_DIRECTORY@@ reporting.email=@@SUPPORT_EMAIL@@ backoffice.url=http://@@BACK_OFFICE_HOST@@ Et voici à quoi ressemblerait le fichier associé src/main/filters/dev.properties :\nAPPLICATION_LOG_DIRECTORY=c:/temp/log/myapp SUPPORT_EMAIL=dev-team@myapp.com BACK_OFFICE_HOST=my.backoffice.com Utilisée depuis plusieurs mois, cette solution a démontré sa viabilité. Notez tout de même un point de vigilance consistant à ne pas utiliser de caractère @ dans les gabarits.\nConclusion Sans prétention, cet article permet de se confronter à la fois à la rigidité et à la souplesse de maven :\nRigidité dans le sens où lorsqu’un plugin ne sait pas faire ce qu’on essaie de lui demander, maven n’offre pas de points d’extension (ex : par scripting, plugins de plugins). Souplesse par la possibilité de faire fonctionner de pair plusieurs plugins : la sortie de l’un est l’entrée de l’autre, retrouvant ainsi une certaine similarité avec les pipes sous linux. Seul regret : le temps passer à rédiger ce billet aurait peut-être dû être employé à debugger la classe MultiDelimiterInterpolatorFilterReaderLineEnding.\nDans le répertoire src/main/resources/configuration, on peut retrouver par exemple un fichier application.properties ressemblant à :\nlog.directory=@@APPLICATION_LOG_DIRECTORY@@ reporting.email=@@SUPPORT_EMAIL@@ backoffice.url=http://@@BACK_OFFICE_HOST@@ Et voici à quoi ressemblerait le fichier associé src/main/filters/dev.properties :\nAPPLICATION_LOG_DIRECTORY=c:/temp/log/myapp SUPPORT_EMAIL=dev-team@myapp.com BACK_OFFICE_HOST=my.backoffice.com Utilisée depuis plusieurs mois, cette solution a démontré sa viabilité. Notez tout de même un point de vigilance consistant à ne pas utiliser de caractère @ dans les gabarits.\nConclusion Sans prétention, cet article permet de se confronter à la fois à la rigidité et à la souplesse de maven :\nRigidité dans le sens où lorsqu’un plugin ne sait pas faire ce qu’on essaie de lui demander, maven n’offre pas de points d’extension (ex : par scripting, plugins de plugins). Souplesse par la possibilité de faire fonctionner de pair plusieurs plugins : la sortie de l’un est l’entrée de l’autre, retrouvant ainsi une certaine similarité avec les pipes sous linux. Seul regret : le temps passer à rédiger ce billet aurait peut-être dû être employé à debugger la classe MultiDelimiterInterpolatorFilterReaderLineEnding.\n","link":"https://javaetmoi.com/2012/07/delimiteurs-de-filtre-maven-sur-plusieurs-caracteres/","section":"posts","tags":["maven"],"title":"Délimiteurs de filtre maven sur plusieurs caractères"},{"body":"Lorsque vous mettez en œuvre Spring Batch pour réaliser des traitements par lots, vous avez le choix d’utiliser une implémentation de JobRepository soit en mémoire soit persistante. L’avantage de cette dernière est triple :\nConserver un historique des différentes exécutions de vos instances de jobs. Pouvoir suivre en temps réel le déroulement de votre batch via, par exemple, l’excellent Spring Batch Admin. Avoir la possibilité de reprendre un batch là où il s’était arrêté en erreur. La contrepartie d’utiliser un JobRepository persistant est de devoir faire reposer le batch sur une base de données relationnelles. Le schéma sur lequel s’appuie Spring Bath est composé de 6 tables. Leur MPD est disponible dans l’ annexe B. Meta-Data Schema du manuel de référence de Spring Batch. SpringSource faisant bien les choses, les scripts DDL de différentes solutions du marché (ex : MySQL, Oracle, DB2, SQL Server, Postgres, H2 …) sont disponibles dans le package org.springframework.batch.core du JAR spring-batch-core-xxx.jar\nQui dit base de données, dit dimensionnement de cette dernière. L’ espace disque requis est alors fonction du nombre d’exécutions estimé, de la nature des informations contextuelles persistées et de la durée de rétention des données. Cette démarche prend tout son sens lorsqu’une instance de base de données est dédiée au schéma de Spring Batch. En faisant quelques hypothèses (ex : sur le taux d’échec) et en mesurant le volume occupé sur plusieurs exécutions des batchs, il est possible de prévoir assez finement l’espace occupé par les données.\nA moins de disposer de ressources infinies ou de n’avoir qu’un seul batch annuel, il est fréquent de fixer une durée de rétention de l’historique. Première option : demander à l’équipe d’exploitation de régulièrement lancer un script SQL de purge. Deuxième option : utiliser Spring Batch pour purger ses propres données !!\nUne Tasklet pour purger les données De base, Spring Batch n’offre pas cette fonctionnalité. Et sur le Jira de SpringSource, je n’ai pas trouvé de demandes d’évolutions allant dans ce sens. Dans le ticket BATCH-1747, Lucas Ward, commiteur Spring Batch, invite les personnes intéressées à passer par des requêtes SQL de suppression après désactivation des contraintes d’intégrité.\nPartant de ce constat, je me suis lancé dans l’écriture d’une tasklet permettant de ne conserver l’historique Spring Batch des N derniers mois. Surement perfectible, en voici le résultat :\npublic class RemoveSpringBatchHistoryTasklet implements Tasklet, InitializingBean { /** * SQL statements removing step and job executions compared to a given date. */ private static final String SQL_DELETE_BATCH_STEP_EXECUTION_CONTEXT = \u0026#34;DELETE FROM %PREFIX%STEP_EXECUTION_CONTEXT WHERE STEP_EXECUTION_ID IN (SELECT STEP_EXECUTION_ID FROM %PREFIX%STEP_EXECUTION WHERE JOB_EXECUTION_ID IN (SELECT JOB_EXECUTION_ID FROM %PREFIX%JOB_EXECUTION where CREATE_TIME \u0026lt; ?))\u0026#34;; private static final String SQL_DELETE_BATCH_STEP_EXECUTION = \u0026#34;DELETE FROM %PREFIX%STEP_EXECUTION WHERE JOB_EXECUTION_ID IN (SELECT JOB_EXECUTION_ID FROM %PREFIX%JOB_EXECUTION where CREATE_TIME \u0026lt; ?)\u0026#34;; private static final String SQL_DELETE_BATCH_JOB_EXECUTION_CONTEXT = \u0026#34;DELETE FROM %PREFIX%JOB_EXECUTION_CONTEXT WHERE JOB_EXECUTION_ID IN (SELECT JOB_EXECUTION_ID FROM %PREFIX%JOB_EXECUTION where CREATE_TIME \u0026lt; ?)\u0026#34;; private static final String SQL_DELETE_BATCH_JOB_EXECUTION_PARAMS = \u0026#34;DELETE FROM %PREFIX%JOB_EXECUTION_PARAMS WHERE JOB_EXECUTION_ID IN (SELECT JOB_EXECUTION_ID FROM %PREFIX%JOB_EXECUTION where CREATE_TIME \u0026lt; ?)\u0026#34;; private static final String SQL_DELETE_BATCH_JOB_EXECUTION = \u0026#34;DELETE FROM %PREFIX%JOB_EXECUTION where CREATE_TIME \u0026lt; ?\u0026#34;; private static final String SQL_DELETE_BATCH_JOB_INSTANCE = \u0026#34;DELETE FROM %PREFIX%JOB_INSTANCE WHERE JOB_INSTANCE_ID NOT IN (SELECT JOB_INSTANCE_ID FROM %PREFIX%JOB_EXECUTION)\u0026#34;; /** * Default value for the table prefix property. */ private static final String DEFAULT_TABLE_PREFIX = AbstractJdbcBatchMetadataDao.DEFAULT_TABLE_PREFIX; /** * Default value for the data retention (in month) */ private static final Integer DEFAULT_RETENTION_MONTH = 6; private String tablePrefix = DEFAULT_TABLE_PREFIX; private Integer historicRetentionMonth = DEFAULT_RETENTION_MONTH; private JdbcTemplate jdbcTemplate; private static final Logger LOG = LoggerFactory.getLogger(RemoveSpringBatchHistoryTasklet.class); @Override public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) { int totalCount = 0; Date date = DateUtils.addMonths(new Date(), -historicRetentionMonth); DateFormat df = new SimpleDateFormat(); LOG.info(\u0026#34;Remove the Spring Batch history before the {}\u0026#34;, df.format(date)); int rowCount = jdbcTemplate.update(getQuery(SQL_DELETE_BATCH_STEP_EXECUTION_CONTEXT), date); LOG.info(\u0026#34;Deleted rows number from the BATCH_STEP_EXECUTION_CONTEXT table: {}\u0026#34;, rowCount); totalCount += rowCount; rowCount = jdbcTemplate.update(getQuery(SQL_DELETE_BATCH_STEP_EXECUTION), date); LOG.info(\u0026#34;Deleted rows number from the BATCH_STEP_EXECUTION table: {}\u0026#34;, rowCount); totalCount += rowCount; rowCount = jdbcTemplate.update(getQuery(SQL_DELETE_BATCH_JOB_EXECUTION_CONTEXT), date); LOG.info(\u0026#34;Deleted rows number from the BATCH_JOB_EXECUTION_CONTEXT table: {}\u0026#34;, rowCount); totalCount += rowCount; rowCount = jdbcTemplate.update(getQuery(SQL_DELETE_BATCH_JOB_EXECUTION_PARAMS), date); LOG.info(\u0026#34;Deleted rows number from the BATCH_JOB_EXECUTION_PARAMS table: {}\u0026#34;, rowCount); totalCount += rowCount; rowCount = jdbcTemplate.update(getQuery(SQL_DELETE_BATCH_JOB_EXECUTION), date); LOG.info(\u0026#34;Deleted rows number from the BATCH_JOB_EXECUTION table: {}\u0026#34;, rowCount); totalCount += rowCount; rowCount = jdbcTemplate.update(getQuery(SQL_DELETE_BATCH_JOB_INSTANCE)); LOG.info(\u0026#34;Deleted rows number from the BATCH_JOB_INSTANCE table: {}\u0026#34;, rowCount); totalCount += rowCount; contribution.incrementWriteCount(totalCount); return RepeatStatus.FINISHED; } protected String getQuery(String base) { return StringUtils.replace(base, \u0026#34;%PREFIX%\u0026#34;, tablePrefix); } public void setTablePrefix(String tablePrefix) { this.tablePrefix = tablePrefix; } public void setHistoricRetentionMonth(Integer historicRetentionMonth) { this.historicRetentionMonth = historicRetentionMonth; } public void setJdbcTemplate(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } @Override public void afterPropertiesSet() throws Exception { Assert.notNull(jdbcTemplate, \u0026#34;The jdbcTemplate must not be null\u0026#34;); } } Le code source de la classe RemoveSpringBatchHistoryTasklet et sa classe de tests unitaires sont disponibles sur le projet Github spring-batch-toolkit.\nCette tasklet peut être utilisée de 2 manières :\nDans un batch dédié à la purge de l’historique Spring Batch, batch qui pourrait par exemple être exécuté mensuellement ou annuellement selon la durée de rétention choisie. Dans un step ajouté à un batch existant, par exemple en tant que step final. Sur mon projet, nous avons opté pour l’option n°2 afin de ne pas démultiplier le nombre de batchs et parce que la mise en production d’un batch ainsi que sa planification s’avèrent toujours laborieux.\nOutre le fait de valider les requêtes SQL et leur ordonnancement, le test unitaire permet de se parer face à une éventuelle migration de schéma suite à une montée de version de Spring Batch.\nConclusion Qui mieux que Spring Batch peut exécuter un traitement de purge pouvant potentiellement manipuler des enregistrements en masse ? Vous connaissez désormais la réponse.\nPour parfaire le code, il aurait été intéressant de déplacer l’exécution des requêtes SQL dans un DAO héritant de la classe AbstractJdbcBatchMetadataDao. Outre un meilleur design, cela aurait permis de faire un appel au DAO de purge ailleurs que dans un batch. Une telle fonctionnalité pourrait très bien avoir sa place dans la console de Spring Batch Admin.\n","link":"https://javaetmoi.com/2012/06/sprint-batch-sauto-nettoie/","section":"posts","tags":["spring-batch","spring-framework","sql"],"title":"Spring Batch s'auto-nettoie"},{"body":"Chez mon client, des tests de stress sont réalisés sur toute nouvelle version d’une application. Outre le fait de qualifier techniquement l’environnement de pré-production, ces tirs permettent de détecter toute dégradation des performances et de prévenir toute montée en charge induite, par exemple, par une nouvelle fonctionnalité. Plus encore, ils permettent de mesurer les gains apportés par d’éventuelles optimisations. Ces tests de stress sont réalisés à l’aide de l’outil Apache JMeter [1].\nAfin de pouvoir comparer des mesures, les cas fonctionnels utilisés lors des tests doivent, dans la mesure du possible, être identiques aux précédents tirs, sachant que ces derniers peuvent dater de plusieurs mois. Entre temps, nombre d’évolutions ont été susceptibles de casser vos tests JMeter. A priori, vous avez donc 2 choix : soit vous les réécrivez, soit vous les maintenez à jour. Si vous en avez déjà écrit, vous vous doutez bien que maintenir dans la durée des tests JMeter a un cout non négligeable. Une 3ième solution présentée ici consiste à la générer !\nJ’ai la chance de travailler dans une équipe ou l’outil Selenium [2] de tests IHM est rentré dans les mœurs. L’automatisation de leur exécution y joue un rôle indéniable. Notre hiérarchie s’est fortement impliquée ; elle a investi de l’énergie et du budget. Un DSL a été mis au point pour faciliter leur écriture et leur maintenance. Alors quand on peut les rentabiliser encore davantage, autant le faire. J’ai donc proposé de ne maintenir que les tests Selenium et de générer les tests JMeter à partir de tests Selenium.\nCet article a pour objectif de vous présenter la démarche adoptée. Si vous êtes intéressés, vous pourrez librement l’adapter en fonction de votre contexte projet.\nLes outils à disposition Serveur Proxy HTTP de JMeter Le client lourd JMeter offre la possibilité d\u0026rsquo; enregistrer toutes les requêtes HTTP transitant entre le navigateur et l’application web à tester. Techniquement, il utilise le mécanisme de proxy HTTP. Le navigateur est configuré pour utiliser le Serveur Proxy HTTP créé dans le Plan de Travail JMeter. Couplé à un Contrôleur Enregistreur, le proxy enregistre les appels avant de les router vers le serveur d’application cible.\nLe tutoriel Proxy step by step [3] proposé sur le site de JMeter fournit toutes les étapes nécessaires pour configuration JMeter.\nDu code source Le fichier .jmx généré lors de l’enregistrement va devoir être manipulé en Java (bien oui, vous êtes sur le blog d’un développeur java). Premier soulagement : il est au format XML et peut donc être aisément parsé une fois son schéma appréhendé. Mails là où il est intéressant d’avoir opté pour un outil open source, c’est que nous allons pouvoir utiliser son API Java pour manipuler les fichiers .jmx de description de plan de tests. Et pour mieux cerner l’API, l’accès au code source est précieux.\nLors de la mise au point du générateur, les artefacts maven de la version 2.5.1 n’étaient pas disponibles sur le repo central maven, chose révolue depuis la version 2.6. JMeter est découpé en plusieurs modules. Dans le cadre du générateur, les modules Core, HTTP set Components ont été nécessaires. Sans oublier jorphan, dont le package collections offre toutes les API nécessaires pour naviguer dans l’arbre du Plan de Tests.\nPar transitivité, il est possible de récupérer tous les artefacts nécessaires en déclarant les dépendances maven suivantes :\n\u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.apache.jmeter\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;ApacheJMeter_http\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;2.6\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.apache.jmeter\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;jorphan\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;2.6\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; Mise en œuvre Afin de pouvoir être rejoué, un scénario de test Selenium enregistré à l’état brut par le proxy JMeter doit être épuré, retravaillé et variabilisé. C’est d’ailleurs toute la plus-value apportée par le générateur.\nTemplate de test Une première étape consiste à insérer le contenu du « fichier brut » (c’est-à-dire tous les éléments de tests enregistrés sous le Contrôleur Enregistreur) dans ce que j’appellerai un template de test. Réutilisable, ce template contient toutes les caractéristiques d’un test de stress type. Voici quelques-unes de ses composantes :\nContrôleur de Transaction dans lequel insérer par programmation tous les éléments de tests enregistrés Variables pré-définies : host et port de l’environnement à tester, délai de réflexion, durée maximum attendue de chargement d’une page Gestionnaire d’entêtes HTTP, par exemple avec le user-agent d’IE 8 si ce dernier est cible Gestionnaires de Cookies http Gestionnaire de Cache HTTP Chargement des comptes utilisateurs (par exemple à partir d’un fichier CSV) Logique du test de stress, par exemple à l’aide d’un Groupes d’Unités configuré pour un test aux limites Compteur de temps fixe pour simuler le « Think Time » des utilisateurs Traitement des données brutes La seconde étape consiste à implémenter les « processeurs » chargés de traiter les éléments de tests enregistrés par le proxy JMeter. A titre d’exemples, voici les traitements effectués par le générateur mis en œuvre dans le cadre d’une application JSF / RichFaces :\nVariabilise le paramètre javax.faces.ViewState renvoyé dans les pages JSF Retire le nom et l\u0026rsquo;adresse IP du serveur web de chaque requête HTTP Supprime toutes les URL contenant des ressources vers selenium-server (ex: http://localhost:3333/selenium-server/core/selenium.css) Variabilise les paramètres d’authentification SSO Supprime tous les gestionnaires d\u0026rsquo;en-tête HTTP enregistrés par le proxy JMeter afin d’utiliser le gestionnaire global déclaré dans le template de test Regroupe tous les appels vers la même page JSF dans même un sous-contrôleur de transaction Supprime tous les appels vers des sites externes (ex : Google Analytics) Améliore la lisibilité des éléments de tests en les renommant Ajoute une assertion sur le temps d\u0026rsquo;exécution d\u0026rsquo;une requête Ajoute une assertion faisant échouer le test JMeter lorsque l\u0026rsquo;utilisateur est redirigé sur une page d\u0026rsquo;erreur technique de l’application Afin d’illustrer cet article et de le rendre plus concret, voici un exemple de processeur :\nChez mon client, des tests de stress sont réalisés sur toute nouvelle version d’une application. Outre le fait de qualifier techniquement l’environnement de pré-production, ces tirs permettent de détecter toute dégradation des performances et de prévenir toute montée en charge induite, par exemple, par une nouvelle fonctionnalité. Plus encore, ils permettent de mesurer les gains apportés par d’éventuelles optimisations. Ces tests de stress sont réalisés à l’aide de l’outil Apache JMeter [1].\nAfin de pouvoir comparer des mesures, les cas fonctionnels utilisés lors des tests doivent, dans la mesure du possible, être identiques aux précédents tirs, sachant que ces derniers peuvent dater de plusieurs mois. Entre temps, nombre d’évolutions ont été susceptibles de casser vos tests JMeter. A priori, vous avez donc 2 choix : soit vous les réécrivez, soit vous les maintenez à jour. Si vous en avez déjà écrit, vous vous doutez bien que maintenir dans la durée des tests JMeter a un cout non négligeable. Une 3ième solution présentée ici consiste à la générer !\nJ’ai la chance de travailler dans une équipe ou l’outil Selenium [2] de tests IHM est rentré dans les mœurs. L’automatisation de leur exécution y joue un rôle indéniable. Notre hiérarchie s’est fortement impliquée ; elle a investi de l’énergie et du budget. Un DSL a été mis au point pour faciliter leur écriture et leur maintenance. Alors quand on peut les rentabiliser encore davantage, autant le faire. J’ai donc proposé de ne maintenir que les tests Selenium et de générer les tests JMeter à partir de tests Selenium.\nCet article a pour objectif de vous présenter la démarche adoptée. Si vous êtes intéressés, vous pourrez librement l’adapter en fonction de votre contexte projet.\nLes outils à disposition Serveur Proxy HTTP de JMeter Le client lourd JMeter offre la possibilité d\u0026rsquo; enregistrer toutes les requêtes HTTP transitant entre le navigateur et l’application web à tester. Techniquement, il utilise le mécanisme de proxy HTTP. Le navigateur est configuré pour utiliser le Serveur Proxy HTTP créé dans le Plan de Travail JMeter. Couplé à un Contrôleur Enregistreur, le proxy enregistre les appels avant de les router vers le serveur d’application cible.\nLe tutoriel Proxy step by step [3] proposé sur le site de JMeter fournit toutes les étapes nécessaires pour configuration JMeter.\nDu code source Le fichier .jmx généré lors de l’enregistrement va devoir être manipulé en Java (bien oui, vous êtes sur le blog d’un développeur java). Premier soulagement : il est au format XML et peut donc être aisément parsé une fois son schéma appréhendé. Mails là où il est intéressant d’avoir opté pour un outil open source, c’est que nous allons pouvoir utiliser son API Java pour manipuler les fichiers .jmx de description de plan de tests. Et pour mieux cerner l’API, l’accès au code source est précieux.\nLors de la mise au point du générateur, les artefacts maven de la version 2.5.1 n’étaient pas disponibles sur le repo central maven, chose révolue depuis la version 2.6. JMeter est découpé en plusieurs modules. Dans le cadre du générateur, les modules Core, HTTP set Components ont été nécessaires. Sans oublier jorphan, dont le package collections offre toutes les API nécessaires pour naviguer dans l’arbre du Plan de Tests.\nPar transitivité, il est possible de récupérer tous les artefacts nécessaires en déclarant les dépendances maven suivantes :\n\u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.apache.jmeter\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;ApacheJMeter_http\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;2.6\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.apache.jmeter\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;jorphan\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;2.6\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; Mise en œuvre Afin de pouvoir être rejoué, un scénario de test Selenium enregistré à l’état brut par le proxy JMeter doit être épuré, retravaillé et variabilisé. C’est d’ailleurs toute la plus-value apportée par le générateur.\nTemplate de test Une première étape consiste à insérer le contenu du « fichier brut » (c’est-à-dire tous les éléments de tests enregistrés sous le Contrôleur Enregistreur) dans ce que j’appellerai un template de test. Réutilisable, ce template contient toutes les caractéristiques d’un test de stress type. Voici quelques-unes de ses composantes :\nContrôleur de Transaction dans lequel insérer par programmation tous les éléments de tests enregistrés Variables pré-définies : host et port de l’environnement à tester, délai de réflexion, durée maximum attendue de chargement d’une page Gestionnaire d’entêtes HTTP, par exemple avec le user-agent d’IE 8 si ce dernier est cible Gestionnaires de Cookies http Gestionnaire de Cache HTTP Chargement des comptes utilisateurs (par exemple à partir d’un fichier CSV) Logique du test de stress, par exemple à l’aide d’un Groupes d’Unités configuré pour un test aux limites Compteur de temps fixe pour simuler le « Think Time » des utilisateurs Traitement des données brutes La seconde étape consiste à implémenter les « processeurs » chargés de traiter les éléments de tests enregistrés par le proxy JMeter. A titre d’exemples, voici les traitements effectués par le générateur mis en œuvre dans le cadre d’une application JSF / RichFaces :\nVariabilise le paramètre javax.faces.ViewState renvoyé dans les pages JSF Retire le nom et l\u0026rsquo;adresse IP du serveur web de chaque requête HTTP Supprime toutes les URL contenant des ressources vers selenium-server (ex: http://localhost:3333/selenium-server/core/selenium.css) Variabilise les paramètres d’authentification SSO Supprime tous les gestionnaires d\u0026rsquo;en-tête HTTP enregistrés par le proxy JMeter afin d’utiliser le gestionnaire global déclaré dans le template de test Regroupe tous les appels vers la même page JSF dans même un sous-contrôleur de transaction Supprime tous les appels vers des sites externes (ex : Google Analytics) Améliore la lisibilité des éléments de tests en les renommant Ajoute une assertion sur le temps d\u0026rsquo;exécution d\u0026rsquo;une requête Ajoute une assertion faisant échouer le test JMeter lorsque l\u0026rsquo;utilisateur est redirigé sur une page d\u0026rsquo;erreur technique de l’application Afin d’illustrer cet article et de le rendre plus concret, voici un exemple de processeur :\n[gist id=2783293]\nLa classe ViewStateExtractor est chargée d’extraire l\u0026rsquo;identifiant du view state JSF à l’aide d’une expression régulière. La valeur est stockée dans la variable JMeter VIEWSTATE. Cette extraction est effectuée lorsque l\u0026rsquo;utilisateur arrive la première fois sur une page donnée. La variable VIEWSTATE est ensuite soumise dans la variable javax.faces.ViewState. Ce comportement est assuré par un second processeur : le ViewStateReplacer.\nA noter que dans nos scénarios de tests, l’utilisateur ne revient jamais sur des pages précédemment visitées. Classe maison, TestElementTree associe un élément de test (classe héritant de org.apache.jmeter.testelement.TestElement) à son arbre parent (de type org.apache.jorphan.collections.HashTree)\nVoici le résultat du générateur vu depuis le client lourd JMeter :\nGénération Enfin, la troisième et dernière étape consiste à orchestrer la lecture du fichier brut, la fusion dans le template de test et l’exécution des différents processeurs. Rien de sorcier. Les méthodes loadTree et saveTree de la classe org.apache.jmeter.save.SaveService sont là pour nous y aider. Parcourir l’arbre d’éléments de test JMeter et appliquer les traitements se code relativement facilement. Point d’attention : l’ordre d’exécution des traitements peut avoir son importance.\nConclusion En une petite semaine de développement et de mise au point, vous avez la possibilité de générer des tests JMeter reprenant les scénarios fonctionnels de vos tests Selenium. Vos tests de stress collent ainsi parfaitement aux use cases fonctionnels. En cas d’IHM web riche, les requêtes Ajax (par exemple liées à l’auto-suggestion) sont enregistrées.\nDésormais, le client lourd JMeter n’est utilisé que pour créer le template de test et vérifier le résultat de la génération.\nPour aller encore plus loin, les quelques interventions manuelles décrites précédemment pourraient être automatisées :\nLa configuration du proxy HTTP du navigateur pourrait par exemple être remplacée par l’utilisation d’un profile Firefox préconfiguré. La configuration et le démarrage du Server Proxy HTTP de JMeter pourraient être programmés via l’API JMeter. Ceci réalisé, la génération des tests pourrait alors se faire en un clic. Et comme nos tests Selenium sont exécutés chaque nuit par une plateforme d’intégration continue. En cas de succès, il serait intéressant de déclencher la génération des tests JMeter. Nous disposerions alors de tests de montée en charge toujours à jour sans plus aucune intervention humaine. Références :\nSite officiel d’Apache JMeter Site official de Selenium Tutorial JMeter Proxy step by step La classe ViewStateExtractor est chargée d’extraire l\u0026rsquo;identifiant du view state JSF à l’aide d’une expression régulière. La valeur est stockée dans la variable JMeter VIEWSTATE. Cette extraction est effectuée lorsque l\u0026rsquo;utilisateur arrive la première fois sur une page donnée. La variable VIEWSTATE est ensuite soumise dans la variable javax.faces.ViewState. Ce comportement est assuré par un second processeur : le ViewStateReplacer.\nA noter que dans nos scénarios de tests, l’utilisateur ne revient jamais sur des pages précédemment visitées. Classe maison, TestElementTree associe un élément de test (classe héritant de org.apache.jmeter.testelement.TestElement) à son arbre parent (de type org.apache.jorphan.collections.HashTree)\nVoici le résultat du générateur vu depuis le client lourd JMeter :\nGénération Enfin, la troisième et dernière étape consiste à orchestrer la lecture du fichier brut, la fusion dans le template de test et l’exécution des différents processeurs. Rien de sorcier. Les méthodes loadTree et saveTree de la classe org.apache.jmeter.save.SaveService sont là pour nous y aider. Parcourir l’arbre d’éléments de test JMeter et appliquer les traitements se code relativement facilement. Point d’attention : l’ordre d’exécution des traitements peut avoir son importance.\nConclusion En une petite semaine de développement et de mise au point, vous avez la possibilité de générer des tests JMeter reprenant les scénarios fonctionnels de vos tests Selenium. Vos tests de stress collent ainsi parfaitement aux use cases fonctionnels. En cas d’IHM web riche, les requêtes Ajax (par exemple liées à l’auto-suggestion) sont enregistrées.\nDésormais, le client lourd JMeter n’est utilisé que pour créer le template de test et vérifier le résultat de la génération.\nPour aller encore plus loin, les quelques interventions manuelles décrites précédemment pourraient être automatisées :\nLa configuration du proxy HTTP du navigateur pourrait par exemple être remplacée par l’utilisation d’un profile Firefox préconfiguré. La configuration et le démarrage du Server Proxy HTTP de JMeter pourraient être programmés via l’API JMeter. Ceci réalisé, la génération des tests pourrait alors se faire en un clic. Et comme nos tests Selenium sont exécutés chaque nuit par une plateforme d’intégration continue. En cas de succès, il serait intéressant de déclencher la génération des tests JMeter. Nous disposerions alors de tests de montée en charge toujours à jour sans plus aucune intervention humaine. Références :\nSite officiel d’Apache JMeter Site official de Selenium Tutorial JMeter Proxy step by step ","link":"https://javaetmoi.com/2012/05/generer-tests-jmeter-selenium/","section":"posts","tags":["jmeter","selenium","test"],"title":"Générer des tests JMeter à partir de Selenium"},{"body":"","link":"https://javaetmoi.com/tags/app-engine/","section":"tags","tags":null,"title":"App-Engine"},{"body":"","link":"https://javaetmoi.com/tags/bigtable/","section":"tags","tags":null,"title":"Bigtable"},{"body":"Au cours de la première matinée de Devoxx France, j’ai pu assister à un Lands-on-Lab permettant de s’initier au SDK et à la plateforme Google App Engine.\nNommé « Le Président est …» [1], ce code labs fut co-animé par Didier Girard (SFEIR), Ludovic Champenois (Google), Martin Görner (Google) et Patrice de Saint Steban (SFEIR). Il consistait à développer en 3h un site web visant à annoncer au soir du 6 mai 2012 le nom du nouveau Président. Une seule contrainte : accueillir un trafic potentiel de 50 millions d’utilisateurs et pouvoir tenir un pic de charge de 2 millions d’utilisateurs aux alentours de 20h. En guise d’exemple, une application démo [2] était déjà disponible en ligne.\nD’actualités et évoquant des chiffres qui exciteraient tout architecte, ce code labs fut la parfaite occasion de m’initier à Google App Engine. Au cours de ce billet, je vous relaterai ce que j’y ai appris et vous donnerai accès au code source que vous pourrez à votre tour déployer sur le PaaS de Google.\nPrésentation de Google App\nApp Engine est un serveur d’application pensé pour le Cloud. Visant historiquement les applications Python, il adresse aujourd’hui les applications Java.\nLe SDK d’App Engine permet de développer une application, la tester puis la déployer dans le Cloud. Il propose également une console d’administration minimaliste.\nBon à savoir : le déploiement se fait au travers du protocole HTTP. Il peut donc passer les proxys et firewalls des entreprises.\nLa partie dite RUN donne accès à de nombreux services : base NoSQL (la fameuse BigTable qui a inspiré HBase), base MySQL, authentification (SSO de Google), CRON, task queue, chanel, images, mail (envoi et réception !!), MemCache, XMPP, URLFetch, BlobStore et bientôt FullText Search (annoncé lors de dernier Google IO). Les speakers insistent sur le service URLFetch qui permet d’utiliser l’infrastructure Google pour récupérer des pages HTTP. Crawler le web est le métier de base de Google. Avec ses systèmes de cache et de résolution de noms de domaine, c’est donc sans doute la stack la plus optimisée au monde.\nFocus sur le DataSore\nLe DataStore c’est quoi ? C’est la solution NoSQL de Google basée sur BigTable. On peut la voir comme une grosse HashMap dans laquelle on stocke des documents dans n’importe quel format: XML, JSON, String, CSV … Charge est au développeur de gérer le marshalling des données lors de leur persistance. Chaque document possède un identifiant et peut avoir plusieurs index. Les index sont utilisés pour effectuer des recherches ; ils sont typés et peuvent être multi-valués. Comme dans le monde relationnel, la création d’un index a un coût. C’est pourquoi Google les facture.\nLe DataStore est optimisé pour être le plus performant en lecture. Il assure la réplication des données, leur accès simultanés et la montée en charge.\nPoint d’attention, les clauses where sont limitées à un seul ordre de comparaison. Ceci amène quelques sophistications : système de classification par intervalle, coordonnées géographiques passées de 2D en 1D. Le DataStore peut être manipulée à l’aide d’une API Java, simple à prendre en main mais basique. Le projet Objectify [3] permet d’apporter une sur-couche ORM utilisant les annotations JPA. Architecture d’AppEngine\nLes applications hébergées sur Google App Engine sont hébergées dans les mêmes Data Centers et les mêmes serveurs physiques que les applications Google comme Google Search, Google Mail, Google Docs … Les applications déployées sur AppEngine profitent donc de l’infrastructure de Google réputée fiable, robuste et hautement scalable.\nA l’origine, l’architecture d’AppEngine était basée sur une architecture dite « système 2000 » : sessions utilisateurs en RAM avec des sticky load balancer. De nombreux problèmes en découlaient : arrêt des serveurs, mémoire insuffisante, lenteur du load balancing, problème de scalabilité.\nL’architecture actuelle résout ces problèmes : instances stateless, état dans un MemCache et persistance dans un DataStore. Comme sur Amazon EC2,les load balancers deGoogle sont configurés le plus simplement possible en round robin, ce qui les rend 10x plus performant que les sticky sessions. Déroulement des Travaux Pratiques\nLes participants au code labs étaient tenus de venir avec un ordinateur portable équipé des prérequis suivants :\nJDK 6 Eclipse Indigo 3.7 en version JEE (la version de base fonctionne mais il faudra par exemple oublier la coloration syntaxique des JSP) Google Plugin for Eclipse 3.7 et Google App Engine Java SDK 1.6.4 disponibles depuis le référentiel de plugins Google pour Ecplise [4] Un compte Gmail Evidemment, les organisateurs avaient paré à tout aléa : une clé USB contenant tous ces outils était à notre disposition. En complément, y figuraient les slides des travaux pratiques[5]. Oh surprise : au format html 5, les slides ont été réalisés avec le projet open source Google HTML5 slides template [6]. Impressionnant, mais pas encore tout à fait au point : navigation nécessitant de mixer clavier et souris, bug d’affichage, nécessite Google Chrome pour aller au-delà du premier slide, bug de rendu avec les cartes graphiques des VAIO Sony …\nBien pensés, les TP permettent de nous initier pas à pas à l’utilisation du SDK et aux différents services du Cloud de Google :\nCréation puis configuration d’un projet Eclipse pour AppEnfine Mise au point en JSP d’un formulaire de soumission de commentaires Exécution en local de l’application Déploiement sur l’infrastructure Google AppEngine Utilisation du UserService d’AppEngine pour authentifier les utilisateurs à partir de leur compte Gmail Persistance des commentaires dans le DataStore en utilisant l’API Java Migration de la couche de persistance vers Objectify Optimisation du rendu de la page avec les commentaires mis en cache grâce au service MemCache Création d’une servlet dont l’appel est planifié par le service CRON Dans le cadre du code labs, je me suis arrêté là. Mon premier site [7] Google AppEngine était en ligne. Bien entendu, pour les plus rapides, il était possible d’aller encore plus loin : compteur partagé, gestion d’index, upload de fichiers, réception et lecture d’un mail.\nPendant la phase de développement, suite à ce que je pensais être une anomalie, j’ai appris que lorsqu’une donnée est persistée dans BigTable, vous n’êtes pas certains de la retrouver immédiatement après lorsque vous requêter en lecture BigTable. En effet, il existe un temps de propagation. J’avoue que c’est assez déroutant, surtout lorsqu’on a pour référence le monde du relationnel où les données commitées en base sont immédiatement disponibles aux autres clients. Qui plus est, ce phénomène peut être accentué par une mauvaise utilisation du cache. Chose néanmoins rassurante : pas besoin de déployer sur AppEngine pour s’en apercevoir, ce comportement est observable depuis Eclipse.\nAvant de déployer son application, il est possible de spécifier sa version dans le fichier appengine-web.xml. Jusqu’à 10 versions d’une même application peuvent être accessibles en ligne simultanément. Pratique pour les mises en production et les éventuels retours arrière. Seule chose à garder en tête : toutes les versions partagent les mêmes données.\nConclusion\nEn quelques heures, j’ai pu mettre en ligne un site fonctionnel écrit en Java. Bien qu’ayant déjà expérimenté le déploiement d’applications sur d’autres plateformes (CloudBees et Heroku pour ne pas les citer), cela reste toujours impressionnant. Grâce au Cloud, le déploiement d’applications Java devient d’une facilité déconcertante, chose impossible il y’a encore quelques années. Sans pouvoir réaliser avec le monde PHP, il y’a désormais de nombreux hébergeurs Java et une concurrence accrue. Ces derniers fournissent pour la plupart un environnement gratuit en dessous d’une certaine charge.\nCet enthousiasme est malheureusement modéré par le lock-in observé sur AppEngine. En effet, plus on utilise de services AppEngine, plus l’application devient dépendante de la plateforme Cloud de Google.\nRéférences :\nPrésentation du code labs « Le Président est … » : http://www.devoxx.com/pages/viewpage.action?pageId=6128177 Application d’exemple pour le code labs de Devoxx : http://electionfr2012.appspot.com/ Site d’Objetify AppEngine : http://code.google.com/p/objectify-appengine/ Référentiel des plugins Eclipse 3.7 de Google : http://dl.google.com/eclipse/plugin/3.7 Code source de l’application exemple et de la présentation : http://code.google.com/p/devoxx-france-appengine/source/browse/ Projet Google HTML5 slides template : http://code.google.com/p/html5slides/ Mon premier site Google AppEngine : http://lepresidentantoine.appspot.com/ ","link":"https://javaetmoi.com/2012/04/devoxx-initiation-google-app-engine/","section":"posts","tags":["app-engine","bigtable","cloud","devoxx","google","html-5","java","nosql","objectify","paas"],"title":"Initiation à Google App Engine"},{"body":"","link":"https://javaetmoi.com/tags/objectify/","section":"tags","tags":null,"title":"Objectify"},{"body":"","link":"https://javaetmoi.com/tags/paas/","section":"tags","tags":null,"title":"Paas"},{"body":" Habitué aux releases maven avec SVN, j’ai rencontré quelques difficultés pour effectuer la première release du projet Hibernate Hydrate [1] hébergé sur GitHub et présenté dans un précédent billet.\nPour rappel, lors d’une release, le plugin maven accède au gestionnaire de code source pour commiter les modifications effectuées sur les pom.xml et créer un tag. Il déploie ensuite les artefacts sur le repo maven distant.\nMes contraintess techniques étaient les suivantes :\nPlateforme de développement : Windows 7, JDK 6, mSysGit Code source Java mavenisé et hébergé sur GitHub Le repo maven sur lequel déployer les artefacts maven est hébergé par CloudBees et accessible par le protocople Webdav [2] Les réponses apportées par ce billet sont :\nConfiguration maven pour GitHub Problème de passphrase SSH spécifique à Windows Configuration maven du repo CloudBees Configuration maven pour GitHub Pour permettre à maven d’accéder en lecture et en écriture à votre repo GitHub, vous devez tout d’abord configurer comme suit la balise de votre pom.xml :\n\u0026lt;scm\u0026gt; \u0026lt;url\u0026gt;https://github.com/arey/maven-config-github-cloudbees\u0026lt;/url\u0026gt; \u0026lt;connection\u0026gt;scm:git:ssh://git@github.com/arey/maven-config-github-cloudbees.git\u0026lt;/connection\u0026gt; \u0026lt;developerConnection\u0026gt;scm:git:ssh://git@github.com/arey/maven-config-github-cloudbees.git\u0026lt;/developerConnection\u0026gt; \u0026lt;/scm\u0026gt; L’accès en écriture sur un repo GitHub requière l’utilisation du protocole SSH. L’URL est conforme à ce qui est spécifié dans la documentation de référence maven[3] :\nscm:git:ssh://server_name[:port]/path_to_repository A noter une syntaxe légèrement différent au chemin SSH affiché sur GitHub : /arey et non :arey, le caractère : étant utilisé pour préciser le port de connexion.\nPour tester la configuration maven, vous pouvez par exemple utiliser le plugin scm pour créer un tag. C’est ce plugin qui est utilisé par le plugin release.\nmvn org.apache.maven.plugins:maven-scm-plugin:1.6:tag -Dtag=test -Dbasedir=. Vous devriez obtenir les logs suivants :\n[INFO] --- maven-scm-plugin:1.6:tag (default-cli) @ maven-config-github-cloudbees --- [INFO] Final Tag Name: \u0026#39;test\u0026#39; [INFO] Executing: cmd.exe /X /C \u0026#34;git tag -F D:\\tmp\\maven-scm-1264232534.commit test\u0026#34; [INFO] Working directory: D:\\dev\\workspaces\\WS_GitHub\\maven-config-github-cloudbees [INFO] Executing: cmd.exe /X /C \u0026#34;git push ssh://git@github.com/arey/maven-config-github-cloudbees.git test\u0026#34; [INFO] Working directory: D:\\dev\\workspaces\\WS_GitHub\\maven-config-github-cloudbees 1 minute. 2 minutes. Le plugin s’arrête là, comme bloqué. L’occupation CPU est à 0%. Ne cherchez pas, vous êtes sous Windows.\nProblème SSH spécifique à Windows En interne, le plugin scm exécute des lignes de commandes git. Sous Windows, la ligne de commande git est exécutée par l’interpréteur de commandes cmd.exe en mode non interactif. Or, lorsque vous essayez manuellement de pousser vos modifications vers GitHub depuis un bash git ou une ligne de commande avec git dans le path, Git vous demande systématiquement votre passphrase :\ngit push Enter passphrase for key \u0026#39;/c/Users/Antoine/.ssh/id_rsa\u0026#39;: L’explication est là : lors d’un tag ou d’un push, le plugin scm est bloqué car il attend votre passphrase. Pour autant, il ne vous demande jamais de le saisir. Vous vous retrouvez bloqué.\nL’une des solutions permettant de résoudre ce problème est de répondre à la question : « Comment faire pour que Windows retienne ma passphrase ? » Stackoverflow.com vous donne la réponse [4].\nEn résumé, vous allez demander à git d’utiliser PuTTY pour communiquer en SSH avec GitHub. L’agent d’authentification SSH Pageant sera utilisé pour conserver votre clés privée Github en mémoire pour que vous puissiez vous authentifier sans avoir besoin de retaper votre phrase de passe à chaque fois. Voici le mode opératoire :\nTélécharger puis décompresser l’archive putty.zip librement téléchargeable depuis le site de Putty [5]. Utiliser PuTTYGen.exe [6] pour convertir au format PuTTY (.ppk) votre clé RSA GitHub généré avec open SSH. Exécuter pageant.exe [7], ajouter la clé au format PuTTY and saisir le passphrase Déclarer la variable d’environnement GIT_SSH en spécifiant le chemin vers plink.exe [8], outil de connexion en ligne de commande utilisé pour automatiser des connexions. Pour tester la configuration, ouvrir une nouvelle fenêtre de commande et exécuter la commande suivante :\nC:\\Software\\Dev\\Putty\u0026gt;plink.exe git@github.com Using username \u0026#34;git\u0026#34;. Server refused to allocate pty Hi arey! You\u0026#39;ve successfully authenticated, but GitHub does not provide shell access. La création d’un tag par le plugin scm de maven doit désormais aboutir.\nConfiguration des repository Cloudbees Avant de pouvoir effectuer une release, il est encore nécessaire de configurer les repository maven de votre forge CloudBees. Il s’agit ici de configuration maven relativement ordinaire.\nLors de la phase de déploiement d’un artefact, 2 repositories sont nécessaires, l’un pour déployer des releases,et l’autre pour déployer des snapshots :\n\u0026lt;distributionManagement\u0026gt; \u0026lt;downloadUrl\u0026gt;https://github.com/arey/maven-config-github-cloudbee\u0026lt;/downloadUrl\u0026gt; \u0026lt;repository\u0026gt; \u0026lt;id\u0026gt;javaetmoi-cloudbees-release\u0026lt;/id\u0026gt; \u0026lt;name\u0026gt;javaetmoi-cloudbees-release\u0026lt;/name\u0026gt; \u0026lt;url\u0026gt;dav:https://repository-javaetmoi.forge.cloudbees.com/release/\u0026lt;/url\u0026gt; \u0026lt;/repository\u0026gt; \u0026lt;snapshotRepository\u0026gt; \u0026lt;id\u0026gt;javaetmoi-cloudbees-snapshot\u0026lt;/id\u0026gt; \u0026lt;name\u0026gt;javaetmoi-cloudbees-snapshot\u0026lt;/name\u0026gt; \u0026lt;url\u0026gt;dav:https://repository-javaetmoi.forge.cloudbees.com/snapshot/\u0026lt;/url\u0026gt; \u0026lt;/snapshotRepository\u0026gt; \u0026lt;/distributionManagement\u0026gt; Point d’attention : les repositories CloudBees ne sont accessibles en écriture que par le protocole WebDAV. Les URL des repository doivent donc être préfixées par un dav:\nL’extension maven wagon-webdav est requis pour que maven puisse interpréter le dav:. A ajouter dans la balise de votre configuration :\n\u0026lt;extensions\u0026gt; \u0026lt;extension\u0026gt; \u0026lt;groupId\u0026gt;org.apache.maven.wagon\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;wagon-webdav\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.0-beta-2\u0026lt;/version\u0026gt; \u0026lt;/extension\u0026gt; \u0026lt;/extensions\u0026gt; Afin que maven puisse accéder à ces repository pour télécharger les snapshots et les releases, il est nécessaire de les déclarer, soit dans le pom.xml de votre projet, soit dans le fichier setting.xml global ou local à l’utilisateur (ce qui est une bien meilleure pratique) :\n\u0026lt;repositories\u0026gt; \u0026lt;repository\u0026gt; \u0026lt;id\u0026gt;javaetmoi-cloudbees-release\u0026lt;/id\u0026gt; \u0026lt;name\u0026gt;javaetmoi-cloudbees-release\u0026lt;/name\u0026gt; \u0026lt;url\u0026gt;https://repository-javaetmoi.forge.cloudbees.com/release/\u0026lt;/url\u0026gt; \u0026lt;releases\u0026gt; \u0026lt;enabled\u0026gt;true\u0026lt;/enabled\u0026gt; \u0026lt;/releases\u0026gt; \u0026lt;snapshots\u0026gt; \u0026lt;enabled\u0026gt;false\u0026lt;/enabled\u0026gt; \u0026lt;/snapshots\u0026gt; \u0026lt;/repository\u0026gt; \u0026lt;repository\u0026gt; \u0026lt;id\u0026gt;javaetmoi-cloudbees-snapshot\u0026lt;/id\u0026gt; \u0026lt;name\u0026gt;javaetmoi-cloudbees-snapshot\u0026lt;/name\u0026gt; \u0026lt;url\u0026gt;https://repository-javaetmoi.forge.cloudbees.com/snapshot/\u0026lt;/url\u0026gt; \u0026lt;releases\u0026gt; \u0026lt;enabled\u0026gt;false\u0026lt;/enabled\u0026gt; \u0026lt;/releases\u0026gt; \u0026lt;snapshots\u0026gt; \u0026lt;enabled\u0026gt;true\u0026lt;/enabled\u0026gt; \u0026lt;/snapshots\u0026gt; \u0026lt;/repository\u0026gt; \u0026lt;/repositories\u0026gt; Lors d’un déploiement distant (ex : mvn deploy), maven doit disposer des paramètres de connexion pour écrire dans l’un ou l’autre des repository. A configurer dans le fichier setting.xml global ou local de l’utilisateur :\n\u0026lt;servers\u0026gt; \u0026lt;server\u0026gt; \u0026lt;id\u0026gt;javaetmoi-cloudbees-snapshot\u0026lt;/id\u0026gt; \u0026lt;username\u0026gt;javaetmoi\u0026lt;/username\u0026gt; \u0026lt;password\u0026gt;Mot de passe CloudBees\u0026lt;/password\u0026gt; \u0026lt;/server\u0026gt; \u0026lt;server\u0026gt; \u0026lt;id\u0026gt;javaetmoi-cloudbees-release\u0026lt;/id\u0026gt; \u0026lt;username\u0026gt;javaetmoi\u0026lt;/username\u0026gt; \u0026lt;password\u0026gt;Mot de passe CloudBees \u0026lt;/password\u0026gt; \u0026lt;/server\u0026gt; Attention, bien que le mot de passe soit celui que vous utilisez pour vous connecter à vore compte CloudBees, le username ne correspond pas à votre adresse email, mais celui spécifié dans la forge CloudBees comme le montre la capture d’écran ci-contre.\nPour figer sa version, le plugin maven release peut également être déclaré dans votre pom.xml :\n\u0026lt;plugin\u0026gt; \u0026lt;groupId\u0026gt;org.apache.maven.plugins\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;maven-release-plugin\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;2.2.2\u0026lt;/version\u0026gt; \u0026lt;/plugin\u0026gt; Ca y’est, toute la configuration est en place. A vous de jouer :\nmvn release:prepare release:perform Conclusion Afin d’avoir sous la main un squelette pour un prochain projet, j’ai initié un projet GitHub regroupant toute la configuration maven nécessaire : https://github.com/arey/maven-config-github-cloudbees [9]. Vous pouvez y télécharger l’intégralité du pom.xml et du settings.xml décrits dans cet article]. Ce projet a fait des émules puisque le code a été forké par la CloudBees-Community [10] de Github.\nPour ne plus avoir besoin à configurer PuTTY, un axe d’amélioration du plugin release serait de pouvoir s’appuyer un provider Git en full java, basé par exemple sur JGit[11] (projet utilisé par le plugin Eclipse EGit). Initié par Olivier Lamy, le projet maven-scm-provider-jgit [12] semble malheureusement s’être arrêté avant que jgit ne bascule sous le giron de la fondation Eclipse. Avis aux contributeurs !!\nRéférences :\nProjet Hibernate Hydrate hébergé sur Github Définition du protocole WebDAV sur Wikipédia Apache Maven SCM Git Implementation dans la documentation de référence maven Why git can’t remembrer my passphrase under Windows sur stackoverflow.com Télechargement de PuTTY Manuel utilisateur de PuTTYgen Manuel utilisateur de Pageant Manuel utilisateur de plink Projet Maven Config pour GitHub \u0026amp; CloudBees hébergé sur GitHub Communauté CloudBees sur GitHub Site officiel du projet JGit hébergé sur Eclipse.org Site officiel du projet maven-scm-provider-jgit hébergé sur Google Code ","link":"https://javaetmoi.com/2012/04/release-maven-windows-github-deploy-cloudbees/","section":"posts","tags":["cloudbees","git","github","maven"],"title":"Release Maven sous Windows d’un projet GitHub déployé sur CloudBees"},{"body":" Dans ce deuxième ticket, j’aimerais vous parler du projet Hibernate Hydrate [1] que j’ai récemment publié sur GitHub. Au cœur de ce projet : une seule classe Java proposant une unique fonctionnalité. En quelques années, c’est la seconde fois que j’ai eu besoin de coder ce genre de fonctionnalité. Aussi, je me suis dit qu’il serait pratique de l’avoir sous le coude pour une prochaine fois et, au passage, vous en faire profiter.\nOrigine des lazy exceptions\nEn quoi consistent ce projet et cette fameuse fonctionnalité ? Eh bien, sous certaines conditions, résoudre un problème récurrent lors de l’utilisation d’Hibernate. En effet, lorsque l’on tente d’accéder à un objet détaché de la session Hibernate, ce dernier n’est pas forcément entièrement chargé en mémoire : son proxy ou ses propriétés peuvent ne pas être initialisés, ce qui est par exemple le cas d’une relation déclarée comme paresseuse (ou lazy). Et c’est à cet instant-là, qu’Hibernate lève la tant redoutée LazyInitializationException.\nPar objet détaché, j’entends un objet évincé de la session (retirée du cache de premier niveau par un session.clear() ou un evict()) ou dont la session est fermée ( session.close())\nDans une application, ce phénomène est susceptible de se produire à plusieurs niveaux :\nCouche présentation : contrôleur (ex : action Struts) ou rendu de la vue (ex : JSP) Exposition d’un web service : marshalling XML, mapping dozer … La documentation d’Hibernate propose plusieurs solutions pour remédier à ce problème. Plus encore, elle explique ce qu’il faut éviter de faire, comme par exemple ouvrir une autre unité de travail (transaction) pour charger les données manquantes.\nSolutions préconisées\nPattern Open Session In View\nUne première solution consiste à utiliser le pattern Open Session In View [2]. Dans une application web, ce pattern peut par exemple être implémenté à l’aide d’un filtre de servlets. L’arrivée d’une requête HTTP initie l’ouverture d’une transaction et de la session Hibernate correspondante. Une fois la vue rendue et prête être renvoyée au client, la session est fermée et la transaction est validée.\nLe pattern Open Session In View ne peut pas s’appliquer dans une architecture technique 3 tiers où la couche présentation et la couche métier sont déployées physiquement sur 2 serveurs différents, et donc 2 JVMs. Ce pattern n’est également pas valable dans le cadre d’une application web riche utilisant Ajax et JavaScript pour récupérer puis parcourir le modèle métier.\nPré-chargement sur mesure\nPersonnellement, je recommande généralement à ce que les transactions soient gérées au niveau de la couche métier. En effet, à ce niveau, le service métier connait l’usage des objets qu’il va charger depuis la base de données. Il est capable d’utiliser le DAO ayant la stratégie de pré-chargement [3] (ou fetching) adaptée au traitement métier.\nPour rappel, le pré-chargement des relations peut être configuré de 2 manières :\nstatiquement au niveau du mapping Hibernate (en XML ou par annotations) dynamiquement lors du requêtage en HQL ( JOIN FETCH) ou par l’API Criteria (méthode setFetchMode()) Par défaut, dans Hibernate 3.x, les associations vers une autre entité ou une collection d’entités sont chargées tardivement ; c’est-à-dire à la demande, lorsque l’on essaie d’accéder à l’entité ou à la collection (et que la session est ouverte). Qui plus est, les stratégies définies statiquement ne sont pas forcément utilisées lors d’un requêtage HQL.\nUne bonne pratique issue du guide de référence d’Hibernate [4] consiste à conserver le comportement par défaut d’Hibernate et à redéfinir la stratégie de pré-chargement à chaque usage. Cela permet d’optimiser votre code et de ramener les données dont vous avez strictement besoin.\nComme illustré dans l’article Hibernate Survival Guide [5], et grâce au cache de premier niveau d’Hibernate, il est parfois plus performant de découper sa requête en plusieurs requêtes, notamment lorsque la grappe d’objets est complexe et la cardinalité des associations importante.\n**\nHybernate Hydrate à la rescousse**\nLa solution que je vais vous présenter peut être utilisée conjointement avec la solution du pré-chargement sur mesure.\nLa méthode statique deepHydrate de la classe LazyLoadingUtil permet de charger dans sa globalité la grappe d’objets qui lui est passée en paramètre. Seule contrainte, cette méthode doit être appelée avant que la session Hibernate et la transaction associée ne soient clôturées.\nVoici un exemple d’utilisation :\nEmployee employee = (Employee) session.get(Employee.class, 1); LazyLoadingUtil.deepHydrate(session, employee); Techniquement, la méthode deepHydrate() utilise le méta-modèle Hibernate (interface ClassMetadata) pour parcourir l’ensemble du graphe des objets persistés et déterminer le type des propriétés et des relations du modèle. Ainsi, elle peut naviguer récursivement dans le graphe. Les proxy rencontrés sont initialisés puis résolus. Les collections persistantes sont initialisés puis itérés.\nDans le cas d’un graphe cyclique, un mécanisme de garde permet d’éviter toute boucle infinie.\nLa classe TestLazyLoadingUtil propose des exemples d’utilisation.\nUne variante est disponible pour les applications utilisant JPA avec Hibernate pour provider : JpaLazyLoadingUtil.\nPour l’essayer, vous avez le choix entre un copier / coller, un git clone ou bien l’ajout d’une dépendance maven et du repo qui va avec :\n\u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.javaetmoi.core\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;javaetmoi-hibernate4-hydrate\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;2.0\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; Les artefacts du projet Hybernate Hydrate sont disponibles sur Maven Central.\nConclusion\nPour terminer cette présentation, voici un tableau récapitulatif qui devrait vous permettre d’orienter votre choix sur l’usage ou non de cette petite librairie :\nScénarios pour lesquels écarter cette solution****Scénarios favorables à son utilisation\nUtilisation du pattern Open Session in View\nProblématiques fortes de performance\nGrappes d’objets risquant de remonter toute la base de données\nGrappe d’objets constituée de nombreuses classes\nDéveloppement rapide d’une application avec chantier d’optimisations portant sur les requêtes critiques\nExploitation du cache de niveau 2 d’Hibernate pour les données en « bout de grappe » (ex : données du référentiel).\nOutil ayant besoin de charger toute une grappe d’objets en mémoire\nDe mon côté, ce modeste travail de capitalisation m’a permis de renouer avec l’open-source ; en tant que contributeur j’entends. Mon dernier code poussé sur Source Forge datait en effet de l’an 2000 …\nCela m’aura également permis de me familiariser davantage avec GitHub : syntaxe MarkDown, pages wiki gérées par git ou bien encore les releases avec maven. Ce dernier point fera d’ailleurs l’objet d’un prochain article.\nJ’ai également pu adhérer au programme Free and OpenSource (FOSS) de CloudBees, ce qui vous permet d’accéder librement au Jenkins d’Hibernate Hydrate [6].\nEnfin, si vous êtes un jour amené à utiliser ce code, je serais intéressé de le savoir. Et si vous voulez contribuer (ex : support d’autres ORM, annotation + post-processeur Spring …), les portes sont grandes ouvertes.\nRéférences :\nProjet Hibernate Hydrate hébergé sur Github Open Session In View du wiki Hibernate de JBoss A Short Primer On Fetching Strategies du wiki Hibernate de JBoss Performance fetching du guide de référence d’Hibernate Hibernate Survival Guide du wiki d’Object Direct Jenkins d’Hibernate Hydrate hébergé sur la plateforme DEV@Cloud de CloudBees[6]. ","link":"https://javaetmoi.com/2012/03/hibernate-dites-adieu-aux-lazy-initialization-exception/","section":"posts","tags":["cloudbees","github","hibernate","hql","jenkins","jpa","open-source","pattern"],"title":"Dites adieu aux LazyInitializationException"},{"body":"","link":"https://javaetmoi.com/tags/hql/","section":"tags","tags":null,"title":"Hql"},{"body":"","link":"https://javaetmoi.com/tags/open-source/","section":"tags","tags":null,"title":"Open-Source"},{"body":"","link":"https://javaetmoi.com/tags/pattern/","section":"tags","tags":null,"title":"Pattern"},{"body":"Le framework Spring est à l’honneur dans le premier article de ce tout jeune blog. En effet, début février j’ai eu l’opportunité de passer la Certification Core Spring 3.0. Et par ce billet, je tenais faire part de mon retour d’expérience mais surtout vous offrir librement un examen blanc.\nComme pré-requis pour passer la certification, sachez tout d’abord qu’il est nécessaire de suivre la formation officielle de 4 jours estampillée SpringSource. En France, elle est exclusivement assurée par le cabinet d’architecture Zenika. Mon formateur fut l’un de co-auteurs du très bon livre Spring par la Pratique.\nA l’issue de cette formation, un voucher vous est en principe envoyé par email par VMWare Education. Sans nouvelle 3 semaines après avoir suivi la formation, j’ai dû les relancer par email : eduoperations@vmware.com et certification@vmware.com. Ce fut efficace. Vous pourrez alors vous inscrire auprès d’un centre agréé Pearson Vue afin de planifier une date d’examen.\nAfin que vous puissiez estimer votre niveau et/ou préparer au mieux votre certification , je vous ai concocté un examen blanc ayant un niveau de difficulté que j’estime similaire à l’examen donnant droit à la certification. Ce dernier étant en anglais, les questions sont rédigées dans la langue de Shakespeare. Vous trouverez cet examen sous 2 formes :\nun fichier au format PDF, les réponses sont en fin de document : spring-certification-3-mock-exam-antoine une version en ligne de l’examen publiée sur le site Skill Guru : http://www.skill-guru.com/test/258/core-spring-3.0-certification-mock-exam Descriptif de l’examen blanc Questions : 50 Durée : 88 minutes Score minimum : 76 % (38 questions)\nRépartition non officielle par thème des 50 questions :\nContainer (13) Test (5) AOP (10) Data Access (5) Transactions (5) Spring MVC (3) Spring Security (3) Remoting (2) JMS (2) JMX (2) Comme vous pourrez le constater, les questions sont toutes assez courtes à lire. En 50 mn, j’avais répondu à l’ensemble les questions. J’ai donc eu le temps de toutes les relire. Bien entendu, la documentation de référence de Spring couvre l’ensemble des réponses de l’examen. Plus léger, mais fort bien conçu, le support de formation qui vous sera remis par Zenika contient également toutes les réponses. Le mien est versionné 3.2 et date de novembre 2011. Apparemment, il semble mis à jour régulièrement : des spécificités de Spring 3.1 étaient évoquées alors que cette version n’était pas encore sortie en GA. Enfin, en complément, voici une liste d’autres ressources web utiles à la préparation de l’examen : des guides, study notes et autres mock exams.\nGuide officiel des notions à connaitre pour passer la certification : http://www.springsource.com/files/core-spring-3.0-certification-study-guide_0.pdf Blog très complet de Jeanne Boyarsky\u0026rsquo;s sur la certification : http://www.selikoff.net/2010/08/20/jeannes-core-spring-3-certification-experiences/ Site de Gavin Lasnitzki dédié à la certification : http://springcert.sourceforge.net/ FAQ du site CodeRanch : http://www.coderanch.com/how-to/java/SpringCertificationFaq Post sur le forum de SpringSource : http://forum.springsource.org/showthread.php?62548-Certification-questions-mock-exams Tests payants sur le site Skill Guru (moins de 2$) : http://www.skill-guru.com/test/81/core-spring-3.0-certification-mock http://skill-guru.com/test/86/core-spring-3.0-certification-mock-test-2 Spring 3 Certification Mock sur Knowledge Black Belt (ex: JavaBlackBelt). N’ayant pas le nombre de points nécessaires, je n’ai malheureusement pas pu tester cet examen blanc : http://knowledgeblackbelt.com/#!Topic/Spring-3-Certification-Mock Si vous avez des remarques/corrections/suggestions à émettre ou si vous souhaitez d’autres informations sur la certification, n’hésitez pas à laisser un commentaire.\n","link":"https://javaetmoi.com/2012/02/core-spring-3-0-certification-mock-exam/","section":"posts","tags":["aop","certification","exam","ioc","java","jms","jmx","mvc","remoting","spring-framework","springsource","test","transaction"],"title":"Core Spring 3.0 Certification Mock Exam"},{"body":"","link":"https://javaetmoi.com/tags/ioc/","section":"tags","tags":null,"title":"Ioc"},{"body":"","link":"https://javaetmoi.com/tags/jmx/","section":"tags","tags":null,"title":"Jmx"},{"body":"Java et moi Ma passion pour la programmation a commencé par le logo (enseigné en CE2 sur des MO5 !!), le turbo pascal (en 3ième), l\u0026rsquo;assembleur x86 (en 2nde) puis le C++ (en terminale). La création de jeux vidéos était mon hobby avec ZeGame ou Les Fourmis.\nJ\u0026rsquo;ai découvert Java en 1999 au cours de mes études à l\u0026rsquo;INSA de Lyon. En 2000, lors d\u0026rsquo;un stage en Californie chez IBM au Silicon Valley Lab , j\u0026rsquo;ai développé ma première application Java avec Swing : le Migration TookKit. J\u0026rsquo;ai eu l’opportunité de travailler avec des pontes de DB2, du XML et de SWT, mais également de participer à la Java One au Moscone Center. Depuis, mis à part 3 années passées sur les technologies .NET, je n\u0026rsquo;ai plus quitté l\u0026rsquo;écosystème du monde Java / Java EE.\nDomaines de compétence La stack Java EE, Spring et Hibernate m\u0026rsquo;accompagnent au quotidien.\nRigoureux, l\u0026rsquo;outillage, l\u0026rsquo;intégration continue, la qualité et le TDD me tiennent particulièrement à coeur. Fin 2010, j\u0026rsquo;ai découvert l\u0026rsquo;outil distribué de gestion de version Git. Depuis, je pousse mes collègues pour passer dessus, quitte à devoir utiliser le bridge git-svn. J\u0026rsquo;espère qu\u0026rsquo;ils se reconnaîtront.\nPourquoi un blog ? Et bien tout d\u0026rsquo;abord, parce que cette idée me trottait dans la tête depuis un petit moment. Ma société ne proposait malheureusement pas à ses ingénieurs de poster des articles sur un blog public. Aussi, après avoir investit pas mal d\u0026rsquo;effort pour mettre au point un examen blanc pour la certification Core Spring 3, il me fallait un endroit où en parler sur le Net : c\u0026rsquo;était l\u0026rsquo;occasion ou jamais d\u0026rsquo;ouvrir mon blog.\nLes billets se focaliseront avant tout sur des retours d’expérience, la présentation d’outils /frameworks ou bien encore des trucs et astuces acquis sur le terrain ou sur mon temps libre.\n","link":"https://javaetmoi.com/about/","section":"pages","tags":null,"title":"A Propos"}]