Č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 curl
ovské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 Server
a 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 Component
u, 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. curl
e.
Component
má oproti Server
u 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í resource
u 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 curl
om, č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 ChocolatesResource
om 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 curl
e.
Ak máte v CLASSPATH
e 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 POST
om!
RESTovský klient pre POST
POST
ovanie 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 Converter
ov, 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, InputStream
y a Reader
y 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.
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á.
Vďaka! Nedošiel som ďalej ako za tutoriál, ale skvelá vec je Restlet in Action od Manningu (http://www.manning.com/louvel/)
Hi did u manage to sort out the problem with your phone i am having the exact same thing right now , and i was jus wondering if u know have a solution to the problem
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. :)