Enterprise Integrationen mit Apache Camel

24.11.2021 Stefan Welsch
Tech event-driven distributed-systems java

Viele Unternehmen wollen ihre Services in der Cloud zur Verfügung stellen. Dabei spielt die Anbindung an verschiedene Systeme eine enorm grosse Rolle. Will man beispielsweise in seiner App Kunden einen bestimmten Produktvorschlag machen, weil dieses Produkt gerade vergünstigt angeboten wird, so muss man Daten aus einer Recommendation Engine lesen, diese mit Daten aus einem SAP System anreichern und letztendlich in einer bestimmten Form der App zur Verfügung stellen. Bevor der Benutzer aber überhaupt auf diese Daten zugreifen kann, muss er natürlich authentisiert und authorisiert sein.

Genau bei diesen Aufgaben soll uns Apache Camel, als Integration Platform helfen und daher will ich euch heute die Vor- und auch die Nachteile von Apache Camel aufzeigen. Da ich aber das Thema Cloud mit aufgreifen will, stelle ich euch am Ende ausserdem Apache Camel K vor. Damit ist es möglich Camel Applikation direkt native im Kubernetes Cluster laufen zu lassen.

Vorher aber wie immer erstmal ein paar Fakten über Apache Camel. Das initiale Release von Camel ist im Juni 2007 erschienen. Somit ist Camel wohl das älteste Projekt, welches wir bis jetzt in unserer Techup Reihe vorgestellt haben. Das Framework wurde von der Apache Software Foundation entwickelt und ist in Java und XML geschrieben. Aufgebaut wurde das Tool auf den von den beiden Autoren Gregor Hohpe und Bobby Woolf geschriebenen Buch Enterprise Integration Pattern, welche speziell für den Entwurf von Enterprise Application Integration und Message Oriented Middleware basierte Systeme geschaffen wurden.

Was ist Apache Camel

Die EIPs (Enterprise Integration Pattern) zeigen uns, dass wir bei der Kommunikation zwischen Komponenten ausschliesslich Messages sowohl als Input, als auch als Output verwenden sollen. Es gibt einen vollständigen Satz von Mustern, aus denen wir auswählen und in unsere verschiedenen Komponenten implementieren können, die dann zusammen das gesamte System bilden.

Apache Camel bietet Schnittstellen für die EIPs, die Basisobjekte, häufig benötigte Implementierungen, Debugging-Tools, ein Konfigurationssystem und viele andere Helfer, die uns eine Menge Zeit sparen, wenn man eine Lösung nach den EIPs implementieren möchten. Es ist also ein vollständig produktionsreifes Framework für Entwickler, die ihre Lösungen nach den EIPs implementieren möchten.

Apache Camel Architektur

Quelle: https://camel.apache.org/manual/architecture.html

Camel Context

Apache Camel besteht aus einem Herzstück, genannt CamelContext. Dies ist im Prinzip die Runtime, die alles andere zusammenhält. Über den Camel Context hat man Zugriff auf die wichtigsten Services, Komponenten, Type Converters, Registry, Endpoints, Routes, Data Formats und Languages.

Routes

In einer Camel-Route wird der Integration Flow definiert. Um beispielsweise zwei Systeme zu integrieren, kann eine Camel-Route codiert werden, welche angibt, wie diese Systeme integriert werden. Will man zum Beispiel Daten von einer Url laden und diese in eine Datei schreiben, so würde man die folgende Route definieren. Dabei ist zu beachten, dass Routen mit einer der DSLs von Camel definiert werden. Wir nutzen in unseren Beispielen ausschliesslich die Java DSL. Aber auch hierunterscheidet man zwischen der Standard Route Builder Java DSL und der Enpoint Route Builder Java DSL. Letztere von beiden bietet uns typsicherheit, wie wir im folgenden Beispiel sehen können. Ich bevorzuge daher ganz klar die Enpoint Route Builder Variante.

Standard Route Builder Java DSL

