Näkymät ja data
Sovelluksemme ovat vastaanottaneet tiettyyn polkuun tulevan pyynnön ja palauttaneet käyttäjälle merkkijonomuodossa olevaa tietoa. Palvelin voi myös luoda käyttäjälle näkymän, jonka selain lopulta näyttää käyttäjälle.
Näkymät luodaan tyypillisesti apukirjastojen avulla siten, että ohjelmoija luo HTML-näkymät ja upottaa HTML-koodiin kirjastospesifejä komentoja. Nämä komennot mahdollistavat mm. tiedon lisäämisen sivuille. Tällaisia HTML-sivuja, joihin on upotettu kirjastokohtaisia tiedon lisäämiseen tarkoitettuja komentoja kutsutaan näkymä-templateiksi (jatkossa template).
Thymeleaf-sivut ("templatet") sijaitsevat projektin kansiossa src/main/resources/templates
tai sen alla olevissa kansioissa. NetBeansissa kansio löytyy kun klikataan "Other Sources"-kansiota.
Alla olevassa esimerkissä luodaan juuripolkua /
kuunteleva sovellus. Kun sovellukseen tehdään pyyntö, palautetaan HTML-sivu, jonka Thymeleaf käsittelee. Spring päättelee käsiteltävän ja palautettavan sivun merkkijonon perusteella. Alla metodi palauttaa merkkijonon "index"
, jolloin Spring etsii kansiosta src/main/resources/templates/
sivua index.html
. Kun sivu löytyy, se annetaan Thymeleafin käsiteltäväksi, jonka jälkeen sivu palautetaan käyttäjälle.
package thymeleaf;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class ThymeleafController {
@GetMapping("/")
public String home() {
return "index";
}
}
Toisin kuin aiemmin, pyyntöjä käsittelevällä metodilla ei ole annotaatiota @ResponseBody
. Mikäli metodilla olisi annotaatio @ResponseBody
, palautettaisiin käyttäjälle merkkijono "index".
Kun annotaatiota @ResponseBody
ei ole määritelty metodille, Spring tietää, että palautettu merkkijono liittyy käyttäjälle näytettävään näkymään. Projektin pom.xml
-tiedostossa olevan spring-boot-starter-thymeleaf
riippuvuuden takia Spring tietää tarkemmin, että kyse on Thymeleaf-kirjastolle käsiteltäväksi annettavasta näkymästä — se siis etsii "index"-merkkijonoon liittyvän tiedoston ja antaa sen Thymeleafin käsiteltäväksi. Lopullinen tulos ohjataan sitten käyttäjälle.
Tiedon lisääminen näkymään Model-luokan avulla
Kun lisäämme Model-olion pyyntöjä käsittelevän metodin parametriksi, lisää Spring-sovelluskehys sen automaattisesti käyttöömme.
package thymeleafdata;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
public class ThymeleafJaDataController {
@GetMapping("/")
public String home(Model model) {
return "index";
}
}
Model on Spring-sovelluskehyksen käyttämä hajautustaulun toimintaa jäljittelevä olio. Alla olevassa esimerkissä määrittelemme pyyntöjä käsittelevälle metodille Model-olion, jonka jälkeen lisäämme lokeroon nimeltä teksti
arvon "Hei mualima!"
. Tämän jälkeen palautetaan merkkijono "index", jonka perusteella Spring päättelee että pyyntö ohjataan Thymeleafille.
package thymeleafdata;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class ThymeleafJaDataController {
@GetMapping("/")
public String home(Model model) {
model.addAttribute("teksti", "Hei mualima!");
return "index";
}
}
Kun käyttäjä tekee pyynnön, joka ohjautuu yllä olevaan metodiin, ohjautuu pyyntö return
-komennon jälkeen Thymeleafille, joka saa käyttöönsä Model-olion ja siihen lisätyt arvot sekä tiedon näytettävästä sivusta.
Oletetaan, että käytössämme olevan index.html
-sivun lähdekoodi on seuraavanlainen:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head>
<title>Otsikko</title>
</head>
<body>
<h1>Hei maailma!</h1>
<h2 th:text="${teksti}">testi</h2>
</body>
</html>
Kun Thymeleaf käsittelee HTML-sivun, se etsii sieltä elementtejä, joilla on th:
-alkuisia attribuutteja. Yllä olevasta sivusta Thymeleaf löytää h2
-elementin, jolla on attribuutti th:text
— <h2 th:text="${teksti}">testi</h2>
. Attribuutti th:text
kertoo Thymeleafille, että elementin tekstiarvo (tässä "testi") tulee korvata attribuutin arvon ilmaisemalla muuttujalla. Attribuutin th:text
arvona on ${teksti}
, jolloin Thymeleaf etsii model
-oliosta arvoa avaimella "teksti"
.
Käytännössä Thymeleaf etsii — koska sivulla olevasta elementistä löytyy attribuutti th:text="${teksti}"
— Model-oliosta lokeron nimeltä teksti
ja asettaa siinä olevan arvon elementin tekstiarvoksi. Tässä tapauksessa teksti testi
korvataan Model-olion lokerosta teksti löytyvällä arvolla, eli aiemman esimerkkimme tekstillä Hei mualima!
.
Annotaatiolla @Controller
merkityssä luokassa oleville metodeille voi määritellä parametrit hyvin vapaasti. Esimerkiksi Model
-olio ja pyynnön mukana tulevat parametrit käsitellään määrittelemällä metodi, jolla on parametrina sekä Model
-olio, että parametrit.
:
Log in to view the quiz
Kokoelmien näyttäminen Thymeleaf-sivulla
Model-oliolle voi asettaa myös arvokokoelmia. Alla luomme "pääohjelmassa" listan, joka asetetaan Thymeleafin käsiteltäväksi menevään Model-olioon jokaisen juuripolkuun tehtävän pyynnön yhteydessä.
package thymeleafdata;
import java.util.List;
import java.util.ArrayList;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
@Controller
public class ListaController {
private List<String> lista;
public ListaController() {
this.lista = new ArrayList<>();
this.lista.add("Hello world!");
this.lista.add("+[-[<<[+[--->]-[<<<]]]>>>-]>-.---.>..>.<<<<-.<+.>>>>>.>.<<.<-.");
}
@GetMapping(value = "/")
public String home(Model model) {
model.addAttribute("lista", lista);
return "index";
}
}
Listan läpikäynti Thymeleafissa tapahtuu attribuutin th:each
avulla. Sen määrittely saa muuttujan nimen, johon kokoelmasta otettava alkio kullakin iteraatiolla tallennetaan, sekä läpikäytävän kokoelman. Perussyntaksiltaan th:each
on seuraavanlainen.
<p th:each="alkio : ${lista}">
<span th:text="${alkio}">hello world!</span>
</p>
Yllä käytämme attribuuttia nimeltä lista
ja luomme jokaiselle sen sisältämälle alkiolle p-elementin, jonka sisällä on span-elementti, jonka tekstinä on alkion arvo. Attribuutin th:each
voi asettaa käytännössä mille tahansa toistettavalle elementille. Esimerkiksi HTML-listan voisi tehdä seuraavalla tavalla.
<ul>
<li th:each="alkio : ${lista}">
<span th:text="${alkio}">hello world!</span>
</li>
</ul>
Klassisin virhe th:each
ia käytettäessä on iteroitavan joukon määrittely merkkijonona th:each="alkio : lista"
. Tämä ei luonnollisesti toimi.
Lombok-projekti ja boilerplaten vähentäminen
Tutustumme kohta olioiden näyttämiseen sivuilla. Tarkastellaan ennen sitä kuitenkin erästä varsin näppärää kirjastoa.
Javan tietokohteita kuvaavat luokat tarvitsevat oletuksena konstruktoreita sekä gettereitä ja settereitä. Esimerkiksi Thymeleaf hyödyntää luokan get-metodeja HTML-sivuja täydentäessä.
Hyvin yksinkertainenkin luokka — kuten alla oleva tapahtumaa kuvaava Event
— sisältää paljon ohjelmakoodia.
public class Event {
private String name;
public Event() {
}
public Event(String name) {
this.name = name;
}
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
}
Suurin osa ohjelmakoodista on oleellista vallitsevien käytänteiden takia — esimerkiksi Thymeleaf tarvitsee getterit — mutta samalla epäoleellista. Edellä kuvattujen luokkien sekä niiden attribuuttien määrän lisääntyessä projekteissa tulee olemaan lopulta satoja ellei tuhansia rivejä "turhahkoa" lähdekoodia. Tällaista usein toistuvaa, samankaltaista koodia kutsutaan termillä boilerplate.
pom.xml
-tiedostoon lombok-riippuvuuden.<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
Projekti tarjoaa mahdollisuuden gettereiden ja settereiden automaattiseen luomiseen siten, että ohjelmoijan ei tarvitse määritellä niitä itse. Ohjelmoijan näkökulmasta edellä kuvattu luokka Event
toimii täysin samalla tavalla, jos konstruktorit ja metodit poistetaan ja luokkaan lisätään muutama annotaatio.
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@NoArgsConstructor
@AllArgsConstructor
@Data
public class Event {
private String name;
}
Edellä käytetyt annotaatiot toimivat seuraavasti: Annotaatio @NoArgsConstructor
luo luokalle parametrittoman konstruktorin, annotaatio @AllArgsConstructor
luo luokalle kaikki attribuutit sisältävän konstruktorin, ja annotaatio @Data
luo attribuuteille getterit, setterit, equals
-metodin, hashcode
-metodin, ja toString
-metodin.
Olioiden käsittely
Modeliin voi lisätä kokoelmien lisäksi myös muunlaisia olioita. Oletetaan, että käytössämme on henkilöä kuvaava luokka.
// importit
@NoArgsConstructor
@AllArgsConstructor
@Data
public class Henkilo {
private String nimi;
}
Henkilo-olion lisääminen on suoraviivaista:
@GetMapping("/")
public String home(Model model) {
model.addAttribute("henkilo", new Henkilo("Le Pigeon"));
return "index";
}
Kun sivua luodaan, henkilöön päästään käsiksi modeliin asetetun avaimen perusteella. Edellä luotu "Le Pigeon"-henkilö on tallessa avaimella "henkilo". Kuten aiemminkin, avaimella pääsee olioon käsiksi.
<h2 th:text="${henkilo}">Henkilön nimi</h2>
Ylläolevaa henkilön tulostusta kokeillessamme saamme näkyville olion toString
-metodin palauttaman arvon.
Pääsemme oliomuuttujiin käsiksi get*Muuttuja*
-metodien kautta. Jos haluamme tulostaa Henkilo-olioon liittyvän nimi
-muuttujan, kutsumme metodia getNimi
, jonka Lombok-projekti generoi käyttöömme automaattisesti mikäli luokalle on määritelty @Data
-annotaatio. Thymeleafin käyttämässä notaatiossa kutsu muuntuu muotoon henkilo.nimi
. Saamme siis halutun tulostuksen seuraavalla tavalla:
<h2 th:text="${henkilo.nimi}">Henkilön nimi</h2>
Olioita listalla
Perussyntaksiltaan th:each
tuli jo hetki sitten tutuksi: listan läpikäynti Thymeleafissa tapahtuu attribuutin th:each
avulla. Sen määrittely saa muuttujan nimen, johon kokoelmasta otettava alkio kullakin iteraatiolla tallennetaan, sekä läpikäytävän kokoelman.
<p th:each="alkio : ${lista}">
<span th:text="${alkio}">hello world!</span>
</p>
Iteroitavan joukon alkioiden ominaisuuksiin pääsee käsiksi aivan samalla tavalla kuin muiden olioiden ominaisuuksiin. Tutkitaan seuraavaa esimerkkiä, jossa listaan lisätään kaksi henkilöä, lista lisätään pyyntöön ja lopulta luodaan näkymä Thymeleafin avulla.
package henkilot;
import java.util.List;
import java.util.ArrayList;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class HenkiloController {
private List<Henkilo> henkilot;
public HenkiloController() {
this.henkilot = new ArrayList<>();
this.henkilot.add(new Henkilo("James Gosling"));
this.henkilot.add(new Henkilo("Martin Odersky"));
}
@GetMapping("/")
public String home(Model model) {
model.addAttribute("list", henkilot);
return "index";
}
}
<p>Ja huomenna puheet pitävät:</p>
<ol>
<li th:each="henkilo : ${list}">
<span th:text="${henkilo.nimi}">Esimerkkihenkilö</span>
</li>
</ol>
Käyttäjälle lähetettävä sivu näyttää palvelimella tapahtuneen prosessoinnin jälkeen seuraavalta.
<p>Ja huomenna puheet pitävät:</p>
<ol>
<li><span>James Gosling</span></li>
<li><span>Martin Odersky</span></li>
</ol>