Apache Camel REST-Server-Interface


Das Camel-Framework bietet eine Vielzahl von Endpunkten an. Darunter sind auch mehrere Rest-Schnittstellen. Für das Backend einer Webseite bieten diese zwar zu geringe Funktionalität, für flexible Integrationslösungen eignen sie sich jedoch umso mehr. Mit nur wenigen Zeilen Code kann eine Rest-Schnittstelle für einen Serverbetrieb eingerichtet werden.

Apache Camel ist ein leichtgewichtiges Framework. Jede Komponente muss explizit eingebunden werden. Für das Rest-Interface stehen uns unterschiedliche Komponenten zur Auswahl. Eine davon ist beispielsweise Spark, welche auf einem eingebetteten Jetty-Server läuft. Eine weitere wäre undertow, welche auf der NIO-Api beruht.

Die Benutzung ist relativ einfach. Hierzu muss lediglich der undertow Endpunkt mit der gewünschten Methode und dem Pfad angegeben werden. Alles weitere wird von Camel im Hintergrund erledigt.

public void configure() {
    from("spark-rest:get:hello")
            .transform().constant("Hello World");
}

Spark mapped den Port standardmäßig auf 4567. Der hier gezeigte Request ist dann unter http://localhost:4567/hello zu erreichen. Mit undertow müsste der Endpoint für den selben Request wie folgt aussehen:

public void configure() {
    from("undertow:http://localhost:4567/hello")
           .transform().constant("Hello World");
}

Beide Endpunkte unterscheiden sich stark voneinander, erledigen jedoch die gleiche Aufgabe. Möchte man bezüglich der eingesetzten Komponente flexibel bleiben, so wäre es kontraproduktiv die Requests wie oben gezeigt zu definieren. Sobald man beispielsweise undertow gegen Spark austauschen möchte steht man vor dem Problem alle Endpunkte umschreiben zu müssen. Um dies zu vermeiden stellt Camel einen Standard-Endpunkt für alle Rest-Requests bereit. Dieser Endpunkt muss nur einmal konfiguriert .

public void configure() {

    restConfiguration().component("undertow").port(4567);
    //restConfiguration().component("spark-rest").port(4567);

    from("rest:get:hello")
            .transform().constant("Hello World");
}

Um das Ganze noch ein wenig komfortabler zu gestalten bietet Camel zudem eine integrierte Rest-DSL an um Routen definieren zu können.

rest("/hello/")
        .get("dsl").to("direct:helloFromDSL")
        .post("dsl").consumes("application/json").to("direct:helloFromDSL");

from("direct:helloFromDSL")
        .transform().constant("Hello from DSL");

In der rest()-Direktive wird zuerst der Basepath definiert. Dieser gilt für alle darauf folgenden Direktiven. In dem obigen Beispiel werden beispielsweise eine POST- und eine GET-Methode für den Pfad /hello/dsl definiert.

Ein- und Ausgaben

Eingaben können auf drei Arten gelesen werden: Header, Parameter und der Post-Body.

Rest-Header werden vom Webbrowser genutzt um Metainformationen an den Webserver zu senden. Rest-Header werden bei Aufrufen von Camel-Routen in Camel-Header übersetzt. Header wie beispielsweise Authorization können so über das Exchange ausgelesen werden.

Bei Parametern kann es sich um Pfad- oder Queryparameter handeln. Beide Typen werden als Camel-Parameter übersetzt. Pfadparameter müssen in den Pfad der Camel-Route durch geschweifte Klammern aufgenommen werden. Ein Pfad wie beispielsweise /hallo/{name}/ erzeugt je nach eingegebener URL einen unterschiedlichen Wert für den Parameter name. Queryparameter auf der anderen Seite können nicht im Vorfeld festgelegt werden. Je nachdem welche Parameter bei dem Aufruf übergeben werden erzeugen diese die entsprechenden Header in der Camel-Route. Ein Aufruf in wie etwa /hello/Hans?test=Success erzeugt einen Header name mit dem Wert Hans und einen Header test mit dem Wert Success.

Die Letzte Möglichkeit Daten einzulesen besteht über den Body des Rest-Requests. Wird ein POST-Request mit einem Body gesendet, so wird der Camel-Body mit diesen Daten befüllt.

Rest-Header und -Body dürfen nicht mit Camel-Header und -Body verwechselt werden. Beides sind völlig unterschiedliche Konstrukte welche nichts miteinander gemein haben. Im Kontext von Rest-Requests auf Camel-Routen stehen diese jedoch in einer engen Beziehung zueinander da beide Konstrukte nicht nur gleich heißen sondern auch die Daten des jeweils anderen übernehmen.

Die folgende Camel-Route nimmt einen Post entgegen und gibt als Antwort alle erhaltenen Header und den Body zurück.

rest("input")
        .post("/{pathParam}/").to("direct:printOut");

from("direct:printOut")
        .process(exchange -> {
            final StringBuilder sb = new StringBuilder();
            sb.append("Headers:\n");
            sb.append(exchange.getIn().getHeaders());
            sb.append("\n\nBody:\n");
            sb.append(exchange.getIn().getBody());
            exchange.getIn().setBody(sb.toString());
        });

Bei Ausgaben verhält es sich ähnlich zu Eingaben. Header werden als Response-Header zurück gegeben. Der Body, welcher zuletzt in die Message geschrieben wurde wird als Content zurück gegeben. Um die Arbeit mit Rückgaben zu erleichtern hat Camel zudem ein paar Konstanten eingeführt, welche in der Exchange-Klasse zu finden sind.

rest("response")
        .get("/{name}/").to("direct:createResponse");

from("direct:createResponse")
        .removeHeaders("*")
        .setHeader(Exchange.HTTP_RESPONSE_CODE, constant(200))
        .setHeader(Exchange.CONTENT_TYPE, constant("text/html"))
        .transform().simple("<html><body>Hello ${exchange.getIn().getHeader('name')}</body></html>");

Es ist wichtig zu wissen, dass wirklich alle Camel-Header auch in den Response geschrieben werden. Von Anmeldedaten beim Request bis hin zu intern verwendeten Camel-Headern landen alle im Response. Daher sollte man diese löschen bevor man den Response zusammenbaut. Es könnte sonst passieren, dass unbeabsichtigt kritische Daten übertragen werden.

Output-Streaming

Eine Rest-Komponente welche Output-Streaming unterstützt ist beispielsweise Restlet. Durch das Output-Streaming werden Daten asynchron an den Aufrufer zurück übermittelt. Damit ist es möglich größere Daten in einzelnen Blöcken zurück zu geben ohne das diese bereits zu Beginn der Übermittlung komplett zur Verfügung stehen. Damit wird nicht nur Arbeitsspeicher gespart, sondern die Gefahr umgangen, dass die Übertragung aufgrund eines Timeouts unterbrochen wird. Für Restlet muss dazu beispielsweise ein InputStream dem Camel-Body übergeben werden. Solange dieser Stream Daten liefert werden diese als Chunks zurück gesendet. Der Stream kann selbst dann noch befüllt werden, wenn die Camel-Route schon durchgearbeitet wurde.

from("restlet:http://localhost:8989/stream")
    .process(exchange -> {
        final PipeStream pi = new PipeStream();
        Thread thread = new Thread(() -> {
            try {
                for (int i = 0; i < 5; i++) {
                    StringBuilder sb = new StringBuilder();
                    for (int j = 0; j < 100000; j++) {
                        sb.append(i);
                    }
                    pi.getOutputStream().write(sb.toString().getBytes());
                    Thread.sleep(5000);
                }
                pi.getOutputStream().close();
                pi.getInputStream().close();
            } catch (IOException | InterruptedException e) {
                e.printStackTrace();
            }

        });
        thread.start();
        exchange.getIn().setBody(pi.getInputStream());
    });
}

In dem obigen Beispiel wird ein InputStream erzeugt welcher in den Body geschrieben wird. Weiterhin wird ein nebenläufiger Thread gestartet. Dieser Thread füllt den Stream fünf mal alle fünf Sekunden mit Daten von 100.000 Byte. Es wird dabei eine PipeStream Helferklasse von Restlet verwendet , welche zwar nützlich ist jedoch nicht unbedingt benutzt werden muss.