Java in the Enterprise

JavaMoney, Money, Money: Eine Einführung in den JSR-354-Standard

Anatole Tresch

@istock/WinMaster

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.

Artikelserie

Teil 1: Grundlagen: API und Usage
Teil 2: SPI: Basics, Rundungen und Währungen
Teil 3: SPI: Konversion, Formatierung und Java EE

Dabei betrachten wir als Erstes die Klasse java.util.Currency. Diese bildet den ISO-4217-Standard 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 [1].

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. 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.

public interface CurrencyUnit {
  String getCurrencyCode();
  int getNumericCode();
  int getDefaultFractionDigits ();
  CurrencyContext getContext (); // new
}
CurrencyUnit currency1 = Monetary.getCurrency("CHF");
CurrencyUnit Currency2 = Monetary.getCurrency(Locale.GERMANY);}
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 folgendes.

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);
  ...
}
// 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();
MonetaryAmountFactory<?> factory = Monetary.getAmountFactory(
  MonetaryAmountFactoryQueryBuilder.of()
    .setPrecision(200).setMaxScale(10).build());
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.

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

Damit funktionale Erweiterungen auch zusammenpassen, definiert der JSR Interoperabilitätsregeln. Diese können in der Spezifikation 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 threadsicher.
  • 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). 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).

// 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
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- und Zielwährung einen Konversionsfaktor und einen Konversionskontext. 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 ConversionQuery beliebig parametriert werden. Noch komfortabler geht es aber mit einer CurrencyConversion, die als MonetaryOperator direkt auf einem Geldbetrag angewendet werden kann (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.

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;
}
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 oder des GitHub Repositorys. 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.

Geschrieben von
Anatole Tresch
Anatole Tresch
A​natole Tresch studierte Wirtschaftsinformatik und war anschließend mehrere Jahre lang als Managing Partner und Berater aktiv. ​Die letzten Jahre war ​Anatole Tresch als technischer Architekt und Koordinator bei der Credit Suisse​ tätig. Aktuell arbeitet Anatole ​als Principal Consultant für die Trivadis AG, ist Specification Lead des JSR 354 (Java Money & Currency) und PPMC Member von Apache Tamaya. Twitter: @atsticks
Kommentare
  1. Muszafa2016-08-17 23:50:20

    Hallo Listening 11 scheint nicht richtig zu sein. Es wird suggeriert als ob MonetaryConversions.getConversion( "CHF", "EUR"); die Wechselrichtung angibt. Dies ist nicht der Fall. In dem Artikel wird nicht näher über den Provider eingegangen, welche an die Stelle vom "EUR" muss. Es wäre Nett zu erläutern das auch hier die Möglichkeit besteht sein eigenen Provider zu erstellen

  2. Anatole Tresch2016-08-18 08:58:04

    Ja. In der Tat, da ist ein Fehler unterlaufen. Der 1. Parameter definiert die Zielwährung. Alle weiteren (es handelt sich imo um einen Ellipse-parameter) definieren eine Kette von providernamen, welche die zu benützenden Provider und ihre priorität festlegen. Wird nichts mitgegeben, so kommt die (konfigurierbare) default-Kette zum Zug.
    Um eigene Provider hinzuzufügen muss das Interface ExchangeRateProvider implementiert und im ServiceContext registriert werden. Per default stützt sich der ServiceContextProvider dabei auf java.util.Services ab....

Schreibe einen Kommentar

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