piątek, 17 października 2014

Własna dystrybucja Karaf

We wcześniejszym wpisie o ServiceMix napisałem, że warto jest budować swoją własną dystrybucję zamiast opierać się na gotowym ServiceMix. Powodów jest kilka, ale według mnie najważniejszy z nich jest taki, że własna dystrybucja może być znacznie lżejsza lub lepiej dopasowana do aplikacji, którą zamierzamy uruchomić. Ważne jest też to, że gotowa dystrybucja z preinstalowaną aplikacją pozwala oszczędzić czas na pisanie instrukcji instalacji dla administratorów i jest w zasadzie samo-wystarczalna: wszystkie zależności powinny (teoretycznie) znaleźć się wewnątrz dystrybucji.

W przykładzie posłużę się najnowszą w chwili obecnej wersją Apache Karaf 3.0.1. Ale na początek, warto wspomnieć, co tak naprawdę wchodzi w skład dystrybucji Apache Karaf.

Struktura dystrybucji


W zasadzie każda dystrybucja Apache Karaf (niezależnie czy jest to właśnie czysty Karaf czy ServiceMix) wygląda identycznie. Po rozpakowaniu archiwum z dystrybucją dostajemy kilka katalogów jak w przykładzie:

Katalog bin zawiera skrypty uruchomieniowe dla unix/windows, katalog data zawiera cache, logi i inne pliki generowane w trakcie działania kontenera, deploy jest to katalog skanowany na obecność artefaktów, które mogą być automatycznie instalowane, etc zawiera konfigurację kontenera, lib podstawowe biblioteki OSGI i najważniejszy w każdej dystrybucji: system zawiera lokalne repozytorium maven, skąd mogą być instalowane kolejne bundle i feature'y. I to właśnie system jest tym, co nas najbardziej interesuje, bo przygotowując dystrybucję decydujemy które bundle i feature'y będą umieszczone w repozytorium system, oraz które z nich będą automatycznie uruchamiane. A wszystko to dzieje się z wykorzystaniem karaf-maven-plugin. Ale zanim dojdziemy do jego użycia, pokażę wykorzystywaną przeze mnie strukturę projektu Maven.

Struktura projektu


Jak to zwykle w korporacjach, obowiązującym standardem jest Maven wykorzystywany do budowania wielomodułowych projektów. Konkretna struktura projektu wygląda mniej więcej tak:

Projekt główny podzielony jest zwykle na 2 moduły: assembly, który zawiera kolejne moduły związane z budowaniem dystrybucji oraz services, który zawiera konkretne usługi wchodzące w skład dystrybucji. W omawianym przykładzie services zawiera tylko jeden projekt z prostym użyciem Apache Camel, które ma na celu tylko i wyłącznie pokazać, że się zainstalowało i działa.

Przykładowy przepływ będzie co wskazany interwał logował na konsolę. A teraz najważniejsze, czyli jak spakować to w gotową dystrybucję.

Budowa dystrybucji

Dystrybucję buduje się w dwóch etapach: pierwszy polega na zebraniu wszystkich naszych usług w grupę feature, drugi polega na przyłączeniu naszego feature do budowanej dystrybucji.

Pierwszy etap, zebranie usług w grupy realizuje się przez utworzenie modułu budującego feature lub KAR (KAR to jest archiwum jar zawierające spakowane bundle opisane przez feature - rzecz specyficzna dla Karaf). Ja zwykle stosuję metodę budującą archiwum KAR, gdyż po drodze i tak powstaje feature, a dostaję jeszcze automatycznie archiwum KAR, które przydaje się czasem przy innych sposobach deploymentu.

W przykładowym projekcie, moduł assembly/custom-feature realizuje funkcję grupującą usługi. Jest to projekt wygenerowany z archetypu karaf-kar-archetype i jego pom.xml wygląda mniej więcej tak:

Na uwagę zasługują 2 szczegóły: typ projektu określony na kar oraz wykorzystanie pluginu karaf-maven-plugin, który obsługuje generowanie pliku feature.xml i archiwum KAR.

