An overview,

1.1 Current Status

The current JDK class used to represent Currency is java.util.currency. This class can only represent Currency types described by **[ISO-4217]**. It has no numerical value associated with it and cannot describe some currencies outside the specification. There is no support for currency calculation, currency conversion, currency formatting, or even a standard type that represents the amount of money. Jsr-354 defines a standard SET of apis to address these issues.

1.2 Purpose of specification

The main objectives of JSR-354 are:

  • It provides the possibility of currency expansion and supports the demands of various business scenarios for currency types and amounts;

  • Provide API for currency calculation;

  • Support and expansion of currency exchange rates;

  • Provides support and extensions for parsing and formatting currencies and currency amounts.

1.3 Application Scenarios

The online store

The unit price of an item in a shopping mall, which is calculated according to the quantity of the item after it is added to the cart. The currency exchange involved with the change of settlement currency type after the change of payment mode in the mall. When users place an order, it involves the calculation of payment amount, tax calculation and so on.

Financial transaction website

On a financial trading website, clients can create virtual portfolios of whatever they want. Show calculated historical, current, and expected returns based on the portfolio created and combined with historical data.

Virtual worlds and gaming sites

Online games will define their own game currency. Users can use the amount in their bank cards to buy game coins, which involves currency exchange. And because there are so many different types of games, the currency type support required must also support dynamic scaling.

Banking and financial applications

Banks and other financial institutions must establish information about currency models in terms of exchange rates, interest rates, stock quotes, current and historical currencies, etc. Often such internal systems also have additional information for the presentation of financial data, such as historical currencies, exchange rates, and risk analysis. So currencies and exchange rates must be historic, regional, and define their duration.

Second, JavaMoney parsing

2.1 Package and engineering structure

2.1.1 package overview

Jsr-354 defines four related packages:

(Figure 2-1 package structure diagram)

Javax. money contains major components such as:

  • CurrencyUnit;

  • MonetaryAmount;

  • MonetaryContext;

  • MonetaryOperator;

  • MonetaryQuery;

  • MonetaryRounding;

  • The related singleton visitor, Monetary.

Javax.money. convert contains currency exchange related components such as:

  • ExchangeRate;

  • ExchangeRateProvider;

  • CurrencyConversion;

  • Related singleton visitors MonetaryConversions.

Javax.money. format contains formatting components such as:

  • MonetaryAmountFormat;

  • AmountFormatContext;

  • Related singleton visitor MonetaryFormats.

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

  • Jsr354-api: contains the Java 8-based JSR 354 API described in this specification;

  • Jsr354-ri: contains a Moneta reference implementation based on Java 8 language features;

  • Jsr354-tck: Contains technology Compatibility suite (TCK). TCK is built using Java 8;

  • Javamoney -parent: is the root POM project of all modules under org.javamoney. This includes the RI/TCK project, but not the JSR354-API (which is separate).

2.2 core API

2.2.1 CurrencyUnit

2.2.1.1 CurrencyUnit Data model

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


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

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

Method getNumericCode() returns an optional value. By default, -1 is returned. ISO currency codes 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 obtain CurrencyUnit

Obtain according to the currency code

CurrencyUnit currencyUnit = Monetary.getCurrency("USD");
Copy the code

Obtain by region

CurrencyUnit currencyUnitChina = Monetary.getCurrency(Locale.CHINA);
Copy the code

The value can be obtained based on search conditions

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

Get all currencyUnits;

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

2.2.1.3 CurrencyUnit Data provider

We enter the Monetary getCurrency series method, you can see these methods is by getting MonetaryCurrenciesSingletonSpi. The class of the corresponding instance of the class, and then call the corresponding getCurrency instance 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(); }}Copy the code

Interface MonetaryCurrenciesSingletonSpi only realizing a DefaultMonetaryCurrenciesSingletonSpi by default. It gets the currency set as all CurrencyProviderSpi implementation classes get the CurrencyUnit 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;
}
Copy the code

Therefore, the data provider for 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 Default implementation class diagram of CurrencyProviderSpi)

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

ConfigurableCurrencyUnitProvider provides support for dynamic change CurrencyUnit. The methods are registerCurrencyUnit and removeCurrencyUnit.

Therefore, if you need to extend CurrencyUnit, you are advised to construct a custom extension based on the interface definition of CurrencyProviderSpi.

