4

I. Overview

1.1 Current status

The current class used to express currency in the JDK is java.util.Currency , this class can only represent the currency type described [ISO-4217] It has no numerical value associated with it, nor can it describe some currencies outside the specification. There is no relevant support for currency calculation, currency conversion, and currency formatting, and even the standard types that can represent currency amounts are not provided with relevant instructions. JSR-354 defines a set of standard APIs to solve these related problems.

1.2 Specification purpose

The main goals of JSR-354 are:

  • Provides the possibility of currency expansion and supports the demands of rich business scenarios for currency types and currency amounts;
  • Provide API for currency amount calculation;
  • Provide support and expansion of currency exchange rates;
  • Provide support and extensions for the analysis and formatting of currencies and currency amounts.

1.3 Usage scenarios

online store

The unit price of the goods in the mall, the total price that needs to be calculated with the quantity of the goods after the goods are added to the shopping cart. The currency exchange involved with the change of settlement currency type after the payment method is switched in the mall. When the user places an order, it involves the calculation of the payment amount, the calculation of taxes and so on.

Financial transaction website

On a financial trading website, customers can create virtual investment portfolios at will. According to the created investment portfolio, combined with historical data to display the calculated historical, current and expected returns.

Virtual world and gaming website

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

Banking and Finance Applications

Banks and other financial institutions must establish currency model information on exchange rates, interest rates, stock quotes, current and historical currencies, etc. Usually such a company's internal system also has additional information represented by financial data, such as historical currencies, exchange rates, and risk analysis. Therefore, currencies and exchange rates must be historically significant and regional, and their validity period must be defined.

Two, JavaMoney analysis

2.1 Package and project structure

2.1.1 Package overview

JSR-354 defines 4 related packages:

(Figure 2-1 Package structure diagram)

javax.money contains the main components such as:

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

javax.money.convert contains currency conversion related components such as:

  • ExchangeRate;
  • ExchangeRateProvider;
  • CurrencyConversion ;
  • The related singleton visitor MonetaryConversions.

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

  • MonetaryAmountFormat;
  • AmountFormatContext;
  • The related singleton visitor MonetaryFormats.

javax.money.spi : Contains the 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 repository contains the following modules:

  • jsr354-api: Contains the JSR 354 API based on Java 8 described in this specification;
  • jsr354-ri: Contains the Moneta reference implementation based on Java 8 language features;
  • jsr354-tck: Contains Technology Compatibility Kit (TCK). TCK is built using Java 8;
  • javamoney-parent: It is the root "POM" project of all modules under org.javamoney. This includes the RI/TCK project, but does not include jsr354-api (it is independent).

2.2 Core API

2.2.1 CurrencyUnit

2.2.1.1 CurrencyUnit data model

CurrencyUnit contains the properties 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 the ISO Currency specification defaults to three digits, and other types of currency codes do not have this restriction.

The return value of the method getNumericCode() is optional. It can return -1 by default. The ISO currency code 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 for currency units.

2.2.1.2 How to get CurrencyUnit

get money according to the coding

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

according to the region

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

according to the query conditions

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

Get all CurrencyUnit;

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

We enter the Monetary.getCurrency series of methods, and we can see that these methods are implemented by obtaining the instance corresponding to the MonetaryCurrenciesSingletonSpi.class implementation class, and then calling the corresponding getCurrency method of the instance.

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();
        }
    }

The interface MonetaryCurrenciesSingletonSpi has only one implementation DefaultMonetaryCurrenciesSingletonSpi by default. The way it obtains the currency collection is: all CurrencyProviderSpi implementation classes obtain the CurrencyUnit collection and take the union.

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. The default implementation provided by Moneta has two providers, as shown in the figure;

(Figure 2-2 CurrencyProviderSpi default implementation class diagram)

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

ConfigurableCurrencyUnitProvider provides support for dynamically changing CurrencyUnit. The methods are: registerCurrencyUnit, removeCurrencyUnit, etc.

Therefore, if you need to expand the CurrencyUnit accordingly, it is recommended to expand the custom structure 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>{

    //获取上下文数据
    MonetaryContext getContext();

    //按条件查询
    default <R> R query(MonetaryQuery<R> query){
        return query.queryFrom(this);
    }

    //应用操作去创建货币金额实例
    default MonetaryAmount with(MonetaryOperator operator){
        return operator.apply(this);
    }
    
    //获取创建货币金额新实例的工厂
    MonetaryAmountFactory<? extends MonetaryAmount> getFactory();

    //比较方法
    boolean isGreaterThan(MonetaryAmount amount);
    ......
    int signum();

    //算法函数和计算
    MonetaryAmount add(MonetaryAmount amount);
    ......
    MonetaryAmount stripTrailingZeros();
}

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

(Figure 2-3 MonetaryAmount default implementation class diagram)

FastMoney is a digital representation optimized for performance, and the amount of currency it represents is an integer type number. Money internally performs arithmetic operations based on java.math.BigDecimal. This implementation can support arbitrary precision and scale. The implementation of RoundedMoney supports implicit rounding after each operation. We need to make a reasonable choice according to our usage scenarios. If FastMoney's digital capabilities are sufficient for your use case, this type is recommended.