Jedynym elementem źródłowym tego projektu jest plik src/main/feature/feature.xml, w którym opisana jest przykładowa usługa wraz z jej zależnościami (w tym przypadku jest to camel-blueprint):

Drugi etap, najciekawszy z tego wszystkiego, realizowany jest w module assembly/custom-distro, a wygenerowany z wykorzystaniem archetypu karaf-assembly-archetype. Jego pom.xml wygląda następująco:

Jest to projekt typu karaf-assembly obsługiwany przez karaf-maven-plugin do budowania dystrybucji. Zależności tego modułu zawierają przede wszystkim odniesienia do plików feature.xml, z których chcemy preinstalować paczki wchodzące w skład naszej dystrybucji:

Na liście zależności znajduje się podstawowy kar ze szkieletem Karafa (framework) oraz pliki feature.xml pochodzące z Karafa, Camela i naszego modułu custom-feature.
Dalej pozostaje tylko wybrać, które konkretne features mają zostać preinstalowane. Dzieje się to w konfiguracji karaf-maven-plugin:

Jeśli wszystko jest gotowe, można zbudować projekt (mvn install) i poszukać w custom-distro/target archiwum z dystrybucją, w naszym przypadku będzie to plik ./assembly/custom-distro/target/custom-distro-1.0.0-SNAPSHOT.tar.gz. Po rozpakowaniu i uruchomieniu powinniśmy móc sprawdzić jakie features i bundle zostały automatycznie uruchomione:

Jak widać, custom-feature jest na liście features, Sample-Service jest na liście aktywnych bundli, w logach można spodziewać się wpisów z aktywności usługi.

Wnioski

Dla mnie najważniejszy wniosek (i zarazem zaleta) budowania własnej dystrybucji jest taki, że dzięki temu możemy dostarczać gotowe rozwiązanie, którego nie trzeba instalować w pustym kontenerze. Koniec z pisaniem mozolnych instrukcji instalacji, możemy ograniczyć się do kilku słów instruktażu związanego z rozpakowaniem archiwum i uruchomieniem skryptu startowego. To skutkuje większą powtarzalnością procesu instalacyjnego i pozwala uniknąć nieprzyjemnych sytuacji podczas wdrożeń na produkcję, gdzie zawsze coś może pójść nie tak, jak byśmy chcieli.

Budowanie tego rodzaju własnych dystrybucji jest małym kroczkiem w kierunku immutable deployments, o którym być może kiedyś coś napiszę. A następnym razem, pokażę jeszcze inny sposób przygotowania własnego distro oparty o Docker i obrazy kontenerów :)

Źródła przykładowego projektu znajdują się na githubie.

piątek, 10 października 2014

Komercyjne wsparcie dla serwerów JEE

Przy okazji kolejnej oferty dla klienta musiałem wybrać serwer aplikacyjny. Oczywiście taki serwer musi być bezpieczny, zgodny i certyfikowany z JEE 7 no i ... ewentualnie komercyjnie wspierany ? Do tego dochodzi fakt, że jeśli klient nie nalega to nie oferuję totalnie komercyjnych rozwiązań z zamkniętymi źródłami, częściej wybieram platformę open-source z ewentualnym wsparciem w postaci jakiejś subskrypcji. No to mając takie wymagania wybór w zasadzie może paść na tylko na Glassfish lub Wildfly (jestem świadom faktu, że istnieją jeszcze inne serwery typu Tomee, Jonas czy Geronimo, ale nie wspierają jeszcze JEE 7).

No więc, co tu wybrać? Glassfish czy Wildfly? W zasadzie oba serwery oferują to samo, oba są open-source i pozbawione jakiegokolwiek wsparcia poza typowym dla OS community. Szukając kolejnych informacji do porównania natknąłem się na dość stary już news, że Oracle wycofuje wsparcie komercyjne dla Glassfish. I nagle wszyscy na blogach piszą, że Glassfish umarł. Arun Gupta (jeszcze niedawno promotor Glassfish) proponuje przejście na Wildfly lub JBoss EAP bo są lepsze i komercyjnie wspierane. Ale czy na pewno ? Wildfly 8.x jest oczywiście zgodny z JEE 7, ale nikt nie daje na niego żadnego wsparcia. Kiedyś będzie z niego być może JBoss EAP 7, ale to pieśń przyszłości. Obecny w tej chwili JBoss EAP 6.x jest wspierany, ale znów zgodny tylko z JEE 6.

