piątek, 23 marca 2012

Apache Camel i integracja usług SOAP.


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>

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>

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>

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>

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:

  1. uruchomienie wymiany danych komponentem timer (w przykładzie co 6s),
  2. przygotowanie żądania i wywołanie usługi ServiceExport,
  3. 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:

  1. ustawienie nagłówków operationName i operationNamespace, które informują CXF o tym, co ma zostać wywołane w usłudze, 
  2. 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 ?