Java na webovém serveru: píšeme REST API

V dnešním díle našeho seriálu se budeme věnovat tvorbě REST API. Jelikož o tomto přístupu jste si zde na Zdrojáku mohli přečíst už dříve, teorii si zopakujeme jen velmi stručně a budeme se věnovat hlavně praktické stránce implementace REST rozhraní v Javě, konkrétně pomocí nástroje Jersey.

Seriál: Java na webovém serveru (16 dílů)

  1. Java na serveru: úvod 8.1.2010
  2. Java na webovém serveru: první web 15.1.2010
  3. Java na webovém serveru: práce s databází 29.1.2010
  4. Java na webovém serveru: práce s databází II 12.2.2010
  5. Java na webovém serveru: lokalizace a formátování 19.2.2010
  6. Java na webovém serveru: autorizace a autentizace 26.2.2010
  7. Java na webovém serveru: autorizace a autentizace II 5.3.2010
  8. Java na webovém serveru: porovnání Javy a PHP 10.3.2010
  9. Java na webovém serveru: Vlastní JSP značky a servlety 17.3.2010
  10. Java na webovém serveru: posílání e-mailů a CAPTCHA 24.3.2010
  11. Java na webovém serveru: píšeme REST API 7.4.2010
  12. Java na webovém serveru: SOAP webové služby 14.4.2010
  13. Java na webovém serveru: hlasování a grafy v SVG 28.4.2010
  14. Java na webovém serveru: Komentáře a integrace s Texy 9.6.2010
  15. Java na webovém serveru: AJAX formuláře 23.6.2010
  16. Java na webovém serveru: implementujeme Jabber 30.6.2010

Dosud jsme se v naší aplikaci zabývali komunikací s uživatelem – uživatelským rozhraním. Aplikace mohou ale komunikovat i s jinými aplikacemi a k tomu slouží API. Přestože „na druhém konci drátu“ v takovém případě není člověk, ale aplikace, hojně se dnes pro realizaci API využívá web resp. HTTP protokol.

Existují dva základní druhy webových služeb: klasické SOAP služby a RESTful služby.

Zatímco SOAP služby odpovídají principu RPC, tedy volání procedur, REST služby jsou orientované na „zdroje“. Tento rozdíl se projevuje i v URL – v případě SOAP bude obsahovat typicky nějaké sloveso, např. přijmiObjednávku, kdežto v případě REST služby bude URL obsahovat jen podstatné jméno objednávka.

Zda zvolit jeden nebo druhý přístup záleží na konkrétním použití. Jako autoři aplikace musíte vědět, zda spíše voláte „procedury“ nebo spíše přistupujete ke „zdrojům“ a provádíte nad nimi CRUD operace. Toto rozhodnutí by mělo vycházet z logické podstaty vaší aplikace. V dnešním díle se budeme zabývat RESTful webovými službami – ovšem o SOAP WS nebudete ochuzeni – přijde na ně řada v dalších dílech seriálu.

Více o RESTu se můžete dočíst v článku REST: architektura pro webové API.

Specifikace a implementace

Jelikož ve světě Javy se relativně hodně dbá na standardizaci, máme zde jednak specifikaci RESTful webových služeb „JSR-000311 JAX-RS: The Java API for RESTful Web Services“ a jednak několik jejích implementací. V naší aplikaci použijeme referenční implementaci zvanou Jersey. Stejně dobře (nebo i lépe) by nám měla posloužit i jakákoli jiná implementace  – např. CXF od Apache. Jersey je ale obsažena v aplikačním serveru, který používáme, takže její využití bude přímočařejší.

Java REST

Jednoduchý příklad

Nejprve si aktualizujeme zdrojové kódy naší aplikace pomocí Mercurialu:

$ hg pull
$ hg up "11. díl"

Případně je můžete stáhnout jako bzip2 archiv přes web.

Pro přehlednost jsem REST API vyčlenil do zvláštního webového modulu ( nekurak.net-rest), aby se jeho kód nepletl dohromady s kódem pro uživatelské rozhraní. Abychom mohli využívat REST webové služby, přidáme si do web.xml servlet, který bude tyto požadavky obsluhovat.

