Interpretation of JavaMoney specification (JSR 354) and corresponding implementation

1, Overview

1.1 current situation

At present, the class used to express currency in JDK is Java util. Currency, this class can only represent the currency type described in [ISO-4217]. It has no value associated with it, nor can it describe some currencies outside the specification. There is no relevant support for currency calculation, currency exchange and currency formatting, and there is no relevant description even for the standard type that can represent the currency amount. JSR-354 defines a set of standard API s to solve these problems.

1.2 purpose of specification

The main objectives of JSR-354 are:

  • Provide possibility for currency expansion and support the demands of rich business scenarios on currency type and currency amount;
  • Provide API for currency amount calculation;
  • Support and expansion of currency exchange rates;
  • Provide support and extension for currency and currency amount parsing and formatting.

1.3 usage scenarios

online store

The unit price of goods in the mall, which is the total price to be calculated according to the quantity of goods after adding the goods to the shopping cart. Currency exchange involved with the change of settlement currency type after switching the payment method in the mall. When the user places an order, it involves payment amount calculation, tax calculation, etc.

Financial transaction website

On a financial trading website, customers can create virtual portfolios at will. The calculated historical, current and expected returns are displayed according to the created portfolio and combined with historical data.

Virtual world and game websites

Online games will define their own game currency. Users can buy game currency through the amount in the bank card, which involves currency exchange. Moreover, due to the wide variety of games, the required currency type support must also be able to support dynamic expansion.

Banking and financial applications

Banks and other financial institutions must establish monetary model information in terms of exchange rate, interest rate, stock quotation, current and historical currency, etc. Usually, such company internal systems also have additional information for financial data representation, such as historical currency, exchange rate and risk analysis. Therefore, currencies and exchange rates must be historical and regional, and their validity ranges must be defined.

2, JavaMoney parsing

2.1 package and engineering structure

2.1. 1 package overview

JSR-354 defines four related packages:

(Figure 2-1 package structure)

javax.money contains major components such as:

  • CurrencyUnit;
  • MonetaryAmount;
  • MonetaryContext;
  • MonetaryOperator;
  • MonetaryQuery;
  • MonetaryRounding ;
  • Related singleton visitor Monetary.

javax.money.convert contains components related to currency exchange, such as:

  • ExchangeRate;
  • ExchangeRateProvider;
  • CurrencyConversion ;
  • Related singleton visitor MonetaryConversions.

javax.money.format contains formatting related components, such as:

  • MonetaryAmountFormat;
  • AmountFormatContext;
  • Related singleton visitor MonetaryFormats.

javax.money.spi: contains SPI interface and boot logic provided by JSR-354 to support different runtime environments and component loading mechanisms.

2.2. 2 module overview

The JSR-354 source code warehouse contains the following modules:

  • Jsr354 API: including the Java 8-based JSR 354 API described in this specification;
  • Jsr354 RI: contains Moneta reference implementation based on Java 8 language features;
  • Jsr354 TCK: contains the technical compatibility Kit (TCK). TCK is built using Java 8;
  • Javamoney parent: org The root "POM" project of all modules under javamoney. This includes the RI/TCK project, but not the jsr354 API (which is stand-alone).

2.2 core API

2.2.1 CurrencyUnit

2.2.1.1 CurrencyUnit data model

CurrencyUnit contains the attribute of the smallest unit of currency, as shown below:

public interface CurrencyUnit extends Comparable<CurrencyUnit>{
    String getCurrencyCode();
    int getNumericCode();
    int getDefaultFractionDigits();
    CurrencyContext getContext();
}

The method getCurrencyCode() returns a different currency code. The currency code based on ISO Currency specification is three digits by default. Other types of currency codes do not have this constraint.

The return value of the method getNumericCode() is optional. - 1 can be returned by default. The code of the ISO currency must match the value of the corresponding ISO code.

defaultFractionDigits defines the number of digits after the decimal point by default. CurrencyContext contains additional metadata information of currency units.

2.2. 1.2 method of obtaining CurrencyUnit

Get by currency code

CurrencyUnit currencyUnit = Monetary.getCurrency("USD");

Obtain by Region

CurrencyUnit currencyUnitChina = Monetary.getCurrency(Locale.CHINA);

Get by query criteria

CurrencyQuery cnyQuery =             CurrencyQueryBuilder.of().setCurrencyCodes("CNY").setCountries(Locale.CHINA).setNumericCodes(-1).build();
Collection<CurrencyUnit> cnyCurrencies = Monetary.getCurrencies(cnyQuery);

Obtain all currencyunits;

Collection<CurrencyUnit> allCurrencies = Monetary.getCurrencies();

2.2.1.3 CurrencyUnit data provider

We entered monetary Getcurrency series of methods. You can see that these methods are obtained by obtaining monetarycurrenciiessingletonspi Class implements the corresponding instance of the class, and then calls the instance to correspond to the getCurrency method.

