Java Magazin   4.2016 - BPM: Kann mehr als man denkt

Erhältlich ab:  März 2016

Autoren / Autorinnen: 
Klaus KreftAngelika Langer ,  
Lars Röwekamp ,  
Sebastian Meyen ,  
Michael Müller ,  
,  
Bernd Rücker ,  
Anatole Tresch ,  
Michael Scholz ,  
Johannes Dienst ,  
Josef AdersbergerMario-Leander ReimerAndreas Zitzelsberger ,  
Milad Jason Daivandy ,  
Lars Röwekamp ,  
Nicolas BärDaniel TakaiChristian Wittwer ,  
Eberhard Wolff

Hand aufs Herz: Wann haben Sie Ihr letztes Java-Applet benutzt? War das im letzten Jahrzehnt? Im vergangenen Jahrhundert? Oder gehören Sie zu der Generation, die überhaupt noch nie von Applets gehört hat? Richtig, wer Steuererklärungen selbst macht, hat womöglich Kontakt mit dem Elster-Applet gehabt, und wer Minecraft im Browser zockt, hatte wohl bis Anfang letzten Jahres Kontakt zu dieser Steinzeittechnologie.

Sie sehen jedenfalls, das Ding, dessen Ende jetzt in den Medien so hohe Wellen geschlagen hat, ist für uns Techies völlig irrelevant. Und ich muss sagen, ich bin froh darüber, dass Oracle endlich, endlich diese Technologie vom Markt nimmt. Wie oft haben wir auf Spiegel Online und Co. lesen müssen, Java sei unsicher, bloß weil eine völlig merkwürdige Technologie, die zu einer Zeit entwickelt wurde, als es noch nicht mal CSS gab, regelmäßig Sicherheitsprobleme hatte. Nein, mit dem Java, über das wir hier im Magazin reden, hatte das beileibe wenig zu tun. Und nein, wir haben Applets schon lange nicht gebraucht und vermissen sie auch nicht. Java als Ganzes hat endlich ein Imageproblem weniger.

Steve Jobs hat immerhin schon 2010 mit seiner Weigerung, auf seinem iPad Flash zu unterstützen, dessen Niedergang eingeläutet. Bald darauf hat Microsoft Silverlight vom Markt genommen, und seit 2013 haben Firefox, Google Chrome und Microsoft Edge auch Java schrittweise verbannt – Oracles Entscheidung war sowas von überfällig. Jetzt sind sämtliche Bypasstechnologien vom Tisch, die nicht akzeptieren wollten, dass der Browser in Kombination mit HTML das richtige Fenster zum World Wide Web ist.

Szenenwechsel: Business Process Management, das Titelthema der vorliegenden Ausgabe, ist nicht gerade ein Hipsterthema, für das sich die Entwicklermassen begeistern, wie etwa coole Nutzeroberflächen mit JavaFX und AngularJS oder auch das Megathema Microservices. BPM ist aber verdammt nützlich und spielt ungebremst eine wichtige Rolle in zahlreichen komplexen Softwareprojekten. Und: BPM hat sich in den letzten Jahren deutlich verändert, ist jünger geworden, agiler, offener, als dies vielleicht so mancher denkt.

Diesen neuen Zugang zum Thema Business Process Management haben wir Bernd Rücker gebeten, in dieser Ausgabe kompakt darzustellen. Er berichtet, wie häufig er noch immer selbst gebastelten Workflows und State Machines begegnet und dass beim BPM noch immer die Befürchtung verbreitet ist, man schieße bei Verwendung einer BPM Engine mit Kanonen auf Spatzen.

Eine spannende Lektüre wünscht

meyen_sebastian_sw.tif_fmt1.jpgSebastian Meyen, Chefredakteur

Website Twitter Google Xing

JSR 354 standardisiert den Umgang mit Geldbeträgen in Java und ist seit Mai 2015 final. Der Standard wird in verschiedenen Projekten weltweit eingesetzt und läuft stabil. Also höchste Zeit, diesen JSR mal etwas genauer unter die Lupe zu nehmen.

