Suche
Teil 2: JSR 354: Basics, Rundungen und Währungen

Go for the Money: Einführung in das Money and Currency API

Anatole Tresch

©iStockphoto.com/RicardoReitmeyer

JSR 354 definiert nicht nur ein API, sondern auch ein mächtiges Service Provider Interface (SPI), das erlaubt, praktisch alle Funktionen den jeweiligen Bedürfnissen in einer Unternehmung anzupassen. Die Expertengruppe diskutierte über Monate hinweg teilweise sehr kontrovers diese Unabhängigkeit des API. Das heute vorliegende Design hat sich zwar erst in der Schlussphase des JSR durchgesetzt, es hat sich aber in der Praxis bewährt.

Artikelserie

Die Struktur des JSR 354 lässt sich in eine Matrix bestehend aus vier Schichten und den drei funktionalen Bereichen zerlegen (Tabelle 1). Die Schichten sind:

  • Die API-Schicht, die von den Benutzern aufgerufen wird.
  • Die API Singletons, eines je funktionalem Bereich: Monetary, MonetaryConversions, MonetaryFormats. Diese delegieren Aufrufe an die Singleton-SPI-Schicht (die entsprechenden Schnittstellen enden auf SingletonSpi).
  • Die Singleton-SPI-Schicht wiederum verwaltet und orchestriert das Provider-SPI, das kleinere abgegrenzte Funktionen implementiert, z. B. bestimmte Formate, Konversionen oder die Zurverfügungstellung von Währungen.
  • Schlussendlich ist auch das Komponenten-Lifecycle-Management nicht fest codiert, um so unterschiedliche Umgebungen wie Java SE und OSGI oder auch CDI/Java EE unterstützen zu können. Diese Funktionalität wird von allen anderen Komponenten genutzt.
Schicht \ Funktionalität Core Konversion Formatierung
API-Singletons javax.money.Monetary javax.money.conversion.
MonetaryConversions
javax.money.
format.MonetaryFormats
API-Singleton-SPI (javax.money.spi) MonetaryAmountsSingletonSpi
MonetaryAmountsSingletonQuerySpi
MonetaryCurrenciesSingletonSpi
MonetaryRounsingsSingletonSpi
MonetaryConversions-
SingletonSpi
MonetaryFormatsSingletonSpi
Provider-SPI MonetaryAmountFactoryProviderSpi
CurrencyProviderSpi
RoundingProviderSpi
javax.money.conversion.
ExchangeRateProvider
MonetaryAmountFormat-
ProviderSpi
Lifecycle-Management javax.money.spi.Bootstrap, javax.money.spi.ServiceProvider

Tabelle 1: Die Struktur des JSR

Betrachten wir zunächst die Klasse javax.money.Monetary, die den Einstiegspunkt für Geldbeträge, Währungen und Rundungen modelliert. In Listing 1 ist gut zu erkennen, wie Methodenaufrufe an das entsprechende Singleton-SPI delegiert werden. Die SPI-Implementierungen werden dabei über die Bootstrap-Komponente beim Laden der Singleton-Klasse einmalig geladen und gecached (Listing 2 und Listing 3). Dieses Muster wird bei allen Singletons angewendet. Wenn also ein Verhalten angepasst oder erweitert werden soll, muss man theoretisch nichts anderes tun als die Singleton-SPI-Schnittstelle zu implementieren und im Bootstrap zu registrieren. Normalerweise ist das aber nicht notwendig, da die Implementierung des Provider-SPI in den meisten Fällen genügt sowie einfacher und schlanker ist. In diesem Artikel werden wir die core-Funktionen des JSR (zugreifbar über die Klasse Monetary) näher betrachten. Die Schnittstellen für die Bereiche Konversion und Formatierung werden im nächsten Artikel behandelt. Bevor wir aber starten, sehen wir uns noch den BootstrapMechanismus an.

