一、概述
1.1 當前現(xiàn)狀
當前JDK中用來表達貨幣的類為java.util.Currency,這個類僅僅能夠表示按照[ISO-4217]描述的貨幣類型。它沒有與之關(guān)聯(lián)的數(shù)值,也不能描述規(guī)范外的一些貨幣。對于貨幣的計算、貨幣兌換、貨幣的格式化沒有提供相關(guān)的支持,甚至連能夠代表貨幣金額的標準類型也沒有提供相關(guān)說明。JSR-354定義了一套標準的API用來解決相關(guān)的這些問題。
1.2 規(guī)范目的
JSR-354主要的目標為:
為貨幣擴展提供可能,支撐豐富的業(yè)務場景對貨幣類型以及貨幣金額的訴求;
提供貨幣金額計算的API;
提供對貨幣兌換匯率的支持以及擴展;
為貨幣和貨幣金額的解析和格式化提供支持以及擴展。
1.3 使用場景
在線商店
商城中商品的單價,將商品加入購物車后,隨著物品數(shù)量而需要計算的總價。在商城將支付方式切換后隨著結(jié)算貨幣類型的變更而涉及到的貨幣兌換等。當用戶下單后涉及到的支付金額計算,稅費計算等。
金融交易網(wǎng)站
在一個金融交易網(wǎng)站上,客戶可以任意創(chuàng)建虛擬投資組合。根據(jù)創(chuàng)建的投資組合,結(jié)合歷史數(shù)據(jù)顯示計算出來的歷史的、當前的以及預期的收益。
虛擬世界和游戲網(wǎng)站
在線游戲會定義它們自己的游戲幣。用戶可以通過銀行卡中的金額去購買游戲幣,這其中就涉及到貨幣兌換。而且因為游戲種類繁多,需要的貨幣類型支持也必須能夠支撐動態(tài)擴展。
銀行和金融應用
銀行等金融機構(gòu)必須建立在匯率、利率、股票報價、當前和歷史的貨幣等方面的貨幣模型信息。通常這樣的公司內(nèi)部系統(tǒng)也存在財務數(shù)據(jù)表示的附加信息,例如歷史貨幣、匯率以及風險分析等。所以貨幣和匯率必須是具有歷史意義的、區(qū)域性的,并定義它們的有效期范圍。
二、JavaMoney解析
2.1 包和工程結(jié)構(gòu)
2.1.1 包概覽
JSR-354 定義了4個相關(guān)包:
(圖2-1 包結(jié)構(gòu)圖)
javax.money包含主要組件如:
CurrencyUnit;
MonetaryAmount;
MonetaryContext;
MonetaryOperator;
MonetaryQuery;
MonetaryRounding ;
相關(guān)的單例訪問者Monetary。
javax.money.convert 包含貨幣兌換相關(guān)組件如:
ExchangeRate;
ExchangeRateProvider;
CurrencyConversion ;
相關(guān)的單例訪問者MonetaryConversions 。
javax.money.format包含格式化相關(guān)組件如:
MonetaryAmountFormat;
AmountFormatContext;
相關(guān)的單例訪問者MonetaryFormats 。
javax.money.spi:包含由JSR-354提供的SPI接口和引導邏輯,以支持不同的運行時環(huán)境和組件加載機制。
2.2.2 模塊概覽
JSR-354源碼倉庫包含如下模塊:
jsr354-api:包含本規(guī)范中描述的基于Java 8的JSR 354 API;
jsr354-ri:包含基于Java 8語言特性的Moneta參考實現(xiàn);
jsr354-tck:包含技術(shù)兼容套件(TCK)。TCK是使用Java 8構(gòu)建的;
javamoney-parent:是org.javamoney下所有模塊的根“POM”項目。這包括RI/TCK項目,但不包括jsr354-api(它是獨立的)。
2.2 核心API
2.2.1 CurrencyUnit
2.2.1.1 CurrencyUnit數(shù)據(jù)模型
CurrencyUnit包含貨幣最小單位的屬性,如下所示:
public interface CurrencyUnit extends Comparable<CurrencyUnit>{
String getCurrencyCode();
int getNumericCode();
int getDefaultFractionDigits();
CurrencyContext getContext();
}
方法getCurrencyCode()返回不同的貨幣編碼?;贗SO Currency規(guī)范的貨幣編碼默認為三位,其他類型的貨幣編碼沒有這個約束。
方法getNumericCode()返回值是可選的。默認可以返回-1。ISO貨幣的代碼必須匹配對應的ISO代碼的值。
defaultFractionDigits定義了默認情況下小數(shù)點后的位數(shù)。CurrencyContext包含貨幣單位的附加元數(shù)據(jù)信息。
2.2.1.2 獲取CurrencyUnit的方式
根據(jù)貨幣編碼獲取
CurrencyUnit currencyUnit = Monetary.getCurrency("USD");
根據(jù)地區(qū)獲取
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數(shù)據(jù)提供者
我們進入Monetary.getCurrency系列方法,可以看到這些方法都是通過獲取MonetaryCurrenciesSingletonSpi.class實現(xiàn)類對應的實例,然后調(diào)用實例對應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默認只有一個實現(xiàn)DefaultMonetaryCurrenciesSingletonSpi。它獲取貨幣集合的實現(xiàn)方式是:所有CurrencyProviderSpi實現(xiàn)類獲取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的數(shù)據(jù)提供者為實現(xiàn)CurrencyProviderSpi的相關(guān)實現(xiàn)類。Moneta提供的默認實現(xiàn)存在兩個提供者,如圖所示;
(圖2-2 CurrencyProviderSpi默認實現(xiàn)類圖)
JDKCurrencyProvider為JDK中[ISO-4217]描述的貨幣類型提供了相關(guān)的映射;
ConfigurableCurrencyUnitProvider為動態(tài)變更CurrencyUnit提供了支持。方法為:registerCurrencyUnit、removeCurrencyUnit等。
因此,如果需要對CurrencyUnit進行相應的擴展,建議按擴展點CurrencyProviderSpi的接口定義進行自定義的構(gòu)造擴展。
2.2.2 MonetaryAmount
2.2.2.1 MonetaryAmount數(shù)據(jù)模型
public interface MonetaryAmount extends CurrencySupplier, NumberSupplier, Comparable<MonetaryAmount>{
//獲取上下文數(shù)據(jù)
MonetaryContext getContext();
//按條件查詢
default <R> R query(MonetaryQuery<R> query){
return query.queryFrom(this);
}
//應用操作去創(chuàng)建貨幣金額實例
default MonetaryAmount with(MonetaryOperator operator){
return operator.apply(this);
}
//獲取創(chuàng)建貨幣金額新實例的工廠
MonetaryAmountFactory<? extends MonetaryAmount> getFactory();
//比較方法
boolean isGreaterThan(MonetaryAmount amount);
......
int signum();
//算法函數(shù)和計算
MonetaryAmount add(MonetaryAmount amount);
......
MonetaryAmount stripTrailingZeros();
}
對應MonetaryAmount提供了三種實現(xiàn)為:FastMoney、Money、RoundedMoney。
(圖2-3 MonetaryAmount默認實現(xiàn)類圖)
FastMoney是為性能而優(yōu)化的數(shù)字表示,它表示的貨幣數(shù)量是一個整數(shù)類型的數(shù)字。Money內(nèi)部基于java.math.BigDecimal來執(zhí)行算術(shù)操作,該實現(xiàn)能夠支持任意的precision和scale。RoundedMoney的實現(xiàn)支持在每個操作之后隱式地進行舍入。我們需要根據(jù)我們的使用場景進行合理的選擇。如果FastMoney的數(shù)字功能足以滿足你的用例,建議使用這種類型。
2.2.2.2 創(chuàng)建MonetaryAmount
根據(jù)API的定義,可以通過訪問MonetaryAmountFactory來創(chuàng)建,也可以直接通過對應類型的工廠方法來創(chuàng)建。如下;
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內(nèi)部基于java.math.BigDecimal,因此它也具有BigDecimal的算術(shù)精度和舍入能力。默認情況下,Money的內(nèi)部實例使用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方法進行相互的轉(zhuǎn)換,方法如下;
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方法進行相互的轉(zhuǎn)換,方法如下;
FastMoney fastMoney = FastMoney.of(144, "CNY");
Money money = Money.from(fastMoney);
fastMoney = FastMoney.from(money);
2.2.2.3 MonetaryAmount的擴展
雖然Moneta提供的關(guān)于MonetaryAmount的三種實現(xiàn):FastMoney、Money、RoundedMoney已經(jīng)能夠滿足絕大多數(shù)場景的需求。JSR-354為MonetaryAmount預留的擴展點提供了更多實現(xiàn)的可能。
我們跟進一下通過靜態(tài)方法Monetary.getAmountFactory(ClassamountType)獲取MonetaryAmountFactory來創(chuàng)建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擴展點的實現(xiàn)類通過方法getAmountFactory來獲得MonetaryAmountFactory。
Moneta的實現(xiàn)方式中MonetaryAmountsSingletonSpi的唯一實現(xiàn)類為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());
}
......
}
最后可以發(fā)現(xiàn)MonetaryAmountFactory的獲取是通過擴展點MonetaryAmountFactoryProviderSpi通過調(diào)用createMonetaryAmountFactory生成的。
所以要想擴展實現(xiàn)新類型的MonetaryAmount,至少需要提供擴展點MonetaryAmountFactoryProviderSpi的實現(xiàn),對應類型的AbstractAmountFactory的實現(xiàn)以及相互關(guān)系的維護。
默認MonetaryAmountFactoryProviderSpi的實現(xiàn)和對應的AbstractAmountFactory的實現(xiàn)如下圖所示;
(圖2-4 MonetaryAmountFactoryProviderSpi默認實現(xiàn)類圖)
(圖2-5 AbstractAmountFactory默認實現(xiàn)類圖)
2.2.3 貨幣金額計算相關(guān)
從MonetaryAmount的接口定義中可以看到它提供了常用的算術(shù)運算(加、減、乘、除、求模等運算)計算方法。同時定義了with方法用于支持基于MonetaryOperator運算的擴展。MonetaryOperators類中定義了一些常用的MonetaryOperator的實現(xiàn):
1)ReciprocalOperator用于操作取倒數(shù);
2)PermilOperator用于獲取千分比例值;
3)PercentOperator用于獲取百分比例值;
4)ExtractorMinorPartOperator用于獲取小數(shù)部分;
5)ExtractorMajorPartOperator用于獲取整數(shù)部分;
6)RoundingMonetaryAmountOperator用于進行舍入運算;
同時繼承MonetaryOperator的接口有CurrencyConversion和MonetaryRounding。其中CurrencyConversion主要與貨幣兌換相關(guān),下一節(jié)作具體介紹。MonetaryRounding是關(guān)于舍入操作的,具體使用方式如下;
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 的方式,其結(jié)果對應的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的擴展實現(xiàn)類通過調(diào)用對應的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();
}
}
默認實現(xiàn)中MonetaryRoundingsSingletonSpi的唯一實現(xiàn)類為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;
}
根據(jù)上述代碼可以得知MonetaryRounding主要來源于RoundingProviderSpi擴展點實現(xiàn)類的getRounding方法來獲取。JSR-354默認實現(xiàn)Moneta中DefaultRoundingProvider提供了相關(guān)實現(xiàn)。如果需要實現(xiàn)自定義的Rounding策略,按照RoundingProviderSpi定義的擴展點進行即可。
2.3 貨幣兌換
2.3.1 貨幣兌換使用說明
上一節(jié)中有提到MonetaryOperator還存在一類貨幣兌換相關(guān)的操作。如下實例所示為常用的使用貨幣兌換的方式;
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通過靜態(tài)方法MonetaryConversions.getConversion來獲取。方法中根據(jù)MonetaryConversionsSingletonSpi的實現(xiàn)調(diào)用getConversion來獲得。
而方法getConversion是通過獲取對應的ExchangeRateProvider并調(diào)用getCurrencyConversion實現(xiàn)的;
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的實現(xiàn)中MonetaryConversionsSingletonSpi只有唯一的實現(xiàn)類DefaultMonetaryConversionsSingletonSpi。
ExchangeRateProvider的獲取如下所示依賴于ExchangeRateProvider的擴展實現(xiàn);
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默認提供的實現(xiàn)有:
CompoundRateProvider
IdentityRateProvider
(圖2-6 ExchangeRateProvider默認實現(xiàn)類圖)
因此,建議的擴展貨幣兌換能力的方式為實現(xiàn)ExchangeRateProvider,并通過SPI的機制加載。
2.4 格式化
2.4.1 格式化使用說明
格式化主要包含兩部分的內(nèi)容:對象實例轉(zhuǎn)換為符合格式的字符串;指定格式的字符串轉(zhuǎn)換為對象實例。通過MonetaryAmountFormat實例對應的format和parse來分別執(zhí)行相應的轉(zhuǎn)換。如下代碼所示;
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 格式化擴展
格式化的使用關(guān)鍵點在于MonetaryAmountFormat的構(gòu)造。MonetaryAmountFormat主要創(chuàng)建獲取方式為MonetaryFormats.getAmountFormat。看一下相關(guān)的源碼;
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();
}
}
相關(guān)代碼說明MonetaryAmountFormat的獲取依賴于MonetaryFormatsSingletonSpi的實現(xiàn)對應調(diào)用getAmountFormat方法。
MonetaryFormatsSingletonSpi的默認實現(xiàn)為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的相關(guān)實現(xiàn),并作為一個擴展點提供出來。默認的擴展實現(xiàn)方式為DefaultAmountFormatProviderSpi。
如果我們需要擴展注冊自己的格式化處理方式,建議采用擴展MonetaryAmountFormatProviderSpi的方式。
2.5 SPI
JSR-354提供的服務擴展點有;
(圖2-7 服務擴展點類圖)
1)處理貨幣類型相關(guān)的CurrencyProviderSpi、MonetaryCurrenciesSingletonSpi;
2)處理貨幣兌換相關(guān)的MonetaryConversionsSingletonSpi;
3)處理貨幣金額相關(guān)的MonetaryAmountFactoryProviderSpi、MonetaryAmountsSingletonSpi;
4)處理舍入相關(guān)的RoundingProviderSpi、MonetaryRoundingsSingletonSpi;
5)處理格式化相關(guān)的MonetaryAmountFormatProviderSpi、MonetaryFormatsSingletonSpi;
6)服務發(fā)現(xiàn)相關(guān)的ServiceProvider;
除了ServiceProvider,其他擴展點上文都有相關(guān)說明。JSR-354規(guī)范提供了默認實現(xiàn)DefaultServiceProvider。利用JDK自帶的ServiceLoader,實現(xiàn)面向服務的注冊與發(fā)現(xiàn),完成服務提供與使用的解耦。加載服務的順序為按類名進行排序的順序;
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的實現(xiàn)中也提供了一種實現(xiàn)PriorityAwareServiceProvider,它可以根據(jù)注解@Priority指定服務接口實現(xiàn)的優(yōu)先級。
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 數(shù)據(jù)加載機制
針對一些動態(tài)的數(shù)據(jù),比如貨幣類型的動態(tài)擴展以及貨幣兌換匯率的變更等。Moneta提供了一套數(shù)據(jù)加載機制來支撐對應的功能。默認提供了四種加載更新策略:從fallback URL獲取,不獲取遠程的數(shù)據(jù);啟動的時候從遠程獲取并且只加載一次;首次使用的時候從遠程加載;定時獲取更新。針對不同的策略使用不同的加載數(shù)據(jù)的方式。分別對應如下代碼中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方法通過觸發(fā)監(jiān)聽器來完成數(shù)據(jù)的加載。而監(jiān)聽器實際上調(diào)用的是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類似,只是放在另外的線程去異步執(zhí)行:
public Future<Boolean> loadDataAsync(final String resourceId) {
return executors.submit(() -> defaultLoaderServiceFacade.loadData(resourceId, resources));
}
loadDataRemote通過調(diào)用LoadableResource的loadRemote來加載數(shù)據(jù)。
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加載數(shù)據(jù)的方式為;
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;
}
定時執(zhí)行的方案與上述類似,采用了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 貨幣類型擴展
當前業(yè)務場景下需要支持v鉆、鼓勵金、v豆等多種貨幣類型,而且隨著業(yè)務的發(fā)展貨幣類型的種類還會增長。我們需要擴展貨幣類型而且還需要貨幣類型數(shù)據(jù)的動態(tài)加載機制。按照如下步驟進行擴展:
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,并且在文件中添加如下內(nèi)容;
com.vivo.finance.javamoney.spi.VFCurrencyProvider
3)java-money.defaults.VFC路徑下添加文件currency.json,文件內(nèi)容如下;
[{
"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實現(xiàn)
CurrencyProviderSpi和LoaderService.LoaderListener,用于擴展貨幣類型和實現(xiàn)擴展的貨幣類型的數(shù)據(jù)加載。其中包含的數(shù)據(jù)解析類VFCurrencyReadingHandler,數(shù)據(jù)模型類VFCurrency等代碼省略。對應的實現(xiàn)關(guān)聯(lián)類圖為;
(圖2-8 貨幣類型擴展主要關(guān)聯(lián)實現(xiàn)類圖)
關(guān)鍵實現(xiàn)為數(shù)據(jù)的加載,代碼如下;
@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 貨幣兌換擴展
隨著貨幣類型的增加,在充值等場景下對應的貨幣兌換場景也會隨之增加。我們需要擴展貨幣兌換并需要貨幣兌換匯率相關(guān)數(shù)據(jù)的動態(tài)加載機制。如貨幣的擴展方式類似,按照如下步驟進行擴展:
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,并且在文件中添加如下內(nèi)容;
com.vivo.finance.javamoney.spi.VFCExchangeRateProvider
java-money.defaults.VFC路徑下添加文件currencyExchangeRate.json,文件內(nèi)容如下;
[{
"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并實現(xiàn)LoaderService.LoaderListener。對應的實現(xiàn)關(guān)聯(lián)類圖為;
(圖2-9 貨幣金額擴展主要關(guān)聯(lián)實現(xiàn)類圖)
3.3 使用場景案例
假設(shè)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);
四、總結(jié)
JavaMoney為金融場景下使用貨幣提供了極大的便利。能夠支撐豐富的業(yè)務場景對貨幣類型以及貨幣金額的訴求。特別是Monetary、MonetaryConversions、MonetaryFormats作為貨幣基礎(chǔ)能力、貨幣兌換、貨幣格式化等能力的入口,為相關(guān)的操作提供了便利。同時也提供了很好的擴展機制方便進行相關(guān)的改造來滿足自己的業(yè)務場景。
文中從使用場景出發(fā)引出JSR 354需要解決的主要問題。通過解析相關(guān)工程的包和模塊結(jié)構(gòu)說明針對這些問題JSR 354及其實現(xiàn)是如果去劃分來解決這些問題的。然后從相關(guān)API來說明針對相應的貨幣擴展,金額計算,貨幣兌換、格式化等能力它是如何來支撐以及使用的。以及介紹了相關(guān)的擴展方式意見建議。接著總結(jié)了相關(guān)的SPI以及對應的數(shù)據(jù)加載機制。最后通過一個案例來說明針對特定場景如何擴展以及應用對應實現(xiàn)。
作者:vivo互聯(lián)網(wǎng)服務器團隊-Hou Xiaobi