public static CurrencyUnit getCurrency(String currencyCode, String... providers) {
    return Optional.ofNullable(MONETARY_CURRENCIES_SINGLETON_SPI()).orElseThrow(
        () -> new MonetaryException("No MonetaryCurrenciesSingletonSpi loaded, check your system setup."))
        .getCurrency(currencyCode, providers);
}

private static MonetaryCurrenciesSingletonSpi MONETARY_CURRENCIES_SINGLETON_SPI() {
        try {
            return Optional.ofNullable(Bootstrap
                    .getService(MonetaryCurrenciesSingletonSpi.class)).orElseGet(
                    DefaultMonetaryCurrenciesSingletonSpi::new);
        } catch (Exception e) {
            ......
            return new DefaultMonetaryCurrenciesSingletonSpi();
        }
    }

By default, there is only one interface monetarycurrenciiessingletonspi that implements defaultmonetarycurrenciiessingletonspi. The implementation method of obtaining the currency collection is: all CurrencyProviderSpi implementation classes obtain the CurrencyUnit collection union set.

public Set<CurrencyUnit> getCurrencies(CurrencyQuery query) {
    Set<CurrencyUnit> result = new HashSet<>();
    for (CurrencyProviderSpi spi : Bootstrap.getServices(CurrencyProviderSpi.class)) {
        try {
            result.addAll(spi.getCurrencies(query));
        } catch (Exception e) {
            ......
        }
    }
    return result;
}

Therefore, the data provider of CurrencyUnit is the relevant implementation class that implements CurrencyProviderSpi. There are two providers for the default implementation provided by Moneta, as shown in the figure;

(Figure 2-2 default implementation class diagram of currencyproviderspi)

JDKCurrencyProvider provides relevant mappings for currency types described in [ISO-4217] in JDK;

Configurablecurrencyunit provider provides support for changing CurrencyUnit dynamically. Methods: registerCurrencyUnit, removeCurrencyUnit, etc.

Therefore, if you need to extend CurrencyUnit accordingly, it is recommended to customize the construction extension according to the interface definition of the extension point CurrencyProviderSpi.

2.2.2 MonetaryAmount

2.2.2.1 MonetaryAmount data model

public interface MonetaryAmount extends CurrencySupplier, NumberSupplier, Comparable<MonetaryAmount>{

    //Get context data
    MonetaryContext getContext();

    //Query by criteria
    default <R> R query(MonetaryQuery<R> query){
        return query.queryFrom(this);
    }

    //Apply operations to create currency amount instances
    default MonetaryAmount with(MonetaryOperator operator){
        return operator.apply(this);
    }
    
    //Gets the factory that created a new instance of the currency amount
    MonetaryAmountFactory<? extends MonetaryAmount> getFactory();

    //Comparison method
    boolean isGreaterThan(MonetaryAmount amount);
    ......
    int signum();

    //Algorithmic functions and calculations
    MonetaryAmount add(MonetaryAmount amount);
    ......
    MonetaryAmount stripTrailingZeros();
}

Corresponding to moneyamount, three implementations are provided: FastMoney, Money and RoundedMoney.

(Figure 2-3 default implementation class diagram of monetaryamount)

FastMoney is a numeric representation optimized for performance. The amount of money it represents is an integer number. Money is internally based on Java math. BigDecimal to perform arithmetic operations, and the implementation can support arbitrary precision and scale. The implementation of RoundedMoney supports implicit rounding after each operation. We need to make reasonable choices according to our usage scenarios. If the digital function of FastMoney is sufficient to meet your use case, this type is recommended.

2.2. 2.2 create MonetaryAmount

According to the API definition, it can be created by accessing the MonetaryAmountFactory or directly through the factory method of the corresponding type. As follows;

FastMoney fm1 = Monetary.getAmountFactory(FastMoney.class).setCurrency("CNY").setNumber(144).create();
FastMoney fm2 = FastMoney.of(144, "CNY");

Money m1 = Monetary.getAmountFactory(Money.class).setCurrency("CNY").setNumber(144).create();
Money m2 = Money.of(144, "CNY");

Because Money is internally based on Java math. BigDecimal, so it also has the arithmetic accuracy and rounding ability of BigDecimal. By default, the internal instance of Money uses mathcontext Desimal64 initialization. And support specified methods;

Money money1 = Monetary.getAmountFactory(Money.class)
                              .setCurrency("CNY").setNumber(144)
                              .setContext(MonetaryContextBuilder.of().set(MathContext.DECIMAL128).build())
                              .create();
Money money2 = Money.of(144, "CNY", MonetaryContextBuilder.of().set(MathContext.DECIMAL128).build());

Money and FastMoney can also be converted to each other through the from method, as follows;

org.javamoney.moneta.Money.defaults.mathContext=DECIMAL128

Both precision and rounding mode can be specified;

org.javamoney.moneta.Money.defaults.precision=256
org.javamoney.moneta.Money.defaults.roundingMode=HALF_EVEN

Money and FastMoney can also be converted to each other through the from method, as follows;

FastMoney fastMoney = FastMoney.of(144, "CNY");

Money money = Money.from(fastMoney);
fastMoney = FastMoney.from(money);

2.2. 2.3 extension of monetaryamount

