Web-sovellusohjelmointi

Arto Vihavainen, kurssiassistenttina Matti Luukkainen

Lukijalle

Tämä materiaali on tarkoitettu kevään 2012 kurssille web-sovellusohjelmointi. Materiaali päivittyy kurssin edetessä ja sisältää myös kurssiin liittyvät tehtävät.

Lue materiaalia siten, että teet samalla itse kaikki lukemasi esimerkit. Esimerkkeihin kannattaa tehdä pieniä muutoksia ja tarkkailla, miten muutokset vaikuttavat ohjelman toimintaan. Äkkiseltään voisi luulla, että esimerkkien tekeminen ja muokkaaminen hidastaa opiskelua. Tämä ei kuitenkaan pidä ollenkaan paikkansa. Oppiminen perustuu oleellisesti aktiiviseen tekemiseen ja rutiinin kasvattamiseen. Esimerkkien ja erityisesti omien kokeilujen tekeminen on parhaita tapoja sisäistää luettua tekstiä.

Pyri tekemään tai ainakin yrittämään tehtäviä sitä mukaa kuin luet tekstiä. Jos et osaa heti tehdä jotain tehtävää, älä masennu, sillä saat ohjausaikoina neuvoja tehtävien tekemiseen.

Tekstiä ei ole tarkoitettu vain kertaalleen luettavaksi. Joudut varmasti myöhemmin palaamaan aiemmin lukemiisi kohtiin tai aiemmin tekemiisi tehtäviin. Tämä teksti ei sisällä kaikkea oleellista web-sovellusohjelmointiin liittyvää. Itse asiassa ei ole olemassa mitään kirjaa josta löytyisi kaikki oleellinen. Eli joudut joka tapauksessa ohjelmoijan urallasi etsimään tietoa myös omatoimisesti. Kurssin harjoitukset sisältävät jo jonkun verran ohjeita, mistä suunnista ja miten hyödyllistä tietoa on mahdollista löytää.

Ohjelmointiympäristö

Ohjelmointiympäristöt tarjoavat kokoelman hyödyllisiä apuvälineitä usein toistuviin tapahtumiin, kuten ohjelmointiprojektien luomiseen, projektin paketointiin ym. Käytämme kurssin esimerkeissä NetBeans-ohjelmointiympäristöä. Ohjelmointiympäristön tai tuottavuutta yhtä paljon helpottavan työkalun käyttäminen on suositeltavaa. Älä siis ohjelmoi <aseta tähän joku perustekstieditori kuten nano, gedit, notepad tai notepad++>:lla. Vaikka ohjelmointiympäristön käyttö voi aluksi tuntua vaikealta tutustumiseen menevän ajan takia, pääset myöhemmin nostamaan käyttämäsi ajan korkoina.

Esimerkeissä ja tehtävissä oletetaan että käytössäsi on NetBeansin versio 7.1 (tai uudempi)

Web-sovelluksista

Web-sovellukset koostuvat hieman yksinkertaistaen 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ä. WebGL 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 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.

Web-sovellusten kehittäminen

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 -- 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ä lähdettäisiin heti rakentamaan isoa järjestelmää -- käytännössä järjestelmän valmistuessa sille ei olisi käyttäjiä sillä kaikki olisivat siirtyneet toiseen aiemmin 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 pitää pystyä myös pääsemään eroon. On paljon hyödyllisempää miettiä asiaa päivä ja käyttää muutama päivä prototyypin tekemiseen -- jota 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.

Ensimmäinen web-sovellus

Ensimmäisen web-sovelluksen tekeminen tapahtuu ohjattuna tehtäväsarjana.

Ohjelmointiympäristöön tutustuminen ja ensimmäinen Servlet

NetBeans

Käynnistä NetBeans. Jos käytössäsi ei ole NetBeansia, lataa se ensin osoitteesta http://www.netbeans.org. Käytämme kurssilla versiota 7.1, mutta uudemmatkin versiot kelpaavat. Kun lataat NetBeansin, valitse versio jossa on käytössä kaikki mausteet (eli all).

NetBeans koostuu useammasta alueesta. Vasemmalla laidalla on projektivalikko (Projects), sekä välilehdet tiedostoille (Files, projektiin liittyvät ei-projektinäkymässä näkyvät tiedostot, esimerkiksi testidata ym) ja palveluille (Services, esim. tietokantayhteydet ja palvelimet). Tällä hetkellä ei meillä ei ole yhtään käytössä olevaa projektia. Hiiren kursorin osoittamassa kohdassa näkyy aktiiviset lähdekooditiedostot. Alalaidassa on tulostusalue. Alalaitaan tulee myös erilaisten palvelujen (esim. kehitysvaiheessa käytettävän integroidun web-palvelimen tulostus -- älä huoli jos tätä ei näy sinulla!).

Ylälaidassa on erilaisia valikoita sekä pikakuvakkeita.

Ensimmäisen web-sovellusprojektin luominen

Valitse File-valikko ja klikkaa New Project-vaihtoehtoa.

Kun valitset "New Project", eli uuden projektin luominen, NetBeans avaa eteesi projektityyppivalikon. Valitse Java Web ja Web Application. Paina Next.

Tämän jälkeen sinulta kysytään projektin nimeä ja sijaintia. Aseta projektin nimeksi eka-servlet, jätä muut asetukset sellaisiksi kuin ne ovat. Varmista että vaihtoehto "Use Dedicated Folder for Storing Libraries" on ruksaamatta ja valitse Next.

Eteesi aukeaa kehityspalvelimen ja projektin asetukset (Server and Settings). Valitse joku palvelin, esimerkiksi Apache Tomcat. Voit valita käyttöösi myös jonkun muun palvelimen kuin Apache Tomcatin. Huom! Valitse Java EE-versioksi "Java EE 5". Aseta kontekstipoluksi (Context Path), eli poluksi missä web-sovelluksesi kuuntelee pyyntöjä, /eka-servlet.

Paina seuraavaksi Finish. Jos painoit Next, älä valitse sovelluskehyksiä (Frameworks) käyttöön -- ne eivät ole oleellisia tässä tehtävässä).

NetBeansin vasemmassa laidassa on nyt aktiivisena projekti eka-servlet -- aktiivisen projektin näkee tummennetusta nimestä. Keskellä näkyy projektiin liittyvä index.jsp-niminen lähdekooditiedosto, joka näytetään oletuksena kun web-sovellus avataan selaimessa.

Vasempaan alalaitaan on ilmestynyt navigaatioalue, joka näyttää aktiivisena (eli keskellä olevassa editointialueessa olevan) tiedoston rakenteen. Tämä on hyödyllinen kun halutaan nopea yleiskuva tiedostosta.

Testaa NetBeansiin integroitua palvelinta

Kun web-projekti on aktiivisena, voit tarkastella sen toimintaa palvelimella. NetBeansiin integroitu web-palvelin käynnistyy kun painat yläpalkissa olevaa vihreää play-nappia tai näppäintä F5.

Projektin käynnistäminen avaa myös uuden selainikkunan, jossa näytetään projektiin liittyvä pääsivu (tässä index.jsp). Uuden selainikkunan automaattisen avautumisen voi asettaa pois päältä projektiin liittyvissä asetuksissa (valitse projekti oikealla hiirennapilla, valitse Properties -> Run -> "Display Browser on Run".

Alla näemme kuvan siitä miltä sovelluksemme näyttää tällä hetkellä. Huomaa että sovelluksen polkuna on eka-servlet eli aiemmin valitsemamme kontekstipolku.

Uuden Servletin luominen

Servlet "servletti" on Java-ohjelma, joka suoritetaan palvelimella käyttäjän selatessa servlettiin liittyvään osoitteeseen.

Luodaan ensimmäinen oma servlet. Alla olevassa kuvassa on näytetty miten lähdekoodikansioon voi luoda servletin. Avaa projekti projektivalikossa painamalla sitä vasemmalla hiirennapilla. Näet neljä erillistä kansiota: Web Pages, joka sisältää web-sivut sekä konfiguraatiotiedostoja, Source Packages, joka sisältää lähdekooditiedostoja, Libraries, joka sisältää käytössä olevia apukirjastoja, ja Configuration Files, joka sisältää oleelliset konfiguraatiotiedostot yhdessä paikassa.

Vasemmassa laidassa näkyvä hakemistorakenne ei ole projektin oikea "fyysinen" hakemistorakenne. Näkymä on tehty helpottamaan käytössä olevien asioiden muistamista.

Valitse "Source Packages" oikealla hiirennapilla, valitse New ja Servlet...

Eteesi aukeaa uuden Servlet-luokan tekoa helpottava työkalu. Valitse Luokan nimeksi (Class Name) AwesomeServlet ja aseta pakkaukseksi (Package) wad. Paina lopuksi Finish.

NetBeansin keskellä olevassa ikkunassa on juuri luodun Servlet-luokan lähdekoodi, vasemmassa alalaidassa on nopea yhteenveto luokassa olevista metodeista. NetBeans luo käyttöösi nipun valmiita metodeja, joita voit laajentaa. Oleellisimpia metodeja ovat doGet ja doPost, jotka käsittelevät pyyntöjä. GET- ja POST-sanan merkitykseen palataan myöhemmin.

NetBeans on luonut käyttöösi erillisen processRequest-metodin, jota doGet ja doPost kutsuvat. Metodi processRequest käyttää HttpServletResponse-olioon liittyvää PrintWriter-olion tarjoamaa println-metodia viestin tulostamiseksi käyttäjän tekemän pyynnön vastaukseksi.

Tutustu web.xml:ään

Web-sovellukset koostuvat useammasta osasta. Servlettimme AwesomeServlet on vain yksi osa kokonaisuutta.

Jotta palvelin tietäisi minne mikäkin pyyntö tulisi ohjata, kuuluu isoon osaan Java-maailman web-sovellusprojekteista konfiguraatiotiedosto web.xml. NetBeansin web-sovellusprojekteissa se löytyy kansiosta "Configuration Files". Etsi web.xml ja avaa se.

Tiedosto web.xml sisältää yksittäiseen projektiin liittyvän servlet-ohjauksen (servlet mapping), joka määrittelee palvelimelle missä osoitteessa mitäkin Servlet-luokkaa tulee kutsua.

Oleellisia AwesomeServlet-servletin kannalta ovat seuraavat rivit:

    <servlet>
        <servlet-name>AwesomeServlet</servlet-name>
        <servlet-class>wad.AwesomeServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>AwesomeServlet</servlet-name>
        <url-pattern>/AwesomeServlet</url-pattern>
    </servlet-mapping>

Määre <servlet> sisältää Servlet-luokan nimen (servlet-name) "AwesomeServlet" ja käännetyn lähdekooditiedoston sijainnin (servlet-class) "wad.AwesomeServlet". Määre <servlet-mapping> sisältää osoitteen (url-pattern) "/AwesomeServlet" mitä aiemmin määritellyn niminen (servlet-name) servlet "AwesomeServlet" kuuntelee. Normaalisti Servleteistä koostuvissa web-sovelluksissa on useampia servlettejä ja niihin liittyviä konfiguraatioita, jokainen Servlet kuuntelee yhtä tai useampaa osoitetta.

Selaa AwesomeServlet-luokan osoitetta kuuntelevaan osoitteeseen

Sovelluksemme kontekstipolku on eka-servlet. Juuri luotu servlettimme kuuntelee osoitteessa /AwesomeServlet. Mene selaimella osoitteeseen <palvelimen osoite>/eka-servlet/AwesomeServlet. Selaimen palvelimelle tekemä pyyntö ohjautuu servletille AwesomeServlet.

Jos et näe yllä olevaa vastausta, varmista että palvelimesi on päällä. Näet myös Servlettiin ohjautuneen pyynnön lisätietoja NetBeansin alalaidassa.

AwesomeServlet-luokan muuttaminen

Muuta servlet-luokassa tapahtuvaa tulostusta siten, että näet viestin Awesomer!.

Asiakas-palvelin -malli

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.

Asiakas-palvelin -mallin haasteita

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 (esim. 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?

Asiakas-palvelin -malli -- Chuck Norris

Valitse sopiva web-selain ja mene osoitteeseen http://www.imdb.com. Kirjoita sivuston ylälaidassa olevaan kenttään "Chuck Norris" ja paina Enter. Mitkä seuraavista askeleista tapahtuivat asiakasohjelmistossa, mitkä palvelinohjelmistossa, mitkä muualla? Jos vastauksesi on muualla, kerro missä. Voit olettaa että asiakasohjelmistolla tarkoitetaan valitsemaasi web-selainta.

  1. Osoitteen http://www.imdb.com kirjoittaminen.
  2. Osoitetta http://www.imdb.com vastaavan palvelimen etsiminen.
  3. Sivun http://www.imdb.com näyttäminen web-selaimen käyttäjälle.
  4. Chuck Norrikseen liittyvien tietojen ja elokuvien haku elokuvadatasta.

Asiakas-palvelin -mallin haasteita

Lue Helsingin sanomien artikkelit Lippupisteen järjestelmässä yhä häiriöitä, Poliisin ja sisäministeriön verkkosivut kaatuivat ja Wikipedian artikkeli Palvelunestohyökkäys. Mitä tekemistä uutisissa kuvatuilla artikkeleilla on palvelunestohyökkäyksen kanssa? Miten lippupiste pystyy parantamaan palvelunsa saatavuutta? Entä miten poliisi ja sisäministeriö? Perustele.

Knock-knock -viestiprotokolla

Eräs suosittu viestiprotokolla (eli säännöstö, joka kertoo kuinka kommunikoinnin tulee kulkea) alkaa sanoilla Knock knock!. Toinen osapuoli vastaa tähän Who's there?. Ensimmäinen osapuoli vastaa jotain, esim. Art, jonka jälkeen toisen osapuolen tulee vastata Art who?. Tähän ensimmäinen osapuoli vastaa viestillä joka päättyy "Bye.".

Palvelin: Knock knock!
Asiakas: Who's there?
Palvelin: Robin
Asiakas: Robin who?
Palvelin: Robin your house! Bye.

Palvelimen lataaminen ja käynnistäminen

Lataa palvelinohjelmisto: ViestiprotokollaPalvelin.jar.

Kun olet saanut ladattua palvelinohjelmiston sinulle sopivaan kansioon, mene kansioon komentotulkkia käyttäen ja käynnistä palvelin sanomalla

java -jar ViestiprotokollaPalvelin.jar

Palvelin käynnistyy oletuksena porttiin 12345. Jos haluat että se käynnistyy johonkin toiseen porttiin, voit antaa portin komentoriviparametrina. Palvelimen käynnistäminen esimerkiksi portissa 55555 tapahtuu kutsulla

java -jar ViestiprotokollaPalvelin.jar 55555

Kun olet saanut palvelimen päälle, siirry seuraavaan askeleeseen.

Asiakas

Täydennä allaoleva asiakasohjelmisto askelten mukaan siten, että sitä voi käyttää kommunikointiin viestiprotokollapalvelimen kanssa. Tehtävää varten kannattaa luoda erillinen ohjelmointiprojekti NetBeansissa.

        Socket yhteys = new Socket("localhost", tähän viestiprotokollapalvelimen portti);
        Scanner viestitPalvelimelta = new Scanner(yhteys.getInputStream());
        PrintWriter viestitPalvelimelle = new PrintWriter(yhteys.getOutputStream(), true);

        Scanner viestitKayttajalta = new Scanner(System.in);

        while (viestitPalvelimelta.hasNextLine()) {
            // 1. lue viesti palvelimelta
            // 2. tulosta palvelimen viesti standarditulostusvirtaan näkyville

            // 3. jos palvelimen viesti loppuu merkkijonon "Bye.", poistu toistolausekkeesta
            
            // 4. pyydä käyttäjältä palvelimelle lähetettävää viestiä
            // 5. kirjoita lähetettävä viesti palvelimelle. Huom! Käytä println-metodia.
        }

Voit asettaa asiakasohjelmiston lähdekoodin main-metodin sisältävään luokkaan. Kun olet saanut ohjelmiston valmiiksi, suorita se. Tulostuksen pitäisi olla esimerkiksi seuraavanlainen (käyttäjän syöttämät tekstit punaisella):

Palvelin: Knock knock!
Kirjoita palvelimelle lähetettävä viesti: viesti
Palvelin: Sinun tulee kysyä "Who's there?"
Kirjoita palvelimelle lähetettävä viesti: Who's there?
Palvelin: Lettuce
Kirjoita palvelimelle lähetettävä viesti: Lettuce who?
Palvelin: Lettuce in! it's cold out here! Bye.

Oraclen oppaasta pistokkeisiin (Socket) on huomattavasti hyötyä tässä tehtävässä: http://docs.oracle.com/javase/tutorial/networking/sockets/index.html

Kun olet saanut tehtävän tehtyä, saat suljettua viestiprotokollapalvelimen esimerkiksi valitsemalla ctrl + c.

Web ja HTTP

"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ä palvelut 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).

URI

"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 -- tai terminä käyttöön jäänyt URL, Uniform Resource Locator) koostuu useammasta osasta, joiden perusteella haluttuun sivustoon voidaan muodostaa oikeanlainen yhteys.

protokolla://isäntäkone[:portti]/polku/../[kohdedokumentti][?kyselyparametrit][#ankkuri]

Osoitteen osat

Mitkä ovat osoitteen http://www.googlefight.com/index.php?lang=en_GB&word1=Batman&word2=Superman protokolla, isäntäkone, portti, polku, kohdedokumentti, kyselyparametrit ja ankkuri? Jos joku osista puuttuu, anna esimerkki osoitteesta jossa se on.

DNS

DNS, Domain Name System, on hajautettu nimipalvelujärjestelmä tietokoneiden ja palveluiden löytämiseksi. Sen tehtävänä on muuntaa tekstuaaliset nimet (esim www.cs.helsinki.fi) 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-osoitteita tarvitaan oikean tietokoneen löytämiseksi.

Nimipalvelimet toimivat hierarkkisesti, korkeimmalla tasolla on 13 juuripalvelinklusteria jotka osaavat ohjata pyynnöt eteenpäin sopiville nimipalvelimille.

HTTP

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.

Käytännössä palvelimet ja selaimet kommunikoivat hyvin harvoin suoraan keskenään (poikkeuksia: omalla koneella toimivan palvelimen käyttö), vaan välissä on yksi tai useampi välityspalvelin, jonka tehtävänä on ohjata pyyntö eteenpäin. Yhteys selaimen ja palvelimen välillä muodostuu siis ketjusta koneita. Lähetettävä viesti ja siihen liittyvä vastaus kulkee kaikkien ketjussa olevien koneiden läpi.

traceroute-työkalu

Linux-ympäristöissä on käytössä traceroute-työkalu, jolla voi tutkia viestin kulkemaa reittiä verkossa. Sitä käytetään kirjoittamalla

traceroute palvelun_osoite.net

Vastauksena on lista palvelimista ja ajoista, jotka kertovat minkä verran viestillä meni kuhunkin osoitteeseen pääsemisessä. Alla esimerkki kyselystä traceroute www.google.fi.

$ traceroute www.google.fi
traceroute to www.google.fi (209.85.137.94), 30 hops max, 60 byte packets
 1  schengen.cs.helsinki.fi (128.214.9.157)  0.164 ms  0.137 ms  0.139 ms
 2  kumpula1-cs.fe.helsinki.fi (128.214.173.153)  0.488 ms  0.523 ms  0.602 ms
 3  vallila2-kumpula1.fe.helsinki.fi (128.214.173.22)  0.724 ms * *
 4  riippa-vallila2.fe.helsinki.fi (128.214.173.241)  0.625 ms  0.608 ms *
 5  * * *
 6  se-tug.nordu.net (109.105.102.61)  7.350 ms  7.406 ms  7.382 ms
 7  se-tug2.nordu.net (109.105.97.18)  7.268 ms  7.549 ms  7.520 ms
 8  * google-gw.nordu.net (109.105.98.6)  7.509 ms *
 9  * 209.85.250.192 (209.85.250.192)  7.717 ms 216.239.43.122 (216.239.43.122)  7.819 ms
10  209.85.249.40 (209.85.249.40)  17.190 ms 72.14.233.180 (72.14.233.180)  17.361 ms 209.85....
11  72.14.233.170 (72.14.233.170)  16.934 ms 72.14.233.172 (72.14.233.172)  16.845 ms  16.739 ms
12  209.85.254.33 (209.85.254.33)  27.601 ms *  18.017 ms
13  lpp01m02-in-f94.1e100.net (209.85.137.94)  16.781 ms  16.917 ms  19.168 ms

Tracerouten näyttämät tähdet (*) tarkoittavat että lähetettyyn kyselyviestiin ei annettu vastausta, tai että vastaus katosi matkalla takaisin. Tätä kutsutaan yleisesti ottaen packet loss:iksi.

Tehtävä: Kirjaudu jollekin TKTL:n koneelle ssh:n yli (esim melkille: melkki.cs.helsinki.fi), ja tutki mitä reittiä viestit kulkevat yhdysvaltojen puolustusministeriön verkkosivuille (defense.gov) ja FBI:n sivuille (fbi.gov). Ovatko verkkosivujen palvelimet samassa maanosassa? Perustele.

Tilattomuus

HTTP on tilaton protokolla, eli se ei tarvitse jatkuvasti avoinna olevaa yhteyttä toimiakseen. HTTP:n versio 1.0 rajoitti palvelimen ja selaimen välisen yhteyden olemassaolon yhteen pyyntö-vastaus -tapahtumaan, jolloin pyyntöjä ei ollut edes mahdollista hoitaa saman yhteyden aikana. HTTP:n versio 1.1 (HTTP/1.1) olettaa että yhteys on olemassa kunnes palvelin tai selain katkaisee sen.

Yhteyden olemassaoloa ja pysyvyyttä ei taata, joten pelkkää yhteyttä ei voida käyttää loppukäyttäjän tunnistamiseen.

HTTP-viestien rakenne: kysely

HTTP-viestit koostuvat riveistä jotka muodostavat otsakkeen, sekä riveistä jotka muodostavat viestin rungon. Viestin runkoa ei ole pakko olla olemassa. Kaksi peräkkäistä rivinvaihtoa kertoo viestin loppuneen. Otsakkeen ensimmäisellä rivillä on erikoisrivi. Palvelimille lähetettävissä viesteissä ensimmäisellä rivillä esitetään pyyntötapa, haluttu polku ja HTTP-versionumero.

PYYNTÖTAPA /POLKU_HALUTTUUN_RESURSSIIN HTTP/versio
otsake-1: arvo
otsake-2: arvo

valinnainen viestin runko

Pyyntötapa kertoo 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.1). Alla esimerkki hyvin yksinkertaisesta -- joskin yleisestä -- pyynnöstä. Huomaa että pyyntöä tehdessä yhteys palvelimeen on jo muodostettu. Palvelimen osoitetta ei merkitä erikseen.

GET /index.html HTTP/1.0

Palvelinkone voi sisältää useampia virtuaalisia palvelimia. Tällöin pelkkä polku haluttuun resurssiin ei riitä oikean resurssin löytämiseen -- se voisi olla millä tahansa koneeseen liittyvällä virtuaalisella palvelimella. Tämä on ratkaistu HTTP/1.1 -protokollassa siten, että pyyntöön tulee aina lisätä käytetyn palvelimen osoitteen kertova Host-otsake.

GET /index.html HTTP/1.1
Host: www.munpalvelin.net

telnet-työkalu

Linux-ympäristöissä on traceroute-työkalun lisäksi käytössä myös telnet, jota voi käyttää yksinkertaisena asiakasohjelmistona. Telnet-yhteyden tietyn koneen tiettyyn porttiin saa luotua komennolla

telnet isäntäkone portti

Luo telnet-yhteys web-palvelimen t-avihavai.users.cs.helsinki.fi porttiin 80 ja lähetä palvelimelle seuraava viesti:

GET / HTTP/1.0

Muistathan että viesti loppuu kahteen rivinvaihtoon.

Avaa seuraavaksi sama yhteys uudestaan, mutta lähetä tällä kertaa viesti:

GET / HTTP/1.1
Host: t-avihavai.users.cs.helsinki.fi

Mitä yhteistä ja mitä eroa vastausviesteissä on? Miksi?

Copy-pastesta voi olla hyötyä tässä tehtävässä...

HTTP-viestin rakenne: vastaus

Palvelimelta tulevissa vastauksissa on ensimmäisellä rivillä HTTP-versionumero, viestiin liittyvä statuskoodi ja 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

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 osoitteessa http://httpcats.herokuapp.com.

HTTP-otsakkeet -- Let's bounce!

Mitä osoitteessa http://t-avihavai.users.cs.helsinki.fi/lets/Bounce on ja miksen voi katsoa sitä selainta käyttäen?

Copy-pastesta voi olla hyötyä tässä tehtävässä...

Kuinka monta kyselyä?

Mene selaimella TKTL:n koneilla olevalle omalle kotisivullesi käyttämällä osoitetta cs.helsinki.fi/u/omatunnus. Jos sinulla ei ole omaa kotisivua käytössä, voit käyttää osoitetta cs.helsinki.fi/u/avihavai. Toimii hyvin, eikö?

Tutki telnetin avulla monta kyselyä selain joutuu oikeasti tekemään päästäkseen katsomaan web-sivua. Miten voit hyödyntää tätä tietoa tehokkaita web-sovelluksia suunniteltaessa?

Copy-pastesta voi olla hyötyä tässä tehtävässä...

Pyyntötavat

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. Nykyaikaisempi tapa on ETag-otsakkeen käyttäminen -- palaamme tähän myöhemmin kurssilla.

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: Sun, 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

Last-Modified -otsake määrittelee tulevaisuudessa olevan päivämäärän (pyyntö tehty Date -otsakkeen määrittelemänä ajanhetkenä). Huomaamme palvelimen otsakkeista 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).

Mitä otsakkeita näet ja mitä ne tarkoittavat?

Mitä otsakkeita (Request headers) näet sivulla http://t-avihavai.users.cs.helsinki.fi/lets/See ? Mitä kukin otsake tarkoittaa? Käytä apunasi sivustoja http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html ja http://en.wikipedia.org/wiki/List_of_HTTP_header_fields.

curl-työkalu

Telnetin lisäksi myös curl on hyödyllinen työväline pyyntöjen tekemiseen. Curl tarjoaa parametrin -i (include headers in output), jota voi käyttää otsaketietojen tarkasteluun pyynnön yhteydessä. Esimerkiksi kysely curl -i hs.fi palauttaa seuraavat tiedot:

HTTP/1.1 301 Object Moved
Location: http://www.hs.fi/
Content-Length: 0

Tehtävä: Mitä otsakkeita saat tekemällä curl-pyynnön osoitteeseen t-avihavai.users.cs.helsinki.fi? Mitä HTTP-versiota curl käyttää kyselyyn? Perustele.

Evästeet ja tilan ylläpitäminen

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 sisältää käyttäjäkohtaista toiminnallisuutta, jonka toteuttamiseen sovelluksella täytyy olla jonkinlainen tieto tilasta. HTTP/1.1 tarjoaa mahdollisuuden tilallisten verkkosovellusten toteuttamiseen evästeiden (cookies) avulla. Evästeitä käytetään istuntojen (session) ylläpitämiseen. Istuntojen avulla pystytään pitämään kirjaa käyttäjän tiedoista useampien pyyntöjen yli.

Evästeet toteutetaan otsakkeiden avulla. Evästettä luotaessa palvelin lähettää selaimelle otsakkeen Set-Cookie, jossa määritellään käyttäjäkohtainen evästetunnus.

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]
Set-Cookie: SESS57a5819a77579dfb1a1466ccceee22a0=0hr0aa2ogdfgkelogg; Max-Age=3600; Domain=".helsinki.fi"

Ylläoleva palvelimelta lähetetty vastaus pyytää selainta tallettamaan evästeen. Eväste SESS57a5819a77579dfb1a1466ccceee22a0=0hr0aa2ogdfgkelogg tulee lisätä jokaiseen helsinki.fi-osoitteeseen tehtävään pyyntöön seuraavan tunnin ajan.

Evästeet tallennetaan selaimen sisäiseen evästerekisteriin. Evästeet lähetetään palvelimelle jokaisen viestin yhteydessä. Selain lähettää evästeen tiedot Cookie-otsakkeessa.

Cookie: SESS57a5819a77579dfb1a1466ccceee22a0=0hr0aa2ogdfgkelogg

Evästeiden nimet ja arvot ovat yleensä monimutkaisia (ja satunnaisesti luotuja) niiden yksilöllisyyden takaamiseksi. Evästeet ovat sekä hyödyllisiä että haitallisia. Evästeiden avulla voidaan luoda yksiöityjä käyttökokemuksia tarjoavia sovelluksia, mutta niitä voidaan käyttää myös käyttäjien seurantaan ympäri verkkoa.

HTML

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

<!DOCTYPE html>
<html lang="fi">
<head>
  <meta charset="UTF-8">
  <title>selainikkunassa näkyvä otsikko</title>
</head>
<body>
  <p>Tekstiä tekstielementin sisällä, tekstielementti runkoelementin sisällä, 
     runkoelementti html-elementin sisällä. Elementin sisältö voidaan asettaa
     useammalle riville.</p>
</body>
</html>

Ylläoleva HTML5-dokumentti sisältää dokumentin tyypin ilmaisevan aloitustägin (<!DOCTYPE html>), dokumentin aloittavan html-elementin (<html>), otsake-elementin ja sivun otsikon (<head>, jonka sisällä <title>), sekä runkoelementin (<body>).

Elementit voivat sisältää attribuutteja, joille voi antaa arvoja. Esimerkiksi ylläolevassa esimerkissä html-elementille on määritelty erillinen attribuutti lang, joka kertoo dokumentissa käytetystä kielestä. Ylläolevan esimerkin otsakkeessa on myös metaelementti, jota käytetään lisävinkin antamiseen selaimelle: "dokumentissa käytetään utf-8 merkistöä". Tämä kannattaa olla dokumenteissa aina.

Nykyaikaiset web-sivut sisältävät paljon muutakin kuin sarjan HTML-elementtejä. Linkitetyt resurssit, kuten kuvat ja tyylitiedostot, ovat oleellisia sivun ulkoasun ja rakenteen luomisessa. Selainpuolella suoritettavat skriptitiedostot, erityisesti Javascript, ovat luoneet huomattavan määrän syvyyttä nykyaikaiseen web-kokemukseen. Tällä kurssilla emme ehdi juurikaan paneutua selainpuolen toiminnallisuuteen, mutta TKTL:llä on sitä varten erillinen kurssi Digitaalisen median tekniikat, kts. viime syksyn kurssisivu.

Lomake

Lomakkeita käytetään tiedon lähettämiseen web-palveluille. HTML-elementti lomakkeelle on<form>. Lomake-elementille voidaan antaa attribuutteina toiminto (action), jolle voidaan määrittelee osoite mihin lomakkeen sisältö lähetetään, ja lomakkeen lähetystapa (method). Lomakkeen lähetystapa (GET tai POST) kertoo lähetetäänkö lomakkeen tiedon kyselyparametreina osana osoitetta (GET) vai pyynnön yhteydessä erillisenä datana (POST). Käytetään lähetystapaa POST.

<form action="kohdeosoite" method="POST">

Jos attribuuttia action ei ole määritelty, lähetetään lomake oletuksena nykyiseen osoitteeseen. Attribuutin method oletusarvo on GET.

Lomakekentät

Lomake-elementin alle voi asettaa useita erilaisia kenttiä. Jos kentän arvon haluaa lähettää eteenpäin, tulee kentällä olla attribuutti nimi (name), jonka arvoa käytetään kenttään asetetun tiedon avaimena.

Lomakkeen lähettäminen

Kun lomake lähetetään selain ohjaa käyttäjän kohdeosoitteeseen siten, että lähetettävän lomakkeen tiedot ovat mukana selaimen tekemässä pyynnössä. Jos lomakkeen lähetystapa on GET, on lomakkeen tiedot osana osoitetta. Lähetystavassa POST arvot tulevat erillisinä.

Alla on lomake jolla voi visualisoida tietojen lähettämistä. Lomakkeiden toimintona on http://t-avihavai.users.cs.helsinki.fi/lets/See), jossa on pyynnössä saatujen tiedojen tulostava web-palvelu.

<form method="POST" action="http://t-avihavai.users.cs.helsinki.fi/lets/See">
  <label>Käyttäjätunnus: <input type="text" name="tunnus" /></label>
  <label>Salasana: <input type="password" name="salasana" /></label>
  <input type="submit" />
</form>

Salasanat ja GET

Tee ylläolevasta lomakkeesta kopio siten, että se lähettää lomakkeen tiedot GET-lähetystapaa käyttäen. Jos oletetaan että jokainen verkon välityspalvelin tallettaa kohdeosoitteen tilastointia varten ja viesti kulkee 12 välityspalvelimen kautta, kuinka moneen paikkaan syöttämäsi salasana tallentui?

Chat-websovellus

Kerrataan seuraavaksi edellä olleita asioita ja toteutetaan pienimuotoinen Chat-verkkopalvelu (lopullinen muoto osoitteessa: http://t-avihavai.users.cs.helsinki.fi/lets/Chat). Voit käyttää allaolevaa Chat-servlettiä runkona.

// täältä puuttuu pakettimäärittely ja importit

public class Chat extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        response.setContentType("text/html;charset=UTF-8");
        PrintWriter out = response.getWriter();
        try {
            // tulostaminen
        } finally {
            out.close();
        }
    }

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        // ei mitään vielä
    }
}

Chat-lomake

Luo Servletluokka Chat, joka kuuntelee haluamaasi polkua. Kopioi ylläoleva runko luomasi Chat-servletluokan päälle kun olet luonut Chat-servletin NetBeansissa -- näin sinun ei tarvitse muistella web.xml:n konfigurointia.

Kun käyttäjä avaa selaimella osoitteen (tekee GET-pyynnön), tulosta PrintWriter-luokasta tehdyn out-olion avulla seuraavanlainen lomake:

Chat

Käytä seuraavaa lähdekoodia lomakkeen luontiin. Varmista että tekstikentän name-attribuutilla on arvona viesti.

<strong>Chat</strong><br />

<form method="POST">
  <input type="text" name="viesti" /><input type="submit" value="Lähetä" />
</form>

Viestin vastaanottaminen

Kun käyttäjä kirjoittaa viestikenttään tekstiä ja painaa Lähetä-nappia, lomakkeen sisältö (eli tekstikentän viesti sisältö) lähetetään POST-tyyppisenä pyyntönä oletusosoitteeseen -- eli Chat-servletillesi. Jos Chat-servletissäsi ei ole doPost-metodia, johon POST-tyyppiset pyynnöt päätyvät, toteuta se.

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        // koodia
    }

Web-palvelin tallentaa pyyntöön liittyvät parametrit HttpServletRequest-olioon, josta niihin pääsee käsiksi mm. metodilla getParameter. Lisää doPost-metodiin toiminnallisuus, joka tulostaa lähetetyn viestin käyttäjälle ja lopettaa suorituksen. Chat-ohjelmasi tulee siis tulostaa vain parametrina saatu viesti-kentän arvo. Esimerkiksi viesti-kentästä lähetetty viesti "Hei!"

Hei!

Viestien väliaikainen tallentaminen

Luo Chat-servletille LinkedList-tyyppinen oliomuuttuja viestit, johon käyttäjän lähettämät viestit tallennetaan merkkijonona.

// täältä puuttuu pakettimäärittely ja importit

public class Chat extends HttpServlet {

    private Queue<String> viestit = new LinkedList();

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
        // .. jne

Muuta doPost-metodin toiminnallisuutta siten, että se pitää kirjaa kymmenestä viimeisimmästä viestistä viestistä viestit-listassa. Tulosta viestit näkyville.

Nyt jos käyttäjä lähettää kaksi viestiä, ensimmäinen "Moi!" ja toinen "Hei!", tulee lomakkeen lähetystä seuraavan sivun näyttää seuraavalta.

Moi!
Hei!

Huomaa ettet voi käyttää selaimen refresh-nappia sivun uudelleenlataamiseen -- se lähettää lomakkeen tiedot uudestaan eteenpäin. Kun haluat nähdä Chat-lomakkeen, tulee sinun mennä osoitteeseen selaimella uudestaan (painaa enteriä osoitepalkissa).

Viestit Chat-sivulle

Lisää viestien tulostus Chat-sivulle. Kun käyttäjä on lähettänyt kaksi viestiä, tulee Chat-sivun näyttää seuraavalta. Viestien lähetyksen jälkeen käyttäjä päätyy vielä edellisessä tehtävässä määritellylle erinäköiselle sivulle.

Chat

Viestit
Moi!
Hei!

Uudelleenohjaus

On hyvin ärsyttävää ettei selaimen refresh-nappia voi käyttää sivun uudelleenlataamiseen. Lisätään doPost-metodiin uudelleenohjaus, joka pyytää selainta siirtymään uuteen -- tapauksessamme vanhaan -- osoitteeseen. Selain tekee siis POST-kutsun jälkeen uuden GET-kutsun, ja näyttää Chat-lomakkeen.

HttpServletResponse-oliolla on metodit sendRedirect, jolla ohjauspyyntö tehdään, ja HttpServletRequest-oliolla on getRequestURI, jolla saadaan nykyinen osoite. Käytä näitä tietoja toteuttaaksesi doPost-metodin loppuun toiminnallisuus, jolla käyttäjä ohjataan chat-sivulle kun hän lähettää uuden viestin.

Kun ohjaus on toteutettu käyttäjä voi viestin kirjoittamisen jälkeen painaa selaimen refresh-nappia ja nähdä päivittyneet viestit chatista -- ilman että hänen aiemmin kirjoittamansa viesti lähetetään uudestaan.

Kirjautuminen

Chat-ohjelmassa ei tällä hetkellä ole mitään tapaa erottaa käyttäjiä toisistaan. Toteutetaan kirjautumistoiminnallisuus. HTTP/1.1 mahdollistaa evästeiden avulla tapahtuvan tilan ylläpitämisen. Web-palvelimet käyttävät evästeitä ns. sessioiden luomiseen ja ylläpitämiseen.

HttpServletRequest-oliolla on metodi getSession, jota kutsumalla saamme viitteen HttpSession-olioon. HttpSession-olioon tallennetut attribuutit, joita voi muokata metodeilla getAttribute ja setAttribute, ovat käytössä toisistaan erillisissä HTTP-pyynnöissä.

Tee erillinen Servlet-luokka nimeltä Login, joka näyttää käyttäjälle seuraavalaisen lomakkeen.

Käyttäjätunnus

Varmista että käyttäjätunnus-kentän nimi on tunnus.

Luo Login-servletille doPost-metodi, jossa tallennetaan lähetetty tunnus sessioon. Tallenna tunnus vain jos sen pituus on väliltä [4,8] (eli vähintään 4 merkkiä, korkeintaan 8 merkkiä). Voit käyttää seuraavaa koodipätkää pohjana.

