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