Starten wir mit der Frage, warum die Funktionalität, die uns die Java-Plattform zur Verfügung stellt, nicht ausreicht. Dabei betrachten wir als Erstes die Klasse java.util.Currency. Diese bildet den ISO-4217-Standard [1] ab, der auch die bekannten Abkürzungen wie CHF und USD definiert. Für viele Anwendungsfälle reicht die gebotene Funktionalität vollauf, trotzdem können viele Anforderungen nicht abgedeckt werden. So enthalten ISO-Codes keine Information über ihre zeitliche und geografische Gültigkeit. Wenn man also Daten über längere Zeiträume speichern will, kann es vorkommen, dass eine gespeicherte Währung nicht mehr klar definiert ist. Als Beispiel stelle man sich griechische Drachmen vor, die bei einem Grexit wieder eingeführt worden wären. Der Währungscode enthält keine Informationen darüber, ob es sich um Drachmen aus der Zeit vor der Einführung des Euro oder nach dem Grexit handelt. Verschlimmert wird dies noch, wenn man bedenkt, dass theoretisch der Standard nach zehn Jahren einen nicht mehr benutzten Währungscode neu vergeben kann. Somit hätten wir die Eindeutigkeit ohne zusätzlichen Kontext vollständig verloren. Doch auch in den Codes selbst lauert Erstaunliches. So gibt es mit dem CFA einen Code, der für zwei Länder mit eigenen Legal Entities identisch ist. Oder umgekehrt sind mit USD, USN und USS gleich drei (!) Codes definiert, die allesamt US-Dollar modellieren. Und wer denkt, die drei Codes für US-Dollar seien eine Ausnahme: weit gefehlt! Auch für Schweizer Franken gibt es CHF, CHE und CHW. Im Gegensatz zu den amerikanischen Codes ist aber standardmäßig nur CHF in der Java-Plattform verfügbar. Die vordefinierten Codes sind in speziellen Dateien in der Java-Laufzeitumgebung untergebracht. Will man nun eigene Codes ergänzen, z. B. BTC für Bitcoins oder virtuelle Währungen, wie Lindon Dollars oder Facebook Coins, so muss man selbst in die JRE eingreifen. Bei Mandantenfähigkeit ist dann aber spätestens Schluss. Es kommt hinzu, dass aufgrund der Einschränkungen des ISO-Standards viele Unternehmen ihre eigenen Schlüsselräume definiert haben, um Währungen auch über längere Zeiträume eindeutig identifizieren zu können. Diese Lösungen können aktuell nur mittels externer Logik mit dem Currency-Typen verbunden werden. Schließlich ist auch die Formatierung der Währungen stark mit der Currency-Klasse verwoben, obschon sich eine Auftrennung des Datenobjekts und der Formatierungslogik in der Praxis durchaus bewährt hat.

Bei den Geldbeträgen und für die Konversion ist der Status quo schnell erklärt: Es gibt keinen Support durch die Plattform. Mit java.math.BigDecimal steht zwar ein numerischer Typ zur Verfügung, der viele Anforderungen abdeckt, aber es besteht keine Möglichkeit, die Währung mit dem numerischen Typen mitzuführen. Ebenso ist das Laufzeitverhalten für anspruchsvolle Low-Latency-Szenarien nicht ausreichend. Weiterführende Konzepte wie das Runden oder die Währungskonversion sucht man vergebens.

Für die Formatierung steht mit java.text.DecimalFormat eine mächtige Lösung zur Verfügung. Allerdings ist auch hier die Funktionalität ungenügend. Zum Beispiel kennt DecimalFormat keine variablen Gruppierungen. Somit ist es nicht möglich, indische Rupien als INR 12,23,123.34 zu formatieren (was aber eigentlich richtig wäre). Ein weiteres Beispiel wäre die adaptive Formatierung in Abhängigkeit des Werts, z. B. CHF 1'345.– für kleinere Beträge und CHF 1.3 Mio für größere. Bei noch komplexeren Anforderungen ist oft auch eine erweiterte Parametrierung sinnvoll, die ebenfalls nicht abgedeckt werden kann. Die fehlende Threadsicherheit kann nur noch historisch erklärt werden.

Es zeigen sich also einige Punkte, an denen die Unterstützung durch die Java-Plattform unbefriedigend ist. Also Grund genug, einen JSR zu starten [2].

Diese Anforderungen müssen erfüllt werden

Aufgrund der Ausgangsanalyse können wir folgende Anforderungen definieren:

  • Es muss möglich sein, zusätzliche Währungen ins System aufzunehmen oder auch bestehende Währungen bei externen Ereignissen rasch anpassen zu können, ohne in die Laufzeitplattform einzugreifen.

  • Währungen müssen mit zusätzlichem Kontext ergänzt werden können. Dies ermöglicht eine klare Identifikation und Zuordnung bezüglich Zeit, Ort und weiteren Aspekten, wie die Abbildung auf interne Schlüssel oder Provider. Um diese Szenarien gut abdecken zu können, sollte es auch möglich sein, nach Währungen zu suchen.

  • Schließlich soll es auch möglich sein, verfügbare Währungen in Abhängigkeit des aktuellen Mandanten definieren zu können.

  • Es soll ein API für die Modellierung von Geldbeträgen, Runden und Währungskonversion definiert werden.

  • Die Formatierung sollte vom Datentyp getrennt behandelt und ebenfalls den individuellen Bedürfnissen angepasst werden können. Dabei soll auch hier die Formatierung flexibel erweiterbar sein, um auch komplexe Anforderungen unterstützen zu können.

  • Das API soll gezielt funktionale Erweiterungspunkte definieren, um weitere Funktionen ergänzen zu können, ohne das bestehende Design anpassen zu müssen. Auch soll es möglich sein, zusätzlichen Kontext mitzugeben, um eine effiziente und nahtlose Integration mit bestehender Logik zu ermöglichen.

  • Das Design muss sich an den Designprinzipien der Plattform orientieren.

  • Das API muss komplett mit SE und wenn möglich auch mit ME kompatibel sein und keine externen Abhängigkeiten aufweisen.

  • Interoperabilität mit bestehenden Artefakten, speziell mit java.util.Currency muss gewährleistet sein.

  • Funktionaler Programmierstil soll effizient unterstützt werden.

Währungen abfragen

