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

2021-12-14 10:14:23 浏览数 (1)

一、概述

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包含货币最小单位的属性,如下所示:

代码语言:txt复制
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的方式

根据货币编码获取

代码语言:txt复制
CurrencyUnit currencyUnit = Monetary.getCurrency("USD");

根据地区获取

代码语言:txt复制
CurrencyUnit currencyUnitChina = Monetary.getCurrency(Locale.CHINA);

按查询条件获取

代码语言:txt复制
CurrencyQuery cnyQuery =             CurrencyQueryBuilder.of().setCurrencyCodes("CNY").setCountries(Locale.CHINA).setNumericCodes(-1).build();
Collection<CurrencyUnit> cnyCurrencies = Monetary.getCurrencies(cnyQuery);

获取所有的CurrencyUnit;

代码语言:txt复制
Collection<CurrencyUnit> allCurrencies = Monetary.getCurrencies();
2.2.1.3 CurrencyUnit数据提供者

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

代码语言:txt复制
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集合取并集。

代码语言:txt复制
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数据模型
代码语言:txt复制
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来创建,也可以直接通过对应类型的工厂方法来创建。如下;

代码语言:txt复制
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初始化。并且支持指定的方式;

代码语言:txt复制
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方法进行相互的转换,方法如下;

代码语言:txt复制
org.javamoney.moneta.Money.defaults.mathContext=DECIMAL128

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

代码语言:txt复制
org.javamoney.moneta.Money.defaults.precision=256
org.javamoney.moneta.Money.defaults.roundingMode=HALF_EVEN

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

代码语言:txt复制
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实例的方式;

代码语言:txt复制
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的方法为;

代码语言:txt复制
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是关于舍入操作的,具体使用方式如下;

代码语言:txt复制
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()的值,比如;

代码语言:txt复制
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,并进行相应的操作;

代码语言:txt复制
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方法来完成。如下所示按条件查询的方式;

代码语言:txt复制
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的方式如下;

代码语言:txt复制
@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还存在一类货币兑换相关的操作。如下实例所示为常用的使用货币兑换的方式;

代码语言:txt复制
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进行相应的货币兑换;

代码语言:txt复制
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实现的;

代码语言:txt复制
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的扩展实现;

代码语言:txt复制
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来分别执行相应的转换。如下代码所示;

代码语言:txt复制
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。看一下相关的源码;

代码语言:txt复制
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,对应的获取方法如下;

代码语言:txt复制
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,实现面向服务的注册与发现,完成服务提供与使用的解耦。加载服务的顺序为按类名进行排序的顺序;

代码语言:txt复制
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指定服务接口实现的优先级。

代码语言:txt复制
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对应的处理方式;

代码语言:txt复制
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方法。

代码语言:txt复制
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类似,只是放在另外的线程去异步执行:

代码语言:txt复制
public Future<Boolean> loadDataAsync(final String resourceId) {
    return executors.submit(() -> defaultLoaderServiceFacade.loadData(resourceId, resources));
}

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

代码语言:txt复制
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加载数据的方式为;

代码语言:txt复制
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做定时器,如下所示;

代码语言:txt复制
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中添加如下配置;

代码语言:txt复制
{-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,并且在文件中添加如下内容;

代码语言:txt复制
com.vivo.finance.javamoney.spi.VFCurrencyProvider

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

代码语言:txt复制
[{
  "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 货币类型扩展主要关联实现类图)

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

代码语言:txt复制
@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中添加如下配置;

代码语言:txt复制
{-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,并且在文件中添加如下内容;

代码语言:txt复制
com.vivo.finance.javamoney.spi.VFCExchangeRateProvider

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

代码语言:txt复制
[{
  "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钻,需要校验支付金额和充值金额是否合法。可以使用如下方式校验;

代码语言:txt复制
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

0 人点赞