RESTful mit Jersey und Spring

26. Juli 2009 von Dirk Dittmar [permalink]

Letzes mal habe ich einen Artikel über JAX-WS geschrieben, was natürlich in einem SOAP Service endete. Da im Moment RESTful Webservices viel hipper sind, habe ich mir mal angesehen wie man mit Spring und Jersey (eine JAX-RS Implementierung) einen RESTful Webservice zur verfügung stellen kann.

Was RESTful Webservices sind und wie sie im Prinzip funktionieren könnt ihr überall im Web nachlesen, deshalb gibt es hier keine Einführung in das Thema.

Meiner Meinung nach haben RESTful Webservices den bestechenden Vorteil der Einfachheit gegenüber SOAP. Das gillt jetzt nicht unbedingt für die Implementierung des Servers, aber mit Sicherheit für die Nutzung des Services und die Anbindung des Clients. In dem folgenden Beispiel läuft die gesammte Kommunikation über HTTP bzw. XML ab und in welcher Sprache / Umgebung sollte das ein Problem darstellen? Sicher ist das bei SOAP auch so, aber dort ist doch etwas mehr notwendig um eine Kommunikation herzustellen. Einen RESTful Webservice kann man auch ohne größere Frameworks nutzen und das soll mir mal einer für SOAP zeigen.

Als Beispiel stelle ich im folgenden die Implementierung eines Adressen-Services vor. Der Service kann nicht besonders viel, nur folgendes:

  • Alle Id’s der gespeicherten Adressen holen
  • Eine Adresse abrufen
  • Eine Adresse einfügen bzw. updaten
  • Eine Adresse löschen

Also der ganz normale CRUD Kram den man so gewohnt ist.

Daten

Was man als erstes braucht ist eine XSD, die Definiert welches XML zur Kommunikation so verwendet wird:

<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
  elementFormDefault="qualified" xmlns:jaxb="http://java.sun.com/xml/ns/jaxb"
  jaxb:version="1.0" xmlns:xjc="http://java.sun.com/xml/ns/jaxb/xjc"
  jaxb:extensionBindingPrefixes="xjc">

  <xs:annotation>
    <xs:appinfo>
      <jaxb:globalBindings>
        <xjc:simple />
      </jaxb:globalBindings>
    </xs:appinfo>
  </xs:annotation>

  <xs:element name="address" type="address" />

  <xs:element name="addresses" type="addresses" />

  <xs:element name="phoneNumber" type="phoneNumber" />

  <xs:element name="exceptionWrapper" type="exceptionWrapper" />

  <xs:complexType name="addresses">
    <xs:sequence>
      <xs:element name="keys" type="xs:string" minOccurs="0"
        maxOccurs="unbounded" />
    </xs:sequence>
  </xs:complexType>

  <xs:complexType name="address">
    <xs:sequence>
      <xs:element name="city" type="xs:string" minOccurs="0" />
      <xs:element name="firstName" type="xs:string" />
      <xs:element name="housenumber" type="xs:string" minOccurs="0" />
      <xs:element name="key" type="xs:string" />
      <xs:element name="lastName" type="xs:string" />
      <xs:element name="numbers" type="phoneNumber" minOccurs="0"
        maxOccurs="unbounded" />
      <xs:element name="postalcode" type="xs:string" minOccurs="0" />
      <xs:element name="street" type="xs:string" minOccurs="0" />
    </xs:sequence>
  </xs:complexType>

  <xs:complexType name="phoneNumber">
    <xs:sequence>
      <xs:element name="areaCode" type="xs:string" minOccurs="0" />
      <xs:element name="number" type="xs:string" />
    </xs:sequence>
  </xs:complexType>

  <xs:complexType name="exceptionWrapper">
    <xs:sequence>
      <xs:element name="stacktrace" type="xs:string" />
    </xs:sequence>
  </xs:complexType>

</xs:schema>

Wie man sieht werden hier vier Datenstrukturen definiert:

  1. Eine Adresse die Telefon-Nummern enthalten kann.
  2. Ein Adressen-Objekt das dazu dient die Schlüssel der Adressen zu transportieren.
  3. Die Telefon-Nummer, die aus einer optionalen Vorwahl und er eigentlichen Nummer besteht.
  4. Ein Exception-Wrapper der dazu dient Exception zum Client zu transportieren.

