Tämä materiaali on lisensoitu Creative Commons BY-NC-SA-lisenssillä, joten voit käyttää ja levittää sitä vapaasti, kunhan alkuperäisten tekijöiden nimiä ei poisteta. Jos teet muutoksia materiaaliin ja haluat levittää muunneltua versiota, se täytyy lisensoida samanlaisella vapaalla lisenssillä. Materiaalien käyttö kaupalliseen tarkoitukseen on ilman erillistä lupaa kielletty.
Tämä materiaali on tarkoitettu Helsingin yliopiston tietojenkäsittelytieteen laitoksen syksyn 2012 kurssille web-palvelinohjelmointi. Materiaaliin on vaikuttanut vahvasti Helsingin yliopistossa keväällä 2012 järjestetty kurssi web-sovellusohjelmointi, sekä siitä saatu palaute. Materiaalin kirjoittaja on Arto Vihavainen ja sen syntyyn ovat vaikuttaneet useat tahot, joista tärkeimmät ovat Matti Luukkainen ja Mikael Nousiainen. Syksyn kurssimateriaaliin liittyvistä ehdotuksista ja ajatuksista tulee kiittää muun muassa Kasper Hirvikoskea ja Hansi Keijosta. Erityinen kiitos kuuluu myös Martin Pärtelille, joka on mahdollistanut TMC:n käytön kurssilla.
Web-sovellukset koostuvat yleisesti ottaen kahdesta: palvelinpuolesta ja selainpuolesta. Palvelinpuoli hoitaa palvelimella tapahtuvan toiminnan, esimerkiksi tietokannan muokkauksen ja erilaiset laskentaoperaatiot. Selaimessa taas näytetään käyttäjälle data ja lähetetään tietoa palvelimelle, joka taas palauttaa käyttäjälle selaimessa näytettävää tietoa.
Käytännössä web-sovellukset koostuvat datasta jolle on määritelty ilmaisumuoto. Selainohjelmointiin ja käyttöliittymäsuunniteluun keskityttäessä painotetaan ulkoasun ja sisällön erotusta toisistaan CSS:n avulla, sekä luodaan interaktiivista toiminnallisuutta Javascriptiin ja nykyaikaisiin web-teknologioihin tukeutuen. HTML5 tukee videoiden näyttämistä, musiikin soittamista ja piirtämistä, ja WebGL taas laajentaa Canvas-toiminnallisuutta tuoden 3D-tuen ja -kiihdytyksen selaimiin.
Web-sovellukset eivät sisällä sivuja samalla tavalla kuin perinteiset web-sivut sisältävät. Vaikka web-sovelluksessa voi ulkopuolisen silmin olla esimerkiksi 5 sivua, uuden sisällön lisääminen tietokantaan tai vastaavaan tietoa tallentavaan järjestelmään mahdollistaa sivumäärän kasvattamisen ilman muutoksia lähdekoodissa. Käyttäjälle tarjotut hakutoiminnallisuudet mahdollistavat lähes rajattoman määrän sivuja; kukaan ei kirjoittaisi näitä käsin.
Pieni määrä sivupohjia (ns. templateja) ja palvelinpuolen sovelluslogiikkaa mahdollistaa sivujen luomisen lennosta suoraan web-osoitetta tai lomakkeella lähettyä dataa käyttäen.
Keskivertokäyttäjälle web-sovellukset näyttävät samalta kuin perinteiset web-sivut. Yksinkertaisen web-sovelluksen lähdekoodia katsottaessa voi olla mahdotonta päätellä ovatko sivut luotu dynaamisesti vai kirjoitettu jotain editoria käyttäen (tai käsin). Web-osoitteesta voi yrittää päätellä jotain (.html, ...), mutta oikeasti web-osoitteiden päätteet ovat vain osa osoitetta: .html -päätteisellä sivulla voi hyvin olla web-sovellus taustalla. Web-sovellus näyttää usein sovellukselta vain niille jotka muokkaavat sovellukseen liittyvää dataa. Sekä sovelluksen käyttö että datan muokkaus tapahtuu yleensä HTML-käyttöliittymää käyttäen. Web-sovellusta voi hallita myös työpöytäsovelluksella, joka muokkaa web-sovelluksen käyttämää dataa.
Javascript (käytännössä AJAX, Asynchronous JavaScript and XML) mahdollistaa sivujen sisällön uudelleen lataamisen ja päivittämisen ilman että käyttäjän tarvitsee tehdä erillisiä pyyntöjä tai kirjoittaa uutta osoitetta selaimen osoitepalkkiin. AJAX mahdollistaa muutosten tekemisen taustalla -- ilman että käyttäjän tarvitsee siirtyä sivulta toiselle -- joka pienentää web-sovellusten ja työpöytäsovellusten välistä eroa.
Työpöytäsovellukset tarjoavat enemmän interaktiivisuutta ja nopeutta web-sovelluksiin verraten. Web-sovellukset mahdollistavat saumattomat ohjelmistojen päivitykset, todellisen tiedot ja dokumenttien jakamisen ja kevyet käyttöliittymät.
Elämme jatkuvaa muutosta. Muutoksesta riippumatta web-sovellukset ovat järjestelmiä, jotka sisältävät dataa. Dataa voi käsitellä web-sivujen kautta ja muita rajapintoja käyttäen. Google tarjoaa Google Documents -palvelua ilmaiseksi kaikkien käyttöön, mahdollistaen käyttäjälle pääsyn dokumentteihin mistä tahansa. Microsoft tarjoaa kaikille ilmaista sähköpostipalvelua, verkko on täynnä ilmaisia webissä toimivia pelejä...
Tälläkin hetkellä ohjelmistoyhtiöt ympäri maailmaa kehittävät uusia innovaatioita, jotka tulevat tulevaisuudessa syrjäyttämään perinteiset työpöytäsovellukset.
Sovellusten arkkitehtuurista perinteisesti puhuttaessa puhutaan talojen tai rakennusten rakentamisesta. Taloa suunnitellessa arkkitehdillä on selkeä tehtävä ja tavoitteet: kerää vaatimukset, tutki vaihtoehtoja ja luo pohjapiirrustus. Pohjapiirrustusta seuraten erillinen joukko työntekijöitä -- rakennusmiehet -- rakentaa konkreettisen rakennuksen.
Ohjelmistoja suunniteltaessa arkkitehti osallistuu sekä ohjelmiston suunnitteluun että kehitykseen, eli konkreettiseen rakentamiseen. Suunnittelussa hän aloittaa perustarpeista ja muutamasta huoneesta, jonka jälkeen jo muutama ihminen alkaa käyttämään rakennusta. Kun alkuperäinen suunnitelma on lähes valmis, rakennukseen muuttaa lisää ihmisiä. Nämä tarvitsevat lisää rakennukselta: uusia huoneita, pesulan, diskon ja luonnollisesti oleskelutilan, jossa on tilaa biljardipöydälle. Arkkitehti soveltaa alkuperäistä suunnitelmaansa mukauttamaan uudet ihmiset ja kehitystyö jatkuu.
Toimintoja kehitettäessä alkuperäiset asukkaat eivät muuta pois, vaan valittavat jatkuvasta rakennusmelusta. Yhä enemmän ihmisiä muuttaa rakennukseen ja rakennukselta vaaditaan taas lisää (mm. cartingrata ja curlinghalli).
Hyvän arkkitehtuurisuunnittelun perusta on mahdollisuuksien huomiointi. Huomioinnilla ei tarkoiteta sitä, että rakennetaan heti aluksi iso järjestelmä -- käytännössä järjestelmän valmistuessa sille ei olisi käyttäjiä sillä kaikki olisivat siirtyneet toiseen aiemmin tarpeellisia ominaisuuksia tarjonneeseen järjestelmään. Jos alkuperäinen suunnitelma tekee järjestelmän laajentamisesta vaikeaa, käyttäjät saattavat vaihtaa palvelua hitauden takia.
Ohjelmistokehityksessä saadaan käytännössä hyvin harvoin ensimmäisellä yrityksellä rakennettua toimiva ja tyydyttävä ratkaisu. Jokaista ohjelmistoa joudutaan laajentamaan, rajaamaan ja refaktoroimaan. Asiakkaalla tai asiakkailla on käytännössä aina uusia toivomuksia ohjelmiston elinkaaren varrella.
Arkkitehtuurin tulee mahdollistaa sopivan kokoisesta palasta aloittaminen sekä rakennettavan sovelluksen laajentaminen, myös toisten kehittäjien toimesta, mahdollisimman kevyesti. Hyvin harvat ohjelmistot ovat vain yhden ihmisen käsialaa, avoimeen lähdekoodiin ja online-versionhallintatyökaluihin (esim github) perustuvat projektit saavat ihmiset eri puolilta maailmaa tekemään työtä yhteisen mielenkiinnon parissa. Ryhmätyössä sovittujen käytänteiden (esim. nimeämiskäytänteet, versionhallinta, testaus, dokumentointi ym.) olemassaolo on oleellista. Kommunikointi niin koodin kautta kuin muita väyliä käyttäen on oleellista -- muuttujanimet a, b, c, foo
ja bar
aiheuttavat lukijalle lähinnä kylmiä väreitä.
Web-sovelluskehityksessä nopeasta kehityssyklistä on paljon hyötyä. Työkaluja valittaessa tarkoituksena on välttää nurkkaan ajautumista: työkaluista tulee pystyä myös pääsemään eroon. On paljon hyödyllisempää miettiä asiaa päivä ja käyttää muutama päivä prototyypin tekemiseen, koska prototyyppiä voidaan parantaa kuukausia, kuin miettiä kuukausi ja sitouttaa itsensä kuukauden aikana luotuun suunnitelmaan. Mitä nopeammin toiminnallisuutta on olemassa, sitä nopeammin siitä saa palautetta. Mitä vähemmän käytämme aikaa yksittäisen toiminnallisuuden toteuttamiseen -- KISS -- sitä helpommin siitä voi tarpeen vaatiessa hankkiutua eroon.
Työkalut ovat oleellinen osa ohjelmistoryhmän yhteisten pelisääntöjä. Tällä kurssilla käytämme ohjelmointiympäristönä NetBeansia, ja ohjelmistoprojektien hallintaan Maven-projektinhallintatyökalua. Nykyaikaisestakin ohjelmistokehityksestä osa tapahtuu terminaalissa eli komentotulkissa, joten käytämme myös sitä kurssin aikana.
Oletamme että käytössäsi on Google Chrome tai vastaavat web-sovelluskehittäjän apuvälineet sisältävä www-selain.
Ohjelmistokehittäjän arjesta huomattava osa menee projekteihin liittyviin tehtäviin kuten ylläpitoon. Jokaisessa ohjelmistoprojektissa on erilaisia lähdekoodiin liittyviä tavoitteita, joita kehittäjien tulee pystyä toteuttamaan. Lähdekoodia tulee pystyä paketoimaan tuotantoon siirettäväksi paketiksi (esim -.jar ja -.war -tiedostot), lähdekoodiin liittyviä testejä tulee pystyä ajamaan erillisellä palvelimella ja lähdekoodista tulee pystyä generoimaan erilaisia raportteja sekä luonnollisesti dokumentaatiota.
Työkalut kuten Apache Ant auttavat projektiin liittyvän lähdekoodin hallinnoinnissa ja kääntämisessä. Ant on käytännössä 2000-luvun alun vastine perinteisille Makefile-tiedostoille. Nykyaikaisempi Apache Maven auttaa käännösprosessin lisäksi projektiin liittyvien kirjastoriippuvuuksien automaattisessa hallinnassa.
Apache Maven on projektinhallintatyökalu, jota voi käyttää ohjelmakoodikäännösten automatisoinnin lisäksi lähes koko projektin elinkaaren hallintaan uuden projektin aloittamisesta lähtien. Maven tarjoaa ohjelmiston elinkaaren hallintaan joukon valmiiksi konfiguroituja vaiheita (phase), joita voidaan suorittaa komentoriviltä. Tyypillisiä jo olemassaolevia vaiheita ovat test, joka suorittaa projektiin liittyvät testit, sekä package, joka paketoi lähdekoodin projektityypistä riippuen sopivaan pakettiin. Oikeastaan maven on sovelluskehys plugin-komponenttien suoritukseen, yksinkertaisimmatkin tehtävät on tehty pluginien avulla.
Jokaisella Maven-projektilla on elinkaari, joka sisältää vaiheet lähtien projektin validoinnista, kääntämisestä ja testaamisesta aina tuotantoon siirtämiseen asti. Tarkempi listaus projektin erilaisista vaiheista löytyy Mavenin dokumentaatiosta. Kukin vaihe koostuu yhdestä tai useammasta projektiin liittyvästä tavoitteesta (goal), jotka suoritetaan vaiheen sisällä. Vaiheet riippuvat myös edellisistä vaiheista, esimerkiksi vaihetta test suoritettaessa Maven suorittaa ensin projektin validoinnin ja kääntämisen.
Mavenin pluginarkkitehtuuri mahdollistaa hyvin monipuolisen toiminnallisuuden. Esimerkiksi raportointiin ja staattiseen koodianalyysiin löytyy omat pluginit, samoin kuin web-palvelimen käynnistämiselle projektin testausta varten. Mavenin plugineista löytyy (ei kattava) lista osoitteessa http://maven.apache.org/plugins/index.html.
Maven automatisoi uusien projektien luomisen archetype-pluginin avulla, jonka avulla käyttäjät voivat tarjota valmiita projektirunkoja toisilleen. Projektirungot toimivat pohjina uusille samaa arkkitehtuuria käyttäville projekteille. Runkojen avulla ohjelmistokehittäjät voivat jakaa hyviksi todettuja käytänteitä, ja ne helpottavat myös organisaatioiden sisällä tapahtuvaa erilaisiin projektityyppeihin liittyvien arkkitehtuurien ylläpitoa.
Ohjelmistokehittäjän näkökulmasta yksi tärkeimmistä ominaisuuksista on riippuvuuksien automaattinen lataaminen. Mavenin avulla projektiin voi konfiguroida kirjastoriippuvuuksia, esimerkiksi riippuvuudet yksikkötestauskirjastoihin ja web-sovelluskehyksen kirjastoihin. Maven lataa riippuvuudet automaattisesti, jolloin kirjastoja ei tarvitse pitää esimerkiksi versionhallinnassa.
"I just had to take the hypertext idea and connect it to the TCP and DNS ideas and – ta-da! – the World Wide Web." -- Tim Berners-Lee
Kolme internetin oleellisinta osaa ovat tapa yksilöidä palveluja ja palvelujen tarjoamia resursseja verkosta (DNS, Domain Name Services ja URI, Uniform Resource Identifier), protokolla viestien lähetykseen verkon yli (HTTP, HyperText Transfer Protocol) ja yhteinen dokumenttien esityskieli (HTML, HyperText Markup Language).
"The most important thing that was new was the idea of URI-or URL, that any piece of information anywhere should have an identifier, which will allow you to get hold of it." -- Tim Berners-Lee
Verkossa sijaitseva sivusto tunnistetaan sille annetun yksilöivän osoitteen perusteella. Osoite (URI eli Uniform Resource Identifier terminä käyttöön jäänyt URL Uniform Resource Locator) koostuu resurssin nimestä ja sijainnista, joiden perusteella kysyttyyn palvelimeen ja resurssiin voidaan muodostaa yhteys.
Periaatteessa palvelimelle tehtävää kyselyä voidaan ajatella metodikutsuna, jonka tulee palauttaa arvo tai heittää poikkeus.
Käytännössä kun käyttäjä kirjoittaa web-selaimen osoitekenttään URIn ja painaa enteriä, web-selain lähtee tekemään kyselyä annettuun osoitteeseen. Koska tekstimuotoiset osoitteet ovat käytännössä vain ihmisiä varten, tulee selaimen ensiksi kääntää haluttu osoite, esimerkiksi www.mooc.fi
, IP-osoitteeksi. Jos IP-osoite on jo tiedossa esimerkiksi aiemmin osoitteeseen tehdyn kyselyjen takia, selain voi ottaa yhteyden IP-osoitteeseen. Jos taas IP-osoite ei ole tiedossa, tulee selaimen ensin tehdä kysely DNS-palvelimelle (Domain Name System), jonka tehtävänä on muuntaa tekstuaaliset osoitteet IP-osoitteiksi (esim. http://www.cs.helsinki.fi -> 128.214.166.78
).
Ilman DNS-palvelimia ihmisten tulisi muistaa IP-osoitteet ulkoa, joka käytännössä tarkoittaisi ettei nykyinen internet toimisi.
IP-osoitteet yksilöivät tietokoneet, ja mahdollistavat koneiden löytämisen verkon yli. Käytännössä yhteys IP-osoitteen määrittelemään koneeseen avataan sovellustason HTTP-protokollan avulla kuljetustason TCP-protokollan yli. TCP-protokollan tehtävänä on varmistaa, että viestit pääsevät perille. Lisää tietoa konkreettisesta tietoliikenteestä kurssilla Tietoliikenteen perusteet.
Kun palvelin vastaanottaa tiettyyn resurssiin liittyvän pyynnön, palauttaa se vastauksen kyselijälle. Kun selain saa vastauksen, tarkistaa se vastaukseen liittyvän statuskoodin ja siihen liittyvät tiedot. Jos vastauksen saa tallettaa välimuistiin, tallennetaan se tulevaisuutta varten. Tämän jälkeen selain päättelee, mitä vastauksella tehdään, ja esimerkiksi renderöi vastaukseen liittyvän web-sivun käyttäjälle.
Käytännössä URIt näyttävät seuraavilta:
protokolla://isäntäkone[:portti]/polku/../[kohdedokumentti][?kyselyparametrit][#ankkuri]
Osoitteen osat
Tutki osoitetta http://www.googlefight.com/index.php?lang=en_GB&word1=Batman&word2=Superman. Mitkä tai mikä ovat/on osoitteen:
Mitkä näistä puuttuvat?
HTTP (HyperText Transfer Protocol) on TCP/IP -protokollapinon sovellustason protokolla. Web-palvelimet ja selaimet keskustelevat HTTP-protokollaa käyttäen. HTTP-protokolla perustuu asiakas-palvelin malliin, jossa jokaista pyyntöä kohden on yksi vastaus (request-response paradigm). Käytännössä HTTP-asiakasohjelma (jatkossa selain) lähettää HTTP-viestin HTTP-palvelimelle (jatkossa palvelin), joka palauttaa HTTP-vastauksen. HTTP-protokollan versio 1.1 on määritelty RFC 2616-spesifikaatiossa.
Asiakas-palvelin -mallissa (Client-Server model) asiakkaat käyttävät palvelimen tarjoamia palveluja. Kommunikointi asiakkaan ja palvelimen välillä tapahtuu usein verkon yli siten, että asiakasohjelmisto ja palvelinohjelmisto sijaitsevat erillisissä fyysisissä sijainneissa (eri tietokoneilla). Palvelinohjelmisto tarjoaa yhden tai useamman palvelun, joita asiakasohjelmisto käyttää.
Käytännössä asiakasohjelmisto tarjoaa käyttöliittymän ohjelmiston loppukäyttäjälle. Asiakasohjelmiston käyttäjän ei tarvitse tietää, että kaikki käytetty tieto ei ole hänen koneella. Käyttäjän tehdessä toiminnon asiakasohjelmisto pyytää tarpeen vaatiessa palvelimelta käyttäjän tarpeeseen liittyvää lisätietoa. Tyypillistä mallille on se, että palvelin tarjoaa vain asiakkaan pyytämät tiedot. Tällöin verkossa liikkuvan tiedon määrä pysyy vähäisenä.
Asiakas-palvelin -malli mahdollistaa hajautetut ohjelmistot: asiakasohjelmistoa käyttävät loppukäyttäjät voivat sijaita eri puolilla maapalloa palvelinohjelmiston sijaitessa tietyssä paikassa.
Haasteena perinteisessä asiakas-palvelin mallissa on se, että palvelin sijaitsee yleensä tietyssä keskitetyssä sijainnissa. Keskitetyillä palveluilla on mahdollisuus ylikuormittua asiakasmäärän kasvaessa. Kapasiteettia rajoittavat muunmuassa palvelimen fyysinen kapasiteetti (rauta), palvelimeen yhteydessä olevan verkon laatu ja nopeus, sekä tarjotun palvelun tyyppi. Esimerkiksi tietokantatransaktiota vaativat pyynnöt vievät huomattavasti enemmän aikaa kuin yksinkertaiset lukuoperaatiot. Asiakas-palvelin mallissa tuottaa haasteita myös vikasietoisuus, miten toimia jos palvelinkoneesta hajoaa esimerkiksi kovalevy?
HTTP-protokollan yli lähetettävät viestit ovat tekstimuotoisia. Viestit koostuvat riveistä jotka muodostavat otsakkeen, sekä riveistä jotka muodostavat viestin rungon. Viestin runkoa ei ole pakko olla olemassa. Viestin loppuminen ilmoitetaan kahdella peräkkäisellä rivinvaihdolla. Palvelimelle lähetettävän viestin, eli kyselyn, ensimmäisellä rivillä on pyyntötapa, halutun resurssin polku ja HTTP-protokollan versionumero.
PYYNTÖTAPA /POLKU_HALUTTUUN_RESURSSIIN HTTP/versio otsake-1: arvo otsake-2: arvo valinnainen viestin runko
Pyyntötapa kertoo HTTP-protokollassa käytettävän pyynnön tavan (esim. GET
tai POST
), polku haluttuun resurssiin kertoo haettavan resurssin sijainnin palvelimella (esim. /index.html
), ja HTTP-versio kertoo käytettävän version (esim. HTTP/1.0
). Alla esimerkki hyvin yksinkertaisesta -- joskin yleisestä -- pyynnöstä. Huomaa että pyyntöä tehdessä yhteys palvelimeen on jo muodostettu, eli palvelimen osoitetta ei merkitä erikseen.
GET /index.html HTTP/1.0
Yksittäisen koneen dedikointi web-palvelimeksi jättää usein huomattavan osan koneen kapasiteetista käyttämättä. Nykyään yleisesti käytössä oleva HTTP/1.1 -protokolla mahdollistaa useamman palvelimen pitämisen samalla koneella virtuaalipalvelintekniikan avulla, jolloin yksittäiset palvelinkoneet voivat sisältää useita palvelimia. Käytännössä IP-osoitetta kuunteleva kone voi joko itsessään sisältää useita ohjelmistoilla emuloituja palvelimia, tai se voi toimia reitittimenä ja ohjata pyynnön tietylle esimerkiksi yrityksen sisäverkossa sijaitsevalle koneelle. Kun yksittäinen IP-osoite voi sisältää useampia palvelimia, pelkkä polku haluttuun resurssiin ei riitä oikean resurssin löytämiseen: resurssi voisi olla millä tahansa koneeseen liittyvällä virtuaalipalvelimella. HTTP/1.1 -protokollassa on pyynnöissä pakko olla mukana käytetyn palvelimen osoitteen kertova Host
-otsake.
GET /index.html HTTP/1.1 Host: www.munpalvelin.net
Palvelimelle tehtyyn pyyntöön saadaan aina jonkinlainen vastaus. Jos tekstimuotoiseen osoitteeseen ei ole liitetty IP-osoitetta DNS-palvelimilla, selain ilmoittaa ettei palvelinta löydy. Jos palvelin löytyy, ja pyyntö saadaan tehtyä palvelimelle asti, tulee palvelimen myös vastata jollain tavalla.
Palvelimelta saatavan vastauksen sisältö on seuraavanlainen. Ensimmäisellä rivillä HTTP-protokollan versio, viestiin liittyvä statuskoodi, sekä statuskoodin selvennys. Tämän jälkeen on joukko otsakkeita, tyhjä rivi, ja mahdollinen vastausrunko. Vastausrunko ei ole pakollinen.
HTTP/versio statuskoodi selvennys otsake-1: arvo otsake-2: arvo valinnainen vastauksen runko
Esimerkiksi:
HTTP/1.1 200 OK Date: Sat, 07 Jan 2012 03:12:45 GMT Server: Apache/2.2.14 (Ubuntu) Vary: Accept-Encoding Content-Length: 973 Connection: close Content-Type: text/html;charset=UTF-8 .. runko ..
Statuskoodit (status code) kuvaavat palvelimella tapahtunutta toimintaa kolmella numerolla. Statuskoodien avulla palvelin kertoo mahdollisista ongelmista tai tarvittavista lisätoimenpiteistä. Yleisin statuskoodi on 200
, joka kertoo kaiken onnistuneen oikein. HTTP/1.1 sisältää viisi kategoriaa vastausviesteihin.
Lisätietoja statuskoodeista osoitteissa http://httpcats.herokuapp.com.
HTTP-protokolla määrittelee kahdeksan erillistä pyyntötapaa (Request method). Yleisimmin käytetyt ovat GET
, POST
ja HEAD
, joiden lisäksi on olemassa PUT
, DELETE
, TRACE
, OPTIONS
ja CONNECT
. Pyyntötavat määrittelevät rajoitteita ja suosituksia viestin rakenteeseen ja niiden prosessointiin palvelinpäässä. Esimerkiksi Java Servlet API (versio 2.5) sisältää seuraavan suosituksen GET-pyyntotapaan liittyen:
The GET method should be safe, that is, without any side effects for which users are held responsible. For example, most form queries have no side effects. If a client request is intended to change stored data, the request should use some other HTTP method.
The GET method should also be idempotent, meaning that it can be safely repeated. Sometimes making a method safe also makes it idempotent. For example, repeating queries is both safe and idempotent, but buying a product online or modifying data is neither safe nor idempotent.
Suomeksi yksinkertaistaen: GET
-tyyppisten pyyntöjen ei pitäisi muuttaa palvelimella olevaa dataa.
GET
GET on yksinkertaisin pyyntötapa. Sitä käytetään esimerkiksi dokumenttien hakemiseen: kun kirjoitat osoitteen selaimen osoitekenttään ja painat enter, selain tekee GET-pyynnön. GET-pyynnöt eivät tarvitse otsaketietoja HTTP/1.1:n vaatiman Host-otsakkeen lisäksi. Mahdolliset kyselyparametrit lähetetään palvelimelle osana haettavaa osoitetta.
GET /lets/See?porkkana=1 HTTP/1.1 Host: t-avihavai.users.cs.helsinki.fi
POST
Käytännön ero POST- ja GET-kyselyn välillä on se, että POST-tyyppisillä pyynnoillä kyselyparametrit liitetään pyynnön runkoon. Rungon sisältö ja koko määritellään otsakeosiossa. POST-kyselyt mahdollistavat multimedian (kuvat, videot, musiikki, ...) lähettämisen palvelimelle.
POST /lets/See HTTP/1.1 Host: t-avihavai.users.cs.helsinki.fi Content-Type: application/x-www-form-urlencoded Content-Length: 10 porkkana=1
HEAD ja otsakkeet
HEAD-kyselyt ovat samantyyppisiä GET-kyselyiden kanssa, mutta niillä pyydetään palvelimelta vain haettavaan dokumenttiin liittyviä otsaketietoja. Tämä mahdollistaa muunmuassa muutosten seurannan palvelimelta saatavan Last-modified
-otsakkeen avulla, sekä palvelimen toiminnallisuuden testaamisen.
HEAD-kyselyä on perinteisesti käytetty välimuistitoiminnallisuuden toteuttamiseen, jolloin selaimen välimuistissa olevista dokumenteista on tehty ensiksi vain HEAD-kysely. Jos dokumentti ei ole muuttunut, eli otsake Last-modified
sisältää tarpeeksi vanhan ajan, ei dokumenttiin liittyvää runkoa ole haettu.
Luodaan telnetillä yhteys palvelimeen www.cs.helsinki.fi
ja kysytään otsaketietoja resurssiin /home/
liittyen. Käytämme HTTP:n versiota 1.1 (HTTP/1.1
), joten lisäämme pyyntöön myös Host
-otsakkeen.
$ telnet www.cs.helsinki.fi 80 Trying 128.214.166.78... Connected to www.cs.helsinki.fi. Escape character is '^]'. HEAD /home/ HTTP/1.1 Host: www.cs.helsinki.fi
Vastauksena saamme palvelimen vastausrivin jossa on HTTP-versio, statuskoodi ja selvennys. Seuraavilla riveillä on otsakkeita, jotka tarjoavat lisätietoa haettavasta dokumentista ja käytettävästä palvelimesta.
HTTP/1.1 200 OK Date: Sat, 07 Jan 2012 07:13:49 GMT Server: Apache/2.2.14 (Ubuntu) X-Powered-By: PHP/5.3.2-1ubuntu4.11 Last-Modified: Sat, 07 Jan 2012 07:13:51 GMT Cache-Control: store, no-cache, must-revalidate Cache-Control: post-check=0, pre-check=0 Vary: Accept-Encoding Content-Type: text/html; charset=utf-8
Vastauksesta huomataan että TKTL:n palvelin on huonosti konfiguroitu: Last-Modified
-otsake, joka kertoo milloin resurssia on muokattu, määrittelee tulevaisuudessa olevan päivämäärän. Otsake Date
taas kertoo hetken, jolloin pyyntö on tehty. Otsakkeissa on myös muuta hyödyllistä tietoa. Esimerkiksi otsake Cache-Control
sisältää välimuistitoiminnallisuuteen liittyviä ohjeita; arvo no-cache
määrittelee että sivun ajankohtaisuus tulee aina varmistaa palvelimelta. Huomaamme myös mielenkiintoisen arvon store
, jota -- sen yleisessä käytössä olemisesta huolimatta -- ei määritellä HTTP/1.1 -otsakkeeseen Cache-Control
liittyvässä osiossa (arvo store
on määrittelemätön laajennus). Käytännössä TKTL:n verkkosivu pyrkii siihen, että selain ei tallenna sitä välimuistiin.
Nykyaikaisempi tapa on HEAD-pyynnön sijaan resurssin version tarkastamiseen on ETag
-otsakkeen käyttö osana GET-pyyntöä. GET-pyyntöä suoritettaessa pyynnön mukana lähetetään otsakkeena resurssin version yksilöivä tunnus. Jos palvelimella oleva versio on sama kuin selaimen tiedossa oleva yksilöivä tunnus, palvelin voi palauttaa yksinkertaisesti vastauksen HTTP 304 Not Modified
, eli resurssi ei ole muuttunut. Kuten HEAD-pyynnössä, myös ETag-otsaketta käyttävässä GET-pyynnössä resurssi tulee olla jo kerran ladattu.
curl-työkalu
Telnetin lisäksi curl on hyödyllinen apuväline pyyntöjen tekemiseen ja tarkasteluun. Curl tarjoaa parametrin -i
(include headers in output), jota voi käyttää palvelimen palauttamien otsaketietojen tarkasteluun. Esimerkiksi kysely curl -i hs.fi
palauttaa seuraavat tiedot:
$ curl -i hs.fi HTTP/1.1 301 Object Moved Location: http://www.hs.fi/ Content-Length: 0
HTTP on tilaton protokolla, eli se ei tarvitse jatkuvasti avoinna olevaa yhteyttä toimiakseen. Tämä tarkoittaa sitä, että HTTP ei osaa yhdistä samalta käyttäjältä tulevia pyyntöjä toisiinsa, jolloin jokainen tehty pyyntö käsitellään omana erillisenä pyyntönään. Käytännössä yhden web-sivustonkin hakeminen saattaa sisältää kymmeniä pyyntöjä, sillä jokaiseen sivuun liittyy joukko kuvia ja skriptitiedostoja, joista kukin on oma erillinen resurssinsa.
Vaikka HTTP on tilaton protokolla, on asiakkaan tunnistamiseen käytetty pitkään erilaisia kiertotapoja. Klassinen tapa kiertää HTTP:n tilattomuus on ollut säilyttää GET-muotoisessa osoitteessa parametreja, joiden perusteella asiakas voidaan identifioida palvelinsovelluksessa. Parametrien käyttö osoitteissa ei ole kuitenkaan ongelmatonta: osoitteessa olevia parametreja voi helposti muokata käsin, jolloin palvelinsovelluksesta saattaa löytyä tietoturva-aukkoja tai ei-toivottua käyttäytymistä.
Eräässä järjestelmässä verkkokaupan toiminnallisuus oli toteutettu siten, että GET-parametrina säilytettiin numeerista ostoskorin identifioivaa tunnusta. Käyttäjäkohtaisuus oli toteutettu palvelinpuolella siten, että tietyllä GET-parametrilla näytettiin aina tietyn käyttäjän ostoskori. Uusien tuotteiden lisääminen ostoskoriin onnistui helposti, sillä pyynnöissä oli aina mukana ostoskorin tunnistava GET-parametri. Ostoskorit oli valitettavasti identifioitu juoksevalla numerosarjalla. Henkilöllä 1 oli ostoskori 1, henkilöllä 2 ostoskori 2 jne.. Koska käytännössä kuka tahansa pääsi katsomaan kenen tahansa ostoskoria vain osoitteessa olevaa numeroa vaihtamalla, olivat ostoskorien sisällöt välillä hyvin mielenkiintoisia.
HTTP-protokollan tilattomuus ei pakota palvelinohjelmistoja tilattomuuteen. Palvelimella tilaa pidetään yllä jollain tietyllä tekniikalla, joka taas ei näy HTTP-protokollaan asti. Nykyään yleisin tekniikka tilattomuuden kiertämiseen on evästeiden käyttö.
HTTP on tilaton protokolla, eli käyttäjän toimintaa ja tilaa ei pysty pitämään yllä puhtaasti HTTP-yhteyden avulla. Käytännössä suurin osa verkkosovelluksista kuitenkin sisältää käyttäjäkohtaista toiminnallisuutta, jonka toteuttamiseen sovelluksella täytyy olla jonkinlainen tieto käyttäjästä ja käyttäjän tilasta. HTTP/1.1 tarjoaa mahdollisuuden tilallisten verkkosovellusten toteuttamiseen evästeiden (cookies) avulla.
Asettamalla käyttäjän tekemän pyynnön vastaukseen eväste, tulee käyttäjän jatkossa pyyntöä tehdessä aina palauttaa kyseinen eväste pyynnön otsaketietoina. Tämä tapahtuu automaattisesti selaimen toimesta. Evästeitä käytetään istuntojen (session) ylläpitämiseen: istuntojen avulla pidetään kirjaa käyttäjästä useampien pyyntöjen yli.
Evästeet toteutetaan otsakkeiden avulla. Kun käyttäjä tekee pyynnön palvelimelle, ja palvelimella halutaan asettaa käyttäjälle eväste, palauttaa palvelun vastauksen mukana otsakkeen Set-Cookie
, jossa määritellään käyttäjäkohtainen evästetunnus. Set-Cookie voi olla esimerkiksi seuraavan näköinen:
Set-Cookie: SESS57a5819a77579dfb1a1466ccceee22a0=0hr0aa2ogdfgkelogg; Max-Age=3600; Domain=".helsinki.fi"
Ylläoleva palvelimelta lähetetty vastaus ilmoittaa pyytää selainta tallettamaan evästeen. Selaimen tulee jatkossa lisätä eväste SESS57a5819a77579dfb1a1466ccceee22a0=0hr0aa2ogdfgkelogg
jokaiseen helsinki.fi
-osoitteeseen. Eväste on voimassa tunnin, eli tunnin kuluttua se voi poistaa. Tarkempi syntaksi evästeen asettamiselle on seuraava:
Set-Cookie: nimi=arvo [; Comment=kommentti] [; Max-Age=elinaika sekunteina] [; Expires=parasta ennen paiva] [; Path=polku tai polunosa jossa eväste voimassa] [; Domain=palvelimen osoite (URL) tai osoitteen osa jossa eväste voimassa] [; Secure (jos määritelty, eväste lähetetään vain salatun yhteyden kanssa)] [; Version=evästeen versio]
Evästeet tallennetaan selaimen sisäiseen evästerekisteriin, josta niitä haetaan aina kun käyttäjä tekee kyselyn johonkin osoitteeseen. Evästeet lähetetään palvelimelle jokaisen viestin yhteydessä Cookie
-otsakkeessa.
Cookie: SESS57a5819a77579dfb1a1466ccceee22a0=0hr0aa2ogdfgkelogg
Evästeiden nimet ja arvot ovat yleensä monimutkaisia ja satunnaisesti luotuja niiden yksilöllisyyden takaamiseksi. Samaan osoitteeseen voi liittyä myös useampia evästeitä. Yleisesti ottaen evästeet ovat sekä hyödyllisiä että haitallisia: niiden avulla voidaan luoda yksiöityjä käyttökokemuksia tarjoavia sovelluksia, mutta niitä voidaan käyttää myös käyttäjien seurantaan ympäri verkkoa.
Kekseliästä
Isossa osassa selaimia on valmiiksi web-sovelluskehityksessä hyödyntäviä apuvälineitä. Esimerkiksi uudemmat Google Chrome-selaimet tarjoavat hyödyllisen työvälineen sivustojen sisällön tarkasteluun. Painamalla F12 tai valitsemalla Tools -> Developer tools, pääset tutkimaan sivun lataamiseen ja sisältöön liittyvää statistiikkaa. Lisäneuvoja löytyy Google Developers -sivustolta.
Avaa developer tools, ja mene osoitteeseen http://www.hs.fi. Valitsemalla developer toolsien välilehden Resources
, löydät valikon erilaisista sivuun liittyvistä resursseista. Avaa Cookies
ja valitse vaihtoehto www.hs.fi
. Kuinka moni palvelu pitää sinusta kirjaa kun menet Helsingin sanomien sivuille?
"In '93 to '94, every browser had its own flavor of HTML. So it was very difficult to know what you could put in a Web page and reliably have most of your readership see it." -- Tim Berners-Lee
HTML on rakenteellinen kuvauskieli, jolla voidaan esittää linkkejä sisältävää tekstiä sekä tekstin rakennetta. HTML koostuu elementeistä, jotka voivat olla sisäkkäin ja peräkkäin. Elementtejä käytetään ohjeina dokumentin jäsentämiseen ja käyttäjälle näyttämiseen. HTML-dokumenteissa elementit avataan elementin nimen sisältävällä pienempi kuin -merkillä (<) alkavalla ja suurempi kuin -merkkiin (>) loppuvalla merkkijonolla (<elementin_nimi>), ja suljetaan merkkijonolla jossa elementin pienempi kuin -merkin jälkeen on vinoviiva (</elementin_nimi>).
HTML:ää voi ajatella myös puumaisena kielenä. Juurisolmuna on elementti <html>
, jonka lapsina ovat elementit <head>
ja <body>
.
Jos elementin sisällä ei ole muita elementtejä tai tekstisolmuja (tekstiä), voi elementin avata ja sulkea samalla merkkijonolla: (<elementin_nimi />).
HTML:stä on useita erilaisia standardeja, joista viimeisin on HTML5, a.k.a. HTML
HTML-elementit muodostavat niinkutsutun DOM-puun, joka sisältää kaikki HTML-sivun elementit ja niiden ominaisuudet. Tämän kurssin puitteissa DOM-puu jää hyvin pieneen sivurooliin, ja tärkeintä omalta kannaltamme on seuraavat asiat:
Lomakekentillä olevat nimi-attribuutit (name) ja niihin liittyvät arvot lähetetään lomakkeen attribuutin action
määrittelemään osoitteeseen. GET-tyyppiset pyynnön lisäävät attribuutit osaksi osoitetta, POST-tyyppisissä pyynnöissä attribuutit kulkevat osana pyyntöä.
Tutkitaan alla olevaa lomaketta:
<form method="GET" action="http://t-avihavai.users.cs.helsinki.fi/lets/See"> <label>Viesti <input type="text" name="viesti" /></label> <input type="submit" /> </form>
Kun lomakkeessa painetaan submit-nappia, lomake lähetetään GET-tyyppisenä pyyntönä osoitteeseen http://t-avihavai.users.cs.helsinki.fi/lets/See
. Lomakkeesta lähtevät tiedot liittyvät lomakkeen kenttiin. Yllä olevassa lomakkeessa on tasan yksi lomakekenttä: viesti. Kentät tunnistetaan lomaketta lähetettäessä niiden name
-attribuutin mukaan. Koska lomakkeen lähetystyyppi on GET (method="GET"
), liitetään lomakkeen kentät osaksi osoitetta.
Esimerkiksi, jos lomakkeen viesti-kenttään kirjoitetaan teksti Hei
, ja lomake lähetetään, kysely tehdään osoitteeseen http://t-avihavai.users.cs.helsinki.fi/lets/See?viesti=Hei
. Huomaa että lomakkeen sisältämät tiedot on jo osana osoitetta.
HTML-dokumentin muodostamaa puuta läpikäydessä elementit voidaan tunnistaa sekä niiden nimen, että niiden tunnuksen perusteella. Tunnus, eli id
, on erillinen elementit yksilöivä attribuutti. Esimerkiksi ylläolevassa lomakkeessa olevaan viestikenttään voi liittää tunnuksen seuraavasti:
<form method="GET" action="http://t-avihavai.users.cs.helsinki.fi/lets/See"> <label>Viesti <input type="text" name="viesti" id="viesti" /></label> <input type="submit" /> </form>
Nyt lomakkeessa oleva viestikenttä voidaan tunnistaa myös sen tunnuksen perusteella. Tunnusten käyttämisen tarve selkenee kurssilla Web-selainohjelmointi, nyt vain todetaan että niitä tarvitaan. Jos haluat lisää tietoa aiheeseen liittyen, W3Schools tarjoaa hyvän pohjustuksen DOM-puiden läpikäyntiin ja muokkaamiseen.
NetBeans ei oletusasetuksillaan suostu käynnistämään web-palvelinta valittaessa Run project
, jos projektiin liittyvät testit eivät mene läpi. Voit muuttaa projektin oletusasetuksia valitsemalla projektin nimen oikealla hiirennapilla -> Properties -> Actions. Valitse Action Run project
, ja lisää sille ominaisuus skipTests=true
. Tämän jälkeen palvelin käynnistyy vaikka testit eivät mene läpi.
Voit myös käynnistää web-pohjaiset projektit komentoriviltä Jetty-palvelimen avulla. Komento mvn jetty:start
käynnistää palvelimen projektin target-kansiossa olevilla tiedostoilla.
Tutustutaan tässä osiossa web-sovellusten rakenteeseen, sekä yksinkertaisten web-sovellusten toteuttamiseen servleteillä. Osa kappaleessa olevista esimerkeistä on tarkoitettu sillaksi HTTPn ja palvelimilla toimivien ohjelmistojen välillä. Sovellukset, joita tässä kappaleessa tehdään ovat osittain kivikautisia ja edustavat vahvasti myös huonoa ohjelmointityyliä. Haukumme itseämme jo nyt.
Servletit ovat Javan teknologia dynaamisen palvelinpuolen web-toiminnallisuuden toteuttamiseen. Nimi Servlet tulee siitä, että servletit palvelevat (serve) käyttäjän tekemiä pyyntöjä: ne vastaanottavat pyyntöjä sekä myös vastaavat niihin.
Käytännössä Servlettejä toteutettaessa ohjelmoija perii javan valmiin HttpServlet-luokan, ja korvaavat yhden tai useamman sen tarjoamista metodeista. Yläluokka HttpServlet tarjoaa metodeja yleisimpien HTTP-protokollan pyyntöjen käsittelyyn. Esimerkiksi metodilla doGet
käsitellään GET-tyyppinen pyyntö, kun taas metodilla doPost
käsitellään POST-tyyppinen pyyntö.
Jokaisella pyyntöä käsittelevällä metodilla on parametrina kaksi rajapintaluokkaa: HttpServletRequest sisältää käyttäjän palvelimelle tekemän pyynnön tiedot ja HttpServletResponse sisältää käyttäjälle palvelimelta lähetettävän vastauksen. Rajapintaluokkien konkreettinen toteutus on käytettävän palvelimen toteuttajien vastuulla, Java tarjoaa vain rajapintamäärittelyt. Palvelinohjelmiston toteuttaja, me, taas käyttävät rajapintoja esimerkiksi pyynnön tietojen tarkasteluun ja vastauksen määrittelyyn.
Palvelimella toimiva web-sovellus voi koostua yhdestä tai useammasta Servletistä. Muutamissa esimerkeissä esiintyvä lets-sovellus sijaitsee osoitteessa http://t-avihavai.users.cs.helsinki.fi/lets/. Sovellukseen liittyy useampia Servlettejä, joista jokainen kuuntelee yhtä tai useampaa osoitetta. Esimerkiksi osoitteeseen http://t-avihavai.users.cs.helsinki.fi/lets/See on konfiguroitu Servlet, jonka tehtävänä on pyyntöön liittyvien otsakkeiden ja parametrien tulostaminen.
Servlettien kuuntelemat polut konfiguroidaan Javan web-sovelluksiin liittyvässä web.xml-tiedostossa.
Uudempi Servlet 3.0-spesifikaatio on osittain poistanut web.xml-tiedoston käytön servlettien konfiguroinnista: osa xml-tiedostoissa tapahtuvasta konfiguraatiosta on siirtynyt annotaatioden avulla tapahtuvaksi. Käytämme tällä kurssilla kuitenkin vielä hieman vanhempaa Servlet 2.5-spesifikaatiota: tausta-ajatuksena versiosta riippumatta on se, että servletit tulee kytkeä kuuntelemaan jotain osoitetta.
Yksi klassisimmista neuvoista ohjelmia rakentaessa on "erota sovelluslogiikka ja käyttöliittymä". Aiemmassa Hello World!- esimerkissämme näin ei ole tehty, vaan käyttöliittymä on osa sovelluslogiikkaa. Ei näin!
Sovelluslogiikan ja käyttöliittymän erottaminen on hyvin tärkeää ohjelmistotuotannon kannalta: isompia sovelluksia rakennettaessa useamman ihmisen tulee pystyä muokkaamaan sen eri osia samanaikaisesti. Jos kaikki on samassa tiedostossa, tiedoston luettavuus kärsii huomattavasti ja muokkaukset aiheuttavat lähes taatusti versionhallintakonflikteja. Sovelluksen eri osien testaaminen vaikeutuu myös huomattavasti, jos sovelluksen eri osia ei ole erotettu.
Paljon käytetty tapa käyttöliittymäkoodin ja sovelluskoodin erottamiseksi on luoda jokaiselle näkymälle oma sivu, johon Servlet-luokka pyynnön lopulta ohjaa. JSP-sivut luodaan WEB-INF
-kansion sisälle omaan kansioon, jotta sivuihin ei pääse käsiksi suoraan selaimella. Tällä kurssilla jsp-sivut sisältävän kansion nimi on jsp
.
Nyt Servletin kuuntelemaan osoitteeseen tuleva pyyntö ohjautuu lopulta kansiossa /WEB-INF/jsp/
olevaan tiedostoon hello.jsp
.
Käytännössä siis pyyntö tapahtuu seuraavasti:
1. Käyttäjä tekee palvelimelle pyynnön käyttäjän selaimella tekemä pyyntö -----------------------> palvelin 2. Palvelin päättelee pyynnön perusteella oikean servletin palvelin: pyynnön polku? -----------------> servlet 3. Servletin koodi suoritetaan 4. Vaihtoehtoinen tilanne: 4a) Jos requestdispatcher-olion avulla ei määritellä seuraavaa kohdetta, vastaus palautetaan käyttäjälle 4b) Muuten, pyyntö ohjataan määritellyyn osoitteeseen, esimerkiksi toiselle servletille. Servletin tai JSPn sisältämät komennot prosessoidaan palvelimella. Lopulta vastaus lähetetään takaisin käyttäjälle
Sovelluksemme sijainti palvelimella riippuu palvelimesta ja sovellukselle määritellystä konfiguraatiosta. Sovellukset ovat harvemmin palvelimen juuriosoitteessa, esim. http://palvelin.net/
. Yleensä niille on oma aliosoite, esimerkiksi http://palvelin.net/sovellus
. Tämä johtaa tilanteeseen, jossa sivulla käytettävien linkkien tulee olla dynaamisia. Esimerkiksi lomakkeiden lähettämisen tulee tapahtua sovellukselle.
Jos HTML-lomakkeen action-kenttä sisältää osoitteen /process
, lomake lähetetään käytettävän palvelimen juuriosoitteeseen. Esimerkiksi jos lomake on palvelimella http://palvelin.net/
, lähetettäisiin /process
-osoitteeseen ohjautuva lomake käytännössä osoitteeseen http://palvelin.net/process
-- riippumatta siitä, missä sovellus oikeasti on.
Tämän takia EL-kielessä pääsee käsiksi myös pyynnön tietoihin. Käyttämällä komentoa ${pageContext.request.contextPath}
jsp-sivulla, sovelluksen osoitteen saa käyttöön. Käytännössä jos lomakkeen action-kentälle antaa arvon ${pageContext.request.contextPath}/process
, lomake lähetetään sovelluksen sisältämään process-osoitteeseen, riippumatta siitä missä osoitteessa lomake oikeasti on.
Jos sovelluksemme olisi hieman isompi ja jos koodi olisi Servlet-luokassa, tulisi Servlet-luokasta helposti hyvin raskas. Toteutetaan erillinen viestejä tuottava viestipalvelu. Viestipalvelun rajapinta on seuraavanlainen:
Juuri toteutettu poliittisia letkautuksia heittelevä Hei Maailma-sovellus on ylläpidettävyydeltään jo siedettävää luokkaa. Periaatteessa sovelluslogiikan vaihtaminen sellaiseksi, joka hakee viimeisimmän uutisen Helsingin sanomien etusivulta vaatisi vain uuden MessageService-rajapinnan toteuttavan luokan tekemistä ja sen vaihtamista HelloServlettiin.
Pyynnön mukana voi lähettää parametreja palvelimelle. GET-tyyppisissä pyynnöissä parametrit ovat osana osoitetta. Esimerkiksi pyyntö osoitteeseen http://palvelin.net/sovellus?parametri1=arvo1¶metri2=arvo2
tekee käytännössä kyselyn osoitteessa http://palvelin.net/sovellus
olevaan sovellukseen, ja antaa sovellukselle kaksi parametria. Parametrin parametri1
arvo on arvo1
ja parametri2
-parametrin arvo on arvo2
.
Jos ylläolevan Servlet-luokan kuuntelemaan osoitteeseen tehtäisiin pyyntö, jossa parametrin name
arvona on Mikke
, olisi processRequest-metodin suorituksen jälkeen attribuutin message
arvo Hello Mikke
.
MVC
MVC (model-view-controller) on ohjelmistoarkkitehtuurityyli, jonka tavoitteena on käyttöliittymän erottaminen sovelluslogiikasta. Model on yleensä näytettävä data, view on itse näkymä, ja controller vastaanottaa käyttäjän tekemät käskyt ja muokkaa niiden pohjalta sekä dataa että näytettävää näkymää.
Miten edellä esitetty Hello Web! -esimerkki liittyy MVC-arkkitehtuuriin? Mikä rooli on jsp-sivuilla, servleteillä, ja pyynnön sisältämällä attribuuteilla?
JSP-sivut ovat dynaamista tietoa sisältäviä HTML-sivuja, jotka prosessoidaan palvelimella käyttäjälle näyttämistä. Palvelimella tapahtuvan prosessoinnin yhteydessä JSP-sivu muunnetaan servletiksi, servletti käännetään, ja lopulta servletin tuottama tulostus näytetään käyttäjälle. Tämä mahdollistaa asiakkaan pyyntöön liittyvien tietojen käsittelyn JSP-sivuilla: JSP-sivut ovat servlettejä ja pääsevät täsmälleen samoihin tietoihin käsiksi kuin servletit. JSP-sivuille on myös mahdollista tuottaa sisältöä suoraan Javalla:
That being said, don't do it.: Java-koodin käyttäminen JSP-sivuilla on indikaattori erittäin huonosta suunnittelusta ja johtaa hyvin vaikeasti ylläpidettäviin näkymiin. Erityisesti JSP-sivulla olevassa koodissa sijaitsevan virheen etsiminen on yhtä tuskaa: virheiden stack tracet liittyvät luonnollisesti JSP-sivusta käännetyn servletin koodiin.
On oleellista kuitenkin ymmärtää, että kaikki sivuilla näytettävä tieto luodaan palvelimella.
Java-koodin käyttäminen JSP-sivuilla on kielletty. Tarvitsemme dynaamisuutta sivuillemme: esimerkiksi listojen tulostaminen ei onnistu ilman ohjelmakoodia. Näimme aiemmin esimerkin EL-kielestä, jonka avulla sivulla voi käyttää ja näyttää pyynnössä olevia attribuutteja. EL-kieli oli alunperin osa JSTL-kieltä, joka on kokoelma tägikirjastoja JSP-sivuilla näytettävän datan prosessointiin.
EL eli Expression Language on kieli, jolla pääsee käsiksi mm. pyynnössä oleviin attribuutteihin. EL-kielen lauseet ilmaistaan ${ }
-merkinnällä, jonka sisällä on lauseke. Näimme aiemmin yksinkertaisen EL:n käyttöesimerkin jossa pyyntöön lisätään servletissä attribuutti, esim. request.setAttribute("viesti", "attribuutin arvo");
, joka näytetään JSP-sivulla, esim. <p>${viesti}</p>
.
EL-kielen piste-operaattorin .
avulla pääsee käsiksi attribuutteina olevien olioiden get-metodeihin. Esimerkiksi lause ${auto.hinta}
tekee auto
-nimellä lisättyyn attribuuttiin metodikutsun getHinta()
. Tutkitaan tätä hieman tarkemmin. Oletetaan että käytössämme on seuraava luokka Item.
Katso lisätietoa erilaisista EL-kielen mahdollisuuksista sivuilta oraclen sivuilta.
Todellisuudessa, kukaan ei toivottavasti ohjelmoi kauppalistaa siten, että jokainen tuote lisätään erillisenä attribuuttina. Tutustutaan seuraavaksi JSTL-kieleen, jonka avulla sivuille saadaan toiminnallisuutta mm. listojen läpikäyntiin.
JSTL eli JSP Standard Tag Library on kokoelma XML-tägikirjastoja, joiden avulla JSP-sivuille voidaan lisätä dynaamista toiminnallisuutta. Ensimmäinen JSTL-spesifikaatio (vuodelta 2002) määrittelee JSTL:n tavoitteeksi JSP-sivuja toteuttavien ihmisten elämän helpottamisen: osalla käyttöliittymäsuunnittelijoista ei ole ohjelmointitaustaa. Koska JSTL-tägit ovat XML:ää, ei niiden käyttö haittaa käyttöliittymäsuunnittelussa. JSTL:ää ja HTML:ää sisältäviä JSP-sivuja voi tarkastella myös suoraan selaimella.
Tutustutaan seuraavaksi web-sovellusten ohjelmoinnissa käytettäviin perusohjelmointikäytänteisiin. Pohjustamme myös siirtymistä Spring-sovelluskehyksen käyttöön.
Erilaisiin pyyntötyyppeihin tulee reagoida eri tavalla. GET-pyynnöt ovat suoria pyyntöjä, joilla käyttäjät pyytävät palvelimella olevia resursseja. POST-pyynnöt taas ovat pyyntöjä datan muokkaamiseen. Esimerkiksi lomakkeet, joilla muokataan palvelimella olevaa dataa, tulee lähettää POST-tyyppisenä pyyntönä.
Ehkäpä tärkein syy tämän säännön noudattamiselle liittyy webissä oleviin hakukoneisiin. Hakukoneet käyvät verkon sivustoja jatkuvasti läpi uuden tiedon hakemiseen. Ne seuraavat sivuilla olevia linkkejä, mutta rajoittavat yleensä pyyntönsä GET-tyyppisiin pyyntöihin. Jos sivuilla olevat normaalit linkit mahdollistavat tiedon poistamisen, sivujen läpikäynti hakukoneiden toimesta voi poistaa sivulla olevan datan.
Jos käyttäjän tekemällä POST-tyyppisellä pyynnöllä muokataan sivulla olevaa dataa (esim. datan lisääminen lomakkeen avulla), tulee käyttäjä ohjata tekemään uusi GET-tyyppinen pyyntö pyynnön prosessoinnin jälkeen. Tällä pyritään poistamaan mahdollisuus POST-tyyppisten pyyntöjen uudelleentekemiseen esimerkiksi F5-näppäintä painettaessa, ja se helpottaa huomattavasti sovelluksen sisäisen rakenteen suunnittelua. Jokaisella servletillä on täsmälleen yksi vastuu: yksi on tiedon lisäämiseen, toinen listaamiseen jne..
Käytännössä käyttäjän ohjaaminen uuden pyynnön tekemiseen tapahtuu palauttamalla POST-pyyntöön HTTP-statuskoodi 303 (tai 302) ja osoite uuteen sijaintiin. Kun selain saa "sivu muuttanut" -viestin, hakee se automaattisesti uudessa osoitteessa olevan sivun.
Termi SOLID lanseerattiin Robert "Uncle Bob" Martinin toimesta 2000 luvun alussa. Termi sisältää hyviä olio-ohjelmoinnissa käytettäviä käytänteitä. SOLID on akronyymi seuraaville käsitteille: Single responsibility principle, Open/closed principle, Liskov substitution principle, Interface segregation principle, ja Dependency inversion principle.
Käytännössä SOLID on lista oliosuunnittelun periaatteita, joita seuratessa järjestelmän ylläpidettävyys ja kehitettävyys on huomattavasti helpompaa. Haluamme että omat web-sovelluksemme seuraavat yllälistattuja periaatteita.
Front Controller on suunnittelumalli, jossa kaikille pyynnöille tarjotaan keskitetty käsittelypaikka. Käytännössä kaikki web-sovellukseen liittyvät pyynnöt ohjataan aluksi yhdelle kontrollipalvelulle, esimerkiksi servletille, joka ohjaa ne eteenpäin riippuen pyynnöstä ja pyynnössä haluttavasta resurssista.
Tyypillinen esimerkki Front Controllerin käytöstä on käyttäjän oikeuksien varmistaminen. Koska kaikki pyynnöt ohjataan ensin tietylle servletille, voidaan servletissä tarkistaa mm. käyttöoikeuksien olemassaolo, sekä mahdollisesti hakea käyttäjäkohtaisia tietoja. Toinen esimerkki on Wizard-tyyppiset sovellukset, joissa sivujen näyttöjärjestys on erittäin tärkeä. Tässä Front Controller voi helposti kontrolloida sivujen näyttöjärjestystä.
Front Controller-suunnittelumallia käytetään web-sovellusten toteuttamiseen liittyvän "bulk"-koodin piilottamiseen. Pyyntöjen prosessoinnit, ohjaukset, ym. tulee ohjelmointikielestä riippumatta aina toteuttaa. Sovelluskehykset tarjoavat valmiit rungot, joita käyttämällä ohjelmoija voi keskittyä web-sovelluksen oleellisempiin asioihin. Huomattava osa aktiivisessa käytössä olevista sovelluskehyksistä käyttää Front Controller-suunnittelumallia, mm. Zend, Symfony, Ruby on Rails, ASP .NET MVC ja Spring.
Pelkkä HTML-merkkien muuntaminen niitä kuvaaviksi merkkijonoiksi ei aina riitä sivuston turvaamiseksi. Merkistön muuntamisen voi kiertää helposti esimerkiksi lähettämällä viesti palvelimelle jollain toisella merkistökoodauksella. Vaikka palvelimelle tulevia viestejä ei muunnettaisikaan ennen tallennusta, tyypillisesti palvelimelta käyttäjälle lähetettävät viestit muunnetaan esimerkiksi käyttäjän selaimen toimesta. Tällöin yllä esitetty "tietoturvaratkaisu" ei ratkaise mitään.
Esimerkiksi HTML-koodi <marquee>web-palvelinohjelmointi</marquee>
näyttää UTF-7 muodossa seuraavalta:
+ADw-marquee+AD4-web-palvelinohjelmointi+ADw-/marquee+AD4-
Dependency injection (riippuvuuksien injektointi) on suunnittelumalli, jonka avulla ohjelmiston käyttämät luokat voidaan päättää ajonaikaisesti. Oletetaan, että käytössämme on luokka HelloWorld
, jonka ainut metodi on merkkijonon palauttava getMessage
.
public class HelloWorld { public String getMessage() { return "Hello World!"; } }
Perinteisesti käytetyt oliot luodaan aina ohjelmoijan toimesta new
-komennolla:
// ... HelloWorld helloWorld = new HelloWorld(); System.out.println(helloWorld.getMessage()); // ...
Toinen lähestymistapa on määritellä käytössä olevat luokat erillisessä konfiguraatiotiedostossa. Sovellusta käynnistettäessä konfiguraatiotiedosto luetaan, ja sen perusteella luodaan oliot varastoon, eli oliokontekstiin
, josta niitä voi tarvittaessa hakea. Esimerkiksi luokan HelloWorld
voisi määritellä erilliseen konfiguraatiotiedostoon siten, että sille määritellään oma tunnus, jonka kautta siihen pääsee käsiksi:
// ... <bean id="helloWorld" class="werkko.HelloWorld" /> // ...
Sovellusta käynnistettäessä konfiguraatiotiedoston lataaja luo oliot oliokontekstiin, josta niihin pääsee käsiksi.
// ... // luodaan context-olio, joka luo konfiguraatiotiedostossa (beans.xml) // määritellyt oliot oliokontekstiin ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml"); // nyt oliota ei luoda itse, vaan sitä käytetään oliokontekstin kautta HelloWorld helloWorld = (HelloWorld) context.getBean("helloWorld"); System.out.println(helloWorld.getMessage()); // ...
Ok, mutta eikö tämä tee vaan kaikesta paljon vaikeampaa?
Ensisilmäyksellä koodi vaikuttaa hyvin paljon monimutkaisemmalta. Pohditaan kuitenkin tämän lähestymistavan hyötyjä.
Mikä ihmeen Bean?
Bean on uudelleenkäytettävä komponentti, jota käytetään tiedon käsittelyyn. Beanit ovat käytännössä tavallisia Java-luokkia, joilla on ennaltamääritelty nimeämiskäytäntö: tietoa asettavat metodit ovat muotoa setArvo(...)
, ja tietoa pyytävät metodit ovat muotoa getArvo()
.
Bean-luokilla on kolme kriteeriä:
get
- ja set
-tyyppisillä metodeilla. Totuusarvon palauttavissa metodeissa käytetään get
-etuliitteen sijaan is
-etuliitettä. Tämän nimeämiskäytännön takia sovelluskehykset pääsevät olion attribuutteihin käsiksi ja voivat asettaa attribuutteja.Serializable
-rajapinta, jonka avulla olion tila voidaan tallentaa tarvittaessa.Näiden nimeämiskäytäntöjen takia esimerkiksi EL-kieli toimii. Luistamme usein kolmannesta vaatimuksesta tällä kurssilla.
Käytännössä riippuvuuksien injektoinnissa on kyse kontrollin antamisesta sovelluskehykselle (Inversion of Control). Sen sijaan, että käyttäjä loisi itse ohjelman tarvitsemat oliot, olioiden luominen ja parametrien asettaminen tapahtuu sovelluskehyksen toimesta (käyttäjän määrittelemän konfiguraation pohjalta). Inversion of Controlin ja Dependency Injectionin hyödyt ovat mm.:
new
-kutsuja ei juurikaan tarvitse tehdä.Tutustu Martin Fowlerin artikkeliin Inversion of Control Containers and the Dependency Injection pattern.
Miten käsitteet "Inversion of Control", "Dependency Injection" ja "Service Locator" liittyvät toisiinsa artikkelin perusteella?
Joissain sovelluskehyksissä riippuvuuksia ei tarvitse juurikaan konfiguroida, vaan sovelluskehys osaa päätellä ne ajonaikaisesti esimerkiksi annotaatioiden tai luokkahierarkian perusteella.
Spring tarjoaa toiminnallisuuden beanien automaattiseen lataamiseen ja konfigurointiin. Käyttämällä automaattista komponenttien hakua (component-scan
), sovellus osaa etsiä käytössä olevat komponentit, ja injektoida niitä tarvittaessa.
Jotta Spring ymmärtää, että luokka tulee ladata kontekstiin, tulee luokalle lisätä @Component
-annotaatio. Esimerkiksi luokan HelloWorld
saa ladattua automaattisesti oliokontekstiin, jos sillä on annotaatio @Component
.
Spring on sovelluskehys, joka tarjoaa hyödyllisiä apuvälineitä Java (ja .NET)-sovellusten toteuttamisen ja testaamisen helpottamiseksi. Se perustuu Dependency Injection -suunnittelumalliin, joka mahdollistaa sovelluksen komponenttien toisistaan riippumattoman suunnittelun ja toteutuksen. Spring on komponenttipohjainen ja tarjoaa tukea mm. tietokanta-, web-, ja mobiilisovellusten toteuttamiseen.
Tutustutaan seuraavaksi yksinkertaisen web-sovelluksen toteuttamiseen Springin avulla: käytämme materiaalissa Springin versiota 3.1.2.RELEASE
.
Springin web-toiminnallisuuden peruskivi on Front Controller-suunnittelumalli sekä Dependency Injection. Spring-sovelluskehyksen web-toiminnallisuuden saa käyttöön lisäämällä projektin pom-tiedostoon seuraavan riippuvuuden.
Puhuimme jo aiemmin siitä, että näkymän pitäisi olla JSP-sivuilla. Yllä olevaa sovellusta voi muokata siten, että se ohjaa pyynnön JSP-sivulle RequestDispatcher-oliota käyttäen. Esimerkiksi, jos käytössämme on kansiossa /WEB-INF/jsp/
oleva hello.jsp
-tiedosto, voi pyynnön ohjata sivulle kuten aiemminkin:
Kontrolleriluokissamme on käytetty metodia processRequest(HttpServletRequest request, HttpServletResponse response)
pyyntöjen käsittelyyn. Nimi processRequest
on tuttu jo tehtävistä ennen Springiä. Mutta. Pyyntöä käsittelevän metodin nimen ei ole pakko olla processRequest
, eikä sen tarvitse saada parametreinaan HttpServletRequest
ja HttpServletResponse
-olioita.
Spring päättelee annotaation @RequestMapping
perusteella metodin, johon pyyntö ohjataan. Metodille määriteltävät parametrit ovat oikeastaan melko vapaat: parametriksi voi määritellä esimerkiksi Writer
-olion, johon kirjoitettu teksti palautetaan käyttäjälle. HttpServletRequest
- tai HttpServletRequest
-olioita käytetään hyvin harvoin parametreina -- pyyntöä käsittelevä metodi voi myös olla parametriton. Seuraava metodi tuottaa käytännössä saman lopputuloksen kuin aiemmin näkemämme pyynnön JSP-sivulla ohjaava processRequest
-metodi.
Ehkäpä oleellisin ja eniten käytetty parametri on Model
, jota käytetään pyynnön attribuuttien tallentamiseen. Model on rajapinta, johon Spring asettaa sopivan toteutuksen. Attribuutin lisääminen Model
-oliolle sen metodin addAttribute
avulla tarkoittaa käytännössä samaa kuin attribuutin lisääminen HttpServletRequest
-oliolle metodilla setAttribute
.
Spring syöttää @RequestMapping
-annotaation omaavalle metodille parametrit sitä kutsuttaessa. Esimerkiksi edellisessä tehtävässä toteutettiin kontrolleriluokan metodi, joka näytti seuraavalta:
Aiemmissa esimerkeissä ohjelmiin liittyvää sovelluslogiikkaa on laitettu milloin minnekin. Esimerkiksi salasanageneraattori-tehtävässä sovelluslogiikka, joskin vähäinen sellainen, oli osana Controller-luokkaa, metodissa generatePassword
. Tämä ei ole hyvän ohjelmointityylin mukaista, sillä tarkoituksena on pitää Controller-luokkien koodimäärä mahdollisimman vähäisenä. Tavoitteena on erottaa sovelluslogiikka, eli ohjelman varsinainen "pihvi", omiin luokkiinsa, joita kutsutaan palveluiksi (Service). Jokainen palveluluokka tarvitsee oman rajapintansa, jotta luokkien väliset riippuvuudet eivät kohdistuisi toteutuksiin, vaan rajapintoihin. Tällöin palvelujen toteutuksia voidaan vaihtaa esimerkiksi testattaessa sovellusta.
Spring tarjoaa toiminnallisuuden palveluiden automaattiseen lisäämiseen @Autowired
-annotaation avulla. Palvelut tunnistetaan @Service
-annotaation avulla. Muokataan kappalessa 4.7 nähtyä Timo Soinin viestipalvelua siten, että se on oliokontekstiin ladattava palvelu.
MVC (model-view-controller) on (vieläkin :)) ohjelmistoarkkitehtuurityyli, jonka tavoitteena on käyttöliittymän erottaminen sovelluslogiikasta. Model on yleensä näytettävä data, view on itse näkymä, ja controller vastaanottaa käyttäjän tekemät käskyt ja muokkaa niiden pohjalta sekä dataa että näytettävää näkymää.
Miten toteuttamamme Spring Web-sovellukset liittyvät MVC-arkkitehtuuriin? Mikä rooli on jsp-sivuilla ja kontrollereilla? Entä mikä osa on Model?
Mihin osaan MVC:tä sovelluksen sovelluslogiikka kuuluu?
Selainten ja palvelinten välisessä matalan tason kommunikoinnissa kaikki viestit siirretään binäärimuodossa joukkona nollia ja ykkösiä. Binäärimuotoinen data käännetään tekstiksi sovitun merkistökoodauksen avulla. Merkistökoodauksia on useita erilaisia, joista perinteisin lienee ASCII (American Standard Code for Information Interchange
). ASCII-merkistössä jokainen merkki kuvataan 8 bitin avulla, josta 7 bittiä on merkeille (viimeinen on esim. pariteettibitti, jota voidaan käyttää merkin oikeellisuuden tarkistamiseen).
ASCII-merkistö ei juurikaan tue erikoismerkkejä. Esimerkiksi suomessa käytetyille ääkkösille ei ole kuvausta ASCII-merkistössä. Erikoismerkkien tarpeen vuoksi on kehitetty lukuisia standardeja, mm. ISO-8859-versio. Merkistö ISO-8859-1 sisältää tuen mm. lähes kaikille suomen kielessä käytettäville merkeille, ja sitä käytetään paljon selainten ja palvelinten välisessä kommunikoinnissa.
Nykyisin web-sivuilla eniten käytetty merkistö on UTF-8, joka kuvaa merkit 8-32 bitin avulla.
Koska erilaisia merkistöjä on satoja ja käyttöjärjestelmävalmistajilla on usein hieman standardeista poikkeava oma merkistö, tulee palvelimella ja selaimella olla yhteisymmärrys käytettävästä merkistöstä. Yhteisymmärrys luodaan sisällyttämällä pyynnön ja vastauksen otsakkeisiin tieto käytetystä merkistöstä.
Esimerkiksi pyyntöön voi lisätä otsakkeen Accept-Charset
, jolla kerrotaan toivottu merkistö.
Accept-Charset: utf-8
Vastauksessa käytetään otsaketta Content-Type
vastauksen sisällön tyypin ja käytetyn merkistön ilmaisemiseen.
Content-Type: text/html; charset=utf-8
HTML-sivuilla usein käytetään vielä lisäksi erillistä otsakekenttää, jolla pyritään valmistamaan käytetyn merkistön oikeellisuus. Esimerkiksi alla olevalla otsakkeella kuvataan sivun sisällöksi text/html
ja merkistöksi utf-8
.
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
Merkistön sopiminen tapahtuu usein automaattisesti, mutta käytännössä ohjelmoijien tulee varautua merkistöongelmiin. Esimerkiksi servlettejä tehdessämme laitoimme servlet-koodin alkuun aina komennon response.setContentType("text/html;charset=UTF-8")
, joka lisää Content-Type
-otsakkeen vastaukseen.
response.setContentType("text/html;charset=UTF-8");
Mielenkiintoisia ongelmia tarjoavat tilanteet, joissa otsaketiedoissa kerrotaan sisällön olevan tiettyä tyyppiä, mutta sisältö onkin erilaista. Jos merkistö on asetettu ASCII-muotoiseksi, mutta sisältö sisältää ASCII-aakkostossa määrittelemättömiä merkkejä, on sisällön tulkinta sovelluksen vastuulla. Merkistöongelmat voivat johtua myös palvelimen käyttämistä kolmannen osapuolen komponenteista. Esimerkiksi tietokannat tallentavat dataa yleensä tietyllä merkistöllä: jos ISO-8859-1 -merkistöä käyttävään tietokantaan tallennetaan UTF-8-muotoista dataa, tulee ainakin osa tallennetusta datasta olemaan korruptoitunutta.
Jos sovelluksessasi on merkistöongelma, aloita seuraavista tarkistuksista:
<%@page pageEncoding="UTF-8" contentType="text/html; charset=UTF-8"%>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
Javan Servlet API sisältää servlettien lisäksi myös filttereitä. Filtterit ovat pyynnön ja vastauksen prosessoijia, joilla voidaan käsitellä pyyntöä ja vastausta ennen niiden servletille saapumista, sekä sen jälkeen kun servlet-koodi on suoritettu. Hyvin yleinen käyttötapa filtterille on merkistön asetus: yksittäistä filtteriä voidaan käyttää otsakkeiden asettamiseen pyyntöön ja vastaukseen.
Omia filttereitä toteutettaessa ohjelmoijan tulee periä rajapinta javax.servlet.Filter
, sekä toteuttaa sen vaatimat metodit. Esimerkiksi alla on filtteri, joka tulostaa viestin ennen ja jälkeen seuraavan kohteen käsittelyä. Metodissa doFilter
tehtävä komento chain.doFilter
ohjaa pyynnön seuraavalle kohteelle, esimerkiksi servletille.
Filtterit konfiguroidaan web.xml
-tiedostoon lähes samoin kuin servletit. Poikkeuksena on se, että servlet
-nimen sijaan käytetään elementtiä nimeltä filter
. Alla on konfiguroitu yllä luotu SimpleFilter
kuuntelemaan kaikkia web-sovellukseen tulevia pyyntöjä.
<filter> <filter-name>CharSetFilter</filter-name> <filter-class>wad.filter.SimpleFilter</filter-class> </filter> <filter-mapping> <filter-name>CharSetFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>
Spring-sovelluskehyksellä on valmis filtteri, joka muuttaa kaikki pyynnöt ja vastaukset (esimerkiksi) UTF-8 merkistöä käyttäväksi: CharacterEncodingFilter. Filtterin saa käyttöön lisäämälle sen web.xml
-tiedostoon.
<!-- Filtteri: Kaikki pyynnöt utf-8:ksi --> <filter> <filter-name>encoding-filter</filter-name> <filter-class> org.springframework.web.filter.CharacterEncodingFilter </filter-class> <init-param> <param-name>encoding</param-name> <param-value>UTF-8</param-value> </init-param> <init-param> <param-name>forceEncoding</param-name> <param-value>true</param-value> </init-param> </filter> <filter-mapping> <filter-name>encoding-filter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>
Jatkossa käytämme ylläolevaa filtteriä osana Spring-sovelluksiamme.
Lue tietokone-lehden UTF-merkistöä käsittelevä artikkeli vuodelta 2001: Unicode ratkaisee merkistöongelmat.
Artikkeli on kirjoitettu yli 10 vuotta sitten. Mitä artikkeli kuvaa merkistöongelmina, ja miten Unicode toimii niihin ratkaisuna?
Tietokanta on tietotekniikassa käytetty termi tietovarastolle. Se on kokoelma tietoja, joilla on yhteys toisiinsa. ... Tietokanta saattaa edustaa jotain selvästi rajattua kohdetta reaalimaailmasta. Tällainen kohde voi olla esimerkiksi yrityksen keräämät tiedot asiakkaistaan. - Wikipedia
Tietokanta on lähes aina webisoftan oleellisin osa, ilman sitä ei ole mitään näytettävää. - Mikael
Tähänastiset sovelluksemme ovat hukanneet käyttämänsä tiedot sovelluksen sammuessa. Tämä johtuu siitä että tietoa ei ole varastoitu mihinkään, vaan se on ollut vain sovelluksen muistissa. Tiedon säilymisen varmistamiseksi tieto tulee tallentaa pysyväismuistiin. Tietoa kuvataan usein taulumuotoisena, esimerkkinä seuraava taulu Henkilo
:
Henkilo | ||
---|---|---|
id | nimi | huone |
1 | Arto | B215 |
2 | Matti | D232 |
3 | Mikael | B215 |
Tietokantoja käytetään tiedon pysyväismuistiin (esim kovalevylle) tallentamiseen. Termiä tietokanta käytetään puhekielessä usein termin tietokannanhallintajärjestelmä korvaajana. Tietokannanhallintajärjestelmät (DBMS, Database Management System) ovat sovelluksia, jotka tarjoavat tukitoiminnallisuuksia tietokannan sydämen, eli tietokantamoottorin päälle. Tietokantamoottori tarjoaa toiminnallisuuden tiedon luomiseen, lukemiseen, päivittämiseen ja poistamiseen. Tätä toiminnallisuutta kutsutaan usein termillä CRUD
(create, read, update and delete).
Esimerkiksi MySQL-tietokannanhallintajärjestelmää käytettäessä käyttäjä voi valita useamman tietokantamoottorin välillä, nykyään yleisin (ja oletus-) vaihtoehto on InnoDB-moottori.
Huomattava osa tietokannanhallintajärjestelmistä toteuttaa ACID-ominaisuudet (Atomicity, Concistency, Isolation, Durability), joilla pyritään turvaamaan järjestelmän luotettavuutta. Käytännössä ACID-ehtoja seuraamalla tallennettavat tiedot ovat eheitä myös virhetilanteissa. ACID koostuu seuraavista osista:
Atomicity
): tietokantaan tehtävä kyselysarja suoritetaan kokonaan, tai sitä ei suoriteta lainkaan. Tähän liittyy käsite transaktio. Tietokantatransaktioiden avulla voidaan suorittaa joukko tietokantakyselyitä siten, että jos yksikin kysely epäonnistuu, yhtäkään ei suoriteta (palataan aiempaan tilaan). Esimerkiksi tilisiirtoja suorittaessa tehdään useampia käskyjä: "Ota rahaa tililtä 1, laita rahaa tilille 2" -- jos vain toinen onnistuu, rahat katoavat.Consistency
): tietokannan tulee olla jokaisen transaktion jälkeen eheässä ja johdonmukaisessa tilassa. Esim. tietokantaan tehtävien muutosten tulee näkyä muille tietokantaa käyttäville ohjelmille aina samalla tavalla, johdonmukaisesti.Isolation
): Tietokantaan tehtävät transaktiot eivät saa vaikuttaa toisiinsa, ja kesken oleva transaktio ei saa näkyä muille transaktioille.Durability
): Kun transaktio on suoritettu loppuun, tehdyt muutokset eivät saa kadota järjestelmästä. Tietokannanhallintajärjestelmät ovat erillisiä järjestelmiä, joihin tulee ottaa yhteys tietokantakyselyjä tehdessä. Käytännössä tietokannanhallintajärjestelmä kuuntelee jotain tiettyä porttia, johon yhteys otetaan. Useampi web-sovellus voi käyttää samaa tietokantapalvelinta. Tämä tuo haasteita skaalautuvuuden kanssa: jos tietokanta noudattaa ACID-periaatteita, ja tietokantaan tehtävien kyselyiden määrä on huomattava, voi tietokannan tehokkuus rajoittaa järjestelmän tehokkuutta. Eräs ratkaisu on tietokantapalvelimien monistaminen, mutta siinäkin on haasteena päivitysten synkronisointi palvelinten kesken.
Käsittelemme tässä kappaleessa relaatiotietokantojen käyttöä osana Javapohjaisia web-palvelinohjelmistoja. Esimerkeissä (ja tehtävissä) on käytössä Javalla kirjoitettu H2-tietokanta (v. 1.3.168), jonka saa käyttöön lisäämällä seuraavan riippuvuuden pom.xml
-tiedostoon.
JDBC (Java Database Connectivity) on Javalle on määritelty standarditapa tietokantayhteyden luomiseen sekä tietoa muokkaavien ja hakevien kyselyjen suorittamiseen. Tausta-ajatuksena JDBCn kehitykselle on ollut yhteisen rajapinnan määrittely tietokannoille -- käytettävään tietokantaan täytyy voida ottaa yhteys, ja jokaiseen tietokantaan tulee pystyä tekemään kyselyitä. Kehitykseen on vaikuttanut ODBC, joka on vastaava projekti C-kielelle. JDBC tarjoaa suoran kytköksen valmiisiin ODBC-toteutuksiin.
Jotta JDBCn avulla voidaan ottaa yhteys tietokantaan, tulee käytössä olla tietokantakohtainen JDBC-ajuri. Käytännössä jokainen tietokannanhallintajärjestelmä tarjoaa JDBC-ajurin. JDBC-ajurin vastuulla on tietokantayhteyden luomiseen liittyvät yksityiskohdat sekä kyselytulosten muuntaminen JDBC-standardin mukaiseen muotoon.
Vuonna 2011 kerätyssä vaarallisimmat ohjelmointivirheet sisältävässä listassa on SQL-injektiot numerona 1.
Käytännössä suurin osa sovelluksista liittyy tietoon: tiedon hakemiseen tietokannasta, tiedon muokkaamiseen ja näyttämiseen, ja tiedon tallentamiseen tietokantaan. Jos hyökkääjä voi vaikuttaa tehtävien SQL-kyselyiden sisältöön, pääsee hän vaikuttamaan myös näytettävään tietoon.
Yksinkertaisimmillaan SQL-injektio on tilanne, jossa sovelluksen käyttäjä syöttää SQL-komennon osaksi siihen kuulumatonta tavaraa. Oletetaan esimerkiksi että sovellus suorittaa kirjautumisen tarkistamalla onko seuraavan kyselyn tulos tyhjä:
// ... String nimi = "nimi"; String salasana = "salasana"; String kysely = "SELECT * FROM user WHERE username = '" + nimi + "' AND password = '" + salasana + "'"; // ...
Jos käyttäjä antaa käyttäjänimen muodossa haha' OR '1'='1
, on kyselyssä tuloksena kaikki taulun user
käyttäjät ja kirjautuminen onnistuu aina.
SQL-injektioesimerkkejä löytyy mm. osoitteesta http://www.unixwiz.net/techtips/sql-injection.html
Prepared Statement-kyselyt ovat valmiiksi määriteltyjä kyselyitä, joissa kyselyissä käytettävät arvot annetaan parametreina. Valmiita kyselyitä käyttämällä pystytään estämään ainakin osa SQL-injektioista, sillä parametreja käytettäessä varmistetaan että parametrien arvot liittyvät aina vain tiettyyn kenttään -- kyselyparametrit eivät "vuoda yli". Prepared Statement-kyselyt mahdollistavat myös kyselyiden optimoinnin, sillä kysely on aina samanmuotoinen.
Valmiit kyselyt määritellään Connection
-olion prepareStatement
-metodin avulla. Metodi palauttaa PreparedStatement
-olion, johon kyselyn käyttämät arvot määritellään parametreina. Esimerkiksi SQL-injektioesimerkissä oleva kysely luodaan valmiiden kyselyiden avulla seuraavasti:
DriverManager
-luokkaa käytettäessä yhteys tietokantaan luodaan yleensä alusta asti. Yhteyden luominen voi kestää jopa sekunteja riippuen ajurin lataamisesta, palvelimen sijainnista ja käytössä olevasta infrastruktuurista. DriverManager
-olion käyttöä suositellumpi tapa JDBC-yhteyksien luomiseen on DataSource
-rajapinnan toteuttavan palvelun käyttö.
Eräs toteutus DataSource-rajapinnalle on Apache Commons-projektin DBCP-projekti. Commons DBCP-projektin saa käyttöön lisäämällä siihen liittyvän riippuvuuden pom.xml
-tiedostoon.
Springin JDBC-komponentti tarjoaa myös tuen tulosten kytkemiseen olioihin. Oletetaan että käytössämme on seuraava luokka User
:
Huomaamme, että tietokantataulujen kuvaaminen luokkien avulla on oikeastaan aika loogista. Tietokantataulun nimi vastaa luokan nimeä ja taulun attribuutit (eli sarakkeiden nimet) ovat luokan attribuutteja. Jokainen luokasta luotava olio vastaa yhtä tietokantataulun riviä. Viitteet eri taulujen välillä voidaan taas kuvata viitteinä olioiden välillä.
Sovelluksia suunniteltaessa yksi lähestymistapa ongelma-alueeseen on tallennettavaa tietoa kuvaavien olioiden luonti. Tämä tapahtuu luonnollisesti yhteistyössä asiakkaan kanssa, jolloin voidaan varmistaa että puhutaan asiakkaan ja ongelma-alueen käyttämällä kielellä. Tutkitaan seuraavaa lentokentän hallintajärjestelmän kuvausta:
Case: Lentokenttä
He told us: "...Basically what we need is a system for monitoring aircrafts and airports. Each aircraft has a unique identifier, e.g. LOL-52, and a numeric capacity. Airports on the other hand reside in a location, have a unique identifier and a name, e.g. e.g. LOL
for Derby Field United States and FUN
for International Tuvalu. Each airport can host one or more aircrafts..."
Ongelma-alueen purkaminen lähtee perinteisesti liikkeelle substantiivien etsimisellä.
He told us: "...Basically what we need is a system for monitoring aircrafts and airports. Each aircraft has a unique identifier, e.g. LOL-52, and a numeric capacity. Airports on the other hand reside in a location, have a unique identifier and a name, e.g. LOL
for Derby Field United States and FUN
for International Tuvalu. Each airport can host one or more aircrafts..."
Tämän jälkeen substantiiveista erotetaan pääkäsitteet, pääkäsitteiden attribuutit, ja ei-oleelliset käsitteet. Pääkäsitteitä ovat aircraft
ja airport
, ja system
on ei-oleellinen käsite. Loput ovat pääkäsitteiden attribuutteja.
Pääkäsitteiden attribuutit löytyy etsimällä lisätietoa antavia lauseita (esim. has a
, in a
, ...). Yllä olevasta esimerkistä löytyy seuraavat käsitteet attribuutteineen.
DAO-pattern (Data Access Object) on suunnittelumalli, jossa tiedon tallennusmekanismi kapseloidaan sovelluslogiikalle näkymättömäksi. DAO-rajapinnan ideana on mahdollistaa tallennusmekanismin helppo vaihtaminen: sovellus voi aluksi säilyttää olioita esimerkiksi muistissa -- rajapinnan käyttäjän ei tarvitse välittää toteutuksesta. Toteutetaan aiemmin esitellyille olioille DAO-oliot ja niihin liittyvät toteutukset.
Käytetään perinteistä CRUD-tyyliä rajapintojen metodien nimeämiseen.
ORM-työkalut (Object Relational Mapping) tarjoavat mm. toiminnallisuuden tietokantataulujen luomiseen määritellyistä luokista. Työkalut hallinnoivat luokkien välisiä viittauksia ja ylläpitävät mm. tietokannan eheyttä. Käyttäjän vastuulle jää yleensä sovellukselle tarpeellisten kyselyiden toteuttaminen niiltä osin kun niitä ei tarjota valmiiksi.
Relaatiotietokantojen käsittelyyn on kehitetty joukko ORM-sovelluksia joista nimekkäin lienee Hibernate. Oracle/Sun standardoi olioiden tallentamisen relaatiotietokantoihin JPA (Java Persistence API) -standardilla. JPA:n toteuttavat kirjastot (esim. Hibernate, EclipseLink) abstrahoivat relaatiotietokannan, ja mahdollistavat kyselyjen tekemisen suoraan ohjelmakoodista.
Käytämme tällä kurssilla EclipseLink
-kirjaston versiota 2.4.0
, jonka saa käyttöön lisäämällä projektiin seuraavat riippuvuudet. Alempi riippuvuus tuo käyttöön JPA2
-APIn.
Luokkaa Persistence
käytetään EntityManagerFactory
-olion ohjelmalliseen luomiseen. Persistence
-luokan luokkametodille createEntityManagerFactory
annetaan parametrina käytettävän konfiguraation nimi, jonka perusteella tiedostosta persistence.xml
etsitään oikea konfiguraatio. Luokkaa Persistence
käytetään vain silloin, jos sovellusta halutaan käyttää erillisenä web-ympäristöstä (esim. yllä ollut demo) -- web-kontekstissa tarvittavat luokat injektoidaan automaattisesti.
Luokka EntityManagerFactory
on tehdasluokka, josta saadaan entiteettien tallentamista hallinnoivia EntityManager
-olioita.
EntityManager
hallinnoi entiteettejä ja niiden tallennusta tietokantaan. Sovelluskehys -- tai sovelluskehittäjä -- luo EntityManager
-olion tarpeen vaatiessa. EntityManager
tarjoaa joukon palveluita, joista oleellisimmat ovat olion tietokantaan lisääminen (metodit persist
ja merge
), poistaminen (metodi remove
) ja hakeminen (metodi find
).
EntityManager
tarjoaa toiminnallisuuden tietokantatransaktioiden hallintaan. Metodi getTransaction()
palauttaa EntityTransaction
-olion, jota käytetään transaktioden aloittamiseen (metodi begin
) ja lopettamiseen (metodi commit
).
Spring tarjoaa apuvälineitä transaktioiden hallintaan. Luokka JpaTransactionManager
delegoi transaktioiden käsittelyn EntityManager
-olioille. Käytännössä transaktiot voidaan määritellä metodi- tai luokkatasolla annotaation @Transactional
avulla. Tällöin annotaatiolla @Transactional
merkittyä metodia suoritettaessa metodin alussa aloitetaan tietokantatransaktio, jossa tehdyt muutokset viedään tietokantaan metodin lopussa. Jos annotaatio @Transactional
määritellään luokkatasolla, se koskee jokaista luokan metodia.
Joissain tapauksissa entiteettien avainarvot halutaan saada tietoon jo ennen tietokantaan tallentamista. Tällöin avainarvoina käytetään sovelluksen itse luomaa mahdollisimman yksilöivää arvoa. Jos avainarvoja ei luoda tietokannan toimesta, on mahdollista että useammalla entiteetillä on sama avainarvo. Tällöin pyritään siihen, että saman avainarvon kahdesti luomisen todennäköisyys on mahdollisimman pieni.
Yleisin ratkaisu satunnaisen avaimen luomiseen on satunnaisen merkkijonon käyttäminen avaimena. Javalla hyvin satunnaisen merkkijonon saa luotua esimerkiksi UUID
-luokan avulla. UUID-luokka arpoo merkkijonon joukosta, jossa on noin 2128 vaihtoehtoa.
Esimerkiksi yllä luotua GroceryItem
-entiteettiä tulee muuttaa seuraavasti, jotta sille voidaan asettaa merkkijonoavain. Avaimen generointi voi tapahtua esimerkiksi luokan parametrittomassa konstruktorissa tai osana luokkaa käyttävää palvelua.
Huomaamme tietokantalogiikassa vieläkin melko paljon toisteisuutta. Esimerkiksi CRUD-operaatiot ovat lähes aina samanlaiset. Tutustutaan seuraavaksi Spring Data JPA-projektiin, joka pyrkii vähentämään tietokantalogiikan toteutukseen tarvittavan koodin määrää.
Olemme huomanneet että iso osa tietokantatoiminnoista on valmiiden operaatioiden toistamista. Käytännössä lähes jokainen JPA:n kanssa paininut paljon tietokantakyselyitä luova sovelluskehittäjä on jossain vaiheessa luonut itselleen hieman seuraavankaltaisen pohjan, jonka perimällä saa käyttöön oleellisimmat toiminnot.
Spring Data JPA ei tarjoa kaikkia kyselyitä valmiiksi. Jos tarvitset tietynlaisen kyselyn, sinun tulee yleensäottaen myös määritellä se. Laajennetaan aiemmin määriteltyä rajapintaa AircraftRepository
siten, että sillä on metodi List<Aircraft> findByCapacity(Integer capacity)
-- eli hae koneet, joilla on tietty kapasiteetti.
Kerrosarkkitehtuuri jakaa sovelluksen kerroksiin; tiedon tallentamiseen ja hakemiseen liittyvään logiikkaan, sovelluksen palveluihin liittyvään logiikkaan, käyttöliittymästä tehtyjä pyyntöjä ohjaavaan kerrokseen, ja käyttöliittymäkerrokseen.
N-tier -arkkitehtuurilla tarkoitetaan yleisesti sovelluksen jakamista itsenäisiin osiin, jotka toimivat vuorovaikutuksessa muiden osien kanssa. Client-Server -arkkitehtuuri on esimerkki 2-tier arkkitehtuurista, 3-tier -arkkitehtuuri taas esimerkiksi jakaa käyttöliittymän, sovelluslogiikan, ja tietokantalogiikan erillisiksi osioikseen. Tällä kurssilla kerrosarkkitehtuurista puhuessamme tarkoitamme yleisesti ottaen seuraavaa jakoa:
Tiedon hakemiseen ja tallentamiseen liittyvässä logiikassa käytetään lähes aina valmiita komponentteja, esim. JPA tai Spring Data JPA. Alimman kerroksen toiminnallisuus -- relaatiotietokannat, key/value -tietokannat, dokumenttitietokannat, jne.. -- ovat web-sovelluskehittäjän näkökulmasta "done", eli omaa ei kannata lähteä toteuttamaan.
Sovelluksen oleellisin osa on palveluissa, eli keskitasossa. Sovelluslogiikka sisältää toiminnallisuuden, joka tekee sovelluksestamme arvokkaan. Sovelluslogiikka käyttää alempana olevan tietokantakerroksen tarjoamia palveluita. Sovelluslogiikkakerros sisältää yleensä myös transaktioiden hallinnan.
Sovelluslogiikkakerroksen päällä on käyttöliittymäkutsuja ohjaava kerros, "kontrollikerros". Käyttöliittymäkutsuja ohjaavan kerroksen tehtävänä on toimia rajapintana sovelluskerroksen ja käyttöliittymän välillä. Se vastaanottaa käyttäjän tekemiä pyyntöjä ja ohjaa niitä eteenpäin tarpeellisille palveluille. Kontrollikerroksen tulee olla mahdollisimman ohut; siinä käytetään sovelluskerroksen tarjoamaa toiminnallisuutta. Kutsun suorittamisen jälkeen kontrollikerros palauttaa dataa jossain muodossa. Javascript-kutsulla voi pyytää XML- tai JSON-muodossa olevaa dataa, jonka näyttäminen tapahtuu selainpuolella. Toisaalta, palautettu data voidaan ohjata erilliselle käyttöliittymän renderöintipalvelulle, joka luo näytettävän HTML-sivun.
Kontrollikerroksen päällä on käyttöliittymä. Käyttöliittymiä voi olla useita erilaisia. Kaikille yhteistä on se, että ne tekevät pyyntöjä web-sovelluksen tarjoamiin osoitteisiin.
Sovelluslogiikka jaetaan oliosuunnittelun periaatteiden mukaisesti toiminnallisuutta tarjoaviin palveluihin, joita kontrollikerros käyttää. Spring tarjoaa hyvät välineet sovelluslogiikan ja kontrollikerroksen erottamiseen. Tutustumme seuraavaksi kontrolliluokan ja sovelluslogiikan yhteistoimintaan. Pieni kertaus ennen sitä.
Jokaisella oliolla on oma selkeä vastuualueensa, ja niiden sekoittamista tulee välttää. Inversion of Control ja Dependency Injection ovat suunnitelumalleja, joilla pyritään vähentämään olioiden turhia riippuvuuksia.
Inversion of Control
Perinteisissä ohjelmistoissa luokkien ilmentymien luominen on ohjelmoijan vastuulla. Huomasimme jo aiemmin että Spring luo käyttöömme luokkia joita tarvitsemme: Kontrollin käännöllä tarkoitetaan ohjelman toiminnan hallinnan vastuun siirtämistä sovelluskehykselle ja ohjelmaa suorittavalle palvelimelle.
Dependency Injection
Dependency Injectionin tehtävänä on syöttää riippuvuudet silloin kun niitä tarvitaan.
Käytännössä siis: palvelin ja sovelluskehys ottaa vastuuta luokkien hallinnoinnista. Sovelluskehys syöttää riippuvuudet niitä tarvittaessa. Molemmat toiminnallisuudet ovat oleellisia kerrosarkkitehtuurin kerrosten toisistaan erottamisessa.
Kontrolleritaso kuuntelee käyttöliittymältä tulevia pyyntöjä, ja vastaanottaa lähetettyä dataa. Käytännössä sovelluskehyksissä pyynnöt ohjataan kontrollereille erillisen front controllerin kautta. Front controller kuuntelee kaikkia sovellukselle ohjattuja pyyntöjä. Springiä käytettäessä käytämme Springin omaa DispatcherServlet
-toteutusta, joka toimii front controllerina.
Kontrolleritason ensisijaisena vastuuna on pyyntöjen kuuntelu, pyyntöjen ohjaaminen oikealle palvelulle, sekä tuotetun tiedon ohjaaminen oikealle näkymälle. Jotta palveluille ei ohjata epäoleellista dataa, esimerkiksi huonoja arvoja sisältäviä parametreja, on kontrolleritason vastuulla myös pyyntöparametrien validointi. Parametrien validointi tapahtuu käytännössä käsin, joko siten, että ohjelmoija itse toteuttaa validoinnin, tai jotain olemassaolevaa validointimekanismia käyttämällä.
Ensimmäinen askel pyyntöparametrien validointiin on pyyntöparametrien asettaminen odotetun tyyppisiin muuttujiin. Esimerkiksi numeerisia arvoja saavan parametrin oikeellisuuden voi varmistaa asettamalla sen Integer
-tyyppiseen muuttujaan. Poikkeustapauksessa tiedämme heti, että käyttäjä on yrittänyt syöttää sovellukselle virheellisiä arvoja.
Alkeellinen muuttujien arvojen validointi toteutetaan kontrolleriin tai erilliseen validointiluokkaan. Oletetaan että seuraava lomake sijaitsee tiedostossa grocery-item-form.jsp
, lomakkeen lähetys ohjautuu luokassa GroceryItemController
olevalle metodille addGroceryItem
. Lisäksi käytössämme on erillinen rajapinta GroceryItemService
, joka tarjoaa metodit tallentamiseen ja listaamiseen.
Kun pohdimme lomakkeita hieman enemmän, huomaamme että ne sopivat melko hyvin olio-ohjelmointiajatteluun. Lomakkeissa syötetään usein tietoa johonkin käsitteeseen -- luokkaan liittyen -- ja yhden lomakkeen sisältämät tiedot kuvautuvat helposti oliona. Onkin itseasiassa suhteellisen helppoa rakentaa oma luokka, joka muuntaa pyynnössä olevat parametrit tietynlaiseksi olioksi.
Ajatustasolla olion luominen pyynnöstä tapahtuu luomalla ensin oliosta ilmentymä, ja sen jälkeen käymällä pyynnön parametreja läpi. Jos pyynnössä on parametri, joka on saman niminen kuin olion attribuutti, asetetaan parametrin arvo olion attribuutiksi. Huomattava osa yleisessä käytössä olevista sovelluskehyksistä tekee tämän jollain tavalla puolestamme. Spring-sovelluskehyksessä tämän toiminnallisuuden saa käyttöön lisäämällä Springin konfiguraatioon komennon <mvc:annotation-driven />
. Myös mvc-nimiavaruus tulee lisätä Springin konfiguraatioon.
Lomakkeiden ja lähetettävän datan validointi on web-sovelluksille hyvin oleellista. Emme halua että käyttäjä pääsee lähettämään sovelluksellemme toisten koneilla suoritettavaa koodia. Ensimmäinen askel -- jonka olemme jo ottaneet -- on tallennettavan datan järkevä esitys. Käytämme datan esittämiseen olioita, joihin on määritelty sopivat kenttien tyypit. Tämä helpottaa työtämme jo hieman: esimerkiksi numerokenttiin ei saa asetettua merkkijonoja.
Tämä ei kuitenkaan vielä riitä.
Javassa on oma API Bean-tyyppisten olioiden validoinnille: Bean Validation API (Javadoc). Bean validation API on, kuten muutkin JSRt (Java Specification Request
, vain rajapinta, jolla voi olla useampi toteuttaja. Käytämme JSR-303 -apin, eli Bean Validation APIn, toteutuksena Hibernate Validator
-projektia (dokumentaatio), jonka saamme käyttöömme lisäämällä pom.xml
-tiedostoon seuraavan riippuvuuden.
Muuttujien validointia varten tarkistettaville muuttujille määritellään annotaatiot. Muokataan aiemmin käytettyä luokkaa Person
siten, että henkilöllä tulee olla henkilötunnus, nimi ja sähköpostiosoite.
Käytännössä siis validointi onnistuu Hibernate Validator-komponentin avulla annotaatioilla. Lisätään validointi seuraavaksi osaksi kontrolleriluokkaa.
Pyynnössä saatavan olion, joka on merkitty annotaatiolla @ModelAttribute
validointi on helpohkoa. Kun olemme lisänneet Spring-konfiguraatioon komennon <mvc:annotation-driven/>
, saamme käyttöön muitakin toiminnallisuuksia kuin olioiden generoinnin pyynnöstä. Yksi ominaisuus on olioiden validointi. Spring lataa automaattisesti käyttöönsä validointikehyksen, jos sellainen löytyy käytössä olevista kirjastoista. Jos olemme lisänneet Hibernate Validator-projektin osaksi pom.xml
-tiedostoa, on se käytössämme automaattisesti.
Itse olion validointi tapahtuu lisäämällä kontrollerimetodissa olevalle @ModelAttribute
-annotaatiolle merkatulle oliolle annotaatio @Valid
(javax.validation.Valid;
).
Spring tarjoaa käyttöömme tägikirjaston lomakkeiden luomiseen JSP-sivulla. Lomakekirjaston saa JSP-sivulla käyttöön lisäämällä sivun alkuun seuraavan komennon.
Vaikka esimerkissämme käyttämäämme Person
-luokkaa ei oltu merkitty @Entity
-annotaatiolla -- eli se ei ollut tallennettavissa JPAn avulla tietokantaan -- mikään ei estä meitä lisäämästä sille @Entity
-annotaatiota. Toisaalta, lomakkeet voivat usein sisältää tietoa, joka liittyy useaan eri talletettavaan olioon. Tällöin onkin fiksua luoda erillinen lomakkeen validointiin tarkoitettu lomakeolio, Form Object
, jonka pohjalta luodaan tietokantaan tallennettavat oliot kunhan validointi onnistuu. Erilliseen lomakeobjektiin voi täyttää myös kannasta haettavia listoja ym. ennalta.
Kun käyttäjä ohjataan tekemään uusi pyyntö redirect:
-komennon avulla, selaimelle käytännössä palautetaan HTTP statuskoodi 303 (tai 302), sekä uusi osoite. Tällöin selain tekee pyynnön uuteen osoitteeseen. Koska HTTP on tilaton protokolla, ei sillä ole välineistöä käyttäjän pyyntöjen yhdistämiseen: aiemmassa pyynnössä käytössä oleva data ei ole käytössä seuraavassa pyynnössä.
Tämä on yleensä täysin hyväksyttävää, ja toivottavaakin, mutta joissain tapauksissa ohjelmoija haluaa aiemmasta pyynnöstä tietoja myös seuraavaan pyyntöön. Tyypillinen käyttötapaus on tietyn informaatioviestin lisääminen sivulle, johon käyttäjä ohjataan. HTTP-protokollan tarjoamat evästeet mahdollistavat tiedon katoamisen kiertämisen: jos kaivattu tieto tallennetaan sessioon, on se olemassa myös seuraavalla pyynnöllä. Vastaava toiminnallisuus on myös useassa sovelluskehyksessä valmiina.
Spring tarjoaa pyynnöissä käytettävän parametrin RedirectAttributes
, johon voi tallentaa attribuutteja, jotka halutaan näkyville seuraavalla pyynnöllä. Käytännössä RedirectAttributes
-olio tallentaa attribuutit sessioon, josta ne lisätään Model
-olion attribuutteihin seuraavan pyynnön yhteydessä.
Jotta RedirectAttributes
toimii, on Spring-konfiguraatiossa oltava merkkijono <mvc:annotation-driven />
.
Pohditaan tilannetta, jossa luomme uutta Person
-oliota. Kun henkilö on luotu, käyttäjä ohjataan sivulle, jossa on henkilön tiedot. Kun käyttäjä tulee sivulle ensimmäisen kerran, sivulla näytetään myös viesti "New person created!". Lisätään Person
-luokalle attribuutti id
, jota käytetään henkilön tunnistamiseen.
REST (representational state transfer) on HTTP-protokollaan perustuva arkkitehtuurimalli erityisesti web-pohjaisten sovellusten toteuttamiseen. Taustaidea on periaatteessa yksinkertainen: osoitteilla määritellään haettavat ja muokattavat resurssit, pyyntömetodit kuvaavat resurssiin kohdistuvaa operaatiota, ja pyynnön rungossa on tarvittaessa resurssiin liittyvää dataa. HTTP:n GET- ja POST-komentojen lisäksi REST-sovellukset käyttävät ainakin PUT ja DELETE-pyyntöjä. Esimerkiksi yksinkertainen henkilöstörekisteri voitaisiin toteuttaa seuraavilla osoitteilla ja pyyntötavoilla.
Oleellisia asioita RESTissä ovat resurssien nimentä web-osoitteita käyttäen sekä HTTP-protokollan tarjoamien pyyntötyyppien käyttö. Osoitteissa käytetään substantiivejä -- ei getPerson?id={tunnus}
vaan /henkilo/{tunnus}
, ja pyynnöt kategorisoidaan pyyntötyyppien mukaan. DELETE-tyyppisessä pyynnössä poistetaan, POST-tyyppisellä pyynnöllä lisätään, PUT-tyyppisellä pyynnöllä joko lisätään tai päivitetään, ja GET-tyyppisellä pyynnöllä haetaan. Kuten normaalissakin HTTP-kommunikaatiossa, GET-pyyntöjen ei tule muuttaa dataa.
REST on hyödyllinen arkkitehtuurimalli mm. olemassaolevien jo hieman ikääntyneiden palveluiden kapselointiin. Sovelluskehittäjä voi kehittää uuden käyttöliittymän, ja käyttää vanhaan sovellukseen liittyvää toiminnallisuutta REST-rajapinnan kautta. REST-arkkitehtuuriin perustuvat palvelut ovat nykyään hyvin yleisiä ja niiden luomiseen on tehty huomattava määrä apuohjelmia. Yksinkertaisen REST-palvelun voi luoda esimerkiksi suoraan olemassaolevasta NetBeansin Web Services -osiossa olevien toimintojen avulla.
Tutustu Roy T. Fieldingin ja Richard N. Taylorin artikkeliin "Principled Design of the Modern Web Architecture"
, jossa REST-arkkitehtuurityyli määritellään.
Vaikka yllä määrittelemme REST-apin HTTP-apin kautta, on Roy Fielding (nykyään?) sitä mieltä, että REST-apissa on oleellista mahdollisuus resurssien välillä navigointiin. Datan tulee olla hypertekstimäistä, ja osoitteiden palauttaman datan tulee sisältää linkit toisiin resursseihin.
Olemme hieman kerettiläisiä, ja määrittelemme REST-apin toisin kuin Fielding toivoo: jos noudattaisimme hypertekstimäistä navigointia emme voisi käyttää PUT- ja DELETE-metodeja.
Luodaan seuraavaksi oma REST-arkkitehtuuria seuraava kontrolleri aiemmin nähtyjen Person
-olioiden luontiin ja muokkaamiseen.
REST-tyyppisten palveluiden ei aina kannata palauttaa kokonaista HTML-sivua: REST-rajapintaa käytetäänkin usein ilman erillistä HTML-sivua. Tarvitsemme kuitenkin jonkun tavan tiedon esittämiseen ja siirtämiseen. Yleisimmät tiedonsiirtomuodot tällä hetkellä ovat JSON ja XML, joista ensimmäisen suosio kasvaa jatkuvasti. Olemme tällä kurssilla lähinnä kiinnostuneita JSON-muotoisesta datasta.
JSON (JavaScript Object Notation) on Javascriptin käyttämä tiedonsiirtoformaatti. JSON-formaatin yksi hienous on se, että JSON-objekteja voi lukea suoraan Javascript-olioiksi. Tämä tulee tutummaksi kurssilla Web-selainohjelmointi. JSON mahdollistaa avain-arvo -parien ja listojen esittämisen. Oletetaan, että käytössämme on seuraava olutta kuvaava luokka Beer
.
JSON-tyyppistä dataa kuuntelevien metodien toiminnallisuutta voi kätevästi testata curl
-komennon avulla. Alla olevassa esimerkissä palvelimelle lähetetään POST-tyyppinen pyyntö, joka sisältää JSON-tyyppistä dataa. Pyyntö tehdään osoitteeseen http://palvelin-ja-sovellus/beer
.
Kenoviivan käyttö mahdollistaa komennon kirjoittamisen useammalle riville.
Jeff Bezos @ Amazon: Anyone who doesn't do this will be fired.
SOA (Service Oriented Architecture), eli palvelukeskeinen arkkitehtuuri, on suunnittelutapa, jossa eri sovelluksen komponentit on suunniteltu toimimaan itsenäisinä avoimen rajapinnan tarjoavina palveluina. Pilkkomalla sovellukset erillisiin palveluihin pyritään luomaan tilanne, jossa palveluita voidaan käyttää myös tulevaisuudessa kehitettävien sovellusten toimesta. Avoimet rajapinnat helpottavat huomattavasti palveluihin integroitumista.
SOA-palveluita käyttävät esimerkiksi toiset palvelut tai selainohjelmistot. Esimerkiksi web-sivustot voivat hakea JSON-muotoista dataa Javascriptin avulla ilman tarvetta omalle palvelinkomponentille. SOA-arkkitehtuuri helpottaa myös ikääntyvien sovellusten jatkokäyttöä: ikääntyvät sovellukset voidaan kapseloida esimerkiksi REST-rajapinnan taakse, jonka kautta sovelluksen käyttö onnistuu myös jatkossa.
Rakentamalla palvelut avoimia rajapintoja käyttäen, palveluiden käyttäjän ei tarvitse välittää palvelun toteutuksessa käytetystä ohjelmointikielestä. Niin pitkään kun palvelun tuottama data seuraa avointa standardia, esimerkiksi JSON-dataa tuottavaa REST-tyyliä, ei palveluiden taustalla olevilla ohjelmointikielillä ole väliä. Oleellista kuitenkin on, että kun palvelua kehitetään, sitä käyttävien sovellusten toiminnan ei tule rikkoutua.
Oikeastaan, olemme jo toteuttaneet SOA-arkkitehtuuriin perustuvia sovelluksia. Esimerkiksi aiemmin toteutettu RESTGull Service toimii palveluna, jota muut voivat käyttää.
Tutustu artikkeliin The Biggest Thing Amazon Got Right: The Platform.
Uskotko että Amazonin ohjelmistokehittäjät olisivat lähteneet SOA-muutokseen ilman Jeff Bezosin viestin kohtaa nro 6?
Seuraako juuri toteuttamasi Pastebin-sovellus SOA-arkkitehtuurityyliä? Jos ei, mitä muutoksia siihen tulisi tehdä?
Tähän mennessä toteuttamissamme sovelluksissa pyynnön suorittaminen on tapahtunut seuraavasti:
Tietokantaoperaation tai palvelukutsun valmistumisen odottaminen ei aina ole käyttäjäystävällistä. Jos sovelluksemme suorittaa esimerkiksi raskaampaa laskentaa tai on muista palveluista johtuen hidas, kannattaa pyyntö suorittaa asynkronisesti.
Asynkroniset metodikutsut tapahtuvat seuraavasti:
Käytännössä asynkroniset metodikutsut toteutetaan luomalla palvelukutsusta erillinen säie, jossa pyyntöä käsitellään. Tämäkin on toteutettu usean sovelluskehyksen puolesta automaattisesti. Asynkronisten metodikutsujen käyttöön saaminen vaatii taas pienen määrän konfiguraatiota. Lisää ensin Springin konfiguraatiotiedostoon nimiavaruusmäärittely spring-task
-nimiavaruudelle.
Springin task
-toiminnallisuuden avulla voimme myös toteuttaa palveluita, joihin kytketyt toiminnallisuudet suoritetaan tietyin aikavälein. Voisimme esimerkiksi haluta RSS-lukijan, joka hakee uusimmat uutiset kerran minuutissa. Annotaatio @Scheduled
mahdollistaa tietyin aikavälein tapahtuvat pyynnöt. Sille voidaan määritellä ajastuksia esimerkiksi cron-formaatissa. Seuraavan komponenttiluokan metodi executeOnceInAMinute
suoritetaan kerran minuutissa.
Sovellusta testatessamme törmäämme uudestaan ja uudestaan HTTP-statuskoodiin 400. Sovelluksen koodia läpikäydessä emme löydä konkreettista virhettä, ja lähdemme etsimään virheen aiheuttajaa muualta. GlassFishin versioon 3.1.2 on päässyt bugi, joka aiheuttaa ongelmia tiedostojen käsittelyssä. Ongelman voi onneksi kiertää vaihtamalla palvelimen vanhempaan versioon.
Kun lähetämme lomakkeen, palvelimen logeissa näkyy lomakkeella lähetetyt arvot.
Huomattava osa sovelluskehityksestä sisältää integroitumista olemassaoleviin ohjelmistokomponentteihin. Olemassaolevat komponentit tarjoavat jonkinlaisen rajapinnan, jota sovelluskehittäjät voivat käyttää integroitumiseen. Integraatio ei ole aina helppoa: joissain tapauksissa komponenttiin tulee rakentaa erillinen integraatiorajapinta ennenkuin komponenttia voi käyttää.
Monoliittiset "minä sisällän kaiken mahdollisen" -sovellukset ovat usein vaikeita ylläpitää, sillä uuden toiminnallisuuden lisääminen vaatii olemassaolevan sovelluksen muokkaamista. Sovellus voi olla kirjoitettu nykyään hyvin vähäisesssä käytössä olevalla kielellä (vrt. pankkijärjestelmät ja COBOL), ja sovellukset sisältävät harvoin muokkausta tukevia automaattisia testejä.
Rakentamalla tarvittu sovellus alusta lähtien palveluorientoituneesti, eli siten, että se koostuu erillisistä palveluista, saadaan jatkokehitys ja ylläpito huomattavasti helpommaksi. Jokainen palvelu kapseloi oman yksittäisen toimintansa, jonka se tekee erittäin hyvin -- yksikään palvelu ei yritä tehdä kaikkea. Konkreettinen sovellus, joka tarjotaan asiakkaalle, toimii palveluita käyttävänä kapelimestarina, jonka tehtävänä on koordinoida palveluiden välistä toimintaa sopivasti.
Esimerkiksi jos palvelu tarjoaa valmiin REST-rajapinnan, on siihen integroituminen helpohkoa. Käytännössä palvelua käyttävän sovelluksen omat palvelut ottavat yhteyden REST-rajapinnan tarjoamaan palveluun, ja hakee sieltä tarvittavaa tietoa.
Periaatteessa REST-palveluita käyttävien sovellusten tekeminen ei poikkea millään tavalla verkkosovelluksia käsittelevien sovellusten tekemisestä. Yksinkertaisimmillaan GET-pyynnön voi toteuttaa Javalla URL
- ja HttpURLConnection
-luokkia käyttämällä seuraavasti.
Jos vastaus sisältää JSON-muotoista dataa, on se osana vastauksen runkoa. Käytännössä Javan valmiita luokkia käytetään harvemmin verkkokyselyjen tekemiseen. Apache Software Foundation-projekti HTTP Components tarjoaa valmiin toiminnallisuuden esimerkiksi pyynnön palauttaman rungon hakemiseen. Esimerkiksi ylläolevan sovelluksen toiminnallisuuden saa toteutettua HttpComponentsin tarjoaman Fluent-APIn avulla seuraavasti.
Palveluiden aggregointi, eli useamman palvelun datan yhdistäminen, on usein tarpeellista sovelluksia tehdessä. Pohditaan esimerkkiä, jossa tavoitteena on luoda sovellus, jossa henkilöitä asetetaan työhuoneisiin. Käytössämme on REST-api henkilöiden ja huoneiden käsittelyyn. Henkilöiden käsittelyyn tarkoitettu API on määritelty seuraavasti.
Huoneiden käsittelyssä käytetty API on lähes samanlainen.
Entä kun haluamme tietää tietyssä huoneessa olevat henkilöt? Tai henkilölle asetettu huone?
Huoneessa olevat henkilöt
Yksi vaihtoehto huoneessa olevien henkilöiden listaamiseksi on muokata huonepalvelun apia siten, että se palauttaa myös huoneeseen liittyvät henkilöt. Tämä ei kuitenkaan ole kovin mielekästä, tai edes järkevää. Huoneista vastaavan palvelun tulee hoitaa vain huoneisiin liittyviä asioita, ei henkilöiden listaamista.
Parempi vaihtoehto on määritellä huoneeseen liittyville henkilöille oma kyselyosoite, joka aggregoi huoneiden ja henkilöiden tiedot yhteen. Määritellään uusi REST-tyyppinen rajapinta huoneen kautta tapahtuvien henkilöiden käsittelyyn.
Henkilön huone
Kuten edellä, yksi vaihtoehto olisi muokata henkilöpalvelua siten, että se palauttaisi myös henkilön huoneen tiedot. Tämä ei kuitenkaan ole mielekästä, sillä henkilöitä käytetään todennäköisesti myös muuhunkin: huonetietojen listaus jokaisella kyselyllä on turhaa.
Määritellään erillinen rajapinta henkilöiden huoneiden käsittelyyn.
Käytännössä palveluita yhdistelevien osoitteiden määrittelyyn ei ole sovittu yhtä oikeaa lähestymistapaa, vaan sovelluskehittäjän tulee itse suunnitella mahdollisimman kuvaavat osoitteet.
Yllä olevissa aggregaattipalveluissa on tarkoituksella jätetty pois henkilöiden ja huoneiden muokkaaminen. Ongelman muodostaa jo usein se, että esimerkiksi huoneisiin liittyvien henkilöiden listaaja ei pakosti esitä henkilöistä samoja tietoja kuin henkilöpalvelu. Oikeastaan henkilöiden ja huoneiden muokkaaminen ei edes kuulu yllä määritellyt rajapinnat toteuttavan palvelun vastuulle.
Suunnitellaan palvelut siten, että ne toteuttavat oman vastuualueensa hyvin, ja jättävät muut tehtävät muille.
Käyttäjien määrän kasvaessa palveluiden tulee pystyä skaalautua mukana. Palveluiden ja sovellusten skaalautumiseen on käytännössä kaksi vaihtoehtoa: resurssien kasvattaminen (vertikaalinen skaalautuminen), sekä palvelinmäärän kasvattaminen (horisontaalinen skaalautuminen). Resurssien kasvattamiseen sisältyy lisämuistin hankinta, algoritmien optimointi ym, kun taas palvelinmäärän kasvattamisessa sovelluskehittäjän tulee luoda mahdollisuus pyyntöjen jakamiseen palvelinten välillä.
Sovellukset eivät skaalaannu lineaarisesti, ja skaalautumiseen liittyy paljon muutakin kuin resurssien lisääminen. Jos yksiytimisellä prosessorilla varustettu palvelin pystyy käsittelemään sata pyyntöä sekunnissa, emme voi laskea, että kahdeksanytiminen prosessori pystyy käsittelemään kahdeksansataa pyyntöä sekunnissa. Skaalautuminen ei ole vain palvelun tehon lisäämistä. Aivan kuten raketin lisääminen autoon vauhdin lisäämiseksi ei pakosti tuota hyvää lopputulosta, yksittäinen tehokas komponentti sovelluksessa ei takaa sovelluksen tehokkuutta.
Käytännössä skaalautumisesta puhuttaessa puhutaan horisontaalisesta skaalautumisesta, jossa käyttöön hankitaan lisää tietyntyyppisiä resursseja (esim. palvelimia). Vertikaalinen skaalautumisen harkinta on mahdollista tietyissä tapauksissa, esimerkiksi tietokantapalvelimen toimintaa suunniteltaessa, mutta yleisesti ottaen horisontaalinen skaalautuminen on kustannustehokkaampaa. (vrt. kahden miestyökuukauden käyttö algoritmin optimointiin, joka aiheuttaa algoritmin 10% tehoparannuksen, vs. ylimääräisen palvelimen hankkiminen, josta saa saman hyödyn.)
Pyyntöjen määrän kasvaessa yksi ratkaisu on palvelinmäärän kasvattaminen. Tällöin pyyntöjen jakaminen palvelinten kesken hoidetaan erillisellä kuormantasaajalla (load balancer), joka ohjaa pyyntöjä palvelimille.
Jos sovellukseen ei liity tilaa, kuormantasaaja voi ohjata pyyntöjä käytössä oleville palvelimille round-robin -tekniikalla. Jos sovellukseen liittyy tila, tulee tietyn asiakkaan tekemät pyynnöt ohjata aina samalle palvelimelle. Tämän voi toteuttaa esimerkiksi siten, että kuormantasaaja lisää pyyntöön evästeen, jonka avulla käyttäjä identifioidaan ja ohjataan oikealle palvelimelle. Tätä lähestymistapaa kutsutaan usein termillä (sticky session).
Pelkkä palvelinmäärän kasvattaminen ja kuormantasaus ei kuitenkaan ole aina tarpeeksi. Kuormantasaus helpottaa verkon kuormaa, mutta ei ota kantaa palvelinten kuormaan. Jos yksittäinen palvelin käsittelee pitkään kestävää laskentaintensiivistä kyselyä, voi kuormantasaaja ohjata tälle palvelimelle lisää kyselyjä "koska eihän se ole vähään aikaan saanut mitään töitä". Käytännössä tällöin kuormantasaaja lisää kuormaa entisestään paljon laskentaa tekevälle palvelimelle. On mahdollista käyttää kuormantasaajaa, joka pitää kirjaa palvelinten tilasta, mutta käytännössä palvelinten kuorma vaihtuu hyvin nopeasti, ja reagointi kuorman vaihtumiseen ei aina ole nopeaa.
Parempi ratkaisu palvelinmäärän kasvattamiselle on palvelinmäärän kasvattaminen ja sovelluksen suunnittelu siten, että laskentaintensiiviset operaatiot käsitellään erillisillä palvelimilla. Käytännössä lähestymistavassa käytetään erillistä laskentaklusteria aikaa vievien laskentaoperaatioiden käsittelyyn, jolloin käyttäjän pyyntöjä kuuntelevan palvelimen kuorma pysyy alhaisena.
Riippuen pyyntöjen määrästä, palvelinkonfiguraatio voidaan toteuttaa jopa siten, että staattiset tiedostot (esim. kuvat) löytyvät erillisiltä palvelimilta, GET-pyynnöt käsitellään asiakkaan pyyntöjä vastaanottavilla palvelimilla, ja datan muokkaamista tai prosessointia vaativat kyselyt (esim POST) ohjataan asiakkaan pyyntöjä vastaanottavien palvelinten toimesta laskentaklusterille.
Kun palvelinohjelmistoja skaalataan siten, että osa laskennasta siirretään erillisille palvelimille, on oleellista että palveluiden välillä kulkevat viestit (pyynnöt ja vastaukset) säilyvät. Eniten käytetty lähestymistapa viestien säilymisen varmentamiseen on viestijonot (messaging, message queues), joiden tehtävänä on toimia viestien väliaikaisena säilytyspisteenä. Käytännössä viestijonot ovat erillisiä palveluita, joihin viestien tuottajat (producer) voivat lisätä viestejä, joita viestejä käyttävät palvelut kuluttavat (consumer).
Viestijonoja käyttävät sovellukset kommunikoivat viestijonon välityksellä. Tuottaja lisää viestejä viestijonoon, josta käyttäjä niitä hakee. Kun viestin sisältämän datan käsittely on valmis, prosessoija lähettää viestin takaisin. Viestijonoissa on yleensä varmistustoiminnallisuus: jos viestille ei ole vastaanottajaa, jää viesti viestijonoon ja se tallennetaan esimerkiksi viestijonopalvelimen levykkeelle. Viestijonojen konkreettinen toiminnallisuus riippuu viestijonon toteuttajasta.
Viestijonosovelluksia on useita, esimerkiksi ActiveMQ ja RabbitMQ. Viestijonoille on myös useita standardeja, joilla pyritään varmistamaan sovellusten yhteensopivuus. Esimerkiksi Javan melko pitkään käytössä ollut JMS-standardi määrittelee viestijonoille APIn, jonka viestijonosovelluksen tarjoajat voivat toteuttaa (vrt. JDBC). Nykyään myös AMQP-protokolla on kasvattanut suosiotaan.
Aivan kuten esimerkiksi REST-tyylisen rajapinnan käyttö osana palveluorientoitunutta arkkitehtuuria, myös viestijonot mahdollistavat eri sovellusten helpon integroimisen. Viestejä tuottava sovellus voi olla toteutettu esimerkiksi COBOLilla, kun taas viestejä vastaanottava sovellus voidaan toteuttaa esimerkiksi Perlillä.
JMS ja ActiveMQ
Viestijonojen käyttö monimutkaistaa yksinkertaista sovellusta ja yksinkertaistaa monimutkaista sovellusta. -- mikke
Web-sovelluskehityksessä nopeasta kehityssyklistä on huomattavasti hyötyä. Työkaluja valittaessa tarkoituksena on välttää nurkkaan ajautumista: työkaluista tulee pystyä myös pääsemään eroon. On paljon hyödyllisempää miettiä asiaa päivä ja käyttää muutama päivä prototyypin tekemiseen, koska prototyyppiä voidaan parantaa kuukausia, kuin miettiä kuukausi ja sitouttaa itsensä kuukauden aikana luotuun suunnitelmaan. Mitä nopeammin toiminnallisuutta on olemassa, sitä nopeammin siitä saa palautetta. Mitä vähemmän käytämme aikaa yksittäisen toiminnallisuuden toteuttamiseen -- KISS -- sitä helpommin siitä voi tarpeen vaatiessa hankkiutua eroon.
Vaikka olemme nähneet tähän mennessä paljon esimerkkejä mm. SOA-arkkitehtuurista, voi ohjelmiston prototyyppin tehdä ensin "yksinkertaiseksi" kerrosarkkitehtuuria noudattavaksi sovellukseksi. Kun prototyypin toiminta on varmistettu, voidaan sovellusta lähteä kehittämään paremmaksi -- skaalautumisen tarve selvenee projektin myötä. Prototyypin kehitys ei missään nimessä tarkoita hyvien ohjelmointikäytänteiden tai järkevän arkkitehtuurin unohtamista. Päinvastoin, prototyypit päätyvät usein tuotantokäyttöön ja jatkokehitykseen. Järkevä arkkitehtuuri ja rajapintojen käyttö mahdollistaa sovelluksen pilkkomisen erillisiin SOA-arkkitehtuuria mukaileviin palveluihin ilman että sovelluksen ulkopuolinen toiminta muuttuu. Rajapintojen alla olevat toteutukset toki muuttuvat.
Jotta sovelluskehitys olisi nopeaa, tulee ohjelmistokehitystiimillä olla käytössä yhteiset työkalut ja käytänteet. Olemme kurssin alusta käyttäneet Mavenia, joka helpottaa sovelluksen riippuvuuksien hallintaa. Tutustutaan tässä muutamaan hyödylliseen käytänteeseen.
Perinteisesti sovelluksia kehitettäessä sovellusta suoritetaan neljässä eri paikassa. (1) Jokaisella ohjelmistokehittäjällä on oma "hiekkalaatikko", jossa koodiin voi tehdä muutoksia vaikuttamatta muiden tekemään työhön. Jokaisella ohjelmistokehittäjällä on yleensä samat tai samankaltaiset työkalut (ohjelmointiympäristö, ...), mikä helpottaa muiden kehittäjien auttamista. Aina kun sovelluskehittäjä on saanut tehtävän valmiiksi, tehtävään liittyvä koodi ja muutokset lähetetään integraatiopalvelimelle esim. git-versionhallintaa käyttäen. (2) Integraatiopalvelimen tehtävänä on yhdistää ohjelmistokehitystiimin lähdekoodit ja suorittaa niihin liittyvät testit jokaisen muutoksen yhteydessä. Integraatiopalvelin kuuntelee käytännössä versionhallintajärjestelmässä tapahtuvia muutoksia, ja hakee uusimman lähdekoodiversion muutoksen yhteydessä. Integraatiopalvelimella on käytössä osajoukko tuotantopalvelimen käyttämästä datasta validointitarkoituksiin.
(3) QA-ympäristö (Staging-palvelin) on lähes identtinen ympäristö tuotantoympäristöön verrattuna. QA-ympäristöön kopioidaan ajoittain tuotantoympäristön data, ja se toimii viimeisenä testaus- ja validointipaikkana (Quality assurance) ennen tuotantoon siirtoa. QA-ympäristöä käytetään myös demo- ja harjoitteluympäristönä. Kun QA-ympäristössä oleva sovellus on päätetty toimivaksi, siirretään sovellus tuotantoympäristöön. (4) Tuotantoympäristö voi olla yksittäinen palvelin, tai se saattaa olla joukko palvelimia, joihin uusin muutos propagoidaan hiljalleen. Tuotantoympäristön tulee olla erillinen ympäristö muista ympäristöistä.
Jatkuvassa integroinnissa (Continuous integration) jokainen ohjelmistoprojektin jäsen lisää päivittäiset muutoksensa olemassaolevaan kokonaisuuteen. Tämä vähentää virheiden löytämiseen ja korjaamiseen kuluvaa aikaa, koska virheet löytyvät jo varhaisessa vaiheessa. Kun virheet löytyvät aikaisin ovat muutokset vielä kehittäjillä mielessä, ja syiden etsiminen vie huomattavasti vähemmän aikaa kuin integraatioita harvemmin tehtäessä.
Jatkuvaa integrointia seuraten ohjelmistokehittäjä hakee kehityksen alla olevan version versionhallinnasta aloittaessaan työn. Hän toteuttaa uuden pienen ominaisuuden testeineen, testaten uutta toiminnallisuutta jatkuvasti. Kun ohjelmistokehittäjä on saanut muutoksen tehtyä, ja kaikki testit menevät läpi hänen paikallisella työasemalla, hän lähettää muutokset versionhallintaan. Kun versionhallintaan tulee muutos, jatkuvaa integrointia suorittava työkalu hakee uusimman version ja suorittaa sille sekä yksikkö- että integraatiotestit.
Testejä sekä paikallisella kehityskoneella että erillisellä integraatiokoneella ajettaessa ohjelmistotiimi mahdollisesti huomaa virheet, jotka ovat piilossa kehittäjän paikallisen konfiguraation johdosta. Kehittäjä ei aina ota koko ohjelmistoa omalle koneelleen -- ohjelmisto voi koostua useista komponenteista -- jolloin kaikkien vaikutusten testaaminen paikallisesti on mahdotonta. Jos käännös ei mene läpi integraatiokoneella, kehittäjän tulee tehdä korjaukset mahdollisimman nopeasti.
Työkaluja automaattiseen kääntämiseen ja jatkuvaan integrointiin ovat muunmuassa Jenkins, Apache Continuum ja CruiseControl. TKTL:llä on opiskelijoiden käyttöön suunnattu Jenkins-asennus osoitteessa http://jenkins.staff.cs.helsinki.fi/.
Jos sovellusta ei ole rakennettu kunnolla, on sen siirtäminen eri ympäristöjen välillä tuskaa. Hyvin rakennetussa sovelluksessa ympäristön vaihtaminen ei aiheuta muutoksia sovelluksen lähdekoodiin. Käytännössä sovellusten hallinta tapahtuu profiilien avulla. Spring tarjoaa profiileille oman konfiguraatiotyylin. Luodaan seuraavaksi sovellus, jossa on erillinen tuotanto- ja kehitysympäristö. Kehitysympäristössä käytössä on muistiin ladattava H2-tietokanta, tuotantoympäristössä PostgreSQL-tietokantaa (oletetaan että pom.xml:ssä) on molempien tarvitsemat riippuvuudet.
Käytännössä profiilien hallinta kannattaa toteuttaa esimerkiksi niin, että integraatio-, qa- ja tuotantoympäristöön määritellään ympäristömuuttuja, joka kertoo käytettävän profiilin. Springiä käytettäessä ympäristömuuttuja on SPRING_PROFILES_ACTIVE
tai spring.profiles.active
.
Ympäristömuuttujan asetus tapahtuu *nix-koneilla export
-komennolla.
Web-sovellusten testaamiseen kuuluu yksikkötestaus, integraatiotestaus ja systeemitestaus. Yksikkötestauksessa testataan sovellukseen kuuluvia yksittäisiä komponentteja ja varmistetaan että niiden tarjoamat rajapinnat toimivat kuten pitäisi. Integraatiotestauksessa testataan että komponentit toimivat yhdessä kuten niiden pitäisi, ja systeemitestauksessa sovellusta testataan ulkopuolelta esim. selaimella.
Yksikkötestauksella tarkoitetaan lähdekoodiin kuuluvien yksittäisten osien testaamista. Termi yksikkö viittaa ohjelman pienimpiin mahdollisiin testattaviin toiminnallisuuksiin, kuten olion tarjoamiin metodeihin. Single responsibility principlen mukaisesti haluamme pilkkoa ohjelmiston pieniin yksittäisen vastuun omaaviin yksikköihin, joiden testaaminen on helppoa: niillä on vain yksi vastuu. Testaus tapahtuu yleensä testausohjelmistokehyksen avulla, jolloin luodut testit voidaan suorittaa automaattisesti. Yleisin Javalla käytettävä testauskehys on JUnit. JUnit-kirjaston saa käyttöön lisäämällä siihen liittyvän riippuvuuden pom.xml-tiedostoon.
Testejä kirjoitetaan usein iteratiivisesti samaan aikaan sovellusta kehitettäessä. Paljon suosiota saanut TDD-menetelmä perustuu siihen, että sovellusta kehitetään kirjoittamalla siihen liittyviä testejä ensin. Tällöin sovellus on pakko pilkkoa pieniin osiin heti alusta.
TDD-sykli tarkemmin: (1) Luo uusi testi joka testaa ei-olemassaolevaa toiminnallisuutta. (2) Aja olemassaolevat testit ja varmista että uusi testi ei mene läpi. Jos testi menee läpi testi on viallinen tai toivottu toiminnallisuus on jo toteutettu. (3) Kirjoita uutta toiminnallisuutta varten yksinkertaisin mahdollinen toteutus, joka läpäisee testin. Uuden toiminnallisuuden tulee olla toteutettu siten, että se läpäisee juuri kirjoitetun testin – ei muuta. (4) Suorita olemassaolevat testit ja varmista että ne menevät läpi. Jos testit eivät mene läpi, tarkista uuden toiminnallisuuden aiheuttamat muutokset. (6) Refaktoroi eli siisti koodia. Esimerkiksi toistuva koodi tulee siirtää omaan metodiinsa. (7) Palaa kohtaan (1)
Yksinkertainen Laskin-esimerkki (ohjelmistojen mallintaminen, kesä 2011):
Testit ovat erittäin oleellisia sovelluksen ylläpitovaiheessa. Käytännössä uuden sovelluskehittäjän liittyessä ohjelmistotiimiin, on olemassaolevan sovelluksen muokkaus erittäin vaikeaa ilman testejä. Yksi muutos voi rikkoa useampia toiminnallisuuksia, joiden rikkoutumisesta olemassaolevat testit ilmoittavat.
Spring tarjoaa JUnit-kirjastolle tuen, jonka avulla saamme Autowired-annotaatiot toimimaan. Lisätään Spring-test -riippuvuus projektimme pom.xml-tiedostoon.
Springin oliokontekstia käyttävät yksikkötestit tarvitsevat kaksi annotaatiota alkuun. Annotaatio @RunWith(SpringJUnit4ClassRunner.class)
kertoo että käytämme Springiä yksikkötestien ajamiseen ja annotaatiolle @ContextConfiguration(locations = {".."})
annetaan käytettävien konfiguraatioiden sijainnit. Testiluokan alku näyttää esimerkiksi seuraavalta:
Ylläolevan konfiguraatiomäärittelyn avulla Spring pyrkii lataamaan oliokontekstin, ja asettamaan halutut oliot testiluokkiin, jossa niiden toiminnallisuutta voidaan testata.
Systeemitestaukseen on monenlaisia työkaluja, joista eräs on Selenium. Selenium on web-testauskehys, joka antaa sovelluskehittäjälle mahdollisuuden käydä läpi sovelluksen käyttöliittymää ohjelmallisesti samalla varmistaen että sivuilla on toivotut asiat. Se simuloi myös Javascript-komponenttien toiminnallisuutta, minkä avulla käyttöliittymän testaus helpottuu huomattavasti. Selenium on erittäin hyödyllinen esimerkiksi käyttötapausten läpikäynnissä. Saamme Seleniumin käyttöön lisäämällä sen osaksi pom.xml
-tiedostoa.
Ajatellaan käyttötapausta jossa käyttäjä haluaa syöttää tunnuksen lomakekenttään ja päätyä toisenlaiselle sivulle. Haluamme löytää lomakekentän nimeltä "name". Kun kenttään asetetaan arvo "Bob" ja kenttään liittyvä lomake lähetetään, tulee sivulla olla lomakekenttä nimeltä "age".
Yllä käytämme HtmlUnitDriver-oliota html-sivun läpikäyntiin. Haemme ensin määritellyn osoitteen, eli surffaamme haluttuun osoitteeseen. Haemme osoitteesta saadusta lähdekoodista kentän name-attribuutilla "name", ja lisäämme kenttään arvon "Bob". Tämän jälkeen lomake lähetetään. Kun lomake on lähetetty, haetaan kenttää jolla attribuutin name arvona on "age". Jos kenttää ei löydy (eli palautettu arvo on null), testi epäonnistuu ja käyttäjä näkee viestin "Element "age" not found.".
Jatketaan sovellusten skaalautumisen pohdintaa vielä hieman. Olemme aiemmin pohtineet vertikaalista ja horisontaalista skaalautumista sekä viestijonojen käyttöä vastauksena käyttäjien määrän lisääntymiseen. Aiemmin esitetyt lähestymistavat auttavat ongelmissamme, mutta voimme vielä parantaa tilannetta.
Sovelluksen suorituskykyä ja toiminnallisuutta mitattaessa oleelliseen rooliin nousee sovelluksen profilointi. Esimerkiksi NetBeans sisältää valmiin sovelluksen profiloijan, jonka avulla voi tutkia eri sovelluksen osissa käytettyä aikaa. Profiloijan käyttäminen yhdessä kuormitustestaustyökalun kanssa (esim. Apache JMeter tai The Grinder) auttaa kyselyiden simuloinnissa, jolloin ongelmakohdat löytyvät helposti.
Kuormitustestauksessa tulee ottaa huomioon sovellukseen tulevien kyselyiden todellinen profiili. Sovelluksen profilointituloksissa on huomattavia eroja riippuen pyyntöprofiileista. Jos 99% sovellukseen kohdistuvista pyynnöistä on GET-tyyppisiä pyyntöjä, ei kuormitustestaustyökalun tule testata 50% POST, 50% GET -tyyppisellä profiililla.
Spring Insight
Spring tarjoaa sovelluskehittäjille Spring Insight-sovelluksen, jonka avulla sovelluksen suorituskyvyn analysointi helpottuu huomattavasti. Se kiinnittyy sovelluksessa tapahtuviin suorituspolkuihin, jotka alkavat esimerkiksi HTTP-pyynnöistä. Jokaisessa suorituspolussa on joukko operaatioita, jotka kuvaavat merkittäviä tapahtumia suorituspolussa. Operaatiot ovat esimerkiksi tietokantakyselyitä tai transaktioiden tallennuksia.
Spring Insight generoi suorituspolkudatasta automaattisia yhteenvetoja, joiden perustella sovelluksen toimintaa voi analysoida melko tarkasti. Tämä mahdollistaa muunmuassa ongelmakohtien löytämisen, jonka avulla sovelluksen suoritustehoa voi parantaa. Lue lisää täältä...
Käytännössä huomattava osa web-palvelinohjelmistoille tulevista kyselyistä on GET-tyyppisiä pyyntöjä. GET-tyyppiset pyynnöt eivät muokkaa palvelimella olevaa dataa, vaan pyytävät vain tietoa. Esimerkiksi tietokannasta dataa hakevat GET-tyyppiset pyynnöt luovat aina yhteyden erilliseen tietokantasovellukseen, josta ne hakevat dataa pyynnön perusteella. Sovellusten skaalautumista pohtiva sovelluskehittäjä alkaakin miettimään että eikö uudestaan ja uudestaan haettavaa dataa voisi tallentaa välimuistiin.
Ovela ohjelmoija hyödyntää Javan valmista kalustoa välimuistin toteuttamiseen. Esimerkiksi Javan luokka LinkedHashMap
tarjoaa mainion pohjan oman välimuistin toteutukseen. Luokan metodia removeEldestEntry
kutsutaan aina uuttaa arvoa lisättäessä. Käytännössä välimuistin voi luoda perimällä luokan LinkedHashMap
, ja korvaamalla metodin removeEldestEntry
siten, että huomioon otetaan välimuistille määriteltävä parametrin cacheSize
, joka määrittelee välimuistin koon.
Melko ovelaa, eikö?
Ovelaa kyllä, muttei fiksua. Keksimme vahingossa taas pyörän uudestaan.
Maailmalla on huomattava määrä valmiita cache-toteutuksia, joista yksi on EHcache. Vaikka tässä tutustumme EHcacheen vain pikaisesti, tarjoaa se toiminnallisuuden muunmuassa hajautettujen välimuistien toteutukseen ja ns. big data -skaalan sovellusten tukemiseen.
Koska käytössämme on Spring, konfiguroidaan EHcache Springille. Spring tarjoaa erilaisten välimuistitoteutusten abstraktion. Lisätään ensin EHcacheen liittyvä riippuvuus pom.xml-tiedostoomme.
Olemme jo tykästyneet annotaatioihin, joten otetaan käyttöön välimuistin hallinnointi annotaatioilla. Huomaa että Springin cache-abstraktio ei ota kantaa välimuistin konfiguraatioon, joten se tulee luoda erikseen. Luodaan EHCachelle yksinkertainen konfiguraatio, joka määrittelee muistissa käytettävän cachen. Erillisen konfiguraatiotiedoston käyttäminen on kätevää, sillä voimme kehityskoneilla käyttää yksinkertaista cachea, kun taas tuotantokoneilla cachekonfiguraatio voi olla täysin erilainen. Luodaan kansioon src/main/resources
, eli Other Sources, tiedosto ehcache-dev.xml
, johon lisätään seuraava konfiguraatio. Tarkemmin konfiguraatiosta löytyy mm. EHcachen omasta dokumentaatiosta.
Lisätään seuraavaksi Spring-konfiguraatioomme (esim. front-controller-servlet.xml
) seuraava konfiguraatio. Haluamme hallinnoida välimuistia annotaatioilla, ja käyttää EHcachea välimuistin toteuttajana. EHcachen konfiguraatiotiedostona on yllä määritelty ehcache-dev.xml
-tiedosto.
Kun olemme konfiguroineet välimuistin, voimme toteuttaa välimuistin hieman eritavalla kuin aiemmin. Spring tarjoaa käyttöömme mm. @Cacheable
- ja @CacheEvict
-annotaatiot, joiden avulla voidaan määritellä välimuistin käyttö ja tyhjennys metodeille. Yllä olevan JpaBeerService
-luokan toteutus hoituisi nyt seuraavasti.
Käytännössä metodille read
luodaan proxy-metodi, joka ensin tarkistaa onko haettavaa tulosta välimuistissa. Välimuistina käytetään välimuistia nimeltä "beers"
, joka on aiemmin konfiguroitu tiedostossa ehcache-dev.xml
. Jos tulos on välimuistissa, palautetaan se sieltä, muuten tulos haetaan tietokannasta ja tallennetaan välimuistiin. Oleellisinta tässä lähestymistavassa on se, että välimuistin konfiguraatiota voi muuttaa käytettävien profiilien avulla. Aiemmin toteutettu ohjelmallisesti tehty välimuisti ei skaalaudu, kun taas valmista cache-komponenttia käyttävä sovellus skaalautuu konfiguraatiosta riippuen.
Välimuistia voi käyttää myös kontrolleritasolla, esimerkiksi JSON-muotoista olutdataa palauttavan kontrollerimetodin tuloksen saa välimuistiin seuraavasti.
Huom! Spring tai EHcache ei tarkista muuttuuko cachen takana oleva data. Dataa muuttavat metodit tulee annotoida sopivasti annotaatiolla @CacheEvict
, jotta välimuistista poistetaan muuttuneet tiedot.
Pilvipalvelut ovat verkossa toimivia palveluita. Pilvipalveluissa maksetaan vain käytetyistä resursseista -- sovelluksesta (SaaS, Software as a Service), alustasta (PaaS, Platform as a Service) tai laskentakapasiteetista (IaaS, Infrastructure as a Service). Pilvipalveluille ominaista on skaalautuvuus, palveluita käytetään vain kun on tarve. Jos käyttäjiä on paljon, käytössä olevien resurssien määrää voi dynaamisesti lisätä -- jos käyttäjiä on vähän, resurssien määrää voidaan laskea.
Sovellusalustaa palveluna (PaaS, Platform as a Service) tarjoavat yritykset mahdollistavat järjestelmän nopeamman kehittämisen -- sovelluskehittäjän ei tarvitse välittää alustasta sillä se on jo valmiina. Kustannusten arviointi on helpompaa sillä pilvipalveluiden laskutus tapahtuu käytön mukaan -- sovelluskehittäjä voi lisätä oman ylläpitokurstannuksen. Sovellusalustat piilottavat taustalla olevan infrastruktuurin, jolloin sovelluskehittäjän ei tarvitse välittää taustalla toimivasta palvelusta. Osa PaaS-tarjoajista toimii IaaS-tarjoajien tarjoamien palveluiden päällä: Esimerkiksi kohta tutuksi tuleva Heroku pyörii Amazonin päällä.
Pilvipalveluiden käyttäminen mahdollistaa sovelluksen skaalautumisen tarpeen mukaan. Kun sovellus on hyvin aktiivisessa käytössä, voidaan siitä luoda useampia resursseja helposti. Toisaalta, jos sovelluksen käyttö pienenee, voidaan käytössä olevia resursseja vähentää. Pilvipalveluilla ei vielä ole yhtenäistä skaalautumismallia, mutta skaalautuminen perustuu samoihin periaatteisiin kuin "normaalien" sovellusten skaalautuminen.
Kun pilvipalvelu huomaa resurssien lisätarpeen, se käynnistää uusia sovellusta pyörittäviä palvelimia. Uusien palvelimien käynnistyksen tulee olla helppoa ja selkeästi määriteltyä, esimerkiksi Herokulla uuden sovelluksen käynnistämisen vaativat komennot on määritelty erilliseen Procfile
-nimiseen tiedostoon, jonka pohjalta uusia palvelimia käynnistetään. Kun uusi palvelin käynnistetään, lisätään se automaattisesti sovellukseen liittyvälle (ohjelmistopohjaiselle) reitittimelle, joka alkaa ohjaamaan pyyntöjä myös uudelle palvelimelle. Reititin tarjotaan käytännössä aina pilvipalvelun puolesta.
Jos pyyntöjen määrä palvelulle vähenee, voidaan käynnissä olevia palvelimia ajaa alas. Esimerkiksi Heroku sammuttaa ylimääräisiä palvelimia jos niille ei ole ollut tarvetta viimeisen tunnin aikana. Maksullisissa pilvipalveluissa yksi palvelin on yleensä aina päällä, jolloin vasteajat ovat aina nopeita. Ilmaiset pilvipalvelut sammuttavat viimeisimmätkin palvelimet jos pyyntöjä ei tule vähään aikaan. Tällöin palvelin käynnistetään vain tarpeen vaatiessa.
Useamman palvelimen pyörittäminen nostaa palvelun vikasietoisuutta. Käytännössä pilvipalveluissa palvelimet pyörivät erillisissa fyysisissä sijainneissa, jolloin vikatapaukset tietyssä koneessa eivät vaikuta koko sovelluksen toimintaan.
Pilvipalvelut eivät ota kantaa sovelluksen sisäiseen rakenteeseen. Esimerkiksi sovelluksen arkkitehtuuria ei muuteta viestijonoja käyttäväksi tarvittaessa. Sovelluksen tehokas arkkitehtuurisuunnittelu on aina sovelluskehittäjän vastuulla. Jos palvelimia käynnistetään paljon sen takia, että sovelluksen suunnittelija ei ole ottanut huomioon erilaisia sovelluksen tarpeita (esim. raskaan prosessoinnin hoitaminen erillisellä palvelulla), tulee pilvipalvelun käyttämisen kustannukset myös olemaan korkeammat sillä palvelimia käynnistetään niiden kuorman perusteella. Raskas prosessointi kannattaa toteuttaa erillisellä palvelulla siten, että prosessointiin erikoistunut palvelu hakee työt erillisestä töitä varastoivasta viestijonosta.
Kannattaa tutustua herokun skaalautumismalliin osoitteessa http://www.heroku.com/how/scale. Miten Herokun malli poikkeaa Amazon Beanstalkin mallista (Best practises, Architectural overview)?.
Autentikointi ja autorisointi. Autentikoinnilla tarkoitetaan sitä, että käyttäjän identiteetti varmistetaan. Autentikointi tapahtuu esimerkiksi käyttäjätunnuksen ja salasanan avulla. Autorisoinnilla taas tarkoitetaan sen varmistamista, että käyttäjä saa tehdä hänen yrittämiään asioita.
HTTP-protokolla vaatii sen toteuttajilta kaksi autentikointitapaa: "Basic autentikoinnin" ja "digest autentikoinnin". Emme käsittele niitä tässä, lisätietoa niistä löytää täältä.
Autentikointi, eli käyttäjän identiteetin varmistaminen, on yksi web-sovellusten oleellisista ominaisuuksista. Käyttäjän identiteetti tarkistetaan yleisimmin käyttäjätunnus-salasana -parin avulla siten, että käyttäjä lähettää ne HTTP-yhteyden yli palvelimelle. Kiitettävä osa nykyaikaisista sovelluskehyksistä tarjoaa autentikointimekanismin osana tietoturvakomponenttiaan. Autentikointisovellukset toimivat yleensä erillisinä filttereinä tarkistaen sovellukselle tehtäviä pyyntöjä.
Käyttäjän identiteetin varmistaminen vaatii käyttäjälistan, joka taas yleensä ottaen tarkoittaa käyttäjän rekisteröintiä palveluun. Käyttäjän rekisteröitymisen vaatiminen heti sovellusta käynnistettäessä voi rajoittaa käyttäjien määrää huomattavasti, joten rekisteröitymistä kannattaa pyytää vasta kun siihen on tarve.
Erillinen rekisteröityminen ei ole aina tarpeen. Web-sovelluksille on käytössä useita kolmannen osapuolen tarjoamia keskitettyjä identiteetinhallintapalveluita. Esimerkiksi OpenAuth:n avulla sovelluskehittäjä voi antaa käyttäjilleen mahdollisuuden käyttää jo olemassaolevia tunnuksia.
Autentikoinnin lisäksi tulee varmistaa, että käyttäjät pääsevät käsiksi vain heille oikeutettuihin resursseihin. Käyttäjät määritellään yleensä joko yksilöllisesti tai roolien kautta. Yleisin lähestymistapa käyttöoikeuksien määrittelyyn on käyttöoikeuslistat (ACL, Access Control List), joiden avulla määritellään käyttäjien (tai käyttäjäroolien) pääsy resursseihin. Käytännössä käyttöoikeuslistat määritellään osoitteille tai metodeille.
Autorisointi pelkkien polkujen perusteella ei aina riitä. Käyttöliitymissä halutaan esimerkiksi tarjota käyttäjäroolikohtaista toiminnallisuutta. Aiemmin lisäämämme riippuvuus spring-security-taglibs
tarjoaa näkymissä käytettävän tägikirjaston, jonka perusteella sivun eri osa-alueiden näkymistä voidaan rajoittaa käyttäjäkohtaisesti.
Näkymätason autorisointi ei myöskään aina riitä. Usein halutaan rajoittaa toiminnallisuutta siten, että tietyt operaatiot (esim. poisto tai lisäys) halutaan mahdollistaa vain tietyille rooleille. Käyttöliittymän näkymää rajoittamalla ei voida rajoittaa kutsuja polkuihin, ja aiemmin luotu polkuihin tehtävien kutsujen rajoitus ei auta esimerkiksi REST-tyyppisissä osoitteissa, varsinkin jos GET-pyyntöihin halutaan oikeus kaikille.
Käyttäjien hakeminen tietokannasta tai muusta palvelusta onnistuu muuttamalla security.xml
-tiedostossa olevaa konfiguraatiota. Esimerkiksi jos käytössämme on USERS
- ja ROLES
-taulut, voimme käyttää erillistä JDBC-yhteyttä käyttäjätunnusten hakemiseen. Kysely tulee toki muokata vastaamaan käytössä olevaa tietokantarakennetta.
Jos pääsynhallinnan haluaa toteuttaa itse, tarjoaa Spring Security tarjoaa rajapinnan UserDetailsService
, joka tulee toteuttaa omaa palvelua toteuttaessa. Kts. esim https://github.com/avihavai/wad-2012/blob/master/v6-runko-db/src/main/java/wad/spring/service/WadUserDetailsService.java.
Lisää tietoa tietokannan käytöstä osana autentikaatiota löytyy Spring Securityn dokumentaatiosta.
Usein kommunikointi selaimen ja palvelimen välillä halutaan salata, erityisesti salasanojen lähetyksen yhteydessä.
HTTPS on HTTP-pyyntöjä SSL (nykyisin myös TLS)-rajapinnan yli. HTTPS mahdollistaa sekä käytetyn palvelun verifioinnin sertifikaattien avulla että lähetetyn ja vastaanotetun tiedon salauksen.
HTTPS-pyynnöissä asiakas ja palvelin sopivat käytettävästä salausmekanismista ennen varsinaista kommunikaatiota. Käytännössä selain ottaa ensiksi yhteyden palvelimen HTTPS-pyyntöjä kuuntelevaan porttiin (yleensä 443), lähettäen palvelimelle listan selaimella käytössä olevista salausmekanismeista. Palvelin valitsee näistä parhaiten sille sopivan (käytännössä vahvimman) salausmekanismin, ja lähettää takaisin salaustunnisteen (palvelimen nimi, sertifikaatti, julkinen salausavain). Selain ottaa mahdollisesti yhteyttä sertifikaatin tarjoajaan -- joka on kolmas osapuoli -- ja tarkistaa onko sertifikaatti kunnossa.
Selain lähettää palvelimelle salauksessa käytettävän satunnaisluvun palvelimen lähettämällä salausavaimella salattuna. Palvelin purkaa viestin ja saa haltuunsa selaimen haluaman satunnaisluvun. Viesti voidaan nyt lähettää salattuna satunnaislukua ja julkista salausavainta käyttäen.
Käytännössä kaikki web-palvelimet tarjoavat HTTPS-toiminnallisuuden valmiina, joskin se täytyy ottaa palvelimilla käyttöön. Esimerkiksi Herokussa HTTPS:n saa käyttöön komennolla heroku addons:add piggyback_ssl
. Lisätietoa Herokun SSL-tuesta osoitteessa http://devcenter.heroku.com/articles/ssl.
Internationalisoinnilla (i18n
) tarkoitetaan sovelluksen rakentamista siten, että sen toteuttaminen usealla kielellä on mahdollista. Lokalisoinnilla (l10n
) taas tarkoitetaan yksttäiselle kieli- tai kulttuurialueelle tehtävää kielitoteutusta.
Sovellusta internatinalisoidessa luodaan oletuskieli, joka ylikirjoitetaan erillisillä lokalisaatioilla. Sovelluksessa lopulta käytettävän kielen valinta tapahtuu ympäristön tai erillisen parametrin avulla. Esimerkiksi Javassa on käytössä Locale
-luokka, joka sisältää määrittelyt eri maiden kielille.
Käytännössä internationalisaatio alkaa määrittelemällä sovellukseen viestikohtaiset tägit. Tägeille tehdään kielikohtaiset resurssitiedostot, josta kielikohtaiset tekstit haetaan tägien paikalle. Lokalisoidun käyttöliittymän luominen tapahtuu siis käytännössä tägien ja resurssitiedostojen perusteella.
Javassa on valmiina ResourceBundle
-luokka, jota voi käyttää resurssien hakemiseen. Käytännössä resurssit ovat kielikohtaisissa properties
-tiedostoissa. Esimerkiksi luokan ResourceBundle
staattisella metodilla getBundle
haetaan resursseja tietyllä kielellä.
Web-sovellusten internationalisointi tapahtuu samalla lähestymistavalla kuin komentorivisovelluksen lokalisointi. Käyttöliittymätekstit muutetaan tageiksi, ja lokalisoidut tekstit tallennetaan niille sopivaan paikkaan. Web-sovellusten lokalisointiin käytetään usein luokkaa ReloadableResourceBundleMessageSource
, joka toimii lähes samoin kuin aiemmin näkemämme ResourceBundleMessageSource
. Itse konfiguraatio asetetaan osaksi front-controller-servlet.xml
-tiedostoa.
Huomattava osa nykyaikaisista web-palveluista lataavat käyttäjälle palvelimelta vain staattisen käyttöliittymän. Käyttöliittymä sisältää joukon JavaScript-komponentteja, jotka tuovat ja vievät dataa tarvittaessa käyttäjälle -- esimerkiksi JSON-muodossa. Seuraavassa tehtäväsarjassa toteutetaan palvelintoiminnallisuus erillisille selainpuolen sovellukselle. Tehtävän sovelluksessa käytettävää selainpuolen toiminnallisuutta toteutetaan näillä näkymin kurssin Web-selainohjelmointi
noin viikolla 4.
Tekemisellä on ollut hyvin iso rooli kurssilla. Kurssin pisteistä 60% saa kurssin tehtävistä, loput kokeesta. Tämä tarkoittaa käytännössä sitä, että kurssilla vaadittu ohjelmointiosaaminen on varmistettu ja arvosteltu jo kurssin aikana. Ohjelmointi- tai konfigurointitehtäviä ei ole kokeessa. Kokeessa keskitytään teorian ja taustakäsitteiden ymmärryksen ja niiden selitystaidon varmistamiseen.
Tiistain luennolla äänestetään kahdesta koevaihtoehdosta:
Vaihtoehto a) Koepaperissa on 9 teoriakysymykstä, joista tulee vastata kuuteen. Yksittäisen vastauksen maksimipituus on 2 sivua ja yksittäisestä kysymyksestä voi saada maksimissaan 4 pistettä.
Vaihtoehto b) Koepaperissa on 5 teoriakysymystä, joista tulee vastata neljään. Yksittäisen vastauksen maksimipituus on 4 sivua, ja yksittäisestä tehtävästä voi saada maksimissaan 6 pistettä.
Kurssilla on käytössä koeleikkuri, joten kokeesta tulee saada yhteensä vähintään 12 pistettä.
Kuvien piirtäminen on hyvin suositeltua kummassakin vaihtoehdossa. Luennolla valittiin vaihtoehto a, eli kokeessa tulee olemaan 9 teoriakysymystä, joista tulee vastata kuuteen.
Luennolla pohdittiin myös sitä, millaisia tyypilliset koekysymykset voisivat olla. Osallistujilta tuli mm. seuraavia ehdotuksia:
Huom! Kokeessa tulee olemaan myös ylimääräinen tehtävä (max 6p). Ylimääräisellä tehtävällä voi korvata viikon, jolta on kerännyt vähiten pisteitä. Koe on kuitenkin ajoitettu siten, että ylimääräiseen tehtävään ei ole varsinaisesti aikaa. Vastaus ylimääräiseen tehtävään onnistunee jos varsinaisten vastausten jäsentelyyn ei tarvitse juurikaan käyttää aikaa. Ylimääräisestä tehtävästä saatuja pisteitä ei huomioida koeleikkurissa.
Käy antamassa kurssista palautetta laitoksen palautejärjestelmään. Mitä olisit toivonut enemmän, mitä vähemmän, mikä meni hyvin ja mitä pitäisi vielä parantaa? Palautejärjestelmän osoite on https://ilmo.cs.helsinki.fi/kurssit/servlet/Lomake
Koska palautteet ovat anoynyymejä, tästä tehtävästä ei saa pisteitä.