Although the three implementations of moneyamount provided by Moneta: FastMoney, Money and RoundedMoney can meet the needs of most scenarios. JSR-354 provides more implementation possibilities for the extension points reserved for MonetaryAmount.

Let's follow up with the static method monetary Getamountfactory (classamounttype) gets the MonetaryAmountFactory to create the MonetaryAmount instance;

public static <T extends MonetaryAmount> MonetaryAmountFactory<T> getAmountFactory(Class<T> amountType) {
    MonetaryAmountsSingletonSpi spi = Optional.ofNullable(monetaryAmountsSingletonSpi())
        .orElseThrow(() -> new MonetaryException("No MonetaryAmountsSingletonSpi loaded."));
    MonetaryAmountFactory<T> factory = spi.getAmountFactory(amountType);
    return Optional.ofNullable(factory).orElseThrow(
        () -> new MonetaryException("No AmountFactory available for type: " + amountType.getName()));
}

private static MonetaryAmountsSingletonSpi monetaryAmountsSingletonSpi() {
    try {
        return Bootstrap.getService(MonetaryAmountsSingletonSpi.class);
    } catch (Exception e) {
        ......
        return null;
    }
}

As shown in the above code, MonetaryAmountFactory needs to be obtained through the implementation class of the MonetaryAmountsSingletonSpi extension point and the method getAmountFactory.

In the implementation of Moneta, the only implementation class of MonetaryAmountsSingletonSpi is DefaultMonetaryAmountsSingletonSpi, and the corresponding method to obtain MonetaryAmountFactory is;

public class DefaultMonetaryAmountsSingletonSpi implements MonetaryAmountsSingletonSpi {

    private final Map<Class<? extends MonetaryAmount>, MonetaryAmountFactoryProviderSpi<?>> factories =
            new ConcurrentHashMap<>();

    public DefaultMonetaryAmountsSingletonSpi() {
        for (MonetaryAmountFactoryProviderSpi<?> f : Bootstrap.getServices(MonetaryAmountFactoryProviderSpi.class)) {
            factories.putIfAbsent(f.getAmountType(), f);
        }
    }

    @Override
    public <T extends MonetaryAmount> MonetaryAmountFactory<T> getAmountFactory(Class<T> amountType) {
        MonetaryAmountFactoryProviderSpi<T> f = MonetaryAmountFactoryProviderSpi.class.cast(factories.get(amountType));
        if (Objects.nonNull(f)) {
            return f.createMonetaryAmountFactory();
        }
        throw new MonetaryException("No matching MonetaryAmountFactory found, type=" + amountType.getName());
    }
    
    ......
}

Finally, it can be found that the MonetaryAmountFactory is obtained through the extension point MonetaryAmountFactoryProviderSpi by calling createMonetaryAmountFactory.

Therefore, if you want to extend and implement a new type of MonetaryAmount, you need to provide at least the implementation of the extension point MonetaryAmountFactoryProviderSpi, the implementation of the corresponding type of abstractmountfactory, and the maintenance of the relationship between them.

The implementation of the default MonetaryAmountFactoryProviderSpi and the corresponding abstractmountfactory are shown in the following figure;

(Figure 2-4 default implementation class diagram of monetaryamountfactoryproviderspi)

(Figure 2-5 AbstractAmountFactory default implementation class diagram)

2.2. 3. Currency amount calculation

It can be seen from the interface definition of MonetaryAmount that it provides common arithmetic operations (addition, subtraction, multiplication, division, modulo, etc.) calculation methods. At the same time, it defines the with method to support the expansion of MonetaryOperator based operations. MonetaryOperators class defines some common implementations of MonetaryOperators:

  • 1) ReciprocalOperator is used to operate the countdown;
  • 2) The PermilOperator is used to obtain the thousandth scale value;
  • 3) PercentOperator is used to obtain the percentage instance value;
  • 4)ExtractorMinorPartOperator is used to obtain the decimal part;
  • 5)ExtractorMajorPartOperator is used to obtain the integer part;
  • 6)RoundingMonetaryAmountOperator is used for rounding operation;

The interfaces that inherit MonetaryOperator include CurrencyConversion and MonetaryRounding. CurrencyConversion is mainly related to currency exchange, which will be introduced in the next section. MonetaryRounding is about rounding. The specific usage is as follows:;

MonetaryRounding rounding = Monetary.getRounding(
    RoundingQueryBuilder.of().setScale(4).set(RoundingMode.HALF_UP).build());
Money money = Money.of(144.44445555,"CNY");
Money roundedAmount = money.with(rounding);  
# roundedAmount. The value of getnumber() is 144.4445

You can also use the default rounding method and specify CurrencyUnit. The scale corresponding to the result is CurrencyUnit Getdefaultfractiondigits(), for example;

MonetaryRounding rounding = Monetary.getDefaultRounding();
Money money = Money.of(144.44445555,"CNY");
MonetaryAmount roundedAmount = money.with(rounding);
#roundedAmount. The scale corresponding to getnumber () is money getCurrency(). getDefaultFractionDigits()