Die existierende Klasse java.util.Currency modelliert ISO 4217 [1]. Im JSR wurde intensiv diskutiert, welche zusätzlichen Eigenschaften notwendig sind, um auch historische und virtuelle Währungen abzubilden. Dabei hat sich gezeigt, dass die bereits bestehende Funktionalität weitgehend ausreicht. Die einzige Ergänzung ist ein zusätzlicher CurrencyContext als typisierten Container für Schlüssel-Werte-Paare (Listing 1). Für den Zugriff auf Währungen wurde ein Monetary Singleton definiert, das ähnliche Funktionen wie die Currency-Klasse bietet (Listing 2). Für weitere Anwendungsfälle können Währungen auch mit einem Query-API abgefragt werden. Die Funktionalität kann dabei mit entsprechenden SPI-Implementationen individuellen Wünschen angepasst werden. So könnte z. B. eine Abfrage historischer Währungen in Europa wie in Listing 3 gezeigt modelliert werden.

Listing 1

public interface CurrencyUnit {
  String getCurrencyCode();
  int getNumericCode();
  int getDefaultFractionDigits ();
  CurrencyContext getContext (); // new
}

Listing 2

CurrencyUnit currency1 = Monetary.getCurrency("CHF");
CurrencyUnit Currency2 = Monetary.getCurrency(Locale.GERMANY);}

Listing 3

Collection<CurrencyUnit> currencies = Monetary.getCurrencies(
  CurrencyQueryBuilder.of().set("continent", "EU")
    .set(LocalDate.class, LocalDate.of(0,1,1)).build());

Geldbeträge

Bei einem Geldbetrag würde man typischerweise die folgenden Aspekte erwarten:

  • Ein numerischer Wert

  • Eine Währung

  • Eine Menge von arithmetischen Operationen, ähnlich wie bei BigDecimal, um mit den Beträgen rechnen zu können.

Diese Definition greift aber zu kurz. Insbesondere, weil in der Praxis die Anforderungen, die an einen numerischen Implementationstypen gestellt werden, massiv streuen. Das betrifft insbesondere die numerische Präzision/Skalierung (z. B. Berechnung des Welt-BIPs in Dollar vs. Rentenverzinsungswerte in Produkterechnungen) und das Laufzeitverhalten (z. B. kleiner Webshop vs. Low-Latency Trading). Dieses Problem wurde gelöst, indem ein Geldbetrag (Listing 4) zusätzlich noch weitere Funktionalität aufweisen muss:

  • Es werden mehrere Implementationstypen unterstützt und entsprechende Interoperabilitätsregeln definiert.

  • Ein Geldbetrag muss im zusätzlichen Kontext zwingend (nebst anderen Eigenschaften) Angaben über die numerischen Fähigkeiten seiner Implementation zur Verfügung stellen.

Die Monetary Singletons erzeugen Geldbeträge so wie Währungen (Listing 5). Der gewünschte Implementationstyp kann dabei entweder direkt referenziert (Listing 5) oder mit dem Query-API abgefragt werden (Listing 6). Erzeugte Geldbeträge sind wie fast alle Datentypen als threadsichere Wertetypen implementiert. Die arithmetischen Funktionen erlauben das Rechnen ähnlich wie mit der BigDecimal-Klasse (Listing 7). Für diejenigen unter uns, die noch double für monetäre Berechnungen benützen, empfiehlt sich [3].

Listing 4

public interface MonetaryAmount 
extends CurrencySupplier, Supplier<Number>, Comparable<MonetaryAmount> {
  CurrencyUnit getCurrency();
  NumberValue getNumber();
  MonetaryAmountFactory<? extends MonetaryAmount> getFactory();
  MonetaryContext getContext();
 
  // Funktionale Erweiterungspunkte
  <R> R query(MonetaryQuery <R> query);
  MonetaryAmount with(MonetaryOperator operator);
 
  // arithmetische Operationen analog zu BigDecimal
  MonetaryAmount add(MonetaryAmount amount);
  MonetaryCmount multiply(MonetaryAmount amount);
  ...
}

Listing 5

// default factory
MonetaryAmount amt = Monetary.getDefaultAmountFactory()
                                             .setCurrency("EUR")
                                             .setNumber(200.5).create();
// explizite Factory
Money amt = Monetary.getAmountFactory(Money.class)
                               .setCurrency("EUR")
                               .setNumber(200.5).create();

Listing 6

MonetaryAmountFactory<?> factory = Monetary.getAmountFactory(
  MonetaryAmountFactoryQueryBuilder.of()
    .setPrecision(200).setMaxScale(10).build());

Listing 7

Money price = Money.of(1250.34, "CHF");
Money halfPrice = price.divide(2);

Funktionale Erweiterungspunkte und mehr

Bereits beim MonetaryAmount-Interface haben wir mit dem MonetaryOperator (Listing 8) einen so genannten funktionalen Erweiterungspunkt angetroffen. Dieser erlaubt es, beliebige Funktionalität, die einen Betrag als Eingabe nimmt und einen Betrag als Ausgabe produziert, extern zu modellieren, und ermöglicht es uns, so die MonetaryAmount-Schnittstelle trotz der Vielfalt an Anwendungsfällen minimal zu halten. Dennoch ist der Mechanismus mächtig: So werden nebst dem Runden auch diverse Umrechnungen wie Prozente, Promille, Minor- und Major-Units oder auch die Währungskonversion mithilfe dieses Konzepts modelliert.

