Ešte jedno RESTované API a la Restlet, pán hlavný! — viacero resources a klienti

Článok bol aktualizovaný 26. 9. 2014 o použitie stavových kódov a zdrojové kódy boli presunuté na GitHub.

V minulom dieli sme si nastavili Restlet, naštartovali jednoduchý čokoládový resource a vytvorili k nemu curlovského klienta.

Dnes si rozšírime čokoládovňu o dodatočný resource, ukážeme si, ako rozhodnúť, ktorý resource obslúži danú URI adresu a v tretej časti si ukážeme vytváranie klienta pomocou RESTovských tried.

Druhý resource

Mapovanie URI

Náš existujúci resource podporoval dve operácie: výpis všetkých čokolád (cez verb GET) a pridanie novej čokolády (cez POST). Teraz si vyrobme ešte jeden resource, ktorý bude zodpovedať jednej konkrétnej čokoláde. Podľa RESTovskej zásady môže byť jedna konkrétna čokoláda identifikovaná nasledovným URI:

http://localhost:8281/chocolate/1

Všimnime si identifikátor 1 v URI adrese. Obvykle ho môžeme priamo namapovať na databázový primárny kľúč, ale nič nám nebráni zvoliť akékoľvek iné priradenie medzi identifikátormi a objektami čokolád (napr. http://localhost:8281/chocolate/choc-n-choc)

Implementácia resourcu

Implementujme teda resource:

public class ChocolateResource extends ServerResource {
    @Get("json")
    public Chocolate findById() {
        return new Chocolate("A random low-cocoa chocolate", 20);
    }
}

V tomto prípade vrátime zakaždým tú istú inštanciu.

Konfigurácia servera

V minulom dieli sme si vystačili s nasledovnou konfiguráciou servera:

Server server = new Server(Protocol.HTTP, 8182, ChocolatesResource.class);
server.start();

V tomto prípade však chceme mať už dva kontroléry a takýto jednoduchý zápis už nestačí.

Namiesto Servera nakonfigurujeme všeobecnejší restletovský komponent… nazývaný Component. Ten je zovšeobecnením architektúry a zodpovedá plusmínus myšlienke servletového kontajnera. Môže obsahovať viacero konektorov (pre podporuju sieťových protokolov), aplikácií (zodpovedajú modulom, resp. sú analógiou WARov v servletoch) a niektoré ďalšie súčasti.

Component dáva jednoduchú možnosť pre zverejnenie viacerých resources a to na rozličných URI adresách.

public static void main(String[] args) throws Exception {
    Component component = new Component();
    component.getServers().add(Protocol.HTTP, 8182);
    component.getDefaultHost().attach("/chocolate", ChocolatesResource.class);
    component.getDefaultHost().attach("/chocolate/{id}", ChocolateResource.class);

    component.start();
}

V kóde sme vytvorili inštanciu Componentu, pridali jeden Server s protokolom HTTP na obvyklom porte 8182 a nastavili mapovanie pre oba resource: ChocolateResources bude sedieť na adrese http://localhost:8182/chocolates/.

Všimnime si príponu URI adresy pre druhý resource: tá obsahuje premennú cesty (path variable) s názvom id. Znamená to, že resource ChocolateResource dokáže obslúžiť napr. tieto adresy

http://localhost:8182/chocolates/1
http://localhost:8182/chocolates/2
http://localhost:8182/chocolates/lindt-excellence-70

Prípony 1, 2 alebo napr. lindt-excellence-7 sa namapujú na path variable id a v resource sa na ne neskôr budeme vediť odkázať.

Testovanie

Skúsme si pustiť server a otestovať správanie v prehliadači, resp. curle.

Component má oproti Serveru jednu výhodu: v štandardnej konfigurácii poskytuje viac logovacích hlášok.

19.11.2012 12:27:18 org.restlet.engine.http.connector.HttpServerHelper start
INFO: Starting the internal HTTP server on port 8182
19.11.2012 12:27:34 org.restlet.engine.log.LogFilter afterHandle
INFO: 2012-11-19    12:27:34    127.0.0.1   -   -   8182    GET /chocolate/1    -   200 -   0   125 http://localhost:8182   Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.64 Safari/537.11    -
19.11.2012 12:27:34 org.restlet.engine.log.LogFilter afterHandle
INFO: 2012-11-19    12:27:34    127.0.0.1   -   -   8182    GET /favicon.ico    -   404 439 0   1   http://localhost:8182   Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.64 Safari/537.11    -

Ak navštívime adresu http://localhost:8182/chocolates/1 z Chrome, v logu sa objavia dva výpisy pre dve HTTP požiadavky: jedna pre prístup k resource a druhá pre získanie faviconu pre zobrazenie v prehliadači.

Prístup k path variables z resourceov

Ako sa dostať k skutočnej hodnote parametra {id} v URL adrese?

Rodičovská trieda podporuje metódu getRequestAttributes(), ktorá vráti mapu medzi reťazcami a objektami zodpovedajúci atribútom požiadavky. V tejto mape sa ocitnú aj hodnoty pre path variables, pričom kľúče budú zhodné s názvami premenných v mapovaní.

Na príklade:

@Get("json")
public Chocolate findById() {
    String stringId = (String) getRequestAttributes().get("id");
    long id = Long.parseLong(stringId);

    return new Chocolate(id, "Unknown chocolate", 10);
}

Tu sme vytiahli reťazcovú hodnotu parametra id, previedli ju na long a použili ako identifikátor čokolády, ktorú vrátime na výstupe. (Áno, je to hlúpy príklad, ale nebudeme to komplikovať.)

Mapa síce má hodnoty typu Object, ale ak si uvedomíme, že skutočné hodnoty path variables sú uložené ako reťazce, môžeme spokojne pretypovávať. Ak však chceme získať čísla, musíme si to urobiť po svojom.

Krajší prístup k path variables z resourceov

Dokumentácia udáva zásadu pre resource: metódy majú pracovať rovno s objektami a jednotlivé divoké prevody medzi dátovými typmi sa majú riešiť inde. Ak si spomenieme na fakt, že resource je stavový objekt (s každou HTTP požiadavkou na server sa vytvorí nová inštancia, ktorá si môže sama naplniť inštančné premenné), tak môžeme už pri vytváraní resourceu povyťahovať hodnoty path variables, poznačiť si ich do inštančných premenných a v obslužných metódach ich rovno používať.

Čo presne znamená pri vytváraní?

Ak by sme to urobili v konštruktore, získame len výnimku NullPointerException. Resource nemá v konštruktore ešte inicializované základné premenné.

Namiesto toho má ServerResource prekryteľnú metódu doInit(), ktorú, citujem „možno použiť na inicializáciu stavu resource”. V jej vnútri už môžeme pristupovať k parametrom, previesť ich hore-dole na príslušné typy, a popchať ich do inštančných premenných.

Toť príklad:

public class ChocolateResource extends ServerResource {
    private Long id;

    @Override
    protected void doInit() throws ResourceException {
        String stringId = (String) getRequestAttributes().get("id");
        this.id = Long.parseLong(stringId);
    }

    @Get("json")
    public Chocolate findById() {
        return new Chocolate(id, "Unknown chocolate", 10);
    }       
}

Reštartnime server, a skúsme nakontaktovať metódy

http://localhost:8182/chocolates/1

a

http://localhost:8182/chocolates/2

a sledujme, čo sa bude diať.

Návratové kódy HTTP

V prípade metódy findById() sme stále vrátil inštanciu tej istej čokolády, čo síce pre náš malý čokoládový e-obchod postačuje, ale v praxi je to nepoužiteľné. Čo ak sa snažíme získať čokoládu, ktorá neexistuje?

V Java filozofii by takéto volanie malo vrátiť null alebo vyhodiť výnimku. Ako na to v RESTe?

Každé REST volanie môže nielen vrátiť výsledok v tele HTTP správy, ale indikovať úspešnosť či neúspešnosť pomocou číselných stavových kódov — ktoré sú presne namapované na stavové kódy z HTTP.

Úplný klasik stavových kódov je 404 (Not Found) a práve tento stavový kód vieme použiť v situácii, keď sa čokoláda nenájde. A keď už sme spomínali výnimky… stavový kód môžeme nastaviť presne vyhodením výnimky ResourceException s vhodným stavovým kódom:

@Get("json")
public Chocolate findById() {
    if(id.equals(1L)) {
        return new Chocolate(id, "A Simple Choco", 42);
    }
    throw new ResourceException(Status.CLIENT_ERROR_NOT_FOUND);     
}

Tým uzatvorme ďalší míľnik.

Všimnime si, že už máme dva resource obsluhujúce dve rôzne adresy s rozličnými metódami.

Restlet a klientské triedy

Čudujsasvete, Restlet umožňuje vytvárať nielen RESTovskú architektúru na strane servera, ale dáva k dispozícii aj sadu tried na vytváranie klientov. Nemusíme viac bojovať s curlom, či rozšíreniami prehliadačov!

RESTovský klient pre GET

Vytvorenie klienta je záležitosť na dva riadky. Začnime najjednoduchšou situáciou: klient sa pripojí k vzdialenému resource a získa jeho reprezentáciu cez verb GET:

ClientResource clientResource = new ClientResource("http://localhost:8182/chocolate/1");
clientResource.get().write(System.out);

Ak máme spustený server s ChocolatesResourceom od minula, výstupom bude:

19.11.2012 11:28:20 org.restlet.engine.http.connector.HttpClientHelper start
INFO: Starting the default HTTP client
{"id":1,"title":"Unknown chocolate","percentage":10}

Prvé dva riadky zodpovedajú logovacím hláškam a tretí obsahuje presne rovnaký výstup, ako sme mohli vidieť v prehliadači alebo v curle.

Ak máte v CLASSPATHe všetky triedy a zavedené JARy pre Jacksona, môžete rovno pracovať s objektami!

ClientResource clientResource = new ClientResource("http://localhost:8182/chocolate/1");
Chocolate chocolate = clientResource.get(Chocolate.class);
System.out.println(chocolate);

Klient získa JSONovskú reprezentáciu resource, ktorý je identifikovaný cez http://localhost:8182/chocolate/1, naštartuje dejsonizáciu a automaticky vytvorí objekt typu Chocolate, s ktorým pracujeme bežným spôsobom.

Skúsme si to aj s POSTom!

RESTovský klient pre POST

POSTovanie cez curl bolo divné — hlavne kvôli nutnosti escapovania JSONu. Ak použijeme klienta, je to jednoduché!

ClientResource clientResource = new ClientResource("http://localhost:8182/chocolate");
clientResource.post(new Chocolate(5L, "Deva Bar", 10));

Dva riadky a veľa mágie, ktorá funguje!

Nemusíme vôbec uvažovať v reprezentáciách, stačí myslieť v bežných objektoch.

Mágia

Keď som tieto riadky videl prvýkrát, bol som podivený, ako klient vie zistiť, že čokoláda sa má zjsonifikovať a nie napr. zoxmliť. Kontaktuje server a spýta sa ho, aké typy budú na výstupe? Nie.

V skutočnosti v sebe nesie klient viacero Converterov, teda tried, ktoré prevádzajú objekty na reprezentácie a naopak. Ak nemáte v klientovi žiadne extensions, použije sa štandardný DefaultConverter, ktorý zvláda reťazce, súbory, formuláre, InputStreamy a Readery a podporuje štandardnú Javácku serializáciu.

Dodaním Jacksona do CLASSPATH zavedieme automatickú podporu pre jsonifikáciu, ktorá bude uprednostnená pred štandardnou konverziou.

RESTovský klient pre POST s vlastnými reprezentáciami

Niekedy sa stane, že máme po ruke hotový reťazec s JSONovskou reprezentáciou a chceme ho rovno zobrať a poslať do resourcu v podobnom duchu ako to robí curl. Keďže budeme posielať reťazce, vytvoríme explicitný objekt typu Representation — presnejšie typu StringRepresentation.

Nezabudneme ale nastaviť media type, pretože StringRepresentation používa implicitne text/plain, ktorý nie je čokoládovými resourcami spracovateľný. V curláckom klientovi sme použili hlavičku HTTP protokolu:

Content-Type:application/json

a to isté musíme urobiť tuto.

Využijeme pritom existujúc konštantu APPLICATION_JSON a získame, čo sme chceli.

ClientResource clientResource = new ClientResource("http://localhost:8182/chocolate");
Representation representation = new StringRepresentation("{\"title\" : \"Lindt Excellence 70%\", \"percentage\": 70 }", MediaType.APPLICATION_JSON);
clientResource.post(representation);

RESTovský klient pre GET zoznamu čokolád

Čo ak chceme vytvoriť klienta pre získanie zoznamu čokolád, ktorý bude vracať rovno zoznamy objektov typu Chocolate?

Prvý pokus zlyhá na syntaktických obmedzeniach Javy:

ClientResource clientResource = new ClientResource("http://localhost:8182/chocolate/");
List<Chocolate> chocolates = clientResource.get(List<Chocolate>.class);

Výraz List<Chocolate>.class sa použiť nedá, pretože generická trieda si svoj typ za behu nepamätá. Prirodzené riešenie je

List chocolates = clientResource.get(List.class);

Toto však tiež nepovedie k výsledku, pretože klient získa zoznam máp (!), kde každá mapa bude zodpovedať jednej inštancii objektu s kľúčmi id, percentage a title . Dejsonifikácia v tomto prípade neprebehne, lebo úbohý Jackson sa nemá ako dozvedieť, že prvky zoznamu sú práve typu Chocolate.

Často používaný hack spočíva vo vytvorení špeciálnej triedy typu ArrayList, do ktorej Jackson nahádže zoznam čokolád.

public static class ChocolateList extends ArrayList<Chocolate> { /** len alias pre triedu **/ }

public static void main(String[] args) throws ResourceException, IOException {
    ClientResource clientResource = new ClientResource("http://localhost:8182/chocolate");
    List<Chocolate> chocolates = clientResource.get(ChocolateList.class);
    System.out.println(chocolates);
}

Sumár

V tomto dieli sme si ukázali, ako možno nakonfigurovať viacero resourceov, namontovať ich na URL adresy, dokonca pracovať so špecifickými parametrami v ceste. Ukázali sme si tiež programového klienta.

To je dostatočné na to, aby sme vedeli vyrábať jednoduché klient-server aplikácie v Restlete!

Projekt

Stiahnite si kompletný ukážkový projekt z GitHubu.

4 thoughts on “Ešte jedno RESTované API a la Restlet, pán hlavný! — viacero resources a klienti

  1. Ahoj, super článok! :) Vedel by si poradiť nejaké podarené zdroje odkiaľ študoval o restflete? Pretože oficiálny web a ich dokumentácia mi príde (okrem dokumentácie “ako začať s restletom”) trocha dosť mätúca a neúplná.

  2. There never seems to be enough time to get it all done! I try to make a list of a few things to get done each day and do them. I try not to make it too long or I never accomplish it all. Does it work? Sometimes. Some days I get it all and more done, some days not so much. Time is our enemy, Petula. :)

Pridaj komentár

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