Mój wniosek jest więc taki: zarówno RedHat jak i Oracle mają w ofercie serwery zgodne z JEE 7, ale pozbawione wsparcia, oraz serwery ze wsparciem zgodne tylko z JEE 6: Oracle Weblogic 12 i JBoss EAP 6. Eh, odrobina marketingu i zamieszanie gotowe :) Jedyna zaleta Wildfly jaką widzę, to że migracja z Wildfly 8 na JBoss EAP 7 będzie potencjalnie łatwiejsza niż z Glassfish na Weblogic. Więc pewnie wybiorę Wildfly.

czwartek, 9 października 2014

Koniec przygody z ServiceMix ?

Jak to koniec z ServiceMix ?

Kolejny klient, kolejna platforma integracyjna do wdrożenia. Niejako wyspecjalizowałem się co nieco w integracji opartej o rozwiązania open-source (jakoś tak już mam, że nie lubię płacić za soft), a w szczególności rodzinę Apache ServiceMix, Camel, ActiveMQ i tak dalej. Zwykle moje projekty oparte były o którąś konkretną wersję ServiceMix, na którym instalowałem swoje paczki/bundle i ewentualne brakujące zależności. W ostatnim czasie dotarło do mnie, że ServiceMix to nic innego jak specyficzna dystrybucja Apache Karaf z preinstalowanymi: ActiveMQ, CXF, Camel i ostatnio Activiti. Cała reszta w zasadzie już wyleciała, mam tu na myśli wsparcie dla JBI (ktoś jeszcze używa?) i NMR, którego w wersji 5 już chyba nie ma. Więc w zasadzie, ServiceMix daje mi całkiem mało. Do tego dochodzi cykl wydawniczy, który jest z lekka opóźniony w stosunku do całej reszty. Jak się chce użyć nowszego Camel’a, to trzeba czekać X czasu aż pojawi się nowe wydanie ServiceMix. W zasadzie wystarczyłoby mi już powodów, aby odejść od używania ServiceMix, ale pojawił się jeszcze RedHat, który kupił FuseSource, zrobiło się zamieszanie i skończyło się wsparcie dla ServiceMix wydawanego przez FuseSource (pod nazwą FuseESB). Pojawił się oczywiście JBossFuse, ale to nie zupełnie to samo. Więć kończę z ServiceMix, pewnie na zawsze.

Więc może Fabric8 ?

Skoro nie ServiceMix, to może coś nowego? Pojawił się Fabric8, projekt open-source sponsorowany przez RedHat, rozwijany przez tą samą ekipę co Camel, ActiveMQ i kiedyś ServiceMix. Brzmi interesująco, centralny rejestr konfiguracji w ZooKeeperze, instalowanie kontenerów przez SSH w sieciach lokalnych i w chmurze, jest też wsparcie dla Docker i nowiutka konsola oparta o Hawtio. Sęk w tym, że to już nie jest tylko kontener OSGI z preinstalowanymi bundlami Camela i CXF. Pojawia się właśnie ZooKeeper, pojawia się ConfigAdmin, który korzysta z Git'a, są rozbudowane profile kontenerów, Karaf jest już tylko jedną z opcji - bo można uruchamiać też gołe aplikacje Javy, SpringBoot, Tomcata i WildFly. Ma to związek z microservices - bo ponoć uberjar SpringBoot jest bardziej micro od usługi na OSGI :) Niee, to nie dla mnie. Integracja sama w sobie jest tak skomplikowana, że dorzucanie tak złożonej machiny raczej nie poprawi sytuacji. Integrację należy upraszczać ! Fabric8 jak dla mnie jest zbyt skomplikowany.

