一、概述

1.1 当前现状

当前JDK中用来表达货币的类为java.util.Currency,这个类仅仅能够表示按照**[ISO-4217]**描述的货币类型。它没有与之关联的数值,也不能描述规范外的一些货币。对于货币的计算、货币兑换、货币的格式化没有提供相关的支持,甚至连能够代表货币金额的标准类型也没有提供相关说明。JSR-354定义了一套标准的API用来解决相关的这些问题。

1.2 规范目的

JSR-354主要的目标为:

  • 为货币扩展提供可能,支撑丰富的业务场景对货币类型以及货币金额的诉求;

  • 提供货币金额计算的API;

  • 提供对货币兑换汇率的支持以及扩展;

  • 为货币和货币金额的解析和格式化提供支持以及扩展。

1.3 使用场景

在线商店

商城中商品的单价,将商品加入购物车后,随着物品数量而需要计算的总价。在商城将支付方式切换后随着结算货币类型的变更而涉及到的货币兑换等。当用户下单后涉及到的支付金额计算,税费计算等。

金融交易网站

在一个金融交易网站上,客户可以任意创建虚拟投资组合。根据创建的投资组合,结合历史数据显示计算出来的历史的、当前的以及预期的收益。

虚拟世界和游戏网站

在线游戏会定义它们自己的游戏币。用户可以通过银行卡中的金额去购买游戏币,这其中就涉及到货币兑换。而且因为游戏种类繁多,需要的货币类型支持也必须能够支撑动态扩展。

银行和金融应用

银行等金融机构必须建立在汇率、利率、股票报价、当前和历史的货币等方面的货币模型信息。通常这样的公司内部系统也存在财务数据表示的附加信息,例如历史货币、汇率以及风险分析等。所以货币和汇率必须是具有历史意义的、区域性的,并定义它们的有效期范围。

二、JavaMoney解析

2.1 包和工程结构

2.1.1 包概览

JSR-354 定义了4个相关包:

(图2-1 包结构图)

javax.money包含主要组件如:

  • CurrencyUnit;

  • MonetaryAmount;

  • MonetaryContext;

  • MonetaryOperator;

  • MonetaryQuery;

  • MonetaryRounding ;

  • 相关的单例访问者Monetary。

javax.money.convert 包含货币兑换相关组件如:

  • ExchangeRate;

  • ExchangeRateProvider;

  • CurrencyConversion ;

  • 相关的单例访问者MonetaryConversions 。

javax.money.format包含格式化相关组件如:

  • MonetaryAmountFormat;

  • AmountFormatContext;

  • 相关的单例访问者MonetaryFormats 。

javax.money.spi:包含由JSR-354提供的SPI接口和引导逻辑,以支持不同的运行时环境和组件加载机制。

2.2.2 模块概览

JSR-354源码仓库包含如下模块:

  • jsr354-api:包含本规范中描述的基于Java 8的JSR 354 API;

  • jsr354-ri:包含基于Java 8语言特性的Moneta参考实现;

  • jsr354-tck:包含技术兼容套件(TCK)。TCK是使用Java 8构建的;

  • javamoney-parent:是org.javamoney下所有模块的根“POM”项目。这包括RI/TCK项目,但不包括jsr354-api(它是独立的)。

2.2 核心API

2.2.1 CurrencyUnit

2.2.1.1 CurrencyUnit数据模型

CurrencyUnit包含货币最小单位的属性,如下所示:


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

方法getCurrencyCode()返回不同的货币编码。基于ISO Currency规范的货币编码默认为三位,其他类型的货币编码没有这个约束。

方法getNumericCode()返回值是可选的。默认可以返回-1。ISO货币的代码必须匹配对应的ISO代码的值。

defaultFractionDigits定义了默认情况下小数点后的位数。CurrencyContext包含货币单位的附加元数据信息。

2.2.1.2 获取CurrencyUnit的方式

根据货币编码获取

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

根据地区获取

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

按查询条件获取

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

获取所有的CurrencyUnit;

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

2.2.1.3 CurrencyUnit数据提供者

我们进入Monetary.getCurrency系列方法,可以看到这些方法都是通过获取MonetaryCurrenciesSingletonSpi.class实现类对应的实例,然后调用实例对应getCurrency方法。

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

接口MonetaryCurrenciesSingletonSpi默认只有一个实现DefaultMonetaryCurrenciesSingletonSpi。它获取货币集合的实现方式是:所有CurrencyProviderSpi实现类获取CurrencyUnit集合取并集。

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

