Kihagyás

1. gyakorlat

Építsünk valamit

A legtöbb esetben a Spring-et backend-ként szokás használni, de a Spring MVC is egy népszerű megoldás. Központi elem a controller vagy controllerek, melyek kapnak egy kérést és azt valamilyen módon kiszolgálják. A válasz lehet legenerált vagy statikus HTML, vagy adat (ha éppen REST API-t) készítünk. A Spring MVC-t alaposan meg fogjuk ismerni a következőkben, melynek szintén része, hogy controller-ekkel dolgozzunk.

Készítsük el a "Contacts" webes alkalmazást, mely egy elektronikus telefonkönyvhöz lesz hasonló! A félév során ezt az alkalmazást fogjuk felépíteni a nulláról, minden alkalommal kicsit továbbfejlesztve azt.

Függőségek

Spring MVC-s alkalmazásokkal fogunk dolgozni, így szükségünk lesz még egy-két dependency-re. Ezeket a legegyszerűbb, akkor hozzáadni amikor a Spring Initializr-t használjuk, de utólagos hozzáadásuk sem túl nagy feladat. Ezen a ponton készíthető egy új alkalmazás az initializr-el vagy kövessük a következő lépéseket:

  • Mivel webes a projekt, ezért adjuk hozzá a spring-boot-starter-web dependency-t!
  • Mivel szeretnénk HTML válaszokat generálni, így adjuk hozzá a spring-boot-starter-thymeleaf HTML templating engine-t a dependency-k közé.

Tehát a pom.xml-be a következő bejegyzéseknek kell belekerülnie:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<dependencies>
    ...
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-thymeleaf</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    ...
</dependencies>

Tehát az alkalmazásunkat a Spring MVC (Model, View, Controller) mentén fogjuk fejleszteni. Fontos, hogy egy jól definiált felosztást, csoportosítást (packaging) alkalmazzunk a projekt felépítése során, mivel így sokkal könnyebb lesz karbantartani az alkalmazásunkat!

Thymeleaf

A Thymeleaf egy modern szerver oldali Java-s template engine (view rétegben dinamikusan renderelhetünk tartalmat) mind webes, mind standalone környezetben.

A Thymeleaf fő célja, hogy elegáns módon úgynevezett "natural template"-eket írhassunk. Ennek lényege, hogy például a HTML tartalmat a böngésző helyesen meg tudja jeleníteni még fejlesztés közben is, így prototypinghoz ideálisan alkalmazható, mely növelheti a kollaboráció sikeressegét a csapaton belül.

Különböző modulokkal (dialektusokkal) rendelkezik, melyek között ott van például a Spring dialektus, de ezen felül további dialektusokkal is rendelkezik, illetve saját magunk is írhatunk konkrét megvalósításokat, mely rendkívül rugalmassá teszi ezt a template engine-t.

A Thymeleaf-et alkalmazhatjuk JSP, JSF helyett.

A HomeController

Készítsünk egy egyszerű Controller-t, mely a / -re (root-ra) érkező kéréseket szolgálja ki!

Tipp: Az összes Controller osztályt érdemes egy controller package-be elhelyezni!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
package hu.suaf.contacts.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class HomeController {

    @GetMapping("/")
    public String home(){
        return "home";
    }

}

Az osztályunkat a @Controller annotációval látjuk el, mely jelzi a Spring számára, hogy ez az osztály egy komponens, melyet az automatikus Component Scanning alkalmával meg is tud így találni a rendszer és be tudja regisztrálni, azaz a Spring készíteni fog egy Bean-t ebből az osztályból, mely belekerül az Application Context-be (nekünk magunknak nem kell példányosítani ezt a controller-t).

Figyelem

Amíg másképp nem rendelkezünk, addig a Controller-eknek az Application osztály package-ében, vagy annak valamely sub-package-ében kell lenniük, különben az Component Scanning nem fogja megtalálni őket.

A @Controller annotáció helyett használhattunk volna @Service, @Component, @Repository annotációkat is, ugyanaz lenne az eredmény azonban fontos a szerepüknek megfelelően annotálni az osztályainkat, így a Controller a legmegfelelőbb választás (később látni fogjuk a többieket is akció közben).

A következő fontos elem a home() metódusra elhelyezett @GetMapping annotáció, mely jelzi hogy HTTP GET kérések kiszolgálását fogja végezni ez a metódus, méghozzá a root útvonalon. Maga a metódus csak a "home" szöveget adja vissza, ami elsőre kicsit gagyinak tűnik, de ezt fogja felhasználni a Thymeleaf (pontosabban ez a string lesz a template logikai neve a rendszerben, tehát majd egy "home" nevű template-et kell készítenünk).

Anélkül, hogy a Thymeleaf-el még bármit is csinálnánk, indítsuk el az alkalmazásunkat! Láthatjuk, hogy így az alkalmazásunk már nem terminál rögtön, mivel van egy controller benne, ami várja a HTTP kéréseket. Nyissuk meg a Postman-t, mellyel HTTP kéréseket küldhetünk megadott útvonalakra! Mivel a Spring Boot alkalmazásunk alapból behúz egy TomCat embedded szervert, így nincs is más dolgunk mint megnézni, hogy amikor elindítottuk az alkalmazásunkat, akkor a webes app (TomCat által), melyik porton van hostolva.

1
Tomcat started on port(s): 8080 (http) with context path ''

Tipp

Az application.properties állományban megadhatunk más portot is, melyen a szervert indítani szeretnénk. Ehhez használjuk a következő sort!

1
server.port = 8000

Eredményképpen a szerver a 8000-es porton indul el a 8080 helyett.

Ezután a Postman-be állítsuk be hogy GET kérést küldünk a http://localhost:8080/ címre. A válasz most egy hiba:

1
2
3
4
5
6
7
{
    "timestamp": "2020-05-08T14:02:28.216+0000",
    "status": 500,
    "error": "Internal Server Error",
    "message": "Error resolving template [home], template might not exist or might not be accessible by any of the configured Template Resolvers",
    "path": "/"
}

Viszont ez jól demonstrálja a spring-boot-starter depenency-k hatalmát. Anélkül, hogy bármi különösebbet kellett volna konfigurálnunk, a keretrendszer már tudja, hogy nem egyszerűen egy string-et szeretnénk visszaküldeni, hanem egy template-re adunk hivatkozást.

A kezdőoldal

Ha utólag adtuk hozzá a projekthez a Thymeleaf-et, akkor hozzunk létre egy templates mappát a resources alatt és azon belül hozzunk létre egy home.html oldalt, amibe valami tetszőleges szöveget írjunk bele! Postman-el is tesztelhetjük ezután a működést, vagy böngészőben is megjeleníthetjük az eredményt ha a localhost:8080-ra navigálunk (Az URL elérése egy GET kérést fog küldeni ilyen esetben. Később azonban a postman még nagyon jól fog jönni, így szokjuk ennek használatát is). A template-ek elnevezési konvenciója:

/templates/<view template neve>.html

Tipp