Własne distro ?

I tu pojawia się pytanie: Co dalej? Skoro ServiceMix nic mi nie daje, a Fabric8 przytłacza złożonością, jakie są dalsze opcje? Okazuje się, że własna dystrybucja Karaf'a jest odpowiedzią! Karaf daje wszystko co jest potrzebne aby taką dystrybucję wykonać: jest karaf-maven-plugin, są przykłady, nic nie stoi na przeszkodzie, aby wykroić dokładnie taki runtime jaki jest nam potrzebny. Żadnych brakujących ani nadmiarowych zależności, możliwość preinstalowania własnych paczek z integracją i usługami, żyć nie umierać! Jest to bardzo wygodne, bo można totalnie pominąć proces deploymentu aplikacji w kontenerze: build maven kończy się po prostu wygenerowaniem preinstalowanej dystrybucji ze wszystkim co jest potrzebne. Nic nie stoi na przeszkodzie, by nawet w ramach jednego projektu generować kilka dystrybucji podzielonych w/g wymagań funkcjonalnych.

Jak mi sił wystarczy, to następnym razem opiszę jak taką dystrybucję przygotować. Howgh !


wtorek, 10 kwietnia 2012

Camel, Quartz i zmienne harmonogramy

Zupełnie nie tak dawno miałem okazję przygotowywać integrację kilku usług, która miała być uruchamiana w/g harmonogramu ze zmiennym cyklem. Kluczowym było to, że w procesie przygotowania żądania do usługi konieczne było wyliczenie zakresu na osi czasu, który miałby odpowiadać dokładnej długości interwału. Suma wszystkich zakresów z całej doby miała wynosić 24h. Dodatkową trudność stanowiło wymaganie, które wymuszało zbieranie żądań weekendowych celem ich przetworzenia na początku kolejnego tygodnia (czyli w poniedziałek od godziny 0:00).

Teoretycznie wszystko proste, ale taki Quartz nie pozwala łączyć wyrażeń CRON'a. To znaczy, że jeśli chciałbym wywoływać jakieś zadanie ze zmiennym cyklem, to jestem zmuszony definiować je tyle razy, ile jest różnych cykli. Udało mi się rozwiązać te niedogodność stosując trasy Camel'a. A w zasadzie tyle tras, ile jest cykli, a każda kończąca się w tym samym miejscu, czyli w kolejce żądań do przetworzenia. Jest to o tyle wygodne, że dodanie nowego cyklu wiąże się tylko z dodaniem nowej trasy do RouteBuilder'a bez wnikania w dalsze szczegóły przetwarzania.

Kolejka żądań powinna być trwała, to znaczy opierać się na czymś a'la JMS, bo ewentualna awaria nie powinna usunąć z pamięci nieprzetworzonych żądań (wymaganie ciągłości zakresu).

A jak zatrzymywać przetwarzanie na czas weekendu? Okazuje się, że z pomocą przychodzi jedna z implementacji RoutePolicy, a mianowicie CronScheduledRoutePolicy, która umożliwia zatrzymywanie i uruchamianie wybranej trasy w określonym czasie.

Czas na przykład. Poniżej jest schematyczny RouteBuilder, który dla większej przejrzystości pozbawiony został logiki wywołań usług, a tym samym generacji żądań (na trasie wędruje tylko Exchange wygenerowany z Quartz). Trzy różne Endpoint'y (day|night|weekend) reprezentują trzy odrębne cykle harmonogramu. Wszystkie zbiegają się w queueEndpoint, skąd są przetwarzane ostatnią trasą i trafiają w finalEndpoint.
package example.schedule;

import org.apache.camel.Endpoint;
import org.apache.camel.EndpointInject;
import org.apache.camel.builder.RouteBuilder;
import org.apache.camel.spi.RoutePolicy;

public class ExampleRouteBuilder extends RouteBuilder {

    @EndpointInject
    protected Endpoint dayCronEndpoint;
    @EndpointInject
    protected Endpoint nightCronEndpoint;
    @EndpointInject
    protected Endpoint weekendCronEndpoint;
    @EndpointInject
    protected Endpoint queueEndpoint;
    @EndpointInject
    protected Endpoint finalEndpoint;