2.2.2.2 Create MonetaryAmount

According to the definition of the API, it can be created by accessing MonetaryAmountFactory, or it can be created directly by the corresponding type of factory method. 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");

Since Money is internally based on java.math.BigDecimal, it also has the arithmetic precision and rounding capabilities of BigDecimal. By default, the internal instance of Money is initialized with MathContext.DECIMAL64. And support the specified method;

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, the method is as follows;

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

At the same time, the 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, the method is 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 have been able to meet the needs of most scenarios. JSR-354 provides more implementation possibilities for the extension points reserved by MonetaryAmount.

Let's follow up the way to create an instance of MonetaryAmount by obtaining MonetaryAmountFactory through the static method Monetary.getAmountFactory(ClassamountType);

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, the MonetaryAmountsSingletonSpi extension point needs to be implemented through the method getAmountFactory to obtain the MonetaryAmountFactory.

In Moneta's implementation, the only implementation class of MonetaryAmountsSingletonSpi is DefaultMonetaryAmountsSingletonSpi, and the corresponding method for obtaining 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 acquisition of MonetaryAmountFactory is generated by calling createMonetaryAmountFactory through the extension point MonetaryAmountFactoryProviderSpi.

Therefore, in order to extend the implementation of the new type of MonetaryAmount, at least the implementation of the extension point MonetaryAmountFactoryProviderSpi, the implementation of the corresponding type of AbstractAmountFactory and the maintenance of the relationship must be provided.

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

(Figure 2-4 MonetaryAmountFactoryProviderSpi default implementation class diagram)

(Figure 2-5 AbstractAmountFactory default implementation class diagram)

2.2.3 Currency amount calculation related

From the interface definition of MonetaryAmount, it can be seen that it provides common arithmetic operations (addition, subtraction, multiplication, division, modulus, etc.) calculation methods. At the same time, the with method is defined to support extensions based on MonetaryOperator operations. Some commonly used MonetaryOperator implementations are defined in the MonetaryOperators class:

  • 1) ReciprocalOperator is used to operate the reciprocal;
  • 2) PermilOperator is used to obtain the value of per thousand scale;
  • 3) PercentOperator is used to obtain the percentage case value;
  • 4) ExtractorMinorPartOperator is used to obtain the fractional part;
  • 5) ExtractorMajorPartOperator is used to obtain the integer part;
  • 6) RoundingMonetaryAmountOperator is used to perform rounding operations;

At the same time, the interfaces that inherit MonetaryOperator are CurrencyConversion and MonetaryRounding. Among them, CurrencyConversion is mainly related to currency exchange, which will be introduced in the next section. MonetaryRounding is about rounding operations, 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.getNumber()的值为:144.4445

You can also use the default rounding method and specify the CurrencyUnit method, and the scale corresponding to the result is the value of currencyUnit.getDefaultFractionDigits(), such as;

MonetaryRounding rounding = Monetary.getDefaultRounding();
Money money = Money.of(144.44445555,"CNY");
MonetaryAmount roundedAmount = money.with(rounding);
#roundedAmount.getNumber()对应的scale为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.getNumber()对应的scale为currency.getDefaultFractionDigits()

Under normal circumstances, the rounding operation is carried out by bitwise 1, for some types of currencies, the smallest unit is not 1, for example, the smallest unit of Swiss franc is 5. In this case, you can pass the attribute cashRounding 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.getNumber()的值为:144.45

Through the acquisition method of MonetaryRounding, we can understand that it is done by calling the corresponding getRounding method through the extended implementation class of MonetaryRoundingsSingletonSpi. The way to query according to conditions is shown below;

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. The method for obtaining MonetaryRounding is as follows;

@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, it can be known that MonetaryRounding is mainly obtained from the getRounding method of the RoundingProviderSpi extension point implementation class. JSR-354 default implementation The DefaultRoundingProvider in Moneta provides related implementations. If you need to implement a custom Rounding strategy, just follow the extension points defined by RoundingProviderSpi.

2.3 Currency Exchange

2.3.1 Instructions for currency exchange

In the previous section, it was mentioned that MonetaryOperator also has a type of currency exchange-related operations. The following example shows the commonly used method 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);

It can also be used to obtain ExchangeRateProvider first, and then obtain 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 is obtained through the static method MonetaryConversions.getConversion. The method calls getConversion according to the realization of MonetaryConversionsSingletonSpi to obtain.

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 Moneta's implementation, MonetaryConversionsSingletonSpi has only the only implementation class DefaultMonetaryConversionsSingletonSpi.

The acquisition of ExchangeRateProvider depends on the extended implementation of ExchangeRateProvider as shown below;

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 ExchangeRateProvider default implementation class diagram)

Therefore, the recommended way to extend currency exchange capabilities is to implement ExchangeRateProvider and load it through the SPI mechanism.

2.4 Format

