Java Magazin: Webservices mit Maven und JAX-WS

Java Magazin 03.2009: Webservices mit Maven und JAX-WS

Der Weg hin zu einer Webservice-Implementierung ist häufig mit vielen Hindernissen gespickt. Wie können diese Probleme gemeistert werden? Der folgende Artikel zeigt Schritt für Schritt ein pragmatisches Vorgehen mittels Maven und JAX-WS.

von Ralf Ebert und Gunnar Morling

Einleitung

Steht man vor der Aufgabe, einen Webservice bereitzustellen, sind zunächst einige Grundsatzentscheidungen zu treffen. So muss festgelegt werden, auf welche Weise der Schnittstellenkontrakt für den Service beschrieben werden soll. Ein möglicher Ansatz hierzu ist das sog. Code-First-Vorgehen, bei dem der Kontrakt aus einem bestehenden Interface abgeleitet wird. Für unser Beispielprojekt möchten wir jedoch den Kontrakt explizit ausformulieren. Diese Herangehensweise wird als Contract-First bezeichnetet.

Bei der Entwicklung von Webservices nach dem Contract-First-Ansatz wird zunächst der Schnittstellenkontrakt in der plattformneutralen Sprache WSDL beschrieben. Die in der Schnittstelle genutzten Typen werden dazu mit einer XML-Schemasprache wie XSD definiert. Daraus kann dann im zweiten Schritt ein Codegerüst generiert werden, das mit Leben gefüllt wird, indem etwa die Geschäftslogik aufgerufen wird. Diese Herangehensweise bietet einige Vorteile:

Weiterhin muss man sich für ein Webservice-Framework entscheiden, welches Service-Requests entgegennimmt, an die Implementierung weiterleitet und schließlich entsprechende Response-Nachrichten an den Aufrufer übermittelt. Hierzu gibt es viele konkurrierende Technologien. Wir werden im Folgenden mit JAX-WS (Java API for XML Web Services) arbeiten. Dies ist das API der Java-EE-Plattform für die Implementierung von Webservices. Es kann aber auch separat eingesetzt werden und ist seit Java 6 Teil des Standard-JDK. Wir werden die Referenzimplementierung (RI) verwenden, welche aktuell in Version 2.1.5 vorliegt.

Schließlich benötigen wir für unser Projekt ein Buildwerkzeug. Hierfür eignet sich hervorragend Apache Maven. Maven definiert eine Standardstruktur für Java-Projekte, lädt automatisch alle benötigten Bibliotheken herunter und erlaubt eine individuelle Anpassung des Buildprozesses mittels Plugins. So werden wir bestehende und ein eigenes Plugin zur Code-Generierung für Webservices nutzen und uns so viel manuelle Schreibarbeit ersparen.

Design der Schnittstelle

Der erste Schritt bei der Erstellung eines Webservices ist das Design des Schnittstellenkontraktes. Idealerweise kommen dazu Vertreter aller beteiligten Systeme zusammen. Zunächst sollten gemeinsam die Operationen, die der Webservice anbieten soll, herausgearbeitet werden. Als Beispiel wird im Folgenden ein einfacher Service für Produktabfragen realisiert. Es soll möglich sein, Produktinformationen anhand einer Artikelnummer abzurufen (getProductById) sowie anhand vorgegebener Kriterien nach Produkten zu suchen (findProducts).

Im XML-Schema definieren wir für jede Operation einen Ein- und Ausgabetypen. Hat man über diese noch keine Klarheit, empfiehlt es sich, mit Beispielnachrichten in XML zu beginnen, z.B.:

<GetProductByIdRequest>
    <Id>123</Id>
</GetProductByIdRequest>

<GetProductByIdResponse>
    <Product>
        <Id>123</Id>
        <Name>Jeans-Hose</Name>
        <Price>89.99</Price>
        <Size>L</Size>
    </Product>
</GetProductByIdResponse>

Ausgehend von diesem Beispiel ist es nun recht einfach, ein entsprechendes Schema abzuleiten. Dabei kann man sich von Tools wie Trang unterstützen lassen, die aus einem vorhandenen XML-Dokument ein Schema erzeugen, das meist nur noch etwas nachbearbeitet werden muss. In unserem Beispiel wurde das Schema gemäß Listing 1 verabschiedet. Die komplexen Ein- und Ausgabetypen sind jeweils in ein Element verpackt und haben nur Unterelemente, aber keine Attribute. Schemaseitig sind damit die Voraussetzungen für den sog. Document/Literal-Stil für Webservice-Schnittstellen gegeben, welcher eine hohe Interoperabilität derselben gewährleistet sowie eine Schemavalidierung der Request- und Responsenachrichten erlaubt. Aus dem deklarierten Target-Namespace wird beim Generieren der Package-Name für die Java-Bindingklassen abgeleitet - im Beispiel de.javamagazin.mvnjaxws.products.types.

<?xml version="1.0" encoding="UTF-8"?>
<schema targetNamespace="http://www.javamagazin.de/mvnjaxws/products/types"
    xmlns="http://www.w3.org/2001/XMLSchema"
    xmlns:products="http://www.javamagazin.de/mvnjaxws/products/types">

<!-- GetProductById -->
<element name="GetProductByIdRequest">
    <complexType>
        <sequence>
            <element name="Id" type="int" />
        </sequence>
    </complexType>
</element>
<element name="GetProductByIdResponse">
    <complexType>
        <sequence>
            <element type="products:Product" name="Product" minOccurs="0" />
        </sequence>
    </complexType>
</element>

<!-- FindProducts -->
<element name="FindProductsRequest">
    <complexType>
        <sequence>
            <element name="MaxResults" type="int" />
            <element name="SearchText" type="string" />
        </sequence>
    </complexType>
</element>
<element name="FindProductsResponse">
    <complexType>
        <sequence>
            <element minOccurs="0" maxOccurs="unbounded" type="products:Product"
                name="Products" />
        </sequence>
    </complexType>
</element>

<!-- Common types -->
<complexType name="Product">
    <sequence>
        <element name="Id" type="int" />
        <element name="Name" type="string" />
        <element name="Price" type="decimal" />
        <element name="Size" type="string" minOccurs="0" />
    </sequence>
</complexType>
</schema>

Listing 1: Das XML-Schema products.xsd

Anlegen eines neuen Maven-Projekts

Auf Grundlage des Schemas kann es nun an die Umsetzung des Webservices gehen. Hierzu legen wir zunächst ein neues Maven-Projekt an. Sofern Sie Maven noch nicht installiert haben, finden Sie auf der Apache Maven-Seite den Download und die Installationsanleitung. Maven erleichtert die Erstellung neuer Projekte durch Projektvorlagen, sog. Archetypen. Zur Erstellung eines Webservice-Projekts können wir mit folgendem Maven-Aufruf den von den Autoren bereitgestellten Archetypen instantws-archetype nutzen:

mvn archetype:generate -DarchetypeCatalog=
	http://maven-instant-ws.googlecode.com/svn/repo/archetype-catalog.xml

Nach dem Download der von Maven benötigten Bibliotheken müssen wir noch einige zur Projektanlage erforderliche Angaben machen:

In dem neu erstellten Projekt finden wir gemäß der vorgegebenen Maven-Projektstruktur folgende Pfade:

Maven-Projekte in Eclipse und NetBeans

Für Eclipse lässt sich mit mvn eclipse:eclipse eine Projektkonfiguration erzeugen. Optional können durch Angabe der Parameter -DdownloadSources=true -DdownloadJavadocs=true die Quellen und JavaDocs der referenzierten Bibliotheken heruntergeladen und eingebunden werden. Nachdem Sie unter Window > Preferences > Java > Build Path > Classpath Variables die Variable M2_REPO auf Ihr lokales Maven-Repository (z.B. /home/<nutzername>/.m2/repository) konfiguriert haben, können Sie das Projekt mit File > Import > General > Existing Projects into Workspace importieren. Alternativ besteht die Möglichkeit, mit dem Plugin M2Eclipse das Maven-Projekt direkt zu öffnen und Maven-Goals wie z.B. generate-resources aus der IDE heraus zu starten.

Auch für NetBeans steht ein Plugin zur Unterstützung von Maven-Projekten bereit. Dieses kann mittels des Plugin-Managers unter Tools > Plugins installiert werden. Anschließend können Maven-Projekte wie normale NetBeans-Projekte geöffnet werden. IDE-Aktionen wie “Build” oder “Test” führen einen entsprechenden Maven-Build aus, während im Kontextmenü unter Custom > Goals... beliebige Maven-Goals aufgerufen werden können. Mit dem Repository-Browser können diverse Maven-Repositories nach Bibliotheken durchsucht und diese als Abhängigkeit zum Projekt hinzugefügt werden. Weitere Hinweise zum Umgang mit Maven-Projekten in NetBeans finden Sie unter NetBeans Wiki: Maven Best Practices.

Einen Web-Service erstellen

Um den Webservice zu erstellen, legen wir das Schema mit den Typdefinitionen unter /src/main/resources/xsd/products.xsd ab. Zu einer vollständigen Webservice-Definition gehört neben dem XML-Schema noch ein WSDL-Dokument, das die Operationen des Services beschreibt. Dieses könnten wir von Hand erstellen, allerdings ist das WSDL-Format recht komplex und wenig intuitiv. Befolgt man bei der Benennung der Nachrichtentypen wie im Beispiel eine feste Konvention, ist es möglich, die WSDL-Datei aus dem XML-Schema abzuleiten. Aus dem Vorhandensein der Elemente FindProductsRequest und FindProductsResponse etwa kann die Operation FindProducts abgeleitet und im WSDL-Dokument angelegt werden. Für die Verwendung im Beispielprojekt wurde durch die Autoren ein Maven-Plugin entwickelt, welches die WSDL-Generierung auf die beschriebene Weise nahtlos in den Buildprozess integriert.

Mittels des Befehls

mvn clean generate-resources

können wir nun den Maven-Build bis zum Buildschritt generate-resources ausführen. Die im pom.xml des Projektes deklarierten Plugins erzeugen dabei die benötigten Artefakte:

Damit haben wir das Grundgerüst für den Service erstellt. Wir implementieren diesen nun, indem wir die Klasse de.javamagazin.mvnjaxws.products.ProductsPort in src/main/java erstellen. Zu Testzwecken geben wir in der Implementierung zunächst nur Beispieldaten zurück. Damit die Klasse als Webservice-Endpunkt anerkannt wird, müssen wir sie mit der Annotation @WebService versehen (siehe Listing 2). Über das Attribut endpointInterface stellt JAX-WS den Bezug zu dem generierten Endpunkt-Interface ProductsPortType her. Obwohl nicht erforderlich, implementieren wir das Interface zusätzlich, um z.B. fehlerhafte Methodensignaturen - etwa, weil wir eine Operation umbenannt haben - bereits zur Compilezeit zu erkennen.

@WebService(endpointInterface = "de.javamagazin.mvnjaxws.products.ProductsPortType")
public class ProductsPort implements ProductsPortType {

    private Map<Integer, Product> sampleProducts = new TreeMap<Integer, Product>();

    public ProductsPort() {
        Product product = new Product();
        product.setId(1);
        product.setName("Jeans-Hose");
        product.setPrice(new BigDecimal("89.99"));
        product.setSize("L");
        sampleProducts.put(product.getId(), product);
        // ...
    }

    public GetProductByIdResponse getProductById(GetProductByIdRequest request) {
        GetProductByIdResponse response = new GetProductByIdResponse();
        response.setProduct(sampleProducts.get(request.getId()));
        return response;
    }

    public FindProductsResponse findProducts(FindProductsRequest request) {
        // ...
    }

}

Listing 2: Webservice-Implementierung ProductsPort.java

Publizieren des Services

Auf dem Weg zu unserem ersten Contract-First-Webservice mit JAX-WS sind nun schon die größten Hürden genommen. Der Service muss nur noch dem JAX-WS-Framework bekannt gemacht werden. Das Vorgehen hierzu unterscheidet sich abhängig von der genutzten JAX-WS-Implementierung sowie dem Deploymentszenario. Für das Beispiel nutzen wir die JAX-WS-Referenzimplementierung in einem reinen Servlet-Container und müssen den Service daher in der Konfigurationsdatei /src/main/webapp/WEB-INF/sun-jaxws.xml bekanntmachen (siehe Listing 3).

<?xml version="1.0" encoding="UTF-8"?>
<endpoints xmlns="http://java.sun.com/xml/ns/jax-ws/ri/runtime" version="2.0">
    <endpoint
        implementation="de.javamagazin.mvnjaxws.products.ProductsPort"
        name="Products"
        url-pattern="/Products"
        wsdl="WEB-INF/wsdl/products.wsdl"
        service="{http://www.javamagazin.de/mvnjaxws/products}ProductsService"
        port="{http://www.javamagazin.de/mvnjaxws/products}ProductsPort" />
</endpoints>

Listing 3: Deployment-Deskriptor sun-jaxws.xml

Zusätzlich ist im Deployment-Deskriptor web.xml das Servlet com.sun.xml.ws.transport.http.servlet.WSServlet zu registrieren. Im Beispielprojekt wurde dies bereits vom Maven-Archetyp erledigt, alle Aufrufe werden automatisch an JAX-WS RI delegiert. In einem Applikationsserver wie Glassfish, der JAX-WS RI bereits von Haus aus enthält, sind beide Deskriptoren nicht erforderlich; vielmehr sind die Informationen zu Servicename und WSDL-Pfad etc. direkt in der @WebService-Annotation anzugeben, woraufhin beim Deployment alle entsprechend annotierten Endpunkte publiziert werden.

Zum Testen können wir nun per

mvn clean jetty:run-war

den Servlet-Container Jetty starten und anschließend den Webservice unter

http://localhost:8080/<Projektname>/Products

erreichen. Ein Aufruf dieser URL in einem Webbrowser zeigt eine Übersichtsseite aller deployten Webservices. Zum Testen des Services kann z.B. soapUI verwendet werden, das eine grafische Oberfläche für Webservice-Aufrufe bereitstellt (siehe Abb. 1).

soapUI

Abb. 1: Request und Response des Products-Services in soapUI

Stolpersteine

Sollten Sie beim Starten des Projektes die Fehlermeldung Caused by: java.lang.NoClassDefFoundError: javax/jws/WebService erhalten, liegt die Ursache vermutlich in einem Konflikt verschiedener Versionen von JAX-WS RI in Ihrem Maven-Repository (siehe JAX-WS API Version Mess with Maven). Der einfachste Workaround ist, das lokale Maven-Repository zu löschen. Leider müssen dann alle abhängigen Bibliotheken Ihrer Maven-Projekte neu heruntergeladen werden.

JAX-WS RI hat die etwas eigenwillige Angewohnheit, bei unvollständigen oder fehlerhaften Angaben in @WebService-Annotationen in den Code-First-Modus zu wechseln und automatisch WSDL/XSD-Dokumente anhand der Java-Schnittstellen zu generieren. Sie sollten die Konsolenausgaben beim Start Ihrer Anwendung beobachten. Sind dabei Ausschriften wie

INFO: Dynamically creating request wrapper Class [...]
18.10.2008 15:48:52 com.sun.xml.ws.model.WrapperBeanGenerator createBeanImage

zu sehen, sollten Sie die Angaben Ihrer @WebService-Annotationen auf Korrektheit überprüfen.

Ein Bug im Maven-Archetype-Plugin führt u.U. dazu, dass der bei der Projektanlage angegebene Archetypkatalog nicht gefunden wird. Befindet sich eine solche fehlerhafte Version des Archetype-Plugins in Ihrem lokalen Repository, führt dies zu folgender Fehlermeldung:

[WARNING] Error reading archetype catalog
http://maven-instant-ws.googlecode.com/svn/repo/archetype-catalog.xml

Dem kann begegnet werden, indem im lokalen Repository der Ordner org/apache/maven/plugins/maven-archetype-plugin und damit das Archetype-Plugin gelöscht wird. Daraufhin wird dieses neu heruntergeladen und auch der Archetypkatalog gefunden.

Nachrichtenvalidierung

Durch das XML-Schema ist der Aufbau der Request- und Responsenachrichten exakt festgelegt. So ist es etwa möglich, den Wertebereich von Attributen oder die Kardinalität von Kollektionen einzuschränken. Vor allem während der Entwicklung kann eine Schemavalidierung aller Nachrichten hilfreich sein, um fehlerhafte Client-Requests, aber auch nicht schemakonforme Antworten der Serviceimplementierung zu identifizieren. Unter JAX-WS RI wird die Nachrichtenvalidierung aktiviert, indem die Annotation @SchemaValidation an der Endpunktklasse angegeben wird:

@WebService(...)
@SchemaValidation
public class ProductsPort implements ProductsPortType {
    //...
}

Senden Clients nun Aufrufe, die nicht dem zugrunde liegenden Schema genügen, oder wird durch die Serviceimplementierung eine invalide Antwort zurückgegeben, erzeugt das Framework einen SOAP-Fault, der die Schemaverletzung beschreibt. Ist darüber hinaus eine spezifische Fehlerbehandlungslogik erforderlich, kann in der Annotation eine Ableitung der Klasse com.sun.xml.ws.developer.ValidationErrorHandler angegeben werden, welche bei Schemaverletzungen aufgerufen wird:

@SchemaValidation(handler=MyValidationHandler.class)

Handler-Chains

Häufig ist wiederkehrende Logik wie das Loggen der ein- und ausgehenden Nachrichten oder die Auswertung von Header-Elementen in mehreren oder allen Webservice-Operationen einer Anwendung durchzuführen. Ähnlich zum Servlet-Filter-Konzept erlaubt JAX-WS die redundanzfreie Realisierung solcher Querschnittsaufgaben mittels sog. Handler-Chains.

Auf diese Weise ist es möglich, beim JAX-WS-Framework Handler zu registrieren, die vor und nach der Ausführung einer Webservice-Operation aufgerufen werden und dabei die übermittelten Nachrichten inspizieren oder auch verändern können. Listing 4 zeigt einen Beispiel-Handler, der die Aufrufe aller Webservice-Operationen zählt:

public class RequestCountHandler implements LogicalHandler<LogicalMessageContext> {

    private static ConcurrentMap<String, AtomicInteger> requestCounts
                         = new ConcurrentHashMap<String, AtomicInteger>();

    private static void incrementCount(String requestName) {
        AtomicInteger count = requestCounts.putIfAbsent(requestName, new AtomicInteger());
        if (count == null)
            count = requestCounts.get(requestName);
        count.incrementAndGet();
    }

    public boolean handleMessage(LogicalMessageContext lmc) {
        try {
            boolean isOutbound = (Boolean) lmc.get(MessageContext.MESSAGE_OUTBOUND_PROPERTY);

            // inbound handler invocation
            if (!isOutbound) {
                DOMResult requestDOM = new DOMResult();
                Transformer transformer = TransformerFactory.newInstance().newTransformer();
                transformer.transform(lmc.getMessage().getPayload(), requestDOM);
                String requestName = requestDOM.getNode().getFirstChild().getNodeName();
                RequestCountHandler.incrementCount(requestName);
            }

        }
        catch (Exception e) {
            e.printStackTrace();
        }

        return true;
    }

    public void close(MessageContext context) {
        // nothing to do here
    }

    public boolean handleFault(LogicalMessageContext context) {
        // nothing to do here
        return false;
    }
}

Listing 4: Implementierung eines LogicalHandler

Hierzu wurde das Interface LogicalHandler implementiert. Die Methode handleMessage() wird vor und nach jeder Operation aufgerufen, für die der Handler registriert ist. Soll aus einem Handler heraus nicht nur auf die Nutzlast einer SOAP-Nachricht sondern auch auf ihren Header zugegriffen werden, ist statt LogicalHandler das Interface SOAPHandler zu implementieren.

Der Handler muss nun noch beim JAX-WS-Framework registriert werden. Dies kann durch Annotation der Endpunktklasse mit @HandlerChain, im oben beschriebenen Deskriptor sun-jaxws.xml oder über eine sog. WSDL-Customization erfolgen. Letzteres erlaubt auch die Definition serviceübergreifender Handler. Listing 5 zeigt beispielhaft eine solche Customization-Datei, die wir unter src/main/webapp/WEB-INF/jaxws/wsdl-customization.xml ablegen. Beim Generierungsprozess wird diese ausgewertet und die generierten Service-Interfaces mit der darin spezifizierten Handler-Chain verknüpft.

<?xml version="1.0" encoding="UTF-8"?>
<jaxws:bindings
    xmlns:jaxws="http://java.sun.com/xml/ns/jaxws"
    xmlns:javaee="http://java.sun.com/xml/ns/javaee">
    <javaee:handler-chains>
        <javaee:handler-chain>
            <javaee:handler>
                <javaee:handler-class>
                    de.javamagazin.mvnjaxws.RequestCountHandler
                </javaee:handler-class>
            </javaee:handler>
        </javaee:handler-chain>
    </javaee:handler-chains>
</jaxws:bindings>

Listing 5: WSDL-Customization zur Deklaration einer Handler-Chain

Fazit

Die Erstellung Contract-First-basierter Webservices wirkt auf viele Entwickler noch immer abschreckend - vor allem aufgrund des recht sperrigen Formats WSDL. Dass dem nicht so sein muss, hat der vorliegende Artikel aufgezeigt. Mit den richtigen Werkzeugen ist Contract-First nur geringfügig aufwendiger als der Code-First-Ansatz, der Entwickler wird aber mit robusten und interoperablen Webservice-Schnittstellen belohnt. Der Einsatz von JAX-WS und Maven ermöglicht rasche Erfolgserlebnisse. Auch weitergehende Anforderungen wie Nachrichtenvalidierung und die Erstellung von Message-Handlern sind auf dieser Grundlage unkompliziert umzusetzen.

Ralf Ebert

Ralf Ebert Ralf Ebert ist freiberuflicher Softwareentwickler und Trainer. Er entwickelt vor allem grafische Benutzeroberflächen für Enterprise-Systeme und berät seine Kunden bei der Umsetzung von Anwendungen in diesem Bereich. Seine Erfahrungen aus zahlreichen Projekten gibt er auch als Trainer und Coach für Eclipse-Technologien weiter.

Gunnar Morling

Gunnar Morling Gunnar Morling arbeitet als Softwareentwickler bei der Saxonia Systems AG in Dresden und verfügt über mehrjährige Erfahrungen bei der Realisierung von Java-Enterprise-Anwendungen. Zu seinen Interessenschwerpunkten zählen das Konfigurationsmanagement mittels Maven sowie die Automatisierung von Integrationstests.

Apps

website screenshot mac os x

Schulungen