Das marshalling/unmarshalling des XML’s findet natürlich mit JAXB statt. Deshalb ist auch die Annotation ganz wichtig! Sie hilft JAXB unsere Objekte richtig zu erzeugen und vereinfacht so den Umgang mit den Objekten. Ohne diese Annotation würde JAXB kein @XmlRootElement an unseren Objekten erzeugen. Ihr könnt hier mehr darüber erfahren: Why does JAXB put @XmlRootElement sometimes but not always?

Spring und Jersey konfigurieren

Bevor wir so richtig loslegen ist es an der Zeit unsere Umgebung einzurichten. Natürlich verwende ich hier Spring. Ohne eine Spring-Integration währe Jersey auch ziehmlich zweckfrei, da man ja sein bestehendes Service-Modell weiter verwenden möchte.

Werfen wir zunächst einen Blick auf die web.xml:

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.4" xmlns="http://java.sun.com/xml/ns/j2ee"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">

  <context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>/WEB-INF/applicationContext.xml</param-value>
  </context-param>

  <context-param>
    <param-name>log4jConfigLocation</param-name>
    <param-value>/WEB-INF/log4j.properties</param-value>
  </context-param>

  <listener>
    <listener-class>org.springframework.web.util.Log4jConfigListener</listener-class>
  </listener>

  <listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
  </listener>
  <listener>
    <listener-class>
      org.springframework.web.context.request.RequestContextListener</listener-class>
  </listener>

  <servlet>
    <servlet-name>Jersey Web Application</servlet-name>
    <servlet-class>
      com.sun.jersey.spi.spring.container.servlet.SpringServlet</servlet-class>
    <init-param>
      <param-name>com.sun.jersey.config.property.packages</param-name>
      <param-value>de.wortzwei.jersey.spring.ext</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
  </servlet>

  <servlet-mapping>
    <servlet-name>Jersey Web Application</servlet-name>
    <url-pattern>/rest/*</url-pattern>
  </servlet-mapping>

</web-app>

Das eigentliche Herzstück bildet hier das SpringServlet und dessen Konfiguration. In der Konfiguration wird angegeben welche Packete nach Klassen durchsucht werden. Diese Klassen werden nur von Jersey verwaltet! Klassen die von Spring verwaltet werden sind im Application-Context zu finden. Die Klassen im Packet de.wortzwei.jersey.spring.ext werde ich später beschreiben (wenn wir zum Exception-Handling / Validation kommen). Sonst ist hier nichts besonderes zu sehen (und das ist gut so).

Der Spring-Kontext ist zum Glück auch nicht besonders dramatisch:

<beans xmlns="http://www.springframework.org/schema/beans"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context"
  xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-2.5.xsd">

  <context:component-scan base-package="de.wortzwei.jersey.spring.rest" />

  <bean id="addressService" class="de.wortzwei.jersey.spring.service.AddressServiceImpl" />

</beans>

Das einzige was auffällt ist das <context:component-scan> Tag. Hier wird angegeben welche Packete von Spring durchsucht werden um dort evtl. verdrahtungen (injections) vorzunehmen. Im Packet de.wortzwei.jersey.spring.rest befinden sich die Resource-Klassen (die Klassen die den eigentlichen Request behandeln) und dort wollen wir ja Zugriff auf unsere Business-Schicht haben.

let’s get RESTful…

Kommen wir zum Herzstück der Anwendung. In der Klasse AddressResource wird fast alles was unsere Anwendung ausmacht getan:

@Component
@Scope("singleton")
@Path("/address")
public class AddressResource {

  @Resource
  private AddressService addressService;

  @GET
  @Produces("application/xml")
  public Addresses getAllAddresses() {
    Addresses addresses = new Addresses();
    addresses.getKeys().addAll(addressService.getAddressKeys());
    return addresses;
  }

  @GET
  @Path("{key}")
  @Produces("application/xml")
  public Address getAddress(@PathParam("key") String key) {
    return addressService.findAddressByKey(key);
  }

  @DELETE
  @Path("{key}")
  public void deleteAddress(@PathParam("key") String key) {
    addressService.deleteAddressByKey(key);
  }

  @PUT
  @Consumes("application/xml")
  public void updateAddress(Address address) {
    addressService.insertAddress(address);
  }

  @POST
  @Consumes("application/xml")
  public void insertNewAddress(Address address) {
    addressService.insertAddress(address);
  }

}

Sehen wir uns die Klasse mal an:

@Component ist eine Spring-Annotation und sagt Spring das es sich bei dieser Klasse um eine Komponente handelt. Komponenten werden von Spring näher untersucht um dort dann evtl. injections oder ähnliches zu machen:

Such classes are considered as candidates for auto-detection when using annotation-based configuration and classpath scanning.

@Scope bestimmt den Scope dieser Komponente wie z.B. “singelton” oder “prototype”. Hier können folgende Werte angegeben werden:
request: Der Lebenzyklus der Komponente wird an den HTTP-Request gebunden. Das heißt das für jeden HTTP-Request eine neue Komponente erzeugt wird.
singelton: Diese Komponente wird nur ein mal für den gesammten Lebenszyklus der Anwendung erzeugt.
prototype: Die Komponente wird für jede Anfrage an diese Komponente neu erzeugt.

Bei @Path handelt es sich um eine JAX-RS Annotation. Es wird der relative Pfad angegeben unter dem dieser Resource zu erreichen ist. In userem Fall ist unsere Resource also unter “address” zu erreichen.

Mit der Annotation @Resource erreichen wir das Spring uns (wann immer diese Komponente nur auch erzeugt wird) das Bean “addressService” injected.

Die Annotationen @GET, @DELETE, @POST und @PUT geben an bei welcher Art des Request die entsprechende Methode aufgerufen wird. @Consumes gibt an welche Daten Konsumiert werden (hier XML). @Produces gibt an welche Daten erzeugt werden (auch XML).

Interessant ist sonst noch die Annotation @PathParam. Mit der Annotation wird der an der Methode symbolisch angegebene Pfad-Name an den Methoden-Parameter gebunden. So ist es z.B. möglich mit einem HTTP-DELETE auf der URL /address/test den Datensatz mit dem Schlüssel “test” zu löschen.

Es gibt noch viele weitere Möglichkeiten derartige “Tricks” zum machen. Dazu muss ich euch aber auf die entsprechende Dokumentation im Internet verweisen.

Validation und Exception-Handling

Wie bereits erwähnt hat unsere Anwendung noch Klassen zur Validierung und zum behandeln von Exceptions. Ohne die Klassen im Packet de.wortzwei.jersey.spring.ext könnten Clients uns mit nicht validen Daten überfluten und Exception die bei uns auftreten könnten den Client gewaltig aus dem Tritt bringen!

Zur Validierung dienen die beiden Klassen ValidatingJAXBContextResolver und ValidatingJAXBContext. Die Klasse ValidatingJAXBContextResolver ist dabei ein @Provider der Jersey einen JAXBContext zur Verfügung stellt. Die Implementierung stellt dann einen ValidatingJAXBContext zur Verfügung. ValidatingJAXBContext ist ein JAXBContext der mit einem ValidationEventCollector ausgestattet ist. Dieser ValidationEventCollector wirft eine selbst geschriebene JAXBParserException wenn ein Validierungsfehler auftritt.

Um unsere JAXBParserException und die anderen Exceptions, die auftreten können vernünftig zu Client zu kommunizieren, dient die Klasse ExtExceptionMapper. Diese Klasse ist auch ein @Provider. Sie stellt Jersey einen ExceptionMapper zur Verfügung. Die Implementierung ist in unserem Beispiel recht einfach gehalten. Im Prinzip wird von jedem Throwable der Stacktrace ausgelesen und mittels der Klasse ExceptionWrapper zum Client übermittelt. Ohne diesen ExceptionMapper-Mechanismus würde der Client einfach nur den Stacktrace als HTML-Seite zu sehen bekommen und diese “Nachricht” ist etwas schwieriger zu verarbeiten wenn man mit vorher definiertem XML gerechnet hat.

Fazit

Und? Ist nicht so schwierig, oder? Das meiste habe ich so ohne weiteres hinbekommen. Das einzig schwierige war der Teil mit dem Validator und dem Exception-Handling.

Viel Spaß beim selber ausprobieren!

Achja! Das voll funktionsfähige Maven-Projekt findet ihr hier: jersey-spring.zip

Tags: , , , ,
Kategorie: Java, Softwareentwicklung

Sie können die Kommentare für diesen Post mit diesem RSS 2.0 Feed verfolgen. Hinterlassen sie einen Kommentar oder einen Trackback von ihrem Blog.

Hinterlasse einen Kommentar: