Wyzwanie
Postanowiłem wykorzystać Apache Camel do integracji pomiędzy systemami, które wystawiają usługi SOAP. Nie było możliwości, aby systemy komunikowały się aktywnie (nawiązując komunikację na zasadzie client-server), pozostała konieczność wstawienia pomiędzy nie kolejnego komponentu, który inicjowałby integrację rzeczonych usług. Komponent integrujący został zrealizowany jako trasa integracyjna zrealizowana z użyciem Camel'a i osadzona na kontenerze ServiceMix.W przykładzie posłużę się fikcyjnymi usługami, które dla podniesienia poziomu trudności wykonane zostały jako usługi ASMX uruchamiane z użyciem Mono pod Linux'em.
Usługa ServiceExport
Pierwsza z usług wystawia metodę ExportCatalog, która pozwala wyeksportować zawartość katalogu z częściami.
Komunikat ExportCatalog zakłada przekazanie parametru, którym jest rok, za który należy wyeksportować katalog.
Schemat żądania ExportCatalog:
<?xml version="1.0" encoding="utf-16"?>
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<ExportCatalog xmlns="http://devbox/2012/03/ServiceExport/">
<year>int</year>
</ExportCatalog>
</soap:Body>
</soap:Envelope>
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<ExportCatalog xmlns="http://devbox/2012/03/ServiceExport/">
<year>int</year>
</ExportCatalog>
</soap:Body>
</soap:Envelope>
Odpowiedź ExportCatalogResponse zawiera element //ExportCatalogResponse/ExportCatalogResult/Parts, który jest elementem, który będzie służył budowie żądania dla usługi ServiceImport.
Schemat odpowiedzi ExportCatalogResponse:
<?xml version="1.0" encoding="utf-16"?>
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<ExportCatalogResponse xmlns="http://devbox/2012/03/ServiceExport/">
<ExportCatalogResult>
<Parts>
<Part>
<ID>string</ID>
<Name>string</Name>
<Description>string</Description>
</Part>
<Part>
<ID>string</ID>
<Name>string</Name>
<Description>string</Description>
</Part>
</Parts>
</ExportCatalogResult>
</ExportCatalogResponse>
</soap:Body>
</soap:Envelope>
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<ExportCatalogResponse xmlns="http://devbox/2012/03/ServiceExport/">
<ExportCatalogResult>
<Parts>
<Part>
<ID>string</ID>
<Name>string</Name>
<Description>string</Description>
</Part>
<Part>
<ID>string</ID>
<Name>string</Name>
<Description>string</Description>
</Part>
</Parts>
</ExportCatalogResult>
</ExportCatalogResponse>
</soap:Body>
</soap:Envelope>
Usługa ServiceImport
Kolejna z usług wystawia metodę ImportCatalog, która służy umieszczeniu zawartości katalogu z częściami w docelowym systemie, który ją wystawia.
Żądanie ImportCatalog zawiera element //ImportCatalog/Catalog/Parts, który jest identyczny z elementem Parts wyłuskanym z odpowiedzi ExportCatalogResponse.
Schemat żądania ImportCatalog:
<?xml version="1.0" encoding="utf-16"?>
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<ImportCatalog xmlns="http://devbox/2012/03/ServiceImport/">
<Catalog>
<Parts>
<Part>
<ID>string</ID>
<Name>string</Name>
<Description>string</Description>
</Part>
<Part>
<ID>string</ID>
<Name>string</Name>
<Description>string</Description>
</Part>
</Parts>
</Catalog>
</ImportCatalog>
</soap:Body>
</soap:Envelope>
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<ImportCatalog xmlns="http://devbox/2012/03/ServiceImport/">
<Catalog>
<Parts>
<Part>
<ID>string</ID>
<Name>string</Name>
<Description>string</Description>
</Part>
<Part>
<ID>string</ID>
<Name>string</Name>
<Description>string</Description>
</Part>
</Parts>
</Catalog>
</ImportCatalog>
</soap:Body>
</soap:Envelope>
W odpowiedzi dostajemy potwierdzenie w formie statusu, które dla wartości 0 oznacza sukces.
Schemat odpowiedzi ImportCatalogResponse:
<?xml version="1.0" encoding="utf-16"?>
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<ImportCatalogResponse xmlns="http://devbox/2012/03/ServiceImport/">
<status>int</status>
</ImportCatalogResponse>
</soap:Body>
</soap:Envelope>
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<ImportCatalogResponse xmlns="http://devbox/2012/03/ServiceImport/">
<status>int</status>
</ImportCatalogResponse>
</soap:Body>
</soap:Envelope>
Komponent integracyjny
Założeniem było utworzenie komponentu w sposób jak najłatwiejszy. Udało się tworząc całą trasę z wykorzystaniem tylko 1 pliku context.xml. Ten plik można osadzić w katalogu deploy instancji ServiceMix, bądź traktować jako element projektu maven i uruchamiać testowo trasę poleceniem mvn camel:run. Pomijam szczegóły tworzenia takiego projektu, zakładam znajomość archetypów maven.
W pierwszym etapie konieczne jest skonfigurowanie 2 par endpoint'ów:
- endpoint'y CXF, które fizycznie wiązane są usługami i muszą mieć wskazanie na adres usługi, jej specyfikację WSDL oraz wskazania na konkretny port i service z WSDL,
- endpoint'y Camel'a, które są wykorzystywane jako elementy trasy integracyjnej i wiążą się bezpośrednio z pierwszą parą endpoint'ów.
Na uwagę zasługuje fakt, że endpoint'y Camel'a mają ustawiony format danych na PAYLOAD, co oznacza, że na trasie integracyjnej można się posługiwać samą treścią komunikatów SOAP z pominięciem koperty. Treść komunikatów w zależności od potrzeb może być traktowana jako String albo drzewo DOM.
Konfiguracja endpoint'ów
<cxf:cxfEndpoint id="cxfExportEndpoint" address="http://192.168.1.104:8080/Export.asmx" wsdlURL="http://192.168.1.104:8080/Export.asmx?wsdl=0" endpointName="export:ExportSoap" serviceName="export:Export" xmlns:export="http://devbox/2012/03/ServiceExport/"> <cxf:features> <bean class="org.apache.cxf.feature.LoggingFeature"/> </cxf:features> </cxf:cxfEndpoint> <cxf:cxfEndpoint id="cxfImportEndpoint" address="http://192.168.1.104:8080/Import.asmx" wsdlURL="http://192.168.1.104:8080/Import.asmx?wsdl=0" endpointName="import:ImportSoap" serviceName="import:Import" xmlns:import="http://devbox/2012/03/ServiceImport/"> <cxf:features> <bean class="org.apache.cxf.feature.LoggingFeature"/> </cxf:features> </cxf:cxfEndpoint> <camel:endpoint id="importEndpoint" uri="cxf:bean:cxfImportEndpoint?dataFormat=PAYLOAD"/> <camel:endpoint id="exportEndpoint" uri="cxf:bean:cxfExportEndpoint?dataFormat=PAYLOAD"/>
Kolejnym krokiem jest konfiguracja trasy integracyjnej. Trasa składa się z 3 dużych kroków:
- uruchomienie wymiany danych komponentem timer (w przykładzie co 6s),
- przygotowanie żądania i wywołanie usługi ServiceExport,
- przygotowanie żądania i wywołanie usługi ServiceImport.
<camel:camelContext id="asmx-context" xmlns="http://camel.apache.org/schema/spring"> <route id="integration"> <from uri="timer://test?period=6000"/> <setHeader headerName="operationName"> <constant>ExportCatalog</constant> </setHeader> <setHeader headerName="operationNamespace"> <constant>http://devbox/2012/03/ServiceExport/</constant> </setHeader> <setBody> <constant><![CDATA[ <ExportCatalog xmlns="http://devbox/2012/03/ServiceExport/"> <year>2012</year> </ExportCatalog> ]]></constant> </setBody> <to ref="exportEndpoint"/> <setHeader headerName="operationName"> <constant>ImportCatalog</constant> </setHeader> <setHeader headerName="operationNamespace"> <constant>http://devbox/2012/03/ServiceImport/</constant> </setHeader> <setBody> <xquery><![CDATA[ <ImportCatalog xmlns="http://devbox/2012/03/ServiceImport/"> <Catalog>{ //*:Parts }</Catalog> </ImportCatalog> ]]></xquery> </setBody> <to ref="importEndpoint"/> </route> </camel:camelContext>
Oba kroki przygotowania żądania są podobne i składają się na nie:
- ustawienie nagłówków operationName i operationNamespace, które informują CXF o tym, co ma zostać wywołane w usłudze,
- oraz ustawienie treści komunikatu.
Żądanie ExportCatalog jest bardzo proste i w przykładzie zakładam jego niezmienność.
Żądanie ImportCatalog jest bardziej skomplikowane, gdyż zależy ono od treści odpowiedzi z pierwszego wywołania. Aby jak najprościej przełożyć komunikat ExportCatalogResponse na ImportCatalog posłużyłem się szablonem XQuery. Szablon ten zakłada zbudowanie elementu <Catalog/> w oparciu o wyrażenie XPath //*:Parts, które znajduje element Parts w uzyskanej wcześniej odpowiedzi.
Podsumowując
Zaprezentowane rozwiązanie jest przede wszystkim proste, składa się na nie 1 plik XML. Dzięki wskazywaniu zarówno adresów usług i ich WSDL, oraz wykorzystaniu formatu danych PAYLOAD nie ma konieczności klasycznego generowania kodu klienckiego usług. Tym samym zyskuje się pewną odporność rozwiązania na zmiany w usługach: o ile element Parts nie może zmienić nazwy, to zmiany w strukturze jego zawartości nie mają wpływu na działanie integracji.
Co ja bym zrobił bez Camel'a ?