Listing 8

public interface MonetaryOperator{
  MonetaryAmount apply(MonetaryAmount amount);
}

Damit funktionale Erweiterungen auch zusammenpassen, definiert der JSR Interoperabilitätsregeln. Diese können in der Spezifikation [4] nachgelesen werden.

Als zweiter funktionaler Erweiterungspunkt wurde MonetaryQuery definiert. Diese Schnittstelle nimmt analog zum MonetaryOperator einen Geldbetrag als Eingabe, kann aber jeden beliebigen Type T als Rückgabe erzeugen. Eine mögliche Anwendung ist die Formatierung, die einen Geldbetrag auf einen String abbildet.

Nebst den funktionalen Erweiterungspunkten finden sich im JSR 354 auch weitere durchgängige Designkonzepte:

  • Datentypen sind mit Interfaces definiert. Instanzen müssen als Wertetypen implementiert werden, das heißt, sie lassen sich lesen, serialisieren und sind thread­sicher.

  • Jeder Datentyp, aber auch Services für das Erzeugen, Runden, Konvertieren und Formatieren von Beträgen, besitzt einen zusätzlichen Kontext, der es erlaubt, benutzerdefinierte Daten integriert mitzugeben.

  • Datentypen als auch Abfragen werden mit entsprechenden Buildern erzeugt.

  • Neue Instanzen werden über Singletons bezogen. Dabei definiert jeder funktionale Bereich genau ein Singleton (Währungen, Beträge, Runden ––> Monetary, Konversion ––> MonetaryConversions, Formatierung ––> MonataryFormats).

  • Für einfachere Fälle bieten die Singletons direkte Zugriffsmethoden an. Für komplexere Szenarien steht ein Query-API zur Verfügung, welches es erlaubt, typisiert beliebige Parameter zu übergeben. Die Implementationen im API delegieren alle Funktionen an entsprechende SPIs (diese werden im nächsten Artikel genauer betrachtet), das man über einen ebenfalls konfigurierbaren ServiceContext lädt.

Pi mal Daumen: richtig Runden

Beim Rechnen mit monetären Daten ist das Runden der Werte ein wichtiger Aspekt. Java selbst bringt bereits diverse mathematische Rundungsalgorithmen mit. Monetäres Runden erfordert unter Umständen jedoch die Umsetzung weiterer Logik (siehe Cash Rounding [5]). Auch ergibt ein Runden des Geldbetrags nach jeder Operation wenig Sinn, da somit unkontrollierbar in einem späteren Berechnungsschritt noch benötigte Präzision verlorengehen könnte. Entsprechend muss das Runden als eigener Aspekt modelliert werden:

  • eine Abbildung eines Geldbetrags auf einen anderen Geldbetrag (MonetaryOperator)

  • einen zusätzlichen RoundingContext, um auch hier weitere Aspekte unterbringen zu können, z. B. den Typ der Rundung

Auch Rundungen werden mithilfe des Monetary Singletons bezogen. Das Runden selbst ist einfaches Anwenden des MonetaryOperator auf den Betrag (Listing 9). Auch beim Runden können komplexere Fälle, wie das Anfragen einer speziellen Rundung für den Barverkehr in der Schweiz mithilfe einer RoundingQuery modelliert werden (Listing 10).

Listing 9

// default: benutzt die defaultFractionUnits der Währung 
MonetaryRounding rounding = Monetary.getDefaultRounding();
// Rundung für ein Land
rounding = Monetary.getRounding(Locale.GERMANY);
// frei definierbare Rundung (SPI gestützt)
rounding = Monetary.getRounding(„myCustomRounding“);
 
MonetaryAmount amt = ...;
amt = amt.with(rounding);  // Runden, Variante 1
amt = rounding.apply(amt); // Runden, Variante 2

Listing 10

MonetaryRounding rounding = Monetary.getRounding(
  RoundingQueryBuilder.of()
    .setCurrency(Monetary.getCurrency("CHF"))
    .set("cashRounding", true).build());

Währungskonversion

Grundlage einer Konversion ist die ExchangeRate. Diese beinhaltet nebst der Quell-ext. JSR 354 unterstützt auch mehrstufige Konversionen (z. B. so genannte Triangular Rates). Eine ExchangeRate ist dabei immer unidirektional. Für die Umkehrung einer Konversion muss eine neue ExchangeRate angefordert werden. Dies geschieht über einen ExchangeRateProvider. Auf diesen kann über das MonetaryConversions Singleton zugegriffen werden. Wie in anderen Bereichen des API kann auch hier die Abfrage mit einer Con­ver­sion­Query beliebig parametriert werden. Noch komfortabler geht es aber mit einer CurrencyConversion, die als MonetaryOperator direkt auf einem Geldbetrag angewendet werden kann (Listing 11).

Listing 11

CurrencyConversion conversion = MonetaryConversions.getConversion("USD");
ExchangeRateProvider prov = MonetaryConversions
  .getExchangeRateProvider ();
ExchangeRate rate = prov.getExchangeRate("CHF", "EUR");
 
