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