public static Set<String> getRoundingProviderNames() {
  return Optional.ofNullable(MONETARY_ROUNDINGS_SINGLETON_SPI)
  .orElseThrow(
  ) -> new MonetaryException(
    "No MonetaryRoundingsSpi loaded, query functionality is not available.")
  .getProviderNames();
}
private static MonetaryCurrenciesSingletonSpi loadMonetaryCurrenciesSingletonSpi() {
  try {
    return Optional.ofNullable(Bootstrap
      .getService(MonetaryCurrenciesSingletonSpi.class))
    .orElseGet(DefaultMonetaryCurrenciesSingletonSpi::new);
  } catch (Exception e) {
    Logger.getLogger(Monetary.class.getName())
    .log(Level.INFO,
      "Failed to load MonetaryCurrenciesSingletonSpi, using default.", e);
    return new DefaultMonetaryCurrenciesSingletonSpi();
  }
}
public final class Monetary {
  private static final MonetaryCurrenciesSingletonSpi
  MONETARY_CURRENCIES_SINGLETON_SPI =
    loadMonetaryCurrenciesSingletonSpi();
  private static final MonetaryAmountsSingletonSpi
  MONETARY_AMOUNTS_SINGLETON_SPI =
    loadMonetaryAmountsSingletonSpi();
  private static final MonetaryAmountsSingletonQuerySpi
  MONETARY_AMOUNTS_SINGLETON_QUERY_SPI =
    loadMonetaryAmountsSingletonQuerySpi();
  private static final MonetaryRoundingsSingletonSpi
  MONETARY_ROUNDINGS_SINGLETON_SPI =
    loadMonetaryRoundingsSingletonSpi();
  private Monetary() {}
  [...]
}

Die Rolle des Bootstrap-Mechanismus

Die Klasse javax.money.spi.Bootstrap stellt den zentralen Zugriffspunkt für alle Komponenten im JSR-354-Kontext bereit. Standardmäßig stützt sich der Bootstrap-Mechanismus auf die Funktionalität der Klasse java.util.ServiceLoader ab. Eine Implementierung des Interface a.b.c.Interface kann also registriert werden, indem eine Textdatei, die den voll qualifizierten Klassennamen der Implementierungsklasse(n) enthält, unter META-INF/services/a.b.c.Interface im Klassenpfad abgelegt wird. Die Referenzimplementierung erweitert hierbei den ServiceLoader mit der Möglichkeit, @Priority-Annotationen zu benutzen, um Services zu priorisieren (Listing 4) oder zu übersteuern.

Es existieren aber auch Laufzeitumgebungen, die mit dem ServiceLoader nicht kompatibel sind (z. B. OSGi). Aus diesem Grund erlaubt der Bootstrap-Mechanismus eine eigene Implementierung des ServiceProviders (Listing 5) bereitzustellen, programmatisch oder mit dem JDK ServiceLoader. Die Methode getPriority() wird in diesem Falle benutzt, um zu entscheiden, welche Implementierung als Delegate für das Bootstrap Singleton benutzt werden soll. Dabei gewinnt der höchste Wert.

@javax.annotation.Priority(5)
public class MyCurrencyProvider implements CurrencyProvider{
  [...]
}
public interface ServiceProvider {
  public int getPriority();

  <T> List<T> getServices(Class<T> serviceType);

  default <T> T getService(Class<T> serviceType) {
    return getServices(serviceType).stream().findFirst().orElse(null);
  }
}

Neue Währungen ergänzen

Nehmen wir an, wir müssen in unserem Projekt auch Bitcoins als Währung unterstützen. Bitcoins sind aber im aktuellen ISO-Standard und auch auf der Java-Plattform nicht definiert. Wir müssen also die beiden Währungscodes BTC und XBT ergänzen. Dazu implementieren wir das Provider-SPI CurrencyProviderSpi (Listing 6). Dank der default-Methoden müssen wir einzig die Methode getCurrencies(CurrencyQuery) definieren (Listing 7). Die gezeigte Implementierung ist dabei absichtlich einfach gehalten. Da eine Query nichts anderes als eine typsichere Map mit beliebigen Parametern ist, können auch wesentlich komplexere Abfragen modelliert werden. Mit zunehmender Komplexität ist die Einführung einer Parameterklasse aber zu empfehlen.
Eine CurrencyQuery kann von einem oder mehreren Providern beantwortet werden. Dieses Muster findet sich im ganzen JSR wieder (Währungen=CurrencyQuery oder Rundungen=RoundingQuery). Dabei implementiert die Referenzimplementierung – in der Singleton-SPI-Schicht – die folgende Orchestrierungslogik:

  1. Alle Provider werden in einer Verarbeitungskette in aufsteigender Reihenfolge abgearbeitet. Die Kette und ihre Reihenfolge wird aufgrund der Prioritäten (@Priority(int)-Annotationen, keine Annotation == @Priority(0)) automatisch aus allen verfügbaren Providern gebildet oder beim Aufruf als Liste/Array von Providernamen explizit übergeben (Listing 8). Einzelne Provider werden dabei über den Providernamen identifiziert, definiert durch die Methode String getProviderName() auf dem Provider-SPI.
  2. Wird ein singuläres Resultat angefragt, so wird der erste Nicht-Null-Wert eines Providers als Resultat zurückgegeben.
  3. Bei mehrwertigen Resultaten (Collections) werden alle Resultate kombiniert.