CurrencyConversion conversion = MonetaryConversions.getConversion("CHF", "EUR");
MonetaryAmount amtCHF = ...;
MonetaryAmount amtEUR = amtCHF.with(conversion); // Konversion!

Formatieren

Um Geldbeträge mit dem JSR-354-API zu formatieren, können Instanzen von MonetaryAmountFormat (Listing 12) über das MonetaryFormats Singleton bezogen werden. Am einfachsten geht das mit einer Locale (Listing 13), aber es können auch komplexere Abfragen ausgeführt werden – entsprechende SPI-Implementationen vorausgesetzt.

Listing 12

public interface MonetaryAmountFormat extends MonetaryQuery <String> {
  AmountFormatContext getContext ();
  String format(MonetaryAmount amount);
  void print(Appendable appendable, MonetaryAmount amount)
    throws IOException;
  MonetaryAmount parse(CharSequence text)
    throws MonetaryParseException;
}

Listing 13

MonetaryAmountFormat fmt =
  MonetaryFormats.getAmountFormat(Locale.US);

Ausblick

JSR 354 definiert ein relativ einfaches, aber mächtiges API, das den Umgang mit Geldbeträgen wesentlich vereinfacht, aber dennoch genügend Funktionalität bietet, um auch komplexere Anwendungsszenarien zu unterstützen. Erweiterungspunkte erlauben es zusätzlich, benötigte Funktionalität auch in Form externer Bibliotheken einfach nach Bedarf hinzuzunehmen. Es steckt aber noch einiges mehr in dem Projekt. Im nächsten Artikel wollen wir uns dann das SPI genauer anschauen. Bis dahin lohnt sich auch ein Besuch auf der OSS-Seite des Projekt  [6] oder des GitHub Repositorys [7]. Für diejenigen unter uns, die noch nicht mit Java 8 arbeiten, stellt der JSR übrigens auch eine Java-7-kompatible Version von API und Implementation zur Verfügung.

tresch_anatole_sw.tif_fmt1.jpgAnatole Tresch studierte Wirtschaftsinformatik und war anschließend mehrere Jahre lang als Managing-Partner und -Berater aktiv. Heute arbeitet Anatole Tresch als Principal Consultant bei der Trivadis AG, ist Specification Lead des JSR 354 (Java Money and Currency) und PPMC Member von Apache Tamaya.

Twitter

Vor zwei Jahren habe ich den Tod der Java Application Server ausgerufen. Doch noch immer nutzen viele Anwendungen trotz so mancher Nachteile eben diese. Warum? Wo ist es vielleicht auch noch sinnvoll? Und welche Alternativen gibt es mittlerweile?

Der ursprüngliche Artikel [1] hat mehrere Gründe aufgeführt, warum Application Server keine zeitgemäße Plattform mehr sind. So ist Two-Phase Commit (2PC) zur Koordination mehrerer transaktionaler Ressourcen aus verschiedenen Gründen kaum sinnvoll. Dieser Punkt wäre einen eigenen Artikel wert, soll hier aber nicht weiter betrachtet werden. Mittlerweile ist es außerdem mit Technologien wie Spring Boot [2] möglich, Transaktionsmanager wie Atomikos oder Bitronix auch ohne Application Server zu nutzen. Das war zwar schon lange möglich, ist nun aber noch einmal wesentlich einfacher geworden. Selbst wenn also 2PC notwendig sein sollte, ist das noch lange kein Grund für den Einsatz eines Application Servers.

Der zweite Grund war, dass Application Server keine vollständige Plattform sind. Praktisch jede Anwendung benötigt neben dem Application Server zusätzliche eigene Bibliotheken. Also fehlen Features, die Anwendungen selbst mitbringen müssen.

Durch den Application Server legt man sich außerdem für alle Bestandteile der Plattform auf eine bestimmte Version fest. Es ist nicht möglich, nur ausgewählte Bestandteile zu aktualisieren, um beispielsweise Security oder Bug Fixes einzuspielen. Und der Application Server unterstützt nur spezielle Anwendungen – nämlich Webanwendungen. Unterstützung für Batchanwendungen oder Integrationsanwendungen fehlen. Also muss eine Anwendung einen Teil der Infrastruktur selbst mitbringen. Als vollständige Plattform ist der Application Server nicht ausreichend. Noch wichtiger ist die Kritik am Application Server als Plattform für mehrere Anwendungen.

Die Anwendungen auf einem Application Server sind nur unzureichend isoliert. Sie können zwar keinen Code anderer Anwendungen laden, aber bezüglich CPU und Speicher bedienen sie sich aus einem Pool. Eine Anwendung, die viel Speicher allokiert, bringt den Application Server und damit alle Anwendungen auf dem Application Server zum Absturz. Ebenso ist meistens kein isoliertes Deployment möglich. Denn meistens wird der Application Server auch beim Deployment einer einzigen Anwendung neu gestartet.

Während ursprünglich mehrere Anwendungen auf einem Application Server deployt werden sollten, ist das Verhältnis mittlerweile umgedreht: Eine Anwendung wird oft auf einem Cluster von Application Servern und damit auf mehreren Application Servern betrieben.