因此,CurrencyUnit的数据提供者为实现CurrencyProviderSpi的相关实现类。Moneta提供的默认实现存在两个提供者,如图所示;

(图2-2 CurrencyProviderSpi默认实现类图)

JDKCurrencyProvider为JDK中[ISO-4217]描述的货币类型提供了相关的映射;

ConfigurableCurrencyUnitProvider为动态变更CurrencyUnit提供了支持。方法为:registerCurrencyUnit、removeCurrencyUnit等。

因此,如果需要对CurrencyUnit进行相应的扩展,建议按扩展点CurrencyProviderSpi的接口定义进行自定义的构造扩展。

2.2.2 MonetaryAmount

2.2.2.1 MonetaryAmount数据模型

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

对应MonetaryAmount提供了三种实现为:FastMoney、Money、RoundedMoney。

(图2-3 MonetaryAmount默认实现类图)

FastMoney是为性能而优化的数字表示,它表示的货币数量是一个整数类型的数字。Money内部基于java.math.BigDecimal来执行算术操作,该实现能够支持任意的precision和scale。RoundedMoney的实现支持在每个操作之后隐式地进行舍入。我们需要根据我们的使用场景进行合理的选择。如果FastMoney的数字功能足以满足你的用例,建议使用这种类型。

2.2.2.2 创建MonetaryAmount

根据API的定义,可以通过访问MonetaryAmountFactory来创建,也可以直接通过对应类型的工厂方法来创建。如下;

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

由于Money内部基于java.math.BigDecimal,因此它也具有BigDecimal的算术精度和舍入能力。默认情况下,Money的内部实例使用MathContext.DECIMAL64初始化。并且支持指定的方式;

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与FastMoney也可以通过from方法进行相互的转换,方法如下;

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

同时可以指定精度和舍入模式;

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

Money与FastMoney也可以通过from方法进行相互的转换,方法如下;

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

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

2.2.2.3 MonetaryAmount的扩展

虽然Moneta提供的关于MonetaryAmount的三种实现:FastMoney、Money、RoundedMoney已经能够满足绝大多数场景的需求。JSR-354为MonetaryAmount预留的扩展点提供了更多实现的可能。

我们跟进一下通过静态方法Monetary.getAmountFactory(ClassamountType)获取MonetaryAmountFactory来创建MonetaryAmount实例的方式;

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

如上代码所示,需要通过MonetaryAmountsSingletonSpi扩展点的实现类通过方法getAmountFactory来获得MonetaryAmountFactory。

Moneta的实现方式中MonetaryAmountsSingletonSpi的唯一实现类为DefaultMonetaryAmountsSingletonSpi,对应的获取MonetaryAmountFactory的方法为;

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

最后可以发现MonetaryAmountFactory的获取是通过扩展点MonetaryAmountFactoryProviderSpi通过调用createMonetaryAmountFactory生成的。

所以要想扩展实现新类型的MonetaryAmount,至少需要提供扩展点MonetaryAmountFactoryProviderSpi的实现,对应类型的AbstractAmountFactory的实现以及相互关系的维护。

默认MonetaryAmountFactoryProviderSpi的实现和对应的AbstractAmountFactory的实现如下图所示;

(图2-4 MonetaryAmountFactoryProviderSpi默认实现类图)

(图2-5 AbstractAmountFactory默认实现类图)

2.2.3 货币金额计算相关

从MonetaryAmount的接口定义中可以看到它提供了常用的算术运算(加、减、乘、除、求模等运算)计算方法。同时定义了with方法用于支持基于MonetaryOperator运算的扩展。MonetaryOperators类中定义了一些常用的MonetaryOperator的实现:

  • 1)ReciprocalOperator用于操作取倒数;

  • 2)PermilOperator用于获取千分比例值;

  • 3)PercentOperator用于获取百分比例值;

  • 4)ExtractorMinorPartOperator用于获取小数部分;

  • 5)ExtractorMajorPartOperator用于获取整数部分;

  • 6)RoundingMonetaryAmountOperator用于进行舍入运算;

同时继承MonetaryOperator的接口有CurrencyConversion和MonetaryRounding。其中CurrencyConversion主要与货币兑换相关,下一节作具体介绍。MonetaryRounding是关于舍入操作的,具体使用方式如下;

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