CurrencyUnit currency = Monetary.getCurrency("CNY");
MonetaryRounding rounding = Monetary.getRounding(currency);
Money money = Money.of(144.44445555,"CNY");
MonetaryAmount roundedAmount = money.with(rounding);
#roundedAmount. The scale corresponding to getnumber () is current getDefaultFractionDigits()

Generally, the rounding operation is carried out by rounding 1. For some types of currencies, the minimum unit is not 1, for example, the minimum unit of Swiss franc is 5. In this case, you can set the property cashrouting to true and perform corresponding operations;

CurrencyUnit currency = Monetary.getCurrency("CHF");
MonetaryRounding rounding = Monetary.getRounding(
    RoundingQueryBuilder.of().setCurrency(currency).set("cashRounding", true).build());
Money money = Money.of(144.42555555,"CHF");
Money roundedAmount = money.with(rounding);
# roundedAmount. The value of getnumber() is 144.45

Through the acquisition method of MonetaryRounding, we can understand that it is completed through the extended implementation class of MonetaryRoundingsSingletonSpi by calling the corresponding getRounding method. Query by criteria is as follows:;

public static MonetaryRounding getRounding(RoundingQuery roundingQuery) {
    return Optional.ofNullable(monetaryRoundingsSingletonSpi()).orElseThrow(
        () -> new MonetaryException("No MonetaryRoundingsSpi loaded, query functionality is not available."))
        .getRounding(roundingQuery);
}

private static MonetaryRoundingsSingletonSpi monetaryRoundingsSingletonSpi() {
    try {
        return Optional.ofNullable(Bootstrap
                                   .getService(MonetaryRoundingsSingletonSpi.class))
            .orElseGet(DefaultMonetaryRoundingsSingletonSpi::new);
    } catch (Exception e) {
        ......
        return new DefaultMonetaryRoundingsSingletonSpi();
    }
}

In the default implementation, the only implementation class of MonetaryRoundingsSingletonSpi is DefaultMonetaryRoundingsSingletonSpi, which obtains MonetaryRounding in the following ways;

@Override
public Collection<MonetaryRounding> getRoundings(RoundingQuery query) {
   ......
    for (String providerName : providerNames) {
        Bootstrap.getServices(RoundingProviderSpi.class).stream()
            .filter(prov -> providerName.equals(prov.getProviderName())).forEach(prov -> {
            try {
                MonetaryRounding r = prov.getRounding(query);
                if (r != null) {
                    result.add(r);
                }
            } catch (Exception e) {
                ......
            }
        });
    }
    return result;
}

According to the above code, MonetaryRounding is mainly obtained from the getRounding method of the RoundingProviderSpi extension point implementation class. The DefaultRoundingProvider in Moneta, the default implementation of JSR-354, provides related implementations. If you need to implement a custom routing policy, follow the extension points defined by the routingproviderspi.

2.3 currency exchange

2.3. 1. Instructions for currency exchange

As mentioned in the previous section, MonetaryOperator also has a kind of currency exchange related operations. The following examples show the commonly used methods of currency exchange;

Number moneyNumber = 144;
CurrencyUnit currencyUnit = Monetary.getCurrency("CNY");
Money money = Money.of(moneyNumber,currencyUnit);
CurrencyConversion vfCurrencyConversion = MonetaryConversions.getConversion("ECB");
Money conversMoney = money.with(vfCurrencyConversion);

You can also obtain the ExchangeRateProvider first and then CurrencyConversion for corresponding currency exchange;

Number moneyNumber = 144;
CurrencyUnit currencyUnit = Monetary.getCurrency("CNY");
Money money = Money.of(moneyNumber,currencyUnit);
ExchangeRateProvider exchangeRateProvider = MonetaryConversions.getExchangeRateProvider("default");
CurrencyConversion vfCurrencyConversion = exchangeRateProvider.getCurrencyConversion("ECB");
Money conversMoney = money.with(vfCurrencyConversion);

2.3. 2 currency exchange extension

CurrencyConversion uses the static method monetaryconversions getConversion to get. Method is obtained by calling getConversion according to the implementation of monetaryconversionsssingletonspi.

The method getConversion is implemented by obtaining the corresponding ExchangeRateProvider and calling getCurrencyConversion;

public static CurrencyConversion getConversion(CurrencyUnit termCurrency, String... providers){
    ......
    if(providers.length == 0){
        return getMonetaryConversionsSpi().getConversion(
            ConversionQueryBuilder.of().setTermCurrency(termCurrency).setProviderNames(getDefaultConversionProviderChain())
            .build());
    }
    return getMonetaryConversionsSpi().getConversion(
        ConversionQueryBuilder.of().setTermCurrency(termCurrency).setProviderNames(providers).build());
}

default CurrencyConversion getConversion(ConversionQuery conversionQuery) {
    return getExchangeRateProvider(conversionQuery).getCurrencyConversion(
        Objects.requireNonNull(conversionQuery.getCurrency(), "Terminating Currency is required.")
    );
}