Application Server und Anwendungen hängen wechselseitig voneinander ab: Die Anwendung verwendet die Funktionen des Application Servers und hängt daher vom Application Server ab. Der Application Server ist speziell für die Anwendung konfiguriert – beispielsweise enthält er die passenden Datenbankverbindungen. Wenn aber zwei Softwarebestandteile wechselseitig voneinander abhängen, sind sie eigentlich nicht mehr zu trennen. Eine Änderung an einem der Bestandteile beeinflusst wegen der Abhängigkeit auch jeweils den anderen. Ein weiteres Indiz für dieses Problem ist, dass oft ein Installationsskript für den Application Server direkt in der Versionskontrolle der Anwendung liegt.

Ein Application Server kann also eigentlich kaum mehrere Anwendungen beheimaten. Bleibt noch die Frage, ob für den professionellen Betrieb einer Anwendung ein Application Server notwendig ist. Schließlich bietet der Application Server eine Umgebung für den Betrieb und das Monitoring der Anwendungen. Es ist aber möglich, auch mehrere Instanzen einer Software in einem Cluster automatisiert zu deployen, zu monitoren und die Logs auszuwerten, ohne dabei einen Application Server zu nutzen. Mittlerweile haben sich viele Werkzeuge aus dem Continuous-Delivery-Kontext [3] dieser Probleme angenommen.

wolff_application_1.tif_fmt1.jpgAbb. 1: Application Server und Anwendung hängen wechselseitig voneinander ab

Bei dieser Kritik an den Application Servern geht es nicht um die APIs oder das Programmiermodell, sondern vor allem um Betrieb und Deployment. Die Frage ist also nicht, ob das Java-EE-Programmiermodell sinnvoll ist, sondern ob das Deployment einer Anwendung auf einem Application Server nützlich ist oder eher Nachteile mit sich bringt. Die Betrachtung betrifft also nicht nur Application Server, die das vollständige Java-EE-Profile implementieren, sondern auch Webserver wie Tomcat, die lediglich das Deployment von WAR-Dateien mit Servlets erlauben.

Alternativen?

Statt die Anwendung auf einem Application Server zu deployen, kann sie ihre gesamte Infrastruktur mitbringen. Man spricht von Uber JAR, Fat JAR Deployment oder auch Containerless Deployment. In einem einzigen JAR-File ist dann die gesamte Anwendung mit den notwendigen Bestandteilen eines Application Servers untergebracht. Diesen Ansatz verfolgen beispielsweise Spring Boot aus dem Spring-Universum oder Play [4], Ratpack [5] oder Vert.x [6] als alternative Ansätze auf der JVM. Zum Zeitpunkt des ursprünglichen Artikels war das Angebot für geeignete Lösungen im Java-EE-Umfeld dünn. Spring Boot bietet einige Unterstützung [7]. Dropwizard [8] fokussiert vor allem auf JAX-RS JSON Web Services und kann damit zumindest einen Teil der Java-EE-APIs anbieten.

wolff_application_2.tif_fmt1.jpgAbb. 2: Fat JAR: Die Anwendung bringt die gesamte Infrastruktur mit – einschließlich des eingebetteten Application Servers

Mittlerweile sind aber auch klassische Java-EE-Anbieter auf diesen Ansatz eingeschwenkt. JBoss bietet mit WildFly Swarm [9] seinen Application Server jetzt auch in einer Form, die Deployment als Fat JAR erlaubt. Ähnliches erlaubt TomEE [10]. Somit kann also mittlerweile auch eine klassische Java-EE-Anwendung ohne Deployment auf einem Application Server in Produktion gebracht werden. Wie schon erwähnt, geht es eben nicht um das Programmiermodell, sondern um das Deployment.

Der Java-EE-Standard selbst hat allerdings auf diese Ansätze nicht reagiert. Ansätze wie WildFly Swarm oder TomEE sind keine Implementierungen des Java-EE-Standards, weil sie das Java-EE-Deployment-Modell nicht unterstützen. Eigentlich wäre es sinnvoll, den Standard in einen Deployment-Teil, der WAR oder EAR Deployment umfasst, und einen API-Teil, der das Programmiermodell umfasst, aufzuteilen. WildFly Swarm oder TomEE mit Fat JARs würden dann den API-Teil implementieren, nicht aber den Deployment-Teil.

Neben dem Deployment als Fat JAR ist bei der Nutzung dieser Ansätze wichtig, dass die Anwendungen auch Metriken und sonstige Informationen für das Monitoring ausgeben, um so die Überwachung der Anwendungen zu ermöglichen.

Die Auswirkungen

Also bieten Application Server keine Vorteile und sind überflüssig. Aber gibt es auch echte Nachteile? Das ist auch tatsächlich der Fall: Der Application Server muss konfiguriert und installiert werden. Das erhöht den Aufwand beim Betrieb und Deployment. Außerdem ist der Betrieb eines Application Servers oft an proprietäre Werkzeuge gebunden, während der Rest der Infrastruktur, wie Datenbanken oder Webserver, mit standardisierten Werkzeugen überwacht und installiert werden kann.