2.2.2 MonetaryAmount

2.2.2.1 MonetaryAmount data model

public interface MonetaryAmount extends CurrencySupplier, NumberSupplier, Comparable<MonetaryAmount>{// getContext data MonetaryContext getContext(); Default <R> R query(MonetaryQuery<R> query){return query.queryfrom (this); Default MonetaryAmount with(MonetaryOperator operator){return operator.apply(this); } // Get the factory to create a new instance of the currency amount MonetaryAmountFactory<? extends MonetaryAmount> getFactory(); // Compare method Boolean isGreaterThan(MonetaryAmount amount); . int signum(); // Algorithm function and calculate MonetaryAmount add(MonetaryAmount amount); . MonetaryAmount stripTrailingZeros(); }Copy the code

MonetaryAmount provides three implementations: FastMoney, Money, and RoundedMoney.

(Figure 2-3 MonetaryAmount default implementation class diagram)

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

2.2.2.2 create MonetaryAmount

Depending on the API definition, it can be created by accessing MonetaryAmountFactory or directly by using the factory method of the corresponding type. The following;

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");
Copy the code

Because Money is internally based on java.math.BigDecimal, it also has the arithmetic precision and rounding capability of BigDecimal. By default, internal instances of Money are initialized using 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());
Copy the code

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

org.javamoney.moneta.Money.defaults.mathContext=DECIMAL128
Copy the code

Precision and rounding mode can be specified;

org.javamoney.moneta.Money.defaults.precision=256
org.javamoney.moneta.Money.defaults.roundingMode=HALF_EVEN
Copy the code

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

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

Money money = Money.from(fastMoney);
fastMoney = FastMoney.from(money);
Copy the code

2.2.2.3 Extension of MonetaryAmount

Although Moneta provides three implementations of MonetaryAmount: FastMoney, Money, and RoundedMoney, it already meets the needs of most scenarios. Jsr-354 provides additional implementation possibilities for the extension points reserved for MonetaryAmount.

We follow up by the method of static Monetary. GetAmountFactory (ClassamountType) obtain MonetaryAmountFactory to create instances of MonetaryAmount way;

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; }}Copy the code

As shown in the code, need to pass MonetaryAmountsSingletonSpi getAmountFactory extension point implementation class through the method to obtain MonetaryAmountFactory.

Moneta is implemented in the only implementation class for DefaultMonetaryAmountsSingletonSpi MonetaryAmountsSingletonSpi, corresponding the 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()); }... }Copy the code

Finally can find MonetaryAmountFactory acquisition is an extension point MonetaryAmountFactoryProviderSpi by calling createMonetaryAmountFactory generated.

So if you want to extend the realization of new type MonetaryAmount, at least you need to provide extension points MonetaryAmountFactoryProviderSpi realization, the realization of the corresponding type of AbstractAmountFactory and relationship maintenance.

The default MonetaryAmountFactoryProviderSpi implementation and the implementation of the corresponding AbstractAmountFactory as shown in the figure below.

(FIG. 2-4 MonetaryAmountFactoryProviderSpi default implementation class diagrams)

(Figure 2-5 AbstractAmountFactory default implementation class diagram)

2.2.3 Currency calculation is related

As you can see from the interface definition of MonetaryAmount, it provides common arithmetic operations (addition, subtraction, multiplication, division, modulus, and so on). The with method is also defined to support extensions based on MonetaryOperator operations. The MonetaryOperators class defines some common implementations of MonetaryOperators:

  • 1) ReciprocalOperator is used to operate and take the reciprocal;

  • 2) PermilOperator is used to obtain the example value of the thousand ratio;

  • 3) PercentOperator is used to get percentage values;

  • 4) ExtractorMinorPartOperator used to get the decimal part;

  • 5) ExtractorMajorPartOperator for integer part;

  • 6) RoundingMonetaryAmountOperator for rounding calculations;

Pawntask’s interface is CurrencyConversion and MonetaryOperator. CurrencyConversion is mainly related to CurrencyConversion, which will be introduced in the next section. Pawnchess is about rounding, which is usually used in the following ways.

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 () has the value 144.4445Copy the code

You can also use the default rounding mode and specify CurrencyUnit way, the result of corresponding scale for CurrencyUnit. GetDefaultFractionDigits () value, such as;

