Osa 2

Näkymät ja data

:
Loading interface...
:
Loading interface...

Kirjaudu sisään nähdäksesi tehtävän.

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).

Tällä kurssilla käyttämämme apuväline näkymän luomiseen on Thymeleaf, joka tarjoaa välineitä datan lisäämiseen HTML-sivuille. Käytännössä näkymiä luodessa luodaan ensin HTML-sivu, jonka jälkeen sivulle lisätään komentoja Thymeleafin käsiteltäväksi.

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.

Loading
Loading

Tiedon lisääminen näkymään Model-luokan avulla

Palvelinohjelmistossa luodun tai haetun datan lisääminen näkymään tapahtuu Model-olion 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.

Loading
:
Loading interface...
:
Loading interface...

Kirjaudu sisään nähdäksesi tehtävän.

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("list", 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>
Lisää kokoelmien läpikäynnistä löytyy Thymeleafin dokumentaatiosta, kts. https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.html#iteration.

Klassisin virhe th:eachia käytettäessä on iteroitavan joukon määrittely merkkijonona th:each="alkio : lista". Tämä ei luonnollisesti toimi.

Loading

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.

Lombok on kirjasto, joka on suunniteltu vähentämään projekteissa esiintyvien toisteisten konstruktorien, getterien ja setterien määrää. Lombokin saa projektin käyttöön lisäämällä projektin 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 on 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>
Loading
Pääsit aliluvun loppuun! Jatka tästä seuraavaan osaan: