乐趣区

关于服务器: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

退出移动版