current position:Home>Interpretation of javamoney specification (JSR 354) and corresponding implementation

Interpretation of javamoney specification (JSR 354) and corresponding implementation

2022-01-26 21:44:05 Vivo Internet technology

author :vivo Internet server team -Hou Xiaobi

One 、 summary

1.1 The current situation

At present JDK The class used to express money in is java.util.Currency, This class can only represent the following [ISO-4217] Describe the type of currency . It has no value associated with it , Nor can it describe some currencies outside the norm . For the calculation of money 、 Currency conversion 、 There is no support for currency formatting , Even the standard type that can represent the monetary amount does not provide relevant instructions .JSR-354 Defines a standard set of API To solve these related problems .

1.2 Normative purpose

JSR-354 The main objectives are :

  • Provide the possibility for currency expansion , Support the demands of rich business scenarios on currency type and currency amount ;

  • Provide currency amount calculation API;

  • Provide support for currency exchange rate and expansion ;

  • Provide support and extension for currency and currency amount parsing and formatting .

1.3 Use scenarios

online store

The unit price of goods in the mall , After adding items to the shopping cart , The total price to be calculated with the number of items . After switching the payment method in the mall, the currency exchange involved with the change of settlement currency type . When the user places an order, the payment amount involved is calculated , Tax calculation, etc .

Financial transaction website

On a financial trading website , Customers can create virtual portfolios at will . According to the portfolio created , Display the calculated historical data in combination with historical data 、 Current and expected benefits .

Virtual worlds and game sites

Online games will define their own game currency . Users can buy game currency through the amount in the bank card , This involves currency exchange . And because there are many kinds 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 be based on the exchange rate 、 The interest rate 、 Stock quotes 、 Currency model information on current and historical currencies . Usually, such company internal systems also have additional information represented by financial data , For example, historical currency 、 Exchange rate and risk analysis . So currencies and exchange rates must be historic 、 Regional , And define their validity range .

Two 、JavaMoney analysis

2.1 Package and engineering structure

2.1.1 Package overview

JSR-354 Defined 4 Related packages :


( chart 2-1 Package structure diagram ) Contains major components such as :

  • CurrencyUnit;

  • MonetaryAmount;

  • MonetaryContext;

  • MonetaryOperator;

  • MonetaryQuery;

  • MonetaryRounding ;

  • Related singleton visitors Monetary. Including currency exchange related components, such as :

  • ExchangeRate;

  • ExchangeRateProvider;

  • CurrencyConversion ;

  • Related singleton visitors MonetaryConversions . Contains formatting related components, such as :

  • MonetaryAmountFormat;

  • AmountFormatContext;

  • Related singleton visitors MonetaryFormats . Include by JSR-354 Provided SPI Interface and boot logic , To support different runtime environments and component loading mechanisms .

2.2.2 An overview of the module

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

  • jsr354-api: Include the based on... Described in this specification Java 8 Of JSR 354 API;

  • jsr354-ri: Include based on Java 8 Characteristic of language Moneta Reference implementation ;

  • jsr354-tck: Includes technical compatibility Suite (TCK).TCK It's using Java 8 Built ;

  • javamoney-parent: yes org.javamoney Remove the root of all modules “POM” project . This includes RI/TCK project , But does not include jsr354-api( It's independent ).

2.2 The core API

2.2.1 CurrencyUnit CurrencyUnit Data model

CurrencyUnit Property containing the smallest unit of currency , As shown below :

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

Method getCurrencyCode() Return different currency codes . be based on ISO Currency The standard currency code defaults to three digits , Other types of currency codes do not have this constraint .

Method getNumericCode() The return value is optional . By default, you can return -1.ISO The currency code must match the corresponding ISO The value of the code .

defaultFractionDigits Defines the number of digits after the decimal point by default .CurrencyContext Contains additional metadata information for currency units . obtain CurrencyUnit The way

Get by currency code

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

Obtain... According to the 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);

Get all CurrencyUnit

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

We enter Monetary.getCurrency Series method , You can see that these methods are obtained by MonetaryCurrenciesSingletonSpi.class Implementation class corresponding instance , Then call instance correspondence. 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(); } }

Interface MonetaryCurrenciesSingletonSpi

There is only one default