        String tunnus = request.getParameter("tunnus");
        
        if(tunnus != null && tunnus.length() > 3 && tunnus.length() < 9) {
            HttpSession session = request.getSession();
            // lisää sessioon attribuutti tunnus

            // ohjaa osoitteeseen request.getContextPath() + "/Chat"
            //   missä "/Chat" on Chat-servletin kuuntelema polku

            // lähetä tiedot vastaukseen ja palaa metodikutsusta
            response.flushBuffer();
            return;
        }

Käyttäjätunnuksen lisääminen kirjoitettavaan viestiin

Muuta Chat-servlettiä siten, että sessiossa oleva käyttäjätunnus tallennetaan jokaisen viestin yhteyteen.

String tunnus = "" + request.getSession().getAttribute("tunnus");
String viesti = tunnus + ": " + request.getParameter("viesti");

Sovelluksen tulee nyt näyttää seuraavanlaiselta, esimerkissä nimimerkki "El Barto" on kirjoittanut viestin "Muerto o Vivo":

Chat
Viestit
El Barto: Muerto o Vivo

Kirjautumisen validointi

Lisää Chat-servletille metodi onKirjautunut, joka tarkistaa että käyttäjä on kirjautunut ja hänen tiedot ovat sessiossa.

private boolean onKirjautunut(HttpServletRequest request, HttpServletResponse response) 
            throws IOException {
    // jos sessiossa ei ole attribuuttia tunnus
    //   ohjaa käyttäjä osoitteeseen request.getContextPath() + "/Login"
    //   palauta tällöin arvo false
    // kaikki ok -- palauta true
}

Kutsu metodia Chat-servletissä sekä doGet- että doPost-metodin alussa. Jos metodi palauttaa arvon false, eli käyttäjä ei ole kirjautunut, älä jatka doGet tai doPost -metodin suoritusta. Yhdelle pyynnölle saa siis antaa vain yhden vastauksen.

Nyt käyttäjän tulee olla kirjautunut voidakseen chättäillä.

Uloskirjautuminen

Luo vielä erillinen Logout-servlet. Kun käyttäjä kutsuu tätä servlettiä kutsutaan HttpSession-olion invalidate-metodia ja kirjoitetaan käyttäjälle viesti "Kiitos!".

Lisää Chat-servletin tulostukseen linkki Logout-servlettiin:

            out.println("<a href=\"" + request.getContextPath() + "/Logout\">kirjaudu ulos</a>");

Sovelluksen tulee nyt näyttää seuraavanlaiselta, esimerkissä nimimerkki "El Barto" on kirjoittanut viestin "Muerto o Vivo":

Chat
Viestit
El Barto: Muerto o Vivo
kirjaudu ulos

Sovelluksen siirtäminen users-koneelle

Siirretään sovellus users-koneelle. Tämä osio on lyhennelmä osoitteessa http://www.cs.helsinki.fi/u/avihavai/edutainment/2011/tsoha/ohje/ olevan oppaan seitsemännestä osasta.

Kone users.cs.helsinki.fi on TKTL:n opiskelijoille tarkoitettu palvelin, jossa voi ajaa omia ohjelmistoja. Users-koneella on käytössä java web-sovelluksia pyörittävä tomcat-palvelin sekä useita tietokannanhallintajärjestelmiä: MySQL, Oracle ja PostgreSQL. Users-koneelle pääsee kirjautumaan komennolla.

ssh users.cs.helsinki.fi -l <omatunnus>

Saamme käyttöömme tomcat-palvelimen komennolla wanna-tomcat. Laitoksella on käytössä tomcatin versio 6.

tunnus@users:~$ wanna-tomcat

This script will create a new tomcat environment for you in directory
/home/tunnus/tomcat. Please see http://users.cs.helsinki.fi/tomcat for more
information. Do you want to create a new tomcat installation
in /home/tunnus/tomcat (y/n)? <syötä y>

....

Tomcat environment has been setup for you. Now you can run 'start-tomcat'.
tunnus@users:~$

Käynnistetään palvelin komennolla start-tomcat.

tunnus@users:~$ start-tomcat
Using CATALINA_BASE:   /home/tunnus/tomcat
Using CATALINA_HOME:   /usr/share/tomcat6
Using CATALINA_TMPDIR: /home/tunnus/tomcat/temp
Using JRE_HOME:        /usr/lib/jvm/java-6-sun
Using CLASSPATH:       /usr/share/tomcat6/bin/bootstrap.jar
Tomcat has been started. It should be visible through URL
http://t-tunnus.users.cs.helsinki.fi/

If you have problems, your tomcat log files are
available from /home/tunnus/tomcat/logs

Please, remember to stop (with stop-tomcat) tomcat instances with are
not used.
tunnus@users:~$ 

Kun menet web-selaimella osoitteeseen http://t-<tunnus>.users.cs.helsinki.fi/, näet sivun jossa on otsikkona viesti "It works!".

Tomcat-palvelimen saa suljettua komennolla stop-tomcat.

Siirretään seuraavaksi sovellus users-koneelle. Valitse NetBeansista chat-projektisi oikealla hiirennapilla, ja valitse Clean and Build. Tämä luo projektista war-tiedoston (web application archive) projektin dist-kansioon.

Kopioidaan paketti users-koneelle.

tunnus@kone:~$ scp ~/NetBeansProjects/<projektinnimi>/dist/<projektinnimi>.war users.cs.helsinki.fi:

Mennään koneelle users.cs.helsinki.fi, ja siirretään pakkaus tomcat-kansiossa olevaan webapps-kansioon.

tunnus@kone:~$ ssh users.cs.helsinki.fi
tunnus@users:~$ mv <projektinnimi>.war tomcat/webapps/
tunnus@users:~$

Jos tomcat on päällä, pyrkii se käynnistämään sovelluksen automaattisesti. Näet tomcatin logeista tomcat/logs/catalina.out ohjelman logiin kirjoittamat viestit. Jos tomcat ei ole päällä, käynnistä se, ja selaa osoitteeseen http://t-tunnus.users.cs.helsinki.fi/<projektinnimi>/Chat.

Ohjelman pitäisi uudelleenohjata sinut sivulle http://t-tunnus.users.cs.helsinki.fi/<projektinnimi>/Login ja pyytää sinua kirjautumaan.

Onneksi olkoon! Olet tehnyt pienen Chat-sovelluksen!

Web-sovelluksen rakenne

Kerrosarkkitehtuuri

Kerrosarkkitehtuuri jakaa web-sovelluksen kolmeen kerrokseen; tiedon tallentamiseen ja hakemiseen liittyvään logiikkaan, sovelluksen palveluihin liittyvään logiikkaan, ja käyttöliittymästä tehtyjä pyyntöjä ohjaavaan kerrokseen. Näiden päällä ja näistä erillisenä toimii käyttöliittymäkerros.

Tiedon hakemiseen ja tallentamiseen liittyvä logiikka ratkaistaan lähes aina valmiita komponentteja käyttäen. 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 tietovarastokerroksen tarjoamia palveluita.

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

Kerrosarkkitehtuurista erillisenä -- 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.

Käyttöliittymä

Dynaamiset web-sivut tarvitsevat hieman logiikkaa sivujen näyttämiseen. Esimerkiksi listamuotoisen datan tulostaminen HTML-sivuksi -- kun listan koko vaihtelee pyynnöstä toiseen -- vaatii toistorakenteen. Perinteisissä web-sivuissa käyttöliittymä koostuu näkymää muokkaavasta koodista (esim. CSS) ja datan sisältävästä koodista (esim. HTML), jotka eivät itsessään sisällä logiikkaa. Yksi nykyaikaisten (web-)sovellusten nyrkkisääntö on pidä käyttöliittymälogiikka ja sovelluslogiikka erillään.

Käyttöliittymien luomiseen web-sovelluksia varten on luotu useita erilaisia lähestymistapoja. Loppupeleissä kaikki näytetään käyttäjälle HTML:nä -- riippumatta siitä miten sovelluskehittäjä luo käyttöliittymän. Tutustutaan seuraavaksi JSP-sivujen luomiseen (Java Server Pages).

JSP

JSP-sivut ovat käytännössä HTML-sivuja, joissa on mahdollista suorittaa myös Java-koodia (Java-koodin sisällyttäminen JSP-sivuille on huonoa ohjelmointityyliä -- eli emme käytä tätä mahdollisuutta). JSP-sivutiedostot loppuvat yleensä päätteeseen jsp.

JSP-sivuja säilytetään erillään Java-koodista. NetBeansin hakemistorakenteessa JSP-sivut ovat web-kansion alla. Projektinäkymässä ne näkyvät kansiossa Web Pages.

<%@page contentType="text/html" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <title>JSP Page</title>
    </head>
    <body>
        <h1>Hello World!</h1>
    </body>
</html>

JSP-sivut muutetaan niitä kutsuttaessa servleteiksi, joiden tuottama sisältö näytetään käyttäjälle. Servlettien ja JSP-sivujen yhteistyö tapahtuu perinteisessä Servlet-maailmassa HttpServletRequest-luokalta saatavan RequestDispatcher-olion avulla. RequestDispatcher-oliota käytetään pyynnön eteenpäin ohjaamiseen.

Seuraava esimerkki ohjaa pyynnön index.jsp-nimiselle JSP-sivulle, joka löytyy web-kansiosta.

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        response.setContentType("text/html;charset=UTF-8");
        
        RequestDispatcher dispatcher = request.getRequestDispatcher("index.jsp");
        dispatcher.forward(request, response);
    }

JSP-sivujen ollessa web-kansiossa käyttäjä pääsee niihin käsiksi suoraan selaamalla osoitteeseen http://osoite/sovellus/sivu.jsp. Jos haluamme pitää JSP-sivut käyttäjältä näkymättömissä, tulee niiden olla web-kansiossa sijaitsevassa WEB-INF -kansiossa (tai sen sisältämässä kansiossa). RequestDispatcher-olion osoittaminen WEB-INF-kansiossa sijaitsevaan JSP-sivuun tapahtuu helposti.

        RequestDispatcher dispatcher = request.getRequestDispatcher("WEB-INF/index.jsp");

Datan näyttäminen sivulla

JSP mahdollistaa pyyntöön lisättyjen attribuuttien näyttämisen sivulla EL-kieltä (Expression Language) käyttäen. EL-kutsut alkavat dollarimerkillä ja avaavalla aaltosulkeella (${), ja päättyvät sulkevaan aaltosulkuun (}). Viittaus attribuuttiin nimeltä porkkana tapahtuisi komennolla ${porkkana}. Attribuutteja voi lisätä pyyntöön HttpServletRequest-olion setAttribute-metodilla. Esimerkiksi viesti-nimisen attribuutin lisääminen pyyntöön tapahtuu seuraavasti.

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        response.setContentType("text/html;charset=UTF-8");

        request.setAttribute("viesti", "kolaa kuluu!");
        
        RequestDispatcher dispatcher = request.getRequestDispatcher("index.jsp");
        dispatcher.forward(request, response);
    }

EL-kutsu, jolla voisimme tulostaa attribuutin viesti sisällön on seuraavanlainen.

  <p>html:ää ja muuta kivaa -- sekä ${viesti}.</p>

Ylläolevan -- standardeja heikosti noudattavan -- JSP-sivun tulostama sisältö olisi kutsun jälkeen seuraavanlainen (olettaen että pyyntö JSP-sivulle tulee edeltävästä doGet-metodista).

  <p>html:ää ja muuta kivaa -- sekä kolaa kuluu!.</p>

EL mahdollistaa myös olioiden get-tyyppisiin metodeihin tehtävät metodikutsut. Tekemällä kutsun ${alkio.tyyppi} kutsuisimme alkio-nimisen attribuutin viittaamaan olioon liittyvää getTyyppi-metodia. Katsotaan seuraavaa Kasvis-luokkaa.

public class Kasvis {
    private String tyyppi;

    public Kasvis(String tyyppi) {
        this.tyyppi = tyyppi;
    }

    public String getTyyppi() {
        return tyyppi;
    }
}
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        response.setContentType("text/html;charset=UTF-8");

        Kasvis porkkana = new Kasvis("porkkana");

        request.setAttribute("kasvis", porkkana);
        
        RequestDispatcher dispatcher = request.getRequestDispatcher("index.jsp");
        dispatcher.forward(request, response);
    }

Attribuuttina kasvis olevaa Kasvis-luokan ilmentymää käyttäen voisimme tehdä JSP-koodissa kutsun ${kasvis.tyyppi}, joka vuorostaan kutsuisi metodia getTyyppi kasvis-attribuutin viittaamalle oliolle -- riippumatta olion tyypistä.

  <p>Ja tänään jälkiruokakasviksena on ${kasvis.tyyppi}.</p>
  <p>Ja tänään jälkiruokakasviksena on porkkana.</p>

Tägikirjastot

EL ei tarjoa toistolausekkeita. JSP:tä varten on kehitetty joukko erillisiä kirjastorutiineja dynaamisten toiminnallisuuksien toteuttamiseen. JSTL (JavaServer Pages Standard Tag Library) on tägikirjasto, jota voi käyttää esimerkiksi toistolauseiden sisällyttämiseen JSP-sivuille. JSTL:n ydintoiminnallisuudet saa JSP-sivun käyttöön lisäämällä JSP-sivun alkuun seuraavan rivin.

<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>

Oleellisin toiminto meille on forEach-lause, jota käytetään kokoelmaolioiden (Collection-rajapinnan toteuttavien olioiden) kanssa seuraavasti.

<pre>
  <c:forEach var="alkio" items="${joukko}">
    ${alkio}
  </c:forEach>
</pre>

Yllä viittaamme attribuuttiin nimeltä joukko. Oletetaan että joukko lisätty pyynnön attribuutiksi seuraavasti.

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        response.setContentType("text/html;charset=UTF-8");

        List<String> lista = Arrays.asList("joukossa", "tyhmyys", "tiivistyy");
        request.setAttribute("joukko", lista);
        
        RequestDispatcher dispatcher = request.getRequestDispatcher("index.jsp");
        dispatcher.forward(request, response);
    }

Toistolausekkeen tulostus olisi seuraava.

<pre>
    joukossa
        
    tyhmyys
        
    tiivistyy

</pre>

Lisää JSTL-kirjastosta löytyy mm. osoitteesta http://www.jsptutorial.net/jsp-standard-tag-library-jstl.aspx.

Huom! Jos -- ja kun -- törmäät virheeseen org.apache.jasper.JasperException: The absolute uri: http://java.sun.com/jsp/jstl/core cannot be resolved in either web.xml or the jar files deployed with this application, lisää JSTL 1.1-kirjasto projektiin valitsemalla "Project Properties" -> Libraries -> Add Library, joka avaa listan josta kirjasto JSTL 1.1 löytyy.

Olemassaolevien sovellusten nykyaikaistaminen

Huomattava osa olemassaolevista web-sovelluksista on luotu jo pidemmän aikaa sitten. Näiden ohjelmistojen kohdalla on kolme vaihtoehtoa.

  1. päivitetään olemassaolevaa versiota tarpeen vaatiessa
  2. luodaan uusi ohjelmisto
  3. lähdetään hiljalleen nykyaikaistamaan ohjelmistoa
Vaihtoehdon valinta riippuu käytettävissä olevasta ajasta (ja rahasta).

Olemassaolevan version päivitys tarpeen vaatiessa on hyvä vaihtoehto kun sovelluksella on jo paljon käyttäjiä ja uudelleenkirjoitus veisi enemmän aikaa (ja rahaa) kuin on käytettävissä.

Uuden ohjelmiston luonnissa tulee varmistaa että mukana on myös vanhan ohjelmiston luonnissa olleita ihmisiä -- jokaisen ohjelmiston kehityksessä on tullut vastaan tapahtumia jotka ovat ohjanneet kehitysprosessia.

Kolmas vaihtoehto on ohjelmiston muokkaaminen nykyaikaisemmaksi. Wanhat sovellukset ovat usein isoja kokonaisuuksia. Eräs muutosprosessi on seuraavanlainen.

  1. erota sovelluslogiikka käyttöliittymäkoodista
  2. erota yksittäiset sivut omiksi komponenteikseen
  3. siirrä käyttöliittymäkoodi erillisiin sivuihin, jotka käyttävät jotain templatejärjestelmää

Seuraava esimerkki selventää ylläolevaa muutosprosessia.

Esimerkki: Kassakirjanpidon siistiminen

Esimerkissä muokataan kassakirjanpitoon suunnattua Servlet-ohjelmaa, johon käyttäjä voi syöttää kassatapahtumia.

// importit

public class Kirjanpito extends HttpServlet {

    private String tiedostonNimi = "data.csv";
    private String erotin = ";";

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        
        
        if (request.getParameter("tapahtuma") != null
                && request.getParameter("kustannus") != null) {
            FileWriter fw = new FileWriter(tiedostonNimi, true);
            fw.write(new SimpleDateFormat("dd.MM.yyyy").format(new Date()) + erotin);
            fw.write(request.getParameter("tapahtuma") + erotin);
            fw.write(request.getParameter("kustannus") + "\n");
            
            fw.flush();
            fw.close();
        }

        response.setContentType("text/html;charset=UTF-8");
        PrintWriter out = response.getWriter();
        try {

            out.println("<html>");
            out.println("<head>");
            out.println("<title>Kirjanpito</title>");
            out.println("</head>");
            out.println("<body>");

            out.println("<strong>Kirjanpito</strong><br />");
            out.println("<form>");
            out.println("Tapahtuma: <input type=\"text\" name=\"tapahtuma\" /><br />");
            out.println("Kustannus: <input type=\"text\" name=\"kustannus\" /><br />");
            out.println("<input type=\"submit\" value=\"Lisää\" />");
            out.println("</form>");

            out.println("<table>");
            out.println("<tr><th>PVM</th><th>Tapahtuma</th><th>Kustannus</th></tr>");
            
            Scanner lukija = new Scanner(new FileReader(tiedostonNimi));
            while(lukija.hasNextLine()) {
                String rivi = lukija.nextLine();
                String[] data = rivi.split(erotin);
                out.println("<tr><td>" + data[0] + 
                        "</td><td>" + data[1] + 
                        "</td><td>" + data[2] + "</td><td></tr>");
            }

            out.println("</table>");

            out.println("</body>");
            out.println("</html>");
        } finally {
            out.close();
        }
    }
}

Ohjelmaa muokatessa ylläpitoystävällisemmäksi ensimmäinen askel on käyttöliittymälogiikan ja sovelluslogiikan erottaminen toisistaan. Luodaan luokka Tapahtumanhallinta, jonka tehtävänä on hoitaa tapahtumien lukeminen ja tallentaminen. Kopioimme toiminnallisuuden suoraan ylläolevasta luokasta. Toteutuksessamme kaikki toiminnallisuus on luokkakohtaista (eli static). Näemme myöhemmin kurssilla huomattavasti järkevämpiä ratkaisuja.

public class Tapahtumanhallinta {

    private static String tiedostonNimi = "data.csv";
    private static String erotin = ";";

    public static void lisaaTapahtuma(String tapahtuma, String kustannus)
            throws IOException {
        FileWriter fw = new FileWriter(tiedostonNimi, true);
        fw.write(new SimpleDateFormat("dd.MM.yyyy").format(new Date()) + erotin);
        fw.write(tapahtuma + erotin);
        fw.write(kustannus + "\n");

        fw.flush();
        fw.close();
    }

    public static List<String> lueTiedostonRivit() throws FileNotFoundException {
        List<String> rivit = new ArrayList();
        Scanner lukija = new Scanner(new FileReader(tiedostonNimi));
        while (lukija.hasNextLine()) {
            rivit.add(lukija.nextLine());
        }
        
        return rivit;
    }
}

Muokataan Kirjanpito-servletin doGet-metodia siten, että se käyttää Tapahtumanhalllinta-luokan tarjoamia palveluita.


    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {

        if (request.getParameter("tapahtuma") != null
                && request.getParameter("kustannus") != null) {
            Tapahtumanhallinta.lisaaTapahtuma(request.getParameter("tapahtuma"),
                                              request.getParameter("kustannus"));
        }
        

        response.setContentType("text/html;charset=UTF-8");
        PrintWriter out = response.getWriter();
        try {

            out.println("<html>");
            out.println("<head>");
            out.println("<title>Kirjanpito</title>");
            out.println("</head>");
            out.println("<body>");

            out.println("<strong>Kirjanpito</strong><br />");
            out.println("<form method=\"POST\">");
            out.println("Tapahtuma: <input type=\"text\" name=\"tapahtuma\" /><br />");
            out.println("Kustannus: <input type=\"text\" name=\"kustannus\" /><br />");
            out.println("<input type=\"submit\" value=\"Lisää\" />");
            out.println("</form>");

            out.println("<table>");
            out.println("<tr><th>PVM</th><th>Tapahtuma</th><th>Kustannus</th></tr>");

            for(String rivi: Tapahtumanhallinta.lueTiedostonRivit() {
                String[] tapahtuma = rivi.split(";");
                out.println("<tr><td>" + tapahtuma[0]
                        + "</td><td>" + tapahtuma[1]
                        + "</td><td>" + tapahtuma[2] + "</td><td></tr>");
            }

            out.println("</table>");

            out.println("</body>");
            out.println("</html>");
        } finally {
            out.close();
        }
    }

GET-metodikutsujen tulee olla turvallisia, eli niiden ei tule tarjota mahdollisuutta sivujen sisällön muokkaamiseen. Muokataan Kirjanpito-luokkaa siten, että lomakkeen tyyppi on POST. Lisäämme samalla doPost-metodin, joka käsittelee POST-tyyppiset kutsut ja ohjaa käyttäjän lopuksi takaisin normaalille sivulle.

Servlettimme Kirjanpito näyttää nyt kokonaisuudessaan seuraavalta.

// importit ym

public class Kirjanpito extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {

        response.setContentType("text/html;charset=UTF-8");
        PrintWriter out = response.getWriter();
        try {

            out.println("<html>");
            out.println("<head>");
            out.println("<title>Kirjanpito</title>");
            out.println("</head>");
            out.println("<body>");

            out.println("<strong>Kirjanpito</strong><br />");
            out.println("<form method=\"POST\">");
            out.println("Tapahtuma: <input type=\"text\" name=\"tapahtuma\" /><br />");
            out.println("Kustannus: <input type=\"text\" name=\"kustannus\" /><br />");
            out.println("<input type=\"submit\" value=\"Lisää\" />");
            out.println("</form>");

            out.println("<table>");
            out.println("<tr><th>PVM</th><th>Tapahtuma</th><th>Kustannus</th></tr>");

            for (String[] tapahtuma : Tapahtumanhallinta.annaTapahtumat()) {
                out.println("<tr><td>" + tapahtuma[0]
                        + "</td><td>" + tapahtuma[1]
                        + "</td><td>" + tapahtuma[2] + "</td><td></tr>");
            }

            out.println("</table>");

            out.println("</body>");
            out.println("</html>");
        } finally {
            out.close();
        }
    }

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) 
            throws ServletException, IOException {
        
        if (request.getParameter("tapahtuma") != null
                && request.getParameter("kustannus") != null) {
            Tapahtumanhallinta.lisaaTapahtuma(
                    request.getParameter("tapahtuma"),
                    request.getParameter("kustannus"));
        }
        
        // POST-kutsun jälkeen takaisin listaukseen
        response.setStatus(302);
        // Kirjanpito-servlet kuuntelee osoitetta /Kirjanpito
        response.setHeader("Location", request.getContextPath() + "/Kirjanpito");
        response.flushBuffer();
    }
}

Tiedämme ohjelmistojen mallintamisesta ja ohjelmointikursseilta että luokalla tulee olla selkeä vastuu. Tapahtuman kuvaaminen merkkijonona ole järkevää. Luodaan luokka Kirjanpitotapahtuma, joka kuvaa yksittäistä kirjanpitotapahtumaa. Kirjanpitotapahtumaan liittyy päivämäärä, tapahtuman kuvaus ja kustannus. Esimerkissä kustannus on merkkijonona, mutta oikeasti sen voisi ilmaista kokonaislukuna (sentteinä) -- jolloin tarvitsisimme myös erillisen kentän valuutalle.

import java.util.Date;

public class Kirjanpitotapahtuma {
    private Date paivamaara;
    private String kuvaus;
    private String kustannus;
   
    public Kirjanpitotapahtuma(String kuvaus, String kustannus) {
        this(new Date(), kuvaus, kustannus);
    }

    public Kirjanpitotapahtuma(Date paivamaara, String kuvaus, String kustannus) {
        this.paivamaara = paivamaara;
        this.kuvaus = kuvaus;
        this.kustannus = kustannus;
    }

    public Date getPaivamaara() {
        return paivamaara;
    }
    
    public String getKuvaus() {
        return kuvaus;
    }

    public String getKustannus() {
        return kustannus;
    }
}

Kun luot luokkaa Kirjanpitotapahtuma muista että kaikki nykyaikaiset editorit osaavat täydentää lähdekoodiin konstruktorit, getterit ja setterit olemassaolevien attribuuttien perusteella. Sinun tarvitsee kirjoittaa vain attribuutit ja valita editorin täydennystoiminnallisuus. NetBeansista se löytyy valitsemalla "Source" ja "Insert Code".

Luokan Kirjanpitotapahtuma luomisen jälkeen luokkaa Tapahtumanhallinta muutetaan siten, että se käsittelee kirjanpitotapahtumaolioita. Jatkossa päivämäärät tallennetaan millisekuntiesityksenä. Jätämme väliin aiemmin tallennetun datan muuntamisen nykyaikaiseen muotoon, eli konversion, tämä tulisi toki myös tehdä.

// importit jne

public class Tapahtumanhallinta {

    private static String tiedostonNimi = "data.csv";
    private static String erotin = ";";

    public static void lisaaTapahtuma(Kirjanpitotapahtuma tapahtuma)
            throws IOException {
        FileWriter fw = new FileWriter(tiedostonNimi, true);
        
        Date pvm = tapahtuma.getPaivamaara();
        fw.write(pvm.getTime() + erotin);
        fw.write(tapahtuma.getKuvaus() + erotin);
        fw.write(tapahtuma.getKustannus() + "\n");
        
        fw.flush();
        fw.close();
    }

    public static List<Kirjanpitotapahtuma> annaTapahtumat() throws FileNotFoundException {
        List<Kirjanpitotapahtuma> tapahtumat = new ArrayList();
        Scanner lukija = new Scanner(new FileReader(tiedostonNimi));
        while (lukija.hasNextLine()) {
            String rivi = lukija.nextLine();
            String[] data = rivi.split(erotin);
            
            try {
                Date pvm = new Date(Long.parseLong(data[0]));
                tapahtumat.add(new Kirjanpitotapahtuma(pvm, data[1], data[2]));
            } catch (ArrayIndexOutOfBoundsException e) {
                System.err.println("Epäkelpo rivi: " + rivi);
            } catch (NumberFormatException e) {
                System.err.println("Epäkelpo rivi: " + rivi);
            }
        }

        return tapahtumat;
    }
}

Kirjanpito-servletin doGet-metodia muutetaan ottamaan tapahtumat huomioon. Käytämme Javan SimpleDateFormat-luokkaa Date-olioiden merkkijonoesitykseksi muutamiseen.

            // ...
            out.println("<table>");
            out.println("<tr><th>PVM</th><th>Tapahtuma</th><th>Kustannus</th></tr>");
            SimpleDateFormat muuntaja = new SimpleDateFormat("dd.MM.yyyy");

            for (Kirjanpitotapahtuma tapahtuma : Tapahtumanhallinta.annaTapahtumat()) {
                out.println("<tr><td>" + muuntaja.format(tapahtuma.getPaivamaara())
                        + "</td><td>" + tapahtuma.getKuvaus()
                        + "</td><td>" + tapahtuma.getKustannus() + "</td><td></tr>");
            }
            // ...

Sovelluslogiikka ja käyttöliittymälogiikka on erotettu toisistaan. Seuraavana tehtävänä on yksittäisten sivujen erottaminen omiksi komponenteikseen. Tällä tarkoitetaan esimerkiksi montaa eri sivua tulostavan Servletin pilkkomista useampaan Servlet-luokkaan. Kirjanpitojärjestelmässä on vain yksi sivu, joten meidän ei tarvitse tehdä pitään.

Kolmantena askeleena on käyttöliittymäkoodin siirtäminen erillisiin sivuihin, jotka käyttävät templatejärjestelmää. Siirrämme jokaisen Servlet-luokan sisältämän käyttöliittymäkoodin (HTML:n) erilliseen JSP-sivuun. Luodaan Kirjanpito-servletin käyttöliittymäkoodin sisältävä sivu, kirjanpito.jsp. Aluksi kopioimme sivulle sovelluksemme HTML-rungon ja lisäämme JSP-otsakkeen joka kertoo sivun käyttämästä merkistöstä.

<%@page contentType="text/html" pageEncoding="UTF-8"%>
<html>
  <head>
    <title>Kirjanpito</title>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
  </head>
  <body>
    <strong>Kirjanpito</strong><br />

    <form method="POST">
      Tapahtuma: <input type="text" name="tapahtuma" /><br />
      Kustannus: <input type="text" name="kustannus" /><br />
      <input type="submit" value="Lisää" />
    </form>

    <table>
      <tr><th>PVM</th><th>Tapahtuma</th><th>Kustannus</th></tr>
    </table>

  </body>
</html>

JSP-sivu asetetaan NetBeansissa projektikansioon "Web Pages" tai fyysiseen kansioon "web".

Voimme ohjata Servletille saapuneen pyynnön toiselle Servletille (tai JSP-sivulle) käyttämällä Servlet-API:n RequestDispatcher-oliota. RequestDispatcher-olioon pääsee käsiksi HttpServletRequest-olion getRequestDispatcher-metodin avulla. Muutamme Kirjanpito-servletin doGet-metodin seuraavanlaiseksi.

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        response.setContentType("text/html;charset=UTF-8");
        RequestDispatcher dispatcher = request.getRequestDispatcher("kirjanpito.jsp");
        dispatcher.forward(request, response);
    }

Yllä käyttöliittymään liittyvä koodi on siirretty pois Kirjanpito-servletistä. Kutsussa request.getRequestDispatcher("kirjanpito.jsp") annettu tiedosto kirjanpito.jsp löytyy ohjelmamme web-kansiosta. Kun avaamme sovelluksen selaimessa, näemme JSP-sivun tuottaman sivun.

Sivulta puuttuu tapahtumien listaus. Lisäämällä Tapahtumanhallinta-luokan tarjoamat kirjanpitotapahtumat attribuutiksi pyyntöön, pääsee niihin käsiksi myös muilla samaa pyyntöä käsittelevillä JSP-sivuilla ja Servleteillä. Pyyntöön voi lisätä attribuutteja HttpServletRequest-luokan setAttribute-metodin avulla. Muutamme Kirjanpito-servletin doGet-metodia seuraavanlaiseksi.

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        response.setContentType("text/html;charset=UTF-8");
        
        request.setAttribute("tapahtumat", Tapahtumanhallinta.annaTapahtumat());
        
        RequestDispatcher dispatcher = request.getRequestDispatcher("kirjanpito.jsp");
        dispatcher.forward(request, response);
    }

Pyyntöön lisätään tapahtumat-niminen attribuutti, joka viittaa kokoelmaan Kirjanpitotapahtuma-tyyppisiä olioita.

JSP:hen liittyy joukko apukirjastoja, joilla voi toteuttaa yksinkertaisia tulostustoiminnallisuuksia JSP-sivuille. JSTL (JavaServer Pages Standard Tag Library) on näistä yleisin. JSTL tarjoaa muunmuassa toiminnon forEach, jonka avulla voidaan iteroida läpi kokoelman sisältämät elementit.

Pyynnön attribuuttina olevan kokoelman "tapahtumat" läpikäynti käy seuraavasti.

<c:forEach var="tapahtuma" items="${tapahtumat}">
    ${tapahtuma.kuvaus}
</c:forEach>

Ylläolevalla esimerkillä tulostaisimme jokaiseen tapahtumat-kokoelmasta saatavaan olioon liittyvän metodin getKuvaus palauttaman arvon. Kutsu ${tapahtuma.kuvaus} kutsuu siis käsiteltävän olion metodia getKuvaus. Jos vastaavaa metodia ei ole olemassa, näemme poikkeuksen.

Lisätään tapahtumaan liittyvien tietojen tulostus JSP-sivulle, huomaa myös lisätty taglib-otsake.

<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@page contentType="text/html" pageEncoding="UTF-8"%>
<html>
  <head>
    <title>Kirjanpito</title>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
  </head>
  <body>
    <strong>Kirjanpito</strong><br />

    <form method="POST">
      Tapahtuma: <input type="text" name="tapahtuma" /><br />
      Kustannus: <input type="text" name="kustannus" /><br />
      <input type="submit" value="Lisää" />
    </form>

    <table>
      <tr><th>PVM</th><th>Tapahtuma</th><th>Kustannus</th></tr>
      <c:forEach var="tapahtuma" items="${tapahtumat}">
          <tr><td>${tapahtuma.paivamaara}</td><td>${tapahtuma.kuvaus}</td><td>${tapahtuma.kustannus}</td></tr>
      </c:forEach>
    </table>

  </body>
</html>

Ohjelmamme toimii lähes oikein, mutta päivämäärien tulostus ei ole halutunlainen. Päivämäärät tulostetaan Date-luokan oletustulostusmuodossa. Onneksi JSTL:ssä on olemassa format-kirjasto joka tarjoaa juuri tätä tapausta varten tarkoitetun formatDate-toiminnon.

<%@taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
// ...
          <fmt:formatDate pattern="dd.MM.yyyy" value="${tapahtuma.paivamaara}" />
// ...

JSP-sivumme näyttää kokonaisuudessaan nyt seuraavalta.

<%@taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@page contentType="text/html" pageEncoding="UTF-8"%>
<html>
  <head>
    <title>Kirjanpito</title>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
  </head>
  <body>
    <strong>Kirjanpito</strong><br />

    <form method="POST">
      Tapahtuma: <input type="text" name="tapahtuma" /><br />
      Kustannus: <input type="text" name="kustannus" /><br />
      <input type="submit" value="Lisää" />
    </form>

    <table>
      <tr><th>PVM</th><th>Tapahtuma</th><th>Kustannus</th></tr>
      <c:forEach var="tapahtuma" items="${tapahtumat}">
          <tr>
            <td><fmt:formatDate pattern="dd.MM.yyyy" value="${tapahtuma.paivamaara}" /></td>
            <td>${tapahtuma.kuvaus}</td>
            <td>${tapahtuma.kustannus}</td></tr>
      </c:forEach>
    </table>

  </body>
</html>

Ja sovelluksemme on pilkottu selkeämpiin osakokonaisuuksiin. Ole!

Huom! Vaikka aluksi voi äyttää siltä että muutimme yksinkertaisen ohjelman paljon monimutkaisemmaksi on muutoksista huomattavasti hyötyä. Tilanteessa jossa sovelluksen tuottamaan sivuun haluttaisiin uusi ulkoasu, erilainen tallennuslogiikka, ja uusia näytettäviä tietoja -- esimerkiksi tehtyjen pyyntöjen määrä -- jokaista näistä tehtävistä voisi työstää rinnakkain.