Kurz: Der Betrieb wird durch einen Application Server unnötig verkompliziert. Diese Probleme werden immer wichtiger, denn aufgrund von Continuous Delivery müssen Anwendungen in den verschiedenen Testphasen installiert werden, und sie werden auch viel öfter in Produktion deployt. Ebenso sind Microservices [11], [12] ein Einflussfaktor: Sie teilen Anwendungen in kleine Module auf, die jeweils einzeln deployt werden. Statt also eine Anwendung einmal pro Quartal in Produktion zu bringen, werden nun mit Microservices viele Anwendungen deployt; und wegen Continuous Delivery auch viel öfter in verschiedenen Umgebungen. Wenn der Aufwand für das Deployment und den Betrieb eines Application Servers eingespart werden kann, ist das gerade in diesen Szenarien hilfreich. Durch Microservices und Continuous Delivery setzten sich außerdem immer mehr Entwickler mit Werkzeugen auseinander, die für den Betrieb und das Deployment relevant sind. Dadurch realisieren sie, dass es neben Java Application Servern zahlreiche alternative Ansätze gibt, die für den Betrieb und das Monitoring ebenfalls nützlich sind. Deswegen betrachten sie auch zunehmend Ansätze ohne Application Server.

Obwohl Microservices und Continuous Delivery ihren Teil zu dem Abschied vom Application Server beitragen, sind sie nicht Voraussetzungen dafür: Natürlich kann eine monolithische klassische Java-EE-Anwendung einmal pro Quartal ohne Application Server ausgeliefert werden. Auch dann ergeben sich durch die niedrigere Komplexität beim Deployment in Produktion, aber mehr noch während der Entwicklung erhebliche Vorteile. Nun stellt sich die Frage, ob Application Server nicht doch in bestimmten Situationen noch sinnvoll sind.

Der Betrieb ist behäbig

Zwar sind Deployment und Betrieb einfacher, aber in vielen Organisationen gibt es bereits etablierte Betriebs- und Deployment-Prozesse, die auf Application Server abgestimmt sind. Der technischen Vereinfachung steht dann der Aufwand zur Umstellung der Prozesse gegenüber. Die Prozesse müssen im Betrieb umgestellt werden, für den oft eher die Stabilität der Prozesse wichtig ist. Außerdem gibt es meistens mehr Deployments auf Testumgebungen, die unter der Kontrolle der Entwicklung stehen. Der Betrieb profitiert nur von den Vereinfachungen beim Deployment in Produktion und hat also nur einen Teil der Vorteile. Obwohl also der Verzicht auf Application Server in diesem Szenario technisch sinnvoll wäre, kann es aufgrund von organisatorischen Herausforderungen dazu kommen, dass dennoch weiter Application Server genutzt werden.

Wenn die technischen Vorteile einmal verstanden sind, sind die oft erzwungene Konstanz im Betrieb und die hohen Investitionen in vorhandene Application-Server-Infrastrukturen sicher der primäre Grund, warum Application Server immer noch im Einsatz sind.

Die Anforderungen sind niedrig

Die Diskussion geht davon aus, dass es für jede Anwendung eine oder mehrere Application-Server-Instanzen gibt. Es gibt aber Fälle, in denen tatsächlich trotz aller technischen Nachteile mehrere Anwendungen auf einem Application Server deployt werden. Das kann sinnvoll sein, wenn die Anforderungen an die Skalierung und die Ausfallsicherheit nicht so hoch sind, sodass kein Betrieb im Cluster notwendig ist und auch die mangelnde Isolation kein Problem ist. Dann haben Application Server echte Vorteile: Eine Anwendung ist nur noch eine WAR-Datei, die in ein bestimmtes Verzeichnis kopiert werden muss und in die Betriebsprozesse integriert ist. Application Server sparen dann also Aufwände im Betrieb. Diese Szenarien sind überraschend, denn Application Server sind ursprünglich für Enterprise-Szenarien entwickelt worden, in denen typischerweise die Ausfallsicherheit und Skalierbarkeit sehr wichtig sind. Nur wenn die Ansprüche in diesen Bereichen nicht sonderlich hoch sind, haben Application Server aber tatsächlich Vorteile und eine Vereinfachung zur Folge.

Micro- und Nanoservices nutzen

Es gibt aber auch andere Gründe für den Einsatz von Application Servern. Interessanterweise können Application Server sinnvoll dazu genutzt werden, um Microservices zu betreiben. Dazu werden mehrere Services auf einem Application Server betrieben – jeder als ein eigenständiges WAR. Im Gegensatz zu richtigen Microservices sind diese Services aber im Fall eines Ausfalls nicht richtig isoliert, weil bei einem Ausfall eines Application Servers mehrere Services ausfallen. Das ist problematisch, weil in einer Microservices-Umgebung viele Services laufen. Wenn der Ausfall eines Service andere Services mitreist, kann es leicht zu Dominoeffekten kommen: Ein Service fällt aus. Das führt nach und nach zum Crash weiterer Services, sodass am Ende das gesamte System nicht mehr zur Verfügung steht. Üblicherweise vermeiden Microservices das Problem, indem sie beim Ausfall eines aufgerufenen Service Vorgabewerte oder Caches nutzen. Das ist aber bei einem gemeinsamen Deployment nicht ausreichend. Wenn ein Service ein Speicherleck hat, bringt er die JVM zum Absturz und damit alle Services, die auf demselben Application Server deployt sind. Vorgabewerte oder Caches nützen dann nichts mehr.

