<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>Java &amp; Moi</title>
	<atom:link href="https://javaetmoi.com/feed/" rel="self" type="application/rss+xml" />
	<link>https://javaetmoi.com</link>
	<description>Développeur Java, Spring &#38; co, et fier de l&#039;être</description>
	<lastBuildDate>Mon, 06 Apr 2026 13:35:47 +0000</lastBuildDate>
	<language>fr-FR</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=6.9.4</generator>

<image>
	<url>https://javaetmoi.com/wp-content/uploads/2022/05/cropped-java-icon-32x32.png</url>
	<title>Java &amp; Moi</title>
	<link>https://javaetmoi.com</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>Découverte de Spring Modulith</title>
		<link>https://javaetmoi.com/2026/04/decouverte-de-spring-modulith/</link>
					<comments>https://javaetmoi.com/2026/04/decouverte-de-spring-modulith/#respond</comments>
		
		<dc:creator><![CDATA[Antoine]]></dc:creator>
		<pubDate>Mon, 06 Apr 2026 13:26:43 +0000</pubDate>
				<category><![CDATA[Retour d'expérience]]></category>
		<category><![CDATA[Spring]]></category>
		<category><![CDATA[Spring Boot]]></category>
		<category><![CDATA[Spring Modulith]]></category>
		<category><![CDATA[Spring Petclinic]]></category>
		<guid isPermaLink="false">https://javaetmoi.com/?p=2642</guid>

					<description><![CDATA[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. J’avais besoin pour cela d’une application simple et universelle. Vous commencez à me connaitre&#160;: mon choix s’est &#8230; <a href="https://javaetmoi.com/2026/04/decouverte-de-spring-modulith/" class="more-link">Continuer la lecture de <span class="screen-reader-text">Découverte de Spring Modulith</span>  <span class="meta-nav">&#8594;</span></a>]]></description>
										<content:encoded><![CDATA[<div class="wp-block-image">
<figure class="alignleft size-full"><a href="https://javaetmoi.com/wp-content/uploads/2026/03/image.png"><img fetchpriority="high" decoding="async" width="225" height="225" src="https://javaetmoi.com/wp-content/uploads/2026/03/image.png" alt="" class="wp-image-2644" srcset="https://javaetmoi.com/wp-content/uploads/2026/03/image.png 225w, https://javaetmoi.com/wp-content/uploads/2026/03/image-150x150.png 150w" sizes="(max-width: 225px) 100vw, 225px" /></a></figure>
</div>


<p>En 2025, j’ai eu l’opportunité de mettre en place <a href="https://spring.io/projects/spring-modulith"><strong>Spring Modulith</strong></a> 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. <br> <br> J’avais besoin pour cela d’une application simple et universelle. Vous commencez à me connaitre&nbsp;: mon choix s’est naturellement porté sur la version canonique de <strong>Spring Petclinic</strong>.</p>



<p>Pris 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 : <a href="https://github.com/spring-petclinic/spring-petclinic-modulith">spring-petclinic-modulith</a>. <br> <br> Dans 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. </p>


<div class="wp-block-image">
<figure class="aligncenter size-medium"><a href="https://javaetmoi.com/wp-content/uploads/2026/04/illustration-spring-petclinic-modulith-v2.png"><img decoding="async" width="232" height="300" src="https://javaetmoi.com/wp-content/uploads/2026/04/illustration-spring-petclinic-modulith-v2-232x300.png" alt="Illustre le découpage en modules de l'application Spring Modulith à l'aide de Spring Modulith" class="wp-image-2654" srcset="https://javaetmoi.com/wp-content/uploads/2026/04/illustration-spring-petclinic-modulith-v2-232x300.png 232w, https://javaetmoi.com/wp-content/uploads/2026/04/illustration-spring-petclinic-modulith-v2-791x1024.png 791w, https://javaetmoi.com/wp-content/uploads/2026/04/illustration-spring-petclinic-modulith-v2-768x995.png 768w, https://javaetmoi.com/wp-content/uploads/2026/04/illustration-spring-petclinic-modulith-v2.png 1024w" sizes="(max-width: 232px) 100vw, 232px" /></a></figure>
</div>


<span id="more-2642"></span>



<h2 class="wp-block-heading">Architecture modulaire</h2>



<p>
  L&rsquo;architecture en <strong>microservices</strong> a le vent en poupe depuis une quinzaine d’années. Pourtant, force est de constater que nombre d’applications métiers restent des <strong>monolithes</strong>. Ce n&rsquo;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 <a href="https://martinfowler.com/bliki/MonolithFirst.html">Monolith First</a> de Martin Fowler). Un monolithe bien structuré, celui qu&rsquo;<a href="https://github.com/odrotbohm">Oliver Drotbohm</a> (le créateur de Spring Modulith) appelle le <strong>modulith </strong>ou que certains qualifient de <strong>modular monolith</strong>, représente souvent le meilleur compromis entre simplicité opérationnelle et maintenabilité au quotidien. Le projet Spring Modulith permet d’outiller cette approche. 
</p>



<p>
  Après plusieurs années de gestation, Spring Modulith a été rendu GA en <strong>août 2023</strong>. Relativement jeune, ce projet apporte un cadre structurant aux applications Spring Boot monolithiques en y introduisant la notion de <strong>modules applicatifs</strong>. Vérification de l&rsquo;architecture au build, documentation générée automatiquement, communication inter-modules par événements, tests d&rsquo;intégration ciblés… le tout sans nécessairement d’infrastructure externe.
</p>



<h2 class="wp-block-heading">Les fonctionnalités de Spring Modulith</h2>



<p>
  Avant de plonger dans le code, prenons un peu de hauteur. Spring Modulith repose sur un principe simple : <strong>chaque sous-package direct du package de la classe principale Spring Boot </strong>(celle annotée avec <strong><em>@SpringBootApplication</em></strong>)<strong> constitue un module applicatif</strong>. Par convention, le package racine du module expose l&rsquo;API publique ; tous les sous-packages sont considérés comme privés.
</p>



<p>
  À partir de cette convention d’organisation, Spring Modulith propose un ensemble de fonctionnalités complémentaires :
</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th><p><strong>Fonctionnalité</strong>
</p></th><th><p><strong>Description</strong>
</p></th></tr></thead><tbody><tr><td><p><strong>Vérification structurelle</strong><span style="font-family: inherit; font-weight: inherit; background-color: rgb(255, 255, 255);"></span></p></td><td>Lors de la construction de l’application, le test d’architecture <code>ApplicationModules.verify()</code> vérifie qu&rsquo;aucun module n&rsquo;accède aux packages internes d&rsquo;un autre module et qu&rsquo;il n&rsquo;existe pas de dépendances cycliques.<span style="font-family: inherit; font-weight: inherit; background-color: rgb(255, 255, 255);"></span></td></tr><tr><td><p><strong>Communication par événements</strong><span style="font-family: inherit; font-weight: inherit; background-color: rgb(255, 255, 255);"></span></p></td><td><p><code>ApplicationEventPublisher</code> et <code>@ApplicationModuleListener</code> permettent de découpler les modules sans appel direct entre beans Spring.<span style="font-family: inherit; font-weight: inherit; background-color: rgb(255, 255, 255);"></span></p></td></tr><tr><td><p><strong>Registre de publication des événements</strong><span style="font-family: inherit; font-weight: inherit; background-color: rgb(255, 255, 255);"></span></p></td><td>Persiste chaque événement en base de données (table <code>event_publication</code>) avant l&rsquo;exécution du listener, ce qui garantit la livraison des évenements au moins une fois (le <em>at-least-once delivery</em>).<span style="font-family: inherit; font-weight: inherit; background-color: rgb(255, 255, 255);"></span></td></tr><tr><td><p><strong>Moments</strong><span style="font-family: inherit; font-weight: inherit; background-color: rgb(255, 255, 255);"></span></p></td><td><p><br>  Publie automatiquement des événements temporels (<code>DayHasPassed</code>, <code>HourHasPassed</code> …) pour remplacer les <code>@Scheduled</code>.<span style="font-family: inherit; font-weight: inherit; background-color: rgb(255, 255, 255);"></span></p></td></tr><tr><td><p><strong>Tests d&rsquo;intégration modulaires</strong><span style="font-family: inherit; font-weight: inherit; background-color: rgb(255, 255, 255);"></span></p></td><td><p><code>@ApplicationModuleTest</code> restreint le chargement du contexte Spring Boot au module applicatif testé. L&rsquo;API <code>Scenario</code> orchestre les tests asynchrones.<span style="font-family: inherit; font-weight: inherit; background-color: rgb(255, 255, 255);"></span></p></td></tr><tr><td><p><strong>Documentation</strong><span style="font-family: inherit; font-weight: inherit; background-color: rgb(255, 255, 255);"></span></p></td><td><p>L’API <code>Documenter</code> produit des diagrammes C4 PlantUML et des Application <em>Module Canvas</em> AsciiDoc décrivant l&rsquo;architecture du code.<span style="font-family: inherit; font-weight: inherit; background-color: rgb(255, 255, 255);"></span></p></td></tr><tr><td><p><strong>Actuator</strong><span style="font-family: inherit; font-weight: inherit; background-color: rgb(255, 255, 255);"></span></p></td><td><p>La sonde <code>/actuator/modulith</code><em> </em>expose le graphe de modules applicatifs au runtime.<span style="font-family: inherit; font-weight: inherit; background-color: rgb(255, 255, 255);"></span></p></td></tr><tr><td><p><strong>Observabilité</strong><span style="font-family: inherit; font-weight: inherit; background-color: rgb(255, 255, 255);"></span></p></td><td><p>L’artefact <code>spring-modulith-observability</code> instrumente automatiquement les beans exposés et génère des <em>spans </em>Micrometer pour chaque interaction inter-modules.<span style="font-family: inherit; font-weight: inherit; background-color: rgb(255, 255, 255);"></span></p></td></tr></tbody></table></figure>



<p>
  La <a href="%5D(https:/docs.spring.io/spring-modulith/reference/index.html">documentation officielle de Spring Modulith</a> 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 <a href="https://github.com/spring-projects/spring-petclinic">Spring Petclinic</a>, 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&rsquo;animaux, ses vétérinaires et la prise de rendez-vous. 
</p>



<h2 class="wp-block-heading">Étape 1 &#8211; Ajouter les dépendances Maven</h2>



<p>L’application Spring Petclinic supporte les deux principaux systèmes de build du monde Java&nbsp;: Maven et Gradle. Spring Petclinic Modulith également. Dans ce billet, par simplicité, nous nous focaliserons sur le <strong>build</strong> <strong>Maven</strong>.</p>



<p>Toute intégration de Spring Modulith commence par l&rsquo;<strong>ajout du BOM </strong>et des premières dépendances. Dans le <a href="https://github.com/spring-petclinic/spring-petclinic-modulith/blob/4.0.0/pom.xml">pom.xml</a>, nous déclarons d&rsquo;abord la version sous forme de properties (bonne pratique Maven) :</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>&lt;spring-modulith.version>2.0.5&lt;/spring-modulith.version> </textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #81A1C1">&lt;spring-modulith.version&gt;</span><span style="color: #D8DEE9FF">2.0.5</span><span style="color: #81A1C1">&lt;/spring-modulith.version&gt;</span><span style="color: #D8DEE9FF"> </span></span></code></pre></div>



<p>La version 2.x de Spring Modulith est compatible Spring Boot 4. </p>



<p>Puis on importe le BOM dans <code>&lt;dependencyManagement&gt;</code><strong> </strong>:</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>&lt;dependencyManagement>
  &lt;dependencies>
    &lt;dependency>
      &lt;groupId>org.springframework.modulith&lt;/groupId>
      &lt;artifactId>spring-modulith-bom&lt;/artifactId>
      &lt;version>${spring-modulith.version}&lt;/version>
      &lt;type>pom&lt;/type>
      &lt;scope>import&lt;/scope>
    &lt;/dependency>
  &lt;/dependencies>
&lt;/dependencyManagement></textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #81A1C1">&lt;dependencyManagement&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">  </span><span style="color: #81A1C1">&lt;dependencies&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">&lt;dependency&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">      </span><span style="color: #81A1C1">&lt;groupId&gt;</span><span style="color: #D8DEE9FF">org.springframework.modulith</span><span style="color: #81A1C1">&lt;/groupId&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">      </span><span style="color: #81A1C1">&lt;artifactId&gt;</span><span style="color: #D8DEE9FF">spring-modulith-bom</span><span style="color: #81A1C1">&lt;/artifactId&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">      </span><span style="color: #81A1C1">&lt;version&gt;</span><span style="color: #D8DEE9FF">${spring-modulith.version}</span><span style="color: #81A1C1">&lt;/version&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">      </span><span style="color: #81A1C1">&lt;type&gt;</span><span style="color: #D8DEE9FF">pom</span><span style="color: #81A1C1">&lt;/type&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">      </span><span style="color: #81A1C1">&lt;scope&gt;</span><span style="color: #D8DEE9FF">import</span><span style="color: #81A1C1">&lt;/scope&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">&lt;/dependency&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">  </span><span style="color: #81A1C1">&lt;/dependencies&gt;</span></span>
<span class="line"><span style="color: #81A1C1">&lt;/dependencyManagement&gt;</span></span></code></pre></div>



<p>
  Et enfin les dépendances minimales :
</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>&lt;!-- Annotations et API publique Spring Modulith -->
&lt;dependency>
  &lt;groupId>org.springframework.modulith&lt;/groupId>
  &lt;artifactId>spring-modulith-api&lt;/artifactId>
&lt;/dependency>
&lt;!-- Support JUnit 5 pour la vérification modulaire -->
&lt;dependency>
  &lt;groupId>org.springframework.modulith&lt;/groupId>
  &lt;artifactId>spring-modulith-starter-test&lt;/artifactId>
  &lt;scope>test&lt;/scope>
&lt;/dependency></textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #616E88">&lt;!-- Annotations et API publique Spring Modulith --&gt;</span></span>
<span class="line"><span style="color: #81A1C1">&lt;dependency&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">  </span><span style="color: #81A1C1">&lt;groupId&gt;</span><span style="color: #D8DEE9FF">org.springframework.modulith</span><span style="color: #81A1C1">&lt;/groupId&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">  </span><span style="color: #81A1C1">&lt;artifactId&gt;</span><span style="color: #D8DEE9FF">spring-modulith-api</span><span style="color: #81A1C1">&lt;/artifactId&gt;</span></span>
<span class="line"><span style="color: #81A1C1">&lt;/dependency&gt;</span></span>
<span class="line"><span style="color: #616E88">&lt;!-- Support JUnit 5 pour la vérification modulaire --&gt;</span></span>
<span class="line"><span style="color: #81A1C1">&lt;dependency&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">  </span><span style="color: #81A1C1">&lt;groupId&gt;</span><span style="color: #D8DEE9FF">org.springframework.modulith</span><span style="color: #81A1C1">&lt;/groupId&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">  </span><span style="color: #81A1C1">&lt;artifactId&gt;</span><span style="color: #D8DEE9FF">spring-modulith-starter-test</span><span style="color: #81A1C1">&lt;/artifactId&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">  </span><span style="color: #81A1C1">&lt;scope&gt;</span><span style="color: #D8DEE9FF">test</span><span style="color: #81A1C1">&lt;/scope&gt;</span></span>
<span class="line"><span style="color: #81A1C1">&lt;/dependency&gt;</span></span></code></pre></div>



<p>
  Deux dépendances suffisent pour démarrer. D’autres dépendances seront ajoutées au fil de l&rsquo;article.
</p>



<h2 class="wp-block-heading">Étape 2 &#8211; Le test de vérification modulaire</h2>



<p>
  C&rsquo;est le point d&rsquo;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 :
</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>package 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();
    }
}</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #81A1C1">package</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">org</span><span style="color: #ECEFF4">.</span><span style="color: #8FBCBB">springframework</span><span style="color: #ECEFF4">.</span><span style="color: #8FBCBB">samples</span><span style="color: #ECEFF4">.</span><span style="color: #8FBCBB">petclinic</span><span style="color: #81A1C1">;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #81A1C1">import</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">org</span><span style="color: #ECEFF4">.</span><span style="color: #8FBCBB">junit</span><span style="color: #ECEFF4">.</span><span style="color: #8FBCBB">jupiter</span><span style="color: #ECEFF4">.</span><span style="color: #8FBCBB">api</span><span style="color: #ECEFF4">.</span><span style="color: #8FBCBB">Test</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #81A1C1">import</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">org</span><span style="color: #ECEFF4">.</span><span style="color: #8FBCBB">springframework</span><span style="color: #ECEFF4">.</span><span style="color: #8FBCBB">modulith</span><span style="color: #ECEFF4">.</span><span style="color: #8FBCBB">core</span><span style="color: #ECEFF4">.</span><span style="color: #8FBCBB">ApplicationModules</span><span style="color: #81A1C1">;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #81A1C1">class</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">ModularityTests</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #8FBCBB">ApplicationModules</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">modules</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">ApplicationModules</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">of</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9">PetClinicApplication</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">class</span><span style="color: #ECEFF4">)</span><span style="color: #81A1C1">;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">@</span><span style="color: #D08770">Test</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">void</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">verifiesModularStructure</span><span style="color: #ECEFF4">()</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #D8DEE9">modules</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">verify</span><span style="color: #ECEFF4">()</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #ECEFF4">}</span></span></code></pre></div>



<p>L’appel <code>ApplicationModules.of(...)</code> scanne les packages de l&rsquo;application et construit un modèle en mémoire des modules détectés. L&rsquo;appel à la méthode <code>verify() </code>s&rsquo;assure ensuite trois aspects :</p>



<ol class="wp-block-list">
<li><strong>Pas de cycle</strong> entre les modules applicatifs</li>



<li><strong>Pas d&rsquo;accès aux packages internes</strong> d&rsquo;un module depuis un autre module</li>



<li><strong>Respect des dépendances explicites</strong> (si configurées via l’annotation <code>@ApplicationModule</code>)</li>
</ol>



<p>Si une de ces règles est violée, le test échoue avec un message d&rsquo;erreur précis. Voici un exemple dans lequel un cycle est détecté&nbsp;: </p>



<pre class="wp-block-code"><code>org.springframework.modulith.core.Violations: - Cycle detected: Slice owner -&gt;                  
                Slice vet -&gt; 
                Slice owner</code></pre>



<p>La version Ultimate d’<strong>IntelliJ IDEA </strong>est packagée avec le <strong>plugin Spring Modulith</strong>. 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 <a href="https://www.jetbrains.com/help/idea/spring-modulith.html#apply-the-spring-modulith-guidelines">documentation de cette fonctionnalité</a>.</p>



<h2 class="wp-block-heading">Étape 3 &#8211; Identifier les modules applicatifs</h2>



<p>Spring Modulith détecte automatiquement les modules à partir des <strong>sous-packages directs</strong> du package contenant la classe main <code>@SpringBootApplication</code>. Dans Spring Petclinic, la classe <code>PetClinicApplication</code> est localisée au niveau du package <strong><em>org.springframework.samples.petclinic</em></strong>.</p>



<p>
  Les modules identifiés étaient à ce stade au nombre de quatre&nbsp;:
</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><tbody><tr><td><p><strong>Module</strong><span style="font-family: inherit; font-weight: inherit; background-color: rgb(255, 255, 255);"></span></p></td><td><p><strong>Package racine</strong><span style="font-family: inherit; font-weight: inherit;"></span></p></td></tr><tr><td><p> owner<span style="font-family: inherit; font-weight: inherit; background-color: rgb(255, 255, 255);"></span></p></td><td><p><code>org.springframework.samples.petclinic.owner</code><span style="font-family: inherit; font-weight: inherit; background-color: rgb(255, 255, 255);"></span></p></td></tr><tr><td><p> vet<span style="font-family: inherit; font-weight: inherit; background-color: rgb(255, 255, 255);"></span></p></td><td><code>o<span style="font-size: revert; font-family: inherit; font-weight: inherit;">rg.springframework.samples.petclinic.vet</span></code></td></tr><tr><td><p>system<span style="font-family: inherit; font-weight: inherit; background-color: rgb(255, 255, 255);"></span></p></td><td><p><code>org.springframework.samples.petclinic.system</code><span style="font-family: inherit; font-weight: inherit; background-color: rgb(255, 255, 255);"></span></p></td></tr><tr><td><p><s>model</s><span style="font-family: inherit; font-weight: inherit; background-color: rgb(255, 255, 255);"></span></p></td><td><p><s><code>org.springframework.samples.petclinic.model</code></s><span style="font-family: inherit; font-weight: inherit; background-color: rgb(255, 255, 255);"></span></p></td></tr></tbody></table></figure>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img decoding="async" width="572" height="578" src="https://javaetmoi.com/wp-content/uploads/2026/03/word-image-2642-1.png" alt="" class="wp-image-2646" srcset="https://javaetmoi.com/wp-content/uploads/2026/03/word-image-2642-1.png 572w, https://javaetmoi.com/wp-content/uploads/2026/03/word-image-2642-1-297x300.png 297w" sizes="(max-width: 572px) 100vw, 572px" /></figure>
</div>


<p>Cette modularisation fonctionnelle de l’application Spring Petclinic avait été réalisée en 2016 par Dave Syer dans la PR <a href="https://github.com/spring-projects/spring-petclinic/pull/200">#200 Modernize Spring apps structure</a>. Oliver Drotbohm avait d’ailleurs participé à la conversation.</p>



<p>Le module <code>model</code> mutualisait 3 classes de base JPA <code>BaseEntity</code>, <code>Person</code>, <code>NamedEntity</code> partagées entre les modules <code>owner</code> et <code>vet</code>. 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 <strong>Bounded Context</strong> possède intégralement son modèle du domaine métier. Les classes <code>BaseEntity</code>, <code>Person</code>, <code>NamedEntity</code> ne sont pas des concepts métier. Ce sont des raccourcis techniques. Inliner le contenu de ces classes techniques dans <code>vet</code> et <code>owner</code> 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 <code>model</code> a été purement et simplement supprimé.</p>


<div class="wp-block-image">
<figure class="aligncenter"><img loading="lazy" decoding="async" width="572" height="482" src="https://javaetmoi.com/wp-content/uploads/2026/03/word-image-2642-2.png" alt="" class="wp-image-2647" srcset="https://javaetmoi.com/wp-content/uploads/2026/03/word-image-2642-2.png 572w, https://javaetmoi.com/wp-content/uploads/2026/03/word-image-2642-2-300x253.png 300w" sizes="auto, (max-width: 572px) 100vw, 572px" /></figure>
</div>


<h2 class="wp-block-heading">Étape 4- Séparer l&rsquo;API publique des détails d&rsquo;implémentation</h2>



<p>
  Spring Modulith attribue un rôle bien défini à chaque package :
</p>



<ol class="wp-block-list">
<li>Le <strong>package racine du module </strong>(ex&nbsp;: <code>vet/</code><em>)</em> expose l&rsquo;API publique : les types que les autres modules ont le droit d&rsquo;utiliser</li>



<li>Les <strong>sous-packages</strong> (ex&nbsp;: <code>vet/internal/</code>) sont considérés comme internes : leur utilisation est interdite depuis les autres modules</li>
</ol>



<p>Pour chaque module de Spring Petclinic, j&rsquo;ai donc commencé par déplacer les classes d&rsquo;implémentation — contrôleurs, repositories, entités JPA dans un sous-package nommé <code>internal</code><strong><em> </em></strong>(nom de package donné par convention, mais tout autre nommage est possible). A ma grande surprise, le package racine de chaque module était vide&nbsp;: les 3 modules étaient parfaitement découplés.</p>



<p>Les 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&rsquo;API publique du module et ce qui est un détail d&rsquo;implémentation. Et c&rsquo;est Spring Modulith qui garantit que cette frontière est respectée via le test <code>verify()</code><strong><em>.</em></strong></p>



<p>Dans 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&nbsp;: découpage en couches techniques pour l’un, architecture hexagonale pour l’autre. <br> Dans Petclinic, le package <code>internal</code> du module <code>owner</code> 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&nbsp;: <code>ui</code>, <code>application</code> et <code>domain</code>.</p>


<div class="wp-block-image">
<figure class="aligncenter"><img loading="lazy" decoding="async" width="286" height="466" src="https://javaetmoi.com/wp-content/uploads/2026/03/word-image-2642-3.png" alt="" class="wp-image-2648" srcset="https://javaetmoi.com/wp-content/uploads/2026/03/word-image-2642-3.png 286w, https://javaetmoi.com/wp-content/uploads/2026/03/word-image-2642-3-184x300.png 184w" sizes="auto, (max-width: 286px) 100vw, 286px" /></figure>
</div>


<h2 class="wp-block-heading">Étape 5 &#8211; Communication par événements entre modules</h2>



<p><br> La communication par évènements est une fonctionnalité phare de Spring Modulith qu’on peut utiliser en déclarant l’artefact <code>spring-modulith-events-api</code>&nbsp;:</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>&lt;dependency> 
  &lt;groupId>org.springframework.modulith&lt;/groupId> 
  &lt;artifactId>spring-modulith-events-api&lt;/artifactId> &lt;/dependency></textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #81A1C1">&lt;dependency&gt;</span><span style="color: #D8DEE9FF"> </span></span>
<span class="line"><span style="color: #D8DEE9FF">  </span><span style="color: #81A1C1">&lt;groupId&gt;</span><span style="color: #D8DEE9FF">org.springframework.modulith</span><span style="color: #81A1C1">&lt;/groupId&gt;</span><span style="color: #D8DEE9FF"> </span></span>
<span class="line"><span style="color: #D8DEE9FF">  </span><span style="color: #81A1C1">&lt;artifactId&gt;</span><span style="color: #D8DEE9FF">spring-modulith-events-api</span><span style="color: #81A1C1">&lt;/artifactId&gt;</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">&lt;/dependency&gt;</span></span></code></pre></div>



<p>
  Pour illustrer ce mécanisme, j&rsquo;ai ajouté à Spring Petclinic un nouveau cas d’utilisation métier : lorsqu&rsquo;un rendez-vous est réservé, le système affecte automatiquement le vétérinaire le moins chargé.
</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://javaetmoi.com/wp-content/uploads/2026/04/screenshot-spring-petclinic-modulith.png"><img loading="lazy" decoding="async" width="1024" height="481" src="https://javaetmoi.com/wp-content/uploads/2026/04/screenshot-spring-petclinic-modulith-1024x481.png" alt="Screenshot of the Veterinarians menu of the Spring Modulith version of Spring Petclinic" class="wp-image-2662" srcset="https://javaetmoi.com/wp-content/uploads/2026/04/screenshot-spring-petclinic-modulith-1024x481.png 1024w, https://javaetmoi.com/wp-content/uploads/2026/04/screenshot-spring-petclinic-modulith-300x141.png 300w, https://javaetmoi.com/wp-content/uploads/2026/04/screenshot-spring-petclinic-modulith-768x360.png 768w, https://javaetmoi.com/wp-content/uploads/2026/04/screenshot-spring-petclinic-modulith.png 1221w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></a><figcaption class="wp-element-caption">Un vétérinaire est automatiquement affecté aux visites planifiées.</figcaption></figure>
</div>


<p>Plutôt que d&rsquo;injecter un bean du module <code>vet</code> dans le module <code>owner</code>, on remplace l&rsquo;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 <a href="https://github.com/spring-petclinic/spring-petclinic-modulith/blob/4.0.0/src/main/java/org/springframework/samples/petclinic/owner/application/VisitScheduler.java">VisitScheduler</a> utilise la classe <code>ApplicationEventPublisher</code> de Spring Framework pour émettre l’évènement <a href="https://github.com/spring-petclinic/spring-petclinic-modulith/blob/4.0.0/src/main/java/org/springframework/samples/petclinic/owner/VisitBooked.java">VisitBooked</a>.</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>@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()));
}</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #ECEFF4">@</span><span style="color: #D08770">Transactional</span></span>
<span class="line"><span style="color: #81A1C1">public</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">void</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">bookVisit</span><span style="color: #ECEFF4">(</span><span style="color: #8FBCBB">Owner</span><span style="color: #D8DEE9FF"> owner</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">Integer</span><span style="color: #D8DEE9FF"> petId</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">Visit</span><span style="color: #D8DEE9FF"> visit</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #8FBCBB">Owner</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">managedOwner</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">owners</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">findById</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9">owner</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">getId</span><span style="color: #ECEFF4">()).</span><span style="color: #88C0D0">orElseThrow</span><span style="color: #ECEFF4">()</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #D8DEE9">managedOwner</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">addVisit</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">petId</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> visit</span><span style="color: #ECEFF4">)</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #D8DEE9">owners</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">flush</span><span style="color: #ECEFF4">()</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #D8DEE9">eventPublisher</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">publishEvent</span><span style="color: #ECEFF4">(</span><span style="color: #81A1C1">new</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">VisitBooked</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9">visit</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">getId</span><span style="color: #ECEFF4">(),</span><span style="color: #D8DEE9FF"> petId</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">visit</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">getDate</span><span style="color: #ECEFF4">()))</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #ECEFF4">}</span></span></code></pre></div>



<p>Le record <a href="https://github.com/spring-petclinic/spring-petclinic-modulith/blob/4.0.0/src/main/java/org/springframework/samples/petclinic/owner/VisitBooked.java">VisitBook</a> fait partie de l’<strong>interface publique </strong>du module <code>owner</code>. On le déclare donc au niveau du package racine du module <code>owner</code>&nbsp;:</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>package org.springframework.samples.petclinic.owner;

public record VisitBooked(int visitId, int petId, LocalDate date) {
}</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #81A1C1">package</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">org</span><span style="color: #ECEFF4">.</span><span style="color: #8FBCBB">springframework</span><span style="color: #ECEFF4">.</span><span style="color: #8FBCBB">samples</span><span style="color: #ECEFF4">.</span><span style="color: #8FBCBB">petclinic</span><span style="color: #ECEFF4">.</span><span style="color: #8FBCBB">owner</span><span style="color: #81A1C1">;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #81A1C1">public</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">record</span><span style="color: #D8DEE9FF"> VisitBooked</span><span style="color: #ECEFF4">(</span><span style="color: #81A1C1">int</span><span style="color: #D8DEE9FF"> visitId</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">int</span><span style="color: #D8DEE9FF"> petId</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">LocalDate</span><span style="color: #D8DEE9FF"> date</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #ECEFF4">}</span></span></code></pre></div>



<p>La classe <a href="https://github.com/spring-petclinic/spring-petclinic-modulith/blob/4.0.0/src/main/java/org/springframework/samples/petclinic/vet/internal/VetEventListener.java">VetEventListener</a> du module <code>vet</code> réagit à cet événement via l’annotation <code>@ApplicationModuleListener </code>de Spring Modulith :</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>@Component
class VetEventListener {

    private final VetRoster vetRoster;

    VetEventListener(VetRoster vetRoster) {
       this.vetRoster = vetRoster;
    }

    @ApplicationModuleListener
    void on(VisitBooked event) {
       vetRoster.assignVet(event);
    }
}</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #ECEFF4">@</span><span style="color: #D08770">Component</span></span>
<span class="line"><span style="color: #81A1C1">class</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">VetEventListener</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">private</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">final</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">VetRoster</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">vetRoster</span><span style="color: #81A1C1">;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #88C0D0">VetEventListener</span><span style="color: #ECEFF4">(</span><span style="color: #8FBCBB">VetRoster</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">vetRoster</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">       </span><span style="color: #81A1C1">this</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">vetRoster</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> vetRoster</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">}</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">@</span><span style="color: #D08770">ApplicationModuleListener</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">void</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">on</span><span style="color: #ECEFF4">(</span><span style="color: #8FBCBB">VisitBooked</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">event</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">       </span><span style="color: #D8DEE9">vetRoster</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">assignVet</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">event</span><span style="color: #ECEFF4">)</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #ECEFF4">}</span></span></code></pre></div>



<p>L&rsquo;annotation <code><strong>@ApplicationModuleListener</strong></code> (source: <code>spring-modulith-events-api</code>) est un sucre syntaxique combinant trois annotations en une : <code><strong>@Async</strong></code> (source: <code>spring-context</code>), <strong><code>@Transactional </code></strong>(source: <code>spring-tx</code>) et <strong><code>@TransactionalEventListener </code></strong>(source: <code>spring-tx</code>). Ce listener s&rsquo;exécute après le commit de la transaction émettrice, dans une nouvelle transaction, de façon asynchrone. Le module <code>owner</code> ne connaît pas le module <code>vet</code>. Le découplage est garanti par la structure des packages.</p>



<p>La mise à jour du tableau de garde des vétérinaires est assurée par le service <a href="https://github.com/spring-petclinic/spring-petclinic-modulith/blob/4.0.0/src/main/java/org/springframework/samples/petclinic/vet/internal/VetRoster.java">VetRoster</a>. L&rsquo;affectation est persistée dans une nouvelle table <code>visit_assignments</code> qui appartient conceptuellement au module <strong><em>vet</em></strong>.</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>CREATE 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
);</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #81A1C1">CREATE</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">TABLE</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">IF</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">NOT</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">EXISTS</span><span style="color: #D8DEE9FF"> visit_assignments (</span></span>
<span class="line"><span style="color: #D8DEE9FF">  visit_id   </span><span style="color: #81A1C1">INT</span><span style="color: #D8DEE9FF">  </span><span style="color: #81A1C1">NOT NULL</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">PRIMARY KEY</span><span style="color: #D8DEE9FF">,</span></span>
<span class="line"><span style="color: #D8DEE9FF">  vet_id     </span><span style="color: #81A1C1">INT</span><span style="color: #D8DEE9FF">  </span><span style="color: #81A1C1">NOT NULL</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">REFERENCES</span><span style="color: #D8DEE9FF"> vets (id),</span></span>
<span class="line"><span style="color: #D8DEE9FF">  visit_date </span><span style="color: #81A1C1">DATE</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">NOT NULL</span></span>
<span class="line"><span style="color: #D8DEE9FF">);</span></span></code></pre></div>



<p>Notez ici un point important : la colonne <code>visit_id</code><strong><em> </em></strong>de cette table est une <strong>référence lâche</strong>, intentionnellement sans clé étrangère vers la table <code>visits</code> du module <code>owner</code>. C&rsquo;est le miroir en base de données du découplage Java : le module <code>vet</code> ne connaît que l&rsquo;identifiant publié dans l&rsquo;événement <a href="https://github.com/spring-petclinic/spring-petclinic-modulith/blob/4.0.0/src/main/java/org/springframework/samples/petclinic/owner/VisitBooked.java">VisitBooked</a>, pas l&rsquo;entité <a href="https://github.com/spring-petclinic/spring-petclinic-modulith/blob/4.0.0/src/main/java/org/springframework/samples/petclinic/owner/domain/Visit.java">Visit</a> elle-même. Les modules communiquent par identifiants, pas par références d&rsquo;objets ni par clés étrangères croisées.</p>



<h2 class="wp-block-heading">Étape 6 – Déclarer les dépendances autorisées</h2>



<p>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&nbsp;: <strong><code>@Modulithic</code></strong> et<code><strong>@ApplicationModule</strong></code>.</p>



<p>On commencer par annoter la classe main de l’application Petclinic avec <strong><code>@Modulithic</code> </strong>:</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>@Modulithic(systemName = "PetClinic")
@SpringBootApplication
public class PetClinicApplication { ...}</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #ECEFF4">@</span><span style="color: #D08770">Modulithic</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9">systemName</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">PetClinic</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #ECEFF4">@</span><span style="color: #D08770">SpringBootApplication</span></span>
<span class="line"><span style="color: #81A1C1">public</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">class</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">PetClinicApplication</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">...}</span></span></code></pre></div>



<p>Puis, dans les fichiers<strong><em> </em></strong><code>package-info.java</code> de chaque module, on utilise <code>@ApplicationModule </code>:</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>// 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 = { "owner" })
package org.springframework.samples.petclinic.vet;

// system/package-info.java — aucune dépendance
@ApplicationModule
package org.springframework.samples.petclinic.system;</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #616E88">// owner/package-info.java — aucune dépendance</span></span>
<span class="line"><span style="color: #ECEFF4">@</span><span style="color: #D08770">ApplicationModule</span></span>
<span class="line"><span style="color: #81A1C1">package</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">org</span><span style="color: #ECEFF4">.</span><span style="color: #8FBCBB">springframework</span><span style="color: #ECEFF4">.</span><span style="color: #8FBCBB">samples</span><span style="color: #ECEFF4">.</span><span style="color: #8FBCBB">petclinic</span><span style="color: #ECEFF4">.</span><span style="color: #8FBCBB">owner</span><span style="color: #81A1C1">;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #616E88">// vet/package-info.java — dépend du module owner</span></span>
<span class="line"><span style="color: #ECEFF4">@</span><span style="color: #D08770">ApplicationModule</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9">allowedDependencies</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">owner</span><span style="color: #ECEFF4">&quot;</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">})</span></span>
<span class="line"><span style="color: #81A1C1">package</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">org</span><span style="color: #ECEFF4">.</span><span style="color: #8FBCBB">springframework</span><span style="color: #ECEFF4">.</span><span style="color: #8FBCBB">samples</span><span style="color: #ECEFF4">.</span><span style="color: #8FBCBB">petclinic</span><span style="color: #ECEFF4">.</span><span style="color: #8FBCBB">vet</span><span style="color: #81A1C1">;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #616E88">// system/package-info.java — aucune dépendance</span></span>
<span class="line"><span style="color: #ECEFF4">@</span><span style="color: #D08770">ApplicationModule</span></span>
<span class="line"><span style="color: #81A1C1">package</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">org</span><span style="color: #ECEFF4">.</span><span style="color: #8FBCBB">springframework</span><span style="color: #ECEFF4">.</span><span style="color: #8FBCBB">samples</span><span style="color: #ECEFF4">.</span><span style="color: #8FBCBB">petclinic</span><span style="color: #ECEFF4">.</span><span style="color: #8FBCBB">system</span><span style="color: #81A1C1">;</span></span></code></pre></div>



<p>Ces garde-fous architecturaux sont ici exploités par le <a href="https://github.com/spring-petclinic/spring-petclinic-modulith/blob/4.0.0/src/test/java/org/springframework/samples/petclinic/ModularityTests.java">ModularityTests</a>. Si un développeur (ou un agent de codage) introduit une dépendance non autorisée, le test échoue immédiatement.</p>



<h2 class="wp-block-heading">Étape 7 — L&rsquo;Event Publication Registry</h2>



<p>Sans harnais de sécurité, un événement publié mais dont le listener échoue serait perdu à jamais. L&rsquo;<a href="https://docs.spring.io/spring-modulith/reference/events.html#publication-registry">Event Publication Registry</a> résout ce problème en persistant chaque événement en base de données <strong>avant</strong> l&rsquo;exécution du listener. <br> Spring Modulith supporte 4 technologies de persistance&nbsp;: JDBC, JPA, MongoDB et Neo4j. <br> Bien que Spring Petclinic repose sur des repositories Spring Data JPA, j’ai choisi d’exploiter le <strong>support</strong> <strong>JDBC</strong> de Spring Modulith. Compatible JPA, il propose la <strong>propriété <code>spring.modulith.events.jdbc.schema-initialization.enabled</code></strong> permettant de créer la <strong>table <code>event_publication</code></strong>.</p>



<p>
  La mise en place tient en une dépendance :
</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>&lt;dependency>
  &lt;groupId>org.springframework.modulith&lt;/groupId>
  &lt;artifactId>spring-modulith-starter-jdbc&lt;/artifactId>
&lt;/dependency></textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #81A1C1">&lt;dependency&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">  </span><span style="color: #81A1C1">&lt;groupId&gt;</span><span style="color: #D8DEE9FF">org.springframework.modulith</span><span style="color: #81A1C1">&lt;/groupId&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">  </span><span style="color: #81A1C1">&lt;artifactId&gt;</span><span style="color: #D8DEE9FF">spring-modulith-starter-jdbc</span><span style="color: #81A1C1">&lt;/artifactId&gt;</span></span>
<span class="line"><span style="color: #81A1C1">&lt;/dependency&gt;</span></span></code></pre></div>



<p>Et trois propriétés dans le fichier de configuration <code>application.properties</code> :</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly># 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</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #D8DEE9FF"># </span><span style="color: #8FBCBB">Crée</span><span style="color: #D8DEE9FF"> automatiquement la table event_publication au démarrage</span></span>
<span class="line"><span style="color: #D8DEE9">spring</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">modulith</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">events</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">jdbc</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">schema</span><span style="color: #81A1C1">-</span><span style="color: #D8DEE9">initialization</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">enabled</span><span style="color: #81A1C1">=true</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D8DEE9FF"># </span><span style="color: #8FBCBB">Supprime</span><span style="color: #D8DEE9FF"> les publications complétées immédiatement</span></span>
<span class="line"><span style="color: #D8DEE9">spring</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">modulith</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">events</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">completion</span><span style="color: #81A1C1">-</span><span style="color: #D8DEE9FF">mode</span><span style="color: #81A1C1">=</span><span style="color: #8FBCBB">DELETE</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D8DEE9FF"># Re</span><span style="color: #81A1C1">-</span><span style="color: #D8DEE9FF">publie les événements non traités au redémarrage</span></span>
<span class="line"><span style="color: #D8DEE9">spring</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">modulith</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">events</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">republish</span><span style="color: #81A1C1">-</span><span style="color: #D8DEE9FF">outstanding</span><span style="color: #81A1C1">-</span><span style="color: #D8DEE9FF">events</span><span style="color: #81A1C1">-</span><span style="color: #D8DEE9FF">on</span><span style="color: #81A1C1">-</span><span style="color: #D8DEE9FF">restart</span><span style="color: #81A1C1">=true</span></span></code></pre></div>



<p>Spring Modulith intercepte chaque appel à <code>publishEvent() </code>et insère une ligne dans la table <code>event_publication </code>au sein de la transaction initiale. Si le listener s&rsquo;exécute avec succès, l&rsquo;entrée est supprimée (mode <strong><code>DELETE</code></strong>). Si le listener échoue ou si l&rsquo;application crashe, l&rsquo;entrée reste en base et sera rejouée au redémarrage de Petclinic. Cette garantie <em>at-least-once delivery </em>fonctionne sans infrastructure externe : pas besoin de Kafka, de RabbitMQ ni de quelconque broker de messages. Un simple SGBD relationnel suffit.</p>



<h2 class="wp-block-heading">Étape 8 &#8211; Moments : les événements temporels</h2>



<p><br> Spring Modulith propose un module <strong><code>spring-modulith-moment</code>s</strong> qui publie automatiquement des événements marquant le passage du temps : <strong><code>HourHasPassed</code></strong>, <strong><code>DayHasPassed</code></strong>, <strong><code>WeekHasPassed</code></strong>, etc. C&rsquo;est une alternative élégante aux classiques <strong><code>@Scheduled </code></strong>de Spring.</p>



<p>Sur notre application, le module vet utilise l’évènement <strong><strong><code>DayHasPassed</code></strong></strong> pour nettoyer quotidiennement les affectations aux vétérinaires dont la date est passée. Dans la classe <a href="https://github.com/spring-petclinic/spring-petclinic-modulith/blob/4.0.0/src/main/java/org/springframework/samples/petclinic/vet/internal/VetEventListener.java">VetEventListener</a>, on déclare une seconde méthode&nbsp;:</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>@EventListener
void on(DayHasPassed event) {
    vetRoster.cleanupPastAssignments(event.getDate());
}</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #ECEFF4">@</span><span style="color: #D08770">EventListener</span></span>
<span class="line"><span style="color: #81A1C1">void</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">on</span><span style="color: #ECEFF4">(</span><span style="color: #8FBCBB">DayHasPassed</span><span style="color: #D8DEE9FF"> event</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #D8DEE9">vetRoster</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">cleanupPastAssignments</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9">event</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">getDate</span><span style="color: #ECEFF4">())</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #ECEFF4">}</span></span></code></pre></div>



<p>Notez l&rsquo;utilisation de l’annotation Spring Framework <code><strong>@EventListener</strong> </code>(et non <code>@ApplicationModuleListener</code>) : l&rsquo;événement <strong><code>DayHasPassed</code></strong> est publié par Spring Modulith lui-même en dehors de toute transaction applicative.</p>



<h2 class="wp-block-heading">Étape 9 &#8211; Tests d&rsquo;intégration modulaires</h2>



<p>La modularité apportée par Spring Modulith présente un autre avantage&nbsp;: sa capacité à <strong>bootstrapper un seul module </strong>en isolation. L&rsquo;annotation <code><strong>@ApplicationModuleTest</strong> </code>remplace ainsi <strong><code>@SpringBootTest</code></strong> 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é.</p>



<p>Exemple d’utilisation sur le test <strong><a href="https://github.com/spring-petclinic/spring-petclinic-modulith/blob/4.0.0/src/test/java/org/springframework/samples/petclinic/owner/application/VisitSchedulerTests.java">VisitSchedulerTests</a></strong>&nbsp;:</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>@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("Annual checkup");

       // When / Then
       scenario.stimulate(() -> visitScheduler.bookVisit(owner, pet.getId(), visit))
          .andWaitForEventOfType(VisitBooked.class)
          .matching(event -> event.petId() == pet.getId())
          .toArriveAndVerify(event -> then(event.petId()).isEqualTo(pet.getId()));
    }

}</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #ECEFF4">@</span><span style="color: #D08770">ApplicationModuleTest</span></span>
<span class="line"><span style="color: #81A1C1">class</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">VisitSchedulerTests</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">@</span><span style="color: #D08770">Autowired</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #8FBCBB">OwnerRepository</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">owners</span><span style="color: #81A1C1">;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">@</span><span style="color: #D08770">Autowired</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #8FBCBB">VisitScheduler</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">visitScheduler</span><span style="color: #81A1C1">;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">@</span><span style="color: #D08770">Test</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">void</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">bookVisitShouldPublishVisitBookedEvent</span><span style="color: #ECEFF4">(</span><span style="color: #8FBCBB">Scenario</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">scenario</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #ECEFF4">       </span><span style="color: #616E88">// Given</span></span>
<span class="line"><span style="color: #D8DEE9FF">       </span><span style="color: #8FBCBB">Owner</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">owner</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">owners</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">findById</span><span style="color: #ECEFF4">(</span><span style="color: #B48EAD">1</span><span style="color: #ECEFF4">).</span><span style="color: #88C0D0">orElseThrow</span><span style="color: #ECEFF4">()</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">       </span><span style="color: #8FBCBB">Pet</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">pet</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">owner</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">getPets</span><span style="color: #ECEFF4">().</span><span style="color: #88C0D0">iterator</span><span style="color: #ECEFF4">().</span><span style="color: #88C0D0">next</span><span style="color: #ECEFF4">()</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">       </span><span style="color: #8FBCBB">Visit</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">visit</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">new</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">Visit</span><span style="color: #ECEFF4">()</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">       </span><span style="color: #D8DEE9">visit</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">setDescription</span><span style="color: #ECEFF4">(</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">Annual checkup</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">)</span><span style="color: #81A1C1">;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #ECEFF4">       </span><span style="color: #616E88">// When / Then</span></span>
<span class="line"><span style="color: #D8DEE9FF">       </span><span style="color: #D8DEE9">scenario</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">stimulate</span><span style="color: #ECEFF4">(()</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">-&gt;</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">visitScheduler</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">bookVisit</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">owner</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">pet</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">getId</span><span style="color: #ECEFF4">(),</span><span style="color: #D8DEE9FF"> visit</span><span style="color: #ECEFF4">))</span></span>
<span class="line"><span style="color: #D8DEE9FF">          </span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">andWaitForEventOfType</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9">VisitBooked</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">class</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">          </span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">matching</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">event </span><span style="color: #8FBCBB">-&gt;</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">event</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">petId</span><span style="color: #ECEFF4">()</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">==</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">pet</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">getId</span><span style="color: #ECEFF4">())</span></span>
<span class="line"><span style="color: #D8DEE9FF">          </span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">toArriveAndVerify</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">event </span><span style="color: #8FBCBB">-&gt;</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">then</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9">event</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">petId</span><span style="color: #ECEFF4">()).</span><span style="color: #88C0D0">isEqualTo</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9">pet</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">getId</span><span style="color: #ECEFF4">()))</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">}</span></span>
<span class="line"></span>
<span class="line"><span style="color: #ECEFF4">}</span></span></code></pre></div>



<p>Notez ici l’utilisation de l&rsquo;<strong>API Scenario</strong> de Spring Modulith Test. On définit un stimulus (l&rsquo;appel à <code>bookVisit</code>), on déclare l&rsquo;événement attendu (<a href="https://github.com/spring-petclinic/spring-petclinic-modulith/blob/4.0.0/src/main/java/org/springframework/samples/petclinic/owner/VisitBooked.java">VisitBooked</a>), on pose un critère de correspondance (<em>matching</em>) et on vérifie. Le tout de manière fluide.</p>



<p>Les logs d’exécution du test donnent un aperçu des beans Spring chargés par <code>@ApplicationModuleTest</code> :</p>



<p>Bootstrapping @org.springframework.modulith.test.ApplicationModuleTest for Owner in mode STANDALONE (class org.springframework.samples.petclinic.PetClinicApplication)…<br><br># Owner<br>&gt; Logical name: owner<br>&gt; Base package: org.springframework.samples.petclinic.owner<br>&gt; Excluded packages: none<br>&gt; Direct module dependencies: none<br>&gt; Spring beans:<br>  o ….application.VisitScheduler<br>  o ….domain.OwnerRepository<br>  o ….domain.PetTypeRepository<br>  o ….ui.OwnerController<br>  o ….ui.PetController<br>  o ….ui.PetTypeFormatter<br>  o ….ui.VisitController</p>



<p><br> Appartenant au module system, la classe de test existante <a href="https://github.com/spring-petclinic/spring-petclinic-modulith/blob/main/src/test/java/org/springframework/samples/petclinic/system/internal/CrashControllerIntegrationTests.java">CrashControllerIntegrationTests</a> a pu bénéficier de l’annotation <code>@ApplicationModuleTest</code>. <br> Contrairement à <code>@SpringBootTest</code>, <code>@ApplicationModuleTest</code> n’expose pas d’attribut <code>properties</code>. Cette limitation a pu être contournée grâce à l’annotation <code>TestPropertySource</code> de Spring Test. </p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>// Avant
@SpringBootTest(webEnvironment = RANDOM_PORT,
       properties = { "spring.web.error.include-message=ALWAYS", "management.endpoints.access.default=none" })
@AutoConfigureTestRestTemplate
class CrashControllerIntegrationTests {

// Après
@ApplicationModuleTest(webEnvironment = RANDOM_PORT)
@TestPropertySource(
       properties = { "spring.web.error.include-message=ALWAYS", "management.endpoints.access.default=none" })
@AutoConfigureTestRestTemplate
class CrashControllerIntegrationTests {</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #616E88">// Avant</span></span>
<span class="line"><span style="color: #ECEFF4">@</span><span style="color: #D08770">SpringBootTest</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9">webEnvironment</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> RANDOM_PORT</span><span style="color: #ECEFF4">,</span></span>
<span class="line"><span style="color: #D8DEE9FF">       </span><span style="color: #D8DEE9">properties</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">spring.web.error.include-message=ALWAYS</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">management.endpoints.access.default=none</span><span style="color: #ECEFF4">&quot;</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">})</span></span>
<span class="line"><span style="color: #ECEFF4">@</span><span style="color: #D08770">AutoConfigureTestRestTemplate</span></span>
<span class="line"><span style="color: #81A1C1">class</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">CrashControllerIntegrationTests</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"></span>
<span class="line"><span style="color: #616E88">// Après</span></span>
<span class="line"><span style="color: #ECEFF4">@</span><span style="color: #D08770">ApplicationModuleTest</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9">webEnvironment</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> RANDOM_PORT</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #ECEFF4">@</span><span style="color: #D08770">TestPropertySource</span><span style="color: #ECEFF4">(</span></span>
<span class="line"><span style="color: #D8DEE9FF">       </span><span style="color: #D8DEE9">properties</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">spring.web.error.include-message=ALWAYS</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">management.endpoints.access.default=none</span><span style="color: #ECEFF4">&quot;</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">})</span></span>
<span class="line"><span style="color: #ECEFF4">@</span><span style="color: #D08770">AutoConfigureTestRestTemplate</span></span>
<span class="line"><span style="color: #81A1C1">class</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">CrashControllerIntegrationTests</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span></code></pre></div>



<p>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.</p>



<h2 class="wp-block-heading">Étape 10 &#8211;  Génération de documentation</h2>



<p>Spring Modulith permet de générer automatiquement de la documentation à partir du modèle de modules. Il suffit d’ajouter l’artefact <strong><code>spring-modulith-docs</code> </strong>dans le <code>pom.xml</code> puis d&rsquo;enrichir notre classe <a href="https://github.com/spring-petclinic/spring-petclinic-modulith/blob/4.0.0/src/test/java/org/springframework/samples/petclinic/ModularityTests.java">ModularityTests</a> :</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>@Test
void writeDocumentation() {
    new Documenter(modules).writeDocumentation();
}</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #ECEFF4">@</span><span style="color: #D08770">Test</span></span>
<span class="line"><span style="color: #81A1C1">void</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">writeDocumentation</span><span style="color: #ECEFF4">()</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">new</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">Documenter</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">modules</span><span style="color: #ECEFF4">).</span><span style="color: #88C0D0">writeDocumentation</span><span style="color: #ECEFF4">()</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #ECEFF4">}</span></span></code></pre></div>



<p>L&rsquo;appel à <code><strong>writeDocumentation()</strong> </code>produit dans le répertoire <code>target/spring-modulith-docs/</code> :</p>



<ol class="wp-block-list">
<li>Des <strong>diagrammes C4 au format PlantUML</strong> (<em>.puml</em>) représentant les relations entre modules</li>



<li>Des «&nbsp;<strong>modules Canvas&nbsp;» au format AsciiDoc</strong> (<em>.adoc</em>) listant pour chaque module : les beans Spring exposés (non visible sur Petclinic) ainsi que les événements publiés et écoutés</li>



<li>Un <strong>document de synthèse</strong> (<em>all-docs.adoc</em>) agrégeant l&rsquo;ensemble des diagrammes et canvas<br></li>
</ol>



<p>Exemple de rendu du fichier <code>module-vet.puml&nbsp;</code>:</p>


<div class="wp-block-image">
<figure class="aligncenter"><img loading="lazy" decoding="async" width="201" height="506" src="https://javaetmoi.com/wp-content/uploads/2026/03/word-image-2642-4.png" alt="" class="wp-image-2649" srcset="https://javaetmoi.com/wp-content/uploads/2026/03/word-image-2642-4.png 201w, https://javaetmoi.com/wp-content/uploads/2026/03/word-image-2642-4-119x300.png 119w" sizes="auto, (max-width: 201px) 100vw, 201px" /></figure>
</div>


<p>
  
  <br>
</p>



<p>Exemple de rendu du fichier <code>module-vet.adoc&nbsp;</code>: <br> <img loading="lazy" decoding="async" width="690" height="261" class="wp-image-2650" src="https://javaetmoi.com/wp-content/uploads/2026/03/word-image-2642-5.png" srcset="https://javaetmoi.com/wp-content/uploads/2026/03/word-image-2642-5.png 690w, https://javaetmoi.com/wp-content/uploads/2026/03/word-image-2642-5-300x113.png 300w" sizes="auto, (max-width: 690px) 100vw, 690px" /></p>



<p>
  L&rsquo;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. 
  <br>
  Cela 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.
</p>



<h1 class="wp-block-heading">Étape 11 &#8211; l&rsquo;endpoint Actuator</h1>



<p>
  Cette dernière étape consiste exposer le graphe de modules au runtime. Deux dépendances Maven sont nécessaires :
</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>&lt;dependency>
  &lt;groupId>org.springframework.modulith&lt;/groupId>
  &lt;artifactId>spring-modulith-actuator&lt;/artifactId>
  &lt;scope>runtime&lt;/scope>
&lt;/dependency>
&lt;dependency>
  &lt;groupId>org.springframework.modulith&lt;/groupId>
  &lt;artifactId>spring-modulith-runtime&lt;/artifactId>
  &lt;scope>runtime&lt;/scope>
&lt;/dependency></textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #81A1C1">&lt;dependency&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">  </span><span style="color: #81A1C1">&lt;groupId&gt;</span><span style="color: #D8DEE9FF">org.springframework.modulith</span><span style="color: #81A1C1">&lt;/groupId&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">  </span><span style="color: #81A1C1">&lt;artifactId&gt;</span><span style="color: #D8DEE9FF">spring-modulith-actuator</span><span style="color: #81A1C1">&lt;/artifactId&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">  </span><span style="color: #81A1C1">&lt;scope&gt;</span><span style="color: #D8DEE9FF">runtime</span><span style="color: #81A1C1">&lt;/scope&gt;</span></span>
<span class="line"><span style="color: #81A1C1">&lt;/dependency&gt;</span></span>
<span class="line"><span style="color: #81A1C1">&lt;dependency&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">  </span><span style="color: #81A1C1">&lt;groupId&gt;</span><span style="color: #D8DEE9FF">org.springframework.modulith</span><span style="color: #81A1C1">&lt;/groupId&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">  </span><span style="color: #81A1C1">&lt;artifactId&gt;</span><span style="color: #D8DEE9FF">spring-modulith-runtime</span><span style="color: #81A1C1">&lt;/artifactId&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">  </span><span style="color: #81A1C1">&lt;scope&gt;</span><span style="color: #D8DEE9FF">runtime</span><span style="color: #81A1C1">&lt;/scope&gt;</span></span>
<span class="line"><span style="color: #81A1C1">&lt;/dependency&gt;</span></span></code></pre></div>



<p>Un appel GET sur l’URL <code>http://localhost:8080/actuator/modulith </code>renvoie le graphe complet des modules au format JSON : noms, packages et dépendances. Cette sonde est pratique pour visualiser l&rsquo;architecture de l’application déployée sans avoir besoin d&rsquo;aller regarder le code ou la documentation. En production, pensez néanmoins à désactiver ou sécuriser cet actuator. </p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>{
  "owner": {
    "displayName": "Owner",
    "basePackage": "org.springframework.samples.petclinic.owner",
    "nested": [],
    "type": "closed",
    "shared": false,
    "namedInterfaces": {
      "&lt;&lt;UNNAMED>>": &#91;
        "org.springframework.samples.petclinic.owner.VisitBooked"
      &#93;
    },
    "initializers": [],
    "dependencies": []
  },
  "system": {
    "displayName": "System",
    "basePackage": "org.springframework.samples.petclinic.system",
    "nested": [],
    "type": "closed",
    "shared": false,
    "namedInterfaces": {
      "&lt;&lt;UNNAMED>>": []
    },
    "initializers": [],
    "dependencies": []
  },
  "vet": {
    "displayName": "Vet",
    "basePackage": "org.springframework.samples.petclinic.vet",
    "nested": [],
    "type": "closed",
    "shared": false,
    "namedInterfaces": {
      "&lt;&lt;UNNAMED>>": []
    },
    "initializers": [],
    "allowedDependencies": &#91;
      "owner"
    &#93;,
    "dependencies": [
      {
        "target": "owner",
        "types": &#91;
          "EVENT_LISTENER"
        &#93;
      }
    ]
  }
}</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">  </span><span style="color: #ECEFF4">&quot;</span><span style="color: #8FBCBB">owner</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">&quot;</span><span style="color: #8FBCBB">displayName</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">Owner</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">,</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">&quot;</span><span style="color: #8FBCBB">basePackage</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">org.springframework.samples.petclinic.owner</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">,</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">&quot;</span><span style="color: #8FBCBB">nested</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">[],</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">&quot;</span><span style="color: #8FBCBB">type</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">closed</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">,</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">&quot;</span><span style="color: #8FBCBB">shared</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">false</span><span style="color: #ECEFF4">,</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">&quot;</span><span style="color: #8FBCBB">namedInterfaces</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">      </span><span style="color: #ECEFF4">&quot;</span><span style="color: #8FBCBB">&lt;&lt;UNNAMED&gt;&gt;</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&#91;</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">org.springframework.samples.petclinic.owner.VisitBooked</span><span style="color: #ECEFF4">&quot;</span></span>
<span class="line"><span style="color: #D8DEE9FF">      </span><span style="color: #ECEFF4">&#93;</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">},</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">&quot;</span><span style="color: #8FBCBB">initializers</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">[],</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">&quot;</span><span style="color: #8FBCBB">dependencies</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">[]</span></span>
<span class="line"><span style="color: #D8DEE9FF">  </span><span style="color: #ECEFF4">},</span></span>
<span class="line"><span style="color: #D8DEE9FF">  </span><span style="color: #ECEFF4">&quot;</span><span style="color: #8FBCBB">system</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">&quot;</span><span style="color: #8FBCBB">displayName</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">System</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">,</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">&quot;</span><span style="color: #8FBCBB">basePackage</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">org.springframework.samples.petclinic.system</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">,</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">&quot;</span><span style="color: #8FBCBB">nested</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">[],</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">&quot;</span><span style="color: #8FBCBB">type</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">closed</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">,</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">&quot;</span><span style="color: #8FBCBB">shared</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">false</span><span style="color: #ECEFF4">,</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">&quot;</span><span style="color: #8FBCBB">namedInterfaces</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">      </span><span style="color: #ECEFF4">&quot;</span><span style="color: #8FBCBB">&lt;&lt;UNNAMED&gt;&gt;</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">[]</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">},</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">&quot;</span><span style="color: #8FBCBB">initializers</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">[],</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">&quot;</span><span style="color: #8FBCBB">dependencies</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">[]</span></span>
<span class="line"><span style="color: #D8DEE9FF">  </span><span style="color: #ECEFF4">},</span></span>
<span class="line"><span style="color: #D8DEE9FF">  </span><span style="color: #ECEFF4">&quot;</span><span style="color: #8FBCBB">vet</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">&quot;</span><span style="color: #8FBCBB">displayName</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">Vet</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">,</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">&quot;</span><span style="color: #8FBCBB">basePackage</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">org.springframework.samples.petclinic.vet</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">,</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">&quot;</span><span style="color: #8FBCBB">nested</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">[],</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">&quot;</span><span style="color: #8FBCBB">type</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">closed</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">,</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">&quot;</span><span style="color: #8FBCBB">shared</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">false</span><span style="color: #ECEFF4">,</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">&quot;</span><span style="color: #8FBCBB">namedInterfaces</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">      </span><span style="color: #ECEFF4">&quot;</span><span style="color: #8FBCBB">&lt;&lt;UNNAMED&gt;&gt;</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">[]</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">},</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">&quot;</span><span style="color: #8FBCBB">initializers</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">[],</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">&quot;</span><span style="color: #8FBCBB">allowedDependencies</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&#91;</span></span>
<span class="line"><span style="color: #D8DEE9FF">      </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">owner</span><span style="color: #ECEFF4">&quot;</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">&#93;,</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">&quot;</span><span style="color: #8FBCBB">dependencies</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">[</span></span>
<span class="line"><span style="color: #D8DEE9FF">      </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #ECEFF4">&quot;</span><span style="color: #8FBCBB">target</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">owner</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">,</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #ECEFF4">&quot;</span><span style="color: #8FBCBB">types</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&#91;</span></span>
<span class="line"><span style="color: #D8DEE9FF">          </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">EVENT_LISTENER</span><span style="color: #ECEFF4">&quot;</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #ECEFF4">&#93;</span></span>
<span class="line"><span style="color: #D8DEE9FF">      </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">]</span></span>
<span class="line"><span style="color: #D8DEE9FF">  </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #ECEFF4">}</span></span></code></pre></div>



<h2 class="wp-block-heading"><br>Conclusion</h2>



<p>
  Vous l’aurez vu&nbsp;: <strong>intégrer Spring Modulith</strong> dans <strong>Spring Petclinic</strong> s&rsquo;est fait <strong>facilement</strong> et de manière très <strong>progressive</strong>. 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<strong>, 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</strong>. 
  <br>
  <br>
  Les 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&nbsp;&lsquo;<strong>évènements</strong> puis de montrer comment utiliser l’<strong>Event Publication Registry</strong>. La base de données existante aura été réutilisée, facilitant son adoption (nul besoin d’infrastructure externe). 
</p>



<p>Ayant 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 : <a href="https://github.com/spring-petclinic/spring-petclinic-modulith">spring-petclinic-modulith</a>. Tous les changements apportés sont visibles à travers <a href="https://github.com/spring-petclinic/spring-petclinic-modulith/commit/512e6b5b41857f85dfa30f77f84a48e81dd1338f"><strong>cet unique commit</strong></a>. N&rsquo;hésitez pas à l’étudier, à expérimenter et à soumettre vos contributions à travers des issues et de Pull Requests.</p>



<p><strong>Ressources</strong>
</p>



<ul class="wp-block-list">
<li><a href="https://github.com/spring-petclinic/spring-petclinic-modulith">spring-petclinic-modulith</a>&nbsp;: le code source complet de la démo  </li>



<li><a href="https://docs.spring.io/spring-modulith/reference/index.html">Documentation officielle de Spring Modulith</a>  </li>



<li><a href="https://github.com/spring-projects/spring-modulith/tree/main/spring-modulith-examples">Spring Modulith examples</a>&nbsp;: les exemples fournis par l&rsquo;équipe Spring  </li>



<li><a href="https://piotrminkowski.com/2023/10/13/guide-to-modulith-with-spring-boot/">Guide to Modulith with Spring Boot</a> : article de blog de Piotr Mińkowski datant de 2023  </li>



<li><a href="https://github.com/xsreality/spring-modulith-with-ddd">spring-modulith-with-ddd</a> : code source d’une application Modular Monolith  basée sur Spring Modulith et le Domain Driven Design  </li>



<li><a href="-%09https:/blog.jetbrains.com/idea/2026/02/migrating-to-modular-monolith-using-spring-modulith-and-intellij-idea">Migrating to Modular Monolith using Spring Modulith and IntelliJ IDEA</a>   </li>



<li><a href="https://martinfowler.com/bliki/MonolithFirst.html">Monolith First</a>&nbsp;: article de Martin Fowler datant de 2015<br>  </li>
</ul>



<p>
  <br>
  <br>
  <br>
  <br>
</p>
]]></content:encoded>
					
					<wfw:commentRss>https://javaetmoi.com/2026/04/decouverte-de-spring-modulith/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Révolutionnez votre prise de notes : du Bullet Journal à Obsidian</title>
		<link>https://javaetmoi.com/2025/10/revolutionnez-votre-prise-de-notes-du-bullet-journal-a-obsidian/</link>
					<comments>https://javaetmoi.com/2025/10/revolutionnez-votre-prise-de-notes-du-bullet-journal-a-obsidian/#comments</comments>
		
		<dc:creator><![CDATA[Antoine]]></dc:creator>
		<pubDate>Thu, 30 Oct 2025 16:45:25 +0000</pubDate>
				<category><![CDATA[Conférence]]></category>
		<category><![CDATA[DevFest]]></category>
		<category><![CDATA[Obsidian]]></category>
		<guid isPermaLink="false">https://javaetmoi.com/?p=2612</guid>

					<description><![CDATA[Lors du Devfest Nantes 2025, Hoani Cross a présenté un talk sur la prise de notes et sa manière d’utiliser Obsidian, un outil de gestion de connaissances en Markdown.
Il y partage sa transition du Bullet Journal manuscrit vers un système numérique, plus flexible et interconnecté.
Obsidian lui permet de centraliser notes personnelles et professionnelles dans un coffre unique, structuré en dossiers et enrichi de métadonnées.
Grâce à ses liens bidirectionnels et aux Map of Content, il construit un Second Cerveau favorisant la connexion des idées.
Hoani exploite plusieurs plugins, comme Tasks, Periodic Notes et Templater, pour automatiser sa gestion quotidienne.
Son usage repose sur des notes journalières (daily notes) où il consigne tâches, réflexions et suivis personnels.
Le numérique lui offre de nombreux avantages : recherche rapide, synchronisation multi-appareils, automatisation et visualisation des données.
Il insiste sur la dimension rituelle de l’ouverture et la clôture des périodes dans son organisation.
L’orateur reconnaît cependant qu’Obsidian demande de la rigueur et du temps pour être bien structuré.
L’auteur de l’article conclut en testant lui-même Obsidian, appréciant sa simplicité et son ergonomie, tout en continuant à utiliser OneNote dans un cadre professionnel.]]></description>
										<content:encoded><![CDATA[
<p>Lors de la conférence <a href="https://devfest2025.gdgnantes.com/">Devfest Nantes 2025</a>, j&rsquo;ai assisté au talk d&rsquo;<strong><a href="https://linktr.ee/hoani.cross">Hoani Cross</a></strong> portant sur la <strong>prise de notes</strong>. Loin d&rsquo;être nouveau, ce sujet m&rsquo;a particulièrement interpellé. Figurez-vous en effet qu&rsquo;une partie des <strong>articles publiés sur ce blog</strong> (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.</p>



<p>Dans son talk, Hoani nous présente le logiciel <strong><a href="https://obsidian.md/">Obsidian</a></strong>, la manière dont <strong>il l&rsquo;utilise au quotidien</strong> pour <strong>noter</strong> et <strong>gérer son activité</strong>, qu&rsquo;elle soit professionnelle ou personnelle.<br>Je suis sorti de sa présentation quelque peu désarçonné. Hoani utilise Obsidian comme un <strong>Bullet Journal</strong> (qu&rsquo;on appelle aussi « <strong>bujo</strong> ») numérique pour compiler <strong>notes</strong>, <strong>pense-bêtes</strong>, <strong>objectifs</strong>, <strong>rappels</strong>, <strong>tracking</strong>, <strong>plannings</strong> et <strong>coups de coeur</strong>. 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.</p>



<p>L&rsquo;autre domaine dans lequel Obsidian semble exceller consiste en la possibilité de se créer un un <strong>Second Cerveau</strong>. Les notes peuvent être reliées ensemble à l&rsquo;aide de <strong>Map Of Content</strong> (MOC). Une alternative aux <strong>hashtags</strong> et l&rsquo;organisation hiérarchisée en <strong>dossiers</strong> et sous-dossiers.<br>Une note de type Map of Content s&rsquo;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 <strong>mind mapping</strong>.<br>Dans ce billet, j&rsquo;aimerais vous restituer la prestation d&rsquo;Hoani et vous laisser découvrir son utilisation Obsidian.</p>



<figure class="wp-block-image size-large"><a href="https://javaetmoi.com/wp-content/uploads/2025/10/DevFest-Nantes-2025-Obsidian.jpg"><img loading="lazy" decoding="async" width="1024" height="770" src="https://javaetmoi.com/wp-content/uploads/2025/10/DevFest-Nantes-2025-Obsidian-1024x770.jpg" alt="" class="wp-image-2613" srcset="https://javaetmoi.com/wp-content/uploads/2025/10/DevFest-Nantes-2025-Obsidian-1024x770.jpg 1024w, https://javaetmoi.com/wp-content/uploads/2025/10/DevFest-Nantes-2025-Obsidian-300x226.jpg 300w, https://javaetmoi.com/wp-content/uploads/2025/10/DevFest-Nantes-2025-Obsidian-768x577.jpg 768w, https://javaetmoi.com/wp-content/uploads/2025/10/DevFest-Nantes-2025-Obsidian.jpg 1390w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></a></figure>



<span id="more-2612"></span>



<h1 class="wp-block-heading">Du papier à Obsidian</h1>



<p>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.<br>Hoani commence par nous rappeler les <strong>bienfaits</strong> de prendre des notes :</p>



<ul class="wp-block-list">
<li><strong>Ralentir</strong> pour assimiler, poser son téléphone pour se focaliser</li>



<li><strong>Graver</strong> dans la mémoire</li>



<li>Transformer l’écoute en <strong>savoir</strong>, <strong>résumer</strong> avec ses propres mots</li>



<li><strong>Entraîner</strong> <strong>le cerveau</strong>&nbsp;: sport mental, reformulation, concentration</li>



<li><strong>Connecter</strong> <strong>des idées</strong> parfois éloignées les unes des autres</li>



<li><strong>Améliorer sa compréhension</strong> tout en construisant une bibliothèque personnelle (le fameux Second Cerveau)</li>
</ul>



<p>Adepte du <strong>Bullet Journal</strong> (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&rsquo;y retrouver, il utilise des conventions de notation appelées <a href="https://macoherence.com/les-clefs-du-bullet-journal/">Bullet Journal Key</a>. Par exemple : un carré pour une tâche.<br>Première limitation : les Bujo ne permettent pas de faire de recherche textuelle.</p>



<p>Hoani a essayé de basculer sur des carnets numériques avec le <a href="https://remarkable.com/">reMarkable</a> et a testé différents logiciels : OneNote, EverNote, Notion, Keep, Joplin. Nul n&rsquo;est arrivé à la hauteur d&rsquo;Obsidian qui est presque parfait pour les raisons suivantes :</p>



<ul class="wp-block-list">
<li>Basé sur le langage de balisage léger <strong>Markdown</strong>, ce qui permet à l&rsquo;auteur de rester propriétaire de ses données</li>



<li>Rendu basé le <strong>moteur web Electron</strong> (comme VS Code)</li>



<li>Logiciel <strong>gratuit</strong>, créé pendant le confinement de la Covid19, mais pas Open Source</li>



<li>Plus de <strong>2 500 plugins</strong> OpenSource</li>



<li><strong>Multiplateforme</strong> : Windows, Linux, MacOS, Android, iOS</li>
</ul>



<p>Obsidian intègre les <strong>fonctionnalités</strong> suivantes &nbsp;:</p>



<ul class="wp-block-list">
<li><strong>Coffre avec navigateur de fichiers</strong> : toutes les notes sont stockées localement dans un dossier (appelé « coffre »), consultable via un explorateur intégré.</li>



<li><strong>Éditeur de Markdown intelligent</strong> : éditeur fluide combinant texte brut et mise en forme instantanée, avec gestion des liens internes, blocs de code, formules et tableaux.</li>



<li><strong>Canvas pour composer ses notes</strong> : 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.</li>



<li><strong>Gestion des <em>daily notes</em></strong> : fonction de journal quotidien permettant de consigner rapidement pensées, tâches ou réflexions, avec génération automatique de notes datées.</li>



<li><strong>Diapositives</strong> : transformation instantanée d’une note en présentation interactive, pratique pour exposer un projet ou partager ses idées sans quitter Obsidian.</li>



<li><strong>Enregistrement audio</strong> : capture vocale intégrée pour enregistrer des idées, réunions ou commentaires, directement stockés et liés dans le coffre.</li>



<li><strong>Vue graphique de l’arborescence des notes</strong> : représentation visuelle du réseau de liens entre notes, offrant une cartographie claire et dynamique de son écosystème de connaissances.</li>
</ul>



<p>L&rsquo;une des forces du logiciel Obsidian réside dans le fait qu&rsquo;il soit multiplafeforme : on peut commencer une note oralement sur son Smartphone puis la reprendre plus tard avec un clavier sur son laptop. Plusieurs <a href="https://help.obsidian.md/sync-notes">techniques de synchronisation&nbsp;des coffres</a> entre différents devices existent :</p>



<ol class="wp-block-list">
<li>Offre de service intégré payant&nbsp;: <strong>Sync</strong> (4$ par mois)</li>



<li>Stockage Cloud avec <strong>iCloud</strong>, <strong>OneDrive</strong>, <strong>Google Drive</strong></li>



<li><strong><a href="https://syncthing.net/">Syncthing</a></strong> (OSS)&nbsp;: synchronisation Peer-to-Peer de fichiers. Vos données ne sont jamais stockées dans le Cloud</li>



<li>Plugin <strong>Git</strong> (instable sur mobile) : personnellement, c&rsquo;est ce dernier que j&rsquo;utilise avec un repo GitHub privé.</li>
</ol>



<p>À noter que l&rsquo;utilisation d&rsquo;un repo Git rend possible le partage en équipe d&rsquo;un coffre Obsidian.</p>



<h1 class="wp-block-heading">Utilisation d&rsquo;Obsidian</h1>



<p>Le coffre d&rsquo;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.</p>



<p>L&rsquo;organisation de son coffre&nbsp;comporte 8 grands dossiers :</p>



<ul class="wp-block-list">
<li>Notes persos</li>



<li>Notes pros</li>



<li>Notes de lecture (livres ou vidéo youtube)</li>



<li>Notes non classées (temporaire)</li>



<li>Second Brain&nbsp;: wiki perso</li>



<li>Bullet Journal (Bujo)</li>



<li>Archives</li>



<li>Templates</li>
</ul>



<p>Chaque note .md suit la structuration suivante :</p>



<ul class="wp-block-list">
<li>En-tête en <strong><a href="https://docs.github.com/en/contributing/writing-for-github-docs/using-yaml-frontmatter">front matter</a></strong> pour ajouter des méta-données</li>



<li>Corps en Markdown</li>



<li>Tags hiérarchisés</li>
</ul>



<p>Le tableau suivant présente la <a href="https://help.obsidian.md/obsidian-flavored-markdown">syntaxe Markdown supportée par Obsidian</a> :</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Syntax</th><th>Description</th></tr></thead><tbody><tr><td><code>[[Link]]</code></td><td><a href="https://help.obsidian.md/links">Internal links</a></td></tr><tr><td><code>![[Link]]</code></td><td><a href="https://help.obsidian.md/embeds">Embed files</a></td></tr><tr><td><code>![[Link#^id]]</code></td><td><a href="https://help.obsidian.md/links#Link%20to%20a%20block%20in%20a%20note">Block references</a></td></tr><tr><td><code>^id</code></td><td><a href="https://help.obsidian.md/links#Link%20to%20a%20block%20in%20a%20note">Defining a block</a></td></tr><tr><td><code>[^id]</code></td><td><a href="https://help.obsidian.md/syntax#Footnotes">Footnotes</a></td></tr><tr><td><code>%%Text%%</code></td><td><a href="https://help.obsidian.md/syntax#Comments">Comments</a></td></tr><tr><td><code>~~Text~~</code></td><td><a href="https://help.obsidian.md/syntax#Bold,%20italics,%20highlights">Strikethroughs</a></td></tr><tr><td><code>==Text==</code></td><td><a href="https://help.obsidian.md/syntax#Bold,%20italics,%20highlights">Highlights</a></td></tr><tr><td><code>```</code></td><td><a href="https://help.obsidian.md/syntax#Code%20blocks">Code blocks</a></td></tr><tr><td><code>- [ ]</code></td><td><a href="https://help.obsidian.md/syntax#Task%20lists">Incomplete task</a></td></tr><tr><td><code>- [x]</code></td><td><a href="https://help.obsidian.md/syntax#Task%20lists">Completed task</a></td></tr><tr><td><code>&gt; [!note]</code></td><td><a href="https://help.obsidian.md/callouts">Callouts</a></td></tr><tr><td>(see link)</td><td><a href="https://help.obsidian.md/advanced-syntax#Tables">Tables</a></td></tr></tbody></table></figure>



<p>Dans son dossier <strong>Bujo</strong>, Hoani s&rsquo;appuie sur le plugin <a href="https://github.com/liamcain/obsidian-periodic-notes">Periodic Notes</a> pour gérer ses notes périodiques et les hiérarchise en année / trimestre / mois / jours.</p>



<p>Le plugin <a href="https://github.com/obsidian-tasks-group/obsidian-tasks">Tasks</a> permet de gérer les <strong>tâches</strong> dans Obsidian. La syntaxe Markdown [x] et [] permet de reconnaitre une tâche terminée d&rsquo;une tâche à faire. L&rsquo;utilisation d&rsquo;Emojis est possible pour les méta-données.<br>A noter la possibilité de créer des <strong>tableaux dynamiques</strong> à l’aide de requête, par exemple pour créer une <strong>liste de TODO</strong> à faire aujourd’hui.</p>



<p>Son <strong>daily</strong> est centré autour d’une simple bullet list renseignée tout au long de la journée.<br>Chaque sujet commence par un tag (ex&nbsp;: #perso, #tech). Hoani utilise les sous-listes pour détailler le sujet.<br>Chaque jour, Hoani ouvre son Daily du jour et</p>



<ul class="wp-block-list">
<li>Regarde les tâches à réaliser</li>



<li>Note en bullet list</li>



<li>Crée une tâche quand nécessaire</li>



<li>Termine les tâches accomplies</li>
</ul>



<h1 class="wp-block-heading">Le bénéfice du numérique</h1>



<p>Comparé au manuscrit, les bénéfices du numérique sont nombreux :</p>



<ul class="wp-block-list">
<li>Personnaliser son daily en ajoutant toutes les infos qui lui passent par la tête</li>



<li>Suivre des variables quotidiennes (comme le temps passé à s&rsquo;entrainer au <a href="https://fr.wikipedia.org/wiki/Kendama">Kendama</a>)</li>



<li>Afficher les tâches à réaliser et être notifié de celles en retard</li>



<li>Automatiser certains traitements à l&rsquo;aide de plugins additionnels&nbsp;: <a href="https://github.com/SilentVoid13/Templater">Templater</a>, <a href="https://github.com/obsidian-tasks-group/obsidian-tasks">Tasks</a> et <a href="https://github.com/blacksmithgu/obsidian-dataview">Dataview</a></li>
</ul>



<p>Le contenu du Daily peut être très riche et comporter :</p>



<ul class="wp-block-list">
<li><strong>Métadonnées</strong> avec tags</li>



<li>Cartouche de <strong>navigation</strong></li>



<li><strong>Widgets de progression</strong> mois/année</li>



<li>Définition de la <strong>tâche ultime</strong> de la journée</li>



<li>Rappel des <strong>tâches</strong> via un filtre de recherche et un report facilité des tâches</li>



<li>Le <strong>journal</strong> en lui-même<br>&#8211;&nbsp;Entrainement au Kendama</li>



<li>Des métriques sur livres (page en cours) et jeux vidéo (temps, winrate)</li>
</ul>



<p>Exemples de métadonnées d&rsquo;un Daily permettant de suivre des variables quotidiennes :</p>



<pre class="wp-block-code"><code>energy_level_morning&nbsp;: 6
energy_level_evening&nbsp;: 7
mood_morning&nbsp;: insomniaque
mood_evening&nbsp;: détendu
walked&nbsp;: 400
working_place_morning&nbsp;: HOME</code></pre>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p></p>
</blockquote>



<p>La <strong>barre de navigation</strong> du Daily&nbsp;permet de passer rapidement du daily précédent au suivant, à la semaine associée …<br>L&rsquo;ajout de <strong>barres de progression</strong> est possible via le plugin <a href="https://github.com/zwpaper/obsidian-progressbar">Progressbar</a>.</p>



<p>Le numérique permet de créer d&rsquo;<strong>autres types de notes périodiques</strong> 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&rsquo;ouverture et la fermeture d&rsquo;une période doit être vécue comme un rituel.<br>Pour automatiser la création de ces différentes notes périodiques, on peut s&rsquo;aider du plugin <a href="https://silentvoid13.github.io/Templater/">Templater</a> qui ajoute à Obsidian un langage de <strong>templating</strong> permettant d&rsquo;exécuter des <strong>fonctions</strong> <strong>JavaScript</strong>. Ce plugin permet de pré-sélectionner un template en fonction du nom du fichier créé.</p>



<h1 class="wp-block-heading">Conclusion</h1>



<p>Après la démonstration de son <strong>utilisation avancée d&rsquo;Obsidian</strong>, Hoani reconnaît que le <strong>démarrage peut être long</strong>, qu&rsquo;il peut être <strong>difficile</strong> de décider quoi noter et surtout <strong>comment organiser son coffre</strong>. Son conseil est de rester rigoureux, y noter ce que l&rsquo;on souhaite et faire en sorte que cela reste amusant et donc pas une corvée. Ses slides sont en ligne : <a href="https://docs.google.com/presentation/d/1PGu22MX_v3q34QKaUvci7ug9hPScYXPSIjAyAhgfToY/edit">[Devfest Nantes 25] Révolutionnez votre prise de notes &#8211; du Bullet Journal à Obsidian</a>. Son <a href="https://github.com/hcross/obsidian">repo GitHub</a> rend public ses templates Obsidian.</p>



<div class="wp-block-group is-content-justification-left is-nowrap is-layout-flex wp-container-core-group-is-layout-f56a869c wp-block-group-is-layout-flex">
<p>Pour ma part, j’<strong>expérimente Obsidian</strong> depuis seulement <strong>deux semaines</strong>, 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”.<br>Pour 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.<br>Au travail, pour ma prise de notes, je reste néanmoins sur <strong>OneNote</strong>, qui s’intègre parfaitement à l’écosystème Microsoft 365 et tire parti de <strong>Copilot</strong> pour la recherche et la synthèse de contenus.</p>
</div>



<figure class="wp-block-image size-large"><a href="https://obsidian.md/"><img loading="lazy" decoding="async" width="1024" height="604" src="https://javaetmoi.com/wp-content/uploads/2025/10/Obsidian-screenshot-1.0-hero-combo-1024x604.png" alt="" class="wp-image-2627" srcset="https://javaetmoi.com/wp-content/uploads/2025/10/Obsidian-screenshot-1.0-hero-combo-1024x604.png 1024w, https://javaetmoi.com/wp-content/uploads/2025/10/Obsidian-screenshot-1.0-hero-combo-300x177.png 300w, https://javaetmoi.com/wp-content/uploads/2025/10/Obsidian-screenshot-1.0-hero-combo-768x453.png 768w, https://javaetmoi.com/wp-content/uploads/2025/10/Obsidian-screenshot-1.0-hero-combo-1536x906.png 1536w, https://javaetmoi.com/wp-content/uploads/2025/10/Obsidian-screenshot-1.0-hero-combo-2048x1209.png 2048w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></a></figure>
]]></content:encoded>
					
					<wfw:commentRss>https://javaetmoi.com/2025/10/revolutionnez-votre-prise-de-notes-du-bullet-journal-a-obsidian/feed/</wfw:commentRss>
			<slash:comments>2</slash:comments>
		
		
			</item>
		<item>
		<title>JSpecify + NullAway + ErrorProne : la configuration Maven ultime pour dire adieu aux NullPointerException</title>
		<link>https://javaetmoi.com/2025/07/jspecify-nullaway-errorprone-la-configuration-maven-ultime-pour-dire-adieu-aux-nullpointerexception/</link>
					<comments>https://javaetmoi.com/2025/07/jspecify-nullaway-errorprone-la-configuration-maven-ultime-pour-dire-adieu-aux-nullpointerexception/#respond</comments>
		
		<dc:creator><![CDATA[Antoine]]></dc:creator>
		<pubDate>Sat, 05 Jul 2025 17:06:00 +0000</pubDate>
				<category><![CDATA[Maven]]></category>
		<category><![CDATA[Retour d'expérience]]></category>
		<category><![CDATA[ErrorProne]]></category>
		<category><![CDATA[JSpecify]]></category>
		<category><![CDATA[NullAway]]></category>
		<guid isPermaLink="false">https://javaetmoi.com/?p=2599</guid>

					<description><![CDATA[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&#160;? En juin 2024, avec l’arrivée de la spécification JSpecify, soutenue &#8230; <a href="https://javaetmoi.com/2025/07/jspecify-nullaway-errorprone-la-configuration-maven-ultime-pour-dire-adieu-aux-nullpointerexception/" class="more-link">Continuer la lecture de <span class="screen-reader-text">JSpecify + NullAway + ErrorProne : la configuration Maven ultime pour dire adieu aux NullPointerException</span>  <span class="meta-nav">&#8594;</span></a>]]></description>
										<content:encoded><![CDATA[
<p><br>La gestion de la <strong>nullabilité</strong> 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 <strong>NullPointerException</strong> survenue en production&nbsp;? En juin 2024, avec l’arrivée de la spécification <a href="https://jspecify.dev/"><strong>JSpecify</strong></a>, soutenue par des acteurs majeurs comme Google, Microsoft, JetBrains, Oracle, Sonar ou bien encore Broadcom (Spring), l’écosystème Java dispose enfin d’une <strong>bibliothèque unifiée d’annotations de nullité</strong>. 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 <a href="https://github.com/uber/NullAway"><strong>NullAway</strong></a> (Uber) et <a href="https://errorprone.info/"><strong>ErrorProne</strong></a> (Google). <br> Ce court article explique comment mettre en place sur un projet d’entreprise la <strong>configuration Maven</strong> correspondante qui fera casser votre build et votre CI lorsque vous essayerez de passer une variable <em>null</em> en paramètre d’une méthode qui ne les accepte pas.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://jspecify.dev/"><img loading="lazy" decoding="async" width="1024" height="528" src="https://javaetmoi.com/wp-content/uploads/2025/07/Screenshot-site-JSpecify-1024x528.png" alt="" class="wp-image-2605" srcset="https://javaetmoi.com/wp-content/uploads/2025/07/Screenshot-site-JSpecify-1024x528.png 1024w, https://javaetmoi.com/wp-content/uploads/2025/07/Screenshot-site-JSpecify-300x155.png 300w, https://javaetmoi.com/wp-content/uploads/2025/07/Screenshot-site-JSpecify-768x396.png 768w, https://javaetmoi.com/wp-content/uploads/2025/07/Screenshot-site-JSpecify.png 1316w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></a></figure>
</div>


<span id="more-2599"></span>



<h3 class="wp-block-heading">Dépendance Maven JSpecify</h3>



<p>
  Ajoutez simplement la dépendance suivante au niveau de la balise &lt;dependencies&gt; de votre pom.xml :
</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>&lt;dependency>
    &lt;groupId>org.jspecify&lt;/groupId>
    &lt;artifactId>jspecify&lt;/artifactId>
    &lt;version>1.0.0&lt;/version>
&lt;/dependency></textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #81A1C1">&lt;dependency&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">&lt;groupId&gt;</span><span style="color: #D8DEE9FF">org.jspecify</span><span style="color: #81A1C1">&lt;/groupId&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">&lt;artifactId&gt;</span><span style="color: #D8DEE9FF">jspecify</span><span style="color: #81A1C1">&lt;/artifactId&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">&lt;version&gt;</span><span style="color: #D8DEE9FF">1.0.0</span><span style="color: #81A1C1">&lt;/version&gt;</span></span>
<span class="line"><span style="color: #81A1C1">&lt;/dependency&gt;</span></span></code></pre></div>



<p>A ce stade, les IDE comme <a href="https://www.jetbrains.com/idea/whatsnew/#page__content-jspecify-support">IntelliJ supportant JSpecify</a> seront à même de détecter des erreurs. Exemple extrait de <a href="https://github.com/spring-projects/spring-petclinic">Sring Petclinic</a> dont les packages Java sont annotés avec <strong>@NullMarked</strong>&nbsp;:</p>



<p>
  <img loading="lazy" decoding="async" width="1031" height="238" src="https://javaetmoi.com/wp-content/uploads/2025/07/word-image-2599-1.png" class="wp-image-2601" srcset="https://javaetmoi.com/wp-content/uploads/2025/07/word-image-2599-1.png 1031w, https://javaetmoi.com/wp-content/uploads/2025/07/word-image-2599-1-300x69.png 300w, https://javaetmoi.com/wp-content/uploads/2025/07/word-image-2599-1-1024x236.png 1024w, https://javaetmoi.com/wp-content/uploads/2025/07/word-image-2599-1-768x177.png 768w" sizes="auto, (max-width: 1031px) 100vw, 1031px" />
  <br>
  <br>
  Dans le cas où ces warnings n’apparaissent pas dans IntelliJ, vérifier que les <strong>inspections Nullability problems </strong>sont bien activées&nbsp;:
</p>



<figure class="wp-block-image"><img loading="lazy" decoding="async" width="1223" height="699" src="https://javaetmoi.com/wp-content/uploads/2025/07/word-image-2599-2.png" alt="" class="wp-image-2602" srcset="https://javaetmoi.com/wp-content/uploads/2025/07/word-image-2599-2.png 1223w, https://javaetmoi.com/wp-content/uploads/2025/07/word-image-2599-2-300x171.png 300w, https://javaetmoi.com/wp-content/uploads/2025/07/word-image-2599-2-1024x585.png 1024w, https://javaetmoi.com/wp-content/uploads/2025/07/word-image-2599-2-768x439.png 768w" sizes="auto, (max-width: 1223px) 100vw, 1223px" /></figure>



<h3 class="wp-block-heading">Configuration du Maven Compiler Plugin avec NullAway et ErrorProne</h3>



<p>
  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 <strong>Error Prone</strong> de Google et de son extension <strong>NullAway</strong> d&rsquo;Uber :
</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>&lt;plugin>
 &lt;groupId>org.apache.maven.plugins&lt;/groupId>
 &lt;artifactId>maven-compiler-plugin&lt;/artifactId>
 &lt;configuration>
   &lt;parameters>true&lt;/parameters>
   &lt;compilerArgs>
     &lt;arg>-XDcompilePolicy=simple&lt;/arg>
     &lt;arg>--should-stop=ifError=FLOW&lt;/arg>
     &lt;arg>-XDaddTypeAnnotationsToSymbol=true&lt;/arg>
     &lt;arg>-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
     &lt;/arg>
   &lt;annotationProcessorPaths>
     &lt;path>
       &lt;groupId>com.google.errorprone&lt;/groupId>
       &lt;artifactId>error_prone_core&lt;/artifactId>
       &lt;version>2.42.0&lt;/version>
     &lt;/path>
     &lt;path>
       &lt;groupId>com.uber.nullaway&lt;/groupId>
       &lt;artifactId>nullaway&lt;/artifactId>
       &lt;version>0.12.11&lt;/version>
     &lt;/path>
   &lt;/annotationProcessorPaths>
 &lt;/configuration>
&lt;/plugin></textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #81A1C1">&lt;plugin&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">&lt;groupId&gt;</span><span style="color: #D8DEE9FF">org.apache.maven.plugins</span><span style="color: #81A1C1">&lt;/groupId&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">&lt;artifactId&gt;</span><span style="color: #D8DEE9FF">maven-compiler-plugin</span><span style="color: #81A1C1">&lt;/artifactId&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">&lt;configuration&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">   </span><span style="color: #81A1C1">&lt;parameters&gt;</span><span style="color: #D8DEE9FF">true</span><span style="color: #81A1C1">&lt;/parameters&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">   </span><span style="color: #81A1C1">&lt;compilerArgs&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">     </span><span style="color: #81A1C1">&lt;arg&gt;</span><span style="color: #D8DEE9FF">-XDcompilePolicy=simple</span><span style="color: #81A1C1">&lt;/arg&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">     </span><span style="color: #81A1C1">&lt;arg&gt;</span><span style="color: #D8DEE9FF">--should-stop=ifError=FLOW</span><span style="color: #81A1C1">&lt;/arg&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">     </span><span style="color: #81A1C1">&lt;arg&gt;</span><span style="color: #D8DEE9FF">-XDaddTypeAnnotationsToSymbol=true</span><span style="color: #81A1C1">&lt;/arg&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">     </span><span style="color: #81A1C1">&lt;arg&gt;</span><span style="color: #D8DEE9FF">-Xplugin:ErrorProne</span></span>
<span class="line"><span style="color: #D8DEE9FF">       -XepOpt:NullAway:AnnotatedPackages=com.javaetmoi.myapp</span></span>
<span class="line"><span style="color: #D8DEE9FF">       -XepOpt:NullAway:UnannotatedSubPackages=com.javaetMoi.myapp.controller.api,com.javaetMoi.myapp.controller.dto </span></span>
<span class="line"><span style="color: #D8DEE9FF">       -XepOpt:NullAway:JSpecifyMode=true</span></span>
<span class="line"><span style="color: #D8DEE9FF">       -XepDisableAllChecks</span></span>
<span class="line"><span style="color: #D8DEE9FF">       -Xep:NullAway:ERROR</span></span>
<span class="line"><span style="color: #D8DEE9FF">       -XepExcludedPaths:.*/src/test/java/.*</span></span>
<span class="line"><span style="color: #D8DEE9FF">       -XepDisableWarningsInGeneratedCode</span></span>
<span class="line"><span style="color: #D8DEE9FF">     </span><span style="color: #81A1C1">&lt;/arg&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">   </span><span style="color: #81A1C1">&lt;annotationProcessorPaths&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">     </span><span style="color: #81A1C1">&lt;path&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">       </span><span style="color: #81A1C1">&lt;groupId&gt;</span><span style="color: #D8DEE9FF">com.google.errorprone</span><span style="color: #81A1C1">&lt;/groupId&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">       </span><span style="color: #81A1C1">&lt;artifactId&gt;</span><span style="color: #D8DEE9FF">error_prone_core</span><span style="color: #81A1C1">&lt;/artifactId&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">       </span><span style="color: #81A1C1">&lt;version&gt;</span><span style="color: #D8DEE9FF">2.42.0</span><span style="color: #81A1C1">&lt;/version&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">     </span><span style="color: #81A1C1">&lt;/path&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">     </span><span style="color: #81A1C1">&lt;path&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">       </span><span style="color: #81A1C1">&lt;groupId&gt;</span><span style="color: #D8DEE9FF">com.uber.nullaway</span><span style="color: #81A1C1">&lt;/groupId&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">       </span><span style="color: #81A1C1">&lt;artifactId&gt;</span><span style="color: #D8DEE9FF">nullaway</span><span style="color: #81A1C1">&lt;/artifactId&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">       </span><span style="color: #81A1C1">&lt;version&gt;</span><span style="color: #D8DEE9FF">0.12.11</span><span style="color: #81A1C1">&lt;/version&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">     </span><span style="color: #81A1C1">&lt;/path&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">   </span><span style="color: #81A1C1">&lt;/annotationProcessorPaths&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">&lt;/configuration&gt;</span></span>
<span class="line"><span style="color: #81A1C1">&lt;/plugin&gt;</span></span></code></pre></div>



<p><strong>Explications clés :</strong>
</p>



<ul class="wp-block-list">
<li>L&rsquo;option <strong>-Xep:NullAway:ERROR </strong>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.<br></li>



<li>L’option &#8211;<strong>Xplugin:ErrorProne</strong> active le plugin ErrorProne.<br>  </li>



<li>L’option <strong>-XepDisableAllChecks</strong> 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.<br>  </li>



<li>L’option <strong>-XepOpt:NullAway:AnnotatedPackages=com.javaetmoi.myapp</strong> active NullAway sur le package Java racine de l’application métier. A noter que cette option peut être remplacer par <strong>-XepOpt:NullAway:OnlyNullMarked </strong>afin de ne scanner que les packages annotés avec <strong>@NullMarked</strong>.<br>  </li>



<li>A contrario, l’option <strong>-XepOpt:NullAway:UnannotatedSubPackages=com.javaetMoi.myapp.controller.api,com.javaetMoi.myapp.controller.dto</strong> 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.<br>  </li>



<li>Dans 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&rsquo;option <strong>-XepExcludedPaths:.*/src/test/java/.*</strong><br>  </li>



<li>L&rsquo;option <strong>-XepOpt:NullAway:JSpecifyMode=true</strong> active le support complet de JSpecify et exploite pleinement la <a href="https://github.com/uber/NullAway/wiki/JSpecify-Support">sémantique de JSpecify</a>, notamment au niveau des types génériques. <br></li>



<li>L&rsquo;argument javac <strong>-XDaddTypeAnnotationsToSymbol=true</strong> est requis par la version 0.12.11 de NullAway lors de l&rsquo;<a href="https://github.com/uber/NullAway/wiki/JSpecify-Support#supported-jdk-versions">utilisation d&rsquo;une version de Java antérieure à <strong>Java 22</strong></a>.</li>
</ul>



<p>Toutes les options de <a href="https://github.com/uber/NullAway">NullAway</a> peuvent être retrouvées sur sa page de <a href="https://github.com/uber/NullAway/wiki/Configuration">Configuration</a>. <br> <br>A partir de la version 16 du langage Java, la&nbsp;<a href="https://errorprone.info/docs/installation">documentation d&rsquo;installation d&rsquo;error prone</a> explique comment activer des flags à la JVM via le fichier&nbsp;<strong>.mvn/jvm.config&nbsp;</strong>:</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>--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</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #d8dee9ff">--add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED</span></span>
<span class="line"><span style="color: #d8dee9ff">--add-exports jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED</span></span>
<span class="line"><span style="color: #d8dee9ff">--add-exports jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED</span></span>
<span class="line"><span style="color: #d8dee9ff">--add-exports jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED</span></span>
<span class="line"><span style="color: #d8dee9ff">--add-exports jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED</span></span>
<span class="line"><span style="color: #d8dee9ff">--add-exports jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED</span></span>
<span class="line"><span style="color: #d8dee9ff">--add-exports jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED</span></span>
<span class="line"><span style="color: #d8dee9ff">--add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED</span></span>
<span class="line"><span style="color: #d8dee9ff">--add-opens jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED</span></span>
<span class="line"><span style="color: #d8dee9ff">--add-opens jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED</span></span></code></pre></div>



<p>
  <br>
  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.
  <br>
  <br>
  Exemple d’une commande <strong><em>mvn compile </em></strong>sur l’exemple précédent&nbsp;:
</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>&#91;ERROR&#93; COMPILATION ERROR : 
&#91;INFO&#93; -------------------------------------------------------------
&#91;ERROR&#93; /Users/arey/Dev/GitHub/spring-petclinic/spring-petclinic/src/main/java/org/springframework/samples/petclinic/owner/OwnerController.java:&#91;106,82&#93; &#91;NullAway&#93; passing @Nullable parameter 'lastName' where @NonNull is required
    (see http://t.uber.com/nullaway )</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #d8dee9ff">&#91;ERROR&#93; COMPILATION ERROR : </span></span>
<span class="line"><span style="color: #d8dee9ff">&#91;INFO&#93; -------------------------------------------------------------</span></span>
<span class="line"><span style="color: #d8dee9ff">&#91;ERROR&#93; /Users/arey/Dev/GitHub/spring-petclinic/spring-petclinic/src/main/java/org/springframework/samples/petclinic/owner/OwnerController.java:&#91;106,82&#93; &#91;NullAway&#93; passing @Nullable parameter &#39;lastName&#39; where @NonNull is required</span></span>
<span class="line"><span style="color: #d8dee9ff">    (see http://t.uber.com/nullaway )</span></span></code></pre></div>



<h3 class="wp-block-heading">Conclusion</h3>



<p>Cinq années auront été nécessaires par le groupe de travail JSpecify pour formaliser et se mettre d’accord sur <strong>quatre annotations Java</strong>. Depuis un an, nos <strong>outillages</strong>, nos <strong>IDE</strong> et nos <strong>frameworks</strong> convergent vers ce lot d’annotations. Quelques <strong>bugs de jeunesse </strong>existent encore, à l’image de <a href="https://github.com/apache/maven-build-cache-extension/pull/358">ce bug Maven Build Cache Extension</a> corrigé par mon collègue Marco et qui faisait échouer la synchronisation Maven IntelliJ avec le message « java: plug-in not found: ErrorProne ». <br></p>



<p>A nos <strong>projets métiers</strong> de se mettre à JSpecify et d’être prêts pour Spring Framework 7 et Spring Boot 4 qui sortiront fin 2025. <br> Si vos projets exploitent les annotations JSR-305 ou JetBrains, migrez vers JSpecify à l’aide de la recette OpenRewrite <a href="https://docs.openrewrite.org/recipes/java/jspecify/migratetojspecify">MigrateToJSpecify</a>. <br> <br>Sachez enfin que Dan Smith a proposé la <a href="https://openjdk.org/jeps/8303099">JEP draft: Null-Restricted and Nullable Types (Preview)</a> visant à ajouter des marqueurs syntaxiques directement au niveau du langage Java. Adopter JSpecify aujourd’hui facilitera l’adoption de cette JEP. <br> <br></p>
]]></content:encoded>
					
					<wfw:commentRss>https://javaetmoi.com/2025/07/jspecify-nullaway-errorprone-la-configuration-maven-ultime-pour-dire-adieu-aux-nullpointerexception/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>De Spring Data JPA à jOOQ</title>
		<link>https://javaetmoi.com/2025/06/de-spring-data-jpa-a-jooq/</link>
					<comments>https://javaetmoi.com/2025/06/de-spring-data-jpa-a-jooq/#respond</comments>
		
		<dc:creator><![CDATA[Antoine]]></dc:creator>
		<pubDate>Mon, 16 Jun 2025 06:57:01 +0000</pubDate>
				<category><![CDATA[Conférence]]></category>
		<category><![CDATA[Retour d'expérience]]></category>
		<category><![CDATA[Spring]]></category>
		<category><![CDATA[Devoxx]]></category>
		<category><![CDATA[jOOQ]]></category>
		<category><![CDATA[JPA]]></category>
		<category><![CDATA[Spring Data]]></category>
		<category><![CDATA[SQL]]></category>
		<guid isPermaLink="false">https://javaetmoi.com/?p=2580</guid>

					<description><![CDATA[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 &#8230; <a href="https://javaetmoi.com/2025/06/de-spring-data-jpa-a-jooq/" class="more-link">Continuer la lecture de <span class="screen-reader-text">De Spring Data JPA à jOOQ</span>  <span class="meta-nav">&#8594;</span></a>]]></description>
										<content:encoded><![CDATA[
<div class="wp-block-media-text is-stacked-on-mobile" style="grid-template-columns:40% auto"><figure class="wp-block-media-text__media"><img loading="lazy" decoding="async" width="750" height="750" src="https://javaetmoi.com/wp-content/uploads/2025/06/jooq-the-easiest-way-to-write-sql-in-java.png" alt="jOOQ: The easiest way to write SQL in Java" class="wp-image-2581 size-full" srcset="https://javaetmoi.com/wp-content/uploads/2025/06/jooq-the-easiest-way-to-write-sql-in-java.png 750w, https://javaetmoi.com/wp-content/uploads/2025/06/jooq-the-easiest-way-to-write-sql-in-java-300x300.png 300w, https://javaetmoi.com/wp-content/uploads/2025/06/jooq-the-easiest-way-to-write-sql-in-java-150x150.png 150w" sizes="auto, (max-width: 750px) 100vw, 750px" /></figure><div class="wp-block-media-text__content">
<p>Lors de la conférence Devoxx France 2025, j’ai participé à un hands-on lab de 2h intitulé <a href="https://www.devoxx.fr/agenda-2025/talk/sortir-des-orms-avec-jooq/">Sortir des ORMs avec jOOQ</a>. Acronyme de « <strong>Java Object Oriented Querying</strong> », <strong>jOOQ</strong> se présente comme une <strong>alternative à JPA </strong>permettant d’<strong>écrire des requêtes SQL</strong> en Java via une <strong>fluent API</strong>. Animé par Sylvain Decout et Samuel Lefebvre, cet atelier visait à migrer une application Spring Boot / JPA vers jOOQ à l’aide du <strong>starter Spring Boot</strong> pour jOOQ. Pour les curieux, le repo de l’atelier est disponible sur Github : <a href="https://github.com/sylvaindecout/jooq-handson">jooq-handson</a>. </p>



<p>Fort 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é : <a href="https://github.com/spring-petclinic/spring-petclinic-jooq"><strong>spring-petclinic-jooq</strong></a>. Bienvenue à ce dernier dans la communauté Spring Petclinic.</p>
</div></div>



<p>
  L’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&nbsp;: pas de SQL, mais une <strong>API Java fluide</strong> et <strong>type-safe</strong> spécifique à jOOQ qu’il va falloir appréhender. Rassurez-vous, cette API se rapproche du SQL&nbsp;: on y retrouve les mots clés <strong>select</strong>, <strong>update</strong>, <strong>insertInto</strong>, <strong>where</strong>, <strong>from</strong>, <strong>join</strong>, <strong>on</strong>, <strong>as </strong>… A ceux-ci, on ajoute des mots clés spécifiques à jOOQ&nbsp;: <strong>paginate</strong>, <strong>fetch</strong>, <strong>convertFrom</strong> … La <a href="https://www.jooq.org/learn/"><strong>documentation</strong></a> de jOOQ est très <strong>complète</strong>. 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&nbsp;:  <a href="https://www.jooq.org/doc/latest/manual/sql-building/column-expressions/json-functions/">JSON functions</a>, <a href="https://blog.jooq.org/how-to-use-sql-pivot-to-compare-two-tables-in-your-database/">PIVOT</a>, <a href="https://www.jooq.org/doc/latest/manual/sql-building/sql-statements/merge-statement/">MERGE</a>, <a href="https://www.jooq.org/doc/latest/manual/sql-building/sql-statements/select-statement/set-operations/set-operation-union/">UNION</a> …
</p>



<p>
  Cet article a pour objectif d’expliquer les <strong>étapes</strong> adoptées pour <strong>migrer l’implémentation Spring Data JPA des repository vers jOOQ</strong>. Des exemples de code avant / après y sont proposés.
</p>



<span id="more-2580"></span>



<h2 class="wp-block-heading">Configuration du build</h2>



<p><a href="https://docs.spring.io/spring-boot/reference/data/sql.html#data.sql.jooq">Spring Boot supporte nativement l’usage des versions commerciales et Open Source de jOOQ</a>. Dans le <strong>pom.xml</strong> ou le fichier <strong>build.gradle</strong>, commencer par déclarer le starter Spring Boot pour jOOQ <strong>spring-boot-starter-jooq</strong>&nbsp;:
</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><textarea class="code-block-pro-copy-button-textarea" aria-hidden="true" readonly>&lt;dependency>
    &lt;groupId>org.springframework.boot&lt;/groupId>
    &lt;artifactId>spring-boot-starter-jooq&lt;/artifactId>
&lt;/dependency></textarea><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #81A1C1">&lt;dependency&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">&lt;groupId&gt;</span><span style="color: #D8DEE9FF">org.springframework.boot</span><span style="color: #81A1C1">&lt;/groupId&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">&lt;artifactId&gt;</span><span style="color: #D8DEE9FF">spring-boot-starter-jooq</span><span style="color: #81A1C1">&lt;/artifactId&gt;</span></span>
<span class="line"><span style="color: #81A1C1">&lt;/dependency&gt;</span></span></code></pre></div>



<p> L’étape suivante consiste à générer les classes Java à partir du schéma de la base de données. <a href="https://www.jooq.org/doc/latest/manual/code-generation/codegen-configuration/">jOOQs propose différentes possibilités </a>: à partir du <strong>script DDL</strong> de création du schéma comme sur Petclinic, de scripts <strong>Liquibase</strong> ou bien encore des <strong>méta-données</strong> d’une base existante. <br> Les plugins <a href="https://www.jooq.org/doc/latest/manual/code-generation/codegen-execution/codegen-maven/"><strong>jooq-codegen-maven</strong></a> ou <a href="https://www.jooq.org/doc/latest/manual/code-generation/codegen-execution/codegen-gradle/"><strong>jooq-codegen-gradle</strong></a> sont à configurer. <br> Voici un exemple extrait de jOOQ Spring Petclinic : </p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><textarea class="code-block-pro-copy-button-textarea" aria-hidden="true" readonly>&lt;plugin>
  &lt;groupId>org.jooq&lt;/groupId>
  &lt;artifactId>jooq-codegen-maven&lt;/artifactId>
  &lt;executions>
    &lt;execution>
      &lt;goals>
        &lt;goal>generate&lt;/goal>
      &lt;/goals>
    &lt;/execution>
  &lt;/executions>
  &lt;configuration>
    &lt;generator>
      &lt;database>
        &lt;name>org.jooq.meta.extensions.ddl.DDLDatabase&lt;/name>
        &lt;properties>
          &lt;property>
            &lt;key>scripts&lt;/key>
            &lt;value>src/main/resources/db/h2/schema.sql&lt;/value>
          &lt;/property>
          &lt;property>
            &lt;key>sort&lt;/key>
            &lt;value>semantic&lt;/value>
          &lt;/property>
          &lt;property>
            &lt;key>unqualifiedSchema&lt;/key>
            &lt;value>none&lt;/value>
          &lt;/property>
          &lt;property>
            &lt;key>defaultNameCase&lt;/key>
            &lt;value>as_is&lt;/value>
          &lt;/property>
        &lt;/properties>
      &lt;/database>
    &lt;/generator>
  &lt;/configuration>
  &lt;dependencies>
    &lt;dependency>
      &lt;groupId>org.jooq&lt;/groupId>
      &lt;artifactId>jooq-meta-extensions&lt;/artifactId>
      &lt;version>${jooq-meta-extensions.version}&lt;/version>
    &lt;/dependency>
  &lt;/dependencies>
&lt;/plugin>
</textarea><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #81A1C1">&lt;plugin&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">  </span><span style="color: #81A1C1">&lt;groupId&gt;</span><span style="color: #D8DEE9FF">org.jooq</span><span style="color: #81A1C1">&lt;/groupId&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">  </span><span style="color: #81A1C1">&lt;artifactId&gt;</span><span style="color: #D8DEE9FF">jooq-codegen-maven</span><span style="color: #81A1C1">&lt;/artifactId&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">  </span><span style="color: #81A1C1">&lt;executions&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">&lt;execution&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">      </span><span style="color: #81A1C1">&lt;goals&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">&lt;goal&gt;</span><span style="color: #D8DEE9FF">generate</span><span style="color: #81A1C1">&lt;/goal&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">      </span><span style="color: #81A1C1">&lt;/goals&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">&lt;/execution&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">  </span><span style="color: #81A1C1">&lt;/executions&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">  </span><span style="color: #81A1C1">&lt;configuration&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">&lt;generator&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">      </span><span style="color: #81A1C1">&lt;database&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">&lt;name&gt;</span><span style="color: #D8DEE9FF">org.jooq.meta.extensions.ddl.DDLDatabase</span><span style="color: #81A1C1">&lt;/name&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">&lt;properties&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">          </span><span style="color: #81A1C1">&lt;property&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #81A1C1">&lt;key&gt;</span><span style="color: #D8DEE9FF">scripts</span><span style="color: #81A1C1">&lt;/key&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #81A1C1">&lt;value&gt;</span><span style="color: #D8DEE9FF">src/main/resources/db/h2/schema.sql</span><span style="color: #81A1C1">&lt;/value&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">          </span><span style="color: #81A1C1">&lt;/property&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">          </span><span style="color: #81A1C1">&lt;property&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #81A1C1">&lt;key&gt;</span><span style="color: #D8DEE9FF">sort</span><span style="color: #81A1C1">&lt;/key&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #81A1C1">&lt;value&gt;</span><span style="color: #D8DEE9FF">semantic</span><span style="color: #81A1C1">&lt;/value&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">          </span><span style="color: #81A1C1">&lt;/property&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">          </span><span style="color: #81A1C1">&lt;property&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #81A1C1">&lt;key&gt;</span><span style="color: #D8DEE9FF">unqualifiedSchema</span><span style="color: #81A1C1">&lt;/key&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #81A1C1">&lt;value&gt;</span><span style="color: #D8DEE9FF">none</span><span style="color: #81A1C1">&lt;/value&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">          </span><span style="color: #81A1C1">&lt;/property&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">          </span><span style="color: #81A1C1">&lt;property&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #81A1C1">&lt;key&gt;</span><span style="color: #D8DEE9FF">defaultNameCase</span><span style="color: #81A1C1">&lt;/key&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #81A1C1">&lt;value&gt;</span><span style="color: #D8DEE9FF">as_is</span><span style="color: #81A1C1">&lt;/value&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">          </span><span style="color: #81A1C1">&lt;/property&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">&lt;/properties&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">      </span><span style="color: #81A1C1">&lt;/database&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">&lt;/generator&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">  </span><span style="color: #81A1C1">&lt;/configuration&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">  </span><span style="color: #81A1C1">&lt;dependencies&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">&lt;dependency&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">      </span><span style="color: #81A1C1">&lt;groupId&gt;</span><span style="color: #D8DEE9FF">org.jooq</span><span style="color: #81A1C1">&lt;/groupId&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">      </span><span style="color: #81A1C1">&lt;artifactId&gt;</span><span style="color: #D8DEE9FF">jooq-meta-extensions</span><span style="color: #81A1C1">&lt;/artifactId&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">      </span><span style="color: #81A1C1">&lt;version&gt;</span><span style="color: #D8DEE9FF">${jooq-meta-extensions.version}</span><span style="color: #81A1C1">&lt;/version&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">&lt;/dependency&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">  </span><span style="color: #81A1C1">&lt;/dependencies&gt;</span></span>
<span class="line"><span style="color: #81A1C1">&lt;/plugin&gt;</span></span>
<span class="line"></span></code></pre></div>



<p>
  La classe org.jooq.meta.extensions.ddl.<strong>DDLDatabase</strong>  provenant de l’extension <a href="https://www.jooq.org/doc/latest/manual/code-generation/codegen-meta-sources/codegen-ddl/">jooq-meta-extensions</a> permet au plugin jooq-codegen-maven d’exploiter le script DDL src/main/resources/db/h2/<a href="https://github.com/spring-petclinic/spring-petclinic-jooq/blob/v3.4.2/src/main/resources/db/h2/schema.sql">schema.sql</a> utilisé par défaut lors du démarrage de l’application avec le profil Spring par défaut.
</p>



<p>
  Dans le package org.jooq.generated.tables, l’exécution du plugin jOOQ génère une <strong>classe Vets</strong> héritant de la classe TableImpl&lt;VetsRecord&gt; et modélisant la table éponyme. La <strong>classe VetsRecord</strong> a également été générée dans le sous-package records. Elle représente une ligne de la table pets.
  <br>
  Nous verrons leurs usages lors de la migration de la classe PetRepository.
</p>



<h2 class="wp-block-heading">Migration des repositories</h2>



<p><strong>L’une des forces de jOOQ est qu’il sait cohabiter aux côtés de JPA</strong>. 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. <br> <br> Premier changement notable : la nature des repositories qui passent d’une interface héritant de l’interface <strong>Repository</strong> de <strong>Spring Data JPA</strong> à une <strong>classe concrète</strong> qu’on annote avec <strong>@Repository</strong> et dont le constructeur accepte une instance de <strong>DSLContext</strong>. <br> <br> Avant : </p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><textarea class="code-block-pro-copy-button-textarea" aria-hidden="true" readonly>public interface VetRepository extends Repository&lt;Vet, Integer> {</textarea><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #81A1C1">public</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">interface</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">VetRepository</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">extends</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB; font-weight: bold">Repository</span><span style="color: #ECEFF4">&lt;</span><span style="color: #8FBCBB">Vet</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">Integer</span><span style="color: #ECEFF4">&gt;</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span></code></pre></div>



<p>  Après: </p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><textarea class="code-block-pro-copy-button-textarea" aria-hidden="true" readonly>@Repository
public class VetRepository {

    private final DSLContext dsl;

    public VetRepository(DSLContext dslContext) {
       this.dsl = dslContext;
    }</textarea><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #ECEFF4">@</span><span style="color: #D08770">Repository</span></span>
<span class="line"><span style="color: #81A1C1">public</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">class</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">VetRepository</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">private</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">final</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">DSLContext</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">dsl</span><span style="color: #81A1C1">;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">public</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">VetRepository</span><span style="color: #ECEFF4">(</span><span style="color: #8FBCBB">DSLContext</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">dslContext</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">       </span><span style="color: #81A1C1">this</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">dsl</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> dslContext</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">}</span></span></code></pre></div>



<p>Continuons par changer l’implémentation d’une première requête SQL simple utilisant un SELECT. <br>Requête native SQL utilisée par Spring Data JPA :</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><textarea class="code-block-pro-copy-button-textarea" aria-hidden="true" readonly>@Query(« SELECT ptype FROM PetType ptype ORDER BY ptype.name »)
List&lt;PetType> findPetTypes();</textarea><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #ECEFF4">@</span><span style="color: #D08770">Query</span><span style="color: #ECEFF4">(</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">SELECT ptype FROM PetType ptype ORDER BY ptype.name</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #8FBCBB">List</span><span style="color: #81A1C1">&lt;</span><span style="color: #D8DEE9FF">PetType</span><span style="color: #81A1C1">&gt;</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">findPetTypes</span><span style="color: #ECEFF4">()</span><span style="color: #81A1C1">;</span></span></code></pre></div>



<p>
  Implémentation équivalente avec jOOQ :
</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><textarea class="code-block-pro-copy-button-textarea" aria-hidden="true" readonly>public List&lt;PetType> findPetTypes() {
    return dsl
       .selectFrom(TYPES)
       .orderBy(TYPES.NAME)
       .fetchInto(PetType.class);
}</textarea><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #81A1C1">public</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">List</span><span style="color: #81A1C1">&lt;</span><span style="color: #D8DEE9FF">PetType</span><span style="color: #81A1C1">&gt;</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">findPetTypes</span><span style="color: #ECEFF4">()</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">return</span><span style="color: #D8DEE9FF"> dsl</span></span>
<span class="line"><span style="color: #D8DEE9FF">       </span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">selectFrom</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">TYPES</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">       </span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">orderBy</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9">TYPES</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">NAME</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">       </span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">fetchInto</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9">PetType</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">class</span><span style="color: #ECEFF4">)</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #ECEFF4">}</span></span></code></pre></div>



<p>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 <strong>selectFrom()</strong>, le « ORDER BY » par la méthode <strong>orderBy()</strong>. 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. <br>Enfin, la méthode <strong>fetchInto()</strong> mappe les lignes retournées par la base dans une liste d’instance de PetType.</p>



<p>Là 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 : </p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><textarea class="code-block-pro-copy-button-textarea" aria-hidden="true" readonly>public 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&lt;Field&lt;?>, Object> 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());
}</textarea><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #81A1C1">public</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">Integer</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">saveOrUpdateDetails</span><span style="color: #ECEFF4">(</span><span style="color: #8FBCBB">Owner</span><span style="color: #D8DEE9FF"> owner</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">if</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9">owner</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">isNew</span><span style="color: #ECEFF4">())</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">       </span><span style="color: #81A1C1">return</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">requireNonNull</span><span style="color: #ECEFF4">(</span></span>
<span class="line"><span style="color: #D8DEE9FF">          </span><span style="color: #D8DEE9">dsl</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">insertInto</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">OWNERS</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">             </span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">set</span><span style="color: #ECEFF4">(</span><span style="color: #88C0D0">mapOwnerToRecord</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">owner</span><span style="color: #ECEFF4">))</span></span>
<span class="line"><span style="color: #D8DEE9FF">             </span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">returningResult</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9">OWNERS</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">ID</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">             </span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">fetchOne</span><span style="color: #ECEFF4">())</span></span>
<span class="line"><span style="color: #D8DEE9FF">          </span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">getValue</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9">OWNERS</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">ID</span><span style="color: #ECEFF4">)</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">}</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">else</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">       </span><span style="color: #D8DEE9">dsl</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">update</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">OWNERS</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">          </span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">set</span><span style="color: #ECEFF4">(</span><span style="color: #88C0D0">mapOwnerToRecord</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">owner</span><span style="color: #ECEFF4">))</span></span>
<span class="line"><span style="color: #D8DEE9FF">          </span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">where</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9">OWNERS</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">ID</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">eq</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9">owner</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">getId</span><span style="color: #ECEFF4">()))</span></span>
<span class="line"><span style="color: #D8DEE9FF">          </span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">execute</span><span style="color: #ECEFF4">()</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">       </span><span style="color: #81A1C1">return</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">owner</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">getId</span><span style="color: #ECEFF4">()</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #ECEFF4">}</span></span>
<span class="line"></span>
<span class="line"><span style="color: #81A1C1">private</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">Map</span><span style="color: #81A1C1">&lt;</span><span style="color: #8FBCBB">Field</span><span style="color: #81A1C1">&lt;?&gt;</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> Object</span><span style="color: #81A1C1">&gt;</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">mapOwnerToRecord</span><span style="color: #ECEFF4">(</span><span style="color: #8FBCBB">Owner</span><span style="color: #D8DEE9FF"> owner</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">return</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">Map</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">of</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9">OWNERS</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">FIRST_NAME</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">owner</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">getFirstName</span><span style="color: #ECEFF4">(),</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">OWNERS</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">LAST_NAME</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">owner</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">getLastName</span><span style="color: #ECEFF4">(),</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">OWNERS</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">ADDRESS</span><span style="color: #ECEFF4">,</span></span>
<span class="line"><span style="color: #D8DEE9FF">       </span><span style="color: #D8DEE9">owner</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">getAddress</span><span style="color: #ECEFF4">(),</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">OWNERS</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">CITY</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">owner</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">getCity</span><span style="color: #ECEFF4">(),</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">OWNERS</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">TELEPHONE</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">owner</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">getTelephone</span><span style="color: #ECEFF4">())</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #ECEFF4">}</span></span></code></pre></div>



<p>Rien de compliqué, mais un peu plus verbeux. <br>L’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. <br>Avec l’implémentation Spring Data JPA, dans le cadre de la mise à jour d’un Owner, <a href="https://www.baeldung.com/spring-data-jpa-skip-select-insert">comme expliqué dans l’article Skip Select Before Insert in Spring Data JPA</a>, Spring Data JPA appelle la méthode <strong>merge()</strong> 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.</p>



<p>Autre 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 <strong>cascading</strong> et l’attribut cascade des annotations comme @OneToMany.</p>



<p>Dans la même idée, lors de requêtes de type SELECT, les <strong>jointures</strong> entre tables devront être systématiquement précisées. A titre d’exemple, charger un animal et son type nécessitera un appel à <strong>join()</strong> : </p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><textarea class="code-block-pro-copy-button-textarea" aria-hidden="true" readonly>@Transactional(readOnly = true)
public Optional&lt;Pet> findByIdWithoutVisits(Integer petId) {
    return dsl.select()
       .from(PETS)
       .join(PETS.types_())
       .where(PETS.ID.eq(petId))
       .fetchOptional(PetRepository::toPet);
}</textarea><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #ECEFF4">@</span><span style="color: #D08770">Transactional</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9">readOnly</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">true</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #81A1C1">public</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">Optional</span><span style="color: #81A1C1">&lt;</span><span style="color: #D8DEE9FF">Pet</span><span style="color: #81A1C1">&gt;</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">findByIdWithoutVisits</span><span style="color: #ECEFF4">(</span><span style="color: #8FBCBB">Integer</span><span style="color: #D8DEE9FF"> petId</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">return</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">dsl</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">select</span><span style="color: #ECEFF4">()</span></span>
<span class="line"><span style="color: #D8DEE9FF">       </span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">from</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">PETS</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">       </span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">join</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9">PETS</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">types_</span><span style="color: #ECEFF4">())</span></span>
<span class="line"><span style="color: #D8DEE9FF">       </span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">where</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9">PETS</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">ID</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">eq</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">petId</span><span style="color: #ECEFF4">))</span></span>
<span class="line"><span style="color: #D8DEE9FF">       </span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">fetchOptional</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">PetRepository</span><span style="color: #81A1C1">::</span><span style="color: #D8DEE9FF">toPet</span><span style="color: #ECEFF4">)</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #ECEFF4">}</span></span></code></pre></div>



<p>Noter ici l’utilisation d’une <a href="https://www.jooq.org/doc/latest/manual/sql-building/sql-statements/select-statement/from-clause/implicit-join/"><strong>jointure implicite</strong></a> basée sur la<strong> clé étrangère</strong>, évitant ainsi d’ajouter une clause <strong>ON</strong> entre la PK de PETS et la FK de TYPES. <br>Avec une <strong>association 1:1</strong>, le chargement et le mapping du type d’animal ne présente aucune difficulté : </p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><textarea class="code-block-pro-copy-button-textarea" aria-hidden="true" readonly>private 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)));
}</textarea><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #81A1C1">private</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">static</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">Pet</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">toPet</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9">org</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">jooq</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">Record</span><span style="color: #D8DEE9FF"> row</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">return</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">new</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">Pet</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9">row</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">get</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9">PETS</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">ID</span><span style="color: #ECEFF4">),</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">row</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">get</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9">PETS</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">NAME</span><span style="color: #ECEFF4">),</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">row</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">get</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9">PETS</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">BIRTH_DATE</span><span style="color: #ECEFF4">),</span></span>
<span class="line"><span style="color: #D8DEE9FF">          </span><span style="color: #81A1C1">new</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">PetType</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9">row</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">get</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9">PETS</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">TYPE_ID</span><span style="color: #ECEFF4">),</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">row</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">get</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9">TYPES</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">NAME</span><span style="color: #ECEFF4">)))</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #ECEFF4">}</span></span></code></pre></div>



<p>
  Le chargement des <strong>associations 1:N</strong> se complexifie. L’usage de l’<a href="https://blog.jooq.org/jooq-3-15s-new-multiset-operator-will-change-how-you-think-about-sql/"><strong>opérateur mulstiset()</strong></a><strong> </strong>du SQL qui est supporté par jOOQ permet de charger les Vets et leurs Specialities en une seule requête&nbsp;:
</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><textarea class="code-block-pro-copy-button-textarea" aria-hidden="true" readonly>public List&lt;Vet> 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&lt;List&lt;Specialty>> MULTISET_SPECIALITIES = multiset(
    select(VET_SPECIALTIES.specialties().ID, VET_SPECIALTIES.specialties().NAME)
       .from(VET_SPECIALTIES)
       .where(VET_SPECIALTIES.VET_ID.eq(VETS.ID)))
    .as(« specialties »)
    .convertFrom(result -> result.map(it -> new Specialty(it.get(SPECIALTIES.ID), it.get(SPECIALTIES.NAME))));


private static Vet toVet(Record4&lt;Integer, String, String, List&lt;Specialty>> row) {
    return new Vet(row.get(VETS.ID), row.get(VETS.FIRST_NAME), row.get(VETS.LAST_NAME),
       new HashSet&lt;>(row.get(MULTISET_SPECIALITIES)));
}</textarea><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #81A1C1">public</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">List</span><span style="color: #81A1C1">&lt;</span><span style="color: #D8DEE9FF">Vet</span><span style="color: #81A1C1">&gt;</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">findAll</span><span style="color: #ECEFF4">(){</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">return</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">dsl</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">select</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9">VETS</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">ID</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">VETS</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">FIRST_NAME</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">VETS</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">LAST_NAME</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> MULTISET_SPECIALITIES</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">       </span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">from</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">VETS</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">       </span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">leftJoin</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9">VETS</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">vetSpecialties</span><span style="color: #ECEFF4">())</span></span>
<span class="line"><span style="color: #D8DEE9FF">       </span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">orderBy</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9">VETS</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">ID</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">       </span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">fetch</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">VetRepository</span><span style="color: #81A1C1">::</span><span style="color: #D8DEE9FF">toVet</span><span style="color: #ECEFF4">)</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #ECEFF4">}</span></span>
<span class="line"></span>
<span class="line"><span style="color: #81A1C1">private</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">static</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">final</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">Field</span><span style="color: #ECEFF4">&lt;</span><span style="color: #8FBCBB">List</span><span style="color: #ECEFF4">&lt;</span><span style="color: #8FBCBB">Specialty</span><span style="color: #ECEFF4">&gt;&gt;</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">MULTISET_SPECIALITIES</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">multiset</span><span style="color: #ECEFF4">(</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #88C0D0">select</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9">VET_SPECIALTIES</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">specialties</span><span style="color: #ECEFF4">().</span><span style="color: #D8DEE9">ID</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">VET_SPECIALTIES</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">specialties</span><span style="color: #ECEFF4">().</span><span style="color: #D8DEE9">NAME</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">       </span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">from</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">VET_SPECIALTIES</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">       </span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">where</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9">VET_SPECIALTIES</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">VET_ID</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">eq</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9">VETS</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">ID</span><span style="color: #ECEFF4">)))</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">as</span><span style="color: #ECEFF4">(</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">specialties</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">convertFrom</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">result </span><span style="color: #8FBCBB">-&gt;</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">result</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">map</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">it </span><span style="color: #8FBCBB">-&gt;</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">new</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">Specialty</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9">it</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">get</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9">SPECIALTIES</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">ID</span><span style="color: #ECEFF4">),</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">it</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">get</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9">SPECIALTIES</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">NAME</span><span style="color: #ECEFF4">))))</span><span style="color: #81A1C1">;</span></span>
<span class="line"></span>
<span class="line"></span>
<span class="line"><span style="color: #81A1C1">private</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">static</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">Vet</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">toVet</span><span style="color: #ECEFF4">(</span><span style="color: #8FBCBB">Record4</span><span style="color: #81A1C1">&lt;</span><span style="color: #D8DEE9FF">Integer</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> String</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> String</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">List</span><span style="color: #81A1C1">&lt;</span><span style="color: #D8DEE9FF">Specialty</span><span style="color: #81A1C1">&gt;&gt;</span><span style="color: #D8DEE9FF"> row</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">return</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">new</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">Vet</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9">row</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">get</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9">VETS</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">ID</span><span style="color: #ECEFF4">),</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">row</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">get</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9">VETS</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">FIRST_NAME</span><span style="color: #ECEFF4">),</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">row</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">get</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9">VETS</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">LAST_NAME</span><span style="color: #ECEFF4">),</span></span>
<span class="line"><span style="color: #D8DEE9FF">       </span><span style="color: #81A1C1">new</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">HashSet</span><span style="color: #ECEFF4">&lt;&gt;(</span><span style="color: #D8DEE9">row</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">get</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">MULTISET_SPECIALITIES</span><span style="color: #ECEFF4">)))</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #ECEFF4">}</span></span></code></pre></div>



<p>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 <a href="https://github.com/spring-petclinic/spring-petclinic-jooq/blob/main/src/main/java/org/springframework/samples/petclinic/owner/OwnerRepository.java">OwnerRepository</a>. <br> <br>Pour finir, les écrans « Find Owners » et « Veterinarians » affichent les résultats de manière paginée. jOOQ supporte la pagination au travers de la <a href="https://github.com/spring-petclinic/spring-petclinic-jooq/blob/main/src/main/java/org/springframework/samples/petclinic/owner/OwnerRepository.java">Seek Method</a> (aussi appelée <a href="https://blog.jooq.org/faster-sql-pagination-with-keysets-continued/">Keyset paging</a>) ou du <a href="https://blog.jooq.org/calculating-pagination-metadata-without-extra-roundtrips-in-sql/">calcul des méta-données de pagination en une seule requête SQL</a>. C’est cette dernière approche qui a été utilisée sur jOOQ Petclinic afin de garder iso-fonctionnels les <strong>écrans paginés</strong>. Les plus curieux peuvent se référer à l’implémentation de la méthode findAll(Pageable pageable) de <a href="https://github.com/spring-petclinic/spring-petclinic-jooq/blob/main/src/main/java/org/springframework/samples/petclinic/vet/VetRepository.java">VetRepository</a> et à la méthode paginate() du <a href="https://github.com/spring-petclinic/spring-petclinic-jooq/blob/main/src/main/java/org/springframework/samples/petclinic/system/JooqHelper.java">JooqHelper</a>. Sur le même modèle que ce que propose Spring Data, des <strong>records Pageable</strong> et <strong>Page</strong> ont été introduits dans la base de code. <br> <img loading="lazy" decoding="async" width="1248" height="554" class="wp-image-2582" src="https://javaetmoi.com/wp-content/uploads/2025/06/word-image-2580-2.png" srcset="https://javaetmoi.com/wp-content/uploads/2025/06/word-image-2580-2.png 1248w, https://javaetmoi.com/wp-content/uploads/2025/06/word-image-2580-2-300x133.png 300w, https://javaetmoi.com/wp-content/uploads/2025/06/word-image-2580-2-1024x455.png 1024w, https://javaetmoi.com/wp-content/uploads/2025/06/word-image-2580-2-768x341.png 768w, https://javaetmoi.com/wp-content/uploads/2025/06/word-image-2580-2-500x222.png 500w" sizes="auto, (max-width: 1248px) 100vw, 1248px" /></p>



<h2 class="wp-block-heading">Au revoir JPA</h2>



<p>
  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&nbsp;…).
</p>



<p>Débarrassé de JPA, nous pouvons revoir en partie le design de l’application qui avait été limité par ce dernier. En effet, <a href="ManyToMany">les entités JPA ne peuvent pas être modélisées avec des record Java</a>. 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 <strong>classes immutables</strong> ont pu être converties en <strong>record</strong>. Exemple du value object <a href="https://github.com/spring-petclinic/spring-petclinic-jooq/blob/v3.4.2/src/main/java/org/springframework/samples/petclinic/vet/Specialty.java">Speciality</a> : </p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><textarea class="code-block-pro-copy-button-textarea" aria-hidden="true" readonly>public record Specialty(Integer id, String name) {
}</textarea><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #81A1C1">public</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">record</span><span style="color: #D8DEE9FF"> Specialty</span><span style="color: #ECEFF4">(</span><span style="color: #8FBCBB">Integer</span><span style="color: #D8DEE9FF"> id</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">String</span><span style="color: #D8DEE9FF"> name</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #ECEFF4">}</span></span></code></pre></div>



<p>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 ?</p>



<h2 class="wp-block-heading">Conclusion</h2>



<p>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 <a href="https://www.jooq.org/notes?version=3.4">sortie en 2010</a>) et est utilisée par de grands comptes comme Apple, Allianz et Mastercard. <br>Notez néanmoins que jOOQ possède un système de <a href="https://www.jooq.org/legal/licensing">double licence </a>: commerciale et Open Source. <a href="https://www.jooq.org/download/support-matrix">Les distributions commerciales de jOOQ maintiennent un support versionné des SGBDR</a>. A contrario, l&rsquo;é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. <br> <br>En replongeant dans le SQL, je me suis aperçu que j’étais passé à côté de certaines fonctionnalités avancées comme les <a href="https://reintech.io/blog/understanding-sql-multiset-data-type">MULTISET</a>. A retenir.</p>



<p>Enfin, 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 <a href="spring-petclinic-jooq">spring-petclinic-jooq</a> en soumettant des Issues ou en proposant des Pull Requests. <br></p>
]]></content:encoded>
					
					<wfw:commentRss>https://javaetmoi.com/2025/06/de-spring-data-jpa-a-jooq/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>L’API Gatherers : l’outil qui manquait à vos Streams</title>
		<link>https://javaetmoi.com/2025/04/api-gatherers-outil-qui-manquait-a-vos-streams/</link>
					<comments>https://javaetmoi.com/2025/04/api-gatherers-outil-qui-manquait-a-vos-streams/#respond</comments>
		
		<dc:creator><![CDATA[Antoine]]></dc:creator>
		<pubDate>Fri, 25 Apr 2025 06:09:01 +0000</pubDate>
				<category><![CDATA[Conférence]]></category>
		<category><![CDATA[Devoxx]]></category>
		<category><![CDATA[Java]]></category>
		<guid isPermaLink="false">https://javaetmoi.com/?p=2551</guid>

					<description><![CDATA[Date : 16 avril 2025Conférence : Devoxx France 2025Speaker : José Paumard (Oracle)Format : Conférence 45 mn Support : slides sur Speakerdeck / replay Youtube Java 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. Tout comme &#8230; <a href="https://javaetmoi.com/2025/04/api-gatherers-outil-qui-manquait-a-vos-streams/" class="more-link">Continuer la lecture de <span class="screen-reader-text">L’API Gatherers : l’outil qui manquait à vos Streams</span>  <span class="meta-nav">&#8594;</span></a>]]></description>
										<content:encoded><![CDATA[
<p><span style="text-decoration: underline;">Date</span> : 16 avril 2025<br><span style="text-decoration: underline;">Conférence</span> : <a href="https://www.devoxx.fr/">Devoxx France 2025</a><br><span style="text-decoration: underline;">Speaker</span> : <a href="https://www.linkedin.com/in/jos%C3%A9-paumard-2458ba5/">José Paumard</a> (Oracle)<br><span style="text-decoration: underline;">Format</span> : Conférence 45 mn <br><span style="text-decoration: underline;">Support</span> : <a href="https://speakerdeck.com/josepaumard/lapi-gatherer-loutil-qui-manquait-a-vos-streams-4df175d2-6466-479e-bff1-369a2cad324a">slides sur Speakerdeck</a> / <a href="https://www.youtube.com/watch?v=__piR-N9pXA">replay Youtube</a></p>



<p><strong>Java Developer Advocate</strong> chez Oracle, <strong>José Paumard </strong>nous présente la nouvelle <a href="https://docs.oracle.com/en/java/javase/24/core/stream-gatherers.html"><strong>API Gatherers</strong></a> qui, depuis Java 24, vient se greffer sur l’<strong>API Stream </strong>Java sortie il y’a 11 ans avec Java 8.</p>



<p>Tout 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 <a href="https://openjdk.org/jeps/485">JEP 485 Stream Gatherers</a> conduite par <a href="https://viktorklang.com/">Viktor Klang</a>. Les plus curieux pourront regarder la <a href="https://www.youtube.com/watch?v=v_5SKpfkI2U">vidéo Youtube</a> du Deep Dive qu’a animé Viktor lors de la conférence JavaOne qui s’est tenue en mars 2025.</p>



<p>L’article <a href="https://dev.java/learn/api/streams/gatherers/">The Gatherer API</a> permet également d’approfondir votre étude des Gatherers. Notez que le site <a href="https://dev.java/">dev.java</a> permet désormais d’exécuter des snippets Java (pas directement dans le navigateur, mais sur un serveur Cloud).</p>



<p>Toutes les classes et interfaces de l’API Gatherers ont été ajoutées au <strong>package java.util.stream</strong>.</p>



<figure class="wp-block-image is-style-default"><img loading="lazy" decoding="async" width="1053" height="593" src="https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2551-1-edited.jpeg" alt="" class="wp-image-2559" srcset="https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2551-1-edited.jpeg 1053w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2551-1-edited-300x169.jpeg 300w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2551-1-edited-1024x577.jpeg 1024w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2551-1-edited-768x433.jpeg 768w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2551-1-edited-500x282.jpeg 500w" sizes="auto, (max-width: 1053px) 100vw, 1053px" /></figure>



<span id="more-2551"></span>



<h2 class="wp-block-heading">Opérations intermédiaires et terminales d’un Stream </h2>



<p>Pour rappel, un Stream se connecte à une source&nbsp;de données (collections, fichier, générateur de nombres aléatoires, regex). Un stream est composé de&nbsp;:</p>



<ol class="wp-block-list">
<li>zéro, une ou plusieurs <strong>opérations intermédiaires</strong> qui retournent un Stream</li>



<li>une seule et unique <strong>opération terminale</strong> qui retourne un résultat et clôture le Sream.</li>
</ol>



<p>Viktor assimile l’API Stream à celle d’un <strong>Builder </strong>: on décrit un pipeline d’opérations puis on appelle l’opération terminale pour déclencher son traitement.</p>



<p>Exemples d’<strong>opération terminales&nbsp;</strong>proposées par l’API Stream :</p>



<ul class="wp-block-list">
<li>reduce()&nbsp;: opération de réduction</li>



<li>findFirst()&nbsp;: renvoie un objet de type Optional qui encapsule le premier élément du Stream s&rsquo;il existe, ne consomme pas tous les éléments du Streams.</li>



<li>collect()&nbsp;: prend en paramètre un Collector</li>



<li>toList()&nbsp;: méthode raccourcie disponible depuis Java 16</li>
</ul>



<p>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&nbsp;: il ne le connait pas. <br><br>Le JDK propose de nombreuses <strong>opérations intermédiaires</strong>&nbsp;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 <strong>opérations stateless</strong> comme filter() et des <strong>opérations statefull</strong> come sorted() qui doivent consommer tous les éléments du stream avant de produire quelque chose vers le down stream.</p>



<p>Il n’y avait pas moyen de créer d’opérations intermédiaires jusqu’aux Gatherers. </p>



<h2 class="wp-block-heading">Que propose l’API Gatherer&nbsp;?</h2>



<p>L’interface générique Gatherer s’appuie sur 3 paramètres&nbsp;:</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="interface Gatherer&lt;T, A, R&gt; { 
    Integrator&lt;A, T, R&gt; integrator(); 
}" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #81A1C1">interface</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">Gatherer</span><span style="color: #ECEFF4">&lt;</span><span style="color: #8FBCBB">T</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">A</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">R</span><span style="color: #ECEFF4">&gt;</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span><span style="color: #D8DEE9FF"> </span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #8FBCBB">Integrator</span><span style="color: #ECEFF4">&lt;</span><span style="color: #8FBCBB">A</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">T</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">R</span><span style="color: #ECEFF4">&gt;</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">integrator</span><span style="color: #ECEFF4">()</span><span style="color: #81A1C1">;</span><span style="color: #D8DEE9FF"> </span></span>
<span class="line"><span style="color: #ECEFF4">}</span></span></code></pre></div>



<ul class="wp-block-list">
<li><strong>T</strong>&nbsp;: type des éléments consommés</li>



<li><strong>A</strong>&nbsp;: type mutable utilisé en interne par les Gatherers</li>



<li><strong>R</strong>&nbsp;: type des éléments poussés dans le down stream</li>
</ul>



<p>Avec sa méthode principale <strong>integrator(),</strong> José compare l’interface <strong>Gatherer</strong> à une interface fonctionnelle de type Supplier.</p>



<p>L’interface Gatherer met à disposition <strong>3 interfaces fonctionnelles</strong> imbriquées dont nous étudierons le fonctionnement&nbsp;: <strong>Downstream</strong>, <strong>Greedy</strong> et <strong>Integrator</strong>. <br>Exemple de le l’<strong>interface Integrator</strong>&nbsp;: </p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="@FunctionalInterface
interface Integrator&lt;A, T, R&gt; {
    boolean integrate(A state, T element, Downstream&lt;? super R&gt; downstream); 
}" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #ECEFF4">@</span><span style="color: #D08770">FunctionalInterface</span></span>
<span class="line"><span style="color: #81A1C1">interface</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">Integrator</span><span style="color: #ECEFF4">&lt;</span><span style="color: #8FBCBB">A</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">T</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">R</span><span style="color: #ECEFF4">&gt;</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">boolean</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">integrate</span><span style="color: #ECEFF4">(</span><span style="color: #8FBCBB">A</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">state</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">T</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">element</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">Downstream</span><span style="color: #ECEFF4">&lt;</span><span style="color: #8FBCBB">?</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">super</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">R</span><span style="color: #ECEFF4">&gt;</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">downstream</span><span style="color: #ECEFF4">)</span><span style="color: #81A1C1">;</span><span style="color: #D8DEE9FF"> </span></span>
<span class="line"><span style="color: #ECEFF4">}</span></span></code></pre></div>



<p>Afin de pouvoir utiliser le Gatherer, l’interface Stream de l’API Stream propose désormais depuis Java 24 la méthode gather&nbsp;: </p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="Stream&lt;R&gt; downStream = upstream.gather(gatherer);" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #8FBCBB">Stream</span><span style="color: #ECEFF4">&lt;</span><span style="color: #8FBCBB">R</span><span style="color: #ECEFF4">&gt;</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">downStream</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">upstream</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">gather</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">gatherer</span><span style="color: #ECEFF4">)</span><span style="color: #81A1C1">;</span></span></code></pre></div>



<p>Le JDK s’enrichit de la <strong>classe factory Gatherers </strong>(notez son pluriel) utilisées par les différentes implémentations des méthodes<strong> of() </strong>de l’interface Gatherer.</p>



<h2 class="wp-block-heading">Publier dans le Downstream </h2>



<p>Un Downstream reçoit des données traitées par une opération intermédiaire. C’est le flux de sortie d’un Gatherer. <br>Voici un exemple de Gatherer chargé de pousser un élément dans le Downstream :</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="Gatherer&lt;T, ?, R&gt; gatherer = Gatherer.of(
     (_, element, downStream) -&gt; downStream.push(element) // returns a boolean
);" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #8FBCBB">Gatherer</span><span style="color: #ECEFF4">&lt;</span><span style="color: #8FBCBB">T</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">?</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">R</span><span style="color: #ECEFF4">&gt;</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">gatherer</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">Gatherer</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">of</span><span style="color: #ECEFF4">(</span></span>
<span class="line"><span style="color: #D8DEE9FF">     </span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">_</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> element</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> downStream</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">-&gt;</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">downStream</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">push</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">element</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #616E88">// returns a boolean</span></span>
<span class="line"><span style="color: #ECEFF4">)</span><span style="color: #81A1C1">;</span></span></code></pre></div>



<p>Le <strong>booléen renvoyé</strong> en retour est important. Son fonctionnement est subtil&nbsp;: renvoyer <strong>false</strong> permet l’<strong>arrêt du traitement des éléments suivants</strong>. 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.</p>



<p>Dans le jargon de l’API Gatherer, lorsqu’un Integrator retourne directement la valeur du downstream.push(element), on dit qu’il est <strong>Greedy</strong>. Il traitera nécessairement tous les éléments du Stream. Son exécution est optimisée. Exemple&nbsp;:</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="Gatherer&lt;T, ?, R&gt; gatherer = Gatherer.of(
    Integrator.of((_, element, downstream) -&gt; downstream.push(element))
);" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #8FBCBB">Gatherer</span><span style="color: #ECEFF4">&lt;</span><span style="color: #8FBCBB">T</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">?</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">R</span><span style="color: #ECEFF4">&gt;</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">gatherer</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">Gatherer</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">of</span><span style="color: #ECEFF4">(</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #D8DEE9">Integrator</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">of</span><span style="color: #ECEFF4">((</span><span style="color: #D8DEE9FF">_</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> element</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> downstream</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">-&gt;</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">downstream</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">push</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">element</span><span style="color: #ECEFF4">))</span></span>
<span class="line"><span style="color: #ECEFF4">)</span><span style="color: #81A1C1">;</span></span></code></pre></div>



<p>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 <strong>Integrator.ofGreedy()</strong> pour instancier un Integrator : </p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="Gatherer&lt;T, ?, R&gt; gatherer = Gatherer.of(
    Integrator.ofGreedy((_, element, downstream) -&gt; downstream.push(element))
);" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #8FBCBB">Gatherer</span><span style="color: #ECEFF4">&lt;</span><span style="color: #8FBCBB">T</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">?</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">R</span><span style="color: #ECEFF4">&gt;</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">gatherer</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">Gatherer</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">of</span><span style="color: #ECEFF4">(</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #D8DEE9">Integrator</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">ofGreedy</span><span style="color: #ECEFF4">((</span><span style="color: #D8DEE9FF">_</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> element</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> downstream</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">-&gt;</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">downstream</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">push</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">element</span><span style="color: #ECEFF4">))</span></span>
<span class="line"><span style="color: #ECEFF4">)</span><span style="color: #81A1C1">;</span></span></code></pre></div>



<p>Un Downstream possède un <strong>état</strong> nommé <strong>rejecting</strong>. La méthode isRejecting() de l’interface Downstream propose d’y accéder. Cet état a 3 propriétés&nbsp;:</p>



<ol class="wp-block-list">
<li>Commence à <strong>false</strong></li>



<li>Ne peut commuter que de <strong>false vers true</strong> (ne peut pas se rouvrir)</li>



<li>L’état de peut <strong>commuter</strong> que lors d’un <strong>push()</strong> =&gt; règle spécifique aux API du JDK</li>
</ol>



<p>José nous met en garde&nbsp;: 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. </p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-highlight-color:rgba(201, 218, 248, 0.2);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="(_, element, downstream) -&gt; {
    if (downstream.isRejecting()) { //
        return false;               // Condition inutile
    }                               // 
    return downstream.push(mapper.apply(element));
}" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">_</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> element</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> downstream</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">-&gt;</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line cbp-line-highlight"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">if</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9">downstream</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">isRejecting</span><span style="color: #ECEFF4">())</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span><span style="color: #D8DEE9FF"> </span><span style="color: #616E88">//</span></span>
<span class="line cbp-line-highlight"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">return</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">false;</span><span style="color: #D8DEE9FF">               </span><span style="color: #616E88">// Condition inutile</span></span>
<span class="line cbp-line-highlight"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">}</span><span style="color: #D8DEE9FF">                               </span><span style="color: #616E88">// </span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">return</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">downstream</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">push</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9">mapper</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">apply</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">element</span><span style="color: #ECEFF4">))</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #ECEFF4">}</span></span></code></pre></div>



<p>José continue sa présentation en nous expliquant les bonnes pratiques à adopter lorsqu’on publie sur le Downstream&nbsp;:</p>



<ul class="wp-block-list">
<li>Ne pas faire de test isRejecting() sur le Downstream</li>



<li>Privilégiez l’usage de la méthode allMath() plus efficace que takeWhile()</li>



<li>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</li>
</ul>



<p>Exemple exempté de bugs&nbsp;:</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="(_, element, downstream) -&gt; {
    try (Stream&lt;R&gt; elements = flatMapper.apply(element);) {
        return elements.allMatch(downstream::push); 
    }
}" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">_</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> element</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> downstream</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">-&gt;</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">try</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">(</span><span style="color: #8FBCBB">Stream</span><span style="color: #ECEFF4">&lt;</span><span style="color: #8FBCBB">R</span><span style="color: #ECEFF4">&gt;</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">elements</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">flatMapper</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">apply</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">element</span><span style="color: #ECEFF4">)</span><span style="color: #81A1C1">;</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">return</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">elements</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">allMatch</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">downstream</span><span style="color: #81A1C1">::</span><span style="color: #D8DEE9FF">push</span><span style="color: #ECEFF4">)</span><span style="color: #81A1C1">;</span><span style="color: #D8DEE9FF"> </span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #ECEFF4">}</span></span></code></pre></div>



<p>Un Downstream n’est <strong>pas un objet thread-safe</strong>. 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 <strong>parallel streams</strong>. <br>A ce titre, la méthode <strong>Gatherer.oSequential()</strong> permet de créer un Gatherer séquentiel (non parallélisable).</p>



<p>L’élément <strong>state</strong> est un état mutable pouvant être utilisé par le Gatherer. En complément de l’Integrator, il est nécessaire de fournir à l’API de création d’un Gatherer un <strong>Supplier</strong> chargé d’initialiser l’état du state.</p>



<p>Exemple d’un Gatherer limitant le nombre d’éléments et initialisant un compteur&nbsp;: </p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-highlight-color:rgba(201, 218, 248, 0.2);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="class Counter { long count = 0L; }

var gatherer = Gatherer.ofSequential( 
    Counter::new, // the initializer
    (state, element, downstream) -&gt; {
        if (state.count++ &lt; limit) {
            return downstream.push(element);
        } else {
            return false;
        }
});" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #81A1C1">class</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">Counter</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">long</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">count</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #B48EAD">0L</span><span style="color: #81A1C1">;</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">}</span></span>
<span class="line"></span>
<span class="line"><span style="color: #8FBCBB">var</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">gatherer</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">Gatherer</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">ofSequential</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF"> </span></span>
<span class="line cbp-line-highlight"><span style="color: #D8DEE9FF">    Counter</span><span style="color: #81A1C1">::new</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #616E88">// the initializer</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">state</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> element</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> downstream</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">-&gt;</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">if</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9">state</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">count</span><span style="color: #81A1C1">++</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">&lt;</span><span style="color: #D8DEE9FF"> limit</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #81A1C1">return</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">downstream</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">push</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">element</span><span style="color: #ECEFF4">)</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #ECEFF4">}</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">else</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #81A1C1">return</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">false;</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #ECEFF4">})</span><span style="color: #81A1C1">;</span></span></code></pre></div>



<p>A noter que l’opérateur <strong>var</strong> retient le type des classes anonymes. <br><br>Pour agir sur l’ensemble des données du Gatherer, on peut stocker les éléments dans une collection&nbsp;tel qu’un HashSet dans l’exemple suivant&nbsp;«&nbsp;Distinct Gatherer&nbsp;»:</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-highlight-color:rgba(201, 218, 248, 0.2);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="var gatherer = Gatherer.ofSequential(
    () -&gt; new Object() { Set&lt;T&gt; set = new HashSet&lt;&gt;(); },
    (state, element, downstream) -&gt; {
        if (state.set.add(element)) {
            return downstream.push(element);
        } else {
            return true;
        }
});" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #8FBCBB">var</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">gatherer</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">Gatherer</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">ofSequential</span><span style="color: #ECEFF4">(</span></span>
<span class="line cbp-line-highlight"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">()</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">-&gt;</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">new</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">Object</span><span style="color: #ECEFF4">()</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">Set</span><span style="color: #ECEFF4">&lt;</span><span style="color: #8FBCBB">T</span><span style="color: #ECEFF4">&gt;</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">set</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">new</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">HashSet</span><span style="color: #ECEFF4">&lt;&gt;()</span><span style="color: #81A1C1">;</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">},</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">state</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> element</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> downstream</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">-&gt;</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">if</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9">state</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">set</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">add</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">element</span><span style="color: #ECEFF4">))</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #81A1C1">return</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">downstream</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">push</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">element</span><span style="color: #ECEFF4">)</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #ECEFF4">}</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">else</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #81A1C1">return</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">true;</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #ECEFF4">})</span><span style="color: #81A1C1">;</span></span></code></pre></div>



<p>Pour publier l’état final d’un Gatherer, on peut ajouter après l’Initializer et l’Integrator une 3ième lambda de type <strong>BiConsumer</strong> agitant comme <strong>finisher</strong> et pouvant consommer tous les éléments du state&nbsp;:</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-highlight-color:rgba(201, 218, 248, 0.2);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="var gatherer = Gatherer.ofSequential(
    () -&gt; new Object() { Set&lt;T&gt; set = new TreeSet&lt;&gt;(); },
    (state, element, downstream) -&gt; { ... },
    (state, downstream) -&gt; { // finisher
        state.set.stream().allMatch(downstream::push);
});" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #8FBCBB">var</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">gatherer</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">Gatherer</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">ofSequential</span><span style="color: #ECEFF4">(</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">()</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">-&gt;</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">new</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">Object</span><span style="color: #ECEFF4">()</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">Set</span><span style="color: #ECEFF4">&lt;</span><span style="color: #8FBCBB">T</span><span style="color: #ECEFF4">&gt;</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">set</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">new</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">TreeSet</span><span style="color: #ECEFF4">&lt;&gt;()</span><span style="color: #81A1C1">;</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">},</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">state</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> element</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> downstream</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">-&gt;</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">...</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">},</span></span>
<span class="line cbp-line-highlight"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">state</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> downstream</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">-&gt;</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span><span style="color: #D8DEE9FF"> </span><span style="color: #616E88">// finisher</span></span>
<span class="line cbp-line-highlight"><span style="color: #D8DEE9FF">        </span><span style="color: #D8DEE9">state</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">set</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">stream</span><span style="color: #ECEFF4">().</span><span style="color: #88C0D0">allMatch</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">downstream</span><span style="color: #81A1C1">::</span><span style="color: #D8DEE9FF">push</span><span style="color: #ECEFF4">)</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #ECEFF4">})</span><span style="color: #81A1C1">;</span></span></code></pre></div>



<h2 class="wp-block-heading">Les Parallel Gatherers</h2>



<p>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&nbsp;:</p>



<ol class="wp-block-list">
<li><strong>Gatherer.of()</strong></li>



<li><strong>Gatherer.ofSequential()</strong></li>
</ol>



<p>Pour supporter le parallélisme, l’API Gatherer adopte le principe suivant&nbsp;: <strong>un objet state par thread</strong>. Cela permet de ne pas utiliser de collections synchronisées dégradant les performances. <br>Dans 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 <strong>Combiner</strong> pour combiner tous les états. <br><img loading="lazy" decoding="async" width="1486" height="563" class="wp-image-2553" src="https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2551-2.png" srcset="https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2551-2.png 1486w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2551-2-300x114.png 300w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2551-2-1024x388.png 1024w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2551-2-768x291.png 768w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2551-2-500x189.png 500w" sizes="auto, (max-width: 1486px) 100vw, 1486px" /></p>



<p><br>Ce <strong>Combiner</strong> est un <strong>4<sup>ième</sup> paramètre</strong> à passer à la méthode factory <strong>of()</strong>&nbsp;: </p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-highlight-color:rgba(201, 218, 248, 0.2);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="var gatherer = Gatherer.of(
    () -&gt; new Object() { Set&lt;T&gt; set = new HashSet&lt;&gt;(); }, 
    (state, element, downstream) -&gt; { // executed in
        state.set.add(element); // different threads
        return true;
    },
    (state1, state2) -&gt; { // combiner
        state1.set.addAll(state2.set);
        return state1;
    },
    (state, downstream) -&gt; { // finisher
        state.set.allMatch(downstream::push);
    }
);" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #8FBCBB">var</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">gatherer</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">Gatherer</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">of</span><span style="color: #ECEFF4">(</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">()</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">-&gt;</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">new</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">Object</span><span style="color: #ECEFF4">()</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">Set</span><span style="color: #ECEFF4">&lt;</span><span style="color: #8FBCBB">T</span><span style="color: #ECEFF4">&gt;</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">set</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">new</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">HashSet</span><span style="color: #ECEFF4">&lt;&gt;()</span><span style="color: #81A1C1">;</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">},</span><span style="color: #D8DEE9FF"> </span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">state</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> element</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> downstream</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">-&gt;</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span><span style="color: #D8DEE9FF"> </span><span style="color: #616E88">// executed in</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #D8DEE9">state</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">set</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">add</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">element</span><span style="color: #ECEFF4">)</span><span style="color: #81A1C1">;</span><span style="color: #D8DEE9FF"> </span><span style="color: #616E88">// different threads</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">return</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">true;</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">},</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">state1</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> state2</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">-&gt;</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span><span style="color: #D8DEE9FF"> </span><span style="color: #616E88">// combiner</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #D8DEE9">state1</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">set</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">addAll</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9">state2</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">set</span><span style="color: #ECEFF4">)</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">return</span><span style="color: #D8DEE9FF"> state1</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">},</span></span>
<span class="line cbp-line-highlight"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">state</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> downstream</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">-&gt;</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span><span style="color: #D8DEE9FF"> </span><span style="color: #616E88">// finisher</span></span>
<span class="line cbp-line-highlight"><span style="color: #D8DEE9FF">        </span><span style="color: #D8DEE9">state</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">set</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">allMatch</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">downstream</span><span style="color: #81A1C1">::</span><span style="color: #D8DEE9FF">push</span><span style="color: #ECEFF4">)</span><span style="color: #81A1C1">;</span></span>
<span class="line cbp-line-highlight"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #ECEFF4">)</span><span style="color: #81A1C1">;</span></span></code></pre></div>



<p>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 <strong>Sequential Gatherer</strong>. Cette fonctionnalité est nouvelle et donc à utiliser avec précaution. Tester les perfs.</p>



<figure class="wp-block-image"><img loading="lazy" decoding="async" width="1453" height="824" src="https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2551-3.png" alt="" class="wp-image-2554" srcset="https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2551-3.png 1453w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2551-3-300x170.png 300w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2551-3-1024x581.png 1024w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2551-3-768x436.png 768w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2551-3-500x284.png 500w" sizes="auto, (max-width: 1453px) 100vw, 1453px" /></figure>



<p>Pour aller plus loin, José nous invite à consulter le repo GitHub <a href="https://github.com/SvenWoltmann/stream-gatherers">SvenWoltmann/stream-gatherers</a>. Le JDK vient avec de nouveaux Gatherers comme scan(), fold() ou bien encore mapConcurrent(). <br>Des librairies tierces comme <a href="https://github.com/tginsberg/gatherers4j">gatherers4j</a> proposent également leur propres gatherers&nbsp;: reverse(), repeat(n), groupBy(fn) …</p>



<p>Pour conclure, retenons qu’<strong>un Gatherer est construit sur 4 éléments</strong>. Tous ne sont pas obligatoires.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://javaetmoi.com/2025/04/api-gatherers-outil-qui-manquait-a-vos-streams/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Les clés de l&#8217;architecture pour les dévs</title>
		<link>https://javaetmoi.com/2025/04/cles-de-l-architecture-pour-les-devs/</link>
					<comments>https://javaetmoi.com/2025/04/cles-de-l-architecture-pour-les-devs/#respond</comments>
		
		<dc:creator><![CDATA[Antoine]]></dc:creator>
		<pubDate>Tue, 22 Apr 2025 17:54:32 +0000</pubDate>
				<category><![CDATA[Retour d'expérience]]></category>
		<category><![CDATA[Architecture]]></category>
		<category><![CDATA[Devoxx]]></category>
		<guid isPermaLink="false">https://javaetmoi.com/?p=2508</guid>

					<description><![CDATA[Conférence : Devoxx France 2025Date : 17 avril 2025Speakers : Cyrille Martraire (Arolla), Eric Le Merdy (QuickSign) remplaçant de Christian Sperandio (Arolla)Format : Conférence (45mn) / Replay Youtube Cette 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é. Un constat est posé. Sur les dix &#8230; <a href="https://javaetmoi.com/2025/04/cles-de-l-architecture-pour-les-devs/" class="more-link">Continuer la lecture de <span class="screen-reader-text">Les clés de l&#8217;architecture pour les dévs</span>  <span class="meta-nav">&#8594;</span></a>]]></description>
										<content:encoded><![CDATA[
<p><span style="text-decoration: underline;">Conférence</span> : <a href="https://www.devoxx.fr/">Devoxx France 2025</a><br><span style="text-decoration: underline;">Date</span> : 17 avril 2025<br><span style="text-decoration: underline;">Speakers</span> : <a href="https://www.linkedin.com/in/martraire/?originalSubdomain=fr">Cyrille Martraire</a> (Arolla), <a href="https://www.linkedin.com/in/eric-le-merdy-bb60704/?originalSubdomain=fr">Eric Le Merdy</a> (QuickSign) remplaçant de <a href="https://www.linkedin.com/in/christian-sperandio-25182a12">Christian Sperandio</a> (Arolla)<br><span style="text-decoration: underline;">Format</span> : Conférence (45mn)  / <a href="https://www.youtube.com/watch?v=ZoYDxF_7LoI&amp;t=528s">Replay Youtube</a></p>



<p>Cette conférence a pour <strong>objectif</strong> d’<strong>ouvrir les portes </strong>en nous donnant les <strong>clés de l’architecture</strong>. Pour seconder Cyrille, Eric a du remplacer Christian au pied levé. <br>Un constat est posé. Sur les <strong>dix dernières années</strong>, les <strong>systèmes</strong> ont changé&nbsp;: ils sont devenus <strong>modulaires</strong>, de plus en plus <strong>distribués</strong>. La modularité permise par le Cloud permet de répartir la charge. Il y’a <strong>de</strong> <strong>plus en plus d’interconnexions entre briques applicatives</strong>. <br><strong>L’architecture bouge tout le temps</strong>, évolue constamment. <br><img loading="lazy" decoding="async" width="1384" height="778" class="wp-image-2509" style="" src="https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-1.jpeg" alt="Cyrille Martraire et Eric Le Merdy sur la scène de Devoxx France 2025" srcset="https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-1.jpeg 1384w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-1-300x169.jpeg 300w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-1-1024x576.jpeg 1024w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-1-768x432.jpeg 768w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-1-500x281.jpeg 500w" sizes="auto, (max-width: 1384px) 100vw, 1384px" /></p>



<p>Que doit-on savoir&nbsp;? Pour commencer, on ne saura jamais tout et il faudra vivre avec. Personne ne sait tout. Même le plus capé des architectes. </p>



<p>Comme fil conducteur, Cyrille et Eric prennent un <strong>exemple réel</strong> issu du monde des <strong>télécommunications</strong>. <br>Pour cahier des charges, le client&nbsp;précise que le <strong>système</strong> va <strong>recevoir des fichiers chaque minute</strong> et doit <strong>les intégrer tous les 15mn</strong>.  Contexte&nbsp;: ces fichiers viennent d’équipements télécom.</p>



<span id="more-2508"></span>



<p>Première question à se poser&nbsp;: «&nbsp;est-ce possible de synchroniser la temporalité&nbsp;?&nbsp;».<br>Réponse du client : «&nbsp;Non, ce n’est pas possible&nbsp;». <br>La brique centrale est nommée <strong>Aggregator</strong>. <br><img loading="lazy" decoding="async" width="1436" height="915" class="wp-image-2510" src="https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-2.png" style="" alt="Diagramme de contexte C4&nbsp;du système" srcset="https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-2.png 1436w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-2-300x191.png 300w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-2-1024x652.png 1024w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-2-768x489.png 768w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-2-471x300.png 471w" sizes="auto, (max-width: 1436px) 100vw, 1436px" /> <br><br><a href="https://c4model.com/diagrams/system-context">Diagramme de contexte C4</a>&nbsp;correspondant :</p>



<figure class="wp-block-image"><img loading="lazy" decoding="async" width="2340" height="1488" src="https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-3.png" alt="" class="wp-image-2511" srcset="https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-3.png 2340w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-3-300x191.png 300w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-3-1024x651.png 1024w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-3-768x488.png 768w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-3-1536x977.png 1536w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-3-2048x1302.png 2048w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-3-472x300.png 472w" sizes="auto, (max-width: 2340px) 100vw, 2340px" /></figure>



<p><strong>Première clé</strong>&nbsp;donnée dans ce talk : commencer par <strong>identifier le problème</strong>. <br>Comment l’appliquer&nbsp;: quel est le but&nbsp;? Ici c’est d’agréger les données reçues. <br>Cette première clé parait banal&nbsp;: penser problème avant de penser à la solution. Cet adage bien connu s’applique&nbsp;: «&nbsp;un problème bien posé est à moitié une solution&nbsp;». <br><br>Après avoir cerner le problème, on continue en prenant en compte les nombreux <a href="https://softwaremill.com/the-importance-of-software-quality-attributes/"><strong>Software</strong> <strong>Quality Attributes</strong>&nbsp;</a>dont font partis le cout, la performance, la sécurité ou bien encore le sourcing des dévelopeurs. Liste complète sur <a href="http://github.com/arc42/arc42-template">arc42-templates</a> et la <a href="https://faq.arc42.org/questions/C-1-2/">FAQ C-1-2</a>. <br><br>Examinons à présent les* contraintes du système. <br>1<sup>ière</sup>&nbsp;contrainte : <strong>disponibilité</strong><br>Toujours Up pour recevoir les données. Calcule de données toutes les 15mn. <br><br>2<sup>nde</sup>&nbsp;contrainte : <strong>performance</strong> <br>Le 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 <strong>6 milliards de données</strong> que le système devra traiter toutes les 15 minutes. <br><img loading="lazy" decoding="async" width="2317" height="1405" class="wp-image-2512" src="https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-4.png" style="" alt="Quality Attributes Clusters" srcset="https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-4.png 2317w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-4-300x182.png 300w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-4-1024x621.png 1024w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-4-768x466.png 768w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-4-1536x931.png 1536w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-4-2048x1242.png 2048w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-4-495x300.png 495w" sizes="auto, (max-width: 2317px) 100vw, 2317px" /> <br>La formule de calcul de l’agrégation est compliquée&nbsp;; ce n’est pas de simples additions. <br>Le 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. <br><br><strong>Seconde clé</strong> donnée dans ce talk&nbsp;: <strong>négocier</strong>, <strong>étudier</strong>, <strong>éduquer</strong> les gens. <br>Pas nécessaire de mettre systématiquement de la cohérence transactionnelle partout.</p>



<p>Les contraintes techniques&nbsp;nous guident pour définir l’architecture technique. Cette approche n’est pas antinomique avec le <strong>DDD</strong>. Dans notre exemple, il existe une corrélation entre les contraintes techniques et le découpage en sous-domaine. <br>On peut identifier <strong>2 sous-domaines</strong>&nbsp;: le parsing lors de l’ingest et le calcul de statistiques. </p>



<p>Une <strong>troisième clé</strong> nous est donnée&nbsp;: <strong>penser modulaire</strong> pour adresser le problème.</p>



<p>Voyons à présent comment implémenter ces 2 sous-domaines. <br>On pourrait partir sur 2 services. Mais dans un premier temps, Eric propose de <strong>commencer par seul service</strong>, <strong>plus simple</strong>, <strong>plus facile à implémenter</strong> et livrer. Par contre, afin de préparer un éventuel futur découplage, on utilise l’approche pragmatique de <strong>modular monolith</strong>. Bel exercice de <strong>frugalité</strong>&nbsp;: une solution distribuée est remplacée par un monolith.</p>



<figure class="wp-block-image"><img loading="lazy" decoding="async" width="1429" height="869" src="https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-5.png" alt="Modular monolith" class="wp-image-2513" srcset="https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-5.png 1429w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-5-300x182.png 300w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-5-1024x623.png 1024w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-5-768x467.png 768w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-5-493x300.png 493w" sizes="auto, (max-width: 1429px) 100vw, 1429px" /></figure>



<p>L’architecture se pense à différents niveaux, à plusieurs. <br><img loading="lazy" decoding="async" width="2342" height="1451" class="wp-image-2514" style="" src="https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-6.png" alt="Architectural perspectives" srcset="https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-6.png 2342w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-6-300x186.png 300w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-6-1024x634.png 1024w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-6-768x476.png 768w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-6-1536x952.png 1536w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-6-2048x1269.png 2048w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-6-484x300.png 484w" sizes="auto, (max-width: 2342px) 100vw, 2342px" /> <br>Les architectes d’entreprise ont souvent une vue d’ensemble globale. Les développeurs vont quant à eux s’intéresser davantage aux technologies. <br>Un conseil, garder en tête cet objectif&nbsp;: bien s’entendre avec tout le monde</p>



<p>Les différents types d’architectures ont leurs avantages et inconvénients. Voici celles qui auraient pu être choisies&nbsp;:</p>



<ol class="wp-block-list">
<li><strong>Microservices</strong>&nbsp;: modularité jusqu’au bout</li>



<li><strong>Modular monolith</strong>&nbsp;: facilite le découpage en microservices</li>



<li><strong>Function as a Service</strong></li>



<li><a href="big%20ball%20of%20mud"><strong>Big Ball of Mud</strong></a>&nbsp;: monolith avec archi spaghetti</li>
</ol>



<p>Parmi les contraintes techniques, le vrai <strong>risque</strong> consiste à tenir le <strong>délai de traitement d’agrégation des données</strong> en dessous des 2 minutes. La première étape consiste à lever ce risque. Il faut lever ce risque et commencer les développements.</p>



<figure class="wp-block-image"><img loading="lazy" decoding="async" width="1451" height="889" src="https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-7.png" alt="Integration options between modules 1" class="wp-image-2515" srcset="https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-7.png 1451w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-7-300x184.png 300w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-7-1024x627.png 1024w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-7-768x471.png 768w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-7-490x300.png 490w" sizes="auto, (max-width: 1451px) 100vw, 1451px" /></figure>



<figure class="wp-block-image"><img loading="lazy" decoding="async" width="1427" height="865" src="https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-8.png" alt="Integration options between modules 2" class="wp-image-2516" srcset="https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-8.png 1427w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-8-300x182.png 300w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-8-1024x621.png 1024w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-8-768x466.png 768w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-8-495x300.png 495w" sizes="auto, (max-width: 1427px) 100vw, 1427px" /></figure>



<p><strong>Réversible</strong>, l’<strong>architecture n°2 est retenue</strong> avec une approche <strong>hexagonale</strong>. On reste pragmatique&nbsp;: 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.</p>



<figure class="wp-block-image"><img loading="lazy" decoding="async" width="1455" height="899" src="https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-9.png" alt="" class="wp-image-2518" srcset="https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-9.png 1455w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-9-300x185.png 300w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-9-1024x633.png 1024w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-9-768x475.png 768w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-9-486x300.png 486w" sizes="auto, (max-width: 1455px) 100vw, 1455px" /></figure>



<p>Une 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&nbsp;: <strong>on sait que çà ne marche pas</strong>. Et on l’a découvert très vite. <br><br>La cause est rapidement identifiée&nbsp;: l’agrégateur du monolith est mono-thread. 3 solutions son envisagées&nbsp;: <br><strong>1. Solution 1</strong>&nbsp;: <strong>mono instance</strong> avec du <strong>multi-threading</strong>. Plus de vCPU, worker pools. <br><img loading="lazy" decoding="async" width="1795" height="1096" class="wp-image-2520" src="https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-10.png" srcset="https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-10.png 1795w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-10-300x183.png 300w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-10-1024x625.png 1024w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-10-768x469.png 768w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-10-1536x938.png 1536w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-10-491x300.png 491w" sizes="auto, (max-width: 1795px) 100vw, 1795px" /> <br><strong>2. Solution 2</strong>&nbsp;: <strong>multi instance avec du pub-sub</strong>. Rien à faire. On s’appuie sur un service du Cloud Provider. Clé&nbsp;: on reconnait les problèmes difficiles et on les délègue à du middleware en managé. <br><img loading="lazy" decoding="async" width="1770" height="1093" class="wp-image-2522" src="https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-11.png" srcset="https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-11.png 1770w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-11-300x185.png 300w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-11-1024x632.png 1024w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-11-768x474.png 768w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-11-1536x949.png 1536w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-11-486x300.png 486w" sizes="auto, (max-width: 1770px) 100vw, 1770px" /> <br><strong>3. Solution 3</strong>&nbsp;: combine multi-thread et multi-instance&nbsp;: trop compliqué et trop chère. Combine tous les inconvénients. A ne pas faire. <br><br>Approche choisie&nbsp;: solution 2. L’architecture est l’art du <strong>tradeoff</strong> (du compromis).</p>



<p><img loading="lazy" decoding="async" width="1808" height="1102" class="wp-image-2524" src="https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-12.png" srcset="https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-12.png 1808w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-12-300x183.png 300w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-12-1024x624.png 1024w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-12-768x468.png 768w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-12-1536x936.png 1536w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-12-492x300.png 492w" sizes="auto, (max-width: 1808px) 100vw, 1808px" /> <br><br>La solution retenue impose la <strong>fin du modular monolith</strong>. Nécessité de passer en <strong>micro-services</strong>&nbsp;: 2 services, 2 deployments et N services <br><br>Réfléchissons à présent sur ce qui pourrait mal se passer avec un <strong>tuyau asynchrone</strong>&nbsp;: messages en double ou triple, manque de ressources, messages perdus … <br><img loading="lazy" decoding="async" width="1787" height="1108" class="wp-image-2526" src="https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-13.png" srcset="https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-13.png 1787w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-13-300x186.png 300w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-13-1024x635.png 1024w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-13-768x476.png 768w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-13-1536x952.png 1536w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-13-484x300.png 484w" sizes="auto, (max-width: 1787px) 100vw, 1787px" /> <br>Le fournisseur de Cloud garantie une partie des problèmes évoqués. <br>Cyrille rappelle la nécessité d’un consumer à être <strong>idempotent</strong> pour gérer les messages en double.</p>



<figure class="wp-block-image"><img loading="lazy" decoding="async" width="1767" height="1076" src="https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-14.png" alt="Pubsub architecture tradeoffs" class="wp-image-2528" srcset="https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-14.png 1767w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-14-300x183.png 300w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-14-1024x624.png 1024w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-14-768x468.png 768w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-14-1536x935.png 1536w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-14-493x300.png 493w" sizes="auto, (max-width: 1767px) 100vw, 1767px" /></figure>



<p>Avant de faire un choix sur l’implémentation de l’adaptateur et assurer la persistance des données (ex&nbsp;: PostgreSQL vs Redis), <strong>Eric propose de rester en mémoire pour tester rapidement en prod</strong>. Cela <strong>permet</strong> de gagner du temps et <strong>de</strong> <strong>vérifier les hypothèses</strong>. <br>On 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.</p>



<p><img loading="lazy" decoding="async" width="2639" height="1618" class="wp-image-2529" src="https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-15.png" srcset="https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-15.png 2639w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-15-300x184.png 300w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-15-1024x628.png 1024w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-15-768x471.png 768w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-15-1536x942.png 1536w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-15-2048x1256.png 2048w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-15-489x300.png 489w" sizes="auto, (max-width: 2639px) 100vw, 2639px" /> <br>Le calcul dure moins de 2 minutes&nbsp;: l’hypothèse est validée. L’adaptateur peut désormais être implémenté avec Redis. <br><br>Message de fond&nbsp;: <strong>l’architecture est évolutive</strong>. Il ne faut pas la mettre en place dès le début. L’architecture est dynamique. Tout bouge. <br><br>L’application est composée de 2 systèmes&nbsp;qui doivent se parler. Un contrat JSON est définit entre dispatcher et aggregator. Le contrat est très explicite avec les unités. <br>Cyrille fait remarquer un problème de typo&nbsp;sur un champ : latency<strong>y</strong>_ms avec 2 lettres y</p>



<figure class="wp-block-image"><img loading="lazy" decoding="async" width="2690" height="1663" src="https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-16.png" alt="" class="wp-image-2534" srcset="https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-16.png 2690w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-16-300x185.png 300w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-16-1024x633.png 1024w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-16-768x475.png 768w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-16-1536x950.png 1536w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-16-2048x1266.png 2048w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-16-485x300.png 485w" sizes="auto, (max-width: 2690px) 100vw, 2690px" /></figure>



<p>Un renommage serait possible mais il est recommandé de positionner 2 champs pour respecter le contrat.</p>



<p><img loading="lazy" decoding="async" width="1944" height="1514" class="wp-image-2537" src="https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-17.png" srcset="https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-17.png 1944w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-17-300x234.png 300w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-17-1024x797.png 1024w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-17-768x598.png 768w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-17-1536x1196.png 1536w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-17-385x300.png 385w" sizes="auto, (max-width: 1944px) 100vw, 1944px" /> <br>Autre clé&nbsp;: <strong>on ne change pas un contrat</strong>. 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. <br><br>Cyrille rappelle les&nbsp;utiles à l’heure de l’IA&nbsp;: <br>&#8211; <strong>Architectural Decision Records</strong> (<strong>ADR)</strong>&nbsp;: template <br>&#8211; <strong>ArchUnit</strong>&nbsp;: try architecture tests</p>



<p><img loading="lazy" decoding="async" width="2679" height="1615" class="wp-image-2540" src="https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-18.png" srcset="https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-18.png 2679w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-18-300x181.png 300w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-18-1024x617.png 1024w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-18-768x463.png 768w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-18-1536x926.png 1536w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-18-2048x1235.png 2048w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-18-498x300.png 498w" sizes="auto, (max-width: 2679px) 100vw, 2679px" /> <br><br>Autres clés proposées par Cyrille pour avoir des <strong>réunions constructives</strong>. <br>Commencer par <strong>time boxer les réunions</strong>. Utiliser un tableau blanc ou numérique. <br>Alterner raisonnement individuel et raisonnement en équipe&nbsp;:</p>



<p>1. Chacun s’isole pour réfléchir de son côté au même problème <br>2. 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. <br><br><strong>Remember</strong> :</p>



<ul class="wp-block-list">
<li><strong>The system = the software + the people</strong></li>



<li><strong>Baby steps</strong>&nbsp;: on apprend progressivement, par petits pas, rapidement =&gt; réduit le risque dans un monde avec beaucoup d’incertitudes</li>



<li><strong>Rester simple</strong></li>



<li><strong>Books</strong>&nbsp;: toutes ces attitudes nécessaires à l’Architecture restent inchangées&nbsp;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.</li>
</ul>



<figure class="wp-block-image"><img loading="lazy" decoding="async" width="2684" height="1605" src="https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-19.png" alt="3 livres recommandés par Cyrille Martraire" class="wp-image-2542" srcset="https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-19.png 2684w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-19-300x179.png 300w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-19-1024x612.png 1024w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-19-768x459.png 768w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-19-1536x919.png 1536w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-19-2048x1225.png 2048w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2508-19-500x300.png 500w" sizes="auto, (max-width: 2684px) 100vw, 2684px" /></figure>
]]></content:encoded>
					
					<wfw:commentRss>https://javaetmoi.com/2025/04/cles-de-l-architecture-pour-les-devs/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Optimisez vos applications Spring Boot avec CDS et le projet Leyden</title>
		<link>https://javaetmoi.com/2025/04/optimisez-vos-applications-spring-boot-avec-cds-et-le-projet-leyden/</link>
					<comments>https://javaetmoi.com/2025/04/optimisez-vos-applications-spring-boot-avec-cds-et-le-projet-leyden/#respond</comments>
		
		<dc:creator><![CDATA[Antoine]]></dc:creator>
		<pubDate>Mon, 21 Apr 2025 12:04:33 +0000</pubDate>
				<category><![CDATA[Conférence]]></category>
		<category><![CDATA[Spring]]></category>
		<category><![CDATA[Devoxx]]></category>
		<category><![CDATA[Spring Boot]]></category>
		<category><![CDATA[Spring Framework]]></category>
		<guid isPermaLink="false">https://javaetmoi.com/?p=2482</guid>

					<description><![CDATA[Conférence : Devoxx France 2025Date : 17 avril 2025Speaker : Sébastien Deleuze (Broadcom)Format : Conférence (45 mn) / Replay Youtube Sébastien est Core Commiter sur Spring Framework. Il intervient également sur des sujets transverses au portfolio Spring&#160;: 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é &#8230; <a href="https://javaetmoi.com/2025/04/optimisez-vos-applications-spring-boot-avec-cds-et-le-projet-leyden/" class="more-link">Continuer la lecture de <span class="screen-reader-text">Optimisez vos applications Spring Boot avec CDS et le projet Leyden</span>  <span class="meta-nav">&#8594;</span></a>]]></description>
										<content:encoded><![CDATA[
<p><span style="text-decoration: underline;">Conférence</span> : <a href="https://www.devoxx.fr/">Devoxx France 2025</a><br><span style="text-decoration: underline;">Date</span> : 17 avril 2025<br><span style="text-decoration: underline;">Speaker</span> : <a href="https://seb.deleuze.fr/">Sébastien Deleuze</a> (Broadcom)<br><span style="text-decoration: underline;">Format</span> : Conférence (45 mn) / <a href="https://www.youtube.com/watch?v=4EAxhhSWgw4">Replay Youtube</a></p>



<p>Sébastien est Core Commiter sur <a href="https://spring.io/projects/spring-framework">Spring Framework</a>. Il intervient également sur des sujets transverses au portfolio Spring&nbsp;: support de Kotlin, null-safety (avec <a href="https://jspecify.dev/">JSpecify</a>) et les sujets d’optimisation. Dans ce talk, il a pour ambition de nous montrer <strong>comment améliorer l’efficacité de 80% des applications Spring</strong>, que ce soit de nouvelles applications ou des applications Legacy.</p>



<figure class="wp-block-image"><img loading="lazy" decoding="async" width="1386" height="714" src="https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2482-1.jpeg" alt="Sébastien Deleuze at Devoxx France 2025" class="wp-image-2483" srcset="https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2482-1.jpeg 1386w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2482-1-300x155.jpeg 300w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2482-1-1024x528.jpeg 1024w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2482-1-768x396.jpeg 768w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2482-1-500x258.jpeg 500w" sizes="auto, (max-width: 1386px) 100vw, 1386px" /></figure>



<p>Les raisons d’améliorer l’efficacité de nos applications sont multiples&nbsp;:</p>



<ul class="wp-block-list">
<li><strong>Baisser le cout</strong> de run des applications</li>



<li><strong>Développement durable</strong> pour diminuer la consommation d’énergie, de mémoire et de CPU</li>



<li><strong>Optimiser</strong> les applications pour les <strong>containers</strong> (sur le Cloud ou OnPremise)</li>
</ul>



<p>Pour arriver à nos fins, Sébastien nous propose 3 technologies&nbsp;:</p>



<ol class="wp-block-list">
<li><strong>CDS</strong>&nbsp;: techno relativement vieille mais qui s’est améliorée au fil des versions de Java</li>



<li><strong>AOT cache</strong>&nbsp;: 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</li>



<li><strong>AOT cache with profiling</strong>&nbsp;: technologie expérimentale et prometeuse</li>
</ol>



<span id="more-2482"></span>



<p>Ces 3 technologies nécessitent un <a id="post-2482-OLE_LINK3"></a><strong>training run</strong>. Cette «&nbsp;exécution d’entrainement de l’application&nbsp;» 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&nbsp;:</p>



<ul class="wp-block-list">
<li><strong>Temps de démarrage</strong> réduit</li>



<li><strong>Empreinte mémoire</strong> réduite</li>



<li><strong>Warmup</strong> de la JVM plus rapide</li>
</ul>



<figure class="wp-block-image"><img loading="lazy" decoding="async" width="1600" height="969" src="https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2482-2.png" alt="Training run workflow" class="wp-image-2484" srcset="https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2482-2.png 1600w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2482-2-300x182.png 300w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2482-2-1024x620.png 1024w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2482-2-768x465.png 768w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2482-2-1536x930.png 1536w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2482-2-495x300.png 495w" sizes="auto, (max-width: 1600px) 100vw, 1600px" /></figure>



<p></p>



<h2 class="wp-block-heading">1. Class Data Sharing (CDS)</h2>



<p>Disponible depuis <strong>Java 9 </strong>(2017), <strong>CDS</strong> a continué à évoluer au fil des versions de Java.</p>



<p>Par facilité (notamment pour la fonctionnalité d’extraction), un prérequis&nbsp;conseillé par Sébastien consiste à utiliser <strong>Java 17</strong> et <strong>Spring Boot 3.3 </strong>et +.</p>



<p>Pour utiliser la fonctionnalité <a href="https://docs.spring.io/spring-framework/reference/integration/cds.html">CDS</a>, une <strong>archive</strong> CDS (<strong>format .jsa</strong>) doit être créée pour le classpath de l&rsquo;application. Spring Framework fournit un mécanisme facilitant la création de cette archive. Une fois l&rsquo;archive disponible, on peut l&rsquo;utiliser via un flag de la JVM.</p>



<p>La création de l’archive CDS nécessite 2 paramètres de JVM&nbsp;:</p>



<ul class="wp-block-list">
<li><strong>-Dspring.context.exit=onRefresh</strong>&nbsp;: démarrage les beans singletons Spring non lazy puis arrête l’application.</li>



<li><strong>-XX:ArchiveClassesAtExit=spring-petclinic.jsa</strong>&nbsp;: création de l’archive CDS lorsque la JVM s’arrête.</li>
</ul>



<figure class="wp-block-image"><img loading="lazy" decoding="async" width="2991" height="1767" src="https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2482-3.png" alt="java -Dspring.context.exit=onRefresh" class="wp-image-2485" srcset="https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2482-3.png 2991w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2482-3-300x177.png 300w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2482-3-1024x605.png 1024w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2482-3-768x454.png 768w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2482-3-1536x907.png 1536w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2482-3-2048x1210.png 2048w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2482-3-500x295.png 500w" sizes="auto, (max-width: 2991px) 100vw, 2991px" /></figure>



<p>Les <strong>plugins Maven</strong> et <strong>Gradle</strong> de <strong>Spring Boot</strong> permettent de créer un <strong>JAR auto-exécutable</strong>. 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 <strong>pas efficiente</strong> avec CDS qui ne supporte pas les JAR imbriqués. La version 3.3 de Spring Boot a facilité le <a href="https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-3.3-Release-Notes#cds-support">support de CDS</a> en ajoutant une <strong>fonctionnalité d’auto-extraction du JAR</strong> via le paramètre <strong>-Djarmode=tools</strong>. Son utilisation est illustrée par la commande suivante&nbsp;:</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="java -Djarmode=tools -jar spring-petclinic.jar extract" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #88C0D0">java</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">-Djarmode=tools</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">-jar</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">spring-petclinic.jar</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">extract</span></span></code></pre></div>



<figure class="wp-block-image"><img loading="lazy" decoding="async" width="1385" height="420" src="https://javaetmoi.com/wp-content/uploads/2025/04/cds-file-layout.png" alt="CDS file layout" class="wp-image-2486" srcset="https://javaetmoi.com/wp-content/uploads/2025/04/cds-file-layout.png 1385w, https://javaetmoi.com/wp-content/uploads/2025/04/cds-file-layout-300x91.png 300w, https://javaetmoi.com/wp-content/uploads/2025/04/cds-file-layout-1024x311.png 1024w, https://javaetmoi.com/wp-content/uploads/2025/04/cds-file-layout-768x233.png 768w, https://javaetmoi.com/wp-content/uploads/2025/04/cds-file-layout-500x152.png 500w" sizes="auto, (max-width: 1385px) 100vw, 1385px" /></figure>



<p>Sébastien fait une démonstration à l’aide de l’application <a href="https://github.com/spring-projects/spring-petclinic">Spring Petclinic</a>.</p>



<p>Commande permettant d’utiliser l’archive CDS au démarrage de l’application :</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="java -XX :SharedArchiveFile=spring-petclinic.jsa – jar spring-petclinic.jar" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #88C0D0">java</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">-XX :SharedArchiveFile=spring-petclinic.jsa</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">–</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">jar</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">spring-petclinic.jar</span></span></code></pre></div>



<p>Le <strong>gain au démarrage</strong> est de <strong>19%.</strong> A noter que l’utilisation de CDS peut engendrer des effets de bord si l’on ne suit pas les <strong>bonnes pratiques</strong> suivantes :</p>



<ul class="wp-block-list">
<li>Utiliser idéalement la <strong>même JVM</strong> pour la capture du CDS et le run de l’application</li>



<li>Spécifier le <strong>classpath</strong> avec la <strong>liste complète des JAR</strong> et <strong>ne pas utiliser de wildcard *</strong></li>



<li>Le <strong>timestamp des JAR </strong>doit être <strong>préservé</strong></li>



<li>Les éventuels JAR additionnels doivent être ajoutés à la fin du classpath.</li>
</ul>



<p>L’étape de démarrage nécessaire à l’enregistrement de l’archive CDS est appelée le <strong>training run</strong>. 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 <a href="https://github.com/spring-projects/spring-lifecycle-smoke-tests/tree/main#training-run-configuration">spring-lifecycle-smoke-tests</a>&nbsp;donnant différents exemples de configuration pour Spring Data, Spring Batch, Spring Coud, Kafka, Spring Security&nbsp;…</p>



<figure class="wp-block-image"><img loading="lazy" decoding="async" width="2382" height="1466" src="https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2482-5.png" alt="Training run configuration for your database" class="wp-image-2487" srcset="https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2482-5.png 2382w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2482-5-300x185.png 300w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2482-5-1024x630.png 1024w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2482-5-768x473.png 768w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2482-5-1536x945.png 1536w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2482-5-2048x1260.png 2048w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2482-5-487x300.png 487w" sizes="auto, (max-width: 2382px) 100vw, 2382px" /></figure>



<p>Par exemple, pour éviter qu’une application Spring Data JPA ne fasse appel à la base de données, il est possible de <strong>désactiver la lecture des méta-données par Hibernate</strong>. Ne pouvant plus déterminer automatiquement le dialect, il est alors nécessaire de lui spécifier.</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="# 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" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #616E88"># Specify explicitly the dialect (here for PostgreSQL, adapt for your database)</span></span>
<span class="line"><span style="color: #88C0D0">spring.jpa.database-platform</span><span style="color: #D8DEE9FF">=org.hibernate.dialect.PostgreSQLDialect</span></span>
<span class="line"></span>
<span class="line"><span style="color: #616E88"># Disable Hibernate usage of JDBC metadata</span></span>
<span class="line"><span style="color: #88C0D0">spring.jpa.properties.hibernate.boot.allow_jdbc_metadata_access</span><span style="color: #D8DEE9FF">=</span><span style="color: #88C0D0">false</span></span>
<span class="line"></span>
<span class="line"><span style="color: #616E88"># Database initialization should typically be performed outside of Spring lifecycle</span></span>
<span class="line"><span style="color: #88C0D0">spring.jpa.hibernate.ddl-auto</span><span style="color: #D8DEE9FF">=none</span></span>
<span class="line"><span style="color: #88C0D0">spring.sql.init.mode</span><span style="color: #D8DEE9FF">=never</span></span></code></pre></div>



<p>Le <a href="https://docs.spring.io/spring-boot/maven-plugin/build-image.html#build-image">support de Buildpack</a> par les plugins Maven et Gradle de Spring Boo transforme une application Spring Boot en une image OCI (Docker). Commandes :</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="mvn spring-boot:build-image
gradle bootBuildImlage" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #88C0D0">mvn</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">spring-boot:build-image</span></span>
<span class="line"><span style="color: #88C0D0">gradle</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">bootBuildImlage</span></span></code></pre></div>



<figure class="wp-block-image"><img loading="lazy" decoding="async" width="1590" height="954" src="https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2482-6.png" alt="CDS support in Buildpacks" class="wp-image-2488" srcset="https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2482-6.png 1590w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2482-6-300x180.png 300w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2482-6-1024x614.png 1024w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2482-6-768x461.png 768w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2482-6-1536x922.png 1536w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2482-6-500x300.png 500w" sizes="auto, (max-width: 1590px) 100vw, 1590px" /></figure>



<p>Le support de CDS est prévu dans Buildpacks via l’activation du <strong>flag </strong><a href="https://github.com/paketo-buildpacks/spring-boot"><strong>BP_JVM_CDS_ENABLED</strong></a>. Exemple de configuration Maven&nbsp;:</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-highlight-color:rgba(201, 218, 248, 0.2);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="&lt;plugin&gt;
	&lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
	&lt;artifactId&gt;spring-boot-maven-plugin&lt;/artifactId&gt;
	&lt;configuration&gt;
		&lt;image&gt;
			&lt;env&gt;
				&lt;BP_JVM_CDS_ENABLED&gt;true&lt;/BP_JVM_CDS_ENABLED&gt;
			&lt;/env&gt;
		&lt;/image&gt;
	&lt;/configuration&gt;
&lt;/plugin&gt;" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #81A1C1">&lt;plugin&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">	</span><span style="color: #81A1C1">&lt;groupId&gt;</span><span style="color: #D8DEE9FF">org.springframework.boot</span><span style="color: #81A1C1">&lt;/groupId&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">	</span><span style="color: #81A1C1">&lt;artifactId&gt;</span><span style="color: #D8DEE9FF">spring-boot-maven-plugin</span><span style="color: #81A1C1">&lt;/artifactId&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">	</span><span style="color: #81A1C1">&lt;configuration&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">		</span><span style="color: #81A1C1">&lt;image&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">			</span><span style="color: #81A1C1">&lt;env&gt;</span></span>
<span class="line cbp-line-highlight"><span style="color: #D8DEE9FF">				</span><span style="color: #81A1C1">&lt;BP_JVM_CDS_ENABLED&gt;</span><span style="color: #D8DEE9FF">true</span><span style="color: #81A1C1">&lt;/BP_JVM_CDS_ENABLED&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">			</span><span style="color: #81A1C1">&lt;/env&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">		</span><span style="color: #81A1C1">&lt;/image&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">	</span><span style="color: #81A1C1">&lt;/configuration&gt;</span></span>
<span class="line"><span style="color: #81A1C1">&lt;/plugin&gt;</span></span></code></pre></div>



<p><strong>Buildpack</strong> effectue le <strong>training run</strong> et <strong>ajoute l’archive jsa dans le container</strong>.</p>



<p>Pour un effort mesuré, le <strong>temps de démarrage de Spring Petclinic</strong> est <strong>réduit de 3 secondes </strong>à 1,8 secondes.</p>



<h2 class="wp-block-heading">2. AOT Cache</h2>



<p>Successeur de CDS, <strong>AOT Cache</strong> (Ahead-Of-Time Cache) est une fonctionnalité de la JVM intégrée à <strong>Java 24</strong> qui permet d’<strong>améliorer l’efficience de nos applications Java</strong>. Permettant de diminuer les temps de démarrage, <a href="https://docs.spring.io/spring-framework/reference/core/aot.html"><strong>Spring AOT</strong></a> 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.</p>



<p>L’utilisation Spring AOT impose certaines contraintes&nbsp;comme la pré-configuration des <strong>profiles Spring</strong>. Le repo Github <a href="https://github.com/sdeleuze/demo-profile-aot">sdeuleuze/demo-profile-aot</a> montre comment activer les profils Spring dans un build Maven et Gradle.</p>



<p>Pensée dans le cadre du <strong>projet Leyden</strong>, La <a href="https://openjdk.org/jeps/483">JEP 483 Ahead-of-Time Class Loading &amp; Linking&nbsp;</a>est disponible dans Java 24 et des améliorations prévues dans les versions suivantes&nbsp;de Java. L’utiliser est un bon investissement.</p>



<figure class="wp-block-image"><img loading="lazy" decoding="async" width="2798" height="1666" src="https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2482-7.png" alt="AOT Cache and Spring AOT are different but they combine well" class="wp-image-2489" srcset="https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2482-7.png 2798w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2482-7-300x179.png 300w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2482-7-1024x610.png 1024w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2482-7-768x457.png 768w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2482-7-1536x915.png 1536w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2482-7-2048x1219.png 2048w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2482-7-500x298.png 500w" sizes="auto, (max-width: 2798px) 100vw, 2798px" /></figure>



<p>La création du cache AOT nécessite 2 étapes :</p>



<p>La <strong>1<sup>ière</sup> étape</strong> consiste à générer le <strong>fichier .aotconf</strong> à l’aide de l’option <strong>-XX:AOTMode=record</strong> et la ligne de commande suivante&nbsp;:</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="java -XX:AOTMode=record -XX:AOTConfiguration=spring-petclinic.aotconf \
-jar spring-petclinic.jar" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #88C0D0">java</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">-XX:AOTMode=record</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">-XX:AOTConfiguration=spring-petclinic.aotconf</span><span style="color: #D8DEE9FF"> </span><span style="color: #EBCB8B">\</span></span>
<span class="line"><span style="color: #D8DEE9FF">-jar </span><span style="color: #A3BE8C">spring-petclinic.jar</span></span></code></pre></div>



<p>Au préalable, comme avec CDS, le JAR auto-exécutable aura été extrait à l’aide de <strong>-Djarmode=tools</strong>.</p>



<p>La 2<strong><sup>ième</sup> étape</strong> consiste à générer un <strong>fichier .aot</strong> avec l’option <strong>-XX:AOTMode=create </strong>et la ligne de commande suivante&nbsp;:</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="java -XX:AOTMode=create -XX:AOTConfiguration=spring-petclinic.aotconf -XX:AOTCache=spring-petclinic.aot -jar spring-petclinic.jar" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #88C0D0">java</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">-XX:AOTMode=create</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">-XX:AOTConfiguration=spring-petclinic.aotconf</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">-XX:AOTCache=spring-petclinic.aot</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">-jar</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">spring-petclinic.jar</span></span></code></pre></div>



<p>Le t<strong>emps de démarrage&nbsp;de Spring Petclinic</strong> <strong>descend à 1,3 secondes</strong>&nbsp;:</p>



<figure class="wp-block-image"><img loading="lazy" decoding="async" width="2811" height="1652" src="https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2482-8.png" alt="Spring Petclinic startup time (seconds)" class="wp-image-2490" srcset="https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2482-8.png 2811w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2482-8-300x176.png 300w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2482-8-1024x602.png 1024w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2482-8-768x451.png 768w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2482-8-1536x903.png 1536w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2482-8-2048x1204.png 2048w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2482-8-500x294.png 500w" sizes="auto, (max-width: 2811px) 100vw, 2811px" /></figure>



<h2 class="wp-block-heading">3. AOT Cache with code compilation and Spring AOT</h2>



<p>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&nbsp;:</p>



<ol class="wp-block-list">
<li><a href="Ahead-of-Time%20Methode%20Profiling"><strong>Ahead-of-Time Method Profiling</strong>&nbsp;</a>: amelioration du temps de chauffe</li>



<li><a href="https://openjdk.org/jeps/8350022"><strong>Ahead-of-time Command Line Ergonomics</strong></a>&nbsp;: une seule étape au lieu de 2 étapes pour créer les fichiers .aotconf et .aot</li>



<li><a href="https://openjdk.org/jeps/8335368"><strong>Ahead-of-time Code Compilation</strong></a>&nbsp;: récupération du code natif du JIT pour le réutiliser en prod</li>
</ol>



<p>Sébastien nous fait une démo live à partir d’une <strong>version du JDK compilée en local avec les dernières fonctionnalités du projet Leyden</strong>. Cette fois-ci, le workflow va être un peu différent&nbsp;: on ne fait pas de stop après le démarrage. On laisse tourner l’application. Sébastien utilise l’outil <a href="https://github.com/hatoo/oha"><strong>oha</strong></a> 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&nbsp;:</p>



<figure class="wp-block-image"><img loading="lazy" decoding="async" width="2359" height="1459" src="https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2482-9.png" alt="Spring Petclinic startup time (secondes)" class="wp-image-2491" srcset="https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2482-9.png 2359w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2482-9-300x186.png 300w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2482-9-1024x633.png 1024w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2482-9-768x475.png 768w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2482-9-1536x950.png 1536w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2482-9-2048x1267.png 2048w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2482-9-485x300.png 485w" sizes="auto, (max-width: 2359px) 100vw, 2359px" /></figure>



<figure class="wp-block-image"><img loading="lazy" decoding="async" width="2811" height="1746" src="https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2482-10.png" alt="Warmup with and without AOT cache" class="wp-image-2492" srcset="https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2482-10.png 2811w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2482-10-300x186.png 300w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2482-10-1024x636.png 1024w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2482-10-768x477.png 768w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2482-10-1536x954.png 1536w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2482-10-2048x1272.png 2048w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2482-10-483x300.png 483w" sizes="auto, (max-width: 2811px) 100vw, 2811px" /></figure>



<ul class="wp-block-list">
<li>Courbe <mark style="background-color:rgba(0, 0, 0, 0)" class="has-inline-color has-vivid-cyan-blue-color">bleue&nbsp;</mark>: JVM classique qui prend son temps pour le warmup</li>



<li>Courbe <mark style="background-color:rgba(0, 0, 0, 0)" class="has-inline-color has-vivid-red-color">rouge</mark>&nbsp;: AOT profiling. Warmup assez rapide.</li>



<li>Courbe <mark style="background-color:rgba(0, 0, 0, 0)" class="has-inline-color has-luminous-vivid-amber-color">jaune</mark>&nbsp;: warmup passé de 30 à 7 secondes. Fonctionne sur des applications Legacy</li>
</ul>



<h2 class="wp-block-heading">Récapitulatif</h2>



<p>Le tableau récapitulatif ci-dessous compare 3 technologies d’optimisation d’une application Java&nbsp;:</p>



<figure class="wp-block-image"><img loading="lazy" decoding="async" width="2724" height="1380" src="https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2482-11.png" alt="Comparing GraalVM, Project CRaC and AOT cache" class="wp-image-2493" srcset="https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2482-11.png 2724w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2482-11-300x152.png 300w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2482-11-1024x519.png 1024w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2482-11-768x389.png 768w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2482-11-1536x778.png 1536w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2482-11-2048x1038.png 2048w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2482-11-500x253.png 500w" sizes="auto, (max-width: 2724px) 100vw, 2724px" /></figure>



<ol class="wp-block-list">
<li><a href="https://www.graalvm.org/"><strong>GraalVM</strong>&nbsp;</a>: la version gratuite de GraalVM vient avec le <a href="https://www.graalvm.org/latest/reference-manual/native-image/optimizations-and-performance/MemoryManagement/">Garbage Collector serial</a> 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&nbsp;: 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&nbsp;: 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.</li>



<li><strong>JVM with </strong><a href="https://docs.azul.com/core/crac/crac-introduction"><strong>Project CRaC</strong>&nbsp;</a>: Sébastien est assez sévère sur cette technologie qui comporte 2 énorme défauts&nbsp;: Linux uniquement mais surtout à cause du cycle de vie&nbsp;nécessitant de restaurer l’application (handles filesystems, sockets réseaux …). Quid du support des autres librairies&nbsp;? L’API de ces librairies peut bloquer. Plus encore&nbsp;: 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.</li>



<li><strong>JVM with AOT cache</strong>&nbsp;: 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.</li>
</ol>



<p>Le projet Leyden et la JVM continue à évoluer. Preuve en est la Pull Request <a href="https://github.com/openjdk/leyden/pull/44">#44</a> datant de février 2025 du projet Leyden&nbsp;: <strong>8350488: [leyden] Experimental AOT-only mode</strong></p>



<p>D’autres améliorations concernant les applications Legacy seront annoncées à la <a href="https://2025.springio.net/">conférence Spring IO</a> qui aura lieu du 21 au 23 mai 2025 à Barcelone.</p>



<p>Enfin, pour ses benchmarks, Sébastien passe une annonce&nbsp;: il recherche de plus grosses applications Open Source basées sur Spring Boot et plus réalistes que l&rsquo;application démo Petclinic.</p>



<p>Si vous voulez creuser le sujet, je vous recommande la lecture de l’article intitulé <a href="https://spring.io/blog/2024/08/29/spring-boot-cds-support-and-project-leyden-anticipation">Spring Boot CDS support and Project Leyden anticipation</a> qu’a publié Sébastien le 29 aout 2024.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://javaetmoi.com/2025/04/optimisez-vos-applications-spring-boot-avec-cds-et-le-projet-leyden/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Spring Petclinic sous extensions Quarkus</title>
		<link>https://javaetmoi.com/2025/04/spring-petclinic-sous-extensions-quarkus/</link>
					<comments>https://javaetmoi.com/2025/04/spring-petclinic-sous-extensions-quarkus/#respond</comments>
		
		<dc:creator><![CDATA[Antoine]]></dc:creator>
		<pubDate>Sun, 13 Apr 2025 16:55:14 +0000</pubDate>
				<category><![CDATA[Retour d'expérience]]></category>
		<category><![CDATA[Spring]]></category>
		<category><![CDATA[Quarkus]]></category>
		<category><![CDATA[Spring Boot]]></category>
		<guid isPermaLink="false">https://javaetmoi.com/?p=2443</guid>

					<description><![CDATA[Spring et Quarkus dans le même repository Git, ou presque. Cela vous intrigue ? Figurez-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 &#8230; <a href="https://javaetmoi.com/2025/04/spring-petclinic-sous-extensions-quarkus/" class="more-link">Continuer la lecture de <span class="screen-reader-text">Spring Petclinic sous extensions Quarkus</span>  <span class="meta-nav">&#8594;</span></a>]]></description>
										<content:encoded><![CDATA[
<p>Spring et Quarkus dans le même repository Git, ou presque. Cela vous intrigue ? <br>Figurez-vous qu’il y’a quelques mois, la lecture du très bon <strong>livre <a href="https://agoncal.teachable.com/p/ebook-understanding-quarkus">Understanding Quarkus 2.x</a></strong> 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 <strong>iso-fonctionnelle</strong>. <br>A travers leur repo <a href="https://github.com/redhat-developer-demos/quarkus-petclinic">quarkus-petclinic</a>, 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&rsquo;ai donc travaillé sur un nouveau fork que je suis fier de vous présenter : <a href="https://github.com/arey/quarkus-spring-petclinic"><strong>quarkus-spring-petclinic</strong></a>. Ajouté à la communauté Spring Petclinic, ce fork a un double objectif :</p>



<ol class="wp-block-list">
<li>Montrer comment <strong>migrer une application Spring Boot 3.4 vers Quarkus 3.21</strong>, avec le minium d&rsquo;effort et en modifiant le moins de code possible</li>



<li>Utiliser les <strong>extensions Spring</strong> proposées par <strong>Quarkus</strong> pour garder un lien avec le monde Spring tout en soulignant l&rsquo;effort de l&rsquo;équipe Quarkus pour supporter Spring, un framework incontournable de l&rsquo;écosystème Java</li>
</ol>



<p>Les <strong>extensions Spring pour Quarkus</strong> utilisées sont au nombre de quatre&nbsp;: <strong>Spring DI</strong>, <strong>Spring Web</strong>, <strong>Spring Data JPA</strong> et <strong>Spring Cache</strong>.<br>Le changement majeur aura été de porter le templating des pages HTML de <strong>Thymeleaf</strong> vers <strong>Qute</strong>.</p>



<p>Dé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&nbsp;: <a href="https://github.com/spring-petclinic/quarkus-spring-petclinic/issues">issue</a> et <a href="https://github.com/spring-petclinic/quarkus-spring-petclinic/pulls">Pull Request </a>sont les bienvenues.</p>



<figure class="wp-block-image"><img loading="lazy" decoding="async" width="1021" height="580" src="https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2443-1.png" alt="" class="wp-image-2444" srcset="https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2443-1.png 1021w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2443-1-300x170.png 300w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2443-1-768x436.png 768w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2443-1-500x284.png 500w" sizes="auto, (max-width: 1021px) 100vw, 1021px" /></figure>



<p>Le <a href="https://github.com/spring-petclinic/quarkus-spring-petclinic/compare/spring-boot-version...v3.21.0">différenciel complet</a> entre la version Spring Boot et la version Quarkus de Petclinic peut-être visualisé sur Github.</p>



<span id="more-2443"></span>



<h2 class="wp-block-heading">Configuration du build Maven et Gradle</h2>



<p>Spring Petclinic supporte les 2 principales plateformes de <strong>build</strong> de l’ecosystème Java, à savoir <strong>Maven</strong> et <strong>Gradle</strong>. Pour chaque dépendance Spring Boot, le tableau ci-dessous dresse l’équivalent utilisé sur la version Quarkus :</p>



<figure class="wp-block-table is-style-stripes"><table class="has-fixed-layout"><tbody><tr><td><p><strong>Dépendances Spring Boot</strong></p></td><td><br><p><strong>Dépendances Quarkus correspondantes</strong></p></td><td><br><p><strong>Commentaire</strong></p></td></tr><tr><td><p>spring-boot-starter-actuator</p></td><td><p>quarkus-smallrye-health</p></td><td><p><a href="https://github.com/smallrye/smallrye-health/">SmallRye Health</a>&nbsp;est une implementation de la&nbsp;<a href="https://github.com/eclipse/microprofile-health/">MicroProfile Health</a>.</p></td></tr><tr><td><p>spring-boot-starter-cache</p><p>cache-api <br>caffeine</p></td><td><p>quarkus-spring-cache</p></td><td><p>Extension Spring Cache pour Quarkus <br>Quarkus utilise par défaut Caffeine.</p></td></tr><tr><td><p>spring-boot-starter-data-jpa</p></td><td><p>quarkus-spring-data-jpa <br>quarkus-narayana-jta</p></td><td><p>Extension Spring Data JPA pour Quarkus <br>Quarkus s’appuie sur Hibernate ORM et Panache. Le gestionnaire de transactions JTA est à ajouter manuellement.</p></td></tr><tr><td><p>spring-boot-starter-web</p></td><td><p>quarkus-spring-web <br>quarkus-rest-jackson</p></td><td><p>L’extension Spring Web pour Quarkus requière quarkus-rest-jackson ou quarkus-resteasy-jackson.</p></td></tr><tr><td><p>spring-boot-starter-validation</p></td><td><p>quarkus-hibernate-validator</p></td><td><p>Les versions Spring Boot et Quarkus de Petclinic s’appuient toutes 2 sur Hibernate Validator.</p></td></tr><tr><td><p>spring-boot-starter-thymeleaf</p></td><td><p>quarkus-qute</p></td><td><p>Pas de correspondance directe car Quarkus utilise Qute pour le templating.</p></td></tr><tr><td><p>spring-boot-starter-tes<span style="font-family: inherit; font-size: inherit; font-weight: inherit; background-color: initial; color: initial;">t</span></p></td><td><p>quarkus-junit5 <br>quarkus-junit5-mockito <br>quarkus-test-h2 <br>rest-assured</p></td><td><p>Rest Assured remplace MockMvc pour tester les contrôleurs REST.</p></td></tr><tr><td><p>h2</p></td><td><p>quarkus-jdbc-h2</p></td><td>&nbsp;</td></tr><tr><td><p>mysql-connector-j</p></td><td><p>quarkus-jdbc-mysql</p></td><td><p>En plus des drivers JDBC, tire le pool de connexions Agroal qui remplace HikariCP.</p></td></tr><tr><td><p>postgresql</p></td><td><p>quarkus-jdbc-postgresql</p></td><td><p>« </p></td></tr><tr><td><p>webjars-locator-lite</p></td><td><p>quarkus-web-dependency-locator</p></td><td><p>Utiles pour les webjars.</p></td></tr><tr><td><p>spring-boot-devtools</p></td><td>&nbsp;</td><td><p>Pas de correspondance directe. Quarkus inclue le mode dev par défaut.</p></td></tr><tr><td><p>spring-boot-docker-compose</p></td><td></td><td><p>Utilisé par les tests d’intégration reposant sur Testcontainers. <br>Pas d’équivalent côté Quarkus qui sait nativement démarrer des conteneurs Docker lorsqu’aucune configuration n’est précisée.</p></td></tr><tr><td><p>(spring-core et spring-beans)</p></td><td><p>quarkus-spring-di</p></td><td><p>Support des annotations Spring d’injection de dépendance, mais en tirant ArC, une implémentation light de CDI spécifique à Quarkus.</p></td></tr><tr><td>&nbsp;</td><td><p>quarkus-container-image-docker</p></td><td><p>Création d’images Docker multi-plateformes.</p></td></tr></tbody></table></figure>



<p><br>Les dépendances vers les 2 <strong>webjars</strong> <strong>bootstrap</strong> et <strong>font-awesome</strong> sont restés inchangées.<br>La migration a été faite avec une approche top-down : on part de la couche persistance pour remonter vers la couche de présentation.</p>



<h2 class="wp-block-heading">Adaptation de la couche Spring Data JPA</h2>



<p>L’<a href="https://quarkus.io/guides/spring-data-jpa">extension Spring Data JPA</a> pour Quarkus présente l’avantage de pouvoir conserver les <strong>conventions de nommage des interfaces des repository Spring Data JPA</strong>. Sous le capot, l’implémentation est générée à l’aide de <strong><a href="https://quarkus.io/guides/hibernate-orm-panache">Panache</a></strong>. Les repository migrés peuvent continuer à implémenter les interfaces <strong>JpaRepository</strong> et <strong>ListCrudRepository</strong>, à utiliser les interfaces Spring Data <strong>Page</strong> et <strong>Pageable</strong> pour la pagination.</p>



<p>Ce 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&nbsp;: déclarer sur l’interface OwnerRepository la méthode findPetTypes manipulant des entités JPA de type PetType et non de type Owner.<br>L’erreur suivante était générée pendant le build&nbsp;:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<pre class="wp-block-preformatted">Query annotations may only use interfaces to map results to non-entity types. Offending query string is "SELECT ptype FROM PetType ptype ORDER BY ptype.name" on method findPetTypes of Repository org.springframework.samples.petclinic.owner.OwnerRepository</pre>



<p>Les 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 :</p>
</blockquote>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><textarea class="code-block-pro-copy-button-textarea" aria-hidden="true" readonly>&lt;dependency> 
  &lt;groupId>io.quarkus&lt;/groupId>
  &lt;artifactId>quarkus-spring-data-jpa-deployment&lt;/artifactId>
&lt;/dependency></textarea><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #81A1C1">&lt;dependency&gt;</span><span style="color: #D8DEE9FF"> </span></span>
<span class="line"><span style="color: #D8DEE9FF">  </span><span style="color: #81A1C1">&lt;groupId&gt;</span><span style="color: #D8DEE9FF">io.quarkus</span><span style="color: #81A1C1">&lt;/groupId&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">  </span><span style="color: #81A1C1">&lt;artifactId&gt;</span><span style="color: #D8DEE9FF">quarkus-spring-data-jpa-deployment</span><span style="color: #81A1C1">&lt;/artifactId&gt;</span></span>
<span class="line"><span style="color: #81A1C1">&lt;/dependency&gt;</span></span></code></pre></div>



<p>Le moyen de contournement a consisté tout simplement à découper en deux l’interface <a href="https://github.com/spring-petclinic/quarkus-spring-petclinic/blame/v3.21.0/src/main/java/org/springframework/samples/petclinic/owner/OwnerRepository.java">OwnerRepository</a>. L’interface <a href="https://github.com/spring-petclinic/quarkus-spring-petclinic/blob/v3.21.0/src/main/java/org/springframework/samples/petclinic/owner/PetTypeRepository.java">PetTypeRepository</a> a été ajoutée et a pour responsabilité l&rsquo;accès aux PetType. On a ainsi un meilleur découplage.</p>



<p><span style="text-decoration: underline;">Second cas dysfonctionnant sous Quarkus&nbsp;</span>:</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><textarea class="code-block-pro-copy-button-textarea" aria-hidden="true" readonly>public interface VetRepository extends Repository&lt;Vet, Integer> {
	Collection&lt;Vet> findAll();
}</textarea><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #81A1C1">public</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">interface</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">VetRepository</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">extends</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB; font-weight: bold">Repository</span><span style="color: #ECEFF4">&lt;</span><span style="color: #8FBCBB">Vet</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">Integer</span><span style="color: #ECEFF4">&gt;</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">	</span><span style="color: #8FBCBB">Collection</span><span style="color: #ECEFF4">&lt;</span><span style="color: #8FBCBB">Vet</span><span style="color: #ECEFF4">&gt;</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">findAll</span><span style="color: #ECEFF4">()</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #ECEFF4">}</span></span></code></pre></div>



<p><span style="text-decoration: underline;">Quarkus génère l’exception suivante</span>&nbsp;:</p>



<pre class="wp-block-preformatted">Caused by: io.quarkus.spring.data.deployment.UnableToParseMethodException: Method 'findAll' of repository 'org.springframework.samples.petclinic.vet.VetRepository' cannot be parsed as there is no proper 'By' clause in the name.</pre>



<p>La classe <a href="https://github.com/quarkusio/quarkus/blob/main/extensions/spring-data-jpa/deployment/src/main/java/io/quarkus/spring/data/deployment/MethodNameParser.java"><strong>MethodNameParser</strong></a><strong> ne supporte pas</strong> le type de retour <strong>Collection</strong>. Triviale, la correction a consisté à le changer en <strong>List</strong>.</p>



<p>Dernier changement mineur&nbsp;apporté à la couche de persistance : l’exception non checkée <strong>DataAccessException</strong> n’est pas supportée par Quarkus. Elle a donc été retirée de l’interface des méthodes des Repository.</p>



<p>Une fois migrée, l’interface <a href="https://github.com/spring-petclinic/quarkus-spring-petclinic/blob/v3.21.0/src/main/java/org/springframework/samples/petclinic/owner/OwnerRepository.java">OwnerRepository</a> n’a aucune adhérence à Quarkus ou Panache. Elle conserve ses <strong>imports</strong> sur les classes de <strong>Spring Data Commons</strong> et <strong>Spring Data JPA</strong>&nbsp;:</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><textarea class="code-block-pro-copy-button-textarea" aria-hidden="true" readonly>package 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&lt;Owner, Integer> {

	Page&lt;Owner> findByLastNameStartingWith(String lastName, Pageable pageable);

	Optional&lt;Owner> findById(@Nonnull Integer id);

	Page&lt;Owner> findAll(Pageable pageable);</textarea><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #81A1C1">package</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">org</span><span style="color: #ECEFF4">.</span><span style="color: #8FBCBB">springframework</span><span style="color: #ECEFF4">.</span><span style="color: #8FBCBB">samples</span><span style="color: #ECEFF4">.</span><span style="color: #8FBCBB">petclinic</span><span style="color: #ECEFF4">.</span><span style="color: #8FBCBB">owner</span><span style="color: #81A1C1">;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #81A1C1">import</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">java</span><span style="color: #ECEFF4">.</span><span style="color: #8FBCBB">util</span><span style="color: #ECEFF4">.</span><span style="color: #8FBCBB">Optional</span><span style="color: #81A1C1">;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #81A1C1">import</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">jakarta</span><span style="color: #ECEFF4">.</span><span style="color: #8FBCBB">annotation</span><span style="color: #ECEFF4">.</span><span style="color: #8FBCBB">Nonnull</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #81A1C1">import</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">org</span><span style="color: #ECEFF4">.</span><span style="color: #8FBCBB">springframework</span><span style="color: #ECEFF4">.</span><span style="color: #8FBCBB">data</span><span style="color: #ECEFF4">.</span><span style="color: #8FBCBB">domain</span><span style="color: #ECEFF4">.</span><span style="color: #8FBCBB">Page</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #81A1C1">import</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">org</span><span style="color: #ECEFF4">.</span><span style="color: #8FBCBB">springframework</span><span style="color: #ECEFF4">.</span><span style="color: #8FBCBB">data</span><span style="color: #ECEFF4">.</span><span style="color: #8FBCBB">domain</span><span style="color: #ECEFF4">.</span><span style="color: #8FBCBB">Pageable</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #81A1C1">import</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">org</span><span style="color: #ECEFF4">.</span><span style="color: #8FBCBB">springframework</span><span style="color: #ECEFF4">.</span><span style="color: #8FBCBB">data</span><span style="color: #ECEFF4">.</span><span style="color: #8FBCBB">jpa</span><span style="color: #ECEFF4">.</span><span style="color: #8FBCBB">repository</span><span style="color: #ECEFF4">.</span><span style="color: #8FBCBB">JpaRepository</span><span style="color: #81A1C1">;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #81A1C1">public</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">interface</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">OwnerRepository</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">extends</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB; font-weight: bold">JpaRepository</span><span style="color: #ECEFF4">&lt;</span><span style="color: #8FBCBB">Owner</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">Integer</span><span style="color: #ECEFF4">&gt;</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D8DEE9FF">	</span><span style="color: #8FBCBB">Page</span><span style="color: #ECEFF4">&lt;</span><span style="color: #8FBCBB">Owner</span><span style="color: #ECEFF4">&gt;</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">findByLastNameStartingWith</span><span style="color: #ECEFF4">(</span><span style="color: #8FBCBB">String</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">lastName</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">Pageable</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">pageable</span><span style="color: #ECEFF4">)</span><span style="color: #81A1C1">;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D8DEE9FF">	</span><span style="color: #8FBCBB">Optional</span><span style="color: #ECEFF4">&lt;</span><span style="color: #8FBCBB">Owner</span><span style="color: #ECEFF4">&gt;</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">findById</span><span style="color: #ECEFF4">(@</span><span style="color: #D08770">Nonnull</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">Integer</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">id</span><span style="color: #ECEFF4">)</span><span style="color: #81A1C1">;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D8DEE9FF">	</span><span style="color: #8FBCBB">Page</span><span style="color: #ECEFF4">&lt;</span><span style="color: #8FBCBB">Owner</span><span style="color: #ECEFF4">&gt;</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">findAll</span><span style="color: #ECEFF4">(</span><span style="color: #8FBCBB">Pageable</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">pageable</span><span style="color: #ECEFF4">)</span><span style="color: #81A1C1">;</span></span></code></pre></div>



<h2 class="wp-block-heading">Adaptation des scripts SQL</h2>



<p>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. <br>L’exécution du script <a href="https://github.com/spring-petclinic/quarkus-spring-petclinic/blob/v3.21.0/src/main/resources/db/h2/data.sql">data.sql</a> échouait avec l’erreur suivante&nbsp;:</p>



<pre class="wp-block-preformatted">Caused by: org.h2.jdbc.JdbcSQLIntegrityConstraintViolationException: Intégrité référentielle violation de contrainte: "FK35UIBOYRPFN1BNDRR5JORCJ0M: PUBLIC.VET_SPECIALTIES FOREIGN KEY(SPECIALTY_ID) REFERENCES PUBLIC.SPECIALTIES(ID) (4)"<br>Referential integrity constraint violation: "FK35UIBOYRPFN1BNDRR5JORCJ0M: PUBLIC.VET_SPECIALTIES FOREIGN KEY(SPECIALTY_ID) REFERENCES PUBLIC.SPECIALTIES(ID) (4)"; SQL statement:<br>INSERT INTO vet_specialties VALUES (4, 2) [23506-230]</pre>



<p>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 <a href="https://github.com/spring-petclinic/quarkus-spring-petclinic/blob/v3.21.0/src/main/resources/db/h2/schema.sql">schema.sql</a>. 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. <a href="https://github.com/quarkusio/quarkus/discussions/30193">Je ne suis apparemment pas le seul</a>. Si vous avez une idée, vous pouvez contribuer à l’<a href="https://github.com/spring-petclinic/quarkus-spring-petclinic/issues/8">issue #8</a>.</p>



<p>En 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&nbsp;:</p>



<figure class="wp-block-image"><img loading="lazy" decoding="async" width="590" height="234" src="https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2443-2.png" alt="" class="wp-image-2445" srcset="https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2443-2.png 590w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2443-2-300x119.png 300w, https://javaetmoi.com/wp-content/uploads/2025/04/word-image-2443-2-500x198.png 500w" sizes="auto, (max-width: 590px) 100vw, 590px" /></figure>



<h2 class="wp-block-heading">Portage des tests AssertJ vers Hamcrest </h2>



<p>Pour les tests unitaires, Quarkus recommande l’utilisation de <strong>JUnit 5</strong> déjà utilisé sur Spring Petclinic. Les assertions de JUnit sont limitées. Là où Spring Petclinic utilise la librairie <a href="https://assertj.github.io/doc/">AssertJ</a>, Quarkus préconise l’utilisation d’<a href="https://hamcrest.org/JavaHamcrest/">H<strong>amcrest</strong></a>. D’après l’<a href="https://github.com/quarkusio/quarkus/issues/38689">issue #38689</a> “Include AssertJ with Quarkus releases”, le support d’AssertJ dans Quarlus ne semble pas planifié. <br><br>Migrer des assertions AssertJ vers les matchers Hamcrest peut être facilitée par la recette Open Rewrite <a href="https://docs.openrewrite.org/recipes/java/testing/hamcrest/migratehamcresttoassertj">MigrateHamcrestToAssertJ</a>. 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. <br><br>Exemple avec la méthode shouldFindSingleOwnerWithPet() extrait de la classe <a href="https://github.com/spring-petclinic/quarkus-spring-petclinic/blob/v3.21.0/src/test/java/org/springframework/samples/petclinic/service/ClinicServiceTests.java">ClinicServiceTests&nbsp;</a>:</p>



<p><span style="text-decoration: underline;">Avant migration sous AssertJ</span>&nbsp;:</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><textarea class="code-block-pro-copy-button-textarea" aria-hidden="true" readonly>@Test
void shouldFindSingleOwnerWithPet() {
	Optional&lt;Owner> optionalOwner = this.owners.findById(1);
	assertThat(optionalOwner).isPresent();
	Owner owner = optionalOwner.get();
	assertThat(owner.getLastName()).startsWith(« Franklin »);
	assertThat(owner.getPets()).hasSize(1);
	assertThat(owner.getPets().get(0).getType()).isNotNull();
	assertThat(owner.getPets().get(0).getType().getName()).isEqualTo(« cat »);
}</textarea><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #ECEFF4">@</span><span style="color: #D08770">Test</span></span>
<span class="line"><span style="color: #81A1C1">void</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">shouldFindSingleOwnerWithPet</span><span style="color: #ECEFF4">()</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">	</span><span style="color: #8FBCBB">Optional</span><span style="color: #ECEFF4">&lt;</span><span style="color: #8FBCBB">Owner</span><span style="color: #ECEFF4">&gt;</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">optionalOwner</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">this</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">owners</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">findById</span><span style="color: #ECEFF4">(</span><span style="color: #B48EAD">1</span><span style="color: #ECEFF4">)</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">	</span><span style="color: #88C0D0">assertThat</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">optionalOwner</span><span style="color: #ECEFF4">).</span><span style="color: #88C0D0">isPresent</span><span style="color: #ECEFF4">()</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">	</span><span style="color: #8FBCBB">Owner</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">owner</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">optionalOwner</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">get</span><span style="color: #ECEFF4">()</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">	</span><span style="color: #88C0D0">assertThat</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9">owner</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">getLastName</span><span style="color: #ECEFF4">()).</span><span style="color: #88C0D0">startsWith</span><span style="color: #ECEFF4">(</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">Franklin</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">)</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">	</span><span style="color: #88C0D0">assertThat</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9">owner</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">getPets</span><span style="color: #ECEFF4">()).</span><span style="color: #88C0D0">hasSize</span><span style="color: #ECEFF4">(</span><span style="color: #B48EAD">1</span><span style="color: #ECEFF4">)</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">	</span><span style="color: #88C0D0">assertThat</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9">owner</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">getPets</span><span style="color: #ECEFF4">().</span><span style="color: #88C0D0">get</span><span style="color: #ECEFF4">(</span><span style="color: #B48EAD">0</span><span style="color: #ECEFF4">).</span><span style="color: #88C0D0">getType</span><span style="color: #ECEFF4">()).</span><span style="color: #88C0D0">isNotNull</span><span style="color: #ECEFF4">()</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">	</span><span style="color: #88C0D0">assertThat</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9">owner</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">getPets</span><span style="color: #ECEFF4">().</span><span style="color: #88C0D0">get</span><span style="color: #ECEFF4">(</span><span style="color: #B48EAD">0</span><span style="color: #ECEFF4">).</span><span style="color: #88C0D0">getType</span><span style="color: #ECEFF4">().</span><span style="color: #88C0D0">getName</span><span style="color: #ECEFF4">()).</span><span style="color: #88C0D0">isEqualTo</span><span style="color: #ECEFF4">(</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">cat</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">)</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #ECEFF4">}</span></span></code></pre></div>



<p><span style="text-decoration: underline;">Après migration sous Hamcrest</span> :</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><textarea class="code-block-pro-copy-button-textarea" aria-hidden="true" readonly>@Test
void shouldFindSingleOwnerWithPet() {
	Optional&lt;Owner> optionalOwner = this.owners.findById(1);
	assertThat(optionalOwner.isPresent(), is(true));
	Owner owner = optionalOwner.get();
	assertThat(owner.getLastName(), startsWith(« Franklin »));
	assertThat(owner.getPets(), hasSize(1));
	assertThat(owner.getPets().get(0).getType(), notNullValue());
	assertThat(owner.getPets().get(0).getType().getName(), is(equalTo(« cat »)));
}</textarea><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #ECEFF4">@</span><span style="color: #D08770">Test</span></span>
<span class="line"><span style="color: #81A1C1">void</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">shouldFindSingleOwnerWithPet</span><span style="color: #ECEFF4">()</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">	</span><span style="color: #8FBCBB">Optional</span><span style="color: #ECEFF4">&lt;</span><span style="color: #8FBCBB">Owner</span><span style="color: #ECEFF4">&gt;</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">optionalOwner</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">this</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">owners</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">findById</span><span style="color: #ECEFF4">(</span><span style="color: #B48EAD">1</span><span style="color: #ECEFF4">)</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">	</span><span style="color: #88C0D0">assertThat</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9">optionalOwner</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">isPresent</span><span style="color: #ECEFF4">(),</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">is</span><span style="color: #ECEFF4">(</span><span style="color: #81A1C1">true</span><span style="color: #ECEFF4">))</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">	</span><span style="color: #8FBCBB">Owner</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">owner</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">optionalOwner</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">get</span><span style="color: #ECEFF4">()</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">	</span><span style="color: #88C0D0">assertThat</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9">owner</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">getLastName</span><span style="color: #ECEFF4">(),</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">startsWith</span><span style="color: #ECEFF4">(</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">Franklin</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">))</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">	</span><span style="color: #88C0D0">assertThat</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9">owner</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">getPets</span><span style="color: #ECEFF4">(),</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">hasSize</span><span style="color: #ECEFF4">(</span><span style="color: #B48EAD">1</span><span style="color: #ECEFF4">))</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">	</span><span style="color: #88C0D0">assertThat</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9">owner</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">getPets</span><span style="color: #ECEFF4">().</span><span style="color: #88C0D0">get</span><span style="color: #ECEFF4">(</span><span style="color: #B48EAD">0</span><span style="color: #ECEFF4">).</span><span style="color: #88C0D0">getType</span><span style="color: #ECEFF4">(),</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">notNullValue</span><span style="color: #ECEFF4">())</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">	</span><span style="color: #88C0D0">assertThat</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9">owner</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">getPets</span><span style="color: #ECEFF4">().</span><span style="color: #88C0D0">get</span><span style="color: #ECEFF4">(</span><span style="color: #B48EAD">0</span><span style="color: #ECEFF4">).</span><span style="color: #88C0D0">getType</span><span style="color: #ECEFF4">().</span><span style="color: #88C0D0">getName</span><span style="color: #ECEFF4">(),</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">is</span><span style="color: #ECEFF4">(</span><span style="color: #88C0D0">equalTo</span><span style="color: #ECEFF4">(</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">cat</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">)))</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #ECEFF4">}</span></span></code></pre></div>



<h2 class="wp-block-heading">Passer à l’annotation @TestTransaction</h2>



<p>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 <strong>io.quarkus.test.TestTransaction</strong> 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.</p>



<p>Exemple avec la méthode <em>shouldInsertOwner()</em> extrait de la classe <a href="https://github.com/spring-petclinic/quarkus-spring-petclinic/blob/v3.21.0/src/test/java/org/springframework/samples/petclinic/service/ClinicServiceTests.java">ClinicServiceTests&nbsp;</a>:</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><textarea class="code-block-pro-copy-button-textarea" aria-hidden="true" readonly>@Test
@TestTransaction
void shouldInsertOwner() {
	Page&lt;Owner> owners = this.owners.findByLastNameStartingWith(« Schultz », pageable);
	int found = (int) owners.getTotalElements();

	Owner owner = new Owner();
	owner.setFirstName(« Sam »);
	owner.setLastName(« Schultz »);
	owner.setAddress(« 4, Evans Street »);
	owner.setCity(« Wollongong »);
	owner.setTelephone(« 4444444444 »);
	this.owners.save(owner);
	assertThat(owner.getId(), is(not(0)));

	owners = this.owners.findByLastNameStartingWith(« Schultz », pageable);
	assertThat(owners.getTotalElements(), is(equalTo(found + 1L)));
}</textarea><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #ECEFF4">@</span><span style="color: #D08770">Test</span></span>
<span class="line"><span style="color: #ECEFF4">@</span><span style="color: #D08770">TestTransaction</span></span>
<span class="line"><span style="color: #81A1C1">void</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">shouldInsertOwner</span><span style="color: #ECEFF4">()</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">	</span><span style="color: #8FBCBB">Page</span><span style="color: #ECEFF4">&lt;</span><span style="color: #8FBCBB">Owner</span><span style="color: #ECEFF4">&gt;</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">owners</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">this</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">owners</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">findByLastNameStartingWith</span><span style="color: #ECEFF4">(</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">Schultz</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> pageable</span><span style="color: #ECEFF4">)</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">	</span><span style="color: #81A1C1">int</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">found</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">(</span><span style="color: #81A1C1">int</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">owners</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">getTotalElements</span><span style="color: #ECEFF4">()</span><span style="color: #81A1C1">;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D8DEE9FF">	</span><span style="color: #8FBCBB">Owner</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">owner</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">new</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">Owner</span><span style="color: #ECEFF4">()</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">	</span><span style="color: #D8DEE9">owner</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">setFirstName</span><span style="color: #ECEFF4">(</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">Sam</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">)</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">	</span><span style="color: #D8DEE9">owner</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">setLastName</span><span style="color: #ECEFF4">(</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">Schultz</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">)</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">	</span><span style="color: #D8DEE9">owner</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">setAddress</span><span style="color: #ECEFF4">(</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">4, Evans Street</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">)</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">	</span><span style="color: #D8DEE9">owner</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">setCity</span><span style="color: #ECEFF4">(</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">Wollongong</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">)</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">	</span><span style="color: #D8DEE9">owner</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">setTelephone</span><span style="color: #ECEFF4">(</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">4444444444</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">)</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">	</span><span style="color: #81A1C1">this</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">owners</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">save</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">owner</span><span style="color: #ECEFF4">)</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">	</span><span style="color: #88C0D0">assertThat</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9">owner</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">getId</span><span style="color: #ECEFF4">(),</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">is</span><span style="color: #ECEFF4">(</span><span style="color: #88C0D0">not</span><span style="color: #ECEFF4">(</span><span style="color: #B48EAD">0</span><span style="color: #ECEFF4">)))</span><span style="color: #81A1C1">;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D8DEE9FF">	owners </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">this</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">owners</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">findByLastNameStartingWith</span><span style="color: #ECEFF4">(</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">Schultz</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> pageable</span><span style="color: #ECEFF4">)</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">	</span><span style="color: #88C0D0">assertThat</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9">owners</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">getTotalElements</span><span style="color: #ECEFF4">(),</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">is</span><span style="color: #ECEFF4">(</span><span style="color: #88C0D0">equalTo</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">found </span><span style="color: #81A1C1">+</span><span style="color: #D8DEE9FF"> </span><span style="color: #B48EAD">1L</span><span style="color: #ECEFF4">)))</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #ECEFF4">}</span></span></code></pre></div>



<h2 class="wp-block-heading">De DataJpaTest à QuarkusTest</h2>



<p>Pour tester les Repository JPA, Spring Boot met à disposition l’annotation <strong>@DataJpaTest</strong> 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.</p>



<p>Pour arriver à un résultat similaire avec Quarkus, l’annotation @DataJpaTest a été remplacée par 2 annotations&nbsp;:</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><textarea class="code-block-pro-copy-button-textarea" aria-hidden="true" readonly>@QuarkusTest
@QuarkusTestResource(H2DatabaseTestResource.class)
class ClinicServiceTests {</textarea><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #ECEFF4">@</span><span style="color: #D08770">QuarkusTest</span></span>
<span class="line"><span style="color: #ECEFF4">@</span><span style="color: #D08770">QuarkusTestResource</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9">H2DatabaseTestResource</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">class</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #81A1C1">class</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">ClinicServiceTests</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span></code></pre></div>



<p>L’annotation @<strong>QuarkusTestResource </strong>permet de référencer la classe <strong>H2DatabaseTestResource</strong> (fournie par l’artefact <strong>io.quarkus:quarkus-test-h2</strong>) chargée de démarrer / arrêter un serveur H2. <br><br>Par 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é <strong>quarkus.hibernate-orm.sql-load-script</strong> du fichier <a href="https://github.com/spring-petclinic/quarkus-spring-petclinic/blob/v3.21.0/src/main/resources/application.properties">application.properties</a> a été positionnée sur <strong>h2</strong>&nbsp;:</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><textarea class="code-block-pro-copy-button-textarea" aria-hidden="true" readonly>quarkus.datasource.db-kind=h2
quarkus.hibernate-orm.log.sql=true
quarkus.hibernate-orm.sql-load-script=db/${quarkus.datasource.db-kind}/data.sql</textarea><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #D8DEE9">quarkus</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">datasource</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">db</span><span style="color: #81A1C1">-</span><span style="color: #D8DEE9FF">kind</span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF">h2</span></span>
<span class="line"><span style="color: #D8DEE9">quarkus</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">hibernate</span><span style="color: #81A1C1">-</span><span style="color: #D8DEE9">orm</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">log</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">sql</span><span style="color: #81A1C1">=true</span></span>
<span class="line"><span style="color: #D8DEE9">quarkus</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">hibernate</span><span style="color: #81A1C1">-</span><span style="color: #D8DEE9">orm</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">sql</span><span style="color: #81A1C1">-</span><span style="color: #D8DEE9FF">load</span><span style="color: #81A1C1">-</span><span style="color: #D8DEE9FF">script</span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF">db</span><span style="color: #81A1C1">/</span><span style="color: #D8DEE9FF">$</span><span style="color: #ECEFF4">{</span><span style="color: #D8DEE9">quarkus</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">datasource</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">db</span><span style="color: #81A1C1">-</span><span style="color: #D8DEE9FF">kind</span><span style="color: #ECEFF4">}</span><span style="color: #81A1C1">/</span><span style="color: #D8DEE9">data</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">sql</span></span></code></pre></div>



<p>La propriété <strong>quarkus.hibernate-orm.sql-load-script</strong> a quant à elle permis de réutiliser le script DML existant <a href="https://github.com/spring-petclinic/quarkus-spring-petclinic/blob/v3.21.0/src/main/resources/db/h2/data.sql">data.sql</a> insérant quelques <strong>données de test</strong>.</p>



<p>A ce stade de la migration vers Quarkus, les tests unitaires de la couche de persistance et de la couche service sont passants.</p>



<h2 class="wp-block-heading">Internationalisation</h2>



<p>Le support de l’internationalisation (<strong>i18n</strong> pour les intimes) est incomplet dans Spring Petclinic (cf. issue <a href="https://github.com/spring-projects/spring-petclinic/issues/1854">#1854</a>). Le <strong>ressource bundle</strong> <a href="https://github.com/spring-petclinic/quarkus-spring-petclinic/tree/v3.21.0/src/main/resources/messages"><strong>messages</strong></a> contient différent fichiers properties de traduction. Les clés sont utilisées dans certains templates Thymeleaf (ex&nbsp;: welcome) et pour les messages d’erreur (ex&nbsp;: required, typeMismatch.birthDate). Ce ressource bundle a pu être réutilisé dans la version Quarkus. <br><br>Qute propose un <a href="https://quarkus.io/guides/qute-reference#type-safe-message-bundles"><strong>mécanisme typesafe de ressource bundle</strong></a> basé sur l’annotation <strong>@ResourceBundle</strong>. La classe <a href="https://github.com/spring-petclinic/quarkus-spring-petclinic/blob/v3.21.0/src/main/java/org/springframework/samples/petclinic/system/AppMessages.java">AppMessages</a> a été ajoutée à Petclinic. En voici un extrait&nbsp;contenant 3 clés :</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><textarea class="code-block-pro-copy-button-textarea" aria-hidden="true" readonly>import io.quarkus.qute.i18n.Message;
import io.quarkus.qute.i18n.MessageBundle;

@MessageBundle(value = « messages », locale = « en »)
public interface AppMessages {

  @Message
  String welcome();

	@Message
	String required();

	@Message
	String typeMismatch_birthDate();</textarea><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #81A1C1">import</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">io</span><span style="color: #ECEFF4">.</span><span style="color: #8FBCBB">quarkus</span><span style="color: #ECEFF4">.</span><span style="color: #8FBCBB">qute</span><span style="color: #ECEFF4">.</span><span style="color: #8FBCBB">i18n</span><span style="color: #ECEFF4">.</span><span style="color: #8FBCBB">Message</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #81A1C1">import</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">io</span><span style="color: #ECEFF4">.</span><span style="color: #8FBCBB">quarkus</span><span style="color: #ECEFF4">.</span><span style="color: #8FBCBB">qute</span><span style="color: #ECEFF4">.</span><span style="color: #8FBCBB">i18n</span><span style="color: #ECEFF4">.</span><span style="color: #8FBCBB">MessageBundle</span><span style="color: #81A1C1">;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #ECEFF4">@</span><span style="color: #D08770">MessageBundle</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9">value</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">messages</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">locale</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">en</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #81A1C1">public</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">interface</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">AppMessages</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D8DEE9FF">  </span><span style="color: #ECEFF4">@</span><span style="color: #D08770">Message</span></span>
<span class="line"><span style="color: #D8DEE9FF">  </span><span style="color: #8FBCBB">String</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">welcome</span><span style="color: #ECEFF4">()</span><span style="color: #81A1C1">;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D8DEE9FF">	</span><span style="color: #ECEFF4">@</span><span style="color: #D08770">Message</span></span>
<span class="line"><span style="color: #D8DEE9FF">	</span><span style="color: #8FBCBB">String</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">required</span><span style="color: #ECEFF4">()</span><span style="color: #81A1C1">;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D8DEE9FF">	</span><span style="color: #ECEFF4">@</span><span style="color: #D08770">Message</span></span>
<span class="line"><span style="color: #D8DEE9FF">	</span><span style="color: #8FBCBB">String</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">typeMismatch_birthDate</span><span style="color: #ECEFF4">()</span><span style="color: #81A1C1">;</span></span></code></pre></div>



<p>Le nom des clés des properties ne semble pas accepter le <strong>caractère point</strong> (ex&nbsp;: <em>@Message(value = « typeMismatch.birthDate »</em>). Certaines clés ont donc dû être renommées (ex&nbsp;: <em>typeMismatch.birthDate</em> vers <em>typeMismatch_birthDate</em>). <br><br>Au runtime, l’usage du ressource bundle Quarkus peut-être utilisé dans un template Qute via le namespace du message bundle. Exemple&nbsp;:</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><textarea class="code-block-pro-copy-button-textarea" aria-hidden="true" readonly> {#for err in errors}
  {#if err == &lsquo;notFound&rsquo;}
    &lt;p>{messages:notFound}&lt;/p>
  {#else}
    &lt;p>{err}&lt;/p>
  {/if}
{/for}</textarea><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span><span style="color: #D8DEE9FF">#</span><span style="color: #81A1C1">for</span><span style="color: #D8DEE9FF"> err in errors</span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">  </span><span style="color: #ECEFF4">{</span><span style="color: #D8DEE9FF">#</span><span style="color: #81A1C1">if</span><span style="color: #D8DEE9FF"> err </span><span style="color: #81A1C1">==</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&#39;</span><span style="color: #A3BE8C">notFound</span><span style="color: #ECEFF4">&#39;</span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">&lt;</span><span style="color: #D8DEE9FF">p</span><span style="color: #81A1C1">&gt;</span><span style="color: #ECEFF4">{</span><span style="color: #D8DEE9FF">messages</span><span style="color: #81A1C1">:</span><span style="color: #D8DEE9FF">notFound</span><span style="color: #ECEFF4">}</span><span style="color: #81A1C1">&lt;/</span><span style="color: #D8DEE9FF">p</span><span style="color: #81A1C1">&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">  </span><span style="color: #ECEFF4">{</span><span style="color: #D8DEE9FF">#</span><span style="color: #81A1C1">else</span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">&lt;</span><span style="color: #D8DEE9FF">p</span><span style="color: #81A1C1">&gt;</span><span style="color: #ECEFF4">{</span><span style="color: #D8DEE9FF">err</span><span style="color: #ECEFF4">}</span><span style="color: #81A1C1">&lt;/</span><span style="color: #D8DEE9FF">p</span><span style="color: #81A1C1">&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">  </span><span style="color: #ECEFF4">{</span><span style="color: #81A1C1">/if</span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #ECEFF4">{</span><span style="color: #81A1C1">/for</span><span style="color: #ECEFF4">}</span></span></code></pre></div>



<p>Ce même ressource bundle peut également être exploité depuis un contrôleur REST. La création de classe <a href="https://github.com/spring-petclinic/quarkus-spring-petclinic/blob/v3.21.0/src/main/java/org/springframework/samples/petclinic/system/I18nHelper.java">I18nHelper</a> permet d’exploiter dynamiquement l’en-tête HTTP <strong>Accept-Language</strong>&nbsp;:</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><textarea class="code-block-pro-copy-button-textarea" aria-hidden="true" readonly>@GetMapping(« / »)
public TemplateInstance processFindForm(@RequestParam(defaultValue = « 1 ») int page, @RequestParam String lastName,
		@HeaderParam(« Accept-Language ») String language) {
	Page&lt;Owner> ownersResults = findPaginatedForOwnersLastName(page, lastName);

if (ownersResults.isEmpty()) {
	// no owners found
	String notFound = I18nHelper.lookupAppMessages(language).notFound();
	return OwnerTemplates.findOwners(List.of(notFound));
}</textarea><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #ECEFF4">@</span><span style="color: #D08770">GetMapping</span><span style="color: #ECEFF4">(</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">/</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #81A1C1">public</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">TemplateInstance</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">processFindForm</span><span style="color: #ECEFF4">(@</span><span style="color: #D08770">RequestParam</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9">defaultValue</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">1</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">int</span><span style="color: #D8DEE9FF"> page</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">@</span><span style="color: #D08770">RequestParam</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">String</span><span style="color: #D8DEE9FF"> lastName</span><span style="color: #ECEFF4">,</span></span>
<span class="line"><span style="color: #D8DEE9FF">		</span><span style="color: #ECEFF4">@</span><span style="color: #D08770">HeaderParam</span><span style="color: #ECEFF4">(</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">Accept-Language</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">String</span><span style="color: #D8DEE9FF"> language</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">	</span><span style="color: #8FBCBB">Page</span><span style="color: #ECEFF4">&lt;</span><span style="color: #8FBCBB">Owner</span><span style="color: #ECEFF4">&gt;</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">ownersResults</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">findPaginatedForOwnersLastName</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">page</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> lastName</span><span style="color: #ECEFF4">)</span><span style="color: #81A1C1">;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #81A1C1">if</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9">ownersResults</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">isEmpty</span><span style="color: #ECEFF4">())</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #ECEFF4">	</span><span style="color: #616E88">// no owners found</span></span>
<span class="line"><span style="color: #D8DEE9FF">	</span><span style="color: #8FBCBB">String</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">notFound</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">I18nHelper</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">lookupAppMessages</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">language</span><span style="color: #ECEFF4">).</span><span style="color: #88C0D0">notFound</span><span style="color: #ECEFF4">()</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">	</span><span style="color: #81A1C1">return</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">OwnerTemplates</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">findOwners</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9">List</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">of</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">notFound</span><span style="color: #ECEFF4">))</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #ECEFF4">}</span></span></code></pre></div>



<p>Possible que Quarkus propose nativement un mécanisme similaire. <a href="https://docs.quarkiverse.io/quarkus-renarde/dev/advanced.html#localisation">Quarkus Renarde</a> utilise quant à lui le header Accept-Language et un cookie.</p>



<p>En passant, l’exemple précédent montre l’usage des <strong>annotations Spring @GetMapping</strong> et <strong>@RequestParam</strong>. L’annotation Spring <strong>@RequestHeader</strong> n’est pas supportée par Quarkus et a dû être substituée par l’annotation <strong>@HeaderParam </strong>de JAX-RS.</p>



<p>Le debuggage de la méthode MessageBundleProcessor::<strong>parseKeyToTemplateFromLocalizedFile</strong> aura nécessité d’ajouter temporairement au classpath la dépendance io.quarkus:quarkus-qute-deployment.</p>



<h2 class="wp-block-heading">Ressources statiques</h2>



<p>Afin de se conforter aux conventions de Quarkus, les ressources statiques (fonts, css et images) ont été <strong>déplacées</strong> du répertoire static/resources vers le <strong>répertoire META-INF/resources</strong>.</p>



<h2 class="wp-block-heading">Migration templates Thymeleaf vers Qute</h2>



<p>Les templates Thymeleaf de Spring Petclinic utilisent le mécanisme <strong>de fragments Thymeleaf</strong> à la fois pour le <strong>gabarit</strong> <strong>des pages</strong> (layout.html) et pour les <strong>tags HTML</strong> réutilisables (inputField.html et selectField.html).</p>



<p>Une première étape a donc consisté à migrer ces fragments Thymeleaf vers une équivalence Qute. La syntaxe de ces 2 moteurs de templating Java diffère beaucoup. A l’aide du <a href="https://quarkus.io/guides/qute-reference">guide de référence de Qute</a>, 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 <a href="https://quarkus.io/guides/qute-reference#include_helper">section {#include fragments/layout}</a>.</p>



<p><span style="text-decoration: underline;">Template Thymeleaf de la page welcome originale</span> :</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><textarea class="code-block-pro-copy-button-textarea" aria-hidden="true" readonly>&lt;!DOCTYPE html>
&lt;html xmlns:th= »https://www.thymeleaf.org » th:replace= »~{fragments/layout :: layout (~{::body},&rsquo;home&rsquo;)} »>
  &lt;body>
    &lt;h2 th:text= »#{welcome} »>Welcome&lt;/h2>
    &lt;div class= »row »>
        &lt;div class= »col-md-12&Prime;>
          &lt;img class= »img-responsive » src= »../static/resources/images/pets.png » th:src= »@{/resources/images/pets.png} »/>
        &lt;/div>
    &lt;/div>
  &lt;/body>
&lt;/html></textarea><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #81A1C1">&lt;!DOCTYPE</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">html</span><span style="color: #81A1C1">&gt;</span></span>
<span class="line"><span style="color: #81A1C1">&lt;html</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">xmlns:th</span><span style="color: #ECEFF4">=</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">https://www.thymeleaf.org</span><span style="color: #ECEFF4">&quot;</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">th:replace</span><span style="color: #ECEFF4">=</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">~{fragments/layout :: layout (~{::body},&#39;home&#39;)}</span><span style="color: #ECEFF4">&quot;</span><span style="color: #81A1C1">&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">  </span><span style="color: #81A1C1">&lt;body&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">&lt;h2</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">th:text</span><span style="color: #ECEFF4">=</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">#{welcome}</span><span style="color: #ECEFF4">&quot;</span><span style="color: #81A1C1">&gt;</span><span style="color: #D8DEE9FF">Welcome</span><span style="color: #81A1C1">&lt;/h2&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">&lt;div</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">class</span><span style="color: #ECEFF4">=</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">row</span><span style="color: #ECEFF4">&quot;</span><span style="color: #81A1C1">&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">&lt;div</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">class</span><span style="color: #ECEFF4">=</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">col-md-12</span><span style="color: #ECEFF4">&quot;</span><span style="color: #81A1C1">&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">          </span><span style="color: #81A1C1">&lt;img</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">class</span><span style="color: #ECEFF4">=</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">img-responsive</span><span style="color: #ECEFF4">&quot;</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">src</span><span style="color: #ECEFF4">=</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">../static/resources/images/pets.png</span><span style="color: #ECEFF4">&quot;</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">th:src</span><span style="color: #ECEFF4">=</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">@{/resources/images/pets.png}</span><span style="color: #ECEFF4">&quot;</span><span style="color: #81A1C1">/&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">&lt;/div&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">&lt;/div&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">  </span><span style="color: #81A1C1">&lt;/body&gt;</span></span>
<span class="line"><span style="color: #81A1C1">&lt;/html&gt;</span></span></code></pre></div>



<p><span style="text-decoration: underline;">Template Qute équivalent de la page welcome</span> :</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><textarea class="code-block-pro-copy-button-textarea" aria-hidden="true" readonly>{#include fragments/layout}
  &lt;body>
    &lt;h2>{messages:welcome}&lt;/h2>
    &lt;div class= »row »>
        &lt;div class= »col-md-12&Prime;>
          &lt;img class= »img-responsive » src= »/images/pets.png » />
        &lt;/div>
    &lt;/div>
  &lt;/body>
{/include}</textarea><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #D8DEE9FF">{#include fragments/layout}</span></span>
<span class="line"><span style="color: #D8DEE9FF">  </span><span style="color: #81A1C1">&lt;body&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">&lt;h2&gt;</span><span style="color: #D8DEE9FF">{messages:welcome}</span><span style="color: #81A1C1">&lt;/h2&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">&lt;div</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">class</span><span style="color: #ECEFF4">=</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">row</span><span style="color: #ECEFF4">&quot;</span><span style="color: #81A1C1">&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">&lt;div</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">class</span><span style="color: #ECEFF4">=</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">col-md-12</span><span style="color: #ECEFF4">&quot;</span><span style="color: #81A1C1">&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">          </span><span style="color: #81A1C1">&lt;img</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">class</span><span style="color: #ECEFF4">=</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">img-responsive</span><span style="color: #ECEFF4">&quot;</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">src</span><span style="color: #ECEFF4">=</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">/images/pets.png</span><span style="color: #ECEFF4">&quot;</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">/&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">&lt;/div&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">&lt;/div&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">  </span><span style="color: #81A1C1">&lt;/body&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">{/include}</span></span></code></pre></div>



<p>Afin d’être enregistrés automatiquement par Quarkus, les <a href="https://quarkus.io/guides/qute-reference#user_tags"><strong>user-defined tags</strong></a> <strong>input</strong> et <strong>select</strong> ont été déplacés dans le <strong>répertoire src/main/resources/templates/tags</strong>. Les 2 exemples de tags suivants permettent de comparer les syntaxes Thymeleaf et Qute.</p>



<p><span style="text-decoration: underline;">Exemple du tag Thymeleaf inputField.html</span> :</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><textarea class="code-block-pro-copy-button-textarea" aria-hidden="true" readonly>&lt;html>
&lt;body>
  &lt;form>
    &lt;th:block th:fragment= »input (label, name, type) »>
      &lt;div th:with= »valid=${!#fields.hasErrors(name)} »
        th:class= »${&lsquo;form-group&rsquo; + (valid ?  » : &lsquo; has-error&rsquo;)} »
        class= »form-group »>
        &lt;label class= »col-sm-2 control-label » th:text= »${label} »>Label&lt;/label>
        &lt;div class= »col-sm-10&Prime;>
            &lt;div th:switch= »${type} »>
                &lt;input th:case= »&lsquo;text' » class= »form-control » type= »text » th:field= »*{__${name}__} » />
                &lt;input th:case= »&lsquo;date' » class= »form-control » type= »date » th:field= »*{__${name}__} »/>
            &lt;/div>
          &lt;span th:if= »${valid} »
            class= »fa fa-ok form-control-feedback »
            aria-hidden= »true »>&lt;/span>
          &lt;th:block th:if= »${!valid} »>
            &lt;span
              class= »fa fa-remove form-control-feedback »
              aria-hidden= »true »>&lt;/span>
            &lt;span class= »help-inline » th:errors= »*{__${name}__} »>Error&lt;/span>
          &lt;/th:block>
        &lt;/div>
      &lt;/div>
    &lt;/th:block>
  &lt;/form>
&lt;/body>
&lt;/html></textarea><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #81A1C1">&lt;html&gt;</span></span>
<span class="line"><span style="color: #81A1C1">&lt;body&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">  </span><span style="color: #81A1C1">&lt;form&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">&lt;</span><span style="color: #D8DEE9">th:block</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">th:fragment</span><span style="color: #ECEFF4">=</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">input (label, name, type)</span><span style="color: #ECEFF4">&quot;</span><span style="color: #81A1C1">&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">      </span><span style="color: #81A1C1">&lt;div</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">th:with</span><span style="color: #ECEFF4">=</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">valid=${!#fields.hasErrors(name)}</span><span style="color: #ECEFF4">&quot;</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #8FBCBB">th:class</span><span style="color: #ECEFF4">=</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">${&#39;form-group&#39; + (valid ? &#39;&#39; : &#39; has-error&#39;)}</span><span style="color: #ECEFF4">&quot;</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #8FBCBB">class</span><span style="color: #ECEFF4">=</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">form-group</span><span style="color: #ECEFF4">&quot;</span><span style="color: #81A1C1">&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">&lt;label</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">class</span><span style="color: #ECEFF4">=</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">col-sm-2 control-label</span><span style="color: #ECEFF4">&quot;</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">th:text</span><span style="color: #ECEFF4">=</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">${label}</span><span style="color: #ECEFF4">&quot;</span><span style="color: #81A1C1">&gt;</span><span style="color: #D8DEE9FF">Label</span><span style="color: #81A1C1">&lt;/label&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">&lt;div</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">class</span><span style="color: #ECEFF4">=</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">col-sm-10</span><span style="color: #ECEFF4">&quot;</span><span style="color: #81A1C1">&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #81A1C1">&lt;div</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">th:switch</span><span style="color: #ECEFF4">=</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">${type}</span><span style="color: #ECEFF4">&quot;</span><span style="color: #81A1C1">&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">                </span><span style="color: #81A1C1">&lt;input</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">th:case</span><span style="color: #ECEFF4">=</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">&#39;text&#39;</span><span style="color: #ECEFF4">&quot;</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">class</span><span style="color: #ECEFF4">=</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">form-control</span><span style="color: #ECEFF4">&quot;</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">type</span><span style="color: #ECEFF4">=</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">text</span><span style="color: #ECEFF4">&quot;</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">th:field</span><span style="color: #ECEFF4">=</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">*{__${name}__}</span><span style="color: #ECEFF4">&quot;</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">/&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">                </span><span style="color: #81A1C1">&lt;input</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">th:case</span><span style="color: #ECEFF4">=</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">&#39;date&#39;</span><span style="color: #ECEFF4">&quot;</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">class</span><span style="color: #ECEFF4">=</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">form-control</span><span style="color: #ECEFF4">&quot;</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">type</span><span style="color: #ECEFF4">=</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">date</span><span style="color: #ECEFF4">&quot;</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">th:field</span><span style="color: #ECEFF4">=</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">*{__${name}__}</span><span style="color: #ECEFF4">&quot;</span><span style="color: #81A1C1">/&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #81A1C1">&lt;/div&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">          </span><span style="color: #81A1C1">&lt;span</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">th:if</span><span style="color: #ECEFF4">=</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">${valid}</span><span style="color: #ECEFF4">&quot;</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #8FBCBB">class</span><span style="color: #ECEFF4">=</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">fa fa-ok form-control-feedback</span><span style="color: #ECEFF4">&quot;</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #8FBCBB">aria-hidden</span><span style="color: #ECEFF4">=</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">true</span><span style="color: #ECEFF4">&quot;</span><span style="color: #81A1C1">&gt;&lt;/span&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">          </span><span style="color: #81A1C1">&lt;</span><span style="color: #D8DEE9">th:block</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">th:if</span><span style="color: #ECEFF4">=</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">${!valid}</span><span style="color: #ECEFF4">&quot;</span><span style="color: #81A1C1">&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #81A1C1">&lt;span</span></span>
<span class="line"><span style="color: #D8DEE9FF">              </span><span style="color: #8FBCBB">class</span><span style="color: #ECEFF4">=</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">fa fa-remove form-control-feedback</span><span style="color: #ECEFF4">&quot;</span></span>
<span class="line"><span style="color: #D8DEE9FF">              </span><span style="color: #8FBCBB">aria-hidden</span><span style="color: #ECEFF4">=</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">true</span><span style="color: #ECEFF4">&quot;</span><span style="color: #81A1C1">&gt;&lt;/span&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #81A1C1">&lt;span</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">class</span><span style="color: #ECEFF4">=</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">help-inline</span><span style="color: #ECEFF4">&quot;</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">th:errors</span><span style="color: #ECEFF4">=</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">*{__${name}__}</span><span style="color: #ECEFF4">&quot;</span><span style="color: #81A1C1">&gt;</span><span style="color: #D8DEE9FF">Error</span><span style="color: #81A1C1">&lt;/span&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">          </span><span style="color: #81A1C1">&lt;/</span><span style="color: #D8DEE9">th:block</span><span style="color: #81A1C1">&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">&lt;/div&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">      </span><span style="color: #81A1C1">&lt;/div&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">&lt;/</span><span style="color: #D8DEE9">th:block</span><span style="color: #81A1C1">&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">  </span><span style="color: #81A1C1">&lt;/form&gt;</span></span>
<span class="line"><span style="color: #81A1C1">&lt;/body&gt;</span></span>
<span class="line"><span style="color: #81A1C1">&lt;/html&gt;</span></span></code></pre></div>



<p><span style="text-decoration: underline;">Exemple équivalent du tag Qute inputField.html</span>&nbsp;:</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><textarea class="code-block-pro-copy-button-textarea" aria-hidden="true" readonly> {#let invalid=result.hasErrors(name)}
      &lt;div class= »form-group {#if invalid} has-error {/if} »>
        &lt;label for= »{name} » class= »col-sm-2 control-label »>{it}
        &lt;/label>
        &lt;div class= »col-sm-10&Prime;>
          &lt;input class= »form-control » id= »{name} » name= »{name} » type= »{type} » value= »{field} » />
          &lt;span class= »fa {#if invalid}fa-remove{#else}fa-ok{/if} form-control-feedback » aria-hidden= »true »>&lt;/span>
          {#if invalid}
            &lt;span class= »help-inline »>{result.getErrorMessage(name)}&lt;/span>
          {/if}
        &lt;/div>
      &lt;/div>
{/let}</textarea><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #D8DEE9FF"> {#let invalid=result.hasErrors(name)}</span></span>
<span class="line"><span style="color: #D8DEE9FF">      </span><span style="color: #81A1C1">&lt;div</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">class</span><span style="color: #ECEFF4">=</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">form-group {#if invalid} has-error {/if}</span><span style="color: #ECEFF4">&quot;</span><span style="color: #81A1C1">&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">&lt;label</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">for</span><span style="color: #ECEFF4">=</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">{name}</span><span style="color: #ECEFF4">&quot;</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">class</span><span style="color: #ECEFF4">=</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">col-sm-2 control-label</span><span style="color: #ECEFF4">&quot;</span><span style="color: #81A1C1">&gt;</span><span style="color: #D8DEE9FF">{it}</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">&lt;/label&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">&lt;div</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">class</span><span style="color: #ECEFF4">=</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">col-sm-10</span><span style="color: #ECEFF4">&quot;</span><span style="color: #81A1C1">&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">          </span><span style="color: #81A1C1">&lt;input</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">class</span><span style="color: #ECEFF4">=</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">form-control</span><span style="color: #ECEFF4">&quot;</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">id</span><span style="color: #ECEFF4">=</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">{name}</span><span style="color: #ECEFF4">&quot;</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">name</span><span style="color: #ECEFF4">=</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">{name}</span><span style="color: #ECEFF4">&quot;</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">type</span><span style="color: #ECEFF4">=</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">{type}</span><span style="color: #ECEFF4">&quot;</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">value</span><span style="color: #ECEFF4">=</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">{field}</span><span style="color: #ECEFF4">&quot;</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">/&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">          </span><span style="color: #81A1C1">&lt;span</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">class</span><span style="color: #ECEFF4">=</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">fa {#if invalid}fa-remove{#else}fa-ok{/if} form-control-feedback</span><span style="color: #ECEFF4">&quot;</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">aria-hidden</span><span style="color: #ECEFF4">=</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">true</span><span style="color: #ECEFF4">&quot;</span><span style="color: #81A1C1">&gt;&lt;/span&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">          {#if invalid}</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #81A1C1">&lt;span</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">class</span><span style="color: #ECEFF4">=</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">help-inline</span><span style="color: #ECEFF4">&quot;</span><span style="color: #81A1C1">&gt;</span><span style="color: #D8DEE9FF">{result.getErrorMessage(name)}</span><span style="color: #81A1C1">&lt;/span&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">          {/if}</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">&lt;/div&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">      </span><span style="color: #81A1C1">&lt;/div&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">{/let}</span></span></code></pre></div>



<h2 class="wp-block-heading">Binding du modèle</h2>



<p>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 <strong>binding des champs du formulaire</strong>. 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 <strong>DataBinder</strong> 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 <em>processCreationForm</em> accepte en paramètre un objet de type Owner&nbsp;bindé avec les champs du formulaire <a href="https://github.com/spring-projects/spring-petclinic/blob/main/src/main/resources/templates/owners/createOrUpdateOwnerForm.html">createOrUpdateOwnerForm.html</a> :</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><textarea class="code-block-pro-copy-button-textarea" aria-hidden="true" readonly>@PostMapping(« /owners/new »)
public String processCreationForm(@Valid Owner owner, BindingResult result, RedirectAttributes redirectAttributes) {</textarea><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #ECEFF4">@</span><span style="color: #D08770">PostMapping</span><span style="color: #ECEFF4">(</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">/owners/new</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #81A1C1">public</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">String</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">processCreationForm</span><span style="color: #ECEFF4">(@</span><span style="color: #D08770">Valid</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">Owner</span><span style="color: #D8DEE9FF"> owner</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">BindingResult</span><span style="color: #D8DEE9FF"> result</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">RedirectAttributes</span><span style="color: #D8DEE9FF"> redirectAttributes</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span></code></pre></div>



<p>Positionnée sur le paramtètre owner, l’annotation <strong>@Valid</strong> permet d’exécuter la validation <strong>Bean Validation / Hibernate Validator</strong>. Je n’ai pas trouvé dans Quarkus l’équivalent des classes <strong>BindingResult</strong> et <strong>RedirectAttibutes</strong>. Ainsi, la signature de cette méthode s’allège en Quarkus&nbsp;:</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><textarea class="code-block-pro-copy-button-textarea" aria-hidden="true" readonly>@PostMapping(« /owners/new »)
public TemplateInstance processCreationForm(Owner owner) {</textarea><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #ECEFF4">@</span><span style="color: #D08770">PostMapping</span><span style="color: #ECEFF4">(</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">/owners/new</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #81A1C1">public</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">TemplateInstance</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">processCreationForm</span><span style="color: #ECEFF4">(</span><span style="color: #8FBCBB">Owner</span><span style="color: #D8DEE9FF"> owner</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span></code></pre></div>



<p>On retrouve l’annotation Spring <strong>@PostMapping</strong> supportée par l’extension Quarkus. Le type de retour n’est plus une String correspondant à la vue MVC à afficher, mais une <strong>TemplateInstance</strong>.</p>



<p>Pour binder la classe Owner, un changement a dû être opéré au niveau de la classe <a href="https://github.com/spring-petclinic/quarkus-spring-petclinic/blob/v3.21.0/src/main/java/org/springframework/samples/petclinic/owner/Owner.java" data-type="link" data-id="https://github.com/spring-petclinic/quarkus-spring-petclinic/blob/v3.21.0/src/main/java/org/springframework/samples/petclinic/owner/Owner.java">Owner</a> et de ses classes parentes <a href="https://github.com/spring-petclinic/quarkus-spring-petclinic/blob/v3.21.0/src/main/java/org/springframework/samples/petclinic/model/Person.java">Person</a> et <a href="https://github.com/spring-petclinic/quarkus-spring-petclinic/blob/v3.21.0/src/main/java/org/springframework/samples/petclinic/model/NamedEntity.java" data-type="link" data-id="https://github.com/spring-petclinic/quarkus-spring-petclinic/blob/v3.21.0/src/main/java/org/springframework/samples/petclinic/model/NamedEntity.java">NamedEntity</a>&nbsp;: <strong>ajouter l’annotation JAX-RS @FormParam</strong> sur les attributs bindés comme address. Extrait de la classe <a href="https://github.com/spring-petclinic/quarkus-spring-petclinic/blob/v3.21.0/src/main/java/org/springframework/samples/petclinic/owner/Owner.java" data-type="link" data-id="https://github.com/spring-petclinic/quarkus-spring-petclinic/blob/v3.21.0/src/main/java/org/springframework/samples/petclinic/owner/Owner.java">Owner</a>&nbsp;:</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><textarea class="code-block-pro-copy-button-textarea" aria-hidden="true" readonly>public class Owner extends Person {

	@Column(name = « address »)
	@NotBlank
	@FormParam(« address »)
	private String address;</textarea><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #81A1C1">public</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">class</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">Owner</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">extends</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB; font-weight: bold">Person</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D8DEE9FF">	</span><span style="color: #ECEFF4">@</span><span style="color: #D08770">Column</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9">name</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">address</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">	</span><span style="color: #ECEFF4">@</span><span style="color: #D08770">NotBlank</span></span>
<span class="line"><span style="color: #D8DEE9FF">	</span><span style="color: #ECEFF4">@</span><span style="color: #D08770">FormParam</span><span style="color: #ECEFF4">(</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">address</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">	</span><span style="color: #81A1C1">private</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">String</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">address</span><span style="color: #81A1C1">;</span></span></code></pre></div>



<p>Sans ce changement, voici le message d’erreur obtenu lors de la création d’un nouveau propriétaire d’animal de compagnie&nbsp;:</p>



<pre class="wp-block-preformatted">2025-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<br>        at org.jboss.resteasy.reactive.server.handlers.RequestDeserializeHandler.handle(RequestDeserializeHandler.java:75)</pre>



<p>Ce ciblage explicite des champs bindés depuis un formulaire HTML pourrait être justifié par des mesures de sécurité.</p>



<p>Pour terminer sur le binding du modèle, l’interface <strong>org.springframework.ui.Model</strong> est conservée dans quarkus-spring-context-api mais <a href="https://github.com/search?q=org%3Aquarkusio%20ModelMap&amp;type=code">ne semble pas être exploitée par Quarkus</a>.</p>



<h2 class="wp-block-heading">Validation des données</h2>



<p>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 <strong>valider les données</strong> avant de les insérer en base de données.</p>



<p>Le guide <a href="https://quarkus.io/guides/validation#a-frontend">Validation with Hibernate Validator</a> explique comment mettre en place Bean Validation sur une API REST. L’annotation <strong>@jakarta.validation.Valid</strong> 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 <em>processCreationForm</em>. Il renvoie directement un flux texte contenant le rapport d’erreur&nbsp;complet. Exemple de la soumission d’un formulaire vide :</p>



<pre class="wp-block-preformatted">ViolationReport{title='Constraint Violation', status=400, violations=[Violation{field='processCreationForm.owner.address', message='ne doit pas être vide'}, Violation{field='processCreationForm.owner.telephone', message='ne doit pas être vide'}, Violation{field='processCreationForm.owner.telephone', message='Telephone must be a 10-digit number'}, Violation{field='processCreationForm.owner.city', message='ne doit pas être vide'}, Violation{field='processCreationForm.owner.lastName', message='ne doit pas être vide'}, Violation{field='processCreationForm.owner.firstName', message='ne doit pas être vide'}]}</pre>



<p>Dans Petclinic, on souhaite renvoyer le formulaire HTML en erreur avec le message d’erreur à côté de chaque champ erroné.</p>



<p>Dans la documentation Quakus Qute, je n’ai pas trouvé l’équivalent de ce que propose Spring Web MVC, grâce notamment à la classe <strong>BindingResult</strong>. Pour contourner cette limitation, j’ai introduit le record <a href="https://github.com/spring-petclinic/quarkus-spring-petclinic/blob/v3.21.0/src/main/java/org/springframework/samples/petclinic/system/Result.java"><strong>Result</strong></a>. L’appel au <strong>Validator</strong> Bean Validation est fait de manière impérative. Son résultat (un ensemble de ConstraintViolation) permet de construire une instance de Result.</p>



<p><span style="text-decoration: underline;">Exemple en Spring</span>&nbsp;:</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><textarea class="code-block-pro-copy-button-textarea" aria-hidden="true" readonly>@PostMapping(« /owners/new »)
public String processCreationForm(@Valid Owner owner, BindingResult result, RedirectAttributes redirectAttributes) {
	if (result.hasErrors()) {
		redirectAttributes.addFlashAttribute(« error », « There was an error in creating the owner. »);
		return VIEWS_OWNER_CREATE_OR_UPDATE_FORM;
	}

	this.owners.save(owner);
	redirectAttributes.addFlashAttribute(« message », « New Owner Created »);
	return « redirect:/owners/ » + owner.getId();
}</textarea><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #ECEFF4">@</span><span style="color: #D08770">PostMapping</span><span style="color: #ECEFF4">(</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">/owners/new</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #81A1C1">public</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">String</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">processCreationForm</span><span style="color: #ECEFF4">(@</span><span style="color: #D08770">Valid</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">Owner</span><span style="color: #D8DEE9FF"> owner</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">BindingResult</span><span style="color: #D8DEE9FF"> result</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">RedirectAttributes</span><span style="color: #D8DEE9FF"> redirectAttributes</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">	</span><span style="color: #81A1C1">if</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9">result</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">hasErrors</span><span style="color: #ECEFF4">())</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">		</span><span style="color: #D8DEE9">redirectAttributes</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">addFlashAttribute</span><span style="color: #ECEFF4">(</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">error</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">There was an error in creating the owner.</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">)</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">		</span><span style="color: #81A1C1">return</span><span style="color: #D8DEE9FF"> VIEWS_OWNER_CREATE_OR_UPDATE_FORM</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">	</span><span style="color: #ECEFF4">}</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D8DEE9FF">	</span><span style="color: #81A1C1">this</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">owners</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">save</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">owner</span><span style="color: #ECEFF4">)</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">	</span><span style="color: #D8DEE9">redirectAttributes</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">addFlashAttribute</span><span style="color: #ECEFF4">(</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">message</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">New Owner Created</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">)</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">	</span><span style="color: #81A1C1">return</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">redirect:/owners/</span><span style="color: #ECEFF4">&quot;</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">+</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">owner</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">getId</span><span style="color: #ECEFF4">()</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #ECEFF4">}</span></span></code></pre></div>



<p><span style="text-decoration: underline;">Exemple équivalent en Quarkus</span>&nbsp;:</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><textarea class="code-block-pro-copy-button-textarea" aria-hidden="true" readonly>@PostMapping(« /new »)
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(« New Owner Created »));
}</textarea><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #ECEFF4">@</span><span style="color: #D08770">PostMapping</span><span style="color: #ECEFF4">(</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">/new</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #81A1C1">public</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">TemplateInstance</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">processCreationForm</span><span style="color: #ECEFF4">(</span><span style="color: #8FBCBB">Owner</span><span style="color: #D8DEE9FF"> owner</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">	</span><span style="color: #8FBCBB">Result</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">result</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">Result</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">from</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9">validator</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">validate</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">owner</span><span style="color: #ECEFF4">))</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">	</span><span style="color: #81A1C1">if</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9">result</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">hasErrors</span><span style="color: #ECEFF4">())</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">		</span><span style="color: #81A1C1">return</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">OwnerTemplates</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">createOrUpdateOwnerForm</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">owner</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> result</span><span style="color: #ECEFF4">)</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">	</span><span style="color: #ECEFF4">}</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D8DEE9FF">	</span><span style="color: #81A1C1">this</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">owners</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">save</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">owner</span><span style="color: #ECEFF4">)</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">	</span><span style="color: #81A1C1">return</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">OwnerTemplates</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">ownerDetails</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">owner</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">Result</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">success</span><span style="color: #ECEFF4">(</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">New Owner Created</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">))</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #ECEFF4">}</span></span></code></pre></div>



<p>Noter l’appel à la méthode <strong>OwnerTemplates::ownerDetails()</strong> dont nous allons étudier le fonctionnement dans le paragraphe suivant.</p>



<p>A noter également un écart de fonctionnement entre les versions Spring Boot et Quarkus de Petclinic&nbsp;: lors de la soumission d’un formulaire (POST), la version Spring utilise une <strong>redirection http </strong>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 <a href="https://docs.quarkiverse.io/quarkus-renarde/1.x/index.html#_redirects_after_post">Quarkus Renarde qui supporte les redirections</a> et le <a href="https://docs.quarkiverse.io/quarkus-renarde/1.x/index.html#_flash_scope">scope flash</a>.</p>



<p>Enfin, dans la version Spring, la classe <a href="https://github.com/spring-projects/spring-petclinic/blob/main/src/main/java/org/springframework/samples/petclinic/owner/PetValidator.java">PetValidator</a> 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&rsquo;annotations <strong>@NotNull</strong> ajoutée sur classe Pet et du support de Bean Validation.</p>



<h2 class="wp-block-heading">Templates Qute type-safe</h2>



<p>Dans la version Quarkus de Petclinic, on note l’introduction de 3 nouvelles classes annotées chacune avec <strong>@CheckedTemplate&nbsp;: </strong><a href="https://github.com/spring-petclinic/quarkus-spring-petclinic/blob/v3.21.0/src/main/java/org/springframework/samples/petclinic/owner/OwnerTemplates.java">OwnerTemplates</a>, <a href="https://github.com/spring-petclinic/quarkus-spring-petclinic/blob/v3.21.0/src/main/java/org/springframework/samples/petclinic/owner/PetTemplates.java">PetTemplates</a> et <a href="https://github.com/spring-petclinic/quarkus-spring-petclinic/blob/v3.21.0/src/main/java/org/springframework/samples/petclinic/vet/VetTemplates.java">VetTemplates</a>. 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 <a href="https://github.com/spring-petclinic/quarkus-spring-petclinic/blob/v3.21.0/src/main/java/org/springframework/samples/petclinic/owner/OwnerTemplates.java">OwnerTemplates</a>&nbsp;:</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><textarea class="code-block-pro-copy-button-textarea" aria-hidden="true" readonly>@CheckedTemplate(basePath = « owners »)
public class OwnerTemplates {

	public static native TemplateInstance findOwners(List&lt;String> errors);

	public static native TemplateInstance ownersList(List&lt;Owner> owners, int currentPage, Page&lt;Owner> page);

	public static native TemplateInstance ownerDetails(Owner owner, Result result);

	public static native TemplateInstance createOrUpdateOwnerForm(Owner owner, Result result);

}</textarea><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #ECEFF4">@</span><span style="color: #D08770">CheckedTemplate</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9">basePath</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">owners</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #81A1C1">public</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">class</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">OwnerTemplates</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D8DEE9FF">	</span><span style="color: #81A1C1">public</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">static</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">native</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">TemplateInstance</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">findOwners</span><span style="color: #ECEFF4">(</span><span style="color: #8FBCBB">List</span><span style="color: #ECEFF4">&lt;</span><span style="color: #8FBCBB">String</span><span style="color: #ECEFF4">&gt;</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">errors</span><span style="color: #ECEFF4">)</span><span style="color: #81A1C1">;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D8DEE9FF">	</span><span style="color: #81A1C1">public</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">static</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">native</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">TemplateInstance</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">ownersList</span><span style="color: #ECEFF4">(</span><span style="color: #8FBCBB">List</span><span style="color: #ECEFF4">&lt;</span><span style="color: #8FBCBB">Owner</span><span style="color: #ECEFF4">&gt;</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">owners</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">int</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">currentPage</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">Page</span><span style="color: #ECEFF4">&lt;</span><span style="color: #8FBCBB">Owner</span><span style="color: #ECEFF4">&gt;</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">page</span><span style="color: #ECEFF4">)</span><span style="color: #81A1C1">;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D8DEE9FF">	</span><span style="color: #81A1C1">public</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">static</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">native</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">TemplateInstance</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">ownerDetails</span><span style="color: #ECEFF4">(</span><span style="color: #8FBCBB">Owner</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">owner</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">Result</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">result</span><span style="color: #ECEFF4">)</span><span style="color: #81A1C1">;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D8DEE9FF">	</span><span style="color: #81A1C1">public</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">static</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">native</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">TemplateInstance</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">createOrUpdateOwnerForm</span><span style="color: #ECEFF4">(</span><span style="color: #8FBCBB">Owner</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">owner</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">Result</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">result</span><span style="color: #ECEFF4">)</span><span style="color: #81A1C1">;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #ECEFF4">}</span></span></code></pre></div>



<p>Les <strong>paramètres</strong> des méthodes correspondent au <strong>modèle de données</strong> requis lors <strong>du rendu des templates Qute</strong>. Lors du build, la classe <em>QuteProcessor</em> vérifie leur concordance. C’est la <strong>magie de Quarkus</strong>. Voici un exemple explicite d’erreur remontée si l’on omet le paramètre owners à la méthode ownersList: </p>



<pre class="wp-block-preformatted">io.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)</pre>



<p>Au niveau de l’annotation @CheckedTemplate, l’attribut <strong>basePath</strong> permet de pointer sur le <strong>répertoire templates/owners </strong>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 <em>templates/owner</em>.</p>



<h2 class="wp-block-heading">Test des contrôleurs</h2>



<p>Le test unitaire Spring Boot de la classe OwnerController utilise l’annotation <strong>@WebMvcTest</strong> pour configurer un contexte d&rsquo;application limité, ciblant uniquement les composants liés à la couche web, ceci afin de tester les endpoints HTTP sans charger l&rsquo;intégralité du contexte Spring de l&rsquo;application. <br>La classe utilitaire <strong>MockMvc</strong> permet à Spring de simuler des requêtes HTTP et de tester les contrôleurs Spring MVC sans démarrer un serveur web. </p>



<p>La migration des tests des contrôleurs REST vers Quarkus demande un peu de travail. En effet, Quarkus préconise l’utilisation de la bibilothèque <a href="https://rest-assured.io/"><strong>REST-assured</strong></a>. Cette dernière permet de tester les API REST en facilitant l&rsquo;envoi de requêtes HTTP et la vérification des réponses de manière fluide et intuitive à l’aide d’une fluent API.</p>



<p>Combinée à l’annotation <strong>@QuarkusTest</strong>, l’annotation <strong>@TestHTTPEndpoint</strong> 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 <a href="https://github.com/quarkusio/quarkus/blob/3.21.0/extensions/spring-web/core/runtime/src/main/java/io/quarkus/spring/web/runtime/SpringWebEndpointProvider.java">SpringWebEndpointProvider</a> qui s’attend à ce qu’une annotation <strong>@RequestMapping</strong> annote le contrôleur REST testé. Pour être testable, <strong>le code de prod a dû être refactoré</strong> : il a été nécessaire de déclarer une annotation <strong>@RequestMapping </strong>au top niveau de chaque contrôleur REST.</p>



<p><span style="text-decoration: underline;">Avant la mise en place du test OwnerControllerTests</span>&nbsp;:</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-highlight-color:rgba(201, 218, 248, 0.2);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><textarea class="code-block-pro-copy-button-textarea" aria-hidden="true" readonly>@RestController
class OwnerController {

	@GetMapping(« /owners/new »)
	public TemplateInstance initCreationForm() {</textarea><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #ECEFF4">@</span><span style="color: #D08770">RestController</span></span>
<span class="line"><span style="color: #81A1C1">class</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">OwnerController</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"></span>
<span class="line cbp-line-highlight"><span style="color: #D8DEE9FF">	</span><span style="color: #ECEFF4">@</span><span style="color: #D08770">GetMapping</span><span style="color: #ECEFF4">(</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">/owners/new</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">	</span><span style="color: #81A1C1">public</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">TemplateInstance</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">initCreationForm</span><span style="color: #ECEFF4">()</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span></code></pre></div>



<p><span style="text-decoration: underline;">Après la mise en place du test OwnerControllerTests</span>&nbsp;:</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-highlight-color:rgba(201, 218, 248, 0.2);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><textarea class="code-block-pro-copy-button-textarea" aria-hidden="true" readonly>@RestController
@RequestMapping(« /owners »)
class OwnerController {

	@GetMapping(« /new »)
	public TemplateInstance initCreationForm() {</textarea><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #ECEFF4">@</span><span style="color: #D08770">RestController</span></span>
<span class="line cbp-line-highlight"><span style="color: #ECEFF4">@</span><span style="color: #D08770">RequestMapping</span><span style="color: #ECEFF4">(</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">/owners</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #81A1C1">class</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">OwnerController</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"></span>
<span class="line cbp-line-highlight"><span style="color: #D8DEE9FF">	</span><span style="color: #ECEFF4">@</span><span style="color: #D08770">GetMapping</span><span style="color: #ECEFF4">(</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">/new</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">	</span><span style="color: #81A1C1">public</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">TemplateInstance</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">initCreationForm</span><span style="color: #ECEFF4">()</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span></code></pre></div>



<p>En prenant comme exemple la méthode testProcessCreationFormSuccess, vous pouvez comparer le code d&rsquo;un test migré de Spring MockMvc vers REST-assured.<br><span style="text-decoration: underline;">Test avec Spring MockMvc</span> : </p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><textarea class="code-block-pro-copy-button-textarea" aria-hidden="true" readonly>@Test
void testProcessCreationFormSuccess() throws Exception {
	mockMvc
		.perform(post(« /owners/new »).param(« firstName », « Joe »)
			.param(« lastName », « Bloggs »)
			.param(« address », « 123 Caramel Street »)
			.param(« city », « London »)
			.param(« telephone », « 1316761638 »))
		.andExpect(status().is3xxRedirection());
}</textarea><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #ECEFF4">@</span><span style="color: #D08770">Test</span></span>
<span class="line"><span style="color: #81A1C1">void</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">testProcessCreationFormSuccess</span><span style="color: #ECEFF4">()</span><span style="color: #D8DEE9FF"> throws Exception </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">	mockMvc</span></span>
<span class="line"><span style="color: #D8DEE9FF">		</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">perform</span><span style="color: #ECEFF4">(</span><span style="color: #88C0D0">post</span><span style="color: #ECEFF4">(</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">/owners/new</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">).</span><span style="color: #88C0D0">param</span><span style="color: #ECEFF4">(</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">firstName</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">Joe</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">			</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">param</span><span style="color: #ECEFF4">(</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">lastName</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">Bloggs</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">			</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">param</span><span style="color: #ECEFF4">(</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">address</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">123 Caramel Street</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">			</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">param</span><span style="color: #ECEFF4">(</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">city</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">London</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">			</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">param</span><span style="color: #ECEFF4">(</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">telephone</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">1316761638</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">))</span></span>
<span class="line"><span style="color: #D8DEE9FF">		</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">andExpect</span><span style="color: #ECEFF4">(</span><span style="color: #88C0D0">status</span><span style="color: #ECEFF4">().</span><span style="color: #88C0D0">is3xxRedirection</span><span style="color: #ECEFF4">())</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #ECEFF4">}</span></span></code></pre></div>



<p><span style="text-decoration: underline;">Test équivalent avec REST-assured</span> :</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><textarea class="code-block-pro-copy-button-textarea" aria-hidden="true" readonly>@Test
void testProcessCreationFormSuccess() {
	RestAssured
    .given()
		    .param(« firstName », « Joe »)
	      .param(« lastName », « Bloggs »)
  	    .param(« address », « 123 Caramel Street »)
		    .param(« city », « London »)
		    .param(« telephone », « 1316761638 »)
		.when()
		    .post(« /new »)
		.then()
		    .statusCode(200)
		    .body(« html.body.div.span », is(« New Owner Created »));
}</textarea><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #ECEFF4">@</span><span style="color: #D08770">Test</span></span>
<span class="line"><span style="color: #81A1C1">void</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">testProcessCreationFormSuccess</span><span style="color: #ECEFF4">()</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">	</span><span style="color: #8FBCBB">RestAssured</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">given</span><span style="color: #ECEFF4">()</span></span>
<span class="line"><span style="color: #D8DEE9FF">		    </span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">param</span><span style="color: #ECEFF4">(</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">firstName</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">Joe</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">	      </span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">param</span><span style="color: #ECEFF4">(</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">lastName</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">Bloggs</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">  	    </span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">param</span><span style="color: #ECEFF4">(</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">address</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">123 Caramel Street</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">		    </span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">param</span><span style="color: #ECEFF4">(</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">city</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">London</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">		    </span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">param</span><span style="color: #ECEFF4">(</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">telephone</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">1316761638</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">		</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">when</span><span style="color: #ECEFF4">()</span></span>
<span class="line"><span style="color: #D8DEE9FF">		    </span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">post</span><span style="color: #ECEFF4">(</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">/new</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">		</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">then</span><span style="color: #ECEFF4">()</span></span>
<span class="line"><span style="color: #D8DEE9FF">		    </span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">statusCode</span><span style="color: #ECEFF4">(</span><span style="color: #B48EAD">200</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">		    </span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">body</span><span style="color: #ECEFF4">(</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">html.body.div.span</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">is</span><span style="color: #ECEFF4">(</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">New Owner Created</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">))</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #ECEFF4">}</span></span></code></pre></div>



<h2 class="wp-block-heading">Du formatter Spring au ParamConverter JAX-RS</h2>



<p>Dans la version Spring, la classe <a href="https://github.com/spring-projects/spring-petclinic/blob/main/src/main/java/org/springframework/samples/petclinic/owner/PetTypeFormatter.java">PetTypeFormatter</a> est chargée de parser et d’afficher une instance de <a href="https://github.com/spring-petclinic/quarkus-spring-petclinic/blob/v3.21.0/src/main/java/org/springframework/samples/petclinic/owner/PetType.java">PetType</a>. Elle s’appuie sur l’interface <a href="https://github.com/spring-projects/spring-framework/blob/v6.2.5/spring-context/src/main/java/org/springframework/format/Formatter.java">Formatter</a> de Spring Framework supportée par Spring MVC.</p>



<p>La migration de cette classe vers Quarkus a consisté à utiliser l’interface <a href="https://docs.redhat.com/en/documentation/red_hat_fuse/6.3/html/apache_cxf_development_guide/restparamconverter#RESTParamConverter"><strong>ParamConverter</strong></a> de JAX-RS. Le paragraphe <a href="https://quarkus.io/guides/rest#parameter-mapping">Parameter mapping</a> du guide <a href="https://quarkus.io/guides/rest">Writing REST Services with Quarkus REST</a> explique comment implémenter une telle classe et la mettre à disposition via un provider implémentant l’interface <strong>ParamConverterProvider</strong>, ce qui a été fait à travers la classe <a href="https://github.com/spring-petclinic/quarkus-spring-petclinic/blob/v3.21.0/src/main/java/org/springframework/samples/petclinic/system/PetclinicParamConverterProvider.java">PetclinicParamConverterProvider</a>.</p>



<p>Exemple de la classe <a href="https://github.com/spring-petclinic/quarkus-spring-petclinic/blob/v3.21.0/src/main/java/org/springframework/samples/petclinic/owner/PetTypeFormatter.java">PetTypeFormatter</a>:</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><textarea class="code-block-pro-copy-button-textarea" aria-hidden="true" readonly>@Component
public class PetTypeFormatter implements ParamConverter&lt;PetType> {

	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&lt;PetType> findPetTypes = this.petTypes.findAllByOrderByName();
		for (PetType type : findPetTypes) {
			if (type.getName().equals(text)) {
				return type;
			}
		}
		throw new IllegalArgumentException(« type not found:  » + text);
	}

}</textarea><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #ECEFF4">@</span><span style="color: #D08770">Component</span></span>
<span class="line"><span style="color: #81A1C1">public</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">class</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">PetTypeFormatter</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">implements</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB; font-weight: bold">ParamConverter</span><span style="color: #ECEFF4">&lt;</span><span style="color: #8FBCBB">PetType</span><span style="color: #ECEFF4">&gt;</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D8DEE9FF">	</span><span style="color: #81A1C1">private</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">final</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">PetTypeRepository</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">petTypes</span><span style="color: #81A1C1">;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D8DEE9FF">	</span><span style="color: #81A1C1">public</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">PetTypeFormatter</span><span style="color: #ECEFF4">(</span><span style="color: #8FBCBB">PetTypeRepository</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">petTypes</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">		</span><span style="color: #81A1C1">this</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">petTypes</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> petTypes</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">	</span><span style="color: #ECEFF4">}</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D8DEE9FF">	</span><span style="color: #ECEFF4">@</span><span style="color: #D08770">Override</span></span>
<span class="line"><span style="color: #D8DEE9FF">	</span><span style="color: #81A1C1">public</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">String</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">toString</span><span style="color: #ECEFF4">(</span><span style="color: #8FBCBB">PetType</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">petType</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">		</span><span style="color: #81A1C1">return</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">petType</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">getName</span><span style="color: #ECEFF4">()</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">	</span><span style="color: #ECEFF4">}</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D8DEE9FF">	</span><span style="color: #ECEFF4">@</span><span style="color: #D08770">Override</span></span>
<span class="line"><span style="color: #D8DEE9FF">	</span><span style="color: #81A1C1">public</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">PetType</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">fromString</span><span style="color: #ECEFF4">(</span><span style="color: #8FBCBB">String</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">text</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">		</span><span style="color: #8FBCBB">Collection</span><span style="color: #ECEFF4">&lt;</span><span style="color: #8FBCBB">PetType</span><span style="color: #ECEFF4">&gt;</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">findPetTypes</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">this</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">petTypes</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">findAllByOrderByName</span><span style="color: #ECEFF4">()</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">		</span><span style="color: #81A1C1">for</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">(</span><span style="color: #8FBCBB">PetType</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">type</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">:</span><span style="color: #D8DEE9FF"> findPetTypes</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">			</span><span style="color: #81A1C1">if</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9">type</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">getName</span><span style="color: #ECEFF4">().</span><span style="color: #88C0D0">equals</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">text</span><span style="color: #ECEFF4">))</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">				</span><span style="color: #81A1C1">return</span><span style="color: #D8DEE9FF"> type</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">			</span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">		</span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">		</span><span style="color: #81A1C1">throw</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">new</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">IllegalArgumentException</span><span style="color: #ECEFF4">(</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">type not found: </span><span style="color: #ECEFF4">&quot;</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">+</span><span style="color: #D8DEE9FF"> text</span><span style="color: #ECEFF4">)</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">	</span><span style="color: #ECEFF4">}</span></span>
<span class="line"></span>
<span class="line"><span style="color: #ECEFF4">}</span></span></code></pre></div>



<p>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é.</p>



<h2 class="wp-block-heading">Conversion des dates</h2>



<p>Les formulaires de l’application Petclinic permettent de saisir la <strong>date de naissance</strong> d’un animal ainsi que sa d<strong>ate de visite</strong> à la clinique vétérinaire. Ces champs dates peuvent être laissées <strong>vides</strong>. La validation des données saisies est faite côté serveur.</p>



<p>Or, la classe <em>org.jboss.resteasy.reactive.server.core.parameters.converters.</em><strong><em>LocalDateParamConverter</em> ne supporte pas les chaines vides</strong>&nbsp;:</p>



<pre class="wp-block-preformatted">Caused by: java.time.format.DateTimeParseException: Text '' 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) ... 14 more</pre>



<p>Sur le même modèle que le PetTypeFormatter vu précédemment, la classe <a href="https://github.com/spring-petclinic/quarkus-spring-petclinic/blob/v3.21.0/src/main/java/org/springframework/samples/petclinic/system/LocalDateParamConverter.java"><strong>LocalDateParamConverter</strong></a> implémentant l’interface ParamConverter a été introduite puis déclarée dans le provider <a href="https://github.com/spring-petclinic/quarkus-spring-petclinic/blob/v3.21.0/src/main/java/org/springframework/samples/petclinic/system/PetclinicParamConverterProvider.java">PetclinicParamConverterProvider</a>.</p>



<h2 class="wp-block-heading">Cache applicatif</h2>



<p>Spring Petclinic utilise Spring Cache et <strong>Caffeine</strong> pour mettre en cache la liste des vétérinaires. La version Quarkus s’appuie sur l’<a href="https://quarkus.io/guides/spring-cache">Extension Quarkus for Spring Cache API</a> qui permet de conserver l’usage de l’annotation <strong>@Cacheable </strong>de <strong>Spring Cache</strong>. <br><br>Une <strong>différence de comportemen</strong>t 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’étaient prises en compte par Quarkus. Une correction a consisté à déplacer l’annotation @Cacheable au niveau du contrôleur <a href="https://github.com/spring-petclinic/quarkus-spring-petclinic/blob/v3.21.0/src/main/java/org/springframework/samples/petclinic/vet/VetController.java">VetController</a>&nbsp;:</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-highlight-color:rgba(201, 218, 248, 0.2);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><textarea class="code-block-pro-copy-button-textarea" aria-hidden="true" readonly>@GetMapping
@Cacheable(« vets »)
public TemplateInstance showVetList(@RequestParam(defaultValue = « 1 ») int page) {
	Vets vets = new Vets();
	Page&lt;Vet> paginated = findPaginated(page);
	vets.getVetList().addAll(paginated.toList());
	return VetTemplates.vetList(paginated.getContent(), page, paginated);
}</textarea><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #ECEFF4">@</span><span style="color: #D08770">GetMapping</span></span>
<span class="line cbp-line-highlight"><span style="color: #ECEFF4">@</span><span style="color: #D08770">Cacheable</span><span style="color: #ECEFF4">(</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">vets</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #81A1C1">public</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">TemplateInstance</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">showVetList</span><span style="color: #ECEFF4">(@</span><span style="color: #D08770">RequestParam</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9">defaultValue</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">1</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">int</span><span style="color: #D8DEE9FF"> page</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">	</span><span style="color: #8FBCBB">Vets</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">vets</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">new</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">Vets</span><span style="color: #ECEFF4">()</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">	</span><span style="color: #8FBCBB">Page</span><span style="color: #ECEFF4">&lt;</span><span style="color: #8FBCBB">Vet</span><span style="color: #ECEFF4">&gt;</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">paginated</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">findPaginated</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">page</span><span style="color: #ECEFF4">)</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">	</span><span style="color: #D8DEE9">vets</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">getVetList</span><span style="color: #ECEFF4">().</span><span style="color: #88C0D0">addAll</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9">paginated</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">toList</span><span style="color: #ECEFF4">())</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">	</span><span style="color: #81A1C1">return</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">VetTemplates</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">vetList</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9">paginated</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">getContent</span><span style="color: #ECEFF4">(),</span><span style="color: #D8DEE9FF"> page</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> paginated</span><span style="color: #ECEFF4">)</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #ECEFF4">}</span></span></code></pre></div>



<p>Devenue inutile avec Quarkus, la classe <a href="https://github.com/spring-projects/spring-petclinic/blob/main/src/main/java/org/springframework/samples/petclinic/system/CacheConfiguration.java">CacheConfiguration</a> a été supprimée.</p>



<h2 class="wp-block-heading">Gestion transactionnelle</h2>



<p>L’extension <a href="https://quarkus.io/extensions/io.quarkus/quarkus-narayana-jta/">Narayana JTA</a> apporte à Quarkus un gestionnaire de transaction JTA utilisable par Hibernate ORM.</p>



<p>L’annotation Spring <strong>org.springframework.transaction.annotation.Transactional</strong> a été remplacée par son équivalant JTA <strong>jakarta.transaction.Transactional</strong>.</p>



<p>Comme pour l’annotation @Cacheable, l’annotation <strong>@Transactional </strong>n’est pas prise en compte par Quarkus lorsqu’elle est utilisée au niveau du VetRepository. Spring Petclinic n’ayant plus de couche service, l’annotation @Transactional a été déplacée au niveau du contrôleur <a href="https://github.com/spring-petclinic/quarkus-spring-petclinic/blob/v3.21.0/src/main/java/org/springframework/samples/petclinic/vet/VetController.java">VetController</a> :</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-highlight-color:rgba(201, 218, 248, 0.2);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><textarea class="code-block-pro-copy-button-textarea" aria-hidden="true" readonly>@GetMapping
@Cacheable(« vets »)
@Transactional
public TemplateInstance showVetList(@RequestParam(defaultValue = « 1 ») int page) {
	Vets vets = new Vets();
	Page&lt;Vet> paginated = findPaginated(page);
	vets.getVetList().addAll(paginated.toList());
	return VetTemplates.vetList(paginated.getContent(), page, paginated);
}</textarea><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #ECEFF4">@</span><span style="color: #D08770">GetMapping</span></span>
<span class="line"><span style="color: #ECEFF4">@</span><span style="color: #D08770">Cacheable</span><span style="color: #ECEFF4">(</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">vets</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">)</span></span>
<span class="line cbp-line-highlight"><span style="color: #ECEFF4">@</span><span style="color: #D08770">Transactional</span></span>
<span class="line"><span style="color: #81A1C1">public</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">TemplateInstance</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">showVetList</span><span style="color: #ECEFF4">(@</span><span style="color: #D08770">RequestParam</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9">defaultValue</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">1</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">int</span><span style="color: #D8DEE9FF"> page</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">	</span><span style="color: #8FBCBB">Vets</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">vets</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">new</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">Vets</span><span style="color: #ECEFF4">()</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">	</span><span style="color: #8FBCBB">Page</span><span style="color: #ECEFF4">&lt;</span><span style="color: #8FBCBB">Vet</span><span style="color: #ECEFF4">&gt;</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">paginated</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">findPaginated</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">page</span><span style="color: #ECEFF4">)</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">	</span><span style="color: #D8DEE9">vets</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">getVetList</span><span style="color: #ECEFF4">().</span><span style="color: #88C0D0">addAll</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9">paginated</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">toList</span><span style="color: #ECEFF4">())</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">	</span><span style="color: #81A1C1">return</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">VetTemplates</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">vetList</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9">paginated</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">getContent</span><span style="color: #ECEFF4">(),</span><span style="color: #D8DEE9FF"> page</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> paginated</span><span style="color: #ECEFF4">)</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #ECEFF4">}</span></span></code></pre></div>



<h2 class="wp-block-heading">Propriétés Spring Boot</h2>



<p>Déclarée le temps de la migration puis supprimée une fois celle-ci terminée, l’<a href="https://quarkus.io/extensions/io.quarkus/quarkus-spring-boot-properties/">extension Quarkus for Spring Boot properties</a> a permis d’identifier les clés Quarkus à convertir&nbsp;dans le fichier <a href="https://github.com/spring-petclinic/quarkus-spring-petclinic/blob/v3.21.0/src/main/resources/application.properties">application.properties</a>. C’est le cas par exemple de la <strong>durée du cache des ressources statiques</strong>, configurées par défaut à 24h dans Quarkus, ramenée à 12h dans Petclinic.</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><textarea class="code-block-pro-copy-button-textarea" aria-hidden="true" readonly>// Avant
spring.web.resources.cache.cachecontrol.max-age=12h 
// Après
quarkus.http.static-resources.max-age=12h</textarea><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #616E88">// Avant</span></span>
<span class="line"><span style="color: #D8DEE9">spring</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">web</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">resources</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">cache</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">cachecontrol</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">max</span><span style="color: #81A1C1">-</span><span style="color: #D8DEE9FF">age</span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF">12h </span></span>
<span class="line"><span style="color: #616E88">// Après</span></span>
<span class="line"><span style="color: #D8DEE9">quarkus</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">http</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">static</span><span style="color: #81A1C1">-</span><span style="color: #D8DEE9">resources</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">max</span><span style="color: #81A1C1">-</span><span style="color: #D8DEE9FF">age</span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF">12h</span></span></code></pre></div>



<h2 class="wp-block-heading">Tests d’intégration avec Testcontainers</h2>



<p>En complément des tests unitaires, Spring Petclinic utilise <a href="https://testcontainers.com/"><strong>Testcontainers</strong></a> pour ses tests d’intégration avec les bases MySQL et PostgreSQL. C’est par exemple le cas du test @SpringBootTest <a href="https://github.com/spring-projects/spring-petclinic/blob/main/src/test/java/org/springframework/samples/petclinic/PostgresIntegrationTests.java">PostgresIntegrationTests</a> qui démarre une base PostgreSQL configurée dans le fichier <a href="https://github.com/spring-projects/spring-petclinic/blob/main/docker-compose.yml">docker-compose.yml</a>, utilisant à ce titre la dépendance <strong>spring-boot-docker-compose</strong>.</p>



<p>Le support par Quarkus de la bibliothèque Testcontainers est particulièrement bien aboutie et presque transparent. La version @QuarkusTest de <a href="https://github.com/spring-petclinic/quarkus-spring-petclinic/blob/v3.21.0/src/test/java/org/springframework/samples/petclinic/PostgresIntegrationTests.java">PostgresIntegrationTests</a> ressemble à un test sans Docker&nbsp;:</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><textarea class="code-block-pro-copy-button-textarea" aria-hidden="true" readonly>@QuarkusTest
@TestProfile(Profiles.Postgres.class)
class PostgresIntegrationTests {

	@Autowired
	private VetRepository vets;

	@Test
	void testFindAll() {
		vets.findAll();
	}

	@Test
	void testOwnerDetails() {
		RestAssured.when()
			.get(« /owners/1 »)
			.then()
			.statusCode(200)
			.contentType(ContentType.HTML)
			.body(containsString(« Owner Information »))
			.body(containsString(« George Franklin »))
			.body(containsString(« 110 W. Liberty St. »))
			.body(containsString(« Madison »))
			.body(containsString(« 6085551023 »))
			.body(containsString(« Leo »))
			.body(containsString(« cat »));
	}

}</textarea><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #ECEFF4">@</span><span style="color: #D08770">QuarkusTest</span></span>
<span class="line"><span style="color: #ECEFF4">@</span><span style="color: #D08770">TestProfile</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9">Profiles</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">Postgres</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">class</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #81A1C1">class</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">PostgresIntegrationTests</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D8DEE9FF">	</span><span style="color: #ECEFF4">@</span><span style="color: #D08770">Autowired</span></span>
<span class="line"><span style="color: #D8DEE9FF">	</span><span style="color: #81A1C1">private</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">VetRepository</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">vets</span><span style="color: #81A1C1">;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D8DEE9FF">	</span><span style="color: #ECEFF4">@</span><span style="color: #D08770">Test</span></span>
<span class="line"><span style="color: #D8DEE9FF">	</span><span style="color: #81A1C1">void</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">testFindAll</span><span style="color: #ECEFF4">()</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">		</span><span style="color: #D8DEE9">vets</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">findAll</span><span style="color: #ECEFF4">()</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">	</span><span style="color: #ECEFF4">}</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D8DEE9FF">	</span><span style="color: #ECEFF4">@</span><span style="color: #D08770">Test</span></span>
<span class="line"><span style="color: #D8DEE9FF">	</span><span style="color: #81A1C1">void</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">testOwnerDetails</span><span style="color: #ECEFF4">()</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">		</span><span style="color: #D8DEE9">RestAssured</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">when</span><span style="color: #ECEFF4">()</span></span>
<span class="line"><span style="color: #D8DEE9FF">			</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">get</span><span style="color: #ECEFF4">(</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">/owners/1</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">			</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">then</span><span style="color: #ECEFF4">()</span></span>
<span class="line"><span style="color: #D8DEE9FF">			</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">statusCode</span><span style="color: #ECEFF4">(</span><span style="color: #B48EAD">200</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">			</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">contentType</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9">ContentType</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">HTML</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">			</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">body</span><span style="color: #ECEFF4">(</span><span style="color: #88C0D0">containsString</span><span style="color: #ECEFF4">(</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">Owner Information</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">))</span></span>
<span class="line"><span style="color: #D8DEE9FF">			</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">body</span><span style="color: #ECEFF4">(</span><span style="color: #88C0D0">containsString</span><span style="color: #ECEFF4">(</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">George Franklin</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">))</span></span>
<span class="line"><span style="color: #D8DEE9FF">			</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">body</span><span style="color: #ECEFF4">(</span><span style="color: #88C0D0">containsString</span><span style="color: #ECEFF4">(</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">110 W. Liberty St.</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">))</span></span>
<span class="line"><span style="color: #D8DEE9FF">			</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">body</span><span style="color: #ECEFF4">(</span><span style="color: #88C0D0">containsString</span><span style="color: #ECEFF4">(</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">Madison</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">))</span></span>
<span class="line"><span style="color: #D8DEE9FF">			</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">body</span><span style="color: #ECEFF4">(</span><span style="color: #88C0D0">containsString</span><span style="color: #ECEFF4">(</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">6085551023</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">))</span></span>
<span class="line"><span style="color: #D8DEE9FF">			</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">body</span><span style="color: #ECEFF4">(</span><span style="color: #88C0D0">containsString</span><span style="color: #ECEFF4">(</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">Leo</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">))</span></span>
<span class="line"><span style="color: #D8DEE9FF">			</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">body</span><span style="color: #ECEFF4">(</span><span style="color: #88C0D0">containsString</span><span style="color: #ECEFF4">(</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">cat</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">))</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">	</span><span style="color: #ECEFF4">}</span></span>
<span class="line"></span>
<span class="line"><span style="color: #ECEFF4">}</span></span></code></pre></div>



<p>L’annotation Quarkus <strong>@TestProfile</strong> permet de référencer l’inner-class Postgres implémentant l’interface <strong>QuarkusTestProfile</strong>.</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><textarea class="code-block-pro-copy-button-textarea" aria-hidden="true" readonly>import io.quarkus.test.junit.QuarkusTestProfile;

public class Profiles {

	public static class Postgres implements QuarkusTestProfile {
		@Override
		public String getConfigProfile() {
			return « postgres-it »;
		}
	}

	public static class MySQL implements QuarkusTestProfile {
		@Override
		public String getConfigProfile() {
			return « mysql-it »;
		}
	}
}</textarea><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #81A1C1">import</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">io</span><span style="color: #ECEFF4">.</span><span style="color: #8FBCBB">quarkus</span><span style="color: #ECEFF4">.</span><span style="color: #8FBCBB">test</span><span style="color: #ECEFF4">.</span><span style="color: #8FBCBB">junit</span><span style="color: #ECEFF4">.</span><span style="color: #8FBCBB">QuarkusTestProfile</span><span style="color: #81A1C1">;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #81A1C1">public</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">class</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">Profiles</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D8DEE9FF">	</span><span style="color: #81A1C1">public</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">static</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">class</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">Postgres</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">implements</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB; font-weight: bold">QuarkusTestProfile</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">		</span><span style="color: #ECEFF4">@</span><span style="color: #D08770">Override</span></span>
<span class="line"><span style="color: #D8DEE9FF">		</span><span style="color: #81A1C1">public</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">String</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">getConfigProfile</span><span style="color: #ECEFF4">()</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">			</span><span style="color: #81A1C1">return</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">postgres-it</span><span style="color: #ECEFF4">&quot;</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">		</span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">	</span><span style="color: #ECEFF4">}</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D8DEE9FF">	</span><span style="color: #81A1C1">public</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">static</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">class</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">MySQL</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">implements</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB; font-weight: bold">QuarkusTestProfile</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">		</span><span style="color: #ECEFF4">@</span><span style="color: #D08770">Override</span></span>
<span class="line"><span style="color: #D8DEE9FF">		</span><span style="color: #81A1C1">public</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">String</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">getConfigProfile</span><span style="color: #ECEFF4">()</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">			</span><span style="color: #81A1C1">return</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">mysql-it</span><span style="color: #ECEFF4">&quot;</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">		</span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">	</span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #ECEFF4">}</span></span></code></pre></div>



<p>Notez la présence de <strong>2 profils Quarkus</strong> <strong>posgres-it&nbsp;</strong>et <strong>mysql-it</strong> dédiés aux tests d’intégrations <br>Dans le fichier <a href="https://github.com/spring-petclinic/quarkus-spring-petclinic/blob/v3.21.0/src/main/resources/application.properties">application.properties</a>, une ligne a été ajoutée pour chacun de ces profils&nbsp;:</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><textarea class="code-block-pro-copy-button-textarea" aria-hidden="true" readonly>%postgres-it.quarkus.datasource.db-kind=postgresql
%mysql-it.quarkus.datasource.db-kind=mysql</textarea><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #81A1C1">%</span><span style="color: #D8DEE9FF">postgres</span><span style="color: #81A1C1">-</span><span style="color: #D8DEE9">it</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">quarkus</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">datasource</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">db</span><span style="color: #81A1C1">-</span><span style="color: #D8DEE9FF">kind</span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF">postgresql</span></span>
<span class="line"><span style="color: #81A1C1">%</span><span style="color: #D8DEE9FF">mysql</span><span style="color: #81A1C1">-</span><span style="color: #D8DEE9">it</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">quarkus</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">datasource</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">db</span><span style="color: #81A1C1">-</span><span style="color: #D8DEE9FF">kind</span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF">mysql</span></span></code></pre></div>



<p>Ces 2 profils ont été ajoutés afin que l’URL JDBC de la base de données ne soit pas valorisée et que Quarkus utilise <strong><a href="https://quarkus.io/guides/dev-services">Dev Services</a> </strong>pour démarrer l’image Docker PostgreSQL.</p>



<h2 class="wp-block-heading">Binaire natif GraalVM</h2>



<p>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’appuyant sur GraalVM.</p>



<p>Le <a href="https://quarkus.io/guides/building-native-image">guide  Building a Native Executable</a> a permis de mettre en place facilement la génération d’un <strong>exécutable natif de Quakus Spring</strong> <strong>Petclinic</strong>. Dans le <a href="https://github.com/spring-petclinic/quarkus-spring-petclinic/blob/v3.21.0/pom.xml">pom.xml</a>, la configuration d’un profile maven <strong>native</strong> permet d’activer la propriété <strong>quarkus.native.enabled</strong>.</p>



<p>Contrairement à la version Spring Boot qui s’appuyait sur une base H2, la version Quarkus requière le démarrage d’une base PosgreSQL ou MySQL.</p>



<p>L’installation de GraalVM (ex&nbsp;: sdk install java 21-graal ) et la déclaration de la variable d’environnement GRAALVM_HOME est nécessaire.</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><textarea class="code-block-pro-copy-button-textarea" aria-hidden="true" readonly>./mvnw package -Dnative -Dquarkus.profile=postgres
docker compose up postgres 
./target/quarkus-spring-petclinic-*-runner</textarea><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #88C0D0">./mvnw</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">package</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">-Dnative</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">-Dquarkus.profile=postgres</span></span>
<span class="line"><span style="color: #88C0D0">docker</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">compose</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">up</span><span style="color: #D8DEE9FF"> </span><span style="color: #A3BE8C">postgres</span><span style="color: #D8DEE9FF"> </span></span>
<span class="line"><span style="color: #88C0D0">./target/quarkus-spring-petclinic-*-runner</span></span></code></pre></div>



<p>Quarkus Spring Petclinic démarre en 126 millisecondes : </p>



<pre class="wp-block-preformatted">2025-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<br>2025-04-13 15:54:29,755 INFO [io.quarkus] (main) Profile postgres activated.<br>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]</pre>



<h2 class="wp-block-heading">Conclusion</h2>



<p>A travers ce billet, vous aurez entre-aperçu les différentes <strong>étapes nécessaires</strong> pour <strong>migrer vers Quarkus et Qute</strong> une <strong>application Spring Web MVC</strong> avec <strong>Thymeleaf</strong> comme moteur de templating et <strong>Spring Data JPA</strong> pour la persistance. L’usage des <strong>extensions Quarkus pour Spring</strong> 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’en ai été assez bluffé. Bravo&nbsp;à eux !</p>



<p>J’ai profité de cette migration pour soumettre une dizaine de Pull Request dans la version originale de Spring Petclinic (ex&nbsp;: PR <a href="https://github.com/spring-projects/spring-petclinic/pull/1775">#1775</a>).</p>



<p>Débutant en Quarkus, je ne serais pas surpris d’apprendre par mes lecteurs des axes d’améliorations. Utilisateur et amateur de Spring depuis 20 ans, j’ai essayé de rester neutre.<strong> A vous de comparer les 2 versions de Petclinic et de vous faire votre avis</strong>. Mon ressenti personnelle est que l’éco-système Java se porte bien et que la concurrence est saine et stimulante !</p>



<h2 class="wp-block-heading">Ressources</h2>



<ul class="wp-block-list">
<li><a href="https://github.com/arey/quarkus-spring-petclinic" data-type="link" data-id="https://github.com/arey/quarkus-spring-petclinic">Repo Github Quarkus Spring Petclinic</a></li>



<li><a href="https://github.com/spring-projects/spring-petclinic">Repo Github Spring Boot Petclinic</a></li>



<li><a href="https://developers.redhat.com/articles/2021/09/20/quarkus-spring-developers-getting-started">Quarkus for Spring developers: Getting started</a></li>



<li><a href="https://developers.redhat.com/blog/2020/04/10/migrating-a-spring-boot-microservices-application-to-quarkus">Migrating a Spring Boot microservices application to Quarkus</a></li>



<li><a href="https://aytartana.wordpress.com/2020/08/26/migrating-springboot-petclinic-rest-to-quarkus/">Migrating SpringBoot PetClinic REST to Quarkus</a></li>
</ul>
]]></content:encoded>
					
					<wfw:commentRss>https://javaetmoi.com/2025/04/spring-petclinic-sous-extensions-quarkus/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Intégrer un Chatbot dans une webapp Java avec LangChain4j</title>
		<link>https://javaetmoi.com/2024/11/integrer-un-chatbot-dans-une-webapp-java-avec-langchain4j/</link>
					<comments>https://javaetmoi.com/2024/11/integrer-un-chatbot-dans-une-webapp-java-avec-langchain4j/#respond</comments>
		
		<dc:creator><![CDATA[Antoine]]></dc:creator>
		<pubDate>Mon, 11 Nov 2024 18:34:24 +0000</pubDate>
				<category><![CDATA[Retour d'expérience]]></category>
		<category><![CDATA[Spring]]></category>
		<category><![CDATA[GenAI]]></category>
		<category><![CDATA[langchain4j]]></category>
		<category><![CDATA[OpenAI]]></category>
		<category><![CDATA[Spring Boot]]></category>
		<guid isPermaLink="false">https://javaetmoi.com/?p=2391</guid>

					<description><![CDATA[Cet article explique comment intégrer un chatbot utilisant l’IA générative dans une application de gestion codée en Java. Nous nous appuierons sur le framework Open Source LangChain4j, une adaptation Java de la célèbre librairie python LangChain, visant à simplifier l&#8217;intégration de grands modèles de langage (LLM). LangChain4j permet de créer des agents conversationnels, des assistants &#8230; <a href="https://javaetmoi.com/2024/11/integrer-un-chatbot-dans-une-webapp-java-avec-langchain4j/" class="more-link">Continuer la lecture de <span class="screen-reader-text">Intégrer un Chatbot dans une webapp Java avec LangChain4j</span>  <span class="meta-nav">&#8594;</span></a>]]></description>
										<content:encoded><![CDATA[<div class="wp-block-image">
<figure class="alignright size-full is-resized"><a href="https://javaetmoi.com/wp-content/uploads/2024/11/logo_langchain4j.png"><img loading="lazy" decoding="async" width="460" height="460" src="https://javaetmoi.com/wp-content/uploads/2024/11/logo_langchain4j.png" alt="Logo du framework LangChain4j" class="wp-image-2393" style="width:282px;height:auto" srcset="https://javaetmoi.com/wp-content/uploads/2024/11/logo_langchain4j.png 460w, https://javaetmoi.com/wp-content/uploads/2024/11/logo_langchain4j-300x300.png 300w, https://javaetmoi.com/wp-content/uploads/2024/11/logo_langchain4j-150x150.png 150w" sizes="auto, (max-width: 460px) 100vw, 460px" /></a></figure>
</div>


<p>Cet article explique comment intégrer un <strong>chatbot</strong> utilisant l’<strong>IA générative </strong>dans une <strong>application de gestion</strong> codée en <strong>Java</strong>.<br><br>Nous nous appuierons sur le framework Open Source <a href="https://docs.langchain4j.dev/"><strong>LangChain4j</strong></a>, une adaptation Java de la célèbre librairie python LangChain, visant à simplifier l&rsquo;intégration de grands modèles de langage (<strong>LLM</strong>). LangChain4j permet de créer des <strong>agents conversationnels</strong>, des <strong>assistants virtuels</strong> (comme notre chatbot), ou des applications capables d&rsquo;effectuer des <strong>analyses de texte</strong> et de répondre en fonction de données contextuelles, le tout sans devoir écrire de code complexe et avec un <strong>haut niveau d’abstraction</strong>. Elle facilite notamment l&rsquo;utilisation des API des Large Langage Model comme <a href="https://docs.langchain4j.dev/integrations/language-models/open-ai">OpenAI</a> et <a href="https://docs.langchain4j.dev/integrations/language-models/hugging-face">Hugging Face</a>, et propose différents connecteurs pour des bases de données vectorielles, incluant <a href="https://docs.langchain4j.dev/integrations/embedding-stores/elasticsearch">Elasticsearch</a> et <a href="https://docs.langchain4j.dev/integrations/embedding-stores/qdrant">Qdrant</a>. Pour accélérer son intégration, LangChain4j propose des extensions pour <strong>Quarkus</strong> et des starters pour <strong>Spring Boot</strong>.</p>



<p>Pour illustrer cet article, nous utiliserons l’illustre application démo <strong>Spring Petclinic</strong> et son récent fork dédié à LangChain4j : <a href="https://github.com/spring-petclinic/spring-petclinic-langchain4j"><strong>spring-petclinic-langchain4j</strong></a><br>Propulsé par Spring Boot, Spring Petclinic s’appuie sur <strong>Spring Data JPA</strong> pour l’accès aux données et <strong>Thymeleaf</strong> pour la couche présentation HTML / CSS / JavaScript.<br>En septembre 2024, Oded Shopen, contributeur en 2020 du fork <a href="https://github.com/spring-petclinic/spring-petclinic-cloud/">Spring Petclinic Cloud</a>, <a href="https://spring.io/blog/2024/09/26/ai-meets-spring-petclinic-implementing-an-ai-assistant-with-spring-ai-part-i">a proposé une intégration de Spring AI dans Spring Petclinic</a>. De son travail, est né le projet <a href="https://github.com/spring-petclinic/spring-petclinic-ai">spring-petclinic-ai</a>. Le repository <a href="https://github.com/spring-petclinic/spring-petclinic-langchain4j">spring-petclinic-langchain4j</a> est un <strong>portage </strong>du framework<strong> <a href="https://spring.io/projects/spring-ai/">Spring AI</a> </strong>vers<strong> LangChain4j</strong>. Y a été ajouté notamment une fonctionnalité de <strong>streaming</strong>.<br>Extraits du sample, les exemples de code s’appuient sur les versions 3.3 de Spring Boot et <strong>0.35.0 de LangChaing4j</strong>.</p>



<span id="more-2391"></span>



<h2 class="wp-block-heading">Démo</h2>



<p>Avant de se plonger dans le code Java, je vous propose de voir le résultat final en visionnant ce <strong>screencast</strong> durant moins de <strong>2 minutes</strong> et dans lequel je pose <strong>4 questions </strong>à l’assistant :</p>



<figure class="wp-block-embed is-type-video is-provider-youtube wp-block-embed-youtube wp-embed-aspect-16-9 wp-has-aspect-ratio"><div class="wp-block-embed__wrapper">
<iframe loading="lazy" title="Démo du chatbot de Spring Petclinic LangChain4j" width="474" height="267" src="https://www.youtube.com/embed/hy2HDMjLr_8?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
</div></figure>



<p>Impressionnant, non&nbsp;? Lorsqu’on pose les mêmes questions en français, le chatbot répond en français.</p>



<h2 class="wp-block-heading">Compte développeur OpenAI</h2>



<p>A ce jour, l’application Spring Petclinic LangChain4j supporte OpenAI et son service hébergé sur Azure&nbsp;: Azure OpenAI. Dans cet article, nous nous focaliserons sur l’intégration <strong>OpenAI</strong>. Pour faire fonctionner ce sample, moyennant quelques euros de crédits, vous aurez besoin d’un <a href="https://platform.openai.com/docs/quickstar">compte développeur OpenAI</a> et d’une clé d’API personnelle exportée en tant que variable d’environnement <strong>OPENAI_API_KEY</strong>.</p>



<p>Si 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 <strong>demo</strong> que OpenAI fournit gratuitement. Seul le modèle <strong>gpt-4o-mini </strong>sera alors disponible avec cette clé et le nombre de <strong>tokens</strong> sera <strong>limité à 5000</strong>.</p>



<pre class="wp-block-code"><code>export OPENAI_API_KEY=demo</code></pre>



<h2 class="wp-block-heading">Déclarer les starters Spring Boot</h2>



<p>La <a href="https://docs.langchain4j.dev/tutorials/spring-boot-integration">documentation Spring Boot Integration</a> 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 <strong>application.properties</strong> (ou application.yaml).</p>



<p>Dans le pom.xml de Spring Petclinic, commençons par déclarer les deux dépendances<strong> langchain4j-spring-boot-starter </strong>et<strong> langchain4j-open-ai-spring-boot-starter</strong>&nbsp;:</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="&lt;properties&gt;
  &lt;langchain4j.version&gt;0.35.0&lt;/langchain4j.version&gt;
&lt;/properties&gt;

&lt;dependency&gt;
  &lt;groupId&gt;dev.langchain4j&lt;/groupId&gt;
  &lt;artifactId&gt;langchain4j-spring-boot-starter&lt;/artifactId&gt;
  &lt;version&gt;${langchain4j.version}&lt;/version&gt;
&lt;/dependency&gt;
&lt;dependency&gt;
  &lt;groupId&gt;dev.langchain4j&lt;/groupId&gt;
  &lt;artifactId&gt;langchain4j-open-ai-spring-boot-starter&lt;/artifactId&gt;
  &lt;version&gt;${langchain4j.version}&lt;/version&gt;
&lt;/dependency&gt;" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #81A1C1">&lt;properties&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">  </span><span style="color: #81A1C1">&lt;langchain4j.version&gt;</span><span style="color: #D8DEE9FF">0.35.0</span><span style="color: #81A1C1">&lt;/langchain4j.version&gt;</span></span>
<span class="line"><span style="color: #81A1C1">&lt;/properties&gt;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #81A1C1">&lt;dependency&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">  </span><span style="color: #81A1C1">&lt;groupId&gt;</span><span style="color: #D8DEE9FF">dev.langchain4j</span><span style="color: #81A1C1">&lt;/groupId&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">  </span><span style="color: #81A1C1">&lt;artifactId&gt;</span><span style="color: #D8DEE9FF">langchain4j-spring-boot-starter</span><span style="color: #81A1C1">&lt;/artifactId&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">  </span><span style="color: #81A1C1">&lt;version&gt;</span><span style="color: #D8DEE9FF">${langchain4j.version}</span><span style="color: #81A1C1">&lt;/version&gt;</span></span>
<span class="line"><span style="color: #81A1C1">&lt;/dependency&gt;</span></span>
<span class="line"><span style="color: #81A1C1">&lt;dependency&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">  </span><span style="color: #81A1C1">&lt;groupId&gt;</span><span style="color: #D8DEE9FF">dev.langchain4j</span><span style="color: #81A1C1">&lt;/groupId&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">  </span><span style="color: #81A1C1">&lt;artifactId&gt;</span><span style="color: #D8DEE9FF">langchain4j-open-ai-spring-boot-starter</span><span style="color: #81A1C1">&lt;/artifactId&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">  </span><span style="color: #81A1C1">&lt;version&gt;</span><span style="color: #D8DEE9FF">${langchain4j.version}</span><span style="color: #81A1C1">&lt;/version&gt;</span></span>
<span class="line"><span style="color: #81A1C1">&lt;/dependency&gt;</span></span></code></pre></div>



<p>Le premier starter <strong>langchain4j-spring-boot-starter</strong> expose la classe d’auto-configuration pour Spring Boot <a href="https://github.com/langchain4j/langchain4j-spring/blob/3fbf707037689cda90f67dc02ca54983cfd1a5ce/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/spring/LangChain4jAutoConfig.java">LangChain4jAutoConfig</a> et donne, entre autre, accès à l’annotation <a href="https://github.com/langchain4j/langchain4j-spring/blob/3fbf707037689cda90f67dc02ca54983cfd1a5ce/langchain4j-spring-boot-starter/src/main/java/dev/langchain4j/service/spring/AiService.java">@AiService</a> que nous utiliserons dans une prochaine étape.<br><br>Le second starter <strong>langchain4j-open-ai-spring-boot-starter</strong> permet quant à lui de parser et binder les propriétés spécifiques à OpenAI du fichier de configuration application.properties (ex&nbsp;: <em>langchain4j.azure-open-ai.chat-model.api-key</em>). Par transitivité, il tire les artefacts langchain4j-open-ai et dev.ai4j:openai4j. En interne, LangChain4j s’appuie sur le <strong>client Java non officiel </strong><a href="https://github.com/ai-for-java/openai4j"><strong>openai4j</strong></a> permettant de connecter des applications Java à l&rsquo;API OpenAI.</p>



<h2 class="wp-block-heading">Configuration OpenAI</h2>



<p>Dans une première version du chatbot ne faisant pas encore l’usage du streaming, ajouter au fichier <a href="https://github.com/spring-petclinic/spring-petclinic-langchain4j/blob/main/src/main/resources/application.properties">application.properties</a> les 4 propriétés suivantes&nbsp;:</p>



<pre class="wp-block-code"><code>langchain4j.open-ai.chat-model.api-key=${OPENAI_API_KEY}<br>langchain4j.open-ai.chat-model.model-name=gpt-4o<br>langchain4j.open-ai.chat-model.log-requests=true<br>langchain4j.open-ai.chat-model.log-responses=true</code></pre>



<p>Plus compact et moins cher que le <strong>modèle gpt-4o</strong> préconisé pour la démo, le modèle <strong>gpt-4o-mini</strong> peut également être utilisé et sait répondre aux exemples de questions suggérées dans le <a href="https://github.com/spring-petclinic/spring-petclinic-langchain4j/blob/main/readme.md">readme.md</a>. &nbsp;</p>



<p>Spring Boot détermine les beans à instancier en fonction des propriétés déclarées. A titre d’exemple, la classe <a href="https://github.com/langchain4j/langchain4j-spring/blob/main/langchain4j-open-ai-spring-boot-starter/src/main/java/dev/langchain4j/openai/spring/AutoConfig.java"><em>AutoConfig</em></a> du starter LangChain4j OpenAI pour Spring Boot, déclare conditionnellement un bean de type <a href="https://github.com/langchain4j/langchain4j/blob/0.35.0/langchain4j-open-ai/src/main/java/dev/langchain4j/model/openai/OpenAiChatModel.java"><em>OpenAiChatModel</em></a>implémentant l’interface agnostique <a href="https://github.com/langchain4j/langchain4j/blob/0.35.0/langchain4j-core/src/main/java/dev/langchain4j/model/chat/ChatLanguageModel.java"><strong><em>ChatLanguageModel</em></strong></a>lorsque la propriété <em>langchain4j.open-ai.<strong>chat-model</strong>.api-key</em> est déclarée. Dans la suite de cet article, nous aurons besoin d’un bean de type <a href="https://github.com/langchain4j/langchain4j/blob/0.35.0/langchain4j-core/src/main/java/dev/langchain4j/model/chat/StreamingChatLanguageModel.java"><em>StreamingChatLanguageModel</em></a> permettant de streamer la réponse du LLM token par token.&nbsp;<br>Sur le même principe, la propriété <em>langchain4j.open-ai.<strong>streaming-chat-model</strong>.api-key</em> déclenchera l’instanciation d’un bean de type <a href="https://github.com/langchain4j/langchain4j/blob/0.35.0/langchain4j-open-ai/src/main/java/dev/langchain4j/model/openai/OpenAiStreamingChatModel.java"><em>OpenAiStreamingChatModel</em></a> implémentant l’interface <em>StreamingChatLanguageModel</em>.</p>



<h2 class="wp-block-heading">Déclarer un AI Service</h2>



<p>Dans la suite de cet article, le code Java dédié au chatbot est localisé dans un package dédié&nbsp;: <a href="https://github.com/spring-petclinic/spring-petclinic-langchain4j/tree/main/src/main/java/org/springframework/samples/petclinic/chat">org.springframework.samples.petclinic.chat</a>.<br><br>Dans le code métier, l’interaction avec le LLM se fait au travers d’une simple interface Java nommée <a href="https://github.com/spring-petclinic/spring-petclinic-langchain4j/blob/main/src/main/java/org/springframework/samples/petclinic/chat/Assistant.java"><strong>Assistant</strong></a>et annotée avec l’annotation <strong>@AiService</strong>. LangChain4j propose un mécanisme similaire à Spring Data et Square Retrofit&nbsp;: 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 <a href="https://docs.langchain4j.dev/tutorials/ai-services">AI Services</a> pour davantage d’explications.<br>L’interface Assistant propose une seule et unique méthode <strong>chat</strong>. Celle-ci accepte une question de l’utilisateur et renvoie la réponse du LLM sous forme de String.</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="import dev.langchain4j.service.SystemMessage;
import dev.langchain4j.service.spring.AiService;

@AiService
interface Assistant {

    @SystemMessage(fromResource = &quot;/prompts/system.st&quot;)
    String chat(String userMessage);

}" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #81A1C1">import</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">dev</span><span style="color: #ECEFF4">.</span><span style="color: #8FBCBB">langchain4j</span><span style="color: #ECEFF4">.</span><span style="color: #8FBCBB">service</span><span style="color: #ECEFF4">.</span><span style="color: #8FBCBB">SystemMessage</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #81A1C1">import</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">dev</span><span style="color: #ECEFF4">.</span><span style="color: #8FBCBB">langchain4j</span><span style="color: #ECEFF4">.</span><span style="color: #8FBCBB">service</span><span style="color: #ECEFF4">.</span><span style="color: #8FBCBB">spring</span><span style="color: #ECEFF4">.</span><span style="color: #8FBCBB">AiService</span><span style="color: #81A1C1">;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #ECEFF4">@</span><span style="color: #D08770">AiService</span></span>
<span class="line"><span style="color: #81A1C1">interface</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">Assistant</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">@</span><span style="color: #D08770">SystemMessage</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9">fromResource</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">/prompts/system.st</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #8FBCBB">String</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">chat</span><span style="color: #ECEFF4">(</span><span style="color: #8FBCBB">String</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">userMessage</span><span style="color: #ECEFF4">)</span><span style="color: #81A1C1">;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #ECEFF4">}</span></span></code></pre></div>



<p>Le bean implémentant cette interface est mise à disposition par Spring et pourra être injecté, par exemple, dans le contrôleur REST.</p>



<h2 class="wp-block-heading">Prompter un Message Système</h2>



<p>Pour répondre à l’utilisateur, nous <strong>guidons le comportement du LLM</strong> en définissant un «&nbsp;<strong>system message</strong>&nbsp;» via l’annotation <strong>@SystemMessage</strong>.&nbsp; Les directives sont externalisées dans le fichier texte <a href="https://github.com/spring-petclinic/spring-petclinic-langchain4j/blob/main/src/main/resources/prompts/system.st">system.st</a>&nbsp;:</p>



<pre class="wp-block-preformatted">You are a friendly AI assistant designed to help with the management of a veterinarian pet clinic called Spring Petclinic.<br>Your job is to answer questions about and to perform actions on the user's behalf, mainly around<br>veterinarians, owners, owners' pets and owners' visits.<br>If you need access to pet owners or pet types, list and locate them without asking the user.<br>You are required to answer in a professional manner. If you don't know the answer, politely inform the user,<br>and then ask a follow-up question to help clarify what they are asking.<br>If you do know the answer, provide the answer but do not provide any additional followup questions.<br>When dealing with vets, if the user is unsure about the returned results, explain that there may be additional data that was not returned.<br>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.<br>For owners, pets or visits - provide the correct data.<br></pre>



<p>Comme expliqué par Oded dans son <a href="https://spring.io/blog/2024/09/26/ai-meets-spring-petclinic-implementing-an-ai-assistant-with-spring-ai-part-i">article de blog</a>, 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.<br>Par 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&nbsp;:</p>



<pre class="wp-block-preformatted">If you need access to pet owners or pet types, list and locate them without asking the user.</pre>



<p>Sans cette directive, le LLM demande l’autorisation de rechercher l’ID de Betty&nbsp;:</p>



<figure class="wp-block-image size-full"><a href="https://javaetmoi.com/wp-content/uploads/2024/11/chat0.png"><img loading="lazy" decoding="async" width="441" height="779" src="https://javaetmoi.com/wp-content/uploads/2024/11/chat0.png" alt="" class="wp-image-2408" srcset="https://javaetmoi.com/wp-content/uploads/2024/11/chat0.png 441w, https://javaetmoi.com/wp-content/uploads/2024/11/chat0-170x300.png 170w" sizes="auto, (max-width: 441px) 100vw, 441px" /></a></figure>



<h2 class="wp-block-heading">Déclarer un contrôleur REST</h2>



<p>Le chabot est appelé depuis le navigateur via une API REST. Déclarer un contrôleur Rest <a href="https://github.com/spring-petclinic/spring-petclinic-langchain4j/blob/main/src/main/java/org/springframework/samples/petclinic/chat/AssistantController.java">AssistantController</a> exposant le <strong>endpoint <em>/chat&nbsp;</em></strong>:</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="@RestController
class AssistantController {

    private final Assistant assistant;

    AssistantController(Assistant assistant) {
       this.assistant = assistant;
    }

    @PostMapping(&quot;/chat&quot;)
    public String chat(@RequestBody String query) {
       return assistant.chat(query);
    }

}" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #ECEFF4">@</span><span style="color: #D08770">RestController</span></span>
<span class="line"><span style="color: #81A1C1">class</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">AssistantController</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">private</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">final</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">Assistant</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">assistant</span><span style="color: #81A1C1">;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #88C0D0">AssistantController</span><span style="color: #ECEFF4">(</span><span style="color: #8FBCBB">Assistant</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">assistant</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">       </span><span style="color: #81A1C1">this</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">assistant</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> assistant</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">}</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">@</span><span style="color: #D08770">PostMapping</span><span style="color: #ECEFF4">(</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">/chat</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">public</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">String</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">chat</span><span style="color: #ECEFF4">(@</span><span style="color: #D08770">RequestBody</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">String</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">query</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">       </span><span style="color: #81A1C1">return</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">assistant</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">chat</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">query</span><span style="color: #ECEFF4">)</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">}</span></span>
<span class="line"></span>
<span class="line"><span style="color: #ECEFF4">}</span></span></code></pre></div>



<p></p>



<p>Démarrer l’application Spring Boot et vérifier le fonctionnement du chatbot via un simple appel curl&nbsp;:</p>



<figure class="wp-block-image size-full"><a href="https://javaetmoi.com/wp-content/uploads/2024/11/curl1.png"><img loading="lazy" decoding="async" width="945" height="70" src="https://javaetmoi.com/wp-content/uploads/2024/11/curl1.png" alt="" class="wp-image-2403" srcset="https://javaetmoi.com/wp-content/uploads/2024/11/curl1.png 945w, https://javaetmoi.com/wp-content/uploads/2024/11/curl1-300x22.png 300w, https://javaetmoi.com/wp-content/uploads/2024/11/curl1-768x57.png 768w, https://javaetmoi.com/wp-content/uploads/2024/11/curl1-500x37.png 500w" sizes="auto, (max-width: 945px) 100vw, 945px" /></a></figure>



<h2 class="wp-block-heading">Paramétrer la mémoire conversationnelle de l’assistant</h2>



<p>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&nbsp;:</p>



<figure class="wp-block-image size-full"><a href="https://javaetmoi.com/wp-content/uploads/2024/11/curl2.png"><img loading="lazy" decoding="async" width="945" height="106" src="https://javaetmoi.com/wp-content/uploads/2024/11/curl2.png" alt="" class="wp-image-2404" srcset="https://javaetmoi.com/wp-content/uploads/2024/11/curl2.png 945w, https://javaetmoi.com/wp-content/uploads/2024/11/curl2-300x34.png 300w, https://javaetmoi.com/wp-content/uploads/2024/11/curl2-768x86.png 768w, https://javaetmoi.com/wp-content/uploads/2024/11/curl2-500x56.png 500w" sizes="auto, (max-width: 945px) 100vw, 945px" /></a></figure>



<p>Pour remédier à ce problème, nous déclarons un bean Spring de type <a href="https://docs.langchain4j.dev/tutorials/ai-services#chat-memory"><strong>ChatMemory</strong></a> qui conserve l’<strong>historique des 10 derniers messages</strong>.</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="@Configuration
class AssistantConfiguration {

    @Bean
    ChatMemory chatMemory() {
       return MessageWindowChatMemory.withMaxMessages(10);
    }

}" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #ECEFF4">@</span><span style="color: #D08770">Configuration</span></span>
<span class="line"><span style="color: #81A1C1">class</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">AssistantConfiguration</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">@</span><span style="color: #D08770">Bean</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #8FBCBB">ChatMemory</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">chatMemory</span><span style="color: #ECEFF4">()</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">       </span><span style="color: #81A1C1">return</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">MessageWindowChatMemory</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">withMaxMessages</span><span style="color: #ECEFF4">(</span><span style="color: #B48EAD">10</span><span style="color: #ECEFF4">)</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">}</span></span>
<span class="line"></span>
<span class="line"><span style="color: #ECEFF4">}</span></span></code></pre></div>



<p></p>



<p>Le prénom donné lors du premier appel est désormais réutilisé par le LLM lors du deuxième appel :</p>



<figure class="wp-block-image size-full"><a href="https://javaetmoi.com/wp-content/uploads/2024/11/curl3.png"><img loading="lazy" decoding="async" width="945" height="100" src="https://javaetmoi.com/wp-content/uploads/2024/11/curl3.png" alt="" class="wp-image-2405" srcset="https://javaetmoi.com/wp-content/uploads/2024/11/curl3.png 945w, https://javaetmoi.com/wp-content/uploads/2024/11/curl3-300x32.png 300w, https://javaetmoi.com/wp-content/uploads/2024/11/curl3-768x81.png 768w, https://javaetmoi.com/wp-content/uploads/2024/11/curl3-500x53.png 500w" sizes="auto, (max-width: 945px) 100vw, 945px" /></a></figure>



<p>Par défaut, les messages sont sauvegardés en mémoire dans un <a href="https://github.com/langchain4j/langchain4j/blob/aa0e48816657640eda75879f1c29c0348643575c/langchain4j-core/src/main/java/dev/langchain4j/store/memory/chat/InMemoryChatMemoryStore.java#L15">InMemoryChatMemoryStore</a>. En cas de <strong>redémarrage</strong> de l’application, les messages volatiles sont perdus. Avec <strong>plusieurs instances</strong> 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 <a href="https://github.com/langchain4j/langchain4j/blob/0.35.0/langchain4j-core/src/main/java/dev/langchain4j/store/memory/chat/ChatMemoryStore.java">ChatMemoryStore</a> afin de <strong>persister les messages</strong> en base ou dans un cache distribué. Se référer à l’exemple <a href="https://github.com/langchain4j/langchain4j-examples/blob/main/other-examples/src/main/java/ServiceWithPersistentMemoryForEachUserExample.java">ServiceWithPersistentMemoryForEachUserExample.java</a>.</p>



<h2 class="wp-block-heading">Supporter plusieurs utilisateurs</h2>



<p>A ce stade, la même instance de <em>ChatMemory</em> est utilisée pour toutes les invocations du service d&rsquo;IA. Cette approche a des limites et ne fonctionnera pas avec plusieurs utilisateurs. Chaque utilisateur a besoin de sa propre instance de <em>ChatMemory</em> pour maintenir sa conversation individuelle.<br>Une <a href="https://docs.langchain4j.dev/tutorials/ai-services/#chat-memory">solution proposée par LangChain4j</a> consiste à utiliser un <a href="https://github.com/langchain4j/langchain4j/blob/0.35.0/langchain4j/src/main/java/dev/langchain4j/memory/chat/ChatMemoryProvider.java"><strong>ChatMemoryProvider</strong></a>&nbsp;:</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="@Configuration
class AssistantConfiguration {

	@Bean
	ChatMemoryProvider chatMemoryProvider() {
		return memoryId -&gt; MessageWindowChatMemory.withMaxMessages(10);
	}
}" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #ECEFF4">@</span><span style="color: #D08770">Configuration</span></span>
<span class="line"><span style="color: #81A1C1">class</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">AssistantConfiguration</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D8DEE9FF">	</span><span style="color: #ECEFF4">@</span><span style="color: #D08770">Bean</span></span>
<span class="line"><span style="color: #D8DEE9FF">	</span><span style="color: #8FBCBB">ChatMemoryProvider</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">chatMemoryProvider</span><span style="color: #ECEFF4">()</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">		</span><span style="color: #81A1C1">return</span><span style="color: #D8DEE9FF"> memoryId </span><span style="color: #8FBCBB">-&gt;</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">MessageWindowChatMemory</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">withMaxMessages</span><span style="color: #ECEFF4">(</span><span style="color: #B48EAD">10</span><span style="color: #ECEFF4">)</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">	</span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #ECEFF4">}</span></span></code></pre></div>



<p></p>



<p>Chaque utilisateur est associé à un <strong>memoryId</strong> qui lui est dédié et dispose donc de sa propre ChatMemory.</p>



<p>La signature de la méthode <em>chat</em> de l’interface <em>Assistant</em> prend désormais un second paramètre nommé memoryId, annoté avec l’annotation <a href="https://github.com/langchain4j/langchain4j/blob/main/langchain4j/src/main/java/dev/langchain4j/service/MemoryId.java"><strong>@MemoryId</strong></a>et de type UUID v4. Le paramètre userMessage est quant à lui annoté avec <a href="https://github.com/langchain4j/langchain4j/blob/0.35.0/langchain4j/src/main/java/dev/langchain4j/service/UserMessage.java"><strong>@UserMessage</strong></a>&nbsp;:</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="import dev.langchain4j.service.MemoryId;
import dev.langchain4j.service.SystemMessage;
import dev.langchain4j.service.spring.AiService;

@AiService
interface Assistant {

    @SystemMessage(fromResource = &quot;/prompts/system.st&quot;)
    String chat(@MemoryId UUID memoryId, @UserMessage String userMessage);

}" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #81A1C1">import</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">dev</span><span style="color: #ECEFF4">.</span><span style="color: #8FBCBB">langchain4j</span><span style="color: #ECEFF4">.</span><span style="color: #8FBCBB">service</span><span style="color: #ECEFF4">.</span><span style="color: #8FBCBB">MemoryId</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #81A1C1">import</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">dev</span><span style="color: #ECEFF4">.</span><span style="color: #8FBCBB">langchain4j</span><span style="color: #ECEFF4">.</span><span style="color: #8FBCBB">service</span><span style="color: #ECEFF4">.</span><span style="color: #8FBCBB">SystemMessage</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #81A1C1">import</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">dev</span><span style="color: #ECEFF4">.</span><span style="color: #8FBCBB">langchain4j</span><span style="color: #ECEFF4">.</span><span style="color: #8FBCBB">service</span><span style="color: #ECEFF4">.</span><span style="color: #8FBCBB">spring</span><span style="color: #ECEFF4">.</span><span style="color: #8FBCBB">AiService</span><span style="color: #81A1C1">;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #ECEFF4">@</span><span style="color: #D08770">AiService</span></span>
<span class="line"><span style="color: #81A1C1">interface</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">Assistant</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">@</span><span style="color: #D08770">SystemMessage</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9">fromResource</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">/prompts/system.st</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #8FBCBB">String</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">chat</span><span style="color: #ECEFF4">(@</span><span style="color: #D08770">MemoryId</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">UUID</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">memoryId</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">@</span><span style="color: #D08770">UserMessage</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">String</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">userMessage</span><span style="color: #ECEFF4">)</span><span style="color: #81A1C1">;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #ECEFF4">}</span></span></code></pre></div>



<p></p>



<p>Le contrôleur REST est adapté en fonction&nbsp;:</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="@PostMapping(value = &quot;/chat/{user}&quot;)
public String chat(@PathVariable UUID user, @RequestBody String query) {

    return assistant.chat(user, query);
}" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #ECEFF4">@</span><span style="color: #D08770">PostMapping</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9">value</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">/chat/{user}</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #81A1C1">public</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">String</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">chat</span><span style="color: #ECEFF4">(@</span><span style="color: #D08770">PathVariable</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">UUID</span><span style="color: #D8DEE9FF"> user</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">@</span><span style="color: #D08770">RequestBody</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">String</span><span style="color: #D8DEE9FF"> query</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">return</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">assistant</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">chat</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">user</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> query</span><span style="color: #ECEFF4">)</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #ECEFF4">}</span></span></code></pre></div>



<p></p>



<p>La <a href="https://github.com/spring-petclinic/spring-petclinic-langchain4j/pull/8">Pull Request #8 Support multiple users with @MemoryId</a> montre un exemple d’illustration côté frontend.</p>



<h2 class="wp-block-heading">Ajouter un widget de chat</h2>



<p>L’<strong>interface web </strong>du chat a été <a href="https://spring.io/blog/2024/09/27/ai-meets-spring-petclinic-implementing-an-ai-assistant-with-spring-ai-part#implementing-the-ui">designée par Oded</a>. Les codes HTML, JavaScript et CSS sont respectivement localisés dans les fichiers <a href="https://github.com/spring-petclinic/spring-petclinic-langchain4j/blob/main/src/main/resources/templates/fragments/layout.html">layout.html</a> et <a href="https://github.com/spring-petclinic/spring-petclinic-langchain4j/blob/main/src/main/resources/static/resources/js/chat.js">chat.js</a> et <a href="https://github.com/spring-petclinic/spring-petclinic-langchain4j/blob/main/src/main/resources/static/resources/css/chat.css">chat.css</a></p>



<p>Certaines réponses d’<strong>OpenAI</strong> sont formattés en <strong>Markdown</strong>.<br>Côté front, la librairie <a href="https://marked.js.org/"><strong>MarkedJS</strong></a> permet de convertir le markdown en HTML. Elle est ajoutée dans la configuration maven en tant que webjar&nbsp;:</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="&lt;dependency&gt;
  &lt;groupId&gt;org.webjars.npm&lt;/groupId&gt;
  &lt;artifactId&gt;marked&lt;/artifactId&gt;
  &lt;version&gt;${webjars-marked.version}&lt;/version&gt;
&lt;/dependency&gt;" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #81A1C1">&lt;dependency&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">  </span><span style="color: #81A1C1">&lt;groupId&gt;</span><span style="color: #D8DEE9FF">org.webjars.npm</span><span style="color: #81A1C1">&lt;/groupId&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">  </span><span style="color: #81A1C1">&lt;artifactId&gt;</span><span style="color: #D8DEE9FF">marked</span><span style="color: #81A1C1">&lt;/artifactId&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">  </span><span style="color: #81A1C1">&lt;version&gt;</span><span style="color: #D8DEE9FF">${webjars-marked.version}</span><span style="color: #81A1C1">&lt;/version&gt;</span></span>
<span class="line"><span style="color: #81A1C1">&lt;/dependency&gt;</span></span></code></pre></div>



<h2 class="wp-block-heading">Ajouter une première fonction</h2>



<p>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&rsquo;appel de fonctions personnalisées renforce la capacité des LLM à fournir des réponses plus pertinentes et contextuelles. Le LLM peut, par exemple, <strong>accéder aux données de l’application</strong>.<br>Le LLM n’appelle pas directement les fonctions&nbsp;: <strong>le modèle produit une sortie de données structurées qui spécifie le nom de la fonction</strong> à appeler ainsi que les <strong>arguments suggérés</strong>. Les fonctions sont appelées par l’application Java ayant appelée le LLM.<br>A noter que tous les LLM ne supportent pas encore l’appel de fonctions.<br><br>LangChain4j facilite et standardise l’<strong>appel de fonctions </strong>via les <a href="https://docs.langchain4j.dev/tutorials/tools"><strong>Tools</strong></a>. Deux niveaux d’abstraction sont proposés&nbsp;:</p>



<ol class="wp-block-list">
<li><strong>Low-level</strong>, en utilisant la classe <a href="https://github.com/langchain4j/langchain4j/blob/0.35.0/langchain4j-core/src/main/java/dev/langchain4j/agent/tool/ToolSpecifications.java">ToolSpecification</a> pour décrire les fonctions au LLM&nbsp;: nom, description, paramètres d’entrée / sortie.</li>



<li><strong>High-level</strong>, à l&rsquo;aide des services d’IA et des méthodes Java annotées <a href="https://github.com/langchain4j/langchain4j/blob/0.35.0/langchain4j-core/src/main/java/dev/langchain4j/agent/tool/Tool.java"><strong>@Tool</strong></a></li>
</ol>



<p>Nous mettrons en œuvre celui de haut niveau permettant d’annoter n&rsquo;importe quelle méthode Java avec l&rsquo;annotation <a href="https://github.com/langchain4j/langchain4j/blob/0.35.0/langchain4j-core/src/main/java/dev/langchain4j/agent/tool/Tool.java"><strong>@Tool</strong></a>. LangChain4j génère automatiquement les <a href="https://github.com/langchain4j/langchain4j/blob/0.35.0/langchain4j-core/src/main/java/dev/langchain4j/agent/tool/ToolSpecifications.java">ToolSpecification</a>s à partir de la signature des méthodes annotées. &nbsp;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 <a href="https://github.com/spring-petclinic/spring-petclinic-langchain4j/blob/main/src/main/java/org/springframework/samples/petclinic/chat/AssistantTool.java"><strong>AssistantTool</strong></a> 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 <strong>getAllOwners</strong> :</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="@Component
public class AssistantTool {

    private final OwnerRepository ownerRepository;

    public AssistantTool(OwnerRepository ownerRepository) {
       this.ownerRepository = ownerRepository;
    }


    @Tool(&quot;List the owners that the pet clinic has: ownerId, name, address, phone number, pets&quot;)
    public OwnersResponse getAllOwners() {
       Pageable pageable = PageRequest.of(0, 100);
       Page&lt;Owner&gt; ownerPage = ownerRepository.findAll(pageable);
       return new OwnersResponse(ownerPage.getContent());
    }

}

record OwnersResponse(List&lt;Owner&gt; owners) {
}" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #ECEFF4">@</span><span style="color: #D08770">Component</span></span>
<span class="line"><span style="color: #81A1C1">public</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">class</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">AssistantTool</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">private</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">final</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">OwnerRepository</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">ownerRepository</span><span style="color: #81A1C1">;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">public</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">AssistantTool</span><span style="color: #ECEFF4">(</span><span style="color: #8FBCBB">OwnerRepository</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">ownerRepository</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">       </span><span style="color: #81A1C1">this</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">ownerRepository</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> ownerRepository</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">}</span></span>
<span class="line"></span>
<span class="line"></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">@</span><span style="color: #D08770">Tool</span><span style="color: #ECEFF4">(</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">List the owners that the pet clinic has: ownerId, name, address, phone number, pets</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">public</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">OwnersResponse</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">getAllOwners</span><span style="color: #ECEFF4">()</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">       </span><span style="color: #8FBCBB">Pageable</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">pageable</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">PageRequest</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">of</span><span style="color: #ECEFF4">(</span><span style="color: #B48EAD">0</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #B48EAD">100</span><span style="color: #ECEFF4">)</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">       </span><span style="color: #8FBCBB">Page</span><span style="color: #ECEFF4">&lt;</span><span style="color: #8FBCBB">Owner</span><span style="color: #ECEFF4">&gt;</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">ownerPage</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">ownerRepository</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">findAll</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">pageable</span><span style="color: #ECEFF4">)</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">       </span><span style="color: #81A1C1">return</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">new</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">OwnersResponse</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9">ownerPage</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">getContent</span><span style="color: #ECEFF4">())</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">}</span></span>
<span class="line"></span>
<span class="line"><span style="color: #ECEFF4">}</span></span>
<span class="line"></span>
<span class="line"><span style="color: #81A1C1">record</span><span style="color: #D8DEE9FF"> OwnersResponse</span><span style="color: #ECEFF4">(</span><span style="color: #8FBCBB">List</span><span style="color: #81A1C1">&lt;</span><span style="color: #D8DEE9FF">Owner</span><span style="color: #81A1C1">&gt;</span><span style="color: #D8DEE9FF"> owners</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #ECEFF4">}</span></span></code></pre></div>



<p></p>



<p>En interne, la classe <em>AssistantTool</em> utilise le repository Spring Data JPA <a href="https://github.com/spring-petclinic/spring-petclinic-langchain4j/blob/main/src/main/java/org/springframework/samples/petclinic/owner/OwnerRepository.java">OwnerRepository</a> utilisé par l’application.<br>Apposée au niveau de l’annotation @Tool, <strong>la description aide le LLM à comprendre quand appeler la fonction</strong>.<br>La fonction <em>getAllOwners()</em> ne prend pas de paramètre. Elle retourne le record <em>OwnersResponse</em> qui contient une liste de <em>Owner</em>. 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.<br>Une fois la fonction appelée, LangChain4j convertit le record <em>OwnersResponse</em> au format JSON pour que le LLM puisse le traiter.</p>



<p>A noter que la méthode <em>getAllOwners</em> 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, <strong>proposer une méthode de recherche multi-critères serait préférable</strong>. C’est ce que propose l’<a href="https://github.com/spring-petclinic/spring-petclinic-langchain4j/issues/9">issue #9</a>.</p>



<p>Interrogeons à présent le chatbot avec la question <em>«&nbsp;Please list the owners that come to the clinic.&nbsp;»</em> et regardons le flux d’échange entre l’application Petclinic et OpenAI.</p>



<figure class="wp-block-image size-full"><a href="https://javaetmoi.com/wp-content/uploads/2024/11/chat1.png"><img loading="lazy" decoding="async" width="422" height="600" src="https://javaetmoi.com/wp-content/uploads/2024/11/chat1.png" alt="" class="wp-image-2407" srcset="https://javaetmoi.com/wp-content/uploads/2024/11/chat1.png 422w, https://javaetmoi.com/wp-content/uploads/2024/11/chat1-211x300.png 211w" sizes="auto, (max-width: 422px) 100vw, 422px" /></a></figure>



<p>Au préalable, dans le fichier <em>application.properties</em>, nous avons activé les logs des requêtes et réponses envoyées à OpenAI&nbsp;:</p>



<pre class="wp-block-code"><code>langchain4j.open-ai.chat-model.log-requests=true<br>langchain4j.open-ai.chat-model.log-responses=true</code></pre>



<p>Lors du 1<sup>er</sup> appel à OpenAI, à côté de la question saisie par l’utilisateur dans le fenêtre de chat, la fonction <em>getAllOwners</em> est proposée dans une liste de tools.<br><br><span style="text-decoration: underline;">Log partiel de la requête #1</span>&nbsp;:</p>



<pre class="wp-block-preformatted">- method: POST<br>- url: https://api.openai.com/v1/chat/completions<br>- headers: [Authorization: Bearer xxxx], [User-Agent: langchain4j-openai]<br>- body: {<br>  "model" : "gpt-4o",<br>  "messages" : [ {<br>    "role" : "system",<br>    "content" : "You are a friendly AI assistant designed to help with the management of a veterinarian pet clinic called Spring Petclinic…"<br>  }, {<br>    "role" : "user",<br>    "content" : "\"Please list the owners that come to the clinic.\"<br>} ],<br>  "temperature" : 0.7,<br>  <strong>"tools" </strong>: [{<br>    <strong>"type" : "function"</strong>,<br>    "function" : {<br>      "name" : "<strong>getAllOwners</strong>",<br>      "description" : "<strong>List the owners that the pet clinic has: ownerId, name, address, phone number, pets</strong>",<br>      "parameters" : {<br>        "type" : "object",<br>        "properties" : { },<br>        "required" : [ ]<br>      }<br>    }<br>  }, …<br></pre>



<p>Comme attendu, OpenAI demande à l’application d’appeler la function <em>getAllOwners</em>.<br><span style="text-decoration: underline;">Log partiel de la réponse #1</span>&nbsp;:</p>



<pre class="wp-block-preformatted">status code: 200<br>- headers: xxxx<br>- body: {<br>  "id": "chatcmpl-AOqizmPVZnGZ9jAB2of6NhayYi2mY",<br>  "object": "chat.completion",<br>  "created": 1730485909,<br>  "model": "gpt-4o-2024-08-06",<br>  "choices": [<br>    {<br>      "index": 0,<br>      "message": {<br>        "role": "assistant",<br>        "content": null,<br>        "<strong>tool_calls</strong>": [<br>          {<br>            "id": "<strong>call_6fe84CTFo3zwOvo10ZBgBqjl</strong>",<br>            "type": "function",<br>            "<strong>function</strong>": {<br>              "name": "<strong>getAllOwners</strong>",<br>              "arguments": "{}"<br>            }<br>          }<br>        ], <br>…. }</pre>



<p>LangChain4j fait aussitôt appel à la méhtode <strong><em>getAllOwners</em></strong> du bean <em>AssistantTool</em>. Le résultat est sérialisé en JSON et placé dans l’attribut <strong>content</strong>&nbsp;lors du second appel au LLM.<br><br><span style="text-decoration: underline;">Log partiel de la requête #2</span>&nbsp;:&nbsp;</p>



<pre class="wp-block-preformatted">- method: POST<br>- url: https://api.openai.com/v1/chat/completions<br>- headers: [Authorization: Bearer sk-Qw...MA], [User-Agent: langchain4j-openai]<br>- body: {<br>  "model" : "gpt-4o",<br>  "messages" : [ {<br>    "role" : "system",<br>    "content" : "You are a friendly AI …"<br>  }, {<br>    "role" : "user",<br>    "content" : "\"Please list the owners that come to the clinic"<br>  }, {<br>    "role" : "assistant",<br>    "tool_calls" : [ {<br>      "id" : "call_6fe84CTFo3zwOvo10ZBgBqjl",<br>      "type" : "function",<br>      "function" : {<br>        "name" : "getAllOwners",<br>        "arguments" : "{}"<br>      }<br>    } ]<br>  }, {<br>    <strong>"role" : "tool"</strong>,<br>    "<strong>tool_call_id</strong>" : "<strong>call_6fe84CTFo3zwOvo10ZBgBqjl</strong>",<br>    "<strong>content</strong>" : "{\n  \"<strong>owners</strong>\": [\n    {\n      \"address\": \"110 W. Liberty St.\",\n      \"city\": \"Madison\",\n      \"telephone\": \"6085551023\",\n      \"pets\": [\n        {\n          \"birthDate\": \"2010-09-07\",\n          \"type\": {\n            \"name\": \"cat\",\n            \"id\": 1\n          },\n          \"visits\": [],\n          \"name\": \"Leo\",\n          \"id\": 1\n        }\n      ],\n      \"firstName\": \"<strong>George</strong>\",\n      \"lastName\": \"<strong>Franklin</strong>\",\n      \"id\": 1\n    },\n    {\n      \"address\": \"638 Cardinal Ave.\",\n      \"city\": \"Sun Prairie\",\n      \"telephone\": \"6085551749\",\n      \"pets\": [\n        {\n          \"birthDate\": \"2012-08-06\",\n          \"type\": {\n            \"name\": \"hamster\",\n            \"id\": 6\n          },\n          \"visits\": [],\n          \"name\": \"Basil\",\n          \"id\": 2\n        }\n      ],\n      \"firstName\": \"<strong>Betty</strong>\",\n      \"lastName\": \"<strong>Davis</strong>\",\n      \"id\": 2\n,   …<br>  } ],<br>  "temperature" : 0.7,<br>  "tools" : [ { …}]<br></pre>



<p>OpenAI utilise le résultat de l’appel à la fonction <em>getAllOwners</em> pour générer une réponse présentant une liste de propriétaires d’animaux formatée en markdown&nbsp;:<br><br><span style="text-decoration: underline;">Log partiel de la réponse #2</span>&nbsp;:</p>



<pre class="wp-block-preformatted">- status code: 200<br>- headers: …<br>- body: {<br>  "id": "chatcmpl-AOqj0Y9yhJjzYtzV7QMXiBU4URkJ7",<br>  "object": "chat.completion",<br>  "created": 1730485910,<br>  "model": "gpt-4o-2024-08-06",<br>  "choices": [<br>    {<br>      "index": 0,<br>      "message": {<br>        <strong>"role": "assistant"</strong>,<br>        "<strong>content</strong>": "Here is a list of the owners at the Spring Petclinic:\n\n1. **<strong>George Franklin</strong>**\n   - Address: 110 W. Liberty St., Madison\n   - Telephone: 6085551023\n   - Pets: \n     - Leo (Cat, born on 2010-09-07)\n\n2. **<strong>Betty Davis</strong>**\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**\...",<br>        …<br>    }<br>  ],<br>  "usage": { … }<br></pre>



<p>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.</p>



<h2 class="wp-block-heading">Agent conversationnel</h2>



<p>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 : </p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p><em>&nbsp;Add a dog for Betty Davis. His name is Moopsie. His birthday is on 2 October 2024.</em></p>



<figure class="wp-block-image size-full"><a href="https://javaetmoi.com/wp-content/uploads/2024/11/agent-question.png"><img loading="lazy" decoding="async" width="383" height="545" src="https://javaetmoi.com/wp-content/uploads/2024/11/agent-question.png" alt="" class="wp-image-2415" srcset="https://javaetmoi.com/wp-content/uploads/2024/11/agent-question.png 383w, https://javaetmoi.com/wp-content/uploads/2024/11/agent-question-211x300.png 211w" sizes="auto, (max-width: 383px) 100vw, 383px" /></a></figure>
</blockquote>



<p>Dans la classe <em>AssistantTool</em>, ajoutons une seconde fonction <em>addPetToOwner</em> permettant à un vétérinaire de déclarer un nouvel animal de compagnie à l’un de ses clients&nbsp;:</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="@Tool(&quot;Add a pet with the specified petTypeId, to an owner identified by the ownerId&quot;)
public AddedPetResponse addPetToOwner(AddPetRequest request) {
    Owner owner = ownerRepository.findById(request.ownerId());
    owner.addPet(request.pet());
    this.ownerRepository.save(owner);
    return new AddedPetResponse(owner);
}" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #ECEFF4">@</span><span style="color: #D08770">Tool</span><span style="color: #ECEFF4">(</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">Add a pet with the specified petTypeId, to an owner identified by the ownerId</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #81A1C1">public</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">AddedPetResponse</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">addPetToOwner</span><span style="color: #ECEFF4">(</span><span style="color: #8FBCBB">AddPetRequest</span><span style="color: #D8DEE9FF"> request</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #8FBCBB">Owner</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">owner</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">ownerRepository</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">findById</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9">request</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">ownerId</span><span style="color: #ECEFF4">())</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #D8DEE9">owner</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">addPet</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9">request</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">pet</span><span style="color: #ECEFF4">())</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">this</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">ownerRepository</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">save</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">owner</span><span style="color: #ECEFF4">)</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">return</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">new</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">AddedPetResponse</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">owner</span><span style="color: #ECEFF4">)</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #ECEFF4">}</span></span></code></pre></div>



<p></p>



<p>Cette fois-ci, la méthode accepte un paramètre de type <a href="https://github.com/spring-petclinic/spring-petclinic-langchain4j/blob/f03f5ae28c2a0d7575fb61ce77ad74b74035ffea/src/main/java/org/springframework/samples/petclinic/chat/AssistantTool.java#L72">AddPetRequest</a>&nbsp;:</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="record AddPetRequest(Pet pet, Integer ownerId) {
}" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #81A1C1">record</span><span style="color: #D8DEE9FF"> AddPetRequest</span><span style="color: #ECEFF4">(</span><span style="color: #8FBCBB">Pet</span><span style="color: #D8DEE9FF"> pet</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">Integer</span><span style="color: #D8DEE9FF"> ownerId</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #ECEFF4">}</span></span></code></pre></div>



<p></p>



<p>Pour ajouter un animal de compagnie, le LLM doit connaitre l’identifiant du propriétaire (le <em>ownerId</em>) et les données caractérisant son compagnon. Cet identifiant peut être récupéré par le LLM via l’appel de la fonction <em>getAllOwners</em>.<br>Le LLM doit également savoir comment valoriser les attributs de la classe <a href="https://github.com/spring-petclinic/spring-petclinic-langchain4j/blob/f03f5ae28c2a0d7575fb61ce77ad74b74035ffea/src/main/java/org/springframework/samples/petclinic/owner/Pet.java">Pet</a>&nbsp;: <em>name</em>, <em>birthDate</em>, <em>visits</em> et <em>type</em>. Les identifiants du type <a href="https://github.com/spring-petclinic/spring-petclinic-langchain4j/blob/f03f5ae28c2a0d7575fb61ce77ad74b74035ffea/src/main/java/org/springframework/samples/petclinic/owner/PetType.java">PetType</a> (ex&nbsp;: 1=cat, 2=dog …) peuvent être listés par le LLM via l’appel de la nouvelle fonction <em>populatePetTypes</em>&nbsp;:</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="@Tool(&quot;List all pairs of petTypeId and pet type name&quot;)
public List&lt;PetType&gt; populatePetTypes() {
    return this.ownerRepository.findPetTypes();
}" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #ECEFF4">@</span><span style="color: #D08770">Tool</span><span style="color: #ECEFF4">(</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">List all pairs of petTypeId and pet type name</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #81A1C1">public</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">List</span><span style="color: #81A1C1">&lt;</span><span style="color: #D8DEE9FF">PetType</span><span style="color: #81A1C1">&gt;</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">populatePetTypes</span><span style="color: #ECEFF4">()</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">return</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">this</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">ownerRepository</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">findPetTypes</span><span style="color: #ECEFF4">()</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #ECEFF4">}</span></span></code></pre></div>



<p></p>



<p>Lorsque OpenAI est interrogé, dans sa première réponse, il demande à LangChain4j d’appeler 2 fonctions / tools. Optimisé, cela évitera les allers-retours&nbsp;:</p>



<p><span style="text-decoration: underline;">Log partiel de la réponse #1</span> :</p>



<pre class="wp-block-preformatted">2024-11-02T18:14:50.532+01:00 DEBUG 10650 --- [.openai.com/...] d.a.openai4j.StreamingRequestExecutor    : onEvent() {"id":"chatcmpl-APC01s26BWq4QFXC1tpIgHuSml798","object":"chat.completion.chunk","created":1730567689,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_159d8341cc","usage":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"id":"call_T0QYuwvX9NGD6kX9KxFLKrDm","type":"function","function":{"name":"<strong>getAllOwners</strong>","arguments":""}}]},"logprobs":null,"finish_reason":null}]}<br>2024-11-02T18:14:50.534+01:00 DEBUG 10650 --- [.openai.com/...] d.a.openai4j.StreamingRequestExecutor    : onEvent() {"id":"chatcmpl-APC01s26BWq4QFXC1tpIgHuSml798","object":"chat.completion.chunk","created":1730567689,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_159d8341cc","usage":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":1,"id":"call_hRf3HX1yLDIU0DtAr5Sjmov5","type":"function","function":{"name":"<strong>populatePetTypes</strong>","arguments":""}}]},"logprobs":null,"finish_reason":null}]}<br>2024-11-02T18:14:50.534+01:00 DEBUG 10650 --- [.openai.com/...] d.a.openai4j.StreamingRequestExecutor    : onEvent() {"id":"chatcmpl-APC01s26BWq4QFXC1tpIgHuSml798","object":"chat.completion.chunk","created":1730567689,"model":"gpt-4o-2024-08-06","system_fingerprint":"fp_159d8341cc","usage":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":1,"function":{"arguments":"{}"}}]},"logprobs":null,"finish_reason":null}]}</pre>



<p>LangChain4j appelle séquentiellement ces 2 fonctions (paralléliser ces appels serait un axe d’optimisation de notre application&nbsp;: <a href="https://github.com/spring-petclinic/spring-petclinic-langchain4j/issues/13">issue #13</a>) puis renvoie les résultats à OpenAI.</p>



<p><span style="text-decoration: underline;">Log partiel de la requête #2</span> :</p>



<pre class="wp-block-preformatted">  "tool_calls" : [ {<br>      "id" : "call_T0QYuwvX9NGD6kX9KxFLKrDm",<br>      "type" : "function",<br>      "function" : {<br>        "name" : "getAllOwners",<br>        "arguments" : "{}"<br>      }<br>    }, {<br>      "id" : "call_hRf3HX1yLDIU0DtAr5Sjmov5",<br>      "type" : "function",<br>      "function" : {<br>        "name" : "populatePetTypes",<br>        "arguments" : "{}"<br>      }<br>    } ]<br>  }, {<br>    "role" : "tool",<br>    "tool_call_id" : "call_T0QYuwvX9NGD6kX9KxFLKrDm",<br>    "content" : "{\n  \"owners\": [\n    {\n      \"address\": \"638 Cardinal Ave.\",\n      \"city\": \"Sun Prairie\",\n      \"telephone\": \"6085551749\",\n      \"pets\": [\n        {\n          \"birthDate\": \"2012-08-06\",\n          \"type\": {\n            \"name\": \"hamster\",\n            \"id\": 6\n          },\n          \"visits\": [],\n          \"<strong>name</strong>\": \"<strong>Basil</strong>\",\n          \"id\": 2\n        }\n      ],\n      \"<strong>firstName</strong>\": \"<strong>Betty</strong>\",\n      \"<strong>lastName</strong>\": \"<strong>Davis</strong>\",\n      \"<strong>id</strong>\": <strong>2</strong>\n    }, …\]\n}"<br>  }, {<br>    "role" : "tool",<br>    "tool_call_id" : "call_hRf3HX1yLDIU0DtAr5Sjmov5",<br>    "content" : "[\n  {\n    \"name\": \"bird\",\n    \"id\": 5\n  },\n  {\n    \"name\": \"cat\",\n    \"id\": 1\n  },\n  {\n    \"<strong>name</strong>\": \"<strong>dog</strong>\",\n    \"<strong>id</strong>\": <strong>2</strong>\n  },\n  {\n    \"name\": \"hamster\",\n    \"id\": 6\n  },\n  {\n    \"name\": \"lizard\",\n    \"id\": 3\n  },\n  {\n    \"name\": \"snake\",\n    \"id\": 4\n  }\n]"<br>  } ],<br></pre>



<p>De ces 2 appels de fonctions, OpenAI déduit l’identifiant de Betty Davis&nbsp;égal à 2 ainsi que l’identifiant d’un chien&nbsp;lui aussi égal à 2. En réponse, il demande à LangChain4j d’appeler la fonction <em>addPetToOwner</em> en lui passant ces deux identifiants, ainsi que le nom et la date de naissance donné par l’utilisateur.</p>



<pre class="wp-block-preformatted">2024-11-02T18:14:51.734+01:00 DEBUG 10650 --- [.openai.com/...] d.l.service.tool.DefaultToolExecutor     : About to execute ToolExecutionRequest { id = "call_7TdLNNZPsMD4ujev8wRytdyf", name = "<strong>addPetToOwner</strong>", <strong>arguments</strong> = "{"request":<strong>{"ownerId":2,"pet":{"name":"Moopsie","birthDate":{"year":2024,"month":10,"day":2},"type":{"id":2}}}}</strong>" } for memoryId 510e5396-3c19-46c2-991c-3200a653f90f<br>2024-11-02T18:14:51.798+01:00 DEBUG 10650 --- [.openai.com/...] d.l.service.tool.DefaultToolExecutor     : Tool execution result: {<br>  "owner": {<br>    "address": "638 Cardinal Ave.",<br>    "city": "Sun Prairie",<br>    "telephone": "6085551749",<br>    "pets": [<br>      {<br>        "birthDate": "2012-08-06",<br>        "type": {<br>          "name": "hamster",<br>          "id": 6<br>        },<br>        "visits": [],<br>        "name": "Basil",<br>        "id": 2<br>      },<br>      {<br>        "birthDate": "2024-10-02",<br>        "type": {<br>          "id": 2<br>        },<br>        "visits": [],<br>        "name": "Moopsie"<br>      }<br>    ],<br>    "firstName": "Betty",<br>    "lastName": "Davis",<br>    "id": 2<br>  }<br>}</pre>



<p>Cette fois-ci, LangChain4j doit passer un paramètre de type <em>AddPetRequest</em> lors de l’appel à la fonction <em>addPetToOwner</em>. La structure de donnée a préalablement été communiquée au LLM lors de la description de la fonction mise à sa disposition&nbsp;:</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="{
  &quot;type&quot;: &quot;function&quot;,
  &quot;function&quot;: {
    &quot;name&quot;: &quot;addPetToOwner&quot;,
    &quot;description&quot;: &quot;Add a pet with the specified petTypeId, to an owner identified by the ownerId&quot;,
    &quot;parameters&quot;: {
      &quot;type&quot;: &quot;object&quot;,
      &quot;properties&quot;: {
        &quot;request&quot;: {
          &quot;type&quot;: &quot;object&quot;,
          &quot;properties&quot;: {
            &quot;ownerId&quot;: {
              &quot;type&quot;: &quot;integer&quot;
            },
            &quot;pet&quot;: {
              &quot;type&quot;: &quot;object&quot;,
              &quot;properties&quot;: {
                &quot;visits&quot;: {
                  &quot;type&quot;: &quot;array&quot;,
                  &quot;items&quot;: {
                    &quot;type&quot;: &quot;object&quot;,
                    &quot;properties&quot;: {
                      &quot;date&quot;: {
                        &quot;type&quot;: &quot;object&quot;,
                        &quot;properties&quot;: {
                          &quot;month&quot;: {
                            &quot;type&quot;: &quot;integer&quot;
                          },
                          &quot;year&quot;: {
                            &quot;type&quot;: &quot;integer&quot;
                          },
                          &quot;day&quot;: {
                            &quot;type&quot;: &quot;integer&quot;
                          }
                        },
                        &quot;required&quot;: []
                      },
                      &quot;description&quot;: {
                        &quot;type&quot;: &quot;string&quot;
                      }
                    },
                    &quot;required&quot;: []
                  }
                },
                &quot;type&quot;: {
                  &quot;type&quot;: &quot;object&quot;,
                  &quot;properties&quot;: {},
                  &quot;required&quot;: []
                },
                &quot;birthDate&quot;: {
                  &quot;type&quot;: &quot;object&quot;,
                  &quot;properties&quot;: {
                    &quot;month&quot;: {
                      &quot;type&quot;: &quot;integer&quot;
                    },
                    &quot;year&quot;: {
                      &quot;type&quot;: &quot;integer&quot;
                    },
                    &quot;day&quot;: {
                      &quot;type&quot;: &quot;integer&quot;
                    }
                  },
                  &quot;required&quot;: []
                }
              },
              &quot;required&quot;: []
            }
          },
          &quot;required&quot;: []
        }
      },
      &quot;required&quot;: [
        &quot;request&quot;
      ]
    }
  }
}" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">  </span><span style="color: #ECEFF4">&quot;</span><span style="color: #8FBCBB">type</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">function</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">,</span></span>
<span class="line"><span style="color: #D8DEE9FF">  </span><span style="color: #ECEFF4">&quot;</span><span style="color: #8FBCBB">function</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">&quot;</span><span style="color: #8FBCBB">name</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">addPetToOwner</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">,</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">&quot;</span><span style="color: #8FBCBB">description</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">Add a pet with the specified petTypeId, to an owner identified by the ownerId</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">,</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">&quot;</span><span style="color: #8FBCBB">parameters</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">      </span><span style="color: #ECEFF4">&quot;</span><span style="color: #8FBCBB">type</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">object</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">,</span></span>
<span class="line"><span style="color: #D8DEE9FF">      </span><span style="color: #ECEFF4">&quot;</span><span style="color: #8FBCBB">properties</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #ECEFF4">&quot;</span><span style="color: #8FBCBB">request</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">          </span><span style="color: #ECEFF4">&quot;</span><span style="color: #8FBCBB">type</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">object</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">,</span></span>
<span class="line"><span style="color: #D8DEE9FF">          </span><span style="color: #ECEFF4">&quot;</span><span style="color: #8FBCBB">properties</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #ECEFF4">&quot;</span><span style="color: #8FBCBB">ownerId</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">              </span><span style="color: #ECEFF4">&quot;</span><span style="color: #8FBCBB">type</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">integer</span><span style="color: #ECEFF4">&quot;</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #ECEFF4">},</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #ECEFF4">&quot;</span><span style="color: #8FBCBB">pet</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">              </span><span style="color: #ECEFF4">&quot;</span><span style="color: #8FBCBB">type</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">object</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">,</span></span>
<span class="line"><span style="color: #D8DEE9FF">              </span><span style="color: #ECEFF4">&quot;</span><span style="color: #8FBCBB">properties</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">                </span><span style="color: #ECEFF4">&quot;</span><span style="color: #8FBCBB">visits</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">                  </span><span style="color: #ECEFF4">&quot;</span><span style="color: #8FBCBB">type</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">array</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">,</span></span>
<span class="line"><span style="color: #D8DEE9FF">                  </span><span style="color: #ECEFF4">&quot;</span><span style="color: #8FBCBB">items</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">                    </span><span style="color: #ECEFF4">&quot;</span><span style="color: #8FBCBB">type</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">object</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">,</span></span>
<span class="line"><span style="color: #D8DEE9FF">                    </span><span style="color: #ECEFF4">&quot;</span><span style="color: #8FBCBB">properties</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">                      </span><span style="color: #ECEFF4">&quot;</span><span style="color: #8FBCBB">date</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">                        </span><span style="color: #ECEFF4">&quot;</span><span style="color: #8FBCBB">type</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">object</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">,</span></span>
<span class="line"><span style="color: #D8DEE9FF">                        </span><span style="color: #ECEFF4">&quot;</span><span style="color: #8FBCBB">properties</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">                          </span><span style="color: #ECEFF4">&quot;</span><span style="color: #8FBCBB">month</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">                            </span><span style="color: #ECEFF4">&quot;</span><span style="color: #8FBCBB">type</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">integer</span><span style="color: #ECEFF4">&quot;</span></span>
<span class="line"><span style="color: #D8DEE9FF">                          </span><span style="color: #ECEFF4">},</span></span>
<span class="line"><span style="color: #D8DEE9FF">                          </span><span style="color: #ECEFF4">&quot;</span><span style="color: #8FBCBB">year</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">                            </span><span style="color: #ECEFF4">&quot;</span><span style="color: #8FBCBB">type</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">integer</span><span style="color: #ECEFF4">&quot;</span></span>
<span class="line"><span style="color: #D8DEE9FF">                          </span><span style="color: #ECEFF4">},</span></span>
<span class="line"><span style="color: #D8DEE9FF">                          </span><span style="color: #ECEFF4">&quot;</span><span style="color: #8FBCBB">day</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">                            </span><span style="color: #ECEFF4">&quot;</span><span style="color: #8FBCBB">type</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">integer</span><span style="color: #ECEFF4">&quot;</span></span>
<span class="line"><span style="color: #D8DEE9FF">                          </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">                        </span><span style="color: #ECEFF4">},</span></span>
<span class="line"><span style="color: #D8DEE9FF">                        </span><span style="color: #ECEFF4">&quot;</span><span style="color: #8FBCBB">required</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">[]</span></span>
<span class="line"><span style="color: #D8DEE9FF">                      </span><span style="color: #ECEFF4">},</span></span>
<span class="line"><span style="color: #D8DEE9FF">                      </span><span style="color: #ECEFF4">&quot;</span><span style="color: #8FBCBB">description</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">                        </span><span style="color: #ECEFF4">&quot;</span><span style="color: #8FBCBB">type</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">string</span><span style="color: #ECEFF4">&quot;</span></span>
<span class="line"><span style="color: #D8DEE9FF">                      </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">                    </span><span style="color: #ECEFF4">},</span></span>
<span class="line"><span style="color: #D8DEE9FF">                    </span><span style="color: #ECEFF4">&quot;</span><span style="color: #8FBCBB">required</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">[]</span></span>
<span class="line"><span style="color: #D8DEE9FF">                  </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">                </span><span style="color: #ECEFF4">},</span></span>
<span class="line"><span style="color: #D8DEE9FF">                </span><span style="color: #ECEFF4">&quot;</span><span style="color: #8FBCBB">type</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">                  </span><span style="color: #ECEFF4">&quot;</span><span style="color: #8FBCBB">type</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">object</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">,</span></span>
<span class="line"><span style="color: #D8DEE9FF">                  </span><span style="color: #ECEFF4">&quot;</span><span style="color: #8FBCBB">properties</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{},</span></span>
<span class="line"><span style="color: #D8DEE9FF">                  </span><span style="color: #ECEFF4">&quot;</span><span style="color: #8FBCBB">required</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">[]</span></span>
<span class="line"><span style="color: #D8DEE9FF">                </span><span style="color: #ECEFF4">},</span></span>
<span class="line"><span style="color: #D8DEE9FF">                </span><span style="color: #ECEFF4">&quot;</span><span style="color: #8FBCBB">birthDate</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">                  </span><span style="color: #ECEFF4">&quot;</span><span style="color: #8FBCBB">type</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">object</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">,</span></span>
<span class="line"><span style="color: #D8DEE9FF">                  </span><span style="color: #ECEFF4">&quot;</span><span style="color: #8FBCBB">properties</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">                    </span><span style="color: #ECEFF4">&quot;</span><span style="color: #8FBCBB">month</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">                      </span><span style="color: #ECEFF4">&quot;</span><span style="color: #8FBCBB">type</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">integer</span><span style="color: #ECEFF4">&quot;</span></span>
<span class="line"><span style="color: #D8DEE9FF">                    </span><span style="color: #ECEFF4">},</span></span>
<span class="line"><span style="color: #D8DEE9FF">                    </span><span style="color: #ECEFF4">&quot;</span><span style="color: #8FBCBB">year</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">                      </span><span style="color: #ECEFF4">&quot;</span><span style="color: #8FBCBB">type</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">integer</span><span style="color: #ECEFF4">&quot;</span></span>
<span class="line"><span style="color: #D8DEE9FF">                    </span><span style="color: #ECEFF4">},</span></span>
<span class="line"><span style="color: #D8DEE9FF">                    </span><span style="color: #ECEFF4">&quot;</span><span style="color: #8FBCBB">day</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">                      </span><span style="color: #ECEFF4">&quot;</span><span style="color: #8FBCBB">type</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">integer</span><span style="color: #ECEFF4">&quot;</span></span>
<span class="line"><span style="color: #D8DEE9FF">                    </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">                  </span><span style="color: #ECEFF4">},</span></span>
<span class="line"><span style="color: #D8DEE9FF">                  </span><span style="color: #ECEFF4">&quot;</span><span style="color: #8FBCBB">required</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">[]</span></span>
<span class="line"><span style="color: #D8DEE9FF">                </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">              </span><span style="color: #ECEFF4">},</span></span>
<span class="line"><span style="color: #D8DEE9FF">              </span><span style="color: #ECEFF4">&quot;</span><span style="color: #8FBCBB">required</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">[]</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">          </span><span style="color: #ECEFF4">},</span></span>
<span class="line"><span style="color: #D8DEE9FF">          </span><span style="color: #ECEFF4">&quot;</span><span style="color: #8FBCBB">required</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">[]</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">      </span><span style="color: #ECEFF4">},</span></span>
<span class="line"><span style="color: #D8DEE9FF">      </span><span style="color: #ECEFF4">&quot;</span><span style="color: #8FBCBB">required</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">[</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">request</span><span style="color: #ECEFF4">&quot;</span></span>
<span class="line"><span style="color: #D8DEE9FF">      </span><span style="color: #ECEFF4">]</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">  </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #ECEFF4">}</span></span></code></pre></div>



<p><br><strong>Le LLM a structuré en JSON les paramètres d’appel de fonction</strong>. La classe <a href="https://github.com/langchain4j/langchain4j/blob/0.35.0/langchain4j/src/main/java/dev/langchain4j/service/tool/DefaultToolExecutor.java">DefaultToolExecutor</a> 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).</p>



<p>Les résultats des 3 appels de fonction sont renvoyés à OpenAI dans une 3<sup>ième</sup> et dernière requête. Ce dernier conclue que l’ajout s’est bien passé et récapitule les informations enregistrées.</p>



<p>Voici un <strong>diagramme de séquences</strong> illustrant les appels que nous venons de décrire&nbsp;:</p>



<figure class="wp-block-image size-full"><a href="https://javaetmoi.com/wp-content/uploads/2024/11/LangChain4j-addPetToOwner.png"><img loading="lazy" decoding="async" width="698" height="661" src="https://javaetmoi.com/wp-content/uploads/2024/11/LangChain4j-addPetToOwner.png" alt="" class="wp-image-2412" srcset="https://javaetmoi.com/wp-content/uploads/2024/11/LangChain4j-addPetToOwner.png 698w, https://javaetmoi.com/wp-content/uploads/2024/11/LangChain4j-addPetToOwner-300x284.png 300w, https://javaetmoi.com/wp-content/uploads/2024/11/LangChain4j-addPetToOwner-317x300.png 317w" sizes="auto, (max-width: 698px) 100vw, 698px" /></a></figure>



<h2 class="wp-block-heading"><br>Response Streaming</h2>



<p>La méthode <em>chat()</em> déclarée dans le <em>@AiService</em> renvoie une simple <em>String</em>. 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.<br>La plupart des LLM propose un moyen de <strong>diffuser la réponse jeton par jeton</strong> au lieu d&rsquo;attendre que l&rsquo;ensemble du texte soit généré. Cette possibilité améliore l&rsquo;expérience de l&rsquo;utilisateur qui n&rsquo;a alors pas besoin d&rsquo;attendre une durée inconnue et peut commencer à lire la réponse presque immédiatement. LangChain4j supporte nativement cette <a href="https://docs.langchain4j.dev/tutorials/ai-services#streaming">fonctionnalité de <strong>Response Streaming</strong></a>. Il sait streamer token par token en utilisant l’interface <a href="https://github.com/langchain4j/langchain4j/blob/main/langchain4j/src/main/java/dev/langchain4j/service/TokenStream.java">TokenStream</a> 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 :</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="interface Assistant {

    @SystemMessage(fromResource = &quot;/prompts/system.st&quot;)
    TokenStream chat(@MemoryId UUID memoryId, @UserMessage String userMessage);

}" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #81A1C1">interface</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">Assistant</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">@</span><span style="color: #D08770">SystemMessage</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9">fromResource</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">/prompts/system.st</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #8FBCBB">TokenStream</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">chat</span><span style="color: #ECEFF4">(@</span><span style="color: #D08770">MemoryId</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">UUID</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">memoryId</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">@</span><span style="color: #D08770">UserMessage</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">String</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">userMessage</span><span style="color: #ECEFF4">)</span><span style="color: #81A1C1">;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #ECEFF4">}</span></span></code></pre></div>



<p></p>



<p><span style="text-decoration: underline;">Remarque</span>&nbsp;: 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 <strong><em>Flux&lt;String&gt;</em></strong> à la place de <em>TokenStream</em>.</p>



<p>Le contrôleur REST <a href="https://github.com/spring-petclinic/spring-petclinic-langchain4j/blob/v3.3.3/src/main/java/org/springframework/samples/petclinic/chat/AssistantController.java">AssistantController</a> doit à son tour être adapté. De la même manière que sur l’application web ChatGPT, nous utilisons la technologie <strong>Server Sent Events</strong> (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 <strong>SseEmitter</strong>, se référer à sa <a href="https://docs.spring.io/spring-framework/reference/web/webmvc/mvc-ann-async.html#mvc-ann-async-sse">documentation</a>.</p>



<p>Chaque token est envoyé dans un message structuré en JSON. L’onglet EventStream de Google Chrome donne un aperçu du résultat&nbsp;:</p>



<figure class="wp-block-image size-full"><a href="https://javaetmoi.com/wp-content/uploads/2024/11/event-stream-chrome.png"><img loading="lazy" decoding="async" width="945" height="654" src="https://javaetmoi.com/wp-content/uploads/2024/11/event-stream-chrome.png" alt="" class="wp-image-2420" srcset="https://javaetmoi.com/wp-content/uploads/2024/11/event-stream-chrome.png 945w, https://javaetmoi.com/wp-content/uploads/2024/11/event-stream-chrome-300x208.png 300w, https://javaetmoi.com/wp-content/uploads/2024/11/event-stream-chrome-768x532.png 768w, https://javaetmoi.com/wp-content/uploads/2024/11/event-stream-chrome-433x300.png 433w" sizes="auto, (max-width: 945px) 100vw, 945px" /></a></figure>



<p>Dans le contrôleur, l’appel à la méthode <em>chat()</em> 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.</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="@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 = &quot;/chat/{user}&quot;)
    public SseEmitter chat(@PathVariable UUID user, @RequestBody String query) {
       SseEmitter emitter = new SseEmitter();
       nonBlockingService.execute(() -&gt; assistant.chat(user, query).onNext(message -&gt; {
          try {
             sendMessage(emitter, message);
          }
          catch (IOException e) {
             LOGGER.error(&quot;Error while writing next token&quot;, e);
             emitter.completeWithError(e);
          }
       }).onComplete(token -&gt; emitter.complete()).onError(error -&gt; {
          LOGGER.error(&quot;Unexpected chat error&quot;, error);
          try {
             sendMessage(emitter, error.getMessage());
          }
          catch (IOException e) {
             LOGGER.error(&quot;Error while writing next token&quot;, 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(&quot;\n&quot;, &quot;&lt;br&gt;&quot;)
          // Escape JSON quotes
          .replace(&quot;\&quot;&quot;, &quot;\\\&quot;&quot;);
       emitter.send(&quot;{\&quot;t\&quot;: \&quot;&quot; + token + &quot;\&quot;}&quot;);
    }

}" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #ECEFF4">@</span><span style="color: #D08770">RestController</span></span>
<span class="line"><span style="color: #81A1C1">class</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">AssistantController</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">private</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">static</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">final</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">Logger</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">LOGGER</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">LoggerFactory</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">getLogger</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9">AssistantController</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">class</span><span style="color: #ECEFF4">)</span><span style="color: #81A1C1">;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">private</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">final</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">Assistant</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">assistant</span><span style="color: #81A1C1">;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">private</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">final</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">ExecutorService</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">nonBlockingService</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">Executors</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">newCachedThreadPool</span><span style="color: #ECEFF4">()</span><span style="color: #81A1C1">;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #88C0D0">AssistantController</span><span style="color: #ECEFF4">(</span><span style="color: #8FBCBB">Assistant</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">assistant</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">       </span><span style="color: #81A1C1">this</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">assistant</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> assistant</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">}</span></span>
<span class="line"></span>
<span class="line"><span style="color: #ECEFF4">    </span><span style="color: #616E88">// Using the POST method due to chat memory capabilities</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">@</span><span style="color: #D08770">PostMapping</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9">value</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">/chat/{user}</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">public</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">SseEmitter</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">chat</span><span style="color: #ECEFF4">(@</span><span style="color: #D08770">PathVariable</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">UUID</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">user</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">@</span><span style="color: #D08770">RequestBody</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">String</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">query</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">       </span><span style="color: #8FBCBB">SseEmitter</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">emitter</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">new</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">SseEmitter</span><span style="color: #ECEFF4">()</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">       </span><span style="color: #D8DEE9">nonBlockingService</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">execute</span><span style="color: #ECEFF4">(()</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">-&gt;</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">assistant</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">chat</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">user</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> query</span><span style="color: #ECEFF4">).</span><span style="color: #88C0D0">onNext</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">message </span><span style="color: #8FBCBB">-&gt;</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">          </span><span style="color: #81A1C1">try</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">             </span><span style="color: #88C0D0">sendMessage</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">emitter</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> message</span><span style="color: #ECEFF4">)</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">          </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">          </span><span style="color: #81A1C1">catch</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">(</span><span style="color: #8FBCBB">IOException</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">e</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">             </span><span style="color: #D8DEE9">LOGGER</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">error</span><span style="color: #ECEFF4">(</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">Error while writing next token</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> e</span><span style="color: #ECEFF4">)</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">             </span><span style="color: #D8DEE9">emitter</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">completeWithError</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">e</span><span style="color: #ECEFF4">)</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">          </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">       </span><span style="color: #ECEFF4">}).</span><span style="color: #88C0D0">onComplete</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">token </span><span style="color: #8FBCBB">-&gt;</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">emitter</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">complete</span><span style="color: #ECEFF4">()).</span><span style="color: #88C0D0">onError</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">error </span><span style="color: #8FBCBB">-&gt;</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">          </span><span style="color: #D8DEE9">LOGGER</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">error</span><span style="color: #ECEFF4">(</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">Unexpected chat error</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> error</span><span style="color: #ECEFF4">)</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">          </span><span style="color: #81A1C1">try</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">             </span><span style="color: #88C0D0">sendMessage</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">emitter</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">error</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">getMessage</span><span style="color: #ECEFF4">())</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">          </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">          </span><span style="color: #81A1C1">catch</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">(</span><span style="color: #8FBCBB">IOException</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">e</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">             </span><span style="color: #D8DEE9">LOGGER</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">error</span><span style="color: #ECEFF4">(</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">Error while writing next token</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> e</span><span style="color: #ECEFF4">)</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">          </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">          </span><span style="color: #D8DEE9">emitter</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">completeWithError</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">error</span><span style="color: #ECEFF4">)</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">       </span><span style="color: #ECEFF4">}).</span><span style="color: #88C0D0">start</span><span style="color: #ECEFF4">())</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">       </span><span style="color: #81A1C1">return</span><span style="color: #D8DEE9FF"> emitter</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">}</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">private</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">static</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">void</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">sendMessage</span><span style="color: #ECEFF4">(</span><span style="color: #8FBCBB">SseEmitter</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">emitter</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">String</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">message</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">throws</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">IOException</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">       </span><span style="color: #8FBCBB">String</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">token</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> message</span></span>
<span class="line"><span style="color: #ECEFF4">          </span><span style="color: #616E88">// Hack line break problem when using Server Sent Events (SSE)</span></span>
<span class="line"><span style="color: #D8DEE9FF">          </span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">replace</span><span style="color: #ECEFF4">(</span><span style="color: #ECEFF4">&quot;</span><span style="color: #EBCB8B">\n</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">&lt;br&gt;</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #ECEFF4">          </span><span style="color: #616E88">// Escape JSON quotes</span></span>
<span class="line"><span style="color: #D8DEE9FF">          </span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">replace</span><span style="color: #ECEFF4">(</span><span style="color: #ECEFF4">&quot;</span><span style="color: #EBCB8B">\&quot;</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #EBCB8B">\\\&quot;</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">)</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">       </span><span style="color: #D8DEE9">emitter</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">send</span><span style="color: #ECEFF4">(</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">{</span><span style="color: #EBCB8B">\&quot;</span><span style="color: #A3BE8C">t</span><span style="color: #EBCB8B">\&quot;</span><span style="color: #A3BE8C">: </span><span style="color: #EBCB8B">\&quot;</span><span style="color: #ECEFF4">&quot;</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">+</span><span style="color: #D8DEE9FF"> token </span><span style="color: #81A1C1">+</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">&quot;</span><span style="color: #EBCB8B">\&quot;</span><span style="color: #A3BE8C">}</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">)</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">}</span></span>
<span class="line"></span>
<span class="line"><span style="color: #ECEFF4">}</span></span></code></pre></div>



<p></p>



<p>A noter un hack (issue <a href="https://github.com/spring-petclinic/spring-petclinic-langchain4j/issues/12">#12</a>) remplaçant les sauts de ligne du LLM pour pallier au <a href="https://medium.com/@thiagosalvatore/the-line-break-problem-when-using-server-sent-events-sse-1159632d09a0">problème connu des sauts de lignes avec SSE</a>.<br><br>En interne, pour streamer la réponse du LLM, LangChain4j utilise l’interface <a href="https://github.com/langchain4j/langchain4j/blob/0.35.0/langchain4j-core/src/main/java/dev/langchain4j/model/chat/StreamingChatLanguageModel.java">StreamingChatLanguageModel</a> (à la place de ChatLanguageModel). Dans le fichier de configuration <a href="https://github.com/spring-petclinic/spring-petclinic-langchain4j/blob/v3.3.3/src/main/resources/application.properties">application.properties</a>, les propriétés langchain4j.open-ai.<strong>chat-model</strong>.xxx sont renommées en langchain4j.open-ai.<strong>streaming-chat-model</strong>.xxx&nbsp;:</p>



<pre class="wp-block-code"><code>langchain4j.open-ai.streaming-chat-model.api-key=${OPENAI_API_KEY}<br>langchain4j.open-ai.streaming-chat-model.model-name=gpt-4o<br>langchain4j.open-ai.streaming-chat-model.log-requests=true<br>langchain4j.open-ai.streaming-chat-model.log-responses=true</code></pre>



<p>Côté front, le code JavaScript du fichier <a href="https://github.com/spring-petclinic/spring-petclinic-langchain4j/blob/v3.3.3/src/main/resources/static/resources/js/chat.js#L58">chat.js</a> a été adapté pour accepter le type MIME <strong>text/event-stream</strong> et parser les messages JSON.</p>



<p>La <a href="https://github.com/spring-petclinic/spring-petclinic-langchain4j/pull/3">Pull Request #3 Response Streaming and SSE</a> décrit tous les changements appliqués côté back et front pour passer au mode streaming.</p>



<h2 class="wp-block-heading">Retrieval Augmented Generation (RAG)</h2>



<p>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 <strong>génération augmentée par récupération</strong>, 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&rsquo;il s&rsquo;agit de données plus récentes ou de <strong>contenus privés non accessibles lors de son entraînement</strong>.<br>Un 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).</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p><span style="text-decoration: underline;">Question</span> : « I&rsquo;m looking for a veterinarian who specializes in both radiography and odontology for my pet »</p>
</blockquote>



<p>A 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.</p>



<figure class="wp-block-image size-full"><a href="https://javaetmoi.com/wp-content/uploads/2024/11/langchain4j-question-llm.png"><img loading="lazy" decoding="async" width="935" height="466" src="https://javaetmoi.com/wp-content/uploads/2024/11/langchain4j-question-llm.png" alt="" class="wp-image-2428" srcset="https://javaetmoi.com/wp-content/uploads/2024/11/langchain4j-question-llm.png 935w, https://javaetmoi.com/wp-content/uploads/2024/11/langchain4j-question-llm-300x150.png 300w, https://javaetmoi.com/wp-content/uploads/2024/11/langchain4j-question-llm-768x383.png 768w, https://javaetmoi.com/wp-content/uploads/2024/11/langchain4j-question-llm-500x249.png 500w" sizes="auto, (max-width: 935px) 100vw, 935px" /></a></figure>



<p>Pour intégrer le RAG à Petclinic, nous devons procéder en 2 étapes : la phase d’<strong>ingestion (indexation)</strong> des vétérinaires et la phase de <strong>requêtage</strong> (retrieval en anglais). La <a href="https://docs.langchain4j.dev/tutorials/rag">documentation de LangChain4j sur le support des RAG</a> propose deux diagrammes illustrant les étapes d’<a href="https://docs.langchain4j.dev/tutorials/rag#indexing">indexation</a> et de <a href="https://docs.langchain4j.dev/tutorials/rag#retrieval">retrieval</a>.</p>



<h2 class="wp-block-heading">Ingestion d’embeddings</h2>



<p>Afin de pouvoir être utilisées par le LLM, les données des 3 tables <em>vets</em>, <em>specialties</em> et <em>vet_specialties</em> doivent préalablement être ingérées et stockées dans une base de données vectorielle. PostgreSQL avec l&rsquo;extension <a href="https://github.com/pgvector/pgvector">pgVector</a> est probablement le choix le plus populaire. Greenplum et Qdrant sont 2 autres bases de données vectorielles. <a href="https://docs.langchain4j.dev/integrations/embedding-stores/">LangChain4j supporte plus de 25 bases vectorielles</a> avec des niveaux plus ou moins avancés.</p>



<p>Lors 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 <strong>embedding</strong> puis stockés dans la base vectorielle. La documentation de LangChain4j parle <strong>d’Embedding Stores</strong>. Pour notre application d’exemple, par simplicité, nous allons utiliser la <a href="https://docs.langchain4j.dev/integrations/embedding-stores/in-memory">base vectorielle en mémoire proposée par LangChain4j</a>. Dans la classe de configuration Spring <a href="https://github.com/spring-petclinic/spring-petclinic-langchain4j/blob/v3.3.3/src/main/java/org/springframework/samples/petclinic/chat/AssistantConfiguration.java">AssistantConfiguration</a>, commençons par déclarer le bean de type <a href="https://docs.langchain4j.dev/integrations/embedding-stores/in-memory">InMemoryEmbeddingStore</a>&nbsp;:</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="@Bean
InMemoryEmbeddingStore&lt;TextSegment&gt; embeddingStore() {
    return new InMemoryEmbeddingStore&lt;&gt;();
}" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #ECEFF4">@</span><span style="color: #D08770">Bean</span></span>
<span class="line"><span style="color: #8FBCBB">InMemoryEmbeddingStore</span><span style="color: #81A1C1">&lt;</span><span style="color: #D8DEE9FF">TextSegment</span><span style="color: #81A1C1">&gt;</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">embeddingStore</span><span style="color: #ECEFF4">()</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">return</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">new</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">InMemoryEmbeddingStore</span><span style="color: #ECEFF4">&lt;&gt;()</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #ECEFF4">}</span></span></code></pre></div>



<p><br>Nous devons ensuite choisir un modèle de embedding. <a href="https://docs.langchain4j.dev/category/embedding-models">LangChain4j en supporte plus de 19</a>. J’ai opté pour un <a href="https://docs.langchain4j.dev/integrations/embedding-models/in-process">modèle de type in-process</a> basé sur le runtime <a href="https://onnxruntime.ai/docs/get-started/with-java.html">ONNX</a>. Ce type de modèle présente l’avantage de pouvoir s’exécuter dans la même JVM que celle de Petclinic.<br>Le repo git <a href="https://github.com/langchain4j/langchain4j-embeddings">langchain4j-embeddings</a> propose une douzaine d’artefact (JAR) embarquant chacun un modèle au f<strong>ormat .onnx</strong>. Parmis eux, on retrouve l’artefact <a href="https://github.com/langchain4j/langchain4j-embeddings/tree/main/langchain4j-embeddings-all-minilm-l6-v2">langchain4j-embeddings-all-minilm-l6-v2</a>.</p>



<p>Le modèle <a href="https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2"><strong>all-MiniLM-L6-v2</strong></a> est un modèle de langage basé sur la famille <a href="https://github.com/microsoft/unilm/tree/master/minilm">MiniLM</a> 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&rsquo;encodage de phrases, tout en restant léger et rapide. Il semble parfait pour notre <strong>chatbot</strong> et la <strong>recherche de similarité</strong>.</p>



<p>Une fois le choix du modèle arrêté, ajoutons sa dépendance dans le pom.xml&nbsp;:</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="&lt;dependency&gt;
  &lt;groupId&gt;dev.langchain4j&lt;/groupId&gt;
  &lt;artifactId&gt;langchain4j-embeddings-all-minilm-l6-v2&lt;/artifactId&gt;
  &lt;version&gt;${langchain4j.version}&lt;/version&gt;
&lt;/dependency&gt;" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #81A1C1">&lt;dependency&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">  </span><span style="color: #81A1C1">&lt;groupId&gt;</span><span style="color: #D8DEE9FF">dev.langchain4j</span><span style="color: #81A1C1">&lt;/groupId&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">  </span><span style="color: #81A1C1">&lt;artifactId&gt;</span><span style="color: #D8DEE9FF">langchain4j-embeddings-all-minilm-l6-v2</span><span style="color: #81A1C1">&lt;/artifactId&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">  </span><span style="color: #81A1C1">&lt;version&gt;</span><span style="color: #D8DEE9FF">${langchain4j.version}</span><span style="color: #81A1C1">&lt;/version&gt;</span></span>
<span class="line"><span style="color: #81A1C1">&lt;/dependency&gt;</span></span></code></pre></div>



<p><br>Dans la classe de configuration Spring <a href="https://github.com/spring-petclinic/spring-petclinic-langchain4j/blob/v3.3.3/src/main/java/org/springframework/samples/petclinic/chat/AssistantConfiguration.java">AssistantConfiguration</a>, déclarons un bean de type <a href="https://docs.langchain4j.dev/tutorials/rag#embedding-model">EmbeddingModel</a> :</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="@Bean
EmbeddingModel embeddingModel() {
    return new AllMiniLmL6V2EmbeddingModel();
}" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #ECEFF4">@</span><span style="color: #D08770">Bean</span></span>
<span class="line"><span style="color: #8FBCBB">EmbeddingModel</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">embeddingModel</span><span style="color: #ECEFF4">()</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">return</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">new</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">AllMiniLmL6V2EmbeddingModel</span><span style="color: #ECEFF4">()</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #ECEFF4">}</span></span></code></pre></div>



<p><br>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 <a href="https://github.com/spring-petclinic/spring-petclinic-langchain4j/blob/v3.3.3/src/main/java/org/springframework/samples/petclinic/chat/EmbeddingStoreInit.java">EmbeddingStoreInit</a> :</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="@Component
public class EmbeddingStoreInit {

    private final Logger logger = LoggerFactory.getLogger(EmbeddingStoreInit.class);

    private final InMemoryEmbeddingStore&lt;TextSegment&gt; embeddingStore;

    private final EmbeddingModel embeddingModel;

    private final VetRepository vetRepository;

    public EmbeddingStoreInit(InMemoryEmbeddingStore&lt;TextSegment&gt; 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&lt;Vet&gt; 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&lt;Vet&gt; vets) {
       ObjectMapper objectMapper = new ObjectMapper();
       try {
          // Convert List&lt;Vet&gt; to JSON string
          StringBuilder jsonArray = new StringBuilder();
          for (Vet vet : vets) {
             String jsonElement = objectMapper.writeValueAsString(vet);
             jsonArray.append(jsonElement).append(&quot;\n&quot;); // For use of the
                                              // DocumentByLineSplitter
          }
          return jsonArray.toString();
       }
       catch (JsonProcessingException e) {
          logger.error(&quot;Problems encountered when generating JSON from the vets list&quot;, e);
          return null;
       }
    }

}" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #ECEFF4">@</span><span style="color: #D08770">Component</span></span>
<span class="line"><span style="color: #81A1C1">public</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">class</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">EmbeddingStoreInit</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">private</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">final</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">Logger</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">logger</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">LoggerFactory</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">getLogger</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9">EmbeddingStoreInit</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">class</span><span style="color: #ECEFF4">)</span><span style="color: #81A1C1">;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">private</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">final</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">InMemoryEmbeddingStore</span><span style="color: #ECEFF4">&lt;</span><span style="color: #8FBCBB">TextSegment</span><span style="color: #ECEFF4">&gt;</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">embeddingStore</span><span style="color: #81A1C1">;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">private</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">final</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">EmbeddingModel</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">embeddingModel</span><span style="color: #81A1C1">;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">private</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">final</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">VetRepository</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">vetRepository</span><span style="color: #81A1C1">;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">public</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">EmbeddingStoreInit</span><span style="color: #ECEFF4">(</span><span style="color: #8FBCBB">InMemoryEmbeddingStore</span><span style="color: #ECEFF4">&lt;</span><span style="color: #8FBCBB">TextSegment</span><span style="color: #ECEFF4">&gt;</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">embeddingStore</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">EmbeddingModel</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">embeddingModel</span><span style="color: #ECEFF4">,</span></span>
<span class="line"><span style="color: #D8DEE9FF">          </span><span style="color: #8FBCBB">VetRepository</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">vetRepository</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">       </span><span style="color: #81A1C1">this</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">embeddingStore</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> embeddingStore</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">       </span><span style="color: #81A1C1">this</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">embeddingModel</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> embeddingModel</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">       </span><span style="color: #81A1C1">this</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">vetRepository</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> vetRepository</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">}</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">@</span><span style="color: #D08770">EventListener</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">public</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">void</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">loadVetDataToEmbeddingStoreOnStartup</span><span style="color: #ECEFF4">(</span><span style="color: #8FBCBB">ApplicationStartedEvent</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">event</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">       </span><span style="color: #8FBCBB">Pageable</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">pageable</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">PageRequest</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">of</span><span style="color: #ECEFF4">(</span><span style="color: #B48EAD">0</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">Integer</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">MAX_VALUE</span><span style="color: #ECEFF4">)</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">       </span><span style="color: #8FBCBB">Page</span><span style="color: #ECEFF4">&lt;</span><span style="color: #8FBCBB">Vet</span><span style="color: #ECEFF4">&gt;</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">vetsPage</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">vetRepository</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">findAll</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">pageable</span><span style="color: #ECEFF4">)</span><span style="color: #81A1C1">;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D8DEE9FF">       </span><span style="color: #8FBCBB">String</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">vetsAsJson</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">convertListToJson</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9">vetsPage</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">getContent</span><span style="color: #ECEFF4">())</span><span style="color: #81A1C1">;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D8DEE9FF">       </span><span style="color: #8FBCBB">EmbeddingStoreIngestor</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">ingestor</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">EmbeddingStoreIngestor</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">builder</span><span style="color: #ECEFF4">()</span></span>
<span class="line"><span style="color: #D8DEE9FF">          </span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">documentSplitter</span><span style="color: #ECEFF4">(</span><span style="color: #81A1C1">new</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">DocumentByLineSplitter</span><span style="color: #ECEFF4">(</span><span style="color: #B48EAD">1000</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #B48EAD">200</span><span style="color: #ECEFF4">))</span></span>
<span class="line"><span style="color: #D8DEE9FF">          </span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">embeddingModel</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">embeddingModel</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">          </span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">embeddingStore</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">embeddingStore</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">          </span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">build</span><span style="color: #ECEFF4">()</span><span style="color: #81A1C1">;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D8DEE9FF">       </span><span style="color: #D8DEE9">ingestor</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">ingest</span><span style="color: #ECEFF4">(</span><span style="color: #81A1C1">new</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">Document</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">vetsAsJson</span><span style="color: #ECEFF4">))</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">}</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">public</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">String</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">convertListToJson</span><span style="color: #ECEFF4">(</span><span style="color: #8FBCBB">List</span><span style="color: #ECEFF4">&lt;</span><span style="color: #8FBCBB">Vet</span><span style="color: #ECEFF4">&gt;</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">vets</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">       </span><span style="color: #8FBCBB">ObjectMapper</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">objectMapper</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">new</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">ObjectMapper</span><span style="color: #ECEFF4">()</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">       </span><span style="color: #81A1C1">try</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #ECEFF4">          </span><span style="color: #616E88">// Convert List&lt;Vet&gt; to JSON string</span></span>
<span class="line"><span style="color: #D8DEE9FF">          </span><span style="color: #8FBCBB">StringBuilder</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">jsonArray</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">new</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">StringBuilder</span><span style="color: #ECEFF4">()</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">          </span><span style="color: #81A1C1">for</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">(</span><span style="color: #8FBCBB">Vet</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">vet</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">:</span><span style="color: #D8DEE9FF"> vets</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">             </span><span style="color: #8FBCBB">String</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">jsonElement</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">objectMapper</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">writeValueAsString</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">vet</span><span style="color: #ECEFF4">)</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">             </span><span style="color: #D8DEE9">jsonArray</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">append</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">jsonElement</span><span style="color: #ECEFF4">).</span><span style="color: #88C0D0">append</span><span style="color: #ECEFF4">(</span><span style="color: #ECEFF4">&quot;</span><span style="color: #EBCB8B">\n</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">)</span><span style="color: #81A1C1">;</span><span style="color: #D8DEE9FF"> </span><span style="color: #616E88">// For use of the</span></span>
<span class="line"><span style="color: #ECEFF4">                                              </span><span style="color: #616E88">// DocumentByLineSplitter</span></span>
<span class="line"><span style="color: #D8DEE9FF">          </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">          </span><span style="color: #81A1C1">return</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">jsonArray</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">toString</span><span style="color: #ECEFF4">()</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">       </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">       </span><span style="color: #81A1C1">catch</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">(</span><span style="color: #8FBCBB">JsonProcessingException</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">e</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">          </span><span style="color: #D8DEE9">logger</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">error</span><span style="color: #ECEFF4">(</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">Problems encountered when generating JSON from the vets list</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> e</span><span style="color: #ECEFF4">)</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">          </span><span style="color: #81A1C1">return</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">null;</span></span>
<span class="line"><span style="color: #D8DEE9FF">       </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">}</span></span>
<span class="line"></span>
<span class="line"><span style="color: #ECEFF4">}</span></span></code></pre></div>



<p><br>La classe EmbeddingStoreInit fait appel au <a href="https://github.com/spring-petclinic/spring-petclinic-langchain4j/blob/v3.3.3/src/main/java/org/springframework/samples/petclinic/vet/VetRepository.java"><em>VetRepository</em></a> pour charger tous vétérinaires de la base, les marshalle en un gros <a href="https://github.com/langchain4j/langchain4j/blob/0.35.0/langchain4j-core/src/main/java/dev/langchain4j/data/document/Document.java">Document</a> JSON puis fait appel à la classe <a href="https://docs.langchain4j.dev/tutorials/rag/#embedding-store-ingestor">EmbeddingStoreIngestor</a> de LangChain4j. Ce EmbeddingStoreIngestor est configuré avec le modèle d’embedding, la base vectorielle où les embeddings seront stockés et un <a href="https://github.com/langchain4j/langchain4j/blob/0.35.0/langchain4j/src/main/java/dev/langchain4j/data/document/splitter/DocumentByLineSplitter.java">DocumentByLineSplitter</a> chargé de découper le volumineux document JSON en <a href="https://github.com/langchain4j/langchain4j/blob/0.35.0/langchain4j-core/src/main/java/dev/langchain4j/data/segment/TextSegment.java">TextSegment</a> censé améliorer la qualité des recherches de similarité et de réduire la taille et le coût d&rsquo;une invite envoyée au LLM.</p>



<p>Une fois le EmbeddingStoreIngestor construit, la méthode <strong>ingest()</strong> 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&nbsp;:</p>



<pre class="wp-block-preformatted">EmbeddingStoreIngestor  : Starting to ingest 1 documents<br>EmbeddingStoreIngestor  : Documents were split into 33 text segments<br>EmbeddingStoreIngestor  : Starting to embed 33 text segments<br>EmbeddingStoreIngestor  : Finished embedding 33 text segments<br>EmbeddingStoreIngestor  : Starting to store 33 text segments into the embedding store<br>EmbeddingStoreIngestor  : Finished storing 33 text segments into the embedding store</pre>



<h2 class="wp-block-heading">Requêtage des embeddings</h2>



<p>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.</p>



<p>Pour utiliser les fonctionnalités RAG, la classe @AiService <a href="https://github.com/spring-petclinic/spring-petclinic-langchain4j/blob/v3.3.3/src/main/java/org/springframework/samples/petclinic/chat/Assistant.java">Assistant</a> passe par l’interface <a href="https://docs.langchain4j.dev/tutorials/rag/#retrieval-augmentor">RetrievalAugmentor</a> et <a href="https://docs.langchain4j.dev/tutorials/rag/#default-retrieval-augmentor">son implémentation par défaut</a> mise à disposition par LangChain4j. Cette interface est chargée d’enrichir le <em>ChatMessage</em> 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 <a href="https://docs.langchain4j.dev/tutorials/rag/#advanced-rag">schéma du paragraphe Advanced RAG</a> de la documentation de LangChain4j. On y voit l’utilisation d’un <a href="https://docs.langchain4j.dev/tutorials/rag/#content-retriever">ContentRetriever</a> pour interroger une base vectorielle, un moteur de recherche, une base SQL ou bien encore un moteur de recherche.</p>



<p>Dans Petclinic, nous déclarons un bean ContentRetriever de type <a href="EmbeddingStoreContentRetriever">EmbeddingStoreContentRetriever</a> chargé de récupérer des données vétérinaires dans notre base vectorielle&nbsp;:</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="@Bean
EmbeddingStoreContentRetriever contentRetriever(InMemoryEmbeddingStore&lt;TextSegment&gt; embeddingStore,
       EmbeddingModel embeddingModel) {
    return new EmbeddingStoreContentRetriever(embeddingStore, embeddingModel);
}" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #ECEFF4">@</span><span style="color: #D08770">Bean</span></span>
<span class="line"><span style="color: #8FBCBB">EmbeddingStoreContentRetriever</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">contentRetriever</span><span style="color: #ECEFF4">(</span><span style="color: #8FBCBB">InMemoryEmbeddingStore</span><span style="color: #81A1C1">&lt;</span><span style="color: #D8DEE9FF">TextSegment</span><span style="color: #81A1C1">&gt;</span><span style="color: #D8DEE9FF"> embeddingStore</span><span style="color: #ECEFF4">,</span></span>
<span class="line"><span style="color: #D8DEE9FF">       </span><span style="color: #8FBCBB">EmbeddingModel</span><span style="color: #D8DEE9FF"> embeddingModel</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">return</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">new</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">EmbeddingStoreContentRetriever</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">embeddingStore</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> embeddingModel</span><span style="color: #ECEFF4">)</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #ECEFF4">}</span></span></code></pre></div>



<p><br>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 :</p>



<pre class="wp-block-preformatted">- method: POST<br>- url: https://api.openai.com/v1/chat/completions<br>- headers: [Accept: text/event-stream], [Authorization: Bearer xxx], [User-Agent: langchain4j-openai]<br>- body: {<br>  "model" : "gpt-4o",<br>  "messages" : [ {<br>    "role" : "system",<br>    "content" : "You are a friendly AI assistant …"<br>  }, {<br>    "role" : "user",<br>    "content" : "\"I'm looking for a veterinarian who specializes in both radiography and odontology for my pet \"\n\ <strong>content Answer using the following information:\n{\"id\":158,\"firstName\":\"Lauren\",\"lastName\":\"Wood\",\"new\":false,\"specialties\":[{\"id\":2,\"name\":\"surgery\",\"new\":false}]}\n{\"id\":159,\"firstName\":\"Gary\",\"lastName\":\"Coleman\",\"new\":false,\"specialties\":[{\"id\":1,\"name\":\"radiology\",\"new\":false},{\"id\":2,\"name\":\"surgery\",\"new\":false}]}\ …</strong>"<br>  } ],<br>  "temperature" : 0.7, … }<br></pre>



<h2 class="wp-block-heading">Routage de questions</h2>



<p>Le dernier point présenté dans cet article consiste à utiliser la fonctionnalité <strong><a href="https://docs.langchain4j.dev/tutorials/rag/#query-router">Query Router</a></strong> 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.<br>Comme son nom le laisse supposer, un Query Router est <strong>responsable de router une requête utilisateur vers le ou les ContentRetriever appropriés</strong> <strong>si nécessaire</strong>.<br><br>L’implémentation de l’interface <a href="https://github.com/langchain4j/langchain4j/blob/0.35.0/langchain4j-core/src/main/java/dev/langchain4j/rag/query/router/QueryRouter.java">QueryRouter</a> 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 supporter 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. <strong>Qui mieux qu’un LLM peut déterminer la sémantique de la question ?</strong><br>La classe <a href="https://github.com/spring-petclinic/spring-petclinic-langchain4j/blob/v3.3.3/src/main/java/org/springframework/samples/petclinic/chat/VetQueryRouter.java">VetQueryRouter</a> 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.</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="class VetQueryRouter implements QueryRouter {

    private static final Logger LOGGER = LoggerFactory.getLogger(VetQueryRouter.class);

    private static final PromptTemplate PROMPT_TEMPLATE = PromptTemplate.from(&quot;&quot;&quot;
          Is the following query related to one or more veterinarians of the pet clinic?
          Answer only 'yes' or 'no'.
          Query: {{it}}
          &quot;&quot;&quot;);

    private final ContentRetriever vetContentRetriever;

    private final ChatLanguageModel chatLanguageModel;

    public VetQueryRouter(ChatLanguageModel chatLanguageModel, ContentRetriever vetContentRetriever) {
       this.chatLanguageModel = chatLanguageModel;
       this.vetContentRetriever = vetContentRetriever;
    }

    @Override
    public Collection&lt;ContentRetriever&gt; route(Query query) {
       Prompt prompt = PROMPT_TEMPLATE.apply(query.text());

       AiMessage aiMessage = chatLanguageModel.generate(prompt.toUserMessage()).content();
       LOGGER.debug(&quot;LLM decided: {}&quot;, aiMessage.text());

       if (aiMessage.text().toLowerCase().contains(&quot;yes&quot;)) {
          return singletonList(vetContentRetriever);
       }
       return emptyList();
    }
}" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #81A1C1">class</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">VetQueryRouter</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">implements</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB; font-weight: bold">QueryRouter</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">private</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">static</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">final</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">Logger</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">LOGGER</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">LoggerFactory</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">getLogger</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9">VetQueryRouter</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">class</span><span style="color: #ECEFF4">)</span><span style="color: #81A1C1">;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">private</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">static</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">final</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">PromptTemplate</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">PROMPT_TEMPLATE</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">PromptTemplate</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">from</span><span style="color: #ECEFF4">(</span><span style="color: #ECEFF4">&quot;&quot;&quot;</span></span>
<span class="line"><span style="color: #A3BE8C">          Is the following query related to one or more veterinarians of the pet clinic?</span></span>
<span class="line"><span style="color: #A3BE8C">          Answer only &#39;yes&#39; or &#39;no&#39;.</span></span>
<span class="line"><span style="color: #A3BE8C">          Query: {{it}}</span></span>
<span class="line"><span style="color: #A3BE8C">          </span><span style="color: #ECEFF4">&quot;&quot;&quot;</span><span style="color: #ECEFF4">)</span><span style="color: #81A1C1">;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">private</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">final</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">ContentRetriever</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">vetContentRetriever</span><span style="color: #81A1C1">;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">private</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">final</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">ChatLanguageModel</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">chatLanguageModel</span><span style="color: #81A1C1">;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">public</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">VetQueryRouter</span><span style="color: #ECEFF4">(</span><span style="color: #8FBCBB">ChatLanguageModel</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">chatLanguageModel</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">ContentRetriever</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">vetContentRetriever</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">       </span><span style="color: #81A1C1">this</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">chatLanguageModel</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> chatLanguageModel</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">       </span><span style="color: #81A1C1">this</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">vetContentRetriever</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> vetContentRetriever</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">}</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">@</span><span style="color: #D08770">Override</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">public</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">Collection</span><span style="color: #ECEFF4">&lt;</span><span style="color: #8FBCBB">ContentRetriever</span><span style="color: #ECEFF4">&gt;</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">route</span><span style="color: #ECEFF4">(</span><span style="color: #8FBCBB">Query</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">query</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">       </span><span style="color: #8FBCBB">Prompt</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">prompt</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">PROMPT_TEMPLATE</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">apply</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9">query</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">text</span><span style="color: #ECEFF4">())</span><span style="color: #81A1C1">;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D8DEE9FF">       </span><span style="color: #8FBCBB">AiMessage</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">aiMessage</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">chatLanguageModel</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">generate</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9">prompt</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">toUserMessage</span><span style="color: #ECEFF4">()).</span><span style="color: #88C0D0">content</span><span style="color: #ECEFF4">()</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">       </span><span style="color: #D8DEE9">LOGGER</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">debug</span><span style="color: #ECEFF4">(</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">LLM decided: {}</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">aiMessage</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">text</span><span style="color: #ECEFF4">())</span><span style="color: #81A1C1">;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D8DEE9FF">       </span><span style="color: #81A1C1">if</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9">aiMessage</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">text</span><span style="color: #ECEFF4">().</span><span style="color: #88C0D0">toLowerCase</span><span style="color: #ECEFF4">().</span><span style="color: #88C0D0">contains</span><span style="color: #ECEFF4">(</span><span style="color: #ECEFF4">&quot;</span><span style="color: #A3BE8C">yes</span><span style="color: #ECEFF4">&quot;</span><span style="color: #ECEFF4">))</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">          </span><span style="color: #81A1C1">return</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">singletonList</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">vetContentRetriever</span><span style="color: #ECEFF4">)</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">       </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">       </span><span style="color: #81A1C1">return</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">emptyList</span><span style="color: #ECEFF4">()</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #ECEFF4">}</span></span></code></pre></div>



<p><br>La déclaration du VetQueryRouter au niveau de <a href="https://github.com/spring-petclinic/spring-petclinic-langchain4j/blob/v3.3.3/src/main/java/org/springframework/samples/petclinic/chat/AssistantConfiguration.java">AssistantConfiguration</a> passe par l’utilisation de la méthode builder de la classe <em>DefaultRetrievalAugmentor</em> :</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="@Bean
RetrievalAugmentor retrievalAugmentor(ChatLanguageModel chatLanguageModel, ContentRetriever vetContentRetriever) {
    return DefaultRetrievalAugmentor.builder()
       .queryRouter(new VetQueryRouter(chatLanguageModel, vetContentRetriever))
       .build();
}" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #ECEFF4">@</span><span style="color: #D08770">Bean</span></span>
<span class="line"><span style="color: #8FBCBB">RetrievalAugmentor</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">retrievalAugmentor</span><span style="color: #ECEFF4">(</span><span style="color: #8FBCBB">ChatLanguageModel</span><span style="color: #D8DEE9FF"> chatLanguageModel</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">ContentRetriever</span><span style="color: #D8DEE9FF"> vetContentRetriever</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">return</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">DefaultRetrievalAugmentor</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">builder</span><span style="color: #ECEFF4">()</span></span>
<span class="line"><span style="color: #D8DEE9FF">       </span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">queryRouter</span><span style="color: #ECEFF4">(</span><span style="color: #81A1C1">new</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">VetQueryRouter</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">chatLanguageModel</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> vetContentRetriever</span><span style="color: #ECEFF4">))</span></span>
<span class="line"><span style="color: #D8DEE9FF">       </span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">build</span><span style="color: #ECEFF4">()</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #ECEFF4">}</span></span></code></pre></div>



<p><br>Petclinic utilisant désormais le <em>ChatLanguageModel</em> et le <em>StreamingChatLanguageModel</em>, le fichier de configuration application.properties doit être complété :</p>



<pre class="wp-block-code"><code>langchain4j.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</code></pre>



<p>Dans les logs applicatifs, un premier appel est désormais envoyé au LLM avant toute autre appel :</p>



<pre class="wp-block-preformatted">- method: POST<br>- url: https://api.openai.com/v1/chat/completions<br>- headers: [Authorization: Bearer sk-Qw...MA], [User-Agent: langchain4j-openai]<br>- body: {<br>  "model" : "gpt-4o-mini",<br>  "messages" : [ {<br>    "role" : "user",<br>    "content" : "<strong>Is the following query related to one or more veterinarians of the pet clinic?\nAnswer only 'yes' or 'no'.\nQuery: \"I'm looking for a veterinarian who specializes in both radiography and odontology for my pet</strong> \"\n"<br>  } ],<br>  "temperature" : 0.7<br>}<br></pre>



<h2 class="wp-block-heading">Conclusion</h2>



<p>Cet article aura montré comment intégrer LangChain4j dans une application de gestion basée sur Spring Boot.</p>



<p>Récapitulons les principales fonctionnalités de LangChain4j qui ont été mises en œuvre :</p>



<ol class="wp-block-list">
<li><strong>AI Service </strong>: définit de manière déclarative l’interface entre notre application Java et un LLM.</li>



<li><strong>Memory</strong> : permet d’historiser les conversations entre l’utilisateur et le LLM, supporte le multi-utilisateurs et la persistance.</li>



<li><strong>System prompt</strong> : 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.</li>



<li><strong>Tooling </strong>(ou <strong>appel de fonction</strong>) : permet au LLM d&rsquo;appeler, si nécessaire, une ou plusieurs méthodes Java de l’application.</li>



<li><strong>Streaming</strong> : réponse au fil de l’eau, token par token, en utilisant côté client le Server-Sent Events.</li>



<li><strong>RAG </strong>: 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.</li>
</ol>



<p>Personnellement, le développement de la version LangChain4j de Spring Petclinic m’aura permis de contribuer modestement au projet Open Source LangChain4j&nbsp;(PR <a href="https://github.com/langchain4j/langchain4j-spring/pull/49">#49</a>, <a href="https://github.com/langchain4j/langchain4j-spring/pull/50">#50</a>, <a href="https://github.com/langchain4j/langchain4j-spring/pull/51">#51</a> et #<a href="https://github.com/langchain4j/langchain4j/pull/2000">2000</a>).<br><br>Je tiens à remercier mon fils Evan pour son <a href="https://youtu.be/hy2HDMjLr_8">montage de ma video Youtube</a>. Merci également à Antonio Goncalves, Julien Dubois, Guillaume Laforge et Valentin Deleplace pour leurs workshops sur LangChain4j avec <a href="https://moaw.dev/workshop/?src=gh:Azure-Samples/azure-openai-rag-workshop-java/docs/workshop-java-quarkus.md">Azure OpenAI</a> et <a href="https://devfest2024.gdgnantes.com/sessions/hands_on_gemini_with_java_and_langchain4j_on_vertex_ai/">Gemini</a>.</p>



<p>Si vous souhaitez contribuez à votre tour à <a href="https://github.com/spring-petclinic/spring-petclinic-langchain4j">Spring Petclinic LangChain4j</a>, des <a href="https://github.com/spring-petclinic/spring-petclinic-langchain4j/issues">issues</a> vous attendent. L’<a href="https://github.com/spring-petclinic/spring-petclinic-langchain4j/issues/10">issue #10</a> 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.</p>



<p><span style="text-decoration: underline;">Ressources</span> :</p>



<ul class="wp-block-list">
<li><a href="https://docs.langchain4j.dev/">Documentation officielle LangChain4j</a></li>



<li>Repository Git <a href="https://github.com/spring-petclinic/spring-petclinic-langchain4j">spring-petclinic-langchain4j</a></li>



<li><a href="https://spring.io/blog/2024/09/26/ai-meets-spring-petclinic-implementing-an-ai-assistant-with-spring-ai-part-i">AI Meets Spring Petclinic: Implementing an AI Assistant with Spring AI</a> (Oded Shopen)</li>



<li><a href="https://platform.openai.com/docs/quickstart)">OpenAI Developer Quickstart</a></li>



<li><a href="https://github.com/ai-for-java/openai4j">Java client library for OpenAI API</a></li>



<li><a href="https://codelabs.developers.google.com/codelabs/gemini-java-developers">Gemini en Java avec Vertex AI et LangChain4j</a> (Google Lab)</li>



<li><a href="https://moaw.dev/workshop/?src=gh:Azure-Samples/azure-openai-rag-workshop-java/docs/workshop-java-quarkus.md">Create your own ChatGPT with Retrieval-Augmented-Generation</a> (Microsoft)</li>
</ul>



<p></p>
]]></content:encoded>
					
					<wfw:commentRss>https://javaetmoi.com/2024/11/integrer-un-chatbot-dans-une-webapp-java-avec-langchain4j/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Compatibilité Jakarta EE 9 de vieux frameworks</title>
		<link>https://javaetmoi.com/2024/08/compatibilite-jakarta-ee-9-de-vieux-frameworks/</link>
					<comments>https://javaetmoi.com/2024/08/compatibilite-jakarta-ee-9-de-vieux-frameworks/#respond</comments>
		
		<dc:creator><![CDATA[Antoine]]></dc:creator>
		<pubDate>Sun, 25 Aug 2024 15:54:14 +0000</pubDate>
				<category><![CDATA[Retour d'expérience]]></category>
		<category><![CDATA[JavaEE]]></category>
		<category><![CDATA[Spring Boot]]></category>
		<category><![CDATA[Tomcat]]></category>
		<guid isPermaLink="false">https://javaetmoi.com/?p=2374</guid>

					<description><![CDATA[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&#160;et des frameworks comme Quarkus ou Spring : Servlet, JSP, JSF, JPA, &#8230; <a href="https://javaetmoi.com/2024/08/compatibilite-jakarta-ee-9-de-vieux-frameworks/" class="more-link">Continuer la lecture de <span class="screen-reader-text">Compatibilité Jakarta EE 9 de vieux frameworks</span>  <span class="meta-nav">&#8594;</span></a>]]></description>
										<content:encoded><![CDATA[
<h2 class="wp-block-heading">De Java EE à Jakarta EE</h2>



<p>En <strong>2017</strong>, <strong>Oracle</strong> a fait <strong>don de la spécification Java EE</strong> (précédemment connu sous le nom de J2EE) à la fondation <strong>Eclipse</strong>. Java EE regroupe différentes API utilisées aussi bien par des serveurs d’applications, des containers de servlets&nbsp;et des frameworks comme Quarkus ou Spring : <strong>Servlet</strong>, JSP, JSF, JPA, JTA, JAX-WS, JAX-RS, JAXB, WebSocket, Bean Validation, CDI, EL … <br></p>



<div class="wp-block-media-text is-stacked-on-mobile" style="grid-template-columns:38% auto"><figure class="wp-block-media-text__media"><img loading="lazy" decoding="async" width="628" height="599" src="https://javaetmoi.com/wp-content/uploads/2024/08/Jakarta_ee_logo.png" alt="" class="wp-image-2384 size-full" srcset="https://javaetmoi.com/wp-content/uploads/2024/08/Jakarta_ee_logo.png 628w, https://javaetmoi.com/wp-content/uploads/2024/08/Jakarta_ee_logo-300x286.png 300w, https://javaetmoi.com/wp-content/uploads/2024/08/Jakarta_ee_logo-315x300.png 315w" sizes="auto, (max-width: 628px) 100vw, 628px" /></figure><div class="wp-block-media-text__content">
<p>Sous l’é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 <strong>2019</strong> est sortie une version <strong>Jakarta EE 8</strong> pleinement compatible avec Java EE 8. Comme seul changement notable pour les dév<strong>, le groupId des artefacts Maven a été renommé de javax à jakarta</strong>. 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.</p>
</div></div>



<p>En décembre 2020, la communauté Java est secouée par la sortie de <strong>Java EE 9</strong>. 20 ans de rétrocompatibilité s’écroulent. Oracle a souhaité conserver la <strong>marque Java</strong>. Les<strong> packages javax.* de la spécification Java EE ont été renommés en jakarta.*</strong>. Certains sous-packages ont également été renommés.&nbsp; <br> Pour exemple, la classe <strong><em>Marshaller</em></strong> de l’API JAXB change de package&nbsp;: de <em>javax.xml.bind.Marshaller</em> vers jakarta.xml.bind.Marshaller</p>



<span id="more-2374"></span>



<p>A cette occasion, le numéro de version majeur a été incrémenté. <br>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:<strong>3.0.0 </strong>sous Jakarta EE 9.</p>



<p>
  A noter&nbsp;que les <strong>packages javax du JDK</strong> et qui n’appartiennent donc pas à Java EE ne sont <strong>pas renommés</strong>. On peut citer&nbsp;: javax.sql, javax.swing,  javax.naming, javax.transaction.xa et javax.naming.
</p>



<p> Ce changement de package Java est on ne peut plus impactant&nbsp;:</p>



<ol class="wp-block-list">
<li><strong>  Le code Java non migré ne fonctionne pas avec un container/runtime plus récent</strong></li>



<li>  <strong>Un ancien container/runtime ne fonctionne pas avec du code Java récent migré</strong><br></li>
</ol>



<p>
  Ce changement a impacté tout l’écosystème Java&nbsp;: les projets Open Source, le code propriétaire / métier, les IDE, les outils de build …
</p>



<p>Quatre 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, <a href="https://spring.io/blog/2022/11/16/spring-framework-6-0-goes-ga">Spring Framework 6.0</a> et <a href="https://spring.io/blog/2022/11/24/spring-boot-3-0-goes-ga">Spring Boot 3.0</a> sont tous les deux sortis en novembre 2022. <br>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 <a href="https://cxf.apache.org/docs/40-migration-guide.html">CXF 4</a>.</p>



<h2 class="wp-block-heading">Migrer des applications legacy</h2>



<p>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. <br><br>Les applications les plus modernes, basées sur <strong>Spring Boot</strong>, <strong>Quarkus</strong> ou <strong>Micronaut</strong>, 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 <a href="https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-3.0-Migration-Guide">guide de migration Spring Boot 3</a> et d’outils comme la <a href="https://docs.openrewrite.org/recipes/java/spring/boot3/upgradespringboot_3_0">recette OpenRewrite Migrate to Spring Boot 3.0</a>, le projet <a href="https://github.com/spring-projects-experimental/spring-boot-migrator">Spring Boot Migrator</a> (SBM) ou bien encore l’<a href="https://www.jetbrains.com/guide/java/tutorials/migrating-javax-jakarta/use-migration-tool/">IntelliJ IDEA&rsquo;s migration tool</a>, les développeurs sont assistés dans leur travail et trouvent les ressources nécessaires sur le Net.</p>



<p>
  A 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 <a href="https://github.com/DozerMapper/dozer">Dozer</a> invite à migrer vers MapStruct ou ModelMapper et propose même un plugin IntelliJ pour faciliter la tâche.
  <br>
  Qu’en est-il de frameworks plus structurants&nbsp;? Je pense notamment à de vieux frameworks frontends sur lesquels sont conçus des centaines d’écrans d’applications de gestion. 
</p>



<p>
  Par exemple, <strong>Struts 1</strong> n’est pas compatible Jakarta EE 9 et les nouveaux packages en jakarta.*  Il s&rsquo;appuie sur l&rsquo;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 <strong>Richfaces</strong> abandonné par JBoss depuis 2016. 
</p>



<p>
  Migrer les écrans d’une application de Struts 1<strong> </strong>vers 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&nbsp;: utiliser <a href="https://github.com/weblegacy/struts1">Struts 1 Reloaded</a> 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. 
</p>



<p>
  Faute de budget conséquent, ces applications seraient-elles vouées à rester ad vitam æternam sur du Spring Boot 2&nbsp;? Non, la suite de cet article explique comment automatiser la compatibilité jakarta de vieux frameworks et de vielles librairies.
</p>



<h2 class="wp-block-heading">Solution technique</h2>



<p>
  Les développeurs du conteneur Tomcat ont adressé cette problématique lors de la sortie de Tomcat 10.&nbsp;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&rsquo;<a href="https://github.com/apache/tomcat-jakartaee-migration"><strong>outil de migration Apache Tomcat pour Jakarta EE.</strong></a> Pratique, cet outil peut être utilisé en dehors de Tomcat, sous forme d&rsquo;un<strong> jar auto-exécutable </strong>ou d&rsquo;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.
</p>



<p>Le projet tomcat-jakartaee-migration effectue tous les changements nécessaires pour migrer une application de Java EE 8 vers Jakarta EE 9 en <strong>renommant chaque package</strong> Java EE 8 vers son remplaçant Jakarta EE 9. Cela inclut les références aux package dans les classes, les constantes de type String, les fichiers de configuration, les JSP, les TLD &#8230; <br>Tous les packages javax.* ne font pas partie de Java EE. Seuls ceux définis par Java EE sont déplacés vers l&rsquo;espace de noms jakarta.*. <br>Il n&rsquo;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&rsquo;utilisation des schémas de Java EE 8 et antérieurs.</p>



<p>
  Cet outil propose <a href="https://github.com/apache/tomcat-jakartaee-migration/blob/main/src/main/java/org/apache/tomcat/jakartaee/EESpecProfiles.java">2 profils </a>: le profil partiel <strong>TOMCAT</strong> ciblant les conteneurs web comme Tomcat et Jetty et le <strong>profil EE </strong>ciblant toutes les dépendances Java EE 8. 
  <br>
  L&rsquo;outil sait parcourir différents types d&rsquo;archives : jar, zip, war &#8230; Via ses converters (<a href="https://github.com/apache/tomcat-jakartaee-migration/blob/1.0.8/src/main/java/org/apache/tomcat/jakartaee/TextConverter.java">TextConverter</a>, <a href="https://github.com/apache/tomcat-jakartaee-migration/blob/1.0.8/src/main/java/org/apache/tomcat/jakartaee/ClassConverter.java">ClassConverter</a>, <a href="https://github.com/apache/tomcat-jakartaee-migration/blob/1.0.8/src/main/java/org/apache/tomcat/jakartaee/ManifestConverter.java">ManifestConvert</a>), il sait également manipuler plusieurs formats de fichiers&nbsp;: 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) &#8230;
  <br>
  <br>
  L&rsquo;<strong>outil tomcat-jakartaee-migration</strong> peut donc aussi bien <strong>travailler</strong> sur des <strong>JAR de librairies tierces </strong>que sur du <strong>code source métier qu&rsquo;on souhaite migrer vers Jakarta EE 9 et même Jakarta EE 10</strong>.
</p>



<h2 class="wp-block-heading">Guide d&rsquo;utilisation</h2>



<p>
  Rendre compatible Jakarta EE 9 des librairies tierces puis les utiliser dans le code métier se fait en 2 étapes :
</p>



<h3 class="wp-block-heading">Etape 1 : migrer les librairies tierces</h3>



<p>1. Récupérer le binaire depuis la page <a href="https://tomcat.apache.org/download-migration.cgi">https://tomcat.apache.org/download-migration.cgi</a></p>



<p>2. Executer la ligne de commande suivante (exemple avec jsf-api-1.2_14.jar) :</p>



<pre class="wp-block-code"><code class=""><code>set MIGRATION_TOOL=C:\dev\jakartaee-migration\lib\jakartaee-migration-1.0.8.jar<br>set M2_REPO=C:\dev\maven\repository<br>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 </code></code></pre>



<p>Le fichier JAR jsf-api-1.2_14-jakarta.jar généré est désormais compatible Jakarta EE 9.<br>Extrait de la classe FacesServlet :</p>



<figure class="wp-block-table"><table><tbody><tr><td><p><strong>jsf-api-1.2_14.jar compatible Java EE 8</strong><span style="font-family: inherit; font-size: inherit; font-weight: inherit; background-color: rgb(255, 255, 255); color: initial;"></span></p></td><td><p><strong>jsf-api-1.2_14-jakarta.jar migré à Jakarta EE 9</strong><span style="font-family: inherit; font-size: inherit; font-weight: inherit; background-color: rgb(255, 255, 255); color: initial;"></span></p></td></tr><tr><td><p><code>package javax.faces.webapp;<br>&nbsp;<br>import javax.faces.FacesException;<br>import javax.faces.FactoryFinder;<br>import javax.faces.context.FacesContext;<br>import javax.faces.context.FacesContextFactory;<br>import javax.faces.lifecycle.Lifecycle;<br>import javax.faces.lifecycle.LifecycleFactory;<br>import javax.servlet.Servlet;<br>import javax.servlet.ServletConfig;<br>import javax.servlet.ServletException;<br>import javax.servlet.ServletRequest;<br>import javax.servlet.ServletResponse;<br>import javax.servlet.UnavailableException;<br>import javax.servlet.http.HttpServletRequest;<br>import javax.servlet.http.HttpServletResponse;<br>&nbsp;<br>import java.io.IOException;<br>import java.util.ResourceBundle;<br>import java.util.logging.Level;<br>import java.util.logging.Logger;<br>&nbsp;<br>public final class FacesServlet implements Servlet {</code></p></td><td><p><code>package jakarta.faces.webapp;<br>&nbsp;<br>import jakarta.faces.FacesException;<br>import jakarta.faces.FactoryFinder;<br>import jakarta.faces.context.FacesContext;<br>import jakarta.faces.context.FacesContextFactory;<br>import jakarta.faces.lifecycle.Lifecycle;<br>import jakarta.faces.lifecycle.LifecycleFactory;<br>import jakarta.servlet.Servlet;<br>import jakarta.servlet.ServletConfig;<br>import jakarta.servlet.ServletException;<br>import jakarta.servlet.ServletRequest;<br>import jakarta.servlet.ServletResponse;<br>import jakarta.servlet.UnavailableException;<br>import jakarta.servlet.http.HttpServletRequest;<br>import jakarta.servlet.http.HttpServletResponse;<br>&nbsp;<br>import java.io.IOException;<br>import java.util.ResourceBundle;<br>import java.util.logging.Level;<br>import java.util.logging.Logger;<br>&nbsp;<br>public final class FacesServlet implements Servlet {</code></p></td></tr></tbody></table></figure>



<p>3. Renouveler l&rsquo;opération pour le JAR du code source.<br>Exemple sur jsf-api-1.2_14-sources.jar :</p>



<pre class="wp-block-code"><code class="">java -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 </code></pre>



<p>4.  Uploader le JAR et ses sources dans le repository binaire d’entreprise (ex&nbsp;: <a href="https://jfrog.com/fr/artifactory/">Artifactory</a> ou <a href="https://www.sonatype.com/products/sonatype-nexus-oss-download">Nexus Sonatype</a>). Privilégiez l’ajout du suffixe -jakarta au numéro de version Maven à l’utilisation d’un classifier Maven.</p>



<p>Cette étape de migration peut être <strong>complètement automatisée</strong> par un pipeline CI <strong>Jenkins</strong> ou <strong>GitLab</strong>. </p>



<h3 class="wp-block-heading">Etape 2 : utiliser les librairies tierces migrées</h3>



<p>1. Comme pré-requis, le code source de l&rsquo;application doit avoir commencé sa migration à Jakarta EE 9 (ou supérieur).<br><br>2. 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&rsquo;application<br><br>3.  Il est ensuite nécessaire d&rsquo;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 le fichiers XML.&nbsp;<br>Exemple du web.xml référençant jakarta.faces.webapp.FacesServlet :</p>



<pre class="wp-block-code"><code class="">&lt;servlet&gt;<br>    &lt;servlet-name&gt;Faces Servlet&lt;/servlet-name&gt;<br>    &lt;servlet-class&gt;jakarta.faces.webapp.FacesServlet&lt;/servlet-class&gt;<br>    &lt;load-on-startup&gt;1&lt;/load-on-startup&gt;<br>&lt;/servlet&gt;</code></pre>



<p>
  Pour y arriver, 4 possibilités s&rsquo;offrent à nous : 
</p>



<ul class="wp-block-list">
<li>  Changements manuels par search / replace</li>



<li>      Appliquer la recette OpenRewrite <a href="https://docs.openrewrite.org/recipes/java/migrate/jakarta/javaxmigrationtojakarta">javaxmigrationtojakarta</a> (ne gère pas le web.xml)</li>



<li>      Utiliser l’<a href="https://www.jetbrains.com/guide/java/tutorials/migrating-javax-jakarta/use-migration-tool/">IntelliJ IDEA&rsquo;s migration tool</a></li>



<li>      Utiliser une nouvelle fois l&rsquo;outil <strong>tomcat-jakartaee-migration</strong></li>
</ul>



<pre class="wp-block-code"><code class="">java -jar %MIGRATION_TOOL% -logLevel=FINEST -profile=EE C:\dev\project\my-webapp C:\dev\project\my-webapp-jakarta</code></pre>



<p>
  Cette dernière option est à privilégier. En attente de prise en compte de la <a href="https://github.com/apache/tomcat-jakartaee-migration/pull/60">PR #60</a> de mon collègue <a href="https://github.com/marcosemiao">Marco</a> pour exclure le sous-répertoire .git et utiliser le répertoire source comme cible.
</p>



<p>5.  Vérifier que tout compile</p>



<pre class="wp-block-code"><code class="">mvn clean install</code></pre>



<p></p>



<h2 class="wp-block-heading">Conclusion</h2>



<p>
  Cette solution présente plusieurs avantages&nbsp;:
</p>



<ul class="wp-block-list">
<li><strong>Simplicité&nbsp;</strong></li>



<li><strong>Cout</strong> défiant toute concurrence</li>



<li>Réutilisation d&rsquo;un <strong>outil Open Source maintenu par l&rsquo;équipe Tomcat</strong> et massivement éprouvé</li>



<li><strong>Automatisation</strong> possible<br>  </li>
</ul>



<p>
  Son principal inconvénient réside dans le fait que <strong>l’application continue à utiliser une librairie non maintenue</strong>. A moyen termes, trouver un financement pour refondre ou migrer l’application vers une technologie cible reste donc préconisé. 
</p>



<p>
  Enfin, d’autres outils que celui d’Apache existe, par exemple <a href="https://github.com/eclipse/transformer">Eclipse Transformer</a>. Avant de vous lancer, comparez-les. 
</p>



<p><strong>Ressources</strong>&nbsp;: </p>



<ul class="wp-block-list">
<li><a href="https://github.com/apache/tomcat-jakartaee-migration">Apache Tomcat migration tool for Jakarta EE</a> (GitHub)</li>



<li><a href="https://blogs.oracle.com/javamagazine/post/transition-from-java-ee-to-jakarta-ee">Transition from Java EE to Jakarta EE</a> (Oracle Java Magazine)</li>



<li><a href="https://jakarta.ee/blogs/javax-jakartaee-namespace-ecosystem-progress/">Javax to Jakarta Namespace Ecosystem Progress</a> (Jakarta EE)</li>



<li><a href="https://www.jetbrains.com/guide/java/tutorials/migrating-javax-jakarta/use-migration-tool/">Using IntelliJ IDEA&rsquo;s migration tool</a> (Jetbrains)</li>



<li><a href="https://github.com/eclipse/transformer">Eclipse Transformer</a> (GitHub)</li>



<li><a href="https://communityovercode.org/wp-content/uploads/2023/10/javax-to-jakarta-tales-from-the-crypt-v2-shawn-mckinney.pdf">Javax to Jakarta Tales From the Crypt</a> (Shawn McKinney)<br>  </li>
</ul>
]]></content:encoded>
					
					<wfw:commentRss>https://javaetmoi.com/2024/08/compatibilite-jakarta-ee-9-de-vieux-frameworks/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
	</channel>
</rss>

<!--
Performance optimized by W3 Total Cache. Learn more: https://www.boldgrid.com/w3-total-cache/?utm_source=w3tc&utm_medium=footer_comment&utm_campaign=free_plugin

Mise en cache de page à l’aide de Disk: Enhanced (SSL caching disabled) 

Served from: javaetmoi.com @ 2026-04-06 16:56:21 by W3 Total Cache
-->