from("http:myserver/myurl")
  .to("http:mydataprovider/myrequesteddata?bridgeEndpoint=true)
  .to("file:///path/to/your/file?fileName=yourFileName);

Endpoint Route Builder Java DSL

from(http("myserver/myurl"))
  .to(http("mydataprovider/myrequesteddata").bridgeEndpoint(true))
  .to(file("/path/to/your/file").fileName("yourFileName")));

Processors

Prozessoren werden verwendet, um Nachrichten während des Routings zu transformieren und zu manipulieren. Müssen wir beispielsweise Daten, welche wir von einer Url laden erstmal modifizieren bevor wir diese weitergeben, können wir dafür einen Processor nutzen. Warum ich “können” schreibe, erkläre ich in einem späteren Schritt, wenn wir uns Beans anschauen.

Components

Komponenten beschreiben in Camel die Erweiterungspunkte. Dadurch können wir Konnektivität zu anderen Systemen hinzufügen. Diese Komponenten stellen anderen Camel Resourcen eine Schnittstelle zur Verfügung, damit diese die Komponente nutzen können.

Apache Camel in der Praxis

Um zu verstehen, was genau Apache Camel macht, erstellen wir uns nun ein Beispielprojekt, welches Daten von einem Rest Endpunkt entgegennimmt, diese aufbereitet und anreichert. Danach werden die Daten an einem Rest-Endpunkt wieder zur Verfügung.

Um ein Camel Projekt zu erstellen, gibt es verschiedene Möglichkeiten. Wir wollen heute eine Spring Boot Applikation erstellen. Ich gehe nicht weiter auf das Setup von Camel Spring Boot ein. Dieses wird in der offiziellen Doku beschrieben.

Um lokale Testdaten zu haben, nutze ich einen Json Mock Server. Dazu wird einfach eine Datei db.json im Projekt erstellt und der Json Server dann mit dieser gestartet. Dieser Server erstellt uns damit 2 Endpunkte (todos und todo-details) die uns die Daten als Json zurückgeben.

{
  "todos": [
    {
      "id": 1,
      "title": "delectus aut autem"
    }
  ], 
  "todo-details": [
    {
      "userId": 1,
      "id": 1,
      "completed": false
    }
  ]
}

RestRoute

Als erstes wollen wir uns nun um den Endpunkt kümmern, welcher vom Benutzer aufgerufen wird. Es gibt also eine Schnittstelle /todos welcher uns alle ToDos mit allen Informationen zurückliefert. Wir erstellen uns also erstmal den Endpoint.

package com.bnova.camel;

import org.apache.camel.builder.endpoint.EndpointRouteBuilder;
import org.springframework.stereotype.Component;

@Component
public class RestRoute extends EndpointRouteBuilder {

	@Override
	public void configure() throws Exception {
		restConfiguration().host("localhost").port(8080);

		rest().get("/todos")
				.consumes("application/json")
				.produces("application/json")
				.to("log:info");
	}
}

Zeile 6: Registrierung unserer Route als Component. Die Route wird damit automatisch beim Starten der Applikation hinzugefügt.

Zeile 7: Wir erstellen unsere Route basierend auf dem EndpointRestBuilder

Zeile 11: Setzen des Host und Ports für unsere Rest API

Zeile 13-16: Anlegen eines Rest Endpoint auf den Pfad /todos, welcher JSON erwartet und JSON auch wieder ausgibt. Wir schreiben den Aufruf erstmal als Information ins Log.

Wir können nun die Applikation erstmal starten und sollten dann das folgend Log erhalten.

mvn spring-boot:run

....
2021-11-08 15:09:25.403  INFO 67677 --- [           main] com.bnova.camel.CamelApplication         : Starting CamelApplication using Java 16.0.1 on Stefan-Welsch-MacBook-Pro.fritz.box with PID 67677 (/Users/swelsch/Development/b-nova-techub/techup-camel/target/classes started by swelsch in /Users/swelsch/Development/b-nova-techub/techup-camel)
2021-11-08 15:09:25.404  INFO 67677 --- [           main] com.bnova.camel.CamelApplication         : No active profile set, falling back to default profiles: default
2021-11-08 15:09:27.075  INFO 67677 --- [           main] c.s.b.CamelSpringBootApplicationListener : Starting CamelMainRunController to ensure the main thread keeps running
2021-11-08 15:09:27.091  INFO 67677 --- [           main] org.eclipse.jetty.util.log               : Logging initialized @2275ms to org.eclipse.jetty.util.log.Slf4jLog
2021-11-08 15:09:27.134  WARN 67677 --- [           main] o.a.c.c.jetty.JettyHttpComponent         : You use localhost interface! It means that no external connections will be available. Don't you want to use 0.0.0.0 instead (all network interfaces)? jetty:http://localhost:8080/todos?httpMethodRestrict=GET
2021-11-08 15:09:27.178  INFO 67677 --- [           main] org.eclipse.jetty.server.Server          : jetty-9.4.44.v20210927; built: 2021-09-27T23:02:44.612Z; git: 8da83308eeca865e495e53ef315a249d63ba9332; jvm 16.0.1+0
2021-11-08 15:09:27.206  INFO 67677 --- [           main] o.e.jetty.server.handler.ContextHandler  : Started o.e.j.s.ServletContextHandler@1c5d3a37{/,null,AVAILABLE}
2021-11-08 15:09:27.234  INFO 67677 --- [           main] o.e.jetty.server.AbstractConnector       : Started ServerConnector@41def031{HTTP/1.1, (http/1.1)}{localhost:8080}
2021-11-08 15:09:27.234  INFO 67677 --- [           main] org.eclipse.jetty.server.Server          : Started @2420ms
2021-11-08 15:09:27.236  INFO 67677 --- [           main] o.a.c.impl.engine.AbstractCamelContext   : Routes startup summary (total:1 started:1)
2021-11-08 15:09:27.236  INFO 67677 --- [           main] o.a.c.impl.engine.AbstractCamelContext   :     Started route1 (rest://get:/todos)
2021-11-08 15:09:27.236  INFO 67677 --- [           main] o.a.c.impl.engine.AbstractCamelContext   : Apache Camel 3.12.0 (camel-1) started in 401ms (build:46ms init:190ms start:165ms)
2021-11-08 15:09:27.240  INFO 67677 --- [           main] com.bnova.camel.CamelApplication         : Started CamelApplication in 2.119 seconds (JVM running for 2.426)

In Zeile 14 können wir sehen, dass die Route registriert wurde. Wir können nun also die Route einmal via curl aufrufen und sehen im Log dann wie erwartet die Info Message.

curl localhost:8080/todos
2021-11-08 15:27:00.147  INFO 67677 --- [qtp177267393-29] info                                     : Exchange[ExchangePattern: InOut, BodyType: org.apache.camel.converter.stream.InputStreamCache, Body: [Body is instance of org.apache.camel.StreamCache]]

ToDo Route

Nun wollen wir uns die Route erstellen, welche uns die Daten vom Backend-Service abruft und entsprechend aufbereitet. Dazu erstellen wir uns eine neue Klasse ToDoRoute . In dieser definieren wir unsere zweite Route.

package com.bnova.camel;

import org.apache.camel.Exchange;
import org.apache.camel.builder.endpoint.EndpointRouteBuilder;
import org.eclipse.jetty.http.HttpMethod;
import org.springframework.stereotype.Component;

@Component
public class ToDoRoute extends EndpointRouteBuilder {

	@Override
	public void configure() throws Exception {
		getContext().setMessageHistory(true);

		from(direct("todo-route"))
				.routeId("todor-route")
				.removeHeaders("*")
				.setHeader(Exchange.HTTP_METHOD, constant(HttpMethod.GET))
				.setHeader(Exchange.CONTENT_TYPE, constant("application/json"))
				.to(http( "localhost:3000/todos").bridgeEndpoint(true));
	}
}

Zeile 15:

Wir starten unsere Route mit einem direkten Einstiegspunkt, welchen wir später in unserer RestRoute angeben.

Zeile 16:

Wir löschen alle Header. Es gibt einen Fehler bei jedem zweiten Aufruf an den Upstream, da irgendein E-Tag ausgewertet wird.

Zeile 17 + 18:

Wir definieren die Header für den Call zum Upstream

Zeile 19:

Hier wird der definitive Aufruf zum Upstream gemacht

Zeile 20:

Wir loggen uns den Body

Nun da wir untere ToDoRoute erstellt haben, wollen wir diese noch in unserer RestRoute aufrufen. Dazu ändern wir die folgende Zeile in der Rest Route folgendermassen ab.

rest().get("/todos")
				.consumes("application/json")
				.produces("application/json")
				.to("log:info") --> .to(direct("todo-route"));

Nun müssen wir die Applikation einmal neu starten und können erneut einen curl absetzen. Wie wir sehen können erhalten wir als Ausgabe das Json des todosendpoint unseres Backend Rest Services.

$ curl localhost:8080/todos
[
  {
    "id": 1,
    "title": "delectus aut autem"
  }
]

Nun wollen wir uns noch die Detailinformationen des Todo’s mit ausgeben lassen. Wir wollen also die Informationen des einen Backend-Service mit den Informationen des zweiten Backend-Service anreichern. Camel bietet uns hierfür einen enricher an, welcher genau für diesen Zweck gemacht wurde, nämlich Content anzureichern. Dazu erweitern wir unsere ToDo Route folgendermassen:

package com.bnova.camel;

import org.apache.camel.AggregationStrategy;
import org.apache.camel.Exchange;
import org.apache.camel.builder.endpoint.EndpointRouteBuilder;
import org.apache.camel.component.jackson.JacksonDataFormat;
import org.apache.camel.component.jackson.ListJacksonDataFormat;
import org.apache.camel.model.dataformat.JsonLibrary;
import org.eclipse.jetty.http.HttpMethod;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import java.io.InputStream;
import java.util.Collection;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;

@Component
public class ToDoRoute extends EndpointRouteBuilder {

	private JacksonDataFormat jacksonDataFormat;

	@PostConstruct
	public void init() {
		this.jacksonDataFormat = new ListJacksonDataFormat(ToDo.class);
	}

	AggregationStrategy aggregationStrategy = new AggregationStrategy() {

		@Override
		public Exchange aggregate(Exchange oldExchange, Exchange newExchange) {
			List<ToDo> toDo = oldExchange.getIn().getBody(List.class);
			try {
				InputStream body = newExchange.getIn().getBody(InputStream.class);
				List<ToDo> toDoDetails = (List<ToDo>) jacksonDataFormat.unmarshal(newExchange, body);
				toDo.addAll(toDoDetails);
				Collection<ToDo> todos = toDo.stream().collect(Collectors.toMap(ToDo::getId, Function.identity(), (left, right) -> {
					left.setCompleted(right.isCompleted());
					left.setUserId(right.getUserId());
					return left;
				})).values();
				oldExchange.getIn().setBody(todos);
			} catch (Exception e) {
				e.printStackTrace();
			}

			return oldExchange;
		}
	};


	@Override
	public void configure() throws Exception {
		getContext().setMessageHistory(true);

		from(direct("todo-route"))
				.removeHeaders("*")
				.setHeader(Exchange.HTTP_METHOD, constant(HttpMethod.GET))
				.setHeader(Exchange.CONTENT_TYPE, constant("application/json"))
				.to(
						http( "localhost:3000/todos")
								.bridgeEndpoint(true))
				.unmarshal(jacksonDataFormat)
				.enrich(http("localhost:3000/todo-details"), aggregationStrategy)
				.marshal().json(JsonLibrary.Jackson, ToDo.class);
	}
}

Zeile 29-50:
Wir definieren unseren Enricher und machen etwas Java-Stream Magic damit wir die eine Liste mit den Daten der anderen Liste anreichern können.

Zeile 64:
Hier wandeln wir die Response von dem ersten Call zum Upstream in eine Liste von ToDo Items

Zeile 65:
Wir nutzen die enrich Komponente um die erste Liste mit einem Call auf den zweiten Backend-Endpoint anzureichern.

Zeile 66:
Nun machen wir aus der Liste wieder einen String, den wir an den Consumer ausgeben.

Schauen wir uns das Ergebnis nach einem Neustart des Servers an.

$ curl localhost:8080/todos1                                                                                                                                                                                                                      2.6.3 16:02:19
[ {
  "userId" : 1,
  "id" : 1,
  "title" : "delectus aut autem",
  "completed" : false
}, {
  "userId" : 1,
  "id" : 2,
  "title" : "quis ut nam facilis et officia qui",
  "completed" : false
}, {
  "userId" : 2,
  "id" : 3,
  "title" : "fugiat veniam minus",
  "completed" : false
}, {
  "userId" : 2,
  "id" : 4,
  "title" : "et porro tempora",
  "completed" : true
}, {
  "userId" : 3,
  "id" : 5,
  "title" : "laboriosam mollitia et enim quasi adipisci quia provident illum",
  "completed" : false
} ]% 

Wie wir sehen können sind unsere Daten aus beiden Systemen nun aggregiert.

Wir haben in diesem Techup nur an der Oberfläche von Apache Camel gekratzt, um zu zeigen, wie effektiv man Endpoints für Consumer bauen kann, welche Daten aus mehreren Backends aggregieren und aufbereiten müssen. Will man also mit einem etablierten Framework und wenig Code eine schnelle Integration zaubern, so ist Apache Camel die richtige Wahl.

Stefan Welsch – Pionier, Stuntman, Mentor. Als Gründer von b-nova ist Stefan immer auf der Suche nach neuen und vielversprechenden Entwicklungsfeldern. Er ist durch und durch Pragmatiker und schreibt daher auch am liebsten Beiträge die sich möglichst nahe an 'real-world' Szenarien anlehnen.