Im Gegensatz zu echten Microservices ist bei der Nutzung eines Application Servers auch die Technologiefreiheit eingeschränkt. Wenn als Basis für die Microservices statt eines Application Servers Docker gewählt wird, können die Microservices beliebige Programmiersprachen und Plattformen nutzen. Auch wenn die Technologien auf die JVM eingeschränkt werden, können Ansätze wie Play oder Vert.x genutzt werden, die andere Programmiersprachen und ein Reactive-Programmiermodell unterstützen. Application Server hingegen unterstützen nur das Java-EE-Programmiermodell.

Ein weiteres Problem bei diesem Vorgehen: Selbst ein unabhängiges Deployment ist nicht wirklich möglich, da in der Praxis beim Deployment eines einzelnen Service in einem WAR meistens der gesamte Application Server neu gestartet werden muss. Das beeinflusst die anderen Services, die dann für einige Zeit ausfallen. Das unabhängige Deployment ist aber eine zentrale Eigenschaft von Microservices. Aufgrund dieser Möglichkeit kann ein Team neue Features ohne Abstimmung mit anderen Teams in Produktion bringen. Das ist kaum möglich, wenn die Services gemeinsam in einem Application Server laufen. Dann wird nämlich das Deployment eines Service die anderen Services zum Ausfall bringen, und das macht vermutlich eine Koordination notwendig.

Das Deployment mehrerer Services auf einem Application Server hat also einige Charakteristika von Microservices, ist aber dennoch ein etwas anderer Ansatz. Insbesondere die Kompromisse bezüglich des unabhängigen Deployments sind schmerzhaft. Das unabhängige Deployment ist eine zentrale Eigenschaft von Microservices. Um diesen Ansatz klar von richtigen Microservices zu trennen, kann für dieses Modell ein anderer Begriff wie Nanoservices [11] sinnvoll sein.

wolff_application_3.tif_fmt1.jpgAbb. 3: Beispiel für Nanoservices: Order, Customer und Catalog sind jeweils fachliche Nanoservices, die jeweils für den Nutzer über HTTP und HTML nutzbar sind; untereinander nutzen sie REST zur Kommunikation; sie sind alle gemeinsam auf einem Tomcat-Server deployt

Docker?

Mit Docker [13] steht ein neuer Ansatz bereit, der zumindest von den Begriffen her etwas mit der Application-Server-Welt zu tun hat: Es gibt auch bei Docker Container, die eine weitgehend isolierte Linux-Umgebung mit eigenen Filesystemen und Netzwerkkonfigurationen bereitstellen. Ebenso gibt es ein Deployment-Modell, das jeweils ein vollständiges Filesystemimage umfasst und damit eine komplette Installation einschließlich Betriebssystem. Es gibt ein ausgefeiltes System, um Ba­sis­images zu ermöglich. So kann eine Installation eines Betriebssystems für mehrere Anwendungen wiederverwendet werden.

Dieser Ansatz lässt sich mit einem Application Server kombinieren: Es gibt ein Basisimage, auf dem der Application Server deployt ist. Jede Anwendung verwendet dieses Basisimage und fügt die Anwendung hinzu. Das scheint zunächst den betrieblichen Aufwand für einen Application Server zu reduzieren. In der Realität sind aber die zentralen Probleme nicht gelöst: Der Application Server muss nach wie vor auf die jeweilige Anwendung angepasst werden, und damit ist das Deployment immer noch komplex. Also hilft Docker nicht dabei, den Einsatz von Application Servern zu vereinfachen.

Fazit

Application Server haben das Enterprise-Java-Umfeld lange dominiert. Es sind teilweise sehr hohe Investitionen in diese Technologien geflossen, und sie sind tief in die Deployment- und Betriebsprozesse verwurzelt. Daher ist es nicht überraschend, dass immer noch viele Application Server in Produktion sind und dort wohl auch bleiben werden. Aus einer rein technischen Perspektive sind Application Server aber in den allermeisten Fällen nicht nur verzichtbar, sondern haben echte Nachteile. Ausnahmen bilden lediglich Szenarien mit recht niedrigen Anforderungen an Verfügbarkeit und Skalierbarkeit oder das beschriebene Nanoservices-Szenario.

Mittlerweile kommen auch immer mehr Technologien auf den Markt, die ohne Application Server auskommen und auf ein Fat JAR Deployment setzen. Dazu zählen auch immer mehr Vertreter der Java-EE-APIs, sodass auch mit diesen klassischen APIs ein Verzicht auf Application Server möglich ist.

Der ursprüngliche Artikel hat zu einigen Diskussionen geführt – für die ich mich an dieser Stelle bedanken möchte. Feedback ist immer hilfreich und das Feedback hat zu diesem Artikel beigetragen. Leider kann ich hier nicht alle namentlich aufführen. Ich freue mich auf das Feedback zu diesem Artikel!

wolff_eberhard_sw.tif_fmt1.jpgEberhard Wolff arbeitet seit mehr als fünfzehn Jahren als Architekt und Berater. Er ist Fellow bei der innoQ. Als Autor hat er über hundert Artikel und Bücher geschrieben und als Sprecher auf zahlreichen internationalen Konferenzen vorgetragen.

Web Twitter