Sisällysluettelo
Tehtävät
Osa 4
Neljäs osio alkaa kertaustehtävällä, jossa täydennetään varauskalenterin toiminnallisuutta. Tämän jälkeen keskitymme web-ohjelmistoille tyypilliseen ohjelmistokehitysprosessiin sekä tähän liittyviin oleellisiin työvälineisiin. Lopulta tutustumme REST-arkkitehtuurimalliin.
Osa 3, kertaus: Reservations
Tässä tehtävässä tehtävänäsi on täydentää kesken jäänyttä varaussovellusta siten, että kaikki käyttäjät näkevät varaukset, mutta vain kirjautuneet käyttäjät pääsevät lisäämään varauksia.
Kun käyttäjä tekee pyynnön sovelluksen juuripolkuun /reservations
, tulee hänen nähdä varaussivu. Allaolevassa esimerkissä tietokannassa ei ole varauksia, mutta jos niitä on, tulee ne listata kohdan Current reservations alla.
Jos kirjautumaton käyttäjä yrittää tehdä varauksen, hänet ohjataan kirjautumissivulle.
Kun kirjautuminen onnistuu, voi käyttäjä tehdä varauksia.
Sovelluksen tulee kirjautumis- ja varaustoiminnallisuuden lisäksi myös varmistaa, että varaukset eivät mene päällekkäin.
Luokassa DefaultController
luodaan muutamia testikäyttäjiä, joita voi (esimerkiksi) käyttää sovelluksen testauksessa. Tarvitset ainakin:
- Palvelun käyttäjän tunnistautumiseen (esim. CustomUserDetailsService, kts. tehtävä 35), jolla täydennät luokkaa SecurityConfiguration
- Tavan aikaleimojen käsittelyyn (kts. esim. tehtävä 30)
- Kontrollerin varausten käsittelyyn ja tekemiseen
Käyttäjät ja oikeudet
Käyttäjillä on usein erilaisia oikeuksia sovelluksessa. Verkkokaupassa kaikki voivat listata tuotteita sekä lisätä tuotteita ostoskoriin, mutta vain tunnistautuneet käyttäjät voivat tehdä tilauksia. Tunnistautuneista käyttäjistä vain osa, esimerkiksi kaupan työntekijät, voivat tehdä muokkauksia tuotteisiin.
Tällaisen toiminnan toteuttamiseen käytetään oikeuksia, joiden lisääminen vaatii muutamia muokkauksia aiempaan kirjautumistoiminnallisuuteemme. Aiemmin näkemässämme luokassa CustomUserDetailsService
noudettiin käyttäjä seuraavasti:
@Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { Account account = accountRepository.findByUsername(username); if (account == null) { throw new UsernameNotFoundException("No such user: " + username); } return new org.springframework.security.core.userdetails.User( account.getUsername(), account.getPassword(), true, true, true, true, Arrays.asList(new SimpleGrantedAuthority("USER"))); }
Palautettavan User
-olion luomiseen liittyy lista oikeuksia. Yllä käyttäjälle on määritelty oikeus USER
, mutta oikeuksia voisi olla myös useampi. Seuraava esimerkki palauttaa käyttäjän "USER" ja "ADMIN" -oikeuksilla.
return new org.springframework.security.core.userdetails.User( account.getUsername(), account.getPassword(), true, true, true, true, Arrays.asList(new SimpleGrantedAuthority("USER"), new SimpleGrantedAuthority("ADMIN")));
Oikeuksia käytetään käytettävissä olevien polkujen rajaamisessa. Voimme rajata luokassa SecurityConfiguration
osan poluista esimerkiksi vain käyttäjille, joilla on ADMIN
-oikeus. Alla olevassa esimerkissä kaikki käyttäjät saavat tehdä GET-pyynnön sovelluksen juuripolkuun. Vain ADMIN
-käyttäjät pääsevät polkuun /clients
, jonka lisäksi muille sivuille tarvitaan kirjautuminen (mikä tahansa oikeus). Kuka tahansa pääsee kirjautumislomakkeeseen käsiksi.
@Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers(HttpMethod.GET, "/").permitAll() .antMatchers("/clients").hasAnyAuthority("ADMIN") .anyRequest().authenticated(); http.formLogin() .permitAll(); }
Oikeuksia varten määritellään tyypillisesti erillinen tietokantataulu, ja käyttäjällä voi olla useampia oikeuksia.
Only for the Selected
Sovelluksessa on toteutettuna käyttäjienhallinta tällä hetkellä siten, että käyttäjillä ei ole erillisiä oikeuksia. Muokkaa sovellusta ja lisää sovellukseen käyttäjäkohtaiset oikeudet. Suojaa tämän jälkeen sovelluksen polut seuraavasti:
- Kuka tahansa saa nähdä polusta
/happypath
palautetun tiedon - Vain USER tai ADMIN -käyttäjät saavat nähdä polusta
/secretpath
palautetun tiedon - Vain ADMIN-käyttäjät saavat nähdä polusta
/adminpath
palautetun tiedon
Lisää sovellukseen myös seuraavat käyttäjät:
Käyttäjätunnus | Salasana | Oikeudet |
---|---|---|
larry | larry | USER |
moe | moe | USER ja ADMIN |
curly | curly | ADMIN |
Tyypillinen ohjelmistokehitysprosessi
Ohjelmiston elinkaareen kuuluu vaatimusmäärittely, suunnittelu, toteutus, testaus, sekä ylläpito ja jatkokehitys. Vaatimusmäärittelyyn kuuluu ohjelmistoon liittyvien toiveiden ja vaatimusten kartoitus, jota seuraa suunnittelu, missä pohditaan miten vaatimukset toteutetaan. Toteutusvaihe sisältää ohjelmointia sekä sovelluksen elinympäristöön liittyvien komponenttien yhteensovittamista. Testaukseen kuuluu sovelluksen testaus niin automaattisesti kuin manuaalisesti. Kun ohjelmisto tai sen osa on toiminnassa, tulee elinkaaren osaksi myös käytössä olevasta ohjelmistosta löytyvien virheiden korjaaminen sekä uusien ominaisuuksien kehittäminen.
Ohjelmointiin ja ohjelmistojen kehitykseen liittyy jatkuva etsiminen ja kokeileminen. Ongelmat pyritään ratkaisemaan kokeilemalla vaihtoehtoja kunnes ongelmaan löytyy sopiva ratkaisu. Jos ongelma on osittain tuttu, on tarkasteltavia vaihtoehtoja vähemmän, ja jos ongelma on tuttu, on siihen tyypillisesti ainakin yksi valmis ratkaisumalli. Tämän hetken suosituimmat ohjelmistokehitysmenetelmät (ketterät menetelmät kuten Scrum ja Kanban) ohjaavat työn läpinäkyvyyteen, oman työskentelyn kehittämiseen sekä siihen, että muiden osallistuminen ohjelmistokehitykseen on helppoa.
Ohjelmistoon liittyvät toiveet ja vaatimukset
Ohjelmistoon liittyvistä toiveista ja vaatimuksista keskustellaan asiakkaan ja käyttäjien kanssa, ja ne kirjataan muistiin. Vaatimukset kirjataan usein lyhyessä tarinamuodossa, joka kerrotaan uutta toiminnallisuutta toivovan henkilön näkökulmasta: "As a (käyttäjän tyyppi) I want (tavoite) so that (syy)." -- esimerkiksi "As a user I want to be able to view the messages so that I can see what others have written". Vaatimuksia kirjattaessa saadaan kuva ohjelmistolta toivotusta toiminnallisuudesta, jonka jälkeen toiminnallisuuksia voidaan järjestää tärkeysjärjestykseen.
Toiminnallisuuksien tärkeysjärjestykseen asettaminen tapahtuu yhdessä asiakkaan ja käyttäjien kanssa. Kun toiminnallisuudet ovat kutakuinkin tärkeysjärjestyksessä, valitaan niistä muutama kerrallaan työstettäväksi. Samalla varmistetaan asiakkaan kanssa, että ohjelmistokehittäjät ja asiakas ymmärtävät toiveen samalla tavalla. Kun toiminnallisuus on valmis, toiminnallisuus näytetään asiakkaalle ja asiakas pääsee kertomaan uusia toiminnallisuustoiveita sekä mahdollisesti uudelleenjärjestelemään vaatimusten tärkeysjärjestystä.
Vaatimuksia ja toiveita, sekä niiden kulkemista projektin eri vaiheissa voidaan käsitellä esimerkiksi Trello:n avulla. Ohje Trellon käyttöön.
Versionhallinta
Ohjelmiston lähdekoodin ja dokumentaatio tallennetaan keskitetysti versionhallintaan, mistä kuka tahansa voi hakea ohjelmistosta uusimman version sekä lähettää sinne uudemman päivitetyn version. Käytännössä jokaisella ohjelmistokehittäjällä on oma hiekkalaatikko, jossa ohjelmistoon voi tehdä muutoksia vaikuttamatta muiden tekemään työhön. Jokaisella ohjelmistokehittäjällä on yleensä samat tai samankaltaiset työkalut (ohjelmointiympäristö, ...), mikä helpottaa muiden kehittäjien auttamista.
Kun ohjelmistokehittäjä valitsee vaatimuksen työstettäväksi, hän tyypillisesti hakee projektin versionhallinnasta projektin uusimman version, sekä lähtee toteuttamaan uutta vaatimusta. Kun vaatimukseen liittyvä osa tai komponentti on valmis sekä testattu paikallisesti (automaattiset testit on olemassa, toimii ohjelmistokehittäjän koneella), lähetetään uusi versio versionhallintapalvelimelle.
Versionhallintapalvelin sisältää myös mahdollisesti useampia versioita projektista. Esimerkiksi git-mahdollistaa ns. branchien käyttämisen, jolloin uusia ominaisuuksia voidaan toteuttaa erillään "päähaarasta". Kun uusi ominaisuus on valmis, voidaan se lisätä päähaaraan. Versionhallinnassa olevia koodeja voidaan myös tägätä julkaisuversioiksi.
Yleisin versionhallintatyökalu on Git, joka on käytössä Githubissa. Ensiaskeleet Githubin käyttöön.
Jatkuva integraatio
Versionhallintapalvelin on tyypillisesti kytketty integraatiopalvelimeen, jonka tehtävänä on suorittaa ohjelmistoon liittyvät testit jokaisen muutoksen yhteydessä sekä tuottaa niistä mahdollisesti erilaisia raportteja. Integraatiopalvelin kuuntelee käytännössä versionhallintajärjestelmässä tapahtuvia muutoksia, ja hakee uusimman lähdekoodiversion muutoksen yhteydessä.
Kun testit ajetaan sekä paikallisella kehityskoneella että erillisellä integraatiokoneella ohjelmistosta huomataan virheitä, jotka eivät tule esille muutoksen tehneen kehittäjän paikallisella koneella (esimerkiksi erilainen käyttöjärjestelmä, selain, ...). On myös mahdollista että ohjelmistosta ei noudeta kaikkia sen osia -- ohjelmisto voi koostua useista komponenteista -- jolloin kaikkien vaikutusten testaaminen paikallisesti on mahdotonta. Jos testit eivät mene läpi integraatiokoneella, korjataan muutokset mahdollisimman nopeasti.
Työkaluja automaattiseen kääntämiseen ja jatkuvaan integrointiin ovat esimerkiksi Travis ja Coveralls. Travis varmistaa että viimeisin lähdekoodiversio kääntyy ja että testit menevät läpi, ja Coveralls tarjoaa välineitä testikattavuuden ja projektin historian tarkasteluun -- tässä hyödyksi on esimerkiksi Cobertura. Kummatkin ovat ilmaisia käyttää kun projektin lähdekoodi on avointa -- kumpikin tarjoaa myös suoran Github-tuen.
Travisin käyttöönottoon vaaditaan käytännössä se, että projekti on esimerkiksi Githubissa ja että sen juurikansiossa on travisin konfiguraatiotiedosto .travis.yml
. Yksinkertaisimmillaan konfiguraatiotiedosto sisältää vain käytetyn ohjelmointikielen -- travis osaa esimerkiksi päätellä projektin tyypin pom.xml
-tiedoston pohjalta. Ohje Traviksen käyttöönottoon.
Nopeasti näytille
Kun uusi vaatimus tai sen osa on saatu valmiiksi, kannattaa viedä palvelimelle palautteen saamista varten. On tyypillistä, että ohjelmistolle on ainakin Staging- ja Tuotanto-palvelimet. Staging-palvelin on lähes identtinen ympäristö tuotantoympäristöön verrattuna. Staging (usein myös QA)-ympäristöön kopioidaan ajoittain tuotantoympäristön data, ja se toimii viimeisenä testaus- ja validointipaikkana (Quality assurance) ennen tuotantoon siirtoa. QA-ympäristöä käytetään myös demo- ja harjoitteluympäristönä. Kun QA-ympäristössä oleva sovellus on päätetty toimivaksi, siirretään sovellus tuotantoympäristöön.
Tuotantoympäristö voi olla yksittäinen palvelin, tai se saattaa olla joukko palvelimia, joihin uusin muutos viedään hiljalleen. Tuotantoympäristö on tyypillisesti erillään muista ympäristöistä mahdollisten virheiden minimoimiseksi.
Käytännössä versioiden päivitys tuotantoon tapahtuu usein automaattisesti. Esimerkiksi ohjelmistoon liittyvä Travis-konfiguraatio voidaan määritellä niin, että jos kaikki testit menevät läpi integraatiopalvelimella, siirretään ohjelmisto automaattisesti tuotantoon. Esimerkiksi Herokussa sijaitsevaan sovellukseen muutokset voidaan hakea automaattisesti Githubista (ohje).
REST-Arkkitehtuurimalli
REST (representational state transfer) on ohjelmointirajapintojen toteuttamiseen tarkoitettu arkkitehtuurimalli (tai "tyyli"), joka määrittelee sovellukset tietoa käsittelevien osien (komponentit), tietokohteiden (resurssit), sekä näitä yhdistävien yhteyksien kautta.
Tietoa käsittelevät osat ovat selainohjelmisto, palvelinohjelmisto, ym. Resurssit ovat sovelluksen käsitteitä (henkilöt, kirjat, laskentaprosessit, laskentatulokset -- mikä tahansa voi käytännössä olla resurssi) sekä niitä yksilöiviä osoitteita. Resurssikokoelmat ovat löydettävissä ja navigoitavissa: resurssikokoelma voi löytyä esimerkiksi osoitteesta /persons
, /books
, /processes
tai /results
. Yksittäisille resursseille määritellään uniikit osoitteet (esimerkiksi /persons/1
), ja niillä on myös määritelty esitysmuoto (esimerkiksi HTML, JSON tai XML); dataa voi lähettää ja vastaanottaa samassa muodossa. Resursseja ja tietoa käsittelevien osien yhteys perustuu tyypillisesti asiakas-palvelin -malliin, missä asiakas tekee pyynnön ja palvelin kuuntelee ja käsittelee vastaanottamiaan pyyntöjä sekä vastaa niihin.
REST-rajapinnat ja Web-sovellukset
HTTP-protokollan yli käsiteltävillä REST-rajapinnoilla on tyypillisesti seuraavat ominaisuudet:
- Juuriosoite resurssien käsittelyyn (esimerkiksi
/books
) - Resurssien esitysmuodon määrittelevä mediatyyppi (esimerkiksi
HTML
,JSON
, ...), joka kertoo asiakkaalle miten resurssiin liittyvä data tulee käsitellä. - Resursseja voidaan käsitellä HTTP-protokollan metodeilla (GET, POST, DELETE, ..)
Kirjojen käsittelyyn ja muokkaamiseen määriteltävä rajapinta voisi olla esimerkiksi seuraavanlainen:
- GET-pyyntö osoitteeseen
/books
palauttaa kaikkien kirjojen tiedot. - GET osoitteeseen
/books/{id}
, missä{id}
on yksittäisen kirjan yksilöivä tunniste, palauttaa kyseisen kirjan tiedot. - PUT osoitteeseen
/books/{id}
, missä{id}
on yksittäisen kirjan yksilöivä tunniste, muokataan kyseisen kirjan tietoja. Kirjan uudet tiedot lähetetään osana pyyntöä. - DELETE osoitteeseen
/books/{id}
poistaa kirjan tietyllä tunnuksella. - POST osoitteeseen
/books
luo uuden kirjan pyynnön rungossa lähetettävän datan pohjalta. Palvelun vastuulla on päättää kirjalle tunnus.
Osoitteissa käytetään tyypillisesti substantiivejä -- ei books?id={id}
vaan /books/{id}
. HTTP-pyynnön tyyppi määrittelee operaation. DELETE-tyyppisellä pyynnöllä poistetaan, POST-tyyppisellä pyynnöllä lisätään, PUT-tyyppisellä pyynnöllä päivitetään tietoja, ja GET-tyyppisellä pyynnöllä haetaan.
Datan muoto on toteuttajan päätettävissä. Tällä hetkellä eräs suosituista datamuodoista on JSON, sillä sen käyttäminen osana selainohjelmistoja on suoraviivaista JavaScriptin kautta. Myös palvelinohjelmistot tukevat olioiden muuttamista JSON-muotoon.
Oletetaan että edelläkuvattu kirjojen käsittelyyn tarkoitettu rajapinta käsittelee JSON-muotoista dataa. Kirjaa kuvaava luokka on seuraavanlainen:
package wad; public class Book { private Long id; private String name; public Long getId() { return this.id; } public void setId(Long id) { this.id = id; } public String getName() { return this.name; } public String setName(String name) { this.name = name; } }
Kun luokasta on tehty olio, jonka id
-muuttujan arvo on 2
ja nimi "Harry Potter and the Chamber of Secrets"
, on sen JSON-esitys seuraavanlainen:
{ "id":2, "name":"Harry Potter and the Chamber of Secrets" }
JSON-notaatio määrittelee olion alkavalla aaltosululla {
, jota seuraa oliomuuttujien nimet ja niiden arvot. Lopulta olio päätetään sulkevaan aaltosulkuun }
. Oliomuuttujien nimet ovat hipsuissa "
sillä ne käsitellään merkkijonoina. Muuttujien arvot ovat arvon tyypistä riippuen hipsuissa. Tarkempi kuvaus JSON-notaatiosta löytyy sivulta json.org.
Pyynnön rungossa lähetettävän JSON-muotoisen datan muuttaminen olioksi tapahtuu annotaation @RequestBody avulla. Annotaatio @RequestBody edeltää kontrollerimetodin parametrina olevaa oliota, johon sovelluskehittäjä pyytää Spring-sovelluskehystä asettamaan JSON-muotoisen datan arvot.
@RequestMapping(method=RequestMethod.POST) public String postBook(@RequestBody Book book) { bookRepository.save(book); return "redirect:/books"; }
Vastauksen saa lähetettyä käyttäjälle JSON-muodossa lisäämällä pyyntöä käsittelevään metodiin annotaatio @ResponseBody. Annotaatio @ResponseBody pyytää Spring-sovelluskehystä asettamaan palvelimen tuottaman datan selaimelle lähetettävän vastauksen runkoon. Jos vastaus on olio, muutetaan se (oletuksena) automaattisesti JSON-muotoiseksi vastaukseksi.
@RequestMapping(method=RequestMethod.GET) @ResponseBody public Book getBook() { Book book = new Book(); book.setName("Spring API"); return book; }
Edellä mainitut annotaatiot voi myös yhdistää. Oletetaan, että käytössä on bookRepository
-niminen olio, jonka metodi save
lisää kirjalle yksilöivän tunnuksen ja varastoi sen myöhempää käyttöä varten. Metodi myös palauttaa viitteen uuteen kirja-olioon. Uuden kirjan lisääminen tapahtuisi tällöin seuraavasti.
@RequestMapping(method=RequestMethod.POST) @ResponseBody public Book postBook(@RequestBody Book book) { return bookRepository.save(book); }
Nyt palvelulle voi lähettää JSON-muotoista dataa; vastaus on myös JSON-muotoinen, mutta luotavaan kirjaan on liitetty sen yksilöivä tunnus.
Voimme lisätä annotaatioon @RequestMapping lisätietoa metodin tuottamasta datasta. Attribuutti consumes
kertoo minkälaista dataa metodin kuuntelema osoite hyväksyy. Metodi voidaan rajoittaa vastaanottamaan JSON-muotoista dataa merkkijonolla "application/json"
. Vastaavasti metodille voidaan lisätä tietoa datasta, jota se tuottaa. Attribuutti produces
kertoo tuotettavan datatyypin. Alla määritelty metodi sekä vastaanottaa että tuottaa JSON-muotoista dataa.
@RequestMapping(method=RequestMethod.POST, consumes="application/json", produces="application/json") @ResponseBody public Book postBook(@RequestBody Book book) { return bookStorage.create(book); }
Jos on toteuttamassa omaa REST-rajapintaa, kannattanee joko käyttää Spring Data REST -komponenttia (palaamme tähän hieman myöhemmin) tai määritellä kontrolleriluokan annotaatioksi @RestController
. Tämä asettaa jokaisen luokan metodiin annotaation @ResponseBody
sekä sopivan datatyypin -- tässä tapauksessa "application/json".
Toteutetaan seuraavaksi kaikki tarvitut metodit kirjojen tallentamiseen. Kontrolleri hyödyntää erillistä luokkaa, joka tallentaa kirjaolioita tietokantaan ja tarjoaa tuen aiemmin määrittelemiemme books-osoitteiden ja pyyntöjen käsittelyyn -- PUT-metodi on jätetty omaa kokeilua varten.
// importit @RestController @RequestMapping("books") public class BookController { @Autowired private BookRepository bookRepository; @RequestMapping(method=RequestMethod.GET) public List<Book> getBooks() { return bookRepository.findAll(); } @RequestMapping(value="/{id}", method=RequestMethod.GET) public Book getBook(@PathVariable Integer id) { return bookRepository.findOne(id); } @RequestMapping(value="/{id}", method=RequestMethod.DELETE) public Book deleteBook(@PathVariable Integer id) { return bookRepository.delete(id); } @RequestMapping(method=RequestMethod.POST) public Book postBook(@RequestBody Book book) { return bookRepository.save(book); } }
ScoreService
Tässä tehtävässä toteutetaan pelitulospalvelu, joka tarjoaa REST-rajapinnan pelien ja tuloksien käsittelyyn. Huom! Kaikki syötteet ja vasteet ovat JSON-muotoisia olioita. Tehtäväpohjassa on toteutettu valmiiksi luokat Game
ja Score
sekä käytännölliset Repository
-rajapinnat.
GameController
Pelejä käsitellään luokan Game
avulla.
Toteuta pakkaukseen wad.controller
luokka GameController
, joka tarjoaa REST-rajapinnan pelien käsittelyyn:
POST /games
luo uuden pelin sille annetun pelin tiedoilla ja palauttaa luodun pelin tiedot. (Huom. vieläkin! Pyynnön rungossa oleva data on aina JSON-muotoista. Vastaukset tulee myös palauttaa JSON-muotoisina.)GET /games
listaa kaikki talletetut pelit.GET /games/{name}
palauttaa yksittäisen pelin tiedot pelin nimen perusteella.DELETE /games/{name}
poistaa nimen mukaisen pelin. Palauttaa poistetun pelin tiedot.
ScoreController
Jokaiselle pelille voidaan tallettaa pelikohtaisia tuloksia (luokka Score
). Jokainen pistetulos kuuluu tietylle pelille, ja tulokseen liittyy aina pistetulos points
numerona sekä pelaajan nimimerkki nickname
.
Toteuta luokka wad.controller.ScoreController
, joka tarjoaa REST-rajapinnan tuloksien käsittelyyn:
POST /games/{name}/scores
luo uuden tuloksen pelillename
ja asettaa tulokseen pelin tiedot. Tuloksen tiedot lähetetään kyselyn rungossa.GET /games/{name}/scores
listaa pelinname
tulokset.GET /games/{name}/scores/{id}
palauttaa tunnuksellaid
löytyvän tuloksenname
-nimiselle pelille.DELETE /games/{name}/scores/{id}
poistaa avaimenid
mukaisen tuloksen peliltäname
(pelin tietoja ei tule pyynnön rungossa). Palauttaa poistetun tuloksen tiedot.
Valmiin palvelun käyttäminen
Toisen sovelluksen tarjoamaan REST-rajapintaan pääsee kätevästi käsiksi RestTemplate-luokan avulla. Voimme luoda oman komponentin kirjojen hakemiseen.
// importit @Service public class BookService { private RestTemplate restTemplate; public BookService() { this.restTemplate = new RestTemplate(); } // tänne luokan tarjoamat palvelut }
- GET osoitteeseen /books palauttaa kaikkien kirjojen tiedot tai osajoukon kirjojen tiedoista -- riippuen toteutuksesta.
// kirjojen hakeminen List<Book> books = restTemplate.getForObject("osoite/books", List.class);
// tunnuksella 5 määritellyn kirjan hakeminen Book book = restTemplate.getForObject("osoite/books/{id}", Book.class, 5);
// tunnuksella 5 määritellyn kirjan hakeminen Book book = restTemplate.getForObject("osoite/books/{id}", Book.class, 5); book.setName(book.getName() + " - DO NOT BUY!"); // kirjan tietojen muokkaaminen restTemplate.put("osoite/books/{id}", book, 5);
// tunnuksella 32 määritellyn kirjan poistaminen restTemplate.delete("osoite/books/{id}", 32);
Book book = new Book(); book.setName("Harry Potter and the Goblet of Fire"); // uuden kirjan lisääminen book = restTemplate.postForObject("osoite/books", book, Book.class);
Usein sovellukset hyödyntävät kolmannen osapuolen tarjoamaa palvelua omien toiminnallisuuksiensa toteuttamiseen. Harjoitellaan tätä seuraavaksi.
GameRater
Palvelu GameRater lisää aiempaan tulospalveluun mahdollisuuden arvostella yksittäisiä pelejä antamalla niille numeroarvosanan 0-5. Arvostelu tehdään kuitenkin erilliseen palveluun, emmekä siis laajenna edellistä palvelua suoraan.
GameRater-palvelun tulee käyttää Tulospalvelu-palvelun REST-rajapintaa, jonka avulla se tarjoaa samanlaisen rajapinnan pelien ja tulosten käsittelyyn. Ainoastaan pelien arvostelut käsitellään ja talletetaan tässä palvelussa! Arvosteluihin käytettävä entiteetti Rating
ja siihen liittyvät palveluluokat on valmiina tehtäväpohjassa.
Huom! Joudut tutkimaan tehtäväpohjassa annettua koodia, jotta voit hyödyntää sitä. Joudut myös lukemaan tehtävän Tulospalvelu kuvausta tämän tehtävän toteutuksessa.
Huom! Valmis Tulospalvelu-palvelu löytyy osoitteesta http://wepa-scoreservice-heroku.herokuapp.com/games
, joten voit tehdä tämän tehtävän täysin riippumatta tulospalvelu-tehtävästä.
GameRestClient ja GameController
Tee luokka wad.service.GameRestClient
, joka toteuttaa rajapinnan GameService
. Luokan tulee käyttää Tulospalvelu-palvelua kaikissa rajapinnan määrittelemissä toiminnoissa. REST-rajapinnan käyttö onnistuu Springin RestTemplate
-luokan avulla.
Huom! GameRestClient
-luokan setUri
-metodi ottaa parametriksi yllä annetun URL-osoitteen valmiiseen Tulospalvelu-palveluun.
Luo luokka wad.controller.GameController
, joka tarjoaa täsmälleen samanlaisen JSON/REST-rajapinnan kuin Tulospalvelu-palvelun GameController
, mutta siten, että jokainen toiminto käyttää valmista Tulospalvelu-palvelua rajapinnan GameService
kautta.
Huom! Muista asettaa GameService
-rajapinnan kautta URL-osoite valmiiseen http://wepa-scoreservice-heroku.herokuapp.com/games
-osoitteeseen ohjelman käynnistyessä, esimerkiksi controller-luokan @PostConstruct
-metodissa.
RatingController
Jokaiselle pelille voidaan tallettaa pelikohtaisia arvosteluja entiteetin Rating
avulla. Arvosteluun liittyy numeroarvosana rating
(0-5).
Arvostelut liittyvät peleihin, jotka on talletettu eri palveluun, joten entiteetin Rating
viittaus peliin täytyy tallettaa suoraan avaimena. Koska peleihin viitataan REST-rajapinnassa pelin nimellä, talletetaan jokaiseen Rating
-entiteettiin pelin nimi attribuuttiin gameName
. Tämän attribuutin avulla voidaan siis löytää arvosteluja pelin nimen perusteella.
Toteuta luokka wad.controller.RatingController
, joka tarjoaa REST-rajapinnan arvostelujen käsittelyyn:
POST /games/{name}/ratings
luo uuden arvostelun pelillename
- ainoa vastaanotettava attribuutti onrating
GET /games/{name}/ratings
listaa talletetut arvostelut pelillename
GET /games/{name}/ratings/{id}
palauttaa yksittäisen arvostelun tiedot pelin nimenname
ja avaimenid
perusteellaDELETE /games/{name}/ratings/{id}
poistaa avaimenid
mukaisen arvostelun
REST-palvelun kypsyystasot
Martin Fowler käsittelee artikkelissaan Richardson Maturity Model REST-rajapintojen kypsyyttä. Richardson Maturity Model (RMM) jaottelee REST-toteutuksen kolmeen tasoon, joista kukin tarkentaa toteutusta.
Aloituspiste on tason 0 palvelut, joita ei pidetä REST-palveluina. Näissä palveluissa HTTP-protokollaa käytetään lähinnä väylänä viestien lähettämiseen ja vastaanottamiseen, ja HTTP-protokollan käyttötapaan ei juurikaan oteta kantaa. Esimerkki tason 0 palvelusta on yksittäinen kontrollerimetodi, joka päättelee toteutettavan toiminnallisuuden pyynnössä olevan sisällön perusteella.
Tason 1 palvelut käsittelevät palveluita resursseina. Resurssit kuvataan palvelun osoitteena (esimerkiksi /books
-resurssi sisältää kirjoja), ja resursseja voidaan hakea tunnisteiden perusteella (esim. /books/nimi
). Edelliseen tasoon verrattuna käytössä on nyt konkreettisia resursseja; olio-ohjelmoijan kannalta näitä voidaan pitää myös olioina joilla on tila.
Tasolla 2 resurssien käsittelyyn käytetään kuvaavia HTTP-pyyntötyyppejä. Esimerkiksi resurssin pyyntö tapahtuu GET-metodilla, ja resurssin tilan muokkaaminen esimerkiksi PUT, POST, tai DELETE-metodilla. Näiden lisäksi palvelun vastaukset kuvaavat tapahtuneita toimintoja. Esimerkiksi jos palvelu luo resurssin, vastauksen tulee olla statuskoodi 201
, joka viestittää selaimelle resurssin luomisen onnistumisesta. Oleellista tällä tasolla on pyyntötyyppien erottaminen sen perusteella että muokkaavatko ne palvelimen dataa vai ei (GET vs. muut).
Kolmas taso sisältää tasot 1 ja 2, mutta lisää käyttäjälle mahdollisuuden ymmärtää palvelun tarjoama toiminnallisuus palvelimen vastausten perusteella. Webissä huomiota herättänyt termi HATEOAS käytännössä määrittelee miten web-resursseja tulisi löytää webistä.
RESTin isä, Roy Fielding, pitää vain tason 3 sovellusta oikeana REST-sovelluksena. Ohjelmistosuunnittelun näkökulmasta jokainen taso parantaa sovelluksen ylläpidettävyyttä -- Level 1 tackles the question of handling complexity by using divide and conquer, breaking a large service endpoint down into multiple resources; Level 2 introduces a standard set of verbs so that we handle similar situations in the same way, removing unnecessary variation; Level 3 introduces discoverability, providing a way of making a protocol more self-documenting. (lähde)
Huom! Sovellusta suunniteltaessa ja toteuttaessa ei tule olettaa että RMM-tason 3 sovellus olisi parempi kuin RMM-tason 2 sovellus. Sovellus voi olla huono riippumatta toteutetusta REST-rajapinnan muodosta -- jossain tapauksissa rajapintaa ei oikeasti edes tarvita; asiakkaan tarpeet ja toiveet määräävät mitä sovelluskehittäjän kannattaa tehdä.
Spring Data Rest
Spring-sovelluskehys sisältää projektin Spring Data REST, minkä avulla REST-palveluiden tekeminen helpottuu hieman. Lisäämällä projektin pom.xml
-konfiguraatioon riippuvuus spring-boot-starter-data-rest
saamme Spring Boot-paketoidun version kyseisestä projektista käyttöömme.
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-rest</artifactId> </dependency>
Nyt Repository-luokkamme tarjoavat automaattisesti REST-rajapinnan, jonka kautta resursseihin pääsee käsiksi. REST-rajapinta luodaan oletuksena sovelluksen juureen, ja tehdään luomalla monikko domain-olioista. Esimerkiksi, jos käytössä on luokka Book
, sekä sille määritelty BookRepository
, joka perii Spring Data JPA:n rajapinnan, generoidaan rajapinnan /books
alle toiminnallisuus kirja-olioiden muokkaamiseen.
RestItemRepository
Luo rajapinta ItemRepository
, joka tarjoaa Item
-olioiden tietokantatallennustoiminnallisuuden. Lisää tämän jälkeen Spring Data REST-riippuvuus pom.xml-tiedostoon, ja tarkista REST-rajapintasi toiminta esimerkiksi Postman REST Clientin avulla. Wat is this magic?
Usein käytännössä sovelluksemme kuitenkin toimivat jo palvelun juuripalvelussa, ja haluaisimme esimerkiksi tarjota rajapinnan erillisessä osoitteesssa. Spring Data REST-projektin konfiguraatiota voi muokata erillisen RepositoryRestMvcConfiguration
-luokan kautta. Alla olevassa esimerkissä REST-rajapinta luodaan osoitteen /api/v1
-alle. Annotaatio @Component
kertoo Springille että luokka tulee ladata käyttöön käynnistysvaiheessa; rajapinta kertoo mistä luokasta on kyse.
// pakkaus ja importit @Component public class CustomizedRestMvcConfiguration extends RepositoryRestConfigurerAdapter { @Override public void configureRepositoryRestConfiguration(RepositoryRestConfiguration config) { config.setBasePath("/api/v1"); } }
Nyt jos sovelluksessa on entiteetti Book
sekä siihen sopiva BookRepository
, on Spring Data REST-rajapinta osoitteessa /api/v1/books
.
MessageApi
Tehtävässä on käytössä nyt jo tutuhko viestien kirjoitus- ja lukemispalvelu. Lisää sovellukseen REST-rajapinta viestien käsittelyyn. GET-pyynnön osoitteeseen /api/messages
tulee palauttaa lista viesteistä, POST-pyyntö osoitteeseen /api/messages
luo uuden viestin, jne.
Käytännössä sovelluksen kehittäjä ei kuitenkaan tyypillisesti halua kaikkia HTTP-protokollan metodeja kaikkien käyttöön. Käytössä olevien metodien rajaaminen onnistuu käytettävää Repository
-rajapintaa muokkaamalla. Alla olevassa esimerkissä BookRepository
-rajapinnan olioita ei pysty poistamaan automaattisesti luodun REST-rajapinnan yli.
// pakkaus import wad.domain.Book; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.rest.core.annotation.RestResource; public interface BookRepository extends JpaRepository<Message, Long> { @RestResource(exported = false) @Override public void delete(Long id); }
Spring Data REST ja RestTemplate
Spring Data RESTin avulla luotavien rajapintojen hyödyntäminen onnistuu RestTemplaten avulla. Esimerkiksi yllä luotavasta rajapinnasta voidaan hakea Resource
-olioita, jotka sisältävät kirjoja. RestTemplaten metodin exchange
palauttaa vastausentiteetin, mikä sisältää hakemamme olion tiedot. Kyselyn mukana annettava ParameterizedTypeReference
taas kertoo minkälaiseksi olioksi vastaus tulee muuntaa.
RestTemplate restTemplate = new RestTemplate(); ResponseEntity<Resource<Book>> response = restTemplate.exchange("osoite/books/1", // osoite HttpMethod.GET, // metodi null, // pyynnön runko; tässä tyhjä new ParameterizedTypeReference<Resource<Book>>() {}); // vastaustyyppi if (response.getStatusCode() == HttpStatus.OK) { Resource<Book> resource = response.getBody(); Book book = resource.getContent(); }
the end.