6. gyakorlat¶
CRUD REST API végpontok¶
Listázás¶
Feladat
Készítsünk egy egyszerű REST végpontot, mely visszaadja az összes létező Contact
-ot az adatbázisból!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
|
Az első fontos lépés, hogy a REST API-ban résztevevő osztályra a @RestController
annotációt elhelyezzük!
Ez az annotáció kettős jelentőséggel bír.
Egyrészt ugyanúgy stereotype annotáció, tehát automatikusan regisztrálja a Spring egy beanként és így bárhol hozzáférhető lesz az alkalmazásukban (@Autowired
).
Másrészt ez az annotáció azt is megmondja, hogy a benne található kezelő metódusok a visszatérési értéküket közvetlenül a válasz body-jába írják (nem pedig egy modellen keresztül cipeljük a view-hoz).
Megjegyzés
A fenti megvalósítás egyenértékű azzal, ha az osztályt a @Controller
annotációval látjuk el, de ebben az esetben az összes metódust annotálnunk kell a @ResponseBody
-val, mely pontosan a fenti második pontot jelenti.
A @RequestMapping
az osztályban megadott összes metódusra ad egy URL prefix-et, melyet tovább pontosíthatunk a metódusra adott Mapping-ekkel (jelen esetben a @GetMapping
nem ad hozzá ehhez semmit, tehát a /api/contact
URL-re küldött GET kéréseket fogja kiszolgálni).
A @RequestMapping
-nél azt is megadjuk, hogy csak akkor szolgáljuk ki a kérést, ha annak Accept
header-ében megtalálható az application/json
.
Ezzel igazából limitáljuk, hogy csak JSON eredményeket adunk, ugyanakkor lehet másik controller-ünk ugyanezzel az útvonallal (Pl.: az MVC-s megvalósításból /contact
), feltéve, hogy azok a kérések nem igénylik a JSON output-ot (máskülönben összevesznének).
Megjegyzés
Amennyiben JSON mellett mondjuk XML-t is engedélyezni szeretnénk, akkor a produces
attribútumban egy listát is megadhatunk:
1 |
|
Amennyiben a klienseink más host-on futnak, akkor szükségünk lehet arra, hogy engedélyezzük a Cross Origin-t a megadott host-nak, vagy ha publikus API-t készítünk, akkor mindenkinek adhatunk hozzáférést:
1 2 3 4 |
|
Ezután nézzük, hogy mi az eredménye a REST API hívásnak!
Tesztelhetjük Postman-el, vagy egyszerűen curl
-el.
Amennyiben alapértelmezett porton futtatjuk az alkalmazásunkat, akkor a következőt kell kiadnunk:
1 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
|
A fenti példában elhelyezett elemekben csak a név és email attribútumokat adtuk meg, így ez teljesen helyes.
Feladat
Készítsünk egy másik végpontot, mely id alapján ad vissza egy contact-ot!
Az alapvető megoldás igen egyszerű:
1 2 3 4 |
|
A fentiben semmilyen extra dolog nincs, a @PathVariable
-el az URL-ben megadott azonosítót leképezzük a metódus paraméterébe.
Egy problémát az jelent, hogy amikor a megadott id
érvénytelen, akkor a getContactById()
hívás eredménye null
lesz, mely nem pont ideális, hiszen ilyen esetben a kliens egy üres body-t kap HTTP 200-as kóddal, vagyis azt mondjuk, hogy minden sikeres volt, pedig nem is létezik az amit kért.
Egy jobb megoldás lehet, ha 404-et adunk vissza.
Az előző példát ehhez kicsit átpofozzuk:
1 2 3 4 5 6 7 8 9 10 |
|
Az új elem, amit használunk a ResponseEntity
, mely egy generikus osztály.
A generikus paramétere nem más, mint a visszaadni kívánt objektum típusa.
A lényeges különbség, hogy így a ResponseEntity
rendelkezik egy status-al és a header-öket is meg tudjuk adni.
A példában megvizsgáljuk, hogy a visszakapott objektum null
-e vagy sem.
Igaz esetben 404-es HTTP status-t szeretnénk visszaadni, melyet a következőképpen tudunk megvalósítani:
1 |
|
A ResponseEntity
statikus metódusainak egy nagy része visszaad egy BodyBuilder
objektumot (mint, ahogy a status is).
Ezeket azok a metódusok adják vissza, melyek a status-t módosítják valamilyen formában, így aztán a BodyBuilder
-rel a válasz body-ját adhatjuk meg.
Amikor készen vagyunk akkor a build()
alkalmazásával kaphatjuk vissza a legyártott ResponseEntity
objektumot.
A 404 jelzésére használhattunk volna ezt is:
1 |
|
A statikus metódusokon kívül használhatjuk a konstruktort is, hogy a megfelelő ResponseEntity
-t előállítsuk.
A legbővebb paraméterlistával megadhatjuk a body-t, a header-t és a status kódot is.
Jelen helyzetben viszont elegendő a status-t beállítanunk, illetve a body-ba elhelyezni a lekért Contact
-ot.
1 |
|
Próbáljuk ki az alkalmazást, mondjuk a 2-es id-val, melynek eredménye a következő:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
Amennyiben érvénytelen id-val hívjuk a végpontot (Pl.: http://localhost:8080/api/contact/5), akkor 404 - Not Found
statust kapunk vissza üres body-val.
Létrehozás¶
Feladat
A lekérések mellett adjunk lehetőséget arra, hogy új contact-ot adjunk a rendszerhez a REST API-n keresztül!
A feladat nem tűnik túl összetettnek, a következő kód megoldást is ad:
1 2 3 4 5 |
|
A @PostMapping
-ben megadjuk, hogy csakis JSON formátumban vagyunk hajlandóak fogadni az adatot (consumes = "application/json"
, azaz az ide érkező kéréseknek a Content-Type
header-je application/json
kell legyen).
Továbbá jelen helyzetben a @ResponseStatus(HttpStatus.CREATED)
-t használjuk a status beállítására, azaz ha lefut a metódusunk, akkor automatikusan az itt megadott status code-al fog visszatérni a HttpServletResponse
-unk.
Ha most kipróbáljuk ezt az alkalmazásunkban, akkor hibát kapunk, pontosabban visszakapunk egy bejelentkezésre felszólító oldalt.
Ez a CSRF védelem miatt van, amire adat lekéréskor (korábbi GET kérések) nem kellett figyelnünk, azonban a POST kéréseknél már lehetnek turpisságok, amiket ki kell védenünk.
Spring-ben a CSRF védelem alapból be van kapcsolva.
Ennek kiküszöbölésére (mivel most nem akarunk semmilyen autentikációt a REST kéréseinkre) hozzá kell nyúlnunk a WebSecurityConfig
osztályhoz.
A korábban megadott configure(HttpSecurity http)
metódust egészítsük ki a legvégén a következővel:
1 2 3 4 5 6 7 |
|
A fenti kódban, mivel nem akarjuk a CSRF-et teljesen kikapcsolni (amit nem is ajánlok), ezért azt mondjuk, hogy csak a /api/**
-ra érkező kéréseknél kapcsoljuk ki a CSRF védelmet.
Ha már itt vagyunk, akkor nézzük meg, hogy mi történik, ha a h2-console
-t szeretnénk jelenleg elérni.
A Security beállításaink miatt be kell jelentkeznünk, pedig magának a h2-console-nak is van bejelentkeztetése.
Ha nem szeretnénk, hogy ide is be kelljen jelentkezni, akkor a WebSecurityConfig
-ban a következőt is adjuk meg:
1 2 3 4 |
|
Ezzel nemes egyszerűséggel azt mondjuk, hogy a /h2-console/
kezdetű URL-eken nem foglalkozunk security-vel.
Frissítés¶
Feladat
Készítsünk REST API végpontot a Contact frissítéséhez!
Ezzel sem lesz nehéz feladatunk:
1 2 3 4 5 6 7 8 9 10 |
|
Viszont néhány dolgot azért szem előtt kell tartanunk! Ha nincs megadva a contact id-ja, akkor használjuk az URL-ben megadottat, máskülönben megvizsgáljuk, hogy a két azonosító megegyezett-e, mivel eltérés esetén valami nem úgy lett elküldve, ahogy a felhasználó szerette volna.
Tegyük fel hogy a 3-as id-val létező kontakt nevét szeretném szerkeszteni, így a http://localhost:8080/api/contact/3
-ra a következő body-val rendelkező PUT kérést küldöm:
1 2 3 |
|
A kontakt sikeresen el is lesz mentve. Viszont, ha lekérdezem a kontaktot akkor a következőt kapom:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
Ez azért történik, mert bár a @RequestBody Contact contact
paraméterbe szépen leképződik egy kontakt, azonban ennek a kontakt objektumnak nincs megadva csak a neve, a többi field null
lesz és ezt mentjük el az adatbázisba.
Tehát, ezen logika alapján, akkor fog jól működni az update, ha a kliens az összes szükséges adatot elküldi.
Amennyiben szeretnénk olyan megoldást, amely nem nullázza ki a meglévő adatot, akkor ajánlatos lehet egy PATCH kérést elkészítése is:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
|
A fenti kéréssorozatot megismételve most már az elvártat kell kapjuk és csak a name
field szabad, hogy módosuljon (group eddig is null
volt):
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
Törlés¶
Feladat
Végül készítsük el a törléshez szükséges API végpontot is!
1 2 3 4 5 |
|
Egy DELETE kéréseket támogató API végpontot definiálunk, melyben töröljük a megadott id-val rendelkező elemet.
Ha eleve nincs ilyen id-jú elem, akkor a célunkat végülis elértük, ugyanis nincs ilyen azonosítójú elem az adatbázisban, ezért nem kezeljük külön, hogy mi történjen akkor, ha az elem nem létezik.
A @ResponseStatus(code=HttpStatus.NO_CONTENT)
a választ a 204-es kóddal látja el, mely szerint sikeres kérést hajtottunk végre, de nincs a válaszban semmi.
Hypermedia engedélyezése¶
Az eddig megírt API teljesen rendben van és nagyszerűen működik.
Viszont a kliensnek teljes egészében ismernie kell az API felépítését.
Például a kliensnek tudnia kell, hogy a http://localhost:8080/api/contact
URL-en kérheti le az összes Contact
-ot, illetve azt is, hogy ha ehhez hozzárakja az id
-t, akkor lekérheti az adott Contact
tulajdonságait.
Ez mindaddig rendben is van ameddig nem változik az API.
Ebben a problémakörben tud segíteni a HATEOAS (Hypermedia as the Engine of Application State), amely önleíró API készítését teszi lehetővé úgy, hogy a visszaadott válaszban linkeket helyez el az adott erőforrások elérésére. Segítségével a kliens magabiztosabban navigálhat az API végpontjain anélkül, hogy pontosan tudnia kéne, hogy milyen végpontot is használ. Ehelyett az API által szolgáltatott (visszaadott) erőforrások közötti kapcsolatokra fókuszál.
Annak érdekében, hogy könnyebben megértsük a fent leírtakat, íme egy példa:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
|
A HATEOAS maga a technika, melynek több megvalósítása is létezik. A fent látható formátum a HAL (Hypertext Application Language).
A listában minden elem rendelkezik egy _links
property-vel, mely olyan hyperlink-eket tárol a kliens számára, amelyeken keresztül navigálhat.
Például a Kiss Béla
kontaktnál a self
megmondja, hogy erről a kontaktról, hol tudunk lekérdezni infokat (ezt a típusával is biztosítja számunkra), továbbá láthatjuk, hogy a kontakthoz tartozó csoportot melyik URL-en kérhetem le.
A fentiek azért nagyon jók, mert a kliensnek nem kell ismernie az API felépítését, nem kell az URL-ekkel machinálnia, egyszerűen a megfelelő linkeket kell követnie, amelyeket a szerver állított elő a számára.
A Spring alkalmazásunkhoz a következő függőséget kell hozzáadnunk, ha szeretnénk használni a HATEOAS nyújtotta lehetőségeket:
1 2 3 4 |
|
Azon felül, hogy ez így bekerül a classpath-ba, ad egy autokonfigurációt is, amely miatt csak a kontrollereket kell kicsit átínunk, hogy használni tudjuk a HATEOAS-t.
Ezután a contacts()
metódust írjuk át a következőképpen:
1 2 3 4 |
|
A fenti példában két új elemmel találkozhatunk: CollectionModel
és EntityModel
, melyek rendre egy kollekció és maga egy entitás HATEOAS megfelelői.
A CollectionModel
wrap metódusát meghívva becsomagolhatjuk a visszakapott kontaktok listáját egy CollectionModel<EntityModel<Contact>>
típusba.
Próbáljuk is ki, hogy mit kapunk eredményül:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
Az eddigiekhez képest annyi a különbség, hogy az egész válasz, mely eddig egy lista volt, most belekerült egy objektumba, melynek van egy _embedded
property-je, amin belül a contactList
adja meg magukat a kontaktokat.
Adjunk hozzá linkeket is!
1 2 3 4 5 6 |
|
CollectionModel
-hez az add(...)
metódussal tudunk egy új linket hozzáadni, melynek eredményeképpen a JSON végére bekerül a következő:
1 2 3 4 5 |
|
Ez az egyik kulcsa annak, hogy elkészíthessük a HATEOAS támogatással bíró válaszokat.
A linkek létrehozásakor a relation type-ot elhagyhatjuk, ha self
típusú linket szeretnénk létrehozni (mivel ez az alapértelmezett), így a fentit írhattuk volna így is:
1 |
|
Kezdetnek nem is rossz, de ez csak a teljes lista elérését adja meg a válaszban, nincs link magukra az egyes kontaktokra, illetve a kontaktokhoz tartozó group-okhoz sem tartozik link.
Vegyük észre, hogy mennyire nem jó ötlet az URL-t hardcode-olni!
Ennek a problémának a leküzdésében az úgynevezett link builder-ek lesznek a segítségünkre.
A konkrét osztály, mely segít nekünk a WebMvcLinkBuilder
, melynek használata a következőképpen nézhet ki:
1 2 3 4 5 6 7 |
|
Ennek következtében a szerver felépíti az URL-t, nekünk csak a .withRel("contact")
-t kell megadnunk.
Az útvonal többi részét a linkTo()
paraméterében megadott ContactRestController
osztályra elhelyezett URL-ből számítja a rendszer.
Amennyiben további URL részeket szeretnénk megadni, akkor használhatjuk a slash("asd")
metódust is, mely egy /
jellel hozzáfűzi az eddigi URL-hez azt, amit a paraméterben megadunk.
Ez akkor jöhet jól, ha nem csak a controller basepath-t használjuk, hanem az adott kezelő metóduson van még valamilyen URL megadva a mapping-ben.
Ilyenkor azonban a slash
helyett használhatunk egy másik alternatívát, mely a kezelő metódus mapping-jét is figyelembe veszi:
1 2 3 |
|
Most hogy ez megvan lássuk, hogy hogyan lehet a kontaktokhoz és a csoportokhoz is hozzáadni a linkeket. A fenti tudásunk alapján csinálhatnánk azt, hogy egyszerűen végig iterálunk a lista összes elemén és hozzáadogatjuk a linkeket, ahogy azt már láttuk is. Ezt viszont elég sok helyen meg kellene ismételnünk, ami nem túl hatékony.
Egy megoldás erre a RepresentationModel<T>
használata, melyből származtatni kell az adott modellt:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
|
Ezen felül szükségünk lesz egy-egy segédosztályra is, amelyek az alap modell osztályaink és a Resource osztályaink közötti átalakításban fognak segíteni nekünk.
Ezeket az osztályokat a RepresentationModelAssemblerSupport<ModelT, ResourceT>
generikus oszályból való származtatással fogjuk megvalósítani:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
|
A fentiek fényében a ContactRestController
-ünk listázás része a következőképpen fog módosulni:
1 2 3 4 5 6 7 8 9 10 11 |
|
Videó¶
A gyakorlat anyagáról készült videó: