wtorek, 3 kwietnia 2012

(Nie) lubię Web Services?

Zasadniczo nie lubię Web Services, bo wydają się być przekombinowane. Te wszystkie kombinacje z generowaniem kodu albo generowaniem kontraktu, wersjonowanie kontraktów, nadmiar różnych specyfikacji (WS-Security, WS-Addressing, WS-ReliableMessaging, …). Za dużo tego jest, aby w sensownym czasie to ogarnąć. Nie wspominając nawet, że większość dodatkowych specyfikacji WS-* istnieje po to, aby naprawić błąd, jakim jest zgwałcenie podstawowych zasad protokołu HTTP. Jaki to błąd? Wystarczy przyjrzeć się jak działa REST, aby zauważyć, że z Web Services jest coś nie tak. Ale nie o tym chciałem.

Zdając sobie sprawę z faktu, że nie da się, z dnia na dzień, wyrzucić tak mocno zakorzenionej w SOA technologii, trzeba sprawić, aby było odrobinę łatwiej jej używać. Bo w czym tak naprawdę leży problem? W mojej opinii w tym, że traktujemy Web Services jak kolejny protokół typu RPC. W tym sensie, że zwykle nie zastanawiamy się, jak będą wyglądać komunikaty SOAP, ale skupiamy się na programowym interface. I tym sposobem radośnie kodujemy w Javie, C#, czymkolwiek się da interface, jego implementację i kod klienta, a samego komunikatu często nie widzimy nawet na oczy. Czasem zdarza się komunikacja heterogeniczna, klient w Javie, serwer w .NET albo na odwrót, wtedy rzeczywiście zaczynamy patrzeć na WSDL i generować z niego kod.

W takim scenariuszu kończymy z prawdziwym chaosem zależności. No bo mamy na przykład: interface i model danych w .NET, z nich wygenerowany kontrakt WSDL, a z niego znowu wygenerowany interface i model danych w Javie (jakiś JAX-WS i JAXB zapewne). Czyli 2 interface, 2 modele danych do marshallingu i WSDL, który to wszystko opisuje. ZA DUŻO. No bo co się stanie jak będziemy chcieli cokolwiek zmienić? Może jakaś nowa metoda, albo może zmiana w modelu? Właściwie to część prac trzeba wykonać od początku. A co z wersjonowaniem? W Javie na przykład nie można mieć na jednym classpath 2 wersji tego samego pakietu (no chyba że w OSGi), w .NET jest podobnie. Więc trzeba sobie radzić zmieniając przestrzenie nazw. Nowy namespace -> nowy pakiet. Do tego to już trzeba architekta integracji zatrudnić, żeby to ogarniał :)

A co by było, gdybyśmy odrzucili patrzenie na Web Services jak na protokół RPC i używali ich w takim ujęciu Document Oriented. Zamiast generować jakieś interface i modele moglibyśmy posługiwać się zwykłymi komunikatami XML. W końcu to właśnie znajduje się w <soap:body/>: czysty dokument XML.



Niech za przykład posłuży bardzo prosta usługa z kategorii "Hello World". Zawiera jedną operację, "Hello", której postać SOAP wygląda mniej więcej tak:


<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
    <soap:Body>
        <Hello xmlns="http://devbox/2012/03/Service">
            <name>John Doe</name>
        </Hello>
    </soap:Body>
</soap:Envelope>

To co najbardziej interesujące znajduje się w tagu <soap:body/>:

<Hello xmlns="http://devbox/2012/03/Service">
    <name>John Doe</name>
</Hello>

Odpowiedz SOAP w/g specyfikacji powinna wyglądać tak:

<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
    <soap:Body>
        <HelloResponse xmlns="http://devbox/2012/03/Service">
            <HelloResult>Hello John Doe</HelloResult>
        </HelloResponse>
    </soap:Body>
</soap:Envelope>

Ale, tak naprawdę to wystarczy, że utworzymy to co ma trafić do taga <soap:body/>:

<HelloResponse xmlns="http://devbox/2012/03/Service">
    <HelloResult>Hello John Doe</HelloResult>
</HelloResponse>

A jak to osiągnąć? Wystarczy wykorzystać duet Apache Camel + CXF. Camelowy CXFEndpoint pozwala wskazać format danych jakim chcemy się posługiwać na trasie przetwarzania. Dostępne są POJO (obiekty JAXB na przykład), PAYLOAD (zawartość <soap:body/> w postaci String'a, Stream'a bądź drzewa DOM) oraz MESSAGE (czysty SOAP bez żadnej analizy zawartości). Nas interesuje PAYLOAD.

Skoro zatem na trasie przetwarzania mam czystą treść XML w postaci na przykład drzewa DOM, to możemy z niej korzystać za pomocą XPath'a: //service:Hello/service:name/text(). To wyrażenie sięga do konkretnej wartości elementu, którą można potem wykorzystać jako parametr usługi na trasie, bądź jak w przykładzie, do generacji odpowiedzi.

Co do generowania odpowiedzi, dość często wystarczy dowolny mechanizm szablonów: velocity, freemarker, czy jak w przykładzie, camelowy język "simple". Całość rozwiązania zawiera się w 1 (słownie: JEDNYM) pliku z konfiguracją kontekstu Spring + Camel:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:camel="http://camel.apache.org/schema/spring"
       xmlns:cxf="http://camel.apache.org/schema/cxf"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:service="http://devbox/2012/03/Service"
       xsi:schemaLocation="
       http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://camel.apache.org/schema/cxf
       http://camel.apache.org/schema/cxf/camel-cxf.xsd
       http://camel.apache.org/schema/spring
       http://camel.apache.org/schema/spring/camel-spring.xsd">

    <cxf:cxfEndpoint id="cxfConsumerEndpoint"
                     endpointName="service:ExampleServiceSoap"
                     serviceName="service:ExampleService"
                     wsdlURL="/WEB-INF/ExampleService.asmx.wsdl"
                     address="/exampleService">
        <cxf:features>
            <ref bean="loggingFeature"/>
        </cxf:features>
    </cxf:cxfEndpoint>

    <bean id="loggingFeature"
          class="org.apache.cxf.feature.LoggingFeature">
        <property name="prettyLogging" value="true"/>
    </bean>

    <camel:endpoint id="consumerEndpoint"
                    uri="cxf:bean:cxfConsumerEndpoint?dataFormat=PAYLOAD"/>

    <camelContext id="service-context"
                  trace="false" useMDCLogging="true"
                  xmlns="http://camel.apache.org/schema/spring">
        <route id="service-route">
            <from ref="consumerEndpoint"/>
            <setHeader headerName="name">
                <xpath>//service:Hello/service:name/text()</xpath>
            </setHeader>
            <transform>
                <simple><![CDATA[
                <HelloResponse xmlns="http://devbox/2012/03/Service">
                    <HelloResult>Hello ${headers.name}</HelloResult>
                </HelloResponse>
                ]]></simple>
            </transform>
        </route>
    </camelContext>

</beans>

Oczywiście doliczyć trzeba specyfikację usługi w formacie WSDL + jakąś postać kontenera (OSGi bundle czy może WAR). Ale całość logiki rozwiązania zawiera się w 1 pliku. I co najważniejsze: nie ma generacji kodu. Zmieniać się może usługa, zmieniać się może model danych, ale my nadal nie musimy niczego generować: wystarczy aktualizować konfigurację trasy przetwarzania.

[EDIT] Żeby uwiarygodnić prostotę przykładu wrzuciłem go na github: https://github.com/sleeperor/ExampleService