Sisällysluettelo
Tehtävät
Osa 1
Web-sovellusten alkeet
Web-sovellukset koostuvat selain- ja palvelinpuolesta. Käyttäjän koneella toimii selainohjelmisto (esim. Google Chrome), jonka kautta käyttäjä tekee pyyntöjä verkossa sijaitsevalle palvelimelle. Kun palvelin vastaanottaa pyynnön, se käsittelee pyynnön ja rakentaa vastauksen. Vastaus voi sisältää esimerkiksi web-sivun HTML-koodia tai jossain muussa muodossa olevaa tietoa.
Selainohjelmointiin ja käyttöliittymäpuoleen keskityttäessä painotetaan rakenteen, ulkoasun ja toiminnallisuuden erottamista toisistaan. Karkeasti voidaan sanoa, että selaimessa näkyvän sivun rakenne määritellään HTML-tiedostoilla, ulkoasu CSS-tiedostoilla ja toiminnallisuus JavaScript-tiedostoilla.
Palvelinpuolen toiminnallisuutta toteutettaessa keskitytään tyypillisesti selainohjelmiston tarvitsevan "APIn" suunnitteluun ja toteutukseen, sivujen muodostamiseen selainohjelmistoa varten, datan tallentamiseen ja käsittelyyn, sekä sellaisten laskentaoperaatioiden toteuttamiseen, joita selainohjelmistossa ei kannata tai voida tehdä.
Ensimmäinen palvelinohjelmisto
Käytämme kurssilla Spring -sovellusperheen Spring Boot -projektia web-sovellusten tekemiseen. Merkittävä osa web-sovellusten rakentamisesta perustuu valmiiden kirjastometodien käyttöön. Niiden avulla määritellään (1) mihin osoitteeseen tulevat pyynnöt käsitellään ja (2) mitä pyynnölle tulee tehdä.
Spring -sovelluskehystä käyttävien web-sovellusten kehityksessä käytettävät osat saa käyttöön lisäämällä projektiin riippuvuuden Spring Boot -projektiin (spring-boot-starter-parent
) sekä web-projektiin (spring-boot-starter-web
).
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.4.2.RELEASE</version> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies>
Kun riippuvuudet on lisätty projektiin ja projektista pääsee käsiksi Spring-sovelluskehyksen metodeihin ja luokkiin, voimme luoda ensimmäisen palvelinohjelmistomme.
package heimaailma; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; @SpringBootApplication @Controller public class HeiMaailmaController { @RequestMapping("*") @ResponseBody public String home() { return "Hei Maailma!"; } public static void main(String[] args) throws Exception { SpringApplication.run(HeiMaailmaController.class, args); } }
Yllä olevassa esimerkissä luodaan pyyntöjä vastaanottava luokka. Pyyntöjä vastaanottavat luokat merkitään @Controller
-annotaatiolla. Tämän perusteella Spring-sovelluskehys tietää, että luokan metodit saattavat käsitellä selaimesta tehtyjä pyyntöjä.
Luokalle on määritelty lisäksi metodi home
, jolla on kaksi annotaatiota: @RequestMapping
ja @ResponseBody
. Annotaation @RequestMapping
avulla määritellään kuunneltava osoite -- tässä kaikki "*"
. Annotaatio @ResponseBody
kertoo sovelluskehykselle, että metodin vastaus tulee näyttää vastauksena sellaisenaan.
Eriytämme pyyntöjä vastaanottavat luokat ja sovelluksen käynnistämiseen käytettävän luokan jatkossa toisistaan.
package heimaailma; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class HeiMaailmaApplication { public static void main(String[] args) throws Exception { SpringApplication.run(HeiMaailmaApplication.class, args); } }
package heimaailma; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; @Controller public class HeiMaailmaController { @RequestMapping("*") @ResponseBody public String home() { return "Hei Maailma!"; } }
Hello World! (Klikkaa tästä niin tehtävä aukeaa)
Kuten huomattava osa ohjelmointikursseista, tämäkin kurssi alkaa tehtävällä, jossa toteutettava ohjelma kirjoittaa tekstin Hello World!
.
Toteuta tehtäväpohjan pakkauksessa wad.helloworld
olevaan HelloWorldController
luokkaan toiminnallisuus, joka kuuntelee kaikkia pyyntöjä. Kun palvelin vastaanottaa pyynnön, tulee palvelimen palauttaa merkkijono "Hello World!".
Käynnistä palvelin painamalla NetBeansin play-nappia tai suorittamalla HelloWorldApplication
-luokan main
-metodi. Avaa nettiselain, mene osoitteeseen http://localhost:8080 ja näet selaimessasi tekstin "Hello World!".
Palvelin sammutetaan NetBeansissa punaista nappia painamalla -- vain yksi sovellus voi olla kerrallaan päällä samassa osoitteessa. Palauta tehtävä lopuksi Test My Code:n submit-napilla.
Palvelinohjelmiston polut
Opimme aiemmin, että voimme kuunnella kaikkia palvelinohjelmistoon tulevia pyyntöjä asettamalla @RequestMapping
-annotaation parametriksi "*"
. Käytännössä tämän parametrin avulla määritellään polku, johon palvelimelle tulevat pyynnöt voidaan ohjata. Tähdellä määritellään, että kaikki pyynnöt päätyvät samalle polulle. Muiden polkujen määrittely on luonnollisesti myös mahdollista.
Antamalla poluksi merkkijonon "/salaisuus"
, kaikki web-palvelimen osoitteeseen /salaisuus
tehtävät pyynnöt ohjautuvat kyseiseen polkuun liitettyyn toiminnallisuuteen. Allaolevassa esimerkissä määritellään polku /salaisuus
ja kerrotaan, että polkuun tehtävät pyynnöt palauttavat merkkijonon "Kryptos"
.
@RequestMapping("/salaisuus") @ResponseBody public String home() { return "Kryptos"; }
Yhteen ohjelmaan voi myös määritellä useampia polkuja sekä niihin liittyviä toiminnallisuuksia. Jokainen polku käsitellään omassa metodissaan. Alla olevassa esimerkissä pyyntöjä vastaanottavaan luokkaan on määritelty kolme erillistä polkua, joista jokainen palauttaa käyttäjälle merkkijonon.
package polut; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; @Controller public class PolkuController { @RequestMapping("/path") @ResponseBody public String path() { return "Polku (path)"; } @RequestMapping("/route") @ResponseBody public String route() { return "Polku (route)"; } @RequestMapping("/trail") @ResponseBody public String trail() { return "Polku (trail)"; } }
Hello Paths
Toteuta pakkauksessa wad.hellopaths
olevaan luokkaan HelloPathsController
seuraava toiminnallisuus:
- Pyyntö polkuun
/hello
palauttaa käyttäjälle merkkijonon "Hello" - Pyyntö polkuun
/paths
palauttaa käyttäjälle merkkijonon "Paths"
Alla olevassa kuvassa on esimerkki tilanteesta, missä selaimella on tehty pyyntö polkuun /hello
Palauta tehtävä TMC:lle kun olet valmis.
Pyynnön parametrit
Pyynnöissä voi lähettää palvelimelle tietoa. Tutustutaan ensin tapaan, missä pyynnön parametrit lisätään osaksi haettavaa osoitetta. Esimerkiksi pyynnössä http://localhost:8080/salaisuus?onko=nauris
on parametri nimeltä onko
, jonka arvoksi on määritelty arvo nauris
.
Parametrien lisääminen pyyntöön tapahtuu lisäämällä osoitteen perään kysymysmerkki, jota seuraa parametrin nimi, yhtäsuuruusmerkki ja parametrille annettava arvo. Pyynnössä tuleviin parametreihin pääsee käsiksi @RequestParam
-annotaation avulla.
Allaolevassa esimerkissä on luotu palvelinohjelma, jonka tehtävänä on tervehtiä kaikkia pyynnön tekijöitä. Ohjelma käsittelee polkuun /hei
tulevia pyyntöjä ja palauttaa niihin vastauksena tervehdyksen. Tervehdykseen liitetään pyynnössä tulevan nimi
-nimisen parametrin arvo.
package parametrit; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; @Controller public class TervehtijaController { @RequestMapping("/hei") @ResponseBody public String tervehdi(@RequestParam String nimi) { return "Hei " + nimi + ", mitä kuuluu?"; } }
Nyt esimerkiksi osoitteeseen http://localhost:8080/hei?nimi=Ada
tehtävä pyyntö saa vastaukseksi merkkijonon Hei Ada, mitä kuuluu?
.
Jos parametreja on useampia, erotellaan ne toisistaan &-merkillä. Seuraavassa esimerkissä pyynnössä on kolme parametria, eka
, toka
ja kolmas
, joiden arvot ovat 1
, 2
ja 3
vastaavasti.
http://localhost:8080/salaisuus?eka=1&toka=2&kolmas=3
Kaikki pyynnössä olevat parametrit saa pyyntöä käsittelevät metodin käyttöön samalla @RequestParam
-annotaatiolla. Allaolevassa esimerkissä kaikki pyynnön parametrit asetetaan Map
-tietorakenteeseen, jonka jälkeen kaikki pyynnön arvojen avaimet palautetaan kysyjälle.
package parametrit; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; @Controller public class PyyntoParametrienNimetController { @RequestMapping("/nimet") @ResponseBody public String nimet(@RequestParam Map<String, String> parametrit) { return parametrit.keySet().toString(); } }
Hello Request Params
Toteuta pakkauksessa wad.hellorequestparams
olevaan luokkaan HelloRequestParamsController
seuraava toiminnallisuus:
- Pyyntö polkuun
/hello
palauttaa käyttäjälle merkkijonon "Hello ", johon on liitettyparam
-nimisen parametrin sisältämä arvo. - Pyyntö polkuun
/params
palauttaa käyttäjälle kaikkien pyynnön mukana tulevien parametrien nimet ja arvot.
Alla olevassa kuvassa on esimerkki tilanteesta, missä selaimella on tehty pyyntö polkuun /params?hello=world&it=works
Palauta tehtävä TMC:lle kun olet valmis.
Calculator
Toteuta tässä tehtävässä pakkauksessa wad.calculator
sijaitsevaan CalculatorController
-luokkaan seuraava toiminnallisuus:
- Pyyntö polkuun
/add
laskee parametrienfirst
jasecond
arvot yhteen ja palauttaa vastauksen käyttäjälle. Huomaa että arvot ovat numeroita, ja ne tulee myös käsitellä numeroina. - Pyyntö polkuun
/multiply
kertoo parametrienfirst
jasecond
arvot ja palauttaa vastauksen käyttäjälle. - Pyyntö polkuun
/sum
laskee kaikkien parametrien arvot yhteen ja palauttaa vastauksen käyttäjälle.
Alla on esimerkki ohjelman toiminnasta, kun pyyntö tehdään /sum
-polkuun.
Palauta tehtävä TMC:lle kun olet valmis.
Näkymät ja data
Tähän asti luomamme sovellukset ovat vastaanottaneet tiettyyn polkuun tulevan pyynnön ja palauttaneet käyttäjälle merkkijonomuodossa olevaa tietoa. Tämä ei kuitenkaan ole ainoa palvelinohjelmistojen toimintatyyppi, vaan 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 HTML-kielellä siten, että HTML-kielen sekaan on upotettu komentoja, joiden perusteella näkymään lisätään palvelimen tuottamaa tietoa.
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-sivut, jonka jälkeen sivuille lisätään komentoja Thymeleafin käsiteltäväksi.
Thymeleaf-sivut ("templatet") sijaitsevat tällä kurssilla projektin kansiossa src/main/resources/templates
tai sen alla olevissa kansioissa. NetBeansissa kansio löytyy kun klikataan "Other Sources"-kansiota.
Allaolevassa esimerkissä luodaan juuripolkua /
kuunteleva sovellus. Kun sovellukseen tehdään pyyntö, palautetaan HTML-sivu, jonka Thymeleaf käsittelee. Thymeleaf päättelee palautettavan sivun metodin palauttaman merkkijonon perusteella. Alla metodi palauttaa merkkijonon "index"
, jolloin Thymeleaf etsii kansiosta src/main/resources/templates/
sivua index.html
. Kun sivu löytyy, Thymeleaf käsittelee sen ja palauttaa sen käyttäjälle. Palaamme tarkemmin tähän käsittelyyn myöhemmin.
package thymeleaf; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; @Controller public class ThymeleafController { @RequestMapping("/") public String home() { return "index"; } }
Huomaa, että pyyntöjä käsittelevällä metodilla ei enää ole annotaatiota @ResponseBody
. Emme siis enää halua, että metodin palauttama arvo näytetään suoraan käyttäjälle, vaan haluamme, että käyttäjälle näytetään merkkijonon ilmaisema näkymä. Näkymä luodaan Thymeleafin avulla.
Hello Thymeleaf
Toteuta tässä tehtävässä pakkauksessa wad.hellothymeleaf
sijaitsevaan HelloThymeleafController
-luokkaan seuraava toiminnallisuus:
- Pyyntö juuripolkuun
/
palauttaa käyttäjälle Thymeleafin avulla kansiossasrc/main/resources/templates/
olevanindex.html
-tiedoston. - Pyyntö polkuun
/video
palauttaa käyttäjälle Thymeleafin avulla kansiossasrc/main/resources/templates/
olevanvideo.html
-tiedoston.
Alla on esimerkki ohjelman toiminnasta, kun selaimella on tehty pyyntö sovelluksen juuripolkuun.
Palauta tehtävä TMC:lle kun olet valmis.
Datan lisääminen näkymään
Palvelinohjelmistossa luodun tai haetun datan lisääminen näkymään tapahtuu Model-tyyppisen 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.RequestMapping; public class ThymeleafJaDataController { @RequestMapping("/") public String home(Model model) { return "index"; } }
Model on Spring-sovelluskehyksen käyttämä Map-rajapinnan toimintaa muistuttava lokerikko, missä jokaisella lokerolla on nimi, mihin arvon voi asettaa. Alla olevassa esimerkissä määrittelemme pyyntöjä käsittelevälle metodille Model-olion, jonka jälkeen lisäämme lokeroon nimeltä teksti
arvon "Hei mualima!"
.
package thymeleafdata; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.RequestMapping; @Controller public class ThymeleafJaDataController { @RequestMapping("/") 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ä myös Model-olion ja siihen lisätyt arvot.
Sivun käsittely Thymeleafissa
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 (testi) tulee korvata attribuutin arvon ilmaisemalla muuttujalla. Attribuutin th:text
arvona on ${teksti}
, jolloin Thymeleaf etsii model
-oliosta avaimella "teksti"
arvoa.
Käytännössä Thymeleaf etsii siis Model-oliosta lokeron nimeltä teksti
ja asettaa siinä olevan arvon h2
-elementin tekstiarvoksi. Tässä tapauksessa teksti testi
korvataan Model-olion lokerosta teksti löytyvällä arvolla, eli tekstillä Hei mualima!
.
Hello Model
Tehtäväpohjan mukana tulevaan HTML-tiedostoon on toteutettu tarina, joka tarvitsee otsikon ja päähenkilön. Toteuta pakkauksessa wad.hellomodel
sijaitsevaan HelloModelController
-luokkaan toiminnallisuus, joka käsittelee juuripolkuun tulevia pyyntöjä ja käyttää pyynnössä tulevia parametreja tarinan täydentämiseen. Voit olettaa, että pyynnön mukana tulevien parametrien nimet ovat title
ja person
.
Lisää pyynnön mukana tulevien parametrien arvot Thymeleafille annettavaan HashMappiin. Otsikon avaimen tulee olla "title"
ja henkilön avaimen tulee olla "person"
. Palautettava sivu on index.html
.
Alla on esimerkki ohjelman toiminnasta, kun juuripolkuun tehdyssä pyynnössä on annettuna otsikoksi Mökkielämää
ja henkilöksi Leena
.
Palauta tehtävä TMC:lle kun olet valmis.
Tiedon lähettäminen palvelimelle
HTML-sivuille voi määritellä lomakkeita (form), joiden avulla käyttäjä voi lähettää tietoa palvelimelle. Lomakkeen määrittely tapahtuu form
-elementin avulla, jolle kerrotaan polku, mihin lomake lähetetään (action), sekä pyynnön tyyppi (method). Pidämme pyynnön tyypin toistaiseksi GET-tyyppisenä.
Lomakkeeseen voidaan määritellä mm. tekstikenttiä (<input type="text"...
) sekä painike, jolla lomake lähetetään (<input type="submit"...
). Alla tekstikentän name
-attribuutin arvoksi on asetettu nimi
. Tämä tarkoittaa sitä, että kun lomakkeen tiedot lähetetään palvelimelle, tulee pyynnössä nimi
-niminen parametri, jonka arvona on tekstikenttään kirjoitettu teksti.
<form th:action="@{/}" method="GET"> <input type="text" name="nimi"/> <input type="submit"/> </form>
Hello Form
Tehtäväpohjassa on toiminnallisuus, jonka avulla sivulla voi näyttää tietoa, ja jonka avulla sivulta lähetetty tieto voidaan myös käsitellä. Tiedon lähettämiseen tarvitaan sivulle kuitenkin lomake.
Toteuta tehtäväpohjan kansiossa src/main/resources/templates
olevaan index.html
-tiedostoon lomake. Lomakkeessa tulee olla tekstikenttä, jonka nimen tulee olla content
. Tämän lisäksi, lomakkeessa tulee olla myös nappi, jolla lomakkeen voi lähettää. Lomakkeen tiedot tulee lähettää juuriosoitteeseen GET-tyyppisellä pyynnöllä.
Kun sovellus toimii oikein, voit vaihtaa sivulla näkyvää otsikkoa lomakkeen avulla.
Listojen käsittely
Thymeleafille annettavalle Model-oliolle voi asettaa tekstin lisäksi 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ä. Jos juuripolkuun lähetetään parametri nimeltä "content"
, lisätään se myös listaan -- alla parametri on määritelty ei-pakolliseksi annotaation @RequestParam
attribuutilla required = false
.
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.RequestMapping; 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!"); } @RequestMapping(value = "/") public String home(Model model, @RequestParam(required = false) String content) { if(content != null && !content.trim().isEmpty()) { this.lista.add(content); } 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.
<pre> <p th:each="alkio : ${lista}"> <span th:text="${alkio}">hello world!</span> </p> </pre>
Yllä käytämme attribuuttia nimeltä lista
ja tulostamme yksitellen sen sisältämät alkiot. 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>
Huom! Eräs klassinen virhe on määritellä iteroitava joukko merkkijonona th:each="alkio : lista"
. Tämä ei luonnollisesti toimi.
Hello List
Tehtäväpohjassa on palvelinpuolen toiminnallisuus, jossa käsitellään juuripolkuun tuleva pyyntö, sekä lisätään lista Thymeleafille sivun käsittelyyn. Tehtäväpohjaan liittyvä HTML-sivu ei kuitenkaan sisällä juurikaan toiminnallisuutta.
Lisää HTML-sivulle (1) listalla olevien arvojen tulostaminen th:each
-komennon avulla ja (2) lomake, jonka avulla palvelimelle voidaan lähettää uusia arvoja.
Alla on esimerkki ohjelman toiminnasta, kun sivulle on lisätty muutama rivi lomakkeen avulla. Viimeisimpänä on juuri lisätty teksti "Hello?".
Notebook
Toteuta tehtäväpohjan pakkauksessa wad.notebook
olevaan NotebookController
-luokkaan muistio-ohjelma, jolle voi lisätä muistiinpanoja. Tee ohjelmastasi sellainen, että jos muistiinpanoja on yli 10, se muistaa ja näyttää niistä vain viimeisimmät 10.
Alla on esimerkki muistiosta, kun siihen on lisätty 3 viestiä.
Pyynnön uudelleenohjaus ja POST
Olemme tähän mennessä toteuttaneet palvelinohjelmistoihimme vain kyvyn käsitellä GET-tyyppisiä pyyntöjä. GET-tyyppisiä pyyntöjä käytetään ensisijaisesti tiedon hakemiseen, eikä niitä oikeastaan pitäisi käyttää laisinkaan tiedon muuttamiseen. Toinen tapa lähettää tietoa palvelimelle on POST
-tyyppiset pyynnöt, joita käytettäessä pyynnön parametrit kulkevat pyynnön rungossa -- palaamme pyynnön erilaisiin muotoihin myöhemmin kurssilla.
Oikeastaan kaikki pyynnöt, joissa lähetetään tietoa palvelimelle, ovat ongelmallisia jos pyynnön vastauksena palautetaan näytettävä sivu. Tällöin käyttäjä voi sivun uudelleenlatauksen (esim. painamalla F5) yhteydessä lähettää aiemmin lähettämänsä datan vahingossa uudelleen. Kokeile tätä jossain aiemmassa tehtävässä kun olet lähettänyt lomakkeella tietoa!
On tyylikkäämpää toteuttaa lomakkeen dataa vastaanottava toiminnallisuus siten, että lähetetyn tiedon käsittelyn jälkeen käyttäjälle palautetaan vastauksena uudelleenohjauspyyntö. Tämän jälkeen käyttäjän selain tekee uuden pyynnön uudelleenohjauspyynnön mukana annettuun osoitteeseen. Tätä toteutustapaa kutsutaan Post/Redirect/Get-suunnittelumalliksi ja sillä mm. estetään lomakkeiden uudelleenlähetys, jonka lisäksi vähennetään toiminnallisuuden toisteisuutta.
POST-pyynnön kuuntelu ja uudelleenohjaus
Alla on toteutettu POST-tyyppistä pyyntöä kuunteleva polku sekä siihen liittyvä toiminnallisuus. Pyynnön tyyppi määritellään annotaation @RequestMapping attribuutiksi (method-attribuutti). Tällöin kuunneltava polku tulee määritellä myös tarkemmin (value-attribuutti). Palauttamalla pyyntöä käsittelevästä metodista merkkijono redirect:/
kerrotaan, että pyynnölle tulee lähettää vastauksena uudelleenohjauspyyntö polkuun "/"
. Kun selain vastaanottaa uudelleenohjauspyynnön, tekee se GET-tyyppisen pyynnön uudelleenohjauspyynnössä annettuun osoitteeseen.
package uudelleenohjaus; import java.util.List; import java.util.ArrayList; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.RequestMapping; 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!"); } @RequestMapping("/") public String home(Model model) { model.addAttribute("list", lista); return "index"; } @RequestMapping(value = "/", method = RequestMethod.POST) public String post(@RequestParam String content) { if(!content.trim().isEmpty()) { this.lista.add(content); } return "redirect:/"; } }
Hello POST/Redirect/GET
Tehtäväpohjassa on sekä muistilappujen listaamistoiminnallisuus, että lomake, jonka avulla voidaan lähettää POST-tyyppisiä pyyntöjä palvelimelle. Toteuta sovellukseen toiminnallisuus, missä palvelin kuuntelee POST-tyyppisiä pyyntöjä, lisää pyynnön yhteydessä tulevan tiedon sovelluksessa olevaan listaan ja uudelleenohjaa käyttäjän tekemään GET-tyyppisen pyynnön juuriosoitteeseen.
Olioita kaikkialla!
Thymeleafille annettavaan Modeliin voi hyvin lisätä myös olioita. Oletetaan, että käytössämme on henkilöä kuvaava luokka.
public class Henkilo { private String nimi; public Henkilo(String nimi) { this.nimi = nimi; } public String getNimi() { return this.nimi; } public void setNimi(String nimi) { this.nimi = nimi; } }
Henkilo-olion lisääminen on suoraviivaista:
@RequestMapping("/") 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 (esim.) merkkijonon Henkilo@29453f44
-- ei ihan mitä toivoimme. Käytännössä Thymeleaf kutsuu edellisessä tapauksessa olioon liittyvää toString
-metodia, jota emme ole määritelleet. Pääsemme oliomuuttujiin käsiksi olemassaolevien getMuuttuja
-metodien kautta. Jos haluamme tulostaa Henkilo-olioon liittyvän nimen, kutsumme metodia getNimi
. 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
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 jo tullut aiemmin tutuksi.
<pre> <p th:each="alkio : ${lista}"> <span th:text="${alkio}">hello world!</span> </p> </pre>
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.RequestMapping; @Controller public class HenkiloController { private List<Henkilo> henkilot; public ListaController() { this.henkilot = new ArrayList<>(); this.henkilot.add(new Henkilo("James Gosling")); this.henkilot.add(new Henkilo("Martin Odersky")); } @RequestMapping("/") 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>
Hello Objects
Tehtäväpohjassa on sovellus, jossa käsitellään Item
-tyyppisiä olioita. Tehtävänäsi on lisätä sovellukseen lisätoiminnallisuutta:
- Kun käyttäjä avaa selaimella sovelluksen juuripolun, tulee hänen lomakkeen lisäksi nähdä lista esineistä. Jokaisesta esineestä tulee tulla ilmi sen nimi (name) ja tyyppi (type).
- Kun käyttäjä lähettää lomakkeella uuden esineen palvelimelle, tulee palvelimen säilöä esine listalle seuraavaa näyttämistä varten. Huomaa, että lomake lähettää tiedot POST-pyynnöllä sovelluksen juureen. Kun esine on säilötty, uudelleenohjaa käyttäjän pyyntö siten, että käyttäjän selain tekee GET-tyyppisen pyynnön sovelluksen juuripolkuun.
Allaolevassa esimerkissä sovellukseen on lisätty olemassaolevan taikurin hatun lisäksi Party hat, eli bilehattu.
Polkumuuttujat
Polkuja käytetään erilaisten resurssien tunnistamiseen ja yksilöintiin. Usein kuitenkin vastaan tulee tilanne, missä luodut resurssit ovat uniikkeja, emmekä niiden tietoja ennen sovelluksen käynnistymistä. Jos haluaisimme näyttää tietyn resurssin tiedot, voisimme lisätä pyyntöön parametrin -- esim esineet?tunnus=3
, minkä arvo olisi haetun resurssin tunnus.
Toinen vaihtoehto on ajatella polkua haettavan resurssin tunnistajana. Annotaatiolle @RequestMapping
määriteltävään polkuun voidaan määritellä polkumuuttuja aaltosulkujen avulla. Esimerkiksi polku "/{arvo}"
ottaisi vastaan minkä tahansa juuripolun alle tulevan kyselyn ja tallentaisi arvon myöhempää käyttöä varten. Tällöin jos käyttäjä tekee pyynnön esimerkiksi osoitteeseen http://localhost:8080/kirja
, tallentuu arvo "kirja" myöhempää käyttöä varten. Polkumuuttujiin pääsee käsiksi pyyntöä käsittelevälle metodille määriteltävän annotaation @PathVariable avulla.
Yksittäisen henkilön näyttäminen onnistuisi esimerkiksi seuravavasti:
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.RequestMapping; @Controller public class HenkiloController { private List<Henkilo> henkilot; public ListaController() { this.henkilot = new ArrayList<>(); this.henkilot.add(new Henkilo("James Gosling")); this.henkilot.add(new Henkilo("Martin Odersky")); } @RequestMapping("/") public String home(Model model) { model.addAttribute("list", henkilot); return "index"; } @RequestMapping("/{id}") public String getOne(Model model, @PathVariable Integer id) { if(id < 0 || id >= this.henkilot.size()) { return home(model); } model.addAttribute("henkilo", henkilot.get(id)); return "henkilo"; } }
Hello Path Variables
Tehtäväpohjassa on sovellus, jossa käsitellään taas edellisestä tehtävästä tuttuja Item
-tyyppisiä olioita. Tällä kertaa esineet kuitenkin kuvastavat hattuja. Kun sovelluksen juureen tehdään pyyntö, käyttäjälle näytetään oletushattu ("default"). Lisää sovellukseen toiminnallisuus, minkä avulla tiettyyn polkuun tehtävä kysely palauttaa sivun, jossa näkyy tietyn hatun tiedot -- huomaa, että voit asettaa polkumuuttujan tyypiksi myös Stringin.
Sovelluksen juuripolkuun tehtävä pyyntö näyttää seuraavanlaisen sivun:
Muihin osoitteisiin tehtävät pyynnöt taas palauttavat tehtäväpohjassa olevasta items
-hajautustaulusta polkuun sopivan hatun. Esimerkiksi pyyntö polkuun /ascot
näyttää seuraavanlaisen sivun:
Hello Individual Pages
Edellisessä tehtävässä käytössämme oli vain yksi sivu. Olisi kuitenkin hienoa, jos jokaiselle hatulle olisi oma sivu -- ainakin sovelluksen käyttäjän näkökulmasta.
Tehtäväpohjassa on valmiina sovellus, joka listaa olemassaolevat hatut ja näyttää ne käyttäjälle. Jokaisen hatun yhteydessä on linkki, jota klikkaamalla pitäisi päästä hatun omalle sivulle.
Toteuta sekä html-sivu (single.html
), että sopiva metodi, joka ohjaa pyynnön sivulle.
Pyyntö sovelluksen juureen luo seuraavanlaisen sivun.
Jos sivulta klikkaa hattua, pääsee tietyn hatun tiedot sisältävälle sivulle. Alla olevassa esimerkissä on klikattu taikurin hattuun liittyvää linkkiä.
Todo Application
Tässä tehtävässä tulee rakentaa tehtävien hallintaan tarkoitettu sovellus. Sovelluksen käyttämät sivut ovat valmiina näkyvissä, itse sovelluksen pääset toteuttamaan itse.
Sovelluksen tulee sisältää seuraavat toiminnallisuudet:
- Kaikkien tehtävien listaaminen. Kun käyttäjä tekee pyynnön sovelluksen juuripolkuun, tulee hänelle näyttää sivu, missä tehtävät on listattuna. Sivulla on myös lomake tehtävien lisäämiseen.
- Yksittäisen tehtävän lisääminen. Kun käyttäjä täyttää lomakkeen sivulla ja lähettää tiedot palvelimelle, tulee sovelluksen lisätä tehtävä näytettävään listaan.
- Yksittäisen tehtävän poistaminen. Kun käyttäjä painaa tehtävään liittyvää
Done!
-nappia, tulee tehtävä poistaa listalta. Toteuta tämä niin, että metodin tyyppi onDELETE
:@RequestMapping(value = "/{item}", method = RequestMethod.DELETE)
- Yksittäisen tehtävän näyttäminen. Kun käyttäjä klikkaa tehtävään liittyvää linkkiä, tulee käyttäjälle näyttää tehtäväsivu. Huom! Tehtävään liittyvien tarkistusten määrä tulee kasvaa aina yhdellä kun sivulla vieraillaan.
Alla kuva tehtävien listauksesta:
Kun tehtävää klikkaa, näytetään erillinen tehtäväsivu:
Kun sivu avataan toisen kerran, kasvaa tehtävien tarkistukseen liittyvä laskuri:
Tiedon tallentaminen
Sovelluksemme -- vaikka huikeita ovatkin -- ovat melko alkeellisia, sillä sovelluksissa käsiteltävää tietoa ei tallenneta mihinkään. Esimerkiksi lomakkeen avulla sovellukselle lähetettävä data katoaa kun sovellus käynnistetään uudestaan. Tämä ei ole kivaa.
Tietokannat ovat palvelinohjelmistosta erillisiä sovelluksia, joiden ensisijainen tehtävä on varmistaa, että käytettävä tieto ei katoa. Otetaan ensiaskeleet tietokannan käyttöön web-palvelinohjelmistoissa -- tutustumme tietokantoihin tarkemmin myöhemmin kurssilla. Käytämme tietokantatoiminnallisuuden toteuttamisessa Spring Data JPA-komponenttia, johon löytyy myös aloituspaketti käyttämästämme Spring Bootista.
Tietokantaan tallennettavat oliot eli entiteetit
Käytämme ORM-kirjastoa (object relational mapping), jonka tehtävänä on muuntaa oliot tietokantaan tallennettavaan muotoon. Karkeasti ajatellen luokka vastaa tietokantataulua ja oliomuuttujat vastaavat tietokannan sarakkeita. Jokainen taulun rivi vastaa yhtä luokasta tehtyä oliota.
Luokat, joista tehdyt oliot voidaan tallentaa tietokantaan, tulee annotoida @Entity
-annotaatiolla. Tämän lisäksi luokille tulee määritellä tunnuskenttä, jonka avulla niihin liittyvät oliot voidaan yksilöidä. Voimme käyttää tunnuskentän luomiseen valmista AbstractPersistable
-yliluokkaa, jota perittäessä kerromme uniikin tunnuksen tyypin. Esimerkiksi Henkilo
-luokasta voidaan tehdä tietokantaan tallennettava seuraavilla muutoksilla.
package wad.domain; import javax.persistence.Entity; import org.springframework.data.jpa.domain.AbstractPersistable; @Entity public class Henkilo extends AbstractPersistable<Long> { private String nimi; public String getNimi() { return this.nimi; } public void setNimi(String nimi) { this.nimi = nimi; } }
Kun käytössämme on tietokantaan tallennettava luokka, voimme luoda tietokannan käsittelyyn käytettävän rajapinnan. Kutsutaan tätä rajapintaoliota nimellä HenkiloRepository
.
// pakkaus import wad.domain.Henkilo; import org.springframework.data.jpa.repository.JpaRepository; public interface HenkiloRepository extends JpaRepository<Henkilo, Long> { }
Rajapinta perii Spring Data-projektin JpaRepository
-rajapinnan; samalla kerromme, että tallennettava olio on tyyppiä Henkilo
ja että tallennettavan olion tunnus on Long
-tyyppiä. Tämä tyyppi on sama kuin aiemmin AbstractPersistable
-luokan perinnässä parametriksi asetettu tyyppi. Spring osaa käynnistyessään etsiä mm. JpaRepository-rajapintaluokan periviä luokkia. Jos niitä löytyy, se luo niiden pohjalta tietokannan käsittelyyn sopivan olion sekä asettaa olion ohjelmoijan haluamiin muuttujiin.
Nämä muuttujat tulee määritellä @Autowired
-annotaatiolla -- jokaiselle muuttujalle tulee oma annotaatio -- palaamme myöhemmin kurssilla tarkemmin tähän ns. olioiden automaattiseen asettamiseen.
Kun olemme luoneet rajapinnan HenkiloRepository
, voimme lisätä sen käyttöömme esimerkiksi kontrolleriluokkaan. Tämä tapahtuu seuraavasti.
// ... @Controller public class HenkiloController { @Autowired private HenkiloRepository henkiloRepository; // ... }
Nyt tietokantaan pääsee käsiksi HenkiloRepository
-olion kautta. Osoitteessa http://docs.spring.io/spring-data/jpa/docs/current/api/org/springframework/data/jpa/repository/JpaRepository.html on JpaRepository-rajapinnan API-kuvaus, mistä löytyy rajapinnan tarjoamien metodien kuvauksia. Voimme esimerkiksi toteuttaa tietokannassa olevien olioiden listauksen sekä yksittäisen olion haun seuraavasti:
// ... @Controller public class HenkiloController { @Autowired private HenkiloRepository henkiloRepository; @RequestMapping(method = RequestMethod.GET) public String list(Model model) { model.addAttribute("list", henkiloRepository.findAll()); return "henkilot"; // erillinen henkilot.html } @RequestMapping(value = "/{id}", method = RequestMethod.GET) public String get(Model model, @PathVariable Long id) { model.addAttribute("henkilo", henkiloRepository.findOne(id)); return "henkilo"; // erillinen henkilo.html } }
Hello Database
Tässä tehtävässä on valmiiksi toteutettuna tietokantatoiminnallisuus sekä esineiden noutaminen tietokannasta. Lisää sovellukseen toiminnallisuus, jonka avulla esineiden tallentaminen tietokantaan onnistuu valmiiksi määritellyllä lomakkeella.
Alla esimerkki sovelluksesta kun tietokantaan on lisätty muutama rivi:
Todo Database
Luo tässä TodoApplication-tehtävässä nähty tehtävien hallintaan tarkoitettu toiminnallisuus mutta siten, että tehtävät tallennetaan tietokantaan. Tässä tehtävässä entiteettiluokan nimen tulee olla Item
ja avaimen tyypin tulee olla Long
:
@Entity public class Item extends AbstractPersistable<Long> { ...
Noudata lisäksi HTML-sivujen rakennetta ja toiminnallisuutta.
the end.