public interface CurrencyProviderSpi {
  default String getProviderName(){
    return getClass().getSimpleName();
  }
  default boolean isCurrencyAvailable(CurrencyQuery query){
    return !getCurrencies(query).isEmpty();
  }
  Set<CurrencyUnit> getCurrencies(CurrencyQuery query);
}
public class BTCProvider implements CurrencyProviderSpi {

  pivate CurrencyUnit BTC = new CurrencyUnitBuilder("BTC")
                                .setProvider(getProviderName())
                                .setDefaultFractionUnits(3)
                                .build();
  private CurrencyUnit XBT = new CurrencyUnitBuilder("XBT")
                                .setProvider(getProviderName())
                                .setDefaultFractionUnits(3)
                                .build();

  public Set<CurrencyUnit> getCurrencies(CurrencyQuery query){
    if(query.getCurrencyCodes().contains("BTC")){
      Set<CurrencyUnit> result = new HashSet<>();
      result.add(BTC)
      return result;
    }else if(query.getCurrencyCodes().contains("XBT")){
      Set<CurrencyUnit> result = new HashSet<>();
      result.add(XBT)
      return result;
    }
    return Collections.emptySet();
  }
}
Collection<CurrencyUnit> currencies = Monetary.getCurrencies(
  CurrencyQueryBuilder.of()
  .setCountries(Locale.CANADA) // Land
  .set(LocalDate.of(1970,1,1)) // Datum
  .setProviderNames("CLDR").build() // Provider
);

Eine Rundung für Bitcoins ergänzen

Wenn wir Listing 7 nochmals genauer betrachten, so sehen wir, dass die minor units für BTC/XBT als Tausendstel modelliert worden sind (setDefaultFractionUnits(3)). Wir möchten nun aber eine Rundung registrieren, die Bitcoins auf Satoshi (1/100 000 000) genau rundet. Dazu müssen wir das Provider-SPI RoundingProviderSpi (Listing 9) implementieren. Die Implementierung (Listing 10) liefert dabei genau dann eine Instanz von SatoshiRounding (Listing 11) zurück, wenn eine Rundung mit Namen Satoshi explizit angefragt worden ist, oder wenn eine Rundung für die Währung mit Code BTC oder XBT angefordert wird.
Mit diesen beiden Ergänzungen sind wir nun in der Lage, Bitcoins zu benutzen und diese bei Bedarf auf Satoshi genau zu runden (Listing 12).

public interface RoundingProviderSpi {
  MonetaryRounding getRounding(RoundingQuery query);
  Set<String> getRoundingNames();

  default String getProviderName(){
    return getClass().getSimpleName();
  }
}
public class SatoshiProvider implements RoundingProviderSpi {
  private final Set<String> roundingNames;
  private MonetaryRounding satoshiRounding = new SatoshiRounding();

  public SatoshiProvider(){
    Set<String> set = new HashSet<>();
    set.add("Satoshi");
    this.roundingNames = Collections.unmodifiableSet(set);
  }
  public MonetaryRounding getRounding(RoundingQuery query){
    CurrencyUnit currency = query.getCurrency();
    if(currency!=null && ("BTX".equals(currency.getCurrencyCode()) ||
                          "XBT".equals(currency.getCurrencyCode()))){
      return satoshiRounding;
    }else if("Satoshi".equals(query.getRoundingName()){
      return satoshiRounding;
    }
    return null;
  }
  public Set<String> getRoundingNames(){
    return roundingNames;
  }
}
class SatoshiRounding implements MonetaryRounding{

  private final RoundingContext CONTEXT =
    RoundingContextBuilder.of("SatoshiProvider", "Satoshi").build();

  @Override
  public RoundingContext getRoundingContext() {
    return CONTEXT;
  }