Realization DefaultMonetaryCurrenciesSingletonSpi. The way it gets the currency set is : all CurrencyProviderSpi Implementation class gets CurrencyUnit Set takes 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 ,CurrencyUnit The data provider for the implementation CurrencyProviderSpi Related implementation classes of .Moneta The default implementation provided has two providers , As shown in the figure ;


( chart 2-2 CurrencyProviderSpi Default implementation class diagram )

JDKCurrencyProvider by JDK in [ISO-4217] The described currency type provides a related mapping ;

ConfigurableCurrencyUnitProvider For dynamic change CurrencyUnit Provided support . Method is :registerCurrencyUnit、removeCurrencyUnit etc. .

therefore , If necessary CurrencyUnit Expand accordingly , It is recommended to press the extension point CurrencyProviderSpi User defined interface definition for custom construction extension .

2.2.2 MonetaryAmount 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 MonetaryAmount Three implementations are provided :FastMoney、Money、RoundedMoney.


( chart 2-3 MonetaryAmount Default implementation class diagram )

FastMoney Is a digital representation optimized for performance , The amount of money it represents is an integer number .Money Internally based on java.math.BigDecimal To perform arithmetic operations , The implementation can support arbitrary precision and scale.RoundedMoney The implementation of supports implicit rounding after each operation . We need to make reasonable choices according to our usage scenarios . If FastMoney Your digital capabilities are sufficient to meet your use cases , This type is recommended . establish MonetaryAmount

according to API The definition of , Can be accessed by MonetaryAmountFactory To create , It can also be created 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 Internally based on java.math.BigDecimal, So it also has BigDecimal Arithmetic accuracy and rounding ability . By default ,Money Internal instance use of MathContext.DECIMAL64 initialization . And support the specified way ;

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 It can also be done through from Methods to convert each other , The method is as follows ;


Both precision and rounding mode can be specified ;


Money And FastMoney It can also be done through from Methods to convert each other , The method is as follows ;

FastMoney fastMoney = FastMoney.of(144, "CNY");
Money money = Money.from(fastMoney);fastMoney = FastMoney.from(money); MonetaryAmount An extension of

although Moneta About MonetaryAmount Three realizations of :FastMoney、Money、RoundedMoney It has been able to meet the needs of most scenarios .JSR-354 by MonetaryAmount Reserved extension points provide more possibilities for implementation .

Let's follow up through the static method Monetary.getAmountFactory(ClassamountType) obtain MonetaryAmountFactory To create MonetaryAmount How instances work ;

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 , Need to pass through

MonetaryAmountsSingletonSpi The implementation class of the extension point passes through the method getAmountFactory To obtain a MonetaryAmountFactory.

Moneta In the way of realization

MonetaryAmountsSingletonSpi The only implementation class of is DefaultMonetaryAmountsSingletonSpi, Corresponding acquisition MonetaryAmountFactory The method to ;

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, we can find out MonetaryAmountFactory Is obtained through the extension point MonetaryAmountFactoryProviderSpi By calling createMonetaryAmountFactory Generated .

So if you want to extend the implementation of new types MonetaryAmount,

At a minimum, you need to provide extension points

MonetaryAmountFactoryProviderSpi The implementation of the , Corresponding to the type of AbstractAmountFactory The implementation of and the maintenance of mutual relations .

Default MonetaryAmountFactoryProviderSpi Implementation and corresponding AbstractAmountFactory The implementation of is shown in the figure below ;


( chart 2-4 MonetaryAmountFactoryProviderSpi Default implementation class diagram )


( chart 2-5 AbstractAmountFactory Default implementation class diagram )

2.2.3 Currency amount calculation

from MonetaryAmount You can see from the interface definition that it provides common arithmetic operations ( Add 、 reduce 、 ride 、 except 、 Modulo, etc ) computing method . At the same time, it defines with Method to support MonetaryOperator Expansion of operation .MonetaryOperators Class defines some common MonetaryOperator The implementation of the :

1)ReciprocalOperator Used to operate countdown ;

2)PermilOperator Used to obtain the thousandth scale value ;

3)PercentOperator Used to get the percentage example value ;

4)ExtractorMinorPartOperator Used to get decimal parts ;

5)ExtractorMajorPartOperator Used to get the integer part ;

6)RoundingMonetaryAmountOperator Used for rounding ;

Simultaneous inheritance MonetaryOperator The interface is CurrencyConversion and MonetaryRounding. among CurrencyConversion Mainly related to currency exchange , The next section gives a detailed introduction .MonetaryRounding It's 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.getNumber() The value of is :144.4445

You can also use the default rounding method and specify CurrencyUnit The way , The result corresponds to scale by currencyUnit.getDefaultFractionDigits() Value , such as ;

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

Generally, the rounding operation is carried out by bit 1, For some types of currencies, the minimum unit is not 1, For example, the smallest unit of Swiss franc is 5. In this case , You can use attributes cashRounding by true, And carry out the corresponding operation ;

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() The value of is :144.45

adopt MonetaryRounding How to get , We can learn that it's all through MonetaryRoundingsSingletonSpi The extension implementation class of is implemented by calling the corresponding getRounding Method to accomplish . Query by criteria 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 MonetaryRoundingsSingletonSpi Of

The only implementation class is

DefaultMonetaryRoundingsSingletonSpi, It gets MonetaryRounding This is done as follows ;

@Overridepublic 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, we can know MonetaryRounding Mainly from RoundingProviderSpi Extension point implementation class getRounding Method to get .JSR-354 Default implementation Moneta in DefaultRoundingProvider Related implementations are provided . If you need to implement custom Rounding Strategy , according to RoundingProviderSpi Define the extension point .

2.3 Currency conversion

2.3.1 Instructions for currency exchange

Mentioned in the previous section MonetaryOperator There is also a kind of operation related to currency exchange . The following example shows the common way of using 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 obtained by first ExchangeRateProvider, And then get CurrencyConversion Make 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 expansion

CurrencyConversion By static method MonetaryConversions.getConversion To get . According to MonetaryConversionsSingletonSpi The implementation calls getConversion To obtain a .

The method getConversion It's through

Get the corresponding ExchangeRateProvider

And call getCurrencyConversion Realized ;

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."));}

Moneta In the implementation of


Only unique implementation classes


ExchangeRateProvider The acquisition of depends on ExchangeRateProvider The extended implementation of ;

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

ExchangeRateProvider The default implementations are :

  • CompoundRateProvider

  • IdentityRateProvider


( chart 2-6 ExchangeRateProvider Default implementation class diagram )

therefore , The proposed way to expand currency exchange capacity is to achieve ExchangeRateProvider, And pass SPI Mechanism loading .

2.4 format

2.4.1 Format instructions

Formatting mainly consists of two parts : Object instance is converted to a formatted string ; Converts a string in the specified format to an object instance . adopt MonetaryAmountFormat The instance corresponds to format and parse To perform the corresponding transformations respectively . 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 to using formatting is MonetaryAmountFormat Construction .MonetaryAmountFormat The main creation and acquisition methods are MonetaryFormats.getAmountFormat. Take a look at the 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(); }}

Relevant code description MonetaryAmountFormat The acquisition of depends on MonetaryFormatsSingletonSpi Implementation corresponding call getAmountFormat Method .

MonetaryFormatsSingletonSpi The default implementation of is DefaultMonetaryFormatsSingletonSpi, 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 MonetaryAmountFormatProviderSpi Related implementation of , And provide it as an extension point . The default extension implementation is DefaultAmountFormatProviderSpi.

If we need to extend and register our own formatting processing method , Extension is recommended MonetaryAmountFormatProviderSpi

The way .

2.5 SPI

JSR-354 The service extension points provided are ;


( chart 2-7 Service extension point class diagram )

1) Handle currency type related CurrencyProviderSpi、MonetaryCurrenciesSingletonSpi;
2) Handle currency exchange related MonetaryConversionsSingletonSpi;
3) Deal with... Related to monetary amounts MonetaryAmountFactoryProviderSpi、MonetaryAmountsSingletonSpi;
4) Process rounding related RoundingProviderSpi、MonetaryRoundingsSingletonSpi;
5) Handling formatting related MonetaryAmountFormatProviderSpi、MonetaryFormatsSingletonSpi;
6) Service discovery related ServiceProvider;

