Malá čokoládová Spring REST aplikácia (List Martinovi)

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 v CLASSPATH.
  • @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 CLASSPATHe? 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 CLASSPATHe 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 @RequestMappingu na kontroléri zlepená s hodnotou @RequestMappingu 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

Pridaj komentár

Vaša e-mailová adresa nebude zverejnená. Vyžadované polia sú označené *