21. Januar 2025

Apache Camel REST DSL

In der Softwareentwicklung spielen flexible Schnittstellen eine zentrale Rolle. Apache Camel bietet mit der REST DSL ein Werkzeug, um APIs effizient zu definieren – ganz nach dem Code-First-Ansatz. Ob mit Java, XML, Spring, YAML oder Annotationen, Entwickler:innen erhalten die Freiheit, ihre bevorzugte Sprache und Methodik zu wählen. In seinem Blogpost gibt Christoph Gächter praktische Einblicke anhand eines Beispiels.

Software Development & Architecture
Kamel in der Wüste als Logo

Apache Camel ermöglicht die Definition von REST Services anhand der Domain Specific Language (DSL) als Code First Approach. Die DSL kann je nach Vorliebe mit Java, XML, Spring, YAML, REST oder Annotation erfolgen. In diesem Beispiel wollen wir Java verwenden, eingebettet in einer Spring Boot Application.

Java DSL Beispiel: Get Request

Am Einfachsten schauen wir uns gleich ein Beispiel an: Im folgenden Code-Ausschnitt wird in der Klasse RestRouteBuilder die Methode configure() gezeigt, welche das REST API definiert.

public class RestRouteBuilder extends RouteBuilder {

  @Override
  public void configure() {

    rest().get("/students/{id}/timetable")
        .param().name("id").type(path).dataType("string").endParam()
        .param().name("from").type(query).dataType("string").dataFormat("date").endParam()
        .param().name("to").type(query).dataType("string").dataFormat("date").endParam()
        .produces(MediaType.APPLICATION_JSON_VALUE)
        .to("direct:getTimetable")

        ...
  }
}

Der definierte GET Request definiert die drei Parameter id, from und to, wobei id im Path, die anderen beiden Parameter in der Query zu finden sind. Alle Parameter sind vom Type String, wobei from und to ein Datum darstellen. Als Output-Format wird JSON vorgegeben.

Soweit ist der GET Request für das REST API vollständig definiert. Zusätzliche Beschreibungen der Parameter oder die zu erwarteten HTTP Codes runden das API ab. Wird dieses zur Laufzeit abgefragt, erfolgt folgende Ausgabe:

{
  "openapi" : "3.0.1",
  "info" : {
    "title" : "Camel REST Service API",
    "version" : "1.2.0"
 },
  "servers" : [ {
    "url" : "/localhost:18080/rest/services/v1/*"
  } ],
  "paths" : {
    "/students/{id}/timetable" : {
      "get" : {
        "summary" : "Get timetable for given student identifier",
        "operationId" : "verb1",
        "parameters" : [ {
          "name" : "id",
          "in" : "path",
          "description" : "Unique student identifier to search for timetable",
          "required" : true,
          "schema" : {
            "type" : "string"
          }
        }, {
          "name" : "from",
          "in" : "query",
          "description" : "Timetable period start",
          "required" : true,
          "schema" : {
            "type" : "string",
            "format" : "date"
          }
        }, {
          "name" : "to",
          "in" : "query",
          "description" : "Timetable period end",
          "required" : true,
          "schema" : {
            "type" : "string",
            "format" : "date"
          }
        } ],
        "responses" : {
          "200" : {
            "description" : "Timetable successfully returned",
            "content" : {
              "application/json" : {
                "schema" : {
                  "$ref" : "#/components/schemas/StudentTimetableResponse"
                }
              }
            }
          },
          "400" : {
            "description" : "Invalid / bad request (client fault)"
          },
          "401" : {
            "description" : "Authentication failure"
          },
          "403" : {
            "description" : "Authorization failure"
          },
          "404" : {
          "description" : "No data found, i.e. the student identifier is unknown"
          },
          "422" : {
            "description" : "Invalid request parameters, e.g. period out of range"
          },
          "500" : {
            "description" : "Internal server error (server fault)"
          }
        }
      }
    }
  }
}

Damit die Abfrage der API Spezifikation möglich ist, kann entweder im Java Code oder in der Konfiguration (z.B. application.yml) das Mapping definiert werden. Schauen wir uns deshalb gleich die Camel Configuration in der Konfigurationsdatei application.yml an:

camel:
   servlet:
     mapping:
       context-path: "${serviceEndpointPath}/*"
   springboot:
     name: "camel-rest-service-v${parsedVersion.majorVersion:X}"
     tracing: false
     stream-caching-enabled: true
     use-breadcrumb: true
   health:
     enabled: true
   rest:
     binding-mode: json
     component: servlet
     context-path: "${serviceEndpointPath}/*"
     data-format-property:
       prettyPrint: false
     host: "${serviceEndpointHost}"

     # rest DSL api-doc configuration
     api-context-path: "/api-doc"
     api-property:
       api.title: "Camel Rest Service API"
       api.version: "1.2.0"

Nebst dem Context Path für das Servlet bzw. für das REST API wird am Schluss der Context Path für das API mit /api-doc angegeben. Zudem werden die Properties wie Titel und Version gesetzt. Die Version entspricht dabei nicht der Build-Version, sondern dient ausschliesslich der Versionierung des API.

Der GET Request wird an die interne Route weitergeleitet (to(„direct:getTimetable“)), welche den Service ausführt:

from("direct:getTimetable")
    .log(INFO, LOGGER_NAME, 
        "getTimetable: id=${headers.id}, from=${headers.from}, to=$headers.to}")
    .routeId("getTimetableRouteId").setExchangePattern(ExchangePattern.InOut)
    .removeHeaders("*", EXCLUDE_PATTERN)
    .bean(this, "getTimeTable(${headers.id}, ${headers.from}, ${headers.to})");

Im Bean (in diesem Beispiel in derselben Instanz) wird schliesslich die Methode getTImeTable mit den notwendigen Parametern aufgerufen, wo dann die eigentliche Logik für die Abfrage des Stundenplans implementiert würde:

protected StudentTimetableResponse getTimeTable(String id, LocalDate from, LocalDate to) {
    log.info("getTimeTable from datasource: id={}, from={}, to={}", id, from, to);
    StudentTimetableResponse response = new StudentTimetableResponse(id, from, to);
    // get timetable from datasource and return response
    return response;
 }

Weitere HTTP Methoden

Selbstverständlich stehen nebst dem GET Request weitere HTTP Methoden wie POST, PUT und DELETE zur Verfügung. Im vorliegenden Beispiel-Code sind die jeweiligen HTTP Methoden implementiert.

rest().post("/students")
     .consumes(MediaType.APPLICATION_JSON_VALUE)
     .produces(MediaType.APPLICATION_JSON_VALUE)
     .to("direct:createStudent")
     .outType(Student.class);

 rest().put("/students/{id}")
     .param().name("id").type(path).dataType("string").endParam()
     .consumes(MediaType.APPLICATION_JSON_VALUE)
     .produces(MediaType.APPLICATION_JSON_VALUE)
     .outType(Student.class)
     .to("direct:updateStudent");

 rest().delete("/students/{id}")
     .param().name("id").type(path).dataType("string").endParam()
     .consumes(MediaType.APPLICATION_JSON_VALUE)
     .to("direct:deleteStudent");

Exeption Handling

Ein wichtiges Thema ist das Exception Handling. Auch hier bietet die DSL die Möglichkeit an, auf bestimmte Exceptions explizit reagieren zu können. Dazu können ein oder mehrere Exception Handler definiert werden. Wichtig in der Definition ist dabei, ob die Exception abschliessen behandelt worden ist (handled(true)) oder nicht. In der Behandlung können HTTP Code und aussagekräftige Meldungen als Response ausgegeben werden.

onException(ValidationException.class, JsonProcessingException.class)
        .id("restErrorHandlerId")
        .handled(true)                     
        .removeHeaders("*", EXCLUDE_PATTERN)         
        .process(errorResponseProcessor);

Fazit

Mit der REST DSL bietet Camel vielseitige Möglichkeiten und Implementierungen (Java, XML, Spring, YAML, REST oder Annotation), um relativ exakt das API zu definieren (Code First Ansatz). Mit Apache Camel 4.6 wird auch der Contract First Approach unterstützt, basierend auf der OpenAPI v3 Spezifikation. Mit den beiden Ansätzen Code First und Contract First ist das OpenAPI eine elegante und zeitgemässe Alternative zu Web Services / WSDL.

Damit haben Entwickler:innen die Möglichkeit, direkt mit dem Camel Framework das API zu definieren und die Requests an die entsprechenden Camel Routen weiterzuleiten. Die Routen ihrerseits bieten die vielfältigen Möglichkeiten von Transformationen, Integrationsdiensten und -fähigkeiten sowie Routingdiensten.

Im verwendeten Ansatz mit Camel, CXF und Spring Boot mag die Type Conversion von eigenen Klassen, aber auch «Quasi-Standard-Typen» wie LocalDate manchmal etwas ungewöhnlich erscheinen. Dies sollte in zukünftigen Releases verbessert werden.

Quellen und weitere Informationen

Das Beispiel stammt aus dem Mandat mit der Berner Fachhochschule (BFH), wo wir den Enterprise Service Bus (ESB) warten und weiterentwickeln konnten. Vielen Dank an die BFH, welche diese Publikation ermöglicht und unterstützt hat.