2.4.1 Format instruction

Formatting mainly consists of two parts: the object instance is converted into a string conforming to the format; the string in the specified format is converted into an object instance. The corresponding conversion is performed respectively 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 structure of MonetaryAmountFormat. MonetaryAmountFormat is mainly created and obtained as 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 related 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 formatting method, it is recommended to extend MonetaryAmountFormatProviderSpi.

2.5 SPI

The service extension points provided by JSR-354 are:

(Figure 2-7 Service extension point class diagram)

1) Dealing with CurrencyProviderSpi and MonetaryCurrenciesSingletonSpi related to currency types;

2) Dealing with MonetaryConversionsSingletonSpi related to currency conversion;

3) MonetaryAmountFactoryProviderSpi, MonetaryAmountsSingletonSpi related to processing currency amounts;

4) Processing rounding-related RoundingProviderSpi, MonetaryRoundingsSingletonSpi;

5) Processing format related MonetaryAmountFormatProviderSpi, MonetaryFormatsSingletonSpi;

6) Service Provider related to service discovery;

In addition to ServiceProvider, other extension points are explained above. The JSR-354 specification provides a default implementation of DefaultServiceProvider. Use the ServiceLoader that comes with the JDK to realize service-oriented registration and discovery, and complete the decoupling of service provision and use. The order of loading services is 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;
    }
}

Moneta's implementation also provides an implementation of PriorityAwareServiceProvider, which can specify the priority of the 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 the dynamic expansion of currency types and changes in currency exchange rates. Moneta provides a set of data loading mechanism to support the corresponding functions. By default, four loading and update strategies are provided: get from the fallback URL, not remote data; get it from the remote at startup and load it only once; load it from the remote when it is used for the first time; get updates regularly. Use different ways of loading data for different strategies. Corresponding to the corresponding processing methods of NEVER, ONSTARTUP, LAZY, and SCHEDULED in the following codes;

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 completes the loading of 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, except that it is placed in another thread for asynchronous execution:

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;
}

The way LoadableResource loads data is;

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, using the Timer that comes with the JDK 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 */));
            }
        }
    }
}

Three, case

3.1 Currency type expansion

In the current business scenario, it is necessary to support multiple currency types such as diamonds, incentive gold, and tokens, and the types of currency types will increase with the development of the business. We need to extend the currency type and also need a dynamic loading mechanism for currency type data. Follow the steps below to expand:

1) Add the following configuration in javamoney.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 the file javax.money.spi.CurrencyProviderSpi under the META-INF.services path, and add the following content to the file;

com.vivo.finance.javamoney.spi.VFCurrencyProvider

3) Add the file currency.json to the java-money.defaults.VFC path, the content of the file is 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 implementation of class VFCurrencyProvider

CurrencyProviderSpi and LoaderService.LoaderListener are used to extend the currency type and implement the data loading of the extended currency type. The data analysis class VFCurrencyReadingHandler, the data model class VFCurrency and other codes included are omitted. The corresponding realization association class diagram is;

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

The key realization is the loading of data, 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

As currency types increase, corresponding currency exchange scenarios in scenarios such as recharging will also increase. We need to expand currency conversion and need a dynamic loading mechanism for currency conversion rate-related data. If the currency expansion method is similar, follow the steps below to expand:

Add the following configuration in javamoney.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 the file javax.money.convert.ExchangeRateProvider under the META-INF.services path, and add the following content to the file;

com.vivo.finance.javamoney.spi.VFCExchangeRateProvider

Add the file currencyExchangeRate.json to the java-money.defaults.VFC path, the content of the file is 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 realization association class diagram is;

(Figure 2-9 Main association realization class diagram of currency amount expansion)

3.3 Use case examples

Assuming that 1 RMB can be exchanged for 100 v beans and 1 RMB can be exchanged for 1 v diamonds, in the current scenario, the user recharges 100 v beans and pays 1 v diamonds. It is necessary to verify whether the payment amount and the recharge amount are legal. You can use the following methods to verify;

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);

Four, summary

JavaMoney provides great convenience for using currency in financial scenarios. It can support the demands of rich business scenarios for currency types and currency amounts. In particular, Monetary, MonetaryConversions, and MonetaryFormats are the entrances of currency basic capabilities, currency conversion, currency formatting and other capabilities, which provide convenience for related operations. At the same time, it also provides a good extension mechanism to facilitate related transformations to meet their own business scenarios.

The main problems that JSR 354 needs to solve are drawn from the usage scenarios. By analyzing the package and module structure of related projects, it is explained that JSR 354 and its implementation are divided to solve these problems. Then, from the relevant API, it is explained how it supports and uses the corresponding currency expansion, amount calculation, currency conversion, formatting and other capabilities. And introduced relevant suggestions on expansion methods. Then it summarizes the related SPI and the corresponding data loading mechanism. Finally, a case is used to illustrate how to expand for a specific scenario and the corresponding implementation of the application.

Author: vivo internet server team-Hou Xiaobi

vivo互联网技术
3.3k 声望10.2k 粉丝