<servlet>
    <servlet-name>REST</servlet-name>
    <servlet-class>com.sun.jersey.spi.container.servlet.ServletContainer</servlet-class>
    <load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
    <servlet-name>REST</servlet-name>
    <url-pattern>/*</url-pattern>
</servlet-mapping>

Servlet si můžeme pojmenovat, jak se nám líbí ( REST). Důležitá je cesta, na kterou je namapován – v našem případě je to přímo kořen webového modulu. Pokud bychom REST API implementovali ve stejném modulu jako zbytek webové aplikace, namapovali bychom servlet např. na /zdroje/*.

Nyní přistoupíme k vlastní implementaci služby. Jedná se o prostou javovou třídu – nemusí implementovat žádné rozhraní ani být potomkem jiné zvláštní třídy – pouze obsahuje anotace z balíčku javax.ws.rs. Nejjednodušší REST služba vypadá takto:

@Path("clanek")
public class ClankyREST {

    private static String data;

    @GET
    @Produces("text/plain")
    public String getText() {
    return "Naposledy sem někdo poslal:n" + data + "n";
    }

    @PUT
    @Consumes("text/plain")
    public String putText(String data) {
    ClankyREST.data = data;
    return "Právě jsme od vás přijali:n" + data + "n";
    }
}

Pomocí anotace třídy @Path("clanek") určíme cestu, na které bude daný zdroj dostupný. Cesta je relativní vůči servletu definovanému výše (v našem případě kořen webového modulu). V případě lokálního testovacího serveru bude výsledné URL vypadat nějak takhle:  http://localhost:8080/nekurak.net-rest/clanek

Následně přidáme anotace k metodám. Pomocí anotací @GET, @PUT, @POST a @DELETE (případně @HEAD) si označíme metody, které mají obsluhovat požadavky klientů – anotace odpovídají stejnojmenným HTTP metodám.

Anotace @Produces a @Consumes slouží k definování MIME typu odesílaných nebo přijímaných dat. Pro jednoduchost jsme zvolili prostý text.

Funkčnost čtení ( GET) si ověříme jednoduše nasměrováním webového prohlížeče na příslušnou URL. Pro vyzkoušení odesílání dat na server ( PUT) můžeme použít nástroj cURL. V Debianu nebo Ubuntu si ho snadno nainstalujete pomocí příkazu:

aptitude install curl

Data naší REST službě odešleme z příkazové řádky takto:

curl -i -X PUT -H "Content-Type: text/plain" --data "Ahoj. Jak se máš?" http://localhost:8080/nekurak.net-rest/clanek

Služba data přijme a uloží si je do statické proměnné.Data si následně můžeme „vyzvednout“ na stejné URL – tentokrát ale použijeme metodu GET (k čemuž nám stačí webový prohlížeč).

Výše uvedená služba moc užitečná není a slouží pouze k osvojení základů. Nyní tedy přistoupíme k reálnějšímu příkladu.

Praktická ukázka

Naším cílem bude vytvořit REST API pro aplikaci Nekuřák.net, resp. její „redakční“ část – napíšeme si API, které nám umožní pracovat s „články“  – ty jsou prozatím velmi jednoduché: jen struktura, která má nadpis a text.

Toto API můžeme použít např. pro komunikaci s editorem, který poběží na našem desktopu a ve kterém budeme moci pohodlně psát články.

Požadované funkce jsou:

  • vypsat si seznam článků v systému
  • získat konkrétní článek
  • vložit nový článek
  • aktualizovat článek
  • smazat článek

Pro tyto úkoly nám už prostý text ( text/plain) stačit nebude, a tak použijeme univerzální formát XML. K tomu budeme potřebovat mapování mezi světem objektů a světem XML – to si naštěstí nemusíme psát ručně a stačí použít JAXB (Java Architecture for XML Binding), díky které to zvládneme celkem bezbolestně pomocí pár anotací.

Vsuvka: mapování mezi objekty a XML

Vytvoříme si jednoduchou obalovou třídu ClanekXML, která bude sloužit pro mapování objektů na XML a XML na objekty. Použijeme anotaci třídy @XmlRootElement(name="clanek"), která říká, že třída se bude mapovat na XML element s názvem clanek, který může být kořenovým XML elementem. Dále přidáme anotaci @XmlElement ke metodám reprezentujícím data (nadpis, id a text).

A to je vše – při převodu instance třídy ClanekXML dojde v vygenerování XML, které vypadá např. takto:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<clanek>
    <id>1</id>
    <nadpis>Úvod</nadpis>
    <text>Text úvodního článku</text>
</clanek>

A zároveň můžeme díky těmto anotacím převádět zpět XML na objekty. Mapování je obousměrné.

XML anotace bychom případně mohli použít rovnou ve třídě cz.frantovo.nekurak.dto.Clanek a obalovou třídu ClanekXML vůbec nevytvářet, ale jelikož konverzi z a do XML potřebujeme jen v REST API a v jiných vrstvách nás XML mapování nezajímá, zvolil jsem přístup s obalovou třídou.

Implementace REST API

Oproti předchozímu jednoduchému příkladu už neukládáme data do statické proměnné, ale voláme metody z EJB vrstvy, která následně volá metody datové vrstvy.

@Path("clanek")
public class ClankyREST {

    private static final String MIME_XML = "text/xml";
    private static final String MIME_TEXT = "text/plain";
    private HledacSluzby hledac = new HledacSluzby();

    /** Vypíšeme seznam všech článků v systému */
    @GET
    @Path("/")
    @Produces(MIME_XML)
    public Collection<ClanekXML> seznam() {
    Collection<ClanekXML> vysledek = new ArrayList<ClanekXML>();
    Collection<Clanek> clanky = hledac.getClanekEJB().getClanky();

    for (Clanek c : clanky) {
        vysledek.add(new ClanekXML(c));
    }

    return vysledek;
    }

    /** Získáme konkrétní článek */
    @GET
    @Path("/{id}")
    @Produces(MIME_XML)
    public ClanekXML ziskej(@PathParam("id") int id) {
    Clanek c = hledac.getClanekEJB().getClanek(id);
    return new ClanekXML(c);
    }

    /**
     * Vložíme nový článek
     * @return ID založeného článku (návratový typ musí být String – ne int)
     */
    @POST
    @Consumes(MIME_XML)
    @Produces(MIME_TEXT)
    public String zaloz(ClanekXML xml) {
    int id = hledac.getClanekEJB().zalozClanek(xml.getClanek());
    return id + "n";
    }

    /** Aktualizujeme článek */
    @PUT
    @Consumes(MIME_XML)
    @Path("/{id}")
    public void uprav(@PathParam("id") int id, ClanekXML xml) {
    xml.setId(id);
    hledac.getClanekEJB().upravClanek(xml.getClanek());
    }

    /** Smažeme článek */
    @DELETE
    @Path("/{id}")
    public void smaz(@PathParam("id") int id) {
    Clanek c = new Clanek();
    c.setId(id);
    hledac.getClanekEJB().smazClanek(c);
    }
}

