Repository navigation
Laskari 7
Tehtävät 1-5 REST-api ja mikropalveluarkkitehtuuri, deadline to 22.10 klo 11:59. Huom, muokkauksena myös kurssipalautteesta tehtäväpiste eli yhteensä 6 tehtävää
Tehtävässä on osat 1-7. Viikon maksimi on 5 tehtävää + kurssipalautteesta 1 piste eli yhteensä 6. Ylimääräisellä 3:lla kohdalla voi kompensoida peruslaskareiden tekijänä aikaisempien viikkojen tekemättäjääneitä tehtäviä. Jos tavoittelet haastavien tehtävien ekstranoppaa, lasketaan mukaan kaikki tämän viikon tehtävistä. Haastavien tehtävien tekijä, lisää palautussubmission kommenttiin jos teit kaikki 8 + 1 tehtävää tai enemmän kuin 6 pisteen arvosta
Yksi viime aikoijen hypetermeistä ohjelmistokehityksessä on ollut microservices eli suomeksi mikropalvelut. Wikipedian määritelmä:
In computing, microservices is a software architecture style in which complex applications are composed of small, independent processes communicating with each other using language-agnostic APIs. These services are small, highly decoupled and focus on doing a small task, facilitating a modular approach to system-building.
Jos haluat tietää lisää mikropalveluista, lue http://martinfowler.com/articles/microservices.html
Teemme tässä tehtävässä ohjelman tai ohjelmiston, joka koostuu muutamasta pieneen asiaan keskittyvästä palvelusta, eli mikropalvelusta sekä näiden kanssa kommunikoivasta asiakasohjelmasta. Palvelut pyörivät webissä ja kommunikointi niiden kanssa tapahtuu HTTP-protokollaa käyttäen, seuraten RESTful-periaatteita. Palvelumme ovat siis REST-apeja.
Emme mene nyt sen tarkemmin siihen mitä kaikkea termi REST pitää sisällään (googlaa jos kiinnostaa), tarkastelemme ainoastaan yhtä tapaa tehdä HTTP REST -apeja. Apimme toimivat seuraavien periaatteiden mukaan
- tieto siirretään json-muodossa
- jos apikutsu ei muuta palvelun tilaa, se tehdään HTTP:n pyyntötyypillä GET
- jos apikutsu muuttaa palvelun tilaa, esim. luo palveluun uuden resurssin (eli esim. rivin tietokantaan), tehdään se HTTP:n pyyntötyypillä POST
Hieman yksinkertaistaen, kun mennään normaalisti katsomaan web-sivun sisältöä, tehdään GET-pyyntö johonkin urliin. Esim HTTP GET osoitteeseen https://ohtustats-s2015.herokuapp.com/submissions/public.json näyttää kurssin tehtävien palautukset json-muodossa. Myös linkkien klikkaus selaimessa aiheuttaa GET-pyynnön. Pyynnön vastauksena siis lähetetään yleensä jotain dataa, eli html-koodi selaimelle näytettäväksi tai json-muotoista "raakadataa". Palautettua dataa sanotaan pyynnön vastauksen (engl. response) _body_ksi. Bodyn mukana lähetetään myös pyynnön statuskoodi. Tuttuja statuskoodeja ovat 200, eli onnistunut pyyntö ja 404 eli document not found.
Myös POST-pyyntö kohdistuu tiettyyn url:iin. Pyynnön mukana lähtee viesti eli body, joka voi sisältää jollakin tavoin enkoodattua tietoa. Selain lähettää websivuilla olevien formien tiedot HTTP POST:illa. Lisää HTTP:n pyyntötyypeistä esim. osoitteessa http://www.w3schools.com/tags/ref_httpmethods.asp
Kloonaa Person-palvelun sisältämä repositorio https://github.com/mluukkai/PersonService
Palvelumme persistoivat oliot MongoDB-tietokantaan. Mongo on ns. dokumenttitietokanta. Hoidamme olioiden käsittelyn Morphia-kirjaston avulla, ja mongosta tehtävässä ei tarvitse tietää oikeastaan mitään. Jos et halua asentaa Mongoa koneellesi, valitse itsellesi vapaa tietokantainstanssi täältä.
Person-palvelu on toteutettu Spark-frameworkilla. Palvelu (ks. tiedosto App.java) on rekisteröitynyt kuuntelemaan seuraavia urleja eli "routeja":
- GET /ping palauttaa tietoja palvelusta
-
GET /persons palauttaa kaikki järjestelmään rekisteröityneet käyttäjät. Ennen routen suorittamista suoritetaan kuitenkin before-filtteri, joka tarkastaa että käyttäjä on tunnistautunut
- tunnistautuminen tapahtuu pyynnön mukana lähetettävän Authorization-headerin sisältönä lähetetyn tokenin eli satunnaisen merkkijonon avulla
- jos autentikaatiotoken ei ole, palauttaa, palautetaan käyttäjälle statuskoodi 401 ja asianmukainen virheilmoitus.
-
POST /persons luo pyynnön mukana lähetettävän jsonin perusteella uuden käyttäjän
- jos käyttäjäolion luominen jostain syystä epäonnistuu, palautetaan statuskoodi 400 ja virheilmoitus
- POST /session saa parametriksi jsonin, jossa on käyttäjätunnus ja salasana. Jos samat löytyvät tietokannasta, luodaan, ja palautetaan autentikaatiotoken, jonka avulla asiakas voi jatkossa suorittaa autentikaatiota vaativia toimenpiteitä (kuten GET /persons)
- HTTP:llä kommunikoitaessa on hyvä määritellä vastausten enkoodausmuoto. Tämä tapahtuu asettamalla vastaukseen Content-Type-headeri. Headerin asettaminen tapahtuu after-filtterissä, joka suoritetaan jokaisen routen suorittamisen jälkeen.
Sovellus käyttää siis tokeneihin perustuvaa autentikaatiota, jossa periaatteena on se, että operaatiot jotka eivät ole kaikille sallittuja on suojattu siten, että pyyntöihin on lisättävä mukaan validi autentikaatiotoken. Token välitetään pyynnön Authorization-headerin arvona. Tokenin saa haltuunsa kirjautumalla, eli sovelluksessamme lähettämällä käyttäjätunnuksen ja salasanan POST-pyynnöllä osoitteeseen '/session'. Tokenautentikaatio on nimenomaan se tapa, millä REST-apien kanssa tapahtuva kommunikaatio tapahtuu. Sovelluksessamme toteutus on suoraviivainen ja noudattaa melkien OAuth 2.0-standardin Resource Owner Password Credentials Grant-flowta. Person-palvelu tallettaa generoidut validit tokenit muistiin, eli token säilyy niin kauan validina kuin palvelu on päällä, uudelleenkäynnistyksen jälkeen vanhat tokenit eivät ole enää voimassa.
Käynnistä sovellus. Käynnistyksen helpottamista varten projektin juuressa on skripti run_person.sh, joka suorittaa pari tuttua maven-komentoa. Sovellus käynnistyy porttiin 4567
Kokeile eri urleja selaimella, curlilla ja postmanilla [https://www.getpostman.com/] Selain on lähes käyttökelvoton apien testailemiseen. Ehkä tarkoitukseen paras työkalu on postman. Tee seuraavat:
- luo käyttäjä, pyynnön mukana menevän jsonin oletetaan olevan muotoa
{
"name": "pihla",
"username": "pihla",
"password": "salainen",
"address": "mannerheimintie"
}
- kirjaudu järjestelmään
- kokeile listata käyttäjät ilman autentikaatiotokenia
- listaa käyttäjät siten että autentikaatiotoken on määritelty
- tee koodiin esifiltteri, jonka avulla jokaisesta pyynnöstä tulostuu alla olevan esimerkin tapaan metodi, headerit ja body eli pyyntöön liittyvä data
---------------------
POST
Accept = */*
Accept-Encoding = gzip, deflate
Accept-Language = en-US,en;q=0.8,fi;q=0.6
Authorization = 39defbca-f5d3-472b-b2d9-c87d8c36c2ad
CSP = active
Cache-Control = no-cache
Connection = keep-alive
Content-Length = 110
Content-Type = text/plain;charset=UTF-8
Host = localhost:4567
Origin = chrome-extension://fhbjgbiflinjbdggehcddcbncdddomop
Postman-Token = 25767dce-6cd2-50d8-eac0-74bd17cbdb58
User-Agent = Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.132 Safari/537.36
{
"name": "pihla",
"username": "pihla",
"password": "salainen",
"address": "mannerheimintie"
}
Pidä koko ajan myös silmällä mitä konsolissa tapahtuu. HUOM jos/kun teet muutoksia koodiin, palvelu täytyy uudelleenkäynnistää, jotta muutokset saadaan voimaan.
Kloonaa repositoriossa https://github.com/mluukkai/ApiClient oleva Person-sovellusta käyttävä komentoriviohjelma itsellesi. Ohjelmassa on tällä hetkellä toiminnallisuus valmiina sisäänkirjautumiseen ja kaikkien käyttäjien listaamiseen. Toteuta ohjelmaan uuden käyttäjän rekisteröinti.
Ohjelma kommunikoi Person-palvelun kanssa käyttäen Apache HTTPClientin uutta hienoa fluent-apia
Huomaa, että projektin juuressa on jälleen ohjelman suoritusta helpottava skripti.
Molemmissa projekteissa, ApiClient ja PersonService on copypastettuna suuri määrä samoja luokkia. Eriytä yhteiset luokat projektiiksi nimeltään esim. DomainLib jonka liität ApiClientin ja PersonServicen riippuvuudeksi.
Ohjelmistomme on osa verkkokauppaa (Amazon on kuuluisa ja varhainen esimerkki mikroarkkitehtuurin soveltamisesta). Tee nyt palvelu, joka vastaa myytävien tuotteiden listauksesta, anna palvelulle nimi ProductService. Palvelu toimii seuraavasti
- tuotteet talletetaan MongoDB:hen. Käytetään samaa tietokantaa mitä PersonService käyttää (Todellisuudessa kaikilla mikropalveluilla on todennäköisesti oma kanta.)
- GET /ping palauttaa tietoja palvelusta (copypaste PersonServicestä)
- GET /products palauttaa kaikkien tuotteiden listan
-
POST /product tallettaa uuden tuoteen
- luotava tuote lähetetään viestin bodynä seuraavassa json-muodossa
{
"name": "Kevytmaito",
"producer": "Valio",
"price": 3,
"inStock": 240
}
- tuotteen luominen onnistuu vain jos pyynnön Authorization-headerissa on validi autentikaatiotokeni
- palvelu varmistaa autentikaatiotokeni validiuden kysymällä asiasta PersonServiceltä
- tarvitset siis PersonServiceen uuden routen, joka mahdollistaa tokenin validiuden tarkastuksen, se voi olla esim muotoa GET '/tokens/:token_value' ja antaa vastauksen muodossa
{
"token": "tokenvaluehere",
"valid": true
}
Laajenna ApiClientiä siten, että voit käyttä sen avulla uutta palvelua. Muista testauksen yhteydessä postman ja debugtulosteiden tekeminen konsoliin
Huomaat että erityisesti HTTP-pyyntöjä tekevässä koodissa on paljon toisteisuutta, ja sama toisteisuus on levinnyt useaan projektiin. Eriytä toisteisuus kirjastoon nimeltään esim. RestLib, jonka käyttö voi näyttää esim. seuraavalta:
// create the object that encapsulates communication with api endpoints
Http http = new Http();
// the library object has methods to get the urls used in app
String personService = http.endpointForPersons();
// use the method get (or post) to make the request
ApiResponse response = http.get(http.endpointForPersons());
// the response object has easy accessors for status and the returned message body
if (response.ok()) {
for (Person person : response.body(new Person[0])) {
System.out.println(person);
}
} else {
System.out.println(response.error());
}
Esimerkissä Http ja ApiResponse ovat siis omatekemiä kirjastoluokkia, jotka piilottavat alemman tason apien käytön.
Muuta clientiä ja Product-palvelua siten että ne käyttävät uutta kirjastoa.
seuraavat 3 eivät enää ole vaadittuja kurssin "normaalilaskareiksi", jos tavoittelet haastavien tehtävien lisäopintopisteitä, niin tee myös seuraavat
Palveluiden ja asiakkaan, tai kirjaston koodissa on kovakoodattuna urleja, palvelun oma porttinumero ja tietokantakonfiguraatioita. Toteuta erillinen palvelu, ConfigurationService, jota käyttäen kaikki muut ohjelman komponentit saavat kaikki konfiguraatiotietonsa. Konfuguraatiopalvelin palauttaa ohjelmiston konfiguraatiot jsonina esim. seuraavaan tapaan:
{
endpoint: {
persons: "http://localhost:4567/persons",
products: "http://localhost:4568/products",
session: "http://localhost:4567/session",
token: "http://localhost:4567/tokens"
},
mongoUrl: "mongodb://ohtu:ohtu@ds055842.mongolab.com:55842/kanta1",
mongoDb: "kanta1",
productsPort: 4568,
personsPort: 4567
}
Voit kovakoodata konfiguraatiot ConfigurationServiceen, tai lukea ne esim. tiedostosta, tai tietokannasta.
Edes konfugraatiopalvelimen osoitetta ei saa kovakoodata muihin komponentteihin, vaan ne saavat osoitteen ympäristömuuttujan (environment variable) kautta.
Nyt jokainen pyyntö ProductServiceen, joka edellyttää tokenin käyttöä pyynnön autentikointiin saa aikaan sen, että ProductServicen on varmistettava tokenin voimassaolo PersonServiceltä. Jos sovelluksessa olevien palveluiden määrä kasvaisi suureksi, olisi se vaara, että tokenien validointi aiheuttaisi suorituskyvyn pullonkaulan.
Muuta autentikoinnin toteutusta siten, jokaisen palvelun on itse mahdollista varmistaa onko autentikaatiotokeni voimassa. Tämä onnistuu esim. liittämällä tokeniin tieto sen voimassaoloajasta. Kun näin tehdään, tokenia ei kuitenkaan voi lähettää sellaisenaan, muutenhan asiakasohjelma voisi väärentää voimassaoloajan. Token, tai ainakin sen voimassaoloaika on siis salattava siten, että ainoastaan palvelimet voivat purkaa salauksen.
Voit toteuttaa tokenin salaamisen esim. Jasypt-kirjaston, metodin StandardPBEStringEncryptor avulla. Älä kovakoodaa salausavainta palveluiden koodiin vaan välitä se esim. ympäristömuuttujan avulla.
Sovellukselle olisi kannattanut ehkä jo tehdä jonkinlaiset testit. Testejä voitaisiin tehdä monelle tasolle: yksikkötestejä luokille ja metodeille, HTTP API:n kautta suoritettavia testejä yksittäiselle palvelulle tai koko sovelluksen tasolla tapahtuvaa, asiakasohjelman toimintaa simuloivia, kaikkia palveluita käyttäviä testejä.
Yksikkötestejä kannattaa mirkopalveluihin tehdä ehkä vain siinä tapauksessa, jos palvelu sisältää monimutkaista toimintalogiikkaa. Palvelumme ovat triviaaleja, joten yksikkötestaus ei kannata. Yhden palvelun tasolla tapahtuva, API:n kautta suoritettava testaus on usein järkevää, jätämme kuitenkin senkin testaustason tällä kertaa välistä ja keskitymme koko sovelluksen testaamiseen.
Tee testejä varten oma projekti, esim. ApiIntegrationTest, ja toteuta sinne testit, joissa kommunikoit palvelujen kanssa samalla tavalla kuin kommunikointi ApiClientin ja palveluiden välillä tapahtuu. Emme tee nyt täydellisiä testejä, riittää että testaat esim. seuraavaat:
- kirjautuminen ei onnistu jos käyttäjää ei ole olemassa
- käyttäjän luominen onnistuu jos pyyntö on parametriensa puolesta validi
- käyttäjän kirjautuminen onnistuu jos salasana oiken
- kaikkien käyttäjien listaa ei näe ilman validin autentikaatiotokenin liittämistä pyynnön Authorization-headeriin
- kaikkien käyttäjien listaa näytetään jos pyynnön Authorization-headerissa on validin token
Testatessa emme halua käyttää "tuotantokäytössä" olevaa tietokantaa. Muuta ConfigurationServiceä siten, että se tarjoaa sekä tuotanto- että testausympäristön konfiguraatiot. Tarkoitus on ajaa testit siten, että kaikki palvelut on käynnistetty testausmoodiin (eli niille on annettu konfiguraatioapin testikonfiguraatiot tarjoava url).
Käytetään testausta varten tietokantaa, jonka mongodb-url on mongodb://ohtu:ohtu@ds035664.mongolab.com:35664/tests. Testien käyttämä tietokanta pitäisi pystyä tyhjentämään testiajojen välissä. Voit hoitaa tyhjentämisen seuraavalla koodilla:
String mongoUrl = "mongodb://ohtu:ohtu@ds035664.mongolab.com:35664/tests";
MongoClientURI connectionString = new MongoClientURI(mongoUrl);
MongoClient mongoClient = new MongoClient(connectionString);
MongoDatabase database = mongoClient.getDatabase("tests");
for (String collectionName : database.listCollectionNames()) {
if ( collectionName.equals("system.indexes")) continue;
MongoCollection collection = database.getCollection(collectionName);
collection.drop();
}Koodi edellyttää että projektilla on riippuvuutena MongoDB Java-driver.
tehtävien kirjaus:
- Kirjaa tekemäsi tehtävät tänne
- huom: tehtävien palautuksen deadline on to 22.10 klo 11:59
On taas aika perinteisen kurssipalautteen: https://ilmo.cs.helsinki.fi/kurssit/servlet/Valinta