2009. december 27., vasárnap

RESTful web szolgáltatások Jersey-vel

Az általam is már többször részletezett, nehézsúlyú, bonyolult SOAP alapú web szolgáltatások és API-k összetettségét ellensúlyozva jelent meg Roy Fielding PhD dolgozata alapján a REST fogalom.

Most egy egyszerű kliens-szerver alkalmazást kellett írnom, ahol különböző eseményeket kellett küldenem a szerver oldalra. Az RMI-t elvetettem, hiszen bonyolultabban vezethető át a protokollja a tűzfalon, és csak Java-ból használható, a SOAP-ot ágyúnak éreztem, képbe került még az XML-RPC, mely nem szabványos, így választásom a JAX-RS-re esett, mellyel Java-ban lehet RESTful web szolgáltatásokat építeni.

A JAX-RS egy specifikáció (JSR 311), melynek jelenlegi legfrissebb verziója az 1.1-es, és több implementációja is létezik, köztük a referencia implementáció, a Jersey, melynek legutolsó verziója a 1.1.4.1.

A REST egy szoftver architektúra, mely létező, bevált protokollokra, szabványokra építkezik, ahelyett, hogy újat találjon ki. Úgynevezett erőforrásokból (resource) építkezik, és mindegyik ilyen erőforrásnak van egyedi azonosítója, és ezeket össze is lehet linkelni. A kliens ezen erőforrásokat kéri le, azok azonosítója alapján, de lehetőség van új erőforrás hozzáadására, módosítására, törlésére is. A kérések egymástól függetlenek, nem létezik munkamenet (session) fogalom, a kliens mindig az adott erőforrás egy adott állapotát kapja vissza. (Emiatt egyszerűbb az architektúra, könnyen megoldható a gyorsítótárazás - cache, valamint a terhelés elosztás.) Az erőforrásokat különböző módon lehet megjeleníteni, pl. egy számsort magával a számok sorozatával, de akár egy grafikonnal. Ezen tulajdonságok alapján jött a rövidítés: representational state transfer.

A felhasznált létező szabványok az URI, mellyel az erőforrások azonosítója adható meg. Az erőforrások különböző megjelenítési módjait MIME type-pal lehet megadni, ami lehet egyszerű szöveg, html, xml, vagy manapság az egyre divatosabb JSON is, vagy speciálisabb esetekben pl. kép is. A leggyakrabban használt protokoll a HTTP, hiszen ez biztosítja a kliens-szerver architektúrát, állapotmentességet, cache-elhetőséget, különböző rétegek kialakítását (akár transzarens módon a titkosítást, lásd https), valamint ez van átengedve a legtöbb hálózati eszközön, tűzfalon. A HTTP metódusaival a CRUD műveleteket is megvalósíthatóak, a legtöbbet használt GET, POST mellett létezik a PUT és DELETE is. Ugyanúgy, ahogy a klasszikus web szolgáltatások esetében, más protokoll is választható.

A JAX-RS a JAX-WS-hez hasonlóan egyszerű POJO-kkal dolgozik, melyekre annotációkat kell használni.

A konkrét példára visszatérve, ami HTTP protokollon lett megvalósítva, a következő URL-eket definiáltam:

  • /events: GET esetén XML formátumban adja vissza az eddig elküldött eseményeket, POST esetén az eseményt várja XML formátumban, melyet elment
  • /events/14: XML formátumban adja vissza a 14-es azonosítójú eseményt

Első esetben a visszaadott XML:

<events>
 <event>
   <id>1</id>
   <date>2009-12-26T17:58:26.571Z</date>
   <message>Első esemény</message>
 </event>
</events>

Amennyiben egy eseményt szeretnénk lekérdezni, vagy felküldeni, csak az event tag tartalma használandó. Amennyiben nem XML-lel akarunk dolgozni, használható az "application/xml" helyett pl. az "application/json" MIME type is.

A RESTful web szolgáltatásomat egy Spring-es alkalmazásba próbáltam beépíteni, mely egy egyszerű NetBeans-es projekt, melynek a build folyamatát Ant vezérli. És itt jött az első meglepetés, hogy a leírások, és a teljes folyamat Maven-re van optimalizálva. A dokumentáció azt részletezi, mit kell a pom.xml-be beírni. Én jobban szeretem az olyan library-ket, amely készítői a nem Maven-t használókra is gondoltak, és legalább összeállítanak egy olyan csomagot (zip, tgz), amiben megtalálhatóak a jar-ok, azok függőségei, teszt esetek, API JavaDoc formátumban, dokumentáció és példa alkalmazások. Ezt most nekem kellett összevadásznom mindenféle kaotikus Maven repository-kből, persze nem sikerült elsőre.

