JTechLog - Viczián IstvánViczián István magyar nyelvű Java blogjahttp://www.jtechlog.hu/favicon.ico2024-03-01T15:31:21+00:00http://www.jtechlog.huViczián IstvánProject Reactor és a szálkezelés2024-01-31T22:00:00+00:00http://www.jtechlog.hu/2024/01/31/project-reactor-szalkezeles<p>A Project Reactor egy Springhez közel álló reaktív könyvtár. Erre épül a Spring Framework 5-ben megjelent WebFlux webes keretrendszer
reaktív webes alkalmazások készítésére. Ez nagyban hasonlít a Spring MVC-re, azonban reaktív módon működik.
Ezzel találkozhatunk akkor is, ha <code class="language-plaintext highlighter-rouge">WebClient</code>-et használunk REST webszolgáltatások hívására
(ez a <code class="language-plaintext highlighter-rouge">RestTemplate</code>-et hivatott leváltani, de úgy tűnik, hogy mégis szükség van egy szinkron megvalósításra is,
ezért jelent meg a 6.1-es Spring Frameworkben a <code class="language-plaintext highlighter-rouge">RestClient</code>). A reaktív elvekről és keretrendszerekről
már írtam a <a href="https://www.jtechlog.hu/2021/08/03/reaktiv-programozas.html">Reaktív programozás</a> posztomban.</p>
<p>A Reactor ún. <em>concurrency-agnostic</em>, ami azt jelenti, hogy nem erőltet ránk semmilyen párhuzamossági modellt.
Azonban a fejlesztőnek lehetőséget ad a szálak használatára.</p>
<p>Ez a poszt azzal foglalkozik, hogy lehet szálakat használni, hogyan befolyásolja a szálak
használatát a <code class="language-plaintext highlighter-rouge">publishOn</code> és <code class="language-plaintext highlighter-rouge">subscribeOn</code> operátor (<code class="language-plaintext highlighter-rouge">Mono</code> és <code class="language-plaintext highlighter-rouge">Flux</code> osztályokban lévő metódusok).</p>
<!-- more -->
<p>A poszthoz tartozó példaprogram <a href="https://github.com/vicziani/jtechlog-reactor-threading">megtalálható a GitHubon</a>.</p>
<h2 id="bevezetés">Bevezetés</h2>
<p>Kezdjük egy egyszerű teszt esettel.</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Test</span>
<span class="kt">void</span> <span class="nf">onMainThread</span><span class="o">()</span> <span class="o">{</span>
<span class="kt">var</span> <span class="n">upperCaseNames</span> <span class="o">=</span> <span class="nc">Flux</span><span class="o">.</span><span class="na">fromIterable</span><span class="o">(</span><span class="n">employees</span><span class="o">)</span>
<span class="o">.</span><span class="na">filter</span><span class="o">(</span><span class="n">employee</span> <span class="o">-></span> <span class="n">employee</span><span class="o">.</span><span class="na">getYearOfBirth</span><span class="o">()</span> <span class="o">>=</span> <span class="mi">1980</span><span class="o">)</span>
<span class="o">.</span><span class="na">doOnNext</span><span class="o">(</span><span class="n">e</span> <span class="o">-></span> <span class="n">log</span><span class="o">.</span><span class="na">info</span><span class="o">(</span><span class="s">"filter"</span><span class="o">))</span>
<span class="o">.</span><span class="na">map</span><span class="o">(</span><span class="nl">Employee:</span><span class="o">:</span><span class="n">getName</span><span class="o">)</span>
<span class="o">.</span><span class="na">doOnNext</span><span class="o">(</span><span class="n">e</span> <span class="o">-></span> <span class="n">log</span><span class="o">.</span><span class="na">info</span><span class="o">(</span><span class="s">"map - getName"</span><span class="o">))</span>
<span class="o">.</span><span class="na">map</span><span class="o">(</span><span class="nl">String:</span><span class="o">:</span><span class="n">toUpperCase</span><span class="o">)</span>
<span class="o">.</span><span class="na">doOnNext</span><span class="o">(</span><span class="n">e</span> <span class="o">-></span> <span class="n">log</span><span class="o">.</span><span class="na">info</span><span class="o">(</span><span class="s">"map - toUpperCase"</span><span class="o">))</span>
<span class="o">;</span>
<span class="n">log</span><span class="o">.</span><span class="na">info</span><span class="o">(</span><span class="s">"Pipeline is ready"</span><span class="o">);</span>
<span class="nc">StepVerifier</span><span class="o">.</span><span class="na">create</span><span class="o">(</span><span class="n">upperCaseNames</span><span class="o">)</span>
<span class="o">.</span><span class="na">expectNext</span><span class="o">(</span><span class="s">"JACK DOE"</span><span class="o">)</span>
<span class="o">.</span><span class="na">expectNext</span><span class="o">(</span><span class="s">"JANE DOE"</span><span class="o">)</span>
<span class="o">.</span><span class="na">verifyComplete</span><span class="o">()</span>
<span class="o">;</span>
<span class="o">}</span>
</code></pre></div></div>
<p>A streameket ismerve könnyen olvasható a forráskód, ami az alkalmazottak listájából
kiszűri az 1980 előtt születetteket, majd lekéri azok neveit, és nagybetűsíti.</p>
<p>A forrás a <code class="language-plaintext highlighter-rouge">fromIterable()</code>, mely visszaad egy <code class="language-plaintext highlighter-rouge">Flux</code>-ot, ami önmaga implementálja a <code class="language-plaintext highlighter-rouge">Publisher</code>
interfészt (lásd előző posztomban a Reactive Streams elméletét és részeit).</p>
<p>Ez után helyezhetjük el az operátorokat, kialakítva így egy futószalagot, vagy csővezetéket, ez a pipeline,
vagy operator chain (lánc).
Ez az ún. deklaratív fázisban történik, ez az <em>assembly time</em>.</p>
<p>Itt valójában három fő operátor szerepel egymás után: <code class="language-plaintext highlighter-rouge">filter</code>, <code class="language-plaintext highlighter-rouge">map</code>, <code class="language-plaintext highlighter-rouge">map</code>. A <code class="language-plaintext highlighter-rouge">doOnNext()</code> operátoroktól
most tekintsünk el, hiszen azok csak naplóznak, hogy lássuk, hogy azt épp milyen szál futtatja.</p>
<p><img src="/artifacts/posts/2024-02-01-project-reactor-szalkezeles/simple-pipeline.drawio.png" alt="Egyszerű pipeline" /></p>
<p>A teszteset <code class="language-plaintext highlighter-rouge">StepVerifier</code>-t használ az ellenőrzésre.</p>
<p>Amit még érdemes tudni, hogy ugyan elkészül a pipeline, azonban semmi nem történik addig, amíg
meg nem történik a tényleges feliratkozás (<code class="language-plaintext highlighter-rouge">Nothing Happens Until You subscribe()</code>).
A feliratkozás alkalmával a feliratkozó jelez (signal) az előtte lévő operátornak,
az szintén az előtte lévő operátornak, és így tovább. Így létrejön egy <em>subscription chain</em>.
Majd ezután indulnak el az elemek a pipeline-on. Ez az <em>execution time</em>.</p>
<p>A feliratkozást itt a <code class="language-plaintext highlighter-rouge">StepVerifier</code> végzi.
Ezért van az, hogy először kerül kiírásra a <code class="language-plaintext highlighter-rouge">Pipeline is ready</code> szöveg, és csak azután
a <code class="language-plaintext highlighter-rouge">doOnNext()</code> paramétereként átadott lambda kifejezésben lévő szövegek.</p>
<p>Érdemes még ismerni az upstream és downstream fogalmát, ugyanis az operátorok
dokumentációja gyakran hivatkozik erre. Egy operátor szempontjából <em>upstream</em>
az operátort megelőző stream, és <em>downstream</em>.</p>
<p><img src="/artifacts/posts/2024-02-01-project-reactor-szalkezeles/upstream-downstream.drawio.png" alt="Upstream, downstream" /></p>
<h2 id="alapértelmezett-szálkezelés">Alapértelmezett szálkezelés</h2>
<p>Ha lefuttatjuk a teszt esetet, látható, hogy a <code class="language-plaintext highlighter-rouge">main</code> szálon kerül lefuttatásra.
Valójában azon szálon kerülnek lefuttatásra az operátorok, amelyik szálon megtörtént a feliratkozás.</p>
<p>Azaz ha a <code class="language-plaintext highlighter-rouge">StepVerifier</code>-t új szálon futtatjuk, akkor minden azon az új szálon fog futni.</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">anotherThread</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Thread</span><span class="o">(()</span> <span class="o">-></span> <span class="nc">StepVerifier</span><span class="o">.</span><span class="na">create</span><span class="o">(</span><span class="n">upperCaseNames</span><span class="o">)</span>
<span class="o">.</span><span class="na">expectNext</span><span class="o">(</span><span class="s">"JACK DOE"</span><span class="o">)</span>
<span class="o">.</span><span class="na">expectNext</span><span class="o">(</span><span class="s">"JANE DOE"</span><span class="o">)</span>
<span class="o">.</span><span class="na">verifyComplete</span><span class="o">())</span>
<span class="o">;</span>
<span class="n">anotherThread</span><span class="o">.</span><span class="na">start</span><span class="o">();</span>
<span class="n">anotherThread</span><span class="o">.</span><span class="na">join</span><span class="o">();</span>
</code></pre></div></div>
<p>Ezzel létrehozunk, elindítunk egy új szálat, és bevárjuk azt. A végrehajtás a <code class="language-plaintext highlighter-rouge">Thread-0</code>
szálon fog futni.</p>
<h2 id="szálakat-módosító-operátorok">Szálakat módosító operátorok</h2>
<p>Vannak olyan operátorok, melyek szálat váltanak, ilyen pl. a <code class="language-plaintext highlighter-rouge">delaySequence()</code>.</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">upperCaseNames</span> <span class="o">=</span> <span class="nc">Flux</span><span class="o">.</span><span class="na">fromIterable</span><span class="o">(</span><span class="n">employees</span><span class="o">)</span>
<span class="o">.</span><span class="na">filter</span><span class="o">(</span><span class="n">employee</span> <span class="o">-></span> <span class="n">employee</span><span class="o">.</span><span class="na">getYearOfBirth</span><span class="o">()</span> <span class="o">>=</span> <span class="mi">1980</span><span class="o">)</span>
<span class="o">.</span><span class="na">doOnNext</span><span class="o">(</span><span class="n">e</span> <span class="o">-></span> <span class="n">log</span><span class="o">.</span><span class="na">info</span><span class="o">(</span><span class="s">"filter"</span><span class="o">))</span>
<span class="o">.</span><span class="na">map</span><span class="o">(</span><span class="nl">Employee:</span><span class="o">:</span><span class="n">getName</span><span class="o">)</span>
<span class="o">.</span><span class="na">doOnNext</span><span class="o">(</span><span class="n">e</span> <span class="o">-></span> <span class="n">log</span><span class="o">.</span><span class="na">info</span><span class="o">(</span><span class="s">"map - getName"</span><span class="o">))</span>
<span class="o">.</span><span class="na">delaySequence</span><span class="o">(</span><span class="nc">Duration</span><span class="o">.</span><span class="na">ofMillis</span><span class="o">(</span><span class="mi">10</span><span class="o">))</span>
<span class="o">.</span><span class="na">map</span><span class="o">(</span><span class="nl">String:</span><span class="o">:</span><span class="n">toUpperCase</span><span class="o">)</span>
<span class="o">.</span><span class="na">doOnNext</span><span class="o">(</span><span class="n">e</span> <span class="o">-></span> <span class="n">log</span><span class="o">.</span><span class="na">info</span><span class="o">(</span><span class="s">"map - toUpperCase"</span><span class="o">))</span>
<span class="o">;</span>
</code></pre></div></div>
<p>A <code class="language-plaintext highlighter-rouge">delaySequence()</code>-t követő <code class="language-plaintext highlighter-rouge">map()</code> operátor itt már a <code class="language-plaintext highlighter-rouge">parallel-1</code> szálon fog futni.</p>
<h2 id="publishon-operátor-használata"><code class="language-plaintext highlighter-rouge">publishOn()</code> operátor használata</h2>
<p>A <code class="language-plaintext highlighter-rouge">publishOn()</code> metódus hatására az azt követő operátor már más szálon kerül meghívásra.
Paraméterül egy <code class="language-plaintext highlighter-rouge">Scheduler</code> implementációt kell átadni, mely hasonló absztrakció, mint
az <code class="language-plaintext highlighter-rouge">ExecutorService</code>. A <code class="language-plaintext highlighter-rouge">Scheduler</code> tartalmaz több <code class="language-plaintext highlighter-rouge">schedule()</code> metódust, melynek
a végrehajtandó feladatot kell átadni. A <code class="language-plaintext highlighter-rouge">Schedulers</code> tartalmaz több factory metódust, mellyel
<code class="language-plaintext highlighter-rouge">Scheduler</code> példányokat lehet létrehozni. Ilyenek pl.:</p>
<ul>
<li><code class="language-plaintext highlighter-rouge">Scheduler.parallel()</code> - állandó méretű thread poollal dolgozó scheduler</li>
<li><code class="language-plaintext highlighter-rouge">Scheduler.boundedElastic() </code> - olyan thread poolal dolgozó scheduler, amely mérete növekedhet, de korlátos</li>
<li><code class="language-plaintext highlighter-rouge">Scheduler.single()</code> - egy újrafelhasználható szállal dolgozó scheduler</li>
<li><code class="language-plaintext highlighter-rouge">Scheduler.immediate()</code> - nem vált szálat</li>
</ul>
<p>Mindegyiknek van egy <code class="language-plaintext highlighter-rouge">new</code> szóval kezdődő változata is. A <code class="language-plaintext highlighter-rouge">parallel()</code> hívás első alkalommal elkészíti a <code class="language-plaintext highlighter-rouge">Scheduler</code>-t, majd mindig
ugyanazt adja vissza, addig a <code class="language-plaintext highlighter-rouge">newParallel()</code> mindig új <code class="language-plaintext highlighter-rouge">Scheduler</code> példányt hoz létre.</p>
<p>A <code class="language-plaintext highlighter-rouge">publishOn()</code> használatát mutatja be az alábbi kódrészlet:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">upperCaseNames</span> <span class="o">=</span> <span class="nc">Flux</span><span class="o">.</span><span class="na">fromIterable</span><span class="o">(</span><span class="n">employees</span><span class="o">)</span>
<span class="o">.</span><span class="na">filter</span><span class="o">(</span><span class="n">employee</span> <span class="o">-></span> <span class="n">employee</span><span class="o">.</span><span class="na">getYearOfBirth</span><span class="o">()</span> <span class="o">>=</span> <span class="mi">1980</span><span class="o">)</span>
<span class="o">.</span><span class="na">doOnNext</span><span class="o">(</span><span class="n">e</span> <span class="o">-></span> <span class="n">log</span><span class="o">.</span><span class="na">info</span><span class="o">(</span><span class="s">"filter"</span><span class="o">))</span>
<span class="o">.</span><span class="na">publishOn</span><span class="o">(</span><span class="nc">Schedulers</span><span class="o">.</span><span class="na">newParallel</span><span class="o">(</span><span class="s">"p1"</span><span class="o">))</span>
<span class="o">.</span><span class="na">map</span><span class="o">(</span><span class="nl">Employee:</span><span class="o">:</span><span class="n">getName</span><span class="o">)</span>
<span class="o">.</span><span class="na">doOnNext</span><span class="o">(</span><span class="n">e</span> <span class="o">-></span> <span class="n">log</span><span class="o">.</span><span class="na">info</span><span class="o">(</span><span class="s">"map - getName"</span><span class="o">))</span>
<span class="o">.</span><span class="na">map</span><span class="o">(</span><span class="nl">String:</span><span class="o">:</span><span class="n">toUpperCase</span><span class="o">)</span>
<span class="o">.</span><span class="na">doOnNext</span><span class="o">(</span><span class="n">e</span> <span class="o">-></span> <span class="n">log</span><span class="o">.</span><span class="na">info</span><span class="o">(</span><span class="s">"map - toUpperCase"</span><span class="o">))</span>
<span class="o">;</span>
</code></pre></div></div>
<p>Ekkor a <code class="language-plaintext highlighter-rouge">filter</code> a <code class="language-plaintext highlighter-rouge">main</code> szálon fut, majd a <code class="language-plaintext highlighter-rouge">publishOn()</code> szálat vált, és a további operátorokat már a <code class="language-plaintext highlighter-rouge">p1</code> <code class="language-plaintext highlighter-rouge">Scheduler</code> futtatja.</p>
<h2 id="publishon-operátor-többszöri-használata"><code class="language-plaintext highlighter-rouge">publishOn()</code> operátor többszöri használata</h2>
<p>Amennyiben több <code class="language-plaintext highlighter-rouge">publishOn()</code> hívás van, többször történik meg a szál váltása, hiszen a <code class="language-plaintext highlighter-rouge">publishOn()</code> hat
az utána következő operátorra.</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">upperCaseNames</span> <span class="o">=</span> <span class="nc">Flux</span><span class="o">.</span><span class="na">fromIterable</span><span class="o">(</span><span class="n">employees</span><span class="o">)</span>
<span class="o">.</span><span class="na">filter</span><span class="o">(</span><span class="n">employee</span> <span class="o">-></span> <span class="n">employee</span><span class="o">.</span><span class="na">getYearOfBirth</span><span class="o">()</span> <span class="o">>=</span> <span class="mi">1980</span><span class="o">)</span>
<span class="o">.</span><span class="na">doOnNext</span><span class="o">(</span><span class="n">e</span> <span class="o">-></span> <span class="n">log</span><span class="o">.</span><span class="na">info</span><span class="o">(</span><span class="s">"filter"</span><span class="o">))</span>
<span class="o">.</span><span class="na">publishOn</span><span class="o">(</span><span class="nc">Schedulers</span><span class="o">.</span><span class="na">newParallel</span><span class="o">(</span><span class="s">"p1"</span><span class="o">))</span>
<span class="o">.</span><span class="na">map</span><span class="o">(</span><span class="nl">Employee:</span><span class="o">:</span><span class="n">getName</span><span class="o">)</span>
<span class="o">.</span><span class="na">doOnNext</span><span class="o">(</span><span class="n">e</span> <span class="o">-></span> <span class="n">log</span><span class="o">.</span><span class="na">info</span><span class="o">(</span><span class="s">"map - getName"</span><span class="o">))</span>
<span class="o">.</span><span class="na">publishOn</span><span class="o">(</span><span class="nc">Schedulers</span><span class="o">.</span><span class="na">newParallel</span><span class="o">(</span><span class="s">"p2"</span><span class="o">))</span>
<span class="o">.</span><span class="na">map</span><span class="o">(</span><span class="nl">String:</span><span class="o">:</span><span class="n">toUpperCase</span><span class="o">)</span>
<span class="o">.</span><span class="na">doOnNext</span><span class="o">(</span><span class="n">e</span> <span class="o">-></span> <span class="n">log</span><span class="o">.</span><span class="na">info</span><span class="o">(</span><span class="s">"map - toUpperCase"</span><span class="o">))</span>
<span class="o">;</span>
</code></pre></div></div>
<p>Ekkor a <code class="language-plaintext highlighter-rouge">filter()</code> a <code class="language-plaintext highlighter-rouge">main</code> szálon fut, a <code class="language-plaintext highlighter-rouge">map()</code>-et a <code class="language-plaintext highlighter-rouge">p1</code>, míg a következő <code class="language-plaintext highlighter-rouge">map()</code>-et már a <code class="language-plaintext highlighter-rouge">p2</code> fogja futtatni.</p>
<p>Azaz kijelenthező, hogy a <code class="language-plaintext highlighter-rouge">publishOn()</code> az operátort követő operátorokra van hatással, egészen a következő
<code class="language-plaintext highlighter-rouge">publishOn()</code> hívásig.</p>
<h2 id="subscribeon-operátor"><code class="language-plaintext highlighter-rouge">subscribeOn()</code> operátor</h2>
<p>A <code class="language-plaintext highlighter-rouge">subscribeOn()</code> operátor dokumentációja elég rejtéjes, azt mondja, hogy a <code class="language-plaintext highlighter-rouge">subscribe()</code>, <code class="language-plaintext highlighter-rouge">onSubscribe()</code> és a <code class="language-plaintext highlighter-rouge">request()</code>
kerül más szálon meghívásra. De mi ennek a jelentősége? Az, hogy ilyenkor valójában a hatás a teljes streamre vonatkozik a
source-tól kezdődően.</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">upperCaseNames</span> <span class="o">=</span> <span class="nc">Flux</span><span class="o">.</span><span class="na">fromIterable</span><span class="o">(</span><span class="n">employees</span><span class="o">)</span>
<span class="o">.</span><span class="na">filter</span><span class="o">(</span><span class="n">employee</span> <span class="o">-></span> <span class="n">employee</span><span class="o">.</span><span class="na">getYearOfBirth</span><span class="o">()</span> <span class="o">>=</span> <span class="mi">1980</span><span class="o">)</span>
<span class="o">.</span><span class="na">doOnNext</span><span class="o">(</span><span class="n">e</span> <span class="o">-></span> <span class="n">log</span><span class="o">.</span><span class="na">info</span><span class="o">(</span><span class="s">"filter"</span><span class="o">))</span>
<span class="o">.</span><span class="na">map</span><span class="o">(</span><span class="nl">Employee:</span><span class="o">:</span><span class="n">getName</span><span class="o">)</span>
<span class="o">.</span><span class="na">doOnNext</span><span class="o">(</span><span class="n">e</span> <span class="o">-></span> <span class="n">log</span><span class="o">.</span><span class="na">info</span><span class="o">(</span><span class="s">"map - getName"</span><span class="o">))</span>
<span class="o">.</span><span class="na">map</span><span class="o">(</span><span class="nl">String:</span><span class="o">:</span><span class="n">toUpperCase</span><span class="o">)</span>
<span class="o">.</span><span class="na">doOnNext</span><span class="o">(</span><span class="n">e</span> <span class="o">-></span> <span class="n">log</span><span class="o">.</span><span class="na">info</span><span class="o">(</span><span class="s">"map - toUpperCase"</span><span class="o">))</span>
<span class="o">.</span><span class="na">subscribeOn</span><span class="o">(</span><span class="nc">Schedulers</span><span class="o">.</span><span class="na">newParallel</span><span class="o">(</span><span class="s">"s1"</span><span class="o">))</span>
<span class="o">;</span>
</code></pre></div></div>
<p>Attól függetlenül, hogy a <code class="language-plaintext highlighter-rouge">subscribeOn()</code> az utolsó operátor, az összes operátort az <code class="language-plaintext highlighter-rouge">s1</code> fogja futtatni!</p>
<p>Ez független attól, hogy hova tesszük a <code class="language-plaintext highlighter-rouge">subscribeOn()</code> hívást. Azaz ez a kód is ugyanazt a működést eredményezi:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">upperCaseNames</span> <span class="o">=</span> <span class="nc">Flux</span><span class="o">.</span><span class="na">fromIterable</span><span class="o">(</span><span class="n">employees</span><span class="o">)</span>
<span class="o">.</span><span class="na">subscribeOn</span><span class="o">(</span><span class="nc">Schedulers</span><span class="o">.</span><span class="na">newParallel</span><span class="o">(</span><span class="s">"s1"</span><span class="o">))</span>
<span class="o">.</span><span class="na">filter</span><span class="o">(</span><span class="n">employee</span> <span class="o">-></span> <span class="n">employee</span><span class="o">.</span><span class="na">getYearOfBirth</span><span class="o">()</span> <span class="o">>=</span> <span class="mi">1980</span><span class="o">)</span>
<span class="o">.</span><span class="na">doOnNext</span><span class="o">(</span><span class="n">e</span> <span class="o">-></span> <span class="n">log</span><span class="o">.</span><span class="na">info</span><span class="o">(</span><span class="s">"filter"</span><span class="o">))</span>
<span class="o">.</span><span class="na">map</span><span class="o">(</span><span class="nl">Employee:</span><span class="o">:</span><span class="n">getName</span><span class="o">)</span>
<span class="o">.</span><span class="na">doOnNext</span><span class="o">(</span><span class="n">e</span> <span class="o">-></span> <span class="n">log</span><span class="o">.</span><span class="na">info</span><span class="o">(</span><span class="s">"map - getName"</span><span class="o">))</span>
<span class="o">.</span><span class="na">map</span><span class="o">(</span><span class="nl">String:</span><span class="o">:</span><span class="n">toUpperCase</span><span class="o">)</span>
<span class="o">.</span><span class="na">doOnNext</span><span class="o">(</span><span class="n">e</span> <span class="o">-></span> <span class="n">log</span><span class="o">.</span><span class="na">info</span><span class="o">(</span><span class="s">"map - toUpperCase"</span><span class="o">))</span>
<span class="o">;</span>
</code></pre></div></div>
<h2 id="subscribeon-operátor-többszöri-használata"><code class="language-plaintext highlighter-rouge">subscribeOn()</code> operátor többszöri használata</h2>
<p>Amennyiben a <code class="language-plaintext highlighter-rouge">subscribeOn()</code> metódust többször használjuk, csak az első hívásnak
lesz hatása, a többinek nem.</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">upperCaseNames</span> <span class="o">=</span> <span class="nc">Flux</span><span class="o">.</span><span class="na">fromIterable</span><span class="o">(</span><span class="n">employees</span><span class="o">)</span>
<span class="o">.</span><span class="na">filter</span><span class="o">(</span><span class="n">employee</span> <span class="o">-></span> <span class="n">employee</span><span class="o">.</span><span class="na">getYearOfBirth</span><span class="o">()</span> <span class="o">>=</span> <span class="mi">1980</span><span class="o">)</span>
<span class="o">.</span><span class="na">doOnNext</span><span class="o">(</span><span class="n">e</span> <span class="o">-></span> <span class="n">log</span><span class="o">.</span><span class="na">info</span><span class="o">(</span><span class="s">"filter"</span><span class="o">))</span>
<span class="o">.</span><span class="na">subscribeOn</span><span class="o">(</span><span class="nc">Schedulers</span><span class="o">.</span><span class="na">newParallel</span><span class="o">(</span><span class="s">"s1"</span><span class="o">))</span>
<span class="o">.</span><span class="na">map</span><span class="o">(</span><span class="nl">Employee:</span><span class="o">:</span><span class="n">getName</span><span class="o">)</span>
<span class="o">.</span><span class="na">doOnNext</span><span class="o">(</span><span class="n">e</span> <span class="o">-></span> <span class="n">log</span><span class="o">.</span><span class="na">info</span><span class="o">(</span><span class="s">"map - getName"</span><span class="o">))</span>
<span class="o">.</span><span class="na">map</span><span class="o">(</span><span class="nl">String:</span><span class="o">:</span><span class="n">toUpperCase</span><span class="o">)</span>
<span class="o">.</span><span class="na">doOnNext</span><span class="o">(</span><span class="n">e</span> <span class="o">-></span> <span class="n">log</span><span class="o">.</span><span class="na">info</span><span class="o">(</span><span class="s">"map - toUpperCase"</span><span class="o">))</span>
<span class="o">.</span><span class="na">subscribeOn</span><span class="o">(</span><span class="nc">Schedulers</span><span class="o">.</span><span class="na">newParallel</span><span class="o">(</span><span class="s">"s2"</span><span class="o">))</span>
<span class="o">;</span>
</code></pre></div></div>
<p>És bár van <code class="language-plaintext highlighter-rouge">subscribeOn()</code> hívás rendre <code class="language-plaintext highlighter-rouge">s1</code> és <code class="language-plaintext highlighter-rouge">s2</code> <code class="language-plaintext highlighter-rouge">Scheduler</code>-rel, minden operátort az <code class="language-plaintext highlighter-rouge">s1</code> fogja futtatni.</p>
<h2 id="publishon-és-subscribeon-keverése"><code class="language-plaintext highlighter-rouge">publishOn()</code> és <code class="language-plaintext highlighter-rouge">subscribeOn()</code> keverése</h2>
<p>Ha a <code class="language-plaintext highlighter-rouge">publishOn()</code> és <code class="language-plaintext highlighter-rouge">subscribeOn()</code> operátorokat együtt használjuk, akkor a
<code class="language-plaintext highlighter-rouge">subscribeOn()</code> a stream elejétől hat egészen az első <code class="language-plaintext highlighter-rouge">publishOn()</code>-ig, ami
szálat vált, egészen a következő <code class="language-plaintext highlighter-rouge">publishOn()</code>-ig, ami ismét szálat vált.</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">upperCaseNames</span> <span class="o">=</span> <span class="nc">Flux</span><span class="o">.</span><span class="na">fromIterable</span><span class="o">(</span><span class="n">employees</span><span class="o">)</span>
<span class="o">.</span><span class="na">filter</span><span class="o">(</span><span class="n">employee</span> <span class="o">-></span> <span class="n">employee</span><span class="o">.</span><span class="na">getYearOfBirth</span><span class="o">()</span> <span class="o">>=</span> <span class="mi">1980</span><span class="o">)</span>
<span class="o">.</span><span class="na">doOnNext</span><span class="o">(</span><span class="n">e</span> <span class="o">-></span> <span class="n">log</span><span class="o">.</span><span class="na">info</span><span class="o">(</span><span class="s">"filter"</span><span class="o">))</span>
<span class="o">.</span><span class="na">publishOn</span><span class="o">(</span><span class="nc">Schedulers</span><span class="o">.</span><span class="na">newParallel</span><span class="o">(</span><span class="s">"p1"</span><span class="o">))</span>
<span class="o">.</span><span class="na">map</span><span class="o">(</span><span class="nl">Employee:</span><span class="o">:</span><span class="n">getName</span><span class="o">)</span>
<span class="o">.</span><span class="na">doOnNext</span><span class="o">(</span><span class="n">e</span> <span class="o">-></span> <span class="n">log</span><span class="o">.</span><span class="na">info</span><span class="o">(</span><span class="s">"map - getName"</span><span class="o">))</span>
<span class="o">.</span><span class="na">publishOn</span><span class="o">(</span><span class="nc">Schedulers</span><span class="o">.</span><span class="na">newParallel</span><span class="o">(</span><span class="s">"p2"</span><span class="o">))</span>
<span class="o">.</span><span class="na">map</span><span class="o">(</span><span class="nl">String:</span><span class="o">:</span><span class="n">toUpperCase</span><span class="o">)</span>
<span class="o">.</span><span class="na">doOnNext</span><span class="o">(</span><span class="n">e</span> <span class="o">-></span> <span class="n">log</span><span class="o">.</span><span class="na">info</span><span class="o">(</span><span class="s">"map - toUpperCase"</span><span class="o">))</span>
<span class="o">.</span><span class="na">subscribeOn</span><span class="o">(</span><span class="nc">Schedulers</span><span class="o">.</span><span class="na">newParallel</span><span class="o">(</span><span class="s">"s1"</span><span class="o">))</span>
<span class="o">;</span>
</code></pre></div></div>
<p>Így a <code class="language-plaintext highlighter-rouge">filter()</code>-t az <code class="language-plaintext highlighter-rouge">s1</code> futtatja, a <code class="language-plaintext highlighter-rouge">map()</code>-et a <code class="language-plaintext highlighter-rouge">p1</code> és a következő <code class="language-plaintext highlighter-rouge">map()</code>-et pedig a <code class="language-plaintext highlighter-rouge">p2</code>.</p>
<p>Ez grafikusan is igen jól ábrázolható.</p>
<p><img src="/artifacts/posts/2024-02-01-project-reactor-szalkezeles/mixed.drawio.png" alt="`publishOn()` és `subscribeOn()` keverése" /></p>
<ul>
<li><a href="https://spring.io/blog/2019/03/06/flight-of-the-flux-1-assembly-vs-subscription">Flight of the Flux 1 - Assembly vs Subscription</a></li>
<li><a href="https://projectreactor.io/docs/core/release/reference/index.html">Reactor 3 Reference Guide</a></li>
<li><a href="https://spring.io/blog/2019/12/13/flight-of-the-flux-3-hopping-threads-and-schedulers">Flight of the Flux 3 - Hopping Threads and Schedulers</a></li>
<li><a href="https://www.woolha.com/tutorials/project-reactor-publishon-vs-subscribeon-difference">Project Reactor - publishOn vs subscribeOn Difference</a></li>
<li><a href="https://eherrera.net/project-reactor-course/">Unraveling Project Reactor</a></li>
</ul>
A REST API hypertext-driven legyen2023-10-11T08:00:00+00:00http://www.jtechlog.hu/2023/10/11/hateoas<p>Bár erős vitákat tapasztalok a REST körül, valahogy a hypertext-driven API-val
ritkán találkozom.</p>
<p>Roy T. Fielding, a REST megálmodója a gyakran idézett <a href="https://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven">REST APIs must be hypertext-driven</a>
cikkjében a következőt mondja:</p>
<blockquote>
<p>In other words, if the engine of application state (and hence the API) is not being driven by hypertext, then it cannot be RESTful and cannot be a REST API. Period.</p>
</blockquote>
<p>Erre erősít rá a <a href="https://martinfowler.com/articles/richardsonMaturityModel.html">Richardson Maturity Model</a> is, mely
három lépésben mutatja be a REST alapvető elemeit:</p>
<ul>
<li>Level 1 - Resources</li>
<li>Level 2 - HTTP Verbs</li>
<li>Level 3 - Hypermedia Controls</li>
</ul>
<p>És egy API csak akkor nevezhető RESTfulnak, ha az összes szinten leírtakat teljesíti, igen a hypermedia control használatát is.</p>
<p>De mit is jelent ez? Az állítás egyszerű: a kliensnek minden előzetes tudás nélkül igénybe kell tudnia venni a
RESTful API mögötti szolgáltatásokat, és ez linkek segítségével valósulhat meg.</p>
<p>Pl. az előző <a href="/2023/08/31/workflow-es-rest.html">Workflow REST API-n posztomban</a> szereplő Issue Tracker alkalmazás
a következőképp adja vissza a hibajegyek listáját:</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
</span><span class="nl">"_embedded"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"jtl:issueResourceList"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="p">{</span><span class="w">
</span><span class="nl">"id"</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w">
</span><span class="nl">"title"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Write a post about REST"</span><span class="p">,</span><span class="w">
</span><span class="nl">"state"</span><span class="p">:</span><span class="w"> </span><span class="s2">"NEW"</span><span class="p">,</span><span class="w">
</span><span class="nl">"_links"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"self"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"href"</span><span class="p">:</span><span class="w"> </span><span class="s2">"http://localhost:8080/api/issues/1"</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="nl">"actions"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"href"</span><span class="p">:</span><span class="w"> </span><span class="s2">"http://localhost:8080/api/issues/1/actions"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">]</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="nl">"_links"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"self"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"href"</span><span class="p">:</span><span class="w"> </span><span class="s2">"http://localhost:8080/api/issues"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
<p>Látható a hibajegyek listáját reprezentáló URL-jét tartalmazó <code class="language-plaintext highlighter-rouge">self</code> link, valamint hibajegyenként
a hibajegyet reprezentáló URL-jének linkje szintén <code class="language-plaintext highlighter-rouge">self</code> névvel, valamint a hibajegyhez tartozó
lépéseket reprenzentáló URL <code class="language-plaintext highlighter-rouge">actions</code> névvel.</p>
<p>Ez a Hypermedia as the engine of application state (HATEOAS), melyet szintén Roy T. Fielding
vezetett be a disszertációjában. A HATEOAS szerint a szerver a kliens számára
linkekkel jelzi, hogy mit tehet.</p>
<p>Az elv mögötte az, hogy a REST API-t is úgy lehessen használni, mint a webet. Ahol vannak oldalak és
linkek és semmilyen előzetes információra nincs szükségem a használatához.</p>
<p>Hogy hogyan viszonyul ez az OpenAPI-hoz (Swaggerhez)? Elvileg a cél ugyanaz, és míg a HATEOAS
a REST gonolatiságához illeszkedik, az OpenAPI inkább az RPC-hez.</p>
<!-- more -->
<h2 id="implementáció">Implementáció</h2>
<p>Ennek előállítására létezik egy <a href="https://spring.io/projects/spring-hateoas">Spring HATEOAS</a>
projekt.</p>
<p>A teljes forráskód <a href="https://github.com/vicziani/jtechlog-rest-workflow">megtalálható GitHubon</a>.</p>
<p>A legegyszerűbb megoldás, hogy legyenek a resource osztályaim a <code class="language-plaintext highlighter-rouge">RepresentationModel</code> osztály leszármazottjai.</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">class</span> <span class="nc">IssueResource</span> <span class="kd">extends</span> <span class="nc">RepresentationModel</span><span class="o"><</span><span class="nc">IssueResource</span><span class="o">></span> <span class="o">{</span>
<span class="o">}</span>
</code></pre></div></div>
<p>Innentől kezdve meg tudom mondani, hogy milyen linkei legyenek:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">model</span><span class="o">.</span><span class="na">add</span><span class="o">(</span><span class="n">linkTo</span><span class="o">(</span><span class="n">methodOn</span><span class="o">(</span><span class="nc">IssueController</span><span class="o">.</span><span class="na">class</span><span class="o">).</span><span class="na">getIssue</span><span class="o">(</span><span class="n">model</span><span class="o">.</span><span class="na">getId</span><span class="o">())).</span><span class="na">withSelfRel</span><span class="o">());</span>
<span class="n">model</span><span class="o">.</span><span class="na">add</span><span class="o">(</span><span class="n">linkTo</span><span class="o">(</span><span class="n">methodOn</span><span class="o">(</span><span class="nc">IssueController</span><span class="o">.</span><span class="na">class</span><span class="o">).</span><span class="na">getIssueActions</span><span class="o">(</span><span class="n">model</span><span class="o">.</span><span class="na">getId</span><span class="o">())).</span><span class="na">withRel</span><span class="o">(</span><span class="s">"actions"</span><span class="o">));</span>
</code></pre></div></div>
<p>Az is látható, hogy nem kézzel állítom össze az URL-t (bár megtehetném), hanem a
controller osztály metódusain található annotációkra támaszkodom. Így elkerülöm
az ismétlést, és ha változtatok az URL-en, elég csak egy helyen ezt megtenni.</p>
<p>Amennyiben kollekcióm van, a controllerből nem <code class="language-plaintext highlighter-rouge">List</code> típussal, hanem
<code class="language-plaintext highlighter-rouge">CollectionModel</code> típussal térek vissza.</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">class</span> <span class="nc">IssueController</span> <span class="o">{</span>
<span class="nd">@GetMapping</span>
<span class="kd">public</span> <span class="nc">CollectionModel</span><span class="o"><</span><span class="nc">IssueResource</span><span class="o">></span> <span class="nf">getIssues</span><span class="o">()</span> <span class="o">{</span>
<span class="kt">var</span> <span class="n">issues</span> <span class="o">=</span> <span class="n">issueService</span><span class="o">.</span><span class="na">getIssues</span><span class="o">();</span>
<span class="k">return</span> <span class="nc">CollectionModel</span><span class="o">.</span><span class="na">of</span><span class="o">(</span><span class="n">issues</span><span class="o">).</span><span class="na">withFallbackType</span><span class="o">(</span><span class="nc">IssueResource</span><span class="o">.</span><span class="na">class</span><span class="o">);</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>A <code class="language-plaintext highlighter-rouge">CollectionModel</code> az <code class="language-plaintext highlighter-rouge">of()</code> metódussal gyártható le. Mivel üres lista esetén elveszik
a generikus típusaban tárolt típusinformáció, ezért hívom meg a <code class="language-plaintext highlighter-rouge">withFallbackType()</code>
metódust.</p>
<p>Többször előfordul, hogy nem én példányosítom le a resource-ot, hanem készen kapom.
Pl. mikor a perzisztens rétegből jön vissza:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">interface</span> <span class="nc">IssueRepository</span> <span class="kd">extends</span> <span class="nc">JpaRepository</span><span class="o"><</span><span class="nc">Issue</span><span class="o">,</span> <span class="nc">Long</span><span class="o">></span> <span class="o">{</span>
<span class="nd">@Query</span><span class="o">(</span><span class="s">"select new issuetracker.IssueResource(i.id, i.title, i.state) from Issue i where i.id = :id"</span><span class="o">)</span>
<span class="nc">Optional</span><span class="o"><</span><span class="nc">IssueResource</span><span class="o">></span> <span class="nf">findIssueResourceById</span><span class="o">(</span><span class="kt">long</span> <span class="n">id</span><span class="o">);</span>
<span class="nd">@Query</span><span class="o">(</span><span class="s">"select new issuetracker.IssueResource(i.id, i.title, i.state) from Issue i order by i.title"</span><span class="o">)</span>
<span class="nc">List</span><span class="o"><</span><span class="nc">IssueResource</span><span class="o">></span> <span class="nf">findAllIssueResourceOrderByTitle</span><span class="o">();</span>
<span class="o">}</span>
</code></pre></div></div>
<p>Ilyenkor használható a <code class="language-plaintext highlighter-rouge">RepresentationModelProcessor</code>. Ez ugyanis észreveszi, ha
egy controllerből <code class="language-plaintext highlighter-rouge">RepresentationModel</code>-lel térek vissza, és automatikusan kiegészíti a
linkekkel.</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Component</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">IssueResourceProcessor</span> <span class="kd">implements</span> <span class="nc">RepresentationModelProcessor</span><span class="o"><</span><span class="nc">IssueResource</span><span class="o">></span> <span class="o">{</span>
<span class="nd">@Override</span>
<span class="kd">public</span> <span class="nc">IssueResource</span> <span class="nf">process</span><span class="o">(</span><span class="nc">IssueResource</span> <span class="n">model</span><span class="o">)</span> <span class="o">{</span>
<span class="n">model</span><span class="o">.</span><span class="na">add</span><span class="o">(</span><span class="n">linkTo</span><span class="o">(</span><span class="n">methodOn</span><span class="o">(</span><span class="nc">IssueController</span><span class="o">.</span><span class="na">class</span><span class="o">).</span><span class="na">getIssue</span><span class="o">(</span><span class="n">model</span><span class="o">.</span><span class="na">getId</span><span class="o">())).</span><span class="na">withSelfRel</span><span class="o">());</span>
<span class="n">model</span><span class="o">.</span><span class="na">add</span><span class="o">(</span><span class="n">linkTo</span><span class="o">(</span><span class="n">methodOn</span><span class="o">(</span><span class="nc">IssueController</span><span class="o">.</span><span class="na">class</span><span class="o">).</span><span class="na">getIssueActions</span><span class="o">(</span><span class="n">model</span><span class="o">.</span><span class="na">getId</span><span class="o">())).</span><span class="na">withRel</span><span class="o">(</span><span class="s">"actions"</span><span class="o">));</span>
<span class="k">return</span> <span class="n">model</span><span class="o">;</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>És ekkor már a megfelelő JSON-nel tér vissza a REST hívás. A válasz content type-ja <code class="language-plaintext highlighter-rouge">application/hal+json</code>.
A Spring HATEOAS ugyanis több media type-ot is támogat. Ezek különböző reprezentációk. Ebből az első
és alapértelmezetten támogatott a Hypertext Application Language (HAL). Ez azt írja le, hogy a
linkeknek hogyan kell megjelenniük a JSON-ben.</p>
<p>Ebben van egy az XML névtérnek megfelelő fogalom, amivel minősíteni tudjuk a saját linkjeinket.
Ez a Curie. Ezt a következő beannel kapcsolhatjuk be.</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Bean</span>
<span class="kd">public</span> <span class="nc">CurieProvider</span> <span class="nf">curieProvider</span><span class="o">()</span> <span class="o">{</span>
<span class="k">return</span> <span class="k">new</span> <span class="nf">DefaultCurieProvider</span><span class="o">(</span><span class="s">"jtl"</span><span class="o">,</span> <span class="nc">UriTemplate</span><span class="o">.</span><span class="na">of</span><span class="o">(</span><span class="s">"https://jtechlog.hu/rels/{rel}"</span><span class="o">));</span>
<span class="o">}</span>
</code></pre></div></div>
<p>Ekkor kicsit változik a JSON.</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
</span><span class="nl">"_embedded"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"jtl:issueResourceList"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="p">{</span><span class="w">
</span><span class="nl">"id"</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w">
</span><span class="nl">"title"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Write a post about REST"</span><span class="p">,</span><span class="w">
</span><span class="nl">"state"</span><span class="p">:</span><span class="w"> </span><span class="s2">"NEW"</span><span class="p">,</span><span class="w">
</span><span class="nl">"_links"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"self"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"href"</span><span class="p">:</span><span class="w"> </span><span class="s2">"http://localhost:8080/api/issues/1"</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="nl">"jtl:actions"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"href"</span><span class="p">:</span><span class="w"> </span><span class="s2">"http://localhost:8080/api/issues/1/actions"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">]</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="nl">"_links"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"self"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"href"</span><span class="p">:</span><span class="w"> </span><span class="s2">"http://localhost:8080/api/issues"</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="nl">"curies"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="p">{</span><span class="w">
</span><span class="nl">"href"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://jtechlog.hu/rels/{rel}"</span><span class="p">,</span><span class="w">
</span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"jtl"</span><span class="p">,</span><span class="w">
</span><span class="nl">"templated"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">]</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
<p>Látható, hogy megjelent egy <code class="language-plaintext highlighter-rouge">jtl</code> nevezetű Curie, melynek definíciója
<code class="language-plaintext highlighter-rouge">https://jtechlog.hu/rels/{rel}</code>. Majd a linkek nevei ezzel
vannak minősítve: <code class="language-plaintext highlighter-rouge">jtl:actions</code>. Igazából az említett címre kéne
elhelyezni a dokumentációt.</p>
<p>A HAL nem támogatja, hogy megmondjuk, hogy milyen URL-eken milyen
HTTP metódusok támogatottak. Ehhez akár egy <code class="language-plaintext highlighter-rouge">OPTIONS</code> kérést
tudunk beküldeni.</p>
<pre><code class="language-plain">> OPTIONS http://localhost:8080/api/issues
< HTTP/1.1 200
< Allow: GET,HEAD,POST,OPTIONS
< Accept-Patch:
< Content-Length: 0
< Date: Wed, 11 Oct 2023 09:42:28 GMT
< Connection: close
</code></pre>
<p>Azaz a <code class="language-plaintext highlighter-rouge">/api/issues</code> címen a <code class="language-plaintext highlighter-rouge">GET</code>, <code class="language-plaintext highlighter-rouge">HEAD</code>, <code class="language-plaintext highlighter-rouge">POST</code> és <code class="language-plaintext highlighter-rouge">OPTIONS</code> támogatott.</p>
<p>Van olyan media type, ami ezt is leírja, ez a <a href="https://rwcbook.github.io/hal-forms/">HAL-FORMS</a>.
A Spring HATEOAS <a href="https://docs.spring.io/spring-hateoas/docs/current/reference/html/#mediatypes">referencia dokumentációjában</a>
megtalálható még több media type is.</p>
<p>Persze ezt kliens oldalon is támogatni kell, azaz nem beégetni az URL-eket, hanem dinamikusan lekérdezni.
Erre hasznos pl. a <a href="https://www.codecentric.de/wissens-hub/blog/traverson">Traverson</a> JavaScript
library, vagy a Spring HATEOAS projekt <code class="language-plaintext highlighter-rouge">Traverson</code> osztálya.</p>
<p>Az, hogy ez kinek mennyire hasznos, mindenki döntse el saját maga. De ne feledd, Roy T. Fielding
szerint enélkül nem REST az API-d.</p>
Workflow REST API-n2023-08-31T08:00:00+00:00http://www.jtechlog.hu/2023/08/31/workflow-es-rest<p>Bár kétségtelenül a REST a legelterjedtebb kommunikációs mód, akár ugyanolyan
platformon fejlesztett alkalmazások között is, REST API tervezéskor
nekem sok kérdés merül fel, melyre nem kapok megnyugtató választ,
és sok olyan megoldással találkozom, amelyek nem felelnek meg a REST elveknek,
vagy egyszerűen csak nem tetszenek.</p>
<p>Sokáig próbáltam megfogalmazni, hogy mi az alapvető problémám, és talán most
sikerült közelebb kerülni. Vegyünk két microservice-t, melyek mindegyike Javaban készült,
Spring Boot használatával. Mindkét oldalon a programozási nyelv alapeleme az osztályok,
ahol érvényesül az egységbezárás, azaz az attribútumok mellett ott vannak
a metódusok, a megfelelő paraméterekkel és visszatérési értékekkel. Ez
a legtöbb fejlesztőnek triviális.</p>
<p>A REST a kettő között viszont egy teljesen
más absztrakció. Bár a REST nem mondja ki, hogy csak HTTP-vel együtt használható,
más protokollal működni még nem láttam.
A REST alapfogalma az erőforrás, és az azon végzett műveletek, melyeket a HTTP metódusok valósítanak meg, mint a <code class="language-plaintext highlighter-rouge">GET</code>, <code class="language-plaintext highlighter-rouge">POST</code>, <code class="language-plaintext highlighter-rouge">PUT</code> és <code class="language-plaintext highlighter-rouge">DELETE</code>.
Ez nehezen feleltethető meg az objektumorientált világnak.
Egy objektum létrehozására tipikusan konstruktort használunk, ebből nem feltétlen egy van.
Ezen kívül egy objektumnak lehet több metódusa, amik nem feltétlenül feleltethetőek meg a HTTP metódusoknak,
hiszen nem csak ezeket az alapműveleteket használjuk, valamint lehet belőle sokkal több is, mint négy.
Míg a Java nyelv szabvány, addig a REST csak egy ajánlás, és az sincs megfelelően
definiálva, hogy hogyan kéne implementálni, az meg aztán pláne nincs, hogy a kettőt
hogyan feleltessük meg egymásnak. És ennek az az eredménye, hogy minden projekten
teljesen más megoldásokat látok, a legtöbb helyen kompromisszumokkal.</p>
<p><img src="/artifacts/posts/2023-08-31-workflow-es-rest/absztakciok.png" alt="Absztrakciók" /></p>
<p>Az RPC alapú kommunikáció esetén igazából nincs ilyen probléma, mert nem szükséges
átfordítás, nincs szükség az absztrakció váltására. Hiszen ott is eljárások vannak,
paraméterekkel és visszatérési értékekkel. A Spring Frameworkben volt is ilyen
lehetőség, hogy egy távoli eljáráshívás történt, azonban a protokollt
kódolás nélkül cserélni lehetett alatta, pl. választhattunk RMI-t,
vagy ki emlékszik még a Hessian és Burlap protokollokra.
Sajnos ez azóta eltűnt.
A SOAP esetén sem volt akkora probléma az átfordítás, hiszen ott is vannak
az operációk (eljárásnak felel meg), és a paraméter, valamint a visszatérési
érték is egy-egy (XML) dokumentum. Vannak modernebb RPC alapú kommunikációs
protokollok is, mint pl. JSON-RPC, vagy az egyre inkább elterjedő
multiplatform gRPC, bináris Protocol Buffers formátummal.</p>
<p>Amennyiben CRUD alkalmazásról van szó, a REST használata talán triviális. De ha
egy olyan alkalmazásról beszélünk, amiben komoly üzleti logika van (és hiszem,
hogy a legtöbb alkalmazásnak ilyennek kéne lennie, csak a fejlesztők “butítják le”
CRUD alkalmazássá, amely végső soron oda is vezethet, hogy borzasztó felhasználói
felületek kerülnek kifejlesztésre), akkor már több kérdés merül fel.</p>
<p>Vegyünk például egy hibajegykezelő alkalmazást (issue tracker), mely rendelkezzen egy minimális
üzleti logikával. Ha hibajegy kerül felvételre, az <em>új</em> (<em>new</em>) állapottal kerül létrehozásra.
Amennyiben valaki elkezd rajta dolgozni, átkerül <em>folyamatban</em> (<em>in_progress</em>) állapotba.
Amennyiben elkészült, átkerül <em>megoldott</em> (<em>resolved</em>) állapotba. Bár a legtöbb hibajegykezelő
alkalmazás lehetővé teszi, hogy egyedi munkafolyamatot lehessen benne létrehozni,
ettől most tekintsünk el, mert akkor megint más problémák merülnek fel.
Ezt egy egyszerű állapotdiagrammal lehet a legegyszerűbben szemléltetni.</p>
<p><img src="/artifacts/posts/2023-08-31-workflow-es-rest/allapotatmenet-diagram.png" alt="Absztrakciók" /></p>
<p>Talán egy hibajegy felvételét még el tudom képzelni REST API-n (bár már ott is vannak
kérdéseim), azonban a különböző munkafolyamat lépések implementálásakor már
bizonytalanabb vagyok. Ez a poszt ezt a problémakört járja körbe,
forráskódokkal alátámasztva.</p>
<!-- more -->
<p>Egy REST API erőforrásokból áll, mely erőforrásokat az URL-ekkel azonosítunk.
Ezekkel tudunk elemi, CRUD műveleteket elvégezni, pl. lekérdezni.
Ebben az alkalmazásban erőforrás a hibajegy, azaz <code class="language-plaintext highlighter-rouge">issue</code>, <code class="language-plaintext highlighter-rouge">GET</code> metódussal
kérdezzük le a <code class="language-plaintext highlighter-rouge">/issues/1</code> URL-en (ahol a legjobb gyakorlat, hogy a resource nevét
többesszámban írjuk, és utána következik annak azonosítója). Ekkor a
következőt kapjuk vissza:</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
</span><span class="nl">"id"</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w">
</span><span class="nl">"title"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Write a post about REST"</span><span class="p">,</span><span class="w">
</span><span class="nl">"state"</span><span class="p">:</span><span class="w"> </span><span class="s2">"NEW"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
<p>Tegyük fel, hogy ezen a hibajegyen elkezdünk dolgozni, kérdés, hogy milyen REST
művelettel kéne ezt megoldani.</p>
<p>Itt az első megoldás lehetne az, hogy egy <code class="language-plaintext highlighter-rouge">PUT</code>-ot küldünk, amivel
átállítjuk a <code class="language-plaintext highlighter-rouge">state</code> mező értékét.</p>
<p>Itt két tábor folytat filozófiai vitát arról, hogy a <code class="language-plaintext highlighter-rouge">PUT</code> esetén a teljes
erőforrást küldeni kell, vagy csak annak azt a részét, amit változtatni akarunk (partial update).
Erre válasz nincs, én most azt választom, hogy teljes erőforrást küldök,
és ha csak egy részét akarom küldeni, arra a <code class="language-plaintext highlighter-rouge">PATCH</code> metódust fogom használni.</p>
<p>(Zárójelben megjegyzem, hogyha mindig teljes
resource-ot küldök, az lehetővé teszi azt, hogy lekérdezés, létrehozás és módosítás
esetén is ugyanazt a modell osztályt használjam. Régebben hajlamos voltam létrehozás
és módosítás esetén mást használni, hiszen mást adatokat akartam küldeni.
Itt ugyan az elterjed DTO elnevezést szokták használni,
de én <code class="language-plaintext highlighter-rouge">Resource</code> postfix-ű osztályokat fogok használni, a
<a href="/2023/08/01/modell-osztalyok.html">DTO-król szóló korábbi posztom alapján</a>.)</p>
<pre><code class="language-plain">PUT http://localhost:8080/api/issues/1
Content-Type: application/json
{
"id": 1,
"title": "Write a post about REST",
"state": "IN_PROGRESS"
}
</code></pre>
<p>Ezzel több bajom is van, nézzük ezeket tételesen:</p>
<ul>
<li>A legfontosabb, hogy nem látszik, hogy itt valójában egy üzleti folyamat került elindításra, ami mögött
több művelet is lehet, pl. ellenőrzések, e-mail kiküldések, stb.</li>
<li>El van rejtve a logika, a kliens tudja, hogy a munka megkezdésekor az
<code class="language-plaintext highlighter-rouge">IN_PROGRESS</code> állapotot kell használni.</li>
<li>Mi az, ami biztosítja azt, hogy közben a többi attribútum nem kerül módosításra, azaz
nem változtatja meg címet, esetleg olyan más attribútumot, ami csak olvasható.
(Erről jut eszembe, hogy mi van akkor, ha a törzsben és az url-ben nem ugyanaz az id szerepel. Miért kell ezt itt is és ott is küldeni?)</li>
<li>Mi van, ha plusz adatokat kell adni a művelethez, melyek nem konkrétan az erőforráshoz tartoznak? Pl. a művelet idejét, a
felhasználót, művelet okát, stb.? Mi van, ha ezeket ugyanúgy REST API-n le kell kérdezni?</li>
</ul>
<p>A másik megoldás, amit látni szoktam, hogy az URL-be elkezdenek igéket bevezetni,
azaz mehet egy <code class="language-plaintext highlighter-rouge">POST</code> a <code class="language-plaintext highlighter-rouge">/issues/1/start-work</code> címre, ahol az URL-ben
egy metódusnévnek megfelelő ige jelenik meg. Ezt viszonylag
egyszerű implementálni, de értelmezésemben nem felel meg a REST gondolatiságának.
Hiszen ez az URL mögött nincs semmilyen resource.
Ez nem más, mint a REST félreértelmezése, és valamilyen hibrid RPC-s
megoldás bevezetése. Ha ilyet használunk, miért döntöttünk a REST mellett?
Így keverjük az elveket, az absztrakciókat.</p>
<p>Mi lehet tehát egy jó megoldás?</p>
<p>Ahhoz, hogy megoldást találjunk, lépjünk egyet vissza, és nézzük meg, hogy hogy nézne
ki ez objektumorientált oldalon. A teljes forráskód <a href="https://github.com/vicziani/jtechlog-rest-workflow">megtalálható GitHubon</a>.</p>
<p>Először vessük el azt a megoldást, hogy lenne egy <code class="language-plaintext highlighter-rouge">Issue</code> osztály, mely
az adatokat tárolja, és egy <code class="language-plaintext highlighter-rouge">IssueService</code>, mely az <code class="language-plaintext highlighter-rouge">Issue</code> státuszát
állítgatja. Ez DDD nevezéktanában a klasszikus vérszegény modell (anemic model), amit ugyan gyakran
használunk, azonban az objektumorientált tervezéstől távol van.</p>
<p>Nézzünk inkább egy jobb megoldást!</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Getter</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">Issue</span> <span class="o">{</span>
<span class="kd">private</span> <span class="nc">Long</span> <span class="n">id</span><span class="o">;</span>
<span class="nd">@Setter</span>
<span class="kd">private</span> <span class="nc">String</span> <span class="n">title</span><span class="o">;</span>
<span class="kd">private</span> <span class="nc">IssueState</span> <span class="n">state</span><span class="o">;</span>
<span class="kd">public</span> <span class="kt">void</span> <span class="nf">startWork</span><span class="o">()</span> <span class="o">{</span>
<span class="c1">// ...</span>
<span class="o">}</span>
<span class="kd">public</span> <span class="kt">void</span> <span class="nf">completeWork</span><span class="o">()</span> <span class="o">{</span>
<span class="c1">// ...</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>Figyeljük meg a következőket! Azokhoz az attribútumokhoz, melyet nem lehet
módosítani, nincs setter metódus. A JPA tud ezek nélkül is működni
(reflecionnel hozzáfér az attribútumhoz). Az <code class="language-plaintext highlighter-rouge">id</code> nem módosítható, és a
<code class="language-plaintext highlighter-rouge">state</code> attribútum értéke sem közvetlenül. Ami viszont üzleti logikához
kötődik, ahhoz külön metódusokat hoztunk létre. Így ránézésre megmondható,
hogy az entitásunk milyen üzleti folyamatokban vesz részt.
(A setterek halmaza nem írja le, hogy milyen műveleteket lehet elvégezni egy entitáson.)
A <code class="language-plaintext highlighter-rouge">title</code> mező viszont csak egy leíró mező, ahhoz üzleti folyamat nem tartozik, ahhoz
nyugodtan létrehozhatunk egy settert (jelen esetben Lombokkal).</p>
<p>A <code class="language-plaintext highlighter-rouge">startWork()</code> és a <code class="language-plaintext highlighter-rouge">completeWork()</code> persze direktben állíthatná a <code class="language-plaintext highlighter-rouge">state</code>
mező értékét, de hamar rájövünk, hogy itt állapotátmenetekről van szó,
amit érdemes enummal megvalósítani. Így talán jobban érvényesül
a single responsibility, hogy az állapotátmeneteket kiszervezzük máshová.</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">enum</span> <span class="nc">IssueState</span> <span class="o">{</span>
<span class="no">NEW</span> <span class="o">{</span>
<span class="nd">@Override</span>
<span class="kd">public</span> <span class="nc">IssueState</span> <span class="nf">startWork</span><span class="o">()</span> <span class="o">{</span>
<span class="k">return</span> <span class="no">IN_PROGRESS</span><span class="o">;</span>
<span class="o">}</span>
<span class="o">},</span>
<span class="no">IN_PROGRESS</span> <span class="o">{</span>
<span class="nd">@Override</span>
<span class="kd">public</span> <span class="nc">IssueState</span> <span class="nf">completeWork</span><span class="o">()</span> <span class="o">{</span>
<span class="k">return</span> <span class="no">RESOLVED</span><span class="o">;</span>
<span class="o">}</span>
<span class="o">},</span>
<span class="no">RESOLVED</span><span class="o">;</span>
<span class="kd">public</span> <span class="nc">IssueState</span> <span class="nf">completeWork</span><span class="o">()</span> <span class="o">{</span>
<span class="k">throw</span> <span class="k">new</span> <span class="nf">IllegalArgumentException</span><span class="o">(</span><span class="s">"You can not complete issue with state: "</span> <span class="o">+</span> <span class="k">this</span><span class="o">);</span>
<span class="o">}</span>
<span class="kd">public</span> <span class="nc">IssueState</span> <span class="nf">startWork</span><span class="o">()</span> <span class="o">{</span>
<span class="k">throw</span> <span class="k">new</span> <span class="nf">IllegalArgumentException</span><span class="o">(</span><span class="s">"You can not start issue with state: "</span> <span class="o">+</span> <span class="k">this</span><span class="o">);</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>Itt megtörténik az ellenőrzés is, hogy munkát indítani csak <code class="language-plaintext highlighter-rouge">NEW</code> állapotú
<code class="language-plaintext highlighter-rouge">Issue</code>-n lehet, befejezni meg csak akkor, ha <code class="language-plaintext highlighter-rouge">IN_PROGRESS</code> állapotban van.
Ha változik a folyamat, elsődlegesen itt kell belenyúlni. Így már implementálhatjuk
a <code class="language-plaintext highlighter-rouge">startWork()</code> és a <code class="language-plaintext highlighter-rouge">completeWork()</code> metódusokat.</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kt">void</span> <span class="nf">startWork</span><span class="o">()</span> <span class="o">{</span>
<span class="n">state</span> <span class="o">=</span> <span class="n">state</span><span class="o">.</span><span class="na">startWork</span><span class="o">();</span>
<span class="o">}</span>
<span class="kd">public</span> <span class="kt">void</span> <span class="nf">completeWork</span><span class="o">()</span> <span class="o">{</span>
<span class="n">state</span> <span class="o">=</span> <span class="n">state</span><span class="o">.</span><span class="na">completeWork</span><span class="o">();</span>
<span class="o">}</span>
</code></pre></div></div>
<p>Nos, valahogy azt kell elérnünk, hogy ezek a metódusok kerüljenek meghívásra egy megfelelő
REST hívás esetén.</p>
<p>A trükk itt az, hogy ugyanúgy, üzleti logikában kell gondolkozni, és az üzlet által
is használt fogalmakat kell használni, ráadásul resource-ok esetén főneveket kell keresni.</p>
<p>A legtriviálisabb példa erre, hogy mikor egy banki alkalmazást fejlesztünk, átutaláskor nem
a <code class="language-plaintext highlighter-rouge">transfer</code> igét használjuk, hanem az <em>átutalás</em> (<code class="language-plaintext highlighter-rouge">transfer</code>) maga is egy entitás, és maga is egy resource,
saját azonosítóval. Szerencsétlen módon angolban az ige és a főnév is <code class="language-plaintext highlighter-rouge">transfer</code>, így nem a legszemléletesebb a példa.</p>
<p>Jelen esetben a munkafolyamatban valamilyen <em>lépést</em>
(most <em>actionnek</em> fordítom, de lehetne akár <em>step</em> is) szeretnénk elvégezni, ami önmaga is lehet
egy resource, méghozzá a hibajegy subresource-a.</p>
<p>Azaz REST oldalon a következőképp nézne ki:</p>
<pre><code class="language-plain">POST http://localhost:8080/api/issues/1/actions
Content-Type: application/json
{
"issueId": 1,
"type": "START_WORK",
}
</code></pre>
<p>Azaz egy <code class="language-plaintext highlighter-rouge">POST</code> metódussal létrehozunk egy új <code class="language-plaintext highlighter-rouge">action</code> típusú resource-ot,
méghozzá az <code class="language-plaintext highlighter-rouge">1</code>-es azonosítójú hibajegy alatt. Látható, hogy a hibajegy azonosítója
az URL-ben és a törzsben is megjelenik (utóbbi helyen azért, hiszen minden adatot küldök <code class="language-plaintext highlighter-rouge">PUT</code>
esetén),
Amit visszakapunk:</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
</span><span class="nl">"id"</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w">
</span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"START_WORK"</span><span class="p">,</span><span class="w">
</span><span class="nl">"createdAt"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2023-08-31T17:04:17.9083384"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
<p>Látható, hogy ez is kap egy azonosítót, és később a hibajegyhez
kapcsolódó összes lépést le lehet kérni a <code class="language-plaintext highlighter-rouge">api/issues/1/actions</code> címen
<code class="language-plaintext highlighter-rouge">GET</code> metódussal, vagy azonosító szerint csak egy lépést
a <code class="language-plaintext highlighter-rouge">api/issues/1/actions/1</code> címen.</p>
<p>Nézzük, megoldottuk-e a felmerült problémákat:</p>
<ul>
<li>Minden lépéshez, üzleti folyamathoz egy külön resource tartozik, szépen látszik,
hogy milyen üzleti folyamat kerül elindításra.</li>
<li>Nem direktben állítjuk a státusz mezőt.</li>
<li>A resource csak olyan mezőket tartalmaz, melyek az üzleti logika indításához kellenek.</li>
<li>Bármikor felvehetünk az erőforráshoz további mezőket.</li>
</ul>
<p>Itt persze implementálni kell, hogy amikor elindul az üzleti logika,
a különböző entitások megfelelően változzanak.
Ezt egy service-ben implementáljuk, ugyanis itt két entitás is változik,
azaz egy entitáson belül nem tudjuk implementálni. (Tervezési döntés,
hogy a hibajegyeket és a lépéseket külön aggregate-be teszem, ugyanis a hibajegy betöltésekor egyáltalán nincs szükségem
a lépésekre. Egyelőre itt ebből elég annyit érteni, hogy nincs referencia a két objektum között. A lépés a hibajegy
elsődleges kulcsára hivatkozik.)</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Transactional</span>
<span class="kd">public</span> <span class="nc">ActionResource</span> <span class="nf">createAction</span><span class="o">(</span><span class="kt">long</span> <span class="n">issueId</span><span class="o">,</span> <span class="nc">ActionResource</span> <span class="n">actionResource</span><span class="o">)</span> <span class="o">{</span>
<span class="kt">var</span> <span class="n">issue</span> <span class="o">=</span> <span class="n">issueRepository</span><span class="o">.</span><span class="na">findById</span><span class="o">(</span><span class="n">issueId</span><span class="o">)</span>
<span class="o">.</span><span class="na">orElseThrow</span><span class="o">(()</span> <span class="o">-></span> <span class="k">new</span> <span class="nc">IllegalArgumentException</span><span class="o">(</span><span class="s">"Can not find issue by id %d"</span><span class="o">.</span><span class="na">formatted</span><span class="o">(</span><span class="n">issueId</span><span class="o">)));</span> <span class="c1">// 1</span>
<span class="kt">var</span> <span class="n">action</span> <span class="o">=</span> <span class="nc">Action</span><span class="o">.</span><span class="na">createFromResource</span><span class="o">(</span><span class="n">actionResource</span><span class="o">,</span> <span class="n">issueId</span><span class="o">);</span> <span class="c1">// 2</span>
<span class="n">actionRepository</span><span class="o">.</span><span class="na">save</span><span class="o">(</span><span class="n">action</span><span class="o">);</span> <span class="c1">// 3</span>
<span class="k">switch</span> <span class="o">(</span><span class="n">action</span><span class="o">.</span><span class="na">getType</span><span class="o">())</span> <span class="o">{</span> <span class="c1">// 4</span>
<span class="k">case</span> <span class="no">START_WORK</span> <span class="o">-></span> <span class="n">issue</span><span class="o">.</span><span class="na">startWork</span><span class="o">();</span>
<span class="k">case</span> <span class="no">COMPLETE_WORK</span> <span class="o">-></span> <span class="n">issue</span><span class="o">.</span><span class="na">completeWork</span><span class="o">();</span>
<span class="k">default</span> <span class="o">-></span> <span class="k">throw</span> <span class="k">new</span> <span class="nc">IllegalArgumentException</span><span class="o">(</span><span class="s">"Invalid action"</span><span class="o">);</span>
<span class="o">}</span>
<span class="k">return</span> <span class="n">issueMapper</span><span class="o">.</span><span class="na">toResource</span><span class="o">(</span><span class="n">action</span><span class="o">);</span> <span class="c1">// 5</span>
<span class="o">}</span>
</code></pre></div></div>
<p>Szóval mi is történik itt?</p>
<ol>
<li>Id alapján betöltésre kerül a hibajegy.</li>
<li>A lépés resource-a alapján létrehozunk egy lépés entitást.</li>
<li>A lépés entitást elmentjük.</li>
<li>Meghívjuk a hibajegy entitás megfelelő metódusát.</li>
<li>Visszatérünk a hibajegy entitáshoz tartozó resource-szal.</li>
</ol>
<p>Röviden tehát létrehozzuk és lementjük a lépés entitást, valamint billentjük a hibajegy státuszát a megfelelő metódusán keresztül.</p>
<p>Itt persze dönthetünk úgy is, hogy a különböző lépéseket (munka elkezdése, munka befejezése) különböző resource-oknak vesszük, így külön url-eken kezeljük.
Dönthetünk úgy, hogy legyen egy interfész melyet minden lépés implementál, indokolt esetben egy absztrakt ősosztály, melynek a lépések
a leszármazottjai.</p>
<p>De mi van akkor, ha csak úgy akarunk egy adatot módosítani, hogy nincs mögötte üzleti logika? Azaz például szeretnénk
módosítani a hibajegy címét? Első megoldásként szóba jöhet, hogy <code class="language-plaintext highlighter-rouge">PUT</code> metódussal a teljes resource-t újra küldjük. Itt azonban
mi biztosítja, hogy minden mező megfelelően került-e visszaküldésre?</p>
<p>A másik megoldás a <code class="language-plaintext highlighter-rouge">PATCH</code> használata. A <code class="language-plaintext highlighter-rouge">PATCH</code> teszi lehetővé, hogy egy resource csak egy részét módosítsuk. (Ugye miért is létezne ez,
ha a <code class="language-plaintext highlighter-rouge">PUT</code> metódussal is meg lehetne ezt tenni?)</p>
<p>Itt specifikusan Javaban olyan probléma adódik, hogy nem tudjuk megkülönböztetni, hogy egy mezőt nem akarunk módosítani, vagy az értékét törölni akarjuk.
Egy JSON-t deserialize-álva nem lehet megkülönböztetni, hogy kihagytuk-e az attribútumot, vagy <code class="language-plaintext highlighter-rouge">null</code> értékkel küldtük, a mező
értéke mindenképp <code class="language-plaintext highlighter-rouge">null</code> lesz.</p>
<p>Azaz nézzük meg a következő két JSON dokumentumot, először egy üres dokumentum</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{}</span><span class="w">
</span></code></pre></div></div>
<p>Majd egy dokumentum, melynek van egy <code class="language-plaintext highlighter-rouge">title</code> mezője:</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
</span><span class="nl">"title"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
<p>Mindkét esetben az <code class="language-plaintext highlighter-rouge">IssueResource</code> <code class="language-plaintext highlighter-rouge">title</code> mezőjének értéke <code class="language-plaintext highlighter-rouge">null</code> lesz. Azaz nem tudjuk megkülönböztetni,
hogy nem akarjuk módosítani, vagy <code class="language-plaintext highlighter-rouge">null</code> értékre akarjuk módosítani.</p>
<p>Rendben, de akkor hogyan is nézzen ki a kérés törzse <code class="language-plaintext highlighter-rouge">PATCH</code> esetén? Erre van az RFC 6902 (JavaScript Object Notation (JSON) Patch)
szabvány, ami egy jó választás lehet. Ha például a címet szeretnénk módosítani, akkor a következő kérést kell beküldenünk.</p>
<pre><code class="language-plain">PATCH http://localhost:8080/api/issues/1
Content-Type: application/json-patch+json
[{
"op": "replace",
"path": "/title",
"value": "Write a post about PATCH"
}]
</code></pre>
<p>(Figyeljük meg, saját content type-ja is van!)</p>
<p>Tehát a <code class="language-plaintext highlighter-rouge">replace</code> műveletet (op - operator) alkalmazzuk, a <code class="language-plaintext highlighter-rouge">/title</code> útvonalra. Ez utóbbi egy másik szabványnak - a RFC6901 (JSON-Pointer value) -
megfelelő formátumú. És persze a <code class="language-plaintext highlighter-rouge">value</code> mezőben a módosított érték.</p>
<p>Erre van több implementáció is, pl. a <a href="https://github.com/java-json-tools/json-patch">json-patch</a>, vagy a
szabványos <a href="https://jakarta.ee/specifications/jsonp/">Jakarta JSON Processing (JSONP)</a>. A szabványossága miatt, valamint Ezeknek a libeknek az a jellemzője,
hogy JSON dokumentumon dolgoznak. Természetesen a cél a resource, nem az entitás, hiszen ez a REST / controller réteg része. Emiatt ennek implementálása elég bonyolult.</p>
<ol>
<li>Le kell kérni az <code class="language-plaintext highlighter-rouge">1</code>-es azonosítóval rendelkező hibajegyet, ez egy entitás.</li>
<li>Át kell alakítani ezt resource-á, és ezt visszaadni a controllernek.</li>
<li>Ezt visszaalakítani JSON-né (serialization).</li>
<li>Erre ráfuttatni a JSON Patch dokumentumot.</li>
<li>Visszaalakítani resource-á (deserialization), majd beküldeni a service rétegnek.</li>
<li>A resource-ot ráfuttatni az entitásra.</li>
</ol>
<p>Azért, mert a JSONP egy szabvány, ezt választottam, és az ezt implementáló Glassfish referencia implementációt.
Ráadásul integrálni lehet a Spring által használt Jacksonnal.</p>
<div class="language-groovy highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">implementation</span> <span class="s1">'com.fasterxml.jackson.datatype:jackson-datatype-jsr353'</span>
<span class="n">implementation</span> <span class="s1">'org.glassfish:javax.json:1.1'</span>
</code></pre></div></div>
<p>Az integráció:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Bean</span>
<span class="kd">public</span> <span class="nc">ObjectMapper</span> <span class="nf">objectMapper</span><span class="o">()</span> <span class="o">{</span>
<span class="k">return</span> <span class="nc">JsonMapper</span><span class="o">.</span><span class="na">builder</span><span class="o">()</span>
<span class="o">.</span><span class="na">addModule</span><span class="o">(</span><span class="k">new</span> <span class="nc">JSR353Module</span><span class="o">())</span>
<span class="o">.</span><span class="na">build</span><span class="o">();</span>
<span class="o">}</span>
</code></pre></div></div>
<p>A patch implementációja a controllerben:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@PatchMapping</span><span class="o">(</span><span class="n">value</span> <span class="o">=</span> <span class="s">"/{id}"</span><span class="o">,</span> <span class="n">consumes</span> <span class="o">=</span> <span class="s">"application/json-patch+json"</span><span class="o">)</span>
<span class="kd">public</span> <span class="nc">IssueResource</span> <span class="nf">patchIssue</span><span class="o">(</span><span class="nd">@PathVariable</span> <span class="kt">long</span> <span class="n">id</span><span class="o">,</span> <span class="nd">@RequestBody</span> <span class="nc">JsonPatch</span> <span class="n">patch</span><span class="o">)</span> <span class="o">{</span>
<span class="kt">var</span> <span class="n">issue</span> <span class="o">=</span> <span class="n">issueService</span><span class="o">.</span><span class="na">findIssueById</span><span class="o">(</span><span class="n">id</span><span class="o">);</span>
<span class="kt">var</span> <span class="n">patched</span> <span class="o">=</span> <span class="n">patch</span><span class="o">(</span><span class="n">issue</span><span class="o">,</span> <span class="n">patch</span><span class="o">);</span>
<span class="kt">var</span> <span class="n">validationResult</span> <span class="o">=</span> <span class="n">validator</span><span class="o">.</span><span class="na">validate</span><span class="o">(</span><span class="n">patched</span><span class="o">,</span> <span class="nc">IssueOperations</span><span class="o">.</span><span class="na">PatchIssue</span><span class="o">.</span><span class="na">class</span><span class="o">);</span>
<span class="k">if</span> <span class="o">(!</span><span class="n">validationResult</span><span class="o">.</span><span class="na">isEmpty</span><span class="o">())</span> <span class="o">{</span>
<span class="k">throw</span> <span class="k">new</span> <span class="nf">ConstraintViolationException</span><span class="o">(</span><span class="n">validationResult</span><span class="o">);</span>
<span class="o">}</span>
<span class="k">return</span> <span class="n">issueService</span><span class="o">.</span><span class="na">update</span><span class="o">(</span><span class="n">patched</span><span class="o">);</span>
<span class="o">}</span>
</code></pre></div></div>
<p>Az is látható, hogy az eredmény objektumra még futtatom a Jakarta Validation
ellenőrzést is (<code class="language-plaintext highlighter-rouge">IssueOperations.PatchIssue</code> validation group használatával).</p>
<p>Ebből a <code class="language-plaintext highlighter-rouge">patch()</code> saját metódus, hogy ne kelljen minden resource-ra külön megírni:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">private</span> <span class="o"><</span><span class="no">T</span><span class="o">></span> <span class="no">T</span> <span class="nf">patch</span><span class="o">(</span><span class="no">T</span> <span class="n">target</span><span class="o">,</span> <span class="nc">JsonPatch</span> <span class="n">patch</span><span class="o">)</span> <span class="o">{</span>
<span class="kt">var</span> <span class="n">json</span> <span class="o">=</span> <span class="n">objectMapper</span><span class="o">.</span><span class="na">convertValue</span><span class="o">(</span><span class="n">target</span><span class="o">,</span> <span class="nc">JsonObject</span><span class="o">.</span><span class="na">class</span><span class="o">);</span>
<span class="kt">var</span> <span class="n">patchedJsonObject</span> <span class="o">=</span> <span class="n">patch</span><span class="o">.</span><span class="na">apply</span><span class="o">(</span><span class="n">json</span><span class="o">);</span>
<span class="k">return</span> <span class="o">(</span><span class="no">T</span><span class="o">)</span> <span class="n">objectMapper</span><span class="o">.</span><span class="na">convertValue</span><span class="o">(</span><span class="n">patchedJsonObject</span><span class="o">,</span> <span class="n">target</span><span class="o">.</span><span class="na">getClass</span><span class="o">());</span>
<span class="o">}</span>
</code></pre></div></div>
<p>Ez végzi a JSON serializationt, JSON patch dokumentum ráfuttatását, majd deserializationt.</p>
<p>Így van egy ugyanolyan resource-om, mintha a teljes resource-t küldtem volna vissza,
azonban betöltésre került adatbázisból, és csak egy mező lett benne módosítva.</p>
<p>Most arról kéne gondoskodni, hogy ebből csak a módosítható attribútum kerüljön bemásolásra
az entitásba. (Ha például az <code class="language-plaintext highlighter-rouge">id</code> vagy <code class="language-plaintext highlighter-rouge">state</code> attribútumra jönne JSON Patch, az galibát okozna.)</p>
<p>Itt ahhoz a megint egyszerű ötlethez nyúlok, hogy ne agyatlanul hozzunk létre setter metódusokat az entitásban,
csak azokra az attribútumokra, melyeket tényleg lehet módosítani. Az entitás tehát még egyszer:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Getter</span>
<span class="nd">@Entity</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">Issue</span> <span class="o">{</span>
<span class="nd">@Id</span>
<span class="nd">@GeneratedValue</span><span class="o">(</span><span class="n">strategy</span> <span class="o">=</span> <span class="nc">GenerationType</span><span class="o">.</span><span class="na">IDENTITY</span><span class="o">)</span>
<span class="kd">private</span> <span class="nc">Long</span> <span class="n">id</span><span class="o">;</span>
<span class="nd">@Setter</span>
<span class="kd">private</span> <span class="nc">String</span> <span class="n">title</span><span class="o">;</span>
<span class="kd">private</span> <span class="nc">IssueState</span> <span class="n">state</span><span class="o">;</span>
<span class="o">}</span>
</code></pre></div></div>
<p>Igen, csak a <code class="language-plaintext highlighter-rouge">title</code> attribútumon van setter! És ismétlem, a JPA-t ez nem zavarja.
És egy újabb trükk, hogy mivel másoljuk át az adatokat a resource-ból az entitásba?</p>
<p>Erre is használhatjuk a MapStruct könyvtárat, ugyanis az nem csak konvertálni tud,
hanem egy objektumot update-elni is egy másik objektum alapján. Definiáljuk
így a metódust a mapperben:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">void</span> <span class="nf">update</span><span class="o">(</span><span class="nd">@MappingTarget</span> <span class="nc">Issue</span> <span class="n">issue</span><span class="o">,</span> <span class="nc">IssueResource</span> <span class="n">resource</span><span class="o">);</span>
</code></pre></div></div>
<p>Ez az <code class="language-plaintext highlighter-rouge">Issue</code> entitásra fogja rágörgetni az <code class="language-plaintext highlighter-rouge">IssueResource</code> adatait. De melyieket?
Szerencsére a MapStruct csak azokat másolja át, melyekhez van setter. Nézzük meg
a generált kódot!</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Override</span>
<span class="kd">public</span> <span class="kt">void</span> <span class="nf">update</span><span class="o">(</span><span class="nc">Issue</span> <span class="n">issue</span><span class="o">,</span> <span class="nc">IssueResource</span> <span class="n">resource</span><span class="o">)</span> <span class="o">{</span>
<span class="k">if</span> <span class="o">(</span> <span class="n">resource</span> <span class="o">==</span> <span class="kc">null</span> <span class="o">)</span> <span class="o">{</span>
<span class="k">return</span><span class="o">;</span>
<span class="o">}</span>
<span class="n">issue</span><span class="o">.</span><span class="na">setTitle</span><span class="o">(</span> <span class="n">resource</span><span class="o">.</span><span class="na">getTitle</span><span class="o">()</span> <span class="o">);</span>
<span class="o">}</span>
</code></pre></div></div>
<p>Igen, látszik, hogy csak a <code class="language-plaintext highlighter-rouge">title</code> mező tartalmát másolja át, az <code class="language-plaintext highlighter-rouge">id</code> és <code class="language-plaintext highlighter-rouge">state</code> mezőket
figyelmen kívül hagyja.</p>
<p>Amennyiben több setterem lenne, a MapStruct is rábeszélhető, hogy csak a megfelelő
attribútumokat másolja a következő annotációkkal:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@BeanMapping</span><span class="o">(</span><span class="n">ignoreByDefault</span> <span class="o">=</span> <span class="kc">true</span><span class="o">)</span>
<span class="nd">@Mapping</span><span class="o">(</span><span class="n">source</span> <span class="o">=</span> <span class="s">"title"</span><span class="o">,</span> <span class="n">target</span> <span class="o">=</span> <span class="s">"title"</span><span class="o">)</span>
<span class="kt">void</span> <span class="nf">update</span><span class="o">(</span><span class="nd">@MappingTarget</span> <span class="nc">Issue</span> <span class="n">issue</span><span class="o">,</span> <span class="nc">IssueResource</span> <span class="n">resource</span><span class="o">);</span>
</code></pre></div></div>
<p>Azaz ignorálunk minden property-t, és csak a <code class="language-plaintext highlighter-rouge">title</code> property-t másoljuk.</p>
<p>Így a service metódus:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Transactional</span>
<span class="kd">public</span> <span class="nc">IssueResource</span> <span class="nf">update</span><span class="o">(</span><span class="kt">long</span> <span class="n">id</span><span class="o">,</span> <span class="nc">IssueResource</span> <span class="n">patchedIssue</span><span class="o">)</span> <span class="o">{</span>
<span class="kt">var</span> <span class="n">issue</span> <span class="o">=</span> <span class="n">issueRepository</span><span class="o">.</span><span class="na">findById</span><span class="o">(</span><span class="n">id</span><span class="o">)</span>
<span class="o">.</span><span class="na">orElseThrow</span><span class="o">(()</span> <span class="o">-></span> <span class="k">new</span> <span class="nc">IllegalArgumentException</span><span class="o">(</span><span class="s">"Can not find issue by id %d"</span><span class="o">.</span><span class="na">formatted</span><span class="o">(</span><span class="n">id</span><span class="o">)));</span>
<span class="n">issueMapper</span><span class="o">.</span><span class="na">update</span><span class="o">(</span><span class="n">issue</span><span class="o">,</span> <span class="n">patchedIssue</span><span class="o">);</span>
<span class="k">return</span> <span class="n">issueMapper</span><span class="o">.</span><span class="na">toResource</span><span class="o">(</span><span class="n">issue</span><span class="o">);</span>
<span class="o">}</span>
</code></pre></div></div>
<p>Azaz betölti az entitást, meghívja a generált <code class="language-plaintext highlighter-rouge">update()</code> metódust, majd újra resource-á alakítja, és azt adja vissza.</p>
<p>Hogy lehetne pl. hibát dobni a felhasználónak, ha olyan mezőt akar módosítani,
melyet nem szabad? Sajnálatos módon a <code class="language-plaintext highlighter-rouge">JsonPatch</code> osztálynak nincsenek lekérdező
metódusai, azaz azt nem tudjuk validálni. Ez ennek a könyvtárnak a hátránya.</p>
<p>Emiatt pl. a Swagger sem képes hozzá dokumentációt gyártani. Ezt úgy tudjuk orvosolni, hogy csinálunk egy
saját osztályt, és a Swaggernek megmondjuk, hogy ezt az osztályt használja a dokumentáció generáláshoz.</p>
<p>Az osztály:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">class</span> <span class="nc">JsonPatchSchema</span> <span class="o">{</span>
<span class="nd">@NotBlank</span>
<span class="kd">public</span> <span class="nc">Op</span> <span class="n">op</span><span class="o">;</span>
<span class="kd">public</span> <span class="kd">enum</span> <span class="nc">Op</span> <span class="o">{</span>
<span class="n">replace</span><span class="o">,</span> <span class="n">add</span><span class="o">,</span> <span class="n">remove</span><span class="o">,</span> <span class="n">copy</span><span class="o">,</span> <span class="n">move</span><span class="o">,</span> <span class="n">test</span>
<span class="o">}</span>
<span class="nd">@NotBlank</span>
<span class="nd">@Schema</span><span class="o">(</span><span class="n">example</span> <span class="o">=</span> <span class="s">"/name"</span><span class="o">)</span>
<span class="kd">public</span> <span class="nc">String</span> <span class="n">path</span><span class="o">;</span>
<span class="nd">@NotBlank</span>
<span class="kd">public</span> <span class="nc">String</span> <span class="n">value</span><span class="o">;</span>
<span class="o">}</span>
</code></pre></div></div>
<p>És a controller metóduson lévő annotáció:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@io</span><span class="o">.</span><span class="na">swagger</span><span class="o">.</span><span class="na">v3</span><span class="o">.</span><span class="na">oas</span><span class="o">.</span><span class="na">annotations</span><span class="o">.</span><span class="na">parameters</span><span class="o">.</span><span class="na">RequestBody</span><span class="o">(</span><span class="n">content</span> <span class="o">=</span> <span class="nd">@Content</span><span class="o">(</span><span class="n">array</span> <span class="o">=</span> <span class="nd">@ArraySchema</span><span class="o">(</span><span class="n">schema</span> <span class="o">=</span> <span class="nd">@Schema</span><span class="o">(</span><span class="n">implementation</span> <span class="o">=</span> <span class="nc">JsonPatchSchema</span><span class="o">.</span><span class="na">class</span><span class="o">))))</span>
</code></pre></div></div>
<p>Sajnálatos módon ez a JSON Patch szabvány nem igazán terjedt el. Hátránya, hogy
el kell végezni a serialization és deserialization műveleteket, nem tudja közvetlenül
a resource objektum gráfon futtatni a műveleteket.</p>
<p>Szóval nem igazán kaptam megnyugtató válaszokat a kérdéseimre.</p>
<p>Nézzünk rá még egyszer az objektumunkra!</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Getter</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">Issue</span> <span class="o">{</span>
<span class="kd">private</span> <span class="nc">Long</span> <span class="n">id</span><span class="o">;</span>
<span class="nd">@Setter</span>
<span class="kd">private</span> <span class="nc">String</span> <span class="n">title</span><span class="o">;</span>
<span class="kd">private</span> <span class="nc">IssueState</span> <span class="n">state</span><span class="o">;</span>
<span class="kd">public</span> <span class="kt">void</span> <span class="nf">startWork</span><span class="o">()</span> <span class="o">{</span>
<span class="c1">// ...</span>
<span class="o">}</span>
<span class="kd">public</span> <span class="kt">void</span> <span class="nf">completeWork</span><span class="o">()</span> <span class="o">{</span>
<span class="c1">// ...</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>Nem lenne egyszerűbb távolról a <code class="language-plaintext highlighter-rouge">setTitle()</code>, <code class="language-plaintext highlighter-rouge">startWork()</code> és <code class="language-plaintext highlighter-rouge">completeWork()</code> metódusokat meghívni,
elfelejtve a REST minden nehézségét?</p>
<p>Amennyiben arról szeretnél olvasni, hogy lehet a különböző műveleteket a REST iránymutatásának megfelelően
URL-ekre és HTTP metódusokra mappelni, olvasd el az InfoQ-n megjelent <a href="https://www.infoq.com/articles/webber-rest-workflow/">How to GET a Cup of Coffee</a>
cikket, valamint az általam javasolt megoldáshoz nagyon hasonló megoldásokat javasló, Thoughtworksnél megjelent
<a href="https://www.thoughtworks.com/insights/blog/rest-api-design-resource-modeling">REST API Design - Resource Modeling</a> cikket.</p>
Hőmérséklet monitorozása2023-08-18T08:00:00+00:00http://www.jtechlog.hu/2023/08/18/homerseklet-monitorozas<p>Nagyon régóta érdekel az IoT (Internet of Things, azaz Internetre köthető kütyük) világa, ugyanis ezek azok az eszközök,
melyek összekötik a virtuális világót a való világgal. Már régóta <a href="2013/12/30/raspberry-pi-alapok.html">rendelkezem egy Raspberry PI számítógéppel</a>,
melyet azóta is lelkesen használok egy alacsony fogyasztású home serverként. Telepítve van rá a Prometheus és Grafana, mely
a DevOps világban egy kvázi standard monitorozó eszköz. Mindkettő ingyenesen használható, nyílt forráskódú eszköz.
A Prometheus különösen alkalmas idősorok hatékony tárolására, gyors lekérdezésére, aggregált műveletek végrehajtására. Az idősorok
olyan megfigyelések, melyeket egymást követő időpontokban (időszakokban) regisztrálták, és ez az időbeliség az adatok fontos tulajdonsága.
Ilyen például egy szerverrel kapcsolatban a bizonyos időpontokban lekérdezett CPU és memóriahasználat, vagy a háttértárakon lévő szabad
hely mérete. A Grafana használatával ezeket tudjuk vizualizálni, gyönyörű dashboardokat létrehozni és akár riasztásokat
beállítani.</p>
<p>De melyik is lehet az az IoT eszköz, melyet a legegyszerűbben, lehetőleg barkácsolás nélkül és olcsón lehetne bekötni ebbe a rendszerbe?
Már régóta szemezem a Xiaomi Mi Temperature and Humidity Monitor 2 hőmérséklet-, és páratartalom mérővel, amihez most 2000 Ft-ért
lehet hozzájutni az <a href="https://mstore.hu/xiaomi-mi-temprerature-humidity-monitor-2-1473">mStore akciója keretében</a>.</p>
<p><img src="/artifacts/posts/2023-08-18-homerseklet-monitorozas/xiaomi-temperature-and-humidity-monitor.jpg" alt="Xiaomi Mi Temperature and Humidity Monitor 2" /></p>
<p>Ez pontosan a LYWSD03MMC modell, mely egy precíz Sensirion szenzorral, 1,5”-os LCD kijelzővel rendelkezik, és Bluetooth 4.2 BLE
vezeték nélküli kapcsolaton keresztül kommunikál. Egy CR2032 elemmel működik, ezt külön szerezzük be, mert nem része
a csomagnak. Ezzel akár fél-egy évig képes üzemelni. Természetesen Bluetooth-on tud kapcsolódni mobiltelefonhoz, illetve
Bluetooth Gateway-hez, azonban én direktbe, egy Linuxos számítógéppel, jelen esetben egy Raspberry PI-vel szeretnék
hozzá kapcsolódni, és kinyerni belőle az adatokat. (Nincs szükség egyedi firmware telepítésére.)</p>
<p>Ezt utána csak be kell kötni a Prometheusba, mely időközönként lekérdezi és eltárolja az adatokat, majd egy Grafana dashboardot
létrehozni, mely megjeleníti azokat.</p>
<p><a href="/artifacts/posts/2023-08-18-homerseklet-monitorozas/dashboard.png" data-lightbox="post-images"><img src="/artifacts/posts/2023-08-18-homerseklet-monitorozas/dashboard_700px.png" alt="Grafana dashboard" /></a></p>
<p>Közben természetesen szerettem volna megismerni a kapcsolódó technológiákat is.</p>
<!-- more -->
<p>A szenzor a BLE (Bluetooth Low Energy) vezeték nélküli kommunikációs technológiát használja, amelyet alacsony energiafogyasztású eszközök közötti adatátvitelre terveztek.
Persze a hatótávolság és adatátvitel korlátozottabb, mint a hagyományos Bluetooth esetén. Az eszközök az ún. Generic Attribute Profile (GATT) specifikáció
alapján kommunikálnak. Ez a következő fogalmakat használja:</p>
<ul>
<li>Client: ami az eszközről lekérdezi az adatokat, kezdeményezi a kommunikációt, kéréseket küld az eszköz számára, és fogadja a válaszokat. Ez lesz a Raspberry PI.</li>
<li>Server: maga az eszköz, jelen esetben a szenzor.</li>
<li>Service: az eszközön belül egy kisebb részegység, mely az egybe tartozó funkciókért felelős. Ezt kell megszólítani, ez adja vissza az adatokat.</li>
<li>Characteristic: átküldött adat, ilyen a hőmérséklet, a páratartalom, és az elem állapota.</li>
</ul>
<p>Ezek az eszközök ezen kívül apró csomagokat szórnak szét, mellyel jelzik, hogy lehet hozzájuk csatlakozni.</p>
<p>Érdekes módon Bluetooth eszközöket a Chrome-ban is fel lehet deríteni, ehhez ne felejtsük el bekapcsolni a számítógépünkön a Bluetooth kapcsolatot.
Utána a <code class="language-plaintext highlighter-rouge">chrome://bluetooth-internals/#devices</code> címre kell ellátogatni, és már láthatjuk is a Bluetooth eszközöket. Nálam bőven tízes nagyságrendű eszköz van,
ezek közül többhöz csatlakozni is lehet, ilyen pl. a hőmérséklet szenzor, okosmérleg, fogkefék, stb. A keresett eszköznek megjelenik a modell azonosítója (LYWSD03MMC),
valamint a MAC-címe (ez egy egyedi cím, formátuma: <code class="language-plaintext highlighter-rouge">1A-E3-2C-9F-68-8D</code>). Ezen a felületen lehet is csatlakozni, és lekérdezni a service-eket,
valamint az alatt a characteristics-et. Minden service egyedi UUID-val is rendelkezik (pl. <code class="language-plaintext highlighter-rouge">0000180f-0000-1000-8000-00805f9b34fb</code>).</p>
<p>De persze sokkal izgalmasabb, hogy hogy lehet ezt Raspberry PI-n elvégezni. Vigyázat, a Raspberry PI 3 lapkán egy Broadcom BCM43438 LAN és Bluetooth Low Energy (BLE) chip van,
mely nem engedélyezi egyszerre a WiFi-t és a Bluetooth-t használni. Így ki kell kapcsolni a WiFi-t, vagy venni egy külön USB-s WiFi vagy Bluetooth adaptert.</p>
<p>A Bluetooth interfész a <code class="language-plaintext highlighter-rouge">hci0</code>, mely a következő paranccsal ellenőrizhető:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>hciconfig
</code></pre></div></div>
<p>Ennek az eredménye valami hasonló:</p>
<pre><code class="language-plain">hci0: Type: Primary Bus: UART
BD Address: B8:27:EB:FE:FE:3B ACL MTU: 1021:8 SCO MTU: 64:1
UP RUNNING
RX bytes:31612 acl:387 sco:0 events:1812 errors:0
TX bytes:19387 acl:60 sco:0 commands:1147 errors:0
</code></pre>
<p>A Bluetooth eszközök felderítése a következő paranccsal lehetséges:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>hcitool lescan
</code></pre></div></div>
<p>Ennek eredménye valami hasonló:</p>
<pre><code class="language-plain">61-0D-49-18-9F-C8 (unknown)
49-49-73-D6-F3-47 (unknown)
24-48-8C-94-96-4C (unknown)
24-48-8C-94-96-4C LYWSD03MMC
C8-6A-2F-5E-10-22 (unknown)
98-87-5C-C1-DC-5E MI_SCALE
</code></pre>
<p>A <code class="language-plaintext highlighter-rouge">LYWSD03MMC</code> megnevezéssel találjuk meg a szenzorunk, és annak a MAC-címét.</p>
<p>Ezek után hozzá kell kapcsolódni az eszközöz, és
beállítani, hogy a mért értékekről küldjön értesítéseket. (Ezt mobiltelefonról is
meg lehetne tenni, de így legalább látunk egy példát, hogy kell alacsony szintű parancsokat
kiadni. Ha mégis telefonról akarjuk aktiválni, akkor
csatlakozzunk az eszközhöz a Mi Home alkalmazásból, majd várjuk meg, míg letölti az adatokat.)
Ha mégis parancssorból szeretnénk beállítani, akkor a <code class="language-plaintext highlighter-rouge">gatttool</code> eszközt kell használni.</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gatttool <span class="nt">-I</span> <span class="nt">-b</span> 24-48-8C-94-96-4C
<span class="o">[</span>24-48-8C-94-96-4C][LE]> connect
Attempting to connect to 24-48-8C-94-96-4C
Connection successful
Notification handle <span class="o">=</span> 0x0036 value: e4 0a 43 c8 0b
<span class="o">[</span>24-48-8C-94-96-4C][LE]>
</code></pre></div></div>
<p>Itt le lehet kérni a service-eket:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">[</span>24-48-8C-94-96-4C][LE]> primary
attr handle: 0x0001, end grp handle: 0x0007 uuid: 00001800-0000-1000-8000-00805f9b34fb
attr handle: 0x0008, end grp handle: 0x000b uuid: 00001801-0000-1000-8000-00805f9b34fb
attr handle: 0x000c, end grp handle: 0x0018 uuid: 0000180a-0000-1000-8000-00805f9b34fb
attr handle: 0x0019, end grp handle: 0x001c uuid: 0000180f-0000-1000-8000-00805f9b34fb
attr handle: 0x001d, end grp handle: 0x0020 uuid: 00010203-0405-0607-0809-0a0b0c0d1912
attr handle: 0x0021, end grp handle: 0x004e uuid: ebe0ccb0-7a0a-4b0c-8a1a-6ff2997da3a6
attr handle: 0x004f, end grp handle: 0x005d uuid: 8edffff0-3d1b-9c37-4623-ad7265f14076
attr handle: 0x005e, end grp handle: 0x0071 uuid: 0000fe95-0000-1000-8000-00805f9b34fb
attr handle: 0x0072, end grp handle: 0x007a uuid: 00000100-0065-6c62-2e74-6f696d2e696d
</code></pre></div></div>
<p>És a service-ben lévő characteristics értékeket a service <code class="language-plaintext highlighter-rouge">attr handle</code> és <code class="language-plaintext highlighter-rouge">end grp handle</code> kiválasztásával.</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">[</span>24-48-8C-94-96-4C][LE]> characteristics 0x0021 0x004e
handle: 0x0022, char properties: 0x0a, char value handle: 0x0023, uuid: ebe0ccb7-7a0a-4b0c-8a1a-6ff2997da3a6
handle: 0x0025, char properties: 0x02, char value handle: 0x0026, uuid: ebe0ccb9-7a0a-4b0c-8a1a-6ff2997da3a6
handle: 0x0028, char properties: 0x0a, char value handle: 0x0029, uuid: ebe0ccba-7a0a-4b0c-8a1a-6ff2997da3a6
handle: 0x002b, char properties: 0x02, char value handle: 0x002c, uuid: ebe0ccbb-7a0a-4b0c-8a1a-6ff2997da3a6
handle: 0x002e, char properties: 0x10, char value handle: 0x002f, uuid: ebe0ccbc-7a0a-4b0c-8a1a-6ff2997da3a6
handle: 0x0032, char properties: 0x0a, char value handle: 0x0033, uuid: ebe0ccbe-7a0a-4b0c-8a1a-6ff2997da3a6
handle: 0x0035, char properties: 0x12, char value handle: 0x0036, uuid: ebe0ccc1-7a0a-4b0c-8a1a-6ff2997da3a6
handle: 0x0039, char properties: 0x02, char value handle: 0x003a, uuid: ebe0ccc4-7a0a-4b0c-8a1a-6ff2997da3a6
handle: 0x003c, char properties: 0x08, char value handle: 0x003d, uuid: ebe0ccc8-7a0a-4b0c-8a1a-6ff2997da3a6
handle: 0x003f, char properties: 0x08, char value handle: 0x0040, uuid: ebe0ccd1-7a0a-4b0c-8a1a-6ff2997da3a6
handle: 0x0042, char properties: 0x0a, char value handle: 0x0043, uuid: ebe0ccd7-7a0a-4b0c-8a1a-6ff2997da3a6
handle: 0x0045, char properties: 0x08, char value handle: 0x0046, uuid: ebe0ccd8-7a0a-4b0c-8a1a-6ff2997da3a6
handle: 0x0048, char properties: 0x18, char value handle: 0x0049, uuid: ebe0ccd9-7a0a-4b0c-8a1a-6ff2997da3a6
handle: 0x004c, char properties: 0x0a, char value handle: 0x004d, uuid: ebe0cff1-7a0a-4b0c-8a1a-6ff2997da3a6
</code></pre></div></div>
<p>Azonban ismert, hogy a <code class="language-plaintext highlighter-rouge">0x0036</code> <code class="language-plaintext highlighter-rouge">char value handle</code> címet kell beállítani <code class="language-plaintext highlighter-rouge">0010</code>, hogy a szenzor küldje az adatokat.
Először olvassuk ki:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">[</span>24-48-8C-94-96-4C][LE]> char-read-hnd 38
Characteristic value/descriptor: 00 00
</code></pre></div></div>
<p>Látszik, hogy most az érték <code class="language-plaintext highlighter-rouge">0000</code>. Állítsuk át <code class="language-plaintext highlighter-rouge">0010</code>-re a következő paranccsal.</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">[</span>24-48-8C-94-96-4C][LE]> char-write-req 0x0038 0100
Characteristic value was written successfully
Notification handle <span class="o">=</span> 0x0036 value: e4 0a 43 c8 0b
Notification handle <span class="o">=</span> 0x0036 value: e4 0a 43 c8 0b
</code></pre></div></div>
<p>Ezek után máris látható, hogy küldi is az értékeket. A küldött érték a <code class="language-plaintext highlighter-rouge">e4 0a 43 c8 0b</code>.
Ennek első két bájtja a hőmérséklet little endian kódolással. Tehát meg kell cserélni a bájtokat,
ez hexában <code class="language-plaintext highlighter-rouge">0a e4</code>, ami decimálisan <code class="language-plaintext highlighter-rouge">2788</code>, ezt el kell osztani <code class="language-plaintext highlighter-rouge">100</code>-zal, így kapjuk a
hőmérsékletet, ami <code class="language-plaintext highlighter-rouge">27,88</code> fok.</p>
<p>Második bájt a páratartalom, melynek értéke hexában <code class="language-plaintext highlighter-rouge">43</code>, ami decimálisan <code class="language-plaintext highlighter-rouge">67</code>, tehát az érték <code class="language-plaintext highlighter-rouge">67 %</code>.</p>
<p>A legutolsó két bájt pedig az elem feszültsége, ugyanígy kell számolni, mint a hőmérsékletet.
Azaz <code class="language-plaintext highlighter-rouge">c8 0b</code>, megcseréljük a byte-okat, az hexában <code class="language-plaintext highlighter-rouge">0b c8</code>, ami decimálisan <code class="language-plaintext highlighter-rouge">3016</code>, amit <code class="language-plaintext highlighter-rouge">100</code>-zal
osztva a <code class="language-plaintext highlighter-rouge">3,016 V</code> értéket kapjuk.</p>
<p>Az <code class="language-plaintext highlighter-rouge">exit</code> paranccsal kiléphetünk.</p>
<p>Érdekessége, hogy kinyerhetünk információt a gyártóról, és ott a <a href="http://www.miaomiaoce.com/">http://www.miaomiaoce.com/</a> címet találjuk,
ami egy kínai oldal, ahol sok hasonló terméket találhatunk.</p>
<p>De persze miért is használnánk ilyen alapszintű műveleteket, ha Pythonban kész könyvtár van rá.
Ez a <a href="https://github.com/IanHarvey/bluepy">bluepy</a>, mely csak Linuxon működik.</p>
<p>Működéséhez először telepítsük a <code class="language-plaintext highlighter-rouge">libglib2.0-dev</code> könyvtárat a következő paranccsal:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>apt-get <span class="nb">install </span>libglib2.0-dev
</code></pre></div></div>
<p>Majd inicializáljunk egy Python projektet, Virtual Environmenttel (hogy ne globálisan telepítsük a függőségeket), és telepítsük a <code class="language-plaintext highlighter-rouge">bluepy</code> függőséget.</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>apt-get <span class="nb">install </span>python3-venv
python <span class="nt">-m</span> venv venv
venv/bin/activate
pip <span class="nb">install </span>bluepy
</code></pre></div></div>
<p>A <a href="http://ianharvey.github.io/bluepy-doc/">dokumentáció</a> szerint <code class="language-plaintext highlighter-rouge">Peripheral</code> osztállyal csatlakozhatunk, és
át kell adni egy callbacket (<code class="language-plaintext highlighter-rouge">DefaultDelegate</code> leszármazott), amit visszahív, ha érték érkezik. Itt már csak el kell végezni a fenti számításokat.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">bluepy</span> <span class="kn">import</span> <span class="n">btle</span>
<span class="k">class</span> <span class="nc">MyDelegate</span><span class="p">(</span><span class="n">btle</span><span class="p">.</span><span class="n">DefaultDelegate</span><span class="p">):</span>
<span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="n">btle</span><span class="p">.</span><span class="n">DefaultDelegate</span><span class="p">.</span><span class="n">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">handleNotification</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">cHandle</span><span class="p">,</span> <span class="n">data</span><span class="p">):</span>
<span class="n">databytes</span> <span class="o">=</span> <span class="nb">bytearray</span><span class="p">(</span><span class="n">data</span><span class="p">)</span>
<span class="n">temperature</span> <span class="o">=</span> <span class="nb">int</span><span class="p">.</span><span class="n">from_bytes</span><span class="p">(</span><span class="n">databytes</span><span class="p">[</span><span class="mi">0</span><span class="p">:</span><span class="mi">2</span><span class="p">],</span><span class="s">"little"</span><span class="p">)</span> <span class="o">/</span> <span class="mi">100</span>
<span class="n">humidity</span> <span class="o">=</span> <span class="nb">int</span><span class="p">.</span><span class="n">from_bytes</span><span class="p">(</span><span class="n">databytes</span><span class="p">[</span><span class="mi">2</span><span class="p">:</span><span class="mi">3</span><span class="p">],</span><span class="s">"little"</span><span class="p">)</span>
<span class="n">battery</span> <span class="o">=</span> <span class="nb">int</span><span class="p">.</span><span class="n">from_bytes</span><span class="p">(</span><span class="n">databytes</span><span class="p">[</span><span class="mi">3</span><span class="p">:</span><span class="mi">5</span><span class="p">],</span><span class="s">"little"</span><span class="p">)</span> <span class="o">/</span> <span class="mi">1000</span>
<span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"Temperature: </span><span class="si">{</span><span class="n">temperature</span><span class="si">}</span><span class="s">, humidity: </span><span class="si">{</span><span class="n">humidity</span><span class="si">}</span><span class="s">, battery: </span><span class="si">{</span><span class="n">battery</span><span class="si">}</span><span class="s">"</span><span class="p">)</span>
<span class="n">mac</span> <span class="o">=</span> <span class="s">"24-48-8C-94-96-4C"</span>
<span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"Connecting to </span><span class="si">{</span><span class="n">mac</span><span class="si">}</span><span class="s">"</span><span class="p">)</span>
<span class="n">connected</span> <span class="o">=</span> <span class="bp">False</span>
<span class="k">try</span><span class="p">:</span>
<span class="c1"># Timeout not released: https://github.com/IanHarvey/bluepy/pull/374
</span> <span class="n">dev</span> <span class="o">=</span> <span class="n">btle</span><span class="p">.</span><span class="n">Peripheral</span><span class="p">(</span><span class="n">mac</span><span class="p">)</span>
<span class="n">connected</span> <span class="o">=</span> <span class="bp">True</span>
<span class="k">print</span><span class="p">(</span><span class="s">"Connection done..."</span><span class="p">)</span>
<span class="n">delegate</span> <span class="o">=</span> <span class="n">MyDelegate</span><span class="p">()</span>
<span class="n">dev</span><span class="p">.</span><span class="n">setDelegate</span><span class="p">(</span><span class="n">delegate</span><span class="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="s">"Waiting for data..."</span><span class="p">)</span>
<span class="n">dev</span><span class="p">.</span><span class="n">waitForNotifications</span><span class="p">(</span><span class="mf">15.0</span><span class="p">)</span>
<span class="k">except</span> <span class="n">btle</span><span class="p">.</span><span class="n">BTLEDisconnectError</span> <span class="k">as</span> <span class="n">error</span><span class="p">:</span>
<span class="k">print</span><span class="p">(</span><span class="n">error</span><span class="p">)</span>
<span class="k">finally</span><span class="p">:</span>
<span class="k">if</span> <span class="n">connected</span><span class="p">:</span>
<span class="n">dev</span><span class="p">.</span><span class="n">disconnect</span><span class="p">()</span>
</code></pre></div></div>
<p>Most már csak arra van szükség, hogy ezt eljuttassuk a Prometheusba. ez ún. pull modellel dolgozik, azaz bizonyos időközönként a
Prometheus hív ki HTTP(S) protokollon, és válaszban kapott értékeket menti el.</p>
<p>A Prometheus az ún. <a href="https://github.com/OpenObservability/OpenMetrics/blob/main/specification/OpenMetrics.md">OpenMetrics</a> formátumot képes feldolgozni.
Ez valami hasonló:</p>
<pre><code class="language-plain">#HELP xiaomi_sensor_exporter_number_of_sensors Number of sensors
#TYPE xiaomi_sensor_exporter_number_of_sensors gauge
xiaomi_sensor_exporter_number_of_sensors 1
#HELP xiaomi_sensor_exporter_temperature_celsius Temperature
#TYPE xiaomi_sensor_exporter_temperature_celsius gauge
xiaomi_sensor_exporter_temperature_celsius{name="workroom",address="A4:C1:38:78:88:5A"} 25.66
#HELP xiaomi_sensor_exporter_humidity_percent Humidity
#TYPE xiaomi_sensor_exporter_humidity_percent gauge
xiaomi_sensor_exporter_humidity_percent{name="workroom",address="A4:C1:38:78:88:5A"} 65
#HELP xiaomi_sensor_exporter_battery_volt Battery
#TYPE xiaomi_sensor_exporter_battery_volt Volt
xiaomi_sensor_exporter_battery_volt{name="workroom",address="A4:C1:38:78:88:5A"} 2.997
</code></pre>
<p>Látható, hogy van négy metrika, <code class="language-plaintext highlighter-rouge">xiaomi_sensor_exporter_number_of_sensors</code>, <code class="language-plaintext highlighter-rouge">xiaomi_sensor_exporter_temperature_celsius</code>, stb.
Nézzük a <code class="language-plaintext highlighter-rouge">xiaomi_sensor_exporter_temperature_celsius</code> metrikát. Ennek meg van adva a neve a <code class="language-plaintext highlighter-rouge">#HELP</code> sorban, értéke <code class="language-plaintext highlighter-rouge">Temperature</code>,
a típusa a <code class="language-plaintext highlighter-rouge">#TYPE</code> sorban, értéke <code class="language-plaintext highlighter-rouge">gauge</code> (mely egy adott pillanatban mért értéket ír le), és maga az érték, mely <code class="language-plaintext highlighter-rouge">25.66</code>. Sőt, ezeket az értékeket címkézni is lehet,
látható, hogy van egy <code class="language-plaintext highlighter-rouge">name="workroom"</code> értékpár és egy <code class="language-plaintext highlighter-rouge">address="A4:C1:38:78:88:5A"</code> értékpár. Ezek alapján később szűrni is lehet.</p>
<p>Találtam olyan projektet, mely képes arra, hogy lekérdezze a hőmérsékleti értékeket, és azokat exportálja OpenMetrics
formátumban, <a href="https://github.com/rostrovsky/blexy">blexy</a> néven. Ez azonban folyamatosan nyitva tartotta a kapcsolatot, és félő, hogy ez
több energiát használ. Docker image sem volt belőle. Valamint ennek használatával nem élhettem volna át az alkotás örömét. Ezért saját
projektet hoztam létre <a href="https://github.com/vicziani/xiaomi-sensor-exporter">xiaomi-sensor-exporter</a> néven. Ez futtat egy http
szervert, és kérésre csatlakozik a szenzorhoz, lekérdezi az értékeket és OpenMetrics formátumban visszaadja.</p>
<p>Én az egyszerűség kedvéért Dockerben futtatom, melyhez az image megtalálható a <a href="https://hub.docker.com/r/vicziani/xiaomi-sensor-exporter">Docker Hubon</a>.</p>
<p>Használatához először létre kell hozni egy konfigurációs állományt, amibe megadjuk az eszközök MAC-címeit, valamint a http szerver portját.</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">port</span><span class="pi">:</span> <span class="m">9093</span>
<span class="na">devices</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">workroom</span>
<span class="na">address</span><span class="pi">:</span> <span class="s">24-48-8C-94-96-4C</span>
</code></pre></div></div>
<p>Utána a következő Docker paranccsal indíthatjuk:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker run <span class="nt">-v</span> /home/pi/xiaomi-sensor-exporter/config.yaml:/app/config/config.yaml <span class="nt">--net</span><span class="o">=</span>host <span class="nt">--privileged</span> <span class="nt">--name</span> xiaomi <span class="nt">-d</span> vicziani/xiaomi-sensor-exporter:0.0.1
</code></pre></div></div>
<p>Sajnos csak <code class="language-plaintext highlighter-rouge">--net=host --privileged</code> paraméterekkel tudtam működésre bírni, így fért hozzá a konténer a Bluetooth stackhez. A <a href="https://stackoverflow.com/questions/28868393/accessing-bluetooth-dongle-from-inside-docker">Stackoverflow-n</a> javasolt megoldások sajnos nekem nem működtek.</p>
<p>Amint elindult, a következőképp érhetők el a metrikák:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>curl http://localhost:9093/metrics
<span class="c">#HELP xiaomi_sensor_exporter_number_of_sensors Number of sensors</span>
<span class="c">#TYPE xiaomi_sensor_exporter_number_of_sensors gauge</span>
xiaomi_sensor_exporter_number_of_sensors 1
<span class="c">#HELP xiaomi_sensor_exporter_temperature_celsius Temperature</span>
<span class="c">#TYPE xiaomi_sensor_exporter_temperature_celsius gauge</span>
xiaomi_sensor_exporter_temperature_celsius<span class="o">{</span><span class="nv">name</span><span class="o">=</span><span class="s2">"workroom"</span>,address<span class="o">=</span><span class="s2">"24-48-8C-94-96-4C"</span><span class="o">}</span> 26.26
<span class="c">#HELP xiaomi_sensor_exporter_humidity_percent Humidity</span>
<span class="c">#TYPE xiaomi_sensor_exporter_humidity_percent gauge</span>
xiaomi_sensor_exporter_humidity_percent<span class="o">{</span><span class="nv">name</span><span class="o">=</span><span class="s2">"workroom"</span>,address<span class="o">=</span><span class="s2">"24-48-8C-94-96-4C"</span><span class="o">}</span> 65
<span class="c">#HELP xiaomi_sensor_exporter_battery_volt Battery</span>
<span class="c">#TYPE xiaomi_sensor_exporter_battery_volt Volt</span>
xiaomi_sensor_exporter_battery_volt<span class="o">{</span><span class="nv">name</span><span class="o">=</span><span class="s2">"workroom"</span>,address<span class="o">=</span><span class="s2">"24-48-8C-94-96-4C"</span><span class="o">}</span> 3.102
</code></pre></div></div>
<p>Ha ez a <code class="language-plaintext highlighter-rouge">http://raspberry.local:9093/metrics</code> címen is elérhető, akkor a Prometheusba a következőképp kell
bekötni a <code class="language-plaintext highlighter-rouge">prometheus.yaml</code> állományba:</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">scrape_configs</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">job_name</span><span class="pi">:</span> <span class="s">xiaomi_sensor</span>
<span class="na">scrape_interval</span><span class="pi">:</span> <span class="s">15m</span>
<span class="na">scrape_timeout</span><span class="pi">:</span> <span class="s">30s</span>
<span class="na">static_configs</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">targets</span><span class="pi">:</span> <span class="pi">[</span><span class="s1">'</span><span class="s">raspberry.local:9093'</span><span class="pi">]</span>
</code></pre></div></div>
<p>Ez 15 percenként fogja lekérni a megadott címről az értékeket.</p>
<p>Ehhez már csak egy Grafana dashboard kell, melyet szintén feltöltöttem a
<a href="https://github.com/vicziani/xiaomi-sensor-exporter/blob/master/xiaomi-sensor-exporter-grafana-dashboard.json">GitHub-ra</a>.</p>
JTechLog Technology Radar2023-08-12T08:00:00+00:00http://www.jtechlog.hu/2023/08/12/technology-radar<p>Emlékszem régebben mennyire nagy esemény volt mindig, mikor kijött a Thoughtworks
<a href="https://www.thoughtworks.com/radar">Technology Radar</a> legfrissebb kiadása.
A Thoughtworksről annyit kell tudni, hogy 1999-ben csatlakozott a céghez
Martin Fowler, és több munkatársával együtt jelentős és meghatározó könyveket írtak,
melyeket én is javaslok elolvasni. Ezek például:</p>
<ul>
<li>Martin Fowler - Refactoring</li>
<li>Martin Fowler - Patterns of Enterprise Application Architecture</li>
<li>Jez Humble, David Farley - Continuous Delivery</li>
<li>Sam Newman - Building Microservices</li>
</ul>
<p>A cég mindig nagyon haladó gondolkodású volt, és bizonyos időközönként
közreadta a Technology Radart, melyben grafikusan ábrázolták, hogy mi is
a véleményük az épp aktuális technológiákról, eszközökről, módszertanokról.</p>
<!-- more -->
<p>Ezeket koncentrikus körökben helyezték el, melyet négy cikkre (quadrants) osztottak:</p>
<ul>
<li>Techniques</li>
<li>Tools</li>
<li>Platforms</li>
<li>Languages & Frameworks</li>
</ul>
<p>A koncentrikus körök, azaz gyűrűk a következő neveket és jelentéseket kapták:</p>
<ul>
<li>Adopt: olyan technológiák, melyeket nagy biztonsággal lehet használni. Természetesen
nem jók mindenre, de az adott probléma megoldására tökéletesek.</li>
<li>Trial: nem feltétlenül érdemes az itt lévő technológiákat használni, de javasolt kipróbálni,
és ha bizonyít, használni.</li>
<li>Assess: nem biztos, hogy érdemes ezeket a technológiákat használni, de mindenképp érdekesek és javasolt
őket szemmel tartani.</li>
<li>Hold: olyan technológiák, melyeket nem érdemes használni. Vagy mert nem volt velük jó tapasztalat,
tele vannak hibával, vagy egyszerűen kimentek a divatból.</li>
</ul>
<p>Sőt, még időbeliséget is lehet rajta jelezni, egy kis nyillal jelölve a technológiánál, hogy befele vagy
kifele mozdult-e a gyűrűk között.</p>
<p>Sajnos azonban a Thoughtworks radarja egy idő után már nem volt számunkra releváns, túl sok olyan
technológiát tartalmazott, melyek nálunk szóba se jöhettek.</p>
<p>Nemrég olvastam a szintén a Thoughtworks holdudvarába tartozó
<em>Mark Richards, Neal Ford - Fundamentals of Software Architecture</em> könyvet, mely felveti, hogy
érdemes ilyen radart kidolgozni akár magunknak, akár a csoportunknak, akár a cégünknek is.</p>
<p>Ezt a könyvet érdemes amúgy is elolvasni, mert nagyon sok jó ötletet, praktikát és eszközt
javasol az architect munkák megkönnyítésére.</p>
<p><a href="/artifacts/posts/2023-08-12-technology-radar/Fundamentals_of_Software_Architecture.jpg" data-lightbox="post-images"><img src="/artifacts/posts/2023-08-12-technology-radar/Fundamentals_of_Software_Architecture_400px.png" alt="Fundamentals of Software Architecture" /></a></p>
<p>Több cég is készített magának ilyen radart:</p>
<ul>
<li><a href="https://opensource.zalando.com/tech-radar/">Zalando Tech Radar</a></li>
<li><a href="https://www.aoe.com/techradar/index.html">AOE Technology Radar</a></li>
<li><a href="https://medium.com/inside-sumup/tech-at-sumup-its-on-our-radar-fab975ac3d17">SumUp Tech Radar</a></li>
<li><a href="https://opensource.zup.com.br/radar/radar/">Zup Tech Radar</a></li>
<li><a href="https://tech-radar.preply.com/">Preply Tech Radar</a></li>
<li><a href="https://techradar.avito.ru/backend">Avito Backend Tech Radar</a></li>
<li><a href="https://techradar.softwareag.com/">Software AG Technology Radar</a></li>
<li><a href="https://medium.com/clearscore/clearscore-tech-radar-2021-549f2f62c95b">ClearScore Tech Radar 2021</a></li>
</ul>
<p>Sőt már kész eszközök is vannak arra, hogy saját radart készítsünk. Például:</p>
<ul>
<li><a href="https://www.thoughtworks.com/radar/byor">Thoughtworks - Build your own radar</a></li>
<li><a href="https://github.com/AOEpeople/aoe_technology_radar">AOE Technology Radar static site generator</a></li>
<li><a href="https://github.com/zalando/tech-radar">Zalando visualization</a></li>
</ul>
<p>Ezzel át tudjuk gondolni, hogy hogyan viszonyulunk a különböző technológiákhoz, és
másokkal is könnyebben meg tudjuk osztani. Természetesen én is kidolgoztam egy ilyet, csak kicsit átszabtam.
A Zalando JavaScriptjét használtam ehhez.</p>
<p>A körcikkek:</p>
<ul>
<li>Languages, frameworks and libraries</li>
<li>Methods and patterns</li>
<li>Platforms and operations</li>
<li>Tools</li>
</ul>
<p>A gyűrűk:</p>
<ul>
<li>Adopt: szívesen használom/használnám éles projektben.</li>
<li>Trial: használtam már valamilyen szinten, azonban éles projektben még meg kéne róla győződnöm, hogy tényleg használható-e.</li>
<li>Assess: szeretném kipróbálni.</li>
<li>Hold: vagy rossz tapasztalatom volt vele, kiment a divatból, vagy találtam jobb megoldást.</li>
</ul>
<p>A 2023.08-as JTechLog Technology Radar tehát a következő:</p>
<p><a href="/artifacts/posts/2023-08-12-technology-radar/jtechlog-technology-radar.png" data-lightbox="post-images"><img src="/artifacts/posts/2023-08-12-technology-radar/jtechlog-technology-radar_700px.png" alt="Fundamentals of Software Architecture" /></a></p>
<p>Interaktív formában elérhető a következő linken is: <a href="/artifacts/technology-radar/index.html">JTechLog Technology Radar</a></p>
Gondolatok a modell osztályokról, entitásokról, DTO-król2023-08-01T08:00:00+00:00http://www.jtechlog.hu/2023/08/01/modell-osztalyok<p>Nagyon szeretem azokat az írásokat, melyek különböző területekről származó fogalmakat
próbálnak valamilyen egységes rendszerbe foglalni, a fogalmaknak egységes elnevezést
adni. Ilyen volt a
<a href="https://reflectoring.io/book/">Tom Hombergs - Get Your Hands Dirty on Clean Architecture (2nd edition)</a>
könyv, mely a modell osztályokkal próbálta ezt tenni. Különösen tetszett, hogy nem egy megoldást
fogad el igaznak, hanem ismerteti a különböző megoldásokat, összehasonlítva azok előnyeit és hátrányait.</p>
<p>A mostanában fejlesztett alkalmazások vagy a klasszikus háromrétegű (3-layer) architektúrával készülnek,
vagy a modernebb Hexagonal vagy Clean Architecture-t használják.</p>
<p>Egy klasszikus Spring Boot alkalmazás esetén a három réteg a repository, service, controller.
Java EE esetén ez a web, üzleti és EIS (Enterprise Information System) rétegek. Én egységesen
úgy fogok rájuk hivatkozni, hogy prezentációs, üzleti logika és perzisztens réteg.</p>
<p>Clean Architecture esetén is megjelennek ezek, az alapvető különbség a függőségek irányában van,
hiszen amíg a klasszikus háromrétegű alkalmazás esetén az üzleti logika réteg függ a perzisztens
rétegtől, addig a Clean Architecture esetén a perzisztens réteg függ az üzleti logika rétegtől.</p>
<p>Minden rétegben szükség van azonban az adatokat tároló osztályokra, nevezzük ezeket modell osztályoknak.
Manapság ezek egyszerű POJO (Plain Old Java Object) osztályok, pár attribútummal, konstruktorokkal, getter/setter metódusokkal.</p>
<p>Kérdés, hogy ebbe a fogalomrendszerbe hogyan illeszkednek be az entitások, DTO-k, stb.?
Melyik rétegben helyezkednek el? Illetve kell-e, és hogyan kell ezek között a megfeleltetést elvégezni (mapping),
konvertálni?
Ez a poszt ebben próbál egyfajta rendet tenni.</p>
<!-- more -->
<h2 id="architektúrák">Architektúrák</h2>
<p>Az első ábran vizuálisan hasonlítjuk össze a két architektúrát. A háromrétegű architektúránál
a szokástól eltérően nem egymás alatt, hanem egymás mellett ábrázoljuk a rétegeket, a könnyebb
összehasonlíthatóság kedvéért. A balról jobbra mutató nyilak a függőség irányát, de a hívás irányát is mutatják.</p>
<p><img src="/artifacts/posts/2023-08-01-modell-osztalyok/architekturak.png" alt="Architektúrák" /></p>
<p>A Clean Architecture esetén középen van a üzleti logika réteg, és erre hivatkozik a prezentációs
és a perzisztens réteg is, tehát látható hogy a függőség a perzisztens rétegnél megfordul.
A hívás iránya viszont nem változik, ugyanúgy balról jobbra. A továbbiakban a nyilakat
szándékosan nem fogom feltüntetni. A Hexagonal Architecture még tovább megy elnevezésekben,
az üzleti logika réteg definiálja ezen kívül a portokat is.
Ezek interfészek a többi réteg számára. A bejövő portok (inbound) azok az interfészek, melyeken
keresztül az üzleti logikát lehet meghívni, ezeket az üzleti logika implementálja.
A kimenő portok (outbound) pedig azok, melyeken keresztül az üzleti logika hív ki.
Az adapterek melyek a külvilággal tartják a kapcsolatot (felhasználók, web, adatbázis), az interfészeken keresztül
kommunikálnak, vagy akár meg is valósítják azokat. Ezért nevezik ezt az architektúrát
Ports & Adapters architektúrának is.</p>
<p>Abban talán egységes az álláspont, hogy az üzleti logika rétegben szereplő modell osztályokat
üzleti entitásnak nevezik. Egyedi azonosítóval rendelkeznek, és módosíthatóak. Ezek lehetnek üzleti logikát nem tartalmazó, csak attribútumokkal és getter/setter metódusokkal
ellátott osztályok. Ez az ún. anemic (vérszegény) model. De a modernebb architektúrák, valamint
a gyakran velük együtt használt DDD (Domain Driven Design) is azt preferálja, hogy az üzleti entitások
egy rich (gazdag, üzleti logikával ellátott) modellt alkossanak.</p>
<p>Ezektől azonban nem feltétlenül elvárt, hogy perzisztálhatóak legyenek.
Persze lehetnek olyan entitások is, melyeket perzisztálni lehet. Gyakran használunk erre valamilyen
ORM eszközt, pl. a JPA-t, mely szerencsétlen módon a perzisztálható osztályokat szintén entitásnak nevezi,
ezáltal összemosva a két fogalmat. Én ezekre perzisztens entitásokként fogok hivatkozni.</p>
<h2 id="stratégiák">Stratégiák</h2>
<p>A legegyszerűbb persze, ha ugyanazt az üzleti entitást használjuk az összes rétegbe. Ekkor nincs
szükség megfeleltetésre sem. Ezt a könyv <em>No Mapping</em> stratégiának hívja.</p>
<p><img src="/artifacts/posts/2023-08-01-modell-osztalyok/no-mapping.png" alt="No mapping" /></p>
<p>Ebben az esetben képzeljünk el egy <code class="language-plaintext highlighter-rouge">Employee</code> entitást, mely el van látva JPA és JSON annotációkkal is,
és ez utazik metódus paraméterekben és visszatérési értékként is.</p>
<p>Előnye, hogy rendkívül egyszerű, hiszen nincs szükség a mappingre. Hátránya, hogy megtöri a Single Responsibility
elvet, hiszen az üzleti adattárolásért, JSON reprezentációért és adatbázis megfeleltetésért is felelős. Ha változik
az entitás, akkor nagy eséllyel változik a JSON dokumentum és a táblaszerkezet is.</p>
<p>A következő stratégia, mikor minden rétegnek saját modellje van, ezt a könyv <em>Two-Way Mapping</em> stratégiának hívja.</p>
<p><img src="/artifacts/posts/2023-08-01-modell-osztalyok/two-way-mapping.png" alt="Two-Way Mapping" /></p>
<p>Ekkor természetesen szükség van a mappingre. A kérdés, hogy mely modell utazik a rétegek között, hol történik
a mapping. Ebben az esetben az üzleti entitás szokott utazni, és a prezentációs és perzisztens réteg is hivatkozik
rá, valamint ezekben a rétegekben történik a mapping is. Azaz az üzleti entitás elhagyja a saját rétegét.</p>
<p>Kérdés lehet ilyenkor, hogy hogyan hívjuk a prezentációs és perzisztens rétegben lévő modell osztályainkat.
A későbbi magyarázat miatt tartózkodnék a DTO elnevezéstől, mindenképp a használt technológia fogalmaira építenék.
Ez REST esetén lehet pl. <code class="language-plaintext highlighter-rouge">Resource</code>, SOAP webszolgáltatások, RPC alapú kommunikáció (pl. gRPC) vagy
aszinkron üzenetküldés esetén lehet pl. <code class="language-plaintext highlighter-rouge">Request</code> és <code class="language-plaintext highlighter-rouge">Response</code>. Klasszikus webes alkalmazásoknál <code class="language-plaintext highlighter-rouge">Form</code> és <code class="language-plaintext highlighter-rouge">View</code> / <code class="language-plaintext highlighter-rouge">PresentationModel</code>.
JPA adatbáziskezelés esetén marad az <code class="language-plaintext highlighter-rouge">Entity</code>.</p>
<p>Képzeljük el, hogy az üzleti entitásunk lesz az <code class="language-plaintext highlighter-rouge">Employee</code>, a prezentációs rétegben egy <code class="language-plaintext highlighter-rouge">EmployeeResource</code> és a
perzisztens rétegben egy <code class="language-plaintext highlighter-rouge">EmployeeEntity</code>. Az <code class="language-plaintext highlighter-rouge">Employee</code> üzleti entitás és a másik kettő között kell konvertálgatni.</p>
<p>Itt már nincs szoros kapcsolat a modellek között, azonban aggályos lehet, hogy az üzleti entitás kikerül az üzleti logika rétegből. Nyilván a mapping egy plusz komplexitást hoz.</p>
<p>A következő, mikor az üzleti entitás a üzleti logika rétegen belül marad, és a metódusok paraméterei és visszatérési
értékei külön osztályok, még szintén az üzleti logika rétegben elhelyezkedő input/output modell. Ez a <em>Full Mapping</em>.</p>
<p><img src="/artifacts/posts/2023-08-01-modell-osztalyok/full-mapping.png" alt="Full Mapping" /></p>
<p>Ekkor az üzleti rétegben megjelenik az input/output modell, melyek adják a metódusok paramétereit, valamint a visszatérési
típusokat. Használhatóak itt pl. a <code class="language-plaintext highlighter-rouge">Command</code>, <code class="language-plaintext highlighter-rouge">Query</code>, <code class="language-plaintext highlighter-rouge">Result</code> elnevezések.</p>
<p>Képzeljük el, hogy bejön REST interfészen POST-tal egy <code class="language-plaintext highlighter-rouge">EmployeeResource</code> kérés,
ezt kell mappelni <code class="language-plaintext highlighter-rouge">CreateEmployeeCommand</code> input modellé, majd ez alapján hozunk létre egy <code class="language-plaintext highlighter-rouge">Employee</code> üzleti entitást.</p>
<p>(Zárójelben megjegyzem, hogy én olyat is szoktam csinálni, hogy a prezentációs rétegnek nem hozok létre saját modellt, hanem
teljes mértékben az input/output modellt használom erre a célra. Bár a könyvben ez nem szerepel, ezt önkényesen <em>Half Mapping</em>
stratégiának nevezem.)</p>
<p><img src="/artifacts/posts/2023-08-01-modell-osztalyok/half-mapping.png" alt="Half Mapping" /></p>
<p>A könyv említ egy olyan megoldást is, mikor bevezet egy közös modell interfészt, melyet az összes modell implementál. Ez csak
getterekkel rendelkezik.
Így a paramétereknél és a visszatérési értékeknél lehet ezt az interfészt használni, nem tudjuk, hogy pontosan milyen példány van mögötte. Ezt <em>One-Way Mapping</em> stratégiának hívja.</p>
<p><img src="/artifacts/posts/2023-08-01-modell-osztalyok/one-way-mapping.png" alt="One-Way Mapping" /></p>
<h2 id="változatok">Változatok</h2>
<p>A dolgot még bonyolítja az, hogy a stratégiaválasztásnak egyáltalán nem kell szimmetrikusnak lennie,
azaz a prezentációs réteg felé használhatunk más stratégiát, mint a perzisztens réteg felé.</p>
<p>Hiszen nagyon gyakori, hogy a prezentációs rétegből beérkező modellt még mappeljük üzleti entitássá,
de az üzleti entitás maga egy JPA entitás is, ami azonnal menthető. Így a prezentációs réteg felé
van egy Half Mapping, míg a perzisztens réteg felé egy No Mapping.</p>
<p>Amennyiben modernebb architektúrát alkalmazunk DDD-vel, ott a perzisztenciát ennél élesebben javasolják elkülöníteni.
Azaz a üzleti entitások, domain entity-k kizárólag Java SE hivatkozásokat tartalmazhatnak, nem lehet kapcsolatban
más technológiával, pl. JPA-val. Így ilyenkor külön perzisztens entitásokat szoktak létrehozni, üzleti entitást
ebbe mappelni, és lementeni. Így az üzleti entitás változtatása nem hozza feltétlen magával az adatbázis módosítást,
ráadásul a kettő szignifikánsan eltérhet.</p>
<p>Külön érdekesség még, hogy nem csak rétegek szerint, hanem irány szerint is használhatunk más mapping
stratégiát. Pl. módosítás (CQS vagy CQRS esetén command) ágon használhatunk pl. Half Mappinget. Míg lekérdezés (query)
ágon akár <em>No Mappinget</em>, azaz natív query-vel már olyan osztályt állítunk elő, amit azonnal viszünk végig a prezentációs
rétegig.</p>
<h2 id="mikor-melyiket-használjuk">Mikor melyiket használjuk?</h2>
<p>A klasszikus válasz: attól függ. Egyszerű CRUD alkalmazás esetén használhatjuk a No Mapping stratégiát.
Amikor azonban két réteg modellje eltér egymástól, a Full Mapping lehet jó választás.</p>
<p>Érdemes megjegyezni, hogy ezt ne véssük kőbe. Indulhatunk a No Mapping stratégiával, és ahogy változik az alkalmazás,
úgy vezetünk be más stratégiát.</p>
<h2 id="további-megfontolások">További megfontolások</h2>
<p>JPA használata esetén amennyiben a JPA entitás átkerül másik rétegbe, oda kell figyelnünk. Ugyanis a tranzakciós
határon belül módosíthatjuk az állapotát. Ez üzleti logika rétegben lehet akár előnyös, ugyanis nem kell
a módosítás után külön metódust hívni, hiszen a JPA automatikusan visszaszinkronizálja az adatbázisba.
Ha az üzleti logika réteg biztosítja a tranzakciós határt, akkor nem kell félni, hogy a prezentációs
réteg módosítja az entitás állapotát, és azt a JPA szinkronizálja az adatbázisba.</p>
<p>Bár a JPA szabvány szerint a perzisztens entitások pehelysúlyú komponensek, POJO-k, egy probléma
azonban adódik, méghozzá a lazy loading, szóval sajnos POJO-ink nem függetlenek az adatbázistól.
Ha egy perzisztens entitáshoz kapcsolódó más entitásokhoz akarunk hozzáférni a tranzakciós határon kívül,
kivételt kaphatunk. Ezért
nekünk kell gondoskodnunk arról, hogy az összes kapcsolódó entitás betöltésre kerüljön,
azaz pl. fetch joinnal betölteni.</p>
<p>Nem érdemes ugyanazt az input/output modell osztályt használni különböző használati eseteknél.
Szóval pl. elgondolkozhatunk, hogy ugyanazt az <code class="language-plaintext highlighter-rouge">EmployeeCommand</code> osztályt használjuk alkalmazott
létrehozásánál és módosításánál is. Csak bizonyos attribútumokat kitöltünk, bizonyos attribútumokat nem.
Javasolt inkább egy <code class="language-plaintext highlighter-rouge">RegisterEmployeeCommand</code> és egy <code class="language-plaintext highlighter-rouge">UpdateEmployeeCommand</code> modell osztályt létrehozni.
Ezáltal teljesül a Single Responsibility, másrészt a forráskód is olvasható marad, hiszen már az osztály
nevéből látszik, hogy mely használati esetben vesz részt.</p>
<p>REST esetén azonban ez megfontolandó, ugyanis a REST szabvány szerint mindig a teljes erőforrás utazik (, és nem annak egy részhalmaza). Ott nincsenek ugyanis külön metódusok, URL-ekben nem használhatunk igéket, csak az erőforrások, és az
azokon végzett CRUD műveltek léteznek.</p>
<p>Mi van akkor, ha különböző képernyőkön az entitás adatainak különböző részhalmazait
szeretnénk megjeleníteni. Itt is dönthetünk úgy, hogy minden adatot leküldünk, és a UI
kiválogatja a megfelelő mezőket. Azonban bonyolult entitások esetén érdemes lehet performancia
okokból több modellt is létrehozni képernyőnként.</p>
<p>Ha ezt ráadásul hatékonyan akarjuk megtenni, dönthetünk amellett, hogy pl. JPA projection query-t, vagy native query-t (akár JDBC-vel) használunk.</p>
<p>Használhatunk REST helyett GraphQL-t is, mely erre a feladatra sokkal jobb választás.</p>
<p>A modell osztályoknak technológiai megszorításai is lehetnek. Pl. egy JPA perzisztens entitásnak lennie
kell paraméter nélküli konstruktorának. Sokszor gondolkodás nélkül létrehozzuk a paraméter nélküli konstruktort,
valamint az összes getter/setter metódust. Érdemes utánanézni a különböző technológiák dokumentációjában,
ugyanis a Hibernate képes privát konstruktorral is, és getter/setter metódusok nélkül is dolgozni.</p>
<p>Persze előfordulhat, hogy hívó és hívott oldalon is ugyanazokat az osztályokat akarjuk használni,
két különböző alkalmazásban. Ekkor eszünkbe juthat, hogy úgy osszuk meg ezeket az osztályokat,
hogy egy külön JAR-ba csomagoljuk, és mindkét alkalmazásban felhasználjuk.
Ezt a megoldást lehetőleg kerüljük, mert nagy függőség, inkább függjünk az API-leírástól,
pl. REST esetén az OpenAPI dokumentumtól, SOAP esetén a WSDL-től, más technológia (gRPC + Protocol Buffer, GraphQL) esetén bármilyen séma leírástól, amiből akár forráskód is könnyen generálható.</p>
<p>A modellek használatának további csomagolási vonzata is lehet. Ugyanis elképzelhető, hogy úgy akarjuk egy alkalmazáson belül korlátozni, hogy mely osztályhoz csak mely rétegek férhetnek hozzá, hogy külön modulba, esetleg JAR-ba tesszük őket.</p>
<h2 id="dto">DTO</h2>
<p>A <a href="https://www.martinfowler.com/eaaCatalog/dataTransferObject.html">Data Transfer Object</a>
kifejezés konkrétan Martin Fowlertől származik,
és a Patterns of Enterprise Application Architecture könyvében már megjelent
2002-ben. Feltételezhetően innen vette át a Core J2EE Patterns könyv is,
bár ott csak <a href="http://www.corej2eepatterns.com/TransferObject.htm">Transfer Object</a>
néven szerepel.</p>
<p>Az elsődleges feladata a hálózati kommunikáció csökkentése távoli hívás esetén (RMI).
Ugyanis ha az adatok túl finom szemcsézettségűek, és emiatt csak sok
hívással lehet átvinni, az hálózati kommunikáció szempontjából költséges.
Megoldás a DTO használata, amivel nagyobb darabokba fogjuk össze az adatokat,
és kevesebb, ideális esetben egy hívással visszük át a hálózaton.</p>
<p>Másik problémaként jelentkezett, hogy bizonyos technológiák esetén (ilyen pl. a J2EE Entity Beanek),
az entitások túlságosan kötődtek az adatbázishoz, nehézsúlyú komponensek voltak, és nem lehetett
ezeket szerializálni, és így átvinni ezeket a hálózaton.</p>
<p>Ezt a mintát az alkotója szerint is sokan félreértették, és ki is kelt ez ellen a
<a href="https://martinfowler.com/bliki/LocalDTO.html">LocalDTO</a> írásában. Itt odáig is elmegy, hogy
lokális esetben (ilyen lehet, ha az alkalmazáson belül a prezentációs és üzleti réteg közötti
átvitelre használjuk) nem hogy nem hasznos, de plusz költség, már-már károsnak tekinthető.</p>
<p>Megemlíti, hogy ezzel biztosítható, hogy az üzleti logika réteg módosítható legyen anélkül,
hogy a kliensre hatással legyen, azonban ne felejtsük el a mapping költségét, ami
“jelentős és fájdalmas is lehet”. (Gondoljunk itt a ModelMapper és MapStruct használatára.)</p>
<p>Azt azonban később elismeri, hogy abban az esetben, ha a prezentációs réteg modellje és a
domain model jelentősen eltér, akkor használhatunk valami mappinget, és utal a
<a href="https://martinfowler.com/eaaDev/PresentationModel.html">Presentation Model</a> mintára,
mely gyakorlatilag olyan modell a GUI rétegben, mely annak igényei szerint van felépítve.</p>
<p>Ezt érthetjük úgy is, hogyha konvertáljuk is az entitásokat, ne a DTO nevet használjuk, mert az nem ezt takarja.
Azonban a világ már túllépett ezen, és sokszor használják ilyenkor a DTO elnevezést.</p>
Spring Security és Spring Boot2023-03-28T08:00:00+00:00http://www.jtechlog.hu/2023/03/28/spring-security-spring-boot<p>Technológiák: Spring Security 6.1, Spring Boot 3.1, Thymeleaf, Spring Data JPA, H2</p>
<p>Utolsó frissítés: 2023. november 15.</p>
<p>(Azért írtam meg ezt a posztot, mert sokan kerestek a Spring Security-re,
továbbá 2022 novemberében kijött a Spring Security 6 és Spring Boot 3, valamint
teljesértékű Spring Security poszt Spring Boottal eddig hiányzott.
A <a href="/2010/01/10/spring-security.html">Spring Security használata Springgel Frameworkkel poszt</a>
továbbra is elérhető.)</p>
<p>A Spring Security egy olyan keretrendszer, mely támogatja az autentikációt,
autorizációt és védelmet biztosít bizonyos támadási formák ellen. A Spring Security
a de-facto szabványos eszköz a biztonság megvalósítására Springes alkalmazásokon belül.</p>
<p>A Spring Security támogatja a felhasználónév és jelszó párossal történő bejelentkezést,
de ezen kívül pl. webszolgáltatások védelmére támogatja a HTTP BASIC, HTTP Digest
és tanúsítvány alapú bejelentkezést, sőt az OAuth 2.0 használatát is.</p>
<p>A felhasználók és a hozzá kapcsolódó szerepkörök tárolhatóak memóriában, adatbázisban, LDAP szerveren, stb.
Ezekhez adottak beépített implementációk, de saját is készíthető. Támogatja a jelszó hashelését
különböző algoritmusokkal. A felhasználóval kapcsolatos információkat képes cache-elni is. Különböző eseményekre eseménykezelőket lehet
aggatni, pl. bejelentkezés, így könnyen megoldható pl. audit naplózás.</p>
<p>Az alkalmazáson belül szerepkörökhöz lehet kötni bizonyos url-eket, valamint metódus szinten is
meg lehet adni, hogy milyen szerepkörrel rendelkező felhasználó hívhatja meg.</p>
<p>A poszthoz egy példa projekt is tartozik, mely <a href="https://github.com/vicziani/jtechlog-boot-security">elérhető a GitHub-on</a>.
Egyszerű Spring Boot webes alkalmazás, Spring Data JPA perzisztens réteggel, Thymeleaf template engine-nel.</p>
<!-- more -->
<h2 id="legegyszerűbb-védelem">Legegyszerűbb védelem</h2>
<p>A Spring Security már akkor védi az alkalmazást, ha szerepel a classpath-on. Ehhez elegendő
létrehozni egy üres alkalmazást a https://start.spring.io/ címen a Spring Web és Spring Security
függőségekkel, és egy <code class="language-plaintext highlighter-rouge">index.html</code> oldalt.</p>
<p>Az alkalmazást elindítva a Spring Security csak bejelentkezés
után enged hozzáférést. A bejelentkező képernyőt a Spring Security generálta ki,
valamint a háttérben létrehozott egy felhasználót <code class="language-plaintext highlighter-rouge">user</code> névvel. Minden indításkor új jelszót generál, melyet kiír a konzolra.
Ha a jelszót elrontom, hibaüzenetet kapok.
Sikeres bejelentkezés után megjelenik az <code class="language-plaintext highlighter-rouge">index.html</code> állomány tartalma. Az oldal újratöltésekor sem kér jelszót.
A Spring Security automatikusan létrehozz egy kijelentkezési lehetőséget is, mely elérhető a <code class="language-plaintext highlighter-rouge">/logout</code> címen.
Ezt meghívva a Spring Security rákérdez, hogy biztos ki akarok-e jelentkezni. Majd megjelenik a bejelentkező
képernyő egy üzenettel.</p>
<p>A Spring Security automatikusan létrehoz egy Basic autentikációs bejelentkezési lehetőséget is.
Ekkor a http kérés fejlécében kell elküldeni a felhasználónevet és a jelszót.</p>
<h2 id="felhasználók-betöltése-adatbázisból-saját-implementációval">Felhasználók betöltése adatbázisból saját implementációval</h2>
<p>A felhasználók betöltését implementálhatjuk magunk is. Ekkor a <code class="language-plaintext highlighter-rouge">UserDetailsService</code>
interfészt kell implementálni. Én egy Spring Data JPA megoldást választottam,
ehhez kell egy entitás is, mely a <code class="language-plaintext highlighter-rouge">UserDetails</code> interfészt implementálja.</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Entity</span>
<span class="nd">@Table</span><span class="o">(</span><span class="n">name</span><span class="o">=</span><span class="s">"users"</span><span class="o">)</span>
<span class="nd">@NoArgsConstructor</span>
<span class="nd">@Data</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">User</span> <span class="kd">implements</span> <span class="nc">UserDetails</span><span class="o">,</span> <span class="nc">Serializable</span> <span class="o">{</span>
<span class="nd">@Id</span>
<span class="nd">@GeneratedValue</span><span class="o">(</span><span class="n">strategy</span> <span class="o">=</span> <span class="nc">GenerationType</span><span class="o">.</span><span class="na">IDENTITY</span><span class="o">)</span>
<span class="kd">private</span> <span class="nc">Long</span> <span class="n">id</span><span class="o">;</span>
<span class="kd">private</span> <span class="nc">String</span> <span class="n">username</span><span class="o">;</span>
<span class="kd">private</span> <span class="nc">String</span> <span class="n">password</span><span class="o">;</span>
<span class="kd">private</span> <span class="nc">String</span> <span class="n">role</span><span class="o">;</span>
<span class="kd">public</span> <span class="nf">User</span><span class="o">(</span><span class="nc">String</span> <span class="n">username</span><span class="o">,</span> <span class="nc">String</span> <span class="n">password</span><span class="o">,</span> <span class="nc">String</span> <span class="n">role</span><span class="o">)</span> <span class="o">{</span>
<span class="k">this</span><span class="o">.</span><span class="na">username</span> <span class="o">=</span> <span class="n">username</span><span class="o">;</span>
<span class="k">this</span><span class="o">.</span><span class="na">password</span> <span class="o">=</span> <span class="n">password</span><span class="o">;</span>
<span class="k">this</span><span class="o">.</span><span class="na">role</span> <span class="o">=</span> <span class="n">role</span><span class="o">;</span>
<span class="o">}</span>
<span class="nd">@Override</span>
<span class="kd">public</span> <span class="nc">Collection</span><span class="o"><</span><span class="nc">GrantedAuthority</span><span class="o">></span> <span class="nf">getAuthorities</span><span class="o">()</span> <span class="o">{</span>
<span class="k">return</span> <span class="nc">List</span><span class="o">.</span><span class="na">of</span><span class="o">(</span><span class="k">new</span> <span class="nc">SimpleGrantedAuthority</span><span class="o">(</span><span class="n">role</span><span class="o">));</span>
<span class="o">}</span>
<span class="nd">@Override</span>
<span class="kd">public</span> <span class="kt">boolean</span> <span class="nf">isAccountNonExpired</span><span class="o">()</span> <span class="o">{</span>
<span class="k">return</span> <span class="kc">true</span><span class="o">;</span>
<span class="o">}</span>
<span class="nd">@Override</span>
<span class="kd">public</span> <span class="kt">boolean</span> <span class="nf">isAccountNonLocked</span><span class="o">()</span> <span class="o">{</span>
<span class="k">return</span> <span class="kc">true</span><span class="o">;</span>
<span class="o">}</span>
<span class="nd">@Override</span>
<span class="kd">public</span> <span class="kt">boolean</span> <span class="nf">isCredentialsNonExpired</span><span class="o">()</span> <span class="o">{</span>
<span class="k">return</span> <span class="kc">true</span><span class="o">;</span>
<span class="o">}</span>
<span class="nd">@Override</span>
<span class="kd">public</span> <span class="kt">boolean</span> <span class="nf">isEnabled</span><span class="o">()</span> <span class="o">{</span>
<span class="k">return</span> <span class="kc">true</span><span class="o">;</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>Látható, hogy a szerepkörök kezelését leegyszerűsítettem úgy, hogy a felhasználónak
csak egy szerepköre lehet, melyet a <code class="language-plaintext highlighter-rouge">role</code> attribútuma tartalmaz. Ezt a <code class="language-plaintext highlighter-rouge">getAuthorities()</code>
metódus konvertál a Spring Security által emészthető formába.</p>
<p>Írtam egy <code class="language-plaintext highlighter-rouge">UserRepository</code> interfészt a Spring Data JPA szerint.</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">interface</span> <span class="nc">UserRepository</span> <span class="kd">extends</span> <span class="nc">JpaRepository</span><span class="o"><</span><span class="nc">User</span><span class="o">,</span> <span class="nc">Long</span><span class="o">></span> <span class="o">{</span>
<span class="nc">Optional</span><span class="o"><</span><span class="nc">User</span><span class="o">></span> <span class="nf">findUserByUsername</span><span class="o">(</span><span class="nc">String</span> <span class="n">username</span><span class="o">);</span>
<span class="o">}</span>
</code></pre></div></div>
<p>Majd írtam egy <code class="language-plaintext highlighter-rouge">UserService</code> osztályt, melynek implementálnia kell
a <code class="language-plaintext highlighter-rouge">UserDetailsService</code> interfészt, melynek van egy
<code class="language-plaintext highlighter-rouge">public UserDetails loadUserByUsername(String username)</code> metódusa.
Ez továbbhív a repository-ba. A Spring Security ezt a metódust
fogja meghívni a felhasználó bejelentkezésekor.</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Service</span>
<span class="nd">@AllArgsConstructor</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">UserService</span> <span class="kd">implements</span> <span class="nc">UserDetailsService</span> <span class="o">{</span>
<span class="kd">private</span> <span class="nc">UserRepository</span> <span class="n">userRepository</span><span class="o">;</span>
<span class="nd">@Override</span>
<span class="kd">public</span> <span class="nc">UserDetails</span> <span class="nf">loadUserByUsername</span><span class="o">(</span><span class="nc">String</span> <span class="n">username</span><span class="o">)</span> <span class="o">{</span>
<span class="k">return</span> <span class="n">userRepository</span><span class="o">.</span><span class="na">findUserByUsername</span><span class="o">(</span><span class="n">username</span><span class="o">)</span>
<span class="o">.</span><span class="na">orElseThrow</span><span class="o">(()</span> <span class="o">-></span> <span class="k">new</span> <span class="nc">UsernameNotFoundException</span><span class="o">(</span><span class="s">"User not found: "</span> <span class="o">+</span> <span class="n">username</span><span class="o">));</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>Majd konfigurálom a Spring Security-t a <code class="language-plaintext highlighter-rouge">SecurityConfig</code> osztályban.</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Configuration</span><span class="o">(</span><span class="n">proxyBeanMethods</span> <span class="o">=</span> <span class="kc">false</span><span class="o">)</span>
<span class="nd">@EnableWebSecurity</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">SecurityConfig</span> <span class="o">{</span>
<span class="nd">@Bean</span>
<span class="kd">public</span> <span class="nc">SecurityFilterChain</span> <span class="nf">filterChain</span><span class="o">(</span><span class="nc">HttpSecurity</span> <span class="n">http</span><span class="o">)</span> <span class="kd">throws</span> <span class="nc">Exception</span> <span class="o">{</span>
<span class="n">http</span>
<span class="o">.</span><span class="na">authorizeHttpRequests</span><span class="o">(</span>
<span class="n">registry</span> <span class="o">-></span> <span class="n">registry</span>
<span class="o">.</span><span class="na">requestMatchers</span><span class="o">(</span><span class="s">"/login"</span><span class="o">)</span>
<span class="o">.</span><span class="na">permitAll</span><span class="o">()</span>
<span class="o">.</span><span class="na">requestMatchers</span><span class="o">(</span><span class="s">"/"</span><span class="o">)</span>
<span class="c1">// ROLE_ prefixet auto hozzáfűzi</span>
<span class="o">.</span><span class="na">hasAnyRole</span><span class="o">(</span><span class="s">"USER"</span><span class="o">,</span> <span class="s">"ADMIN"</span><span class="o">)</span>
<span class="o">)</span>
<span class="o">.</span><span class="na">formLogin</span><span class="o">(</span><span class="n">conf</span> <span class="o">-></span> <span class="n">conf</span>
<span class="o">.</span><span class="na">loginPage</span><span class="o">(</span><span class="s">"/login"</span><span class="o">)</span>
<span class="o">)</span>
<span class="o">.</span><span class="na">logout</span><span class="o">(</span><span class="nc">Customizer</span><span class="o">.</span><span class="na">withDefaults</span><span class="o">());</span>
<span class="k">return</span> <span class="n">http</span><span class="o">.</span><span class="na">build</span><span class="o">();</span>
<span class="o">}</span>
<span class="nd">@Bean</span>
<span class="kd">public</span> <span class="nc">PasswordEncoder</span> <span class="nf">encoder</span><span class="o">()</span> <span class="o">{</span>
<span class="k">return</span> <span class="k">new</span> <span class="nf">BCryptPasswordEncoder</span><span class="o">();</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>Az <code class="language-plaintext highlighter-rouge">@EnableWebSecurity</code> annotációt rá kell tenni arra a <code class="language-plaintext highlighter-rouge">@Configuration</code>
annotációval ellátott osztályra, mely a <code class="language-plaintext highlighter-rouge">SecurityFilterChain</code> beant fogja létrehozni.</p>
<p>A <code class="language-plaintext highlighter-rouge">filterChain()</code> metódus adja meg a részletes konfigurációt. A bejelentkező
oldal a <code class="language-plaintext highlighter-rouge">/login</code> címen lesz elérhető, és ezt bejelentkezés nélkül el kell tudni érni.
Az összes többi oldal eléréséhez szükséges a <code class="language-plaintext highlighter-rouge">ROLE_USER</code> és <code class="language-plaintext highlighter-rouge">ROLE_ADMIN</code> szerepkör. Fontos,
hogy a <code class="language-plaintext highlighter-rouge">hasAnyRole()</code> hívásakor már ne használjunk <code class="language-plaintext highlighter-rouge">ROLE_</code> előtagot, azt a metódus
alapból hozzáfűzi.
Kijelentkezni a <code class="language-plaintext highlighter-rouge">/logout</code> alapértelmezett címen lehet.</p>
<p>A Spring Security használatakor az egyik leggyakoribb hiba, hogy a
bejelentkezési képernyőt is letiltjuk, így végtelen ciklus alakulhat ki,
erre a böngésző figyelmeztet.</p>
<p>A jelszó hasheléséhez a BCrypt algoritmust használom, ehhez létrehoztam egy <code class="language-plaintext highlighter-rouge">BCryptPasswordEncoder</code>
beant.</p>
<h2 id="adatbázis-létrehozása">Adatbázis létrehozása</h2>
<p>Létre kell hozni a táblát is, valamint létrehozok két alapértelmezett felhasználót.</p>
<p>Ehhez a <code class="language-plaintext highlighter-rouge">schema.sql</code> fájlban létrehozom a táblát.</p>
<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">create</span> <span class="k">table</span> <span class="n">users</span> <span class="p">(</span><span class="n">id</span> <span class="nb">bigint</span> <span class="k">generated</span> <span class="k">by</span> <span class="k">default</span> <span class="k">as</span> <span class="k">identity</span> <span class="p">(</span><span class="k">start</span> <span class="k">with</span> <span class="mi">1</span><span class="p">),</span>
<span class="n">username</span> <span class="nb">varchar</span><span class="p">(</span><span class="mi">255</span><span class="p">),</span> <span class="n">password</span> <span class="nb">varchar</span><span class="p">(</span><span class="mi">255</span><span class="p">),</span> <span class="k">role</span> <span class="nb">varchar</span><span class="p">(</span><span class="mi">255</span><span class="p">),</span> <span class="k">primary</span> <span class="k">key</span> <span class="p">(</span><span class="n">id</span><span class="p">));</span>
</code></pre></div></div>
<p>A felhasználóknál szükségem van a jelszavuk hash-ére. Ehhez létrehozok egy teszt osztályt.</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nc">BCryptTest</span> <span class="o">{</span>
<span class="nd">@Test</span>
<span class="kt">void</span> <span class="nf">testEncode</span><span class="o">()</span> <span class="o">{</span>
<span class="nc">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="k">new</span> <span class="nc">BCryptPasswordEncoder</span><span class="o">().</span><span class="na">encode</span><span class="o">(</span><span class="s">"user"</span><span class="o">));</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>Ennek eredménye:</p>
<pre><code class="language-plain">$2a$10$WK5DYDlnywXj9Yni1kj4WOdEpBOriamVlY8UI8Isa38ermsz1TH4S
</code></pre>
<p>Érdekessége, hogy futtatásonként más értéket ad vissza. A hash magában foglal egy véletlenszerűen generált
saltot is, azért, hogy ne lehessen jelszó adatbázisok alapján feltörni. Három részből
áll, melyek dollárjelekkel (<code class="language-plaintext highlighter-rouge">$</code>) vannak elválasztva. Az első az algoritmus
verziója, példánkban <code class="language-plaintext highlighter-rouge">2a</code>. A második az ún. <em>cost</em> paraméter, példánkban <code class="language-plaintext highlighter-rouge">10</code>.
A harmadik részben az első 22 karakter a salt, a második 31 karakter pedig a hash.</p>
<p>A <code class="language-plaintext highlighter-rouge">data.sql</code> fájlba hozom létre a felhasználókat.</p>
<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">insert</span> <span class="k">into</span> <span class="n">users</span> <span class="p">(</span><span class="n">username</span><span class="p">,</span> <span class="n">password</span><span class="p">,</span> <span class="k">role</span><span class="p">)</span> <span class="k">values</span> <span class="p">(</span><span class="s1">'user'</span><span class="p">,</span> <span class="s1">'$2a$10$WK5DYDlnywXj9Yni1kj4WOdEpBOriamVlY8UI8Isa38ermsz1TH4S'</span><span class="p">,</span> <span class="s1">'ROLE_USER'</span><span class="p">);</span>
<span class="k">insert</span> <span class="k">into</span> <span class="n">users</span> <span class="p">(</span><span class="n">username</span><span class="p">,</span> <span class="n">password</span><span class="p">,</span> <span class="k">role</span><span class="p">)</span> <span class="k">values</span> <span class="p">(</span><span class="s1">'admin'</span><span class="p">,</span> <span class="s1">'$2a$10$r3fC/h15stMd/RkqSuNaPesFQaFJmg6Z7/x77vWoxsZCUmdbm0gt2'</span><span class="p">,</span> <span class="s1">'ROLE_ADMIN'</span><span class="p">);</span>
</code></pre></div></div>
<h2 id="saját-bejelentkezési-űrlap-létrehozása">Saját bejelentkezési űrlap létrehozása</h2>
<p>Saját űrlap létrehozásához Thymeleafet használtam. Ehhez kell egy <code class="language-plaintext highlighter-rouge">UserController</code> controller.</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">package</span> <span class="nn">jtechlog.jtechlogbootsecurity</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">lombok.AllArgsConstructor</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">lombok.extern.slf4j.Slf4j</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">org.springframework.security.core.annotation.AuthenticationPrincipal</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">org.springframework.stereotype.Controller</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">org.springframework.web.bind.annotation.GetMapping</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">org.springframework.web.bind.annotation.ModelAttribute</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">org.springframework.web.bind.annotation.PostMapping</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">org.springframework.web.servlet.ModelAndView</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">java.util.Map</span><span class="o">;</span>
<span class="nd">@Controller</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">UserController</span> <span class="o">{</span>
<span class="nd">@GetMapping</span><span class="o">(</span><span class="s">"/login"</span><span class="o">)</span>
<span class="kd">public</span> <span class="nc">ModelAndView</span> <span class="nf">login</span><span class="o">()</span> <span class="o">{</span>
<span class="k">return</span> <span class="k">new</span> <span class="nf">ModelAndView</span><span class="o">(</span><span class="s">"login"</span><span class="o">);</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>És a Thymeleaf template <code class="language-plaintext highlighter-rouge">login.html</code> néven.</p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp"><!DOCTYPE html></span>
<span class="nt"><html</span> <span class="na">xmlns:th=</span><span class="s">"http://www.thymeleaf.org"</span><span class="nt">></span>
<span class="nt"><body></span>
<span class="nt"><div</span> <span class="na">th:if=</span><span class="s">"${param.error}"</span><span class="nt">></span>
Sikertelen bejelentkezés
<span class="nt"></div></span>
<span class="nt"><div</span> <span class="na">th:if=</span><span class="s">"${param.logout}"</span><span class="nt">></span>
Sikeres kijelentkezés
<span class="nt"></div></span>
<span class="nt"><form</span> <span class="na">th:action=</span><span class="s">"@{login}"</span> <span class="na">method=</span><span class="s">"post"</span><span class="nt">></span>
<span class="nt"><input</span> <span class="na">type=</span><span class="s">"text"</span> <span class="na">name=</span><span class="s">"username"</span><span class="nt">/></span>
<span class="nt"><input</span> <span class="na">type=</span><span class="s">"password"</span> <span class="na">name=</span><span class="s">"password"</span><span class="nt">/></span>
<span class="nt"><input</span> <span class="na">type=</span><span class="s">"submit"</span> <span class="na">value=</span><span class="s">"Bejelentkezés"</span><span class="nt">/></span>
<span class="nt"></form></span>
<span class="nt"></body></span>
<span class="nt"></html></span>
</code></pre></div></div>
<p>A Thymeleafen kívül a <code class="language-plaintext highlighter-rouge">org.thymeleaf.extras:thymeleaf-extras-springsecurity6</code> függőségre is szükség van,
és ekkor az űrlapba automatikusan legenerálja egy
rejtett mezőben a CSRF tokent is.</p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt"><form</span> <span class="na">action=</span><span class="s">"login"</span> <span class="na">method=</span><span class="s">"post"</span><span class="nt">></span>
<span class="nt"><input</span> <span class="na">type=</span><span class="s">"hidden"</span> <span class="na">name=</span><span class="s">"_csrf"</span> <span class="na">value=</span><span class="s">"R2gwhxjM6tmpj-kCsdOwjy-52oT1Mjh35HfNrrGjEIQ4BzcDclpSsC74jOyEuI8x1f6EvRvf9-XFVwpa3UP8mIfAJOEOMA81"</span><span class="nt">/></span>
<span class="c"><!-- ... --></span>
<span class="nt"></form></span>
</code></pre></div></div>
<p>A CSRF támadási módot egy példával a legegyszerűbb leírni. Amennyiben egy webbankon
bejelentkezik egy felhasználó, majd átnavigál egy rosszindulatú lapra, ott létre lehet
hozni egy olyan űrlapot, mely a webbankra küld egy post metódusú kérést, pl. egy átutalást.
Erre megoldás a CSRF token, melyet a szerver állít elő minden űrlap lekérésekor, elhelyezni
az űrlapban egy rejtett mezőben, és az űrlap mindig vissza is küldi. Ezt támadó oldal
nem ismerheti, így visszaküldeni sem tudja, és így a szerver elutasítja.</p>
<h2 id="felhasználó-lekérése">Felhasználó lekérése</h2>
<p>Egy controller <code class="language-plaintext highlighter-rouge">@RequestMapping</code> annotációjával ellátott
metódusának paramétereként is definiálhatjuk a bejelentkezett felhasználót,
ellátva az <code class="language-plaintext highlighter-rouge">@AuthenticationPrincipal</code> annotációval, akkor a Spring
paraméterül átadja azt.</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@GetMapping</span><span class="o">(</span><span class="n">value</span> <span class="o">=</span> <span class="s">"/"</span><span class="o">)</span>
<span class="kd">public</span> <span class="nc">ModelAndView</span> <span class="nf">index</span><span class="o">(</span><span class="nd">@AuthenticationPrincipal</span> <span class="nc">User</span> <span class="n">user</span><span class="o">)</span> <span class="o">{</span>
<span class="n">log</span><span class="o">.</span><span class="na">debug</span><span class="o">(</span><span class="s">"Logged in user: {}"</span><span class="o">,</span> <span class="n">user</span><span class="o">);</span>
<span class="k">return</span> <span class="k">new</span> <span class="nf">ModelAndView</span><span class="o">(</span><span class="s">"index"</span><span class="o">,</span>
<span class="nc">Map</span><span class="o">.</span><span class="na">of</span><span class="o">(</span><span class="s">"users"</span><span class="o">,</span> <span class="n">userService</span><span class="o">.</span><span class="na">listUsers</span><span class="o">(),</span> <span class="s">"user"</span><span class="o">,</span> <span class="k">new</span> <span class="nc">User</span><span class="o">()));</span>
<span class="o">}</span>
</code></pre></div></div>
<p>A Java kódból a következőképpen kérhetjük le a bejelentkezés után
a felhasználót:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">user</span> <span class="o">=</span> <span class="o">(</span><span class="nc">User</span><span class="o">)</span> <span class="nc">SecurityContextHolder</span><span class="o">.</span><span class="na">getContext</span><span class="o">().</span><span class="na">getAuthentication</span><span class="o">().</span><span class="na">getPrincipal</span><span class="o">();</span>
</code></pre></div></div>
<p>A <code class="language-plaintext highlighter-rouge">Context</code> <code class="language-plaintext highlighter-rouge">ThreadLocal</code> változó, így szálanként egyedi. A metódus
visszatérési értékét kényszeríthetjük a saját <code class="language-plaintext highlighter-rouge">User</code> osztályunkra.</p>
<h2 id="használat-thymeleafben">Használat Thymeleafben</h2>
<p>Amennyiben használni akarjuk a Spring Security funkcióit Thymeleaf template-ben,
definiálnunk kell a névteret:</p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp"><!DOCTYPE html></span>
<span class="nt"><html</span> <span class="na">xmlns:th=</span><span class="s">"http://www.thymeleaf.org"</span>
<span class="na">xmlns:sec=</span><span class="s">"http://www.thymeleaf.org/thymeleaf-extras-springsecurity6"</span><span class="nt">></span>
</code></pre></div></div>
<p>A felhasználó nevének és különböző tulajdonságainak megjelenítésére a <code class="language-plaintext highlighter-rouge">authentication</code>
tag való:</p>
<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt"><span</span> <span class="na">sec:authentication=</span><span class="s">"name"</span><span class="nt">></span>Bob<span class="nt"></span></span>
</code></pre></div></div>
<p>Egy feltétel szerint megjeleníteni egy HTML részletet a következőképp lehet.
A <code class="language-plaintext highlighter-rouge">div</code> törzse csak akkor jelenik meg, ha a felhasználó belépett.</p>
<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt"><div</span> <span class="na">sec:authorize=</span><span class="s">"isAuthenticated()"</span><span class="nt">></span>
<span class="nt"></div></span>
</code></pre></div></div>
<p>A következő <code class="language-plaintext highlighter-rouge">div</code> törzse csak akkor jelenik meg, ha a felhasználó rendelkezik
<code class="language-plaintext highlighter-rouge">ROLE_ADMIN</code> jogosultsággal.</p>
<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt"><div</span> <span class="na">sec:authorize=</span><span class="s">"hasRole('ROLE_ADMIN')"</span><span class="nt">></span>
<span class="nt"></div></span>
</code></pre></div></div>
<h2 id="felhasználónév-megjegyzése">Felhasználónév megjegyzése</h2>
<p>A Spring Security sikertelen bejelentkezés esetén nem jegyzi meg a
felhasználónevet. Ezt nekünk kell implementálni.</p>
<p>Ennek megoldására írni kell egy
<code class="language-plaintext highlighter-rouge">SimpleUrlAuthenticationFailureHandler</code> leszármazottat, mely sikertelen
bejelentkezés esetén kerül meghívásra, és a felhasználónevet a sessionbe menti.
Utána a bejelentkező oldalon ezt ki kell venni.</p>
<p>A <code class="language-plaintext highlighter-rouge">UsernameInUrlAuthenticationFailureHandler</code> implementációja:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">class</span> <span class="nc">UsernameInUrlAuthenticationFailureHandler</span> <span class="kd">extends</span> <span class="nc">SimpleUrlAuthenticationFailureHandler</span> <span class="o">{</span>
<span class="kd">private</span> <span class="kd">static</span> <span class="kd">final</span> <span class="nc">String</span> <span class="no">LAST_USERNAME_KEY</span> <span class="o">=</span> <span class="s">"LAST_USERNAME"</span><span class="o">;</span>
<span class="kd">public</span> <span class="nf">UsernameInUrlAuthenticationFailureHandler</span><span class="o">()</span> <span class="o">{</span>
<span class="kd">super</span><span class="o">(</span><span class="s">"/login?error"</span><span class="o">);</span>
<span class="o">}</span>
<span class="nd">@Override</span>
<span class="kd">public</span> <span class="kt">void</span> <span class="nf">onAuthenticationFailure</span><span class="o">(</span>
<span class="nc">HttpServletRequest</span> <span class="n">request</span><span class="o">,</span> <span class="nc">HttpServletResponse</span> <span class="n">response</span><span class="o">,</span>
<span class="nc">AuthenticationException</span> <span class="n">exception</span><span class="o">)</span>
<span class="kd">throws</span> <span class="nc">IOException</span><span class="o">,</span> <span class="nc">ServletException</span> <span class="o">{</span>
<span class="kt">var</span> <span class="n">usernameParameter</span> <span class="o">=</span>
<span class="nc">UsernamePasswordAuthenticationFilter</span><span class="o">.</span><span class="na">SPRING_SECURITY_FORM_USERNAME_KEY</span><span class="o">;</span>
<span class="kt">var</span> <span class="n">lastUserName</span> <span class="o">=</span> <span class="n">request</span><span class="o">.</span><span class="na">getParameter</span><span class="o">(</span><span class="n">usernameParameter</span><span class="o">);</span>
<span class="kt">var</span> <span class="n">session</span> <span class="o">=</span> <span class="n">request</span><span class="o">.</span><span class="na">getSession</span><span class="o">(</span><span class="kc">false</span><span class="o">);</span>
<span class="k">if</span> <span class="o">(</span><span class="n">session</span> <span class="o">!=</span> <span class="kc">null</span> <span class="o">||</span> <span class="n">isAllowSessionCreation</span><span class="o">())</span> <span class="o">{</span>
<span class="n">request</span><span class="o">.</span><span class="na">getSession</span><span class="o">().</span><span class="na">setAttribute</span><span class="o">(</span><span class="no">LAST_USERNAME_KEY</span><span class="o">,</span> <span class="n">lastUserName</span><span class="o">);</span>
<span class="o">}</span>
<span class="kd">super</span><span class="o">.</span><span class="na">onAuthenticationFailure</span><span class="o">(</span><span class="n">request</span><span class="o">,</span> <span class="n">response</span><span class="o">,</span> <span class="n">exception</span><span class="o">);</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>Ezt természetesen beanként kell deklarálni, és beállítani a <code class="language-plaintext highlighter-rouge">configure(HttpSecurity http)</code>
metódusban:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Bean</span>
<span class="kd">public</span> <span class="nc">SecurityFilterChain</span> <span class="nf">filterChain</span><span class="o">(</span><span class="nc">HttpSecurity</span> <span class="n">http</span><span class="o">,</span> <span class="nc">UsernameInUrlAuthenticationFailureHandler</span> <span class="n">failureHandler</span><span class="o">)</span> <span class="kd">throws</span> <span class="nc">Exception</span> <span class="o">{</span>
<span class="n">http</span>
<span class="o">.</span><span class="na">authorizeHttpRequests</span><span class="o">()</span>
<span class="o">.</span><span class="na">requestMatchers</span><span class="o">(</span><span class="s">"/login"</span><span class="o">)</span>
<span class="o">.</span><span class="na">permitAll</span><span class="o">()</span>
<span class="o">.</span><span class="na">requestMatchers</span><span class="o">(</span><span class="s">"/"</span><span class="o">)</span>
<span class="c1">// ROLE_ prefixet auto hozzáfűzi</span>
<span class="o">.</span><span class="na">hasAnyRole</span><span class="o">(</span><span class="s">"USER"</span><span class="o">,</span> <span class="s">"ADMIN"</span><span class="o">)</span>
<span class="o">.</span><span class="na">and</span><span class="o">()</span>
<span class="o">.</span><span class="na">formLogin</span><span class="o">()</span>
<span class="o">.</span><span class="na">loginPage</span><span class="o">(</span><span class="s">"/login"</span><span class="o">)</span>
<span class="o">.</span><span class="na">failureHandler</span><span class="o">(</span><span class="n">failureHandler</span><span class="o">)</span>
<span class="o">.</span><span class="na">and</span><span class="o">()</span>
<span class="o">.</span><span class="na">logout</span><span class="o">();</span>
<span class="k">return</span> <span class="n">http</span><span class="o">.</span><span class="na">build</span><span class="o">();</span>
<span class="o">}</span>
<span class="nd">@Bean</span>
<span class="kd">public</span> <span class="nc">UsernameInUrlAuthenticationFailureHandler</span> <span class="nf">usernameInUrlAuthenticationFailureHandler</span><span class="o">()</span> <span class="o">{</span>
<span class="k">return</span> <span class="k">new</span> <span class="nf">UsernameInUrlAuthenticationFailureHandler</span><span class="o">();</span>
<span class="o">}</span>
</code></pre></div></div>
<p>Valamint a megváltozott form, az előző felhasználónevet a sessionből veszi ki:</p>
<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Felhasználónév: <span class="nt"><input</span> <span class="na">type=</span><span class="s">"text"</span> <span class="na">name=</span><span class="s">"username"</span> <span class="na">th:value=</span><span class="s">"${session.lastUsername}"</span><span class="nt">/></span>
</code></pre></div></div>
<h2 id="metódus-szintű-jogosultságkezelés">Metódus szintű jogosultságkezelés</h2>
<p>Ezen kívül a Spring Security képes arra is, hogy különböző metódusok
meghívása esetén is végezzen jogosultság ellenőrzést. Ezt deklaratív
módon, annotációval is meg lehet adni. Ekkor egyrészt deklarálni kell,
hogy metódus szintű hozzáférés ellenőrzést szeretnénk, ekkor a
<code class="language-plaintext highlighter-rouge">@EnableMethodSecurity</code> annotációt kell elhelyezni az <code class="language-plaintext highlighter-rouge">SecurityConfig</code>
osztályunkon:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Configuration</span>
<span class="nd">@EnableWebSecurity</span>
<span class="nd">@EnableMethodSecurity</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">SecurityConfig</span> <span class="o">{</span>
<span class="c1">// ...</span>
<span class="o">}</span>
</code></pre></div></div>
<p>Valamint használjuk a <code class="language-plaintext highlighter-rouge">@PreAuthorize</code> annotációt a védendő metóduson:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@PreAuthorize</span><span class="o">(</span><span class="s">"hasAuthority('ROLE_ADMIN')"</span><span class="o">)</span>
<span class="kd">public</span> <span class="kt">void</span> <span class="nf">addUser</span><span class="o">(</span><span class="nc">String</span> <span class="n">name</span><span class="o">,</span> <span class="nc">String</span> <span class="n">password</span><span class="o">,</span> <span class="nc">String</span> <span class="n">roles</span><span class="o">)</span> <span class="o">{</span>
<span class="c1">// ...</span>
<span class="o">}</span>
</code></pre></div></div>
<h2 id="tesztelés">Tesztelés</h2>
<p>Amennyiben az autentikációt is tesztelni akarjuk, a következőket használhatjuk.
Először kell a következő függőség.</p>
<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt"><dependency></span>
<span class="nt"><groupId></span>org.springframework.security<span class="nt"></groupId></span>
<span class="nt"><artifactId></span>spring-security-test<span class="nt"></artifactId></span>
<span class="nt"><scope></span>test<span class="nt"></scope></span>
<span class="nt"></dependency></span>
</code></pre></div></div>
<p>A <code class="language-plaintext highlighter-rouge">@WithMockUser</code> annotáció bejelentkeztet egy <code class="language-plaintext highlighter-rouge">user</code> felhasználót, <code class="language-plaintext highlighter-rouge">USER</code>
szerepkörrel.</p>
<p>Ezt természtesen paraméterezni is lehet, pl. <code class="language-plaintext highlighter-rouge">@WithMockUser(roles = {"ADMIN"})</code>.</p>
<p>Amennyiben azonban egy felhasználót a klasszikus módon szeretnénk bejelentkeztetni,
úgy, mintha az űrlapon történne a bejelentkezés, adjuk meg a felhasználónevét
a <code class="language-plaintext highlighter-rouge">@WithUserDetails</code> annotációval, pl. <code class="language-plaintext highlighter-rouge">@WithUserDetails("admin")</code>.</p>
<p>A MockMvc-nek is meg lehet adni a bejelentkezett felhasználót a következőképp:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">mockMvc</span><span class="o">.</span><span class="na">perform</span><span class="o">(</span><span class="n">post</span><span class="o">(</span><span class="s">"/"</span><span class="o">)</span>
<span class="o">.</span><span class="na">param</span><span class="o">(</span><span class="s">"username"</span><span class="o">,</span> <span class="s">"johndoe"</span><span class="o">)</span>
<span class="o">.</span><span class="na">param</span><span class="o">(</span><span class="s">"password"</span><span class="o">,</span> <span class="s">"johndoe"</span><span class="o">)</span>
<span class="o">.</span><span class="na">param</span><span class="o">(</span><span class="s">"role"</span><span class="o">,</span> <span class="s">"ROLE_USER"</span><span class="o">)</span>
<span class="o">.</span><span class="na">with</span><span class="o">(</span><span class="n">user</span><span class="o">(</span><span class="s">"admin"</span><span class="o">).</span><span class="na">roles</span><span class="o">(</span><span class="s">"ADMIN"</span><span class="o">))</span>
<span class="o">.</span><span class="na">with</span><span class="o">(</span><span class="n">csrf</span><span class="o">()))</span>
<span class="o">.</span><span class="na">andExpect</span><span class="o">(</span><span class="n">status</span><span class="o">().</span><span class="na">is3xxRedirection</span><span class="o">());</span>
</code></pre></div></div>
Modularizált alkalmazás fejlesztése a Spring Modulith-tal2022-12-19T09:00:00+00:00http://www.jtechlog.hu/2022/12/19/spring-modulith<h2 id="bevezetés">Bevezetés</h2>
<p>A microservice alkalmazások népszerűsége továbbra is töretlen. Miért is választják sokan ezt
az architektúrát? Egyik ok természetesen a hype factor, sokan szeretnék kipróbálni, valamint
hogy bekerüljön az önéletrajzukba. További ok, hogy sokan megcsömörlöttek a monolitikus
alkalmazásoktól, hiszen sok kötöttséggel járnak mind fejlesztés, mind üzemeltetési oldalról.
Ebből talán a legfontosabb, hogy gyakori jellemzője a spagetti kód, ennek következményeképp ha valahol
belenyúlunk az alkalmazásba, lehet, hogy másik helyen romlik el, ezért ha telepíteni akarunk,
ha biztosra akarunk menni, a teljes alkalmazást újra kéne tesztelnünk. Erre megoldás lehet
a microservice architektúra, ahol az alkalmazásunkat lazán kapcsolódó szolgáltatásokra
bontjuk fel. Ezzel kapcsolatban azonban a leggyakrabban elhangzó kérdés, hogy hol
a határ, hol vágjunk, mi alapján bontsuk szét az alkalmazásunkat szolgáltatásokra.
Monolitikus alkalmazásnál további kötöttségek a technológiai kötöttségek,
valamint hogy a teljes alkalmazást egyben lehet csak telepíteni.
És érdekes módon, csak ritkán szoktam azzal az indokkal találkozni, hogy azért
választották a microservice architektúrát, mert gond volt a skálázhatósággal,
aminek pedig nagy szerepe volt a kialakulásában.</p>
<p>Gyakran elfelejtjük, hogy egy monolitikus alkalmazásnak sem kéne szükségszerűen
egyben lennie, hanem azt is felépíthetjük lazán kapcsolt komponensekből.
A hiányzó láncszem itt a modul. A modularizált monolitikus alkalmazást
szokás modulith-nak nevezni. Ennek létét két okból is fontosnak tartom.
Egyrészt úgy vélem, hogy ahol nem tudnak modularizált alkalmazást fejleszteni,
ott nem érdemes a microservice architektúrával foglalkozni, ugyanis ezek
a technológiák nem mutatják meg, hogy hogy kell vágni. És a rossz vágásnak
az eredménye ugyanúgy spagetti lesz, de már meg lesz nehezítve az
elosztottságból adódó technológiai és üzemeltetési bonyodalmakkal is.
Többek által is jónak tartott út a microservice-ek felé, hogy először modularizáljuk
az alkalmazásunkat, majd utána emeljük ki a moduljainkat külön service-ekbe.
A másik ok, amit érdemes észben tartani, hogy már egyre több helyről
hallani, hogy a microservice architektúra nem vált be, nem váltotta be
az ígéreteket, a szervezet még nem állt készen (pl. agilis módszertanok, DevOps, CI/CD hiánya - igen, ezek
a microservice-ek előfeltételei), nem volt szükség skálázhatóságra, még rosszabb lett performanciában, stb.</p>
<p>Technológiailag a modulok azonban elég kevésbé támogatottak. Kezdeti próbálkozás volt az OSGi,
azonban komplexitása miatt nem terjedt el, pedig olyan igéretei vannak, mint a futás közbeni plugin telepítés,
valamint egy library-nek különböző verziói a classpath-on. Szabványos megoldást a
Java Platform Module System próbált adni a Java 9-ben, de annak ellenére, hogy már mikor
megjelent, szintén nem sikerült még elterjednie. A leggyakrabban használt megoldás a
build rendszer által biztosított modularizáció, gondoljunk itt a Maven multi module projectre.
Illetve a Gradle is azt hangoztatja, hogy multi module projektek kezelésében jobb és
gyorsabb, mint a Maven. Azonban ez is plusz komplexitással jár, különösen a build folyamat, a CI/CD terén.</p>
<p>A kézenfekvő megoldás a Java csomagok használata lenne, azonban ez sajnos túl kevés eszközt ad a kezünkbe,
a láthatósági módosítók csak nagyon szegényes hozzáférés szabályozást nyújtanak.
Ennek kiegészítésére jelent meg az <a href="https://spring.io/projects/spring-modulith">Spring Modulith</a> projekt, mely több jó megoldást is ad.
Nem hiszek feltétlenül abban, hogy ez az eszköz el fog terjedni, de a benne lévő
ötleteket érdemes ismerni, és akár a saját projekjeinkben is bevezetni.</p>
<!-- more -->
<h2 id="csomagok-és-az-archunit">Csomagok és az ArchUnit</h2>
<p>Az lenne megfelelő, ha nyelvi szinten meg lehetne mondani, hogy mely csomagokból csak
mely más csomagokat érhetőek el. Ekkor már használhatnánk a csomagokat a
modulok tárolására, és a modulok közötti függőségek szabályozására. Valamint
a moduljainkat rétegekbe rendezhetnénk, és itt is megadhatnánk, hogy mely
rétegből mely más rétegek használhatóak.</p>
<p>Pont erre találták ki a remek <a href="https://www.archunit.org/">ArchUnit</a> eszközt,
melynek használatával ezeket a függőségeket unit tesztben tudjuk leírni,
és ha valaki megtöri ezeket a szabályokat, a unit teszt hibára fut.</p>
<p>A következő kódrészleg például definiál három réteget.</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">layeredArchitecture</span><span class="o">()</span>
<span class="o">.</span><span class="na">consideringAllDependencies</span><span class="o">()</span>
<span class="o">.</span><span class="na">layer</span><span class="o">(</span><span class="s">"Controller"</span><span class="o">).</span><span class="na">definedBy</span><span class="o">(</span><span class="s">"..controller.."</span><span class="o">)</span>
<span class="o">.</span><span class="na">layer</span><span class="o">(</span><span class="s">"Service"</span><span class="o">).</span><span class="na">definedBy</span><span class="o">(</span><span class="s">"..service.."</span><span class="o">)</span>
<span class="o">.</span><span class="na">layer</span><span class="o">(</span><span class="s">"Persistence"</span><span class="o">).</span><span class="na">definedBy</span><span class="o">(</span><span class="s">"..persistence.."</span><span class="o">)</span>
<span class="o">.</span><span class="na">whereLayer</span><span class="o">(</span><span class="s">"Controller"</span><span class="o">).</span><span class="na">mayNotBeAccessedByAnyLayer</span><span class="o">()</span>
<span class="o">.</span><span class="na">whereLayer</span><span class="o">(</span><span class="s">"Service"</span><span class="o">).</span><span class="na">mayOnlyBeAccessedByLayers</span><span class="o">(</span><span class="s">"Controller"</span><span class="o">)</span>
<span class="o">.</span><span class="na">whereLayer</span><span class="o">(</span><span class="s">"Persistence"</span><span class="o">).</span><span class="na">mayOnlyBeAccessedByLayers</span><span class="o">(</span><span class="s">"Service"</span><span class="o">)</span>
</code></pre></div></div>
<p>Ha esetleg a <code class="language-plaintext highlighter-rouge">service</code> csomagban lévő osztályba injektálunk egy <code class="language-plaintext highlighter-rouge">controller</code>
csomagban lévő bármilyen komponenst, a unit teszt azonnal elszáll.</p>
<p>Ezt a gondolkodást viszi tovább a Spring Modulith.</p>
<h2 id="spring-modulith">Spring Modulith</h2>
<p>A Spring Modulith azt a gondolatot implementálja, hogy az alkalmazásunk
legyen egy monolitikus alkalmazás, egy Maven modulban, és ez legyen
csomagokra bontva, üzleti funkciók alapján (ezek a modulok),
és ez alatt legyenek a modulok rétegekre bontva.</p>
<p>Ezzel már nem lesz spagetti kódunk, de az az előnye is megmarad, hogy nem kapjuk meg a
microservice architektúra bonyolultságát. Később, ha erre szükség van,
kiszervezhetjük a moduljainkat külön service-ekbe, csak a lokális
metódushívásokat kell valamilyen más technológiára kicserélni.</p>
<p>Képzeljünk el egy alkalmazást, mely az alkalmazottakat, és a
hozzájuk tartozó szakértelmeket tárolja.</p>
<p>Már az elején modularizált alkalmazásban gondolkodjuk, a modulokat
a különböző üzleti területek alapján alkossuk meg.
Az <code class="language-plaintext highlighter-rouge">employees</code> modul tartja nyilván az alkalmazottakat, a címükkel,
míg a <code class="language-plaintext highlighter-rouge">skills</code> modul pedig azt, hogy milyen szakértelmek vannak,
és hogy az alkalmazottak milyen szakértelmekkel rendelkeznek.</p>
<p><img src="/artifacts/posts/images/mentoring-app.drawio.png" alt="Alkalmazás felépítése" /></p>
<p>A példa alkalmazás forráskódja megtalálható a <a href="https://github.com/vicziani/jtechlog-spring-modulith">GitHubon</a>.</p>
<p>A csomagszerkezet a következő legyen:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>mentioring-app/
├─ employees/
│ ├─ internal/
│ ├─ EmployeesFacade
├─ skills/
│ ├─ internal/
</code></pre></div></div>
<p>A legkülső csomagok adják a modulokat, név szerint az <code class="language-plaintext highlighter-rouge">employees</code> és a <code class="language-plaintext highlighter-rouge">skills</code>.
Azt a döntést hoztam, hogy a <code class="language-plaintext highlighter-rouge">skills</code> modulból lehet hívni az <code class="language-plaintext highlighter-rouge">employees</code> modult,
az egy alacsonyabb szintű modul. Az ˙internal˙ csomagban lévő osztályokra
nem lehet más csomagokból hivatkozni. Azaz a <code class="language-plaintext highlighter-rouge">skills</code> modul osztályai csak
az <code class="language-plaintext highlighter-rouge">EmployeesFacade</code> osztályra tudnak hivatkozni (pl. injektálni, hívni).</p>
<p>A teszteset:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">modules</span> <span class="o">=</span> <span class="nc">ApplicationModules</span><span class="o">.</span><span class="na">of</span><span class="o">(</span><span class="nc">MentoringAppApplication</span><span class="o">.</span><span class="na">class</span><span class="o">);</span>
<span class="n">modules</span><span class="o">.</span><span class="na">verify</span><span class="o">();</span>
</code></pre></div></div>
<p>Abban az esetben, ha körkörös hivatkozás alakulna ki, azaz pl. az <code class="language-plaintext highlighter-rouge">employees</code>
csomagból történne hivatkozás a <code class="language-plaintext highlighter-rouge">skills</code> csomagra, azonnal elbukna a teszteset.</p>
<p>Sőt, a következő kódrészlettel akár <a href="https://c4model.com/">C4 diagramot</a> is tudunk
generálni.</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">new</span> <span class="nc">Documenter</span><span class="o">(</span><span class="n">modules</span><span class="o">)</span>
<span class="o">.</span><span class="na">writeModulesAsPlantUml</span><span class="o">()</span>
<span class="o">.</span><span class="na">writeIndividualModulesAsPlantUml</span><span class="o">();</span>
</code></pre></div></div>
<p>Ez egy PlantUML diagramot állít elő (diagram as a code, meghatározott formátumú szövegből generál le diagramot).</p>
<p><img src="/artifacts/posts/images/spring-modulith-c4.png" alt="C4 diagram" /></p>
<p>A Spring Modulith ezen kívül lehetőséget biztosít arra is, hogy
tesztesetekben csak az egyik modul kerüljön betöltésre, vagy a modulok egy
bizonyos kombinációja.</p>
<p>A modulok belső felépítésére ugyanúgy ArchUnit szabályokat írhatunk.</p>
<h2 id="entitások-kezelése">Entitások kezelése</h2>
<p>JPA használata során megszoktuk, hogy az entitásaink kapcsolatban állnak egymással.
Ebben az esetben könnyen kialakulnak függőségek, sőt talán körkörös függőségek is.
Ezzel rendkívül komplexszé válik az alkalmazásunk, nagyon oda kell figyelni a lazy
betöltésekre, N+1 problémára, stb. Ezt mindenképp érdemes elkerülni, de hogyan?</p>
<p>Itt a DDD egy ötletét hívom segítségül, hogy az ORM kapcsolatokat csak
ún. bounded contexten belül használom, a bounded contexteken, és így a modulokon
átnyúló kapcsolatokat is csak azonosítókkal reprezentálom.</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Entity</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">EmployeeSkills</span> <span class="o">{</span>
<span class="nd">@Id</span>
<span class="kd">private</span> <span class="nc">Long</span> <span class="n">id</span><span class="o">;</span>
<span class="kd">private</span> <span class="nc">Long</span> <span class="n">employeeId</span><span class="o">;</span>
<span class="c1">// ...</span>
<span class="o">}</span>
</code></pre></div></div>
<p>Ezzel ugyan picit kényelmetlenebbé válhatnak a lekérdezések, azonban
sokkal jobban kontrollálni tudom mi kerül lekérdezésre, és
architektúrálisan is megfelelő lesz az alkalmazás.</p>
<h2 id="körkörös-függőségek">Körkörös függőségek</h2>
<p>Mi van akkor, ha előjön olyan igény, hogy az <code class="language-plaintext highlighter-rouge">employees</code> modulból is
hívni akarjuk a <code class="language-plaintext highlighter-rouge">skills</code> modult. Pl. ha egy alkalmazottat
törölni akarunk, akkor törölni kell a szakértelmeit is.
Erre több megoldásunk is lehet, itt vethetjük be a dependency
inversiont, azaz a függőségek irányának megfordítását. Ennek
egyik tervezési mintája az observer design pattern. Amit
a Spring eventekkel implementál. Sőt, ezt a Spring
Modulith tovább is gondolja, ugyanis képes a tranzakcionális
események használatára, mely eseményeket ráadásul adatbázisba is képes írni,
akár relációs adatbázisba JPA-val vagy JDBC-vel, akár MongoDB-be.</p>
<p>A kód törléskor az <code class="language-plaintext highlighter-rouge">employee</code> modulban:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Service</span>
<span class="nd">@AllArgsConstructor</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">EmployeeService</span> <span class="o">{</span>
<span class="c1">// ...</span>
<span class="kd">private</span> <span class="nc">ApplicationEventPublisher</span> <span class="n">publisher</span><span class="o">;</span>
<span class="nd">@Transactional</span>
<span class="kd">public</span> <span class="kt">void</span> <span class="nf">deleteEmployee</span><span class="o">(</span><span class="kt">long</span> <span class="n">id</span><span class="o">)</span> <span class="o">{</span>
<span class="nc">Employee</span> <span class="n">employee</span> <span class="o">=</span> <span class="n">employeeRepository</span><span class="o">.</span><span class="na">findByIdWithAddresses</span><span class="o">(</span><span class="n">id</span><span class="o">)</span>
<span class="o">.</span><span class="na">orElseThrow</span><span class="o">(()</span> <span class="o">-></span> <span class="k">new</span> <span class="nc">NotFoundException</span><span class="o">(</span><span class="s">"Employee not found with id: "</span> <span class="o">+</span> <span class="n">id</span><span class="o">));</span>
<span class="n">employeeRepository</span><span class="o">.</span><span class="na">delete</span><span class="o">(</span><span class="n">employee</span><span class="o">);</span>
<span class="n">publisher</span><span class="o">.</span><span class="na">publishEvent</span><span class="o">(</span><span class="k">new</span> <span class="nc">EmployeeHasDeletedEvent</span><span class="o">(</span><span class="n">id</span><span class="o">));</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>Ennek lekezelése a <code class="language-plaintext highlighter-rouge">skills</code> oldalon:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Service</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">SkillsService</span> <span class="o">{</span>
<span class="nd">@Async</span>
<span class="nd">@TransactionalEventListener</span>
<span class="kd">public</span> <span class="kt">void</span> <span class="nf">handleEmployeeHasDeletedEvent</span><span class="o">(</span><span class="nc">EmployeeHasDeletedEvent</span> <span class="n">event</span><span class="o">)</span> <span class="o">{</span>
<span class="kt">var</span> <span class="n">employeeSkills</span> <span class="o">=</span> <span class="n">employeeSkillsRepository</span><span class="o">.</span><span class="na">findByEmployeeId</span><span class="o">(</span><span class="n">event</span><span class="o">.</span><span class="na">getEmployeeId</span><span class="o">());</span>
<span class="k">if</span> <span class="o">(</span><span class="n">employeeSkills</span><span class="o">.</span><span class="na">isPresent</span><span class="o">())</span> <span class="o">{</span>
<span class="n">employeeSkillsRepository</span><span class="o">.</span><span class="na">delete</span><span class="o">(</span><span class="n">employeeSkills</span><span class="o">.</span><span class="na">get</span><span class="o">());</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<h2 id="tracing">Tracing</h2>
<p>A Spring Modulith azt is biztosítja, hogy a tracing eszközök (pl. Zipkin) számára azt is
elküldi, hogy melyik hívás melyik modulban történt. Így az ábrán is látható
módon nyomon követhető, hogy a <code class="language-plaintext highlighter-rouge">skills</code> modul áthív az <code class="language-plaintext highlighter-rouge">employees</code>
modulba, az kiad egy SQL lekérdezést, majd önmaga is kiad négy SQL lekérdezést.</p>
<p><a href="/artifacts/posts/images/spring-modulith-tracing.jpg" data-lightbox="post-images"><img src="/artifacts/posts/images/spring-modulith-tracing_750.jpg" alt="Spring Boot indulás" /></a></p>
Perzisztens réteg technológiák és a MyBatis2022-10-06T08:00:00+00:00http://www.jtechlog.hu/2022/10/06/mybatis<h2 id="bevezetés">Bevezetés</h2>
<p>Amikor adatbázist kell választani, főleg a relációs adatbázisok és az SQL kerülnek szóba.
Az ezzel való kapcsolattartásra az alkalmazás oldalon főleg a JPA szabványt, és
annak valamelyik implementációját, pl. a Hibernate-et használjuk.</p>
<p>Régóta sokan ódzkodnak ettől a technológiától, különböző okok miatt.
Hamar lehet vele eredményeket elérni, azonban nagyon nehéz megérteni a mélységeit.
Ennek hiányában azonban az alkalmazásunk rosszul teljesíthet, az N + 1 probléma miatt
nagyon sok SQL utasítást adhat ki, és mivel a JPA implementáció ezeket generálja,
nem tudjuk finoman szabályozni. A JPA ezen kívül nehezebben alkalmazható egy
már meglévő, esetleg nem körültekintően megtervezett adatbázisra, sokkal inkább támogatja azt,
ha a Java osztályokból indulunk ki.</p>
<p>Ezért érdemes megnézni, hogy milyen alternatív technológiák vannak, és ezek
milyen jellemzőkkel rendelkeznek.</p>
<p>A technológia kiválasztása valamilyen szinten hat az alkalmazásunk architektúrájára is.
Többrétegű alkalmazás esetén a perzisztens réteg tartja a kapcsolatot az adatbázissal.
Itt különböző architektúrális mintákat használhatunk, ebből egy pár darab:</p>
<ul>
<li>Repository pattern, a Domain Driven Design (DDD) könyvből</li>
<li>Table és Row Data Gateway a Patterns of Enterprise Application Architecture (Martin Fowler) könyvből</li>
<li>Data Mapper ugyanonnan</li>
<li>Data Access Object (DAO) a Java EE tervezési minták közül</li>
</ul>
<p>Bizonyos neveket ráadásul bizonyos technológiák is használnak, ilyen pl. a repository,
melyet a Spring Framework is használ a perzisztens rétegének elnevezésére, amit
a Spring Data JPA is átvett.</p>
<p>Az eredeti tervem az volt, hogy ezek jelentését részletesen kifejtem, és összehasonlítom
őket. De rossz hírem van. Arra jöttem rá, hogy ezeket a fogalmak nincsenek jól
definiálva, nem összehasonlíthatóak, és <a href="https://stackoverflow.com/questions/804751/what-is-the-difference-between-the-data-mapper-table-data-gateway-gateway-da">mindenki másra használja ezeket</a>. És ezt
a különböző technológiák tovább bonyolítják, ugyanis saját komponenseik
elnevezésére használják ezeket a fogalmakat, helytelenül.</p>
<p>(Egy szemléletes példa erre, hogy a DDD szerint a repository egy olyan objektum, mely
az üzleti logika és az és az üzleti objektumok adatbázisból olvasásáért vagy oda írásáért felelős ún.
mapping réteg között helyezkedik el, és az üzleti objektumokat a kollekciókhoz hasonlóan kezeli. Ezen túl
komplex lekérdezési lehetőséget is biztosít úgy, hogy a lekérdezési feltételeket dinamikusan lehet
összeállítani. A Spring Data JPA repository-ja ezzel pont ellentétes, előre definiálnunk kell
a metódusokat, melyek csak egy jól meghatározott feltétellel hívhatóak meg.
Saját véleményem szerint a repository-nak a JPA Criteria Query API-ja sokkal jobban megfelel.)</p>
<p>Ezért úgy döntöttem, nem én fogok ezekben a fogalmakban rendet szabni, hanem inkább
egy tulajdonság rendszert állítok össze, ami alapján a perzisztens technológiák
osztályozhatóak. Ez legyen a következő:</p>
<ul>
<li>Szabvány-e vagy egyedi implementáció</li>
<li>Ingyenes-e</li>
<li>SQL lekérdező nyelvet kell használni, vagy saját nyelve van, amiből maga generál le SQL utasításokat</li>
<li>Van-e benne mapping, azaz az adatbázisból jövő adatokat automatikusan meg tudja-e feleltetni az objektumokkal</li>
<li>Elég-e interfészeket definiálni, amihez maga generálja ki az implementációt.</li>
<li>Képes-e metódusnév alapján implementációt generálni</li>
</ul>
<p>A mapping tipikusan reflectionnel működik, és rendelkezik alapkonfigurációval
(pl. az attribútum neve megegyezik az oszlopnévvel), de ez személyre is szabható.</p>
<p>Azt is eldöntöttem, hogy nem a különböző tervezési minták neveit fogom használni, hanem
a különböző technológiák elnevezési konvencióit.</p>
<p>Ezek alapján a JPA:</p>
<ul>
<li>Szabványos, a Java EE szabvány része, implementációi pl. a Hibernate vagy az EclipseLink</li>
<li>Mindkét elterjedt implementációja ingyenes</li>
<li>Saját nyelve van, a JPQL, vagy a Criteria Query API is használható</li>
<li>(Object-Relational Mapping) ORM eszköz, azaz megfelelteti az objektumokat az adatbázisból jövő adatokkal</li>
<li>Nem elég interfészeket definiálni</li>
<li>Ezért metódusnév alapján sem tud implementációt generálni</li>
</ul>
<!-- more -->
<h2 id="technológiák">Technológiák</h2>
<p>Azonban sok egyéb technológia is van, számomra azok különösen érdekesek, melyet a Spring Boot is támogat.
Ezek:</p>
<ul>
<li>Natív JDBC</li>
<li>Natív Hibernate</li>
<li>Natív JPA</li>
<li>Spring JdbcTemplate</li>
<li>Spring Data JDBC</li>
<li>Spring Data R2DBC</li>
<li>Spring Data JPA</li>
<li>MyBatis</li>
<li>JOOQ</li>
</ul>
<p>Java EE környezetben érdemes még megemlítenem az Apache DeltaSpike Data technológiát is, mely
a Spring Data JPA megfelelője Java EE környezetben.</p>
<p>A natív JDBC-t már semmiképp nem érdemes használni, hiszen rendkívül körülményes. A Spring
JdbcTemplate egy jó alternatíva, egy vékony réteg a JDBC fölé.</p>
<p>A Hibernate-et én már nem választanám magába, csak JPA-n keresztül.</p>
<p>A Spring Data egy projekt gyűjtemény, és a benne lévő projektek célja, hogy különböző
adatbázisokat és technológiákat egy egységes modell alapján lehessen kezelni.
Egyszerűen lehessen a mappinget elvégezni
és lehetőleg az interfész és a metódus nevek alapján is tudjon implementációt generálni.
Az R2DBC reaktív programozást tesz lehetővé a reaktív R2DBC adatbázis driver használatával.
A JPA-t önmagában nem választanám, kizárólag a Spring Data JPA-val.</p>
<p>A <a href="https://mybatis.org/mybatis-3/">MyBatis</a> egy egyedi implementáció, ami egy jó átmenet a JDBC és a JPA között, ugyanis a nyelve
még az SQL, azonban képes a mappingre.</p>
<p>Tud interfész implementációt generálni, sőt van egy <a href="https://baomidou.com/">MyBatis-Plus</a> kiegészítése, ami képes
metódusnevek alapján implementációt generálni. Van egy <a href="https://plugins.jetbrains.com/plugin/10119-mybatisx">MyBatisX</a> IDEA
plugin is, mely segít a fejlesztésben.</p>
<p>A MyBatis jól jöhet, ha egy már létező adatbázishoz akarunk kapcsolódni.</p>
<p>Elterjedt még a <a href="https://www.jooq.org/">JOOQ</a>, mely egy egyedi eszköz, van ingyenes és fizetős verziója is.
Segítségével az adatbázisból tudunk Java osztályokat generálni, majd fluent API-val SQL lekérdezéseket
megfogalmazni.</p>
<p>Nagy félreértés az, hogyha van egy perzisztens technológiánk, akkor mindenre azt kell használni. A Java EE
kifejezetten azt írja, hogy a JPA megfelelő kevés számú, de bonyolult objektumgráf kezelésére, és ha
nagytömegű adatot akarunk kezelni, akkor használjuk JDBC-t. A JOOQ sem a JPA helyére akar lépni, hanem
kiegészítő technológiaként arra az esetre, ha elértük a JPA határait.</p>
<h2 id="mybatis">MyBatis</h2>
<p>Ebben a posztban azonban a MyBatisról írok a továbbiakban. A MyBatisnak van Spring Boot illesztése, ez a
<a href="https://mybatis.org/spring-boot-starter/mybatis-spring-boot-autoconfigure/">MyBatis-Spring-Boot-Starter</a>
library. A MyBatis a http://start.spring.io címen is kiválasztható.</p>
<p>A példaprojekt elérhető a <a href="https://github.com/vicziani/jtechlog-mybatis">GitHubon</a>.</p>
<p>A MyBatist többféleképp is használhatjuk. Egyrészt használhatunk interfészeket, és
annotációkat a következő módon:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Mapper</span>
<span class="kd">public</span> <span class="kd">interface</span> <span class="nc">EmployeeMapper</span> <span class="o">{</span>
<span class="nd">@Insert</span><span class="o">(</span><span class="s">"insert into employees(id, name) values (seq_employees.nextval, #{name})"</span><span class="o">)</span>
<span class="nd">@Options</span><span class="o">(</span><span class="n">useGeneratedKeys</span> <span class="o">=</span> <span class="kc">true</span><span class="o">,</span> <span class="n">keyProperty</span><span class="o">=</span><span class="s">"id"</span><span class="o">)</span>
<span class="kt">void</span> <span class="nf">save</span><span class="o">(</span><span class="nc">Employee</span> <span class="n">employee</span><span class="o">);</span>
<span class="nd">@Select</span><span class="o">(</span><span class="s">"select id, name from employees where id = #{id}"</span><span class="o">)</span>
<span class="nc">Employee</span> <span class="nf">findById</span><span class="o">(</span><span class="kt">long</span> <span class="n">id</span><span class="o">);</span>
<span class="o">}</span>
</code></pre></div></div>
<p>Ezután már csak injektálnunk kell, és meghívni a metódusait. Látható, hogy az annotációk SQL
utasításokat tartalmaznak.</p>
<p>A másik megoldásban ún. mapper XML állományokat készítünk,
amiben leírjuk az adatbázis műveleteket, és ezekre hivatkozunk egy <code class="language-plaintext highlighter-rouge">SqlSession</code> példány használatakor.</p>
<p>Az <code class="language-plaintext highlighter-rouge">application.properties</code>-ben először meg kell mondani ezen állományok helyét:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>mybatis.mapper-locations=/mappers/employees.xml
</code></pre></div></div>
<p>Ekkor a mapper xml állományt az <code class="language-plaintext highlighter-rouge">src/main/resources/mappers/employees.xml</code> elérési útvonalon helyezzük el
a következő tartalommal:</p>
<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp"><?xml version="1.0" encoding="utf-8" ?></span>
<span class="cp"><!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd"></span>
<span class="nt"><mapper</span> <span class="na">namespace=</span><span class="s">"jtechlog.mybatis.EmployeeRepository"</span><span class="nt">></span>
<span class="nt"><insert</span> <span class="na">id=</span><span class="s">"saveEmployee"</span> <span class="na">useGeneratedKeys=</span><span class="s">"true"</span> <span class="na">keyProperty=</span><span class="s">"id"</span><span class="nt">></span>
insert into employees(id, name) values (seq_employees.nextval, #{name})
<span class="nt"></insert></span>
<span class="nt"><select</span> <span class="na">id=</span><span class="s">"findEmployeeById"</span> <span class="na">resultType=</span><span class="s">"jtechlog.mybatis.Employee"</span><span class="nt">></span>
select id, name from employees where id = #{id}
<span class="nt"></select></span>
<span class="nt"></mapper></span>
</code></pre></div></div>
<p>(Ezt az IDEA megfelelő pluginnal automatikusan tudja kódkiegészíteni.)</p>
<p>Majd hozzunk létre egy Spring repository-t:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Repository</span>
<span class="nd">@AllArgsConstructor</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">EmployeeRepository</span> <span class="o">{</span>
<span class="kd">private</span> <span class="nc">SqlSession</span> <span class="n">session</span><span class="o">;</span>
<span class="kd">public</span> <span class="kt">void</span> <span class="nf">save</span><span class="o">(</span><span class="nc">Employee</span> <span class="n">employee</span><span class="o">)</span> <span class="o">{</span>
<span class="n">session</span><span class="o">.</span><span class="na">insert</span><span class="o">(</span><span class="s">"saveEmployee"</span><span class="o">,</span> <span class="n">employee</span><span class="o">);</span>
<span class="o">}</span>
<span class="kd">public</span> <span class="nc">Employee</span> <span class="nf">findById</span><span class="o">(</span><span class="kt">long</span> <span class="n">id</span><span class="o">)</span> <span class="o">{</span>
<span class="k">return</span> <span class="n">session</span><span class="o">.</span><span class="na">selectOne</span><span class="o">(</span><span class="s">"findEmployeeById"</span><span class="o">,</span> <span class="n">id</span><span class="o">);</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<h2 id="mybatis-plus">MyBatis-Plus</h2>
<p>A MyBatis-Plus egyik hátránya, hogy a dokumentációja csak kínai nyelven elérhető.</p>
<p>A példaprojekt elérhető a <a href="https://github.com/vicziani/jtechlog-mybatisplus">GitHubon</a>.</p>
<p>Ehhez is van starter:</p>
<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt"><dependency></span>
<span class="nt"><groupId></span>com.baomidou<span class="nt"></groupId></span>
<span class="nt"><artifactId></span>mybatis-plus-boot-starter<span class="nt"></artifactId></span>
<span class="nt"><version></span>3.5.2<span class="nt"></version></span>
<span class="nt"></dependency></span>
</code></pre></div></div>
<p>Itt is egy interfészt kell írnunk, de öröklődnie kell a
<code class="language-plaintext highlighter-rouge">BaseMapper</code> interfészből, mely sok előregyártott metódust tartalmaz.</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Mapper</span>
<span class="kd">public</span> <span class="kd">interface</span> <span class="nc">EmployeeMapper</span> <span class="kd">extends</span> <span class="nc">BaseMapper</span><span class="o"><</span><span class="nc">Employee</span><span class="o">></span> <span class="o">{</span>
<span class="o">}</span>
</code></pre></div></div>
<p>Az <code class="language-plaintext highlighter-rouge">Employee</code> osztályon van pár magáért beszélő annotáció:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@TableName</span><span class="o">(</span><span class="s">"employees"</span><span class="o">)</span>
<span class="nd">@KeySequence</span><span class="o">(</span><span class="s">"seq_employees"</span><span class="o">)</span>
</code></pre></div></div>
<p>Ezután ezeket a metódusokat máris használhatjuk, pl. a tesztesetben:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">employee</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Employee</span><span class="o">(</span><span class="s">"John Doe"</span><span class="o">);</span>
<span class="n">employeeMapper</span><span class="o">.</span><span class="na">insert</span><span class="o">(</span><span class="n">employee</span><span class="o">);</span>
<span class="kt">var</span> <span class="n">employeeToSelect</span> <span class="o">=</span> <span class="n">employeeMapper</span><span class="o">.</span><span class="na">selectById</span><span class="o">(</span><span class="n">employee</span><span class="o">.</span><span class="na">getId</span><span class="o">());</span>
<span class="n">assertEquals</span><span class="o">(</span><span class="s">"John Doe"</span><span class="o">,</span> <span class="n">employeeToSelect</span><span class="o">.</span><span class="na">getName</span><span class="o">());</span>
</code></pre></div></div>
<p>Ezen kívül lehetőség van metódusokat névkonvenció szerint megadni,
amihez automatikusan fog implementációt gyártani a MyBatis-Plus.</p>
<p>A kigenerált SQL utasításokat a naplózás állításával tudjuk megnézni,
amihez az <code class="language-plaintext highlighter-rouge">application.properties</code>-ben a következő bejegyzést kell
tennünk:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>logging.level.jtechlog.mybatis.EmployeeMapper=debug
</code></pre></div></div>
Spring Boot 3 újdonságai2022-09-13T08:00:00+00:00http://www.jtechlog.hu/2022/09/13/spring-boot-3<p>Frissítés: 2023. október 3.</p>
<h2 id="bevezetés">Bevezetés</h2>
<p>Mivel a hétvégén megkaptam, hogy írjak már Javas cikkeket, így ebben a posztban
a Spring Boot 3 újdonságait veszem sorra. A Spring Boot 3-as sorozat már a Spring
Framework 6-os sorozatára építkezik, ennek újdonságait nem fogom külön tárgyalni.
A poszt megírásának a pillanatában a legfrissebb verzió a 3.1.4.</p>
<p>Az említendő változások a következő területeket érintik:</p>
<ul>
<li>Alapkövetelmény a Java 17</li>
<li>Jakarta EE 9 függőségek</li>
<li>Problem Details</li>
<li>Tracing</li>
<li>Natív futtatható fájl elkészítése</li>
</ul>
<!-- more -->
<h2 id="rfc-7807---problem-details">RFC 7807 - Problem Details</h2>
<p>Ami számomra a legrelevánsabb, hogy a Spring Boot 3 már támogatja a
<a href="https://datatracker.ietf.org/doc/html/rfc7807">RFC 7807 szabványt</a>, mely meghatározza, hogy hiba esetén milyen
formátumban kell a hibát jelezni.</p>
<p>REST webszolgáltatások esetén azt láthatjuk, hogy mindegyik API
másképp jelzi a hibát, erre próbál a szabvány valamilyen egységes
formátumot definiálni.</p>
<p>A Spring pl. a következő hibát adja ha az URL-ben szöveget adunk át ott, ahol számot vár.
Ez a Spring saját hibaformátuma, mely nem követi a szabványt.</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
</span><span class="nl">"timestamp"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2023-10-03T11:30:03.538+00:00"</span><span class="p">,</span><span class="w">
</span><span class="nl">"status"</span><span class="p">:</span><span class="w"> </span><span class="mi">400</span><span class="p">,</span><span class="w">
</span><span class="nl">"error"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Bad Request"</span><span class="p">,</span><span class="w">
</span><span class="nl">"path"</span><span class="p">:</span><span class="w"> </span><span class="s2">"/api/employees/foo"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
<p>Vannak erre külön libraryk, pl. a <a href="https://github.com/zalando/problem">Zalando Problem</a>,
és ennek Spring illesztése a <a href="https://github.com/zalando/problem-spring-web">Problems for Spring MVC and Spring WebFlux</a>.
Azonban a Spring Boot 3-as verziótól kezdve ezekre nincs szükség, ugyanis a Problem Details szabványt a Spring Boot beépítve támogatja.</p>
<p>A példaprojekt elérhető a <a href="https://github.com/vicziani/jtechlog-employees-sb3">GitHubon</a>. MariaDB adatbázist használ és REST-en CRUD műveleteket biztosít.</p>
<p>Abban az esetben, ha az <code class="language-plaintext highlighter-rouge">application.properties</code> állományban felvesszük a
<code class="language-plaintext highlighter-rouge">spring.mvc.problemdetails.enabled = true</code> értéket, akkor a következő
hibát kapjuk.</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
</span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"about:blank"</span><span class="p">,</span><span class="w">
</span><span class="nl">"title"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Bad Request"</span><span class="p">,</span><span class="w">
</span><span class="nl">"status"</span><span class="p">:</span><span class="w"> </span><span class="mi">400</span><span class="p">,</span><span class="w">
</span><span class="nl">"detail"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Failed to convert 'id' with value: 'foo'"</span><span class="p">,</span><span class="w">
</span><span class="nl">"instance"</span><span class="p">:</span><span class="w"> </span><span class="s2">"/api/employees/foo"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
<p>Itt a headerben a <code class="language-plaintext highlighter-rouge">Content-Type</code> értéke <code class="language-plaintext highlighter-rouge">application/problem+json</code>, így a válasz megfelel a szabványnak.
A Problem Details bekapcsoláskor a <code class="language-plaintext highlighter-rouge">ResponseEntityExceptionHandler</code> aktiválódik, mely több kivételt is kezel,
pl. a fent létrejött <code class="language-plaintext highlighter-rouge">MethodArgumentTypeMismatchException</code> kivételt. Amennyiben szeretnénk
ezt személyre szabni, vagy kiegészíteni, akkor létrehozhatunk egy leszármazottat.</p>
<p>Saját kivétel esetén is megadhatjuk, hogy mi legyen a törzs tartalma, ehhez a <code class="language-plaintext highlighter-rouge">ProblemDetail</code>
osztályt kell használni, hiszen ez reprezentálja a visszaadott hibát.
Ha egy <code class="language-plaintext highlighter-rouge">@RequestMapping</code> vagy <code class="language-plaintext highlighter-rouge">@ExceptionHandler</code> metódusból ezzel térünk vissza, máris a
megfelelő hibát kapjuk. Használható a <code class="language-plaintext highlighter-rouge">ErrorResponse</code> interfész is, mely a státuszkódot és a http fejléceket is
tartalmazza.</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@ControllerAdvice</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">EmployeesExceptionHandler</span> <span class="o">{</span>
<span class="nd">@ExceptionHandler</span>
<span class="kd">public</span> <span class="nc">ProblemDetail</span> <span class="nf">handle</span><span class="o">(</span><span class="nc">EmployeeNotFoundException</span> <span class="n">exception</span><span class="o">)</span> <span class="o">{</span>
<span class="k">return</span> <span class="nc">ProblemDetail</span><span class="o">.</span><span class="na">forStatusAndDetail</span><span class="o">(</span><span class="nc">HttpStatus</span><span class="o">.</span><span class="na">NOT_FOUND</span><span class="o">,</span> <span class="n">exception</span><span class="o">.</span><span class="na">getMessage</span><span class="o">());</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>Ekkor a visszakapott hiba a következő.</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
</span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"about:blank"</span><span class="p">,</span><span class="w">
</span><span class="nl">"title"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Not Found"</span><span class="p">,</span><span class="w">
</span><span class="nl">"status"</span><span class="p">:</span><span class="w"> </span><span class="mi">404</span><span class="p">,</span><span class="w">
</span><span class="nl">"detail"</span><span class="p">:</span><span class="w"> </span><span class="s2">"employee not found"</span><span class="p">,</span><span class="w">
</span><span class="nl">"instance"</span><span class="p">:</span><span class="w"> </span><span class="s2">"/api/employees/100"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
<p>Kivételek esetén le lehet származni a <code class="language-plaintext highlighter-rouge">ErrorResponseException</code> osztályból, azonban
én nem szeretem, ha az üzleti rétegben szereplő exceptionnek van REST-re hivatkozása.</p>
<p>A Bean Validation validációs hiba esetén a <code class="language-plaintext highlighter-rouge">MethodArgumentNotValidException</code> kivételt dobja,
mely implementálja a <code class="language-plaintext highlighter-rouge">ErrorResponse</code> interfészt, azonban nem mondja meg, hogy milyen mezőkkel
van probléma. Tehát valami hasonló hibát kapunk:</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
</span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"about:blank"</span><span class="p">,</span><span class="w">
</span><span class="nl">"title"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Bad Request"</span><span class="p">,</span><span class="w">
</span><span class="nl">"status"</span><span class="p">:</span><span class="w"> </span><span class="mi">400</span><span class="p">,</span><span class="w">
</span><span class="nl">"detail"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Invalid request content."</span><span class="p">,</span><span class="w">
</span><span class="nl">"instance"</span><span class="p">:</span><span class="w"> </span><span class="s2">"/api/employees"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
<p>Ezen a következő kóddal segíthetünk.</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Data</span>
<span class="nd">@AllArgsConstructor</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">Violation</span> <span class="o">{</span>
<span class="kd">private</span> <span class="nc">String</span> <span class="n">name</span><span class="o">;</span>
<span class="kd">private</span> <span class="nc">String</span> <span class="n">message</span><span class="o">;</span>
<span class="o">}</span>
</code></pre></div></div>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@ControllerAdvice</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">EmployeesExceptionHandler</span> <span class="o">{</span>
<span class="nd">@ExceptionHandler</span>
<span class="kd">public</span> <span class="nc">ProblemDetail</span> <span class="nf">handle</span><span class="o">(</span><span class="nc">MethodArgumentNotValidException</span> <span class="n">exception</span><span class="o">)</span> <span class="o">{</span>
<span class="nc">ProblemDetail</span> <span class="n">problemDetail</span> <span class="o">=</span> <span class="nc">ProblemDetail</span><span class="o">.</span><span class="na">forStatusAndDetail</span><span class="o">(</span><span class="nc">HttpStatus</span><span class="o">.</span><span class="na">BAD_REQUEST</span><span class="o">,</span> <span class="s">"Constraint Violation"</span><span class="o">);</span>
<span class="nc">List</span><span class="o"><</span><span class="nc">Violation</span><span class="o">></span> <span class="n">violations</span> <span class="o">=</span> <span class="n">exception</span><span class="o">.</span><span class="na">getBindingResult</span><span class="o">().</span><span class="na">getFieldErrors</span><span class="o">().</span><span class="na">stream</span><span class="o">()</span>
<span class="o">.</span><span class="na">map</span><span class="o">((</span><span class="nc">FieldError</span> <span class="n">fe</span><span class="o">)</span> <span class="o">-></span> <span class="k">new</span> <span class="nc">Violation</span><span class="o">(</span><span class="n">fe</span><span class="o">.</span><span class="na">getField</span><span class="o">(),</span> <span class="n">fe</span><span class="o">.</span><span class="na">getDefaultMessage</span><span class="o">()))</span>
<span class="o">.</span><span class="na">toList</span><span class="o">();</span>
<span class="n">problemDetail</span><span class="o">.</span><span class="na">setProperty</span><span class="o">(</span><span class="s">"violations"</span><span class="o">,</span> <span class="n">violations</span><span class="o">);</span>
<span class="k">return</span> <span class="n">problemDetail</span><span class="o">;</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>Azaz manuálisan konvertáljuk át a hibákat <code class="language-plaintext highlighter-rouge">List<Violation></code> példánnyá. Ekkor a következő
hibát kapjuk.</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
</span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"about:blank"</span><span class="p">,</span><span class="w">
</span><span class="nl">"title"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Bad Request"</span><span class="p">,</span><span class="w">
</span><span class="nl">"status"</span><span class="p">:</span><span class="w"> </span><span class="mi">400</span><span class="p">,</span><span class="w">
</span><span class="nl">"detail"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Constraint Violation"</span><span class="p">,</span><span class="w">
</span><span class="nl">"instance"</span><span class="p">:</span><span class="w"> </span><span class="s2">"/api/employees"</span><span class="p">,</span><span class="w">
</span><span class="nl">"violations"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="p">{</span><span class="w">
</span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"name"</span><span class="p">,</span><span class="w">
</span><span class="nl">"message"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Name can not be blank"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">]</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
<h2 id="java-17-és-jakarta-ee-9">Java 17 és Jakarta EE 9</h2>
<p>Itt túl sok érdekesség nincs, nyilvánvalóan kihaszálhatjuk az új nyelvi elemeket,
és a Java EE API-k újdonságait. Annyi változás van, hogy változnak a csomagnevek,
a példa alkalmazásban a következőket kellett pl. módosítani:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">jakarta.validation.Valid</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">jakarta.validation.constraints.NotBlank</span><span class="o">;</span>
<span class="kn">import</span> <span class="nn">jakarta.persistence.*</span><span class="o">;</span>
</code></pre></div></div>
<p>Azaz a Bean Validationt és a JPA-t érintette, de ide tartoznak a Servlet API
osztályai is (<code class="language-plaintext highlighter-rouge">javax.</code> csomagneveket kell <code class="language-plaintext highlighter-rouge">jakarta.</code> csomagnévre cserélni).</p>
<h2 id="tracing">Tracing</h2>
<p>A distributed tracingről már írtam <a href="/2021/10/04/mdc-trace.html">egy előző posztban</a>.
Spring Boot esetén a <a href="https://spring.io/projects/spring-cloud-sleuth">Spring Cloud Sleuth projektet</a>
kellett használni.</p>
<p>A Spring Boot 3-nak viszont már szerves része lesz. Eddig is használta a Micrometert,
de csak metrikák publikálásához. A Micrometer elrejtette a különböző metrikákat gyűjtő eszközök
közötti különbséget. Úgy is mondhatjuk, hogy a metrikáknak a Micrometer olyan,
mint az SLF4J a naplózó keretrendszernek. Hiszen támogat majdnem húsz metrikákat gyűjtő
eszközt, pl. Elastic, Graphite, Prometheus, stb.</p>
<p>Majd megjelent a <a href="https://micrometer.io/docs/tracing">Micrometer Tracing</a>.
A Micrometer Tracing a következő tracer library-kat támogatja: OpenZipkin Brave és OpenTelemetry.
Ezek kezelik az adatokat és küldik valamelyik exporter/reporter felé, ami pedig továbbküldi valamilyen
külső rendszernek.</p>
<p>Az exporter/reporter implementációk közül a következők vannak:</p>
<ul>
<li>Zipkin felé Brave-vel</li>
<li>OpenTelemetry által támogatott implementációk felé, a Zipkinnek ez is tud küldeni</li>
<li>Tanzu Observability by Wavefront felé</li>
</ul>
<p>A Spring Cloud Sleuth pedig meg fog szűnni, hiszen a magja átkerült a Micrometer Tracing projektbe, ezért
hosszabb távon már nem érdemes építeni rá.</p>
<p>Sőt megjelent a <a href="https://micrometer.io/docs/observation">Micrometer Observation</a> is. Itt az ötlet az,
hogy instrumentáljuk a kódot, és az így nyert adatok megjelenthetnek a metrikák és a trace-ek
között is.</p>
<p>Ráadásul már nagyon sok library-hez elkészültek ilyen instrumentációk, listájuk
<a href="https://micrometer.io/docs/observation#_existing_instrumentations">itt olvasható</a>. Kiemelném
a következő library-ket: JDBC, JMS, Resilience4j, Spring MVC, Spring Security, Spring Kafka, CXF, gRPC, stb.</p>
<p>A példaprojekt elérhető a <a href="https://github.com/vicziani/jtechlog-mmt">GitHubon</a>.</p>
<p>A példaprojektben Zipkint választottam, melyet a legegyszerűbb Dockerben elindítani.</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker run <span class="nt">-d</span> <span class="nt">-p</span> 9411:9411 <span class="nt">--name</span> zipkin openzipkin/zipkin
</code></pre></div></div>
<p>A projektben a Brave implementációt választottam, amihez a következő függőségeket kellett felvenni:</p>
<div class="language-groovy highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">implementation</span> <span class="s1">'org.springframework.boot:spring-boot-starter-actuator'</span>
<span class="n">implementation</span> <span class="s1">'io.micrometer:micrometer-tracing-bridge-otel'</span>
<span class="n">implementation</span> <span class="s1">'io.opentelemetry:opentelemetry-exporter-zipkin'</span>
</code></pre></div></div>
<p>Az <code class="language-plaintext highlighter-rouge">application.properties</code>-ben még kellett állítgatni:</p>
<div class="language-properties highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="py">spring.application.name</span><span class="p">=</span><span class="s">jtechlog-mmt</span>
<span class="py">management.tracing.enabled</span><span class="p">=</span><span class="s">true</span>
<span class="py">management.tracing.sampling.probability</span><span class="p">=</span><span class="s">1.0</span>
<span class="py">management.zipkin.tracing.connect-timeout</span><span class="p">=</span><span class="s">5s</span>
</code></pre></div></div>
<p>A <code class="language-plaintext highlighter-rouge">spring.application.name</code> a service neve lesz.
Az <code class="language-plaintext highlighter-rouge">management.tracing.enabled</code> property-vel a tracing kerül bekapcsolásra. A <code class="language-plaintext highlighter-rouge">management.tracing.sampling.probability</code> értékével megmondjuk, hogy minden
kérés legyen rögzítve, mert az alapbeállítás <code class="language-plaintext highlighter-rouge">0.1</code>, azaz minden tizedik. A <code class="language-plaintext highlighter-rouge">management.zipkin.tracing.connect-timeout</code> azért
kellett, mert néha timeoutolt a Zipkin kapcsolat, és ezért eldobott spaneket.</p>
<p>Egy span létrehozása a következő kódrészlettel történhet:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">return</span> <span class="nc">Observation</span><span class="o">.</span><span class="na">createNotStarted</span><span class="o">(</span><span class="s">"controller.hello"</span><span class="o">,</span> <span class="n">observationRegistry</span><span class="o">)</span>
<span class="o">.</span><span class="na">lowCardinalityKeyValue</span><span class="o">(</span><span class="s">"framework"</span><span class="o">,</span> <span class="s">"spring"</span><span class="o">)</span>
<span class="o">.</span><span class="na">observe</span><span class="o">(()</span> <span class="o">-></span> <span class="o">{</span>
<span class="k">return</span> <span class="n">helloService</span><span class="o">.</span><span class="na">hello</span><span class="o">();</span>
<span class="o">});</span>
</code></pre></div></div>
<p>Ez a következőképp fog kinézni a Zipkinben (feltételezve, hogy a service-ben is van egy <code class="language-plaintext highlighter-rouge">service.hello</code> span):</p>
<p><a href="/artifacts/posts/images/zipkin_sb.png" data-lightbox="post-images"><img src="/artifacts/posts/images/zipkin_sb_750.png" alt="Zipkin" /></a></p>
<p>A <code class="language-plaintext highlighter-rouge">spring.application.name</code> property-ben beállított név lett a service neve.</p>
<p>Látható, hogy a http kérés is egy külön span, és jó sok taggel rendelkezik, pl. az URL, a HTTP metódus, a HTTP státuszkód, stb.</p>
<p>A <code class="language-plaintext highlighter-rouge">controller.hello</code> lett a következő span neve.
A <code class="language-plaintext highlighter-rouge">lowCardinalityKeyValue()</code> metódussal olyan tageket lehet felvenni, melyek kevés értéket vehetnek fel.
Van egy <code class="language-plaintext highlighter-rouge">highCardinalityKeyValue()</code> párja is, ha az értékek sokfélék lehetnek.</p>
<p>Metóduson használható az <code class="language-plaintext highlighter-rouge">@Observed</code> annotáció is, mellyel mindezt deklaratív módon lehet megadni. Ehhez kell egy <code class="language-plaintext highlighter-rouge">ObservedAspect</code>
bean az application contextbe, és egy <code class="language-plaintext highlighter-rouge">org.springframework.boot:spring-boot-starter-aop</code> függőség.</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Bean</span>
<span class="nc">ObservedAspect</span> <span class="nf">observedAspect</span><span class="o">(</span><span class="nc">ObservationRegistry</span> <span class="n">observationRegistry</span><span class="o">)</span> <span class="o">{</span>
<span class="k">return</span> <span class="k">new</span> <span class="nf">ObservedAspect</span><span class="o">(</span><span class="n">observationRegistry</span><span class="o">);</span>
<span class="o">}</span>
</code></pre></div></div>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@GetMapping</span><span class="o">(</span><span class="s">"/"</span><span class="o">)</span>
<span class="nd">@Observed</span><span class="o">(</span><span class="n">name</span> <span class="o">=</span> <span class="s">"controller.hello"</span><span class="o">,</span> <span class="n">contextualName</span> <span class="o">=</span> <span class="s">"controller.hello"</span><span class="o">,</span> <span class="n">lowCardinalityKeyValues</span> <span class="o">=</span> <span class="o">{</span><span class="s">"framework"</span><span class="o">,</span> <span class="s">"spring"</span><span class="o">})</span>
<span class="kd">public</span> <span class="nc">String</span> <span class="nf">hello</span><span class="o">()</span> <span class="o">{</span>
<span class="k">return</span> <span class="n">helloService</span><span class="o">.</span><span class="na">hello</span><span class="o">();</span>
<span class="o">}</span>
</code></pre></div></div>
<p>A <code class="language-plaintext highlighter-rouge">contextualName</code> lesz a span neve.</p>
<p>Kapcsoljuk be az aktuátorokat az <code class="language-plaintext highlighter-rouge">application.properties</code> fájlban.</p>
<div class="language-properties highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="py">management.endpoints.web.exposure.include</span><span class="p">=</span><span class="s">*</span>
</code></pre></div></div>
<p>Ekkor a <code class="language-plaintext highlighter-rouge">http://localhost:8080/actuator/metrics/controller.hello</code> címen lekérdezhetjük az ide tartozó
metrikákat is.</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
</span><span class="nl">"name"</span><span class="p">:</span><span class="s2">"controller.hello"</span><span class="p">,</span><span class="w">
</span><span class="nl">"baseUnit"</span><span class="p">:</span><span class="s2">"seconds"</span><span class="p">,</span><span class="w">
</span><span class="nl">"measurements"</span><span class="p">:[</span><span class="w">
</span><span class="p">{</span><span class="w">
</span><span class="nl">"statistic"</span><span class="p">:</span><span class="s2">"COUNT"</span><span class="p">,</span><span class="w">
</span><span class="nl">"value"</span><span class="p">:</span><span class="mi">3</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="p">{</span><span class="w">
</span><span class="nl">"statistic"</span><span class="p">:</span><span class="s2">"TOTAL_TIME"</span><span class="p">,</span><span class="w">
</span><span class="nl">"value"</span><span class="p">:</span><span class="mf">0.0032814</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="p">{</span><span class="w">
</span><span class="nl">"statistic"</span><span class="p">:</span><span class="s2">"MAX"</span><span class="p">,</span><span class="w">
</span><span class="nl">"value"</span><span class="p">:</span><span class="mf">0.0019493</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"availableTags"</span><span class="p">:[</span><span class="w">
</span><span class="p">{</span><span class="w">
</span><span class="nl">"tag"</span><span class="p">:</span><span class="s2">"framework"</span><span class="p">,</span><span class="w">
</span><span class="nl">"values"</span><span class="p">:[</span><span class="w">
</span><span class="s2">"spring"</span><span class="w">
</span><span class="p">]</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="p">{</span><span class="w">
</span><span class="nl">"tag"</span><span class="p">:</span><span class="s2">"method"</span><span class="p">,</span><span class="w">
</span><span class="nl">"values"</span><span class="p">:[</span><span class="w">
</span><span class="s2">"hello"</span><span class="w">
</span><span class="p">]</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="p">{</span><span class="w">
</span><span class="nl">"tag"</span><span class="p">:</span><span class="s2">"error"</span><span class="p">,</span><span class="w">
</span><span class="nl">"values"</span><span class="p">:[</span><span class="w">
</span><span class="s2">"none"</span><span class="w">
</span><span class="p">]</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="p">{</span><span class="w">
</span><span class="nl">"tag"</span><span class="p">:</span><span class="s2">"class"</span><span class="p">,</span><span class="w">
</span><span class="nl">"values"</span><span class="p">:[</span><span class="w">
</span><span class="s2">"hello.HelloApplication"</span><span class="w">
</span><span class="p">]</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">]</span><span class="w">
</span></code></pre></div></div>
<p>Látható, hogy 3-szor hívtam meg.</p>
<p>A trace id és a span id értékét is meg lehet jeleníteni a logban. Ehhez az <code class="language-plaintext highlighter-rouge">application.properties</code>
fájlban kell felvenni a következőt:</p>
<div class="language-properties highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="py">logging.pattern.console</span><span class="p">=</span><span class="s">%d{HH:mm:ss} [%X{traceId}/%X{spanId}] %clr(%-5.5p{5}) %-40.40logger{40} %m%n</span>
</code></pre></div></div>
<h1 id="natív-futtatható-fájl">Natív futtatható fájl</h1>
<p>Úgy látszik, hogy a fejlesztők legtöbb erőforrását ennek a támogatása viszi el. Itt a GraalVM integrációról van szó.
Az elmúlt 20 év munkáját kell most átgondolni, hiszen eddig a Java alkalmazásokkal kapcsolatban az volt az elvárás,
hogy azért, hogy gyorsan szolgálják ki a felhasználókat, nem baj, ha lassabban indulnak. Manapság a cloud és a lambda (serverless)
megoldások elterjedésével azonban a processznek gyorsan kéne elindulnia. Az viszont csak natív binárissal képzelhető el.</p>
<p>A Spring Framework és Spring Boot következő verziójában ez <a href="https://spring.io/blog/2021/12/09/new-aot-engine-brings-spring-native-to-the-next-level">kiemelten hangsúlyos</a>.</p>
<p>A fordítás a következő lépésekből áll:</p>
<ul>
<li>Forráskód fordítása</li>
<li>Ahead-Of-Time Engine, mely a bájtkód elemzésével előkészíti a natív fordítást</li>
<li>Natív fordítás</li>
</ul>
<p>Ez az AOT eddig külön plugin volt, most viszont bekerül a Spring Bootba.</p>
<p>A natív fordítást a <a href="https://bell-sw.com/liberica-native-image-kit/">Bellsoft Liberica Native Image Kit (NIK)</a>
végzi, mely a GraalVM-re és Liberica JDK-ra épít.</p>
<p>Ez történhet Docker konténerben a Cloud Native Buildpacks segítségével.</p>
<p>Ehhez a <code class="language-plaintext highlighter-rouge">build.gradle</code> fájlba a következő plugint kell felvenni:</p>
<div class="language-groovy highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">id</span> <span class="s1">'org.graalvm.buildtools.native'</span> <span class="n">version</span> <span class="s1">'0.9.27'</span>
</code></pre></div></div>
<p>Majd a következő parancs kiadásával előáll az image.</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gradlew bootBuildImage
</code></pre></div></div>
<p>Ez nekem több, mint 10 percig futott.</p>
<p>Utána Docker Compose-t használtam, hogy egy paranccsal lehessen
elindítani az adatbázis és az alkalmazás konténert, ráadásul úgy,
hogy egy hálózatban legyenek.</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">cd </span>employees
docker compose up
</code></pre></div></div>
<p>Az alkalmazás indítási ideje 0,2 - 0,3 másodperc!</p>
<p>Lehetne natív futtatható állományt is előállítani, azonban ekkor
a telepíteni kell a gépre GraalVM disztribúciót. Linux és MacOS
esetén ez működhet az SDKMAN! eszközzel.</p>
<p>Windows esetén még a Visual Studio Build Tools és a Windows SDK
eszközöket is telepíteni kell.</p>
<h1 id="natív-image-ek-spring-boot-2-es-verzión-deprecated">Natív image-ek Spring Boot 2-es verzión (deprecated)</h1>
<p>A Spring Native aktuális stabil verziója (0.12.1) a
Spring Boot 2.7.1 verzióját támogatja.</p>
<p>A példaprojekt elérhető a <a href="https://github.com/vicziani/jtechlog-employees-sb2-native">GitHubon</a>.</p>
<p>Ehhez kellett a Spring Native függőség:</p>
<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt"><dependency></span>
<span class="nt"><groupId></span>org.springframework.experimental<span class="nt"></groupId></span>
<span class="nt"><artifactId></span>spring-native<span class="nt"></artifactId></span>
<span class="nt"><version></span>0.12.1<span class="nt"></version></span>
<span class="nt"></dependency></span>
</code></pre></div></div>
<p>Valamint az AOT plugin.</p>
<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt"><plugin></span>
<span class="nt"><groupId></span>org.springframework.experimental<span class="nt"></groupId></span>
<span class="nt"><artifactId></span>spring-aot-maven-plugin<span class="nt"></artifactId></span>
<span class="nt"><version></span>0.12.1<span class="nt"></version></span>
<span class="nt"><executions></span>
<span class="nt"><execution></span>
<span class="nt"><id></span>generate<span class="nt"></id></span>
<span class="nt"><goals></span>
<span class="nt"><goal></span>generate<span class="nt"></goal></span>
<span class="nt"></goals></span>
<span class="nt"></execution></span>
<span class="nt"><execution></span>
<span class="nt"><id></span>test-generate<span class="nt"></id></span>
<span class="nt"><goals></span>
<span class="nt"><goal></span>test-generate<span class="nt"></goal></span>
<span class="nt"></goals></span>
<span class="nt"></execution></span>
<span class="nt"></executions></span>
<span class="nt"></plugin></span>
</code></pre></div></div>
<p>Valamint a <a href="https://paketo.io/">Paketo Buildpacks</a> konfiguráció.
Ez képes Docker image-t előállítani a forráskódból. Ezért szükséges, hogy
legyen Docker feltelepítve. Viszont mivel a build is Docker konténerben
történik (builder image/container), másra nincs szükség.</p>
<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt"><plugin></span>
<span class="nt"><groupId></span>org.springframework.boot<span class="nt"></groupId></span>
<span class="nt"><artifactId></span>spring-boot-maven-plugin<span class="nt"></artifactId></span>
<span class="nt"><configuration></span>
<span class="nt"><image></span>
<span class="nt"><builder></span>paketobuildpacks/builder:tiny<span class="nt"></builder></span>
<span class="nt"><env></span>
<span class="nt"><BP_NATIVE_IMAGE></span>true<span class="nt"></BP_NATIVE_IMAGE></span>
<span class="nt"></env></span>
<span class="nt"></image></span>
<span class="nt"></configuration></span>
<span class="nt"></plugin></span>
</code></pre></div></div>
<p>A Spring Native csak a Spring repo-jából tölthető le.</p>
<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt"><repositories></span>
<span class="nt"><repository></span>
<span class="nt"><id></span>spring-milestones<span class="nt"></id></span>
<span class="nt"><name></span>Spring Milestones<span class="nt"></name></span>
<span class="nt"><url></span>https://repo.spring.io/milestone<span class="nt"></url></span>
<span class="nt"><snapshots></span>
<span class="nt"><enabled></span>false<span class="nt"></enabled></span>
<span class="nt"></snapshots></span>
<span class="nt"></repository></span>
<span class="nt"><repository></span>
<span class="nt"><id></span>spring-release<span class="nt"></id></span>
<span class="nt"><name></span>Spring release<span class="nt"></name></span>
<span class="nt"><url></span>https://repo.spring.io/release<span class="nt"></url></span>
<span class="nt"><snapshots></span>
<span class="nt"><enabled></span>false<span class="nt"></enabled></span>
<span class="nt"></snapshots></span>
<span class="nt"></repository></span>
<span class="nt"></repositories></span>
<span class="nt"><pluginRepositories></span>
<span class="nt"><pluginRepository></span>
<span class="nt"><id></span>spring-release<span class="nt"></id></span>
<span class="nt"><name></span>Spring release<span class="nt"></name></span>
<span class="nt"><url></span>https://repo.spring.io/release<span class="nt"></url></span>
<span class="nt"></pluginRepository></span>
<span class="nt"><pluginRepository></span>
<span class="nt"><id></span>spring-milestones<span class="nt"></id></span>
<span class="nt"><name></span>Spring Milestones<span class="nt"></name></span>
<span class="nt"><url></span>https://repo.spring.io/milestone<span class="nt"></url></span>
<span class="nt"><snapshots></span>
<span class="nt"><enabled></span>false<span class="nt"></enabled></span>
<span class="nt"></snapshots></span>
<span class="nt"></pluginRepository></span>
<span class="nt"></pluginRepositories></span>
</code></pre></div></div>
<p>Eztán már csak a <code class="language-plaintext highlighter-rouge">mvn spring-boot:build-image</code> parancsot kell kiadni. Ez az én gépemen 14 percig futott. (Figyeljünk, hogy a 17-es JDK-t használjuk.)</p>
<p>Utána Docker Compose-t használtam, hogy egy paranccsal lehessen
elindítani az adatbázis és az alkalmazás konténert, ráadásul úgy,
hogy egy hálózatban legyenek.</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">cd </span>employees
docker compose up
</code></pre></div></div>
<p><a href="/artifacts/posts/images/spring-boot-native.png" data-lightbox="post-images"><img src="/artifacts/posts/images/spring-boot-native_750.png" alt="Spring Boot indulás" /></a></p>
Fejlesztőként mivel akadályozom a tesztelők munkáját?2022-08-20T08:00:00+00:00http://www.jtechlog.hu/2022/08/20/fejlesztok-es-tesztelok<h2 id="bevezetés">Bevezetés</h2>
<p>Frissítve: 2022. november 04-én, kiegészítve a <em>Rossz gyakorlat: Egy projekten már teszteltünk, nem vált be</em>,
a <em>Rossz gyakorlat: nem megfelelő üzleti fogalmakat használok</em>, a <em>Rossz gyakorlat: nem megfelelő branch-en történik a tesztelés</em>, és a <em>Rossz gyakorlat: nem bontom részproblémákra a problémát</em> fejezetekkel</p>
<p>Frissítve: 2022. augusztus 21-én, kiegészítve a <em>Naplózás fontossága</em> résszel, valamint a cache-re vonatkozó ajánlásokkal</p>
<p>Ahhoz, hogy sikeres szoftvert tudjunk szállítani, hiszem, hogy nagyon fontos a fejlesztők és
a tesztelők közötti szoros együttműködés. És mikor tesztelőket említek, ugyanúgy gondolok
a manuális és automata tesztelőkre is. Az irányzat, mely a fejlesztők és az üzemeltetők
közötti kapcsolat fontosságát hangsúlyozza, a DevOps nevet kapta. Célja egy olyan kultúra
kialakítása, gyakorlatok és eszközök kiválasztása, ahol a fejlesztők és az üzemeltetők
közös munkával tudnak gyorsan, megbízhatóan alkalmazásokat és megoldásokat szállítani.
Ez a fogalom manapság igencsak felkapott, <em>de méltatlannak érzem, hogy a tesztelőkkel
való közös munka fontosságának kiemelése korántsem ennyire hangsúlyos</em>.</p>
<p>Történetileg kialakult, hogy a fejlesztés és az üzemeltetés a legtöbb cégnél elvált
egymástól, tisztán elválasztott szervezeti egységekben, sőt akár külön cégekben működtek,
melyek között a kommunikáció finoman szólva is döcögős volt. Sőt, úgy érződött, hogy a két csoportnak
eltérő a célja. Az üzemeltetők stabilitást, biztonságot, tervezhetőséget szerettek volna,
míg a fejlesztők mindig az örökös változásért, fejlődésért harcoltak. Észrevehetjük,
ugyanez megfigyelhető a tesztelőknél is, egyrészt a különválás, a nehézkes kommunikáció,
és a látszólag ellentétes cél. Van, hogy a fejlesztők által késznek minősített alkalmazást
a tesztelők “visszadobnak”. Kezdjük felismerni, hogy az üzemeltetés és a fejlesztés
szétválasztása káros, <em>de vajon így érezzük a tesztelők munkájával kapcsolatban is</em>?
Ráadásul az egyes agilis módszertanok, mint pl. a Scrum szerint a csapat felelős
a sprint végén a kész termék leszállításáért, és ebben olyan egyenrangú csapattagok vesznek részt,
akik persze rendelkeznek speciális ismeretekkel, pl. üzleti elemzés, fejlesztés, tesztelés vagy
üzemeltetés.</p>
<p>Voltam olyan helyen, ahol a fentebb bemutatott elszigeteltség jelen volt, és mindig dolgoztam ennek
csökkentésén. Voltan olyan projekten is, ahol ezen különböző ismeretekkel rendelkező
szakemberek egy csapatban dolgoztak. Régóta tartok fejlesztői tanfolyamokat, köztük
kezdő fejlesztői tanfolyamokat, amin egyre több tesztelővel találkozok, aki el akar mozdulni
az automata tesztelés irányába. Sőt részt vettem tesztelői bootcampek megszervezésében is, ahol nagyon sokat
tanultam a tesztelő kollégáimtól, és beleláthattam ennek a szakmának a szépségeibe is, és erősítették bennem a hitet,
hogy mennyire fontos az együttműködés. Sőt, automata teszt eszközökkel kapcsolatos képzéseket is tartok, amin szintén sok tesztelő vesz részt.</p>
<p>Ezen tapasztalataim alapján, és a tesztelők (akár szünetekben elmesélt) történeteit meghallgatva gyakran úgy látom,
hogy a fejlesztőknek a teszteléssel kapcsolatban rengeteg tévhit él a fejében, és rengeteg rossz gyakorlatot folytatnak. Ebben a posztban ezeket próbálom felsorolni, megcáfolni. Lesz szó általános elvekről, de pár helyen lemegyek technológiai szintre is. Egyes szám első személyben fogok írni, fejlesztő lévén, még akkor is,
ha magam nem így gondolkozom, így megpróbálok senkit sem megsérteni. Félreértés ne essék, nem ellentéteket szeretnék szítani, hanem megoldásokat kínálni.</p>
<!-- more -->
<h2 id="tévhit-tesztelni-bárki-tud">Tévhit: “Tesztelni bárki tud.”</h2>
<p>Fejlesztőként gondolhatom úgy, hogy a teszteléshez nincsen szükség speciális ismeretekre, alapvető IT
tudással már el lehet kezdeni a tesztelést. Ez nincs így, ugyanis a tesztelés nemcsak speciális szakmai ismereteket igényel,
hanem olyan emberi tulajdonságokat is, amikkel gyakran a fejlesztők nem rendelkeznek. Ha megnéztek egy tesztelői oktatás
tematikáját máris látható, hogy mennyi mindennel kéne tisztában lenni. Szemezgessünk belőle pár dolgot.</p>
<ul>
<li>Tesztelői ismeretek
<ul>
<li>Tesztelési alapelvek</li>
<li>Tesztfolyamat</li>
<li>Tesztmunkatermékek</li>
<li>Teszttípusok és tesztszintek</li>
<li>Teszttervezési módszertanok és azok alkalmazási területei</li>
<li>Specifikáció alapú tervezési technikák</li>
<li>Teszttervezési technikák, azok alkalmazhatóságai, előnyök, hátrányok. Fehér- és feketedoboz, tapasztalat alapú teszttechnikák. Ekvivalencia-partícionálás, határérték-elemzés, döntési tábla tesztelés, állapotátmenet, használatai eset tesztelés. Lefedettség.</li>
<li>Tesztmenedzsment</li>
<li>Tesztmenedzsment szoftverek</li>
</ul>
</li>
<li>IT ismeretek
<ul>
<li>Szoftverfejlesztési életciklusmodellek, szerepkörök</li>
<li>Operációs rendszerek, irodai szoftverek használata</li>
<li>Hálózati fogalmak</li>
<li>Architektúrák</li>
<li>Adatbáziskezelés, SQL</li>
<li>Verziókezelés</li>
<li>Alapfokú algoritmizálás</li>
<li>Alapvető szoftvertervezési módszertan ismeretek</li>
</ul>
</li>
</ul>
<p>Amennyiben automata tesztelésről beszélünk, akkor további ismeretek lehetnek szükségesek:</p>
<ul>
<li>Felületi technológiák (pl. HTML, CSS, JavaScript) ismerete.</li>
<li>Valamilyen programozási nyelv ismerete</li>
<li>API leíró technológiák</li>
<li>Felületi és API automata teszteszközök ismerete (Selenium, SoapUI, Postman, stb.)</li>
<li>Terheléses teszteszközök ismerete (JMeter, stb.)</li>
</ul>
<p>Amennyiben az emberi oldalt nézzük, gyakran szükséges tulajdonságok:</p>
<ul>
<li>Törekvés a minőségi munkára</li>
<li>Komplex problémák megértése</li>
<li>Folyamat szintű gondolkodás</li>
<li>Jó kommunikációs képesség</li>
<li>El kell fogadnia, hogy alapvetően más által elkészített munkát kell használnia, vizsgálnia, elemeznie és minősítenie</li>
<li>Monotonitástűrés</li>
<li>Kreativitás</li>
</ul>
<p>A tesztelés egy külön szakma. Saját elméleti és gyakorlati ismeretekkel, módszertanokkal, eszközökkel. Széleskörű
irodalommal, követendő trendekkel, meetupokkal, konferenciákkal, folyamatos tanulással.</p>
<p>Lehet, hogy te nem találkoztál ilyen tesztelővel. Én sokkal, sőt próbálunk ilyen szakembereket képezni.</p>
<h2 id="tévhit-tesztelni-én-is-tudok">Tévhit: “Tesztelni én is tudok.”</h2>
<p>Amint megírom az első tesztet, legyen akár manuális, akár automata teszt, fejlesztőként azt hiszem, hogy máris tudok
tesztelni. Ez nincs így. Ha egy projekten összehasonlítom az első tesztjeimet a fél év elteltével írt tesztjeimmel, nagyon nagy különbségeket fedezek fel. Pont ahogy a kódolási képességem is fejlődik. Minél több tesztet írok,
annál több tapasztalatom lesz. Hogyan kerüljem el az ismétlést, nem csak automata, hanem manuális tesztforgatókönyvek esetében is. Hogyan készítsem elő a tesztelendő rendszert, hogyan hozzam a megfelelő állapotba (inicializáljam az adatbázist). Hogyan írjam meg úgy a teszteseteket, hogy más is el tudja olvasni, sőt könnyen karban tudja tartani. Hogyan oldom meg, hogy a teszteket akár több manuális tesztelő párhuzamosan tudja futtatni, akár ugyanazon a rendszeren. Hogy futtatom az automata teszteket párhuzamosan, akár egy clusteren. Hogyan tudok gyorsan futó teszteket írni. Hogyan oldom meg, hogy a tesztek ne legyenek törékenyek, azaz az alkalmazás változtatásával ne kelljen a teszteket is állandóan változtatni. Hogyan kezelem az alkalmazások külső függőségeit, külső authentikációt, más rendszereket. Hogy írok olyan teszteket, melyből könnyen lokalizálni lehet a hibát.</p>
<h2 id="tévhit-nincs-szükség-tesztelőkre">Tévhit: “Nincs szükség tesztelőkre!”</h2>
<p>A legnagyobb tévhit, ha azt hiszem, hogy nincs szükség tesztelőkre. Fejlesztőként nem rendelkezem azzal a speciális
ismeretekkel és képességekkel, mellyel a tesztelők igen. Az időm nagy részét fejlesztéssel, az üzleti problémák megértésével,
különböző technológiák megismerésével és alkalmazásával töltöm. Ebbe nehezen fér bele az, hogy egy más szakmát is hasonló
mélységben megismerjek. Ha rosszul értem meg az üzleti problémát, hibás tesztesetet fogok írni. Amennyiben magam írom a saját funkciómra a tesztesetet, ugyanazt a hibát fogom véteni, amit a funkció fejlesztése közben. Persze ebben picit segíthet, ha más fejlesztő írja a tesztesetet, de ezt nagyon ritkán láttam működni.</p>
<p>Fejlesztőként a szoftvert általában úgy nézem, “mintha benne ülnék”. A tesztelő kívülről látja, ugyanúgy, mint a felhasználó.</p>
<p>Nagyot tévedhetek abban is, hogy nincs szükség manuális vagy automata tesztelésre. Egyiket sem lehet elhagyni. A gépek csinálják azt, amihez értenek, és az ember is. A gépek jók az ismétlésben, a monoton
feladatok újbóli, gyors, hiba nélküli elvégzésében. Az emberek jók a kreatív, gondolkodást igénylő feladatok
elvégzésében.</p>
<p>Ha el akarunk mozdulni a continuous integration vagy delivery felé, az automata tesztelés elengedhetetlen. Hiszen milyen más lépés tudná automatikusan biztosítani, az alkalmazás minőségét?</p>
<h2 id="rossz-gyakorlat-egy-projekten-már-teszteltünk-nem-vált-be">Rossz gyakorlat: Egy projekten már teszteltünk, nem vált be</h2>
<p>Gyakran hallottam ellenérvként azt is, hogy egy projekten már próbáltuk a tesztelés valamely ágát, pl. manuális tesztelést,
vagy automata felületi/API tesztelést, de nem váltotta be a hozzá fűzött reményeket, nem hozott ki annyi hibát, túl
nehézkes volt a tesztek karbantartása. Ebben az esetben nem az a megfelelő hozzáállás, hogy a teszteléstől fordulok el,
hanem meg kell vizsgálni, hogy a tesztelés során mit csináltam rosszul. Jó technikát alkalmaztam? Jó eszközt választottam?
Nem bonyolítottam túl? Betartottam a tesztpiramist? Jók voltak a tesztjeim?</p>
<h2 id="tévhit-legyen-a-tesztelők-egy-külön-szervezeti-egység-akár-külön-cég">Tévhit: “Legyen a tesztelők egy külön szervezeti egység, akár külön cég.”</h2>
<p>A teljes continuous delivery mozgatórugója, a gyors feedback. Tesztelők egyik alapszabálya, hogy minél hamarabb kiderül a hiba, annál kisebb a kijavításának költsége. Ennek egy összetevője az is, hogy a fejlesztőknél is költséges a context switch, azaz ha valamin elkezdek dolgozni, akkor nehezen térek vissza egy korábbi fejlesztéshez, abban hibát javítani. Ráadásul fejlesztőként szomjazom a feedbackre, a visszajelzésre.</p>
<p>Erre példaként azt tudom felhozni, hogy amikor agilis Scrum csapatban dolgoztam, és végeztem a fejlesztéssel, csak szóltam
a mellettem ülő tesztelőnek, aki sokszor azonnal rá tudott nézni a funkcióra, és pár percen belül tudott egy gyors feedbacket adni, hogy alapvetően jó lesz-e, majd nem sokkal később részletesebben is át tudta nézni. Sőt akár odafordította a monitorát, és megmutatta, hogy mi a hiba. Ez összehasonlíthatatlan azzal, mikor hetek múlva, issue trackerben kapok egy hibajelzést.</p>
<p>Amennyiben a tesztelők külön szervezeti egységben, esetleg cégben dolgoznak, nőnek a kommunikációs problémák, így nő a hibajavítások költsége is.</p>
<h2 id="tévhit-elég-ha-a-tesztelők-a-kész-szoftvert-kapják-meg-tesztelésre">Tévhit: “Elég, ha a tesztelők a kész szoftvert kapják meg tesztelésre.”</h2>
<p>Amennyiben a tesztelő csak az elkészült funkcionalitást kapja meg tesztelésre, nagyon sokat veszítünk. A tesztelőt már az igényfelmérési, tervezési folyamatba is be kell vonni. Ő már az ügyféllel is másképp (gyakran jobban) kommunikál, olyanokat kérdez meg, amik eszembe sem jutnak. Nem egyszer azt vettem észre, hogy a fejlesztett szoftvert legjobban a tesztelő ismeri. Fejlesztőként általában nem használom annyit, a szoftvert csak belülről látom, és gyakran csak egyik-másik funkcióját ismerem a mélységeiben. Az üzleti elemzők hajlamosak arra, hogy ismerik ugyan az üzleti követelményeket, de a szoftver aktuális állapotát már nem látják át. A tesztelő minden idejét a szoftver használatával tölti.</p>
<p>Érdekes, hogy kezd elterjedni erre elterjedni a “shift-left” kifejezés, ami azt jelenti, hogy a tesztelőt minél hamarabb vonjuk be a munkafolyamatba. Ezt tesztelőktől évtizedek óta hallom, mégis csak most kezd el terjedni menedzsment körökben. Mennyit is ér egy hangzatos név!</p>
<p>A tesztelő már az üzleti követelményeket is másképp elemzi. Már arra gondol, hogy hogyan lehet tesztelni. A részletes tervezés során előfordult, hogy a jól felépített technológiai megoldásomat a tesztelő egy jól irányzott kérdésel azonnal romokba döntötte, ami általában úgy kezdődött, hogy “és arra gondoltál, hogy mi van akkor, ha?”.</p>
<p>Amennyiben a sztori leírásában már a tesztelővel együtt definiáljuk az elfogadási kritériumokat, akkor sokkal jobb minőségű szoftvert tudunk gyártani. Egyesek ezt a csúcsra járatták, hiszen a BDD-ben (Behavior-driven development) a három amigó (üzleti elemző, fejlesztő és tesztelő) formálisan, ezzel automatizáltan futtathatóan definiálják az elfogadási kritériumokat. Ilyenkor a TDD (Test driven development) elveit követve előbb a teszteket írják meg, ráadásul mindenki által érthető nyelven (domain-specific language - DSL).</p>
<p>A tesztelőt kihagyni a tervezésből hatalmas hiba. Hisz a fentieken kívül ez remek módja az információátadásnak, és a tesztelő technológiai irányba való továbbképzésének is.</p>
<p>Ha a tesztelést a végére hagyjuk, annak ismerjük a következményeit. A fejlesztés csúszik, a tesztelőknek alig marad idejük a sprint vagy a projekt végén, akkor kell megtervezniük, megírniuk a teszteseteket, futtatniuk, teszt adatokat legyártaniuk. Minél előbb bevonjuk őket, annál többet tudnak akár előre dolgozni.</p>
<h2 id="rossz-gyakorlat-nem-bontom-részproblémákra-a-problémát">Rossz gyakorlat: nem bontom részproblémákra a problémát</h2>
<p>Meglepődve tapasztaltam, hogy még tapasztalt fejlesztőknél is sokszor nehezen megy a dekompozíció, azaz
egy probléma részproblémákra bontása. Nem egyszer hallottam egy ügyféligény elemzése során a fejlesztőktől, hogy ez egy több hetes feladat, nem lehet részekre bontani, mert önmagában nem értelmes, egyben kell kifejleszteni.</p>
<p>Az rendkívül rizikós minden szempontból. Egyrészt a tesztelő ilyenkor gyakran nem szokott közbülső fejlesztéseket megkapni, hanem a végén, egyszerre kapja meg az egészet. Ez gyakran túl komplex átlátni, és idő sincs a teljeskörű tesztelésre.
Másrészt az ügyfél is későn kapja meg, és tud visszajelzést adni. Lehet, hogy már akkor, mikor rossz irányba mentünk el a fejlesztéssel.</p>
<p>Azaz a feladatokat apróbb feladatokra, lehetőleg pár napos fejlesztésekre kell lebontani (scrum esetén óvakodjunk a 8 vagy annál nagyobb storypont értékkel rendelkező sztoriktól), úgy, hogy ezek tesztelhetőek is legyenek. Igen, lehet, hogy ezzel némi
pluszmunkát vállalunk, mert bizonyos funkciókat mockolnunk kell, azonban egyenletesebben tudjuk tesztelni a funkciókat és jobban tudjuk lokalizálni a hibákat, előbb kapunk visszajelzést.</p>
<h2 id="rossz-gyakorlat-nem-megfelelő-üzleti-fogalmakat-használok">Rossz gyakorlat: nem megfelelő üzleti fogalmakat használok</h2>
<p>A pontos kommunikációnak az egyik alapfeltétele, hogy azonos nyelvet beszéljünk, ugyanazokat az üzleti fogalmakat használjuk.
Gyakran előfordul, hogy nem jól használjuk az üzleti fogalmakat, vagy mást értünk alattuk. A Domain-Driven Design
alapeleme a mindenütt jelenlévő nyelv (ubiquitous language), mely a közös nyelv, mely elemeit használjuk
a követelmények, később az üzleti modellünk leírására. Gyakran egy egyszerű fogalomszótár csodákat képes tenni,
hogy ugyanazokat a szavakat használjuk, és ugyanazt értsük alatta.</p>
<p>Ennek egy speciális, hazánkban gyakran előforduló fajtája, amikor az üzleti fogalmakat nem megfelelően
fordítjuk angolra, esetlen tükörfordítást alkalmazunk, vagy a speciálisan magyar üzleti fogalomnak nincs is angol
megfelelője (pl. jogi, eljárásbeli fogalmak). Itt is érdemes egy közös használatú szótárt bevezetni.</p>
<h2 id="rossz-gyakorlat-nem-definiálom-az-elfogadási-kritériumokat">Rossz gyakorlat: Nem definiálom az elfogadási kritériumokat.</h2>
<p>Bár a Scrum nem definiálja az elfogadási kritérium (acceptance criteria) fogalmát, érdemes megírni a user story-k esetén. Ráadásul a tesztelővel együtt. Ezek story-nként azok a feltételek, amelyek, ha teljesülnek, a story elfogadható. Fejlesztőként elkövettem azt a hibát, hogy a story-ban azt írtam le, hogy mi az üzleti követelmény, vagy mit kell tenni a fejlesztőnek, mit kell módosítani az alkalmazáson. Sok félreértést előzhetünk meg, ha ezeket pontosan, az üzleti elemző és tesztelő szemszögéből próbáljuk meg közösen definiálni. Ez nem azonos a Definition of Done-nal (DoD). Ez utóbbi ugyanis az összes story-ra vonatkozó általános követelményeket tartalmazza, pl. lett-e a kód review-zva, megvan-e a tesztlefedettség, megírtuk-e hozzá a súgót.</p>
<h2 id="rossz-gyakorlat-nem-veszem-figyelembe-a-tesztpiramist">Rossz gyakorlat: Nem veszem figyelembe a tesztpiramist.</h2>
<p>A tesztpiramist Mike Cohn mutatta be a <em>Succeeding with Agile</em> könyvében,
annak elképzelésére, hogyan helyezzük el a különböző szintjeit a tesztelésnek.</p>
<p>A legalsó szinten vannak a unit tesztek, melyek az adott programozási nyelv
legkisebb egységét tesztelik, objektumorientált nyelvek esetén ez az osztály
szint. Középső szinten az integrációs tesztek helyezkednek el, melyek már
az osztályok együttműködését tesztelik. Végül a legfelsőbb szint az
End-to-end tesztek, melyekkel a teljes alkalmazást teszteljük,
az adott környezetben, azok függőségeivel integrálva. Ráadásul nem egy-egy kiragadott
funkció darabkát, hanem teljes üzleti folyamatot az elejétől a végéig.</p>
<p><img src="/artifacts/posts/2022-08-20-fejlesztok-es-tesztelok/pyramid.png" alt="Tesztpiramis" /></p>
<p>A tesztpiramis formája abból következik, hogy az alaptól felfelé
a tesztek egyre nagyobb hatókörrel dolgoznak, egyre erőforrásigényesebb
a karbantartásuk és futtatásuk, és pont ezért felfelé mozdulva érdemes
ezekből egyre kevesebbet írni.</p>
<p>A unit és integrációs teszteket a fejlesztők írják, az end-to-end teszteket azonban
a tesztelők. Azonban láttam olyat is, hogy annyira kézreeső integrációs teszt
eszközt sikerült választani, amit a tesztelők is használni tudtak, ilyen
pl. REST webszolgáltatások tesztelésére a <a href="https://rest-assured.io/">REST-assured</a>.
Vagy olyat is, hogy a fejlesztők írtak E2E teszteket.</p>
<p>A gyakori hiba, amit véthetek az az, hogy kihagyok egy szintet. Akik csak a unit tesztekre
esküsznek, azok abban bíznak, hogyha a kis építőkockák hibátlanok, akkor ezek tökéletesen
fognak együttműködni. Ez nem igaz, az integrációt is ezer helyen lehet elrontani. Akik
nem szeretik a unit teszteket, azzal érvelnek, hogy a fejlesztő a funkcionalitás mellett
elrontja a unit teszteket is. Igaz, azonban a unit teszteknek nem ez az elsődleges feladatuk.
A unit tesztek megfogják azokat a hibákat, mikor jól értem az algoritmust, de elrontom.
A unit tesztek ráadásul a refactoring folyamat építőkockái. Hányszor hallom fejlesztőktől hogy refactoringoltak
egy funkciót, de nem írtak unit tesztet. Az nem refactoring. A refactoring célja a kód átstruktúrálása,
annak működésének változatlanul hagyásával. (Hogy később az új funkciót könnyebb legyen lefejleszteni.)
És a változatlanságot csak a unit tesztek biztosíthatják. Sajnos sokszor látom, hogy a struktúrális
változtatást, és az új funkció bevezetését hibásan egy lépésben hajtják végre a fejlesztők.
Valamint a téves hiedelemmel ellentétben a unit tesztek akár gyorsíthatják a fejlesztési folyamatot, hiszen
egy funkció teljeskörű kipróbálásához nem kell az alkalmazást elindítanom (kedvenc példám egy
validációs regexp egy eldugott képernyőn), a teszteset ezredmásodpercek alatt lefut.</p>
<p>Amennyiben a tesztelést külön szervezeti egység vagy cég végzi, nagyon gyakran meg szokták sérteni a tesztpiramist.
Ugyanis ekkor a tesztelők nincsenek tisztában azzal, hogy milyen unit és integrációs tesztek vannak, így mindenre
E2E teszteket írnak. Ez nagy költségpazarlás, hiszen az E2E tesztek a legtörékenyebbek és így a legköltségesebbek is.
E2E tesztekből ugyanis tipikusan kevésnek kell lennie, és a kritikus üzleti folyamatokra koncentráljanak.</p>
<p>Szóval igen, még a unit és integrációs tesztek tervezésébe is érdemes bevonni a tesztelőt. Egyrészt jó tanácsokkal tud szolgálni, a különböző tesztelési technikák egy tapasztalata alapján, valamint segítséget kap arra vonatkozóan, hogy mire érdemes E2E tesztet írni.</p>
<h2 id="rossz-gyakorlat-a-technológia-érdekes-az-üzleti-funkcionalitás-nem">Rossz gyakorlat: A technológia érdekes, az üzleti funkcionalitás nem.</h2>
<p>Többször írtam technológiai sztorikat, melyeknek nem volt üzleti vonzata, és úgy gondoltam, hogy tervezéséhez nem kell tesztelő, és tesztelni sem kell és lehet. Sokszor tévesen refactorignak nevezetem. Ilyenkor csak valamilyen technológiai módosítást végeztem a saját gyönyörűségemre. Ezekkel nagyokat hibáztam. Minden sztorinak kell üzleti vonzatának lennie. Úgy nem változtatok semmit, ami nem előfeltétele egy, akár később bevezetendő üzleti funkciónak. Amikor nem vontam be a tesztelőt, gyakran új hibákat vittem be a rendszerbe. Nagyon óvakodjunk a technológiai/refactoring sztoriktól, és ha lehet kerüljük őket.</p>
<h2 id="rossz-gyakorlat-tesztelési-eszköz-érdekes-a-tesztesetek-megírása-már-nem">Rossz gyakorlat: Tesztelési eszköz érdekes, a tesztesetek megírása már nem.</h2>
<p>Fejlesztőként rendkívül vonzó az, hogy kipróbálok egy új eszközt, legyen akár egy új teszteszköz. Ezt integrálom a projektbe, valamilyen szinten megismerem, de a tesztek írását már másra hagyom. Bevezetek pl. egy új BDD eszközt, legyen ez a Cucumber, melynek nyelve a Gherkin, megírok vele egy tesztet, majd elvesztem iránta az érdeklődésem, benn marad a projektben, anélkül, hogy ezt bárki tovább vinné. Előbb-utóbb a teszteket ignorálják, kikapcsolják.</p>
<p>A tesztelés során nem a teszteszköznek van értéke. Sőt, talán a legjobb stratégia, ha ettől függetlenedünk. (Ahogy a fejlesztésnél is a keretrendszertől, és az üzleti logikára koncentrálunk.) Az igazi tudomány a tesztesetek megtervezése és implementálása, ráadásul úgy, hogy a fentebb felsorolt követelményeknek megfeleljenek.</p>
<h2 id="rossz-gyakorlat-egy-tutorial-alapján-használom-a-teszteszközt">Rossz gyakorlat: Egy tutorial alapján használom a teszteszközt.</h2>
<p>Egy tutorial alapján egy új teszteszköz bevezetése triviális, egy képzett fejlesztő számára akár egy-két nap alatt megugorható. Azonban önáltatás, ha azt hiszem, hogy ez elegendő.</p>
<p>Sajnos nagyon sok hibás Selenium WebDriver bevezetést láttam. A következmény, hogy a tesztek lassúak, karbantarthatatlanok, törékenyek. És a fejlesztők végső véleménye az, hogy magával az eszközzel van a baj. A valóság azonban az, hogy tapasztalat nélkül, a szakirodalom elolvasását mellőzve vetették be. Gyakran még a weboldalon található dokumentációban szereplő <a href="https://www.selenium.dev/documentation/test_practices/encouraged/">Encouraged behaviors</a> és <a href="https://www.selenium.dev/documentation/test_practices/discouraged/">Discouraged behaviors</a> fejezetet sem olvassák el. Sok projektet láttam, melyekben az ajánlásokat nem tartják be, valamint nem veszik figyelembe az ellenjavallatokat. Hadd hozzak pár példát!</p>
<h3 id="tesztesetek-függetlensége">Tesztesetek függetlensége</h3>
<p>A teszteseteknek egymástól függetlennek kell lennie, nem lehet az egyik kimenete a másik bemenete. Nem lehet közöttük sorrendiség. Ezt betartva könnyebben azonosítható a hiba, másrészt párhuzamosan futtathatóak maradnak a tesztesetek.</p>
<h3 id="alkalmazás-állapotának-beállítása">Alkalmazás állapotának beállítása</h3>
<p>Kicsit az előzőből következik. Ha például az egyik tesztesetben egy felhasználót kell módosítanom, akkor hogyan kerül oda az a felhasználó. Egyik módszer, ha az előző tesztesetben létrehozott felhasználót módosítom. Ez hibás, hiszen a teszteseteknek függetleneknek kell lenniük. Másik megoldás, ha a felületen hozom létre a tesztesetben. Ezzel sajnos sok lesz a kódismétlés, és nagyon lassú lesz a lefutás. Akkor mit javasol a Selenium dokumentációja? Azt, hogy a felhasználót API-n keresztül hozzuk létre, ami egyrészt sokkal gyorsabb, másrészt sokkal kevésbé törékeny, mint a felhasználói felület. Ez lehet akár közvetlen adatbázis hozzáférésen keresztül, vagy SOAP/REST webszolgáltatással. Sajnos ehhez viszont sokszor nem férnek hozzá a tesztelők, vagy nincs kellően dokumentálva.</p>
<h3 id="megfelelő-selectorok-használata">Megfelelő selectorok használata</h3>
<p>A mai napig többször hallom, hogy a fejlesztők az XPath használatát javasolják a tesztelőknek. A dokumentáció azonban egyértelműen a CSS selectorok használatát javasolja, ugyanis mivel a böngészőnek egy natív technológiája, a böngészők fejlesztői a CSS selectorokat teljesítmény-hangolják, és gyorsabbak, mint az XPath lekérdezések. De backend fejlesztőként önszántamból miért is ajánlanám a CSS-t?</p>
<h3 id="mire-nem-jó-a-selenium">Mire nem jó a Selenium?</h3>
<ul>
<li>A Selenium WebDriverrel ne töltsünk fel és le fájlokat, helyette valami mást kliens library-t használjunk.</li>
<li>Ne ellenőrizzünk HTTP státuszkódokat, hiszen azt a felhasználó úgysem látja.</li>
<li>Ne akarjunk többfaktoros bejelentkezést tesztelni. A teszt környezetben az a legegyszerűbb, ha az autentikációt kikapcsoljuk. Sajnos ez nagyon sok alkalmazásban nem beállítható. Sok projektet láttam, hogy heteket töltöttek azzal, hogy egyáltalán be tudjanak tesztesetből jelentkezni.</li>
<li>A Selenium WebDriver nem jó terheléses tesztelésre, mert lassú.</li>
</ul>
<h2 id="tévhit-nekünk-speciális-igényeink-vannak">Tévhit: “Nekünk speciális igényeink vannak.”</h2>
<p>Fejlesztőként elkövethetem azt a hibát, hogy azt hiszem, hogy speciális igényeink vannak, és ezért kell különleges eszközt használnom, vagy egy adott eszközt máshogy használnom. Oktatóként sok céggel találkoztam, ahol a vezető fejlesztő elmondta, hogy nekik milyen speciális igényeik vannak, majd elsorolta olyanokat, melyek pontosan megegyeztek egy más cég speciális igényeivel.</p>
<p>A Convention over configuration több mint húsz éve ismert. Azaz inkább idomuljunk a konvenciókhoz, és ne akarjunk egyedi megoldásokat. Ha nekünk speciális igényeink vannak, akkor nagyon el kell gondolkodni azon, hogy miért, és nem csak “vélt” igényekről van-e szó. Sajnos sokszor azt látom, hogy ezek az igények ráadásul teljesen máshonnan jönnek, olyan helyről, ahol nincsnek igazából tisztában a napi rutinnal, ilyen pl. a management, vagy sokszor a IT biztonság felől.</p>
<h2 id="rossz-gyakorlat-tesztelési-keretrendszert-fejlesztek">Rossz gyakorlat: Tesztelési keretrendszert fejlesztek.</h2>
<p>A “Nekünk speciális igényeink vannak.” tévhitre adott egyik megoldás. A <a href="/2011/05/11/ne-fejlesszunk-sajat-keretrendszert.html">Miért ne fejlesszünk saját keretrendszert</a> posztomban már kifejtettem, hogy ez miért nem jó. Sajnos azt látom, hogy az automata tesztelés világában ez még mindig nagyon gyakori.</p>
<p>A tesztelők általában nem szeretik a mások által kifejlesztett, hibás, igényeiknek nem megfelelő, a konvenciókat nem betartó, black box-ként működő, általuk nem továbbfejleszthető keretrendszereket. Ezen tulajdonságok mindegyike csak kötöttséget ad. Fejlesztőként mi sem szeretjük a más által írt céges keretrendszereket, melyekben szerzett tudást máshol nem tudjuk hasznosítani. (Többször hallottam állásinterjún, hogy a jelölt fejlesztő kijelentette, hogyha saját céges keretrendszer van, akkor ahhoz a céghez nem megy dolgozni.) Azonban ezek köztünk vannak, szóval úgy látszik, írni viszont szeretjük őket.</p>
<h2 id="rossz-gyakorlat-nem-próbálom-ki-az-általam-fejlesztett-funkciót-a-tesztelő-úgyis-megteszi">Rossz gyakorlat: Nem próbálom ki az általam fejlesztett funkciót, a tesztelő úgyis megteszi.</h2>
<p>Klasszikus probléma, úgy adok át egy funkciót tesztelésére, hogy előtte nem próbáltam ki. Amikor a tesztelő az első kattintás után visszadobja, hogy nem működik, akkor megfogadom, hogy soha többet nem csinálok ilyet. Ugye ezt nem kell jobban kifejtenem, hogy ez milyen tiszteletlenség az irányukba, és mennyi pluszmunka? (Nála is context switch, release kitelepítésének ideje, tesztadatok előállítása, stb.)</p>
<h2 id="rossz-gyakorlat-nem-osztok-meg-kellő-információt-a-tesztelőkkel">Rossz gyakorlat: Nem osztok meg kellő információt a tesztelőkkel.</h2>
<p>A Selenium WebDriver dokumentációja azt írja, ha E2E tesztet akarok írni, akkor az alkalmazás állapotát lehetőleg API-n keresztül állítsam be. Ha ezt egyszerűen akarom megfogalmazni, ez gyakran azt jelenti, hogy fel kell tölteni az adatbázist tesztadatokkal.</p>
<p>A hiba, amit elkövethetek, hogy nem dokumentálom sem az adatbázisszerkezetet, sem az API-t, pl. a webszolgáltatásokat. Sokan az agilitást tévesen úgy értelmezik, hogy nem kell dokumentálni.</p>
<p>Hányszor láttam azt, hogy a tesztelők saját maguk térképezték fel az adatbázist, a felületet nyomkodva, és nézve, hogy mi is változik az adatbázisban. Hányszor láttam kódokat az adatbázisban, amihez nem volt magyarázat. (A Clean Code ellenjavalja a kódok használatát, hiszen az adatbázisok már vannak úgy hangolva, hogy hosszabb szövegeket is optimálisan tároljanak és indexeljenek.)
Már egy pár oldalas Entity Relationship Diagram, pár magyarázó szóval is rengeteget ér.</p>
<p>Ugyanígy már vannak technológiák, szabványok, formátumok az API dokumentálására is. SOAP esetén ott a WSDL, REST esetén ott az OpenAPI, sőt egy erre épülő, dokumentációt is megjeleníteni képes, a REST webszolgáltatások meghívását is biztosító eszköz, a <a href="https://swagger.io/">Swagger</a>. Az OpenAPI egy zseniális találmánya, hogy képes példa értékek tárolására is (<code class="language-plaintext highlighter-rouge">example</code>), ami remek tipp lehet a tesztelők számára, hogy milyen értékekkel töltsék fel a hiányzó adatokat.</p>
<h2 id="rossz-gyakorlat-nálam-működik">Rossz gyakorlat: Nálam működik!</h2>
<p>Ha a tesztelő megkeres egy hibával, nem lehet az az első dolgom, hogy megpróbálom lepattintani. Például azzal, hogy a böngésző cache-t törölted-e? Amennyiben egy új verzió kirakásánál a cache a tesztelőknél problémát okoz, akkor problémát fog okozni a felhasználóknál is. A http protokoll amúgy nagyon jó megoldásokat biztosít ennek finom szabályozására, melyeket ráadásul a keretrendszerek is támogatnak (pl. URL generálás, hash használata, cache headerök, ETag, stb).</p>
<p>Amennyiben hibát jelez, akkor nem az ő feladata annak kiderítése, hogy vajon miért is áll fenn a hiba. Nem küldöm vissza, hogy nézze meg ezt is, azt is, stb. Ezt már nekem kell végig debuggolnom.</p>
<p>A legegyszerűbb, ha hozzá tudok kapcsolódni az adott tesztelő tesztrendszeréhez, és ott tudom megnézni a problémát. Gyakran előfordul, hogy nálam épp nem lehet reprodukálni. (Ilyenkor figyeljünk arra, hogy ne dobjuk el véletlenül a tesztadatait - sajnos ez már velem megtörtént, azóta preferálom a tesztkörnyezetek mentését is.)</p>
<h2 id="rossz-gyakorlat-nehezen-állítható-elő-új-tesztkörnyezet">Rossz gyakorlat: Nehezen állítható elő új tesztkörnyezet.</h2>
<p>Amennyiben olyan alkalmazást fejlesztek, melyet nagyon nehéz feltelepíteni (láttam több, mint 20 oldalas telepítési leírást!), megnehezítem a tesztelők munkáját. Ekkor alakul ki az a gyakorlat, hogy csak limitált számú tesztkörnyezet van, amivel nagyon sok probléma szokott lenni:</p>
<ul>
<li>Nem egyértelmű, hogy ki a felelőse. Láttam, mikor egy demó vagy User Acceptance Test során szoftvert frissítettek.</li>
<li>Nem ismert állapotban van, nem tudni, hogy milyen verzió van kinn</li>
<li>Alacsony a rendelkezésre állása</li>
<li>Párhuzamos felhasználásból adódóan rengeteg probléma jelentkezhet. Láttam, hogy egy rendszeren terheléses tesztet futtattak, miközben funkcionális tesztet próbáltak rajta végezni.</li>
<li>Külső rendszerekkel való kapcsolat kérdéses. Láttam olyat, hogy az egyik rendszer teszt környezete rá volt kötve a másik rendszer éles környezetére.</li>
</ul>
<p>Itt szokott előjönni az a probléma, hogy milyen adatokkal töltsük fel a teszt környezetet. Erre egy gyakori példa, hogy az éles környezetről hozzuk át adatokat. Sajnos sokszor meg is reked a dolog, és a tesztelők éles adatokkal tesztelnek, mely nem biztos, hogy megfelel a biztonsági követelményeknek. Ekkor kell bevetni az anonimizálást, mely során konzisztens módon összekeverik az adatokat. Erre rendkívül jó eszközök vannak már.</p>
<p>A konténerizáció (Docker, Kubernetes, stb.), valamint az infrastructure as code lehetővé teszi, hogy akár egy paranccsal elindítsunk több szoftverkomponensből álló környezetet. Ezt mi odáig vittük el, hogy az üzletkötők laptopján egy paranccsal el tudtunk indítani egy környezetet, és akár Internet elérés nélkül is tudtak demózni.</p>
<p>Nagy segítség a tesztelők számára, ha saját alkalmazáspéldányt tudnak elindítani.</p>
<p>Ehhez lazán kapcsolódik, hogy az első munkahelyemen, az első héten azt tanították nekem, hogyha egy szoftverről egy kattintásra nem derül ki annak verziószáma, akkor ott komoly bajok vannak. Erre figyeljünk, hogy a szoftver verziószámát akár adatbázisból, akár felületen, akár API-n le lehessen kérdezni, sőt a logba is kerüljön be induláskor. (A legjobb, ha a Git commit hash-sel együtt.) Így a tesztelő pontosabb hibajelentést tud leadni, a pontos verzió megjelölésével.</p>
<h2 id="rossz-gyakorlat-nem-készítem-fel-az-alkalmazásom-hogy-tesztelhető-legyen">Rossz gyakorlat: Nem készítem fel az alkalmazásom, hogy tesztelhető legyen.</h2>
<p>Fejlesztőként régen én is azt az elvet vallottam, hogy az alkalmazásban nem lehet olyan kód, ami a teszteléssel kapcsolatos.
Azóta azonban a Clean Architecture könyv óta változott a véleményem, ugyanis a teszt eszközöket is az architektúra részének tekinti, ugyanúgy, mint az adatbázist, vagy a felhasználói felületet.</p>
<p>A fentieket figyelembe véve a következőkön kell elgondolkozni, és figyelembe venni a fejlesztés során:</p>
<ul>
<li>Dokumentáljuk az adatbázist</li>
<li>Dokumentáljuk az API-t</li>
<li>Autentikáció legyen kikapcsolható</li>
<li>Captcha legyen kikapcsolható</li>
<li>Zseniális ötlet, hogy minden egyes kéréshez rendeljünk egy azonosítót, mely azonosítót aztán a logban is megjelenítünk</li>
<li>Legyen az alkalmazás konténerizált, könnyen el lehessen indítani egy új példányt</li>
<li>Segítsünk az adatok anonimizálásában</li>
<li>Legyen könnyen lekérdezhető az alkalmazás verziószáma</li>
<li>A cache helyes alkalmazása bonyolult feladat, ahogy annak tesztelése is. Biztosítsunk lehetőséget arra, hogy a cache-t törölni lehessen, illetve tartalmát le lehessen kérdezni. Jó ötlet, ha a teszt környezetben jelöljük (pl. http headerben), hogy a válasz a cache-ből került kiszolgálásra.</li>
<li>Könnyítsük meg a felületi tesztelést: adjunk azonosítókat a felületi elemekhez. Ezzel nagymértékben megkönnyítjük a felületi E2E teszteket írók munkáját.</li>
</ul>
<h3 id="naplózás-fontossága">Naplózás fontossága</h3>
<p>Sokan a naplózást csak a hibakeresés egy eszközének tartják. Azonban nagyon hasznos lehet egy tesztelő számára is.</p>
<p>Az alkalmazás indulását egyértelműen jelezzük a naplóban, ahogy az alkalmazás verziószámát is írjuk ki! Informatív naplóüzeneteket használjunk! Az <code class="language-plaintext highlighter-rouge">error</code>, vagy <code class="language-plaintext highlighter-rouge">fatal</code> szintű naplóbejegyzéseket tényleg csak akkor írjunk ki, hogyha azzal valamit tennünk kell! Sokszor látom, hogy egy napló tele van hibákkal, mire a fejlesztő csak legyint, hogy az úgy van jól.</p>
<p>Naplózzuk ki az adatbázis felé küldött SQL utasításokat is! Nagyon megkönnyíti a tesztelő munkáját, ha minden képernyő betöltéskor, vagy API híváskor látja, milyen SQL utasítások kerülnek lefuttatásra.</p>
<p>Ugyanúgy naplózzuk a külső rendszerek felé menő kéréseket, valamint az arra kapott válaszokat! Lehetőleg még bármilyen feldolgozás előtt, a natív kéréseket és válaszokat.</p>
<p>A naplóhoz egy kereshető, szűkíthető felület is tartozzon. (Klasszikusan az ELK stack: Elasticsearch - Logback - Kibana, vagy valami modernebb alternatívája.) Különösen egy elosztott rendszer esetén nem túl egyszerű különböző gépeken lévő fájlokból összevadászni a megfelelő információkat. (Itt figyeljünk, hogy a gépek órája össze legyen szinkronizálva, sok helyen látom, hogy a szerverek órája között akár perces eltérések is szoktak lenni.)</p>
<h2 id="rossz-gyakorlat-nem-megfelelő-branch-en-történik-a-tesztelés">Rossz gyakorlat: nem megfelelő branch-en történik a tesztelés</h2>
<p>Mostanában elég sokféle branching stratégia elterjedt, úgymint GitFlow, GitHub Flow, GitLab Flow. A Git kifejezetten támogatja
az új branch létrehozását, hamar kialakult a Feature branch fogalma, melyen egy új funkció kerül lefejlesztésre,
a release branch, melyről verziót adunk, vagy a hotfix, melyeken a javításokat szállítjuk.</p>
<p>Ekkor azonban azonnal előjön a kérdés, hogy hol történjen a tesztelés? Jó ötletnek tűnhet, hogy a feature branch-en, így jól tudjuk izolálni, hogy mi rontotta el a funkcionalitást. Valamint ezzel biztosítjuk, hogy nem kerülhet be funkció ismert hibával a
fősodorba. Igen, de merge során, más feature-ökkel konfliktusba kerülve jelenhetnek meg új hibák. Vagy teszteljük csak a mastert?
Vagy teszteljünk kétszer, dupla munkával?</p>
<p>A Continuous Delivery egyszerűsíti a branch-elést, Trunk-based developmentent javasolja, és a masteren tesztelést. Csak nagyon apró feature branch-ekkel dolgozik, melyet érdemes mielőbb visszavezetni a masterre.</p>
<h2 id="tévhit-ha-működik-az-már-elég">Tévhit: “Ha működik, az már elég.”</h2>
<p>A tesztelés alatt nem csak a funkcionális tesztelést értjük. A szoftvernek meg kell felelnie bizonyos nem funkcionális követelményeknek is. Pl. legyen hibatűrő, magas rendelkezésre állású, nagy teljesítményű, skálázható, feleljen meg a biztonsági követelményeknek, és legyen könnyen használható. Ezekre lehet futtatni performancia és stresszteszteket, biztonsági teszteket (penetration testing), használhatósági teszteket (usability testing), melyek mindegyike külön tudomány.</p>
<p>Ha visszajön, hogy ezek egyikén elbukott az alkalmazás, akkor az én dolgom megvizsgálni, hogy pontosan mi is lehet a probléma, és nem a tesztelőktől elvárni, hogy a hiba okát is kiderítsék.</p>
<h2 id="összefoglalás">Összefoglalás</h2>
<p>Remélem sikerült éreztetni, hogy a fejlesztők és a tesztelők közötti közös munkának mennyi aspektusa van, és fejlesztőként
mennyit tudunk azért tenni, hogy ez az együttműködés a lehető leggördülékenyebb legyen. Egy tapasztalt tesztelővel való közös munka nagyon sokat ad, megismerhetsz egy más látásmódot, egy más világot.</p>
Gondolatok az objektumorientált programozásról2022-04-22T08:00:00+00:00http://www.jtechlog.hu/2022/04/22/osztaly-invarians<p>Egy fejlesztő számára a legtöbbször öröm egy új keretrendszer vagy lib kipróbálása, és használatba vétele,
azonban szerintem ugyanilyen fontos újra és újra visszanyúlni az objektumorientált alapokhoz, az osztályok és
metódusok tervezéshez. Ebben a posztban egy egyszerű példán keresztül próbálom bemutatni, hogy ennek is
milyen mélységei lehetnek, és mennyire nem egyértelmű a jó megoldás kiválasztása. Az itt megismert fogalmak
használata egyszerűsíti a fejlesztők közötti kommunikációt is.</p>
<p>A posztot az ihlette, hogy szó esett az invariánsról a Clean Code könyvben. És nem sokkal később
a Domain-Driven Design könyvben is megemlítették.</p>
<!-- more -->
<p>A példában egy meetupra lehet jelentkezni, amíg van szabad hely. Van egy <code class="language-plaintext highlighter-rouge">Meetup</code> osztály, entitás,
valamint egy <code class="language-plaintext highlighter-rouge">MeetupService</code>, ami a felhasználói felület felől fogadja a hívásokat. A példaprojekt
elérhető a <a href="https://github.com/vicziani/jtechlog-dbc">GitHubon</a>.</p>
<p>A <code class="language-plaintext highlighter-rouge">Meetup</code>
osztály tartalma a következő.</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Data</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">Meetup</span> <span class="o">{</span>
<span class="kd">private</span> <span class="nc">Long</span> <span class="n">id</span><span class="o">;</span>
<span class="kd">private</span> <span class="kt">int</span> <span class="n">limit</span><span class="o">;</span>
<span class="kd">private</span> <span class="nc">List</span><span class="o"><</span><span class="nc">String</span><span class="o">></span> <span class="n">attendees</span><span class="o">;</span>
<span class="o">}</span>
</code></pre></div></div>
<p>A <code class="language-plaintext highlighter-rouge">limit</code> tartalmazza a szabad helyek számát, az <code class="language-plaintext highlighter-rouge">attendees</code> pedig a jelentkezők e-mail címeit.</p>
<p>A <code class="language-plaintext highlighter-rouge">MeetupService</code> <code class="language-plaintext highlighter-rouge">attend()</code> metódusa tartalmazza az üzleti logikát.
Azaz egy meetupra csak akkor lehet jelentkezni, hogyha van elég szabad hely.
Ennek egy kezdeti megközelítése lehet a következő kódrészlet.</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">class</span> <span class="nc">MeetupService</span> <span class="o">{</span>
<span class="kd">private</span> <span class="nc">MeetupRepo</span> <span class="n">meetupRepo</span><span class="o">;</span>
<span class="kd">public</span> <span class="kt">boolean</span> <span class="nf">attend</span><span class="o">(</span><span class="kt">int</span> <span class="n">meetupId</span><span class="o">,</span> <span class="nc">List</span><span class="o"><</span><span class="nc">String</span><span class="o">></span> <span class="n">attendees</span><span class="o">)</span> <span class="o">{</span>
<span class="nc">Meetup</span> <span class="n">meetup</span> <span class="o">=</span> <span class="n">meetupRepo</span><span class="o">.</span><span class="na">findById</span><span class="o">(</span><span class="n">meetupId</span><span class="o">);</span>
<span class="k">if</span> <span class="o">(</span><span class="n">meetup</span><span class="o">.</span><span class="na">getAttendees</span><span class="o">().</span><span class="na">size</span><span class="o">()</span> <span class="o">+</span> <span class="n">attendees</span><span class="o">.</span><span class="na">size</span><span class="o">()</span> <span class="o"><=</span> <span class="n">meetup</span><span class="o">.</span><span class="na">getLimit</span><span class="o">())</span> <span class="o">{</span>
<span class="n">meetup</span><span class="o">.</span><span class="na">getAttendees</span><span class="o">().</span><span class="na">addAll</span><span class="o">(</span><span class="n">attendees</span><span class="o">);</span>
<span class="k">return</span> <span class="kc">true</span><span class="o">;</span>
<span class="o">}</span>
<span class="k">else</span> <span class="o">{</span>
<span class="k">return</span> <span class="kc">false</span><span class="o">;</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>Manapság gyakran látok hasonló kódrészleteket üzleti alkalmazásokban, azonban van hova fejlődni.
Egyrészt intő jel lehet, hogy az <code class="language-plaintext highlighter-rouge">attend()</code> metódus a <code class="language-plaintext highlighter-rouge">Meetup</code> több attribútumára is hivatkozik egyszerre.</p>
<p>Másrészt a metódusok láncolt hívása a <em>Demeter törvényét</em> sértik meg. Azaz a <code class="language-plaintext highlighter-rouge">meetup.getAttendees().addAll(attendees)</code>
metódushívások miatt a <code class="language-plaintext highlighter-rouge">MeetupService</code> jobban ismeri a <code class="language-plaintext highlighter-rouge">Meetup</code> osztály belső szerkezetét, mint az egészséges lenne.</p>
<p>De a fenti kódrészlettel az alapvető probléma: nem objektumorientált. Az objektumorientáltság alapja, hogy
az adatokat, és a rajtuk végzett műveleteket <em>egységbe zárja</em>. Ez a fenti kódnál nincs így,
hiszen a <code class="language-plaintext highlighter-rouge">Meetup</code> osztály tartalmazza az attribútumokat, a <code class="language-plaintext highlighter-rouge">MeetupService</code> pedig a rajtuk végzett műveleteket.
Ez ún. <em>anemic modell</em>, vagy magyarul vérszegény modell, melyet Martin Fowler jegyzett le először, és minősített antipatternnek.</p>
<p>Más néven ezt <em>transaction scriptnek</em> is nevezzük, mely szépen egymás után, egy helyen leírja a tranzakcióban végzendő
műveleteket.</p>
<p>Amennyiben objektumorientáltabban szeretnénk szervezni a kódunkat, akkor a <em>Domain-Driven Design</em> nyújthat segítséget,
mely kifejezetten szorgalmazza az olyan entitások használatát, amik maguk tartalmazzák az üzleti logikát.</p>
<p>Nézzük az ennek megfelelően módosított kódot:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Data</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">Meetup</span> <span class="o">{</span>
<span class="kd">private</span> <span class="nc">Long</span> <span class="n">id</span><span class="o">;</span>
<span class="kd">private</span> <span class="kt">int</span> <span class="n">limit</span><span class="o">;</span>
<span class="kd">private</span> <span class="nc">List</span><span class="o"><</span><span class="nc">String</span><span class="o">></span> <span class="n">attendees</span><span class="o">;</span>
<span class="kd">public</span> <span class="kt">boolean</span> <span class="nf">hasSpotsFor</span><span class="o">(</span><span class="nc">List</span><span class="o"><</span><span class="nc">String</span><span class="o">></span> <span class="n">newAttendees</span><span class="o">)</span> <span class="o">{</span>
<span class="k">return</span> <span class="n">attendees</span><span class="o">.</span><span class="na">size</span><span class="o">()</span> <span class="o">+</span> <span class="n">newAttendees</span><span class="o">.</span><span class="na">size</span><span class="o">()</span> <span class="o"><=</span> <span class="n">limit</span><span class="o">;</span>
<span class="o">}</span>
<span class="kd">public</span> <span class="kt">void</span> <span class="nf">attend</span><span class="o">(</span><span class="nc">List</span><span class="o"><</span><span class="nc">String</span><span class="o">></span> <span class="n">newAttendees</span><span class="o">)</span> <span class="o">{</span>
<span class="n">attendees</span><span class="o">.</span><span class="na">addAll</span><span class="o">(</span><span class="n">newAttendees</span><span class="o">);</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>Itt már nem csak getter és setter metódusok vannak, hanem az üzleti logika is itt található.
Nézzük a módosított <code class="language-plaintext highlighter-rouge">MeetupService</code> osztályt!</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">class</span> <span class="nc">MeetupService</span> <span class="o">{</span>
<span class="kd">private</span> <span class="nc">MeetupRepo</span> <span class="n">meetupRepo</span><span class="o">;</span>
<span class="kd">public</span> <span class="kt">boolean</span> <span class="nf">attend</span><span class="o">(</span><span class="kt">int</span> <span class="n">meetupId</span><span class="o">,</span> <span class="nc">List</span><span class="o"><</span><span class="nc">String</span><span class="o">></span> <span class="n">attendees</span><span class="o">)</span> <span class="o">{</span>
<span class="nc">Meetup</span> <span class="n">meetup</span> <span class="o">=</span> <span class="n">meetupRepo</span><span class="o">.</span><span class="na">findById</span><span class="o">(</span><span class="n">meetupId</span><span class="o">);</span>
<span class="kt">boolean</span> <span class="n">success</span> <span class="o">=</span> <span class="kc">false</span><span class="o">;</span>
<span class="k">if</span> <span class="o">(</span><span class="n">meetup</span><span class="o">.</span><span class="na">hasSpotsFor</span><span class="o">(</span><span class="n">attendees</span><span class="o">))</span> <span class="o">{</span>
<span class="n">meetup</span><span class="o">.</span><span class="na">attend</span><span class="o">(</span><span class="n">attendees</span><span class="o">);</span>
<span class="n">success</span> <span class="o">=</span> <span class="kc">true</span><span class="o">;</span>
<span class="o">}</span>
<span class="k">return</span> <span class="n">success</span><span class="o">;</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>Itt egyrészt módosultak a <code class="language-plaintext highlighter-rouge">Meetup</code> hívások, a <code class="language-plaintext highlighter-rouge">MeetupService</code> már nem lát bele annyira a
<code class="language-plaintext highlighter-rouge">Meetup</code> működésébe.</p>
<p>Valamint a struktúrált programozásnak megfelelően azt a szabályt is érvényre juttattam, hogy
a metódusnak csak egy kilépési pontja lehetséges, azaz egy <code class="language-plaintext highlighter-rouge">return</code> utasítás található benne.</p>
<p>A fenti kóddal kapcsolatban az a probléma, hogy nincs leírva sem informálisan, sem formálisan,
hogy a <code class="language-plaintext highlighter-rouge">Meetup</code> <code class="language-plaintext highlighter-rouge">attend()</code> metódusát csak akkor lehet meghívni, hogyha a <code class="language-plaintext highlighter-rouge">hasSpotsFor()</code>
metódus igaz értéket ad vissza. Ha a <code class="language-plaintext highlighter-rouge">MeetupService</code> ezen ellenőrzés nélkül hívja meg
az <code class="language-plaintext highlighter-rouge">attend()</code> metódust, akkor a <code class="language-plaintext highlighter-rouge">Meetup</code> objektumunk <em>inkonzisztens</em>, azaz ellentmondást
tartalmazó állapotba kerül át, azaz a résztvevők száma nagyobb lesz, mint a helyek száma.</p>
<p>A továbblépéshez először tisztázzunk pár fogalmat! Az objektumorientált fogalomkörben
a <code class="language-plaintext highlighter-rouge">Meetup</code> a server, a <code class="language-plaintext highlighter-rouge">MeetupService</code> pedig annak a kliense. Én azonban inkább a <em>hívott</em>
és a <em>hívó</em> szavakat fogom erre használni. A <em>logikai kifejezések</em> pedig olyan kifejezések,
melyek logikai típusú értéket adnak vissza, azaz igaz vagy hamis értékeket.</p>
<p>A <code class="language-plaintext highlighter-rouge">Meetup</code> osztály <code class="language-plaintext highlighter-rouge">attend()</code> metódusának előfeltétele, hogy egyrészt a jelentkezőket tartalmazó lista
ne legyen se <code class="language-plaintext highlighter-rouge">null</code>, se üres, hiszen akkor a hívásnak nincs értelme. Valamint a lista mérete legyen kisebb,
vagy egyenlő, mint a szabad helyek száma.</p>
<p>A metódushoz tartozó <em>előfeltétel</em>, angolul <em>precondition</em>, tehát egy logikai kifejezés, aminek
igaznak kell lennie a metódus meghívásakor.</p>
<p>Ahogy a példában láthatjuk, az előfeltételek egy metódushívás paraméterére vonatkozhatnak (ne legyen <code class="language-plaintext highlighter-rouge">null</code> vagy üres),
de vonatkozhatnak az adott objektum állapotára is (lista mérete legyen kisebb,
vagy egyenlő, mint a szabad helyek száma).</p>
<p>A Java típusos nyelv, ezért a paramétereknek is meg kell adni típusát, ezzel is valójában további előfeltételeket fogalmazunk meg,
hiszen paraméterül csak <code class="language-plaintext highlighter-rouge">String</code> objektumok listáját lehet átadni.</p>
<p>Mit tegyünk akkor, ha a hívó mégsem teljesíti az előfeltételt? Dönthetünk úgy, hogy ebben
az esetben kivételt dobunk.</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kt">void</span> <span class="nf">attend</span><span class="o">(</span><span class="nc">List</span><span class="o"><</span><span class="nc">String</span><span class="o">></span> <span class="n">newAttendees</span><span class="o">)</span> <span class="o">{</span>
<span class="k">if</span> <span class="o">(</span><span class="n">newAttendees</span> <span class="o">==</span> <span class="kc">null</span> <span class="o">||</span> <span class="n">newAttendees</span><span class="o">.</span><span class="na">isEmpty</span><span class="o">())</span> <span class="o">{</span>
<span class="k">throw</span> <span class="k">new</span> <span class="nf">IllegalArgumentException</span><span class="o">(</span><span class="s">"Must contain attendees"</span><span class="o">);</span>
<span class="o">}</span>
<span class="k">if</span> <span class="o">(!</span><span class="n">hasSpotsFor</span><span class="o">(</span><span class="n">newAttendees</span><span class="o">))</span> <span class="o">{</span>
<span class="k">throw</span> <span class="k">new</span> <span class="nf">IllegalArgumentException</span><span class="o">(</span><span class="s">"Has no spots"</span><span class="o">);</span>
<span class="o">}</span>
<span class="n">attendees</span><span class="o">.</span><span class="na">addAll</span><span class="o">(</span><span class="n">newAttendees</span><span class="o">);</span>
<span class="o">}</span>
</code></pre></div></div>
<p>Ezt a megoldást hívják <em>offenzív programozásnak</em>, ugyanis a hívót támadjuk azért (kivételt dobunk neki), mert nem teljesítette az előfeltételeket.
Azaz egy programozási hibára egy kivétellel válaszolunk, amit ha a hívó oldal nem kezel, leáll a program működése.
Úgy is szoktunk rá hivatkozni, hogy <em>fail fast</em> azaz a programozási hiba a lehető leghamarabb derüljön ki, és egy elég
szélsőséges működéssel, konkrétan leállással. Ha hamar kiderül, akkor hamar javítani is lehet.</p>
<p>Ezzel a feltétellel csak az az egy probléma van, hogy a feltétel sosem lesz igaz, hiszen a <code class="language-plaintext highlighter-rouge">MeetupService</code> már
gondoskodik arról, hogy ne kerüljön meghívásra, ha nem teljesülnek az előfeltételek.</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kt">boolean</span> <span class="nf">attend</span><span class="o">(</span><span class="kt">int</span> <span class="n">meetupId</span><span class="o">,</span> <span class="nc">List</span><span class="o"><</span><span class="nc">String</span><span class="o">></span> <span class="n">attendees</span><span class="o">)</span> <span class="o">{</span>
<span class="k">if</span> <span class="o">(</span><span class="n">attendees</span> <span class="o">==</span> <span class="kc">null</span> <span class="o">||</span> <span class="n">attendees</span><span class="o">.</span><span class="na">isEmpty</span><span class="o">())</span> <span class="o">{</span>
<span class="k">throw</span> <span class="k">new</span> <span class="nf">IllegalArgumentException</span><span class="o">(</span><span class="s">"Must contain attendees"</span><span class="o">);</span>
<span class="o">}</span>
<span class="nc">Meetup</span> <span class="n">meetup</span> <span class="o">=</span> <span class="n">meetupRepo</span><span class="o">.</span><span class="na">findById</span><span class="o">(</span><span class="n">meetupId</span><span class="o">);</span>
<span class="kt">boolean</span> <span class="n">success</span> <span class="o">=</span> <span class="kc">false</span><span class="o">;</span>
<span class="k">if</span> <span class="o">(</span><span class="n">meetup</span><span class="o">.</span><span class="na">hasSpotsFor</span><span class="o">(</span><span class="n">attendees</span><span class="o">))</span> <span class="o">{</span>
<span class="n">meetup</span><span class="o">.</span><span class="na">attend</span><span class="o">(</span><span class="n">attendees</span><span class="o">);</span>
<span class="n">success</span> <span class="o">=</span> <span class="kc">true</span><span class="o">;</span>
<span class="o">}</span>
<span class="k">return</span> <span class="n">success</span><span class="o">;</span>
<span class="o">}</span>
</code></pre></div></div>
<p>Itt már az is látszik, hogy a <code class="language-plaintext highlighter-rouge">MeetupService</code> osztályban lévő <code class="language-plaintext highlighter-rouge">attend()</code> metódus sem kaphat
üres listát, ez már a felhasználói felületnek át sem kellett volna engednie.</p>
<p>A plusz elágazások nehezítik az olvashatóságot, és egy bonyolultabb feltétel futás közben is overhead.
Az üres lista ellenőrzésénél ráadásul azt is láthatjuk, hogy ugyanaz az előfeltétel
a hívási láncban akár többször is előfordulhat.</p>
<p>Egy kicsit olvashatóbb megoldás a Guava <code class="language-plaintext highlighter-rouge">Preconditions</code> osztály használata, mellyel
a <code class="language-plaintext highlighter-rouge">Meetup</code> osztály <code class="language-plaintext highlighter-rouge">attend()</code> metódusa a következőképp váltható ki.</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kt">void</span> <span class="nf">attend</span><span class="o">(</span><span class="nc">List</span><span class="o"><</span><span class="nc">String</span><span class="o">></span> <span class="n">newAttendees</span><span class="o">)</span> <span class="o">{</span>
<span class="nc">Preconditions</span><span class="o">.</span><span class="na">checkArgument</span><span class="o">(</span><span class="n">newAttendees</span> <span class="o">!=</span> <span class="kc">null</span> <span class="o">&&</span> <span class="n">newAttendees</span><span class="o">.</span><span class="na">size</span><span class="o">()</span> <span class="o">></span> <span class="mi">0</span><span class="o">);</span>
<span class="nc">Preconditions</span><span class="o">.</span><span class="na">checkArgument</span><span class="o">(</span><span class="n">hasSpotsFor</span><span class="o">(</span><span class="n">newAttendees</span><span class="o">));</span>
<span class="n">attendees</span><span class="o">.</span><span class="na">addAll</span><span class="o">(</span><span class="n">newAttendees</span><span class="o">);</span>
<span class="o">}</span>
</code></pre></div></div>
<p>Ha a feltétel nem teljesül, akkor ugyanúgy <code class="language-plaintext highlighter-rouge">IllegalArgumentException</code> kivételt dobnak.</p>
<p>Feltétel és kivételdobás helyett választhatjuk az <code class="language-plaintext highlighter-rouge">assert</code> nyelvi elem használatát is.
(Nem keverendő a unit tesztekben használt assert utasításokkal!)
Az assert szintén egy logikai kifejezést vár, és ha nem teljesül, akkor egy
kivételt dob.</p>
<p>A <code class="language-plaintext highlighter-rouge">Meetup</code> osztály <code class="language-plaintext highlighter-rouge">attend()</code> metódusa így módosul:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kt">void</span> <span class="nf">attend</span><span class="o">(</span><span class="nc">List</span><span class="o"><</span><span class="nc">String</span><span class="o">></span> <span class="n">newAttendees</span><span class="o">)</span> <span class="o">{</span>
<span class="k">assert</span> <span class="n">newAttendees</span> <span class="o">!=</span> <span class="kc">null</span> <span class="o">&&</span> <span class="n">newAttendees</span><span class="o">.</span><span class="na">size</span><span class="o">()</span> <span class="o">></span> <span class="mi">0</span><span class="o">;</span>
<span class="k">assert</span> <span class="nf">hasSpotsFor</span><span class="o">(</span><span class="n">newAttendees</span><span class="o">);</span>
<span class="n">attendees</span><span class="o">.</span><span class="na">addAll</span><span class="o">(</span><span class="n">newAttendees</span><span class="o">);</span>
<span class="o">}</span>
</code></pre></div></div>
<p>Ettől egyrészt a kód is átláthatóbb lett, másrészt az <code class="language-plaintext highlighter-rouge">assert</code> utasítás
csak akkor dob kivételt a hamis logikai kifejezés esetén, ha a JVM-et
a <code class="language-plaintext highlighter-rouge">-ea</code> (enable assertions) kapcsolóval indítjuk.</p>
<p>Ha nem ezt a kapcsolót használjuk, akkor a paraméterként átadott
kifejezéseket ki sem értékeli, nem hívja meg a metódusokat, nincs overhead.</p>
<p>És ekkor ezt hagyjuk bekapcsolva az automata (pl. unit) és manuális tesztek futtatásakor,
éles rendszeren azonban kapcsoljuk ki.</p>
<p>Mikor melyiket használjuk?</p>
<ul>
<li>Amennyiben az osztályunkat más, a saját fennhatóságunk alatt lévő osztályok hívják,
amit könnyen tudunk módosítani, használjunk asserteket!</li>
<li>Amennyiben az osztályunkat külső kliensek hívják, dobjunk kivételt!</li>
<li>Amennyiben a feltétel programhiba miatt nem teljesül, használjunk asserteket!</li>
<li>Amennyiben a hibával az alkalmazáson belül magasabb szinten valamit kezdeni tudunk, használjunk kivételt!</li>
</ul>
<p>Így az assert használata az entitásban jó választás lehet, és a service-ben pedig hagyjuk
a kivételt!</p>
<p>Nézzünk további hibás eseteket! Mi van akkor, ha olyanok próbálnak jelentkezni, akik
már jelentkeztek? Vagy mi van akkor, ha a jelentkezés valami miatt kétszer fut be?
(Pl. hálózati hiba miatt újra próbálkozás.) Ekkor nem biztos, hogy az a legjobb
megoldás, hogy hibát dobunk vissza. Ilyenkor érdemes ezt a hibát valahogy kezelni.
Ebben az esetben pl. az érvénytelen, második jelentkezéseket figyelmen kívül hagyjuk.</p>
<p>Ehhez új metódus kell a <code class="language-plaintext highlighter-rouge">Meetup</code> osztályba, ami leválogatja, hogy melyek azok
a jelentkezők, akik még nem jelentkeztek. És a service csak ezeket adja hozzá.</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="nc">List</span><span class="o"><</span><span class="nc">String</span><span class="o">></span> <span class="nf">getNotAttended</span><span class="o">(</span><span class="nc">List</span><span class="o"><</span><span class="nc">String</span><span class="o">></span> <span class="n">newAttendees</span><span class="o">)</span> <span class="o">{</span>
<span class="k">return</span> <span class="n">newAttendees</span><span class="o">.</span><span class="na">stream</span><span class="o">().</span><span class="na">filter</span><span class="o">(</span><span class="n">newAttendee</span> <span class="o">-></span> <span class="o">!</span><span class="n">attendees</span><span class="o">.</span><span class="na">contains</span><span class="o">(</span><span class="n">newAttendee</span><span class="o">)).</span><span class="na">toList</span><span class="o">();</span>
<span class="o">}</span>
</code></pre></div></div>
<p>A <code class="language-plaintext highlighter-rouge">MeetupService</code> <code class="language-plaintext highlighter-rouge">attend()</code> metódusa.</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kt">boolean</span> <span class="nf">attend</span><span class="o">(</span><span class="kt">int</span> <span class="n">meetupId</span><span class="o">,</span> <span class="nc">List</span><span class="o"><</span><span class="nc">String</span><span class="o">></span> <span class="n">attendees</span><span class="o">)</span> <span class="o">{</span>
<span class="k">if</span> <span class="o">(</span><span class="n">attendees</span> <span class="o">==</span> <span class="kc">null</span> <span class="o">||</span> <span class="n">attendees</span><span class="o">.</span><span class="na">isEmpty</span><span class="o">())</span> <span class="o">{</span>
<span class="k">throw</span> <span class="k">new</span> <span class="nf">IllegalArgumentException</span><span class="o">(</span><span class="s">"Must contain attendees"</span><span class="o">);</span>
<span class="o">}</span>
<span class="nc">Meetup</span> <span class="n">meetup</span> <span class="o">=</span> <span class="n">meetupRepo</span><span class="o">.</span><span class="na">findById</span><span class="o">(</span><span class="n">meetupId</span><span class="o">);</span>
<span class="kt">boolean</span> <span class="n">success</span> <span class="o">=</span> <span class="kc">false</span><span class="o">;</span>
<span class="nc">List</span><span class="o"><</span><span class="nc">String</span><span class="o">></span> <span class="n">notAttendedYet</span> <span class="o">=</span> <span class="n">meetup</span><span class="o">.</span><span class="na">getNotAttended</span><span class="o">(</span><span class="n">attendees</span><span class="o">);</span>
<span class="k">if</span> <span class="o">(</span><span class="n">notAttendedYet</span><span class="o">.</span><span class="na">size</span><span class="o">()</span> <span class="o">></span> <span class="mi">0</span> <span class="o">&&</span> <span class="n">meetup</span><span class="o">.</span><span class="na">hasSpotsFor</span><span class="o">(</span><span class="n">notAttendedYet</span><span class="o">))</span> <span class="o">{</span>
<span class="n">meetup</span><span class="o">.</span><span class="na">attend</span><span class="o">(</span><span class="n">notAttendedYet</span><span class="o">);</span>
<span class="n">success</span> <span class="o">=</span> <span class="kc">true</span><span class="o">;</span>
<span class="o">}</span>
<span class="k">return</span> <span class="n">success</span><span class="o">;</span>
<span class="o">}</span>
</code></pre></div></div>
<p>És új assert kerül a <code class="language-plaintext highlighter-rouge">Meetup</code> osztályba, ami ellenőrzi, hogy ne jelentkezzen az, aki már jelentkezett.</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kt">boolean</span> <span class="nf">containsAny</span><span class="o">(</span><span class="nc">List</span><span class="o"><</span><span class="nc">String</span><span class="o">></span> <span class="n">newAttendees</span><span class="o">)</span> <span class="o">{</span>
<span class="k">return</span> <span class="n">newAttendees</span><span class="o">.</span><span class="na">stream</span><span class="o">().</span><span class="na">anyMatch</span><span class="o">(</span><span class="n">newAttendee</span> <span class="o">-></span> <span class="n">attendees</span><span class="o">.</span><span class="na">contains</span><span class="o">(</span><span class="n">newAttendee</span><span class="o">));</span>
<span class="o">}</span>
<span class="kd">public</span> <span class="kt">void</span> <span class="nf">attend</span><span class="o">(</span><span class="nc">List</span><span class="o"><</span><span class="nc">String</span><span class="o">></span> <span class="n">newAttendees</span><span class="o">)</span> <span class="o">{</span>
<span class="k">assert</span> <span class="n">newAttendees</span> <span class="o">!=</span> <span class="kc">null</span> <span class="o">&&</span> <span class="n">newAttendees</span><span class="o">.</span><span class="na">size</span><span class="o">()</span> <span class="o">></span> <span class="mi">0</span><span class="o">;</span>
<span class="k">assert</span> <span class="nf">hasSpotsFor</span><span class="o">(</span><span class="n">newAttendees</span><span class="o">);</span>
<span class="k">assert</span> <span class="o">!</span><span class="n">containsAny</span><span class="o">(</span><span class="n">newAttendees</span><span class="o">);</span>
<span class="n">attendees</span><span class="o">.</span><span class="na">addAll</span><span class="o">(</span><span class="n">newAttendees</span><span class="o">);</span>
<span class="o">}</span>
</code></pre></div></div>
<p>Ez abban az esetben, ha már megtörtént a jelentkezés, nem dob hibát, hanem megpróbálja kezelni.</p>
<p>Ezt a fajta programozást <em>defenzív</em>, azaz védekező programozásnak nevezzük.
Itt a kezelhető, külső forrásból származó kivételeket próbáljuk kezelni,
valamilyen kerülő megoldást találni. Azaz felkészülünk, és kezeljük a váratlan helyzeteket. (Szemben az offenzív programozással,
ahol a belső, nem kezelhető programozási hibától próbáljuk megvédeni magunkat úgy, hogy hibát dobunk, így hamar kiderül és javítható.
Ezek a nem várt programozási hibák pl. a nem várt paraméter, vagy nem várt, azaz nem dokumentált visszatérési érték.)
A defenzív programozásnál gyakran alapértelmezett értékeket, alapértelmezetten lefutó kódblokkokat használunk.</p>
<p>Ezt szokták nevezni <em>fail safe</em> megoldásnak is, hiszen nem áll le hibával a futás, hanem kezeljük a kivételes helyzetet, és fut tovább a program.</p>
<p>Az előbbi esetben a metódusunk ráadásul <em>idempotens</em>, azaz ha kétszer meghívjuk, ugyanaz az eredményt adja vissza. Második híváskor az állapot nem változik.
Ez nagyon jó tulajdonság akkor, ha fel kell készülnünk arra, hogy egy üzenetet akár kétszer is megkaphatunk.</p>
<p>Ez a megoldás sem jó minden esetben. Egyrészt túl sok plusz kód szükséges hozzá, plusz elágazások. Olyan kivételes helyzetet is próbálunk kezelni, ami nem
fog jelentkezni. Lehet, hogy valódi hibát nyel el, amit próbál, tévesen kezelni. (Erre példa, amikor a metódust úgy írjuk meg, hogy a túl hosszú String
végét levágjuk, hiszen úgysem lesz sosem olyan hosszú.)</p>
<p>Most, hogy azt is láttuk, hogy lehet az előfeltételeket formálisan definiálni, nézzük meg, hogy hogyan lehet azt leírni, hogy egy
metódusnak mi az eredménye. Ez a hívó oldalnak fontos, hogy formálisan is le legyen írva, hogy mire számíthat akkor, ha a hívott metódus helyesen fut le.
Nézzük mit jelent ez a <code class="language-plaintext highlighter-rouge">Meetup</code> osztályban!</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kt">void</span> <span class="nf">attend</span><span class="o">(</span><span class="nc">List</span><span class="o"><</span><span class="nc">String</span><span class="o">></span> <span class="n">newAttendees</span><span class="o">)</span> <span class="o">{</span>
<span class="c1">// Preconditions</span>
<span class="k">assert</span> <span class="n">newAttendees</span> <span class="o">!=</span> <span class="kc">null</span> <span class="o">&&</span> <span class="n">newAttendees</span><span class="o">.</span><span class="na">size</span><span class="o">()</span> <span class="o">></span> <span class="mi">0</span><span class="o">;</span>
<span class="k">assert</span> <span class="nf">hasSpotsFor</span><span class="o">(</span><span class="n">newAttendees</span><span class="o">);</span>
<span class="k">assert</span> <span class="o">!</span><span class="n">containsAny</span><span class="o">(</span><span class="n">newAttendees</span><span class="o">);</span>
<span class="n">attendees</span><span class="o">.</span><span class="na">addAll</span><span class="o">(</span><span class="n">newAttendees</span><span class="o">);</span>
<span class="c1">// Postconditions</span>
<span class="k">assert</span> <span class="n">attendees</span><span class="o">.</span><span class="na">contains</span><span class="o">(</span><span class="n">newAttendees</span><span class="o">);</span>
<span class="o">}</span>
</code></pre></div></div>
<p>Ennek a hivatalos neve az <em>utófeltétel</em>, azaz <em>postcondition</em>. Az is látható, hogy szintén megadható
assert használatával. Az utófeltételben szerepelhetnek paraméterek, az objektum állapota, de még a visszatérési érték is.</p>
<p>Az előfeltételekben és utófeltételekben bizonyos feltételek ismétlődhetnek. Ezek lehetnek olyan feltételek,
melyeknek az osztály <em>állapotának</em> (attribútumainak értékeinek összességének) mindig meg kell felelnie. Jelen példánkban ez az, hogy
a résztvevők száma sosem haladhatja meg a helyek számát.</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kt">void</span> <span class="nf">attend</span><span class="o">(</span><span class="nc">List</span><span class="o"><</span><span class="nc">String</span><span class="o">></span> <span class="n">newAttendees</span><span class="o">)</span> <span class="o">{</span>
<span class="c1">// Preconditions</span>
<span class="k">assert</span> <span class="n">newAttendees</span> <span class="o">!=</span> <span class="kc">null</span> <span class="o">&&</span> <span class="n">newAttendees</span><span class="o">.</span><span class="na">size</span><span class="o">()</span> <span class="o">></span> <span class="mi">0</span><span class="o">;</span>
<span class="k">assert</span> <span class="nf">hasSpotsFor</span><span class="o">(</span><span class="n">newAttendees</span><span class="o">);</span>
<span class="k">assert</span> <span class="o">!</span><span class="n">containsAny</span><span class="o">(</span><span class="n">newAttendees</span><span class="o">);</span>
<span class="n">attendees</span><span class="o">.</span><span class="na">addAll</span><span class="o">(</span><span class="n">newAttendees</span><span class="o">);</span>
<span class="c1">// Postconditions</span>
<span class="k">assert</span> <span class="n">attendees</span><span class="o">.</span><span class="na">contains</span><span class="o">(</span><span class="n">newAttendees</span><span class="o">);</span>
<span class="c1">// Invariant</span>
<span class="k">assert</span> <span class="nf">checkAttendeesHaveSpots</span><span class="o">();</span>
<span class="o">}</span>
<span class="kd">private</span> <span class="kt">boolean</span> <span class="nf">checkAttendeesHaveSpots</span><span class="o">()</span> <span class="o">{</span>
<span class="k">return</span> <span class="n">attendees</span><span class="o">.</span><span class="na">size</span><span class="o">()</span> <span class="o"><=</span> <span class="n">limit</span><span class="o">;</span>
<span class="o">}</span>
</code></pre></div></div>
<p>Ezt egy külön metódusba szerveztem ki, hiszen majd több metódusban is fel akarom használni.
Az <code class="language-plaintext highlighter-rouge">attend()</code> metódusban szintén asserttel hívom meg.</p>
<p>Ezt a feltételt hívják <em>invariánsnak</em>. Az osztály attribútumainak értékei mindig meg kell, hogy feleljenek
ennek a feltételnek. Ennek a feltételnek a teljesülését a konstruktorok és a publikus
metódusok tartják fel. Ennek a feltételnek a konstruktor hívása előtt, minden metódus hívása előtt és
után is teljesülnie kell. A konstruktor és a metódusok futása közben ideiglenesen lehet hamis,
de a lefutás végén mindig igaznak kell lennie.</p>
<p>Mire való tehát az invariáns? Az adott osztály fejlesztőjének mondja meg, hogy mire kell figyelnie, ha
konstruktort vagy metódust változtat, vagy újat ad hozzá. Elvileg nem kell ellenőrizni, hiszen
pont a fejlesztő garantálja ezt. Ha ezt még szigorúbban akarjuk garantálni, akkor kell ezeket az
invariánsokat mindig ellenőrizni. Es sajnos nem megy másképp, csak minden metódus futása utáni
feltételes kifejezés kiértékelésével.</p>
<p>Képzeljük el, hogy kell egy új metódus, ami a helyek számát csökkenti. Ez nem mehet a
már jelentkezettek száma alá.</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kt">void</span> <span class="nf">decreaseLimitTo</span><span class="o">(</span><span class="kt">int</span> <span class="n">newLimit</span><span class="o">)</span> <span class="o">{</span>
<span class="k">assert</span> <span class="n">newLimit</span> <span class="o">></span> <span class="mi">0</span> <span class="o">&&</span> <span class="n">newLimit</span> <span class="o"><</span> <span class="n">limit</span><span class="o">;</span>
<span class="k">assert</span> <span class="n">newLimit</span> <span class="o">>=</span> <span class="n">attendees</span><span class="o">.</span><span class="na">size</span><span class="o">();</span>
<span class="n">limit</span> <span class="o">=</span> <span class="n">newLimit</span><span class="o">;</span>
<span class="k">assert</span> <span class="n">limit</span> <span class="o">==</span> <span class="n">newLimit</span><span class="o">;</span>
<span class="k">assert</span> <span class="nf">checkAttendeesHaveSpots</span><span class="o">();</span>
<span class="o">}</span>
</code></pre></div></div>
<p>Látható, ha biztosak akarunk lenni, hogy a metódus meghívása után is teljesül az invariáns,
akkor ellenőrizni kell azt (<code class="language-plaintext highlighter-rouge">assert checkAttendeesHaveSpots()</code>).</p>
<p>A <em>Design by contract</em> az a megközelítés, mely az absztrakt adattípusokat kiegészíti előfeltételekkel (precondition), utófeltételekkel (postcondition) és
invariánsokkal. (A Hoare logikában is előfeltételeket és utófeltételeket lehet formálisan megadni, ami programhelyesség bizonyításra használható, és a Design by contract egy előzményének tekinthető.) Ezek pontos és formális leírása alkotja a szerződést. A Design by contract feltételezi, hogy mindkét fél, azaz a hívó és a hívott fél is teljesíti a szerződés feltételeit. A DbC szorgalmazza, hogy a szerződést előre írjuk meg, lehetőleg a tervezés részeként.</p>
<p>Ideális esetben nem az üzleti logikával keverve kéne a szerződést ellenőrzni. Ideális esetben a szerződés megsértése fordítási időben jelentkezik. Ilyen
pl. típusok használata, ugyanis ha paraméterül egész számot kell átadni, de én lebegőpontos számot adok át, akkor a program le sem fordul.</p>
<p>Sajnos a Design by contract alkalmazására Javaban nincs kielégítő megoldás.</p>
<p>Az egyik mód, ha a forráskódban, megjegyzésekben definiáljuk. Sajnos ez nem formális, nem automatikusan nem is ellenőrizhető. Azonban ez is több, mint a semmi. Ha más
megoldást nem is választunk, legalább ezt használjuk.</p>
<p>Fontos, hogy a Design by contract a programozói hibáktól próbál megóvni minket. Ezért hasznos, ha fejlesztés és tesztelés közben bekapcsolható, éles környezetben
viszont kikapcsolható. Ezért alkalmas az assertek használata. Amúgy a Java assertion egy <a href="https://docs.oracle.com/cd/E19683-01/806-7930/assert-13/index.html">régi dokumentációja</a>
is ezeket a fogalmakat használja.</p>
<p>A Design by contract nem váltja ki a tesztelést, hanem kiegészíti, akár a unit tesztelést, akár a manuális tesztelést teszt környezetben bekapcsolt assertekkel. Már azelőtt kiderül a programozási hiba, hogy hibás érték jönne vissza. Az assert érdekessége ebben az esetben, hogy nem a teszt része, hanem a tesztelendő rendszer (SUT) része.
Az előfeltételek figyelembe vételével a tesztesetek szűkíthetőek.</p>
<p>Amúgy voltak régebbi próbálkozások a témában, de ezek egyike sem terjedt el. Egyrészt a Google nevéhez fűződő annotáció alapú
<a href="https://github.com/nhatminhle/cofoja">Cofoja</a>, sajnos több éve nem fejlesztik.
A másik a <a href="http://www.valid4j.org/">valid4j</a>, szintén évek óta nem fejlődő projekt. Érdekes még az <a href="https://github.com/sebthom/oval">OVal</a>, mely archiválásra került, azzal
a javaslattal, hogy használjunk Bean Validationt.</p>
<p>A Bean Validation használata erre amúgy érdekes felvetés. Az invariánst meg tudjuk adni saját class level constraint annotációval, amire viszont valamikor meg kell hívnunk
az ellenőrzést. Az sem szimpatikus, hogy a validator a <code class="language-plaintext highlighter-rouge">Meetup</code> osztály csak a publikus API-jához fér hozzá, azaz a belső állapotát nem tudja feltétlen ellenőrizni.</p>
<p>A elő- és utófeltétel ellenőrzésére jó lenne a method-level constraint, de ennek is vannak hátrányai. Valahogy rá kéne beszélni a JVM-et, hogy
a metódus hívása előtt és után futtassa le a paraméterek vagy a visszatérési érték ellenőrzését. Ezt viszont valamilyen AOP megoldással lehetne.
Amit viszont nem biztos, hogy a domain rétegben alkalmazni kéne, ezzel megsértve azt az egyszerűséget, hogy ott lehetőleg csak Java SE-t alkalmazzunk, és
legyen keretrendszer független.</p>
<p>A Bean Validation példa bemutatása túlmutat a poszt keretein, de akit érdekel, megnézheti a megoldást a példa projektben.</p>
<p>Nagyon komoly, és a mai napig fejlődő megoldás az <a href="https://www.openjml.org/">OpenJML</a>. Ehhez létezik egy külön nyelv,
a <a href="https://www.cs.ucf.edu/~leavens/JML/index.shtml">Java Modeling Language</a>, melyben a szerződés formálisan, JavaDoc-ban vagy annotációval megadható, és ellenőrizhető.</p>
<p>Ha egy kicsit kitekintünk, a Design by contract az Eiffel programozási nyelvtől származik, ami nyelvi szinten támogatta. Az előfeltételeket a <code class="language-plaintext highlighter-rouge">require</code>,
az utófeltételeket az <code class="language-plaintext highlighter-rouge">ensure</code> kulcsszó után lehetett írni. (Nem annyira limitált, mint az <code class="language-plaintext highlighter-rouge">assert</code> használata.)</p>
<p>Pythonban nincs nyelvi szintű támogatás, azonban a dekorátorok miatt egyszerűen implementálható, és több library is létezik, pl. az
<a href="https://pypi.org/project/icontract/">icontract</a>.</p>
<p>Most csak a <code class="language-plaintext highlighter-rouge">Meetup</code> osztályon belüli üzleti metódusokkal foglalkoztunk, azonban mi a helyzet a setterekkel? Hamar felismerhető, hogy egy
figyelmetlen <code class="language-plaintext highlighter-rouge">setLimit()</code> vagy <code class="language-plaintext highlighter-rouge">setAttendees()</code> hívás teljesen megboríthatja az invariánsainkat, és itt is kéne elő- és utófeltételeket
alkalmazni. Ha azonban kódot teszünk bele, akkor egy gyakori gyakorlatot szegünk meg, hiszen setterbe nem nagyon szoktunk kódot írni.</p>
<p>És a getterek? Sajnos azzal is van probléma. Nézzük a következő kódrészletet:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">meetup</span><span class="o">.</span><span class="na">getAttendees</span><span class="o">().</span><span class="na">addAll</span><span class="o">(</span><span class="n">newAttendees</span><span class="o">);</span>
</code></pre></div></div>
<p>Ez gyakorlatilag megkerüli az üzleti metódusokat, és közvetlen változtatja a kollekció tartalmát. Erre megoldás lehet, ha egy
módosíthatatlan burkoló példányt ad vissza a getter metódus. (Már így is több logika van benne, mint kéne.)</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="nc">List</span><span class="o"><</span><span class="nc">String</span><span class="o">></span> <span class="nf">getAttendees</span><span class="o">()</span> <span class="o">{</span>
<span class="k">return</span> <span class="nc">Collections</span><span class="o">.</span><span class="na">unmodifiableList</span><span class="o">(</span><span class="n">attendees</span><span class="o">);</span>
<span class="o">}</span>
</code></pre></div></div>
<p>Ekkor a fenti kódrészlet kivételt fog dobni. Sajnos a Lombok alkotói mereven elzárkóztak, hogy valami hasonló megoldást építsenek bele,
ugyanis nem csak a kollekciókkal, hanem bármilyen változtatható állapotú osztállyal baj van, amire nem lehet általános megoldást hozni.</p>
<p>Az invariánsokkal ráadásul az öröklődésnél is probléma van. Ha a leszármazott direkt hozzáférhet az ős attribútumaihoz, pl. protected,
megkerülheti az invariánsok biztosítását, megsértheti a laza függőséget. Ezért inkább kerüljük a protected hozzáférést, vagy használjunk
kompozíciót. A leszármazottak az invariáns feltételeit szigoríthatják, de nem lazíthatják.</p>
Szoftverfejlesztés okosan Pythonnal2022-02-03T09:00:00+00:00http://www.jtechlog.hu/2022/02/03/Szoftverfejlesztes-okosan-Pythonnal<p>Bár egyetértek azzal, hogy a programozással kapcsolatos szakirodalom nyelve
az angol, mégis üdvözlöm a magyar nyelvű könyveket, és blogokat
(hiszen ezért írom a JTechLogot is). Ezek a tartalmak itthon egy olyan
réteget is meg tudnak szólítani, akik esetleg kedvet kapva, ezen
az úton elindulva elkezdik fogyasztani az angol szakirodalmat is.
Gondolok például az középiskolásokra, felsőoktatási intézmények
diákjaira, manapság annyira népszerű
bootcampeken tanulókra, rokon szakmákból érdeklődőkre (pl. üzleti
elemzők, manuális tesztelők, matematikusok, stb.)</p>
<p>Ezért is tartom rajta a szemem a magyar nyelven megjelenő
könyveken. Sok megkeresés érkezik hozzám azzal kapcsolatban is,
hogy mivel ajánlott kezdeni, és a programozással kapcsolatos
fórumokon, listákon, csoportokon is újra és újra előjön a kérdés.
Mindig érdekelt az is, hogy egy kezdőknek szóló könyv hogyan
próbál bevezetni rendkívül absztrakt fogalmakat, mennyire hoz új példákat,
mennyire mer elrugaszkodni az eddigi tanítási módszerektől.
Különösen a magyar szerzők könyveinek örülök.</p>
<p>Már 2020-ban láttam Guta Gábor <em>Szoftverfejlesztés okosan Pythonnal : agilis csapatok közös nyelve</em>
című könyvét, és múltkor, mikor ki szerettem volna venni a könyvtárból, hogy
belelapozzak, és nem találtam benn, döntöttem el, hogy írok neki, hátha van neki
egy-két elfekvőben lévő példány. Gáborral már régóta ismerjük egymást,
akkor még mindketten Javaval foglalkoztunk, azóta váltott át, és merült bele
nagyon mélyen a Python világába. Mostanában ebben a témában ad elő
különböző szakmai konferenciákon is, pl. kifejezetten ajánlom a
<em>Python és a turbó csiga</em> című előadását a Python teljesítményhangolásról,
mely a HWSW free! meetup-sorozaton <a href="https://www.youtube.com/watch?v=MCJZWrPVgcs">hangzott el</a> 2020. februárjában.</p>
<p><a href="/artifacts/posts/images/szoftverfejlesztes-okosan-pythonnal-agilis-csapatok-kozos-nyelve.jpg" data-lightbox="post-images"><img src="/artifacts/posts/images/szoftverfejlesztes-okosan-pythonnal-agilis-csapatok-kozos-nyelve-thumb.jpg" alt="Kép leírása" /></a></p>
<!-- more -->
<p>Pár napon belül már meg is kaptam a dedikált példányt, és nagy lelkesedéssel vetettem
bele magam. Már az elején nagy várakozásokkal indultam neki, ismervén Gábort,
valamint láttam, hogy dr. Juhász István lektorálta, aki a legnagyobbra becsült tanáraim
egyike a Debreceni Egyetemen. Sőt a köszönetnyilvánításban Szathmáry László is megjelenik,
aki évfolyamtársam volt, és mindig szakmai precizitásáról volt híres (nem egy jegyzetéből tanultam).
A könyv a következő témákat tárgyalja, ebben a sorrendben:</p>
<ul>
<li>Kifejezés</li>
<li>A függvény</li>
<li>Az osztály</li>
<li>A vezérlési szerkezet</li>
<li>A szekvencia</li>
<li>A modul</li>
</ul>
<p>A könyv tehát a Python programozási nyelv alapjait mutatja be 118 oldalon.
A felépítése több helyen is okozott nekem kellemes meglepetéseket. Egyrészt
már a tartalomjegyzékben is látható, hogy előbb tárgyalja az osztályokat,
és csak utána a vezérlési szerkezeteket, ami egy elég merész döntés,
különösen Python esetén. Én teljes mértékben támogatom ezt a sorrendet,
Javaban sokkal jobban adja magát, de Pythonban még nem mertem vele kísérletezgetni
oktatásaim során.</p>
<p>A másik érdekessége, ami szintén egy nagyon jó döntés, hogy minden fejezet két részből áll.
Az első rész fontosabb koncepciók mentén tárgyalja a nyelvet, míg a második rész
megy bele részletekbe, tárgyalja a pontos szintaktikát, mintegy referenciaszerűen.</p>
<p>Belelapozva azonnal látható, hogy jó sok kód van benne. Sőt, a könyv folyamatosan hivatkozik
egy példaprojektre, mely a könyv végén is megtalálható, de regisztráció után a
<a href="https://smartpython.axonmatics.com/">könyv honlapjáról is letölthető</a>.</p>
<p>Ami szintén azonnal látható, hogy a könyv tele van sok, a megértést segítő
ábrával. A vezérlési szerkezeteknél természetesen folyamatábrákkal,
az osztályoknál azonban UML osztálydiagramokkal találkozhatunk. Bár
az UML manapság nem annyira divatos, én szeretem, mert ez a szabványos
jelölésrendszer lehetővé teszi, hogy egy pillantásra megértsük az osztályokat
és közöttük lévő kapcsolatokat. És a könyv csak annyit mutat be és használ az UML
nyelvből, amennyit feltétlenül szükséges.</p>
<p>Letudja a kötelező köröket is, az első fejezet bemutatja, hogyan kell elindulni a
fejlesztéssel, mit és hogyan kell telepíteni a különböző operációs rendszerekre
(Windows 10, MacOS X, Ubuntu Linux 18.04).</p>
<p>Ami egy magyar könyv esetén rendkívül idegesítő tud lenni, az az erőltetett
magyarítások használata. Van kiadó, ahol minden angol szót kötelező lefordítani,
és így gyakorlatilag olvashatatlan, úgy kell visszafejteni, hogy miről is van szó.
Gábor nem esett ebbe a hibába, nagyon-nagyon könnyeden, folyamatosan olvasható a
könyv, látszik, hogy gyakorló szakember írta.</p>
<p>És talán a legnehezebb kérdés, kinek szól a könyv? A könyv hátoldalán szereplő szöveg
szerint hasznos lehet olyanoknak, akik csak szeretnének belelátni a programozók munkájába, valamint
kezdő és gyakorlott programozóknak is. A bevezető kitér arra, hogy a könyv tömörsége miatt
nem fért bele irgalmatlan mennyiségű gyakorlati feladat, ami a programozói tudás elsajátításához szükséges.</p>
<p>Kihangsúlyoznám, hogy a könyv rendkívül tömör (118 oldal!). Gyakorlott programozóként,
akinek a Python nyelv sem volt ismeretlen, egy A4-esnyi dolgot gyűjtöttem ki,
aminek a könyv elolvasása után még utána szerettem volna nézni. Olyan fogalmakba
megy bele, amelybe a kezdő Python könyvek többszörös oldalszám mellett sem jutnak el
(különösen az objektumorientáltság terén, de szó esik a Python-projektek szerkezetéről,
virtuális környezetekről, kódellenőrző eszközökről, dokumentáció generálásról).</p>
<p>Én ezért azoknak ajánlanám a könyvet, akiknek már van valamennyi programozói gyakorlatuk,
akár más nyelven, akár csak felszínesen ismerik a Pythont, és szeretnék megismerni, hogy
mire képes, milyen lehetőségei vannak, és az utalások alapján képesek továbbmenni.</p>
<p>Nekik kiváló, mert tömörsége, felépítése, és nem utolsó sorban, mert jól van megírva,
a lehető legrövidebb idő alatt a lehető legtöbbet lehet megtudni a Pythonról.</p>
<p>A könyv tökéletes lehet egy Python oktatás támogatására is, ahol a bővebb magyarázatot
szóban mellé lehet tenni. És ismétlésképp is elegendő a könyv megfelelő
fejezeteit átnézni.</p>
<p>Azoknak is javaslom, akik most tanulnak programozni, vagy most tanulják a Pythont, de én egy másik, szájbarágósabb,
sokkal több gyakorlati példát tartalmazó könyv mellé javaslom. Amellett képes abszolút kezdők számára ez a
könyv tündökölni, hiszen ad egyfajta iránymutatást, hogy meddig érdemes eljutni, valamint egy tömör referenciát,
amit később újra és újra fel lehet lapozni.</p>
<p>A teljesség kedvéért hadd említsem még meg a magyar nyelven megjelent Python programozással foglalkozó könyveket.
Az egyik a Mark Summerfield: <em>Python 3 programozás</em>. Itt egy fordításról van szó. A könyv több, mint 500 oldal,
és referenciaszerűen tárgyalja a Python programozási nyelvet. Ezt a könyvet nem ajánlom kezdőknek,
inkább a tudás csiszolására, esetleg vizsgára való felkészülésre javaslom. A fordítást sok helyen
meg kell fejteni, emiatt, és a szakmai tömörsége miatt az olvasásban gyakran meg kell állni, és elgondolkodni,
hogy pontosan hogy kell ezt értelmezni. Azonban van szó XML-ről, szálakról, hálózatkezelésről, adatbázis programozásról,
reguláris kifejezésekről és a GUI programozásról is.</p>
<p>Emellett még két könyv elérhető magyarul, ráadásul ingyen. Az egyik Gérard Swinnen: <em>Tanuljunk meg programozni Python nyelven</em>,
mely <a href="https://mek.oszk.hu/08400/08435/08435.pdf">letölthető</a> a Magyar Elektronikus könyvtárból. Ez is fordítás, és
abszolút kezdőknek szól. Ez azonban sajnos elavult, mert a Python 2-es verzióját tárgyalja.</p>
<p>A másik a Peter Wentworth, Jeffrey Elkner, Allen B. Downey, and Chris Meyers: <em>Hogyan gondolkozz úgy, mint egy informatikus: tanulás Python3 segítségével</em>,
mely szintén fordítás. A könyv szintén teljesen kezdőknek való és már a Python 3-as verziójáról szól.
Én sokat tanítottam az előző és ebből a könyvből is, arra tökéletesen megfelel. Bár ez hamar behozza a teknőcgrafikát,
aminek én annyira nem vagyok nagy barátja. Ez utóbbi könyveket nem ajánlom olyan szakembereknek,
akik már ismernek egy másik programozási nyelvet.</p>
<p>És bónuszként említeném meg Carol Vorderman: <em>Programozás gyerekeknek</em> című könyvét, melynek fordítása
a HVG Kiadó gondozásában jelent meg. Ez egy rendkívül szórakoztató könyv a gyerekeknek,
mely először a Scratch világába vezet be, és arról nyergel át a Python nyelvre.
A gyerekek izlésének tökéletesen megfelel, tele van színes képekkel, apró, sikerélményt biztosító
gyakorlati játékokkal, feladatokkal, és még kitekintést is ad, pl. hogy hogyan épül fel a
számítógép, illetve mit kell tudni valóságban a programozásról, milyen programozási nyelvek vannak,
hogyan lehet mobilalkalmazást fejleszteni, és mik azok a vírusok. Ez a könyv sok közös vidám órát
szerzett a kisfiammal, pláne az online oktatás időszakában.</p>
DLNA, az otthoni médiahálózat alapja2022-01-05T09:00:00+00:00http://www.jtechlog.hu/2022/01/05/dlna-otthoni-mediahalozat<p>Amikor itthon elkezdtem egy médiahálózatot kialakítani, akkor azonnal belefutottam a DLNA-ba, és mindig
is érdekelt, hogy pontosan hogyan működik. Otthoni médiahálózatnak nevezem azt, mikor egy lokális hálózatra
több eszköz is csatlakozik, mely vagy tárol, vagy lejátszik médiatartalmat, pl. képet, zenét, filmet.
Elvárás ezekkel kapcsolatban, hogy a hálózaton együtt tudjanak működni.</p>
<p>A médiatartalmat valamilyen háttértáron (winchester) tároljuk. Ilyen lehet pl. az asztali számítógépbe, vagy
laptopba épített HDD vagy SSD. Ide tartoznak a NAS-ok (Network Attached Storage), melyekben szintén ilyen
eszközök dolgoznak. De költséghatékony megoldás lehet egy Raspberry PI-hoz (sőt, akár közvetlen routerhez)
USB-vel csatlakoztatott külső merevlemez is.</p>
<p>Lejátszó eszköz lehet pl. az okostévé, okostelefon, asztali multimédia lejátszók
(Google Chromecast, Apple TV, Android TV-t futtató eszközök, bár nem akartam leírni ezt a szót, de “TV-okosítók”, stb.),
de lehet akár bármelyik asztali számítógép vagy laptop. (Ebből is látszik, hogy egy eszköz egyben akár médiatartalmat
tároló és lejátszó is lehet.) Sőt a játékkonzolok is képesek tartalmat lejátszani.</p>
<p>Ha már ezek ugyanazon a hálózaton vannak elvárás lehet, hogy a médiatartalmat tárolók a médiatartalmat más eszközökkel is
meg tudják osztani, és a lejátszók pedig bármelyik eszközön tárolt tartalmat le tudják játszani. És lehetőleg nekünk ezzel
a legkevesebb dolgunk legyen.</p>
<p>Pontosan ennek a megoldására alakult meg a Digital Living Network Alliance (DLNA) szövetség, akik kidolgozták a
pontosan ezt a nevet viselő szabványt is. Ez a gyakran emlegetett Universal Plug and Play (UPnP) szabványra épül,
ami pedig olyan hálózati protokollok összessége, melyek lehetővé teszik, hogy a hálózatra kötött
eszközök mindenféle beállítás nélkül megtalálják egymást, és együtt tudjanak működni. Ez az ún.
zero-configuration networking.</p>
<p>Ebben a posztban protokoll szinten bemutatom a DLNA-t, Python kódokkal érthetőbbé téve.</p>
<!-- more -->
<h2 id="választott-megoldásom">Választott megoldásom</h2>
<p>Nálam ez korábban úgy működött, hogy van egy Ubuntu Linuxot futtató eszköz, beépített merevlemezzel,
media server szoftverrel, melyről gyakorlatilag bármely képernyővel rendelkező eszköz képes
videókat lejátszani. Ezt cseréltem le egy Raspberry Pi-re.</p>
<p>Egy media server szoftver képes indexelni az eszközön található médiatartalmat, valamint
a többi eszközt kiszolgálni, a videót streamelni. A legismertebb ezek közül a Kodi és a Plex.</p>
<p>Azonban ezek elég nehézsúlyú eszközök, nagyobb erőforrásigénnyel. Olyan szoftvert kerestem, ami
pont annyit tud, amennyire szükségem van. Sokáig az elterjedt MiniDLNA-t használtam, amit azóta átneveztek
<a href="https://sourceforge.net/projects/minidlna/">ReadyMedia-ra</a>. Azonban ennek a fejlesztése eléggé leállt,
így másik megoldás után kellett néznem.
Az egyik versenyző a <a href="https://github.com/UniversalMediaServer/UniversalMediaServer/">Universal Media Server</a> volt,
azonban a tudat, hogy Javaban implementálták, valamint a szegényes dokumentációja elvette tőle a kedvem.
A befutó a MiniDLNA lecserélésében a <a href="https://gerbera.io/">Gerbera</a> lett. Nyílt forráskódú, C++ nyelven
implementálták, jó a dokumentációja, van elérhető Docker image a legtöbb platformon, és JavaScriptben
pluginelhető. (Ez a MediaTomb forkja, mely azóta megszűnt.)</p>
<p>Media playerként asztali számítógépen a <a href="https://www.videolan.org/vlc/index.html">VLC media playert</a>
használtam, mobilon a VLC for Mobile-t. A tévém egy LG okostévé, melynek Smart Share szolgáltatása
képes a media server által szolgáltatott tartalmat lejátszani. (Ebből is látszik, hogy bizonyos cégek valamilyen
fantázianév mögé bújtatják a DLNA-t.)</p>
<p><img src="/artifacts/posts/2022-01-05-dlna-otthoni-mediahalozat/dlna-network.png" alt="Médiahálózat" /></p>
<h2 id="dlna-működésének-bemutatása">DLNA működésének bemutatása</h2>
<p>Hogyan is néz ez ki technológiailag? Az egyszerűség kedvéért a tartalmat tároló eszköz legyen a DLNA szerver,
a lejátszó pedig a DLNA kliens.</p>
<ul>
<li>A DLNA kliens felderíti a hálózatra kötött szervereket, ehhez a UPnP SSDP (Simple Service Discovery Protocol)
protokollt használja. A hálózatra kiküld egy multicast üzenetet, melyre a szerverek válaszolnak.</li>
<li>A DLNA kliens a választott szervertől lekéri annak képességeit, SCPD (Service Control Point Definition)
protokollt használva. Ilyen képesség, hogy a szerver ki tudja listázni a tárolt tartalmakat. Ezeket a képességeket
szolgáltatásokon keresztül lehet igénybe venni.</li>
<li>A DLNA kliens meghívja a kiválasztott szolgáltatást, pl. lekéri a tárolt tartalmak listáját. A szerver a válaszban
visszaküldi a tárolt tartalmak adatait (címét, formátumát, méretét, létrehozás/módosítás dátumát),
de legfőképp az elérhetőségét, mely egy url.</li>
<li>A DLNA kliens az adott url-ről lejátsza a tartalmat.</li>
</ul>
<p>Nézzük ezt még részletesebben!</p>
<h2 id="ssdp-felderítés">SSDP: felderítés</h2>
<p>A DLNA kliens kiküld a <code class="language-plaintext highlighter-rouge">239.255.255.250</code> ip-re, <code class="language-plaintext highlighter-rouge">1900</code>-as portra egy UDP multicast üzenetet. Ez valójában egy
HTTPU (HTTP over UDP) üzenet, mely a HTTP-hez hasonló, szöveges, kérés-válasz protokoll, üzenet fejléccel és törzzsel.</p>
<p>Maga az üzenet tartalma a következő:</p>
<pre><code class="language-plain">M-SEARCH * HTTP/1.1
HOST:239.255.255.250:1900
ST:upnp:rootdevice
MX:2
MAN:"ssdp:discover"
</code></pre>
<ul>
<li><code class="language-plaintext highlighter-rouge">M-SEARCH</code> a HTTP metódus, a <code class="language-plaintext highlighter-rouge">*</code> karakter vonatkozik arra, hogy nem egy erőforrást kérünk le. Majd jön a protokoll. A következő sorok alkotják az üzenet fejlécét.</li>
<li><code class="language-plaintext highlighter-rouge">HOST</code> tartalmazza az üzenet címzettjét.</li>
<li><code class="language-plaintext highlighter-rouge">MAN</code> az extension scope, értéke kötelezően <code class="language-plaintext highlighter-rouge">"ssdp:discover"</code>, vigyázat, szigorúan idézőjelek között.</li>
<li><code class="language-plaintext highlighter-rouge">ST</code> a search target. Itt un. root device-ot keresünk, melyek nincsenek más device-okba ágyazva.</li>
<li><code class="language-plaintext highlighter-rouge">MX</code> a maximum wait, azaz mennyit várunk a válaszra, másodpercben.</li>
</ul>
<p>Erre válaszolnak a DLNA szerverek, valami hasonló üzenettel:</p>
<pre><code class="language-plain">HTTP/1.1 200 OK
CACHE-CONTROL: max-age=1800
DATE: Thu, 06 Jan 2022 20:49:26 GMT
EXT:
SERVER: Linux/5.10.63-v7+, UPnP/1.0, Portable SDK for UPnP devices/1.14.0
ST: upnp:rootdevice
USN: uuid:93131041-22f0-48fa-a6b5-9af718bbc5ae::upnp:rootdevice
LOCATION: http://192.168.0.145:49494/description.xml
</code></pre>
<p>Ezek csak a tipikus fejlécek, ezen kívül más fejlécek is előfordulnak.</p>
<ul>
<li><code class="language-plaintext highlighter-rouge">HTTP/1.1 200 OK</code> jelenti a sikeres választ.</li>
<li><code class="language-plaintext highlighter-rouge">CACHE-CONTROL</code> visszaadott eredményt meddig cache-elheti a kliens.</li>
<li><code class="language-plaintext highlighter-rouge">DATE</code> a válasz előállításának ideje.</li>
<li><code class="language-plaintext highlighter-rouge">EXT</code> visszafele kompatibilitási okokból legyen benne.</li>
<li><code class="language-plaintext highlighter-rouge">SERVER</code> egy szöveges leírás a szerverről.</li>
<li><code class="language-plaintext highlighter-rouge">ST</code> a típusa, itt egy root device.</li>
<li><code class="language-plaintext highlighter-rouge">USN</code> egyedi azonosítója.</li>
<li><code class="language-plaintext highlighter-rouge">LOCATION</code> egy url, ahonnan letölthetőek a DLNA szerver részletes adatai.</li>
</ul>
<p>És mit is ér az egész, ha nem próbáljuk ki egy Python szkripttel, hogy mi van,
ha kiküldünk egy <code class="language-plaintext highlighter-rouge">M-SEARCH</code> kérést a hálózatra.</p>
<p>A Python példaprogramok megtalálhatóak a <a href="https://github.com/vicziani/jtechlog-dlna-python">GitHubon</a>.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">socket</span>
<span class="n">msg</span> <span class="o">=</span> <span class="s">'''M-SEARCH * HTTP/1.1
HOST:239.255.255.250:1900
ST:upnp:rootdevice
MX:2
MAN:"ssdp:discover"
'''</span><span class="p">.</span><span class="n">replace</span><span class="p">(</span><span class="s">"/n"</span><span class="p">,</span> <span class="s">"/r/n"</span><span class="p">)</span>
<span class="n">client_socket</span> <span class="o">=</span> <span class="n">socket</span><span class="p">.</span><span class="n">socket</span><span class="p">(</span><span class="n">socket</span><span class="p">.</span><span class="n">AF_INET</span><span class="p">,</span> <span class="n">socket</span><span class="p">.</span><span class="n">SOCK_DGRAM</span><span class="p">,</span> <span class="n">socket</span><span class="p">.</span><span class="n">IPPROTO_UDP</span><span class="p">)</span>
<span class="n">client_socket</span><span class="p">.</span><span class="n">setsockopt</span><span class="p">(</span><span class="n">socket</span><span class="p">.</span><span class="n">IPPROTO_IP</span><span class="p">,</span> <span class="n">socket</span><span class="p">.</span><span class="n">IP_MULTICAST_TTL</span><span class="p">,</span> <span class="mi">2</span><span class="p">)</span>
<span class="n">client_socket</span><span class="p">.</span><span class="n">bind</span><span class="p">((</span><span class="s">"192.168.0.213"</span><span class="p">,</span> <span class="mi">1901</span><span class="p">))</span> <span class="c1"># 1.
</span><span class="n">client_socket</span><span class="p">.</span><span class="n">settimeout</span><span class="p">(</span><span class="mi">2</span><span class="p">)</span>
<span class="n">client_socket</span><span class="p">.</span><span class="n">sendto</span><span class="p">(</span><span class="n">msg</span><span class="p">.</span><span class="n">encode</span><span class="p">(</span><span class="s">"utf-8"</span><span class="p">),</span> <span class="p">(</span><span class="s">'239.255.255.250'</span><span class="p">,</span> <span class="mi">1900</span><span class="p">))</span>
<span class="k">try</span><span class="p">:</span>
<span class="k">while</span> <span class="bp">True</span><span class="p">:</span>
<span class="n">data</span><span class="p">,</span> <span class="n">addr</span> <span class="o">=</span> <span class="n">client_socket</span><span class="p">.</span><span class="n">recvfrom</span><span class="p">(</span><span class="mi">65507</span><span class="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="n">addr</span><span class="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="n">data</span><span class="p">.</span><span class="n">decode</span><span class="p">(</span><span class="s">"utf-8"</span><span class="p">))</span>
<span class="k">except</span> <span class="n">socket</span><span class="p">.</span><span class="n">timeout</span><span class="p">:</span>
<span class="k">pass</span>
</code></pre></div></div>
<p>Nálam erre azonnal válaszolt a Raspberry Pi-n futó Gerbera szerver, az LG TV és
a UPC routerem (!!!) is.</p>
<p>Amit érdemes megjegyezni, hogy a kimenő üzenetnél sorvége karakternek nem elég a <code class="language-plaintext highlighter-rouge">\n</code>, ki kell
cserélni <code class="language-plaintext highlighter-rouge">\r\n</code> karakterekre. Valamint a biztonság kedvéért legyen a végén
két sortörés.</p>
<p>Itt azonban hamar belefutottam egy problémába. Az <code class="language-plaintext highlighter-rouge">1.</code>-essel jelölt sor nélkül
a kérés nem arra a hálózati interfészre került elküldésre, melyre szerettem volna,
így nem kapták meg a másik hálózaton található eszközök, és nem is válaszoltak rá.
Mivel van Docker a gépemen, van egy <code class="language-plaintext highlighter-rouge">vEthernet (Default Switch)</code> és egy
<code class="language-plaintext highlighter-rouge">vEthernet (WSL)</code> hálózati interfész, melyek a HyperV-hez, illetve a
Windows Subsystem for Linux-hoz tartoznak. Az UDP üzenetet ezek egyikére küldte ki,
amit csak Wireshark használatával sikerült kinyomoznom.</p>
<h2 id="gerbera-elindítása-dockerben">Gerbera elindítása Dockerben</h2>
<p>Amennyiben nincs semmilyen DLNA server a hálózaton, használhatjuk a Gerberat.
Linuxon futtassuk, ugyanis a Gerbera sajnos Windowson nem futtatható.</p>
<p>Azonban futtatható Docker konténerben is, ha csak ideiglenesen akarjuk kipróbálni.
Sőt, én Raspberry Pi-ra nem találtam feltelepíthető csomagot, ezért azon is
Docker konténerben futtatom.</p>
<p>Erre a következő parancs használható:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker run <span class="nt">-d</span> <span class="nt">--name</span> my-gerbera <span class="nt">--network</span> host <span class="nt">--privileged</span> <span class="nt">--restart</span> unless-stopped <span class="se">\</span>
<span class="nt">-v</span> /home/pi/gerbera:/var/run/gerbera <span class="nt">-v</span> /home/pi/Videos:/content:ro gerbera/gerbera
</code></pre></div></div>
<p>Itt is érdemes átnézni néhány kapcsolót. A <code class="language-plaintext highlighter-rouge">--privileged</code> kapcsolóval a konténer
olyan jogokat kap a host gépen, mintha a processt konténeren kívül futtatnánk.</p>
<p>A legérdekesebb a <code class="language-plaintext highlighter-rouge">--network host</code> kapcsoló. Ekkor a konténer nem kap saját
IP-címet, hanem a host hálózatához kapcsolódik. Látható tehát, hogy a Gerbera
futtatásánál tehát nem a teljes izoláció volt a célom, csupán annyi, hogy
ne kelljen telepíteni a host operációs rendszerre. Ekkor nem is kell a
<code class="language-plaintext highlighter-rouge">-p</code> kapcsolóval a portokat kihozni.</p>
<p>Ez azért nagyon fontos, mert ha a konténer saját ip-t kap, akkor
nem felel az UDP multicast üzenetekre.</p>
<p>Ezt a Gerbera fejlesztői is írják, hogy <a href="https://github.com/gerbera/gerbera/issues/1280">az UDP multicast nem proxy-zható</a>,
így a saját címét sem képes behazudni a válasz üzenetekben.</p>
<p>És itt kell kitérnem arra is, hogy sajnos Windowson Docker konténerben
sem lehet futtatni a Gerberat. Ez azért van, mert a <code class="language-plaintext highlighter-rouge">--network host</code>
kapcsoló Windows rendszeren <a href="https://docs.docker.com/network/host/">nem működik</a>.</p>
<p>A <code class="language-plaintext highlighter-rouge">-v /home/pi/gerbera:/var/run/gerbera</code>
kapcsolóval a host <code class="language-plaintext highlighter-rouge">/home/pi/gerbera</code> könyvtárát mountolom a
konténer <code class="language-plaintext highlighter-rouge">/var/run/gerbera</code> könyvtárára. Ez azért jó, mert a konfigurációs állomány,
a <code class="language-plaintext highlighter-rouge">config.xml</code>, nem a konténerben lesz, hanem a hoston. A <code class="language-plaintext highlighter-rouge">-v /home/pi/Videos:/content:ro</code>
kapcsolóval a host <code class="language-plaintext highlighter-rouge">/home/pi/Videos</code> könyvtárát mountolom a konténer <code class="language-plaintext highlighter-rouge">/content</code>
könyvtárára, read-only módban. A Gerbera ugyanis alapértelmezésben a
<code class="language-plaintext highlighter-rouge">/content</code> könyvtárat olvassa be.</p>
<p>Ekkor a szerver elérhető a <code class="language-plaintext highlighter-rouge">http://localhost:49494</code> címen, vagy lokális hálózaton
pl. a <code class="language-plaintext highlighter-rouge">http://192.168.0.145</code> címen. Ezért volt a <code class="language-plaintext highlighter-rouge">http://192.168.0.145:49494/description.xml</code>
cím látható az SSDP válaszüzenetben.</p>
<p><img src="/artifacts/posts/2022-01-05-dlna-otthoni-mediahalozat/gerbera.png" alt="Gerbera" /></p>
<p>Ezután már a VLC-ben is meg fog jelenni, ha a <em>Nézet / Lejátszólista</em> menüpontban
kiválasztjuk az <em>Univerzális Plug’n’Play</em> elemet.</p>
<p><img src="/artifacts/posts/2022-01-05-dlna-otthoni-mediahalozat/vlc-dlna.png" alt="VNC DLNA" /></p>
<h2 id="saját-udp-szerver">Saját UDP szerver</h2>
<p>Természetesen egy kis UDP szervert írtam Pythonban, ami válaszol az előbbi kliensnek.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">socket</span> <span class="kn">import</span> <span class="o">*</span>
<span class="kn">from</span> <span class="nn">kiss_headers</span> <span class="kn">import</span> <span class="n">parse_it</span>
<span class="kn">import</span> <span class="nn">platform</span>
<span class="n">INTERFACE_IP</span> <span class="o">=</span> <span class="s">"192.168.0.213"</span>
<span class="k">print</span><span class="p">(</span><span class="s">"Running server"</span><span class="p">)</span>
<span class="n">udp_server</span> <span class="o">=</span> <span class="n">socket</span><span class="p">(</span><span class="n">AF_INET</span><span class="p">,</span> <span class="n">SOCK_DGRAM</span><span class="p">)</span>
<span class="n">udp_server</span><span class="p">.</span><span class="n">bind</span><span class="p">((</span><span class="s">""</span><span class="p">,</span> <span class="mi">1900</span><span class="p">))</span>
<span class="n">mreq</span> <span class="o">=</span> <span class="n">inet_aton</span><span class="p">(</span><span class="s">"239.255.255.250"</span><span class="p">)</span> <span class="o">+</span> <span class="n">inet_aton</span><span class="p">(</span><span class="n">INTERFACE_IP</span><span class="p">)</span>
<span class="n">udp_server</span><span class="p">.</span><span class="n">setsockopt</span><span class="p">(</span><span class="n">IPPROTO_IP</span><span class="p">,</span> <span class="n">IP_ADD_MEMBERSHIP</span><span class="p">,</span> <span class="n">mreq</span><span class="p">)</span>
<span class="k">while</span> <span class="bp">True</span><span class="p">:</span>
<span class="n">data</span><span class="p">,</span> <span class="n">address</span> <span class="o">=</span> <span class="n">udp_server</span><span class="p">.</span><span class="n">recvfrom</span><span class="p">(</span><span class="mi">1500</span><span class="p">)</span>
<span class="n">text</span> <span class="o">=</span> <span class="n">data</span><span class="p">.</span><span class="n">decode</span><span class="p">(</span><span class="s">"utf-8"</span><span class="p">)</span>
<span class="n">method</span> <span class="o">=</span> <span class="n">text</span><span class="p">.</span><span class="n">split</span><span class="p">()[</span><span class="mi">0</span><span class="p">]</span>
<span class="n">headers</span> <span class="o">=</span> <span class="n">parse_it</span><span class="p">(</span><span class="n">text</span><span class="p">)</span>
<span class="k">if</span> <span class="n">method</span> <span class="o">==</span> <span class="s">"M-SEARCH"</span> <span class="ow">and</span> <span class="n">headers</span><span class="p">.</span><span class="n">ST</span> <span class="o">==</span> <span class="s">"upnp:rootdevice"</span><span class="p">:</span>
<span class="k">print</span><span class="p">(</span><span class="s">"Handle M-SEARCH"</span><span class="p">)</span>
<span class="n">response</span> <span class="o">=</span> <span class="sa">f</span><span class="s">"""HTTP/1.1 200 OK
EXT:
LOCATION: http://</span><span class="si">{</span><span class="n">INTERFACE_IP</span><span class="si">}</span><span class="s">:8080/rootDesc.xml
SERVER: </span><span class="si">{</span><span class="n">platform</span><span class="p">.</span><span class="n">system</span><span class="p">()</span><span class="si">}</span><span class="s">/</span><span class="si">{</span><span class="n">platform</span><span class="p">.</span><span class="n">release</span><span class="p">()</span><span class="si">}</span><span class="s">, UPnP/1.0, JTechLog UPnP Server 0.0.1
ST: upnp:rootdevice
USN: uuid:fea4bf14-6da5-11ec-90d6-0242ac120003::upnp:rootdevice
"""</span><span class="p">.</span><span class="n">replace</span><span class="p">(</span><span class="s">"</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="s">"</span><span class="se">\r\n</span><span class="s">"</span><span class="p">)</span>
<span class="n">udp_server</span><span class="p">.</span><span class="n">sendto</span><span class="p">(</span><span class="n">response</span><span class="p">.</span><span class="n">encode</span><span class="p">(</span><span class="s">"utf-8"</span><span class="p">),</span> <span class="n">address</span><span class="p">)</span>
</code></pre></div></div>
<p>Ennek futtatásához azonban kell egy külső függőség is, a <code class="language-plaintext highlighter-rouge">kiss_headers</code>, ezzel tudom a
legegyszerűbben parse-olni a válasz fejléceket.</p>
<p>Persze ekkor már belefutottam abba, hogy a 1900-as portot foglalta a Spotify.</p>
<p>Ennek kinyomozása Windowson:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>netstat <span class="nt">-ano</span> | findstr <span class="s2">"1900"</span>
tasklist | findstr <span class="s2">"10380"</span>
</code></pre></div></div>
<p>Az első parancs megkeresi, hogy melyik pid-del rendelkező folyamat foglalja az 1900-as
portot, a második pedig kikeresi, hogy az adott pid-hez valójában melyik alkalmazás tartozik.</p>
<p>Ezt futtatva az is kiderült, hogy a lokális hálózaton egyrészt a UPC router is dobál <code class="language-plaintext highlighter-rouge">NOTIFY</code> UDP
broadcast üzeneteket, valamint a Google Chrome is <code class="language-plaintext highlighter-rouge">M-SEARCH</code> üzeneteket, melyek
az ún. <a href="https://chromium.googlesource.com/chromium/src.git/+/refs/heads/main/docs/media/media_router.md">Chrome Media Routerhez</a>
tartoznak.</p>
<h2 id="scpd-a-szerver-képességeinek-meghatározása">SCPD, a szerver képességeinek meghatározása</h2>
<p>A <code class="language-plaintext highlighter-rouge">LOCATION</code> fejlécben szereplő címet már egyszerű HTTP <code class="language-plaintext highlighter-rouge">GET</code> metódussal kell lekérni, és
ekkor valami hasonló XML-t kapunk:</p>
<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt"><root</span> <span class="na">xmlns=</span><span class="s">"urn:schemas-upnp-org:device-1-0"</span> <span class="na">xmlns:sec=</span><span class="s">"http://www.sec.co.kr/dlna"</span><span class="nt">></span>
<span class="nt"><specVersion></span>
<span class="nt"><major></span>1<span class="nt"></major></span>
<span class="nt"><minor></span>0<span class="nt"></minor></span>
<span class="nt"></specVersion></span>
<span class="nt"><device></span>
<span class="nt"><dlna:X_DLNADOC</span> <span class="na">xmlns:dlna=</span><span class="s">"urn:schemas-dlna-org:device-1-0"</span><span class="nt">></span>DMS-1.50<span class="nt"></dlna:X_DLNADOC></span>
<span class="nt"><friendlyName></span>Gerbera<span class="nt"></friendlyName></span>
<span class="nt"><manufacturer></span>Gerbera Contributors<span class="nt"></manufacturer></span>
<span class="nt"><modelDescription></span>Free UPnP AV MediaServer, GNU GPL<span class="nt"></modelDescription></span>
<span class="nt"><UDN></span>uuid:93131041-22f0-48fa-a6b5-9af718bbc5ae<span class="nt"></UDN></span>
<span class="nt"><deviceType></span>urn:schemas-upnp-org:device:MediaServer:1<span class="nt"></deviceType></span>
<span class="nt"><presentationURL></span>http://192.168.0.145:49494/<span class="nt"></presentationURL></span>
<span class="nt"><iconList></span>
<span class="nt"><icon></span>
<span class="nt"><mimetype></span>image/png<span class="nt"></mimetype></span>
<span class="nt"><width></span>120<span class="nt"></width></span>
<span class="nt"><height></span>120<span class="nt"></height></span>
<span class="nt"><depth></span>24<span class="nt"></depth></span>
<span class="nt"><url></span>/icons/mt-icon120.png<span class="nt"></url></span>
<span class="nt"></icon></span>
<span class="nt"></iconList></span>
<span class="nt"><serviceList></span>
<span class="nt"><service></span>
<span class="nt"><serviceType></span>urn:schemas-upnp-org:service:ContentDirectory:1<span class="nt"></serviceType></span>
<span class="nt"><serviceId></span>urn:upnp-org:serviceId:ContentDirectory<span class="nt"></serviceId></span>
<span class="nt"><SCPDURL></span>cds.xml<span class="nt"></SCPDURL></span>
<span class="nt"><controlURL></span>/upnp/control/cds<span class="nt"></controlURL></span>
<span class="nt"><eventSubURL></span>/upnp/event/cds<span class="nt"></eventSubURL></span>
<span class="nt"></service></span>
<span class="nt"></serviceList></span>
<span class="nt"></device></span>
<span class="nt"><URLBase></span>http://192.168.0.145:49494/<span class="nt"></URLBase></span>
<span class="nt"></root></span>
</code></pre></div></div>
<p>Ebből pár lényegtelen taget eltávolítottam. Ami talán egyértelműen látszik, hogy ez
az XML írja le a DLNA serverünket, ez jelenik meg pl. a VLC-ben, vagy az okostévén.
A neve a <code class="language-plaintext highlighter-rouge"><friendlyName></code> tagen belül található, átküldésre kerül az ikonjának
az elérhetősége is, de a legfontosabb talán a <code class="language-plaintext highlighter-rouge"><serviceList></code> tagen belüli
szolgáltatás leírások. Ebből látható, hogy a <code class="language-plaintext highlighter-rouge">ContentDirectory</code>
szolgáltatás leírása elérhető a <code class="language-plaintext highlighter-rouge">cds.xml</code> címen. Így a <code class="language-plaintext highlighter-rouge">http://192.168.0.145:49494/cds.xml</code>
címen a következő XML-t kapjuk vissza.</p>
<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt"><scpd</span> <span class="na">xmlns=</span><span class="s">"urn:schemas-upnp-org:service-1-0"</span><span class="nt">></span>
<span class="nt"><specVersion></span>
<span class="nt"><major></span>1<span class="nt"></major></span>
<span class="nt"><minor></span>0<span class="nt"></minor></span>
<span class="nt"></specVersion></span>
<span class="nt"><actionList></span>
<span class="nt"><action></span>
<span class="nt"><name></span>Browse<span class="nt"></name></span>
<span class="nt"><argumentList></span>
<span class="nt"><argument></span>
<span class="nt"><name></span>ObjectID<span class="nt"></name></span>
<span class="nt"><direction></span>in<span class="nt"></direction></span>
<span class="nt"><relatedStateVariable></span>A_ARG_TYPE_ObjectID<span class="nt"></relatedStateVariable></span>
<span class="nt"></argument></span>
<span class="nt"><argument></span>
<span class="nt"><name></span>BrowseFlag<span class="nt"></name></span>
<span class="nt"><direction></span>in<span class="nt"></direction></span>
<span class="nt"><relatedStateVariable></span>A_ARG_TYPE_BrowseFlag<span class="nt"></relatedStateVariable></span>
<span class="nt"></argument></span>
<span class="c"><!-- ... --></span>
<span class="nt"></argumentList></span>
<span class="nt"></action></span>
<span class="c"><!-- ... --></span>
<span class="nt"></actionList></span>
<span class="c"><!-- ... --></span>
<span class="nt"></scpd></span>
</code></pre></div></div>
<p>Ez szintén egy erősen megrövidített lista, a lényeg azonban látható. A
<code class="language-plaintext highlighter-rouge">ContentDirectory</code> szolgáltatás tartalmaz egy <code class="language-plaintext highlighter-rouge">Browse</code>
actiont, melynek a bemeneti és kimeneti paraméterei is fel vannak sorolva.
Ez az action való arra, hogy a médiatartalmat listázni tudjuk.</p>
<h2 id="action-meghívása-médiatartalmak-lekérdezése">Action meghívása: médiatartalmak lekérdezése</h2>
<p>Az Action meghívása SOAP over HTTP formátumban/protokollon történik.
Igen-igen, ki hinné, hogy ilyen elavult technológiák vannak.
Ez egy HTTP <code class="language-plaintext highlighter-rouge">POST</code> hívás, ahol a törzsben egy XML utazik.</p>
<p>Ezt már nem akartam leprogramozni, hanem helyette a Python
<code class="language-plaintext highlighter-rouge">upnpy</code> könyvtárát használtam.</p>
<p>Itt megint okozott bonyodalmat a több hálózati interfész,
de csak sikerült meghívnom a <code class="language-plaintext highlighter-rouge">socket</code> <code class="language-plaintext highlighter-rouge">bind()</code> metódusát.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">upnpy</span>
<span class="n">upnp</span> <span class="o">=</span> <span class="n">upnpy</span><span class="p">.</span><span class="n">UPnP</span><span class="p">()</span>
<span class="n">upnp</span><span class="p">.</span><span class="n">ssdp</span><span class="p">.</span><span class="n">socket</span><span class="p">.</span><span class="n">bind</span><span class="p">((</span><span class="s">"192.168.0.213"</span><span class="p">,</span> <span class="mi">1901</span><span class="p">))</span>
<span class="n">devices</span> <span class="o">=</span> <span class="n">upnp</span><span class="p">.</span><span class="n">discover</span><span class="p">()</span>
<span class="k">print</span><span class="p">(</span><span class="n">devices</span><span class="p">)</span>
<span class="n">device</span> <span class="o">=</span> <span class="nb">next</span><span class="p">(</span><span class="n">device</span> <span class="k">for</span> <span class="n">device</span> <span class="ow">in</span> <span class="n">devices</span> <span class="k">if</span> <span class="n">device</span><span class="p">.</span><span class="n">friendly_name</span> <span class="o">==</span> <span class="s">"Gerbera"</span><span class="p">)</span>
<span class="n">services</span> <span class="o">=</span> <span class="n">device</span><span class="p">.</span><span class="n">get_services</span><span class="p">()</span>
<span class="k">print</span><span class="p">(</span><span class="n">services</span><span class="p">)</span>
<span class="n">service</span> <span class="o">=</span> <span class="n">device</span><span class="p">[</span><span class="s">"ContentDirectory"</span><span class="p">]</span>
<span class="k">print</span><span class="p">(</span><span class="n">service</span><span class="p">.</span><span class="n">get_actions</span><span class="p">())</span>
<span class="n">result</span> <span class="o">=</span> <span class="n">service</span><span class="p">.</span><span class="n">Browse</span><span class="p">(</span><span class="n">ObjectID</span><span class="o">=</span><span class="mi">0</span><span class="p">,</span> <span class="n">BrowseFlag</span><span class="o">=</span><span class="s">"BrowseDirectChildren"</span><span class="p">,</span> <span class="n">Filter</span><span class="o">=</span><span class="s">"*"</span><span class="p">,</span> <span class="n">StartingIndex</span><span class="o">=</span><span class="mi">0</span><span class="p">,</span> <span class="n">RequestedCount</span><span class="o">=</span><span class="mi">5000</span><span class="p">,</span>
<span class="n">SortCriteria</span><span class="o">=</span><span class="s">""</span><span class="p">)</span>
<span class="n">xml</span> <span class="o">=</span> <span class="n">result</span><span class="p">[</span><span class="s">"Result"</span><span class="p">]</span>
<span class="k">print</span><span class="p">(</span><span class="n">xml</span><span class="p">)</span>
</code></pre></div></div>
<p>Itt a paraméterek megadásával gyűlt meg a bajom, hogy milyen értékeket kell átadnom. Ehhez
megint csak a Wiresharkot hívtam segítségül, hogy pontosan milyen kérést is ad ki. Alább látható,
persze az eredeti kérés nem formázott.
Ezután már egyszerű volt a paramétereket visszafejteni.</p>
<pre><code class="language-plain">POST /upnp/control/cds HTTP/1.1
HOST: 192.168.0.145:49494
CONTENT-LENGTH: 440
CONTENT-TYPE: text/xml; charset="utf-8"
SOAPACTION: "urn:schemas-upnp-org:service:ContentDirectory:1#Browse"
USER-AGENT: 6.2.9200 2/, UPnP/1.0, Portable SDK for UPnP devices/1.6.19
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"
s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<s:Body>
<u:Browse xmlns:u="urn:schemas-upnp-org:service:ContentDirectory:1">
<ObjectID>0</ObjectID>
<BrowseFlag>BrowseDirectChildren</BrowseFlag>
<Filter>*</Filter>
<StartingIndex>0</StartingIndex>
<RequestedCount>5000</RequestedCount>
<SortCriteria></SortCriteria>
</u:Browse>
</s:Body>
</s:Envelope>
</code></pre>
<p>Itt ismét egy szép XML jött vissza:</p>
<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp"><?xml version="1.0" encoding="UTF-8"?></span>
<span class="nt"><DIDL-Lite</span> <span class="na">xmlns=</span><span class="s">"urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/"</span> <span class="na">xmlns:dc=</span><span class="s">"http://purl.org/dc/elements/1.1/"</span>
<span class="na">xmlns:upnp=</span><span class="s">"urn:schemas-upnp-org:metadata-1-0/upnp/"</span> <span class="na">xmlns:sec=</span><span class="s">"http://www.sec.co.kr/dlna"</span><span class="nt">></span>
<span class="nt"><container</span> <span class="na">id=</span><span class="s">"4"</span> <span class="na">parentID=</span><span class="s">"0"</span> <span class="na">restricted=</span><span class="s">"1"</span> <span class="na">childCount=</span><span class="s">"4"</span><span class="nt">></span>
<span class="nt"><dc:title></span>Video<span class="nt"></dc:title></span>
<span class="nt"><upnp:class></span>object.container<span class="nt"></upnp:class></span>
<span class="nt"></container></span>
<span class="nt"><item</span> <span class="na">id=</span><span class="s">"67"</span> <span class="na">parentID=</span><span class="s">"5"</span> <span class="na">restricted=</span><span class="s">"1"</span><span class="nt">></span>
<span class="nt"><dc:title></span>bbb sunflower 1080p 30fps normal<span class="nt"></dc:title></span>
<span class="nt"><upnp:class></span>object.item.videoItem<span class="nt"></upnp:class></span>
<span class="nt"><dc:created></span>2013-12-16<span class="nt"></dc:created></span>
<span class="nt"><dc:description></span>Creative Commons Attribution 3.0 - http://bbb3d.renderfarming.net<span class="nt"></dc:description></span>
<span class="nt"><upnp:artist></span>Blender Foundation 2008, Janus Bager Kristensen 2013<span class="nt"></upnp:artist></span>
<span class="nt"><upnp:composer></span>Sacha Goedegebure<span class="nt"></upnp:composer></span>
<span class="nt"><upnp:genre></span>Animation<span class="nt"></upnp:genre></span>
<span class="nt"><res</span> <span class="na">bitrate=</span><span class="s">"435178"</span> <span class="na">bitsPerSample=</span><span class="s">"16"</span> <span class="na">duration=</span><span class="s">"0:10:34.533"</span> <span class="na">nrAudioChannels=</span><span class="s">"2"</span>
<span class="na">protocolInfo=</span><span class="s">"http-get:*:video/mp4:DLNA.ORG_PN=AVC_MP4_EU;DLNA.ORG_OP=01;DLNA.ORG_CI=0"</span>
<span class="na">resolution=</span><span class="s">"1920x1080"</span> <span class="na">sampleFrequency=</span><span class="s">"48000"</span> <span class="na">sec:acodec=</span><span class="s">"mp3"</span> <span class="na">sec:vcodec=</span><span class="s">"h264"</span> <span class="na">size=</span><span class="s">"276134947"</span><span class="nt">></span>
http://192.168.0.145:49494/content/media/object_id/67/res_id/0/ext/file.mp4
<span class="nt"></res></span>
<span class="nt"></item></span>
<span class="nt"></DIDL-Lite></span>
</code></pre></div></div>
<p>És ezen már látszik, hogy a főkönyvtár tartalmát adja vissza, benne az alkönyvtárakat és
videó állományokat. A videó állományokról a címén és az url-jén kívül sok hasznos információ is megtalálható,
mint pl. a mérete, felbontása, hossza, formátuma, video és audio codec, stb. Sőt, ha a videófájlban
megtalálható, pl. a készítő, műfaja, létrehozás dátuma, megjegyzés, stb.</p>
<p>Ebből az információkat én már Pythonban, XPath használatával olvastam ki, melyhez az <code class="language-plaintext highlighter-rouge">lxml</code>
csomagot használtam. (Látható, hogy a névterek használata hogy megbonyolítja a kódot.) Az alábbi
kódrészlet a címeket írja ki.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">lxml.etree</span> <span class="k">as</span> <span class="n">etree</span>
<span class="n">root</span> <span class="o">=</span> <span class="n">etree</span><span class="p">.</span><span class="n">fromstring</span><span class="p">(</span><span class="n">xml</span><span class="p">.</span><span class="n">encode</span><span class="p">())</span>
<span class="k">for</span> <span class="n">element</span> <span class="ow">in</span> <span class="n">root</span><span class="p">.</span><span class="n">xpath</span><span class="p">(</span><span class="s">"//didl:item[./upnp:class[text() = 'object.item.videoItem']]/dc:title"</span><span class="p">,</span>
<span class="n">namespaces</span><span class="o">=</span><span class="p">{</span><span class="s">"dc"</span><span class="p">:</span> <span class="s">"http://purl.org/dc/elements/1.1/"</span><span class="p">,</span>
<span class="s">"didl"</span><span class="p">:</span> <span class="s">"urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/"</span><span class="p">,</span>
<span class="s">"upnp"</span><span class="p">:</span> <span class="s">"urn:schemas-upnp-org:metadata-1-0/upnp/"</span><span class="p">}):</span>
<span class="k">print</span><span class="p">(</span><span class="n">element</span><span class="p">.</span><span class="n">text</span><span class="p">)</span>
</code></pre></div></div>
<h1 id="saját-upnp-szerver">Saját UPnP szerver</h1>
<p>Ha az UDP szervert már megírtam Pythonban, akkor nem láttam akadályát, hogy egy teljes UPnP szervert
megírjak, hiszen innentől kezdve már csak a HTTP kéréseket kell kiszolgálni.</p>
<p>Ehhez viszont konkurens kéne elindítani egy UDP-n és egy TCP-n hallgató folyamatot. Ez egy remek
alkalom volt, hogy kipróbáljam a Python Async IO megoldását.</p>
<p>Az Async IO úgy oldja meg a konkurens futást, hogy valójában egy szál dolgozik, azonban
az egyik feladat IO-ra vár, addig a szál tud a másik feladat számításigényes dolgaival foglalkozni.</p>
<p>A Real Python oldalon egy <a href="https://realpython.com/async-io-python/">remek példa található</a>
az aszinkron működésre, Miguel Grinberg 2017 PyCon konferencián tartott beszédéből, ahol az
aszinkron működést Polgár Judit szimultán sakkjához hasonlítja.</p>
<p>Event loopnak hívják azt az egy szálat, ami dolgozik.</p>
<p>Az Async IO szíve a coroutine, mely egy speciális generator függvény. Ennek futását még a <code class="language-plaintext highlighter-rouge">return</code>
előtt megszakíthatja az interpreter, és átadhatja a vezérlést egy másik coroutine függvénynek.</p>
<p>Coroutine deklarálásánál használhatók a <code class="language-plaintext highlighter-rouge">async</code> / <code class="language-plaintext highlighter-rouge">await</code> kulcsszavak. Következzék erre rövid példa.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">async</span> <span class="k">def</span> <span class="nf">main</span><span class="p">():</span>
<span class="k">print</span><span class="p">(</span><span class="s">'hello'</span><span class="p">)</span>
<span class="k">await</span> <span class="n">asyncio</span><span class="p">.</span><span class="n">sleep</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="s">'world'</span><span class="p">)</span>
<span class="n">asyncio</span><span class="p">.</span><span class="n">run</span><span class="p">(</span><span class="n">main</span><span class="p">())</span>
</code></pre></div></div>
<p>És akkor következhet az így megírt UDP szerver.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">asyncio</span>
<span class="kn">from</span> <span class="nn">kiss_headers</span> <span class="kn">import</span> <span class="n">parse_it</span>
<span class="kn">from</span> <span class="nn">socket</span> <span class="kn">import</span> <span class="o">*</span>
<span class="kn">import</span> <span class="nn">platform</span>
<span class="n">MULTICAST_PORT</span> <span class="o">=</span> <span class="mi">1900</span>
<span class="n">MULTICAST_GROUP</span> <span class="o">=</span> <span class="s">"239.255.255.250"</span>
<span class="k">class</span> <span class="nc">SsdpProtocol</span><span class="p">(</span><span class="n">asyncio</span><span class="p">.</span><span class="n">BaseProtocol</span><span class="p">):</span>
<span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">interface_ip</span><span class="p">):</span>
<span class="bp">self</span><span class="p">.</span><span class="n">transport</span> <span class="o">=</span> <span class="bp">None</span>
<span class="bp">self</span><span class="p">.</span><span class="n">interface_ip</span> <span class="o">=</span> <span class="n">interface_ip</span>
<span class="k">def</span> <span class="nf">connection_made</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">transport</span><span class="p">):</span>
<span class="k">print</span><span class="p">(</span><span class="s">"UDP connection made"</span><span class="p">)</span>
<span class="bp">self</span><span class="p">.</span><span class="n">transport</span> <span class="o">=</span> <span class="n">transport</span>
<span class="k">def</span> <span class="nf">connection_lost</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">ex</span><span class="p">):</span>
<span class="k">print</span><span class="p">(</span><span class="s">"UDP connection lost"</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">datagram_received</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">data</span><span class="p">,</span> <span class="n">address</span><span class="p">):</span>
<span class="n">text</span> <span class="o">=</span> <span class="n">data</span><span class="p">.</span><span class="n">decode</span><span class="p">(</span><span class="s">"utf-8"</span><span class="p">)</span>
<span class="n">method</span> <span class="o">=</span> <span class="n">text</span><span class="p">.</span><span class="n">split</span><span class="p">()[</span><span class="mi">0</span><span class="p">]</span>
<span class="n">headers</span> <span class="o">=</span> <span class="n">parse_it</span><span class="p">(</span><span class="n">text</span><span class="p">)</span>
<span class="k">if</span> <span class="n">method</span> <span class="o">==</span> <span class="s">"M-SEARCH"</span> <span class="ow">and</span> <span class="n">headers</span><span class="p">.</span><span class="n">ST</span> <span class="o">==</span> <span class="s">"upnp:rootdevice"</span><span class="p">:</span>
<span class="k">print</span><span class="p">(</span><span class="s">"Handle M-SEARCH"</span><span class="p">)</span>
<span class="n">response</span> <span class="o">=</span> <span class="sa">f</span><span class="s">"""HTTP/1.1 200 OK
EXT:
LOCATION: http://</span><span class="si">{</span><span class="bp">self</span><span class="p">.</span><span class="n">interface_ip</span><span class="si">}</span><span class="s">:8080/rootDesc.xml
SERVER: </span><span class="si">{</span><span class="n">platform</span><span class="p">.</span><span class="n">system</span><span class="p">()</span><span class="si">}</span><span class="s">/</span><span class="si">{</span><span class="n">platform</span><span class="p">.</span><span class="n">release</span><span class="p">()</span><span class="si">}</span><span class="s">, UPnP/1.0, JTechLog UPnP Server 0.0.1
ST: upnp:rootdevice
USN: uuid:fea4bf14-6da5-11ec-90d6-0242ac120003::upnp:rootdevice
"""</span><span class="p">.</span><span class="n">replace</span><span class="p">(</span><span class="s">"</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="s">"</span><span class="se">\r\n</span><span class="s">"</span><span class="p">)</span>
<span class="bp">self</span><span class="p">.</span><span class="n">transport</span><span class="p">.</span><span class="n">sendto</span><span class="p">(</span><span class="n">response</span><span class="p">.</span><span class="n">encode</span><span class="p">(</span><span class="s">"utf-8"</span><span class="p">),</span> <span class="n">address</span><span class="p">)</span>
<span class="k">async</span> <span class="k">def</span> <span class="nf">run_udp_server</span><span class="p">(</span><span class="n">interface_ip</span><span class="p">):</span>
<span class="k">print</span><span class="p">(</span><span class="s">"Starting UDP server"</span><span class="p">)</span>
<span class="n">udp_server</span> <span class="o">=</span> <span class="n">socket</span><span class="p">(</span><span class="n">AF_INET</span><span class="p">,</span> <span class="n">SOCK_DGRAM</span><span class="p">)</span>
<span class="n">udp_server</span><span class="p">.</span><span class="n">bind</span><span class="p">((</span><span class="s">""</span><span class="p">,</span> <span class="n">MULTICAST_PORT</span><span class="p">))</span>
<span class="n">mreq</span> <span class="o">=</span> <span class="n">inet_aton</span><span class="p">(</span><span class="n">MULTICAST_GROUP</span><span class="p">)</span> <span class="o">+</span> <span class="n">inet_aton</span><span class="p">(</span><span class="n">interface_ip</span><span class="p">)</span>
<span class="n">udp_server</span><span class="p">.</span><span class="n">setsockopt</span><span class="p">(</span><span class="n">IPPROTO_IP</span><span class="p">,</span> <span class="n">IP_ADD_MEMBERSHIP</span><span class="p">,</span> <span class="n">mreq</span><span class="p">)</span>
<span class="n">loop</span> <span class="o">=</span> <span class="n">asyncio</span><span class="p">.</span><span class="n">get_running_loop</span><span class="p">()</span>
<span class="n">transport</span><span class="p">,</span> <span class="n">protocol</span> <span class="o">=</span> <span class="k">await</span> <span class="n">loop</span><span class="p">.</span><span class="n">create_datagram_endpoint</span><span class="p">(</span>
<span class="k">lambda</span><span class="p">:</span> <span class="n">SsdpProtocol</span><span class="p">(</span><span class="n">interface_ip</span><span class="p">),</span>
<span class="n">sock</span><span class="o">=</span><span class="n">udp_server</span><span class="p">)</span>
<span class="k">try</span><span class="p">:</span>
<span class="k">await</span> <span class="n">asyncio</span><span class="p">.</span><span class="n">sleep</span><span class="p">(</span><span class="mi">3600</span><span class="p">)</span> <span class="c1"># Serve for 1 hour.
</span> <span class="k">except</span> <span class="n">asyncio</span><span class="p">.</span><span class="n">CancelledError</span><span class="p">:</span>
<span class="k">print</span><span class="p">(</span><span class="s">"Cancelled UDP server"</span><span class="p">)</span>
<span class="k">finally</span><span class="p">:</span>
<span class="n">transport</span><span class="p">.</span><span class="n">close</span><span class="p">()</span>
<span class="n">asyncio</span><span class="p">.</span><span class="n">run</span><span class="p">(</span><span class="n">run_udp_server</span><span class="p">(</span><span class="s">"192.168.0.213"</span><span class="p">))</span>
</code></pre></div></div>
<p>A HTTP protokollt nem akartam leprogramozni, hanem helyette
az Async IO-ra épülő <code class="language-plaintext highlighter-rouge">aiohttp</code> könyvtárat használtam.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">HttpHandler</span><span class="p">:</span>
<span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">interface_ip</span><span class="p">,</span> <span class="n">http_port</span><span class="p">):</span>
<span class="bp">self</span><span class="p">.</span><span class="n">interface_ip</span> <span class="o">=</span> <span class="n">interface_ip</span>
<span class="bp">self</span><span class="p">.</span><span class="n">http_port</span> <span class="o">=</span> <span class="n">http_port</span>
<span class="k">async</span> <span class="k">def</span> <span class="nf">handle</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">request</span><span class="p">):</span>
<span class="k">print</span><span class="p">(</span><span class="s">"Handle HTTP request to 'rootDesc.xml'"</span><span class="p">)</span>
<span class="n">text</span> <span class="o">=</span> <span class="sa">f</span><span class="s">"""<?xml version="1.0"?>
<root xmlns="urn:schemas-upnp-org:device-1-0" xmlns:dlna="urn:schemas-dlna-org:device-1-0">
<specVersion>
<major>1</major>
<minor>0</minor>
</specVersion>
<device>
<deviceType>upnp:rootdevice</deviceType>
<friendlyName>JTechLog</friendlyName>
</device>
<URLBase>http://</span><span class="si">{</span><span class="bp">self</span><span class="p">.</span><span class="n">interface_ip</span><span class="si">}</span><span class="s">:</span><span class="si">{</span><span class="bp">self</span><span class="p">.</span><span class="n">http_port</span><span class="si">}</span><span class="s">/</URLBase>
</root>
"""</span>
<span class="k">return</span> <span class="n">web</span><span class="p">.</span><span class="n">Response</span><span class="p">(</span><span class="n">text</span><span class="o">=</span><span class="n">text</span><span class="p">)</span>
<span class="k">async</span> <span class="k">def</span> <span class="nf">run_http_server</span><span class="p">(</span><span class="n">interface_ip</span><span class="p">):</span>
<span class="k">print</span><span class="p">(</span><span class="s">"Starting HTTP server"</span><span class="p">)</span>
<span class="n">app</span> <span class="o">=</span> <span class="n">web</span><span class="p">.</span><span class="n">Application</span><span class="p">()</span>
<span class="n">handler</span> <span class="o">=</span> <span class="n">HttpHandler</span><span class="p">(</span><span class="n">interface_ip</span><span class="p">,</span> <span class="mi">8080</span><span class="p">)</span>
<span class="n">app</span><span class="p">.</span><span class="n">add_routes</span><span class="p">([</span><span class="n">web</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">'/rootDesc.xml'</span><span class="p">,</span> <span class="n">handler</span><span class="p">.</span><span class="n">handle</span><span class="p">)])</span>
<span class="n">runner</span> <span class="o">=</span> <span class="n">web</span><span class="p">.</span><span class="n">AppRunner</span><span class="p">(</span><span class="n">app</span><span class="p">)</span>
<span class="k">await</span> <span class="n">runner</span><span class="p">.</span><span class="n">setup</span><span class="p">()</span>
<span class="n">site</span> <span class="o">=</span> <span class="n">web</span><span class="p">.</span><span class="n">TCPSite</span><span class="p">(</span><span class="n">runner</span><span class="p">,</span> <span class="n">host</span><span class="o">=</span><span class="n">interface_ip</span><span class="p">,</span> <span class="n">port</span><span class="o">=</span><span class="mi">8080</span><span class="p">)</span>
<span class="k">await</span> <span class="n">site</span><span class="p">.</span><span class="n">start</span><span class="p">()</span>
<span class="k">try</span><span class="p">:</span>
<span class="k">await</span> <span class="n">asyncio</span><span class="p">.</span><span class="n">sleep</span><span class="p">(</span><span class="mi">3600</span><span class="p">)</span> <span class="c1"># Serve for 1 hour.
</span> <span class="k">except</span> <span class="n">asyncio</span><span class="p">.</span><span class="n">CancelledError</span><span class="p">:</span>
<span class="k">print</span><span class="p">(</span><span class="s">"Cancelled HTTP server"</span><span class="p">)</span>
<span class="n">asyncio</span><span class="p">.</span><span class="n">run</span><span class="p">(</span><span class="n">run_http_server</span><span class="p">(</span><span class="s">"192.168.0.213"</span><span class="p">))</span>
</code></pre></div></div>
<p>De hogy indítjuk el egymás mellett a kettőt?</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">async</span> <span class="k">def</span> <span class="nf">run_servers</span><span class="p">(</span><span class="n">interface_ip</span><span class="p">):</span>
<span class="n">udp_server_task</span> <span class="o">=</span> <span class="n">asyncio</span><span class="p">.</span><span class="n">ensure_future</span><span class="p">(</span><span class="n">run_udp_server</span><span class="p">(</span><span class="n">interface_ip</span><span class="p">))</span>
<span class="n">http_server_task</span> <span class="o">=</span> <span class="n">asyncio</span><span class="p">.</span><span class="n">ensure_future</span><span class="p">(</span><span class="n">run_http_server</span><span class="p">(</span><span class="n">interface_ip</span><span class="p">))</span>
<span class="k">await</span> <span class="n">asyncio</span><span class="p">.</span><span class="n">gather</span><span class="p">(</span><span class="n">udp_server_task</span><span class="p">,</span> <span class="n">http_server_task</span><span class="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="s">"Starting servers"</span><span class="p">)</span>
<span class="n">asyncio</span><span class="p">.</span><span class="n">run</span><span class="p">(</span><span class="n">run_servers</span><span class="p">(</span><span class="s">"192.168.0.213"</span><span class="p">))</span>
</code></pre></div></div>
<p>Amit még szerettem volna megoldani, hogy hogy tud kezelni Linux signalokat,
azaz pl. amikor nyomok egy <em>Ctrl + C</em> billentyűzetkombinációt a konzolban.</p>
<p>Láthattuk, hogy a szerverek indításakor <code class="language-plaintext highlighter-rouge">asyncio.sleep()</code> függvényt hívtam,
és kezeltem a <code class="language-plaintext highlighter-rouge">asyncio.CancelledError</code> kivételt. Hát küldjünk akkor
<code class="language-plaintext highlighter-rouge">cancel</code>-t az összes feladatnak.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">SignalHandler</span><span class="p">:</span>
<span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">tasks</span><span class="p">):</span>
<span class="bp">self</span><span class="p">.</span><span class="n">tasks</span> <span class="o">=</span> <span class="n">tasks</span>
<span class="k">def</span> <span class="nf">handle</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="k">print</span><span class="p">(</span><span class="s">"Got SIGINT signal"</span><span class="p">)</span>
<span class="k">for</span> <span class="n">task</span> <span class="ow">in</span> <span class="bp">self</span><span class="p">.</span><span class="n">tasks</span><span class="p">:</span>
<span class="n">task</span><span class="p">.</span><span class="n">cancel</span><span class="p">()</span>
<span class="n">loop</span> <span class="o">=</span> <span class="n">asyncio</span><span class="p">.</span><span class="n">get_event_loop</span><span class="p">()</span>
<span class="n">signal_handler</span> <span class="o">=</span> <span class="n">SignalHandler</span><span class="p">([</span><span class="n">udp_server_task</span><span class="p">,</span> <span class="n">http_server_task</span><span class="p">])</span>
<span class="n">loop</span><span class="p">.</span><span class="n">add_signal_handler</span><span class="p">(</span><span class="n">signal</span><span class="p">.</span><span class="n">SIGINT</span><span class="p">,</span> <span class="n">signal_handler</span><span class="p">.</span><span class="n">handle</span><span class="p">)</span>
</code></pre></div></div>
Szoftverfejlesztés és Önfejlesztés podcast2021-10-22T08:00:00+00:00http://www.jtechlog.hu/2021/10/22/szoftverfejlesztes-es-onfejlesztes<p>A <a href="https://careercompass.hu/podcast/">Szoftverfejlesztés és Önfejlesztés podcastban</a>
Marhefka István meghívott egy kötetlen beszélgetésre.
Szó esett a folyamatos tanulásról, oktatásról, technológiákról, microservice-ekről,
szakmai életutamról.</p>
<p>A podcast első évadát is érdemes meghallgatni, ahol István a beszélgetőtársával hétről hétre azt
boncolgatják, hogyan kapcsolódik a technológia és az ember.</p>
<p>A második évadban sikeres szoftverfejlesztőket kért meg, hogy meséljenek szakmai útjukról.</p>
<!-- more -->
<p>A beszélgetés elérhető az összes népszerű csatornán, Youtube-on, Spotify-on, iTuneson, stb.</p>
<iframe width="854" height="480" src="https://www.youtube.com/embed/LFkaRqiM4Qs" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen=""></iframe>
MDC naplózáskor és distributed tracing2021-10-04T08:00:00+00:00http://www.jtechlog.hu/2021/10/04/mdc-trace<p>A naplózó keretrendszerekben (legalábbis a Log4J-ben és a Logbackben) elég régóta ott
a Mapped Diagnostic Context, ami egy nagyon hasznos eszköz, mégis aránylag ritkán
látom használni.</p>
<p>Egy terhelés alatt lévő webes alkalmazásnál a naplóbejegyzéseket alapvetően elég nehéz szétválogatni pl.
kérésenként, sessionönként/felhasználónként. Az MDC segítségével minden napló bejegyzésbe
elhelyezhetük olyan adatot (pl. session azonosítót, felhasználónevet), mely ezek azonosítását teszi lehetővé. Ráadásul tehetjük
ezt anélkül, hogy a naplózás helyén a hívást, és benne az üzenetet módosítanunk kéne.</p>
<p>A naplóüzenetek címkézése, azonosítása különösen fontos lehet elosztott, microservice környezetben,
ahol különböző gépekről beérkező naplóüzeneteket kell összekötnünk.</p>
<!-- more -->
<p>A megvalósítás egyszerű, az MDC-t úgy képzeljük el, mint egy mapet, ami az adott szálhoz van
kötve. Ebbe a mapben helyezhetünk el értékeket, melyek egész addig ott lesznek, míg a szál
el nem végezte a feladatát. Utána egyszerűen konfigurálhatjuk, hogy ezek az értékek jelenjenek meg
minden naplóüzenetben. A szálhoz kötés egy <code class="language-plaintext highlighter-rouge">ThreadLocal</code> példánnyal történik.</p>
<h2 id="egyszerű-alkalmazásban">Egyszerű alkalmazásban</h2>
<p>Mivel az MDC a Log4J-ben és a Logbackben is elérhető, az SLF4J is tartalmazza.
Mivel különösen webes alkalmazásnál hasznos, én egy Spring Bootos alkalmazással
fogom bemutatni, amiben Logback az alapértelmezett naplózó implementáció. Ettől függetlenül
mindenütt használható, ahol naplózás van.</p>
<p>A példa projekt elérhető a <a href="https://github.com/vicziani/jtechlog-mdc">GitHubon</a>.</p>
<p>Az MDC-be értéket betenni a következőképp lehet:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">MDC</span><span class="o">.</span><span class="na">put</span><span class="o">(</span><span class="s">"username"</span><span class="o">,</span> <span class="s">"johndoe"</span><span class="o">);</span>
</code></pre></div></div>
<p>Utána már csak az <code class="language-plaintext highlighter-rouge">application.properties</code> fájlban (, vagy ha több mindent szeretnénk megadni, akkor
a Logback konfigurációs XML fájljában) kell konfigurálni a layoutot:</p>
<div class="language-properties highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="py">logging.pattern.console</span> <span class="p">=</span> <span class="s">%d{HH:mm:ss.SSS} [%thread] [%X{username}] %-5level %logger{36} - %msg%n</span>
</code></pre></div></div>
<p>Látható, hogy a <code class="language-plaintext highlighter-rouge">%X{username}</code> részlet írja ki a bejelentkezett felhasználót minden napló bejegyzés
esetén. Azaz legyen a hívás a következő.</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">log</span><span class="o">.</span><span class="na">info</span><span class="o">(</span><span class="s">"Hello Spring Boot"</span><span class="o">);</span>
</code></pre></div></div>
<p>A hozzá tartozó naplóbejegyzés:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>16:33:38.349 [http-nio-8080-exec-1] [johndoe] INFO hello.HelloController - Hello Spring Boot
</code></pre></div></div>
<p>Figyeljük meg a példában, hogy egy Spring MVC controllerből naplózunk, és a <code class="language-plaintext highlighter-rouge">log</code> attribútumot
a Lombok <code class="language-plaintext highlighter-rouge">@Slf4j</code> annotáció hozza létre.</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@RestController</span>
<span class="nd">@Slf4j</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">HelloController</span> <span class="o">{</span>
<span class="nd">@GetMapping</span><span class="o">(</span><span class="s">"hello"</span><span class="o">)</span>
<span class="kd">public</span> <span class="nc">String</span> <span class="nf">sayHello</span><span class="o">()</span> <span class="o">{</span>
<span class="n">log</span><span class="o">.</span><span class="na">info</span><span class="o">(</span><span class="s">"Hello Spring Boot"</span><span class="o">);</span>
<span class="k">return</span> <span class="s">"Hello Spring Boot"</span><span class="o">;</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>Ez jó, ha van felhasználónk, de mi van akkor, ha a különböző http kérésekhez tartozó napló üzeneteket
szeretnénk megkülönböztetni? Ekkor érdemes minden kéréshez gyártani egy egyedi azonosítót,
legyen ez a tracking id, és egy Servlet Filterrel tegyük ezt meg, minden bejövő http kérésnél.</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Component</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">TraceFilter</span> <span class="kd">extends</span> <span class="nc">OncePerRequestFilter</span> <span class="o">{</span>
<span class="kd">public</span> <span class="kd">static</span> <span class="kd">final</span> <span class="nc">String</span> <span class="no">TRACE_ID_MDC_KEY</span> <span class="o">=</span> <span class="s">"traceId"</span><span class="o">;</span>
<span class="nd">@Override</span>
<span class="kd">protected</span> <span class="kt">void</span> <span class="nf">doFilterInternal</span><span class="o">(</span><span class="nc">HttpServletRequest</span> <span class="n">request</span><span class="o">,</span> <span class="nc">HttpServletResponse</span> <span class="n">response</span><span class="o">,</span> <span class="nc">FilterChain</span> <span class="n">filterChain</span><span class="o">)</span>
<span class="kd">throws</span> <span class="nc">ServletException</span><span class="o">,</span> <span class="nc">IOException</span> <span class="o">{</span>
<span class="kt">var</span> <span class="n">traceId</span> <span class="o">=</span> <span class="no">UUID</span><span class="o">.</span><span class="na">randomUUID</span><span class="o">().</span><span class="na">toString</span><span class="o">();</span>
<span class="no">MDC</span><span class="o">.</span><span class="na">put</span><span class="o">(</span><span class="no">TRACE_ID_MDC_KEY</span><span class="o">,</span> <span class="n">traceId</span><span class="o">);</span>
<span class="k">try</span> <span class="o">{</span>
<span class="n">filterChain</span><span class="o">.</span><span class="na">doFilter</span><span class="o">(</span><span class="n">request</span><span class="o">,</span> <span class="n">response</span><span class="o">);</span>
<span class="o">}</span> <span class="k">finally</span> <span class="o">{</span>
<span class="no">MDC</span><span class="o">.</span><span class="na">remove</span><span class="o">(</span><span class="no">TRACE_ID_MDC_KEY</span><span class="o">);</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>Figyeljük meg, hogy a Springben lévő <code class="language-plaintext highlighter-rouge">OncePerRequestFilter</code> az ősosztály, ami kérésenként egyszer fut csak le.
(Ezt egy request attribute-tal oldja meg.)</p>
<p>Ennek megfelelően a módosított pattern a következő.</p>
<div class="language-properties highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="py">logging.pattern.console</span> <span class="p">=</span> <span class="s">%d{HH:mm:ss.SSS} [%thread] [%X{traceId}] %-5level %logger{36} - %msg%n</span>
</code></pre></div></div>
<p>És a kiírt napló üzenet egy generált UUID-t tartalmaz.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>16:33:38.349 [http-nio-8080-exec-1] [84cb2c52-c021-41e0-bddb-a9c53472a550] INFO hello.HelloController - Hello Spring Boot
</code></pre></div></div>
<p>Észrevehetjük, hogy a REST webszolgáltatások tesztelésére használt Postman is küld egy
egyedi azonosítót kérésenként <code class="language-plaintext highlighter-rouge">Postman-Token</code> http headerben. Ez jól használható
teszteléskor, hogy meg tudjuk feleltetni a naplóüzeneteket a kéréseinknek. (Amúgy
az említett header azért került bele, mert a Chrome-ban volt egy bug, a http kérések kezelésében.
Ha elküldött egy http kérést, és ha közben ugyanazokkal a paraméterekkel elküldött egy másikat is,
akkor ha az első visszatért, akkor annak válaszát adta vissza a második válaszaként is. Ha viszont
akár csak a headerben is különböztek, már a második választ adta vissza a második hívás válaszaként.)</p>
<p>Módosítsuk a filtert, hogy vegye ki ezt a headert, tegye át az MDC-be, és írja ki a naplóba.</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Component</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">TraceFilter</span> <span class="kd">extends</span> <span class="nc">OncePerRequestFilter</span> <span class="o">{</span>
<span class="kd">public</span> <span class="kd">static</span> <span class="kd">final</span> <span class="nc">String</span> <span class="no">TRACE_ID_MDC_KEY</span> <span class="o">=</span> <span class="s">"traceId"</span><span class="o">;</span>
<span class="kd">public</span> <span class="kd">static</span> <span class="kd">final</span> <span class="nc">String</span> <span class="no">POSTMAN_TOKEN_MDC_KEY</span> <span class="o">=</span> <span class="s">"postmanToken"</span><span class="o">;</span>
<span class="kd">public</span> <span class="kd">static</span> <span class="kd">final</span> <span class="nc">String</span> <span class="no">POSTMAN_TOKEN_HEADER_NAME</span> <span class="o">=</span> <span class="s">"Postman-Token"</span><span class="o">;</span>
<span class="nd">@Override</span>
<span class="kd">protected</span> <span class="kt">void</span> <span class="nf">doFilterInternal</span><span class="o">(</span><span class="nc">HttpServletRequest</span> <span class="n">request</span><span class="o">,</span> <span class="nc">HttpServletResponse</span> <span class="n">response</span><span class="o">,</span> <span class="nc">FilterChain</span> <span class="n">filterChain</span><span class="o">)</span>
<span class="kd">throws</span> <span class="nc">ServletException</span><span class="o">,</span> <span class="nc">IOException</span> <span class="o">{</span>
<span class="kt">var</span> <span class="n">traceId</span> <span class="o">=</span> <span class="no">UUID</span><span class="o">.</span><span class="na">randomUUID</span><span class="o">().</span><span class="na">toString</span><span class="o">();</span>
<span class="kt">var</span> <span class="n">postmanToken</span> <span class="o">=</span> <span class="n">request</span><span class="o">.</span><span class="na">getHeader</span><span class="o">(</span><span class="no">POSTMAN_TOKEN_HEADER_NAME</span><span class="o">);</span>
<span class="kt">var</span> <span class="n">userAgent</span> <span class="o">=</span> <span class="n">request</span><span class="o">.</span><span class="na">getHeader</span><span class="o">(</span><span class="no">USER_AGENT_HEADER_NAME</span><span class="o">);</span>
<span class="no">MDC</span><span class="o">.</span><span class="na">put</span><span class="o">(</span><span class="no">TRACE_ID_MDC_KEY</span><span class="o">,</span> <span class="n">traceId</span><span class="o">);</span>
<span class="no">MDC</span><span class="o">.</span><span class="na">put</span><span class="o">(</span><span class="no">POSTMAN_TOKEN_MDC_KEY</span><span class="o">,</span> <span class="n">postmanToken</span><span class="o">);</span>
<span class="k">try</span> <span class="o">{</span>
<span class="n">filterChain</span><span class="o">.</span><span class="na">doFilter</span><span class="o">(</span><span class="n">request</span><span class="o">,</span> <span class="n">response</span><span class="o">);</span>
<span class="o">}</span> <span class="k">finally</span> <span class="o">{</span>
<span class="no">MDC</span><span class="o">.</span><span class="na">remove</span><span class="o">(</span><span class="no">TRACE_ID_MDC_KEY</span><span class="o">);</span>
<span class="no">MDC</span><span class="o">.</span><span class="na">remove</span><span class="o">(</span><span class="no">POSTMAN_TOKEN_MDC_KEY</span><span class="o">);</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>A módosított pattern a következő.</p>
<div class="language-properties highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="py">logging.pattern.console</span> <span class="p">=</span> <span class="s">%d{HH:mm:ss.SSS} [%thread] [%X{traceId},%X{postmanToken}] %-5level %logger{36} - %msg%n</span>
</code></pre></div></div>
<p>És a kiírt napló üzenet.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>16:33:38.349 [http-nio-8080-exec-1] [84cb2c52-c021-41e0-bddb-a9c53472a550,1e8653f5-83f6-4bb0-9d45-004af1867763] INFO hello.HelloController - Hello Spring Boot
</code></pre></div></div>
<p>Ahol az első UUID a szerver által kiosztott trace id, a második pedig a Postman által küldött Postman Token.</p>
<p>A Logback érdekes tulajdonsága, hogy különböző filtereket lehet definiálni, ezzel szűrni a naplóüzeneteket.
Módosítsuk a filterünket, hogy ne csak a <code class="language-plaintext highlighter-rouge">Postman-Token</code> értekét vegye ki a headerből, hanem a <code class="language-plaintext highlighter-rouge">User-Agent</code>
értékét is, majd szintén tegye át az MDC-be.</p>
<p>Ezután pl. ez alapján a <code class="language-plaintext highlighter-rouge">MDCFilter</code>-rel tudunk szűrni. Ehhez viszont már nem elegendő az <code class="language-plaintext highlighter-rouge">application.properties</code>
állomány, hanem saját xml konfigurációt kell alkalmazni. Viszont ezt meg kell adni az <code class="language-plaintext highlighter-rouge">application.properties</code>
fájlban.</p>
<div class="language-properties highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="py">logging.config</span> <span class="p">=</span> <span class="s">classpath:hello-logback.xml</span>
</code></pre></div></div>
<p>És a <code class="language-plaintext highlighter-rouge">hello-logback.xml</code> tartalma a következő.</p>
<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp"><?xml version="1.0" encoding="UTF-8"?></span>
<span class="nt"><configuration></span>
<span class="nt"><property</span> <span class="na">name=</span><span class="s">"CONSOLE_LOG_PATTERN"</span> <span class="na">value=</span><span class="s">"%d{HH:mm:ss.SSS} [%thread] [%X{traceId},%X{postmanToken}] %-5level %logger{36} - %msg%n"</span><span class="nt">/></span>
<span class="nt"><include</span> <span class="na">resource=</span><span class="s">"org/springframework/boot/logging/logback/defaults.xml"</span><span class="nt">/></span>
<span class="nt"><include</span> <span class="na">resource=</span><span class="s">"org/springframework/boot/logging/logback/console-appender.xml"</span><span class="nt">/></span>
<span class="nt"><turboFilter</span> <span class="na">class=</span><span class="s">"ch.qos.logback.classic.turbo.MDCFilter"</span><span class="nt">></span>
<span class="nt"><MDCKey></span>userAgent<span class="nt"></MDCKey></span>
<span class="nt"><Value></span>PostmanRuntime/7.28.4<span class="nt"></Value></span>
<span class="nt"><OnMatch></span>DENY<span class="nt"></OnMatch></span>
<span class="nt"></turboFilter></span>
<span class="nt"><root</span> <span class="na">level=</span><span class="s">"INFO"</span><span class="nt">></span>
<span class="nt"><appender-ref</span> <span class="na">ref=</span><span class="s">"CONSOLE"</span><span class="nt">/></span>
<span class="nt"></root></span>
<span class="nt"></configuration></span>
</code></pre></div></div>
<p>Itt is látható egy kis Logback + Spring Bootos trükk, hogy include-áljuk a default
konfigot a <code class="language-plaintext highlighter-rouge">defaults.xml</code> és <code class="language-plaintext highlighter-rouge">console-appender.xml</code> állományokból.
(Valamint nagyon fontos megjegyzés, hogy ezekben az xml állományokban akár
environment property-ket is alkalmazhatunk, ami különösen hasznos lehet, ha az
alkalmazásunkat pl. Docker konténerben futtatjuk. Ekkor a <code class="language-plaintext highlighter-rouge">${USERAGENT_PREFIX}</code>
formátumot használhatjuk.)</p>
<p>Az <code class="language-plaintext highlighter-rouge">MDCFilter</code> csak pontos egyezőséget képes vizsgálni. Saját filter implementálása
gyerekjáték.</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">class</span> <span class="nc">UserAgentMdcFilter</span> <span class="kd">extends</span> <span class="nc">TurboFilter</span> <span class="o">{</span>
<span class="kd">private</span> <span class="nc">String</span> <span class="n">prefix</span><span class="o">;</span>
<span class="nd">@Override</span>
<span class="kd">public</span> <span class="nc">FilterReply</span> <span class="nf">decide</span><span class="o">(</span><span class="nc">Marker</span> <span class="n">marker</span><span class="o">,</span> <span class="nc">Logger</span> <span class="n">logger</span><span class="o">,</span> <span class="nc">Level</span> <span class="n">level</span><span class="o">,</span> <span class="nc">String</span> <span class="n">format</span><span class="o">,</span> <span class="nc">Object</span><span class="o">[]</span> <span class="n">params</span><span class="o">,</span> <span class="nc">Throwable</span> <span class="n">t</span><span class="o">)</span> <span class="o">{</span>
<span class="kt">var</span> <span class="n">userAgent</span> <span class="o">=</span> <span class="no">MDC</span><span class="o">.</span><span class="na">get</span><span class="o">(</span><span class="nc">TraceFilter</span><span class="o">.</span><span class="na">USER_AGENT_MDC_KEY</span><span class="o">);</span>
<span class="k">if</span> <span class="o">(</span><span class="n">userAgent</span> <span class="o">!=</span> <span class="kc">null</span> <span class="o">&&</span> <span class="n">userAgent</span><span class="o">.</span><span class="na">toLowerCase</span><span class="o">().</span><span class="na">startsWith</span><span class="o">(</span><span class="n">prefix</span><span class="o">.</span><span class="na">toLowerCase</span><span class="o">()))</span> <span class="o">{</span>
<span class="k">return</span> <span class="nc">FilterReply</span><span class="o">.</span><span class="na">DENY</span><span class="o">;</span>
<span class="o">}</span>
<span class="k">return</span> <span class="nc">FilterReply</span><span class="o">.</span><span class="na">NEUTRAL</span><span class="o">;</span>
<span class="o">}</span>
<span class="kd">public</span> <span class="kt">void</span> <span class="nf">setPrefix</span><span class="o">(</span><span class="nc">String</span> <span class="n">prefix</span><span class="o">)</span> <span class="o">{</span>
<span class="k">this</span><span class="o">.</span><span class="na">prefix</span> <span class="o">=</span> <span class="n">prefix</span><span class="o">;</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>A <code class="language-plaintext highlighter-rouge">DENY</code> azt jelenti, hogy a naplóbeüzenetet ki kell szűrni. További értékei a <code class="language-plaintext highlighter-rouge">NEUTRAL</code>, amikor
további filterek dönthetnek, vagy a <code class="language-plaintext highlighter-rouge">ACCEPT</code>, amikor a naplóüzenet azonnal kiírható.</p>
<p>Az ehhez tartozó xml konfiguráció olvasható alább.</p>
<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt"><turboFilter</span> <span class="na">class=</span><span class="s">"hello.UserAgentMdcFilter"</span><span class="nt">></span>
<span class="nt"><prefix></span>PostmanRuntime<span class="nt"></prefix></span>
<span class="nt"></turboFilter></span>
</code></pre></div></div>
<p>Látható, hogy milyen elegáns a konfiguráció is, hiszen csak egy settert kellett definiálni.</p>
<h2 id="microservice-környezetben">Microservice környezetben</h2>
<p>Microservice környezetben kihívást okozhat a különböző microservice-ek által
beküldött naplóüzenetek összepárosítása. Képzeljük el, hogy az <code class="language-plaintext highlighter-rouge">A</code>
microservice hívja a <code class="language-plaintext highlighter-rouge">B</code>-t, és az a <code class="language-plaintext highlighter-rouge">C</code>-t, és szeretnénk a kéréshez
tartozó, de az összes rendszerben keletkezett naplóüzeneteket egyben
látni.</p>
<p>Erre való a <a href="https://microservices.io/patterns/observability/distributed-tracing.html">Distributed tracing</a>
minta. Az alapötlet ugyanaz. A kéréshez társítsunk egy azonosítót (elnevezési konvenció szerint tracing id),
valamint minden rendszerben végrehajtott egy vagy több művelet kapjon szintén egy egyedi azonosítót (elnevezési
konvenció szerint span id). Ezek a spanek hierarchiába rendezhetők, hiszen az <code class="language-plaintext highlighter-rouge">A</code> rendszerben
a kérés kapott egy <code class="language-plaintext highlighter-rouge">A-1</code> span id-t, ez továbbhívott a <code class="language-plaintext highlighter-rouge">B</code> rendszerbe, átadva ez az id-t, és
az ott végrehajtott művelet kapot egy <code class="language-plaintext highlighter-rouge">B-1</code> id-t. A <code class="language-plaintext highlighter-rouge">B-1</code> spannek az <code class="language-plaintext highlighter-rouge">A-1</code> a szülője.</p>
<p>Más nevezéktanban correlation id-ként hivatkoznak a rendszereken átívelő azonosítóra.</p>
<p>Ahhoz, hogy ez működjön, ezeket az azonosítókat az első hívás helyén ki kell osztani,
ki kell naplózni (ehhez nagyon jól jön a fent említett MDC), és tovább kell adni, és
a továbbadottat ki is kell olvasni.
Ezek programozása nem nagy kihívás, talán a továbbadás lehet érdekesebb. Ez REST hívás
esetén lehet http headerben, aszinkron üzenetek küldése esetén lehet az üzenet fejlécében.</p>
<p>Azonban, hogy ne kelljen ezt nekünk magunk implementálni, Spring Boot alkalmazásban használhatjuk a
<a href="https://spring.io/projects/spring-cloud-sleuth">Spring Cloud Sleuth</a>
projektet. Ez létrehoz egy beant, melyet lehet aztán injektálni, és programozottan
elindíthatunk és leállíthatunk egy spant, és természetesen elvégzi az azonosítók
generálását is (még API is van rá, ez az <a href="https://opentracing.io/specification/">OpenTracing API</a>). Valamint ezeket az azonosítókat a naplóüzenetekben is elhelyezi (MDC használatával).
Alkalmazhatunk deklaratív konfigurációt is annotációk használatával.
De a legnagyobb ereje talán abban van, hogy különböző eszközökhöz implementálva van
ezen azonosítók továbbadása, azaz a propagáció. Erre több mechanizmus is van,
pl. <code class="language-plaintext highlighter-rouge">AWS</code>, <code class="language-plaintext highlighter-rouge">B3</code>, <code class="language-plaintext highlighter-rouge">W3C</code>. A B3 pl. a <a href="https://github.com/openzipkin/b3-propagation">specifikáció alapján</a>
nagyon egyszerű:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>X-B3-TraceId: 80f198ee56343ba864fe8b2a57d3eff7
X-B3-ParentSpanId: 05e3ac9a4f6e3b90
X-B3-SpanId: e457b5a2e4d86bd1
X-B3-Sampled: 1
</code></pre></div></div>
<p>Ha ezt http headerben, vagy üzenet headerben szeretnénk továbbadni, nem kell magunknak megoldanunk,
mert a Spring Cloud Sleuth tartalmaz integrációt a következő eszközökhöz: Rest Template, WebClient,
RabbitMQ, Kafka, JMS, OpenFeign, stb.</p>
<p>Ezzel már megtörténik az id generálás, propagáció, a propagált beolvasása is, a naplózás.
Már csak meg kéne jeleníteni. Erre való Zipkin, aminek http-n,<br />
Kafkán, vagy bármilyen soron keresztül is képes továbbítani az adatokat.</p>
<p>A Zipkin grafikusan tudja megjeleníteni a trace-hez tartozó spaneket, az idő függvényében.</p>
<p><img src="/artifacts/posts/images/zipkin.png" alt="Zipkin" /></p>
Reaktív programozás2021-08-03T15:00:00+00:00http://www.jtechlog.hu/2021/08/03/reaktiv-programozas<p>A reaktív programozás mostanában elég pezsgő irányzat, melynek fejlődését érdemes
nyomon követni. A reaktív programozás mibenlétéről megoszlanak a vélemények. Amit
azonban először érdemes megemlíteni, a Reaktív Kiáltvány (<a href="https://www.reactivemanifesto.org/">The Reactive Manifesto</a>).</p>
<p>A kiáltvány szerint a modern alkalmazásokkal kapcsolatban már más
nem funkcionális követelmények merülnek fel, pl. válaszidő, rendelkezésre
állás, adatmennyiség, skálázhatóság és hibatűrés tekintetében, valamint
más környezetekben futnak, a felhő és konténerizáció nagyon elterjedt.</p>
<p>Ezen alkalmazásoknak négy jellemzőjét definiálja a kiáltvány, melyek a következők:</p>
<ul>
<li>Reszponzív (Responsive): az alkalmazásnak minden esetben gyors választ kell adnia</li>
<li>Ellenálló (Resilient): az alkalmazás gyorsan válaszoljon hiba esetén is</li>
<li>Rugalmas (Elastic): az alkalmazás gyorsan válaszoljon nagy terhelés esetén is</li>
<li>Üzenetvezérelt : rendszerek elemei aszinkron, nem blokkoló módon, üzenetekkel kommunikálnak</li>
</ul>
<p>Reaktív programozás használatakor az alkalmazást
úgy építjük fel, hogy az az adatok aszinkron folyamára reagáljon. Ezen
adatfolyamok lehetnek a felhasználói interakciók, más alkalmazásoktól vagy (pl. IoT) eszközöktől érkező
üzenetek.</p>
<p>Az első három feltétel elég általános, talán az üzenetvezéreltség némileg technológiaibb. Ennek
segítségével lehet megoldani a rendszer komponensei közötti laza kapcsolatot. Megvalósítható vele
a lokális transzparencia, azaz a komponensek helyzetüktől függetlenül szólíthatók meg. Egyszerűbbé
válik a hibakezelés, pl. a hálózati kiesés. Segít a terheléselosztásban, skálázhatóságban.
Valamint megóvhatja a komponenseket a túlzott terheléstől.</p>
<p>Ebben a posztban szó lesz a reaktív programozás kialakulásáról, tulajdonságairól, irányvonalairól, különböző
eszközökről, szabványosításáról, valamint hogyan használhatjuk Springen belül.</p>
<p>A példaprojekt elérhető <a href="https://github.com/vicziani/employees-webflux-r2dbc">GitHubon</a>.</p>
<!-- more -->
<h2 id="történeti-háttér">Történeti háttér</h2>
<p>A Reaktív Kiáltvány betartását számos tényező hátráltatja.</p>
<p>Ebből a legfontosabb a szinkron IO használata. Ez szinte az alkalmazás összes rétegében
tetten érhető. Egyrészt a web konténer megvárja, míg beérkezzen a teljes HTTP kérés,
és ezt szinkron szolgálja ki. Más rendszerek SOAP vagy REST webszolgáltatását
szinkron módon hívja. A fájlt a fájlrendszerből szinkron módon olvassa be.
Az adatokat az adatbázistól szinkron módon kéri le.</p>
<p>Érdekes módon Javaban már az 1.4-es verziótól kezdve van erre megoldás, a Java NIO (New IO, vagy másnéven Non-blocking IO)
személyében. Ez az operációs rendszer lehetőségeit használja ki,
elindítja a műveletet, pl. socketről olvasást, de nem várja meg az eredményt, hanem egy callbacket ad át,
mely visszahívásra kerül a művelet befejezésekor, és ezzel jelentős mennyiségű CPU időt takarít meg.
Ide kapcsolódnak a következő Java interfészek és osztályok: <code class="language-plaintext highlighter-rouge">java.nio.Buffer</code>, pl. <code class="language-plaintext highlighter-rouge">ByteBuffer</code>,
<code class="language-plaintext highlighter-rouge">java.nio.channels.Channel</code>, pl. <code class="language-plaintext highlighter-rouge">AsynchronousFileChannel</code>.</p>
<p>Sajnos azonban ez kevésbé elterjedt. Adatbáziskezelésre JDBC-t és rá épülő
JPA-t használunk, mely szintén szinkron. A webes kiszolgálás Servlet API-val történik, valamint
erre épülő szabványokra és keretrendszerekre (JSF, JAX-WS, JAX-RS, Spring MVC), melyek szintén
szinkron módon működnek. Vannak ugyan erre épülő keretrendszerek, mint a Netty, de ezek
kevésbé elterjedtek.</p>
<p>Az alkalmazások gyakran kollekciókon dolgoznak (<code class="language-plaintext highlighter-rouge">List</code>, <code class="language-plaintext highlighter-rouge">Set</code>, <code class="language-plaintext highlighter-rouge">Map</code>, stb.), melyek elemeit teljes mértékben
betöltjük a memóriába, és így dolgozunk rajta. Az <code class="language-plaintext highlighter-rouge">Iterator</code> és <code class="language-plaintext highlighter-rouge">Stream</code> használata már
előrelépés.</p>
<p>Tipikus webes alkalmazásnál, ami a Servlet API-ra épül minden kérésnél egy új szál kerül elindításra.
Itt szálanként 1 MB stack memóriafoglalással kell számolnunk, valamint a szálak közötti váltás (context switch)
jelentős mennyiségű CPU időt visz el. Ráadásul a tranzakciókezelést is a szálakhoz kötjük.</p>
<p>A túlzott terhelés esetén gyakran előfordul, hogy a CPU az IO-ra vár, feltorlódnak a kérések, megnő a memóriahasználat,
ennek hatására a GC több CPU-t használ, nő a context switch számossága, ami szintén a CPU-t terheli. Jobb esetben
csak belassul <em>minden</em> kérés kiszolgálása, rosszabb esetben elkezdi eldobálni a kéréseket.</p>
<h2 id="a-megoldás">A megoldás</h2>
<p>Amennyiben a termelő a saját ütemében állítja elő az adatot,
túlterhelheti a fogyasztót, ez hálózati protokolloknál ismert jelenség, megoldása a flow control, vagy push back, melynek
több implementációja is ismert.</p>
<p>Ahhoz, hogy a rendszer reszponzív tudjon maradni, meg kell akadályozni, hogy ezen
elemek olyan ütemben érkezzenek be, hogy azok elárasszák a feldolgozó komponenst (fogyasztó),
ezáltal az túlterhelődjön, belassuljon, esetleg hibázzon. Erre egy mechanizmus a
back pressure, az ellenállóképesség az elárasztással szemben. Ennek egyik
típusa a non-blocking back pressure, mely úgy oldja meg ezt a védelmet, hogy
a feldolgozó komponens kéri el a következő elemeket az elemek forrásától (termelő), annyit,
amennyit biztonságosan fel tud dolgozni, ezáltal megakadályozva a túlterhelést.</p>
<p>A reaktív programozást tipikusan funkcionális stílusban használjuk (functional reactive programming - FRP), ahol az
alapegység a függvény. Ennek jellemzője, hogy deklaratív, ezáltal könnyebben
olvasható, karbantartható és javítható. Apró egységekből, újrafelhasználható
operátorokból komplex megoldásokat lehet elkészíteni. Mivel az alapegysége az állapotmentes, mellékhatásmentes
függvények, könnyebben lehet vele párhuzamos algoritmusokat implementálni (könnyebben olvasható, kisebb a
hibázás lehetősége).</p>
<h2 id="reaktív-library-k">Reaktív library-k</h2>
<p>Ez az elv programozási nyelv független. A legtöbb programozási nyelvhez
több reaktív library is elérhető, sőt vannak olyanok,
melyek a legtöbb programozási nyelven elérhetőek és hasonlóan használhatóak (pl. ilyen a ReactiveX, Eclipse Vert.X).
Ezen library-k Java nyelven is elérhetőek (pl. ReactiveX/RxJava), de vannak további Java közeli implementációk is,
mint pl. az Akka, vagy a Project Reactor, mely a Spring mögött álló Pivotal megvalósítása.</p>
<p>Ezen library-k a funkcionális reaktív programozást teszik lehetővé, és a következőket ígérik:</p>
<ul>
<li>Olvashatóbb, karbantarthatóbb, hibamentesebb lesz a kód</li>
<li>Sok boilerplate kód eliminálható</li>
<li>Hibakezelést nem külön ágon kell megvalósítani, hanem a deklaratív leírás részét képzi</li>
<li>Defacto standard megoldásokat lehet használni gyakran felmerülő problémákra</li>
<li>Callback-hellt el lehet vele kerülni, melybe nem funkcionális programozás esetén, aszinkron hívásokkor hamar beleütközhetünk</li>
</ul>
<p>Nézzük meg, hogy pl. egy alkalmazott listát hogyan szűrünk, transzformálunk a különböző library-kkal.</p>
<p>Pl. RxJava esetén:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">Flowable</span><span class="o">.</span><span class="na">fromIterable</span><span class="o">(</span><span class="n">employees</span><span class="o">)</span>
<span class="o">.</span><span class="na">filter</span><span class="o">(</span><span class="n">employee</span> <span class="o">-></span> <span class="n">employee</span><span class="o">.</span><span class="na">getYearOfBirth</span><span class="o">()</span> <span class="o">>=</span> <span class="mi">2001</span><span class="o">)</span>
<span class="o">.</span><span class="na">map</span><span class="o">(</span><span class="nl">Employee:</span><span class="o">:</span><span class="n">getName</span><span class="o">)</span>
<span class="o">.</span><span class="na">map</span><span class="o">(</span><span class="nl">String:</span><span class="o">:</span><span class="n">toUpperCase</span><span class="o">)</span>
<span class="o">.</span><span class="na">subscribe</span><span class="o">(</span><span class="nc">System</span><span class="o">.</span><span class="na">out</span><span class="o">::</span><span class="n">println</span><span class="o">);</span>
</code></pre></div></div>
<p>Project Reactor:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">Flux</span><span class="o">.</span><span class="na">fromIterable</span><span class="o">(</span><span class="n">employees</span><span class="o">)</span>
<span class="o">.</span><span class="na">filter</span><span class="o">(</span><span class="n">employee</span> <span class="o">-></span> <span class="n">employee</span><span class="o">.</span><span class="na">getYearOfBirth</span><span class="o">()</span> <span class="o">>=</span> <span class="mi">2001</span><span class="o">)</span>
<span class="o">.</span><span class="na">map</span><span class="o">(</span><span class="nl">Employee:</span><span class="o">:</span><span class="n">getName</span><span class="o">)</span>
<span class="o">.</span><span class="na">map</span><span class="o">(</span><span class="nl">String:</span><span class="o">:</span><span class="n">toUpperCase</span><span class="o">)</span>
<span class="o">.</span><span class="na">subscribe</span><span class="o">(</span><span class="nc">System</span><span class="o">.</span><span class="na">out</span><span class="o">::</span><span class="n">println</span><span class="o">);</span>
</code></pre></div></div>
<p>A Project Reactor támogatja a tesztelést is a <code class="language-plaintext highlighter-rouge">StepVerifier</code> segítségével:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">StepVerifier</span><span class="o">.</span><span class="na">create</span><span class="o">(</span><span class="n">names</span><span class="o">)</span> <span class="c1">// Flux<String></span>
<span class="o">.</span><span class="na">expectNext</span><span class="o">(</span><span class="s">"John Doe"</span><span class="o">)</span>
<span class="o">.</span><span class="na">verifyComplete</span><span class="o">();</span>
</code></pre></div></div>
<p>Referenciaként álljon itt a Java 8-as megoldás streammel:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">employees</span><span class="o">.</span><span class="na">stream</span><span class="o">()</span>
<span class="o">.</span><span class="na">filter</span><span class="o">(</span><span class="n">employee</span> <span class="o">-></span> <span class="n">employee</span><span class="o">.</span><span class="na">getYearOfBirth</span><span class="o">()</span> <span class="o">>=</span> <span class="mi">2001</span><span class="o">)</span>
<span class="o">.</span><span class="na">map</span><span class="o">(</span><span class="nl">Employee:</span><span class="o">:</span><span class="n">getName</span><span class="o">)</span>
<span class="o">.</span><span class="na">map</span><span class="o">(</span><span class="nl">String:</span><span class="o">:</span><span class="n">toUpperCase</span><span class="o">)</span>
<span class="o">.</span><span class="na">forEach</span><span class="o">(</span><span class="nc">System</span><span class="o">.</span><span class="na">out</span><span class="o">::</span><span class="n">println</span><span class="o">);</span>
</code></pre></div></div>
<p>Látható, hogy mennyire hasonlóak a különböző library megoldásai, elemi
operátorokkal dolgoznak. Ezek ráadásul nagyon hasonlóak a Java 8 Stream API-ban
található operátorokhoz, annyi különbséggel, hogy ezen library-kban akár több száz ilyen
operátor is található. Ezek dokumentálása egységes, és nagyon látványos,
ún. marble diagramokkal dolgozik. Pl. itt látható a <code class="language-plaintext highlighter-rouge">map()</code> metódus diagramja.</p>
<p><img src="/artifacts/posts/2021-08-03-reaktiv-programozas/mapForFlux.svg" width="600" alt="Marble diagram" /></p>
<h2 id="project-reactor">Project Reactor</h2>
<p>A Project Reactor azonban nem csak egy library, hanem egy teljes megoldás, ugyanis
a következő modulok is hozzá tartoznak:</p>
<ul>
<li>Reactor Netty: HTTP, TCP, UDP kliens/szerver, Netty-re építve</li>
<li>Reactor Kafka: Kafka integráció</li>
<li>Reactor RabbitMQ: RabbitMQ integráció</li>
</ul>
<p>Alapvetően a <code class="language-plaintext highlighter-rouge">Mono</code> és <code class="language-plaintext highlighter-rouge">Flux</code> nevezetű típusos adatfolyamokra épít, ahol mindkettő
implementálja a <code class="language-plaintext highlighter-rouge">Publisher</code> interfészt, és az előbbi nulla vagy egy elemet (mint a Java 8 <code class="language-plaintext highlighter-rouge">Optional</code>), míg
az utóbbi nulla vagy több elemet tartalmazhat (mint a Java 8 <code class="language-plaintext highlighter-rouge">Stream</code>). (Érdekes, hogy a kettő közötti
átjárhatóságot csak a Java 9-ben implementálták, ahol megjelent az <code class="language-plaintext highlighter-rouge">Optional</code> <code class="language-plaintext highlighter-rouge">stream()</code> metódusa.)</p>
<h2 id="reactive-streams">Reactive Streams</h2>
<p>Azonban hamar felmerült az igény, hogy ezen library-kat össze lehessen egymással kapcsolni,
ehhez azonban közös interfészekre volt igény. Ezeket a <a href="https://www.reactive-streams.org/">Reactive Streams</a>
kezdeményezésen belül alakították ki. A téma fontosságát mutatja,
hogy ez bekerült a Java 9-be Flow API néven (<code class="language-plaintext highlighter-rouge">java.util.concurrent.Flow</code> osztály belső
interfészei és osztályai). A következő osztályok és interfészek
kerültek kialakításra: <code class="language-plaintext highlighter-rouge">Publisher</code>, <code class="language-plaintext highlighter-rouge">Subscriber</code>, <code class="language-plaintext highlighter-rouge">Subscription</code>.</p>
<p><img src="/artifacts/posts/2021-08-03-reaktiv-programozas/java-9-flow-api.png" alt="Java 9 Flow API" /></p>
<p>A <code class="language-plaintext highlighter-rouge">Publisher</code> mely az elemeket állítja elő. Erre egy <code class="language-plaintext highlighter-rouge">Subscriber</code> fel tud iratkozni,
ekkor jön létre egy <code class="language-plaintext highlighter-rouge">Subscription</code> objektum. Ezen keresztül lehet kérni a következő
elemeket a <code class="language-plaintext highlighter-rouge">request()</code> metódussal. Ennek hatására az elemek előállításra, majd
átadásra kerülnek, a <code class="language-plaintext highlighter-rouge">Subscriber</code> <code class="language-plaintext highlighter-rouge">onNext()</code> metódusának. Érdekesség még, hogy a <code class="language-plaintext highlighter-rouge">java.util.Observable</code>
Java 9-től deprecated, és a Flow API-t javasolja.</p>
<p>A Java standard osztálykönyvtárban ezen interfészeknek nincs sok implementációjuk, bár a JavaDoc
leírja, hogyan lehet ezeket megvalósítani. Egyedül a Java 11-ben megjelent, beépített aszinkron
nem-blokkoló <code class="language-plaintext highlighter-rouge">java.net.http.HttpClient</code> használja.</p>
<p>Az API megjelenésével nézzük meg, hogyan lehet egy RxJava-s <code class="language-plaintext highlighter-rouge">Flowable</code>-ből egy
Project Reactoros <code class="language-plaintext highlighter-rouge">Flux</code>-ot létrehozni (egy egyszerű <code class="language-plaintext highlighter-rouge">from()</code> metódussal).
Hiszen mindkettő implementálja a <code class="language-plaintext highlighter-rouge">Publisher</code> interfészt.</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">Flowable</span><span class="o"><</span><span class="nc">String</span><span class="o">></span> <span class="n">names</span> <span class="o">=</span> <span class="nc">Flowable</span><span class="o">.</span><span class="na">fromIterable</span><span class="o">(</span><span class="n">employees</span><span class="o">)</span>
<span class="o">.</span><span class="na">filter</span><span class="o">(</span><span class="n">employee</span> <span class="o">-></span> <span class="n">employee</span><span class="o">.</span><span class="na">getYearOfBirth</span><span class="o">()</span> <span class="o">>=</span> <span class="mi">2001</span><span class="o">)</span>
<span class="o">.</span><span class="na">map</span><span class="o">(</span><span class="nl">Employee:</span><span class="o">:</span><span class="n">getName</span><span class="o">);</span>
<span class="nc">Flux</span><span class="o">.</span><span class="na">from</span><span class="o">(</span><span class="n">names</span><span class="o">)</span>
<span class="o">.</span><span class="na">map</span><span class="o">(</span><span class="nl">String:</span><span class="o">:</span><span class="n">toUpperCase</span><span class="o">)</span>
<span class="o">.</span><span class="na">subscribe</span><span class="o">(</span><span class="nc">System</span><span class="o">.</span><span class="na">out</span><span class="o">::</span><span class="n">println</span><span class="o">);</span>
</code></pre></div></div>
<p>Hiába van ilyen API-nk, egy reaktív architektúrát csak akkor tudjuk kihasználni, ha minden eleme aszinkron és nem-blokkoló.
Azaz sem a webes keretrendszer, sem az adatbázishozzáférés nem lehet szinkron blokkoló.</p>
<h2 id="spring-webflux">Spring WebFlux</h2>
<p>A Spring Framework 5 egyik legnagyobb újdonsága egy
reaktív webes keretrendszer, a Spring WebFlux. Jellemzője, hogy aszinkron, nem blokkoló futást és
funkcionális programozást tesz lehetővé a Project Reactorra építve, valamint a más platformokon bizonyító
keretrendszerekhez hasonlóan a kiszolgálás kevés újrafelhasználható szálon történik (event loop workers).</p>
<p><img src="/artifacts/posts/2021-08-03-reaktiv-programozas/Non-blocking-request-processing.png" alt="Request processing" /></p>
<p>A Spring fejlesztői úgy döntöttek, hogy nem ágaztatják el a Spring MVC keretrendszer kódját, hanem
a Spring MVC tapasztalataira építve vele párhuzamosan fejlesztik ki a Spring WebFluxot.
Ez azonban már az alapokban eltér, ez ugyanis nem a Servlet API-ra épít,
hanem a Reactive HTTP API-ra. (Ezt a keretrendszert használva Spring Boot esetén már nem a Tomcat,
hanem a Netty lesz az alapértelmezetten beépített konténer.)</p>
<p>WebFlux esetén is lehet ugyanúgy controllereket létrehozni a <code class="language-plaintext highlighter-rouge">@RestController</code>, <code class="language-plaintext highlighter-rouge">@GetMapping</code>, stb.
annotációkkal.</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@RestController</span>
<span class="nd">@RequestMapping</span><span class="o">(</span><span class="s">"/api/employees"</span><span class="o">)</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">EmployeeController</span> <span class="o">{</span>
<span class="kd">private</span> <span class="nc">EmployeeService</span> <span class="n">employeeService</span><span class="o">;</span>
<span class="nd">@GetMapping</span>
<span class="kd">public</span> <span class="nc">Flux</span><span class="o"><</span><span class="nc">EmployeeDto</span><span class="o">></span> <span class="nf">listEmployees</span><span class="o">()</span> <span class="o">{</span>
<span class="k">return</span> <span class="n">employeeService</span><span class="o">.</span><span class="na">listEmployees</span><span class="o">();</span>
<span class="o">}</span>
<span class="nd">@PostMapping</span>
<span class="kd">public</span> <span class="nc">Mono</span><span class="o"><</span><span class="nc">EmployeeDto</span><span class="o">></span> <span class="nf">createEmployee</span><span class="o">(</span><span class="nd">@RequestBody</span> <span class="nc">Mono</span><span class="o"><</span><span class="nc">CreateEmployeeCommand</span><span class="o">></span> <span class="n">command</span><span class="o">)</span> <span class="o">{</span>
<span class="k">return</span> <span class="n">employeeService</span><span class="o">.</span><span class="na">createEmployee</span><span class="o">(</span><span class="n">command</span><span class="o">);</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>Látható, hogy itt a paraméterek és a visszatérési értékek <code class="language-plaintext highlighter-rouge">Mono</code> vagy <code class="language-plaintext highlighter-rouge">Flux</code> típusúak.</p>
<p>De ezek mellett alkalmazhatunk ún. router functionöket,
melyekkel funkcionális módon adhatjuk meg, hogy melyik URL esetén mely függvény kerüljön meghívásra.</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Configuration</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">EmployeeController</span> <span class="o">{</span>
<span class="kd">private</span> <span class="nc">EmployeeService</span> <span class="n">employeeService</span><span class="o">;</span>
<span class="nd">@Bean</span>
<span class="kd">public</span> <span class="nc">RouterFunction</span><span class="o"><</span><span class="nc">ServerResponse</span><span class="o">></span> <span class="nf">route</span><span class="o">()</span> <span class="o">{</span>
<span class="k">return</span> <span class="nc">RouterFunctions</span>
<span class="o">.</span><span class="na">route</span><span class="o">(</span><span class="nc">RequestPredicates</span><span class="o">.</span><span class="na">GET</span><span class="o">(</span><span class="s">"/api/employees"</span><span class="o">),</span> <span class="nl">employeeService:</span><span class="o">:</span><span class="n">listEmployees</span><span class="o">)</span>
<span class="o">.</span><span class="na">and</span><span class="o">(</span><span class="n">route</span><span class="o">(</span><span class="no">POST</span><span class="o">(</span><span class="s">"/api/employees"</span><span class="o">),</span> <span class="nl">employeeService:</span><span class="o">:</span><span class="n">createEmployee</span><span class="o">);</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<h2 id="perzisztens-réteg-r2dbc-vel">Perzisztens réteg R2DBC-vel</h2>
<p>A NoSQL adatbázisoknál hamarabb találunk aszinkron nem-blokkoló drivert (pl. MongoDB esetén alapból ilyen),
azonban a klasszikus JDBC driverek mind szinkron és blokkoló. Erre hozták létre a <a href="https://r2dbc.io/">R2DBC</a>
projektet, melyben H2, PostgreSQL, Microsoft SQL Server és MySQL-hez van implementáció.</p>
<p>Ez a következőképp használható pl. H2 adatbázis esetén:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">ConnectionFactory</span> <span class="n">connectionFactory</span> <span class="o">=</span> <span class="nc">ConnectionFactories</span>
<span class="o">.</span><span class="na">get</span><span class="o">(</span><span class="s">"r2dbc:h2:mem:///testdb"</span><span class="o">);</span>
<span class="nc">Mono</span><span class="o">.</span><span class="na">from</span><span class="o">(</span><span class="n">connectionFactory</span><span class="o">.</span><span class="na">create</span><span class="o">())</span>
<span class="o">.</span><span class="na">flatMapMany</span><span class="o">(</span><span class="n">connection</span> <span class="o">-></span> <span class="n">connection</span>
<span class="o">.</span><span class="na">createStatement</span><span class="o">(</span><span class="s">"SELECT firstname FROM PERSON WHERE age > $1"</span><span class="o">)</span>
<span class="o">.</span><span class="na">bind</span><span class="o">(</span><span class="s">"$1"</span><span class="o">,</span> <span class="mi">42</span><span class="o">)</span>
<span class="o">.</span><span class="na">execute</span><span class="o">())</span>
<span class="o">.</span><span class="na">flatMap</span><span class="o">(</span><span class="n">result</span> <span class="o">-></span> <span class="n">result</span>
<span class="o">.</span><span class="na">map</span><span class="o">((</span><span class="n">row</span><span class="o">,</span> <span class="n">rowMetadata</span><span class="o">)</span> <span class="o">-></span> <span class="n">row</span><span class="o">.</span><span class="na">get</span><span class="o">(</span><span class="s">"firstname"</span><span class="o">,</span> <span class="nc">String</span><span class="o">.</span><span class="na">class</span><span class="o">)))</span>
<span class="o">.</span><span class="na">doOnNext</span><span class="o">(</span><span class="nc">System</span><span class="o">.</span><span class="na">out</span><span class="o">::</span><span class="n">println</span><span class="o">)</span>
<span class="o">.</span><span class="na">subscribe</span><span class="o">();</span>
</code></pre></div></div>
<p>Ahhoz, hogy ezt ne kelljen ilyen alacsony szinten használni, a Spring Data irányelveihez illeszkedve
létrehozták a Spring Data R2DBC projektet is. Egyrészt biztosít egy <code class="language-plaintext highlighter-rouge">DatabaseClient</code>, melyen keresztül
funkcionális módon lehet hozzáférni az adatbázishoz. Ezen kívül a szokásos módon képes repository
interfészhez implementációt is generálni.</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">interface</span> <span class="nc">EmployeesRepository</span> <span class="kd">extends</span> <span class="nc">ReactiveCrudRepository</span><span class="o"><</span><span class="nc">Employee</span><span class="o">,</span> <span class="nc">Long</span><span class="o">></span> <span class="o">{</span>
<span class="o">}</span>
</code></pre></div></div>
<p>A tranzakciókezelés a klasszikus architektúra esetén deklaratív esetben a <code class="language-plaintext highlighter-rouge">@Transactional</code>
annotációval működik, és mögötte a szálhoz kapcsolt <code class="language-plaintext highlighter-rouge">Transaction</code> objektum áll (<code class="language-plaintext highlighter-rouge">ThreadLocal</code>-lal implementálva).
Itt is használható a <code class="language-plaintext highlighter-rouge">@Transactional</code> annotáció, de már más implementáció van mögötte.</p>
<p>A service rétegben történhet az entitás és DTO-k közötti megfeleltetés, nézzük is meg, hogyan történhet
mindez reaktív módon:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">class</span> <span class="nc">EmployeeService</span> <span class="o">{</span>
<span class="kd">private</span> <span class="kd">final</span> <span class="nc">EmployeeRepository</span> <span class="n">employeeRepository</span><span class="o">;</span>
<span class="kd">public</span> <span class="nc">Flux</span><span class="o"><</span><span class="nc">EmployeeDto</span><span class="o">></span> <span class="nf">listEmployees</span><span class="o">()</span> <span class="o">{</span>
<span class="k">return</span> <span class="n">employeeRepository</span><span class="o">.</span><span class="na">findAll</span><span class="o">().</span><span class="na">map</span><span class="o">(</span><span class="k">this</span><span class="o">::</span><span class="n">toEmployeeDto</span><span class="o">);</span>
<span class="o">}</span>
<span class="kd">public</span> <span class="nc">Mono</span><span class="o"><</span><span class="nc">EmployeeDto</span><span class="o">></span> <span class="nf">createEmployee</span><span class="o">(</span><span class="nd">@RequestBody</span> <span class="nc">Mono</span><span class="o"><</span><span class="nc">CreateEmployeeCommand</span><span class="o">></span> <span class="n">command</span><span class="o">)</span> <span class="o">{</span>
<span class="k">return</span> <span class="n">command</span>
<span class="o">.</span><span class="na">map</span><span class="o">(</span><span class="k">this</span><span class="o">::</span><span class="n">toEmployee</span><span class="o">)</span>
<span class="o">.</span><span class="na">flatMap</span><span class="o">(</span><span class="nl">employeeRepository:</span><span class="o">:</span><span class="n">save</span><span class="o">)</span>
<span class="o">.</span><span class="na">map</span><span class="o">(</span><span class="k">this</span><span class="o">::</span><span class="n">toEmployeeDto</span><span class="o">);</span>
<span class="o">}</span>
<span class="kd">private</span> <span class="nc">EmployeeDto</span> <span class="nf">toEmployeeDto</span><span class="o">(</span><span class="nc">Employee</span> <span class="n">employee</span><span class="o">)</span> <span class="o">{</span>
<span class="k">return</span> <span class="k">new</span> <span class="nf">EmployeeDto</span><span class="o">(</span><span class="n">employee</span><span class="o">.</span><span class="na">getId</span><span class="o">(),</span> <span class="n">employee</span><span class="o">.</span><span class="na">getName</span><span class="o">());</span>
<span class="o">}</span>
<span class="kd">private</span> <span class="nc">Employee</span> <span class="nf">toEmployee</span><span class="o">(</span><span class="nc">CreateEmployeeCommand</span> <span class="n">command</span><span class="o">)</span> <span class="o">{</span>
<span class="k">return</span> <span class="k">new</span> <span class="nf">Employee</span><span class="o">(</span><span class="n">command</span><span class="o">.</span><span class="na">getName</span><span class="o">());</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<h2 id="integráció">Integráció</h2>
<p>Természetesen ha külső rendszer REST webszolgáltatását akarjuk meghívni, akkor is aszinkron nem-blokkoló módon kell megtennünk.
Erre biztosítja a Spring a <code class="language-plaintext highlighter-rouge">org.springframework.web.reactive.function.client.WebClient</code> osztályt.</p>
<p>Ha a reaktív gondolkodásmódba jobban beépülő kommunikációs formát akarunk választani, akkor
használható az RSocket bináris protokoll, vagy használhatunk valamilyen üzenetküldő megoldást, pl.
RabbitMQ-t, vagy Kafkát.</p>
<p>A témáról workshopot is tartottam a <a href="https://ithon.info/megmerettetes/">III. Országos IT Megmérettetés Díjátadó rendezvényén</a>
(<a href="https://youtu.be/E4VgPN9Q6Gk">YouTube videó</a>), melynek a Training360 is kiemelt támogatója. A
slide-ok <a href="/artifacts/2019-11-workshop/index.html">itt is elérhetőek</a>.</p>
<p>Az ahhoz tartozó példaprojekt megtekinthető a <a href="https://github.com/vicziani/jtechlog-distance-webflux">GitHub-on</a>.
Ez egy háromrétegű alkalmazás, Spring WebFlux-szal, Spring Data R2DBC-vel, H2 adatbázissal.
Sőt WebClienttel egy http kérést is küldd egy külső alkalmazás felé.</p>
JWT kezelése Java/Jakarta EE környezetben JAX-RS-sel2021-06-02T06:00:00+00:00http://www.jtechlog.hu/2021/06/02/jwt-jax-rs<p>Egy korábbi posztban (<a href="/2019/03/18/jwt-es-spring-security.html">JWT és Spring Security</a>)
írtam arról, hogy mi az a JWT token, és hogyan lehet használni Spring Security-vel.
Ez a poszt azt írja le, hogy lehet JAX-RS-sel használni (ami a Java/Jakarta EE szabvány része is).</p>
<p>A példámban a Jetty servlet containert, Jersey JAX-RS implementációt fogom használni,
és a <a href="https://github.com/jwtk/jjwt">Java JWT</a> könyvtárat. A szabványos megoldásnak
köszönhetően bármelyik komponens cserélhető.</p>
<p>A folyamat nagyon egyszerű:</p>
<ul>
<li>A felhasználó bejelentkezik, és ezáltal kap egy HttpOnly Cookie-t, benne a tokennel, melyet minden kéréssel visszaküld
a szervernek</li>
<li>Egy JAX-RS <code class="language-plaintext highlighter-rouge">ContainerRequestFilter</code> példányon megy keresztül minden kérés. Ez ellenőrzi, kicsomagolja a tokent, és
létrehoz egy új <code class="language-plaintext highlighter-rouge">SecurityContext</code> példányt, mely tárolja a bejelentkezett felhasználó adatait, pl. a felhasználónevet és szerepkörét</li>
<li>A JAX-RS végpontban, azaz a <code class="language-plaintext highlighter-rouge">Resource</code>-ban már paraméter injectionnel elérhető a <code class="language-plaintext highlighter-rouge">SecurityContext</code>, de működik pl. a <code class="language-plaintext highlighter-rouge">@RolesAllowed</code>
annotáció is</li>
</ul>
<!-- more -->
<p>A poszthoz egy példa projekt is tartozik, mely <a href="https://github.com/vicziani/jtechlog-jwt-javaee">elérhető a GitHubon</a>.
Indítható az <code class="language-plaintext highlighter-rouge">mvn package jetty:run</code> paranccsal, példa http kérések a <code class="language-plaintext highlighter-rouge">src/test/http/hello.http</code> fájlban találhatóak.
Automata integrációs tesztek is vannak.</p>
<p>Bejelentkezéshez el kell küldeni a következő kérést:</p>
<pre><code class="language-plain">POST http://localhost:8080/api/auth
Content-Type: application/json
{
"username": "user",
"password": "user"
}
</code></pre>
<p>Ezt az <code class="language-plaintext highlighter-rouge">AuthResource</code> kapja meg, ez először a JJWT használatával előállít egy tokent:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">return</span> <span class="nc">Jwts</span><span class="o">.</span><span class="na">builder</span><span class="o">()</span>
<span class="o">.</span><span class="na">setSubject</span><span class="o">(</span><span class="n">username</span><span class="o">)</span>
<span class="o">.</span><span class="na">claim</span><span class="o">(</span><span class="no">ROLE_CLAIM</span><span class="o">,</span> <span class="n">role</span><span class="o">)</span>
<span class="o">.</span><span class="na">setIssuedAt</span><span class="o">(</span><span class="k">new</span> <span class="nc">Date</span><span class="o">(</span><span class="n">now</span><span class="o">))</span>
<span class="o">.</span><span class="na">setExpiration</span><span class="o">(</span><span class="k">new</span> <span class="nc">Date</span><span class="o">(</span><span class="n">now</span> <span class="o">+</span> <span class="no">EXPIRATION</span><span class="o">))</span>
<span class="o">.</span><span class="na">signWith</span><span class="o">(</span><span class="nc">SignatureAlgorithm</span><span class="o">.</span><span class="na">HS512</span><span class="o">,</span> <span class="no">SECRET</span><span class="o">.</span><span class="na">getBytes</span><span class="o">())</span>
<span class="o">.</span><span class="na">compact</span><span class="o">();</span>
</code></pre></div></div>
<p>Látható, hogy a szabványos mezőkön kívül a <code class="language-plaintext highlighter-rouge">claim()</code> metódussal saját mezőket is fel lehet venni,
a kód a felhasználó szerepkörét is eltárolja a tokenben.</p>
<p>Majd visszaad egy cookie-t, mely tartalmazza a tokent:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">NewCookie</span> <span class="n">newCookie</span> <span class="o">=</span>
<span class="k">new</span> <span class="nf">NewCookie</span><span class="o">(</span><span class="no">COOKIE_NAME</span><span class="o">,</span> <span class="n">token</span><span class="o">,</span> <span class="s">"/"</span><span class="o">,</span> <span class="s">"http://localhost:8080"</span><span class="o">,</span> <span class="s">"JWT"</span><span class="o">,</span> <span class="mi">30</span> <span class="o">*</span> <span class="mi">60</span><span class="o">,</span> <span class="kc">true</span><span class="o">,</span> <span class="kc">true</span><span class="o">);</span>
<span class="k">return</span> <span class="nc">Response</span><span class="o">.</span><span class="na">ok</span><span class="o">(</span><span class="k">new</span> <span class="nc">MessageResponse</span><span class="o">(</span><span class="s">"Successful"</span><span class="o">))</span>
<span class="o">.</span><span class="na">cookie</span><span class="o">(</span><span class="n">newCookie</span><span class="o">)</span>
<span class="o">.</span><span class="na">build</span><span class="o">();</span>
</code></pre></div></div>
<p>Ez a http válaszban valahogy így néz ki:</p>
<pre><code class="language-plain">Set-Cookie: token=eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJ1c2VyIiwicm9sZSI6InVzZXIiLCJpYXQiOjE2MjI2MzkwMDgsImV4cCI6MTYyMjY0MDgwOH0.VxwmtuuAUPXlhYZVfX7uHc4RQWt8laR65Kb4YmKNm2azSPtvAkWO8fluVp_H4a2v9j_uvtK4fFE_qBraxmBqzg;Version=1;Comment=JWT;Domain=http://localhost:8080;Path=/;Max-Age=1800;Secure;HttpOnly
</code></pre>
<p>Ha ezt bemásoljuk a <a href="https://jwt.io/">https://jwt.io/</a> címen található dekóderbe, akkor a következőt kapjuk:</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
</span><span class="nl">"sub"</span><span class="p">:</span><span class="w"> </span><span class="s2">"user"</span><span class="p">,</span><span class="w">
</span><span class="nl">"role"</span><span class="p">:</span><span class="w"> </span><span class="s2">"user"</span><span class="p">,</span><span class="w">
</span><span class="nl">"iat"</span><span class="p">:</span><span class="w"> </span><span class="mi">1622639008</span><span class="p">,</span><span class="w">
</span><span class="nl">"exp"</span><span class="p">:</span><span class="w"> </span><span class="mi">1622640808</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
<p>Ezt a tokent kell minden alkalommal elküldeni fejlécben:</p>
<pre><code class="language-plain">GET http://localhost:8080/api/hello/user
Accept: application/json
Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJ1c2VyIiwicm9sZSI6InVzZXIiLCJpYXQiOjE2MjI2MzkwMDgsImV4cCI6MTYyMjY0MDgwOH0.VxwmtuuAUPXlhYZVfX7uHc4RQWt8laR65Kb4YmKNm2azSPtvAkWO8fluVp_H4a2v9j_uvtK4fFE_qBraxmBqzg
</code></pre>
<p>Ezt a címet a <code class="language-plaintext highlighter-rouge">JwtFilter</code> fogja fogadni, mely egy <code class="language-plaintext highlighter-rouge">ContainerRequestFilter</code>. Fontos, hogy rajta legyen a
<code class="language-plaintext highlighter-rouge">@Provider</code> és <code class="language-plaintext highlighter-rouge">@Priority(Priorities.AUTHENTICATION)</code> annotáció. Az implementálásnál arra is kell figyelni,
hogy a <code class="language-plaintext highlighter-rouge">/api/auth</code> cím esetén ne aktiválódjon, különben a bejelentkezéshez is bejelentkezést fog kérni.</p>
<p>A filter kiveszi a tokent a headerből, ellenőrzi és kicsomagolja a JJWT segítségével.</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">Jwts</span><span class="o">.</span><span class="na">parser</span><span class="o">()</span>
<span class="o">.</span><span class="na">setSigningKey</span><span class="o">(</span><span class="no">SECRET</span><span class="o">.</span><span class="na">getBytes</span><span class="o">())</span>
<span class="o">.</span><span class="na">parseClaimsJws</span><span class="o">(</span><span class="n">token</span><span class="o">)</span>
<span class="o">.</span><span class="na">getBody</span><span class="o">();</span>
</code></pre></div></div>
<p>A tokenből olvassa ki
a felhasználónevet és a szerepkört is. Ahhoz, hogy bejelentkeztessen egy felhasználót, egy <code class="language-plaintext highlighter-rouge">SecurityContext</code>
interfészt implementáló osztályt kell megvalósítanunk, aminek <code class="language-plaintext highlighter-rouge">getUserPrincipal()</code> metódusa adja vissza a
felhasználó nevét, az <code class="language-plaintext highlighter-rouge">isUserInRole(String role)</code> metódusa pedig ellenőrzi, hogy a felhasználó
rendelkezik-e a paraméterként átadott szerepkörrel.</p>
<p>A vezérlés ezután a <code class="language-plaintext highlighter-rouge">HelloResource</code>-ra ugrik.</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@GET</span>
<span class="nd">@RolesAllowed</span><span class="o">(</span><span class="s">"user"</span><span class="o">)</span>
<span class="kd">public</span> <span class="nc">MessageResponse</span> <span class="nf">sayHello</span><span class="o">(</span><span class="nd">@Context</span> <span class="nc">SecurityContext</span> <span class="n">context</span><span class="o">)</span> <span class="o">{</span>
<span class="nc">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="s">"Username: "</span> <span class="o">+</span> <span class="n">context</span><span class="o">.</span><span class="na">getUserPrincipal</span><span class="o">().</span><span class="na">getName</span><span class="o">());</span>
<span class="nc">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="s">"Has user role? "</span> <span class="o">+</span> <span class="n">context</span><span class="o">.</span><span class="na">isUserInRole</span><span class="o">(</span><span class="s">"user"</span><span class="o">));</span>
<span class="k">return</span> <span class="k">new</span> <span class="nf">MessageResponse</span><span class="o">(</span><span class="s">"Hello JAX-WS!"</span><span class="o">);</span>
<span class="o">}</span>
</code></pre></div></div>
<p>Itt a <code class="language-plaintext highlighter-rouge">@RolesAllowed("user")</code> annotáció hatására
ellenőrzésre kerül, hogy a felhasználó rendelkezik-e az adott szerepkörrel. Ekkor automatikusan
meghívja a <code class="language-plaintext highlighter-rouge">isUserInRole(String role)</code> metódust. Ha ez <code class="language-plaintext highlighter-rouge">false</code> értéket ad vissza, akkor
403 (Forbidden) státuszkódot kapunk vissza. Ez a deklaratív ellenőrzés.</p>
<p>Azonban a bejelentkezett felhasználót, és szerepköreit programozottan is le lehet kérdezni,
ekkor paraméter injectiont alkalmazva egy <code class="language-plaintext highlighter-rouge">SecurityContext</code> példányt kapunk, ha rajta van
egy <code class="language-plaintext highlighter-rouge">@Context</code> annotáció.</p>
<p>Amire még figyelni kell, hogy ahhoz, hogy a <code class="language-plaintext highlighter-rouge">@RolesAllowed</code> annotációk működjenek,
a Jersey-nek be kell kapcsolni a <code class="language-plaintext highlighter-rouge">RolesAllowedDynamicFeature</code>-t. Ez a következő osztállyal
lehetséges:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@ApplicationPath</span><span class="o">(</span><span class="s">"api"</span><span class="o">)</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">JerseyConfig</span> <span class="kd">extends</span> <span class="nc">ResourceConfig</span> <span class="o">{</span>
<span class="kd">public</span> <span class="nf">JerseyConfig</span><span class="o">()</span> <span class="o">{</span>
<span class="n">packages</span><span class="o">(</span><span class="s">"hello"</span><span class="o">);</span>
<span class="n">register</span><span class="o">(</span><span class="nc">RolesAllowedDynamicFeature</span><span class="o">.</span><span class="na">class</span><span class="o">);</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>Az automata tesztesetek elindítanak egy beépített Grizzly Http Servert, és itt is
kell a <code class="language-plaintext highlighter-rouge">RolesAllowedDynamicFeature</code> regisztrálása. Majd egyszerű, szabványos
JAX-RS kliens hívásokkal ellenőrzik a működést.</p>
Elosztott tranzakciókezelés Spring Boottal2021-01-04T19:00:00+00:00http://www.jtechlog.hu/2021/01/04/spring-boot-elosztott-tranzakciokezeles<p>A tranzakciókezelésről már többször írtam. Egy bevezető
elérhető a <a href="/2010/05/31/tranzakciokezeles.html">Tranzakciókezelés EJB 3 és Spring környezetben</a>
címen is, melyet most frissítettem.</p>
<p>A jelenlegi posztban viszont azt írom le, hogyan lehet Spring Boottal elosztott
tranzakciókezelést végezni.</p>
<p>Elosztott tranzakciókezelést akkor használunk, ha egy tranzakcióba
szeretnénk foglalni két külön adatbázisban történő műveletet,
vagy még gyakoribb példa, ha egy tranzakcióba szeretnénk foglalni
egy adatbázis beszúrást és egy JMS üzenet elküldését. Látható,
hogy nem csak adatbázisok képesek tranzakcióban részt venni, hanem
pl. JMS-en elérhető üzenetkezelő middleware-ek
(Message Oriented Middleware - MOM) is. Összefoglaló nevén ezek
un. erőforráskezelők.</p>
<p>Az elosztott tranzakciókezeléshez egy tranzakció koordinátort kell kinevezni, aki
irányítja a tranzakciót. Itt jön képbe a two-phase commit
protocol (2PC), ahol első körben az erőforráskezelők felkészülnek a
tranzakcióra, és második körben hagyják jóvá azt. Minden
erőforráskezelőnek vétójoga van. Az erőforráskezelők a tranzakció
koordinátorral az X/Open XA protokollon keresztül kommunikálnak.</p>
<!-- more -->
<p>Java környezetben elosztott tranzakciók kezelésére a JTA szabványt
használjuk. Ez két részből áll. A <code class="language-plaintext highlighter-rouge">javax.transaction</code> csomagban
lévő interfészek és annotációk lehetővé teszik, hogy az
alkalmazás a tranzakciókra vonatkozó szabályokat adjon meg.
Itt van pl. a <code class="language-plaintext highlighter-rouge">@javax.transaction.Transactional</code> annotáció.
Spring esetén nem ezt, hanem a saját <code class="language-plaintext highlighter-rouge">@org.springframework.transaction.annotation.Transactional</code>
annotációját használjuk.
A <code class="language-plaintext highlighter-rouge">javax.transaction.xa</code> csomagban lévő interfészek pedig
API-t biztosítanak a tranzakció koordinátor (, melyet a JTA Transaction Managernek hív) és az
erőforráskezelők (Resource Manager) közötti kommunikációra. Így tudnak egymáshoz
lazán kapcsolódva együttműködni.</p>
<p><img src="/artifacts/posts/2021-01-04-spring-boot-elosztott-tranzakciokezeles/jta.png" alt="JTA" /></p>
<p>Több JTA implementáció is van, mint pl. az Atomicos vagy Narayana. A Spring
jelenleg az Atomicost támogatja. Ehhez használatához csak annyit kell tenni,
hogy a hozzá tartozó starter projektet fel kell venni függőségként a <code class="language-plaintext highlighter-rouge">pom.xml</code> állományba.</p>
<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt"><dependency></span>
<span class="nt"><groupId></span>org.springframework.boot<span class="nt"></groupId></span>
<span class="nt"><artifactId></span>spring-boot-starter-jta-atomikos<span class="nt"></artifactId></span>
<span class="nt"></dependency></span>
</code></pre></div></div>
<p>A <a href="https://github.com/vicziani/jtechlog-jms-transaction">https://github.com/vicziani/jtechlog-jms-transaction</a>
címen található egy példa projekt. Ez elindít egy beágyazott
H2 adatbázist, melyhez Spring Data JPA-val fér hozzá. Valamint elindít
egy beágyazott Apache Artemis JMS providert.</p>
<p><img src="/artifacts/posts/2021-01-04-spring-boot-elosztott-tranzakciokezeles/alkalmazas.png" alt="Alkalmazás" /></p>
<p>Ha beküldünk egy kérést, akkor az <code class="language-plaintext highlighter-rouge">EmployeesService</code> indít egy tranzakciót,
és beszúr egy rekordot az <code class="language-plaintext highlighter-rouge">employees</code>
táblába az <code class="language-plaintext highlighter-rouge">EmployeeRepository</code> használatával,
valamint elküld egy üzenetet a <code class="language-plaintext highlighter-rouge">employees.queue</code> sorba a <code class="language-plaintext highlighter-rouge">JmsTemplate</code>
használatával. A sort egy <code class="language-plaintext highlighter-rouge">EmployeesQueueListener</code> figyeli,
mely ha üzenetet kap, továbbhív az <code class="language-plaintext highlighter-rouge">EventsService</code>-be.
Ha egy kivételt dobunk, akkor sem az adatbázis
beszúrás, sem a JMS üzenetküldés nem kerül végrehajtásra,
hiszen mindkettő rollbackel.</p>
<p>A projekthez egy <code class="language-plaintext highlighter-rouge">EmployeesIT</code> integrációs teszt is tartozik,
mely teszteli a helyes értékkel a commitot és helytelen értékkel a rollbacket.
Egyrészt adatbázisból lekérdezi a rekordokat.
Másrészt mockolja az <code class="language-plaintext highlighter-rouge">EventsService</code>-t, és azt figyeli, hogy annak metódusa meghívásra
került-e, ezzel vizsgálva, hogy érkezett-e üzenet a sorba.</p>
A Java EE jelene, jövője és tesztelése2020-12-28T20:00:00+00:00http://www.jtechlog.hu/2020/12/28/java-ee-jelen-jovo-teszt<p>A Java EE technológiai manapság igencsak megosztó. Bár már sokan temetik, ettől függetlenül
igenis van jelene. Egyrészt sok alkalmazás épül Java EE technológiára. Ezeket
folyamatosan üzemeltetni kell, karban kell tartani és tovább kell fejleszteni.</p>
<p>Több projektről is hallottam,
melynek célja az volt, hogy a Java EE technológiát lecseréljék,
azonban ennek költsége, időigénye rendkívül magas volt, anélkül, hogy
új funkciók bekerültek volna. Egy ilyen projekt nem a management kedvence.
Több esetben azt is láttam, hogy az új techológiában a fejlesztőcsapat nem volt
kellően jártas, és hasonló szörnyet hozott létre. Volt, ahol ugyanaz a
team dolgozott a régi alkalmazás karbantartásán, valamint az új fejlesztésén.
Elképzelhető, mennyire voltak lelkesek, milyen minőségű munkát végeztek,
mikor a régihez kellett nyúlni. Volt, ahol két külön csapat dolgozott,
és óhatatlanul is kialakultak feszültségek. Mindkét felállásban
megvan a veszélye, hogy lelőjjék az hibásan “refaktor” néven
emlegetett projektet. Pl. elfogy a keret, nem hozza meg
az elvárt eredményt, bejön egy fontosabb projekt, hisz a régi
úgyis működik.</p>
<p>A <em>Clean Architecture</em> könyv szerint is a keretrendszer csak implementációs
részlet. Az érték az üzleti tudásban és funkciókban van. A túlzott
függőség a keretrendszerekre, ezekbe fektetett túlzott energia rendkívül sok
veszéllyel jár.</p>
<p>A Java EE folyamatosan fejlődik, érdemes figyelemmel követni az újdonságokat.
Folyamatosan vannak igények Java EE oktatásokra is, hiszen a projekten dolgozókat meg kell ismertetni
a szabványok új verzióival, valamint új fejlesztők is csatlakoznak, akiket be
kell tanítani. És ezen projekteken gyakori a unit és integrációs tesztek hiánya.
És ez egy részben a fejlesztőknek, de más részben a technológiának is felróható.</p>
<p>Szóval nézzük meg, hogyan lehet modernizálni egy Java EE alkalmazást úgy,
hogy figyelembe vegyük a modern architektúrális elveket,
de ezzel párhuzamosan egyre kevésbé függjünk a keretrendszeren, és hogyan vezessünk be unit és integrációs
teszteket.</p>
<!-- more -->
<p>De először nézzük, hogy milyen változások történtek a szabvány háza táján.
A Java EE egy szabvány, melynek több implementációja létezik, ezek az
alkalmazásszerverek, pl. JBoss/WildFly, Oracle WebLogic, IBM WebSphere Application Server, Glassfish,
Payara, stb. A Java EE ún. esernyő szabvány (umbrella), mely alatt több szabvány is van, pl. CDI, JPA, JMS, stb.
Ezek nagy része amúgy külön is elérhető Java SE környezetben.</p>
<p>A Java EE sokáig az Oracle fennhatósága alatt állt, azonban a 8-as verzió után megszabadult tőle,
és átkerült az Eclipse közösséghez. Mivel a Java név még mindig az Oracle-é, ezért át
kellett nevezni Jakarta EE-vé. A Jakarta EE 8-as verzió semmilyen újdonságot nem tartalmaz,
kizárólag licence téren lett rendbe téve, és kompatibilis a Java EE 8-cal. A Jakarta EE 9 azonban már nem,
ugyanis a csomagokat is átnevezték <code class="language-plaintext highlighter-rouge">javax.*</code> névről <code class="language-plaintext highlighter-rouge">jakarta.*</code> névre.</p>
<p>Hogy mi a hosszú távú cél? Természetesen továbbra is meg akar maradni szabványnak, mely
lehetővé teszi Cloud Native nagyvállalati alkalmazások fejlesztését. Nem akarnak szakítani
a hagyományokkal, ki akarják használni a Java EE történetéből adódó elterjedtségét.
Azonban szeretnék átláthatóbban, gyorsabban fejleszteni úgy, hogy a közösség elvárásainak is jobban megfeleljen.
Ráncfelvarrásokat is szeretnének végezni, pl. áttérni mindenhol Mavenre, alkalmazkodni a Java 9
modulrendszerhez, átgondolni a függőségeket, egységesíteni a dokumentációt. Sőt, célként megfogalmazták
a tesztelhetőséget is.</p>
<p>Nézzük meg kicsit visszafele, hogy milyen újdonságok jelentek meg a különböző verziókban, hogy
átlássuk, hogy merre tart a fejlődés. A Java EE 7-ben jelent meg a natív JSON támogatás (JSON-P), REST kliens
API, WebSocket támogatás. A Java EE 8-ban jelent meg a JSON binding (JSON-B), server sent event támogatás, reactive
REST kliens, HTTP/2 támogatás. Látható tehát, hogy a webes, REST webszolgáltatások terén bekövetkezett
változtatásokat próbálják követni.</p>
<p>Azonban érdemes egy kicsit figyelembe venni azt is, hogy a meglévő tecnológiákkal kapcsolatosan
milyen trendek vannak. Ebből a talán egyik legszembetűnőbb a Context and Dependency
Injection (CDI) térnyerése. A Java EE 6-ban jelent meg, és a Java EE 8
már a 2.0 verzióját tartalmazza. A probléma az volt, hogy minden technológia maga
definiálta, hogyan fér hozzá a különböző erőforrásokhoz, valamint hogyan definiálja
a komponenseket és ezek közötti kapcsolatokat.
Így volt pl. JPA esetén az <code class="language-plaintext highlighter-rouge">EntityManager</code> <code class="language-plaintext highlighter-rouge">@PersistenceContext</code> annotációval
volt injektálható. Az EJB-k esetén az EJB-ket a típusuk alapján kellett annotálni, pl. <code class="language-plaintext highlighter-rouge">@Stateless</code>.
Másik EJB-hez <code class="language-plaintext highlighter-rouge">@EJB</code> annotációval lehetett hozzáférni.
Ha egy <code class="language-plaintext highlighter-rouge">DataSource</code>-hoz akartunk hozzáférni, akkor a <code class="language-plaintext highlighter-rouge">@Resource</code> annotációt kellett használni.
A JSF-ben a controller jellegű komponenseket a <code class="language-plaintext highlighter-rouge">@ManagedBean</code> annotációval lehetett elérni.
Az egészet még bonyolította a JNDI rendkívüli bonyolultsága, és aluldefiniáltsága,
melyről <a href="https://www.jtechlog.hu/2011/02/27/konfiguracios-parameterek-WildFly.html">többször írtam</a>.
A szabványok közötti atjárás mindig problémás volt.</p>
<p>Ezen problémákra próbál megoldást találni a CDI. Akit bővebben érdekel a téma, a
<a href="https://www.apress.com/gp/book/9781484243626">Pro CDI 2 in Java EE 8</a> könyv
több, mint harmincöt oldalon keresztül tárgyalja a CDI történetét. Olyan nevek
jelennek itt meg, mint Gavin King (aki a Hibernate alkotója, és a CDI specifikálásának a vezetője is volt),
valamint Rod Johnson (igen, a Spring egyik atyja, aki szintén részt vett a specifikációban). De rajtuk kívül az összes nagy cég és
híres fejlesztő megjelenik a történetben, ami szinte a teljes Java világ története, érdemes elolvasni.</p>
<p>A CDI tehát a Java EE komponens modellje. A komponensek a következő plusz tulajdonságokkal
rendelkeznek egy egyszerű Java osztályhoz képest:</p>
<ul>
<li>Létrehozásukat és függőségeiket a konténer felügyeli (természetesen az utóbbit Dependency
Injection segítségével)</li>
<li>Scope-pal rendelkeznek, azaz definiált az élettartamuk (pl. az alkalmazás teljes élettartama,
csak addig éljen, mint a session vagy request, stb.)</li>
<li>Cserélhetőek (ez a Dependency Injectionből eléggé következik)</li>
<li>Életciklusuk van, melyet a konténer vezérel (példányosítja, különböző állapotokba teheti, megszüntetheti)</li>
<li>Különböző életciklushoz vagy egyébhez köthető eseményekre lehet reagálni (pl. ha létrejön vagy megszűnik a komponens, vagy
kérést kap) - itt vethető be a hagyományos Aspect Oriented Programming (AOP) is</li>
<li>Névvel rendelkeznek</li>
</ul>
<p>Hosszú távon egyértelmű, hogy a CDI a teljes Java EE specfikációt át fogja itatni, és
próbálja egységesíteni a komponensmodellt. Sőt, ami szintén különösen fontos, hogy a CDI
2.0 szabvány definiálja, hogy a CDI hogy használható Java SE-ben is.</p>
<p>Ahogy említettem, a CDI tehát maga is egy szabvány, aminek az egyik legelterjedtebb
implementációja a Weld.</p>
<p>A CDI igen komolyan összevethető a Spring Framework IoC (Inversion of Control)
konténerével (mely az <code class="language-plaintext highlighter-rouge">ApplicationContext</code> interfész mögött bújik meg).</p>
<p>Időközben a Java EE fejlődésének ütemével többen nem voltak megelégedve, így összeálltak, és
kidolgozták a <a href="https://microprofile.io/">MicroProfile</a> szabványt. Ez is egy szabványgyűjtemény, mely
a Java EE tapasztalataira épít, de különösképp a microservice-ekre koncentrál, és dinamikusabban
fejlődik. Ez tartalmazza a Java EE szabványok egy részét. És igen, a magja ugyanúgy a CDI,
és tartalmazza a JSON-P, JSON-B és JAX-RS szabványokat, látható hogy a cél a REST webszolgáltatások
implementálása. Valamint olyan kísérleti szabványok is bekerültek, mint a Config, Fault Tolerance,
Health, JWT, Metrics, OpenAPI, OpenTracing és Rest Client, sőt megjelent egy GraphQL API is.</p>
<p>Ennek olyan implementációi vannak, mint a helidon MP, Quarkus vagy WildFly (volt egy Thorntail
implementáció is, amit megszüntettek). A Quarkus arról híres, hogy képes GraalVM-en is futni,
így mind az indulási idő, mind a memóriahasználat, mind a válaszidő igen alacsony tud maradni.
És ezek cloud, microservices, elasztikus konténerizált környezetben igen fontosak.</p>
<p>A MicroProfile annyira elismert, hogy már a Java EE is ígéri, hogy figyeli a specifikációt, és amit érdemes,
átemel belőle.</p>
<p>Mit jelent mindez egy legacy Java EE alkalmazás esetén? Számomra annyit, hogy amerre indulni érdemes,
az mindenképp a CDI alapos megismerése és bevezetése, ahol csak lehet. Akár az EJB beanek
kiváltására is. Amennyiben komolyan akarjuk venni a keretrendszer függetlenséget,
érdemes először egyszerű POJO-kban gondolkozni, és mindent azokban megvalósítani. Ha ez kevés,
továbbléphetünk a CDI beanek irányában, ekkor már tudjuk használni a teljes CDI eszköztárat,
ebből természetesen a dependency injection a legfontosabb. És csak akkor használjunk EJB-ket,
ha feltétlen szükség van valami általuk nyújtott szolgáltatásra, amit a CDI nem tud. Ilyen
pl. passziválható session bean (bár jobb, ha ezt elkerüljük), távoli metódushívás, párhuzamos metódushívás,
ütemezés, stb. Régebben ide tartozott a tranzakciókezelés is (az EJB <code class="language-plaintext highlighter-rouge">@TransactionAttribute</code>)
annotációjával, azonban JTA API <code class="language-plaintext highlighter-rouge">javax.transaction.Transactional</code> annotációja a CDI beaneken is
értelmezett. Az implementáció CDI interceptorokkal valósítható meg. (A tranzakciókezelésről EJB környezetben
elérhető egy <a href="https://www.jtechlog.hu/2010/05/31/tranzakciokezeles.html">régebbi posztom</a>.)</p>
<p>Természetesen az egyszerűbb funkciókat ugyanúgy implementálhatjuk POJO-kban vagy CDI beanekben,
és az EJB-k csak a konténer szolgáltatásaihoz férjenek hozzá, és a hívást delegálják
POJO-khoz vagy CDI beanekhez. De mi van, ha már létező alkalmazásunk van? Akkor érdemes
úgy vágni az EJB beaneket, hogy szervezzük ki az üzleti logikát POJO-kba vagy CDI beanekbe,
és az EJB-k ezekbe hívjanak tovább.</p>
<h2 id="tesztelés">Tesztelés</h2>
<p>De mi köze mindennek a Java EE alkalmazások teszteléséhez?
Sajnos a Java EE-t kezdetben nem úgy alakították ki, hogy könnyen tesztelhető legyen.</p>
<h3 id="unit-tesztelés">Unit tesztelés</h3>
<p>Az EJB-k unit tesztelésére egy kitűnő ajánlás már régóta, hogy az üzleti logikát
szervezzük ki egyszerű POJO-kba, melyeket már lehet unit tesztelni. Csak a
konténer szolgáltatásaihoz való hozzáférés legyen az EJB beanekbe, és
csak szükség esetben adjuk a POJO-k felé. (Ebben az esetben azonban már
gyönyörűen lehet mockolni.)</p>
<p>Ez a CDI megjelenésével is egy tartható irány, ugyanis a CDI beanek alapjában véve maguk
is POJO-k. Bár számomra rendkívül furcsa, hogy az injektálás legtöbbször az attribútumon
elhelyezett <code class="language-plaintext highlighter-rouge">@Inject</code> annotációval történik. Pont a unit tesztelés miatt a
Spring a kötelező függőségeknél (és az encapsulation betartása miatt is)
a constructor injekctiont javasolja. Ez sokszor működik CDI beaneknél is, de futottam
bele olyan esetbe, ahol nem, és nem is elterjedt megoldás. Amúgy mockolásra
úgyis legtöbbször a Mockitot használjuk, ami szintén tud private attribútumba
injektálni, így a probléma inkább csak teoretikus.</p>
<h3 id="java-ee-komponens-integrációs-tesztelés">Java EE komponens-integrációs tesztelés</h3>
<p>Nem szeretem az integrációs tesztelés megnevezést, mert az önmagában nem írja le,
hogy miket integrálva tesztelünk. Így választottam inkább a Java EE komponens-integrációs
tesztelés nevet, mely leírja, hogy azt akarjuk kipróbálni, hogy a Java EE komponensei
megfelelően működnek-e együtt. Ide tartoznak a POJO-k, CDI beanek és az EJB-k is.</p>
<h3 id="arquillian">Arquillian</h3>
<p>Amennyiben az ember Java EE integrációs tesztelés témakörben keresgél, a legtöbbször
az Arquillian eszközbe botlik bele. Az Arquilliant legtöbbször ún. in-container
tesztelésre használják. Azaz vesznek egy futó alkalmazásszervert,
kiválogatatják a komponenseket, amiket tesztelni akarnak, becsomagolják
egy jar-ba vagy war-ba, mellécsomagolják a tesztesetet, és az egészet
deploy-olják az alkalmazásszerverre, és ott futtatják le a tesztesetet is.</p>
<p>Terveztem egy Arquillian posztsorozatot, azonban őszintén bevallom, hogy
olyan szinten kiábrándultam belőle már csak kis pilot projektek során is,
hogy a használatát semmiképp nem javaslom. Meg is indoklom, miért.</p>
<ul>
<li>A <code class="language-plaintext highlighter-rouge">ShrinkWrap</code> részével lehet összeállítani a pici telepítő csomagot,
un. micro-deploymentet. No ezzel összeválogatni, hogy milyen komponensek
kellenek, az egy kész kínszenvedés. Nyilván nem csak a saját class
fájlaink kellenek, hanem különböző erőforrás fájlok, mint pl.
<code class="language-plaintext highlighter-rouge">beans.xml</code>, <code class="language-plaintext highlighter-rouge">persistence.xml</code>, <code class="language-plaintext highlighter-rouge">web.xml</code>, stb. Ezek bevarázsolása szintén
kihívás. Sőt 3rd party library-k is kellenek, ezek a <code class="language-plaintext highlighter-rouge">shrinkwrap-resolver-impl-maven</code>
használatával hivatkozhatóak be, ami beolvassa a <code class="language-plaintext highlighter-rouge">pom.xml</code> fájlt, és
nekünk kell megadni a projekt koordinátákat. (A tranzitív függőségek hol mennek, hol nem.)</li>
<li>Ha már úgyis fut egy alkalmazásszerver, akkor nekem úgyis mindegy, akár a teljes alkalmazást
felpakolhatnám rá. Lassan indul, lassan áll le, foglalkozni kell vele. Persze az Arquillian is indíthatja,
de az még lassabb.</li>
<li>Ha már úgyis fut az alkalmazásszerver, akkor akár meghajthatom az API-t (pl. egy REST Assured)
használatával, vagy a felületet (pl. Selenium WebDriverrel). Ezek kevesebb szenvedéssel járnak,
és a tesztautomatizálásra fogékonyabb tesztelő kollégák is meg tudják írni.</li>
<li>Nem képesek több éve megoldani, hogy ugyanazt a deploymentet lehessen több teszt osztályból is
használni, és ne telepítse újra.</li>
<li>A Spring Boot finom integrációs teszteléshez használt mechanizmusaihoz képest olyan
bumfordi, esetlen és lassú, hogy nem említhető egy lapon.</li>
<li>Van egy olyan elvem, hogy amelyik eszközzel már egy demó projektet összerakni is
kihívás, azt nem választom éles projektben.</li>
</ul>
<h3 id="integrációs-tesztelés-welddel">Integrációs tesztelés Welddel</h3>
<p>Akkor mi lehet a megoldás? Hogy nem törekedünk a teljes lefedettségre, és
csak azokat a komponenseket integrációs teszteljük, melyek Java SE
környezetben is elérhetőek. És ahogy említettem a CDI 2.0 legnagyobb
újdonság a Java SE-ben való futtatási lehetőség. Sőt, a Weldnek
van egy <code class="language-plaintext highlighter-rouge">weld-junit5</code> modulja is, mely képes arra, hogy a teszt osztályt is
CDI tulajdonságokkal ruházza fel, pl. bármilyen CDI beant lehessen injektálni.
Ezen kívül a használatával a teljes CDI konténert is személyre tudjuk szabni.</p>
<p>Szóval indítsunk el egy CDI konténert, telepítsük bele a CDI beaneket, és ezen
futtassuk a teszteket. Itt a következő probléma, ami szembe fog jönni, hogy
nincs tranzakciónk. És ha tranzakciót akarunk, akkor szükségünk van egy
transaction managerre. Erre megfelelő lehet a <a href="https://narayana.io/">Narayana</a>.
Ennek integrációjáról továbbiak <a href="http://jbossts.blogspot.com/2019/04/jta-and-cdi-integration.html">itt olvashatóak</a>.</p>
<p>Amúgy gondolkodtam még az <a href="https://deltaspike.apache.org/documentation/jpa.html">Apache DeltaSpike JPA modulon</a>
is, azonban az saját csomagban lévő <code class="language-plaintext highlighter-rouge">@Transactional</code> annotációt használt.</p>
<p>Valamint sok példában láttam, hogy az <code class="language-plaintext highlighter-rouge">EntityManager</code>-t <code class="language-plaintext highlighter-rouge">@Inject</code>
annotációval injektálja. Ezt nem szerettem volna, mindenütt hagyományosan a <code class="language-plaintext highlighter-rouge">@PersistenceContext</code>
annotációval injektáljuk, szóval én is egy olyan példát szerettem volna, ami ezzel
működik. Szerencsére a <a href="https://github.com/weld/weld-junit/tree/master/junit5">Weld JUnit 5 Extensions</a>
ezt is támogatja a <a href="https://github.com/weld/weld-junit/tree/master/junit5#mock-injection-services">Mock injection services</a>
használatával.</p>
<p>A <a href="https://github.com/vicziani/javaee-testing">https://github.com/vicziani/javaee-testing</a> címen tehát egy olyan példa projekt található,
ami tartalmaz egy DAO-t (, ami egy CDI bean), mely JPA-val van implementálva, az <code class="language-plaintext highlighter-rouge">EntityManager</code> a <code class="language-plaintext highlighter-rouge">@PersistenceContext</code>
annotációval van injektálva. Természetesen egy entitást kezel. Valamint van egy service, mely szintén
egy CDI bean, és a DAO-t <code class="language-plaintext highlighter-rouge">@Inject</code>annotációval injektálja, és ő indítja a tranzakciót
a <code class="language-plaintext highlighter-rouge">@javax.transaction.Transactional</code> annotáció használatával.</p>
<p>Nézzük is a tesztesetet. Egy in-memory H2 adatbázist használ.</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@EnableAutoWeld</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">EmployeeServiceIT</span> <span class="o">{</span>
<span class="nd">@Inject</span>
<span class="nc">EmployeeService</span> <span class="n">employeeService</span><span class="o">;</span>
<span class="nd">@WeldSetup</span>
<span class="kd">public</span> <span class="nc">WeldInitiator</span> <span class="n">weldInitializator</span> <span class="o">=</span> <span class="nc">WeldInitiator</span><span class="o">.</span><span class="na">from</span><span class="o">(</span><span class="nc">WeldInitiator</span><span class="o">.</span><span class="na">createWeld</span><span class="o">()</span>
<span class="o">.</span><span class="na">addBeanClasses</span><span class="o">(</span><span class="nc">EmployeeDao</span><span class="o">.</span><span class="na">class</span><span class="o">,</span> <span class="nc">EmployeeService</span><span class="o">.</span><span class="na">class</span><span class="o">)</span>
<span class="o">.</span><span class="na">addExtension</span><span class="o">(</span><span class="k">new</span> <span class="nc">TransactionExtension</span><span class="o">())</span>
<span class="o">)</span>
<span class="o">.</span><span class="na">setPersistenceContextFactory</span><span class="o">(</span><span class="nl">EmployeeServiceIT:</span><span class="o">:</span><span class="n">createEntityManager</span><span class="o">)</span>
<span class="o">.</span><span class="na">build</span><span class="o">();</span>
<span class="kd">private</span> <span class="kd">static</span> <span class="nc">EntityManager</span> <span class="nf">createEntityManager</span><span class="o">(</span><span class="nc">InjectionPoint</span> <span class="n">ip</span><span class="o">)</span> <span class="o">{</span>
<span class="kt">var</span> <span class="n">factory</span> <span class="o">=</span> <span class="nc">Persistence</span><span class="o">.</span><span class="na">createEntityManagerFactory</span><span class="o">(</span><span class="s">"pu"</span><span class="o">);</span>
<span class="k">return</span> <span class="n">factory</span><span class="o">.</span><span class="na">createEntityManager</span><span class="o">();</span>
<span class="o">}</span>
<span class="nd">@Test</span>
<span class="kt">void</span> <span class="nf">testCreate</span><span class="o">()</span> <span class="o">{</span>
<span class="n">employeeService</span><span class="o">.</span><span class="na">create</span><span class="o">(</span><span class="s">"John Doe"</span><span class="o">);</span>
<span class="n">employeeService</span><span class="o">.</span><span class="na">create</span><span class="o">(</span><span class="s">"Jack Doe"</span><span class="o">);</span>
<span class="kt">var</span> <span class="n">employees</span> <span class="o">=</span> <span class="n">employeeService</span><span class="o">.</span><span class="na">findAll</span><span class="o">();</span>
<span class="n">assertThat</span><span class="o">(</span><span class="n">employees</span><span class="o">).</span><span class="na">extracting</span><span class="o">(</span><span class="nl">Employee:</span><span class="o">:</span><span class="n">getName</span><span class="o">)</span>
<span class="o">.</span><span class="na">containsExactly</span><span class="o">(</span><span class="s">"Jack Doe"</span><span class="o">,</span> <span class="s">"John Doe"</span><span class="o">);</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>Az <code class="language-plaintext highlighter-rouge">@EnableAutoWeld</code> egy JUnit 5 extension, mely elindít egy
Weldet. Ennek hatására lehet az <code class="language-plaintext highlighter-rouge">EmployeeService</code>-t is injektálni.</p>
<p>A <code class="language-plaintext highlighter-rouge">@WeldSetup</code> annotációval ellátott rész konfigurálja a Weldet.
Látható, hogy a <code class="language-plaintext highlighter-rouge">EmployeeDao</code> és <code class="language-plaintext highlighter-rouge">EmployeeService</code> osztályok vannak csak
hozzáadva. Valamint itt van hozzáadva a Narayana <code class="language-plaintext highlighter-rouge">TransactionExtension</code> CDI
extension, mely beregisztrálja az CDI interceptorokat, melyek ráugornak
a <code class="language-plaintext highlighter-rouge">@Transactional</code> annotációra (nyilván proxizás van a háttérben).</p>
<p>A <code class="language-plaintext highlighter-rouge">setPersistenceContextFactory()</code> metódushívás állítja be, hogy
mikor egy CDI beannek <code class="language-plaintext highlighter-rouge">EntityManager</code>-re van szüksége, hogy
kerüljön az létrehozásra. Itt egy method reference van átadva,
mely lejjebb van definiálva.</p>
<p>Majd jön a teszteset. Ez elment két entitást, majd lekérdezi ezeket.</p>
<p>A projektben található egy <code class="language-plaintext highlighter-rouge">jbossts-properties</code> állomány is, mely azt
mondja meg, hogy hova tegye az ideiglenes állományait a
Narayana.</p>
<p>A <a href="https://github.com/vicziani/javaee-testing-transactionservices">https://github.com/vicziani/javaee-testing-transactionservices</a> címen egy bonyolultabb példa projekt
található, melyet <a href="https://in.relation.to/2019/01/23/testing-cdi-beans-and-persistence-layer-under-java-se/">ez a poszt</a>
és ez a <a href="https://github.com/hibernate/hibernate-demos/tree/master/other/cdi-jpa-testing">példa projekt</a>
ihletett.</p>
<p>Ebben már több minden működni fog. Egyrészt a Hibernate-nek be van
állítva a <code class="language-plaintext highlighter-rouge">javax.persistence.bean.manager</code> property-ben a <code class="language-plaintext highlighter-rouge">BeanManager</code>, hogy
működjön az injection a JPA Entity Listenerekben.</p>
<p>Másrészt be van indítva egy JNDI szerver (<code class="language-plaintext highlighter-rouge">jboss:jnpserver</code> függőség), és a <code class="language-plaintext highlighter-rouge">DataSource</code> és a <code class="language-plaintext highlighter-rouge">TransactionManager</code> is
ide van bindolva. (Ehhez kell a <code class="language-plaintext highlighter-rouge">jndi.properties</code> is.) Valamint van egy <code class="language-plaintext highlighter-rouge">TransactionalConnectionProvider</code>
mely a Hibernate-nek van beregisztrálva, hogy ezen keresztül kérje le az adatbázis kapcsolatot. Az ezen
keresztül lekért adatbázis kapcsolat tranzakcióját innentől kezdve a JTA kezeli. Ezen kívül egy
Weld SPI <code class="language-plaintext highlighter-rouge">TransactionServices</code> implementációra is szükség van, melyet szintén be kell regisztrálni
az <code class="language-plaintext highlighter-rouge">addServices()</code> metódussal.</p>
Docker Layers Spring Boot alkalmazásnál2020-10-18T20:00:00+00:00http://www.jtechlog.hu/2020/10/18/docker-layers<p>Amennyiben dockerizáljuk a Spring Bootos alkalmazásunkat, oda kell
figyelni néhány dologra. Nem elegendő ugyanis, hogy a jar
fájlunkat bemásoljuk (über jar, szép német kifejezéssel, vagy
fat jar - tehát a jar mely a függőségeket is tartalmazza).</p>
<p>A Docker ugyanis a hatékony adattárolás miatt egy image-et
nem egy megbonthatatlan fájlként tárol, hanem ún. layerekben.
Ugyanis ha kiindulunk egy image-ből, és pl. új fájlokat másolunk,
ezzel létrehozva egy új image-et, nem tárolja el az új image-et
teljes egészében, csak a különbséget, azaz a felmásolt fájlokat.
Ez a különbség a layer. Ezzel jelentős helyet takarít meg, hiszen
a két image-ben közös fájlokat csak egyszer tárolja el. Így
persze egy image több layer fájlból épül fel.</p>
<p>Vegyünk egy egyszerű Spring Boot alkalmazást, és egy hozzá
tartozó <code class="language-plaintext highlighter-rouge">Dockerfile</code> fájlt.</p>
<div class="language-docker highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">FROM</span><span class="s"> adoptopenjdk:14-jre-hotspot</span>
<span class="k">WORKDIR</span><span class="s"> /opt/app</span>
<span class="k">COPY</span><span class="s"> target/*.jar demo.jar</span>
<span class="k">CMD</span><span class="s"> ["java", "-jar", "demo.jar"]</span>
</code></pre></div></div>
<p>Ebben a posztban azt fogom leírni, hogy ez miért nem jó!</p>
<p>Az egyszerűség kedvéért képzeljük el, hogy
ez egy olyan image, mely egy <code class="language-plaintext highlighter-rouge">ubuntu</code> image-ből
épül fel, arra épül rá az AdoptOpenJDK,
kialakítva az <code class="language-plaintext highlighter-rouge">adoptopenjdk</code> image-et, majd
arra a saját alkalmazásunk, egy JAR állománnyal, melynek
eredménye a <code class="language-plaintext highlighter-rouge">demo</code> image. Ez layer szinten elnagyoltan így néz ki.</p>
<p><img src="/artifacts/posts/2020-10-18-docker-layers/docker-layers.png" alt="Docker layers" /></p>
<p>Ebből az ábrából már elég szépen látható, hogy mennyi tárhelyet nyerünk,
ha az <code class="language-plaintext highlighter-rouge">ubuntu</code> és <code class="language-plaintext highlighter-rouge">adoptopenjdk</code> layer csak egyszer kerül letárolásra.
(Gondoljunk bele, ha napi több commit esetén napi több image-et hoz
létre a CI szerver.)</p>
<p>A probléma ott kezdődik, hogy viszont az alkalmazáshoz tartozó réteg, amiben esetleg
egy-két fájl változik, újra és újra letárolásra kerül, hiszen az mindig egy új külön réteget
hoz létre.</p>
<p>A Spring Boot azonban a
<a href="https://spring.io/blog/2020/01/27/creating-docker-images-with-spring-boot-2-3-0-m1">2.3.0.M2 verziótól kezdve beépített támogatást tartalmaz</a>, hogy
magát az alkalmazást is több rétegre bontsuk fel.</p>
<!-- more -->
<p>Az alapötlet egyszerű. A 3rd party library-khoz tartozó JAR-ok, akár a Tomcat,
akár a Spring Boot JAR-jai is sokkal ritkábban változnak, mint a class
fájljaink, ezért pakoljuk át ezeket egy külön rétegbe.</p>
<p>Ehhez egyrészt elő kell készíteni, hogy a JAR állományunk is rétegelt legyen.
Ehhez a <code class="language-plaintext highlighter-rouge">spring-boot-maven-plugin</code> plugint kell konfigurálnunk a <code class="language-plaintext highlighter-rouge">pom.xml</code>.
fájlban.</p>
<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt"><build></span>
<span class="nt"><plugins></span>
<span class="nt"><plugin></span>
<span class="nt"><groupId></span>org.springframework.boot<span class="nt"></groupId></span>
<span class="nt"><artifactId></span>spring-boot-maven-plugin<span class="nt"></artifactId></span>
<span class="nt"><configuration></span>
<span class="nt"><layers></span>
<span class="nt"><enabled></span>true<span class="nt"></enabled></span>
<span class="nt"></layers></span>
<span class="nt"></configuration></span>
<span class="nt"></plugin></span>
<span class="nt"></plugins></span>
<span class="nt"></build></span>
</code></pre></div></div>
<p>Ekkor egy olyan jar jön létre, mely külön könyvtárakba képes kicsomagolni
az alkalmazás különböző részeit. Ezt a következő paranccsal érhetjük el:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>java <span class="nt">-Djarmode</span><span class="o">=</span>layertools <span class="nt">-jar</span> demo.jar extract
</code></pre></div></div>
<p>Még egy trükköt fogunk alkalmazni, az ún. <a href="https://docs.docker.com/develop/develop-images/multistage-build/">multi-stage buildet</a>.
Ezzel azt lehet elérni, hogy létrehozunk egy Docker konténert, melyben előkészítjük az alkalmazásunkat (kitömörítjük az előző paranccsal),
és ennek eredményét, a könyvtárakat külön másoljuk át a végleges konténerbe. Ez azért fog működni, mert minden egyes külön
kiadott <code class="language-plaintext highlighter-rouge">COPY</code> parancs egy külön layert fog létrehozni.</p>
<p>Így a <code class="language-plaintext highlighter-rouge">Dockerfile</code> a következő:</p>
<div class="language-docker highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">FROM</span><span class="w"> </span><span class="s">adoptopenjdk:14-jre-hotspot</span><span class="w"> </span><span class="k">as</span><span class="w"> </span><span class="s">builder</span>
<span class="k">WORKDIR</span><span class="s"> app</span>
<span class="k">COPY</span><span class="s"> target/*.jar demo.jar</span>
<span class="k">RUN </span>java <span class="nt">-Djarmode</span><span class="o">=</span>layertools <span class="nt">-jar</span> demo.jar extract
<span class="k">FROM</span><span class="s"> adoptopenjdk:14-jre-hotspot</span>
<span class="k">WORKDIR</span><span class="s"> app</span>
<span class="k">COPY</span><span class="s"> --from=builder app/dependencies/ ./</span>
<span class="k">COPY</span><span class="s"> --from=builder app/spring-boot-loader/ ./</span>
<span class="c"># COPY --from=builder app/snapshot-dependencies/ ./</span>
<span class="k">COPY</span><span class="s"> --from=builder app/application/ ./</span>
<span class="k">ENTRYPOINT</span><span class="s"> ["java", \</span>
"org.springframework.boot.loader.JarLauncher"]
</code></pre></div></div>
<p>Az első blokk tehát létrehoz egy konténert, melybe kicsomagolja
külön könyvtárakba a JAR állományt (<code class="language-plaintext highlighter-rouge">dependencies</code>,
<code class="language-plaintext highlighter-rouge">spring-boot-loader</code>, <code class="language-plaintext highlighter-rouge">snapshot-dependencies</code> és
<code class="language-plaintext highlighter-rouge">application</code>). Látható, hogy még a snapshot
függőségeknek is egy külön könyvtárt hoz létre. (Hiszen
gyakrabban változhatnak, mint a végleges verziójú függőségek.)
A második blokk külön <code class="language-plaintext highlighter-rouge">COPY</code> parancsban átmásolja a
könyvtárakat, mindegyik eredményeként egy új layert hozva
létre. Látható, hogy a <code class="language-plaintext highlighter-rouge">snapshot-dependencies</code>
könyvtárhoz tartozó <code class="language-plaintext highlighter-rouge">COPY</code> megjegyzésbe van téve. Ez azért van,
mert a Docker Linuxon képes <code class="language-plaintext highlighter-rouge">layer does not exist</code> hibaüzenettel
elszállni, ha üres könyvtárat másolunk. (Az én alkalmazásomban
nem volt SNAPSHOT dependency, így ezt nem kellett másolni.)
Az alkalmazásunk így (nagyjából) a következő layerekből épül fel.</p>
<p><img src="/artifacts/posts/2020-10-18-docker-layers/spring-boot-layers.png" alt="Spring Boot layers" /></p>
<p>Már direkt nem egymásba ágyazva rajzoltam a rétegeket, jelezve, hogy
ezek függetlenek egymástól. Az alkalmazásom egy darab statikus html
állományt tartalmazott. Látható, hogy ebben az esetben mennyi helyet
takarítunk meg, ha csak a html állományt szerkesztjük. Buildenként
legalább 16 MB-ot. És képzeljünk el egy olyan alkalmazást, ahol
sokkal több függőség, van, akár 100 MB környékén. Ehhez képest a saját
class állományaink mérete tényleg elhanyagolható lehet.</p>
<p>És most nézzük végig, hogy pontosan mi is történik a háttérben.
Először induljunk ki egy Spring Bootos alkalmazásból
a http://start.spring.io oldalon, Web függőséggel.
Az <code class="language-plaintext highlighter-rouge">src/main/resources/static</code> könyvtárban helyezzünk el egy
<code class="language-plaintext highlighter-rouge">index.html</code> állományt. A buildeléshez adjuk ki a
<code class="language-plaintext highlighter-rouge">./mvnw package -DskipTests</code> parancsot. A <code class="language-plaintext highlighter-rouge">Dockerfile</code> legyen
az ebben a posztban említett első, <strong>rossz</strong> <code class="language-plaintext highlighter-rouge">Dockerfile</code>.
A Docker image előállításához adjuk ki a <code class="language-plaintext highlighter-rouge">docker build -t demo .</code>
parancsot.</p>
<p>Ekkor valami ilyesmit fogunk látni:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Sending build context to Docker daemon 16.68MB
Step 1/4 : FROM adoptopenjdk:14-jre-hotspot
<span class="nt">---</span><span class="o">></span> 14a0e3b4f7f3
Step 2/4 : WORKDIR /opt/app
<span class="nt">---</span><span class="o">></span> 41ba9592b425
Step 3/4 : COPY target/<span class="k">*</span>.jar demo.jar
<span class="nt">---</span><span class="o">></span> 30d8ff34aa90
Step 4/4 : CMD <span class="o">[</span><span class="s2">"java"</span>, <span class="s2">"-jar"</span>, <span class="s2">"demo.jar"</span><span class="o">]</span>
<span class="nt">---</span><span class="o">></span> Running <span class="k">in </span>9cc7f691622f
Removing intermediate container 9cc7f691622f
<span class="nt">---</span><span class="o">></span> 7698b9790f4f
Successfully built 7698b9790f4f
Successfully tagged demo:latest
</code></pre></div></div>
<p>Az image-hez tartozó layereket a <code class="language-plaintext highlighter-rouge">docker history demo</code>
paranccsal tudjuk lekérdezni.</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>e8cbe617fe62 4 minutes ago /bin/sh <span class="nt">-c</span> <span class="c">#(nop) CMD ["java" "-jar" "demo.… 0B </span>
56864ac365e8 4 minutes ago /bin/sh <span class="nt">-c</span> <span class="c">#(nop) COPY file:6670678da1e96212… 16.5MB </span>
41ba9592b425 2 weeks ago /bin/sh <span class="nt">-c</span> <span class="c">#(nop) WORKDIR /opt/app 0B </span>
14a0e3b4f7f3 3 weeks ago /bin/sh <span class="nt">-c</span> <span class="c">#(nop) ENV JAVA_HOME=/opt/java/o… 0B </span>
<missing> 3 weeks ago /bin/sh <span class="nt">-c</span> <span class="nb">set</span> <span class="nt">-eux</span><span class="p">;</span> <span class="nv">ARCH</span><span class="o">=</span><span class="s2">"</span><span class="si">$(</span>dpkg <span class="nt">--prin</span>… 167MB
<missing> 3 weeks ago /bin/sh <span class="nt">-c</span> <span class="c">#(nop) ENV JAVA_VERSION=jdk-14.0… 0B </span>
<missing> 3 weeks ago /bin/sh <span class="nt">-c</span> apt-get update <span class="o">&&</span> apt-get ins… 35.7MB
<missing> 3 weeks ago /bin/sh <span class="nt">-c</span> <span class="c">#(nop) ENV LANG=en_US.UTF-8 LANG… 0B </span>
<missing> 3 weeks ago /bin/sh <span class="nt">-c</span> <span class="c">#(nop) CMD ["/bin/bash"] 0B </span>
<missing> 3 weeks ago /bin/sh <span class="nt">-c</span> <span class="nb">mkdir</span> <span class="nt">-p</span> /run/systemd <span class="o">&&</span> <span class="nb">echo</span> <span class="s1">'do… 7B
<missing> 3 weeks ago /bin/sh -c [ -z "$(apt-get indextargets)" ] 0B
<missing> 3 weeks ago /bin/sh -c set -xe && echo '</span><span class="c">#!/bin/sh' > /… 745B </span>
<missing> 3 weeks ago /bin/sh <span class="nt">-c</span> <span class="c">#(nop) ADD file:4974bb5483c392fb5… 63.2MB </span>
</code></pre></div></div>
<p>Azért látható, hogy jóval több, mint három réteg van a háttérben.
Viszont látható, hogy a JAR-t hordozó <code class="language-plaintext highlighter-rouge">568</code> kezdetű réteg kb. 17 MB.</p>
<p>Ha most változtatjuk az <code class="language-plaintext highlighter-rouge">index.html</code> állományt, és újra eljárjuk az esőtáncot
(Maven és Docker build), akkor a következő lesz az eredménye:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>7698b9790f4f 46 seconds ago /bin/sh <span class="nt">-c</span> <span class="c">#(nop) CMD ["java" "-jar" "demo.… 0B </span>
30d8ff34aa90 47 seconds ago /bin/sh <span class="nt">-c</span> <span class="c">#(nop) COPY file:ff842752b1d34e46… 16.5MB </span>
41ba9592b425 2 weeks ago /bin/sh <span class="nt">-c</span> <span class="c">#(nop) WORKDIR /opt/app 0B </span>
14a0e3b4f7f3 3 weeks ago /bin/sh <span class="nt">-c</span> <span class="c">#(nop) ENV JAVA_HOME=/opt/java/o… 0B </span>
<missing> 3 weeks ago /bin/sh <span class="nt">-c</span> <span class="nb">set</span> <span class="nt">-eux</span><span class="p">;</span> <span class="nv">ARCH</span><span class="o">=</span><span class="s2">"</span><span class="si">$(</span>dpkg <span class="nt">--prin</span>… 167MB
<missing> 3 weeks ago /bin/sh <span class="nt">-c</span> <span class="c">#(nop) ENV JAVA_VERSION=jdk-14.0… 0B </span>
<missing> 3 weeks ago /bin/sh <span class="nt">-c</span> apt-get update <span class="o">&&</span> apt-get ins… 35.7MB
<missing> 3 weeks ago /bin/sh <span class="nt">-c</span> <span class="c">#(nop) ENV LANG=en_US.UTF-8 LANG… 0B </span>
<missing> 3 weeks ago /bin/sh <span class="nt">-c</span> <span class="c">#(nop) CMD ["/bin/bash"] 0B </span>
<missing> 3 weeks ago /bin/sh <span class="nt">-c</span> <span class="nb">mkdir</span> <span class="nt">-p</span> /run/systemd <span class="o">&&</span> <span class="nb">echo</span> <span class="s1">'do… 7B
<missing> 3 weeks ago /bin/sh -c [ -z "$(apt-get indextargets)" ] 0B
<missing> 3 weeks ago /bin/sh -c set -xe && echo '</span><span class="c">#!/bin/sh' > /… 745B </span>
<missing> 3 weeks ago /bin/sh <span class="nt">-c</span> <span class="c">#(nop) ADD file:4974bb5483c392fb5… 63.2MB </span>
</code></pre></div></div>
<p>A <code class="language-plaintext highlighter-rouge">41b</code> kezdetű layerig nem hozott létre a Docker új layert, hanem a már meglévőket hasznáta fel.
Azonban a <code class="language-plaintext highlighter-rouge">30d</code> az új JAR-t tartalmazó 17 MB-os új layer.</p>
<p>Nos nézzük meg ugyanezt az <strong>helyes</strong> <code class="language-plaintext highlighter-rouge">Dockerfile</code> használatával.</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Sending build context to Docker daemon 50.08MB
Step 1/10 : FROM adoptopenjdk:14-jre-hotspot as builder
<span class="nt">---</span><span class="o">></span> 14a0e3b4f7f3
Step 2/10 : WORKDIR app
<span class="nt">---</span><span class="o">></span> Using cache
<span class="nt">---</span><span class="o">></span> 5764828bedbc
Step 3/10 : COPY target/<span class="k">*</span>.jar demo.jar
<span class="nt">---</span><span class="o">></span> db86930a59d5
Step 4/10 : RUN java <span class="nt">-Djarmode</span><span class="o">=</span>layertools <span class="nt">-jar</span> demo.jar extract
<span class="nt">---</span><span class="o">></span> Running <span class="k">in </span>992df808b7fa
Removing intermediate container 992df808b7fa
<span class="nt">---</span><span class="o">></span> 0adad9dcf537
Step 5/10 : FROM adoptopenjdk:14-jre-hotspot
<span class="nt">---</span><span class="o">></span> 14a0e3b4f7f3
Step 6/10 : WORKDIR app
<span class="nt">---</span><span class="o">></span> Using cache
<span class="nt">---</span><span class="o">></span> 5764828bedbc
Step 7/10 : COPY <span class="nt">--from</span><span class="o">=</span>builder app/dependencies/ ./
<span class="nt">---</span><span class="o">></span> d99f206d05f4
Step 8/10 : COPY <span class="nt">--from</span><span class="o">=</span>builder app/spring-boot-loader/ ./
<span class="nt">---</span><span class="o">></span> 7d91d24a8157
Step 9/10 : COPY <span class="nt">--from</span><span class="o">=</span>builder app/application/ ./
<span class="nt">---</span><span class="o">></span> f462da7b9616
Step 10/10 : ENTRYPOINT <span class="o">[</span><span class="s2">"java"</span>, <span class="s2">"org.springframework.boot.loader.JarLauncher"</span><span class="o">]</span>
<span class="nt">---</span><span class="o">></span> Running <span class="k">in </span>bdbebbee0abe
Removing intermediate container bdbebbee0abe
<span class="nt">---</span><span class="o">></span> ab4fe2572b78
Successfully built ab4fe2572b78
Successfully tagged demo:latest
</code></pre></div></div>
<p>Sokkal hosszabb, hiszen itt már multi-stage build van. Látszik, hogy a 6-ik
lépésig még <code class="language-plaintext highlighter-rouge">Using cache</code>, azaz a már meglévő layereket használja,
onnan a különböző könyvtáraknak külön layert hoz létre.
Mit mond a <code class="language-plaintext highlighter-rouge">docker history demo</code> parancs?</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>IMAGE CREATED CREATED BY SIZE COMMENT
ab4fe2572b78 53 seconds ago /bin/sh <span class="nt">-c</span> <span class="c">#(nop) ENTRYPOINT ["java" "org.s… 0B </span>
f462da7b9616 53 seconds ago /bin/sh <span class="nt">-c</span> <span class="c">#(nop) COPY dir:f2f5110fcfc7bf2e7… 4.17kB </span>
7d91d24a8157 12 minutes ago /bin/sh <span class="nt">-c</span> <span class="c">#(nop) COPY dir:34fffe734ed638d06… 241kB </span>
d99f206d05f4 12 minutes ago /bin/sh <span class="nt">-c</span> <span class="c">#(nop) COPY dir:a06c3500a0e17c527… 16.4MB </span>
5764828bedbc 12 minutes ago /bin/sh <span class="nt">-c</span> <span class="c">#(nop) WORKDIR /app 0B </span>
14a0e3b4f7f3 3 weeks ago /bin/sh <span class="nt">-c</span> <span class="c">#(nop) ENV JAVA_HOME=/opt/java/o… 0B </span>
<missing> 3 weeks ago /bin/sh <span class="nt">-c</span> <span class="nb">set</span> <span class="nt">-eux</span><span class="p">;</span> <span class="nv">ARCH</span><span class="o">=</span><span class="s2">"</span><span class="si">$(</span>dpkg <span class="nt">--prin</span>… 167MB
<missing> 3 weeks ago /bin/sh <span class="nt">-c</span> <span class="c">#(nop) ENV JAVA_VERSION=jdk-14.0… 0B </span>
<missing> 3 weeks ago /bin/sh <span class="nt">-c</span> apt-get update <span class="o">&&</span> apt-get ins… 35.7MB
<missing> 3 weeks ago /bin/sh <span class="nt">-c</span> <span class="c">#(nop) ENV LANG=en_US.UTF-8 LANG… 0B </span>
<missing> 3 weeks ago /bin/sh <span class="nt">-c</span> <span class="c">#(nop) CMD ["/bin/bash"] 0B </span>
<missing> 3 weeks ago /bin/sh <span class="nt">-c</span> <span class="nb">mkdir</span> <span class="nt">-p</span> /run/systemd <span class="o">&&</span> <span class="nb">echo</span> <span class="s1">'do… 7B
<missing> 3 weeks ago /bin/sh -c [ -z "$(apt-get indextargets)" ] 0B
<missing> 3 weeks ago /bin/sh -c set -xe && echo '</span><span class="c">#!/bin/sh' > /… 745B </span>
<missing> 3 weeks ago /bin/sh <span class="nt">-c</span> <span class="c">#(nop) ADD file:4974bb5483c392fb5… 63.2MB </span>
</code></pre></div></div>
<p>Létrejött a <code class="language-plaintext highlighter-rouge">d99</code>, <code class="language-plaintext highlighter-rouge">7d9</code>, <code class="language-plaintext highlighter-rouge">f46</code> layer, összesen ezek is 17 MB-ot tesznek ki.</p>
<p>Azonban miután belenyúltam az <code class="language-plaintext highlighter-rouge">index.html</code> állományba, a következő került kiírásra:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Sending build context to Docker daemon 50.08MB
Step 1/10 : FROM adoptopenjdk:14-jre-hotspot as builder
<span class="nt">---</span><span class="o">></span> 14a0e3b4f7f3
Step 2/10 : WORKDIR app
<span class="nt">---</span><span class="o">></span> Using cache
<span class="nt">---</span><span class="o">></span> 5764828bedbc
Step 3/10 : COPY target/<span class="k">*</span>.jar demo.jar
<span class="nt">---</span><span class="o">></span> b8ab4c3ea04f
Step 4/10 : RUN java <span class="nt">-Djarmode</span><span class="o">=</span>layertools <span class="nt">-jar</span> demo.jar extract
<span class="nt">---</span><span class="o">></span> Running <span class="k">in </span>20a96aa8e12d
Removing intermediate container 20a96aa8e12d
<span class="nt">---</span><span class="o">></span> 7a1f919cadf2
Step 5/10 : FROM adoptopenjdk:14-jre-hotspot
<span class="nt">---</span><span class="o">></span> 14a0e3b4f7f3
Step 6/10 : WORKDIR app
<span class="nt">---</span><span class="o">></span> Using cache
<span class="nt">---</span><span class="o">></span> 5764828bedbc
Step 7/10 : COPY <span class="nt">--from</span><span class="o">=</span>builder app/dependencies/ ./
<span class="nt">---</span><span class="o">></span> Using cache
<span class="nt">---</span><span class="o">></span> d99f206d05f4
Step 8/10 : COPY <span class="nt">--from</span><span class="o">=</span>builder app/spring-boot-loader/ ./
<span class="nt">---</span><span class="o">></span> Using cache
<span class="nt">---</span><span class="o">></span> 7d91d24a8157
Step 9/10 : COPY <span class="nt">--from</span><span class="o">=</span>builder app/application/ ./
<span class="nt">---</span><span class="o">></span> e1ad63d4bbbd
Step 10/10 : ENTRYPOINT <span class="o">[</span><span class="s2">"java"</span>, <span class="s2">"org.springframework.boot.loader.JarLauncher"</span><span class="o">]</span>
<span class="nt">---</span><span class="o">></span> Running <span class="k">in </span>61a30ff4519c
Removing intermediate container 61a30ff4519c
<span class="nt">---</span><span class="o">></span> eb4519e26e13
Successfully built eb4519e26e13
Successfully tagged demo:latest
</code></pre></div></div>
<p>Látható, hogy itt már szinte az összes layert cache-eli, kivéve az utolsó layert, mely
a html fájlt tartalmazza. A history:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>IMAGE CREATED CREATED BY SIZE COMMENT
eb4519e26e13 20 seconds ago /bin/sh <span class="nt">-c</span> <span class="c">#(nop) ENTRYPOINT ["java" "org.s… 0B </span>
e1ad63d4bbbd 20 seconds ago /bin/sh <span class="nt">-c</span> <span class="c">#(nop) COPY dir:41e31d1d5706c11a9… 4.18kB </span>
7d91d24a8157 16 minutes ago /bin/sh <span class="nt">-c</span> <span class="c">#(nop) COPY dir:34fffe734ed638d06… 241kB </span>
d99f206d05f4 16 minutes ago /bin/sh <span class="nt">-c</span> <span class="c">#(nop) COPY dir:a06c3500a0e17c527… 16.4MB </span>
5764828bedbc 16 minutes ago /bin/sh <span class="nt">-c</span> <span class="c">#(nop) WORKDIR /app 0B </span>
14a0e3b4f7f3 3 weeks ago /bin/sh <span class="nt">-c</span> <span class="c">#(nop) ENV JAVA_HOME=/opt/java/o… 0B </span>
<missing> 3 weeks ago /bin/sh <span class="nt">-c</span> <span class="nb">set</span> <span class="nt">-eux</span><span class="p">;</span> <span class="nv">ARCH</span><span class="o">=</span><span class="s2">"</span><span class="si">$(</span>dpkg <span class="nt">--prin</span>… 167MB
<missing> 3 weeks ago /bin/sh <span class="nt">-c</span> <span class="c">#(nop) ENV JAVA_VERSION=jdk-14.0… 0B </span>
<missing> 3 weeks ago /bin/sh <span class="nt">-c</span> apt-get update <span class="o">&&</span> apt-get ins… 35.7MB
<missing> 3 weeks ago /bin/sh <span class="nt">-c</span> <span class="c">#(nop) ENV LANG=en_US.UTF-8 LANG… 0B </span>
<missing> 3 weeks ago /bin/sh <span class="nt">-c</span> <span class="c">#(nop) CMD ["/bin/bash"] 0B </span>
<missing> 3 weeks ago /bin/sh <span class="nt">-c</span> <span class="nb">mkdir</span> <span class="nt">-p</span> /run/systemd <span class="o">&&</span> <span class="nb">echo</span> <span class="s1">'do… 7B
<missing> 3 weeks ago /bin/sh -c [ -z "$(apt-get indextargets)" ] 0B
<missing> 3 weeks ago /bin/sh -c set -xe && echo '</span><span class="c">#!/bin/sh' > /… 745B </span>
<missing> 3 weeks ago /bin/sh <span class="nt">-c</span> <span class="c">#(nop) ADD file:4974bb5483c392fb5… 63.2MB </span>
</code></pre></div></div>
<p>Azaz az összes réteg ugyanaz, mint az előbb, kivéve az utolsó 4kB-os layert. A 16 MB-os
<code class="language-plaintext highlighter-rouge">d99</code> layert újrahasznosította. Ez alapján már el lehet képzelni, hogy a
layerek használatávál mennyi tárhelyet és hálózati erőforrást takarítunk meg.</p>
RabbitMQ használata Spring Boottal2020-09-11T21:00:00+00:00http://www.jtechlog.hu/2020/09/11/rabbitmq<p>Amennyiben aszinkron üzenetküldést szeretnénk megvalósítani különböző
rendszerek vagy microservice-ek között, a RabbitMQ egy
jó választásnak tűnik. Nyílt forráskódú, széleskörben elterjedt, kellően
pehelysúlyú, könnyen telepíthető különböző környezetekben,
több protokollt és programozási nyelvet is támogat, és könnyen
illeszthető Spring Boothoz. Clusterezhető és monitorozható.
Ezt fogom bemutatni azzal együtt, hogy hogy
lehet unit és integrációst teszteket írni. Ez utóbbihoz a
<a href="https://www.testcontainers.org/">Testcontainers</a> projektet fogom használni,
melynek segítségével JUnit tesztesetekből fogok Docker konténert indítani.</p>
<p>A poszthoz a GitHubon egy példaprojekt is tartozik a <a href="https://github.com/vicziani/jtechlog-rabbitmq">jtechlog-rabbitmq</a>
néven.</p>
<!-- more -->
<p>Az egyszerűség kedvéért indítsunk el a RabbitMQ-t egy Docker konténerben a következő
paranccsal.</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker run <span class="nt">-d</span> <span class="nt">--hostname</span> my-rabbit <span class="nt">--name</span> my-rabbit <span class="nt">-p</span> 5672:5672 <span class="nt">-p</span> 15672:15672 rabbitmq:3-management
</code></pre></div></div>
<p>Ezzel egy olyan konténer indul el. A <code class="language-plaintext highlighter-rouge">5672</code>-es porton lehet hozzá kapcsolódni, és igénybe
venni az üzenetküldési funkciókat, míg a <code class="language-plaintext highlighter-rouge">15672</code>-es porton a management felülethez
lehet hozzáférni.</p>
<p>Szintén az egyszerűség jegyében nem két alkalmazás fog kommunikálni egymással, hanem
az alkalmazáson belül az egyik komponens küld egy üzenetet és a másik fogadja.</p>
<p>Az üzenetek tárolása a RabbitMQ-n belül is sorokban történik, mint a legtöbb
messaging rendszer esetén. Az üzenet küldője a producer, az üzenet fogadója a
consumer.</p>
<p>Az alkalmazás felépítését a következő ábra mutatja. Az <code class="language-plaintext highlighter-rouge">EmployeesController</code>
hívja az <code class="language-plaintext highlighter-rouge">EmployeesService</code> service-t, mely elküld egy üzenetet az <code class="language-plaintext highlighter-rouge">employeees.queue</code>
sorba. Az <code class="language-plaintext highlighter-rouge">EmployeesQueueListener</code> figyeli a sort, és az kerül automatikusan
meghívásra, ha a sorba üzenet érkezik. Ez hívja tovább az <code class="language-plaintext highlighter-rouge">EventsService</code>
service-t.</p>
<p><img src="/artifacts/posts/2020-10-11-rabbitmq/alkalmazas.png" alt="Alkalmazás" /></p>
<p>A RabbitMQ-t az AMQP protokollon szólítjuk meg. Az AMQP egy egyszerű, nyílt,
platform és programozási nyelv független protokoll, melyen message-oriented
middleware-ekhez lehet kapcsolódni. (Szemben pl. a JMS-sel, ami Javaban
használható csak.) Szerencsére a protokollt a Spring Boot elrejti előlünk,
sokat nem kell vele foglalkozni.</p>
<p>Használatához vegyük fel a <code class="language-plaintext highlighter-rouge">ṗom.xml</code> fájlban a következő függőséget:</p>
<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt"><dependency></span>
<span class="nt"><groupId></span>org.springframework.boot<span class="nt"></groupId></span>
<span class="nt"><artifactId></span>spring-boot-starter-amqp<span class="nt"></artifactId></span>
<span class="nt"></dependency></span>
</code></pre></div></div>
<p>Ez tranzítív be fogja hozni a RabbitMQ AMQP kliens könyvtárat is (<code class="language-plaintext highlighter-rouge">com.rabbitmq:amqp-client</code>).</p>
<p>A RabbitMQ érdekessége, hogy kliensből tudunk létrehozni sort a RabbitMQ szerveren. Ehhez adjuk
meg a következőt:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Configuration</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">RabbitMqConfig</span> <span class="o">{</span>
<span class="nd">@Bean</span>
<span class="kd">public</span> <span class="nc">Queue</span> <span class="nf">queue</span><span class="o">()</span> <span class="o">{</span>
<span class="k">return</span> <span class="k">new</span> <span class="nf">Queue</span><span class="o">(</span><span class="s">"employees.queue"</span><span class="o">);</span>
<span class="o">}</span>
<span class="nd">@Bean</span>
<span class="kd">public</span> <span class="nc">MessageConverter</span> <span class="nf">messageConverter</span><span class="o">()</span> <span class="o">{</span>
<span class="k">return</span> <span class="k">new</span> <span class="nf">Jackson2JsonMessageConverter</span><span class="o">();</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>Ez egyrészt létrehozza a <code class="language-plaintext highlighter-rouge">employees.queue</code> sort a RabbitMQ szerveren, valamint létrehoz egy
Jackson <code class="language-plaintext highlighter-rouge">MessageConverter</code>-t. Ekkor ha küldéskor átadunk egy Java objektumot, azt
automatikusan Jacksonnal JSON-né konvertálja.</p>
<p>A kapcsolódás automatikusan a <code class="language-plaintext highlighter-rouge">localhost</code> címre a <code class="language-plaintext highlighter-rouge">5672</code> porton történik.
Ezt konfigurálni az <code class="language-plaintext highlighter-rouge">application.properties</code>-ben lehet a <code class="language-plaintext highlighter-rouge">spring.rabbitmq.host</code> és
<code class="language-plaintext highlighter-rouge">spring.rabbitmq.port</code> konfigurációs paraméterek megadásával.</p>
<p>A küldéshez injektáljunk egy <code class="language-plaintext highlighter-rouge">RabbitTemplate</code>-et,
és ennek metódusaival tudunk üzenetet küldeni.</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">rabbitTemplate</span><span class="o">.</span><span class="na">convertAndSend</span><span class="o">(</span><span class="s">"employees.queue"</span><span class="o">,</span> <span class="k">new</span> <span class="nc">EmployeeHasCreatedEvent</span><span class="o">(</span><span class="n">command</span><span class="o">.</span><span class="na">getName</span><span class="o">()));</span>
</code></pre></div></div>
<p>Ahol az első paraméter a sor neve (erre még később visszatérünk, mert nem ilyen egyszerű a helyzet),
a második pedig az objektum. Ez az objektum kerül átkonvertálásra JSON formátumba.</p>
<p>Az üzenetküldést úgy tudjuk meghívni, hogy a <code class="language-plaintext highlighter-rouge">http://localhost:8080/api/employees</code> címre
postoljuk a következő JSON-t:</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
</span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"John Doe"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
<p>Az üzenet fogadásához a metódusra, melyet szeretnénk, hogy az
üzenet fogadásakor meghívásra kerüljön, rá kell tennünk a <code class="language-plaintext highlighter-rouge">@RabbitListener</code> annotációt.</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@RabbitListener</span><span class="o">(</span><span class="n">queues</span> <span class="o">=</span> <span class="s">"employees.queue"</span><span class="o">)</span>
<span class="kd">public</span> <span class="kt">void</span> <span class="nf">receiveEvent</span><span class="o">(</span><span class="nc">EmployeeHasCreatedEvent</span> <span class="n">event</span><span class="o">)</span> <span class="o">{</span>
<span class="c1">// ...</span>
<span class="o">}</span>
</code></pre></div></div>
<p>Az annotáció paraméterekor meg kell adni a sor nevét. A bejövő üzenet tartalmát
Jacksonnal megpróbálja JSON-ből <code class="language-plaintext highlighter-rouge">EmployeeHasCreatedEvent</code> objektummá alakítani.</p>
<p>Az alkalmazás indításakor hozzákapcsolódik a RabbitMQ-hoz, erről a következő
log üzenet tájékoztat:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>o.s.a.r.c.CachingConnectionFactory :
Created new connection: rabbitConnectionFactory#2954b5ea:0/SimpleConnection@6cd64b3f [delegate=amqp://guest@127.0.0.1:5672/, localPort= 52106]
</code></pre></div></div>
<p>A RabbitMQ admin felülete elérhető a <code class="language-plaintext highlighter-rouge">http://localhost:15672</code> címen, alapértelmezett felhasználónév/jelszó
a <code class="language-plaintext highlighter-rouge">guest/guest</code>. Bejelentkezve látható a létrehozott sor és hogy egy alkalmazás kapcsolódott hozzá.</p>
<p>A RabbitMQ azonban nem csak point-to-point kommunikációt tesz lehetővé, hanem
támogatja a Publish/Subscribe módot is, hogy egy producer több consumernek küld
üzenetet. Másik konfiguráció a Work queues, ahol egy sorból több consumer
veszi ki az üzenetet, egyszerre csak az egyik. Ezzel gyorsan lehet pl. egy terheléselosztást
implementálni. Ezen kívül lehet ún. üzenetirányítást (Routing) is konfigurálni,
mely az üzenet valamilyen tulajdonsága (pl. címzés alapján) dobálja szét az üzeneteket
a különböző sorok között.</p>
<p>Ezek támogatására egy plusz absztrakciós szintet vezettek be, amivel mindegyik mód
egyszerűen konfigurálható. Az üzenet ugyanis nem egy sorba történik, hanem egy ún.
<em>Exchange</em>-be, ami dobálja tovább az üzeneteket a sorokba. Azonban mivel
támogatja azt is, hogy az üzenet tulajdonsága alapján döntsön, hogy melyik sorba
továbbítsa, ezért az üzenetnek át lehet adni egy <code class="language-plaintext highlighter-rouge">Routing Key</code>-t, mely
ebben segít. Ez az üzenethez tartozik, azt minősíti.</p>
<p>És az előbbi, point-to-point üzenet esetén nem adtuk meg hogy melyik Exchange-be
menjen az üzenet, ezért az ún. <em>Default Exchange</em>-be került átküldésre, mely
úgy működik, hogy abba a sorba továbbítja az üzenetet, mely a Routing Key-be meg van adva.
Tehát az előbb a <code class="language-plaintext highlighter-rouge">convertAndSend()</code> metódus első parmétere nem a sor neve,
hanem a Routing Key, mely jelen esetben megegyezik a sor nevével.</p>
<p>A <em>Binding</em> az a mechanizmus, ami megmondja, hogy az Exchange-ből a Routing Key alapján
melyik sorba is kerüljön az üzenet továbbításra.</p>
<p>Ha pl. egy Routingot akarunk megvalósítani, akkor az Exchange-hez <code class="language-plaintext highlighter-rouge">orange</code>
Routing Key-jel bindoljuk a <code class="language-plaintext highlighter-rouge">Q1</code> sort, míg a <code class="language-plaintext highlighter-rouge">black</code> Routing Key-jel a
<code class="language-plaintext highlighter-rouge">Q2</code> sort. Így nem kell az alkalmazásnak tudnia, hogy melyik sorba küldje az üzenetet,
csak az üzenetről kell megmondania (küldéskor), hogy az <code class="language-plaintext highlighter-rouge">orange</code> vagy <code class="language-plaintext highlighter-rouge">black</code>.</p>
<p><img src="/artifacts/posts/2020-10-11-rabbitmq/exchange.png" alt="Exchange" /></p>
<p>A küldés unit teszteléséhez csak mockoljuk ki a <code class="language-plaintext highlighter-rouge">RabbitTemplate</code>-et.</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Test</span>
<span class="kt">void</span> <span class="nf">testSend</span><span class="o">()</span> <span class="o">{</span>
<span class="n">employeesService</span><span class="o">.</span><span class="na">createEmployee</span><span class="o">(</span><span class="k">new</span> <span class="nc">CreateEmployeeCommand</span><span class="o">(</span><span class="s">"John Doe"</span><span class="o">));</span>
<span class="n">verify</span><span class="o">(</span><span class="n">rabbitTemplate</span><span class="o">).</span><span class="na">convertAndSend</span><span class="o">(</span><span class="n">eq</span><span class="o">(</span><span class="s">"employees.queue"</span><span class="o">),</span>
<span class="n">argThat</span><span class="o">((</span><span class="nc">Object</span> <span class="n">e</span><span class="o">)</span> <span class="o">-></span> <span class="o">((</span><span class="nc">EmployeeHasCreatedEvent</span><span class="o">)</span><span class="n">e</span><span class="o">).</span><span class="na">getName</span><span class="o">().</span><span class="na">equals</span><span class="o">(</span><span class="s">"John Doe"</span><span class="o">)));</span>
<span class="o">}</span>
</code></pre></div></div>
<p>Itt a <code class="language-plaintext highlighter-rouge">rabbitTemplate</code> Mockitoval mockolt.</p>
<p>A fogadás unit teszteléséhez hívjuk meg a Listener megfelelő metódusát,
mintha csak a Spring Boot tette volna. És nézzük meg, hogy megfelelően
hív tovább. Ezért érdemes egy Listener osztályba különválasztani az
üzenet fogadását, mely kizárólag fogadja, és ha kell konvertálja az üzenetet.</p>
<p>Az integrációs teszteléshez használjuk a <a href="https://www.testcontainers.org/">Testcontainers</a> projektet.
Fel kell venni a következő függőségeket:</p>
<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt"><dependency></span>
<span class="nt"><groupId></span>org.testcontainers<span class="nt"></groupId></span>
<span class="nt"><artifactId></span>testcontainers<span class="nt"></artifactId></span>
<span class="nt"><version></span>${testcontainers.version}<span class="nt"></version></span>
<span class="nt"><scope></span>test<span class="nt"></scope></span>
<span class="nt"></dependency></span>
<span class="nt"><dependency></span>
<span class="nt"><groupId></span>org.testcontainers<span class="nt"></groupId></span>
<span class="nt"><artifactId></span>junit-jupiter<span class="nt"></artifactId></span>
<span class="nt"><version></span>${testcontainers.version}<span class="nt"></version></span>
<span class="nt"><scope></span>test<span class="nt"></scope></span>
<span class="nt"></dependency></span>
</code></pre></div></div>
<p>A teszteseten használjuk a <code class="language-plaintext highlighter-rouge">@Testcontainers</code> annotációt.
Egy Docker konténert a következőképp lehet elindítani:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@SpringBootTest</span>
<span class="nd">@Testcontainers</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">EmployeesIT</span> <span class="o">{</span>
<span class="nd">@Container</span>
<span class="kd">static</span> <span class="nc">GenericContainer</span> <span class="n">rabbit</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">GenericContainer</span><span class="o">(</span><span class="s">"rabbitmq:3"</span><span class="o">)</span>
<span class="o">.</span><span class="na">withExposedPorts</span><span class="o">(</span><span class="mi">5672</span><span class="o">);</span>
</code></pre></div></div>
<p>A <code class="language-plaintext highlighter-rouge">@Container</code> annotáció hatására a Testcontainers elindít egy
új Docker konténert a <code class="language-plaintext highlighter-rouge">rabbitmq:3</code> image alapján, és kihozza a <code class="language-plaintext highlighter-rouge">5672</code>-es
portját. A teszteset lefutása után le is állítja és törli azt.</p>
<p>A probléma abból adódik, hogy egy random szabad portot választ, ahova
az <code class="language-plaintext highlighter-rouge">5672</code>-es portot kihozza. És ezt át kell adni a Spring Bootnak
induláskor. Ehhez Spring Boot integrációs tesztnél egy <code class="language-plaintext highlighter-rouge">ApplicationContextInitializer</code>
interfészt kell implementálni, és az osztályt megadni a <code class="language-plaintext highlighter-rouge">@ContextConfiguration</code>
annotáció paramétereként.</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@SpringBootTest</span>
<span class="nd">@Testcontainers</span>
<span class="nd">@ContextConfiguration</span><span class="o">(</span><span class="n">initializers</span> <span class="o">=</span> <span class="nc">EmployeesIT</span><span class="o">.</span><span class="na">Initializer</span><span class="o">.</span><span class="na">class</span><span class="o">)</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">EmployeesIT</span> <span class="o">{</span>
<span class="nd">@Container</span>
<span class="kd">static</span> <span class="nc">GenericContainer</span> <span class="n">rabbit</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">GenericContainer</span><span class="o">(</span><span class="s">"rabbitmq:3"</span><span class="o">)</span>
<span class="o">.</span><span class="na">withExposedPorts</span><span class="o">(</span><span class="mi">5672</span><span class="o">);</span>
<span class="kd">public</span> <span class="kd">static</span> <span class="kd">class</span> <span class="nc">Initializer</span> <span class="kd">implements</span>
<span class="nc">ApplicationContextInitializer</span><span class="o"><</span><span class="nc">ConfigurableApplicationContext</span><span class="o">></span> <span class="o">{</span>
<span class="nd">@Override</span>
<span class="kd">public</span> <span class="kt">void</span> <span class="nf">initialize</span><span class="o">(</span><span class="nc">ConfigurableApplicationContext</span> <span class="n">configurableApplicationContext</span><span class="o">)</span> <span class="o">{</span>
<span class="nc">TestPropertyValues</span> <span class="n">values</span> <span class="o">=</span> <span class="nc">TestPropertyValues</span><span class="o">.</span><span class="na">of</span><span class="o">(</span>
<span class="s">"spring.rabbitmq.host="</span> <span class="o">+</span> <span class="n">rabbit</span><span class="o">.</span><span class="na">getContainerIpAddress</span><span class="o">(),</span>
<span class="s">"spring.rabbitmq.port="</span> <span class="o">+</span> <span class="n">rabbit</span><span class="o">.</span><span class="na">getMappedPort</span><span class="o">(</span><span class="mi">5672</span><span class="o">)</span>
<span class="o">);</span>
<span class="n">values</span><span class="o">.</span><span class="na">applyTo</span><span class="o">(</span><span class="n">configurableApplicationContext</span><span class="o">);</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>A teszteset felülírja az <code class="language-plaintext highlighter-rouge">EventsService</code> service-t egy mock service-zel (<code class="language-plaintext highlighter-rouge">@MockBean</code> annotációval),
és azt ellenőrzi, hogy a metódusa meghívásra került-e. Igen ám, de itt az az
izgalmas, hogy a hívás nem szinkron történik, ugyanis az üzenet átkerül a RabbitMQ-ba,
és majd az valamikor kézbesíti. Ezt a Mockitonak meg lehet adni, és ő
képes várni a hívás tényére, sőt még timeoutot is tudunk megadni:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Test</span>
<span class="kt">void</span> <span class="nf">testSendAndReceive</span><span class="o">()</span> <span class="o">{</span>
<span class="n">employeesController</span><span class="o">.</span><span class="na">createEmployee</span><span class="o">(</span><span class="k">new</span> <span class="nc">CreateEmployeeCommand</span><span class="o">(</span><span class="s">"John Doe"</span><span class="o">));</span>
<span class="n">verify</span><span class="o">(</span><span class="n">eventsService</span><span class="o">,</span> <span class="n">timeout</span><span class="o">(</span><span class="mi">4000</span><span class="o">).</span><span class="na">times</span><span class="o">(</span><span class="mi">1</span><span class="o">))</span>
<span class="o">.</span><span class="na">processEvent</span><span class="o">(</span><span class="n">argThat</span><span class="o">(</span><span class="n">e</span> <span class="o">-></span> <span class="n">e</span><span class="o">.</span><span class="na">getName</span><span class="o">().</span><span class="na">equals</span><span class="o">(</span><span class="s">"John Doe"</span><span class="o">)));</span>
<span class="o">}</span>
</code></pre></div></div>
<p>Az említett kód vár maximum 4 másodpercet, amíg az <code class="language-plaintext highlighter-rouge">eventsService</code> <code class="language-plaintext highlighter-rouge">processEvent()</code> metódusa
meghívásra nem kerül.</p>
Időzónák használata2020-07-22T09:00:00+00:00http://www.jtechlog.hu/2020/07/22/idozonak<p>Az időzóna az a terület, ahol az óráknak azonos időt kéne mutatniuk. Funkcionális okok miatt
ezeket nem a földrajzi hosszúságok határozzák meg, hanem tipikusan az országhatárokhoz igazodnak.
Mindegyik időzónát a UTC világidőhöz (egyezményes világidő) viszonyítják. A UTC a GMT-t váltotta, de ez utóbbi már elavult,
ne használjuk. A magyarországi időzóna a téli időszámításkor a CET (ami egy órával van előrébb
a UTC-nél, jelölése ezért <code class="language-plaintext highlighter-rouge">UTC+1</code>), nyáron pedig CEST (, ami kettővel).</p>
<p>A dátumok formázásra van a ISO-8601 szabvány, ennek felel meg például a
<code class="language-plaintext highlighter-rouge">2011-12-03T10:15:30+01:00[Europe/Budapest]</code> formátum is, mely tartalmazza az
UTC-hez képest az eltolást, és az időzóna nevét.</p>
<p>Ebben a posztban megvizsgálom, hogy hogy lehet kezelni az időzónát operációs rendszer szinten,
adatbázisban, Java SE-ben, valamint egy Springes alkalmazásban, JPA/Hibernate perzisztens
réteggel, és Jackson JSON library-vel.</p>
<p>Talán nem is fontos a teljes poszt megértése, inkább azt érdemes megjegyezni, hogy hol történnek
konverziók, és ezeket hogy érdemes debuggolni.</p>
<!-- more -->
<h2 id="operációs-rendszer">Operációs rendszer</h2>
<p>Az időzóna beállítások már az operációs rendszernél kezdődnek. Windows
esetén lekérdezni parancssorból a <code class="language-plaintext highlighter-rouge">tzutil /g</code> paranccsal lehet. Ez nálam
a <code class="language-plaintext highlighter-rouge">Central Europe Standard Time</code> értéket adja vissza. Az elérhető
időzónákat a <code class="language-plaintext highlighter-rouge">tzutil /l</code> paranccsal lehet kilistázni. Linuxon a <code class="language-plaintext highlighter-rouge">date +"%Z %z"</code>
paranccsal kérdezhető le, ez nálam <code class="language-plaintext highlighter-rouge">CEST +0200</code>. Az elérhető időzónák
lekérdezhetőek a <code class="language-plaintext highlighter-rouge">timedatectl list-timezones</code> paranccsal.</p>
<h2 id="adatbázis">Adatbázis</h2>
<p>A következő elem az adatbázis. Ebben a posztban a PostgreSQL-t fogom megvizsgálni.
Időzóna lekérdezése a <code class="language-plaintext highlighter-rouge">show timezone;</code> utasítással történik. Az elérhető időzónákat
a következő lekérdezés adja vissza:</p>
<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">select</span> <span class="o">*</span> <span class="k">from</span> <span class="n">pg_timezone_names</span><span class="p">;</span>
</code></pre></div></div>
<p>Az időzóna beállítás Postgresql alatt lehet globális (pl. a <code class="language-plaintext highlighter-rouge">postgresql.conf</code>
állományban), de felülírható a sessionben is.</p>
<p>Ez úgy demonstrálható, hogy lekérdezzük az időt, majd átállítjuk
az időzónát, majd újra lekérdezzük. Beállításra a <code class="language-plaintext highlighter-rouge">set time zone</code> parancs használható.
(Működik a <code class="language-plaintext highlighter-rouge">SET TIMEZONE TO</code> is, de az nem felel meg annyira az SQL szabványnak.)</p>
<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">set</span> <span class="nb">time</span> <span class="k">zone</span> <span class="s1">'UTC'</span><span class="p">;</span>
<span class="k">select</span> <span class="n">now</span><span class="p">();</span> <span class="c1">-- 2020-07-17 13:59:57</span>
<span class="k">set</span> <span class="nb">time</span> <span class="k">zone</span> <span class="s1">'Europe/Budapest'</span><span class="p">;</span>
<span class="k">select</span> <span class="n">now</span><span class="p">();</span> <span class="c1">-- 2020-07-17 15:59:57</span>
</code></pre></div></div>
<p><strong>De vigyázz!</strong> Vannak olyan kliensek, melyek a visszakapott dátum
típusú értéket azonnal átkonvertálják a kliens időzónájára.
Ezt úgy lehet ellenőrizni, hogy már a szerver oldalon
karakterlánccá alakítjuk.</p>
<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">select</span> <span class="n">now</span><span class="p">();</span>
<span class="k">select</span> <span class="n">to_char</span><span class="p">(</span><span class="n">now</span><span class="p">(),</span> <span class="s1">'yyyy-mm-dd hh24:mi:ss[TZ]'</span><span class="p">);</span>
</code></pre></div></div>
<p>Ha nekem eltért a session időzónája, és a kliens
időzónája, akkor különböző értékeket adott vissza. Én a DBeavert használtam, és ez
konvertálta az időket. Ezt meg lehet akadályozni a <code class="language-plaintext highlighter-rouge">Properties / Editors / Data Editor / Data Formats</code>
ablakon a <code class="language-plaintext highlighter-rouge">Use native date/time format</code> kipipálásával.</p>
<h2 id="intermezzo">Intermezzo</h2>
<p>A helyzet ennél még bonyolultabb. A PostgreSQL-t egy Docker konténerben indítottam
az alábbi parancs megadásával:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker run --name timezone-postgres -e POSTGRES_PASSWORD=timezone -d -p 5432:5432 postgres
</code></pre></div></div>
<p>Kíváncsi voltam, mi van megadva ekkor a konfigurációs fájlban (<code class="language-plaintext highlighter-rouge">/var/lib/postgresql/data/postgresql.conf</code>).</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ docker exec -it timezone-postgres cat /var/lib/postgresql/data/postgresql.conf | grep timezone
timezone = 'Etc/UTC'
</code></pre></div></div>
<p>Valamint megnéztem, hogy mit ad vissza a parancssori kliens.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ docker exec -it timezone-postgres psql -U postgres -c "show timezone;"
TimeZone
----------
Etc/UTC
(1 row)
</code></pre></div></div>
<p>Azaz belépés után a <code class="language-plaintext highlighter-rouge">show timezone;</code> parancs <code class="language-plaintext highlighter-rouge">Europe/Budapest</code> értéket adott vissza. Ez azért van, mert
a DBeaver felülvágja a platform alapértelmezett időzónájával. Ők ezt persze a PostgreSQL JDBC Driver
ismert tulajdonságára vezetik vissza.
Ennek megoldása, hogy vagy átállítjuk a DBeaver alatti JVM-ben az időzónát vagy a <code class="language-plaintext highlighter-rouge">dbeaver.ini</code> fájlban,
vagy parancssori paraméterben (<code class="language-plaintext highlighter-rouge">-vmargs -Duser.timezone=UTC</code>). Vagy belépés után azonnal <code class="language-plaintext highlighter-rouge">set time zone</code> parancsot adunk ki.</p>
<p>Tanulság: sose higgyünk a grafikus klienseknek időzóna ügyben!</p>
<p>Az IntelliJ IDEA-ba épített nem trükközik így.</p>
<h2 id="timestamp-with-time-zone">Timestamp with time zone</h2>
<p>A legnagyobb meglepetés a típusok körül érhet. Időzónának értelme a dátum és idő együttesénél van. Erre
a <code class="language-plaintext highlighter-rouge">timestamp</code> típus használható. Azonban van egy <code class="language-plaintext highlighter-rouge">timestamp</code> és egy <code class="language-plaintext highlighter-rouge">timestamp with time zone</code> típus is.
Azonban ez utóbbi az időt mindig UTC-ben tárolja!
(<a href="https://www.postgresql.org/docs/12/datatype-datetime.html">Lásd dokumentáció!</a>)</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>All timezone-aware dates and times are stored internally in UTC. They are converted to local time in the
zone specified by the TimeZone configuration parameter before being displayed to the client.
</code></pre></div></div>
<p>A különbség beszúráskor és lekérdezéskor is jelentkezik. Amikor <code class="language-plaintext highlighter-rouge">timestamp</code> mezőbe szúrunk be,
és megadunk offset-et, akkor azt teljesen figyelmen kívül, hagyja, azaz levágja.</p>
<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">drop</span> <span class="k">table</span> <span class="n">if</span> <span class="k">exists</span> <span class="n">employees</span><span class="p">;</span>
<span class="k">create</span> <span class="k">table</span> <span class="n">employees</span> <span class="p">(</span><span class="n">id</span> <span class="n">int8</span> <span class="k">generated</span> <span class="k">by</span> <span class="k">default</span> <span class="k">as</span> <span class="k">identity</span><span class="p">,</span>
<span class="n">name</span> <span class="nb">varchar</span><span class="p">(</span><span class="mi">255</span><span class="p">),</span> <span class="n">valid_from</span> <span class="nb">timestamp</span><span class="p">);</span>
<span class="k">insert</span> <span class="k">into</span> <span class="n">employees</span> <span class="p">(</span><span class="n">name</span><span class="p">,</span> <span class="n">valid_from</span><span class="p">)</span> <span class="k">values</span> <span class="p">(</span><span class="s1">'John Doe'</span><span class="p">,</span> <span class="s1">'2020-04-01 10:00:00.00+0200'</span><span class="p">);</span>
<span class="k">select</span> <span class="n">name</span><span class="p">,</span> <span class="n">to_char</span><span class="p">(</span><span class="n">valid_from</span><span class="p">,</span> <span class="s1">'yyyy-mm-dd hh24:mi:ss[TZ]'</span><span class="p">)</span> <span class="k">from</span> <span class="n">employees</span><span class="p">;</span>
<span class="c1">-- 2020-04-01 10:00:00[]</span>
</code></pre></div></div>
<p>Azonban ha <code class="language-plaintext highlighter-rouge">timestamp with time zone</code> típust használunk, akkor figyelembe veszi, sőt átkonvertálja UTC
értékre és úgy tárolja.</p>
<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">drop</span> <span class="k">table</span> <span class="n">if</span> <span class="k">exists</span> <span class="n">employees</span><span class="p">;</span>
<span class="k">create</span> <span class="k">table</span> <span class="n">employees</span> <span class="p">(</span><span class="n">id</span> <span class="n">int8</span> <span class="k">generated</span> <span class="k">by</span> <span class="k">default</span> <span class="k">as</span> <span class="k">identity</span><span class="p">,</span>
<span class="n">name</span> <span class="nb">varchar</span><span class="p">(</span><span class="mi">255</span><span class="p">),</span> <span class="n">valid_from</span> <span class="nb">timestamp</span> <span class="k">with</span> <span class="nb">time</span> <span class="k">zone</span><span class="p">);</span>
<span class="k">insert</span> <span class="k">into</span> <span class="n">employees</span> <span class="p">(</span><span class="n">name</span><span class="p">,</span> <span class="n">valid_from</span><span class="p">)</span> <span class="k">values</span> <span class="p">(</span><span class="s1">'John Doe'</span><span class="p">,</span> <span class="s1">'2020-04-01 10:00:00.00+0200'</span><span class="p">);</span>
<span class="k">select</span> <span class="n">name</span><span class="p">,</span> <span class="n">to_char</span><span class="p">(</span><span class="n">valid_from</span><span class="p">,</span> <span class="s1">'yyyy-mm-dd hh24:mi:ss[TZ]'</span><span class="p">)</span> <span class="k">from</span> <span class="n">employees</span><span class="p">;</span>
<span class="c1">-- 2020-04-01 08:00:00[UTC]</span>
</code></pre></div></div>
<p>A különbség csak a <code class="language-plaintext highlighter-rouge">create table</code> utasításban van, a típus itt <code class="language-plaintext highlighter-rouge">timestamp with time zone</code>.</p>
<p>Lekérdezéskor a különbség annyi, hogy <code class="language-plaintext highlighter-rouge">with time zone</code> esetén amikor lekérdezünk, akkor figyelembe veszi a session időzóna
beállítását. Nézzünk is rá egy összehasonlítást. Az első példa <code class="language-plaintext highlighter-rouge">with time zone</code> nélkül.</p>
<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">set</span> <span class="nb">time</span> <span class="k">zone</span> <span class="s1">'Europe/Budapest'</span><span class="p">;</span>
<span class="k">drop</span> <span class="k">table</span> <span class="n">if</span> <span class="k">exists</span> <span class="n">employees</span><span class="p">;</span>
<span class="k">create</span> <span class="k">table</span> <span class="n">employees</span> <span class="p">(</span><span class="n">id</span> <span class="n">int8</span> <span class="k">generated</span> <span class="k">by</span> <span class="k">default</span> <span class="k">as</span> <span class="k">identity</span><span class="p">,</span>
<span class="n">name</span> <span class="nb">varchar</span><span class="p">(</span><span class="mi">255</span><span class="p">),</span> <span class="n">valid_from</span> <span class="nb">timestamp</span><span class="p">);</span>
<span class="k">insert</span> <span class="k">into</span> <span class="n">employees</span> <span class="p">(</span><span class="n">name</span><span class="p">,</span> <span class="n">valid_from</span><span class="p">)</span> <span class="k">values</span> <span class="p">(</span><span class="s1">'John Doe'</span><span class="p">,</span> <span class="s1">'2020-04-01 10:00:00.00[UTC]'</span><span class="p">);</span>
<span class="k">select</span> <span class="n">name</span><span class="p">,</span> <span class="n">valid_from</span> <span class="k">from</span> <span class="n">employees</span><span class="p">;</span>
<span class="c1">-- 2020-04-01 10:00:00</span>
</code></pre></div></div>
<p>UTC-ben szúrunk be <code class="language-plaintext highlighter-rouge">10:00</code> órát, és bár a session időzóna UTC+2, mégis <code class="language-plaintext highlighter-rouge">10:00</code> órát kapunk vissza.</p>
<p>És most nézzük meg <code class="language-plaintext highlighter-rouge">with time zone</code> típussal:</p>
<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">set</span> <span class="nb">time</span> <span class="k">zone</span> <span class="s1">'Europe/Budapest'</span><span class="p">;</span>
<span class="k">drop</span> <span class="k">table</span> <span class="n">if</span> <span class="k">exists</span> <span class="n">employees</span><span class="p">;</span>
<span class="k">create</span> <span class="k">table</span> <span class="n">employees</span> <span class="p">(</span><span class="n">id</span> <span class="n">int8</span> <span class="k">generated</span> <span class="k">by</span> <span class="k">default</span> <span class="k">as</span> <span class="k">identity</span><span class="p">,</span>
<span class="n">name</span> <span class="nb">varchar</span><span class="p">(</span><span class="mi">255</span><span class="p">),</span> <span class="n">valid_from</span> <span class="nb">timestamp</span> <span class="k">with</span> <span class="nb">time</span> <span class="k">zone</span><span class="p">);</span>
<span class="k">insert</span> <span class="k">into</span> <span class="n">employees</span> <span class="p">(</span><span class="n">name</span><span class="p">,</span> <span class="n">valid_from</span><span class="p">)</span> <span class="k">values</span> <span class="p">(</span><span class="s1">'John Doe'</span><span class="p">,</span> <span class="s1">'2020-04-01 10:00:00.00[UTC]'</span><span class="p">);</span>
<span class="k">select</span> <span class="n">name</span><span class="p">,</span> <span class="n">valid_from</span> <span class="k">from</span> <span class="n">employees</span><span class="p">;</span>
<span class="c1">-- 2020-04-01 12:00:00+02</span>
</code></pre></div></div>
<p>A különbség csak a <code class="language-plaintext highlighter-rouge">create table</code> utasításban van, a típus itt <code class="language-plaintext highlighter-rouge">timestamp with time zone</code>.</p>
<p>Ekkor a visszaadott érték <code class="language-plaintext highlighter-rouge">2020-04-01 12:00:00+02</code>, azaz <code class="language-plaintext highlighter-rouge">12:00</code> óra, ráadásul időzóna megjelöléssel (<code class="language-plaintext highlighter-rouge">+02</code>).
Azaz itt figyelembe veszi az időzónát. De az adatbázisban <strong>nem</strong> tárol időzónát!</p>
<h2 id="java-se">Java SE</h2>
<p>Nézzük az időzónák kezelését Javaban.</p>
<p>A JDK az alapértelmezett időzónát az operációs rendszertől kéri le. Ezt le lehet kérni pl. a <code class="language-plaintext highlighter-rouge">ZoneId.systemDefault()</code>
metódussal. Ezt parancssorból a <code class="language-plaintext highlighter-rouge">-Duser.timezone=UTC</code> megadásával felül is tudjuk bírálni. Az elérhető
időzónák lekérdezhetőek a <code class="language-plaintext highlighter-rouge">ZoneId.getAvailableZoneIds()</code> metódussal.</p>
<p>A régi <code class="language-plaintext highlighter-rouge">Date</code> osztály nem tárol időzónát. Egy időpillanatot reprezentál UTC-ben, valójában az
“epoch”-tól eltelt ezredmásodpercek számát tárolja.
Lehetőleg ezt a típust már modern alkalmazásokban <strong>ne</strong> használjuk! Ezért ezt a típust nem is fogom vizsgálni.
Akit érdekel, a posztban leírt teszteseteket kipróbálhatja.
Azonban a hibakereséshez érdemes ismerni, mert az időt a JDBC Driver kaphatja <code class="language-plaintext highlighter-rouge">java.sql.Date</code> vagy <code class="language-plaintext highlighter-rouge">Timestamp</code> típusként
is, amely mindkettő <code class="language-plaintext highlighter-rouge">java.util.Date</code> leszármazott. Az a speciális tulajdonsága van, hogy a <code class="language-plaintext highlighter-rouge">toString()</code> metódusát
úgy implementálták, hogy figyelembe veszi a JVM időzónáját, és abban írja ki.
A <code class="language-plaintext highlighter-rouge">LocalDateTime</code> tárol dátumot és időt, időzóna nélkül. A <code class="language-plaintext highlighter-rouge">ZonedDateTime</code> reprezentál egy időpillanatot, és tárolja hozzá az időzónát is.</p>
<h2 id="spring-boot-alkalmazás">Spring Boot alkalmazás</h2>
<p>És most nézzük meg egy komplett Spring Boot alkalmazást REST API-val. Alapesetben a JSON (de)szerializációt
a Jackson library végzi. Az adatbázis réteget Spring Data JPA-val implementáltam,
mely alatt Hibernate dolgozik, ez hozza létre az adatbázis sémát is. PostgreSQL adatbázist használtam. A példa projekt elérhető a
<a href="https://github.com/vicziani/jtechlog-timezone">GitHubon</a>.</p>
<p>Az érdekesség kedvéért az alkalmazás Windowson fut CEST időzónában, míg a PostreSQL UTC-ben, egy Docker konténerben.</p>
<p>Az <code class="language-plaintext highlighter-rouge">Employee</code> entitás tartalmaz egy <code class="language-plaintext highlighter-rouge">validFrom</code> attribútumot.</p>
<p><a href="/artifacts/posts/2020-07-22-idozonak/timezone.png" data-lightbox="post-images"><img src="/artifacts/posts/2020-07-22-idozonak/timezone_750.png" alt="Alkalmazás" /></a></p>
<h2 id="controller-réteg">Controller réteg</h2>
<p>Az attribútum először legyen <code class="language-plaintext highlighter-rouge">LocalDateTime</code> típusú.</p>
<p>Alkalmazott létrehozásához
a <code class="language-plaintext highlighter-rouge">http://localhost:8080/api/employees</code> címre kell a következő JSON-t <code class="language-plaintext highlighter-rouge">post</code> metódussal elküldeni.</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
</span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"John Doe"</span><span class="p">,</span><span class="w">
</span><span class="nl">"validFrom"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2020-04-01T10:00:00"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
<p>Amennyiben időzónát is megadok, a Jackson kivétellel elszáll. Helyes érték esetén, ha a szerver oldalon
kiíratom a mező értékét, szintén <code class="language-plaintext highlighter-rouge">2020-04-01T10:00:00</code> értéket kapok, azaz egy az egyben letárolja a változóban.</p>
<p>Majd kipróbáltam, hogy a következő Jackson paramétert állítottam be az <code class="language-plaintext highlighter-rouge">application.properties</code>
állományban.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>spring.jackson.time-zone=UTC
</code></pre></div></div>
<p>Itt bármit is állítottam be, nem volt hatással az alkalmazás működésére, tehát <code class="language-plaintext highlighter-rouge">LocalDateTime</code> esetén
azt tárolja el, ami jön.</p>
<p>Majd a típust átállítottam <code class="language-plaintext highlighter-rouge">ZonedDateTime</code> típusra.</p>
<p>Ekkor már az előző JSON-t el sem fogadja, kivételt dob. Mindenképp meg kell adni az időzónát is.</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
</span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"John Doe"</span><span class="p">,</span><span class="w">
</span><span class="nl">"validFrom"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2020-04-01T10:00:00+02:00"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
<p>Ekkor átváltja UTC-re (attól függetlenül, hogy a JVM CEST-ben volt), és már Java oldalon így jelenik meg: <code class="language-plaintext highlighter-rouge">2020-04-01T08:00Z[UTC]</code>.
(Érdekesség, hogy a <code class="language-plaintext highlighter-rouge">z</code> karakter arra utal, hogy a NATO által használt fonetikus ábécében <em>Zulunak</em> mondják.
Ugyanis a <code class="language-plaintext highlighter-rouge">UTC+1</code> az Alpha time, a <code class="language-plaintext highlighter-rouge">UTC-2</code> a Bravo time, és így tovább a Zulu time-ig, ami a <code class="language-plaintext highlighter-rouge">UTC</code>.)</p>
<p>Majd a következő beállítást használtam az <code class="language-plaintext highlighter-rouge">application.properties</code> fájlban:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>spring.jackson.time-zone=Europe/Budapest
</code></pre></div></div>
<p>Ezután szerver oldalon mindig CEST-re konvertálta az időt. Sőt, a beállítás előtt minden időt UTC-ben adott vissza,
a beállítás után minden időt CEST-ben adott vissza.</p>
<h2 id="repository-réteg">Repository réteg</h2>
<p>A repository rétegben történő átváltások már kicsit bonyolultabbak. Nézzük, hogy milyen lépésekből áll,
és hogy lehet ezeket debuggolni:</p>
<ul>
<li>Entitásban megjelenik a <code class="language-plaintext highlighter-rouge">LocalDateTime</code> vagy <code class="language-plaintext highlighter-rouge">ZonedDateTime</code> típus. Ennek értékét kiírattam. Szerencsére ez
ISO-8601 szabvány szerinti és nem függ a JVM időzónájától. Valamint a biztonság kedvéért kiírattam a JVM
időzónáját is.</li>
<li>Az entitás átadásra kerül a Hibernate-nek</li>
<li>A Hibernate képes a paraméterek naplózására is, a következő beállítások bekapcsolásával az <code class="language-plaintext highlighter-rouge">application.properties</code>
állományban.</li>
</ul>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>logging.level.org.hibernate.SQL=debug
logging.level.org.hibernate.type=trace
</code></pre></div></div>
<p>Sajnos ez túl sokat nem ér, ugyanis egy az egyben az entitásban szereplő értéket írja ki.</p>
<ul>
<li>A Hibernate átkonvertálja az entitásban szereplő értéket <code class="language-plaintext highlighter-rouge">Timestamp</code> típussá. Majd meghívja a
JDBC Driver <code class="language-plaintext highlighter-rouge">PreparedStatement.setTimestamp(int, Timestamp)</code> metódusát. Ezt úgy ellenőriztem, hogy
a megnyitottam a JDBC Driver <code class="language-plaintext highlighter-rouge">PgPreparedStatement</code> osztályát, és breakpointokat helyeztem el.</li>
<li>Az érdekesség akkor történt, mikor az <code class="language-plaintext highlighter-rouge">application.properties</code> állományban a következő beállítást írtam:</li>
</ul>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>spring.jpa.properties.hibernate.jdbc.time_zone=UTC
</code></pre></div></div>
<p>Ezzel az adatbázis időzónáját mondom meg a Hibernate-nek.
Ekkor azonban a <code class="language-plaintext highlighter-rouge">setTimestamp(int, Timestamp, Calendar)</code> metódus került meghívásra.
Látható, hogy ez
egy <code class="language-plaintext highlighter-rouge">Calendar</code>-t, melyben be van állítva az időzóna (az <code class="language-plaintext highlighter-rouge">application.properties</code> fájlban lévő), és a paraméterként kapott, UTC-ben tárolt
<code class="language-plaintext highlighter-rouge">Timestamp</code> értéket erre az időzónára alakítja. Ezt a <code class="language-plaintext highlighter-rouge">Timestamp</code> és <code class="language-plaintext highlighter-rouge">Calendar</code> értéket folyamatosan figyeltem.</p>
<ul>
<li>Biztos ami biztos elhelyeztem egy <a href="https://github.com/p6spy/p6spy">P6Spy</a> wrappert/JDBC Drivert is, ami
naplózza az összes keresztülmenő forgalmat. Sajnos ettől nem sokkal lettem okosabb, mert csak a paraméterül
átadott <code class="language-plaintext highlighter-rouge">Timestamp</code>-et írja ki, ráadásul saját <code class="language-plaintext highlighter-rouge">SimpleDateFormat</code>-tal. Ez amúgy egy hasznos eszköz, és
Spring Boot alkalmazás esetén elég felvenni a <code class="language-plaintext highlighter-rouge">com.github.gavlyukovskiy:p6spy-spring-boot-starter</code>
függőséget.</li>
<li>Az érdekesség a JDBC Driveren belül történik. Amennyiben a <code class="language-plaintext highlighter-rouge">setTimestamp()</code> metódus nem kap <code class="language-plaintext highlighter-rouge">Calendar</code> objektumot,
úgy a JVM időzónája alapján konvertálja <code class="language-plaintext highlighter-rouge">String</code> típussá a paraméterként kapott <code class="language-plaintext highlighter-rouge">Timestamp</code> értéket.
Amennyiben kap <code class="language-plaintext highlighter-rouge">Calendar</code> objektumot (a Hibernate beállítással), akkor annak az időzónájára konvertál.
A JDBC Driver ezt a <code class="language-plaintext highlighter-rouge">String</code> értéket küldi tovább az adatbázisnak.</li>
<li>Ha az adatbázisban lévő mező típusa <code class="language-plaintext highlighter-rouge">timestamp</code>, akkor levágja az offsetet, és csak az idő értéket veszi figyelembe.
Ha viszont <code class="language-plaintext highlighter-rouge">timestamp with time zone</code>, akkor figyelembe veszi a session alapján, és UTC-re konvertálja.
Ráadásul a session időzónája a JVM időzónájával egyezik meg a PostgreSQL JDBC drivernél.</li>
</ul>
<p>Nézzük meg, hogy mitől függhet az időzónák kezelése a repository rétegben:</p>
<ul>
<li>Entitásban lévő attribútum típusa: <code class="language-plaintext highlighter-rouge">LocalDateTime</code> vagy <code class="language-plaintext highlighter-rouge">ZonedDateTime</code>.</li>
<li>Adatbázisban milyen típust használunk: <code class="language-plaintext highlighter-rouge">timestamp</code> vagy <code class="language-plaintext highlighter-rouge">timestamp with time zone</code></li>
<li>Mi a JVM időzónája</li>
<li>A Hibernate kap-e <code class="language-plaintext highlighter-rouge">spring.jpa.properties.hibernate.jdbc.time_zone</code> beállítást</li>
</ul>
<h2 id="tesztesetek">Tesztesetek</h2>
<p>Nézzük meg sorban a különböző kombinációkat!</p>
<p>Ha az attribútum típusa <code class="language-plaintext highlighter-rouge">LocalDateTime</code>, akkor a Hibernate adatbázisban automatikusan <code class="language-plaintext highlighter-rouge">timestamp</code> típussal hozza létre
a hozzá tartozó oszlopot. A JVM időzónája az alapértelmezett <code class="language-plaintext highlighter-rouge">Europe/Budapest</code>, és nincs Hibernate beállítás (<strong>1. eset</strong>).</p>
<p>Ekkor az adatbázisba a beküldött <code class="language-plaintext highlighter-rouge">10:00</code> érték került. Az első példánál nézzük végig a teljes folyamatot.
A JSON-ban beküldött <code class="language-plaintext highlighter-rouge">10:00</code> érték <code class="language-plaintext highlighter-rouge">10:00</code> <code class="language-plaintext highlighter-rouge">LocalDateTime</code> lett. Mivel a JVM időzónája <code class="language-plaintext highlighter-rouge">Europe/Budapest</code>,
ezt egy <code class="language-plaintext highlighter-rouge">8:00[UTC]</code> <code class="language-plaintext highlighter-rouge">Timestamp</code> értékké konvertálja a Hibernate. (Ha konzolra kiíratjuk, <code class="language-plaintext highlighter-rouge">10:00</code> jelenik meg, mert
a <code class="language-plaintext highlighter-rouge">toString()</code> figyelembe veszi a JVM időzónáját.) Mivel nincs megadva Hibernate paraméter, ezért a JDBC Drivernek
nem kerül <code class="language-plaintext highlighter-rouge">Calendar</code> átadásra, ezért ezt az értéket JVM default <code class="language-plaintext highlighter-rouge">Europe/Budapest</code> időzónájára konvertálja, ami <code class="language-plaintext highlighter-rouge">10:00+02</code>.
Mivel az adatbázisban a típus <code class="language-plaintext highlighter-rouge">timestamp</code>, a végét levágja, azaz <code class="language-plaintext highlighter-rouge">10:00</code> értékként tárolja el.</p>
<p>Majd az <code class="language-plaintext highlighter-rouge">application.properties</code> állományban a <code class="language-plaintext highlighter-rouge">spring.jpa.properties.hibernate.jdbc.time_zone</code> értékét <code class="language-plaintext highlighter-rouge">UTC</code>-re állítottam (<strong>2. eset</strong>).
Ekkor az adatbázisba a <code class="language-plaintext highlighter-rouge">8:00</code> érték került. Ez azért történt, mert a <code class="language-plaintext highlighter-rouge">8:00[UTC]</code> <code class="language-plaintext highlighter-rouge">Timestamp</code> <code class="language-plaintext highlighter-rouge">String</code>-gé
konvertáláskor egy <code class="language-plaintext highlighter-rouge">UTC</code> Calendar át lett adva, ezért a <code class="language-plaintext highlighter-rouge">String</code> <code class="language-plaintext highlighter-rouge">8:00+00</code> lett.</p>
<p>Ha a <code class="language-plaintext highlighter-rouge">-Duser.timezone</code> parancssori paramétert <code class="language-plaintext highlighter-rouge">UTC</code>-re állítottam, akkor <code class="language-plaintext highlighter-rouge">10:00</code> került az adatbázisba (<strong>3. eset</strong>).
A <code class="language-plaintext highlighter-rouge">10:00</code>-át ekkor <code class="language-plaintext highlighter-rouge">UTC</code>-ben értelmezte, ezért a <code class="language-plaintext highlighter-rouge">Timestamp</code> <code class="language-plaintext highlighter-rouge">10:00[UTC]</code> lett, amit <code class="language-plaintext highlighter-rouge">10:00+00</code> <code class="language-plaintext highlighter-rouge">String</code>-gé alakított át,
hiszen nem kellett konverzió, mert a JVM időzóna <code class="language-plaintext highlighter-rouge">UTC</code>.</p>
<p>A következő kísérlet az volt, hogy átállítottam az adatbázisban a mező típusát <code class="language-plaintext highlighter-rouge">timestamp with time zone</code>
értékre. Ez úgy a legegyszerűbb, hogy a JPA sémagenerálását annotációval konfiguráltam. A <code class="language-plaintext highlighter-rouge">validFrom</code> mezőre
a <code class="language-plaintext highlighter-rouge">@Column(name = "valid_from", columnDefinition = "timestamp with time zone")</code> JPA annotációt tettem (<strong>4. eset</strong>).
Az adatbázisba került érték ekkor a <code class="language-plaintext highlighter-rouge">8:00[UTC]</code> lett. Így a JDBC <code class="language-plaintext highlighter-rouge">String</code> <code class="language-plaintext highlighter-rouge">10:00+02</code>, hiszen CEST-re kellett átváltani,
de ezt megfelelően UTC-re konvertálta az adatbázis.</p>
<p>Ekkor hiába állítgattam a JPA beállítást (<code class="language-plaintext highlighter-rouge">spring.jpa.properties.hibernate.jdbc.time_zone</code>),
nem volt hatással a működésre. Hiszen itt mindig küldött offsettet, bármilyen cél időzónába is kellett átváltani,
és azt megfelelően kezelte le az adatbázis.</p>
<p>Ha viszont beállítottam a <code class="language-plaintext highlighter-rouge">-Duser.timezone=UTC</code> JVM paramétert, akkor <code class="language-plaintext highlighter-rouge">10:00[UTC]</code> érték került be.
Ez logikus, hiszen már a entitásnál <code class="language-plaintext highlighter-rouge">10:00</code> volt <code class="language-plaintext highlighter-rouge">UTC</code>-ben értelmezve, így a <code class="language-plaintext highlighter-rouge">Timestamp</code> is ez lett (<strong>5. eset</strong>).</p>
<p>A következő kísérlet, mikor nem használok semmilyen konfigurációt, de a típust átállítottam
<code class="language-plaintext highlighter-rouge">ZonedDateTime</code> típusra. Ekkor a sémageneráláskor még mindig egyszerű <code class="language-plaintext highlighter-rouge">timestamp</code> típussal hozza létre a mezőt (<strong>6. eset</strong>).</p>
<p>Innentől kezdve mindig jó érték szerepelt az entitásban, és a <code class="language-plaintext highlighter-rouge">Timestamp</code> is mindig <code class="language-plaintext highlighter-rouge">8:00[UTC]</code>.
Az adatbázisba
a <code class="language-plaintext highlighter-rouge">10:00</code> került. Ekkor a JVM időzónájába váltotta, azonban a <code class="language-plaintext highlighter-rouge">+02:00</code>-t levágta az adatbázis.</p>
<p>A <code class="language-plaintext highlighter-rouge">8:00</code> érték kerül be az adatbázisba, ha a <code class="language-plaintext highlighter-rouge">spring.jpa.properties.hibernate.jdbc.time_zone</code>
konfigurációs paraméter értékét állítjuk <code class="language-plaintext highlighter-rouge">UTC</code>-re (<strong>7. eset</strong>). Hiszen ekkor <code class="language-plaintext highlighter-rouge">UTC</code>-re konvertálva
<code class="language-plaintext highlighter-rouge">08:00+00</code> lesz a <code class="language-plaintext highlighter-rouge">String</code>.</p>
<p>Majd a JVM időzónáját
átállítottam UTC-re a <code class="language-plaintext highlighter-rouge">-Duser.timezone=UTC</code> paraméterrel. Ekkor a <code class="language-plaintext highlighter-rouge">String</code> <code class="language-plaintext highlighter-rouge">8:00</code> és az adatbázisba a jó <code class="language-plaintext highlighter-rouge">8:00</code> érték
kerül bele (<strong>8. eset</strong>).</p>
<p>Végül pedig Java oldalon maradt a <code class="language-plaintext highlighter-rouge">ZonedDateTime</code>, és adatbázis oldalon a <code class="language-plaintext highlighter-rouge">timestamp with time zone</code>. Ekkor is
helyesen került az adatbázisba (<strong>9. eset</strong>). A működésen ekkor sem a Hibernate konfigurációja, sem a JVM időzónája nem változtatott.
Ez azért van, mert a <code class="language-plaintext highlighter-rouge">String</code>-be mindig bekerült az offset (mikor melyik), és ezt a <code class="language-plaintext highlighter-rouge">timestamp with time zone</code> miatt a
PostgreSQL mindig figyelembe is vette.</p>
<p><a href="/artifacts/posts/2020-07-22-idozonak/tesztesetek.png" data-lightbox="post-images"><img src="/artifacts/posts/2020-07-22-idozonak/tesztesetek_750.png" alt="Tesztesetek" /></a></p>
<h2 id="összefoglalás">Összefoglalás</h2>
<ul>
<li>Ha <code class="language-plaintext highlighter-rouge">ZonedDateTime</code> típust és <code class="language-plaintext highlighter-rouge">timestamp with time zone</code> beállítást használunk, akkor minden helyesen fog
működni, ugyanis végig tárolva és feldolgozva lesz az időzóna.</li>
<li>Ha <code class="language-plaintext highlighter-rouge">LocalDateTime</code> típust használunk, ott nincs időzóna tárolva, azaz bejátszhat a Jackson konfiguráció és a JVM
időzónája is.</li>
<li>Ha <code class="language-plaintext highlighter-rouge">timestamp</code> típust használunk, akkor vigyázni kell, hogy legyen beállítva a Hibernate konfiguráció,
vagy legyen a JVM megfelelő időzónában.</li>
</ul>
Óda az integrációs tesztekhez2020-03-22T09:00:00+00:00http://www.jtechlog.hu/2020/03/22/oda-az-integracios-tesztekhez<p>Megrendezésre került 2019. október 17-én a <a href="https://training360.com/">Training360</a>
<em>Nézz be a hype mögé</em> fejlesztői meetupja. Ezen az <em>Integrációs tesztek nehézségei (Javaban)</em>
címmel tartottam előadást, bár inkább az integrációs tesztek pozitívumait
taglaltam.</p>
<p>Figyelem! A következő poszt nyugalom megzavarására alkalmas elemeket tartalmaz.
Célom annak a hangsúlyozása, hogy olyan alapvető állításokat, tételeket is
néha meg kell kérdőjeleznünk, mint a tesztpiramis. Ezért a posztban
találkozhattok némi hangsúly áthelyezéssel, kéretik ezt a helyén kezelni.</p>
<p><img src="/artifacts/posts/2020-03-22-oda-az-integracios-tesztekhez/2019-meetup-photo_750.jpg" alt="Fotó a meetupról" /></p>
<p>A rendezvényre készült <a href="/artifacts/2019-10-meetup/meetup-2019-10.html">diák elérhetőek itt</a>.</p>
<p>A posztban végigveszem a tesztpiramist, és az ezzel kapcsolatos fogalmakat,
sőt fenntartásaimat is. Majd megvizsgálok egy alternatív megközelítést,
mely különösen alkalmazható microservice-ekre. Közben példákat is hozok egy egyszerű
Spring Boot alkalmazás tesztelésére. A példa projekt elérhető a <a href="https://github.com/vicziani/jtechlog-cities">GitHubon</a>.</p>
<!-- more -->
<h2 id="tesztpiramis">Tesztpiramis</h2>
<p>A tesztpiramist Mike Cohn mutatta be a <em>Succeeding with Agile</em> könyvében,
annak elképzelésére, hogyan helyezzük el a különböző szintjeit a tesztelésnek.</p>
<p>A legalsó szinten vannak a unit tesztek, melyek az adott programozási nyelv
legkisebb egységét tesztelik, objektumorientált nyelvek esetén ez az osztály
szint. Középső szinten az integrációs tesztek helyezkednek el, melyek már
az osztályok együttműködését tesztelik. Végül a legfelsőbb szint az
End-to-end tesztek, melyekkel a teljes alkalmazást teszteljük,
az adott környezetben, azok függőségeivel integrálva. Ráadásul nem egy-egy kiragadott
funkció darabkát, hanem teljes üzleti folyamatot az elejétől a végéig.</p>
<p><img src="/artifacts/posts/2020-03-22-oda-az-integracios-tesztekhez/pyramid.png" alt="Tesztpiramis" /></p>
<p>A tesztpiramis formája abból következik, hogy az alaptól felfelé
a tesztek egyre nagyobb hatókörrel dolgoznak, egyre erőforrásigényesebb
a karbantartásuk és futtatásuk, és pont ezért felfelé mozdulva érdemes
ezekből egyre kevesebbet írni.</p>
<p>Sajnos már ez is több kérdést felvet bennem. Egyrészt a fogalmak nem egyértelműen
definiáltak, mindenki mást ért alatta. Már az sem teljesen egyértelmű, hogy egy
alkalmazás részeit hogyan nevezzük. A legalsó szinten vannak az osztályok, ebben
megegyezthetünk, azonban az egyel magasabb szinten mik helyezkednek el?
Nevezik ezeket moduloknak (pl. Java Application Architecture könyv, OSGi),
komponenseknek (pl. a Clean Architecture könyv, ami nagyon szembe megy pl. a
Spring Framework/Java EE elnevezésével, ahol egy komponens egy bean), plugineknek, stb.
Már az alkalmazásra is különböző neveket szoktak használni, mint rendszer,
service, stb. A Clean Architecture könyv és a microservices architektúra service-nek
hívja az alkalmazást és ez számomra
azért zavaró, mert a Spring Framework is így hívja a háromrétegű architektúrában az üzleti
logika rétegben elhelyezkedő beaneket. Én az osztály (és igen, ide kell
érteni ebben az esetben az interfészeket, enumokat, annotációkat, stb.),
modul, alkalmazás neveket fogom használni.</p>
<p>A unit tesztelésnél egyértelmű, hogy a külső függőségeket ki kell mockolni.
Igen, de egy osztály a Java SE osztálykönyvtár rengeteg elemét használhatja, mint
pl. a <code class="language-plaintext highlighter-rouge">String</code>, <code class="language-plaintext highlighter-rouge">List</code>, stb. Ezek külső függőségek? Nyilván nem, ezért mondhatjuk,
hogy ezeket ne mockoljuk. Mi van ez esetben az olyan külső könyvtárakkal, melyek
hasonló adatszerkezeteket implementálnak, mint pl. a Guava vagy az Apache Commons
Collections? És mi a helyzet a hasonló saját osztályainkkal, value objectjeinkkel?
Mi van az olyan külső függőségekkel, mint pl. a naplózáshoz az SLF4J?
Unit tesztnek nevezhető-e az, ha beindul egy konténer, pl. a Spring Framework,
vagy annak egy része?
(A Spring Framework unit tesztnek nevezi azt, ha egy komponenst tesztelsz,
de beindít bizonyos Springes eszközöket, egy kisebb konténert.)
Hol húzható meg a határ?</p>
<p>Az integrációs teszt esetén talán kevesebb a kérdés, hiszen gyakorlatilag minden
tesztre, melyben egynél több osztály szerepel, ráhúzhatjuk az integrációs teszt
jelzőt. Az elnevezésben egy kis kavar, hogy integrációs tesztnek szokták nevezni
azokat a teszteket is, ahol több alkalmazást integrálunk, és azok együttműködését
vizsgáljuk.</p>
<p>Az E2E tesztekkel kapcsolatban szintén elég sok kérdés merül fel. Csak felületi
teszteket foglal magában? Vagy ide sorolhatóak az API tesztek, amikor az alkalmás
valamely más interfészét, pl. REST webszolgáltatását szólítjuk meg. E2E tesztnek
nevezhető-e az, ha csak egy részfunkciót tesztelünk a felületen keresztül?
Teljes izolációban teszteljük másik alkalmazásoktól, vagy az E2E pont azt jelenti,
hogy integráljuk más alkalmazásokhoz?</p>
<p>Ezen kívül ilyen fogalmak is felbukkannak, hogy szolgáltatás teszt (service test),
komponens teszt (component test), rendszerteszt (system test), ezek vajon mit jelentenek?</p>
<p>Azt hiszem, hogy ebből már érthető, hogy az alapvető problémám ezzel a területtel
kapcsolatban az, hogy nincsen jó, kellően egzakt terminológia, ugyanazon fogalmak alatt
mások mást értenek. Ráadásul a microservices architektúra elterjedése egy kicsit
még jobban összezavarta ezt, és az amúgy sem kialakult terminológia nem tudott
alkalmazkodni az új módszerekhez.</p>
<p>Az automata tesztelés, és az ehhez tartozó eszközök (test harness) annyira alapvető fontosságúak, hogy az architektúra
részét kell képezniük, és így meg is tervezendő. Az összes elterjedt architektúra így említi, mint pl.
a hexagonal architecture, onion architecture és clean architecture. Azonban még mindig úgy látom,
hogy a tesztelést sokan teljesen függetlenül kezelik, sok helyen külön csapat foglalkozik vele,
akik ráadásul támogatást sem kapnak munkájuk elvégzéséhez.</p>
<h2 id="kételyek-a-unit-teszteléssel-kapcsolatban">Kételyek a unit teszteléssel kapcsolatban</h2>
<p>A példákban egy olyan alkalmazást fogok mutatni (ami egy microservice-ként is megállja a helyét), mely egy háromrétegű Spring Boot
alkalmazás, mely városok adatait tartja nyilván. Nem implementáltam a JavaScript frontendet, REST API-n elérhető. Alatta H2
adatbázis. Egy városnak ismeri a koordinátáját. Haversine algoritmust használva kiszámolja és visszaadja annak távolságát
Budapesttől. Valamint visszaadja a városban mért hőmérsékletet is, ehhez egy külső szolgáltatást vesz igénybe (Időkép).</p>
<p><img src="/artifacts/posts/2020-03-22-oda-az-integracios-tesztekhez/cities-architecture.png" alt="Tesztpiramis" /></p>
<p>A unit tesztelés ígéreteit azt hiszem mindannyian ismerjük. A további tárgyaláshoz azonban érdemes még
meg ismerni a unit tesztelés két megközelítését:</p>
<ul>
<li>Állapot alapú: a megfelelő bemenetre az elvárt kimenetet kapjuk eredményül</li>
<li>Viselkedés alapú: a megfelelő osztályokkal a megfelelő módon működött együtt: a mockolt függőségeken megnézzük, hogy megfelelően kerültek-e meghívásra</li>
</ul>
<p>Azonban manapság kezdenek megjelenni kritikák is a unit teszteléssel kapcsolatban. Az első és legfontosabb, hogy
amennyiben azt a modellt követjük, hogy minden osztályhoz külön teszt osztályt hozunk létre, és minden egyes
publikus metódushoz legalább egy teszt metódust, a tesztjeink finoman granuláltak lesznek, és amennyiben egy nagyobb
refactoringot szeretnénk elvégezni, akkor az nagyon sok tesztesetet fog érinteni, ami a Fragile Test Problem.
Valójában ezzel a módszerrel implementációs részleteket (implementation details) tesztelünk.</p>
<p>Nézzük a következő controller osztályt, amin nem teljesen egyértelmű a unit teszt hasznossága.</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@RestController</span>
<span class="nd">@RequestMapping</span><span class="o">(</span><span class="s">"/api/cities"</span><span class="o">)</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">CityController</span> <span class="o">{</span>
<span class="kd">private</span> <span class="nc">CityService</span> <span class="n">cityService</span><span class="o">;</span>
<span class="kd">public</span> <span class="nf">CityController</span><span class="o">(</span><span class="nc">CityService</span> <span class="n">cityService</span><span class="o">)</span> <span class="o">{</span>
<span class="k">this</span><span class="o">.</span><span class="na">cityService</span> <span class="o">=</span> <span class="n">cityService</span><span class="o">;</span>
<span class="o">}</span>
<span class="nd">@GetMapping</span><span class="o">(</span><span class="s">"/{city}"</span><span class="o">)</span>
<span class="kd">public</span> <span class="nc">CityDetails</span> <span class="nf">getCity</span><span class="o">(</span><span class="nd">@PathVariable</span> <span class="nc">String</span> <span class="n">city</span><span class="o">)</span> <span class="o">{</span>
<span class="k">return</span> <span class="n">cityService</span><span class="o">.</span><span class="na">getCityDetails</span><span class="o">(</span><span class="n">city</span><span class="o">);</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>Mivel van egy service függősége, azt mockkal kell helyettesíteni. Amit tesztelhetünk, hogy amit
a service visszaad, azt megfelelően vissza adja-e (állapot), valamint megfelelő paraméterrel továbbhív-e a
service-be (viselkedés). Azonban én mindkettőt feleslegesnek tartom, hiszen azt teszteli, hogy meg tudok-e hívni egy metódust.
Amit itt érdemes tesztelni pl. hogy az annotációk megfelelően vannak-e elhelyezve, jó url-en érhető-e el, a paraméterek jól
kerülnek-e beolvasásra, <code class="language-plaintext highlighter-rouge">CityDetails</code> megfelelően kerül-e JSON-ba leszerializálásra, jó-e a HTTP státuszkód, stb.</p>
<p>Erre van a Spring Bootban lehetőség a <code class="language-plaintext highlighter-rouge">@WebMvcTest</code> használatával, ami csak a controller réteget indítja el,
és a service réteget mockolni kell, és unit tesztnek is hívja, hiszen egy controllert tesztel, azonban
elindítja a Springet. Ezért ez nekem inkább az integrációs rétegbe tartozik.</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@WebMvcTest</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">CityControllerIT</span> <span class="o">{</span>
<span class="nd">@Autowired</span>
<span class="nc">CityController</span> <span class="n">cityController</span><span class="o">;</span>
<span class="nd">@MockBean</span>
<span class="nc">CityService</span> <span class="n">cityService</span><span class="o">;</span>
<span class="nd">@Autowired</span>
<span class="nc">MockMvc</span> <span class="n">mockMvc</span><span class="o">;</span>
<span class="nd">@Test</span>
<span class="kt">void</span> <span class="nf">testGetCity</span><span class="o">()</span> <span class="kd">throws</span> <span class="nc">Exception</span> <span class="o">{</span>
<span class="n">when</span><span class="o">(</span><span class="n">cityService</span><span class="o">.</span><span class="na">getCityDetails</span><span class="o">(</span><span class="n">any</span><span class="o">())).</span><span class="na">thenReturn</span><span class="o">(</span>
<span class="k">new</span> <span class="nf">CityDetails</span><span class="o">(</span><span class="s">"Debrecen"</span><span class="o">,</span><span class="mf">47.52883333</span><span class="o">,</span><span class="mf">21.63716667</span><span class="o">,</span> <span class="mi">10</span><span class="o">,</span> <span class="nc">Optional</span><span class="o">.</span><span class="na">of</span><span class="o">(</span><span class="s">"8°C"</span><span class="o">)));</span>
<span class="n">mockMvc</span><span class="o">.</span><span class="na">perform</span><span class="o">(</span><span class="n">get</span><span class="o">(</span><span class="s">"/api/cities/Debrecen"</span><span class="o">))</span>
<span class="o">.</span><span class="na">andExpect</span><span class="o">(</span><span class="n">status</span><span class="o">().</span><span class="na">isOk</span><span class="o">())</span>
<span class="o">.</span><span class="na">andDo</span><span class="o">(</span><span class="n">print</span><span class="o">())</span>
<span class="o">.</span><span class="na">andExpect</span><span class="o">(</span><span class="n">jsonPath</span><span class="o">(</span><span class="s">"$.temperature"</span><span class="o">,</span> <span class="n">equalTo</span><span class="o">(</span><span class="s">"8°C"</span><span class="o">)));</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>Nézzük meg az üzleti logika réteget, a service-t. Itt már bonyolultabb a helyzet.</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="nc">CityDetails</span> <span class="nf">getCityDetails</span><span class="o">(</span><span class="nc">String</span> <span class="n">nameOfTheCity</span><span class="o">)</span> <span class="o">{</span>
<span class="kt">var</span> <span class="n">mayBeCity</span> <span class="o">=</span> <span class="n">cityRepository</span><span class="o">.</span><span class="na">findByName</span><span class="o">(</span><span class="n">nameOfTheCity</span><span class="o">);</span>
<span class="k">if</span> <span class="o">(</span><span class="n">mayBeCity</span><span class="o">.</span><span class="na">isEmpty</span><span class="o">())</span> <span class="o">{</span>
<span class="k">throw</span> <span class="k">new</span> <span class="nf">CityNotFoundException</span><span class="o">(</span><span class="s">"City not found with name: "</span> <span class="o">+</span> <span class="n">nameOfTheCity</span><span class="o">);</span>
<span class="o">}</span>
<span class="kt">var</span> <span class="n">city</span> <span class="o">=</span> <span class="n">mayBeCity</span><span class="o">.</span><span class="na">get</span><span class="o">();</span>
<span class="kt">var</span> <span class="n">distance</span> <span class="o">=</span> <span class="n">calculateDistance</span><span class="o">(</span><span class="n">city</span><span class="o">);</span>
<span class="kt">var</span> <span class="n">temperature</span> <span class="o">=</span> <span class="n">getTemperature</span><span class="o">(</span><span class="n">nameOfTheCity</span><span class="o">);</span>
<span class="k">return</span> <span class="k">new</span> <span class="nf">CityDetails</span><span class="o">(</span><span class="n">city</span><span class="o">.</span><span class="na">getName</span><span class="o">(),</span> <span class="n">city</span><span class="o">.</span><span class="na">getLat</span><span class="o">(),</span> <span class="n">city</span><span class="o">.</span><span class="na">getLon</span><span class="o">(),</span> <span class="n">distance</span><span class="o">,</span> <span class="n">temperature</span><span class="o">);</span>
<span class="o">}</span>
</code></pre></div></div>
<p>Ami elsőnek szemet szúr, hogy egyrészt van benne elágazás, másrészt több forrásból gyűjti be
az adatokat. Egyrészt az adatbázisból betölti a város koordinátáit, valamint egy másik
service, a <code class="language-plaintext highlighter-rouge">HaversineCalculator</code> segítségével kiszámolja a távolságot egy másik várostól,
harmadrészt beszerzi a hőmérsékletet a <code class="language-plaintext highlighter-rouge">TemperatureGateway</code> segítségével. Ekkor már magyarázható
a unit teszt szükségessége.</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">void</span> <span class="nf">testGetCityDetails</span><span class="o">()</span> <span class="o">{</span>
<span class="n">when</span><span class="o">(</span><span class="n">cityRepository</span><span class="o">.</span><span class="na">findByName</span><span class="o">(</span><span class="n">eq</span><span class="o">(</span><span class="s">"Budapest"</span><span class="o">))).</span><span class="na">thenReturn</span><span class="o">(</span><span class="nc">Optional</span><span class="o">.</span><span class="na">of</span><span class="o">(</span><span class="k">new</span> <span class="nc">City</span><span class="o">(</span><span class="mi">1L</span><span class="o">,</span> <span class="s">"Budapest"</span><span class="o">,</span> <span class="mf">47.4825</span><span class="o">,</span><span class="mf">19.15933333</span><span class="o">)));</span>
<span class="n">when</span><span class="o">(</span><span class="n">cityRepository</span><span class="o">.</span><span class="na">findByName</span><span class="o">(</span><span class="n">eq</span><span class="o">(</span><span class="s">"Debrecen"</span><span class="o">))).</span><span class="na">thenReturn</span><span class="o">(</span><span class="nc">Optional</span><span class="o">.</span><span class="na">of</span><span class="o">(</span><span class="k">new</span> <span class="nc">City</span><span class="o">(</span><span class="mi">1L</span><span class="o">,</span> <span class="s">"Debrecen"</span><span class="o">,</span><span class="mf">47.52883333</span><span class="o">,</span><span class="mf">21.63716667</span><span class="o">)));</span>
<span class="n">when</span><span class="o">(</span><span class="n">haversineCalculator</span><span class="o">.</span><span class="na">calculateDistance</span><span class="o">(</span><span class="n">anyDouble</span><span class="o">(),</span> <span class="n">anyDouble</span><span class="o">(),</span> <span class="n">anyDouble</span><span class="o">(),</span> <span class="n">anyDouble</span><span class="o">())).</span><span class="na">thenReturn</span><span class="o">(</span><span class="mf">10.0</span><span class="o">);</span>
<span class="n">when</span><span class="o">(</span><span class="n">temperatureGateway</span><span class="o">.</span><span class="na">getTemperature</span><span class="o">(</span><span class="n">anyString</span><span class="o">())).</span><span class="na">thenReturn</span><span class="o">(</span><span class="s">"8°C"</span><span class="o">);</span>
<span class="kt">var</span> <span class="n">cityDetails</span> <span class="o">=</span> <span class="n">cityService</span><span class="o">.</span><span class="na">getCityDetails</span><span class="o">(</span><span class="s">"Debrecen"</span><span class="o">);</span>
<span class="n">assertAll</span><span class="o">(</span>
<span class="o">()</span> <span class="o">-></span> <span class="n">assertEquals</span><span class="o">(</span><span class="s">"Debrecen"</span><span class="o">,</span> <span class="n">cityDetails</span><span class="o">.</span><span class="na">getName</span><span class="o">()),</span>
<span class="o">()</span> <span class="o">-></span> <span class="n">assertEquals</span><span class="o">(</span><span class="mf">47.52883333</span><span class="o">,</span> <span class="n">cityDetails</span><span class="o">.</span><span class="na">getLat</span><span class="o">()),</span>
<span class="o">()</span> <span class="o">-></span> <span class="n">assertEquals</span><span class="o">(</span><span class="mf">21.63716667</span><span class="o">,</span> <span class="n">cityDetails</span><span class="o">.</span><span class="na">getLon</span><span class="o">()),</span>
<span class="o">()</span> <span class="o">-></span> <span class="n">assertEquals</span><span class="o">(</span><span class="mf">10.0</span><span class="o">,</span> <span class="n">cityDetails</span><span class="o">.</span><span class="na">getDistance</span><span class="o">()),</span>
<span class="o">()</span> <span class="o">-></span> <span class="n">assertEquals</span><span class="o">(</span><span class="nc">Optional</span><span class="o">.</span><span class="na">of</span><span class="o">(</span><span class="s">"8°C"</span><span class="o">),</span> <span class="n">cityDetails</span><span class="o">.</span><span class="na">getTemperature</span><span class="o">())</span>
<span class="o">);</span>
<span class="o">}</span>
</code></pre></div></div>
<p>És itt már érdemes olyan esetekre is unit tesztet írni, mint:</p>
<ul>
<li>Mi van, ha nem található az adott város</li>
<li>Mi van, ha nem található az a város, melytől a távolságot mérjük</li>
<li>Mi van, ha a külső szolgáltatás hívása kivételt dob</li>
</ul>
<p>Ezek a példában megtalálhatóak.</p>
<p>A perzisztens réteg unit tesztelésével kapcsolatban is vannak kérdések.
A legtöbb esetben ezek egyszerű hívások a JDBC megfelelő objektumai (<code class="language-plaintext highlighter-rouge">DataSource</code>, <code class="language-plaintext highlighter-rouge">Connection</code>, stb.), a
<code class="language-plaintext highlighter-rouge">JdbcTemplate</code> vagy <code class="language-plaintext highlighter-rouge">EntityManager</code> felé. Vannak fenntartásaim azzal kapcsolatban, hogy érdemes-e
ezeket mockolni. A Spring Data JPA esetén csak az interfészt kell megírni, és azt a keretrendszer maga implementálja,
ezért érdekes, hogyan lehet ezeket unit tesztelni. Itt JPA estén megint képbe jönnek az annotációk,
valamint a lekérdezések, melyeket jó lenne tesztelni, azonban unit teszttel nem lehet.</p>
<p>A Spring Bootnak erre is van megoldása a <code class="language-plaintext highlighter-rouge">@DataJpaTest</code> annotációval, szintén unit tesztnek hívja, egy repository tesztelése a célja,
de azon kívül, hogy elindítja a Springet, még egy beépített adatbázist is elindít (pl. H2).
Ezért nekem ez szintén az integrációs rétegbe tartozik.</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@DataJpaTest</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">CityRepositoryIT</span> <span class="o">{</span>
<span class="nd">@Autowired</span>
<span class="nc">CityRepository</span> <span class="n">cityRepository</span><span class="o">;</span>
<span class="nd">@Test</span>
<span class="kt">void</span> <span class="nf">test_findByName</span><span class="o">()</span> <span class="o">{</span>
<span class="kt">var</span> <span class="n">city</span> <span class="o">=</span> <span class="n">cityRepository</span><span class="o">.</span><span class="na">findByName</span><span class="o">(</span><span class="s">"Budapest"</span><span class="o">);</span>
<span class="n">assertEquals</span><span class="o">(</span><span class="mf">47.4825</span><span class="o">,</span> <span class="n">city</span><span class="o">.</span><span class="na">get</span><span class="o">().</span><span class="na">getLat</span><span class="o">());</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>A más rendszerekkel való kapcsolattartásért felelős, ún. gateway osztályok tesztelése megint
kérdéses. Itt protokolltól függően biztos valamilyen 3rd party library-t használunk,
anélkül tesztelni nem feltétlen érdemes.</p>
<p>Nézzük az adott példán, hogy az Időkép meghívása <a href="https://jsoup.org/">jsouppal</a>
történik. Ez egyrészt egy 3rd party library, valamint egy http kapcsolatot épít fel,
valamint a visszaadott adatszerkezetet konvertálja saját szerkezetbe.</p>
<p>Ebből az első kettő tesztelése mindenképp integrációs tesztelés körébe tartozik.</p>
<p>A külső alkalmazás, amelyhez kapcsolódunk, egyszerűen kimockolható,
erre több eszköz is létezik, mint pl. a <a href="http://wiremock.org/">WireMock</a> vagy
<a href="https://www.mock-server.com">MockServer</a>. Ezek különálló http szerverként
futtathatóak (persze mindkettőt integrálták a JUnithoz is), és megadhatóak,
hogy milyen kérésre milyen választ (pl. html, json, stb.) adjanak vissza. Így a teljes
http stack is meghajtásra kerül. Használatuk nem csak akkor hasznos, ha úgy fejlesztünk,
hogy a kapcsolódó alkalmazás nincs kész, esetleg nem elérhető a fejlesztés közben, hanem
a hibaágak is nagyon jól tesztelhetőek, pl. mi van akkor, ha a külső alkalmazás nem,
vagy csak lassan válaszol, hibás választ ad vissza, stb. Mindkettővel található teszteset
a példa alkalmazásban.</p>
<h2 id="kételyek-a-e2e-teszteléssel-kapcsolatban">Kételyek a E2E teszteléssel kapcsolatban</h2>
<p>Az E2E tesztelést a legtöbb kritika azért éri, mert a futtatásuk és karbantartásuk
erőforrás igényes. Emiatt a tesztek futtatásáról is viszonylag későn kapunk visszajelzést.
Ezért ezek számát tartsuk alacsonyan.</p>
<p>A Clean Architecture könyv úgy fogalmaz, hogy a GUI egy törékeny, gyakran változó
réteg, ezért lehetőleg a legkevésbé függjünk tőle. Sok felületi teszt esetén
megint csak belefuthatunk a Fragile Test Problem jelenségbe.</p>
<p>Amennyiben a E2E teszteket úgy értelmezzük, hogy a tesztek során az alkalmazás
más alkalmazáshoz is kapcsolódik, abban az esetben a kihívás még nagyobb. Ekkor ugyanis
a megfelelő verziójú, megfelelő állapotban lévő külső alkalmazásokat kell biztosítani,
ráadásul lehetőleg a minimális emberi erőforrás bevonásával. Képzeljük ezt el
akár több tíz microservice esetén (ami konténerizációs, és azt <em>orkesztráló</em>
technológia nélkül esélytelen). És akkor nem is beszéltünk arról, hogy hogyan lehet
ezen környezetben a különböző alkalmazásokból release-elni. És ez csak teszt környezet.</p>
<p>Az E2E tesztelés fontosságával kapcsolatban nincs kétség, azonban a mennyiségét
érdemes alacsonyan tartani. Mindenképp csak a fő üzleti funkcionálitás tesztelésére
javaslom, ami “pénzt termel”. Még egy irányt szeretnék itt megemlíteni. Akik rájöttek arra, hogy
mennyire nehéz, vagy költséges egy ilyen teszt környezet felállítani, ami ráadásul
az éles környezet hasonmása, kitalálták az élesben tesztelés fogalmát. Nyilván
ez csak bizonyos alkalmazások esetén vállalható. Előfeltétele, hogy profi monitorozás legyen,
és azonnal észre lehessen venni a hibákat, valamint hiba esetén azonnal, automatikus módon
vissza lehessen állni egy előző verzióra. Ismert fogalom itt a <em>Blue-Green deployment</em>, mely során
párhuzamosan él a régi és új verzió, és bármikor vissza lehet billenteni. Valamint a <em>Canary release</em>,
mikor az új verziót egyszerre állítják élesbe a felhasználók csak egy szűk körének.</p>
<h2 id="testing-honeycomb">Testing honeycomb</h2>
<p>A <a href="https://labs.spotify.com/2018/01/11/testing-of-microservices/">Spotify ajánlása</a> kifejezetten microservice-k esetén a testing honeycomb.
Ez azt jelenti, hogy az integrációs tesztekből írjunk a legtöbbet.</p>
<p>A Clean Architecture könyv is ezt javasolja, hogy ne annyira erőltessük a unit tesztek használatát,
hiszen azzal az implementációs részleteket teszteljük, és nehéz a karbantartásuk.</p>
<p><img src="/artifacts/posts/2020-03-22-oda-az-integracios-tesztekhez/honeycomb.png" alt="Testing honeycomb" /></p>
<p>(Nevét arról kapta, hogy alakja a méhkaptárban lévő hatszög alakú lépsejtekhez hasonlít.)</p>
<p>Az integrációs tesztek a következő előnyökkel rendelkeznek:</p>
<ul>
<li>Függetlenek az implementációs részletektől, ha az API-ra építünk, egy belső refaktor
nem fogja eltörni a teszteket.</li>
<li>Használatukkal ellenőrizhetőek a unit tesztekkel nem lefedhető részek, mint pl. a controller rétegben
a JSON szerializálás, URL mapping, vagy a repository rétegben az adatbázis integráció.</li>
<li>A külső rendszerek mockolásával a gateway réteg is tesztelhető. Azonban nem kell a
külső rendszereket is telepíteni, integrálni.</li>
<li>A legkisebb munkával a legnagyobb lefedettséget érjük el.</li>
<li>Gyorsabbak, mint az E2E tesztek.</li>
</ul>
<p>Persze az integrációs tesztek alkalmazásakor is rengeteg kérdés merül fel. Az alapkérdés, hogy
az osztályok mely körét teszteljük az integrációs teszttel. Ahogy említettem, lehet csak a controllert,
a repository-t, a gateway-t, de ha értelmes tesztet akarunk, már ezek is az integrációs tesztek közé tartoznak.</p>
<p>A következő lépés lehet, hogy a külső erőforrásokkal kapcsolatban lévő osztályokat mockoljuk.
Ilyen a példa alkalmazás esetén a <code class="language-plaintext highlighter-rouge">CityRepository</code>, mely adatbázishoz kapcsolódik, és a
<code class="language-plaintext highlighter-rouge">TemperatureGateway</code>, ami az Időképhez. A hozzá tartozó teszt a <code class="language-plaintext highlighter-rouge">InMemoryCityIT</code>, mely a <code class="language-plaintext highlighter-rouge">CityController</code> és <code class="language-plaintext highlighter-rouge">CityService</code>
osztályokat is meghajtja.</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@SpringBootTest</span>
<span class="nd">@EnableAutoConfiguration</span><span class="o">(</span><span class="n">exclude</span> <span class="o">=</span> <span class="o">{</span><span class="nc">DataSourceAutoConfiguration</span><span class="o">.</span><span class="na">class</span><span class="o">,</span>
<span class="nc">DataSourceTransactionManagerAutoConfiguration</span><span class="o">.</span><span class="na">class</span><span class="o">,</span>
<span class="nc">HibernateJpaAutoConfiguration</span><span class="o">.</span><span class="na">class</span><span class="o">})</span>
<span class="nd">@AutoConfigureMockMvc</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">InMemoryCityIT</span> <span class="o">{</span>
<span class="nd">@MockBean</span>
<span class="nc">CityRepository</span> <span class="n">cityRepository</span><span class="o">;</span>
<span class="nd">@MockBean</span>
<span class="nc">TemperatureGateway</span> <span class="n">temperatureGateway</span><span class="o">;</span>
<span class="nd">@Autowired</span>
<span class="nc">MockMvc</span> <span class="n">mockMvc</span><span class="o">;</span>
<span class="nd">@Test</span>
<span class="kt">void</span> <span class="nf">test_getCity</span><span class="o">()</span> <span class="kd">throws</span> <span class="nc">Exception</span> <span class="o">{</span>
<span class="n">when</span><span class="o">(</span><span class="n">cityRepository</span><span class="o">.</span><span class="na">findByName</span><span class="o">(</span><span class="n">anyString</span><span class="o">())).</span><span class="na">thenReturn</span><span class="o">(</span>
<span class="nc">Optional</span><span class="o">.</span><span class="na">of</span><span class="o">(</span><span class="k">new</span> <span class="nc">City</span><span class="o">(</span><span class="mi">1L</span><span class="o">,</span> <span class="s">"Debrecen"</span><span class="o">,</span><span class="mf">47.52883333</span><span class="o">,</span><span class="mf">21.63716667</span><span class="o">)));</span>
<span class="n">when</span><span class="o">(</span><span class="n">temperatureGateway</span><span class="o">.</span><span class="na">getTemperature</span><span class="o">(</span><span class="n">anyString</span><span class="o">())).</span><span class="na">thenReturn</span><span class="o">(</span><span class="s">"8°C"</span><span class="o">);</span>
<span class="n">mockMvc</span><span class="o">.</span><span class="na">perform</span><span class="o">(</span><span class="n">get</span><span class="o">(</span><span class="s">"/api/cities/Debrecen"</span><span class="o">))</span>
<span class="o">.</span><span class="na">andExpect</span><span class="o">(</span><span class="n">status</span><span class="o">().</span><span class="na">isOk</span><span class="o">())</span>
<span class="o">.</span><span class="na">andDo</span><span class="o">(</span><span class="n">print</span><span class="o">())</span>
<span class="o">.</span><span class="na">andExpect</span><span class="o">(</span><span class="n">jsonPath</span><span class="o">(</span><span class="s">"$.temperature"</span><span class="o">,</span> <span class="n">equalTo</span><span class="o">(</span><span class="s">"8°C"</span><span class="o">)));</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p><img src="/artifacts/posts/2020-03-22-oda-az-integracios-tesztekhez/teszteles-hatokore-mock.png" alt="Mockolt osztályok" /></p>
<p>A következő lépés, hogy az alkalmazást már a REST-assured 3rd party library-val hajtjuk meg,
az adatbázis egy beágyazott H2, és a <code class="language-plaintext highlighter-rouge">TemperatureGateway</code> egy WireMockkal megvalósított
beágyazott http szerverhez kapcsolódik.</p>
<p><img src="/artifacts/posts/2020-03-22-oda-az-integracios-tesztekhez/teszteles-hatokore-embed.png" alt="Mockolt osztályok" /></p>
<p>Amennyiben még jobban le akarjuk választani az alkalmazásunk a keretrendszerektől, külön indítsuk el
az alkalmazást, melyhez külön processzben futó REST-assured kapcsolódik, adatbázisa valós
adatbázis, és egy külön processzben futó WireMock szerverhez kapcsolódik a hőmérséklet adatokért.</p>
<p><img src="/artifacts/posts/2020-03-22-oda-az-integracios-tesztekhez/teszteles-hatokore-backing.png" alt="Mockolt osztályok" /></p>
<h2 id="összefoglalás">Összefoglalás</h2>
<p>A teszteléssel kapcsolatban nincsen pontos, kialakult terminológia, és nagyon kevés a jól bevált
recept is. Sokáig azt hittük, hogy a teszt piramissal tévedni nem nagyon lehet,
de ennek is megmutatkoztak a gyengeségei. Látszik, hogy az integrációs tesztek
bizonyos esetekben kezdenek átvenni szerepeket a unit tesztektől, és a gyors indulás valamint
a beágyazható eszközök miatt az E2E tesztektől is. A unit tesztek még mindig nagyon fontosak,
de ott használjuk őket, ahol tényleg értelme van, nem feltétlenül jó csak unit tesztekkel elérni a
90%-os lefedettséget.</p>
<p>A tesztelés nagyon fontos, kezeljük az architektúra részeként, és ugyanolyan alapossággal tervezzük is meg.
A bemutatott utak közül válasszuk azt, ami az alkalmazásunkhoz a legjobban illik, és
rendszeresen vizsgáljuk felül a döntésünket. Nem mindig az válik be nálunk is, ami másoknál,
és változtassunk, amennyiben úgy érezzük, hogy az automatizált tesztekbe fektetett energia nem térül meg.</p>
Clean Architecture2020-03-03T10:00:00+00:00http://www.jtechlog.hu/2020/03/03/clean-architecture<p>Robert C. Martin erőteljes hatást gyakorolt napjaink szoftverfejlesztésre.
Egyike volt az Agile Manifesto aláíróinak, és számos tervezési elv
kötődik a nevéhez. Ő alkotta meg a SOLID elvek rövidítést, mely
saját és mástól átvett elveket is tartalmaz (pl. a Liskov-féle helyettesítési
elvet Barbara Liskovtól). Ő a szerzője a közismert Clean Code
és a The Clean Coder könyveknek. (A Clean Code-ról egy
előző <a href="https://www.jtechlog.hu/2019/02/24/clean-code.html">posztban írtam</a>.) A 2017-ben megjelent
Clean Architecture könyvről szól ez a poszt, és a poszt végén
a saját véleményem is megosztom veletek.</p>
<p><img src="/artifacts/posts/2020-03-03-clean-architecture/clean-architecture-cover.jpg" alt="Clean Architecture könyv" /></p>
<p>A Clean Architecture a szerző által megfogalmazott architektúráról ír,
azonban részletesen bemutatja azokat az elveket is, melyek mentén eljutott a
javasolt architektúráig. Talán nem meglepő, hogy a SOLID elvekből indult ki,
és ezeket próbálja magasabb absztrakciós szinten is alkalmazni. Egy objektumorientált
alkalmazás legfinomabban szemcsézett darabkái az osztályok, több osztály
összetartozó egységét ő komponensnek (component) nevezi (klasszikus modul fogalom), és
a teljes alkalmazást szolgáltatásnak (service). Nála a komponens a
külön release-elhető, csomagolható és deploy-olható egység, Java esetén a JAR,
C# esetén pl. a DLL fájl. A könyv próbál programozási nyelv független maradni,
és főleg Java és C# példákat hoz.</p>
<p>Az architektúra hasonlít is a manapság elterjedt 3-rétegű architektúrához,
azonban jelentős különbségeket is tartalmaz. Azt gondolom, hogy az elveket
mindenképp érdemes megismerni, azt meg mindenki döntse el maga, hogy az
elvek alapján létrehozott architektúrát mennyire szeretné követni.</p>
<!-- more -->
<p>Már az architektúra definíciója is szimpatikus nekem, ugyanis szerinte azon
tervezési döntések, melyek célja a szoftvert a lehető legkevesebb emberi
erőforrásból létrehozni és karbantartani. Az architekt szerinte az
a <em>programozó</em>, aki a programozási feladatain felül a csapatát a megfelelő
tervezési döntésekkel a maximális hatékonyság felé tereli.</p>
<p>Szerinte az architektúrális tervezés egyik fő feladata a komponensek közötti határok meghúzása,
a komponensekre bontás.</p>
<p>Erre máris egy elég meredek megoldást javasol, a döntés elhalasztásának elvét.
Ugyanis ezen döntések közös tulajdonsága, hogy legtöbbször kevesebb információ
áll rendelkezésünkre, mint amennyi a döntéshez szükséges lenne. Így annak
elhalasztásával biztosíthatjuk, hogy még több információt tudunk addig begyűjteni.</p>
<p>Nyilván az objektumorientált tervezésből indul ki, és szerinte az oo egyik
legnagyobb újdonsága a polimorfizmus. Ugyanis ez teszi lehetővé a
dependency inversiont (SOLID elvek utolsó eleme). Vigyázzunk, ez nem azonos
a dependency injectionnel, amit sajnos a Wikipedia magyar
<a href="https://hu.wikipedia.org/wiki/F%C3%BCgg%C5%91s%C3%A9g_befecskendez%C3%A9s%C3%A9nek_elve">szócikke</a>
is összekever.</p>
<p>A dependency inversion architektúra szempontjából azért kiemelten fontos, ugyanis
használatával meg lehet fordítani a függőség irányát. Nézzük a következő
példát, melyet az ábra is reprezentál.</p>
<p><img src="/artifacts/posts/2020-03-03-clean-architecture/dependency-inversion.png" alt="Dependency Inversion" /></p>
<p>Klasszikus esetben az üzleti logika rétegben lévő osztály tartalmaz referenciát
a perzisztens rétegben lévő data access objectre (DAO). A függőség itt az üzleti
logika felől mutat a DAO felé. Azonban ha bevezetünk egy interfészt, amit az
üzleti logika rétegben helyezünk el, és azt implementálja a DAO, máris megfordul a
függőség, és a DAO felől az üzleti réteg felé fog mutatni. Ez az architektúra egyik
alapötlete is. Az is látható, hogy ebben az esetben a függőség a hívási lánc
irányával is ellentétes lesz.</p>
<p>Erős állítása az is, hogy a szoftverfejlesztés elején nem tervezhető meg
a komponensek és a közöttük lévő kapcsolatok. Ezért inkább úgy kell megtervezni,
hogy ez később dinamikusan módosítható legyen.</p>
<p>A Clean Architecture a szoftver alapvetően két fő részre osztja. A policy
alkotja az üzleti követelményekre adott válaszokat. Míg a details
adja meg a válaszokat a nem-funkcionális követelményekre, olyan részletek,
melyek ugyan szükségesek a program futásához, de a vele kapcsolatos döntéseket
érdemes későbbre halasztani. Ez összecseng azzal, hogy a policy adja az igazi
értéket, és először azzal érdemes foglalkozni (lásd még DDD). A details
körébe tartozik a IO, adatbázis, futtatókörnyezet, keretrendszerek és az API is
a más rendszerek számára.</p>
<p>A policy körébe tartoznak a kritikus üzleti szabályok (Critical Business Rules) és a használati esetek (use-case).
A kritikus üzleti szabályok az az üzleti logika, ami az üzlet alapját képezi, amelyet
informatikai rendszerek nélkül is alkalmaznának, akár kockás papíron. Ezek az üzleti
adatok és a rajta végzett műveletek. Ezeket a
szoftverben entitások (Entity) implementálják (nem keverendő össze az ORM entitás fogalmával!).
Elméleti szinten ezek akár több alkalmazásban is újrafelhasználhatóak.
A használati esetek a bemeneti adatokból (input vagy request), az entitásokkal
kommunikáló lépésekből és a kimeneti adatokból (output var response) állnak. Ezek valójában
az aktorok (szoftverek felhasználói) és a entitások interakcióját írják le. Ide már
az alkalmazásspecifikus üzleti szabályok tartoznak.</p>
<p>A policy-t, azaz az entity és use-case réteget úgy lenne célszerű implementálni, hogy teljes mértékben
keretrendszer, UI és adatbázisfüggetlen legyen. Lehetőleg tiszta objektumorientált
modell legyen (és ne anemic model), és a programozási nyelv beépített eszközeit használjuk csak
(azaz Java esetén tiszta Java SE kód). Itt a kódban klasszikus adatszerkezetek jelenhetnek meg,
mint list, set, map, stb. Nem jelenhet meg benne semmilyen keretrendszer, adatbáziskezelés, ORM. Ez adja az
alkalmazás magját. Ezt lehet UML-ben megtervezni. Ennek unit tesztelhetőnek kell lennie.</p>
<p>Egy külsőbb réteg az ún. Interface Adapters. Ez csatolja hozzá a konkrét komponenseket
az üzleti logikához, az adatbázist, UI-t, keretrendszereket és külső rendszereket (Frameworks and Drivers)
az üzleti logikához.</p>
<p>És amennyiben alkalmazkodunk ahhoz a szabályhoz, hogy a kevésbé stabil (gyakran változó) komponenseknek
kell függeniük a stabil komponensektől, valamint a konkrét komponenseknek kell
függenie a magasabb absztrakciós szinten lévő komponensektől, a details
komponensei függeni fognak a policy-től.</p>
<p><img src="/artifacts/posts/2020-03-03-clean-architecture/clean-architecture.jpg" alt="Clean Architecture" /></p>
<p>Ha plasztikusan akarjuk megfogalmazni, akkor a legfontosabbak az entitások, azon függnek a
használati esetek, és arról lógnak a details-ek. Az üzleti logika stabil, a details-ek
implementációs részletek, könnyebben módosíthatóak. És látható, hogy itt van a legnagyobb
ellentmondás a 3-rétegű architektúrával. Nem az üzleti logika épül az adatbázisrétegre,
hanem az üzleti logikától függ az adatbázis réteg. Természetesen dependency inversion
használatával lehet ezt a függőséget megfordítani. Az üzleti logika csak interfészeket
deklarál, amin keresztül a perzisztens réteggel kommunikál, és azt a perzisztens réteg
implementálja.</p>
<p>A 3-rétegű architektúrában kérdés szokott lenni, hogy hova tegyük a többi rendszerrel való
integrációt. A probléma főleg akkor van, amikor egyrészt a külső rendszernek
küldünk is adatot, de fogadunk is tőle. Küldéskor az üzleti logika hívja,
így adódik, hogy az üzleti logika függjön az integrációs rétegtől,
azaz legyen mondjuk a perzisztens réteggel egy szinten. De mi van akkor, ha
a másik rendszer meg hívja a mi rendszerünket, esetleg egy kétirányú,
sor alapú kapcsolat van. A Clean Architecture ezt is a külső körön helyezi el,
egységes modellben.</p>
<p>Ezen elvekből és architektúrából több érdekes, már-már meredek állítást is le lehet vezetni.</p>
<p>A teszteléssel kapcsolatosan két érdekes állítás is szerepel a könyvben. Egyrészt
nem célravezető úgy megírni a unit teszteket, hogy minden egyes osztályhoz egy külön teszt
osztály, és minden metódushoz egy vagy több teszt metódus. Ebben az esetben
nagyon függni fogunk az implementációs részletektől, és hamar a Fragile Test Problemmel
találkozunk szembe, méghozzá azzal, hogy ha refactorálunk nagyon nagy mennyiségű teszt
fog eltörni. Erre a javaslat az, hogy a használati eseteket API szinten teszteljük,
és ne írjunk teszteseteket a belső osztályaira, így könnyebben átszervezhetőek.</p>
<p>Másik nagyon fontos állítás, hogy a teszt esetek az architektúra részét képzik, azaz
ugyanúgy ugyanazon szabályok alapján kell megtervezni őket. Ezek természetesen a
külső (Frameworks and Drivers) körön helyezkednek el. És az állítás ezzel kapcsolatban az,
hogy ezek se függjenek kevésé stabil, gyakran változó komonenseken, azaz főleg ne felületi tesztek legyenek,
hanem a használati eseteket hajtsák meg.</p>
<p>Remek ötlet, hogy a Clean Architecture nem veti el
annak lehetőségét, hogy olyan API-t fejlesszünk, melyeket csak a teszt esetek használnak,
a tesztelést megkönnyítendő. Azonban ez biztonsági kockázatot hordozhat, ezért tegyük külön
komponensbe, és ne deploy-oljuk éles környezetbe.</p>
<p>A könyvben szerepel a kódduplikálással kapcsolatos állítás is, méghozzá az, hogy nem
feltétlenül rossz.
Az ehhez kapcsolódó példa ragadott meg a legjobban. Képzeljünk el két képernyőt, amelyek
teljesen ugyanúgy néznek ki, csak két teljesen különböző üzleti entitáshoz tartoznak.
Az egyik képernyőn pl. hallgatókat, a másik képernyőn kurzusokat lehet karbantartani.
Mivel hasonlítanak egymáshoz, felmerülhet az igény, hogy ne legyen a kódban duplikáció,
próbálunk egy magasabb absztrakciós szintet kialakítani, és ezzel elbonyolítjuk a kódot.
Itt azonban a kódduplikáció lenne a megfelelő változás, hiszen a kód két különböző
üzleti fogalomhoz tartozik, és nagy a valószínűsége, hogy a képernyők a jövőben
két teljesen különböző irányba fognak továbbfejlődni.</p>
<p>Azt tapasztalom, hogy a szoftveren belüli rétegekkel kapcsolatban sokan túláradó
érzelmekkel nyilatkoznak. Van, akik szerint az adat a legfontosabb, így
kezdjük az adatbázistervezéssel, az alkalmazás csak egy felület, ami bármikor lecserélhető.
Vannak akik szerint az adatbázis egy buta tár, és az üzleti logika az elsődleges.
Van akik szerint a UI nem fontos, az csak csicsa, csakis a backend kód számít.
A backend viszont nem adja el a terméket, bármennyire szépen is van megírva.
Nemegyszer hallani ebből az okból kifolyólag a dba-k, backend és frontend fejlesztők közötti
ellentétekről. Szerintem az a
hozzáállás a célravezető, ha még csak nem is gondolunk ilyen jellegű összehasonlításokra,
hanem elfogadjuk, hogy az alkalmazás szerves része mindhárom, nincs alá- vagy fölérendelt viszony.
Mindegyik másért felelős, mindegyiket a legjobb tudásunk szerint kell megírnunk, együttműködve.
A lényeg, hogy a határvonalakat a lehető legjobban húzzuk meg.</p>