private static MonetaryConversionsSingletonSpi getMonetaryConversionsSpi() {
    return Optional.ofNullable(Bootstrap.getService(MonetaryConversionsSingletonSpi.class))
        .orElseThrow(() -> new MonetaryException("No MonetaryConversionsSingletonSpi " +
                                                 "loaded, " +
                                                 "query functionality is not " +
                                                 "available."));
}

In the implementation of Moneta, MonetaryConversionsSingletonSpi only has a unique implementation class DefaultMonetaryConversionsSingletonSpi.

The acquisition of ExchangeRateProvider is as follows, which depends on the extension implementation of ExchangeRateProvider;

public DefaultMonetaryConversionsSingletonSpi() {
    this.reload();
}

public void reload() {
    Map<String, ExchangeRateProvider> newProviders = new ConcurrentHashMap();
    Iterator var2 = Bootstrap.getServices(ExchangeRateProvider.class).iterator();

    while(var2.hasNext()) {
        ExchangeRateProvider prov = (ExchangeRateProvider)var2.next();
        newProviders.put(prov.getContext().getProviderName(), prov);
    }

    this.conversionProviders = newProviders;
}

public ExchangeRateProvider getExchangeRateProvider(ConversionQuery conversionQuery) {
    ......
    List<ExchangeRateProvider> provInstances = new ArrayList();
    ......

    while(......) {
       ......
        ExchangeRateProvider prov = (ExchangeRateProvider)Optional.ofNullable((ExchangeRateProvider)this.conversionProviders.get(provName)).orElseThrow(() -> {
            return new MonetaryException("Unsupported conversion/rate provider: " + provName);
        });
        provInstances.add(prov);
    }

    ......
        return (ExchangeRateProvider)(provInstances.size() == 1 ? (ExchangeRateProvider)provInstances.get(0) : new CompoundRateProvider(provInstances));
    }
}

The default implementations provided by ExchangeRateProvider are:

  • CompoundRateProvider
  • IdentityRateProvider

(Figure 2-6 default implementation class diagram of exchangerateprovider)

Therefore, the proposed way to expand currency exchange capability is to implement the ExchangeRateProvider and load it through the SPI mechanism.

2.4 formatting

2.4. 1 format instructions

Formatting mainly includes two parts: converting an object instance into a string conforming to the format; Converts a string in the specified format to an object instance. Perform the corresponding conversion through the format and parse corresponding to the MonetaryAmountFormat instance. As shown in the following code;

MonetaryAmountFormat format = MonetaryFormats.getAmountFormat(Locale.CHINESE);
MonetaryAmount monetaryAmount = Money.of(144144.44,"VZU");
String formattedString = format.format(monetaryAmount);

MonetaryAmountFormat format = MonetaryFormats.getAmountFormat(Locale.CHINESE);
String formattedString = "VZU 144,144.44";
MonetaryAmount monetaryAmount = format.parse(formattedString);

2.4. 2 format extension

The key point of formatting is the construction of MonetaryAmountFormat. MonetaryAmountFormat is mainly created and obtained by monetaryformats getAmountFormat. Take a look at the relevant source code;

public static MonetaryAmountFormat getAmountFormat(AmountFormatQuery formatQuery) {
    return Optional.ofNullable(getMonetaryFormatsSpi()).orElseThrow(() -> new MonetaryException(
        "No MonetaryFormatsSingletonSpi " + "loaded, query functionality is not available."))
        .getAmountFormat(formatQuery);
}

private static MonetaryFormatsSingletonSpi getMonetaryFormatsSpi() {
    return loadMonetaryFormatsSingletonSpi();
}

private static MonetaryFormatsSingletonSpi loadMonetaryFormatsSingletonSpi() {
    try {
        return Optional.ofNullable(Bootstrap.getService(MonetaryFormatsSingletonSpi.class))
            .orElseGet(DefaultMonetaryFormatsSingletonSpi::new);
    } catch (Exception e) {
        ......
        return new DefaultMonetaryFormatsSingletonSpi();
    }
}

The relevant code shows that the acquisition of MonetaryAmountFormat depends on the implementation of MonetaryFormatsSingletonSpi and calls the getAmountFormat method accordingly.

The default implementation of MonetaryFormatsSingletonSpi is DefaultMonetaryFormatsSingletonSpi, and the corresponding acquisition method is as follows;

public Collection<MonetaryAmountFormat> getAmountFormats(AmountFormatQuery formatQuery) {
    Collection<MonetaryAmountFormat> result = new ArrayList<>();
    for (MonetaryAmountFormatProviderSpi spi : Bootstrap.getServices(MonetaryAmountFormatProviderSpi.class)) {
        Collection<MonetaryAmountFormat> formats = spi.getAmountFormats(formatQuery);
        if (Objects.nonNull(formats)) {
            result.addAll(formats);
        }
    }
    return result;
}

It can be seen that it ultimately depends on the relevant implementation of MonetaryAmountFormatProviderSpi and is provided as an extension point. The default extension implementation is DefaultAmountFormatProviderSpi.

If we need to extend and register our own format processing method, we recommend extending MonetaryAmountFormatProviderSpi.

2.5 SPI