还可以使用默认的舍入方式以及指定CurrencyUnit 的方式,其结果对应的scale为currencyUnit.getDefaultFractionDigits()的值,比如;

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

一般情况下进行舍入操作是按位进1,针对某些类型的货币最小单位不为1,比如瑞士法郎最小单位为5。针对这种情况,可以通过属性cashRounding为true,并进行相应的操作;

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

通过MonetaryRounding的获取方式,我们可以了解到都是通过MonetaryRoundingsSingletonSpi的扩展实现类通过调用对应的getRounding方法来完成。如下所示按条件查询的方式;

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

默认实现中MonetaryRoundingsSingletonSpi的唯一实现类为DefaultMonetaryRoundingsSingletonSpi,它获取MonetaryRounding的方式如下;

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

根据上述代码可以得知MonetaryRounding主要来源于RoundingProviderSpi扩展点实现类的getRounding方法来获取。JSR-354默认实现Moneta中DefaultRoundingProvider提供了相关实现。如果需要实现自定义的Rounding策略,按照RoundingProviderSpi定义的扩展点进行即可。

2.3 货币兑换

2.3.1 货币兑换使用说明

上一节中有提到MonetaryOperator还存在一类货币兑换相关的操作。如下实例所示为常用的使用货币兑换的方式;

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

也可用通过先获取ExchangeRateProvider,然后再获取CurrencyConversion进行相应的货币兑换;

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 货币兑换扩展

CurrencyConversion通过静态方法MonetaryConversions.getConversion来获取。方法中根据MonetaryConversionsSingletonSpi的实现调用getConversion来获得。

而方法getConversion是通过获取对应的ExchangeRateProvider并调用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."));
}

Moneta的实现中MonetaryConversionsSingletonSpi只有唯一的实现类DefaultMonetaryConversionsSingletonSpi。

ExchangeRateProvider的获取如下所示依赖于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));
}
}

ExchangeRateProvider默认提供的实现有:

  • CompoundRateProvider

  • IdentityRateProvider

(图2-6 ExchangeRateProvider默认实现类图)

因此,建议的扩展货币兑换能力的方式为实现ExchangeRateProvider,并通过SPI的机制加载。

2.4 格式化

2.4.1 格式化使用说明

格式化主要包含两部分的内容:对象实例转换为符合格式的字符串;指定格式的字符串转换为对象实例。通过MonetaryAmountFormat实例对应的format和parse来分别执行相应的转换。如下代码所示;

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 格式化扩展

格式化的使用关键点在于MonetaryAmountFormat的构造。MonetaryAmountFormat主要创建获取方式为MonetaryFormats.getAmountFormat。看一下相关的源码;

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

相关代码说明MonetaryAmountFormat的获取依赖于MonetaryFormatsSingletonSpi的实现对应调用getAmountFormat方法。

MonetaryFormatsSingletonSpi的默认实现为DefaultMonetaryFormatsSingletonSpi,对应的获取方法如下;

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

可以看出来最终还是依赖于MonetaryAmountFormatProviderSpi的相关实现,并作为一个扩展点提供出来。默认的扩展实现方式为DefaultAmountFormatProviderSpi。

如果我们需要扩展注册自己的格式化处理方式,建议采用扩展MonetaryAmountFormatProviderSpi的方式。

2.5 SPI

JSR-354提供的服务扩展点有;

(图2-7 服务扩展点类图)

1)处理货币类型相关的CurrencyProviderSpi、MonetaryCurrenciesSingletonSpi;

2)处理货币兑换相关的MonetaryConversionsSingletonSpi;

3)处理货币金额相关的MonetaryAmountFactoryProviderSpi、MonetaryAmountsSingletonSpi;

4)处理舍入相关的RoundingProviderSpi、MonetaryRoundingsSingletonSpi;

5)处理格式化相关的MonetaryAmountFormatProviderSpi、MonetaryFormatsSingletonSpi;

6)服务发现相关的ServiceProvider;

除了ServiceProvider,其他扩展点上文都有相关说明。JSR-354规范提供了默认实现DefaultServiceProvider。利用JDK自带的ServiceLoader,实现面向服务的注册与发现,完成服务提供与使用的解耦。加载服务的顺序为按类名进行排序的顺序;

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的实现中也提供了一种实现PriorityAwareServiceProvider,它可以根据注解@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 数据加载机制

