关于服务器:JavaMoney规范JSR-354与对应实现解读

一、概述

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

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理