The service extension points provided by JSR-354 include:;

(Figure 2-7 class diagram of service extension point)

1) Handle CurrencyProviderSpi and monetarycurrenciesssingletonspi related to currency type;

2) Handle MonetaryConversionsSingletonSpi related to currency exchange;

3) Handle MonetaryAmountFactoryProviderSpi and MonetaryAmountsSingletonSpi related to currency amount;

4) Process rounding related RoundingProviderSpi and MonetaryRoundingsSingletonSpi;

5) Handle MonetaryAmountFormatProviderSpi and MonetaryFormatsSingletonSpi related to formatting;

6) Service providers related to service discovery;

Except for ServiceProvider, other extension points are described above. The JSR-354 specification provides the default implementation DefaultServiceProvider. Use the ServiceLoader provided by JDK to realize service-oriented registration and discovery, and complete the decoupling of service provision and use. The order of loading services is the order sorted by class name;

private <T> List<T> loadServices(final Class<T> serviceType) {
    List<T> services = new ArrayList<>();
    try {
        for (T t : ServiceLoader.load(serviceType)) {
            services.add(t);
        }
        services.sort(Comparator.comparing(o -> o.getClass().getSimpleName()));
        @SuppressWarnings("unchecked")
        final List<T> previousServices = (List<T>) servicesLoaded.putIfAbsent(serviceType, (List<Object>) services);
        return Collections.unmodifiableList(previousServices != null ? previousServices : services);
    } catch (Exception e) {
        ......
        return services;
    }
}

The implementation of Moneta also provides an implementation PriorityAwareServiceProvider, which can specify the Priority of service interface implementation according to the annotation @ Priority.

private <T> List<T> loadServices(final Class<T> serviceType) {
    List<T> services = new ArrayList<>();
    try {
        for (T t : ServiceLoader.load(serviceType, Monetary.class.getClassLoader())) {
            services.add(t);
        }
        services.sort(PriorityAwareServiceProvider::compareServices);
        @SuppressWarnings("unchecked")
        final List<T> previousServices = (List<T>) servicesLoaded.putIfAbsent(serviceType, (List<Object>) services);
        return Collections.unmodifiableList(previousServices != null ? previousServices : services);
    } catch (Exception e) {
        ......
        services.sort(PriorityAwareServiceProvider::compareServices);
        return services;
    }
}

public static int compareServices(Object o1, Object o2) {
    int prio1 = 0;
    int prio2 = 0;
    Priority prio1Annot = o1.getClass().getAnnotation(Priority.class);
    if (prio1Annot != null) {
        prio1 = prio1Annot.value();
    }
    Priority prio2Annot = o2.getClass().getAnnotation(Priority.class);
    if (prio2Annot != null) {
        prio2 = prio2Annot.value();
    }
    if (prio1 < prio2) {
        return 1;
    }
    if (prio2 < prio1) {
        return -1;
    }
    return o2.getClass().getSimpleName().compareTo(o1.getClass().getSimpleName());
}

2.6 data loading mechanism

For some dynamic data, such as dynamic expansion of currency type and change of currency exchange rate. Moneta provides a set of data loading mechanism to support the corresponding functions. Four loading and updating strategies are provided by default: obtain from the fallback URL without obtaining remote data; When starting, it is obtained remotely and loaded only once; Load from remote when using for the first time; Get updates regularly. Different methods of loading data are used for different strategies. Corresponding to the processing methods corresponding to NEVER, ONSTARTUP, LAZY and SCHEDULED in the following codes respectively;

public void registerData(LoadDataInformation loadDataInformation) {
    ......

    if(loadDataInformation.isStartRemote()) {
        defaultLoaderServiceFacade.loadDataRemote(loadDataInformation.getResourceId(), resources);
    }
    switch (loadDataInformation.getUpdatePolicy()) {
        case NEVER:
            loadDataLocal(loadDataInformation.getResourceId());
            break;
        case ONSTARTUP:
            loadDataAsync(loadDataInformation.getResourceId());
            break;
        case SCHEDULED:
            defaultLoaderServiceFacade.scheduledData(resource);
            break;
        case LAZY:
        default:
            break;
    }
}

The loadDataLocal method loads the data by triggering the listener. The listener actually calls the newDataLoaded method.

public boolean loadDataLocal(String resourceId){
    return loadDataLocalLoaderService.execute(resourceId);
}

public boolean execute(String resourceId) {
    LoadableResource load = this.resources.get(resourceId);
    if (Objects.nonNull(load)) {
        try {
            if (load.loadFallback()) {
                listener.trigger(resourceId, load);
                return true;
            }
        } catch (Exception e) {
            ......
        }
    } else {
        throw new IllegalArgumentException("No such resource: " + resourceId);
    }
    return false;
}

public void trigger(String dataId, DataStreamFactory dataStreamFactory) {
    List<LoaderListener> listeners = getListeners("");
    synchronized (listeners) {
        for (LoaderListener ll : listeners) {
            ......
            ll.newDataLoaded(dataId, dataStreamFactory.getDataStream());
            ......
        }
    }
    if (!(Objects.isNull(dataId) || dataId.isEmpty())) {
        listeners = getListeners(dataId);
        synchronized (listeners) {
            for (LoaderListener ll : listeners) {
                ......
                ll.newDataLoaded(dataId, dataStreamFactory.getDataStream());
                ......
            }
        }
    }
}