  @Override
  public MonetaryAmount apply(MonetaryAmount amount) {
    NumberValue value = amount.getNumber().round(new MathContext(6,
        RoundingMode.HALF_EVEN));
    return amount.getFactory().setNumber(value).create();
  }
}
CurrencyUnit bitcoin = Monetary.getCurrency("BTC");
MonetaryAmount btcAmount = ...; // whatever calculation to get some bitcoin amount
// round it to Satoshi
MonetaryAmount btxRounded = btcAmount
  .with(Monetary.getRounding(btcAmount.getCurrencUnit());

Eine eigene Klasse für Geldbeträge registrieren

Zum Schluss sehen wir noch, wie Implementierungstypen für Geldbeträge ergänzt werden können. In den meisten Fällen ist das nicht nötig, denn der JSR bringt in seiner Referenzimplementierung bereits mächtige Implementierungen von MonetaryAmount mit Money und FastMoney. Money benutzt BigDecimal als interne Repräsentation. FastMoney benutzt einen long-Wert, wobei die letzten fünf Stellen als minor-units interpretiert werden.
Nehmen wir an, wir hätten einen entsprechenden Typen implementiert (MySuperFastMoney). Um die neue Klasse verfügbar zu machen, müssen wir das Provider SPI MonetaryAmountFactoryProviderSpi implementieren (Listing 13). Dabei ist die adäquate Definition des MonetaryContext wichtig. MonetaryAmountFactoryProviderSpi liefert effektiv zwei MonetaryContext-Instanzen zurück: eine Instanz, welche die default numerischen Eigenschaften der Implementierung beschreibt und eine Instanz, welche die maximalen numerischen Eigenschaften der Implementierung beschreibt.

Die Angaben im MonetaryContext werden von der Komponente MonetaryAmountSingletonQuerySpi benutzt, um bei Anfragen für einen Betragstypen die möglichst passende MonetaryAmountFactory zurückzugeben. Unsere Implementierung stellt eine fixe numerische Repräsentation bereit (setFixedScale(true)). Somit ist der MonetaryContext für beide Fälle identisch. Zusätzlich haben wir durch die Angabe von QueryInclusionPolicy.DIRECT_REFERENCE_ONLY definiert, dass unsere Betragsimplementierung nur explizit angefordert werden kann – mit Angabe des Zieltyps. Schließlich müssen wir auch noch MonetaryAmountFactory implementieren. Dieses Interface nimmt die unterschiedlichen Parameter wie die Währung oder den numerischen Betrag entgegen und erzeugt mit der Methode create() einen neuen Geldbetrag. Damit sich das Ganze gut mit anderen Betragstypen integriert, müssen MySuperFastMoneyAmountFactory und MySuperFastMoney diverse weitere Anforderungen und Interoperabilitätsregeln implementieren. Interessierten empfehle ich außerdem die Klassen Money und FastMoney in der Referenzimplementierung näher zu betrachten.

public final class MySuperFastMoneyAmountFactoryProvider implements MonetaryAmountFactoryProviderSpi {

  private final MonetaryContext CONTEXT =
    MonetaryContextBuilder.of(MySuperFastMoney.class)
                          .setFixedScale(true)
                          .setPrecision(16)
                          .setMaxScale(16)
                          .setProviderName(
    MySuperFastMoneyAmountFactoryProvider.class.getSimpleName())
                          .build();
  @Override
  public Class getAmountType() {
    return MySuperFastMoney.class;
  }

  @Override
  public QueryInclusionPolicy getQueryInclusionPolicy(){
    return QueryInclusionPolicy.DIRECT_REFERENCE_ONLY;
  }

  @Override
  public MonetaryAmountFactory createMonetaryAmountFactory() {
    return new MySuperFastMoneyAmountFactory();
  }

  @Override
  public MonetaryContext getDefaultMonetaryContext() {
    return CONTEXT;
  }
}

Ausblick

Dieser Artikel zeigte einen ersten Überblick über das SPI-Design von JSR 354. Dabei fällt auf, dass einige wenige Designentscheidungen durchgängig zur Anwendung kommen und deshalb ein rascher Einstieg in das Innenleben möglich ist. Das SPI ist in verschiedene Stufen aufgegliedert, die Möglichkeiten und Komplexität geschickt ausbalancieren. Einfache Anwendungsfälle wie die Erweiterung von neuen Währungen und Rundungen sind sehr einfach umzusetzen, während das SPI aber auch für komplexere Einsatzszenarien flexibel und anpassbar bleibt. Einzig die Erweiterung des JSR mit zusätzlichen Betragstypen ist recht aufwendig und komplex, da dort diverse Interoperabilitätsvorgaben eingehalten werden müssen. Glücklicherweise sind aber die bereits von der Referenzimplementierung beigesteuerten Implementierungen bereits sehr mächtig und lassen kaum noch Wünsche offen.

Im nächsten Teil werden wir sehen, wie wir neue Währungskonversionen und neue Formate definieren können. Und wir werden mithilfe des Bootstrap-Mechanismus den JSR transparent mit CDI im Java-EE-Umfeld integrieren.

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

Schreibe einen Kommentar

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