针对一些动态的数据,比如货币类型的动态扩展以及货币兑换汇率的变更等。Moneta提供了一套数据加载机制来支撑对应的功能。默认提供了四种加载更新策略:从fallback URL获取,不获取远程的数据;启动的时候从远程获取并且只加载一次;首次使用的时候从远程加载;定时获取更新。针对不同的策略使用不同的加载数据的方式。分别对应如下代码中NEVER、ONSTARTUP、LAZY、SCHEDULED对应的处理方式;

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方法通过触发监听器来完成数据的加载。而监听器实际上调用的是newDataLoaded方法。

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和loadDataLocal类似,只是放在另外的线程去异步执行:

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

loadDataRemote通过调用LoadableResource的loadRemote来加载数据。

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加载数据的方式为;

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

定时执行的方案与上述类似,采用了JDK自带的Timer做定时器,如下所示;

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.1 货币类型扩展

当前业务场景下需要支持v钻、鼓励金、v豆等多种货币类型,而且随着业务的发展货币类型的种类还会增长。我们需要扩展货币类型而且还需要货币类型数据的动态加载机制。按照如下步骤进行扩展:

1)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)META-INF.services路径下添加文件javax.money.spi.CurrencyProviderSpi,并且在文件中添加如下内容;

com.vivo.finance.javamoney.spi.VFCurrencyProvider

3)java-money.defaults.VFC路径下添加文件currency.json,文件内容如下;