Metoda seznam() slouží k vypsání všech článků. Uživatel pošle HTTP GET požadavek na danou službu např. http://localhost:8080/nekurak.net-rest/clanek a obdrží výpis ve formátu XML.

Metoda ziskej() slouží k získání jednoho článku. Opět používáme HTTP GET, ale jelikož už přistupujeme ke konkrétnímu zdroji, URL obsahuje číslo článku – např. http://localhost:8080/nekurak.net-rest/clanek/1.

URL požadavku nemusíme nijak ručně parsovat – pouze zadáme jako cestu vhodný vzor: @Path("/{id}") a následně si necháme číslo článku vložit do příslušného parametru metody: @PathParam("id") int id. Implementace JAX-RS se už postará o kontrolu parametrů a k nám už se dostane bezpečná proměnná typu int. Pokud by např. uživatel požadoval URL /nekurak.net-rest/clanek/xyz, dostal by odpověď 404 Nenalezeno, jelikož na dané adrese není mapován žádný zdroj.

Třetí metoda slouží k vytváření nových článků. Používáme HTTP metodu POST a URL číslo článku neobsahuje – to se totiž vygeneruje automaticky (v datové vrstvě) a klient ho dostane zpět v těle HTTP odpovědi. Operaci si můžeme vyzkoušet z příkazové řádky pomocí nástroje cURL:

curl -i -X POST -H "Content-Type: text/xml" --data "<clanek><nadpis>Ahoj</nadpis><text>vloženo CURLem</text></clanek>" -u franta http://localhost:8080/nekurak.net-rest/clanek

Metoda uprav() slouží k aktualizaci existujícího článku. Který článek se bude aktualizovat závisí na URL. Odeslání dat je obdobné, pouze tentokrát použijeme HTTP PUT operaci.

Jelikož zapisovat složitější dokumenty do příkazové řádky je nepřehledné, můžeme si je připravit jako soubor na disku a odeslat následujícím způsobem:

cat připravený-soubor.xml | curl -i -X PUT -H "Content-Type: text/xml" --data @- -u franta http://localhost:8080/nekurak.net-rest/clanek/1

Všimněte si, že v REST metodách, kde přijímáme vstup od uživatele se nemusíme nijak starat o konverzi XML na objekty – pouze deklarujeme jako parametr metody daný datový typ ( ClanekXML) a automaticky dostáváme jeho instanci.

Poslední metoda slouží k mazání článků. Nepracuje s žádnými vstupními ani výstupními daty (nejsou potřeba). Jediná informace se předává v URL – číslo článku, který chceme smazat.

curl -i -X DELETE -u franta http://localhost:8080/nekurak.net-rest/clanek/1

Zabezpečení

Jelikož REST API umožňuje provádět i zápis a mazání, povolíme přístup jen oprávněným uživatelům. Využijeme toho, co jsme se naučili v jednom z předešlých dílů: Autorizace a autentizace, a zabezpečíme metody EJB (třída ClanekEJB) pomocí anotace @RolesAllowed("redaktor"). Povolíme tak přístup jen vybraným uživatelům s rolí redaktor.

Nezapomeneme na úpravu web.xml, kde nastavíme způsob přihlašování – pro REST API zvolíme HTTP BASIC autentizaci (zatímco pro uživatelské rozhraní jsme používali HTML formuláře).

<security-role>
    <role-name>redaktor</role-name>
</security-role>
<security-constraint>
    <web-resource-collection>
    <web-resource-name>Nekuřák.net REST API</web-resource-name>
    <url-pattern>/*</url-pattern>
    <http-method>PUT</http-method>
    <http-method>POST</http-method>
    <http-method>DELETE</http-method>
    </web-resource-collection>
    <auth-constraint>
    <role-name>redaktor</role-name>
    </auth-constraint>
</security-constraint>
<login-config>
    <auth-method>BASIC</auth-method>
    <realm-name>nekurakNET</realm-name>
</login-config>

Pokud bychom úpravu web.xml neprovedli, uživatel by neměl možnost se přihlásit a pokusy o vytváření nebo mazání by končily neúspěchem ( AccessException: CORBA NO_PERMISSION). A pokud bychom naopak vynechali anotace uvnitř EJB tříd, bylo by zabezpečení naší aplikace velice křehké – záviselo by pouze na nastavení prezentační webové vrstvy. Potřebné jsou tedy obě úpravy.

Závěr

Dnes jsme se naučili základy tvorby REST API v Javě pomocí JAX-RS. Jedná se o poměrně rozsáhlou problematiku, nicméně po přečtení dnešního článku byste měli být schopní naprogramovat API, které poslouží např. pro komunikaci desktopových klientů s danou webovou aplikací. REST API můžete rovněž využít při tvorbě moderních AJAXových aplikací – implementace serverové části takové aplikace pomocí JAX-RS může být vhodnější než implementace pomocí JSP nebo klasických servletů.

Odkazy

Franta Kučera působí jako Java vývojář na volné noze. Programování je jeho koníčkem už od dětství. Kromě toho má rád Linux, relační SŘBD a XML.

Věděli jste, že nám můžete zasílat zprávičky? (Jen pro přihlášené.)

Komentáře: 10

Přehled komentářů

František Kučera Odkaz
peter Re: Odkaz
peter SOAP vs REST
František Kučera Re: SOAP vs REST
peter Re: SOAP vs REST
František Kučera Re: SOAP vs REST
peter Re: SOAP vs REST
marmax Re: SOAP vs REST
marmax Re: SOAP vs REST
peter Re: SOAP vs REST
Zdroj: https://www.zdrojak.cz/?p=3206