    private RoutePolicy scheduleRoutePolicy;

    public void setScheduleRoutePolicy(RoutePolicy scheduleRoutePolicy) {
        this.scheduleRoutePolicy = scheduleRoutePolicy;
    }

    @Override
    public void configure() throws Exception {
        from(dayCronEndpoint)
                .routeId("day-cron")
                .to(queueEndpoint);

        from(nightCronEndpoint)
                .routeId("night-cron")
                .to(queueEndpoint);

        from(weekendCronEndpoint)
                .routeId("weekend-cron")
                .to(queueEndpoint);

        from(queueEndpoint)
                .routeId("process")
                .routePolicy(scheduleRoutePolicy)
                .to(finalEndpoint);
    }
}
Konfiguracja całości sprowadza się do uzupełnienia definicji Endpoint'ów o poprawne URI i wstrzyknięcia odpowiednio skonfigurowanej RoutePolicy do ExampleRouteBuildera:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:camel="http://camel.apache.org/schema/spring"
       xsi:schemaLocation="
       http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://camel.apache.org/schema/spring
       http://camel.apache.org/schema/spring/camel-spring.xsd">

    <camel:camelContext xmlns="http://camel.apache.org/schema/spring" trace="true">
        <routeBuilder ref="exampleRouteBuilder"/>

        <endpoint id="dayCronEndpoint"     uri="quartz://d/?cron=0+*/15+9-16+?+*+MON-FRI"/>
        <endpoint id="nightCronEndpoint"   uri="quartz://n/?cron=0+*/30+17-8+?+*+MON-FRI"/>
        <endpoint id="weekendCronEndpoint" uri="quartz://w/?cron=0+0+*+?+*+SAT-SUN"/>
        <endpoint id="queueEndpoint"       uri="seda:queue"/>
        <endpoint id="finalEndpoint"       uri="log:example?level=INFO"/>
    </camel:camelContext>

    <bean id="exampleRouteBuilder" class="example.schedule.ExampleRouteBuilder">
        <property name="scheduleRoutePolicy">
            <bean class="org.apache.camel.routepolicy.quartz.CronScheduledRoutePolicy">
                <property name="routeStartTime" value="0 0 0 ? * MON"/>
                <property name="routeStopTime"  value="0 0 0 ? * SAT"/>
            </bean>
        </property>
    </bean>

</beans>
Sam przykład jest nieco bezużyteczny, ale z powodzeniem obrazuje jak solidne rozwiązania można tworzyć przy pomocy kilku linii kodu i dobrze dobranych komponentów.

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

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 ?

środa, 14 marca 2012

Apache CXF vs WCF i TransportWithMessageCredentials

Apache CXF ma problem w komunikacji z Windows Communication Framework (WCF). Konkretnie chodzi o przypadek, w którym klient CXF wywołuje usługę WCF wykorzystującą schemat zabezpieczeń TransportWithMessageCredentials. W moim scenariuszu używam generacji Timestamp w połączeniu z Signature i BinarySecurityToken. Problem objawia się, gdy WCF próbuje parsować nagłówek <wsse:security/>. Wywołanie zwykle kończy się błędem:

<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
   <s:Body>
      <s:Fault>
         <faultcode xmlns:a="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">a:InvalidSecurity</faultcode>
         <faultstring xml:lang="pl-PL">An error occurred when verifying security for the message.</faultstring>
      </s:Fault>
   </s:Body>
</s:Envelope>

WCF zakłada określoną kolejność elementów nagłówka . To czy jest to zgodne czy nie ze specyfikacją - nie wnikam. Niestety, biblioteka wss4j, która jest używana w Apache CXF (i nie tylko, bo Axis i SOAP UI również jej używają) nie pozwala na poziomie jakiejkolwiek konfiguracji wpłynąć na kolejność elementów tego nagłówka. Został zgłoszony błąd WSS-231 do biblioteki wss4j, ale nie wygląda na to, żeby miał zostać rozwiązany w najbliższej przyszłości.