[{
"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)添加类VFCurrencyProvider实现

CurrencyProviderSpi和LoaderService.LoaderListener,用于扩展货币类型和实现扩展的货币类型的数据加载。其中包含的数据解析类VFCurrencyReadingHandler,数据模型类VFCurrency等代码省略。对应的实现关联类图为;

(图2-8 货币类型扩展主要关联实现类图)

关键实现为数据的加载,代码如下;

@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 货币兑换扩展

随着货币类型的增加,在充值等场景下对应的货币兑换场景也会随之增加。我们需要扩展货币兑换并需要货币兑换汇率相关数据的动态加载机制。如货币的扩展方式类似,按照如下步骤进行扩展:

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

META-INF.services路径下添加文件javax.money.convert.ExchangeRateProvider,并且在文件中添加如下内容;

com.vivo.finance.javamoney.spi.VFCExchangeRateProvider

java-money.defaults.VFC路径下添加文件currencyExchangeRate.json,文件内容如下;

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

添加类VFCExchangeRateProvider

继承AbstractRateProvider并实现LoaderService.LoaderListener。对应的实现关联类图为;

(图2-9 货币金额扩展主要关联实现类图)

3.3 使用场景案例

假设1人民币可以兑换100v豆,1人民币可以兑换1v钻,当前场景下用户充值100v豆对应支付了1v钻,需要校验支付金额和充值金额是否合法。可以使用如下方式校验;

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

四、总结

JavaMoney为金融场景下使用货币提供了极大的便利。能够支撑丰富的业务场景对货币类型以及货币金额的诉求。特别是Monetary、MonetaryConversions、MonetaryFormats作为货币基础能力、货币兑换、货币格式化等能力的入口,为相关的操作提供了便利。同时也提供了很好的扩展机制方便进行相关的改造来满足自己的业务场景。

文中从使用场景出发引出JSR 354需要解决的主要问题。通过解析相关工程的包和模块结构说明针对这些问题JSR 354及其实现是如果去划分来解决这些问题的。然后从相关API来说明针对相应的货币扩展,金额计算,货币兑换、格式化等能力它是如何来支撑以及使用的。以及介绍了相关的扩展方式意见建议。接着总结了相关的SPI以及对应的数据加载机制。最后通过一个案例来说明针对特定场景如何扩展以及应用对应实现。

作者:vivo互联网服务器团队-Hou Xiaobi

JavaMoney规范(JSR 354)与对应实现解读的更多相关文章

  1. Hibernate Validator 6.0.9.Final - JSR 380 Reference Implementation: Reference Guide

    Preface Validating data is a common task that occurs throughout all application layers, from the pre ...

  2. mysql 数据库 规范

    目录 mysql 数据库 规范 基础规范 命名规范 表设计规范 字段设计规范 索引设计规范 SQL编写规范 行为规范 mysql 数据库 规范 基础规范 必须使用InnoDB存储引擎 解读:支持事务. ...

  3. JAVA JDK1.5-1.9新特性

    1.51.自动装箱与拆箱:2.枚举(常用来设计单例模式)3.静态导入4.可变参数5.内省 1.61.Web服务元数据2.脚本语言支持3.JTable的排序和过滤4.更简单,更强大的JAX-WS5.轻量 ...

  4. Java 9中新的货币API

    译文出处: Java译站   原文出处:Michael Scharhag JSR 354定义了一套新的Java货币API,计划会在Java 9中正式引入.本文中我们将来看一下它的参考实现:JavaMo ...

  5. Java 9 New Features

    Java 9 概述 1. jdk 9 的发布.经过 4 次跳票,历经曲折的 java 9 终于终于在 2017 年 9 月 21 日发布. 2. Java 9 中哪些不得不说的新特性?java 9 提 ...

  6. java9新特性-22-总结

    1.在java 9 中看不到什么? 1.1 一个标准化和轻量级的JSON API 一个标准化和轻量级的JSON API被许多java开发人员所青睐.但是由于资金问题无法在Java 9中见到,但并不会削 ...

  7. JBoss 系列六十九:CDI 基本概念

    概述 如果说EJB,JPA是之前JEE(JEE5及JEE5之前)中里程碑式的规范,那么在JEE6,JEE7中CDI可以与之媲美,CDI(Contexts and Dependency Injectio ...

  8. Dagger2 入门解析

    前言 在为dropwizard选择DI框架的时候考虑了很久.Guice比较成熟,Dagger2主要用于Android.虽然都是google维护的,但Dagger2远比guice更新的频率高.再一个是, ...

  9. Java Restful Web Service 学习指南

    Restful是一种架构style,目前常说的有restful web service, resultful http.现在热搜榜的微服务,大多数会采用Restful方式. JAX-RS 作为一个Re ...

随机推荐

  1. 菜鸡的Java笔记 日期操作类

    日期操作类        Date 类与 long 数据类型的转换        SimpleDateFormat 类的使用        Calendar 类的使用                如 ...

  2. Python 循环控制

    for循环        Python for循环可以遍历任何序列的项目,如一个列表或者一个字符串        for 变量 in 列表.字典.字符串.函数:            执行语句     ...

  3. [hdu7012]Miserable Faith

    类似于[NOI2021]轻重边的逆过程,操作1即为对$u$​执行access(根为1),$dist(u,v)$​即为$u$​到$v$​的虚边数 对前者用LCT维护,并记录轻重边的切换,显然切换总量为$ ...

  4. [bzoj5415]归程

    首先肯定要预处理出每一个点到1的最短路(别写spfa) 然后以海拔为边权,建一棵kruskal重构树 用倍增找到vi最后一个小于pi的祖先,然后在子树中取min(预处理) 1 #include< ...

  5. 洛谷 P4484 - [BJWC2018]最长上升子序列(状压 dp+打表)

    洛谷题面传送门 首先看到 LIS 我们可以想到它的 \(\infty\) 种求法(bushi),但是对于此题而言,既然题目出这样一个数据范围,硬要暴搜过去也不太现实,因此我们需想到用某种奇奇怪怪的方式 ...

  6. Codeforces 1553I - Stairs(分治 NTT+容斥)

    Codeforces 题面传送门 & 洛谷题面传送门 u1s1 感觉这道题放到 D1+D2 里作为 5250 分的 I 有点偏简单了吧 首先一件非常显然的事情是,如果我们已知了排列对应的阶梯序 ...

  7. 【豆科基因组】大豆适应性位点GWAS分析 [转载]

    目录 材料与方法 结果分析 本文利用99085个高质量SNP 通过STRUCTURE,PCA和neighbour-joining tree的群体结构分析将地方品种分为三个亚群,这些亚群表现出地理上的遗 ...

  8. ping 的原理

    ping 的原理ping 程序是用来探测主机到主机之间是否可通信,如果不能ping到某台主机,表明不能和这台主机建立连接.ping 使用的是ICMP协议,它发送icmp回送请求消息给目的主机.ICMP ...

  9. arm三大编译器的不同选择编译

    ARM 系列目前支持三大主流的工具链,即ARM RealView (armcc), IAR EWARM (iccarm), and GNU Compiler Collection (gcc).     ...

  10. 苹果ios通过描述文件获取udid

    苹果ios通过描述文件获取udid 需要准备的东西 1,安装描述文件只支持https的回调地址,所以需要申请https域名 2,描述文件签名,不安装也可,只要能接受红色的字 步骤: 1,准备xml文件 ...