MVC-näkökulmasta JSP:n ja Servletin perinteinen yhteistyö on seuraava.

  1. Näkymä -> Ohjaaja: Käyttäjä tekee pyynnön osoitteeseen, jossa toimii Servlet-olio Servlet-olio.
  2. Ohjaaja -> Malli: Ohjaaja tekee pyyntöjä palveluita tarjoaville luokille, luoden ja hakien dataa. Data asetetaan pyynnön attribuutiksi.
  3. Ohjaaja -> Näkymä: Servletissä valitaan käyttöliittymän sisältävä JSP-sivu. Pyyntö ohjataan näkymää luovaan palveluun.
  4. Näkymä -> Malli: JSP-sivu renderöidään käyttäjää varten pyynnön attribuuttina olevaa dataa käyttäen. Sivu lähetetään vastauksena käyttäjälle.
  5. Näkymä -> Ohjaaja: ...

Aikaisemmat syntimme, osa 1

Olemme aiemmin tehneet vääryyksiä. Onneksi ehdimme vielä korjaamaan ne.

Ensimmäinen web-sovellus

Muunna aiemmin luomasi web-sovellus eka-servlet sellaiseksi, että lähdekoodissa ei tulosteta ollenkaan HTML-koodia. Siirrä siis kaikki HTML-koodi uudelle jsp-sivulle -- älä käytä sivua index.jsp.

Uudelleenohjaus sivulta index.jsp

Muuta sivun index.jsp sisältö seuraavanlaiseksi.

<meta http-equiv="refresh" content="0;url=${pageContext.request.requestURI}AwesomeServlet">

Nyt kaikki web-sovelluksen juuripolkuun tulevat pyynnöt uudelleenohjataan /AwesomeServlet -osoitteeseen.

Kävijälaskuri

Lisää sovellukseen kävijälaskuri. Sovelluksen ei tarvitse tallentaa kävijöiden määrää erilliseen tietovarastoon -- kävijöiden määrän pitäminen muistissa on tarpeeksi.

<html>
<head>
<title>Servlet AwesomeServlet</title>
</head>
<body>
<h1>Awesomer! x ${kavijoita}</h1>
</body>
</html>

Sovelluksen siirto Users-koneelle

Siirrä sovellus eka-servlet Users-koneelle.

Aikaisemmat syntimme, osa 2

Refaktoroi aiemmin toteuttamasi Chat-sovellus siten, että käyttöliittymäkoodi (HTML) on jsp-tiedostoissa, viestit omassa luokassa, ja ohjauslogiikka Servlet-luokissa. Kun olet valmis, siirrä uusi versiosi Users-koneelle.

Testaus, viikko 1

Tarkista luomasi ohjelmistot osoitteessa http://t-avihavai.users.cs.helsinki.fi/tester olevassa palvelussa. Jos tiedät että ohjelmistossasi on jotain vialla, ja web-palvelu ei tarkasta siihen liittyvää vikaa, korjaa se silti.

  1. Awesome-palvelun testaaja haluaa että sivullasi on laskuri, joka kasvaa pyyntöjen välillä
  2. Chat-palvelun testaaja haluaa että:
    1. Palvelulle annetaan Chat-palvelun osoite.
    2. Käyttäjä ohjataan kirjautumissivulle aloitussivulta kun hän ei ole kirjautunut.
    3. Kirjautumissivulla on kenttä nimeltä tunnus. Käyttäjä ohjataan chat-sivulle kun tunnus-kenttään kirjoittaa tunnuksen ja lähettää lomakkeen.
    4. Chat-sivulla on kenttä viesti. Kun viestikenttään kirjoittaa viestin, ja lähettää lomakkeen, viesti näkyy käyttäjälle.
    5. Chat-sivulla on linkki, joka sisältää tekstin kirjaudu. Linkin klikkaaminen siirtää käyttäjän pois chat-sivulta.
    6. Kun käyttäjä on kirjautunut ulos, eli klikannut tekstin kirjaudu sisältävää linkkiä, käyttäjä ei pääse chat-sivulle ilman uutta kirjautumista.

Käytetyt tunnit, viikko 1

Kirjaa osoitteessa http://t-avihavai.users.cs.helsinki.fi/tunnit olevaan palveluun ensimmäisen viikon materiaalin ja tehtävien parissa käyttämäsi aika.

Ohjelmistoprojektin hallinta

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, lähdekoodista tulee pystyä generoimaan erilaisia raportteja ja 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 lisäkirjastojen hallinnassa. Työkalut ovat oleellinen osa ohjelmistoryhmän yhteisten pelisääntöjä, ja ne muunmuassa tukevat jatkuvaa integrointia.

Maven

Apache Maven on projektinhallintatyökalu, jota voi käyttää käynnösten automatisoinnin lisäksi lähes koko projektin elinkaaren hallintaan uuden projektin aloittamisesta lähtien. Maveniin voi konfiguroida tavoitteita (goals), joita suoritetaan tarpeen vaatiessa. Tyypillinen tavoite on build, joka paketoi lähdekoodin projektityypistä riippuen sopivaan pakettiin. Oikeastaan maven on sovelluskehys plugin-komponenttien suoritukseen, yksinkertaisimmatkin tehtävät on tehty pluginien avulla.

Mavenin pluginit jakautuvat karkeasti kahteen osa-alueeseen, käännösprosessiin ja raportointiin. Käännösprosessiin liittyvät pluginit suoritetaan käännösvaiheessa, ja näitä on muunmuassa testaaminen, paketointi, ym. Raportointiin liittyvät pluginit taas hoitavat tehtäviä JavaDoc-dokumentaation luomisesta staattisten lähdekoodianalyysityökalujen suorittamiseen ja niiden luoman raportoinnin hallinnointiin. Mavenin plugineista löytyy (ei kattava) lista osoitteessa http://maven.apache.org/plugins/index.html.

Maven automatisoi uusien projektien luomisen archetype-mekanisminsa 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.

Tutustutaan seuraavaksi yksinkertaisen (ei-web) -sovelluksen luomiseen mavenia käyttäen.

Maveniin tutustuminen

Mavenin asennus

Jos käytössäsi ei ole mavenia, asenna se osoitteesta http://maven.apache.org/download.html. Tietojenkäsittelytieteen laitoksella käytössämme on versio 2.2.1, jatkossa oletamme että käytössäsi on se tai uudempi.

Uuden projektin luominen sovellusrungosta

Kutsu komentoriviltä seuraavaa komentoa. Parametrilla -DgroupId kerrotaan katto-organisaation tai ryhmän tunnus, parametrilla -DartifactId kerrotaan luotavan sovelluksen nimi.

mvn archetype:generate -DgroupId=fi.organisaatio -DartifactId=sovelluksen-nimi

Voit vastailla mavenin kysymyksiin kysymyksiin enter-painalluksilla, jolloin maven käyttää oletusvastauksia. Maven luo projektillesi seuraavanlaisen kansiorakenteen.

sovelluksen-nimi/
├── pom.xml
└── src
    ├── main
    │   └── java
    │       └── fi
    │           └── organisaatio
    │               └── App.java
    └── test
        └── java
            └── fi
                └── organisaatio
                    └── AppTest.java


Sovelluksen ja testien lähdekoodit ovat eritelty erillisiin kansioihin. Lähdekoodien runkokansio on src, jonka alla on kansiot main ja test. Projektiin liittyvä pom.xml -tiedosto (Project Object Model) näyttää seuraavalta.

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>fi.organisaatio</groupId>
  <artifactId>sovelluksen-nimi</artifactId>
  <version>1.0-SNAPSHOT</version>
  <packaging>jar</packaging>

  <name>sovelluksen-nimi</name>
  <url>http://maven.apache.org</url>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>

  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>3.8.1</version>
      <scope>test</scope>
    </dependency>
  </dependencies>
</project>

Alkuosio määrittelee projektin tiedot, keskellä määritellään että projekti käyttää UTF-8 -merkistökoodausta, ja alaosassa määritellään kirjastot, joista projekti riippuu. Projektille on määritelty riippuvuus JUnit-sovelluskirjastoon. Käytännössä tämä tarkoittaa sitä, että maven noutaa JUnit-kirjaston sovellustamme varten.

Testien suorittaminen

Mavenin testit suoritetaan maveniin sisäänrakennetulla surefire-pluginilla. Suorita projektiin liittyvät testit kirjoittamalla mvn test pom.xml -tiedoston sisältävässä kansiossa.

mvn test

Näet viestin, jonka loppupuolella on merkkijono BUILD SUCCESSFUL

...
[INFO] BUILD SUCCESSFUL
...

Riippuvuudet ja JUnit 4

Mavenin pom.xml -sisältää tietoa projektiin liittyvästä konfiguraatiosta. Riippuvuudet ilmaistaan elementin <dependencies>-alla. Mavenin oletusprojektin pom.xml -tiedostossa on oletuksena JUnit-versio 3.papu. Muuta versioksi 4.10.

  ...
  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.10</version>
      <scope>test</scope>
    </dependency>
  </dependencies>
  ...

Aja testit uudestaan komennolla mvn test. Huomaat että ennen testien suorittamista maven lataa JUnit-version 4.10 käyttöösi. Koska JUnit on yhteensopiva taaksepäin, testit menevät läpi. Älä enää jatkossa hallinnoi jar-tiedostoja käsin.

Paketointi

Kirjoittaessamme komennon mvn, näemme seuraavan viestin.

You must specify at least one goal or lifecycle phase to perform build steps.
The following list illustrates some commonly used build commands:

  mvn clean
    Deletes any build output (e.g. class files or JARs).
  mvn test
    Runs the unit tests for the project.
  mvn install
    Copies the project artifacts into your local repository.
  mvn deploy
    Copies the project artifacts into the remote repository.
  mvn site
    Creates project documentation (e.g. reports or Javadoc).

Please see
http://maven.apache.org/guides/introduction/introduction-to-the-lifecycle.html
for a complete description of available lifecycle phases.

Selaamalla osoitteeseen http://maven.apache.org/guides/introduction/introduction-to-the-lifecycle.html näemme tarkemman listan toiminnoista, joita maven tarjoaa ohjelmistoprojektin eri elinkaarivaiheisiin. Yksi vaihe on packaging, joka sisältää muunmuassa komennot test -- eli yllä käyttämämme komennon -- ja package -- eli paketoinnin. Suorita komento

mvn package

Käytössäsi on nyt .jar-tiedosto, johon projektiin liittyvät käännetyt lähdekooditiedostot on paketoitu.

Maven pluginit Like A Boss

Kaikille plugineille ei ole yksiosaista komentoa, vaan jotkut tulee suorittaa kaksiosaisella komennolla mvn plugin:tavoite -komentoa. Etsi osoitteessa http://maven.apache.org/plugins/index.html olevasta listasta Checkstyle ja PMD-pluginit ja

  1. ota selvää miten Checkstyle -pluginin voi suorittaa ja suorita se nykyiselle projektille
  2. ota selvää miten PMD:n Copy/Paste Detector (CPD) -työkalun voi suorittaa ja suorita se nykyiselle projektille.

Tutustu myös luotuihin raportteihin. Huomaa että raportit luodaan pluginien oletusasetuksilla -- jokainen organisaatio voi toki luoda itselleen sopivat asetukset.

Clean

Projektiisi liittyvään kansiorakenteeseen on ilmestynyt kansio target, joka sisältää käännettyjä lähdekooditiedostoja. Koska pidämme mavenista, käytämme sen putsauskomentoa. Kirjoita komento mvn clean.

Projektiin liittyvät raportit ja lähdekooditiedostot on nyt poistettu.

Kuten ylläolevassa tehtävässä huomasimme, mavenin avulla voimme hallinnoida projekteihin liittyviä riippuvuuksia ja tehdä paljon muuta. Riippuvuuksien automatisoitu hallinta on nykyaikaisissa web-sovelluksissa oleellista -- käytännössä kaikki sovellukset koostuvat useammasta komponentista. Myös muut työkalut, kuten Checkstyle ja PMD, helpottavat sovituissa koodikäytänteissä pysymistä.

Maven ja web-projektit

Maven on yleisesti käytössä oleva projektinhallintatyökalu. Mikään ei estä meitä käyttämästä mavenia myös web-sovellusten hallintaan. NetBeans 7.1 tarjoaa hyvän maven-integraation. Tutustutaan siihen seuraavassa tehtäväsarjassa.

Maven ja NetBeans

Tutustutaan seuraavaksi NetBeansin maven-integraatioon.

Huom! Jos et käytä NetBeansin versiota 7.1, ala käyttämään sitä viimeistään nyt. Aiempien versioiden Maven-integraatio ei ole läheskään yhtä hyvä kuin version 7.1. Laitoksen koneilla versio 7.1 löytyy kansiosta /opt/netbeans-7.1/, suoritettava binääri on /opt/netbeans-7.1/bin/netbeans.

Uuden projektin luominen

Luo uusi projekti valitsemalla File -> New Project. Valitse projektin tyypiksi Maven ja Web Application.

Aseta projektin nimeksi maven-intro ja ryhmän tunnukseksi wad. Paina Next.

Valitse palvelimeksi joku Java EE 5-versiota tukeva palvelin. NetBeansin mukana tuleva GlassFish käy hyvin. Huom! Varmista että valitset version Java EE 5.

Olé! Projekti on luotu.

Riippuvuusanalyysi

Valitse projektin nimi oikealla hiirennäppäimellä ja valitse vaihtoehto Show Dependency Graph. NetBeans näyttää sinulle projektiisi liittyvät riippuvuudet. Riippuvuuden analysointia helpottavasta työkalusta on hyötyä tulevaisuudessa -- kolmannen osapuolen komponentit saattavat viitata toisiinsa ristiin, tai viitata eri versioihin. Näiden näkeminen visuaalisesti auttaa ongelmatilanteissa huomattavasti.

Muutos ja users.cs.helsinki.fi -koneelle siirtäminen

Etsi käsiisi projektissa oleva index.jsp, ja muokkaa sitä siten, että <h1>-tason otsikkona on Hello Maven!

Valitse vasaran kuva, eli Build Main project (olettaen että projekti maven-intro on aktiivisena -- aktiivinen projekti näkyy tummennettuna).

Vasaran klikkaaminen luo projektista war-tiedoston, jonka voi kopioida users-koneelle. Tiedosto luodaan projektin alla olevaan kansioon target. Huomaa että war-tiedosto saa pidemmän -- versionumeron sisältävän -- nimen. Esimerkissämme nimeksi tulee maven-intro-1.0-SNAPSHOT.war.

Kopioi tiedosto nimellä maven-intro.war users.cs.helsinki.fi-koneelle.

omakone$ scp .../maven-intro-1.0-SNAPSHOT.war tunnus@users.cs.helsinki.fi:

Ja käy siirtämässä tiedosto tomcat/webapps/ -kansioon.

users$ mv maven-intro-1.0-SNAPSHOT.war tomcat/webapps/maven-intro.war

Huom! Nimeämme tiedoston uudestaan siirtovaiheessa. Tiedoston nimi määrittelee polun, jossa web-sovellus loppujenlopuksi toimii. Esimerkiksi maven-intro.war -niminen tiedosto käynnistetään osoitteeseen , my-test.war -niminen tiedosto käynnistetään osoitteeseen .../my-test/

Varmista että näet sovelluksen osoitteessa http://t-omatunnus.users.cs.helsinki.fi/maven-intro/.

Käytämme ohjelmistoissa jatkossa Mavenia.

Jatkuva integrointi

Maven helpottaa jatkuvaa integrointiprosessia huomattavasti tarjoamalla komentoriviltä suoritettavat ja ohjelmointiympäristöstä riippuvat tavoitteet.

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. Virheiden löytyessä aikaisin, tehdyt muutokset ovat kehittäjillä vielä mielessä, ja virheiden etsiminen vie huomattavasti vähemmän aikaa kuin silloin, jos integraatioita tehtäisiin harvemmin.

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 Apache Continuum, CruiseControl ja Jenkins.

Vinkki työpaikalle: Kun sovelluskehittäjä lähettää muutoksen versionhallintaan ja testit menevät läpi integraatiopalvelimella, soita ryhmätilassa yksinkertainen ja rentouttava ääni -- esim tuulenpuhallus. Jos testit eivät mene läpi, soita huumoria ja/tai ärsytystä herättävä kappale -- esim Benny Hill Theme tai kurssikanavalla nimimerkin EiZei ehdottama The Price is Right losing horn.

Web-sovelluskehykset

Kuten normaalit sovelluskehykset, web-sovelluskehykset tarjoavat valmiin rungon ja toiminnallisuutta kehitettävälle ohjelmistolle. Web-sovelluskehykset pyrkivät helpottamaan usein toistettujen toiminnallisuuksien tekemistä, esimerkiksi helpottamalla tietokantayhteyksien ja osoitteita kuuntelevien palveluiden konfigurointia ja hallinnointia.

Käydään läpi pikaisesti suunnittelumalleja ja ideoita web-sovelluskehysten takana.

Templatejärjestelmä

Lähes kaikilla web-sovelluskehyksillä on jonkinlainen templatejärjestelmä, jonka avulla käyttöliittymäkoodiin voi lisätä palvelimella generoitua dataa. Templatejärjestelmät tarjoavat myös sivun osien lataamista erikseen (esim. otsakkeet), sekä -- riippuen sovelluskehyksestä -- käyttöliittymäkomponenttikohtaisia lisätoiminnallisuuksia (esim. lomakkeiden generointi).

Front Controller

Front Controller on suunnittelumalli, jossa kaikki sovellukseen liittyvät pyynnöt ohjautuvat aluksi yhdelle kontrollipalvelulle, esimerkiksi servletille, joka ohjaa ne eteenpäin muualla määritellyn konfiguraation perusteella. Tämä mahdollistaa web-sovelluskehyksille sovelluskehyskohtaisen kuunneltavien osoitteiden ja tarjottavien palveluiden konfiguroinnin.

MVC

Suurin osa web-sovelluskehyksistä helpottaa MVC-mallin käyttämistä tarjoamalla valmiin arkkitehtuuripohjan. Käyttäjän vastuulle jää yleensä sovelluslogiikan tuottaminen. MVC-mallin lopullinen käyttäminen riippuu lopullisesta sovelluksesta. Toisin kuin perinteisessä MVC-mallissa, web-sovelluksissa pyritään yleensä siihen, että näkymästä ei pysty muokkaamaan mallia suoraan.

Web-sovellukset toteutetaan yleensä kerrosarkkitehtuuria käyttäen (Multitier architecture). Yleisimmin käytetty kerrosarkkitehtuuri on kolmikerrosarkkitehtuuri, jossa päällimmäisenä on sovelluksen kontrollilogiikka ja näyttökerros, keskellä sovelluslogiikka ja alimmaisena tietovarastoon liittyvä logiikka.

Automaattinen konfigurointi

Sovelluskehykset seuraavat usein ns. Convention-over-Configuration -paradigmaa, jossa käyttäjän tulee seurata sovelluskehyksille tyypillisiä ohjelmointikäytänteitä. Käytännössä ajatuksena on se, ettei kaikkea tule konfiguroida itse, vaan sovelluskehys tekee huomattavan osan määrittelystä käyttäjältä piilossa.

Sovellustyypit ja sopivan web-sovelluskehyksen valinta

Web-sovellukset voi jakaa karkeasti viiteen erilaiseen ryhmään.

  1. CRUD-sovellus, sanoista Create, Read, Update, Delete. CRUD-sovellukset tarjoavat lähinnä toiminnallisuuksia datan luomiseen, lukemiseen, päivittämiseen ja poistamiseen. Sovellukset, joissa päätavoitteena on yksinkertaisen lisää-poista-muokkaa -lomakkeen tekeminen, hyötyvät paljon web-sovelluskehyksistä jotka tarjoavat mahdollisimman paljon automaattista koodin generointia ja kofigurointia (esim. rails, groovy, roo).

  2. Perinteinen web-sovellus, ohut asiakaspuoli (thin client). Perinteiset web-sovellukset koostuvat useammasta sivusta, joiden välillä käyttäjä voi surffata. Mahdollisuus myös rooleihin (esim. kirjautumaton asiakas, asiakas ja admin). Sivujen sisältöä yleensä muokataan erillisellä admin-käyttöliittymällä. Perinteisiä web-sovelluksia voidaan toteuttaa useammalla eri tavalla. Esimerkiksi Apache Wicket on komponenttipohjainen (component-based) web-sovelluskehys, jossa käyttäjä ohjelmoi sivujen sisällöt. Pyyntöpohjaiset (request-based) sovelluskehykset taas toteutetaan sovelluksen tarjoamien osoitteiden näkökulmasta. Riippuen toteutettevasta sivustosta, sovelluskehittäjän / palvelua tarvitsevan kannattaa tutustua myös olemassaoleviin sisällönhallintajärjestelmiin.

  3. Rikas asiakas, (rich client), laajentaa perinteistä web-sovellusta lisäämällä dynaamista toiminnallisuutta, esimerkiksi AJAXin avulla. Rikas asiakas -malli ei tuo vain lisää käyttöliittymään, vaan se vaatii web-sovellukselta myös rajapintoja datan hakemiseen.

  4. Rikkaat internet-sovellukset (rich internet applications). Rikkaat internet-sovellukset muistuttavat hyvin paljolti perinteisiä työpöytäsovelluksia. Sovelluksen latauksen jälkeen suurin osa työstä tapahtuu selainpuolella, ja selain ja palvelin kommunikoivat asynkronisesti. Selainpuoli toteutetaan esimerkiksi Ajax-tekniikoita käyttäen, esimerkiksi Google Web Toolkitin avulla. Palvelinpuoli tarjoaa rajapinnat selaimelle datan hakemiseen.

  5. Portaalit
  6. mahdollistavat useiden sovellusten näyttämisen yhtenä sovelluksena. Portaaleja kehitettäessä oleellista on palveluiden integraatio. Käytännössä portaalien kehityksessä käytetään useampia web-sovelluskehyksiä.

Käytettävän sovelluskehyksen valinta riippuu sovelluksen tarkoituksesta. Esimerkiksi prototyyppiä rakennettaessa nopeaan kehitykseen tarkoitetut sovelluskehykset (spring, spring roo, wicket, rails jne) ovat hyödyllisiä. Toisaalta, prototyyppejä lähdetään usein laajentamaan suoraan, jolloin sovelluskehyksen ja arkkitehtuuristen päätösten tulee mukautua laajentamiseen.

Oleellista sovelluskehyksen valinnassa on sen taustalla oleva yhteisö tai yritys ja tarjottu tuki, sekä tietenkin oma osaaminen. Mikään web-sovelluskehys ei tee työtä puolestasi, vaan sinä itse olet se, joka loppupeleissä sovelluksen toteuttaa. Sovelluskehykset tuovat vain tukea toteutukseen. Mitä paremmin tunnet sovelluskehyksen, sitä paremmin pystyt käyttämään sitä tukena. Kannattaa pyrkiä tutustumaan useampaan sovelluskehykseen, ja sitä kautta löytää sopivat vaihtoehdot erilaisille tavoitteille -- ja kehittää samalla omaa osaamistaan.

SOLID

Robert "Uncle Bob" Martin lanseerasi 2000 luvun alussa termin SOLID olio-ohjelmoinnin suunnitteluun ja kehitykseen. 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.

Spring

Spring on sovelluskehys joka sisältää myös web-sovelluskehyksen. Spring on rakennettu komponenttiperustaiseksi, joten sovelluskehittäjä voi ottaa käyttöönsä vain halutut osat. Muut osat voidaan jättää pois -- tai korvata muilla tekniikoilla. Tällä kurssilla olemme lähinnä kiinnostuneet web-sovelluskehyspuolesta ja Spring Web MVC:stä. Spring on ehkäpä yleisin käytetty vaihtoehtoinen sovelluskehys JavaEE-standardille.

Spring sisältää käytännössä kaikki perustoiminnallisuudet kuin muutkin Web-sovelluskehykset. Asiakkaan tekemät pyynnöt ohjataan Front Controllerille, joka ohjaa pyynnöt pyyntöjä vastaanottaville kontrolliluokille. Kontrolliluokat ovat normaaleja Java-luokkia, eli POJOja (Plain Old Java Objects). Kontrolliluokat kommunikoivat palvelujen kanssa ja rakentavat mallin, jonka Front Controller ohjaa näkymää luovalle palvelulle. Näkymän voi luoda esimerkiksi JSP-sivuilla.

Ensimmäinen Spring-sovellus

Luodaan spring-jea -niminen Maven web-projekti NetBeansissa.

Iso osa Spring-konfiguraatiosta tapahtuu annotaatioiden avulla. Java EE 5-mallissa käytämme myös XML:ää: ns. DispatcherServlet -- eli Front Controller -- tulee konfiguroida web.xml -tiedostoon.

Ensimmäinen Spring-sovellus

Tässä tehtäväsarjassa tehdään askeleittain yksinkertainen Spring web-sovellus.

Projektin luominen NetBeansissa

Luo NetBeansissa Maven-websovellusprojekti nimeltä spring-jea. Kun luot projektin, Varmista että valitset käyttöösi Java EE 5:den. Voit valita pakkaukseksi (Package) nimen wad. Nimi wad on mielivaltainen nimi, ja tulee lyhenteestä Web Application Development. Pakkauksia käytetään normaalisti lähdekoodin loogiseen jakamiseen.

Luodun projektihakemiston fyysinen rakenne on seuraavanlainen, jos käytät Glassfish -palvelinta käytössäsi on lisäksi tiedosto glassfish-web.xml kansiossa WEB-INF. Tällöin myös kansio META-INF ja sen sisältö puuttuu. Tämä ei haittaa.