except ServiceProvider, Other extension points are described above .JSR-354 The specification provides a default implementation DefaultServiceProvider. utilize JDK Self contained ServiceLoader, Realize service-oriented registration and discovery , Complete the decoupling of service provision and use . The order in which the services are loaded 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 An implementation is also provided in the implementation of PriorityAwareServiceProvider, It can be based on annotations @Priority Specifies the priority of the service interface implementation .

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 , For example, the dynamic expansion of currency type and the change of currency exchange rate .Moneta A set of data loading mechanism is provided to support the corresponding functions . Four load update strategies are provided by default : from fallback URL obtain , Do not get remote data ; At startup, it is obtained remotely and loaded only once ; Load from remote for the first time ; Get updates regularly . Different methods of loading data are used for different strategies . They correspond to the following codes NEVER、ONSTARTUP、LAZY、SCHEDULED Corresponding processing method ;

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

loadDataLocal Method completes the loading of data by triggering the listener . The listener actually calls 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 and loadDataLocal similar , Just put it on another thread to execute asynchronously :

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

loadDataRemote By calling LoadableResource Of loadRemote To load data .

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 The way to load data is ;

protected boolean load(URI itemToLoad, boolean fallbackLoad) { InputStream is = null; ByteArrayOutputStream stream = new ByteArrayOutputStream(); try{ URLConnection conn; String proxyPort ="proxy.port"); String proxyHost =""); String proxyType ="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 =; while (read > 0) { stream.write(data, 0, read); read =; } setData(stream.toByteArray()); ...... return true; } catch (Exception e) { ...... } finally { ...... } return false;}

The scheme of timing execution is similar to the above , Adopted JDK Self contained Timer Make a 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、 ... and 、 Case study

3.1 Currency type extension

In the current business scenario, you need to support v drill 、 Incentive fund 、v Beans and other currency types , And with the development of business, the types of currency will grow . We need to extend the currency type and also need the dynamic loading mechanism of currency type data . Follow the steps below to expand :

1) Add the following configuration ;


2) Add files under path, And add the following to the file ;

3)java-money.defaults.VFC Add files under path currency.json, The contents of the document 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 the class VFCurrencyProvider Realization


and LoaderService.LoaderListener, Data loading for extending currency types and implementing extended currency types . It contains data parsing classes VFCurrencyReadingHandler, Data model class VFCurrency Wait for the code to be omitted . The corresponding implementation Association class diagram is ;


( chart 2-8 Currency type extension is mainly related to the implementation class diagram )

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

@Overridepublic 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);
int newSize = CURRENCY_UNITS.size(); loadState = "Loaded " + resourceId + " currency:" + (newSize - oldSize);; } 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 , In recharge and other scenarios, the corresponding currency exchange scenarios will also increase . We need to expand currency exchange and need a dynamic loading mechanism for data related to currency exchange rate . For example, the expansion mode of currency is similar , Follow the steps below to expand : Add the following configuration ;

{-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 files under path, And add the following to the file ;

java-money.defaults.VFC Add files under path currencyExchangeRate.json, The contents of the document 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 the class VFCExchangeRateProvider

Inherit AbstractRateProvider

And implement LoaderService.LoaderListener. The corresponding implementation Association class diagram is ;


( chart 2-9 Currency amount extension is mainly related to the implementation class diagram )

3.3 Use scenario cases

hypothesis 1 RMB is convertible 100v bean ,1 RMB is convertible 1v drill , User recharge in the current scenario 100v The beans paid for 1v drill , It is necessary to verify whether the payment amount and 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 It provides great convenience for using money in the financial scenario . It can support the demands of rich business scenarios on currency type and currency amount . especially Monetary、MonetaryConversions、MonetaryFormats As a monetary base 、 Currency conversion 、 The entrance of currency formatting and other capabilities , It provides convenience for relevant 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 JSR 354 The main problem to be solved . By analyzing the package and module structure of related projects, it is explained to solve these problems JSR 354 And its implementation is to solve these problems . Then from the relevant API To illustrate the corresponding currency expansion , Amount calculation , Currency conversion 、 Formatting and other capabilities, how it is supported and used . It also introduces the relevant expansion methods, opinions and suggestions . Then it summarizes the related problems 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 .


Guess you like

copyright notice
author[Vivo Internet technology],Please bring the original link to reprint, thank you.

Random recommended