Z pomocą przychodzi mechanizm interceptorów w Apache CXF, dzięki któremu możliwa jest modyfikacja wygenerowanego komunikatu żądania na poziomie drzewa DOM. Przykładowy kod:

package devbox.cxf;

import org.apache.cxf.binding.soap.SoapMessage;
import org.apache.cxf.binding.soap.interceptor.AbstractSoapInterceptor;
import org.apache.cxf.interceptor.Fault;
import org.apache.cxf.phase.Phase;
import org.apache.cxf.ws.security.wss4j.WSS4JOutInterceptor;
import org.w3c.dom.Node;

import javax.xml.soap.*;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

public class FixSecurityHeaderOutInterceptor extends AbstractSoapInterceptor {

    public FixSecurityHeaderOutInterceptor() {
        super(Phase.PRE_PROTOCOL);
        getAfter().add(WSS4JOutInterceptor.class.getName());
    }

    public void handleMessage(SoapMessage message) throws Fault {
        boolean isOutbound = message == message.getExchange().getOutMessage()
                || message == message.getExchange().getOutFaultMessage();

        if (isOutbound) {
            message.getInterceptorChain().add(new FixSecurityHeaderOutEndingInterceptor());
        }
    }

    public class FixSecurityHeaderOutEndingInterceptor extends AbstractSoapInterceptor {
        public FixSecurityHeaderOutEndingInterceptor() {
            super(Phase.POST_PROTOCOL_ENDING);
        }

        public void handleMessage(SoapMessage message) throws Fault {
            try {
                SOAPPart soap = (SOAPPart) message.getContent(Node.class);
                SOAPHeader header = soap.getEnvelope().getHeader();

                // wsse:Security
                SOAPHeaderElement security = null;
                Iterator iterator = header.examineAllHeaderElements();
                while (iterator.hasNext()) {
                    SOAPHeaderElement headerElement = (SOAPHeaderElement) iterator.next();
                    if (headerElement.getNodeName().equals("wsse:Security")) {
                        security = headerElement;
                        break;
                    }
                }

                // no security - no job
                if (security == null) {
                    return;
                }

                Map<String, SOAPElement> elementMap = new HashMap<String, SOAPElement>();

                Iterator childElements = security.getChildElements();
                while (childElements.hasNext()) {
                    SOAPElement element = (SOAPElement) childElements.next();
                    elementMap.put(element.getTagName(), element);
                    element.detachNode();
                }

                security.addChildElement(elementMap.get("wsu:Timestamp"));
                security.addChildElement(elementMap.get("wsse:BinarySecurityToken"));
                security.addChildElement(elementMap.get("ds:Signature"));
            } catch (SOAPException e) {
                throw new Fault(e);
            }
        }
    }
}

Kod sprowadza się do wyłuskania elementu nagłówka, zabrania z niego wszystkich interesujących elementów i powtórne ich dodanie w kolejności akceptowalnej przez WCF. Najtrudniejsze było trafienie w odpowiednie fazy, w których ma się wykonać ten kod. Metoda prób i błędów pozwoliła trafić w dziesiątkę. Zostało jeszcze wszystko skonfigurować:

<cxf:cxfEndpoint id="cxfProducerEndpoint" xmlns:cxf="http://camel.apache.org/schema/cxf"
address="${producerEndpointURI}" wsdlURL="wsdl/SomeService.wsdl">
        <cxf:inInterceptors>
            <ref bean="producerWSS4JInInterceptor"/>
        </cxf:inInterceptors>
        <cxf:outInterceptors>
            <ref bean="producerWSS4JOutInterceptor"/>
            <bean class="devbox.cxf.FixSecurityHeaderOutInterceptor"/>
        </cxf:outInterceptors>
</cxf:cxfEndpoint>

W przykładzie celowo pominięto szczegóły konfiguracji WSS4J, są bez znaczenia. Ważne jest, aby na końcu listy interceptorów OUT umieścić ten, który naprawia rzeczony nagłówek.