Az alkalmazásba a következő JAR-okat kellett tennem: jsr311-api-1.1.jar, jersey-core-1.1.4.1.jar, jersey-server-1.1.4.1.jar (az utóbbi kettő helyettesíthető a jersey-bundle-1.1.4.1.jar állománnyal), valamint az asm-3.1.jar állományt. Mivel Spring-et használok, kellett a jersey-spring-1.1.4.1.jar is.

Először készítsük el az eseményt reprezentáló osztályt:

public class Event {
private Long id;

private Date date;

private String message;

// konstruktorok, getter/setter metódusok
}

Utána készítsük el az erőforrást reprezentáló osztályt:

@Path("/events")
@Component
public class EventResource {

@GET
@Produces({"application/xml", "application/json"})
public List listEvents() {
// DAO hívás, események betöltése
}

@GET
@Path("/{id}")
@Produces({"application/xml", "application/json"})
public Event findEvent(@PathParam("id") long id) {
  // DAO hívás, esemény betöltése
}

@POST
@Consumes({"application/xml", "application/json"}) {
public void createEvent(Event event)
// DAO hívás, esemény mentése
}
}

Látható, hogy az erőforrásban három metódust definiáltunk, rendre a következő funkciókkal: események betöltése, esemény betöltése, esemény mentése. Mindhárom a /events címen érhető el, mint az osztályon lévő @Path annotáció mutatja. Az első két funkció GET metódussal érhető el, és XML és JSON kimenetet is képes gyártani (@Produces annotáció), a harmadik POST metódussal, és képes XML és JSON bemenetet is fogadni (@Consumes annotáció). A második metódusnál figyeljük meg, hogy az URL-ben megadhatók változók is, melyre később a @PathParam annotációval hivatkozunk.

Ahhoz, hogy a kérés ki is legyen szolgálva, a web.xml-be betesszük a következő részt:

<servlet>
 <servlet-name>Jersey Spring Web Application</servlet-name>
 <servlet-class>com.sun.jersey.spi.spring.container.servlet.SpringServlet</servlet-class>
</servlet>
<servlet-mapping>
 <servlet-name>Jersey Spring Web Application</servlet-name>
 <url-pattern>/events/*</url-pattern>
</servlet-mapping>

Valamint a Spring applicationContext.xml állományába:

<context:annotation-config/>

<context:component-scan base-package="jtechlog.restful" />

Ez eredményezi, hogy a @Component annotáció hatására a Spring beolvassa az EventResource osztályt.

Amikor böngészőből meg akartam hívni a /events URL-t a következő hibaüzenetet kaptam:

SEVERE: Internal server error
javax.ws.rs.WebApplicationException
at com.sun.jersey.spi.container.ContainerResponse.write(ContainerResponse.java:253)
at com.sun.jersey.server.impl.application.WebApplicationImpl._handleRequest(WebApplicationImpl.java:814)
at com.sun.jersey.server.impl.application.WebApplicationImpl.handleRequest(WebApplicationImpl.java:740)
at com.sun.jersey.server.impl.application.WebApplicationImpl.handleRequest(WebApplicationImpl.java:731)
at com.sun.jersey.spi.container.servlet.WebComponent.service(WebComponent.java:372)
at com.sun.jersey.spi.container.servlet.ServletContainer.service(ServletContainer.java:452)
at com.sun.jersey.spi.container.servlet.ServletContainer.service(ServletContainer.java:633)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:717)
...

Ebből látszik, hogy a hibaüzenet nem informatív, nem mondja meg, hogy konkrétan mi is a hiba. Némi gondolkodás után rájöttem, hogy mivel az XML-t JAXB-vel próbálja legyártani, érdemes lenne tenni egy @XmlRootElement annotációt az Event osztályra. Ezután tökéletesen működött.

És most nézzük a kliens kódot, mondjuk az esemény mentésére (úgy tűnik, itt már nem a standard javax.ws.rs csomagokat kell importálni, hanem a com.sun.jersey.api.client csomagokat):

ClientConfig cc = new DefaultClientConfig();
Client c = Client.create(cc);
WebResource wr = c.resource("http://localhost:8080/restful/events");
Event event = new Event(new Date(), "Első esemény");
wr.path("events").type(MediaType.APPLICATION_XML_TYPE).post(event);

Látható, hogy ez a RESTful web szolgáltatások használata nem csak az API ismeretét igényli, hanem egy másfajta gondolkodásmódot is.

2 megjegyzés:

  1. Köszi a REST fogalmának szabatos összefoglalását, még biztosan hasznomra lesz :)

    Nem tudom, hogy mennyire ismerős számodra a "konkurrens" implementáció, a JBoss-féle RESTEasy. Ha nem ismerős, akkor ajánlom figyelmedbe, ha ismerős, akkor írhatnál róla tapasztalatokat :)

    Üdv,
    M

    VálaszTörlés
  2. Sajnos nem ismerem a RESTEasy-t, de amint valami miatt felidegesít a Jersey, kipróbálom azt is.

    VálaszTörlés