Az üres HTML állományba írd be, hogy `html, majd Ctrl + Space és válaszd ki a megfelelő html verziót (esetünkben 5-ös html). Ekkor legenerál az IntelliJ egy alap HTML törzset.

Ahhoz, hogy Thymeleaf-es elemeket is használhassunk be kell húznunk a html-be a Thymeleaf-es xml namespace-t. Használjuk is, úgy hogy egy stock photo-t rakunk bele a html-be! A statikus erőforrásokat, mint például a képeket a Thymeleaf szintén egy előre definiált szerkezetben keresi. A resources alá (a templates-el egy szinten) hozzunk létre egy static mappát, melyben egy images mappát is létrehozunk a képeknek! Keressünk a neten egy szimpatikus contacts témájú logót, amit helyezzük el az előbbi mappába contacts_logo.png néven! Ekkor a home.html a következőképpen alakul:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
<!doctype html>
<html lang="en" xmlns:th="http://thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
    Hello there!
    <img th:src="@{/images/contacts_logo.png}" />
</body>
</html>

A képnek a th:src-vel adjuk meg a forrását, melyben így használhatjuk a @{...} alakú kifejezéseket, melyek az application root-hoz képesti relatív útvonal megadását teszi lehetővé.

Próbáljuk ki most az alkalmazásunkat!

Spring dev tools

Amikor fejlesztünk, akkor az alkalmazásunkat mindig újra kell indítani, amikor valamilyen kódot megváltoztatunk. A Spring Dev Tools ebben nyújthat segítséget. Előnyei a következőek:

  • Alkalmazás újratöltése, amikor a kód változik
  • Automatikus böngésző frissítés, amikor frissült valamilyen browser-related erőforrás
  • Template cache automatikus letiltása
  • Beépített H2 konzol, ha H2-t használunk DB-nek
  • Nem IDE függő plugin

Először hozzá kell adnunk a dev toolset-et a projekt függőségeihez.

1
2
3
4
5
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-devtools</artifactId>
    <scope>runtime</scope>
</dependency>

A scope megadásával a production code-ba nem kerül bele a devtools.

Ha IntelliJ-t használunk a fejlesztéshez, akkor ennyi sajnos nem elég az automatikus újratöltéshez. IntelliJ-ben kicsit trükkös összelőni a dolgokat. Ehhez kövessük a következőket:

  1. File –> Settings... –> Build, Execution, Deployment –> Compiler –> Build project automatically (kipipálni)
  2. File –> Settings... –> Advanced Settings –> Allow auto-make to start even if developed application is currently running (kipipálni)

Tipp

Előfordulhat, hogy régebbi IntelliJ verzió esetén az utóbbi beállítási lehetőség nem létezik. Ekkor nyomd meg a Shift+Control+A billentyűkombinációt -> Írd be, hogy Registry, majd keresd meg és engedélyezd a compiler.automake.allow.when.app.running bejegyzést!

Ekkor ha futtatjuk az alkalmazásunkat, akkor automatikusan újrafordítja a projektet, amikor valami változik. Azonban a böngésző nem frissül automatikusan, F5-öt kell nyomni, hogy ez megtörténjen. Abban az esetben, ha ezt is szeretnénk belőni, akkor tegyük a következőket:

  1. Töltsük le a LiveReload plugin-t Chrome alá
  2. Miután futtatjuk az alkalmazásunkat a böngészőben engedélyeznünk kell a LiveReload plugin-t a jobb felső sarokban
  3. Ezután, amikor változtatunk a kódon, akkor az automatikusan újrafordul, illetve a böngésző is frissül.

Van még egy fontos dolog, amit még tisztázni kell. A Java kódok frissítését úgy végzi a rendszer, hogy két class loadert használ, egyet a mi saját java osztályainkhoz (mivel valószínűleg ezek változnak gyakran) és egyet a library-k kódjához. Ezt azért csinálja, hogy időt nyerjen vele, mivel változáskor csak az első loader által érintett elemek töltődnek újra. Ez sajnos azt is jelenti, hogy amikor például egy dependency módosul, akkor nem tudja az automatikus frissítést megtenni a rendszer, ilyenkor egy hard restart kell.

Teszt írása az alkalmazáshoz

Ha működik az alkalmazásunk, nézzük hogy hogyan írhatunk hozzá tesztet. Webes alkalmazások tesztelése trükkös lehet, de szerencsére a Spring ehhez is ad támogatást.

Először is adjuk hozzá a következő dependency-t:

1
2
3
4
5
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

A teszt végrehajt egy HTTP GET hívást a /-ra, és figyeli, hogy az a home-ot adja-e vissza illetve azt, hogy a template milyen tartalmat generál.

Lássuk magát a tesztet:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package hu.suaf.contacts;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
import static org.hamcrest.Matchers.containsString;


import hu.suaf.contacts.controller.HomeController;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.web.servlet.MockMvc;

@ExtendWith(SpringExtension.class)
@WebMvcTest(HomeController.class)  // Web teszt a HomeController osztályhoz
public class HomeControllerTest {

    @Autowired
    private MockMvc mockMvc;  // Injektáljuk a MockMvc

    @Test
    public void testHomePage() throws Exception{
        mockMvc.perform(get("/"))   // GET kérés
                .andExpect(status().isOk())  // 200-at kell visszaadjon
                .andExpect(view().name("home"))  // a home view-ra kell hogy menjünk
                .andExpect(content().string(containsString("Hello")));  // elvárt szöveg
    }
}

Tipp

A spring-boot-starter-test további függőségeket húz be, többek között a JUnit-ot is. A fenti példa a JUnit 5-ös verziójával készült. Ha JUnit-ból valamely 4-es verzió áll rendelkezésünkre, vagy kifejezetten ezt a verziót szeretnénk használni az 5-össel szemben, akkor az @ExtendWith(SpringExtension.class) helyett használjuk a @RunWith(SpringRunner.class) annotációt, valamint az org.junit.jupiter.api.Test helyett az org.junit.Test-et importáljuk.

Az osztályt @WebMvcTest annotációval látjuk el. Ez a speciális, Spring által biztosított annotáció azt a célt szolgálja, hogy ezt a tesztet Spring MVC alkalmazás kontextben futtassa a rendszer. Egész pontosan a HomeController regisztrálásra kerül a kontextben, így tudunk számára request-eket küldeni a tesztek közben. A MockMvc injektálásával irányíthatjuk a tesztünket, hozzáférhetünk a mokkolt objektumhoz.

Az egyetlen teszt metódus (@Test-el ellátva) ezt a mokkolt objektumot használja, melyen keresztül küldhetünk egy GET kérést a homePage-nek ("/", azaz a rootnak küldjük). Ezután leteszteljük, hogy az elvárt eredményeket kaptuk-e (200-al tér vissza, logikai neve a "home", és kirenderelődött a "Hello" szöveg). Amennyiben valamelyik feltétel sérül, akkor a teszt el fog hasalni.

Kontaktok kezelése

Az alkalmazásunk biztosítson felületet kapcsolatok felvételére!

A Spring-ben is a controller feladata az adat elérése és annak megfelelő feldolgozása. A View feladata, hogy ezen adatot megjelenítse a felhasználó számára, jelen esetben HTML tartalmat adjon.

Amiket el kell készítenünk:

  • Domain model, mely egy kapcsolat leírására szolgál
  • Egy controller, amin keresztül hozzáadhatunk és lekérhetünk kontaktokat
  • Maga a view template, ami rendereli a kontakt formot és a kontaktok listáját (külön-külön view) a böngészőben.

Domain model

Egy kapcsolat a következőkkel rendelkezzen!

  • egyedi azonosító
  • név
  • telefonszám
  • email
  • lakcím
  • születésnap
  • létrehozás dátuma

Ennek tükrében a model osztályunk:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
package hu.suaf.contacts.model;

import lombok.Data;
import java.util.Date;

@Data
public class Contact {

    private Long id;
    private String name;
    private String phone;
    private String email;
    private String address;
    private Date birthDate;
    private Date createdAt;

}

De mi a nyavaja az a lombok és miért jó ha ezt használjuk? A lombok nem a Spring része, de igen hasznos kis library, mivel segítségével sokkal tömörebb model osztályokat írhatunk, illetve változtatások alkalmával nem kell mindent átnevezni (pl getter, setter). Ha megfigyeljük, akkor nincs se konstruktor, se getter/setter megadva. Ezeket mind a lombok generálja futásidőben, amihez nem kell mást csinálnunk mint a @Data annotációt rárakni az osztályunkra. Ez ellátja az osztályunk adattagjait getter/setter párosokkal (nyilván ha final a field akkor setter-t nem fog hozzáadni), továbbá készít toString, hashCode, equals metódusokat és egy paraméteres konstruktort is (az összes paraméter szerepel benne). Látható, hogy a @Data annotáció összetett és sok dolgot legenerál magától. Ha ezek közül valamelyiket nem szeretnénk, akkor szükség lehet arra, hogy egyenként tudjuk ezeket befolyásolni. A @Data megegyezik a következő lombok-os annotációk összességével:

  • @Getter
  • @Setter
  • @RequiredArgsConstructor
  • @ToString
  • @EqualsAndHashCode

A lombok használatához az alábbi dependency-re lesz szükségünk:

1
2
3
4
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
</dependency>

Előfordulhat, hogy az IDE hibát jelezni amint használnánk valamelyik generált metódust, hiszen ezeket csak futásidőben generálja le a rendszer. Amennyiben IntelliJ-t használunk, akkor a lombok honlapján leírt lépéseket kell követnünk. Más IDE-hez is megtalálhatjuk itt a szükséges beállításokat.

Controller elkészítése

A controllerek az alkalmazásunk lelkét alkotják, elsődleges feladatuk a HTTP kérések kezelése, majd a kérés továbbpasszolása a megfelelő view-nak, ami kirendereli a HTML-t (MVC esetében), vagy közvetlenül ők írják a választ nem pedig a view (ilyenkor beszélünk RESTful controller-ről). Utóbbival később fogunk foglalkozni, egyelőre mindig átpasszoljuk a view-nak a kérést és ő intézi a megjelenítést. Nézzük miket is kell tudnia a controllerünknek, ahhoz hogy összerakhassunk egy kontaktot:

  • HTTP GET kérés kezelése a /contact/create URL-en
  • Meg kell adni a megfelelő adatokat
  • Át kell passzolni a kérést a megfelelő view-nak, ami majd a renderelést végzi

Nézzük is, hogy hogyan nézhet ez ki:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
package hu.suaf.contacts.controller;


import hu.suaf.contacts.model.Contact;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@RequestMapping("/contact")
public class ContactController {

    @GetMapping("/create")
    public String showContactCreateForm(Contact contact){
        return "contact-create";
    }

}

A controller-re itt is rárakjuk a @Controller stereotype annotációt, így automatikusan regisztrálja a Spring a bean-ek között. Jelen esetben az osztályra helyezünk el egy @RequestMapping("/contact") annotációt, mellyel az osztály handler metódusai egységesen a /contact URL alá kerülnek regisztrálásra. Ettől függetlenül a showContactCreateForm metódus @GetMapping annotációjában megadhatunk további URL részeket, pl: @GetMapping("/create"), mely esetben a a metódus a /contact/create URL-re fut le. Jelen esetben ez a kérés csupán továbbhajít minket a contact-create.html oldalra.

View template

Írjuk meg a view template-et! Ahogy korábban is tettük a /resources/templates alá hozzunk létre egy contact-create.html oldalt!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
<!doctype html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" integrity="sha384-JcKb8q3iqJ61gNV9KGb8thSsNjpSL0n8PARn9HuZOnIxN0hoP+VmmDGMN5t9UJ0Z" crossorigin="anonymous">
    <script src="https://code.jquery.com/jquery-3.5.1.slim.min.js" integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj" crossorigin="anonymous"></script>
    <script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.1/dist/umd/popper.min.js" integrity="sha384-9/reFTGAW83EW2RDu2S0VKaIzap3H66lZH81PoYlFhbGU+6BZp6G7niu735Sk7lN" crossorigin="anonymous"></script>
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js" integrity="sha384-B4gt1jrGC7Jh4AgTPSdUtOBvfO8shuf57BaghqFfPlYxofvL8/KUEfYiJOMMV+rV" crossorigin="anonymous"></script>
    <title>Create Contact</title>
</head>
<body>

<div class="container">
    <!--/*@thymesVar id="contact" type="hu.suaf.contacts.model.Contact"*/-->
    <form action="#" th:action="@{/contact/create}" th:object="${contact}" method="post">
        <div class="form-group">
            <label for="name">Name</label>
            <input type="text" class="form-control" id="name" th:field="*{name}">
        </div>
        <div class="form-group">
            <label for="email">Email</label>
            <input type="email" class="form-control" id="email" th:field="*{email}">
        </div>
        <div class="form-group">
            <label for="phone">Phone</label>
            <input type="text" class="form-control" id="phone" th:field="*{phone}">
        </div>
        <div class="form-group">
            <label for="address">Address</label>
            <input type="text" class="form-control" id="address" th:field="*{address}">
        </div>
        <div class="form-group">
            <label for="birthDate">Birth Date</label>
            <input type="date" class="form-control" id="birthDate" th:field="*{birthDate}">
        </div>
        <button type="submit" class="btn btn-primary">Submit</button>
    </form>
</div>

</body>
</html>

Mielőtt megnéznénk, hogy pontosan milyen Thymeleaf-es elemeket használunk a fenti példában, érdemes egy kicsit általánosságában beszélni a Thymeleaf-ről.

Spring MVC-ben két interface szolgálja ki a template engine-eket:

  • org.springframework.web.servlet.View
  • org.springframework.web.servlet.ViewResolver

A View felelős a megadott HTML tartalom rendereléséért, melyet általában a template engine (pl.: Thymeleaf) végez. A ViewResolver egy olyan objektum, mely megadott kérésre (műveletre) és locale-ra visszaadja a megfelelő View-t. Tipikusan a controller-ek megkérik a ViewResolver-t, hogy továbbítsa őket a megadott nevű view-hoz (elérhető String visszatéréssel, ahogyan azt mi is csináltuk). Fontos, hogy ViewResolver-ből egy egész láncolatunk is lehet (hasonlóan, mint a FilterChain), de a láncolat végén el kell, hogy dőljön, hogy melyik View végzi a renderelést.

Ha a fenti kódot megvizsgáljuk, akkor látunk többféle kifejezést is a HTML-be ágyazva (pl.: ${contact}). Nézzük meg, hogy milyen típusú kifejezéseket adhatunk meg.

  • ${...}: Variable expression
  • *{...}: Selection expresison
  • #{...}: Message (i18n) expression
  • @{...}: Link (URL) expression
  • ~{...}: Fragment expression

Röviden nézzük át azokat melyeket a fenti példában használunk (a többit később vesszük majd elő).

Variable expression

Spring-el való integráció esetében az ilyen expression a Spring EL (Spring Expression Language) része. A kifejezéssel a context változókra (a Spring model attribute is szokta hívni) adhatunk kifejezéseket. Egy példa lehet:

1
${book.author.name}

Használatuk általában attributumokban történik:

1
<span th:text="${book.author.name}">
, mely kifejezés megegyezik a következő Java kóddal:

1
((Book)context.getVariable("book")).getAuthor().getName()

Variable expression-ök előfordulhatnak olyan helyen is ahol közvetlenül nem állítunk elő output-ot (nem rendereli egyből), pl: iterációkban:

1
<li th:each="book : ${books}">
, a fenti eset is hasonló. Itt a form-ot kötjük hozzá a contact változóhoz.

Selection expression

Hasonló a variable expression-höz, de annyi különbséggel, hogy ezek az előzőleg kiválaszott objektumon értékelődnek ki, nem pedig az összes context változón. Pl:

1
*{customer.name}

Egy ilyen kiválasztást tesz meg például a form-on megadott th:object. Így a form fieldekre megadott selection expression-ök ezen a contact objektumon értékelődnek ki. Pl: *{name} nem mást jelent mint ${contact.name}, ami megegyezik a

1
((Contact)context.getVariable("contact")).getName()
Java-s kifejezéssel.

A fenti példában a th:field használata miatt egy kötés jön létre a form field-jei és a contact objektum adattagjai között.

Ezt már láttuk korábban is, amikor a home.html oldalon megadtuk a képünk elérését:

1
<img th:src="@{/images/contacts_logo.png}" />

Alapvetően arről van szó, hogy URL-eket rakunk össze ezekkel a kifejezésekkel, úgy hogy context és session infokat adhatunk hozzá. Pl. a következő kifejezés:

1
<a th:href="@{/order/list}">...</a>
megegyezik ezzel:

1
<a href="/myapp/order/list">...</a>

viszont nem kell nyilvántartani, lekéregetni a context-et (myapp), majd összefűzni a megadott url-el.

Amennyiben a rendszerben engedélyezve van a session követés akkor akár a következőt is kaphatjuk:

1
<a href="/myapp/order/list;jsessionid=23fa31abd41ea093">...</a>

, azaz a rendszer automatikusan beilleszti a session id-t az URL-be.

Ami később fontos lehet, amikor majd egy-egy kontaktot kiválasztva egy details oldalra szeretnénk átvinni a felhasználót, az az, hogy az URL is kaphat paramétereket. Pl:

1
<a th:href="@{/contact/details(id=${contactId})}">...</a>
, ami a következővé alakul át (feltéve, hogy a contactId 23-mal egyenlő):

1
<a href="/myapp/contact/details?id=23">...</a>

A Link Expression-ök lehetnek relatív megadások is. Ilyen esetben az application context nem kerül bele az url-be prefixként. Pl:

1
<a th:href="@{../documents/report}">...</a>

Most, hogy helyére tettünk néhány Thymeleaf-es dolgot vesézzük ki alaposabban a megírt kódot! Az első fontos rész, hogy a form method attribútuma POST, viszont nincs beállítva action, ahol megmondanánk hogy a kérést hova küldjük (pontosabban megadtuk, de a # ezt jóformán semmire állítja). Ilyen esetben ugyanarra az url-re fogja küldeni, ahol vagyunk, vagyis a /contact/create URL-re (itt fogjuk majd megírni azt a metódust, ami kezeli a POST-ot a ContactController-ben). Ugyanakkor megadjuk a th:action-t melyben most megadjuk, hogy hova is menjen a kérés (link expression használatával).

A következő fontos elem a th:object attribútum. Ebben az atribútumban adhatjuk meg az ún. command object-et, mely alatt a Spring a form-backing bean-eket (azaz a form-hoz rendelt bean-eket) érti. A th:object-et máshol is használhatjuk (nem csak form-on), de ebben a kontextusban fontos, hogy a következő megkötések vannak:

  • a th:object atribútum értékének csak variable expression-t adhatunk meg, melyben csak a model attribútum nevét adjuk meg (magáról a model attribútumokról hamarosan hallunk még sokkal többet, de lényeg, hogy ez az, amit a controller átad a view számára). Fontos, hogy nem lehet benne property kiválasztás (tehát a ${contact} helyes, de a ${contact.name} már nem az).
  • A <form>-on belül már nem használhatunk másik th:object-et (HTML form-ok amúgy sem ágyazhatóak egymásba, különben következetlenség lépne fel).

Tipp: A HTML oldal header-jébe behúztuk a Bootstrap CSS és JS elemeket, így használhatjuk a Bootstrap nyújtotta előnyöket.

Amennyiben az alkalmazásnkat jelen állapotában futtatjuk, akkor látnunk kell egy form-ot, amely bekéri a megfelelő kontakt tulajdonságokat (név, email, stb.), valamint van egy gombunk, amit ha megnyomunk, akkor azt fogja írni, hogy a POST kérés típus nem támogatott (Request method 'POST' not supported). A következőkben nézzük meg, hogy hogyan lehet ezt kezelni, azaz a POST-ban elküldött adatokat feldolgozni.

POST kezelése

Az egyszerűség kedvéért csak annyit csinálunk a POST kezelésekor, hogy logoljuk az elküldött adatokat, majd újra a /contact/create oldalt mutatjuk.

1
2
3
4
5
@PostMapping("/create")
    public String createContact(Contact contact){
        log.info(contact.toString());
        return "contact-create";
    }

A GetMapping-hez hasonlóan létezik @PostMapping is (továbbá az összes alap kéréstípushoz van megfelelő: @DeleteMapping, @PutMapping, stb). Mivel a form-ban azt mondtuk, hogy a POST a /contact/create-re érkezzen, így itt is a /create-et adtuk meg, ugyanúgy, ahogy a GET esetében is. A kód jelen pillanatban még hibás, mivel nincs log objektumunk. Aki használt már logolást a rendszerében az tudja, hogy a logger példányosítása mindig kopizással, de legalábbis unalmas gépeléssel történik. Valami ilyesmi lenne ha mondjuk az Slf4J loggert használnánk:

1
private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(ContactController.class);

ezen felül a logolás config property-jeit is meg kellene adnunk. A lombok ebben is támogatást nyújt. A fenti adattag létrehozás helyett használhatjuk egyszerűen a Slf4J annotációt az osztályon és máris ismerni fogja a log objektumot a rendszer.

Az Slf4J használatáról további infok itt találhatóak.

Az egyik érdekes dolog, hogy a metódus paramétere egy Contact. Mivel a view-ban megmondtuk, hogy a name és azemail, stb form-data állítódjék be a felületi elemeknek megfelelően (th:object és th:field használata), így ezeket át fogja passzolni ennek a contact objektumnak, amit automatikusan létre is hoz a rendszer. Ezután simán logoljuk, a contact-ot, amit látnunk is kell majd a konzolon. Továbbá érdemes lehet megnéznünk a böngésző devTools-a alatt, hogy a hálózati küldések során milyen adatokat küldött el a POST kérés.

Egy rész még kimaradt a kódból, mely igen fontos. A dátumok változatos megadásuk miatt nem biztos, hogy automatikusan parse-olhatóak, így erről nekünk kell gondoskodnunk. Amennyiben csak az adott controller-ben szeretnénk használni a konvertert, akkor megadhatjuk a következőképpen:

1
2
3
4
@InitBinder
public void initBinder(WebDataBinder binder) {
    binder.registerCustomEditor(Date.class, new CustomDateEditor(new SimpleDateFormat("yyyy-MM-dd"), true));
}
Ezzel megadjuk, hogy a Date típusú adattag milyen kinézetű String-ből képződik, illetve engedélyezünk üres field-et is (ilyen esetben a birthDate null értéket fog felvenni).

Ezzel meg is volnánk, szóval futtassuk le az alkalmazásunkat. Ha létrehozunk egy contact-ot a /contact/create oldalon, akkor az IDE-ben valami hasonló log sornak kell megjelennie a konzolon (ez az alapértelmezett beállítása az Slf4J-nek):

1
2020-08-11 15:34:03.120  INFO 3836 --- [nio-8080-exec-3] h.s.c.controller.ContactController       : Contact(id=null, name=spring, phone=+36 123 4567, email=spring@foo.bar, address=Alsóbucsaröcsöge, Fő utca 2., birthDate=Thu Aug 13 00:00:00 CEST 2020, createdAt=null)

Feladatok

Feladat

Készítsünk tesztet a /contact/create-hez

További anyagok


Utolsó frissítés: 2021-09-28 18:22:35
Back to top