.
|-- nb-configuration.xml
|-- pom.xml
`-- src
    `-- main
        |-- java
        |   `-- wad
        `-- webapp
            |-- index.jsp
            |-- META-INF
            |   `-- context.xml
            `-- WEB-INF
                `-- web.xml

NetBeansin sisällä projekti näyttää kutakuinkin seuraavalta.

Spring-kirjastojen hakeminen

Mene sivulle http://www.springsource.org/download ja lataa.. Eiku! Opimme aiemmin käyttämään Mavenia, joten tehdään tämä kuten pitääkin.

Tällä hetkellä projektisi pom.xml, eli tiedosto joka sisältää Maven-konfiguraation, näyttää kutakuinkin seuraavalta.

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>wad</groupId>
  <artifactId>spring-jea</artifactId>
  <packaging>war</packaging>
  <version>1.0-SNAPSHOT</version>

  <name>spring-jea</name>
  <url>http://maven.apache.org</url>

  <dependencies>
    <dependency>
      <groupId>javax.servlet</groupId>
      <artifactId>servlet-api</artifactId>
      <version>2.5</version>
      <scope>provided</scope>
    </dependency>
    <dependency>
      <groupId>javax.servlet.jsp</groupId>
      <artifactId>jsp-api</artifactId>
      <version>2.1</version>
      <scope>provided</scope>
    </dependency>
  </dependencies>

  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>2.0.2</version>
        <configuration>
          <source>1.5</source>
          <target>1.5</target>
        </configuration>
      </plugin>
    </plugins>
  </build>
</project>

Konfiguraatio määrittelee riippuvuudet Javan servlet-apiin ja jsp-apiin, sekä käyttää kääntämispluginia määrittelemään oletuslähdekoodiversion.

Riippuvuuksien hakeminen ja Mavenin konfigurointi on oma taiteenlajinsa -- helpohkoa kun sen osaa. Riippuvuuksien hakupalvelu löytyy muunmuassa osoitteessa http://mvnrepository.com/, jonka lisäksi iso osa ohjelmistokomponenttien valmistajista tarjoaa omia jakelujaan ja maven-palvelimia. MVNRepositorystä löytyy Spring-kirjastot, joita tarvitsemme. Springin omalta kotisivu http://www.springsource.org löytäisimme myös oleelliset komponentit.

Tyhjennä alkuperäinen pom-konfiguraatiosi, ja kopioi allaoleva sen tilalle. Allaolevassa konfiguraatiossa olemme lisänneet spring-kirjastot ja loggaukseen käytettäviä kirjastoja. Muutimme lisäksi Javan lähdekoodiversioksi 1.6 (Java 6).

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>wad</groupId>
    <artifactId>spring-jea</artifactId>
    <packaging>war</packaging>
    <version>1.0-SNAPSHOT</version>

    <name>spring-jea</name>
    <url>http://maven.apache.org</url>

    <dependencies>
        
        <!-- spring -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webmvc</artifactId>
            <version>3.1.0.RELEASE</version>
        </dependency>
        
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>3.1.0.RELEASE</version>
            <exclusions>
                <!-- käytetään simple logging facadea commons logging-kirjaston sijaan -->
                <exclusion>
                    <groupId>commons-logging</groupId>
                    <artifactId>commons-logging</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <!-- servlet ja jsp api -->
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>servlet-api</artifactId>
            <version>2.5</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>javax.servlet.jsp</groupId>
            <artifactId>jsp-api</artifactId>
            <version>2.1</version>
            <scope>provided</scope>
        </dependency>

        <!-- jstl -->
        <dependency>
	    <groupId>jstl</groupId>
	    <artifactId>jstl</artifactId>
	    <version>1.2</version>
        </dependency>
         
        <!-- loggauskirjastot -->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>1.6.1</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>jcl-over-slf4j</artifactId>
            <version>1.6.1</version>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-log4j12</artifactId>
            <version>1.6.1</version>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.16</version>
            <scope>runtime</scope>
        </dependency>
    </dependencies>
    
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>2.3.2</version>
                <configuration>
                    <source>1.6</source>
                    <target>1.6</target>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

SLF eli Simple Logging Facade on fasaadi lähes kaikkien loggauskirjastojen käyttöön. Jostain syystä Java-kehittäjät ovat kehittäneet huomattavan määrän erilaisia loggauskirjastoja, joiden tarkoitus on tarjota toiminnallisuus -- logien kirjoittamiseen. Tähän liittyy ns. Not invented here -syndrooma, kts. http://en.wikipedia.org/wiki/Not_invented_here. SLF toimii kattokirjastona muille loggauskirjastoille.

Kun tallennat ylläolevan pom.xml-sisällön, huomaat vähän ajan kuluttua että Projektin nimessä on pieni keltainen kolmio, joka ilmaisee ongelmaa projektissa. Kun viet hiiren projektin nimen päälle, näet seuraavankaltaisen viestin:

Viesti käytännössä kertoo että projektiin liittyviä riippuvuuksia ei löydy paikallisesta varastosta. Maven lataa paikalliset lähdekooditiedostot käyttäjätunnuksen alle kansioon .m2/repository/. Lähdekoodiriippuvuudet saa ladattua (esimerkiksi) klikkaamalla kansiota Dependencies oikealla hiirennapilla ja valitsemalla Download Declared Dependencies.

Klikkaa Download Declared Dependencies, jolloin NB hakee riippuvuudet käyttöösi. Koska luotamme NetBeansiin vain silloin tällöin, valitse vielä Clean and Build. Riippuvuuksien lataamisessa menee hetki:

(kuvassa riippuvuuksia järjestelty siistimpään asetelmaan..)

Front Controllerin konfigurointi

Avaa projektiin liittyvä web.xml -tiedosto. Springin Front Controller on Servlet nimeltä DispatcherServlet, ja se löytyy pakkauksesta org.springframework.web.servlet. DispatcherServlet ottaa alustusparametrina parametrin contextConfigLocation, jonka arvoksi tulee sovelluksen konfiguraatiotiedoston sijainti. Asetetaan konfiguraatiotiedoston sijainniksi /WEB-INF/spring-context.xml.

Front Controller kuuntelee kaikkia sovellukseen kohdistuvia pyyntöjä, joten sen kuuntelemaksi osoitteeksi tulee /.

Muuta web.xml-tiedoston sisältö seuraavanlaiseksi.

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee" 
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee 
         http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
    <display-name>spring-jea</display-name>
    
    <!-- front controller -->
    <servlet>
        <servlet-name>spring-front-controller</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>/WEB-INF/spring-context.xml</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>        
 
    <servlet-mapping>
        <servlet-name>spring-front-controller</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>

    <session-config>
        <session-timeout>
            30
        </session-timeout>
    </session-config>
    <welcome-file-list>
        <welcome-file>index.jsp</welcome-file>
    </welcome-file-list>
</web-app>

Konfiguraatioparametrin load-on-startup arvo 1 tarkoittaa että Servlet ladataan heti kun on mahdollista muistiin (joissain tapauksissa servletit ladataan vasta kun ensimmäinen pyyntö tehdään).

spring-context.xml

Uuden xml-tiedoston luominen tapahtuu esimerkiksi seuraavasti NetBeansissa: Valitse File -> New File -> XML -> XML Document. Kun olet saanut dokumentin luotua, voit tyhjentää sen ja käyttää sitä pohjana. Jos valitset projektin oikealla hiirennapilla, tiedoston voi luoda valitsemalla New -> Other -> XML -> XML Document.

Luo kansioon WEB-INF xml-tiedosto nimeltä spring-context.xml. Annoimme tämän aiemmin contextConfigLocation-parametrina Springin DispatcherServlet-luokalle. Tiedosto spring-context.xml sisältää osan ohjelmiston konfiguratiosta. Java-sovelluskehitystä mollaavat kutsuvat tätä xml-viidakoksi.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="
        http://www.springframework.org/schema/mvc 
          http://www.springframework.org/schema/mvc/spring-mvc-3.0.xsd
        http://www.springframework.org/schema/beans 
          http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
        http://www.springframework.org/schema/context 
          http://www.springframework.org/schema/context/spring-context-3.0.xsd">
 
    <!-- DispatcherServletin (front-controllerin) konfiguraatio, jolla määritellään pyynnön kulku. -->
 
    <!-- Sovelluksemme lähdekooditiedostot sijaitsevat pakkauksessa wad tai sen alipakkauksissa-->
    <context:component-scan base-package="wad" />
 
    <!-- Käytetään Spring MVC:tä annotaatioiden avulla -->
    <mvc:annotation-driven /> 
</beans>

Web-sovelluksen (ja Spring-sovelluskehystä käyttävän sovelluksen) konfigurointi ilman yhtään xml-riviä on mahdollista esimerkiksi käyttämällä ns. Bootstrap-mekanismia, jossa sovellus ja palvelin käynnistetään erillisen käynnistysohjelman avulla. Sama onnistuu ilman bootstrappia Java EE 6:ssa. Käytämme kuitenkin xml:ää ja Java EE 5:ttä -- osittain nähdäksemme mitä takana liikkuu, osittain koska laitoksen users-ympäristö ei tue Java EE:n versiota 6.

Ensimmäinen kontrolleri

Luo luokka (ei servlet!) EkaController pakkaukseen wad. Spring etsii oleelliset luokat annotaatioiden perusteella, kontrolleriluokilla on annotaatio @Controller. Kontrolleriluokilla on huomattava määrä konfiguraatiomahdollisuuksia, tärkein tällä hetkellä @RequestMapping. Annotaation @RequestMapping avulla voimme määritellä metodille (ja luokalle) kuunneltavat osoitteet. Luodaan luokkaan merkkijonon palauttava metodi heiSpring().

package wad;

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

@Controller
public class EkaController {

    @RequestMapping("eka")
    public String heiSpring() {
        System.out.println("Heippa!");
        return "index.jsp";
    }
}

Ylläolevaa lähdekoodia katsoessa varmaan arvaatkin että merkkijonona palautetaan jsp-sivu, jolle pyyntö ohjataan. Ylläoleva kontrollerimme tulostaa standarditulostusvirtaan viestin "Heippa!", ja ohjaa pyynnön sivulle index.jsp.

Käynnistä palvelin ja kokeile sen toimintaa. Kun teet pyynnön osoitteeseen <palvelinosoite>:<portti>/spring-jea/eka, näet palvelimen logeissa viestin "Heippa!". Selaimessa näkyy sivun index.jsp-sisältö.

Huom! Riippuen käyttämästäsi palvelimesta, on mahdollista että sivulle index.jsp ei ohjauduta automaattisesti. Tällöin sinun tulee kirjoittaa sivu index.jsp käsin palvelimen osoitteen perään. NetBeansilla voit myös määritellä aloitusosoitteen valitsemalla projektin oikealla hiirennäppäimellä, valitsemalla Properties -> Run -> ja asettamalla kentän Relative URL -arvoksi /index.jsp.

Jos et löydä palvelua osoitteesta <palvelinosoite>:<portti>/spring-jea/eka, tarkista logeista mihin palvelu on käynnistetty. NetBeansin vanhemmilla versioilla Glassfish voi lisätä sovelluksen esimerkiksi osoitteeseen <palvelinosoite>:<portti>/spring-jea-1.0-SNAPSHOT/eka.

Huom! Jos Maven tai NetBeans ilmoittaa että sinulta puuttuu joku kirjasto käynnistäessäsi palvelinta, ongelma todennäköisesti johtuu aiemmista käytössäsi olevista kirjastoista. Käy poistamassa kotikansiossasi olevasta .m2 -hakemistosta kansio repositories. Maven luo kansioon repositories paikallisen varaston käytetyille kirjastoille, ja on mahdollista että sinulla on ollut Springin aiempi versio kirjastossasi.

Oman viestin näyttäminen sivulla EL-kielen avulla

Lisätään JSP-tutustumisestamme tutun EL-kielen avulla viesti ${viesti} JSP-sivulle, ja muutetaan näytettäväksi tekstiksi Hello Spring. Muuta index.jsp:n sisältö seuraavanlaiseksi.

<%@page contentType="text/html" pageEncoding="UTF-8"%>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
   "http://www.w3.org/TR/html4/loose.dtd">

<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <title>JSP Page</title>
    </head>
    <body>
        <h1>Hello Spring * ${viesti}</h1>
    </body>
</html>

JSP-sivua renderöidessä pyynnön attribuuteista etsitään siis attribuuttien nimeltä viesti. Servleteillä lisäsimme attribuutit pyyntöön HttpServletRequest-olion metodilla setAttribute.

Kun haet osoitteen <palvelinosoite>:<portti>/spring-jea/eka uudestaan näet seuraavanlaisen sivun.

Spring, kuten mikään muukaan sovelluskehys, ei tee taikatemppuja puolestamme.

Kontrolleriluokissa olevien metodien siisteys, osa 1

Lisää luokassa EkaController olevalle metodille heiSpring HttpServletRequest -tyyppinen parametri.

    public String heiSpring(HttpServletRequest request) {

Ja muuta metodia siten, että asetat request-oliolle attribuutin viesti. Alla attribuutille nimeltä viesti on asetettu arvo "2".

    @RequestMapping("eka")
    public String heiSpring(HttpServletRequest request) {
        System.out.println("Heippa!");

        request.setAttribute("viesti", "2");
        return "index.jsp";
    }

Lataa sivu uudelleen, näet nyt seuraavanlaisen tekstin.

Ok, miten ihmeessä toi HttpServletRequest pääsee tonne?

Kontrolleriluokissa olevien metodien siisteys, osa 2

Haluammekin pääsyn myös HttpSession-olioon, teemme seuraavaksi yksinkertaisen laskuria joka laskee käyttäjäkohtaiset pyynnöt session avulla. Muuta metodi heiSpring sellaiseksi, että se saa parametrinaan HttpServletRequest-luokan ilmentymän lisäksi HttpSession-luokan ilmentymän.

Lisää luokkaan toiminnallisuus: Jos sessiossa on olemassa attribuutti "kaynnit", kasvatetaan sen arvoa yhdellä. Muuten asetetaan sen arvoksi 1. Lisää käyntien määrä pyynnön attribuutin "viesti" arvoksi.

Muutostesi jälkeen lähdekoodin tulisi näyttää suunnilleen seuraavalta.

    @RequestMapping("eka")
    public String heiSpring(HttpServletRequest request, HttpSession session) {
        System.out.println("Heippa!");

        int kaynteja = 0;
        if (session.getAttribute("kaynnit") != null) {
            kaynteja = (Integer) session.getAttribute("kaynnit");
        }

        kaynteja = kaynteja + 1;
        session.setAttribute("kaynnit", kaynteja);

        request.setAttribute("viesti", kaynteja);
        return "index.jsp";
    }

Nyt kun haet osoitteen <palvelinosoite>:<portti>/spring-jea/eka, sinulla on henkilökohtainen käyntilaskuri.

Mitä tässä oikein tapahtuu?

Spring tuo kontrolliluokkiisi tarvitsemasi luokat silloin kun sinä niitä tarvitset.

Model

HttpServletRequest on passé. MVC-mallin mukaisesti haluamme täyttää pyynnössä mallin. Spring tarjoaa meille luokan Model, johon lisäämme pyynnön attribuutit. Muutetaan metodia heiSpring siten, että se saa parametrinaan Model-luokan ilmentymän. Attribuutti viesti lisätään Model-luokan ilmentymälle metodilla addAttribute. Ohjelmasi pitäisi olla kokonaisuudessaan nyt seuraavanlainen.

package wad;

import javax.servlet.http.HttpSession;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class EkaController {

    @RequestMapping("eka")
    public String heiSpring(Model model, HttpSession session) {
        System.out.println("Heippa!");

        int kaynteja = 0;
        if (session.getAttribute("kaynnit") != null) {
            kaynteja = (Integer) session.getAttribute("kaynnit");
        }

        kaynteja = kaynteja + 1;
        session.setAttribute("kaynnit", kaynteja);


        model.addAttribute("viesti", kaynteja);
        return "index.jsp";
    }
}

Tarkista vielä että web-sovelluksesi tarjoama toiminnallisuus ei muuttunut millään tavalla.

Onneksi olkoon, olet tehnyt ensimmäisen Spring-web sovelluksesi ja tutustunut Springin kontrollilogiikaan

Näkymät ja Controller-luokat

Palautimme edellisessä tehtävässä JSP-sivun nimen kontrolliluokan metodissa. Kontrolliluokan metodi toimii oletuksena kuin Servlet-teknologiasta tuttu DispatcherServlet, eli sille voi antaa osoitteen mihin pyyntö ohjataan. Sovelluskehittäjät haluavat usein pitää näkymätiedostoja WEB-INF -kansion alla, jolloin selainohjelmiston käyttäjät eivät pääse niihin suoraan käsiksi. Jos et halua että käyttäjä voi kirjoittaa suoraan .../tiedosto.jsp, pidä tiedosto WEB-INF -kansion alla.Voimme konfiguroida tiedostojen sijainnin joko spring-context.xml-tiedoston sisään XML:nä:

<!-- ohjaa palautetut nimet /WEB-INF/view-kansiossa oleviin jsp-sivuihin -->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
    <property name="prefix" value="/WEB-INF/view/" />
    <property name="suffix" value=".jsp" />
</bean>

Tai ohjelmallisesti:

package wad.config;
 
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.ViewResolver;
import org.springframework.web.servlet.view.InternalResourceViewResolver;
 
@Configuration
public class WadConfig {
 
    // ohjaa palautetut nimet /WEB-INF/view-kansiossa oleviin jsp-sivuihin
    @Bean
    ViewResolver viewResolver() {
        InternalResourceViewResolver resolver = new InternalResourceViewResolver();
        resolver.setPrefix("WEB-INF/view/");
        resolver.setSuffix(".jsp");
        return resolver;
    }
}

Jos käytät ohjelmallista konfiguraatiota, tarvitset lisäkirjaston nimeltä CGLIB käyttöösi. CGLIB tarjoaa toiminnallisuutta koodin automaattiseen generointiin, lisätietoa osoitteesta http://cglib.sourceforge.net/. CGLIB-kirjaston saa käyttöön Mavenin avulla lisäämällä riippuvuuden pom.xml-tiedostoon.

        <dependency>
            <groupId>cglib</groupId>
            <artifactId>cglib</artifactId>
            <version>2.2.2</version>
        </dependency> 

Kumpaa tahansa ylläolevaa lähestymistapaa käytettäessä kontrolliluokasta palautettu merkkijono "sivu" näytettäisiin kansiossa WEB-INF/sivu.jsp-tiedoston avulla. Huomaa että loppuliite suffix on määritelty .jsp:ksi, joten kontrollerimetodilta palautettavaan merkkijonoon ei lisätä .jsp -päätettä.

Springin vahvuutena on näkymäteknologioiden erotus sovelluslogiikasta. Käyttäjä voi käytännössä valita oman näkymäteknologian, tekniikan vaihto vaatii toki konfigurointia. Käytämme kurssilla lähinnä JSP:tä, lisää tietoa näkymien konfiguroinnista springistä löytyy mm. osoitteista http://static.springsource.org/spring/docs/current/spring-framework-reference/html/mvc.html#mvc-viewresolver ja http://static.springsource.org/spring/docs/current/spring-framework-reference/html/view.html.

Pyynnön parametrit

Pyynnössä lähetettyjen parametrien käsittely on myös suhteellisen helppoa. Parametrit saadaan kiinni annotaatiolla @RequestParam, jota seuraa muuttuja johon parametri asetetaan. Esimerkiksi seuraava kontrolleriluokan metodi tulostaNimi tulostaa osoitteeseen palvelin:portti/sovellus/tulosta saadun pyynnön parametrina saadun muuttujan nimi arvon.

    @RequestMapping("tulosta")
    public String tulostaNimi(@RequestParam String nimi) {
        System.out.println("Nimi: " + nimi);
        return "done";
    }

Yllä oleva metodi ottaa pyynnöstä parametrin nimeltä nimi ja tulostaa sen arvon standarditulostusvirtaan. Metodi palauttaa merkkijonon done, jonka avulla -- konfiguraatiosta riippuen -- valitaan näkymä.

Vaikka HTTP:llä parametrit ovat aina merkkijonomuotoisia, voimme pyytää Springiä muuttamaan parametrit valmiiksi meitä varten. Esimerkiksi seuraava metodi summaa kahden kokonaisluvun arvon.

    @RequestMapping("summaa")
    public String summaaArvot(@RequestParam Integer eka, @RequestParam Integer toka) {
        System.out.println("Summa: " + (eka + toka));
        return "done";
    }

Laskin

Tässä tehtävässä toteutetaan yksinkertainen laskin, joka tarjoaa operaatiot summa- kerto- ja jakolaskut.

Runko

Luo itsellesi projektirunko jossa käytössäsi on Spring-sovelluskehys. Käytä näkymänohjausta siten, että kun palautat kontrolliluokkien metodeista merkkijonon, merkkijonon perusteella haetaan JSP-sivua kansiosta /WEB-INF/view/.

JSP-sivu

Luo kansioon /WEB-INF/view/ JSP-sivu laskin.jsp, joka sisältää seuraavan määrittelyn:

<%@page contentType="text/html" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <title>JSP Page</title>
    </head>
    <body>
        <h1>Tulos on ${tulos}</h1>
    </body>
</html>

Summalasku

Luo luokka LaskinController. Määrittele se kontrolleriksi @Controller-annotaation avulla.

Luo luokkaan metodi summalasku, joka kuuntelee osoitetta summa. Metodi saa kaksi pyyntöparametria eka ja toka, jotka molemmat ovat kokonaislukuja, sekä mallin (Model), johon tulos tallennetaan myöhempää näyttöä varten.

Laske metodissa parametrina saatujen lukujen summa, ja lisää se attribuuttina "tulos" malliin. Palauta metodista merkkijono "laskin", jolloin web-sovelluksesi pitäisi ohjata tulos kansiossa /WEB-INF/view/ olevalle laskin.jsp-sivulle.

Voit testata laskintasi käyttämällä selaintasi. Esimerkiksi pyyntö osoitteeseen .../sovellus/summa?eka=1&toka=3 tulee lisätä malliin arvo 4 ja tulostaa se käyttäjälle seuraavannäköisenä sivuna.

Kertolasku ja jakolasku

Lisää metodit kertolasku, joka kuuntelee osoitetta kerto, ja jakolasku, joka kuuntelee osoitetta jako. Muista että Javassa kahden kokonaisluvun jakolaskun tulos on myös kokonaisluku. Saat tulokseksi liukuluvun kertomalla esimerkiksi ensimmäisen luvun 1.0:lla ennen laskua.

Siirrä ohjelmasi users-koneelle ja palauta se

Saat ohjelman paketoitua kuten aiemminkin valitsemalla "clean & build". Siirrä laskimesi users-koneelle, ja lähetä se tarkastettavaksi osoitteessa http://t-avihavai.users.cs.helsinki.fi/tester2/ olevaan palveluun.

Huom! Kun palautat vain yhtä tehtävää kerrallaan, tarkastaja valittaa ettei muita ole lähetetty. Tämä on täysin ok -- jos et näe virheilmoitusta omaan tehtävääsi liittyen.

Web-sovelluksen vaatimukset:

Listat

Näemme usein käyttötapauksia, joissa meidän tulee ottaa kiinni useampia samannimisiä parametreja. Samannimiset parametrit voidaan ottaa kiinni määrittelemällä taulukkotyyppinen muuttuja ja käyttämällä tuttua @RequestParam-annotaatiota.

    @RequestMapping("montaViestia")
    public String tulostaNimi(@RequestParam String[] viestit) {
        for (String viesti : viestit) {
            System.out.println(viesti);
        }
        
        return "done";
    }

Ylläoleva kontrolliluokan metodi pystyy käsittelemään esimerkiksi allaolevan lomakkeen.

    <form action="montaViestia" method="POST">
        <input type="text" name="viestit" ><br>
        <input type="text" name="viestit" ><br>
        <input type="text" name="viestit" ><br>
        <input type="submit">
    </form>

Lomakkeista ja olioista

On myös mahdollista asettaa pyynnön attribuutteja luokan ilmentymän arvoiksi. Luokalla täytyy olla tätä varten setterit (set-metodit). Haluamme kerätä henkilöltä nimen ja sähköpostiosoitteen. Luodaan tätä varten oma luokka Henkilo, jonka sisältö on seuraavanlainen.

public class Henkilo {

    private String nimi;
    private String email;

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public String getNimi() {
        return nimi;
    }

    public void setNimi(String nimi) {
        this.nimi = nimi;
    }
}

Kai muistat että sinun tarvitsee kirjoittaa luokalle vain attribuutit, NetBeansin insert code-toiminnallisuus hoitaa loput?

Henkilo-luokalla on kentät nimi ja email. Luodaan seuraavaksi lomake henkilön tietojen pyytämiseen.

    <form action="yhteystiedot" method="POST">
        <span>Nimi: <input type="text" name="nimi" ></span><br>
        <span>Sähköpostiosoite: <input type="text" name="email" ></span><br>
        <input type="submit">
    </form>

Ja lopuksi kontrollerimetodi. Saamme annotaatiolla @ModelAttribute asetettua pyyntöön liittyviä parametreja suoraan olion arvoksi.

    @RequestMapping("yhteystiedot")
    public String tulostaHenkilo(@ModelAttribute Henkilo henkilo) {
        System.out.println("Henkilön nimi: " + henkilo.getNimi());
        System.out.println("Henkilön sähköpostiosoite: " + henkilo.getEmail());

        return "done";
    }

Kilpailuun osallistuminen

Luo web-sovellus jonka avulla voidaan ilmoittautua kilpailuun. Kilpailuun ilmoittautumisia ei tallenneta mihinkään, vaan ne näytetään suoraan käyttäjälle.

Käytä lomakkeena seuraavaa (tarvitset siis kontrolleriluokan jossa on osoitetta "osallistu" kuunteleva metodi:

    <form action="osallistu" method="POST">
        <span>Nimi: <input type="text" name="nimi" ></span><br>
        <span>Email: <input type="text" name="email" ></span><br>
        <span>Saa mainostaa jatkossa: <input type="checkbox" name="osallistuuKilpailuun" ></span><br>
        <input type="submit">
    </form>

Voit määritellä kontrolliluokan metodin ottamaan vastaan vain POST-tyyppisiä pyyntöjä @RequestMapping-annotaation avulla:

@RequestMapping(value="osoite", method=RequestMethod.POST)

Kun lomake on lähetetty, tulosta lomakkeen tiedot seuraavaa sivupohjaa käyttäen. Sivupohjaan on jo määritelty tarvitut EL-tägit.

<%@page contentType="text/html" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <title>Ilmoittautuminen</title>
    </head>
    <body>
        <h2>Osallistujan nimi: ${osallistuja.nimi}</h2>
        <h2>Osallistujan email: ${osallistuja.email}</h2>
        <h2>Osallistuu: ${osallistuja.osallistuuKilpailuun}</h2>
    </body>
</html>

Huom! On ok että Osallistuu -kohtaan tulostuu tyhjää kun käyttäjä ei ruksaa checkbox-nappia

Siirrä ohjelmasi users-koneelle, ja lähetä se tarkastettavaksi osoitteessa http://t-avihavai.users.cs.helsinki.fi/tester2/ olevassa palvelussa.

POST

Kontrolleriluokan metodin voi asettaa kuuntelemaan vain POST-kutsuja @RequestMapping-annotaation parametreillä

@RequestMapping(value="osoite", method=RequestMethod.POST)

POST-tapahtumien jälkeen haluamme tehdä uudelleenohjauksen normaaliin listaukseen, jotta refresh-napin painaminen ei haittaisi. Servleteillä käytettiin HttpServletRequest-luokan sendRedirect-metodia, Springissä voimme palauttaa merkkijonon "redirect:/uusiosoite".

Pyyntöparametriannotaatioiden suoritusjärjestyksestä

Kun teet pyynnön Spring yrittää ensin asettaa pyynnon parametreja @ModelAttribute-annotaatiolla merkattuihin olioihin. Tämän jälkeen arvoja asetetaan @RequestParam-annotaatiolla merkattuihin muuttujiin. Voit siis myös käyttää kumpiakin kenttiä sovellusta rakennettaessa, esimerkiksi seuraavasti:

    @RequestMapping("yhteystiedot")
    public String tulostaHenkilo(@ModelAttribute Henkilo henkilo, @RequestParam Integer piilotunnus) {
        System.out.println("Henkilön nimi: " + henkilo.getNimi());
        System.out.println("Henkilön sähköpostiosoite: " + henkilo.getEmail());

        System.out.println("Piilotunnus: " + piilotunnus);

        return "done";
    }
    <form action="yhteystiedot" method="POST">
        <span>Nimi: <input type="text" name="nimi" ></span><br>
        <span>Sähköpostiosoite: <input type="text" name="email" ></span><br>
        <input type="hidden" name="piilotunnus" value="42" >
        <input type="submit">
    </form>

Piilotunnus on määritelty lomakkeeseen hidden-kenttänä jolla on arvo 42. Syötekentän tyyppi hidden tarkoittaa sitä että kenttää ei näytetä käyttäjälle.

Oletusarvot ja parametrin pakollisuus

Pyynnön parametrille voi määritellä oletusarvon. Tämä auttaa esimerkiksi checkbox-tyyppisissä kentissä. Valintaruutukentästä ei lähetetä tietoa palvelimelle jos sitä ei ole valittu.

    <form action="check" method="POST">
        <span>Asia selvä? <input type="checkbox" name="kaikkiOk" > Jep! </span><br>
        <input type="submit">
    </form>
    @RequestMapping("check")
    public String tulostaHenkilo(@RequestParam(required=false, defaultValue="false") Boolean kaikkiOk) {
        System.out.println("Mukana: " + ok);
        return "done";
    }

Oletusarvo määritellään @RequestParam-annotaation parametrin defaultValue avulla. Huomaa että defaultValue saa arvokseen aina merkkijonon. Parametrin voi määritellä vapaaehtoiseksi asettamalla RequestParam-annotaation parametrille required arvo false.

Spring tarjoaa myös oman tägikirjaston lomakkeiden luomiseen JSP-sivuille. Emme kuitenkaan tutustu siihen vielä.

Sovelluslogiikka ja Service-annotaatio

Sovelluslogiikka jaetaan oliosuunnittelun periaatteiden mukaisesti toiminnallisuutta tarjoaviin palveluihin, joita kontrollikerros käyttää. Spring tarjoaa hyvät välineet sovelluslogiikan ja kontrollikerroksen erottamiseen. Edellisessä kappaleessa tutustuimme kontrollilogiikan ja näkymän erottamiseen, sekä Springin kontrolliluokan toimintaan. Tutustumme seuraavaksi kontrolliluokan ja sovelluslogiikan yhteistoimintaan.

Inversion of Control ja Dependency Injection

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.

Palveluesimerkki: HitCounter

Luodaan palvelu jonka tehtävänä on yksittäisten vierailujen määrän laskeminen. Oletetaan että meillä on käytössä aiemmin ensimmäinen Spring-sovellus -kohdassa luotu tyhjä projekti.

Määritellään rajapinta HitCounter-palvelulle. Rajapinta tarjoaa kaksi metodia: getCount(), joka palauttaa kävijöiden määrän, ja incrementCount(), joka kasvattaa kävijöiden määrää yhdellä.

package wad.hitcounter;

public interface HitCounter {
    int getCount();
    void incrementCount();
}

Viimeistään tässä vaiheessa tekisimme oikeasti muutaman testin HitCounter-palvelulle. Keskitymme kuitenkin tässä esimerkissä itse palveluun, palaamme testaamiseen myöhemmin.

Luodaan rajapinnalle toteutus SimpleHitCounter. Toteutus merkitään annotaatiolla @Service. Annotaatiolle vinkkaamme sovelluskehykselle luokan SimpleHitCounter olevan luokka, jota sovelluskehyksen tulee hallinnoida.

package wad.hitcounter;

@Service
public class SimpleHitCounter implements HitCounter {
    private int count = 0;

    @Override
    public int getCount() {
        return count;
    }

    @Override
    public void incrementCount() {
        count++;
    }
}

Luodaan seuraavaksi näkymä, jonka tehtävänä on kertoa käyntien määrä. Esimerkissämme näkymä sijaitsee tiedostossa check.jsp.

<%@page contentType="text/html" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <title>uno dos tres</title>
    </head>
    <body>
        <h1>Hits: ${hits}</h1>
    </body>
</html>

Näkymässämme on EL-tägi hits, jonka tehtävänä on näyttää osumien määrä. Luodaan lopuksi kontrolleri HitController, joka vastaanottaa pyynnöt osoitteeseen hitme, kutsuu HitCounter-palvelun tarjoamia metodeja, ja palauttaa lopuksi luodun mallin näkymää varten.

package wad.hitcounter;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class HitController {

    @Autowired
    HitCounter hitCounter;

    @RequestMapping("hitme")
    public String incrementAndReturn(Model model) {
        hitCounter.incrementCount();
        model.addAttribute("hits", hitCounter.getCount());
        
        return "check";
    }
}

Kontrolliluokassa HitController oleellisinta on @Autowired -annotaatio ja se, että käytämme HitCounter-rajapintaa. Sovelluskehys löytää rajapinnan HitCounter toteuttavan luokan SimpleHitCounter, jolla on annotaatio @Service. Sovelluskehys osaa annotaation ja toteutetun rajapinnan avulla päätellä että luokasta SimpleHitCounter tulee luoda ilmentymä kontrolliluokkaamme.

Kun käynnistämme palvelun ja kutsumme hitme-sivua, näemme lopputuloksen. Alla olevassa kuvassa sivulla on käyty jo muutama kerta.

Edellisen esimerkin pääosat:

10 * HitCounter

Muokkaa ylläolevaa sovellusta siten, että kasvatat osumien määrää kymmenellä yhden sijaan. Kun osumien määrä on yli 100, aloita laskeminen nollasta.

Kun olet valmis, siirrä ohjelmasi users-koneelle, ja lähetä se tarkastettavaksi osoitteessa http://t-avihavai.users.cs.helsinki.fi/tester2/ olevassa palvelussa.

Spring-Chat

Päivitä viikolla 1 toteuttamasi Chat-sovellus Spring-sovelluskehystä käyttäväksi.

Kun olet valmis, siirrä ohjelmasi users-koneelle, ja lähetä se tarkastettavaksi osoitteessa http://t-avihavai.users.cs.helsinki.fi/tester2/ olevaan palveluun.

Varastonhallinta

Tehdään ohjattuna yksinkertainen varastonhallinta. Varastonhallinta tarjoaa esineille lisäystoiminnallisuuden, poistotoiminnallisuuden ja listaustoiminnallisuuden HTML-käyttöliittymän kautta.

Luo (tai kopioi aiempi) spring-web-projekti käyttöösi. Luo projektille pakkaus wad.varasto.

Domain

Suunnittelemme varastonhallintaa esineille. Esineillä on nimi ja paino. Lisätään jokaiselle esineelle lisäksi yksilöllinen tunniste. Luo luokka esine pakkaukseen wad.varasto.domain. Tähän pakkaukseen tulisivat muutkin ns. Domain-objektit, jotka kuvaavat sovellusaluetta.

Kopioi allaoleva Esine-luokan runko käyttöösi. Lisää luokalle vielä getterit ja setterit.

// pakkaus

public class Esine {
    private static int LASKURI = 1;

    private int id;
    private String nimi;
    private Double paino;

    public Esine() {
        id = LASKURI++;
    }
    
    // getterit ja setterit
}

Näkymä

Seuraavaksi näkymä. Näkymässä on toiminnallisuus esineiden lisäämiseen, listaamiseen ja poistamiseen. JSP-sivulla on käytössä JSTL forEach-lausekkeiden tekemiseen.

<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@page contentType="text/html" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <title>Varasto</title>
    </head>
    <body>
        <h1>Varasto</h1>

        <h2>Lisää esine</h2>
        <form action="lisaa" method="POST">
            Esineen nimi: <input type="text" name="nimi"><br/>
            Esineen paino (desimaalilukuna): <input type="text" name="paino"><br/>
            <input type="submit" value="Lisää">            
        </form>

        <h2>Esineet</h2>
        <ul>
            <c:forEach var="esine" items="${esineet}">
                <li>${esine.nimi}, paino ${esine.paino}, <a href="poista/${esine.id}">poista</a></li>
            </c:forEach>
        </ul>
    </body>
</html>

Käyttöliittymän voi rakentaa jo ennen kuin sovellus tarjoaa yhtäkään konkreettista toiminnallisuutta. Käyttöliittymädrafteista on hyvä lähteä eteenpäin myös asiakkaalle sovellusta esiteltäessä.

EsinePalvelu

Koodia tuottava kaverisi ehti määritellä EsinePalvelulle seuraavan rajapinnan.

package wad.varasto.service;

import java.util.List;
import wad.varasto.domain.Esine;

public interface EsinePalvelu {
    void lisaa(Esine esine);
    List<Esine> listaa();
    void poista(int esineId);
}

Toteuta rajapinnan määrittelemät toiminnallisuudet toteuttava luokka SimpleEsinePalvelu. Käytä ArrayListiä esineiden tallettamiseen.

package wad.varasto.service;

import java.util.ArrayList;
import java.util.List;
import org.springframework.stereotype.Service;
import wad.varasto.domain.Esine;

@Service
public class SimpleEsinePalvelu implements EsinePalvelu {
    private List<Esine> esineet = new ArrayList();
   
    // ...

Voit poistaa esineen listasta id-kentän avulla esimerkiksi seuraavasti.

        Esine esine = null;
        for (Esine e : esineet) {
            if (e.getId() == esineId) {
                esine = e;
                break;
            }
        }
        
        if (esine != null) {
            esineet.remove(esine);
        }

VarastoController, listaaminen ja lisääminen

Alla on annettu pohja VarastoControllerille. VarastoControllerin tulee olla pakkauksessa wad.varasto.controller. Lisää pohjaan ensin metodi, joka kuuntelee osoitetta "listaa", ja lisää näkymälle vietävään malliin kaikki esineet. Käytä EsinePalvelu-rajapinnan määrittelemää listaa-metodia esineiden listaamiseen.

Kun listaa-osoitetta kuunteleva palvelu on toteutettu, lisää metodi esineen lisäämiseksi. Käytä EsinePalvelu-rajapinnan määrittelemää lisaa-metodia. Lisää-palvelun tulee toimia osoitteessa "lisaa" ja se saa kuunnella vain POST-pyyntöjä. Kun esine on lisätty, ohjaa pyyntö listaa-osoitteeseen.

// importit

@Controller
public class VarastoController {

    @Autowired
    private EsinePalvelu esinePalvelu;

    @RequestMapping("*")
    public String nayta() {
        // oletus, ohjataan kaikki pyynnöt listaa-osoitteeseen
        return "redirect:/listaa";
    }
    
    //..

}

VarastoController, poistaminen

Käyttöliittymälogiikkaa suunniteltaessa sinne päätyi linkki joka osoittaa poista/id -osoitteeseen, missä id on esineen tunnus.

    <a href="poista/${esine.id}">poista</a>

Polussa määriteltäviä muuttujia saa otettua kiinni @PathVariable-annotaation avulla. Toteuta metodi poistotoiminnallisuuden lisäämiseen seuraavan rungon pohjalta.

    @RequestMapping(value = "poista/{esineId}")
    public String poista(@PathVariable Integer esineId) {
        // ...

Ylläoleva metodi kuuntelee polkuun "poista/{esineId}" tulevia pyyntöjä. Merkintä {esineId} kertoo sovelluskehyksellemme että polussa tulee sovellukselle tarpeellista tietoa. Tiedon saa käyttöön @PathVariable-annotaatiolla merkityllä muuttujalla. Muuttujalla on sama nimi kuin polkumäärittelyssä olevalla merkinnällä.

Ohjaa käyttäjä poista-metodin suorituksen lopuksi "listaa"-osoitteeseen.

Kun olet valmis, siirrä ohjelmasi users-koneelle, ja lähetä se tarkastettavaksi osoitteessa http://t-avihavai.users.cs.helsinki.fi/tester2/ olevassa palvelussa.

Palvelukeskeiset arkkitehtuurit

Palvelukeskeisissä arkkitehtuureissä palvelut on toimivat itsenäisinä palveluina joita käytetään avoimien rajapintojen kautta. Tutustutaan aluksi palvelukeskeisiin arkkitehtuureihin muutaman tehtävän avulla.

SOA HitCounter

Luodaan palvelu, joka käyttää keskitettyä palvelua kävijöiden laskemiseen. Palvelu toimii osoitteessa http://t-avihavai.users.cs.helsinki.fi/hitcounter/ -- kyselyiden rajapintana on HTTP.

Apache HTTPComponents on kätevä kirjasto kyselyjen tekemiseen HTTP:n yli, joten otetaan se käyttöön. Saat sen ladattua lisäämällä projektiisi seuraavan riippuvuuden:

        <dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpclient</artifactId>
            <version>4.1.2</version>
        </dependency>

HTTPComponents -kirjaston osoitteeseen url tehtävän pyynnön vastauksen voi lukea esimerkiksi seuraavasti:

    private String getResponseBody(String url) {
        HttpClient httpclient = new DefaultHttpClient();
        HttpGet httpget = new HttpGet(url);
        try {
            HttpResponse response = httpclient.execute(httpget);
            return readInputStream(response.getEntity().getContent());
        } catch (IOException ex) {
            Logger.getLogger(SimpleHitCounter.class.getName()).log(Level.SEVERE, null, ex);
        }

        return null;
    }

    private String readInputStream(InputStream is) {
        StringBuilder sb = new StringBuilder();
        Scanner sc = new Scanner(is);
        while (sc.hasNextLine()) {
            sb.append(sc.nextLine()).append("\n");
        }

        return sb.toString();
    }

Tehtävä: Toteuta kävijöiden määrää laskeva web-sovellus, joka käyttää laskemiseen osoitteessa http://t-avihavai.users.cs.helsinki.fi/hitcounter/{opiskelijanumerosi} toimivaa palvelua. Jos opiskelijanumerosi on 012345678, tee pyynnöt osoitteeseen http://t-avihavai.users.cs.helsinki.fi/hitcounter/012345678. Palvelu palauttaa aina numeron, joka kertoo käyntien määrän.

Toteuta sovelluksesi logiikka seuraavan rajapinnan alle kerrostetun arkkitehtuurin mukaisesti.

// pakkaus

public interface HitCounter {
    int getAndIncrementCount();
}

Sovelluksen sivupohjana voit käyttää esimerkiksi seuraavaa.

<%@page contentType="text/html" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <title>uno dos tres</title>
    </head>
    <body>
        <h1>SOA Hits: ${hits}</h1>
    </body>
</html>

Saat merkkijonomuodossa olevan numeron muunnettua numeroksi Integer-luokan parseInt-metodilla.

Kun olet valmis, siirrä ohjelmasi users-koneelle, ja lähetä se tarkastettavaksi osoitteessa http://t-avihavai.users.cs.helsinki.fi/tester2/ olevassa palvelussa.

Tehdyt tehtävät

Toteuta palvelu, jota käytetään ensimmäisen viikon tehtävien tarkistamiseen. Käytä HTTP-rajapintaa osoitteessa http://t-avihavai.users.cs.helsinki.fi/tehdyt/{opiskelijanumerosi} toimivaa palvelua. Jos opiskelijanumerosi on 012345678, tee pyynnöt osoitteeseen http://t-avihavai.users.cs.helsinki.fi/tehdyt/012345678

Käytä seuraavaa lomaketta oman palvelusi käyttöliittymän luomiseen.

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
    "http://www.w3.org/TR/html4/loose.dtd">
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <title>Tarkistus</title>
    </head>
    <body>
        <h1>Viikon 1 tehtävien tarkistus</h1>

        <p>Syötä opiskelijanumerosi ja paina nappia:</p>
        <form action="check" method="POST">
            <input type="text" name="opiskelijanumero">
            <input type="submit">
        </form>
    </body>
</html>

Tulosta palvelun palauttamat tiedot palautus sellaisenaan käyttäjälle.

Kun olet valmis, siirrä ohjelmasi users-koneelle, ja lähetä se tarkastettavaksi osoitteessa http://t-avihavai.users.cs.helsinki.fi/tester2/ olevassa palvelussa.

Käytetyt tunnit, viikko 2

Kirjaa osoitteessa http://t-avihavai.users.cs.helsinki.fi/tunnit2 olevaan palveluun toisen viikon materiaalin ja tehtävien parissa käyttämäsi aika.

Koska usersilla on huomattava määrä käyttäjiä, sammuta palvelimesi komennolla stop-tomcat jos et käytä sitä muuhun.

Palvelukeskeisissä arkkitehtuureissä järjestelmä rakentuu useammasta pienemmästä järjestelmästä. Osajärjestelmiä -- palveluita -- käytetään avoimien rajapintojen kautta, esimerkiksi HTTP:n yli. Omaa kolmannen osapuolen palveluihin perustuvaa järjestelmää rakennettaessa oleellista on järkevä arkkitehtuuri -- esimerkiksi kerrosarkkitehtuuri. Ylläolevissa tehtävissä SOA Hitcounter ja Tehdyt tehtävät kolmannen osapuolen palvelun käyttö tulee kapseloida -- sovelluslogiikkaa ei saa ikinä sotkea kontrolleriluokkiin.

REST

REST (representational state transfer) on ehkäpä yleisin lähestymistapa palvelukeskeisten arkkitehtuurien rakentamiseen. RESTin taustaidea on yksinkertainen tiedon hallinta web-osoitteita ja HTTP-protokollaa käyttäen. HTTP:n GET- ja POST-komentojen lisäksi REST-sovellukset käyttävät ainakin PUT ja DELETE-pyyntöjä. Esimerkiksi 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 getHenkilo?id={tunnus} vaan /henkilo/{tunnus} -- ja pyynnöt kategorisoidaan pyyntötyyppien mukaan. DELETE-tyyppisessä pyynnössä poistetaan, PUT-tyyppisessä pyynnössä lisätään tai päivitetään, GET-tyyppisessä pyynnössä haetaan. Kuten normaalissakin HTTP-kommunikaatiossa, GET-pyyntöjen ei tule muuttaa käytössä olevien resurssien tietoja.

Käytettyjen resurssien muuttamisesta Aiemmin kurssilla on puhuttu siitä, että GET-pyynnöillä ei tule muuttaa tiedon tilaa. Tällä tarkoitetaan sitä, että riippumatta siitä kuinka monta kertaa pyyntö tehdään, lopputuloksen tulisi olla sama. GET-pyyntöjä voi käyttää turvallisesti myös tiedon poistamiseen -- yksi kutsu ei REST-tyyppiseen osoitteeseen /poista/{tunnus} tulee luoda sama lopputulos kuin tuhannen peräkkäisen pyynnön saaman osoitteeseen.

REST on hyvä rajapinta 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. Palvelua luodessa palvelun toiminnallisuutta kuvaava dokumentaatio on oleellinen!

REST-palvelut ovat hyvin yleisiä ja niiden luomiseen on tehty huomattava määrä apuohjelmia. Yksinkertaisen REST-palvelun voi luoda esimerkiksi suoraan olemassaolevasta tietokannasta -- kannattaa tutustua NetBeansin Web Services -osioon.

Seuraavissa esimerkeissä käytetään allaolevaa luokkaa Olut.

public class Olut {

    private int id;
    private String name;

    public Olut() {
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public boolean equals(Object obj) {
        if(super.equals(obj)) {
            return true;
        }
        
        if(!(obj instanceof Olut)) {
            return false;
        }
        
        Olut o = (Olut) obj;
        return o.getId() == this.getId();
    }
    
    @Override
    public int hashCode() {
        int hash = 7;
        hash = 43 * hash + this.id;
        return hash;
    }
}

Esimerkki: Yksinkertainen REST-rajapinta Springiä käyttäen

Alla on esimerkki REST-tyylisen rajapinnan avulla luodusta olutpalvelusta. Olutpalvelussa käyttäjä voi lisätä, muokata ja poistaa oluita.

@Controller
public class OlutController {

    @Autowired
    private OlutPalvelu olutPalvelu;

    @RequestMapping(method = RequestMethod.GET, value = "olut")
    public String listaaOluet(Model model) {
        model.addAttribute("oluet", olutPalvelu.listaaOluet());
        return "listaus";
    }

    @RequestMapping(method = RequestMethod.GET, value = "olut/{olutId}")
    public String naytaOlut(Model model, @PathVariable Integer olutId) {
        model.addAttribute("olut", olutPalvelu.annaOlut(olutId));
        return "olutnakyma";
    }

    @RequestMapping(method = RequestMethod.POST, value = "olut")
    public String lisaaOlut(@ModelAttribute Olut olut) {
        olut = olutPalvelu.lisaaOlut(olut);
        return "redirect:/olut/" + olut.getId(); // luotu olut
    }

    @RequestMapping(method = RequestMethod.DELETE, value = "olut/{olutId}")
    public String poistaOlut(@PathVariable Integer olutId) {
        olutPalvelu.poistaOlut(olutId);
        return "redirect:/olut";
    }

    @RequestMapping(method = RequestMethod.PUT, value = "olut/{olutId}")
    public String muokkaaTaiLisaaOlut(@ModelAttribute Olut olut, @PathVariable Integer olutId) {
        olut = olutPalvelu.muokkaaTaiLisaaOlut(olutId, olut);
        return "redirect:/olut/" + olut.getId(); // luotu tai muokattu olut
    }
}

Kuten huomaat, REST-toteutuksemme käyttää aiemmin tutuksi tulleita kommunikointimenetelmiä. Osoitteet identifioivat resurssin, pyyntötavat halutun resurssin. Ylläolevan esimerkin ohjaustyyleistä kannattaa ottaa mallia myös omiin sovelluksiin. Kontrolleriluokan metodit -- kuten kontrollerit kokonaisuudessaan -- tulee pitää mahdollisimman pieninä. Varsinainen sovelluslogiikka tulee hoitaa muualla.

Yllä käytetyn olutpalvelun rajapinta on seuraava.

public interface OlutPalvelu {
    Olut lisaaOlut(Olut olut);
    void poistaOlut(int tunnus);
    Olut muokkaaTaiLisaaOlut(int tunnus, Olut olut);
    Olut annaOlut(int tunnus);
    List<Olut> listaaOluet();
}

Olutvarasto

Toteuta ylläolevan esimerkin näyttämä sovellus. HTTP-rajapinnan eli kuunneltavien osoitteiden ja pyyntötapojen tulee olla samat. Luo rajapinnalle OlutPalvelu oma toteutus, jossa oluet tallennetaan muistiin. Jokaiselle oluelle tulee luoda uniikki tunniste. Kun olet valmis, lisää sovellus users-palvelimelle ja tarkistuta se osoitteessa http://t-avihavai.users.cs.helsinki.fi/tester3/ olevassa palvelussa.

Palvelusi tulee siis mm. kuunnella DELETE-tyyppisiä pyyntöjä osoitteisiin. Esim. DELETE-pyyntö osoitteeseen ".../olut/3" poistaa oluen tunnuksella 3.

Jos palvelusi toimii osoitteessa http://t-avihavai.users.cs.helsinki.fi/Olutvarasto, ja esimerkiksi oluen lisääminen tulisi tehdä POST-pyynnöllä osoitteeseen http://t-avihavai.users.cs.helsinki.fi/Olutvarasto/olut, lähetä osoite http://t-avihavai.users.cs.helsinki.fi/Olutvarasto/ palautuksessa.

Huom! Käytä toteutuksessasi valmista Olut-luokkaa. Sinun ei tarvitse toteuttaa käyttöliittymää PUT ja DELETE -komentojen testaamiseen (puhtailla HTML-lomakkeilla tämä on käytännössä mahdotonta.)

Ekalla viikolla tutuksi tulleesta komennosta curl on tässäkin hyötyä -- voit testata DELETE ja PUT -komentoja curlin avulla.

Tiedonsiirtoformaatit

Palveluiden ei kannata palauttaa kokonaista HTML-sivua koska niitä usein käytetään pienempien osakokonaisuuksien käsittelemiseen. Osakokonaisuuksia varten tarvitsemme yleisesti hyväksyttyjä ja avoimia tiedonsiirtomuotoja. Yleisimmät tiedonsiirtomuodot tällä hetkellä ovat JSON ja XML, joista ensimmäisen suosio kasvaa jatkuvasti.

JSON

JSON (JavaScript Object Notation) on javascriptin käyttämä tiedonsiirtoformaatti. JSON mahdollistaa avain-arvo -parien ja listojen esittämisen. Avain-arvo -pari esitetään seuraavasti (alla esitetty ylläolevan luokan ilmentymä).

{"name":"Hacker-Pschorr Hefe Weisse","id":10}

Listarakenteessa avain-arvo -parit esitetään pilkulla erotettuna hakasulkeiden ([]) sisällä.

[{"name":"Hacker-Pschorr Hefe Weisse","id":10},
 {"name":"Buttface Amber Ale","id":11},
 {"name":"Yellow Snow","id":12}]

Arvot voivat olla merkkijonoja, numeroita, toisia avain-arvo -pareja, listoja, totuusarvoja ja tyhjiä null -elementtejä.

Kuten suurin osa sovelluskehyksistä, Spring tarjoaa palvelun olioiden JSON-muotoon muuttamiseksi. Lisäämällä kontrolleriluokan metodille määrittelyn @ResponseBody kerromme vastauksen sisältävän vastausrungon -- tällöin näkymää ei luoda erillisellä näkymäkomponentilla (esim JSP-renderöijällä), vaan sovelluskehys päättelee muodon. Spring renderöi oliot oletuksena JSON-muodossa. Aiemmin näkemämme REST-esimerkki JSON-muotoisilla vastauksilla näyttäisi seuraavalta.

@Controller
public class OlutController {

    @Autowired
    private OlutPalvelu olutPalvelu;

    @RequestMapping(method = RequestMethod.GET, value = "olut")
    @ResponseBody
    public List<Olut> listaaOluet(Model model) {
        return olutPalvelu.listaaOluet();
    }

    @RequestMapping(method = RequestMethod.GET, value = "olut/{olutId}")
    @ResponseBody
    public Olut naytaOlut(Model model, @PathVariable Integer olutId) {
        return olutPalvelu.annaOlut(olutId);
    }

    @RequestMapping(method = RequestMethod.POST, value = "olut")
    public String lisaaOlut(@ModelAttribute Olut olut) {
        olut = olutPalvelu.lisaaOlut(olut);
        return "redirect:/olut/" + olut.getId(); // luotu olut
    }

    @RequestMapping(method = RequestMethod.DELETE, value = "olut/{olutId}")
    public String poistaOlut(@PathVariable Integer olutId) {
        olutPalvelu.poistaOlut(olutId);
        return "redirect:/olut";
    }

    @RequestMapping(method = RequestMethod.PUT, value = "olut/{olutId}")
    public String muokkaaTaiLisaaOlut(@ModelAttribute Olut olut, @PathVariable Integer olutId) {
        olut = olutPalvelu.muokkaaTaiLisaaOlut(olutId, olut);
        return "redirect:/olut/" + olut.getId(); // luotu tai muokattu olut
    }
}

Muutimme vain näkymän palauttavien metodien toiminnallisuutta. Näkymään (JSP-sivulle) ohjauksen sijaan palautamme olion -- sovelluskehys hoitaa olion käännöksen JSON-dokumentiksi.

Myös toiseen suuntaan kääntäminen onnistuu. Voimme vastaanottaa JSON-dokumentteja kontrollerimetodeissa määrittelemällä hyväksyttävän tiedostoformaatin (@RequestMapping-annotaation parametri consumes), sekä rakentaa pyynnön rungosta olio. Esimerkiksi aiemmin määritelty PUT-komento voidaan muokata seuraavanlaiseksi.

    @RequestMapping(method = RequestMethod.PUT, value = "olut/{olutId}", consumes="application/json")
    public String muokkaaTaiLisaaOlut(@RequestBody Olut olut, @PathVariable Integer olutId) {
        olut = olutPalvelu.muokkaaTaiLisaaOlut(olutId, olut);
        return "redirect:/olut/" + olut.getId(); // luotu tai muokattu olut
    }

Ylläolevan muokkaaTaiLisaa-metodin toiminnallisuutta voi testata curl-komennon avulla seuraavasti (kenoviiva mahdollistaa komennon kirjoittamisen useammalle riville).

curl -X PUT -H "Content-Type: application/json; charset=utf-8" \
-d "{\"name\":\"Blithering Idiot\", \"id\":13}" \
http://palvelin-ja-sovellus/rest/olut/13

JSON Olutvarasto

Laajenna edellistä tehtävää tai toteuta Olutvarasto siten, että

Muu toiminnallisuus kuten edellä olevassa tehtävässä -- PUT ja POST-komentojen tulee myös toimia. Tehtävä olettaa että POST-pyynnöillä lähetettäville oluille luodaan uudet tunnukset jos vastaavaa olutta ei ole jo olemassa. Kannattaa käyttää vertailuun myös name-kenttää pelkän id:n lisäksi.

Kun olet valmis, lisää sovellus users-palvelimelle ja tarkistuta se osoitteessa http://t-avihavai.users.cs.helsinki.fi/tester3/ olevassa palvelussa..

Huom! Käytä toteutuksessasi valmista Olut-luokkaa.

Lisää myös Jackson JSON-riippuvuus Maven-konfiguraatioosi (tämä on joissain palvelimissa valmiiksi mukana).

        <dependency>
            <groupId>org.codehaus.jackson</groupId>
            <artifactId>jackson-jaxrs</artifactId>
            <version>1.9.3</version>
        </dependency>

XML

XML on rakenteinen dataformaatti, jota kontrolloidaan elementtien avulla. XML-dokumentilla on vain yksi juurielementti.

<olut>
  <id>10</id>
  <nimi>Hacker-Pschorr Hefe Weisse</nimi>
</olut>

Elementtejä voidaan sisällyttää toisiin elementteihin.

<oluet>
  <olut>
    <id>10</id>
    <nimi>Hacker-Pschorr Hefe Weisse</nimi>
  </olut>
  <olut>
    <id>11</id>
    <nimi>Buttface Amber Ale</nimi>
  </olut>
  <olut>
    <id>12</id>
    <nimi>Yellow Snow</nimi>
  </olut>
</oluet>

Kuten JSON-muotoon käännettäessä, myös XML-muotoon kääntäminen tapahtuu automaattisesti. Olioiden XML-muotoon kääntämiseksi tarvitaan JAXB (Java XML Binding) annotaatiot käännettävälle luokalle. Olut-luokan XML-muotoon kääntämiseksi tarvitsemme annotaatiot @XmlRootElement, joka kertoo dokumentin juurielementin, ja @XmlElement, joka kertoo elementin.

@XmlRootElement
public class Olut {
    private int id;
    private String name;

    public Olut() {
    }

    public int getId() {
        return id;
    }

    @XmlElement
    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    @XmlElement
    public void setName(String name) {
        this.name = name;
    }
}

Tämän lisäksi tarvitsemme erillisen luokan monikkoa varten -- emme voi tulostaa olutlistaa Javan List-rajapinnan avulla. Määritellään erillinen juurielementti OlutLista:

@XmlRootElement(name = "oluet")
public class OlutLista {

    private List<Olut> olut;

    public OlutLista() {
    }

    public void setOlut(List<Olut> olut) {
        this.olut = olut;
    }

    public List<Olut> getOlut() {
        return olut;
    }
}

GET-pyyntöjä vastaanottaviin kontrolliluokkiin ei tarvitse tehdä muutoksia. Jos haluamme ottaa vastaan XML-muotoista dataa, voimme määritellä @RequestMapping-annotaation consumes parametrille arvon application/xml.

    @RequestMapping(method = RequestMethod.PUT, value = "olut/{olutId}", consumes="application/xml")
    public String muokkaaTaiLisaaOlut(@RequestBody Olut olut, @PathVariable Integer olutId) {
        olut = olutPalvelu.muokkaaTaiLisaaOlut(olutId, olut);
        return "redirect:/olut/" + olut.getId(); // luotu tai muokattu olut
    }

Toiminnallisuutta voi testata taas curl-komennon avulla. Esimerkiksi seuraava lähettäisi He'brew: The Chosen Beer nimisen oluen palveluun.

curl -X PUT -H "Content-Type: application/xml; charset=utf-8" \
-d "<olut><name>He'brew: The Chosen Beer</name><id>18</id></olut>" \
http://palvelin-ja-sovellus/olut/18

Käytämme tällä kurssilla JSON-muotoista dataa.

Merkistöongelmista

Käyttöjärjestelmät ja selaimet haluavat usein ajaa kaikkia käyttämään niiden määrittelemää merkistökoodausta. Haluamme oikeasti käyttää UTF-8 -merkistöä tai vastaavaa. Javan HTTP-implementaatio tarjoaa filtteritoiminnallisuuden, jonka avulla jokainen pyyntö pystytään käsittelemään ennen kuin se päätyy sovellukselle. Vastaavasti filttereillä pystytään vaikuttamaan myös vastauksen sisältöön. Spring-sovelluskehys tarjoaa filtterin, joka muuttaa kaikki pyynnöt UTF-8 merkkisiksi. Filtterin saa päälle lisäämällä sen konfiguraation web.xml-tiedoston alkuun.

    <!-- 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 -- kunnes toisin sanotaan -- toteuttamiemme Spring-projektien web.xml-tiedosto on siis seuraavanlainen (display-name -kenttää saa toki vaihtaa).

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee" 
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee           
                http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
                    
    <display-name>spring-sovellus</display-name>
    
    <!-- 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>
    
    <!-- Front controller -->
    <servlet>
        <servlet-name>spring-front-controller</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>/WEB-INF/spring-context.xml</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>
    
    <servlet-mapping>
        <servlet-name>spring-front-controller</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>
    
    <session-config>
        <session-timeout>
            30
        </session-timeout>
    </session-config>
    
    <welcome-file-list>
        <welcome-file>index.jsp</welcome-file>
    </welcome-file-list>
</web-app>

Tietovarastot

Tähänastiset sovelluksemme ovat hukanneet käyttämänsä tiedot sovelluksen sammussa. Tämä johtuu siitä että emme ole varastoineet tietoa. Web-sovelluskehityksessä yleisin tapa tiedon varastointiin on relaatiotietokannan käyttäminen olemassaolevan rajapinnan kautta. Kuten sovelluslogiikkaa kehittäessä, haluamme kapseloida myös tietovarastokerroksen.

Lähes kaikki tietovarastot tarjoavat ns. CRUD-toiminnallisuuden (create, read, update, delete), jonka lisäksi käyttäjä voi luoda sovelluskohtaisia apuvälineitä.

DAO Pattern

DAO (Data Access Object) pattern on suunnittelumalli, jossa varastointimekanismi kapseloidaan sovelluslogiikalle näkymättömäksi. Tietovarastokerrosta käytetään DAO-rajapinnan yli (@Repository annotaatio Spring-sovelluskehyksessä) kuten normaaleja palveluita. Lopullinen tiedon tallennuspaikka riippuu toteutuksesta -- rajapinnan käyttäjä ei siitä tarvitse välittää. Toteutetaan aiemmin esitellylle olutvarastolle oma tietovarastorajapinta ja siihen liittyvä toteutus.

public interface OlutDao {
    void persist(Olut olut);
    void remove(Olut olut);
    Olut findById(int id);
}

Olemme kiinnostuneita aluksi kolmesta toiminnallisuudesta. Lisääminen (persist), poistaminen (remove) ja avaimen perusteella etsiminen (findById) -- perinteisen CRUD-nimeämisen sijaan web-sovelluksissa käytetään yleensä yllä esitettyä nimeämistyyliä.

Luodaan ensimmäinen toteutus, jota voimme käyttää oluiden tallentamiseen. Toteutuksemme kapseloi yksinkertaisen listarakenteen.

@Repository
public class ListaOlutDao implements OlutDao {

    private Map<Integer, Olut> olutMap = new TreeMap<Integer, Olut>();

    @Override
    public void persist(Olut olut) {
        olutMap.put(olut.getId(), olut);
    }

    @Override
    public void remove(Olut olut) {
        olutMap.remove(olut.getId());
    }

    @Override
    public Olut findById(int id) {
        return olutMap.get(id);
    }
}

Ylläolevan toteutuksen voi lisätä Spring-sovelluskehyksen palveluluokkiin @Autowired-annotaation avulla.

@Service
public class OlutPalvelu {

    @Autowired
    private OlutDao olutDao;

    ...

Uutta -- samaa rajapintaa käyttävää toteutusta -- kehittäessämme meidän tulee määritellä palvelulle yksilöivä nimi automaattista hakemista varten. Esimerkiksi tiedostoja käyttävä luokka TiedostoOlutDao, jonka toteutukset on piilotettu, voidaan yksilöidä @Repository-annotaatioon annettavalla arvolla.

@Repository(value="tiedosto")
public class TiedostoOlutdao implements OlutDao {
    ...

    @Override
    public void persist(Olut olut) {
        ...
    }

    @Override
    public void remove(Olut olut) {
        ...
    }

    @Override
    public Olut findById(int id) {
        ...
    }   
}

Kun saman rajapinnan toteuttaa useampi palvelu, tulee jokaisella palvelulla olla tunnus. Lisätään myös listalle oma tunnus.

@Repository(value="lista")
public class ListaOlutDao implements OlutDao {
    ...

Nyt voimme käyttää @Autowired-annotaation lisäksi @Qualifier-annotaatiota, jolla kerrotaan mikä toteutus halutaan käyttöön. Alla käytössä ListaOlutDao.

@Service
public class OlutPalvelu {

    @Autowired
    @Qualifier("lista")
    private OlutDao olutDao;

    ...

Dao-rajapintojen käyttöä helpotetaan yleensä perinnän avulla. Alla on esitetty geneeristä rajapintaa käyttävä Huoneen hallinta.

public interface DAO<T> {
    public void create(T instance);
    public T read(int id);
    public void delete(T instance);
    public T update(T instance);
    public List<T> list();
}

public abstract class JPADao<T> implements DAO<T> {

    @PersistenceContext
    EntityManager entityManager;

    private Class clazz;
    
    public JPADao(Class clazz) {
        this.clazz = clazz;
    }   
    
    @Override
    public void create(T instance) {
        entityManager.merge(instance);
    }

    @Override
    public T read(int id) {
        return (T)entityManager.find(clazz, id);
    }

    @Override
    public void delete(T instance) {
        entityManager.remove(instance);
    }

    @Override
    public T update(T instance) {
        return entityManager.merge(instance);
    }
    
    @Override
    public List<T> list() {
        // JPA:ssa on myös ohjelmallinen API kyselyjen raketamiseen
        CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
        CriteriaQuery query = criteriaBuilder.createQuery(clazz);
        return entityManager.createQuery(query).getResultList();
    }
}
@Repository
public class JPAHuoneDao extends JPADao<Huone> implements HuoneDao  {

    public JPAHuoneDao() {
        super(Huone.class);
    }
}

Relaatiotietokannat ja ORM

Relaatiotietokantojen käsittelyyn on kehitetty joukko 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.

ORM-työkalut (Object Relational Mapping) tarjoavat toiminnallisuutta tietokantataulujen luomiseen suoraan luokkamäärittelyjen perusteella. Työkalut hallinnoivat luokkien välisiä viittauksia ja ylläpitävät mm. tietokannan eheyttä. Käyttäjän vastuulle jää sovellukselle tarpeellisten kyselyiden toteuttaminen niiltä osin kun niitä ei tarjota valmiiksi.

Entity

Tietokantaan tallennettavat luokat tulee annotoida @Entity-annotaatiolla. Annotaation @Entity lisäksi luokalle tulee määritellä avainkenttä @Id-annotaation avulla. Alla esimerkki luokasta Esine.

import java.io.Serializable;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Entity
public class Esine implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.TABLE)
    private int id;
    private String nimi;
    private Double paino;

    public Esine() {
    }

    public int getId() {
        return id;
    }

    public String getNimi() {
        return nimi;
    }

    public void setNimi(String nimi) {
        this.nimi = nimi;
    }

    public Double getPaino() {
        return paino;
    }

    public void setPaino(Double paino) {
        this.paino = paino;
    }
}

JPA voi luoda tietokantataulut automaattisesti annettujen määrittelyjen perusteella. Toisaalta, on myös mahdollista määritellä luokat tietyn tietokantaskeeman mukaisiksi. Annotaatioiden @Entity ja @Column avulla voimme antaa luokan attribuuteille -- tietokantataulun sarakkeille -- tarkempia määreitä.

...

@Entity(name="ESINE")
public class Esine implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.TABLE)
    @Column(name="ID")
    private int id;
    @Column(name="NIMI", nullable=false)
    private String nimi;
    @Column(name="PAINO")
    private Double paino;
   
    ...

Entiteettien käyttö tapahtuu EntityManager-rajapinnan avulla.

Entiteetteihin viittaaminen

Toisiin entiteetteihin viittaaminen tapahtuu kuten normaalistikin Javalla ohjelmoidessa. Osallistumisrajoitteet -- yksi moneen (one to many), moni yhteen (many to one), moni moneen (many to many) lisätään annotaatioiden avulla. Luodaan esimerkiksi luokka Henkilo, joka voi omistaa joukon esineitä. Kukin esine on vain yhden henkilön omistama -- suhde siis yksi moneen -- annotaatio @OneToMany.

@Entity
public class Henkilo implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.TABLE)
    private int id;
    private String nimi;
    @OneToMany
    private List<Esine> esineet;

    ...

Yllä olevaa esimerkkiä käytettäessä luokalle Esine luodaan tietokantatauluun automaattisesti sarake, johon tallennetaan omistavan henkilön id.

Moni-moneen yhteys tapahtuu tietokantatauluja suunniteltaessa liitostaulun avulla. JPA:ssa moni-moneen yhteydet määritellään annotaatiolla @ManyToMany. Tällöin yhteys tulee merkitä kummallekin puolelle. Jos henkilö voi omistaa useita esineitä, ja esineellä voi olla useita omistajia, toteutus on seuraavanlainen.

@Entity
public class Henkilo implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.TABLE)
    private int id;
    private String nimi;
    @ManyToMany
    private List<Esine> esineet;
    ...
@Entity
public class Esine implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.TABLE)
    private int id;
    private String nimi;
    private Double paino;
    @ManyToMany(mappedBy = "esineet")
    private List<Henkilo> henkilot;

Yllä oleva määritelmä luo liitostaulun Esine- ja Henkilötaulun välille. Esine-luokassa olevassa @ManyToMany-annotaatiossa oleva parametri mappedBy = "esineet" kertoo että Esine-luokan henkilot-kenttä kytketään luokan Henkilo listaan esineet.

EntityManager

EntityManager hallinnoi entiteettejä ja niiden tallennusta tietokantaan. Sovelluskehys -- tai sovelluskehittäjä -- luo EntityManagerin tarpeen vaatiessa. EntityManager tarjoaa joukon palveluita, oleellisimmat meidän kannalta ovat lisääminen (komennot persist ja merge), poistaminen (komento remove) ja hakeminen (komento find). EntityManager injektoidaan sovellukselle @PersistenceContext-annotaation avulla. Olettaen että aiemmin nähty Olut-luokka on annotoitu @Entity-annotaatiolla, voisimme toteuttaa OlutDao-rajapinnan seuraavasti.

@Repository("db")
public class DbOlutDao implements OlutDao {
    
    @PersistenceContext
    private EntityManager entityManager;

    @Override
    public void persist(Olut olut) {
        entityManager.persist(olut);
    }

    @Override
    public void remove(Olut olut) {
        entityManager.remove(olut);
    }

    @Override
    public Olut findById(int id) {
        return entityManager.find(Olut.class, id);
    }    
}

Yllä oletamme että transaktioiden hallinta tapahtuu toisaalla -- tästä lisää myöhemmin Springin yhteydessä.

EntityManager-olioiden konfigurointi tapahtuu persistence.xml-tiedoston avulla. Persistence.xml listaa hallinnoitavat entiteetit, sekä määrittelee käytetylle JPA-rajapinnan toteutukselle tarpeellisia parametreja.

persistence.xml

JPA-konfiguraatio määritellään tiedostossa persistence.xml, joka listaa hallinnoitavat entiteetit sekä määrittelee käytetylle JPA-rajapinnan toteutukselle tarpeellisia parametreja. Alla esimerkkikonfiguraatio, jossa käytössä on EclipseLink.

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.0" 
	     xmlns="http://java.sun.com/xml/ns/persistence" 
	     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
	     xsi:schemaLocation="http://java.sun.com/xml/ns/persistence 
				 http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd">
  <persistence-unit name="persistenceUnitEclipseLink" transaction-type="RESOURCE_LOCAL">
    <-- käytetty JPA-toteutus //-->
    <provider>org.eclipse.persistence.jpa.PersistenceProvider</provider>

    <-- käytetylle JPA-toteutukselle annettavat parametrit //-->
    <properties>
      <property name="showSql" value="true"/>
      <property name="eclipselink.ddl-generation" value="drop-and-create-tables"/>
      <property name="eclipselink.ddl-generation.output-mode" value="database"/>
      <property name="eclipselink.weaving" value="false"/>
      <property name="eclipselink.logging.level" value="FINE"/>
    </properties>

    <-- hallinnoitavat entiteettiluokat //-->
    <class>wad.varasto.domain.Esine</class>
  </persistence-unit>
</persistence>

JPQL

JPQL (Java Persistence Query Language on kyselykieli, jonka avulla @Entity-annotaatioilla merkittyjen luokkien instansseja voidaan hakea tietokannasta. Kyselyt ovat SQL-kyselyiden kaltaisia. JPQL ei tue kaikkia SQL-kielen ominaisuuksia -- EntityManager-ilmentymän avulla on mahdollista kirjoittaa myös puhtaita SQL-kyselyitä. Yksinkertainen kaikki Esine-oliot listaava kysely on seuraavanlainen.

SELECT e FROM Esine e

EntityManager-luokan ilmentymän avulla voimme hakea listan esineitä vastaavasti.

String kysely = "SELECT e FROM Esine e";
Query q = entityManager.createQuery(kysely);
List<Esine> esineet = q.getResultList();

Ehtojen lisääminen tapahtuu parametrien avulla

String kysely = "SELECT e FROM Esine e WHERE e.nimi = :nimi";
Query q = entityManager.createQuery(kysely);
q.setParameter("nimi", "vaasi");
List<Esine> esineet = q.getResultList();

Transaktioiden hallinta

Lähes kaikki relaatiotietokannat toteuttavat ACID-sääntöjä noudattavan transaktiomallin. Kukin palvelutason toiminto -- esimerkiksi poistaminen -- suoritetaan omassa transaktiossaan. Web-sovelluksia kehitettäessä vastuu transaktioidenhallinnasta voidaan antaa palvelimelle (globaali transaktionhallinta), tai hallinnoida transaktioita itse tai sovelluskehyksen avulla (lokaali transaktionhallinta). Globaali transaktionhallinta toteutetaan yleensä JTA (Java Transaction API)-rajapinnan avulla. Käytämme kurssilla lokaalia transaktionhallintaa, sillä mm. Tomcat ei tue JTA-rajapintaa suoraan.

Springissä transaktioiden määrittely tapahtuu @Transactional-annotaation avulla, jonka avulla metodi voidaan määritellä transaktion sisällä suoritettavaksi. Annotaatiolle voidaan määritellä parametrien avulla ylimääräistä toiminnallisuutta -- esimerkiksi kyselyn tyyppi. Esimerkiksi vain lukemista toteuttavat metodit kannattaa annotoida @Transactional(readOnly=true), jolloin sovelluskehys pystyy optimoimaan kyselyjen suoritusta. Transaktiot määritellään yleensä palvelutasolla, esimerkiksi Esineen poistaminen.

    @Override
    @Transactional
    public void poista(int esineId) {
        Esine esine = varastoDao.read(esineId);
        if (esine != null) {
            varastoDao.remove(esine);
        }
    }

Spring ja ORM

Seuraavaksi nidotaan edeltävä kappale yhteen. Alla listatut konfiguraatiot voi ladata myös GitHubista osoitteesta https://github.com/avihavai/wad-2012/tree/master/dbvarasto. Projektin saa ZIP-tiedostona klikkaamalla sivun vasemmassa ylälaidassa olevaa ZIP-linkkiä.

Konfiguraatio

spring-context.xml

Alla spring-context.xml konfiguraatio. Määrittelemme käyttöömme muistiin ladattavan tietokannan (HSQLDB). EntityManager-luokat saadaan EntityManagerFactory-oliolta, jonka toteuttajana on EclipseLink. Tiedosto persistence.xml löytyy classpathista. Lisäksi, haluamme että voimme hallinnoida transaktioita annotaatioiden avulla.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xmlns:jdbc="http://www.springframework.org/schema/jdbc"
       xsi:schemaLocation="
        http://www.springframework.org/schema/mvc 
          http://www.springframework.org/schema/mvc/spring-mvc-3.0.xsd
        http://www.springframework.org/schema/beans 
          http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
        http://www.springframework.org/schema/context 
          http://www.springframework.org/schema/context/spring-context-3.0.xsd
        http://www.springframework.org/schema/tx 
          http://www.springframework.org/schema/tx/spring-tx-3.0.xsd
        http://www.springframework.org/schema/jdbc 
           http://www.springframework.org/schema/jdbc/spring-jdbc-3.0.xsd">

    <!-- DispatcherServletin (front-controllerin) konfiguraatio, jolla määritellään pyynnön kulku. -->
 
    <!-- Sovelluksemme lähdekooditiedostot sijaitsevat pakkauksessa wad tai sen alipakkauksissa-->
    <context:component-scan base-package="wad" />

    <!-- Käytetään Spring MVC:tä annotaatioiden avulla -->
    <mvc:annotation-driven /> 
       
    <!-- Mahdollistetaan konfigurointi annotaatioilla -->
    <context:annotation-config />
    
    <!-- Käytetään muistiin ladattavaa tietokantaa -->
    <jdbc:embedded-database id="dataSource" type="HSQL"/>

    <!-- Käytetään EclipseLinkkiä JPA-toteutuksena, määritellään myös persistence.xml-tiedoston sijainti -->
    <bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
        <!-- älä muuta persistence.xml -tiedoston sijaintia  -->
        <property name="persistenceXmlLocation" value="classpath:persistence.xml" />
        <property name="persistenceUnitName" value="persistenceUnitEclipseLink" /> 
        <property name="dataSource" ref="dataSource"/>
        <property name="jpaVendorAdapter">
            <bean class="org.springframework.orm.jpa.vendor.EclipseLinkJpaVendorAdapter" />
        </property>
        <property name="loadTimeWeaver"> 
            <bean class="org.springframework.instrument.classloading.InstrumentationLoadTimeWeaver" /> 
        </property>
    </bean>
    
    <!-- Hallinnoidaan transaktioita automaattisesti -->
    <bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
        <property name="entityManagerFactory" ref="entityManagerFactory"/>
    </bean>
    
    <tx:annotation-driven transaction-manager="transactionManager" />
    
    <!-- Käytetään geneerisiä poikkeuksia -->
    <bean class="org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor"/>
    <bean class="org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor"/>
    
    <!-- Ohjataan näkymät JSP-sivuille -->
    <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="prefix" value="/WEB-INF/view/" />
        <property name="suffix" value=".jsp" />
    </bean>
</beans>

pom.xml

Lisätään aiempaan Project Object Model-konfiguraatioomme riippuvuuksia tietokantayhteyden hallintaan. Näiden lisäksi helpotimme versiointia lisäämällä properties-elementin, sekä lisäsimme sijainteja riippuvuuksien noutamiseen.

<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
            http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>wad</groupId>
    <artifactId>dbvarasto</artifactId>
    <packaging>war</packaging>
    <version>1.0-SNAPSHOT</version>

    <name>dbvarasto</name>
    <url>http://maven.apache.org</url>
    
    <properties>
        <spring-version>3.1.0.RELEASE</spring-version>
        <eclipselink-version>2.3.2</eclipselink-version>
        <eclipselink-jpa-version>2.0.0</eclipselink-jpa-version>
        <hsqldb-version>1.8.0.10</hsqldb-version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <!-- spring -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webmvc</artifactId>
            <version>${spring-version}</version>
        </dependency>
        
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>${spring-version}</version>
            <exclusions>
                <!-- käytetään simple logging facadea commons logging-kirjaston sijaan -->
                <exclusion>
                    <groupId>commons-logging</groupId>
                    <artifactId>commons-logging</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <!-- servlet ja jsp api -->
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>servlet-api</artifactId>
            <version>2.5</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>javax.servlet.jsp</groupId>
            <artifactId>jsp-api</artifactId>
            <version>2.1</version>
            <scope>provided</scope>
        </dependency>

        <!-- jstl -->
        <dependency>
            <groupId>jstl</groupId>
            <artifactId>jstl</artifactId>
            <version>1.2</version>
        </dependency>
        
        <!-- cglib @configuration-konffeille -->
        <dependency>
            <groupId>cglib</groupId>
            <artifactId>cglib</artifactId>
            <version>2.2.2</version>
        </dependency> 
        
        <!-- db -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-orm</artifactId>
            <version>${spring-version}</version>
        </dependency>
        
        <dependency>
            <groupId>org.eclipse.persistence</groupId>
            <artifactId>eclipselink</artifactId>
            <version>${eclipselink-version}</version>
        </dependency>
        <dependency> 
            <groupId>org.eclipse.persistence</groupId>
            <artifactId>javax.persistence</artifactId>
            <version>${eclipselink-jpa-version}</version>
        </dependency>
               
            
        <dependency>
            <groupId>org.hsqldb</groupId>
            <artifactId>hsqldb</artifactId>
            <version>${hsqldb-version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
            <version>${spring-version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-instrument</artifactId>
            <version>${spring-version}</version>
        </dependency>
            
        <!-- loggauskirjastot -->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>1.6.1</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>jcl-over-slf4j</artifactId>
            <version>1.6.1</version>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-log4j12</artifactId>
            <version>1.6.1</version>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.16</version>
            <scope>runtime</scope>
        </dependency>
    </dependencies>
    
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>2.3.2</version>
                <configuration>
                    <source>1.6</source>
                    <target>1.6</target>
                </configuration>
            </plugin>
        </plugins>
    </build>
    
    <repositories>
        <repository>
            <id>java.net repo</id>
            <url>http://download.java.net/maven/2/</url>
            <layout>default</layout>
        </repository>
        
        <repository>
            <id>jboss repo</id>
            <url>http://repository.jboss.com/maven2</url>
        </repository>
        
        <repository>
            <id>eclipselink repo</id>
            <url>http://download.eclipse.org/rt/eclipselink/maven.repo</url>            
        </repository>
    </repositories>
</project>

persistence.xml

Tiedosto persistence.xml sisältää tarkemmat määrittelyt tallennettaviin tiedostoihin liittyen. Alla olevassa konfiguraatiossa hallinnoimme vain luokkaa wad.varasto.domain.Esine.

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.0" xmlns="http://java.sun.com/xml/ns/persistence" 
                              xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
                              xsi:schemaLocation="http://java.sun.com/xml/ns/persistence                 
                                  http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd">
  <persistence-unit name="persistenceUnitEclipseLink" transaction-type="RESOURCE_LOCAL">
    <provider>org.eclipse.persistence.jpa.PersistenceProvider</provider>
    <class>wad.varasto.domain.Esine</class>
    <exclude-unlisted-classes>true</exclude-unlisted-classes>
    <properties>
      <property name="showSql" value="true"/>
      <property name="eclipselink.ddl-generation" value="drop-and-create-tables"/>
      <property name="eclipselink.ddl-generation.output-mode" value="database"/>
      <property name="eclipselink.weaving" value="false"/>
      <property name="eclipselink.logging.level" value="FINEST"/>
    </properties>
  </persistence-unit>
</persistence>

Tiedoston persistence.xml voi lisätä kansioon Other Sources, jonka saa näkyviin lisäämällä tiedostoja projektin fyysiseen kansioon src/main/resources.

Joudut todennäköisesti hakemaan lisätietoa esim. verkosta, irkistä ja kavereilta näiden tehtävien tekemiseen. Tämä on tarkoituskin. Linkeistä http://docs.oracle.com/javaee/5/tutorial/doc/bnbpz.html, http://www.vogella.de/articles/JavaPersistenceAPI/article.html ja http://www.devx.com/Java/Article/33650/0 on mahdollisesti hyötyä. Älä kuitenkaan käytä niiden konfiguraatiovinkkejä..

Tehtävissä ei oteta kantaa käyttöliittymän ulkomuotoon -- toteuta käyttöliittymästä itsellesi mielekäs.

Huonehallinta

Muokkaa GitHubissa osoitteessa https://github.com/avihavai/wad-2012/tree/master/dbvarasto olevasta projektista huonehallintasovellus, jossa on toiminnallisuus huoneiden lisäämiseen ja poistamiseen. Huoneilla on tietoina tunnus, kerros ja kapasiteetti. Hallinnoinnin tulee tapahtua JSP-sivun avulla.

Muista muokata persistence.xml-tiedostoa siten, että se sisältää luomasi entiteetin.

Tehtävä näytetään koetilaisuudessa (jos tehty).

Henkilöstörekisteri

Tee tämä tehtävä edellisen tehtävän jatkoksi. Lisää toimintoihin henkilöstörekisterin ylläpito, jossa on toiminnallisuus henkilöiden lisäämiseen ja poistamiseen. Henkilöillä on tietoina nimi, osoite ja puhelinnumero. Hallinnoinnin tulee tapahtua JSP-sivun avulla.

Muista muokata persistence.xml-tiedostoa siten, että se sisältää luomasi entiteetin.

Tehtävä näytetään koetilaisuudessa (jos tehty).

Henkilöt huoneisiin

Yhdistä ylläolevat sovellukset uudeksi sovellukseksi siten, että henkilöitä voi lisätä huoneisiin. Jokaisella henkilöllä on vain yksi huone, mutta huoneessa voi olla useampi henkilö. Huoneen kapasiteetti määrää maksimimäärän henkilöitä mitä huoneeseen voi laittaa. Huom! Annotaatiosta @OneToMany on tässä hyötyä. Hallinnoinnin tulee tapahtua JSP-sivun avulla.

Tehtävä näytetään koetilaisuudessa (jos tehty).

Elokuvat ja Genret

Toteuta sovellus johon tallennetaan tietoa elokuvista ja niihin liittyvistä genreistä. Määrittele Elokuvalle oma taulu, elokuvasta tulee tallentaa ainakin nimi, pituus ja valmistusvuosi. Määrittele myös oma taulu Genrelle. Genrestä tulee tallentaa vain tyyppi. Elokuvan ja Genren välillä kannattaa olla ManyToMany-mäppäys.

Sovelluksella tulee olla toiminnallisuus elokuvien ja genrejen lisäämiseen, sekä elokuvien listaus genren perusteella.

Tehtävä näytetään koetilaisuudessa (jos tehty).

Pilvipalvelut

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

To the Cloud!

Luo käyttäjätunnus Heroku-palveluun osoitteessa https://api.heroku.com/signup ja seuraa osoitteessa http://devcenter.heroku.com/articles/java olevia ohjeita. Käytä herokun tarjoamaa valmista sovelluspohjaa.

Muokkaa sovellusta siten, että viestin "Hello from Java!" sijaan sovellus tulostaa viestin "Hello from Helsinki!".

Kun olet valmis tarkistuta sovellus osoitteessa http://t-avihavai.users.cs.helsinki.fi/tester3/ olevassa palvelussa.

Käytetyt tunnit, viikko 3

Kirjaa osoitteessa http://t-avihavai.users.cs.helsinki.fi/tunnit3 olevaan palveluun kolmannen viikon materiaalin ja tehtävien parissa käyttämäsi aika.

Validointi

Olemme tähän mennessä jättäneet web-sovelluksille syötetyn datan validoinnin huomioimatta. Käytännössä kuka tahansa pystyi (ja pystyy!) lisäämään esimerkiksi chat-sovelluksiimme koodia, jota ajetaan käyttäjän koneella. Koodi voi olla yksinkertaista ja harmitonta, esimerkiksi seuraavan lähdekoodipätkän lisääminen chattiin vain kertoo osan mahdollisuuksista.

<SCRIPT SRC=http://www.cs.helsinki.fi/u/avihavai/trololo.js></SCRIPT>

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 tallentamiseen domain-objekteja, joihin olemme määritelleet 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 verkkosovellusten käyttämän yksinkertaisen datan validoinnille: Bean Validation API, Javadoc. Bean validation API on, kuten muutkin JSR-apit, vain rajapinta, jolla on useampi toteuttaja. Valitsemme käyttöömme Hibernaten Validator-toteutuksen, johon liittyvät riippuvuudet saamme mavenin avulla kätevästi.

        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-validator</artifactId>
            <version>4.1.0.Final</version>
            <classifier/>
            <exclusions>
                <exclusion>
                    <groupId>javax.xml.bind</groupId>
                    <artifactId>jaxb-api</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>com.sun.xml.bind</groupId>
                    <artifactId>jaxb-impl</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>javax.validation</groupId>
            <artifactId>validation-api</artifactId>
            <version>1.0.0.GA</version>
            <classifier/>
        </dependency>

Yllä poistamme javax.xml.bind.jaxb-api ja com.sun.xml.bind.jaxb-impl kirjastot lisäämästämme Hibernate-riippuvuudesta. Ne on jo ladattu aiemmin käyttämiemme Spring-projektien mukana.

Muuttujien validointi

Muuttujien validointia varten tulee määritellä rajoitteet (constraints) tarkistettaville muuttujille. Validointirajapinnan tarjoamat rajoitteet löytyvät javax.validation.constraints-pakkauksen dokumentaatiosta.

Luodaan luokka Henkilo. Henkilöllä on henkilötunnus ja nimi. Sovitaan että henkilötunnus ei saa koskaan olla null, ja sen tulee olla tasan 11 merkkiä pitkä. Etunimi saa koostua vain yksittäisestä sanasta (säännölliset lausekkeet ja lama, jea!), ja sen maksimipituus on 10.

Henkilötunnusta varten pakkauksesta javax.validation.constraints löytyy annotaatiot @NotNull ja @Size. Annotaatioihin voidaan määritellä myös virheviestit message-attribuutin avulla.

import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;

public class Henkilo {
  @NotNull(message="Henkilötunnus ei saa olla tyhjä")
  @Size(min=11, max=11, message="Henkilötunnuksessa tulee olla tasan 11 merkkiä")
  private String hetu;

  // getterit ja setterit
}

Etunimeä varten on olemassa annotaatio @Pattern, jolle voimme määritellä säännöllisiä lausekkeita. Javan Pattern-luokan dokumentaatiota lukemalla huomaamme että säännöllinen lauseke \\w+ sopii meille hyvin.

import javax.validation.constraints.NotNull;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;

public class Henkilo {
  @NotNull(message="Henkilötunnus ei saa olla tyhjä")
  @Size(min=11, max=11, message="Henkilötunnuksessa tulee olla tasan 11 merkkiä")
  private String hetu;

  @Pattern(regexp="\\w+", 
          message="Etunimen tulee koostua tasan yhdestä sanasta, ja se ei saa sisältää erikoismerkkejä.")
  @Size(min = 1, max = 10, message="Etunimen tulee olla vähintään yhden merkin pituinen, korkeintaan 10 merkkiä.")
  private String etunimi;

  // getterit ja setterit
}

Validointi konfiguroidaan annotaatioilla, kuten iso osa muistakin sovelluksiemme osista.

Kontrollerit, jotka vihaavat validoimattomia kenttiä

Validoinnin lisääminen kontrollerille on helppoa. Lisäämällä annotaation @Valid (javax.validation.Valid;) kontrollerille lähetettävän luokan alkuun, voimme määritellä luokan olevan validoitava.

    @RequestMapping(value = "/henkilo", method = RequestMethod.POST)
    public String postHenkilo(@Valid @ModelAttribute Henkilo henkilo) {
        // .. toteutus
   }

Validointi on aktivoitu. Validointivirheet eivät kuitenkaan ole kovin kaunista luettavaa. Tällä hetkellä esimerkiksi virheellisen etunimen kohdalla saamme statuskoodin 500, sekä hieman kaoottisen ilmoituksen.

org.springframework.web.util.NestedServletException: Request processing failed; 
  nested exception is org.springframework.validation.BindException: 
  org.springframework.validation.BeanPropertyBindingResult: 1 errors
Field error in object 'henkilo' on field 'etunimi': rejected value [<]; 
  codes [Pattern.henkilo.etunimi,Pattern.etunimi,Pattern.java.lang.String,Pattern]; 
  arguments [org.springframework.context.support.DefaultMessageSourceResolvable: 
  codes [henkilo.etunimi,etunimi]; arguments []; 
    default message [etunimi],[Ljavax.validation.constraints.Pattern$Flag;@6d8611d5,\w+]; 
    default message [Etunimen tulee koostua tasan yhdestä sanasta, ja se ei saa sisältää erikoismerkkejä.]

Helpotetaan elämäämme hieman kytkemällä tulokset lomakkeisiin.

Springin lomakkeet ja BindingResult

Validoidessamme lomakkeita Springin avulla, käytämme BindingResult-luokkaa, johon virheet lisätään sekä Springin lomakkeita, joilla virheet voi näyttää helposti.

BindingResult

Luokka BindingResult tallentaa pyyntöjen olioihin mäppäämisessä tapahtuvat virheet itseensä. Kutakin pyyntöä kohden luodaan uusi BindingResult. Voimme pyytää Springiä lisäämään BindingResult-olion kontrolleriimme aivan kuten muitakin olioita. Seuraavassa esimerkki kontrollerista, jossa mäppäyksen tulos lisätään BindingResult-olioon.

    @RequestMapping(value = "/henkilo", method = RequestMethod.POST)
    public String postHenkilo(@Valid @ModelAttribute Henkilo henkilo, BindingResult result) {
        // .. toteutus
   }

Ylläolevassa esimerkissä kaikki virheet mitä validoinnissa huomataan tallennetaan suoraan BindingResult-olioon. Oliolla on metodi hasErrors, jonka perusteella voimme päättää jatketaanko pyynnön prosessointia vai ei. Yleinen muoto lomakedataa tallentaville kontrollereille on seuraavanlainen:

    @RequestMapping(value = "/henkilo", method = RequestMethod.POST)
    public String postHenkilo(@Valid @ModelAttribute("henkilo") Henkilo henkilo, BindingResult result) {
        if(result.hasErrors()) {
            return "henkilo";
        }

        // .. toteutus
   }

Yllä oletetaan että lomake lähetettiin sivulta "henkilo". Eli jos näemme virheen validoinnissa, palaamme takaisin sivulle. Annotaation @ModelAttribute outo parametri command selvenee meille kohta. Tutkitaan seuraavaksi hieman Springin lomakkeita. BindingResult-olio asetetaan aina heti ModelAttributen jälkeen -- se kertoo juuri sitä edeltävän olion luomisen onnistumisesta.

Lomakkeet

Spring tarjoaa käyttöömme taglibin lomakkeiden luomiseen ja hallinnointiin. Springin lomaketaglibin saa käyttöön komennolla:

<%@ taglib uri="http://www.springframework.org/tags/form" prefix="form"%>

Tämä tarkoittaa että voimme käyttää springin lomakkeita form:-etuliitteellä. Lomakkeet määritellään kuten normaalit HTML-lomakkeet, mutta muutamalla lisällä. Lomakkeen attribuutti commandName kertoo mihin lomakkeen kentät tulee pyrkiä liittämään. Sitä käytetään yhdessä kontrolleriluokan ModelAttribute-annotaation kanssa. Lomakkeen kentät määritellään polkujen path avulla, jotka kertovat ModelAttribute-annotaatiolla merkityn olion kentät. Ehkä oleellisin on kuitenkin tägi <form:errors path="..." />, jonka avulla saamme kenttiin liittyvät virheet esille.

<%@ taglib uri="http://www.springframework.org/tags/form" prefix="form"%>
<%@page contentType="text/html" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <title>Uusi henkilö</title>
    </head>
    <body>
        <h1>Uusi</h1>
        <form:form commandName="henkilo" action="${pageContext.request.contextPath}/henkilo" method="POST">
            <form:input path="hetu" /><form:errors path="hetu" /><br/>
            <form:input path="etunimi" /><form:errors path="etunimi" /><br/>
            <input type="submit">
        </form:form>
    </body>
</html>

Yllä on määritelty lomake, joka lähettää lomakkeen tiedot osoitteessa <sovellus>/henkilo olevalle kontrollerille. Kontrollerilla tulee olla määritelty ModelAttribute("henkilo")-annotaatio, jolla määritellään lomakkeen vastaanottaminen. Annotaation @ModelAttribute("henkilo") jälkeen tulee luokka, johon pyynnön data lähetetään. Lomakkeessa

Koska pyrimme validoimaan lähetettyä dataa, liitämme vielä @Valid-annotaation ennen annotaatiota @ModelAttribute("henkilo").

Ylläolevaa lomaketta kuunteleva kontrolleri olisi esimerkiksi seuraavanlainen -- olettaen että lomake olisi sivulla henkilo.jsp

    @RequestMapping(value = "/henkilo", method = RequestMethod.POST)
    public String postHenkilo(@Valid @ModelAttribute("henkilo") Henkilo henkilo, BindingResult result) {
        if(result.hasErrors()) {
            return "henkilo";
        }

        // .. tallennus
   }

Jos lomakkeella lähetetyissä kentissä on virheitä, virheet tallentuvat BindingResult-olioon. Tarkistamme kontrollerimetodissa ensin virheiden olemassaolon -- jos virheitä on, palataan takaisin lomakkeeseen. Huomaa että virheet ovat pyyntökohtaisia, ja esimerkiksi kutsu "redirect:/henkilo" kadottaisi virheet. Lomakkeen error-kenttiin täytetään BindingResult-olion sisältämät virheviestit -- virhetapauksissa myös juuri luotava olio palautetaan takaisin lomakkeelle, jolloin kenttiin täytetään vanhat arvot.

Huom! Springin lomakkeita käytettäessä lomakesivut haluavat käyttöönsä olion, johon data pyritään kytkemään jo sivua ladattaessa. Esimerkiksi ylläolevaan henkilolomakkeeseen ohjaava GET-pyyntöjä kuunteleva kontrolleri olisi seuraavanlainen:

    @RequestMapping(value = "/henkilo", method = RequestMethod.GET)
    public String getHenkilo(Model model) {
        model.addAttribute("henkilo", new Henkilo());
        return "henkilo";
    }

Lomakkeista löytyy lisää tietoa Springin näkymäteknologioihin liittyvän dokumentaation osassa: http://static.springsource.org/spring/docs/current/spring-framework-reference/html/view.html. Jotta annotaatioilla avulla tapahtuva validointi toimisi, tulee spring-context.xml-konfiguraatiossamme olla rivi <mvc:annotation-driven />.

Chatin validointi

Luo chat-sovellus (tai käytä pohjana aiemmin toteuttamaasi sovellusta), jossa käyttäjät voivat kirjoittaa vain normaaleista sanoista koostuvia viestejä. Poista siis mahdollisuus XSS (Cross Site Scripting)-hyökkäysten tekoon. Kun epäilet olevasi valmis, hyökkää sivuasi vastaan sivun http://ha.ckers.org/xss.html esimerkeillä.

Huom! Varmista myös että käyttäjätunnukseen ei voi lisätä epäilyttäviä viestejä.

Kun olet valmis, ruksaa tehtävä tehdyksi osoitteessa http://t-avihavai.users.cs.helsinki.fi/tester4 olevassa palvelussa.

Yleistä validoinnista ja lomakkeista

Vaikka ylläolevassa esimerkissämme käyttämäämme Henkilo-luokkaa ei oltu merkitty @Entity-annotaatiolla -- eli se ei ollut tallennettavissa 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. Silloin tällöin onkin fiksua luoda erillinen lomakkeen validointiin tarkoitettu lomakeobjekti, jonka pohjalta luodaan tietokantaan tallennettavat oliot -- jos validointi on kunnossa. Erilliseen lomakeobjektiin voi täyttää myös esimerkiksi kannasta haettavia listoja ym. ennalta.

Myytävät asunnot

Luo asunnon myynti-ilmoitusta varten tarkoitettu lomake, jossa kysytään myyjän tiedoista nimi, puhelinnumero ja sähköpostiosoite. Asunnosta kysytään asunnon koko neliömetreinä, osoitetta (postinumero, kaupunki, katuosoite), asunnon rakennusvuosi sekä asunnon kuntoa (hyvä, keskiverto, huono, remontoitava).

Toteuta lomakkeen validointi yhden lomakeobjektin avulla, mutta käytä taustalla kahta erillistä luokkaa Henkilo ja Asunto. Varmista ettei kukaan pääse rikkomaan sivuasi. Tallenna lopuksi henkilö ja asunto tietokantaan. Tee lomakeobjektin muunnos Henkilo- ja Asunto-objekteiksi palvelukerroksessa. Voit käyttää viime viikolla tarjottua tietokantapohjaa sovellusta varten.

Luo myös yksinkertainen sivu, joka listaa kaikki myytävät asunnot.

Sinun ei tarvitse varautua tilanteeseen jossa henkilöllä olisi samaan aikaan monta asuntoa myynnissä.

Kun olet valmis, ruksaa tehtävä tehdyksi osoitteessa http://t-avihavai.users.cs.helsinki.fi/tester4 olevassa palvelussa.

Web-sivusi on tasan niin turvallinen kuin sen heikoiten validoitu datan vastaanottaja.

Lisää tietokannoista

Oman tietokannan käyttö

Olemme käyttäneet automaattisesti muistiin ladattavaa tietokantaa sovelluksemme kehitykseen. Mikään ei tietenkään estä jonkun toisaalla toimivan tietokannan käyttöä. Jotta saisimme haluamamme tietokannan käyttöön, meidän tulee ladata tietokannalle sopiva ajuri yhteyden luomista varten sekä konfiguroida dataSource, eli kohde tiedon hakuun.

Muistiin ladattavan tietokannan dataSource-konfiguraatio on ollut muotoa:

<jdbc:embedded-database id="dataSource" type="HSQL"/>

Jonka lisäksi olemme tarvinneet HSQLDB-ajurin. Näiden konfiguraatio löytyy ylempää materiaalista.

MySQL

MySQL:n saa käyttöön lisäämällä ajuririippuvuuden pom.xml-tiedostoon.

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.18</version>
        </dependency>

Ja muuttamalla datasource-konfiguraation seuraavanlaiseksi:

     <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <property name="driverClassName" value="com.mysql.jdbc.Driver"/>
        <property name="url" value="jdbc:mysql://localhost:3306/awesome"/>
        <property name="username" value="root"/>
        <property name="password" value=""/>
    </bean>

Yllä oletetaan että MySQL toimii paikallisen koneen portissa 3306 ja tietokannan nimi on awesome. Tietokannan käyttäjätunnus on root ja salasanaa ei ole annettu. Jos olet konfiguroinut dialekteja, muista myös muuttaa ne -- emme tällä kurssilla ole tutustuneet edelliseen.

PostgreSQL

Kuten MySQL:n, myös PostgreSQL:n konfigurointi on helpohkoa. PostgreSQL-ajurin saa ladattua lisäämällä seuraavan riippuvuuden pom.xml-tiedostoon.

        <dependency>
            <groupId>postgresql</groupId>
            <artifactId>postgresql</artifactId>
            <version>8.4-702.jdbc3</version>
            <classifier/>
        </dependency>

Tämän lisäksi myös dataSource tulee konfiguroida.

     <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <property name="driverClassName" value="org.postgresql.Driver"/>
        <property name="url" value="jdbc:postgresql://localhost:5432/awesome"/>
        <property name="username" value="root"/>
        <property name="password" value=""/>
    </bean>

Jatkamme toistaiseksi paikallisten tietokantojen käyttämistä...

Kahdensuuntaiset relaatiot ja propagointi

Viime viikolle tarkoitetuissa tehtävissä olemme törmänneet useampaan otteeseen tilanteeseen, jossa haluaisimme tallentaa olion joka viittaa olioon, josta viitataan takaisin. Esimerkiksi henkilön lisäämisessä huoneeseen henkilö tietää huoneesta ja huone henkilöstä. Tämä on tuottanut päänvaivaa monelle.

Normaali ratkaisu on ollut seuraava. Alussa oletamme että meillä on käytössämme DAO-tyyppinen luokka huoneRepository, josta voimme kysyä huoneita sekä tallettaa huoneita. Lisäksi käytössämme on henkiloRepository, jonka avulla voimme tallentaa henkilöitä. Jatkossa puhumma DAO-luokista ja Repository-luokista hieman sekaisin -- tarkoitamme kuitenkin samaa asiaa.

    @Override
    @Transactional
    public void lisaaHenkilo(Henkilo henkilo, Long huoneId) {
        Huone huone = huoneRepository.findOne(huoneId); // etsitään oikea huone
        henkilo.setHuone(huone);
        henkilo = henkiloRepository.save(henkilo); // tallennetaan henkilö merge-komennolla
                                                   // näin saamme viitteen luotuun henkilöön

        // MUTTA! Tässä vaiheessa henkilöä ei ole vielä lisätty huoneeseen sillä
        // kukaan ei ole kertonut huoneelle että henkilö on siellä. 

        // lisätään henkilö vielä huoneeseen.
        huone.getHenkilot().add(henkilo);
        huoneRepository.save(huone);
    }

Kaksisuuntaisten talletusten ylläpito käsin ei ole kovin mukavaa. Jos luotava henkilö on täysin uusi, seuraava Huone-luokan uusi lisaaHenkilo-metodin luominenkaan ei auta, sillä käytössämme oleva viite henkilö-olioon ei ole sama viite kuin se, joka luodaan kun henkilö luodaan tietokantaan.

// Huone-luokka
    public void lisaaHenkilo(Henkilo henkilo) {
        if(!henkilot.contains(henkilo)) {
            henkilot.add(henkilo);
        }

        henkilo.setHuone(this);
    } 

Tässä tulee avuksi tallennusten propagointi. Annotaatiolle @OneToMany (sekä muille x-to-x) voi määritellä parametrin cascade, jolla määrittelemme mitkä toiminnot pitää propagoida eteenpäin viitatuille olioille. Voimme esimerkiksi tallentaa Huone-luokan siten, että siihen lisätty -- eli vielä tallentamaton -- henkilö tallennetaan. Tämä tapahtuu seuraavasti. Lisätään Cascade-määre luokalle Huone.

@Entity
public class Huone implements Serializable {
    // id ja muut kentät
    @OneToMany(mappedBy = "huone", cascade={CascadeType.MERGE, CascadeType.PERSIST})
    private List<Henkilo> henkilot;
   
    // getterit ja setterit

Ylläolevassa esimerkissä oletetaan että luokalla Henkilo on attribuutti private Huone huone, sekä siihen liittyvät getterit ja setterit. Nyt aina kun tallennamme Huone-olion, tallennamme siihen liittyvät tallentamattomat henkilöt. Muokataan metodia, jolla lisätään huone.

    @Override
    @Transactional
    public void lisaaHenkilo(Henkilo henkilo, Long huoneId) {
        Huone huone = huoneRepository.findOne(huoneId); // etsitään oikea huone
        huone.getHenkilot().add(henkilo);
        henkilo.setHuone(huone);
        huoneRepository.save(huone); // nyt myös juuri luotava henkilö tallentuu!
    }

Vaikka ylläoleva ratkaisu on jo mukavan näköinen on siinä vielä parannettavaa. Joudumme vieläkin asettamaan viittauksen henkilöstä huoneeseen! Lisätään taas edellisessä ratkaisussa yritetty lisaaHenkilo-metodi luokalle Huone.

// Huone-luokka
    public void lisaaHenkilo(Henkilo henkilo) {
        if(!henkilot.contains(henkilo)) {
            henkilot.add(henkilo);
        }

        henkilo.setHuone(this);
    } 

Nyt metodimme lisaaHenkilo on sopivan siisti.

// luokka 
    @Override
    @Transactional
    public void lisaaHenkilo(Henkilo henkilo, Long huoneId) {
        Huone huone = huoneRepository.findOne(huoneId); // etsitään oikea huone
        huone.lisaaHenkilo(henkilo);
        huoneRepository.save(huone); // nyt myös juuri luotava henkilö tallentuu!
    }

Annotaatioille @OneToOne, @OneToMany, @ManyToOne ja @ManyToMany voi siis lisätä attribuuttina cascade-arvon, joka kertoo propagoidaanko tehtyjä muutoksia myös viitatuille olioille. Lisää tietoa löytyy muunmuassa Oraclen JavaEE-dokumentaatiosta täältä.

Henkilöt huoneisiin, osa 2

Jos et ole vielä tehnyt tehtävää 33: Henkilöt huoneisiin, tee se nyt.

Kun olet tehnyt tehtävän 33, siisti henkilöiden ja huoneiden tallennus ylläolevaa esimerkkiä seuraten.

Kun olet valmis, ruksaa tehtävä tehdyksi osoitteessa http://t-avihavai.users.cs.helsinki.fi/tester4 olevassa palvelussa.

Spring Data

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.

// yleiskäyttönen
public abstract class JPADao<T> implements DAO<T> {

    @PersistenceContext
    EntityManager entityManager;

    private Class clazz;
    
    public JPADao(Class clazz) {
        this.clazz = clazz;
    }   
    
    @Override
    public void create(T instance) {
        entityManager.merge(instance);
    }

    @Override
    public T read(int id) {
        return (T)entityManager.find(clazz, id);
    }

    @Override
    public void delete(T instance) {
        entityManager.remove(instance);
    }

    @Override
    public T update(T instance) {
        return entityManager.merge(instance);
    }
    
    @Override
    public List<T> list() {
        // CriteriaBuilder on ohjelmallinen API kyselyjen tekemiseen
        CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
        CriteriaQuery query = criteriaBuilder.createQuery(clazz);
        return entityManager.createQuery(query).getResultList();
    }
}

Useat ohjelmistokehittäjät tarjoavat myös tekeleitään muiden käyttöön (esim. daofusion ja generic-dao). Näissäkin joudut aina perimään jonkun luokan.

Spring Data (http://www.springsource.org/spring-data/) on Spring-sovelluskehykseen liittyvä projekti, joka helpottaa nykyaikaisten tietovarastojen käyttöä. Sen puitteissa on toteutettu myös yllä näytettyjä välineitä JPA:n käyttöön. Spring Data JPAn etuna muihin "geneeriset daot"-toteutuksiin on integroituminen Spring-sovelluskehykseen, ja sitä kautta pääsy inversion of control ja dependency injection -mekanismeihin. Springin modulaarisesta rakenteesta johtuen joudumme lisäämään Spring Data JPA-komponentin projektimme riippuvuuksiin halutessamme sen käyttöön.

        <dependency>
            <groupId>org.springframework.data</groupId>
            <artifactId>spring-data-jpa</artifactId>
            <version>1.0.2.RELEASE</version>
        </dependency> 

Tämän lisäksi Spring Data haluaa tietää missä repository-luokkamme sijaitsevat. Spring-kontekstiin voi määritellä seuraavan rivin:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       // ... 
       xmlns:jpa="http://www.springframework.org/schema/data/jpa"
       xsi:schemaLocation="
       // ...
        http://www.springframework.org/schema/data/jpa
           http://www.springframework.org/schema/data/jpa/spring-jpa.xsd">

    <!-- Sovelluksen repository-luokat (DAO-luokat) sijaitsevat pakkauksen wad alla -->
    <jpa:repositories base-package="....repository" />
    
    // jne

Esimerkki: Luodaan luokat Team, joka kuvaa joukkuetta, ja Player, joka kuvaa pelaajaa. Yhdessä joukkueessa on monta pelaajaa, mutta kukin pelaaja kuuluu vain yhteen joukkueeseen.

// importit

@Entity
public class Team implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
    private String name;
    @OneToMany(mappedBy = "team", cascade={CascadeType.MERGE, CascadeType.PERSIST})
    private List<Player> players;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
    
    public void addPlayer(Player player) {
        if(!players.contains(player)) {
            players.add(player);
        }
        
        player.setTeam(this);
    }

    public List<Player> getPlayers() {
        return players;
    }

    public void setPlayers(List<Player> players) {
        this.players = players;
    }
}
// importit

@Entity
public class Player implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
    @NotNull(message = "Name must be defined.")
    @Size(min = 1, max = 40, message = "Name length must be between 1 and 40.")
    @Pattern(regexp = "\\w+", message = "Name must contain only words.")
    private String name;
    @ManyToOne(cascade = {CascadeType.ALL})
    private Team team;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public void setTeam(Team team) {
        this.team = team;
    }

    public Team getTeam() {
        return team;
    }
}

Luodaan seuraavaksi tietovarastotason toteutukset, oletetaan että tarvitsemme yleiset luo, lue, listaa ja poista-toiminnallisuudet. Luodaan ensin rajapinnat molemmille.

public interface TeamRepository extends JpaRepository<Team, Long> {
}
public interface PlayerRepository extends JpaRepository<Player, Long> {
}

Yllä käytämme pohjana Spring Data JPAn tarjoamaa rajapintaa, joka listaa perinteiset metodit. Luodaan seuraavaksi rajapintojen vaatimat toteutukset.



Done. Huh, kovaa työtä.

Seuraavaksi vielä palvelut, joilla tietovarastoja käytetään. Ensin rajapinnat.

public interface TeamService {
    void create(Team team);
    List<Team> list();
}
public interface PlayerService {
    public void saveOrUpdate(Player player, Long teamId);
    public List<Player> list();
}

Ja sitten rajapintojen toteutukset.

@Service
public class TeamServiceImpl implements TeamService {

    @Autowired
    TeamRepository teamRepository;

    @Override
    @Transactional
    public void create(Team team) {
        teamRepository.save(team);
    }

    @Override
    @Transactional(readOnly = true)
    public List<Team> list() {
        return teamRepository.findAll();
    }
}
@Service
public class PlayerServiceImpl implements PlayerService {

    @Autowired
    private PlayerRepository playerRepository;

    @Autowired
    private TeamRepository teamRepository;

    @Override
    @Transactional(readOnly = true)
    public List<Player> list() {
        return playerRepository.findAll();
    }

    @Override
    @Transactional
    public void saveOrUpdate(Player player, Long teamId) {
        Team t = teamRepository.findOne(teamId);
        t.addPlayer(player);
        teamRepository.save(t);
    }
}

Tämän jälkeen kontrollerit, näkymä ja lepuutus. Valmista.

Hei! Eihän tuolla toteutettu noita repositoryjä! Ei niin. Avainsanoina tälle magialle perintä, inversion of control, dependency injection ja sovelluskehykset.

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 PlayerRepository siten, että sillä on metodi List<Player> findByName(String name) -- eli hae pelaajat joilla on tietty nimi.

public interface PlayerRepository extends JpaRepository<Player, Long> {
    List<Player> findByName(String name);
}

Ylläoleva esimerkki on esimerkki kyselystä, johon Spring Data ei tarvitse toteutusta. Se arvaa että kysely olisi muotoa SELECT p FROM Player p WHERE p.name = :name, eli luo kyselyn meille valmiiksi. Lisää Spring Data JPA:n kyselyjen arvaamisesta löytyy sen dokumentaatiosta. Tehdään toinen esimerkki jossa joudumme oikeasti luomaan myös kyselyn itse.

public interface PlayerRepository extends JpaRepository<Player, Long> {
    List<Player> findMatti();
}

Ja vielä konkreettinen toteutus. Huomaa että toteutus on kuten muutkin repository-luokkamme.

@Repository
public class PlayerRepositoryImpl implements PlayerRepository {
    @PersistenceContext
    private EntityManager entityManager;

    public List<Player> findMatti() {
        ... toteutus
    }
}

Elokuvat ja Genret, osa 2

Jos et ole toteuttanut viikon 3 tehtävää 34, Elokuvat ja Genret, toteuta se nyt.

Muokkaa Elokuvat ja Genret toteutustasi siten, että käytät Repository-tasolla Spring Data JPA:ta. Lisää sovellukseesi myös syötteen validointi.

Kun olet valmis, ruksaa tehtävä tehdyksi osoitteessa http://t-avihavai.users.cs.helsinki.fi/tester4 olevassa palvelussa.

Players 'n Teams 'n Budgets

Muokkaa osoitteessa https://github.com/avihavai/wad-2012/tree/master/spring-data-jpa-eclipselink-validation olevaa projektia siten, että joukkueen nimi validoidaan. Muuta myös pelaajien validointia siten, että pelaajan nimi voi koostua useammasta sanasta.

Tutustu projektin rakenteeseen tarkemmin ja lisää uusi entiteettiluokka AnnualBudget, jolla määritellään joukkueen vuosittainen budjetti. Budjettiin ei tarvitse määritellä avainkenttien lisäksi muuta kuin vuosi ja summa. Luo budjetin lisäykselle oma lomake.

Tämäkin palautetaan vasta koetilanteessa -- ole valmis selittämään mitä jouduit muokkaamaan ja miksi? Mitä et joutunut muokkaamaan?

Kun olet valmis, ruksaa tehtävä tehdyksi osoitteessa http://t-avihavai.users.cs.helsinki.fi/tester4 olevassa palvelussa.

Nykyaikaiset web-palvelut

Huomattava osa nykyaikaisista web-palveluista tarjoavat lataavat käyttäjälle palvelimelta vain staattisen käyttöliittymän. Käyttöliittymään kuuluu joukko javascript-komponentteja, jotka tuovat ja vievät dataa tarvittaessa käyttäjälle -- esimerkiksi JSON-muodossa. Seuraava tehtäväsarja selventää sovellusten kahtiajakoa.

Tökkel

Tökkel on ajan- ja projektinhallintaan kehitetty web-sovellus.

Tässä tehtävässä kehitetään osa Tökkel-sovelluksen palvelinpuolen toiminnallisuudesta. Demo valmiista sovelluksesta osoitteesta http://tokkel.herokuapp.com/app/index.html.

Selainpuolesta kiinnostuneille: Tökkelin käyttöliittymä on rakennettu Twitter Bootstrap (v2)-käyttöliittymäkirjaston avulla. Käyttöliittymäkirjaston lisäksi sovelluksen selainpuolella käytetään Backbone.js -nimistä kirjastoa REST-kyselyiden tekemiseen. Backbone.js pohjautuu Underscore.js-kirjastoon, joka tarjoaa joukon apufunktioita. Näiden lisäksi käytössä on JQuery-javascriptkirjasto.

Käytännössä nykyaikaisissa web-sovelluksissa käytetään sovelluskehyksiä sekä selainpuolella että palvelinpuolella. Vaikka painotamme palvelinpuolen toiminnallisuutta tällä kurssilla, on selainpuolen monipuolinen -- varsinkin javascriptin -- tuntemus tärkeää web-sovelluskehittäjälle.

Tökkelin kimppuun..

Tökkeliin tutustuminen

Lataa Tökkel-projekti osoitteesta https://github.com/avihavai/wad-2012/tree/master/tokkel-hw. Kun käynnistät sovelluksen, Tökkelin toiminnallisuus on osoitteessa http://palvelin/sovellus/app/index.html. Tökkelille on jo toteutettu tehtävien listaaminen ja lisääminen. Tutustu Tökkelin rakenteeseen ja testaile tehtävien lisäämistä.

Kontrolleri projekteille

Projektien hallintaan liittyvän kontrollerin tulee tarjota viisi eri toiminnallisuutta:

Kun toteutat kontrolleritason metodeja, kannattaa aluksi tulostella debug-viestejä palvelimen logiin. Näin saat tarkistettua milloin pyyntö käyttöliittymältä onnistuu ja milloin ei. Kun saat kaikki kyselyt vastaan -- ja saat palautettua esim. debug-olioita, voit siirtyä eteenpäin.

Palvelu ja tietokantatoiminnallisuus projekteille

Kun kontrollerisi toimii, toteuta projekteja varten palvelu- ja tietokantatoiminnallisuus. Ota mallia tehtävien hallinnassa tehdystä toiminnallisuudesta.

Huomaa että sinun tulee myös muokata TaskService-rajapinnan toteutusta. Kun tehtävää lisätään, liitä se projektiin.

He-ro-kuuu!

Kun sovelluksesi toimii, lähetä se Herokuun. Projektipohjassa on jo valmiiksi toiminnallisuus, jonka avulla projekti toimii Herokussa. Sinun tarvitsee vain luoda projekti herokun päähän (avainsanoja: cedar, stack) ja lähettää se heidän palveluun (avainsanoja: git, push, heroku, ..).

Kun olet valmis, palauta sovelluksen juuriosoite (esim. http://blazing-sword-19491.herokuapp.com) osoitteessa http://t-avihavai.users.cs.helsinki.fi/tester4 olevaan palveluun.

Web-sovellusten testaaminen

Web-sovellusten testaamiseen kuuluu yksikkötestaus, integraatiotestaus ja kuormitustestaus. 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 kuormitustestauksessa testataan miten tehokkaasti sovellukset kestävät kuormitusta.

Yksikkötestaus

Yksikkötestausta tehdään tyypillisesti JUnit-kirjaston avulla. Spring tarjoaa JUnit-kirjastolle integraation, jonka avulla saamme Autowired-annotaatiot toimimaan. Lisätään riippuvuudet projektimme pom.xml-tiedostoon.

        <dependency>
          <groupId>org.springframework</groupId>
          <artifactId>spring-test</artifactId>
          <version>3.1.0.RELEASE</version>
          <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>junit</groupId>  
            <artifactId>junit</artifactId>  
            <version>4.8.1</version>  
            <scope>test</scope>
        </dependency>

Yllä oleva määre scope kertoo milloin riippuvuutta tarvitaan. Käytämme sitä vain testien ajamiseen. Testataan ensin DAO-rajapintaamme PlayerRepository varmistaaksemme että JUnit toimii. Voit käyttää tehtävässä Players 'n Teams 'n Budgets olevaa tehtäväpohjaa.

public interface PlayerRepository extends JpaRepository<Player, Long> {
}

Spring Data tarjoaa tämänhetkisen PlayerRepository-luokan toiminnallisuudet, joten olisi hyvin outoa jos testit eivät toimisi. Luodaan uusi testi valitsemalla NetBeansista New -> Other -> JUnit -> JUnit Test. Annetaan testiluokalle nimeksi PlayerRepositoryTest ja lisätään se pakkaukseen wad.spring.repository, olettaen että lähdekoodimmekin sijaitsevat samassa pakkauksessa. Huomaa että lähdekoodit ja testikoodit päätyvät erillisiin kansioihin -- juurin näin sen pitääkin olla. Kun testiluokka on luotu, on projektin rakenne kutakuinkin seuraavanlainen.

.
|-- nb-configuration.xml
|-- pom.xml
`-- src
    |-- main
    |   |-- java
    |   |   `-- wad
    |   |       `-- spring
    |   |           |-- config
    |   |           |   `-- Config.java
    |   |           |-- controller
    |   |           |   |-- HomeController.java
    |   |           |   |-- PlayerController.java
    |   |           |   `-- TeamController.java
    |   |           |-- domain
    |   |           |   |-- Player.java
    |   |           |   `-- Team.java
    |   |           |-- repository
    |   |           |   |-- PlayerRepository.java
    |   |           |   `-- TeamRepository.java
    |   |           `-- service
    |   |               |-- PlayerServiceImpl.java
    |   |               |-- PlayerService.java
    |   |               |-- TeamServiceImpl.java
    |   |               `-- TeamService.java
    |   |-- resources
    |   |   `-- META-INF
    |   |       `-- persistence.xml
    |   `-- webapp
    |       |-- index.jsp
    |       |-- META-INF
    |       |   `-- context.xml
    |       `-- WEB-INF
    |           |-- glassfish-web.xml
    |           |-- spring-context.xml
    |           |-- spring-database.xml
    |           |-- view
    |           |   |-- list.jsp
    |           |   `-- player.jsp
    |           `-- web.xml
    `-- test
        `-- java
            `-- wad
                `-- spring
                    `-- repository
                        `-- PlayerRepositoryTest.java

Paljon tavaraa pienessä projektissa... Kopioi vielä konfiguraatiotiedostot spring-context.xml ja spring-database.xml kansioon src/main/resources/, ja lisää kumpaankin sana -test loppuun. Käytämme näitä konfiguraatioita testien ajamiseen. Projektin runko on nyt seuraavanlainen:

.
|-- nb-configuration.xml
|-- pom.xml
`-- src
    |-- main
    |   |-- java
    |   |   `-- wad
    |   |       `-- spring
    |   |           |... jne
    |   |-- resources
    |   |   |-- META-INF
    |   |   |   `-- persistence.xml
    |   |   |-- spring-context-test.xml
    |   |   `-- spring-database-test.xml
    |   `-- webapp
    |       |... jne
    |... jne
    |
    `-- test
        `-- java
            `-- wad
                `-- spring
                    `-- repository
                        `-- PlayerRepositoryTest.java

Springiä käyttävät JUnit-yksikkötestit tarvitsevat kaksi annotaatiota alkuun. Annotaatio @RunWith(SpringJUnit4ClassRunner.class) kertoo että käytämme Springiä yksikkötestien ajamiseen, annotaatio @ContextConfiguration(locations = {"classpath:spring-context-test.xml", "classpath:spring-database-test.xml"}) kertoo että käytämme classpathissa sijaitsevia tiedostoja spring-context-test.xml ja spring-database-test.xml testien ajamiseen. Testiluokan alku näyttää siis seuraavalta:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"classpath:spring-context-test.xml",
    "classpath:spring-database-test.xml"})
public class PlayerRepositoryTest {
   ...

Otetaan käyttöön DAO PlayerRepository @Autowired-annotaation avulla ja luodaan ensimmäinen testi. Testi testaa että tietokannassa olevien objektien määrä kasvaa yhdellä kun objekti lisätään tietokantaan.

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"classpath:spring-context-test.xml",
    "classpath:spring-database-test.xml"})
public class PlayerRepositoryTest {

    @Autowired
    PlayerRepository playerRepository;

    @Test
    public void createIncrementsElementCountByOne() {
        long countAtStart = playerRepository.count();

        Player p = new Player();
        p.setName("Matti");
        playerRepository.save(p);

        long countAtEnd = playerRepository.count();
        Assert.assertTrue("Player count should be increased by one when adding an element.",
                countAtStart + 1 == countAtEnd);
    }
}

Kun testi ajetaan, nähdään vihreä palkki!

Yksikkötestejä

Kirjoita osoitteessa https://github.com/avihavai/wad-2012/tree/master/spring-data-jpa-eclipselink-validation olevalle projektille kolme yksikkötestiä. Ensimmäinen testaa että tallennettujen joukkueiden määrä kasvaa yhdellä kun joukkue tallennetaan tietokantaan. Toinen testaa että kun pelaaja nimeltä "Pekka" tallennetaan tietokantaan, tietokannassa on tämän jälkeen olemassa pelaaja nimeltä Pekka. Kolmas testaa että propagointi toimii: pelaajien määrä kasvaa kahdella kun jo olemassaolevaan joukkueeseen lisätään kaksi pelaajaa ja se tallennetaan tietokantaan.

        Team team = new Team();
        team.setName("LeTeam");
        team = teamRepository.save(team);
        
        Player player = new Player();
        player.setName("Matti");
        team.addPlayer(player);

        player = new Player();
        player.setName("Pekka");
        team.addPlayer(player);
        teamRepository.save(team);

Kun olet valmis, ruksaa tehtävä tehdyksi osoitteessa http://t-avihavai.users.cs.helsinki.fi/tester4 olevassa palvelussa.

Joukkueen poistaminen

Laajenna edellisen tehtävän kolmatta testiä siten, että kutsut lopussa joukkueen poistamista. Määrittele testi siten että pelaajien määrän tulee olla testin lopussa sama kuin testin alussa.

        ...
        player.setName("Pekka");
        team.addPlayer(player);
        team = teamRepository.save(team);
        
        teamRepository.delete(team);
        ...

Huomaat että testi ei mene läpi. Tee tarvittavat muutokset ohjelmakoodissa jotta testi menee läpi.

Kun olet valmis, ruksaa tehtävä tehdyksi osoitteessa http://t-avihavai.users.cs.helsinki.fi/tester4 olevassa palvelussa.

Rajapintojen käyttämisen hyvä puoli on se, että voimme korvata käytettyjä luokkia lennossa. Esimerkiksi, testimme testaa TeamService-rajapinnan toteuttamaa luokkaa, johon injektoidaan rajapinnan TeamRepository-toteuttama luokka. Määrittelemällä ohjelmallisesti oman konfiguraation, voimme vaihtaa TeamService-luokan käyttämän DAO:n joksikin muuksi. Alla esimerkkinä luokka TeamServiceTest.

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"classpath:spring-context-test.xml",
    "classpath:spring-database-test.xml"})
public class TeamServiceTest {

    @Autowired
    TeamService teamService;

    @Configuration
    static class Config {

        // inject this into teamService
        @Bean
        public TeamRepository teamRepository() {
            return new TeamRepositoryTestImpl();
        }
    }

    @Test
    public void testNewTeamHasBeenCreated() {
        Team t = new Team();
        t.setName("Tiimi");
        teamService.create(t);

        List<Team> teams = teamService.list();
        Assert.assertTrue("", teams.size() == 1);
    }
}

Testissä käytetään normaalin TeamRepository-rajapinnan toteuttaman luokan sijaan meidän omaa TeamRepositoryTestImpl-luokkaa. Testeissä käytettävät luokat tulee säilyttää testilähdekoodien kanssa samassa paikassa, eli emme halua niitä projektin käyttöön.

Huom! Ylläoleva esimerkki ei ole toiminut kaikilla, voit tehdä tehtävään toteutuksen myös esimerkiksi siten, että injektoimme TeamService-olion, jolle asetamme käsin tarpeellisen TeamRepositoryTest-olion. Tämä toimii seuraavasti:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
public class TeamServiceTest {

    @Autowired
    private TeamService teamService;

    @Configuration
    static class Config {

        // this bean will be injected into the TeamService class
        @Bean
        public TeamService teamService() {
            TeamService teamService = new TeamServiceImpl(new TeamRepositoryTestImpl());
            return teamService;
        }
    }
    // ...
}

Ylläolevan lisäksi luokka TeamServiceImpl vaatii erillisen konstruktorin, jolle asetetaan TeamRepository. Huomaa että Autowired-annotaatio on siirretty konstruktorin alkuun. Spring-lisää nyt TeamRepository-luokan ilmentymän automaattisesti tälle konstruktorille.

@Service
public class TeamServiceImpl implements TeamService {

    private TeamRepository teamRepository;

    public TeamServiceImpl() {
    }

    @Autowired
    public TeamServiceImpl(TeamRepository teamRepository) {
        this.teamRepository = teamRepository;
    }
    // ...

Testiluokan injektointi

Tee ylläoleva esimerkki ja määrittele sitä varten erillinen TeamRepository-rajapinnan toteuttava toteutus. Huomaat että toteutus vaatii melko paljon metodeja -- toteuta vain oleellisimmat. Luo seuraavat testit: 1) Testaa että tiimin lisäämisen jälkeen käytössä on samanniminen tiimi. 2) Testaa että tiimin lisäämisen jälkeen tiimilistan koko kasvaa aina yhdellä. 3) Testaa että jokaisella tiimillä on eri id.

Kun olet valmis, ruksaa tehtävä tehdyksi osoitteessa http://t-avihavai.users.cs.helsinki.fi/tester4 olevassa palvelussa.

Käytetyt tunnit, viikko 4

Kirjaa osoitteessa http://t-avihavai.users.cs.helsinki.fi/tunnit4 olevaan palveluun neljännen viikon materiaalin ja tehtävien parissa käyttämäsi aika.

Edellisessä esimerkissä käytimme erikseen määriteltyä sisäluokkaa. Sisäluokan määrittely ei ole pakollista, ja testit voi toteuttaa toki myös määrittelemällä erillisen konfiguraatioluokan.

@Configuration
public class TeamServiceConfig {

    @Bean
    public TeamService teamService() {
        TeamService teamService = new TeamServiceImpl(new TeamRepositoryTestImpl());
        return teamService;
    }
}

Jotta ajettava testi löytää konfiguraatioluokan, kerromme testille luokan sijainnin @ContextConfiguration-annotaatiolla.

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = TeamServiceConfig.class)
public class TeamServiceTest {

    @Autowired
    private TeamService teamService;

    // ...

Profiilit

Haluamme erilliset konfiguraatiot testaamiseen ja tuotantoon. Testikonfiguraatioiden kopioiminen tuotantokonfiguraatioista loi hieman copy-paste -koodia, sekä ylimääräisen sijainnin konfiguraatioiden hallinnointiin, mikä taas tarkoittaa lisäpaikkaa jossa asiat voivat mennä rikki. Ei hyvä.

Yksi ratkaisu tähän pulmaan on erillisten profiilien käyttö. Profiilien käyttö mahdollistaa eri konfiguraatioiden käytön erilaisissa ympäristöissä. Paikallisille kehitysympäristöille halutaan usein käyttöön oma profiili, jossa on käytössä muistiin ladattava tietokanta. Integraatiopalvelimella halutaan yleensä käyttää erillistä palvelinta, samoin kuin tuotantopalvelimella.

Käytännössä profiilien hallinta kannattaa toteuttaa esimerkiksi niin, että tuotantopalvelimille ja integraatiopalvelimille 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.

Esimerkki

Haluamme erilliset konfiguraatiot tuotantokäyttöön ja kehityskäyttöön. Tuotantokäytössä käytämme MySQL-tietokantaa, kehityskäytössä muistiin ladattavaa tietokantaa. Sovitaan että tuotantokäytössä käytämme profiilia production, muulloin käytössä profiili dev.

Asetetaan profiili tuotantokoneelle, tässä tehty ympäristömuuttujana

export SPRING_PROFILES_ACTIVE=production

Konfigurointi profiilien avulla siten, että voimme sisällyttää beans-solmuja beans-solmun sisään on tullut Springiin vasta versiossa 3.1, joten käytetään xml-skeemojen versioita 3.1.

       ...
       xsi:schemaLocation="
        http://www.springframework.org/schema/mvc 
          http://www.springframework.org/schema/mvc/spring-mvc-3.1.xsd
        http://www.springframework.org/schema/beans 
          http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
        http://www.springframework.org/schema/context 
          http://www.springframework.org/schema/context/spring-context-3.1.xsd
        http://www.springframework.org/schema/tx 
          http://www.springframework.org/schema/tx/spring-tx-3.1.xsd
        http://www.springframework.org/schema/jdbc 
           http://www.springframework.org/schema/jdbc/spring-jdbc-3.1.xsd
        http://www.springframework.org/schema/data/jpa
           http://www.springframework.org/schema/data/jpa/spring-jpa.xsd">
      ...

Määritellään käytetyt datalähteet (dataSource) erikseen profiileille. Tuotantoprofiili käyttää MySQL-tietokantaa, kehitysprofiili muistiin ladattavaa tietokantaa. Profiileihin liittyvät konfiguraatiot tulee määritellä XML-tiedoston loppuun.


    ... muu konfiguraatio
  
    <beans profile="production">        
        <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
            <property name="driverClassName" value="com.mysql.jdbc.Driver"/>
            <property name="url" value="jdbc:mysql://localhost:3306/awesome"/>
            <property name="username" value="root"/>
            <property name="password" value=""/>
        </bean>
    </beans>
    
    <beans profile="dev,default">
        <jdbc:embedded-database id="dataSource" type="HSQL"/> 
    </beans>
</beans>

Nyt käytössämme on kaksi erillistä konfiguraatiota käytettävälle tietokannalle. Tuotantokäytössä käytetään MySQL:ää, muulloin käytössä on muistiin ladattava HSQL-tietokanta. Profiilin dev pariksi on määritelty default, joka on oletusprofiili.

Huom! Jos käytössäsi on persistence.xml -tiedosto -- kuten meillä tässä vaiheessa on -- kannattaa siihen luoda kaksi erillistä konfiguraatiota. Toinen kehitysympäristöä ja toinen tuotantoympäristöä varten -- toisessa tietokantataulujen automaattinen generointi, toisessa ei.

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.0" xmlns="http://java.sun.com/xml/ns/persistence" 
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
             xsi:schemaLocation="http://java.sun.com/xml/ns/persistence                               
             http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd">
    <persistence-unit name="persistenceUnitDev" transaction-type="RESOURCE_LOCAL">
        <properties>
            <property name="showSql" value="true"/>
            <property name="eclipselink.ddl-generation" value="drop-and-create-tables"/>
            <property name="eclipselink.ddl-generation.output-mode" value="database"/>
            <property name="eclipselink.weaving" value="false"/>
            <property name="eclipselink.logging.level" value="FINEST"/>
        </properties>
    </persistence-unit>
    
    <persistence-unit name="persistenceUnitProduction" transaction-type="RESOURCE_LOCAL">
        <properties>
            <property name="showSql" value="true"/>
            <property name="eclipselink.ddl-generation.output-mode" value="database"/>
            <property name="eclipselink.weaving" value="false"/>
            <property name="eclipselink.logging.level" value="FINEST"/>
        </properties>
    </persistence-unit>
</persistence>

Tällöin konfiguraatiosi muuttuu hieman. Lisäämme profiileihin tiedon Persistence Unit-konfiguraation nimestä, jota haluamme käyttää, sekä viitteen tähän nimeen konfiguraatioon, joka luo EntityManagerFactory-olion.


    ...
    <bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
        <property name="packagesToScan" value="wad.spring.domain"/>
        <property name="persistenceUnitName" value="${persistenceUnitName}" /> 
        <property name="dataSource" ref="dataSource"/>
        <property name="jpaVendorAdapter">
            <bean class="org.springframework.orm.jpa.vendor.EclipseLinkJpaVendorAdapter" />
        </property>
    </bean>

    ...

    <beans profile="production">
        <bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
            <property name="properties" value="persistenceUnitName=persistenceUnitProduction"/>
        </bean>
        
        <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
            <property name="driverClassName" value="com.mysql.jdbc.Driver"/>
            <property name="url" value="jdbc:mysql://localhost:3306/awesome"/>
            <property name="username" value="root"/>
            <property name="password" value=""/>
        </bean>
    </beans>
    
    <beans profile="dev,default">
        <bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
            <property name="properties" value="persistenceUnitName=persistenceUnitDev"/>
        </bean>
        <jdbc:embedded-database id="dataSource" type="HSQL"/> 
    </beans>
</beans>

Nyt käytössä on kaksi erillistä kantayhteyttä, sekä erilliset persistence.xml-konfiguraatiot.

Huom! Jos haluat käyttää profiileja Herokussa, voit asettaa Procfileen ylimääräisen parametrin. Esimerkiksi tokkelissa Procfile oli seuraavanlainen.

java $JAVA_OPTS -jar target/dependency/jetty-runner.jar --port $PORT target/*.war

Jos haluamme käyttöön tietyn profiilin, voimme asettaa sen seuraavasti. Alla olevassa esimerkissä profiiliksi on valittu production.

java -Dspring.profiles.active=production $JAVA_OPTS -jar target/dependency/jetty-runner.jar --port $PORT target/*.war

Profiilimäärittelyjä voi käyttää myös testeissä. Esimerkiksi seuraavassa määritellään testi joka ajetaan profiililla on production. Konfiguraatiotiedostojen sijainti on määritelty projektiin liittyvänä polkuna.

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"file:src/main/webapp/WEB-INF/spring-context.xml",
    "file:src/main/webapp/WEB-INF/spring-database.xml"})
@ActiveProfiles("production")
public class PlayerRepositoryTest {
   ...

Integraatiotestaus

Teimme oikeastaan jo edellä integraatiotestejä yksikkötestauksen lomassa. Integraatiotestauksen ideana on tarkistaa toimivatko eri komponentit yhdessä. Komponentit -- kuten tietokanta ja tietokantaa käyttävä logiikka -- voidaan testata komponentti kerrallaan vaihtamalla toteutusta Mock-toteutukseksi kuten teimme TeamService-luokan tietokantatason kanssa.

Tutkitaan tässä kontrolleritason toimintojen testaamista. Tätä varten tarvitsemme -- yleensä -- päällä olevan sovelluksen.

Selenium

Selenium on yksi monista web-testauskehyksistä. Seleniumin Webdriver -osa antaa sovelluskehittäjälle mahdollisuuden käydä läpi sovelluksen käyttöliittymää ohjelmallisesti, ja varmistaa 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ä. Selenium löytyy Mavenin avulla.

        <dependency>
            <groupId>org.seleniumhq.selenium</groupId>
            <artifactId>selenium-java</artifactId>
            <version>2.13.0</version> 
            <scope>test</scope> 
        </dependency>

Ajatellaan käyttötapausta jossa käyttäjä haluaa syöttää tunnuksen lomakekenttään ja päätyä toisenlaiselle sivulle. Haluamme tässä löytää lomakekentän nimeltä "tunnus". Kun kenttään asetetaan arvo "Arvo" ja kenttään liittyvä lomake lähetetään, tulee sivulla olla lomakekenttä nimeltä "viesti".

        // luodaan olio sivujen läpikäyntiin
        WebDriver driver = new HtmlUnitDriver();

        // haetaan haluttu osoite (aiemmin määritelty muuttuja)
        driver.get(osoite);

        // haetaan kenttä nimeltä tunnus
        WebElement element = driver.findElement(By.name("tunnus"));

        // asetetaan kenttään arvo
        element.sendKeys("Arvo");

        // lähetetään lomake
        element.submit();
	
        // haetaan kenttä nimeltä "viesti"
	element = driver.findElement(By.name("viesti"));
	
        if (element == null) {
            System.out.println("Ei löydy!");
        } else {
            System.out.println("Löytyi!");
        }

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 "tunnus", ja lisätään kenttään arvo "Arvo". Tämän jälkeen lomake lähetetään. Kun lomake on lähetetty, haetaan kenttää jolla attribuutin name arvona on "viesti". Jos kenttää ei löydy tulostetaan viesti "Ei löydy!", muuten tulostetaan viesti "Löytyi!".

Testejä voi määritellä yksikkötesteiksi kuten muitakin testejä. Alla sama esimerkki yksikkötestinä, joka testaa integraatiopalvelimella olevaa palvelua. Huomaa että konkreettisissa tapauksissa osoite ladattaisiin erikseen konfiguraatiosta.


public class SampleIntegrationTest {

    static String osoite = "http://t-avihavai.users.cs.helsinki.fi/lets/Chat";
    private WebDriver webDriver;

    @Before
    public void setup() {
        webDriver = new HtmlUnitDriver();
    }

    @Test
    public void test() {
        // haetaan haluttu osoite (aiemmin määritelty muuttuja)
        webDriver.get(osoite);

        // haetaan kenttä nimeltä tunnus
        WebElement element = webDriver.findElement(By.name("tunnus"));
        Assert.assertNotNull(element);

        // asetetaan kenttään arvo
        element.sendKeys("Arvo");

        // lähetetään lomake
        element.submit();

        // haetaan kenttä nimeltä "viesti"
        element = webDriver.findElement(By.name("viesti"));
        Assert.assertNotNull(element);
    }
}

Seleniumia käytetään yleensä yhdessä JBehave-kirjaston kanssa, joka antaa mahdollisuuden testitapausten luomiseen erillisen tekstiformaatin avulla. Lisää JBehave-kirjastosta osoitteessa http://jbehave.org/reference/stable/.

Selenium

Toteuta joukko Selenium-testejä, jossa testaat osoitteessa http://t-avihavai.users.cs.helsinki.fi/lets/Chat toimivaa Chat-palvelua. Palvelussa on kirjautumissivulla kenttä "tunnus", jonka pitäisi hyväksyä 4-8 merkkiä pitkät tunnukset. Chat-sivulla käyttäjä kirjoittaa viestejä "viesti"-nimiseen kenttään. Viestit ovat rajoittamattoman pitkiä, sovelluksen tulee estää ääkkösten ja nuolien käyttäminen viesteihin.

Luo testit siten, että voit yksilöidä testien perusteella asiat jotka eivät toimi -- ei siis vain yhtä testiä. Kerää ylös asiat jotka eivät toimineet.

Kun olet valmis, ruksaa tehtävä tehdyksi osoitteessa http://t-avihavai.users.cs.helsinki.fi/tester5 olevassa palvelussa.

Spring Test MVC

Selenium sopii erinomaisesti integraatiotestaukseen, mutta vaatii päällä olevan palvelimen. Palvelin toimii mustana laatikkona, jolloin palvelimen sisäistä toimintaa ei pysty verifioimaan muuten kuin tulosteiden avulla. Lisäksi palvelimen käynnistäminen on usein hidas prosessi, joka ei ole aina toivottua.

Toinen -- Seleniumia tukeva -- lähestymistapa, joka ei vaadi käynnissä olevaa palvelinta, on Springin Test MVC -komponentti. Sen löytää mavenista seuraavilla riippuvuuksilla.

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test-mvc</artifactId>
            <version>1.0.0.BUILD-SNAPSHOT</version>
            <scope>test</scope>
        </dependency>

Tarvitset sitä varten myös pom.xml-tiedostoon erillisen repository-merkinnän. Spring Test MVC on suhteellisen uusi komponentti, joten sitä ei ole vielä normaaleissa tietovarastoissa.

        <repository>
            <id>spring-snapshot</id>
            <name>Spring Maven SNAPSHOT Repository</name>
            <url>http://maven.springframework.org/snapshot</url>
        </repository>

Spring Test MVC mahdollistaa kyselyjen simuloinnin palvelimen näkökulmasta. Sen avulla voi mm. testata että pyynnön vastaukseen liittyvässä modelissa on halutut tiedot, ja että vastaus ohjataan oikeaan paikkaan. Testikehystä käytetään MockMvc-olion avulla, joka toimii Spring-sovelluskehyksenä testille ilman erillistä tarvetta palvelimelle.

Testaus MockMvc:n avulla kyselyn kautta. Ensin teemme kyselyn (esimerkiksi GET-pyynnön haluttuun osoitteeseen), jonka jälkeen kysymme vastaukselta (tai kehykseltä) toivottuja ominaisuuksia. Alla ensimmäinen esimerkki, jossa teemme ensin pyynnön osoitteeseen "/player", ja oletamme että vastauskoodina on 200 eli ok.

    @Test
    public void responseOkWhenGetRequestToPlayer() throws Exception {
        mockMvc.perform(MockMvcRequestBuilders.get("/player")).
                andExpect(MockMvcResultMatchers.status().isOk());
    }

Testin toivotut ominaisuudet ketjutetaan. Ylläolevaa testiä voitaisiin jatkaa siten, että haluamme että pyynnön vastaus ohjataan tietylle jsp-sivulle.

    @Test
    public void jspResponseOkWhenGetRequestToPlayer() throws Exception {
        mockMvc.perform(MockMvcRequestBuilders.get("/player")).
                andExpect(MockMvcResultMatchers.status().isOk()).
                andExpect(MockMvcResultMatchers.forwardedUrl("/WEB-INF/view/player.jsp"));
    }

Vastaavasti voimme testata myös model-objektissa olevia tietoja. Haluamme esimerkiksi että modeliin on lisätty kaksi attribuuttia, ja että attribuuttien nimet ovat "player" ja "teams".

    @Test
    public void requestToPlayer() throws Exception {
        mockMvc.perform(MockMvcRequestBuilders.get("/player")).
                andExpect(MockMvcResultMatchers.status().isOk()).
                andExpect(MockMvcResultMatchers.forwardedUrl("/WEB-INF/view/player.jsp")).
                andExpect(MockMvcResultMatchers.model().size(2)).
                andExpect(MockMvcResultMatchers.model().attributeExists("player")).
                andExpect(MockMvcResultMatchers.model().attributeExists("teams"));
    }

Testiluokka kokonaisuudessaan, huomaa että konfiguraatio tapahtuu sekä Setup-vaiheessa että kontekstin avulla. Tähän on lupailtu parannuksia myöhemmissä versioissa.


import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.web.server.MockMvc;
import org.springframework.test.web.server.request.MockMvcRequestBuilders;
import org.springframework.test.web.server.result.MockMvcResultMatchers;
import org.springframework.test.web.server.setup.MockMvcBuilders;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"file:src/main/webapp/WEB-INF/spring-context.xml", 
    "file:src/main/webapp/WEB-INF/spring-database.xml"})
public class PlayerIntegrationTest {
    
    MockMvc mockMvc;
    
    @Before
    public void setup() {
        String[] contextLoc = {"file:src/main/webapp/WEB-INF/spring-context.xml",
            "file:src/main/webapp/WEB-INF/spring-database.xml"};
        String warDir = "src/main/webapp";
        mockMvc = MockMvcBuilders.xmlConfigSetup(contextLoc).
                configureWebAppRootDir(warDir, false).build();
    }
    
    @Test
    public void requestToPlayer() throws Exception {
        mockMvc.perform(MockMvcRequestBuilders.get("/player")).
                andExpect(MockMvcResultMatchers.status().isOk()).
                andExpect(MockMvcResultMatchers.forwardedUrl("/WEB-INF/view/player.jsp")).
                andExpect(MockMvcResultMatchers.model().size(2)).
                andExpect(MockMvcResultMatchers.model().attributeExists("player")).
                andExpect(MockMvcResultMatchers.model().attributeExists("teams"));
    }    
}

Spring Test MVC

Lataa osoitteessa https://github.com/avihavai/wad-2012/tree/master/spring-data-jpa-eclipselink-validation oleva projekti ja luo sille kaksi Spring Test MVC-testitapausta. Toinen tarkastaa että GET-pyyntö osoitteeseen "/team" ohjaa pyynnön osoitteeseen "/home" (tässä avainsanasta redirectedUrl on hyötyä). Toinen tarkastaa että GET-pyyntö osoitteeseen "/home" lisää vastaukseen liittyvään modeliin attribuutit "players" ja "teams" ja ohjaa vastauksen sivulle "/WEB-INF/view/list.jsp".

Lisää Spring Test MVC:stä löytyy mm. osoitteessa http://rstoyanchev.github.com/spring-31-and-mvc-test/ olevista kalvoista.

Kun olet valmis, ruksaa tehtävä tehdyksi osoitteessa http://t-avihavai.users.cs.helsinki.fi/tester5 olevassa palvelussa.

Kuormitustestaus

Kuormitustestauksella testataan sovelluksen sovelluksen toimintaa suuremmilla käyttäjämassoilla. Käyttäjäkokemuksen kannalta oleellisia ovat nopeat vastausajat. Karkeasti ajatellen voidaan ajatella että vastausajat liittyvät käyttäjätyytyväisyys -- nopeusnäkökulmasta -- seuraavasti: 0.1 s = huikea palvelu, 1 s = ok!, 10 s = etsinpä toisen tarjoajan.

Kuormitustestauksella pyritään välttämään tilanteita, joissa palvelu ei suuren käyttäjämääränsä tai käyttäjien toiminnan takia toimikaan.

Esimerkki

VR:n lippupalvelu: http://www.tietoviikko.fi/cio/vr+myontaa+itongelmat+olisi+pitanyt+tunnistaa+etukateen/a697637.

Tiedon ja Accenturen VR:lle toimittaman uuden lippukaupan ja -järjestelmän ongelmiin oli lopulta Suonikon mukaan kaksi pääsyytä.

Verkkokaupan käyttäjämäärät oli arvioitu liian pieniksi lanseerauksen yhteydessä. Asiaa korjattiin sekä laite- että sovelluspalvelintasolla heti julkistuksen jälkeen ja samalla lisättiin mahdollisuus rajoittaa käyttäjämäärää.

”Julkisuudessa on keskitytty pohtimaan käyttöönottohetken kuormitusta. Se ei kuitenkaan enää ollut syy pitkittyneeseen automaattien poissaoloon”, Suonikko selittää.

Toinen ongelma oli nimittäin sovelluspalvelimien varusohjelmiston virhe. Oraclen Weblogic-tuotteen versiossa 10 oli tunnettu muistinkäsittelyyn liittyvä bugi, joka tietyssä kovassa kuormitustilassa aiheutti ongelmia ja järjestelmään syntyi epävakautta. Tämä johti lippujärjestelmän sulkemiseen.

1). Käyttäjiä oli enemmän kuin oletettu, 2) Sovelluksen käyttämässä komponentissa oli bugi, joka ilmeni kovassa kuormitustilassa. Oliko sovellusta testattu kovassa kuormitustilassa?

Kuormitustestaukseen usein käytettäviä työkaluja ovat The Grinder ja Apache JMeter.

Skaalautuvuus

Käyttäjien määrän kasvaessa sovelluksen tulee skaalautua mukana. 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 tulee eteen pyyntöjen jakaminen oikeille palvelimille.

Käytännössä skaalautumisesta puhuttaessa puhutaan horisontaalisesta skaalautumisesta, jossa käyttöön hankitaan lisää palvelimia.

Horisontaalinen skaalautuminen

Palvelinmäärän kasvaessa oleellinen asia on pyyntöjen tasainen jakaminen palvelimille. Käytännössä pyyntöjen jakaminen tapahtuu erillisellä palvelimella (tai palvelimilla) -- kuormantasaajan toimesta (load balancer), joka ohjaa pyyntöjä eteenpäin.

Skaalautumiseen liittyvää pohdintaa

Miten toteuttaisit kuormantasauksen jos käytetty sovellus ei tarvitse tilaa? Entä jos sovellukseen liittyy tila jota ylläpidetään esimerkiksi keksin avulla?

Vastaa osoitteessa osoitteessa http://t-avihavai.users.cs.helsinki.fi/tester5 olevaan palveluuun.

Jos sovellukseen ei liity tilaa, voi kuormantasaaja esimerkiksi ohjata pyyntöjä round-robin -tekniikalla. Tosiaalta, kuormantasaaja voi myös seurata palvelinten tilaa, ja ohjata pyyntöjä aina palvelimelle, jolla on vähiten kuormaa. Jos taas sovellukseen liittyy tila, tulee pyynnöt ohjata aina samalle palvelimelle. Tämän voi toteuttaa esimerkiksi siten, että kuormantasaaja lisää pyyntöön keksin jonka avulla käyttäjä identifioidaan ja ohjataan oikealle palvelimelle.

Asynkroniset palvelukutsut

Tähän mennessä toteuttamissamme palveluissa pyynnön suorittaminen on tapahtunut seuraavasti:

  1. Pyyntö lähetetään palvelimelle
  2. Palvelin vastaanottaa pyynnön ja ohjaa pyynnön oikealle kontrollerille
  3. Kontrolleri vastaanottaa pyynnön ja ohjaa pyynnön oikealle palvelulle
  4. Palvelu vastaanottaa pyynnön, suorittaa pyyntöön liittyvät operaatiot mahdollisesti muiden palveluiden kanssa, ja palauttaa vastauksen kontrollerille
  5. Kontrolleri ohjaa pyynnön sopivalle näkymälle, joka palautetaan käyttäjälle.

Käytännössä -- riippuen sovelluksesta -- meidän ei tarvitse aina odottaa tietokantaoperaatioiden valmistumista. Jos operaatio -- esimerkiksi raskaampi laskenta, tiedon lähetys erilliseen palveluun tai tietynlaisen raportin generointi -- on hidas, kannattaa operaatio suorittaa asynkronisesti.

Asynkroniset metodikutsut tapahtuvat seuraavasti:

  1. Pyyntö lähetetään palvelimelle
  2. Palvelin vastaanottaa pyynnön ja ohjaa pyynnön oikealle kontrollerille
  3. Kontrolleri vastaanottaa pyynnön ja ohjaa pyynnön oikealle palvelulle
  4. Palvelu asettaa pyynnön suoritusjonoon ja palvelukutsusta palataan heti (palvelun tyyppi on void)
  5. Kontrolleri ohjaa pyynnön sopivalle näkymälle, joka palautetaan käyttäjälle.

Asynkroniset metodikutsut

Kannattaa käyttää sovelluksessa pohjana osoitteessa https://github.com/avihavai/wad-2012/tree/master/v5-runko olevaa runkoa.

Toteutetaan sovellus, joka näyttää käyttäjälle osoitteessa http://t-avihavai.users.cs.helsinki.fi/slow/Service olevan maailmat räjäyttävän palvelun tuottamaa tietoa. Oletetaan että sovelluksemme saa paljon pyyntöjä, jolloin asiakkaalle ei tarvitse näyttää aina viimeisintä tietoa. Voimme siis toteuttaa pyynnön asynkronisesti.

Javan valmista kalustoa käyttämällä voi tehdä pyynnön annettuun osoitteeseen esimerkiksi seuraavasti:

    public static void main(String[] args) throws Exception {
        URLConnection conn = new URL("http://t-avihavai.users.cs.helsinki.fi/slow/Service").openConnection();
        Scanner sc = new Scanner(conn.getInputStream());
        while(sc.hasNextLine()) {
            System.out.println(sc.nextLine());
        }
    }

Palvelun rajapinta

Oman sovelluksemme palvelun -- joka kapseloi osoitteessa http://t-avihavai.users.cs.helsinki.fi/slow/Service toimivan palvelun -- tulee tarjota kaksi erillistä toimintoa. Yksi on pyynnön tekemiseen, toinen on tiedon pyytämiseen. Rajapinta on siis seuraavanlainen

public interface MaailmaPalvelu {
  void pyyda();
  String lue();
}

Jatka allaolevaa toteutusta siten, että luet pyynnön vastauksen viimeisintieto-muuttujaan.

public class MaailmaPalveluImpl implements MaailmaPalvelu {
    private String viimeisinTieto;

    @Override
    public void pyyda() {
        // tee pyyntö osoitteeseen http://t-avihavai.users.cs.helsinki.fi/slow/Service ja tallenna saatu vastaus
        // muuttujaan viimeisinTieto
    }

    @Override
    public String lue() {
        return viimeisinTieto;
    }
}

Kontrolleri ja sivu

Luo palvelulle kontrolleri ja näkymä. Näkymässä ei tarvitse olla muuta kuin palautunut tieto. Testaa palveluasi -- testatessa huomaat että pyyntö ei ole asynkroninen ja joudut odottamaan tiedon hakemista.

Asynkroniset metodikutsut

Lisää MaailmaPalveluImpl-toteutukseen pyyda-metodikutsun yläpuolelle annotaatio @Async, ja yritä uudelleen.

Huomaa että viestin saapumiseen kestää hetki, eli aivan aluksi palvelussasi muuttujan viimeisinTieto arvo on aluksi null.

Onneksi olkoon! Olet toteuttanut (ehkä) elämäsi ensimmäisen asynkronisen palvelun.

Kun olet valmis, ruksaa tehtävä tehdyksi osoitteessa http://t-avihavai.users.cs.helsinki.fi/tester5 olevassa palvelussa.

Kuten huomasit, asynkronisten metodikutsujen tekeminen Springin avulla on melko helppoa. Kuten normaalisti, tähänkin liittyi konfiguraatiota. Springin konfiguraatiotiedostoon oli lisätty seuraavat rivit:


 xmlns:task="http://www.springframework.org/schema/task"
 xsi:schemaLocation="
        ...
        http://www.springframework.org/schema/task
          http://www.springframework.org/schema/task/spring-task-3.1.xsd
        ..."

Yllä määrittelimme nimiavaruuden task varten. Tämän jälkeen tarvitsemme vain rivin, joka sanoo että tehtävien hallinta hoidetaan annotaatioiden avulla.

    <task:annotation-driven />

Voimme vastaavasti toteuttaa palveluita, jotka tekevät toiminnallisuuksia 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. Seuraava komponentti -- huomaa annotaatio @Component -- Spring lataa komponentin käyttöön sovelluksen alustuksessa, tulostaa jokaisen minuutin ensimmäisellä sekunnilla tämänhetkisen ajan.

@Component
public class AllOkService {

    @Scheduled(cron = "1 * * * * *")
    public void minuteHasPassed() {
        System.out.println(new Date());
    }
}

Lisää Springin Async- ja Scheduled-annotaatiosta mm. osoittteessa http://static.springsource.org/spring/docs/current/spring-framework-reference/html/scheduling.html.

Viestijonot

Palveluorientoituneissa arkkitehtuureissa oleellista on palveluiden välillä kulkevien pyyntöjen ja vastausten säilyminen. Yksi 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).

Riippuen palvelusta, viestejä kuluttavat palvelut voivat lähettää vastauksen takaisin viestijonoon, josta viesti joko haetaan myöhemmin tai lähetetään viestin tarvitsevalle palvelulle. Viestijonot, jotka lähettävät viestin takaisin, toteuttavat yleensä varmistustoiminnallisuuden -- jos vastaanottaja ei ole päällä, lähetetään viesti uudelleen myöhemmin.

Viestijonot mahdollistavat myös pub/sub -toiminnallisuuden (publish, subscribe), jossa sovellukset voivat kirjautua viestijonoon ja saada viestejä sitä mukaan kun niitä saapuu. Tämä on hyödyllistä esimerkiksi RSS-syötteissä, pelituloksissa ja vastaavissa sovelluksissa, missä päivitykset eivät ole säännöllisiä vaan tapahtuvat silloin tällöin.

Viestijonostandardeja on useita, Javalla on oma -- suhteellisen pitkään käytössä ollut -- JMS, jonka toteuttaa mm. ActiveMQ. Uudemmissa sovelluksissa AMQP-protokolla on kasvattanut suosiotaan -- AMQP:n toteuttaa muunmaussa RabbitMQ.

Viestijonot -- kuten palveluorientoituneet arkkitehtuurit yleensäottaen -- mahdollistavat eri sovellusten helpon integroitumisen. Viestejä tuottava sovellus voi olla toteutettu esimerkiksi RoR:illa, kun taas viestejä vastaanottava sovellus voidaan toteuttaa esimerkiksi Javalla.

ActiveMQ

Lataa ActiveMQ osoitteesta http://activemq.apache.org/activemq-543-release.html. Yleinen *nix-versio käy hyvin.

Kun olet ladannut pakkauksen, pura se sinulle sopivaan kansioon. Käynnistä ActiveMQ seuraavaksi komennolla:

apache-activemq-5.4.3 $ ./bin/activemq start

Springin konfigurointi

Spring tarvitsee ActiveMQ:ta varten erillisen konffin. Lataa projektipohja osoitteesta https://github.com/avihavai/wad-2012/tree/master/v5-activemq. Projektipohjaan on konfiguroitu JMS-template, jota Spring käyttää. Projektipohja myös olettaa että ActiveMQ on käynnissä portissa 61616, joka on ActiveMQ:n oletusportti.

ActiveMQ:n varsinainen konfiguraatio on tiedostossa spring-activemq.xml, joka sijaitsee WEB-INF -kansiossa. Tämän lisäksi pom.xml-tiedostoon on lisätty tarpeelliset riippuvuudet.

JmsSender

Projektissa on määritelty rajapinta JmsSender, joka mahdollistaa viestien lähettämisen. Toteuta rajapinta siten, että lähetät viestin jonoon nimeltä "the_queue".

Voit lähettää viestin JmsTemplate-luokan avulla (saat sen @Autowired-annotaatiolla käyttöösi). JmsTemplatella on metodi send, jolle määritellään sekä jonon nimi, että lähetettävä viesti. Voit määritellä lähetettävän viestin MessageCreator -rajapinnan avulla seuraavasti:

  MessageCreator messageCreator = new MessageCreator() {
  
      @Override
      public javax.jms.Message createMessage(javax.jms.Session sn) throws JMSException {
          return sn.createTextMessage(message);
      }
  };

Huomaa että viestillä (message) tulee olla määrittely final, jotta voit asettaa sen toisen luokan sisään.

JmsReader

Projektissa on määritelty myös rajapinta JmsReader, joka mahdollistaa viestien lukemisen. Toteuta rajapinta siten, että luet viestit jonosta nimeltä "the_queue".

Voit lukea viestin JmsTemplate-luokan avulla -- kuten edellisessä osassa, saat sen käyttöösi @Autowired-annotaatiolla. JmsTemplatella on metodi receive, jolle määritellään jono josta viesti luetaan. Huomaa että viesti on muotoa javax.jms.Message vaikka lähetetty viesti oli muotoa TextMessage. Voit muuttaa tyypin castaamalla.

Testaaminen

Varmista että sovelluksesi toimii. Kutsu sovelluksen kotiosoitteeseen, .../home, lähettää ensin viestin viestijonoon, jonka jälkeen se pyytää viestiä viestijonolta (kts. HomeController). Varmista että saat viestin jonolta. Jos et, palaa taaksepäin.

Kun olet valmis, sulje ActiveMQ komennolla.

apache-activemq-5.4.3 $ ./bin/activemq stop

Ruksaa tehtävä tehdyksi osoitteessa http://t-avihavai.users.cs.helsinki.fi/tester5 olevassa palvelussa.

Viestijonoon lähetettävät viestit voisivat hyvin olla esimerkiksi JSON-muotoisia viestejä. Kuten huomasit, viestijonot ovat yleensä käyttäjän nimeämiä, ja viestijonoja ylläpitävä palvelin voi hallinnoida useampaa jonoa samaan aikaan.

Lisää tietokannoista

JPA ja olioiden tila

Olemme tähän mennessä käyttäneet JPA:ta erilaisten rajapintojen kautta pohtimatta suuremmin sen taustatoteutusta. JPA-rajapinnan toteuttavat sovellukset eivät ole vain abstraktio relaatiotietokannan päälle, vaan ne hallinnoivat myös tietokantaan tallennettavien olioiden tilaa. Tutkimme aiemmin PlayerService-esimerkkiä, jossa uusi pelaaja tallennetaan joukkueeseen määritellyn cascade-määreen avulla.

@Service
public class PlayerServiceImpl implements PlayerService {

    @Autowired
    private PlayerRepository playerRepository;

    @Autowired
    private TeamRepository teamRepository;

    // ... 

    @Override
    @Transactional
    public void saveOrUpdate(Player player, Long teamId) {
        Team t = teamRepository.findOne(teamId);
        t.addPlayer(player);
        teamRepository.save(t);
    }
}

Koska tietokannasta ladatuilla olioilla on tila, jota hallinnoidaan sovelluskehyksen puolesta, ylläoleva toiminnallisuus voidaan toteuttaa myös seuraavasti.

@Service
public class PlayerServiceImpl implements PlayerService {

    @Autowired
    private PlayerRepository playerRepository;

    @Autowired
    private TeamRepository teamRepository;

    // ... 

    @Override
    @Transactional
    public void saveOrUpdate(Player player, Long teamId) {
        Team t = teamRepository.findOne(teamId);
        t.addPlayer(player);
    }
}

Yllä JPA:n toteuttava ORM-sovelluskehys tallentaa pelaajan hallinnoituun joukkueeseen puolestamme. Tämä onnistuu vain, koska joukkueella on määriteltynä Cascade-määre pelaajiin liittyen.

Käytännössä siis JPA pitää yllä viitteitä olioihin transaktioiden ajan. Transaktion aikana hallinnoituihin olioihin tehdyt muutokset tehdään myös tietokantaan. Hallinnoidut oliot ovat olioita jotka on haettu tietokannasta tai joihin on luotu viite. Esimerkiksi seuraavassa meillä on viite hallinnoituun olioon.

    @Override
    @Transactional
    public void savePlayer(Player player) {
        player = playerRepository.save(player);
        // nyt player-viite on hallinnoitu
        player.setName("Bonus");
        // transaktion jälkeen tietokannassa oleva
        // player-oliota vastaavan rivin name-arvo on Bonus 
    }

Klassinen virhe on olla ottamatta viitettä haltuun.

    @Override
    @Transactional
    public void savePlayer(Player player) {
        playerRepository.save(player);
        
        // pelaaja on tallennettu kantaan, mutta player-viite ei ole
        // olio, joka on hallinnoitu. Hallinnoidun olion viite olisi
        // saatu ylläolevalta save-metodilta
        player.setName("Bonus");
        // transaktion jälkeen tietokannassa oleva
        // player-oliota vastaavan rivin name-arvo ei ole muuttunut 
    }

Entiteettien hallinnointi on tarpeen esimerkiksi sovelluksissa, joissa tietokantaan talletettavia olioita käsitellään suoraan käyttöliittymästä: esimerkiksi JSF-käyttöliittymäkirjasto perustuu osittain siihen, että näytettävien olioiden tilaa hallinnoidaan pyyntöjen yli.

Reseptipalvelu

Toteuta sovellus, johon tallennetaan ruokareseptejä. Jokaiseen reseptiin kuuluu yksi tai useampi raaka-aine. Samaa raaka-ainetta voi käyttää useampi resepti.

Sovelluksen tulee mahdollistaa reseptien listaus, raaka-aineiden ja reseptien lisääminen, sekä reseptien poistaminen. Kun resepti poistetaan, varmista etteivät reseptiin liittyvät raaka-aineet viittaa poistettuun eli olemattomaan reseptiin.

Luo reseptejä varten myös näkymät. Validoi mahdolliset lomakkeet jottei käyttäjä pääse tekemään ilkeyksiä.

Kun olet valmis, ruksaa tehtävä tehdyksi osoitteessa http://t-avihavai.users.cs.helsinki.fi/tester5 olevassa palvelussa.

NoSQL

Olemme käyttäneet kurssilla tiedon varastointiin relaatiotietokantoja. Relaatiotietokannat toimivat käytännössä erinomaisesti suurimpaan osaan web-sovelluskehityksen tarpeista, ne tarjoavat komentotulkin tiedon tarkasteluun, ja niiden sisältö on kaikille käyttäjille sama. Haasteena ympäristöissä joissa kyselyiden määrä on suuri on skaalautuvuus.

NoSQL (alunperin tietokannat, joissa ei perinteistä SQL-tulkkia, nykyään not only SQL) -tietokannat pyrkivät nopeuteen luotettavuuden kustannuksella. NoSQL-tietokannat noudattavat yleensä BASE-periaatetta ACID-periaatteen sijaan. Yleisesti ottaen NoSQL-kannoissa yksinkertaiset kyselyt voivat olla monia kertoja nopeampia kuin perinteisiin SQL-kantoihin verrattaessa -- vastaavasti monimutkaisissa kyselyissä relaatiotietokantojen kyselynoptimoijat ovat edellä.

NoSQL vai YesSQL

Lue artikkelit http://www.tietokone.fi/lehti/tietokone_12_2009/sql_on_tiensa_paassa_8077, http://blog.dynatrace.com/2011/10/05/nosql-or-rdbms-are-we-asking-the-right-questions/ ja http://www.thewindowsclub.com/difference-sql-nosql-comparision.

Milloin itse valitsisit NoSQL-tietokannan, ja milloin relaatiotietokannan?

Vastaa osoitteessa http://t-avihavai.users.cs.helsinki.fi/tester5 olevaan palveluun.

Viikon 5 tunnit

Kirjaa osoitteessa http://t-avihavai.users.cs.helsinki.fi/tunnit5 olevaan palveluun viidennen viikon materiaalin ja tehtävien parissa käyttämäsi aika.

Tietoturva

OWASP top ten

Vierailuluento, Jani Kirmanen -- Silverskin Oy

OWASP (Open Web Application Security Project) on verkkosovelluksien tietoturvaan keskittynyt kansainvälinen järjestö. Sen tavoitteena on tiedottaa tietoturvariskeistä ja sitä kautta edesauttaa turvallisten web-sovellusten kehitystä. OWASP-yhteisö pitää yllä listaa merkittävimmistä web-tietoturvariskeistä. Vuoden 2010 lista oli seuraava:

  1. Injection

  2. Cross-Site Scripting (XSS)

  3. Broken Authentication and Session Management
  4. Insecure Direct Object References

  5. Cross-Site Request Forgery (CSRF)

  6. Security Misconfiguration

  7. Insecure Cryptographic Storage

  8. Failure to Restrict URL Access

  9. Insufficient Transport Layer Protection

  10. Unvalidated Redirects and Forwards

Tutustu listaan tarkemmin osoitteessa https://www.owasp.org/index.php/Category:OWASP_Top_Ten_Project.

HTTPS

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.

Autentikointi

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 HTTPS-yhteyden yli palvelimelle. Kiitettävä osa nykyaikaisista sovelluskehyksistä tarjoaa autentikointimekanismin osana tietoturvakomponenttiaan. Tietoturvatoteutukset toimivat yleensä joko sovelluksen päällä filtteröiden sovellukselle tehtäviä pyyntöjä, tai osana sovellusta tarkistellen myös sovelluksen sisäistä toimintaa.

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 OpenID:n avulla sovelluskehittäjä voi antaa käyttäjilleen mahdollisuuden käyttää jo olemassaolevia tunnuksia.

Käyttöoikeudet

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, osalla web-sovelluskehyksistä on kehittyneempi tietoturvakomponentti, joka mahdollistaa myös sovelluksen käyttämien tietorakenteiden sisällön rajoittamisen käyttäjä- tai roolikohtaisesti.

Spring Security

Spring-sovelluskehyksen tietoturvakomponentti on Spring Security (aiemmin Acegi Security). Spring tarjoaa tietoturvakomponenttinsa kautta autentikoinnin sekä käyttöoikeuksien hallinnan.

Spring Securityn käyttöönotto

Web-sovelluksille kohdistuvat pyynnöt JavaEE-maailmassa kulkevat ensiksi palvelimelle. Palvelin pitää kirjaa sovelluksista, ja ohjaa pyynnön oikealle sovellukselle. JavaEE-standardissa on määritelty filtterit, joita voidaan suorittaa ennen pyynnön ohjautumista Servleteille -- näimme aiemmin jo erään filtterin käyttötapauksen lisätessämme automaattisen merkistökoodauksen sovelluksellemme kappaleessa 11 Merkistöongelmista.

Spring security voidaan ottaa käyttöön vain filtterinä, jolloin se tarkistaa että pyynnöt ohjautuvat oikeisiin resursseihin, tai osana Spring-sovelluskehystä, jolloin voimme kontrolloida pyyntöjä metoditasolla. Koska käytämme komponenttipohjaista arkkitehtuuria, tarvitsemme Spring Securityyn liittyvät riippuvuudet projektiimme. Saamme ne haettua Mavenista kuten riippuvuudet yleensäkin:

        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-web</artifactId>
            <version>3.1.0.RELEASE</version>
            <exclusions>
                <exclusion>
                    <groupId>org.springframework</groupId>
                    <artifactId>spring-tx</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-taglibs</artifactId>
            <version>3.1.0.RELEASE</version>
        </dependency>
        
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-acl</artifactId>
            <version>3.1.0.RELEASE</version>
        </dependency>

Yllä haemme tietoturvapuoleen liittyvät oleelliset komponentit. Spring Securityn versiolla 3.1.0.RELEASE on riippuvuus komponentin spring-tx versioon 3.0.6.RELEASE. Tämän komponentin uudempi versio tulee muiden käyttämiemme komponenttien mukana, joten olemme poistaneet sen exclusion-merkinnällä.

Hyvä tapa tarkistella riippuvuuksia on esimerkiksi NetBeansin integroitu riippuvuusverkko-toiminnallisuus. Komentoriviä ja mavenia käytettäessä taas komento mvn dependency:tree näyttää projektin riippuvuudet.

Riippuvuuksien lataamisen lisäksi tarvitsemme filtterimäärittelyn web.xml-tiedostoon. Web.xml on tiedosto, jonka palvelimet lukevat ensin sovellusta ladattaessa.

    <!-- konfiguraatiotiedosto tietoturvalle -->
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>/WEB-INF/spring-security.xml</param-value>
    </context-param>
        
    <!-- Filtteri: security, kaikki pyynnöt filtterin läpi -->
    <filter>
        <filter-name>springSecurityFilterChain</filter-name>
        <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
    </filter>

    <filter-mapping>
        <filter-name>springSecurityFilterChain</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

Filtterimäärittelyssä kerromme ensin sovelluksen kontekstille konfiguraatiotiedoston sijainnin -- Spring Security hakee konfiguraationsa web-sovelluksen kontekstista. Tämän jälkeen määrittelemme filtterin springSecurityFilterChain, joka toimii käytännössä välittää pyyntöjä eteenpäin sovelluksellemme. Kuuntelemme kaikkia sovellukseen liittyviä osoitteita (<url-pattern>/*</url-pattern>).

Oleellinen osa konfiguraatiosta on luotuna, nyt sovellukseemme tulevat pyynnöt kulkevat tietoturvafiltterin läpi. Tämän lisäksi määrittelemme tietoturvaan liittyvät asetukset. Esimerkissämme rajoitetaan pääsy tiettyihin osoitteisiin (admin/ ja student/), ja luodaan käyttöön kirjautumislomake ja logout-osoite. Lisäksi määritellään käyttäjät -- normaalisti nämä tulevat esimerkiksi tietokannasta.

<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="http://www.springframework.org/schema/security"
             xmlns:beans="http://www.springframework.org/schema/beans"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="
        http://www.springframework.org/schema/beans 
            http://www.springframework.org/schema/beans/spring-beans-3.1.xsd 
        http://www.springframework.org/schema/security 
            http://www.springframework.org/schema/security/spring-security-3.1.xsd">

    <!-- osoitteiden rajoitus -->
    <http use-expressions="true">
        <intercept-url pattern="/admin/**" access="hasRole('lecturer') or hasRole('assistant')" />
        <intercept-url pattern="/student/**" access="hasRole('student')" />
        <intercept-url pattern="/**" access="permitAll" />
        
        <!-- huom! kun teet sovellusta tuotantokäyttöä varten, permitAll-oletus ei ole hyvä idea! /** -->
        
        <!-- näytä kirjautumislomake tarvittaessa -->
        <form-login />
        <!-- mahdollisuus logouttiin, ohjaus logoutin jälkeen osoitteeseen /home -->
        <logout logout-success-url="/home" />
    </http>
    
    <!-- dummy-käyttäjät -->
    <authentication-manager>
        <authentication-provider>
            <user-service>
                <user name="matti" password="bonus" authorities="lecturer, assistant, student" />
                <user name="mikael" password="mccartney" authorities="assistant, student" />
                <user name="arto" password="av" authorities="student" />
            </user-service>
        </authentication-provider>
    </authentication-manager>
</beans:beans>

Oleellista ylläolevassa konfiguraatiossa on Springin EL-kieli, jonka avulla voimme vaatia erilaisia käyttäjärooleja eri tilanteisiin. Esimerkiksi rivi:

        <intercept-url pattern="/admin/**" access="hasRole('lecturer') or hasRole('assistant')" />

Sanoo että kaikiin pyyntöihin jotka osoitetaan osoitteeseen sovellus/admin/ tarvitaan joko rooli lecturer tai rooli assistant. Roolit määritellään yllä authentication-provider -elementin user-service -osiossa.

Osoitteet kiinniottavat intercept-url-elementit tarkistetaan yksi kerrallaan ylhäältä alaspäin kun pyyntöä käsitellessä. Pyyntö pysähtyy ensimmäiseen vastaantulevaan kieltoon. Tekstihahmo /** tarkoittaa kaikkia juurikansion _ja_ alihakemistojen kansioita ja tiedostoja, esimerkiksi yllä viimeinen sääntö antaa kaikkiin sovelluksen osoitteisiin oikeudet jos ja vain jos yksikään aiemmista ehdoista ei ole pysäyttänyt pyyntöä.

Nyt kun käynnistämme sovelluksen ja pyrimme osoitteeseen sovellus/admin/, Spring security ohjaa käyttäjän kirjautumissivulle jos hänellä ei ole oikeuksia sivuun.

Lähes kaikki tietoturvakehykset tarjoavat valmiin kirjautumissivun, johon pyynnöt ohjataan jos käyttäjä pyrkii resursseihin joihin hänellä ei ole oikeutta. Kun täytämme ylläolevaan lomakkeeseen käyttäjätunnuksen "arto" ja salasanan "av", saamme virheviestin. Toisaalta, jos yritämme tunnuksilla "matti" ja "bonus", pääsemme toivottuun osoitteeseen.

Springin lomake lähettää parametrit j_username ja j_password osoitteeseen /j_spring_security_check, jota Spring security kuuntelee. Logout-toiminnallisuuden tarjoamiseen riittää linkki osoitteeseen /j_spring_security_logout.

Oma login-sivu

Toteuta osoitteessa https://github.com/avihavai/wad-2012/tree/master/v6-runko olevaan runkoon oma login-sivusi. Pääset alkuun määrittelemällä form-login elementille osoitteen johon kirjautuessa halutaan siirtyä:

    <form-login login-page="/login" />

Vinkki: JSTL-tägikirjaston avulla saat luotua sovellukseen liittyvän osoitteen helposti komennolla <c:url value="/j_spring_security_check"/>. Muista mihin osoitteeseen lomakkeesi tiedot pitää lähettää _ja_ mitkä tiedot ovat, muista myös että salasanaa ei saa _koskaan_ lähettää GET-metodilla.

Kun olet valmis, ruksaa tehtävä tehdyksi osoitteessa http://t-avihavai.users.cs.helsinki.fi/tester6 olevassa palvelussa.

Metodien suojaaminen

Joskus haluamme suojata pyyntöjä metoditasolla. Metoditason pyyntöjen tarkistamista varten joudumme rekisteröimään Spring securityn konfiguraatiotiedostossa, jossa kontrollerit otetaan käyttöön. Viikon 6 esimerkkisovelluksessa tämä tiedosto on spring-context.xml. Tietoturvaa varten määrittelemme xml-nimiavaruuden:

// tämä lisätään nimiavaruuksien määrittelyyn
       xmlns:security="http://www.springframework.org/schema/security"
// nämä rivit xsi:schemaLocation-määrittelyyn
        http://www.springframework.org/schema/security 
           http://www.springframework.org/schema/security/spring-security-3.1.xsd

Tiedostojen määrittelyn jälkeen voimme käyttää niiden tarjoamia toimintoja. Elementti global-method-security tarjoaa määrittelyn metodien tarkasteluun. Huomaa että määrittelimme Spring Securityn nimiavaruudeksi security, joten käytämme etuliitettä security myös elementissä.

    <security:global-method-security pre-post-annotations="enabled"/>

Ylläoleva komento määrittelee käyttöömme metodien tarkastelun sekä annotaatiot, joilla voimme määritellä esi- ja jälkiehtoja metodien toiminnalle. Ehdot määritellään yleensä rajapintoihin, esimerkiksi seuraava SecureService-rajapinta määrittelee palvelun, jolla on kolme metodia:


public interface SecureService {

    @PreAuthorize("hasRole('lecturer')")
    public void executeOnlyIfAuthenticatedAsLecturer();

    @PreAuthorize("isAuthenticated()")
    public void executeOnlyIfAuthenticated();

    public void executeFreely();
}

Metodilla executeOnlyIfAuthenticatedAsLecturer on annotaatio @PreAuthorize("hasRole('lecturer')"), joka määrittelee että metodikutsun tekevän pyynnön tehneellä käyttäjän tulee olla kirjautunut roolilla lecturer. Metodikutsu executeOnlyIfAuthenticated vaatii että käyttäjä on kirjautunut, viimeinen metodikutsu ei vaadi käyttöoikeuksia.

Annotaation @PreAuthorize lisäksi mielenkiintoinen on @PostFilter, jonka avulla voimme poistaa arvoja metodin palauttamasta tietorakenteesta. Määritellään palvelu ObjectFactory, joka palauttaa kokoelman olioita. Määritellään lisäksi sääntö, että olioilla tulee olla kentän awesome-arvo false, tai arvo true _ja_ käyttäjällä rooli lecturer.

public interface ObjectFactory {
    @PostFilter("(!filterObject.awesome) or (filterObject.awesome and hasRole('lecturer'))")
    public List<SampleObject> getObjects();
}

Jos käyttäjällä ei ole roolia lecturer, ei metodin palauttama lista sisällä koskaan palauta olioita joissa kentän awesome arvo on true.

PostFilter-annotaatiolla ei tule korvata järkeviä kyselyitä. Filtteröinti tietokannassa on yleisesti ottaen huomattavasti tehokkaampaa kuin filtteröinti palvelimella.

PostFilter ja JPA

Mitä ongelmia @PostFilter-annotaatio voi tuoda JPA-kyselyiden kanssa? Miksi?

Kirjoita vastauksesi osoitteessa http://t-avihavai.users.cs.helsinki.fi/tester6 olevaan palveluun.

Yleisimmät Spring Securityn EL-kielen komennot

Komentoja voi myös kytkeä yhteen avainsanojen or ja and avulla. Lisää komentoja ja tietoa komennoista löytyy osoitteesta http://static.springsource.org/spring-security/site/docs/3.1.x/reference/el-access.html.

Käyttäjätunnuksen löytäminen

Haluamme usein päästä käsiksi tällä hetkellä kirjautuneeseen käyttäjään. Käyttäjään pääsee käsiksi kontrollereista hyödyntämällä Springin tarjoamaa dependency-injection -mekanismia. Spring security tallentaa käyttäjät palvelimelle Javan java.security.Principal -olioina. Voimme siis luoda kontrollerimetodin, johon olio injektoidaan:

@Controller
public class AuthenticatedController {

   @RequestMapping("/details")
   public void showDetails(Principal principal) {
       if (principal == null) {
           System.out.println("Not logged in!");
       } else {
           System.out.println("Logged in. Username: " + principal.getName());
       }
   }
}

Saamme halutessamme käyttäjätunnuksen käsiin myös tietoturvakontekstista pyynnöllä SecurityContextHolder.getContext().getAuthentication(). Pyyntö palauttaa Authentication-olion, josta saamme käyttäjätunnuksen tarvittaessa.

    Authentication auth = SecurityContextHolder.getContext().getAuthentication();
    if (auth == null) {
        System.out.println("Not logged in!");
    } else {
        System.out.println("Logged in. Username: " + auth.getName());
    }

Tietoturvakontekstin suora käyttö ei kuitenkaan ole kovin kaunista, sillä se sitoo meidät suoraan tietoturvatoteutukseen. Tyypillisempi lähestymistapa on tietoturvakontekstin kapselointi erilliseen palveluun esimerkiksi seuraavasti.

public interface CredentialsService {
    public String getName();
}
@Service
public class CredentialsServiceImpl implements CredentialsService {
    public String getName() {
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        if (auth == null) {
            return null;
        }

        return auth.getName();
    }
}

Käyttäjät tietokannasta

Aiemmassa esimerkissä käyttäjät listattiin suoraan konfiguraatiotiedostoon. Voimme myös ladata käyttäjät tietokannasta. Spring Security olettaa seuraavanlaisen tietokantaskeeman (kts http://static.springsource.org/spring-security/site/docs/3.1.x/reference/appendix-schema.html:

  create table users(
      username varchar_ignorecase(50) not null primary key,
      password varchar_ignorecase(50) not null,
      enabled boolean not null);

  create table authorities (
      username varchar_ignorecase(50) not null,
      authority varchar_ignorecase(50) not null,
      constraint fk_authorities_users foreign key(username) references users(username));
      create unique index ix_auth_username on authorities (username,authority);

Jos määrittelemme JPA-tietokantaoliot Users ja Authorities, jotka luovat täsmälleen ylläolevat taulut, voimme käyttää tietokantaa jdbc:n yli. Tällöin authentication-provider-konfiguraatio muuttuu seuraavanlaiseksi. Alla oletetaan että datalähteen nimi on dataSource.

    <authentication-provider>
        <jdbc-user-service data-source-ref="dataSource"/>
    </authentication-provider>

Toisaalta, voimme määritellä myös oman palvelun käyttäjän tietojen hakemiseen. 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.

Käyttäjien hallinta

Toteuta osoitteessa https://github.com/avihavai/wad-2012/tree/master/v6-runko-db olevaan runkoon opiskelijoiden hallintatoiminnallisuus.

Hallintatoiminnallisuus tarjoaa lecturer-roolissa oleville henkilöille (tällä hetkellä vain matti) mahdollisuuden opiskelijoiden lisäämiseen. Opiskelijoiden lisääminen näyttää lomakkeen, johon voidaan täyttää nimi, käyttäjätunnus ja salasana -- salasana on oletuksena "vaihda".

Kun opiskelija kirjautuu luennoitsijan luomilla tunnuksilla, hän näkee omat tietonsa (nimi, käyttäjätunnus ja salasana) lomakkeessa, ja voi muuttaa niitä.

Huom! Varmista että opiskelijalla on pääsy vain hänen omiin tietoihinsa!

Kun olet valmis, ruksaa tehtävä tehdyksi osoitteessa http://t-avihavai.users.cs.helsinki.fi/tester6 olevassa palvelussa.

JSP-sivujen näkyvyyden rajoittaminen käyttäjäroolien perusteella

Spring Security tarjoaa myös taglibin JSP-sivuja varten. Taglib-määrittely on seuraavanlainen.

<%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %>

Security taglibin oleellisin komento on authorize, jonka avulla voimme rajoittaa käyttäjien toimintaa esimerkiksi roolien perusteella. Attribuutille access voidaan määritellä käyttöoikeudet Springin EL-kielellä. Esimerkiksi seuraava JSP-sivu lisää Youtube-videon vain roolille lecturer

.

<%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %>
<html>
....

<sec:authorize access="hasRole('lecturer')"> 
  <!-- video näkyy vain roolin lecturer omaaville käyttäjille -->
  <iframe width="853" height="480" 
        src="http://www.youtube.com/embed/kfVsfOSbJY0" 
        frameborder="0" allowfullscreen></iframe>
</sec:authorize>
...

Voimme myös hyödyntää sovelluksessa määriteltyjä käyttöoikeuslistoja authorize-tagin avulla. Attribuutti url tarjoaa mahdollisuuden rajoittaa näkyvyyden resursseihin vain niille, keillä olisi pääsy tiettyyn osoitteeseen. Alla rajoitamme edellisen videon näyttämisen vain henkilöille, joilla on pääsy osoitteeseen /admin.

<%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %>
<html>
....

<sec:authorize url="/admin"> 
  <!-- video näkyy vain osoitteeseen /admin pääseville käyttäjille -->
  <iframe width="853" height="480" 
        src="http://www.youtube.com/embed/kfVsfOSbJY0" 
        frameborder="0" allowfullscreen></iframe>
</sec:authorize>
...

Käyttäjien hallinta, osa 2

Muokkaa edellisessä sovelluksessa olevaa lomaketta siten, että opiskelija voi muokata vain nimeään ja salasanaansa. Lisää sovellukseen etusivulle (osoitteeseen /home) myös opiskelijoiden nimien listaus. Jos käyttäjällä on rooli lecturer, saa hän nähdä myös opiskelijoiden käyttäjätunnuksen.

Kun olet valmis, ruksaa tehtävä tehdyksi osoitteessa http://t-avihavai.users.cs.helsinki.fi/tester6 olevassa palvelussa.

Käyttäjien hallinta, osa 3

Lisää sovellukseen kurssit. Jokaisella kurssilla on nimi, yksi luennoija, ja joukko opiskelijoita. Luennoijat voivat luoda kursseja, kurssin luova luennoitsija asetetaan oletuksena myös luennoijaksi. Kurssit listataan etusivulla osoitteessa /home, jos käyttäjä on kirjautunut, voi hän myös ilmoittautua kurssille (kurssin vieressä on tällöin linkki ilmoittaudu). Ilmoittaudu-linkki saa näkyä vaikka opiskelija on jo ilmoittautunut kurssille -- varmista kuitenkin että sama henkilö ei voi ilmoittautua samalle kurssille useasti.

Näytä etusivulla myös kullekin kurssille ilmoittautuneiden määrä!

Tämä tehtävä vastaa kahta tehtävämerkintää

Kun olet valmis, ruksaa tehtävä tehdyksi osoitteessa http://t-avihavai.users.cs.helsinki.fi/tester6 olevassa palvelussa.

Taas pilvessä!

Tökkel-palvelumme -- vaikka huikea onkin -- käytti vain muistiin ladattavaa tietokantaa. Pilvipalvelut toimivat siten, että sovellus poistetaan käytöstä silloin kun sille ei ole kysyntää. Tämä tarkoittaa sitä, että tökkelkin -- kuten muutkin vähän käytössä olevat sovellukset -- poistetaan muistista silloin kun niitä ei tarvita. Tämä johtaa siihen, että muistiin ladattavan tietokannan tiedot katoavat.

Tarvitsemme siis konkreettisen tietokannan käyttöömme. Pilvipalveluiden tarjoajat tarjoavat yleensä myös tietokantakonfiguraatioita sovelluskehittäjille. Amazonin kautta voisi käyttää esimerkiksi MySQL:ää, Heroku taas tarjoaa PostgreSQL-tietokannan käyttöön. Oikeastaan, jokaisella herokun käyttäjällä on käytössä 5 megatavun testitietokanta.

StudentDevs

Käytä tässä sovelluksessa osoitteessa https://github.com/avihavai/wad-2012/tree/master/v6-pilveen olevaa sovelluspohjaa. Sovellukseen on valmiiksi konfiguroitu herokun PostgreSQL-tuki tuotantoympäristöön, kehitysympäristössä sovellus käyttää muistiin ladattavaa tietokantaa.

StudentDevs -palvelu tarjoaa toiminnallisuuden opiskelijoiden ja ohjelmointitöitä tarjoavien asiakkaiden lähentämiseksi. Asiakkaat voivat lisätä palveluun projekteja, joista opiskelijat voivat tehdä tarjouksen. Tulevaisuudessa palvelu tarjoaa paljon muutakin -- toteutetaan aluksi perustoiminnallisuudet.

Toteuta StudentDevs -palvelun runko. Rungossa tulee olla opiskelijoita ja asiakkaita. Asiakkaat pystyvät luomaan projekteja, joilla on nimi, kuvaus ja tykkäysten määrä. Asiakkaat voivat myös päivittää lisäämiensä projektien tietoja. Opiskelijat voivat listata projektit, jolloin projektista näkyy nimi ja tykkäysten määrä. Opiskelijat voivat tykätä projektista, joka tapahtuu projektin nimen vieressä olevaa +-linkkiä klikkaamalla. Tällöin projektiin liittyvä tykkäysten määrä kasvaa yhdellä. Kukin opiskelija voi tykätä yhdestä projektista vain kerran. Yksittäisen projektin tietoja tulee päästä katsomaan projektilistassa olevien linkkien avulla.

Huom! Sinun ei tarvitse toteuttaa sivuja, joissa opiskelijoita ja asiakkaita luodaan. Kannattanee toteuttaa tätä varten erillinen kontrolleri, joka lisää opiskelijoita ja asiakkaita tietokantaan.

Varmista että kaikki sovelluksesi kentät validoidaan, eli että ilkeä käyttäjä ei pysty tekemään tuhoja. Asiakkaat saavat muokata vain omia projektejaan.

Lisää sovellus lopulta herokuun. Avainaskeleet:

1. heroku login
2. git init (sovelluksen juurikansiossa)
3. git add .
4. git commit -m "initial commit"
5. heroku create --stack cedar
6. git push heroku master

Jatkossa sovellukseen tehtyjä muutoksia voi lisäillä komennoilla "git add tiedosto", "git commit -am "commitviesti"", "git push heroku".

Tämä tehtävä vastaa kolmea tehtävämerkintää

Kun olet valmis, lisää palvelun heroku-osoite osoitteessa http://t-avihavai.users.cs.helsinki.fi/tester6 olevaan palveluun.

Kokeesta

Kurssin koe järjestetään henkilökohtaisena tai pienryhmissä tapahtuvana näyttökokeena, jossa osallistuja osoittaa osaamisensa kuulustelijoille. Näyttökokeessa keskustellaan ei-ennalta sovituista kurssiin liittyvistä teemoista, joiden hallitsemista ja ymmärtämistä edellytetään osallistujalta.

Esimerkkikysymyksiä koetilanteessa:

Valmistaudu esittelemään toteuttamiasi sovelluksia koetilanteessa. Kokeessa katsotaan useampaa sovellusta, yhden saat valita itse, loput päätetään palauttamiesi sovellusten joukosta. Oleellista sovelluksessa on hyvä rakenne, järkevä muuttujien nimentä ja jatkokehityksen helppous.

Arvostelusta: Kurssin arvostelu perustuu pitkälti tehtyjen tehtävien määrään. Jos koetilanteessa tulee ilmi että hallitset aihepiirin hyvin, voidaan harjoitustehtävien määrän määräämää arvosanaa nostaa yhdellä. Jos taas koetilanteessa huomataan että et ole itse tehnyt tehtäviä, arvosanaa voidaan laskea hylättyyn asti.

Koeilmoittautumisesta tulee lisätietoa lähipäivinä.

Rästitehtäviä

Jos haluat paikata aiemmilla viikoilla väliin jääneitä rästitehtäviä ylimääräisillä harjoituksilla, ota yhteyttä.

Kurssipalaute

Käy antamassa kurssipalautetta osoitteessa https://ilmo.cs.helsinki.fi/kurssit/servlet/Valinta. Kirjoita palautteeseen erityisesti mitä odotit kurssilta?, ja miten kurssia voisi mielestäsi parantaa seuraavaa toteutusta ajatellen? -- olettaen että kurssi järjestetään uudelleen.

Vastapalautteessa otetaan huomioon myös tuntikirjauksia tehdessä tehdyt palautteet. Kun kirjoitat palautetta viimeisen viikon tehtävistä, oletamme että annoit myös kurssipalautetta.

Viimeisen viikon tunnit

Kirjaa osoitteessa http://t-avihavai.users.cs.helsinki.fi/tunnit6 olevaan palveluun kuudennen viikon materiaalin ja tehtävien parissa käyttämäsi aika.