Milý Martin, chcel si vedieť, ako vyzerá minimalistická RESTovská aplikácia v Springu 4.x.
Tu je.
Predovšetkým, zíde sa ti Maven. Nielenže sa vysporiada so závislosťami v Springu, ale dá ti k dispozícii fajnový plugin pre Jetty, v ktorom bude spúšťanie servera vecou na 10 znakov.
Závislosti
Začni teda POMkom, do ktorého daj nasledovné závislosti:
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>4.1.0.RELEASE</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.0.1</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.4.2</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.3.0</version>
</dependency
Prvá závislosť dodá k dispozícii Web MVC modul Springu, a spolu s ním sa vďaka závislostiam dotiahne zvyšok jadra tohto frameworku. Druhá závislosť predstavuje Servlet API: keďže Spring beží na servletoch, bez tohto JARka sa nezaobídeme. A tretia dvojzávislosť predstavuje Jacksona: nie toho mŕtveho speváka, ale živú knižnicu, ktorá vie mapovať objekty na JSON v oboch smeroch.
Plugin pre Jetty
Keď už máš otvorený POM, dodaj doňho aj zmieneý plugin pre Jetty:
<plugin>
<groupId>org.mortbay.jetty</groupId>
<artifactId>jetty-maven-plugin</artifactId>
<version>8.1.12.v20130726</version>
</plugin>
Dostaneš neskôr k dispozícii mavenovský goal jetty:run
, ktorým si spustíš server.
Nastavenie jadra webu
Vravel si, že si pamútáš, ako Javácke webaplikácie využívajú web.xml
. Poteším ťa: tu žiadne takéto súbory nebude treba. Namiesto neho použijeme springácky web initializer, v ktorom sa automaticky nakonfiguruje dispatcher servlet pre Spring, ktorý bude spracovávať všetky prichádzajúce požiadavky. Podrobnejšie nemám čas sa rozpisovať, ale nesmúť: obšírnejšie informácie môžeš nájsť v samostatnom článku.
public class WebInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class<?>[] {
ChocolateWebApplicationContext.class
};
}
@Override
protected String[] getServletMappings() {
return new String[] { "/*" };
}
@Override
protected Class<?>[] getRootConfigClasses() {
return null;
}
}
Inicializér bude spracovávať naozaj všetky požiadavky (určuje to mapovanie /*
) a zároveň nakonfiguruje springovskú mašinériu beanov v konfiguráku s menom ChocolateWebApplicationContext
.
Nastavenie Springu
Spomínal si tiež, že si pamätáš časy, keď sa Spring konfiguroval pomocou XML súborov, a keď si všetci mysleli, že XML je skvelé, lebo bez rekompilácie projektu možno predrôtovať nastavenia infraštruktúry projektu.
Vysvitlo, že pre mnoho prípadov je omnoho lepšia konfigurácia priamo v kóde. súbor ChocolateWebApplicationContext
je presne Java analógiou súboru beans.xml
. Ako uvidíš, je pomerne jednoduchý, ale vysvetlím ti ho krok po kroku.
package sk.upjs.ics.novotnyr.chocolate;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
@Configuration
@EnableWebMvc
@ComponentScan("sk.upjs.ics.novotnyr.chocolate")
public class ChocolateWebApplicationContext {
/* vsetko je autodiscovernute */
}
Predovšetkým, trieda konfiguráku nemá žiaden kód — to neprekáža, lebo mnoho vecí sa udeje automaticky.
- Anotácia
@Configuration
hovorí, že táto trieda predstavuje konfiguráciu springáckych beanov. @EnableWebMvc
zase pozapína mnoho vlastností súvisiacich s webom a MVC frameworkom: zaregistruje sa mnoho tried riešiacich mapovanie HTTP požiadaviek na metódy, ale vyrieši sa tiež automatické vyhľadaniw Jackksona vCLASSPATH
.@ComponentScan
zase hovorí, v ktorých balíčkoch sa majú automaticky vyhľadať nielen beany určené na automatické drôtovanie (autowiring), ale aj kontroléry, teda základné triedy obsluhujúce REST požiadavky.
A nečuduj sa, toto je naozaj celá konfigurácia Springu.
Nastavenie kontroléra
Kontrolér je trieda, ktorej metódy budú obsluhovať URL adresy pre RESTovské požiadavky.
Minimalistický kontrolér môže vyzerať takto:
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/chocolates")
public class ChocolateController {
private List<Chocolate> chocolates = new CopyOnWriteArrayList<Chocolate>();
public ChocolateController() {
chocolates.add(new Chocolate("lindt", "Lindt", 72));
chocolates.add(new Chocolate("choc-o-crap", "Choc'o'crap", 10));
chocolates.add(new Chocolate("brownella", "Brownella", 52));
}
@RequestMapping
public List<Chocolate> list() {
return chocolates;
}
}
Ak si niekedy používal REST API pomocou Jersey, veľmi rýchlo prídeš na to, že mnoho konceptov je rovnakých a odlišnosť spočíva len v iných anotáciách.
Anotácie kontroléra
Začnime zhora: @RestController
znamená, že táto trieda predstavuje kontrolér pre REST požiadavky, že návratové hodnoty jej metód budú automaticky serializované do výstupu pre klienta (prehliadač), a že sa má automaticky zaregistrovať v springovskom kontexte pri pátraní spôsobenom anotáciou @ComponentScan
.
Druhá anotácia, @RequestMapping
hovorí, že základná prípona v URL adrese pre tento kontrolér bude /chocolates
.
V konštruktore si vytvoríš a naplníš inštanciu zoznamu s ukážkovými dátami: a tento zoznam musí byť vláknovo bezpečný, pretože kontrolér v Springu bude singleton a budú k nemu pristupovať viaceré vlákna súčasne (zodpovedajúce súčasným HTTP požiadavkam).
Anotácie metódy pre GET
Pozri sa teraz na metódu list()
, ktorá sa zavolá vo chvíli, keď klientská aplikácia navštívi adresu v duchu http://localhost:8080/chocolates
, a to s príponou cesty /chocolates
, tento druhý request mapping prevezme všeobecnejšiu špecifikáciu cesty z anotácie nad kontrolérom a použije ju. A prečo metóda HTTP GET? Tá je totiž implicitná.
Návratová hodnota metódy je zoznam čokolád, čo je bežný Java objekt. Pamätáš si však na Jacksona v CLASSPATH
e? Vďaka nemu sa tento zoznam automagicky premení na JSONovský reťazec v HTTP odpovedi. (A Jackson nepotrebuje žiadnu špeciálnu konfiguráciu ani anotácie.)
Spustenie
Môžeš si to skúsiť: spusti mvn jetty:run
a navštív http://localhost:8080/chocolates. Uvidíš odpoveď:
[{"id":"lindt","title":"Lindt","percentage":72},{"id":"choc-o-crap","title":"Choc'o'crap","percentage":10},{"id":"brownella","title":"Brownella","percentage":52}]
Anotácie metódy pre POST
Teraz si skúsme aj opačný postup: dodajme metódu, ktorá prijme JSONovský string a vytvorí novú entitu. (V REST filozofii pôjde o mapovanie na HTTP POST).
@RequestMapping(method = RequestMethod.POST)
public Chocolate add(@RequestBody Chocolate chocolate) {
chocolates.add(chocolate);
return chocolate;
}
Ako vidíš, opäť je to metóda anotovanú cez @RequestMapping
. V tomto prípade však môžeš uviesť aj HTTP metódu, na ktorú má reagovať táto metóda v objekte.
Pozorovať môžeš ešte jednu anotáciu: parameter metódy objektu @RequestBody
hovorí, že celé telo v HTTP požiadavke sa má automaticky namapovať na objekt typu Chocolate
. A keďže máme v CLASSPATH
e Jacksona, Spring sa automaticky postará o to, aby akákoľvek HTTP POST požiadavka, ktorá má uvedený Content-Type
ako JSON (teda application/json
), sa pomocou tohto mŕtveho speváka deserializovala na bežný čokoládový objekt.
Žiaľ, toto sa už nedá vyskúšať pomocou čistého browsera (ten funguje len cez HTTP GET). Na testovanie je najlepšie zobrať niektorý z pluginov prehliadača: napríklad Firefox má svoj Poster, či HTTP Requester. Dôležité je, že požiadavka musí ísť tiež na adresu http://localhost:8080/chocolates, a musí mať správny Content Type.
Len bokom ti podotknem, že metóda vracia @Chocolate
. To nie je povinné, ale podľa RESTovských zásad je užitočné, ak sa klientovi na požiadavku vráti nejaký neprázdny obsah: v tomto prípade, keďže sa hrajkáme, stačí vrátiť to, čo prišlo na vstupe: teda čokoládu
Vylepšenie HTTP POSTu
Príklad by si mohol ešte vylepšiť. Hovorí sa, že ak sa úspešne podarí vytvoriť RESTovský resource, server má odpovedať so stavovým kódom HTTP 201 Created. Nie je nič ľahšie: stačí dodať nad metódu add()
anotáciu @ResponseStatus
s príslušným stavovým kódom:
@RequestMapping(method = RequestMethod.POST)
@ResponseStatus(value = HttpStatus.CREATED)
public Chocolate add(@RequestBody Chocolate chocolate) {
Metódy s parametrami URL adries
Zatiaľ si videl len dve metódy: jednu na získanie zoznamu všetkých čokolád a druhú na vytvorenie novej čokolády. Pri RESTe však často budeš potrebovať získať informácie o jedinej čokoláde. Podľa restovaných zásad sa takáto informácia o čokoláde Lindt namapuje napr. URL adresu:
http://localhost:8080/chocolates/lindt
Obslúžiť takéto volanie v REST kontroléri možno vytvorením novej metódy, ktorá zároveň dokáže inteligentne vytiahnuť z URL adresy identifikátor čokolády.
@RequestMapping("/{id}")
public Chocolate get(@PathVariable String id) {
Chocolate chocolate = findById(id);
if(chocolate == null) {
throw new ChocolateNotFoundException();
}
return chocolate;
}
Opäť si všimni anotáciu @RequestMapping
: tá teraz udáva už aj parameter /{id}
. Všetko, čo je v kučeravých zátvorkách (id
) udáva tzv. path parameter alebo path variable, teda premennú cesty. Ak klient navštívi adresu http://localhost:8080/chocolates/lindt, Spring sa snaží vypátrať metódu v kontroléri, ktorá dokáže obslúžiť požiadavku s touto URL, a používa pri tom anotácie @RequestMapping
. Do úvahy pritom berie nielen anotáciu na kontroléri, ale aj na metóde, pričom jednotlivé prípony ciest sa snaží “zlepiť” dohromady.
Prípona URL v @RequestMapping
u na kontroléri zlepená s hodnotou @RequestMapping
u na metóde dá dohromady /chocolates/{id}
.
Ak si chceš vyzdvihnúť hodnotu parametra cesty id
v metóde, použi na to štandardný parameter metódy String id
. Aby bolo jasné, že sa do parametra má napchať hodnota z URL adresy… tiež na to použiješ anotáciu. V tomto prípade pôjde o anotáciu @PathVariable
.
Iste sa pýtaš, ako sa odhadne názov parametra. V Jersey je nutné v analogickej anotácii explicitne povedať, že parameter String id
sa má namapovať na premennú cesty id
. Spring však používa vúdú, ktoré toto hádanie urobí automaticky (premenná cesty id
sa namapuje na rovnomenný parameter metódy).
Zvyšok metódy by mal byť jasný: vrátime bežnú inštanciu čokolády serializovanú na JSON.
S jedinou špecialitou: tou je prípad, že sa čokoláda s daným ID nenájde.
Výnimky
Ako vidíš, v metódach kontroléra možno hádzať výnimky. REST však vôbec nepozná koncept výnimky (koniec koncov, klient môže byť implementovaný v hocijakom, aj nieOO jazyku.). Detekovať výnimočné stavy môžeme pomocou HTTP stavových kódov a vhodne navolenému obsahu v tele odpovede HTTP.
Ak voláš REST adresu pre získanie objektu, ktorý neexistuje, mala by sa zjaviť stará známa klasika: stavový kód 404 (Not found).
A ako tieto stavové kódy súvisia s výnimkami? Jednoducho. Ak si vytvoríš triedu pre vlastnú výnimku, dodáš nad ňu anotáciu @ResponseStatus
(áno, túto anotáciu som už raz použil vyššie), a v kóde metódy túto výnimku vyhodíš, Spring ju automaticky odchytí a vráti stavový kód HTTP, ktorý uvedieš v tejto anotácii.
Tu je príklad výnimky, ktorá sa prevedie na stavový kód 404:
@ResponseStatus(value = HttpStatus.NOT_FOUND)
public class ChocolateNotFoundException extends RuntimeException {
// no body needed
}
Záver
Záverom tohto listu ti už len prajem veľa šťastia a zdaru pri vlastných projektoch.
S priateľským pozdravom
Róbert
P. S. Kompletné zdrojáky nájdeš na GitHube, v repozitári novotnyr/chocolate-springmvc-rest.
Ďalšie zdroje na čítanie
- Tutoriál Designing and Implementing RESTful Web Services with Spring
- Building a RESTful Web Service — budovanie REST endpointov pomocou Springu a Spring Bootu
- Spring MVC bez
web.xml
- Spring MVC 2.5: starší článok z roku 2009 k Spring MVC 2.5, ale mnoho ideí stále platí.
- Spring MVC: jarý rámec pre webové aplikácie — prezentácia Bezadis ’11