写点什么

JavaMoney 规范 (JSR 354) 与对应实现解读

  • 2021 年 12 月 14 日
  • 本文字数:16935 字

    阅读完需:约 56 分钟

一、概述


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=256org.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 的方式如下;


@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;}
复制代码


根据上述代码可以得知 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 货币类型扩展主要关联实现类图)


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


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

发布于: 3 小时前阅读数: 8
用户头像

官方公众号:vivo互联网技术,ID:vivoVMIC 2020.07.10 加入

分享 vivo 互联网技术干货与沙龙活动,推荐最新行业动态与热门会议。

评论

发布
暂无评论
JavaMoney规范(JSR 354)与对应实现解读