loadDataAsync is similar to loadDataLocal, but it is placed in another thread to execute asynchronously:

public Future<Boolean> loadDataAsync(final String resourceId) {
    return executors.submit(() -> defaultLoaderServiceFacade.loadData(resourceId, resources));
}

loadDataRemote loads data by calling loadRemote of LoadableResource.

public boolean loadDataRemote(String resourceId, Map<String, LoadableResource> resources){
   return loadRemoteDataLoaderService.execute(resourceId, resources);
}

public boolean execute(String resourceId,Map<String, LoadableResource> resources) {

    LoadableResource load = resources.get(resourceId);
    if (Objects.nonNull(load)) {
        try {
            load.readCache();
            listener.trigger(resourceId, load);
            load.loadRemote();
            listener.trigger(resourceId, load);
            ......
            return true;
        } catch (Exception e) {
            ......
        }
    } else {
        throw new IllegalArgumentException("No such resource: " + resourceId);
    }
    return false;
}

LoadableResource loads data in the following ways:;

protected boolean load(URI itemToLoad, boolean fallbackLoad) {
    InputStream is = null;
    ByteArrayOutputStream stream = new ByteArrayOutputStream();
    try{
        URLConnection conn;
        String proxyPort = this.properties.get("proxy.port");
        String proxyHost = this.properties.get("proxy.host");
        String proxyType = this.properties.get("proxy.type");
        if(proxyType!=null){
            Proxy proxy = new Proxy(Proxy.Type.valueOf(proxyType.toUpperCase()),
                                    InetSocketAddress.createUnresolved(proxyHost, Integer.parseInt(proxyPort)));
            conn = itemToLoad.toURL().openConnection(proxy);
        }else{
            conn = itemToLoad.toURL().openConnection();
        }
        ......
            
        byte[] data = new byte[4096];
        is = conn.getInputStream();
        int read = is.read(data);
        while (read > 0) {
            stream.write(data, 0, read);
            read = is.read(data);
        }
        setData(stream.toByteArray());
        ......
        return true;
    } catch (Exception e) {
        ......
    } finally {
        ......
    }
    return false;
}

The timing execution scheme is similar to the above. The Timer provided by JDK is used as the Timer, as shown below;

public void execute(final LoadableResource load) {
    Objects.requireNonNull(load);
    Map<String, String> props = load.getProperties();
    if (Objects.nonNull(props)) {
        String value = props.get("period");
        long periodMS = parseDuration(value);
        value = props.get("delay");
        long delayMS = parseDuration(value);
        if (periodMS > 0) {
            timer.scheduleAtFixedRate(createTimerTask(load), delayMS, periodMS);
        } else {
            value = props.get("at");
            if (Objects.nonNull(value)) {
                List<GregorianCalendar> dates = parseDates(value);
                dates.forEach(date -> timer.schedule(createTimerTask(load), date.getTime(), 3_600_000 * 24 /* daily */));
            }
        }
    }
}

3, Case

3.1 currency type extension

In the current business scenario, it is necessary to support multiple currency types such as v-diamond, incentive gold and v-bean, and the types of currency types will increase with the development of business. We need to extend the currency type and also need the dynamic loading mechanism of currency type data. Expand as follows:

1)javamoney. Add the following configuration to properties;

{-1}load.VFCurrencyProvider.type=NEVER
{-1}load.VFCurrencyProvider.period=23:00
{-1}load.VFCurrencyProvider.resource=/java-money/defaults/VFC/currency.json
{-1}load.VFCurrencyProvider.urls=http://localhost:8080/feeds/data/currency
{-1}load.VFCurrencyProvider.startRemote=false

2) Add javax.xml file under META-INF.services path money. spi. Currencyproviderspi, and add the following contents to the file;

com.vivo.finance.javamoney.spi.VFCurrencyProvider

3)java-money. defaults. Add the file currency. Under the VFC path JSON, the contents of the file are as follows;

[{
  "currencyCode": "VZU",
  "defaultFractionDigits": 2,
  "numericCode": 1001
},{
  "currencyCode": "GLJ",
  "defaultFractionDigits": 2,
  "numericCode": 1002
},{
  "currencyCode": "VBE",
  "defaultFractionDigits": 2,
  "numericCode": 1003
},{
  "currencyCode": "VDO",
  "defaultFractionDigits": 2,
  "numericCode": 1004
},{
  "currencyCode": "VJP",
  "defaultFractionDigits": 2,
  "numericCode": 1005
}
]

4) Add class VFCurrencyProvider implementation

CurrencyProviderSpi and loaderservice Loaderlistener, which is used to extend the currency type and implement the data loading of the extended currency type. The data analysis class VFCurrencyReadingHandler and data model class vfcurrencyare omitted. The corresponding implementation Association class diagram is;

