Apache Camel: Agile Enterprise-Integration

Eine realistischere Anwendung

Um die Fülle an Features in Camel besser einzuschätzen, und um zu zeigen, wie elegant und präzise die Camel Java DSL sein kann, wird im nächsten Beispiel ein Legacy-System, das die Bestellungen für Kunden generiert, mit einem System für die Preisbestimmung ermittelt. Im System für die Preisbestimmung werden die Rabatte mit komplexen Geschäftsregeln berechnet. Das Legacy-System, auf dem die Bestellungen generiert werden, ist ein wirklich altes und schlecht wartbares System, das als Integrationsmöglichkeit nur die Generierung von CSV-(Comma-Separated-Values-)Dateien anbietet. Anderseits ist das System für die Preisbestimmung eine moderne Webapplikation, die einen einfachen, aber effektiven REST-Service für die Berechnung der Preise der Bestellungen offeriert. Da die verwendeten Regeln für die Berechnung des Preises wirklich komplex und somit auf dem System sehr rechenintensiv sind, muss eine Belastung des Systems mit mehr als einer Preisberechnung pro zwei Sekunden vermieden werden. Das Einlesen einer großen CVS-Datei und das Senden der Preisberechnungen auf einmal würden zu einem Zusammenbruch des Systems führen. Das System soll deshalb die Bestellungen prinzipiell von der CSV-Datei orders.csv (Listing 6) lesen, für jede Bestellung den Preisberechnungsservice aufrufen, das berechnete Total in eine entsprechenden Reihenfolge setzen und in einem späteren Schritt die Ergebnisse in eine andere CSV-Datei für die Auslieferung speichern. Weil Camel Ressourcen (z. B. Dateien) zyklisch abfragen kann, ist es in diesem Fall gar nicht nötig, einen Datenproduzenten (Producer) für das Senden der Meldungen an einen Context (Listing 7) zu verwenden. Die ganze Konfiguration sollte nun wie in Listing 8 aussehen.

Listing 6
customer_1, 10, 100
customer_2, 20, 200
customer_2, 30, 300
customer_3, 40, 400
customer_1, 50, 500
Listing 7
public static void main(String[] args) throws Exception {
    DefaultCamelContext camelContext = new DefaultCamelContext();
    camelContext.addRoutes(new Example3Routes());
    camelContext.start();
    Thread.sleep(10000);
    camelContext.stop();
}
Listing 8
public class Example3Routes extends RouteBuilder {
    private static final String REQUEST_TEMPLATE = "" +
            "";
    private static final String BINDY_PACKAGE = "com.example.camel";
 
    @Override
    public void configure() throws Exception {
        from("file:src/resources/?fileName=orders.csv&noop=true")
                .unmarshal().bindy(BindyType.Csv, BINDY_PACKAGE)
                .split().method(ProductOrderSplitter.class)
                .log("Read: '${body}'")
                .enrich("direct:calculate_total",
                        new PriceAggregationStrategy())
                .log("Written: '${body}'")
                .marshal().bindy(BindyType.Csv, BINDY_PACKAGE)
                .to("file:src/resources/?fileName=totals.csv&fileExist=Append");

        from("direct:calculate_total")
                .setBody().simple(PRICE_REQUEST_TEMPLATE)
                .throttle(1).timePeriodMillis(2000)
         .to("restlet:http://localhost:9999/priceCalculator?restletMethod=post")
                .transform().xpath("/priceResponse/price/text()",
                                   Integer.class);
    }

Während die Konfiguration der Routen für das Verständnis nicht wirklich ausführlich beschrieben werden muss, verlangen einige wirklich neue Punkte an dieser Stelle ergänzende Erklärungen. Um den Inhalt der CSV-Datei, die die Bestellungen enthält, zu lesen, bietet Camel einen so genannten Endpunkt. Dieser agiert als Verbraucher (Consumer, File Reader) oder als Produzent (File Writer). Sobald der Inhalt der CSV-Datei eingelesen worden ist, wird jede Zeile in eine Instanz der ProdukctOrder-Klasse (Listing 9) konvertiert [3]. Das ist ein komfortables POJO, annotiert mit Apache-Bindy-Annotationen [4], das beim Unmarshalling-Prozess zum Erzeugen des entsprechenden Beans verwendet wird. Sobald die Zeilen der Bestellungsdatei eingelesen und in POJOs entpackt (unmarshalled) sind, kann das Splitter Enterprise Integration Pattern [5] angewendet werden. Mittels einer einfachen Java-Klasse (Listing 10) wird die Nachricht, die alle Produktbestellungen enthält, in individuelle Meldungen aufgesplittet, sodass eine Nachricht nur eine Bestellung enthält. Diesen Bestellungen fehlen noch die Informationen für den Totalpreis, der nicht in der originalen CSV-Datei steht und vom Preisbestimmungssystem neu berechnet werden muss. Für die Bewältigung dieser Aufgabe wird das Enricher Enterprise Integration Pattern [6] konfiguriert, das die Meldungen für einen anderen Endpunkt weiterleitet und den Totalpreis in die Bestgellung setzt. Das geschieht am Schluss mittels der Aggregation-Strategy-Implementation (Listing 11). Sobald die Bestellinformation vollständig ist, werden die Bestellungen zurück in eine CSV-Datei umgewandelt (marshalling) und mittels eines Dateiendpunkts werden sie in eine Totaldatei geschrieben.

Listing 9
@CsvRecord(separator = ",")
public class ProductOrder {
    @DataField(pos = 1, trim = true)
    private String customer;

    @DataField(pos = 2, trim = true, pattern = "0")
    private int quantity;

    @DataField(pos = 3, trim = true, pattern = "0")
    private int price;

    @DataField(pos = 4, trim = true, pattern = "0")
    private int total;

  // Getters and setters omitted
}
Listing 10
public class ProductOrderSplitter {
    public List split(List> body) {
        List result = new ArrayList();
        for (Map map : body) {
            for (ProductOrder productOrder : map.values()) {
                result.add(productOrder);
            }
        }
        return result;
    }
}
Listing 11
public class PriceAggregationStrategy implements AggregationStrategy {
    public Exchange aggregate(Exchange exchange, Exchange exchange1) {
        ProductOrder order = exchange.getIn().getBody(ProductOrder.class);
        Integer total = exchange1.getIn().getBody(Integer.class);
        order.setTotal(total);
        return exchange;
    }
}

Um den REST-Service des Preisbestimmungssystems aufzurufen (was innerhalb des direct:calculate_total-Endpunkts geschieht), muss zuerst ein XML-Fragment vom Typ PriceRequest generiert werden. Dafür könnte wieder der JAXB Marshalling Support in Camel (wie schon beim Apache Bindy Support gesehen) verwendet werden. Aber für diesen Fall ist es besser, einen rudimentäreren Mechanismus, wie das Simple-Template mit der XML-Struktur zu verwenden. Das wird durch den Camel Feature Expression Languages Support [7] illustriert. Vor dem Aufruf und um zu vermeiden, dass der Preisbestimmungsservice mit einer großen Anzahl an Anfragen geflutet wird, wird ein weiteres der mächtigen Enterprise Integration Patterns von Camel, das Throttler Pattern, konfiguriert [8]. Mit diesem einfachen Konstrukt kann die Anzahl der Meldungen pro Sekunde zwischen zwei Endpunkten kontrolliert werden. Zuletzt wird in diesem Beispiel der REST-Preisbestimmungsservice aufgerufen. Dazu wird der Restlet-Endpunkt verwendet, der den Preis, der als Zahlenwert in der XML-Response-Nachricht steht, mittels einer XPath Expression entpackt. Da in diesem Beispiel neue Typen von Endpunkten (Restlet, Bindy) verwendet werden, müssen die POM-Abhängigkeiten dementsprechend erweitert werden (Listing 12).

Listing 12
org.apache.camelcamel-bindy${camel.version}org.apache.camelcamel-restlet${camel.version}
Kommentare

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.