MonetaryRounding rounding = Monetary.getDefaultRounding(); Money Money = Money) of (144.44445555, "CNY"); MonetaryAmount roundedAmount = money.with(rounding); # roundedAmount. GetNumber () corresponding to the scale for the money. The 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 () corresponding to the scale for the currency. GetDefaultFractionDigits ()Copy the code

Generally, the rounding operation is carried out by 1. For some types of currencies, the minimum unit is not 1. For example, the minimum unit of The Swiss franc is 5. In this case, the cashchess attribute is true and actions are performed accordingly.

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 () has the value :144.45Copy the code

Through MonetaryRounding access, we can learn by MonetaryRoundingsSingletonSpi extension implementation class by calling the corresponding getRounding method to complete. The following is the way to query by condition;

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

The default implementation of the only implementation class for DefaultMonetaryRoundingsSingletonSpi MonetaryRoundingsSingletonSpi, it takes MonetaryRounding way 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;
}
Copy the code

As can be seen from the above code, MonetaryRobot’s mission is mainly obtained from the GetrMission method of the RoundingProviderSpi extension point implementation class. The default jSR-354 implementation DefaultRoundingProvider in Moneta provides the related implementation. If a custom strategy needs to be implemented, PawnChess can do it according to the Extension points defined by RoundingProviderSpi.

2.3 Currency Exchange

2.3.1 Instructions for Currency Exchange

As mentioned in the previous section, the MonetaryOperator also has a class of currency-exchange related operations. The following example shows the common way of using money 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);
Copy the code

You can also obtain an ExchangeRateProvider and then a CurrencyConversion for the 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);
Copy the code

2.3.2 Currency exchange expansion

MonetaryConversions CurrencyConversion by static method. GetConversion to obtain. Methods according to the implementation of MonetaryConversionsSingletonSpi call getConversion.

The method getConversion is implemented by getting 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."));
}
Copy the code

Moneta implementation MonetaryConversionsSingletonSpi DefaultMonetaryConversionsSingletonSpi only the implementation of the class.

The acquisition of ExchangeRateProvider as shown below depends on the extended 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)); }}Copy the code

The default implementations provided by ExchangeRateProvider are:

  • CompoundRateProvider

  • IdentityRateProvider

(Figure 2-6 Default implementation class diagram of ExchangeRateProvider)

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

2.4 the formatting

2.4.1 Formatting Instructions

Formatting consists of two parts: converting an object instance to a formatted string; Converts a string of the specified format to an object instance. Convert the MonetaryAmountFormat instance to format and parse. 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);Copy the code

2.4.2 Formatting An Extension

The key to formatting is the MonetaryAmountFormat construction. MonetaryAmountFormat main create access for 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(); }}Copy the code

Relevant code instructions MonetaryAmountFormat access relies on the implementation of MonetaryFormatsSingletonSpi corresponding call getAmountFormat method.

MonetaryFormatsSingletonSpi default implementation for DefaultMonetaryFormatsSingletonSpi, corresponding to the access 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;
}
Copy the code

Can see ultimately depends on the MonetaryAmountFormatProviderSpi related implementation, and provide out as an extension point. The default extension for DefaultAmountFormatProviderSpi implementation way.

If we need to extend the registered own formatting handling, recommended to extend MonetaryAmountFormatProviderSpi way.

2.5 SPI

The service extension points provided by JSR-354 are;

(Figure 2-7 Service extension point class diagram)

1) handle currency types related CurrencyProviderSpi, MonetaryCurrenciesSingletonSpi;

2) handle currency exchange related MonetaryConversionsSingletonSpi;

3) processing related MonetaryAmountFactoryProviderSpi, MonetaryAmountsSingletonSpi monetary amount;

4) dealing with the rounding of RoundingProviderSpi, MonetaryRoundingsSingletonSpi;

5) to handle the related format MonetaryAmountFormatProviderSpi, MonetaryFormatsSingletonSpi;

6) ServiceProvider related to service discovery;

All extension points except ServiceProvider are described above. The JSR-354 specification provides the default implementation of DefaultServiceProvider. Use the JDK’s own ServiceLoader to achieve service-oriented registration and discovery, and complete the decoupling of service provision and use. The services are loaded in 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;
    }
}
Copy the code

Moneta implementation PriorityAwareServiceProvider also provides a kind of implementation, it can be specified according to the comments @ Priority service interface implementation of 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()); }Copy the code

2.6 Data Loading Mechanism

For some dynamic data, such as the dynamic expansion of currency types and currency exchange rate changes. Moneta provides a data loading mechanism to support corresponding functions. By default, four load update strategies are provided: fetch from the FALLback URL, not remote data; Get it remotely and load it only once at startup; Load from remote when first used; Periodically get updates. Use different ways to load data for different policies. The processes of NEVER, ONSTARTUP, LAZY, and SCHEDULED in the following codes are applied 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;
    }
}
Copy the code

The loadDataLocal method loads data by triggering a 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()); . }}}}Copy the code

LoadDataAsync is similar to loadDataLocal, but is executed asynchronously on a different thread:

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

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; }Copy the code

LoadableResource loads data as follows;

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; }Copy the code

The timing execution scheme is similar to the above, using the BUILT-IN Timer of 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 */)); }}}}Copy the code

Three cases,

3.1 Expansion of currency types

The current business scenario needs to support multiple currency types such as V-diamond, gold and v-bean, 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 the currency type data. Follow these steps to extend:

1) Add the following configuration to 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
Copy the code

2) meta-inf. Services directory add files javax.mail. Money. Spi. CurrencyProviderSpi, and add the following content in the file;

com.vivo.finance.javamoney.spi.VFCurrencyProvider
Copy the code

Java-money.defaults. VFC add file currency.json to java-money.defaults.VFC

[{
  "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
}
]
Copy the code

4) Add class VFCurrencyProvider implementation

CurrencyProviderSpi and LoaderService LoaderListener, currency types used in extended currency type and implementation of the data load. The data parsing class VFCurrencyReadingHandler and the data model class VFCurrency are omitted. Corresponding implementation association class diagram;

(Figure 2-8 Main Association Implementation class diagram of currency type extension)

The key implementation is data loading, 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(); }}Copy the code

3.2 Currency exchange expansion

With the increase of currency types, the corresponding currency exchange scenarios in recharge scenarios will also increase. We need to expand currency exchange and need dynamic loading mechanisms for data related to currency exchange rates. If the expansion mode of currency is similar, follow the following steps to expand:

Add the following configuration to 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
Copy the code

Meta-inf. Add documentation services directory path javax.mail. Money. Convert. ExchangeRateProvider, and add the following content in the file;

com.vivo.finance.javamoney.spi.VFCExchangeRateProvider
Copy the code

Java – money. Defaults. VFC path add files currencyExchangeRate. Json, file contents 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 varies", "factor" : "1 e + 2"}, {" date ":" 2021-05-13 ", "currency" : "VDO", "factor" : "0.1666"}, {" date ":" 2021-05-13 ", "currency" : "VJP", "factor" : "23.4400"}]Copy the code

Add the class VFCExchangeRateProvider

Inheritance AbstractRateProvider and implement LoaderService LoaderListener. Corresponding implementation association class diagram;

(Figure 2-9 Main Association Realization class diagram of monetary amount extension)

3.3 Application Scenarios

Assume that 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 recharging 100V beans, so it is necessary to verify whether the payment amount and recharging amount are legal. You can use the following verification methods;

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);
Copy the code

Four,

JavaMoney provides great convenience for using money in financial scenarios. It can support the demands of various business scenarios for currency types and amounts. In particular, Monetary, MonetaryConversions, and MonetaryFormats serve as portals to currency infrastructure, currency exchange, currency formatting, and so on, facilitating operations. At the same time, it also provides a good extension mechanism for relevant modifications to meet their own business scenarios.

The main problems that need to be solved in JSR 354 are introduced from the application scenarios. JSR 354 and its implementation are divided to solve these problems by analyzing the package and module structure of related engineering. Then from the related API to explain how it is supported and used for the corresponding currency extension, amount calculation, currency exchange, formatting and other capabilities. As well as the introduction of the relevant expansion of the way suggestions. It then summarizes the related SPIs and the corresponding data loading mechanism. Finally, a case is given to illustrate how to extend and apply the corresponding implementation for specific scenarios.

Hou Xiaobi, Vivo Internet Server Team