(Figure 2-8 main association implementation class diagram of currency type extension)

The key implementation is data loading, and the code is as follows;

@Override
public void newDataLoaded(String resourceId, InputStream is) {
    final int oldSize = CURRENCY_UNITS.size();
    try {
        Map<String, CurrencyUnit> newCurrencyUnits = new HashMap<>(16);
        Map<Integer, CurrencyUnit> newCurrencyUnitsByNumricCode = new ConcurrentHashMap<>();
        final VFCurrencyReadingHandler parser = new VFCurrencyReadingHandler(newCurrencyUnits,newCurrencyUnitsByNumricCode);
        parser.parse(is);

        CURRENCY_UNITS.clear();
        CURRENCY_UNITS_BY_NUMERIC_CODE.clear();
        CURRENCY_UNITS.putAll(newCurrencyUnits);
        CURRENCY_UNITS_BY_NUMERIC_CODE.putAll(newCurrencyUnitsByNumricCode);

        int newSize = CURRENCY_UNITS.size();
        loadState = "Loaded " + resourceId + " currency:" + (newSize - oldSize);
        LOG.info(loadState);
    } catch (Exception e) {
        loadState = "Last Error during data load: " + e.getMessage();
        LOG.log(Level.FINEST, "Error during data load.", e);
    } finally{
        loadLock.countDown();
    }
}

3.2 currency exchange expansion

With the increase of currency types, the corresponding currency exchange scenarios in recharge and other scenarios will also increase. We need to expand currency exchange and dynamic loading mechanism of data related to currency exchange rate. If the currency expansion method is similar, follow the steps below:

javamoney. Add the following configuration to properties;

{-1}load.VFCExchangeRateProvider.type=NEVER
{-1}load.VFCExchangeRateProvider.period=23:00
{-1}load.VFCExchangeRateProvider.resource=/java-money/defaults/VFC/currencyExchangeRate.json
{-1}load.VFCExchangeRateProvider.urls=http://localhost:8080/feeds/data/currencyExchangeRate
{-1}load.VFCExchangeRateProvider.startRemote=false

Add javax.xml file under META-INF.services path money. convert. Exchange rateprovider, and add the following contents to the file;

com.vivo.finance.javamoney.spi.VFCExchangeRateProvider

java-money. defaults. Add the file currencyexchangerate. Under the VFC path JSON, the contents of the file are as follows;

[{
  "date": "2021-05-13",
  "currency": "VZU",
  "factor": "1.0000"
},{
  "date": "2021-05-13",
  "currency": "GLJ",
  "factor": "1.0000"
},{
  "date": "2021-05-13",
  "currency": "VBE",
  "factor": "1E+2"
},{
  "date": "2021-05-13",
  "currency": "VDO",
  "factor": "0.1666"
},{
  "date": "2021-05-13",
  "currency": "VJP",
  "factor": "23.4400"
}
]

Add class VFCExchangeRateProvider

Inherit AbstractRateProvider and implement loaderservice LoaderListener. The corresponding implementation Association class diagram is;

(Figure 2-9 main association implementation class diagram of currency amount extension)

3.3 use cases

Suppose 1 RMB can be exchanged for 100v beans and 1 RMB can be exchanged for 1v diamonds. In the current scenario, the user has paid 1v diamonds for 100v beans. It is necessary to verify whether the payment amount and recharge amount are legal. The following methods can be used for verification;

Number rechargeNumber = 100;
CurrencyUnit currencyUnit = Monetary.getCurrency("VBE");
Money rechargeMoney = Money.of(rechargeNumber,currencyUnit);

Number payNumber = 1;
CurrencyUnit payCurrencyUnit = Monetary.getCurrency("VZU");
Money payMoney = Money.of(payNumber,payCurrencyUnit);

CurrencyConversion vfCurrencyConversion = MonetaryConversions.getConversion("VBE");
Money conversMoney = payMoney.with(vfCurrencyConversion);
Assert.assertEquals(conversMoney,rechargeMoney);

4, Summary

JavaMoney provides great convenience for using money in financial scenarios. It can support the demands of rich business scenarios on currency type and currency amount. In particular, Monetary, MonetaryConversions and MonetaryFormats, as the entrances of currency base capabilities, currency exchange, currency formatting and other capabilities, provide convenience for related operations. At the same time, it also provides a good extension mechanism to facilitate relevant transformation to meet their own business scenarios.

Starting from the use scenario, this paper leads to the main problems that JSR 354 needs to solve. By analyzing the package and module structure of related projects, it shows that JSR 354 and its implementation are divided to solve these problems. Then it explains how to support and use the corresponding currency extension, amount calculation, currency conversion, formatting and other capabilities from the relevant API s. It also introduces the relevant expansion methods, opinions and suggestions. Then it summarizes the relevant SPI and the corresponding data loading mechanism. Finally, a case is given to illustrate how to extend and apply the corresponding implementation for a specific scenario.

Author: vivo Internet server team - Hou Xiaobi

Keywords: Java server api

Added by hawkenterprises on Tue, 14 Dec 2021 06:26:05 +0200