文章從使用場景出發引出JSR 354需要解決的主要問題。通過解析相關工程的包和子產品結構說明針對這些問題JSR 354及其實作是如果去劃分來解決這些問題的。
目前JDK中用來表達貨币的類為java.util.Currency,這個類僅僅能夠表示按照[ISO-4217]描述的貨币類型。它沒有與之關聯的數值,也不能描述規範外的一些貨币。對于貨币的計算、貨币兌換、貨币的格式化沒有提供相關的支援,甚至連能夠代表貨币金額的标準類型也沒有提供相關說明。JSR-354定義了一套标準的API用來解決相關的這些問題。
JSR-354主要的目标為:
為貨币擴充提供可能,支撐豐富的業務場景對貨币類型以及貨币金額的訴求;
提供貨币金額計算的API;
提供對貨币兌換匯率的支援以及擴充;
為貨币和貨币金額的解析和格式化提供支援以及擴充。
線上商店
商城中商品的單價,将商品加入購物車後,随着物品數量而需要計算的總價。在商城将支付方式切換後随着結算貨币類型的變更而涉及到的貨币兌換等。當使用者下單後涉及到的支付金額計算,稅費計算等。
金融交易網站
在一個金融交易網站上,客戶可以任意建立虛拟投資組合。根據建立的投資組合,結合曆史資料顯示計算出來的曆史的、目前的以及預期的收益。
虛拟世界和遊戲網站
線上遊戲會定義它們自己的遊戲币。使用者可以通過銀行卡中的金額去購買遊戲币,這其中就涉及到貨币兌換。而且因為遊戲種類繁多,需要的貨币類型支援也必須能夠支撐動态擴充。
銀行和金融應用
銀行等金融機構必須建立在匯率、利率、股票報價、目前和曆史的貨币等方面的貨币模型資訊。通常這樣的公司内部系統也存在财務資料表示的附加資訊,例如曆史貨币、匯率以及風險分析等。是以貨币和匯率必須是具有曆史意義的、區域性的,并定義它們的有效期範圍。
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接口和引導邏輯,以支援不同的運作時環境群組件加載機制。
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(它是獨立的)。
CurrencyUnit包含貨币最小機關的屬性,如下所示:
方法getCurrencyCode()傳回不同的貨币編碼。基于ISO Currency規範的貨币編碼預設為三位,其他類型的貨币編碼沒有這個限制。
方法getNumericCode()傳回值是可選的。預設可以傳回-1。ISO貨币的代碼必須比對對應的ISO代碼的值。
defaultFractionDigits定義了預設情況下小數點後的位數。CurrencyContext包含貨币機關的附加中繼資料資訊。
根據貨币編碼擷取
根據地區擷取
按查詢條件擷取
擷取所有的CurrencyUnit;
我們進入Monetary.getCurrency系列方法,可以看到這些方法都是通過擷取MonetaryCurrenciesSingletonSpi.class實作類對應的執行個體,然後調用執行個體對應getCurrency方法。
接口MonetaryCurrenciesSingletonSpi預設隻有一個實作DefaultMonetaryCurrenciesSingletonSpi。它擷取貨币集合的實作方式是:所有CurrencyProviderSpi實作類擷取CurrencyUnit集合取并集。
是以,CurrencyUnit的資料提供者為實作CurrencyProviderSpi的相關實作類。Moneta提供的預設實作存在兩個提供者,如圖所示;
(圖2-2 CurrencyProviderSpi預設實作類圖)
JDKCurrencyProvider為JDK中[ISO-4217]描述的貨币類型提供了相關的映射;
ConfigurableCurrencyUnitProvider為動态變更CurrencyUnit提供了支援。方法為:registerCurrencyUnit、removeCurrencyUnit等。
是以,如果需要對CurrencyUnit進行相應的擴充,建議按擴充點CurrencyProviderSpi的接口定義進行自定義的構造擴充。
對應MonetaryAmount提供了三種實作為:FastMoney、Money、RoundedMoney。
(圖2-3 MonetaryAmount預設實作類圖)
FastMoney是為性能而優化的數字表示,它表示的貨币數量是一個整數類型的數字。Money内部基于java.math.BigDecimal來執行算術操作,該實作能夠支援任意的precision和scale。RoundedMoney的實作支援在每個操作之後隐式地進行舍入。我們需要根據我們的使用場景進行合理的選擇。如果FastMoney的數字功能足以滿足你的用例,建議使用這種類型。
根據API的定義,可以通過通路MonetaryAmountFactory來建立,也可以直接通過對應類型的工廠方法來建立。如下;
由于Money内部基于java.math.BigDecimal,是以它也具有BigDecimal的算術精度和舍入能力。預設情況下,Money的内部執行個體使用MathContext.DECIMAL64初始化。并且支援指定的方式;
Money與FastMoney也可以通過from方法進行互相的轉換,方法如下;
同時可以指定精度和舍入模式;
雖然Moneta提供的關于MonetaryAmount的三種實作:FastMoney、Money、RoundedMoney已經能夠滿足絕大多數場景的需求。JSR-354為MonetaryAmount預留的擴充點提供了更多實作的可能。
我們跟進一下通過靜态方法Monetary.getAmountFactory(ClassamountType)擷取MonetaryAmountFactory來建立MonetaryAmount執行個體的方式;
如上代碼所示,需要通過MonetaryAmountsSingletonSpi擴充點的實作類通過方法getAmountFactory來獲得MonetaryAmountFactory。
Moneta的實作方式中MonetaryAmountsSingletonSpi的唯一實作類為DefaultMonetaryAmountsSingletonSpi,對應的擷取MonetaryAmountFactory的方法為;
最後可以發現MonetaryAmountFactory的擷取是通過擴充點MonetaryAmountFactoryProviderSpi通過調用createMonetaryAmountFactory生成的。
是以要想擴充實作新類型的MonetaryAmount,至少需要提供擴充點MonetaryAmountFactoryProviderSpi的實作,對應類型的AbstractAmountFactory的實作以及互相關系的維護。
預設MonetaryAmountFactoryProviderSpi的實作和對應的AbstractAmountFactory的實作如下圖所示;
(圖2-4 MonetaryAmountFactoryProviderSpi預設實作類圖)
(圖2-5 AbstractAmountFactory預設實作類圖)
從MonetaryAmount的接口定義中可以看到它提供了常用的算術運算(加、減、乘、除、求模等運算)計算方法。同時定義了with方法用于支援基于MonetaryOperator運算的擴充。MonetaryOperators類中定義了一些常用的MonetaryOperator的實作:
1)ReciprocalOperator用于操作取倒數;
2)PermilOperator用于擷取千分比例值;
3)PercentOperator用于擷取百分比例值;
4)ExtractorMinorPartOperator用于擷取小數部分;
5)ExtractorMajorPartOperator用于擷取整數部分;
6)RoundingMonetaryAmountOperator用于進行舍入運算;
同時繼承MonetaryOperator的接口有CurrencyConversion和MonetaryRounding。其中CurrencyConversion主要與貨币兌換相關,下一節作具體介紹。MonetaryRounding是關于舍入操作的,具體使用方式如下;
還可以使用預設的舍入方式以及指定CurrencyUnit 的方式,其結果對應的scale為currencyUnit.getDefaultFractionDigits()的值,比如;
一般情況下進行舍入操作是按位進1,針對某些類型的貨币最小機關不為1,比如瑞士法郎最小機關為5。針對這種情況,可以通過屬性cashRounding為true,并進行相應的操作;
通過MonetaryRounding的擷取方式,我們可以了解到都是通過MonetaryRoundingsSingletonSpi的擴充實作類通過調用對應的getRounding方法來完成。如下所示按條件查詢的方式;
預設實作中MonetaryRoundingsSingletonSpi的唯一實作類為DefaultMonetaryRoundingsSingletonSpi,它擷取MonetaryRounding的方式如下;
根據上述代碼可以得知MonetaryRounding主要來源于RoundingProviderSpi擴充點實作類的getRounding方法來擷取。JSR-354預設實作Moneta中DefaultRoundingProvider提供了相關實作。如果需要實作自定義的Rounding政策,按照RoundingProviderSpi定義的擴充點進行即可。
上一節中有提到MonetaryOperator還存在一類貨币兌換相關的操作。如下執行個體所示為常用的使用貨币兌換的方式;
也可用通過先擷取ExchangeRateProvider,然後再擷取CurrencyConversion進行相應的貨币兌換;
CurrencyConversion通過靜态方法MonetaryConversions.getConversion來擷取。方法中根據MonetaryConversionsSingletonSpi的實作調用getConversion來獲得。
而方法getConversion是通過擷取對應的ExchangeRateProvider并調用getCurrencyConversion實作的;
Moneta的實作中MonetaryConversionsSingletonSpi隻有唯一的實作類DefaultMonetaryConversionsSingletonSpi。
ExchangeRateProvider的擷取如下所示依賴于ExchangeRateProvider的擴充實作;
ExchangeRateProvider預設提供的實作有:
CompoundRateProvider
IdentityRateProvider
(圖2-6 ExchangeRateProvider預設實作類圖)
是以,建議的擴充貨币兌換能力的方式為實作ExchangeRateProvider,并通過SPI的機制加載。
格式化主要包含兩部分的内容:對象執行個體轉換為符合格式的字元串;指定格式的字元串轉換為對象執行個體。通過MonetaryAmountFormat執行個體對應的format和parse來分别執行相應的轉換。如下代碼所示;
格式化的使用關鍵點在于MonetaryAmountFormat的構造。MonetaryAmountFormat主要建立擷取方式為MonetaryFormats.getAmountFormat。看一下相關的源碼;
相關代碼說明MonetaryAmountFormat的擷取依賴于MonetaryFormatsSingletonSpi的實作對應調用getAmountFormat方法。
MonetaryFormatsSingletonSpi的預設實作為DefaultMonetaryFormatsSingletonSpi,對應的擷取方法如下;
可以看出來最終還是依賴于MonetaryAmountFormatProviderSpi的相關實作,并作為一個擴充點提供出來。預設的擴充實作方式為DefaultAmountFormatProviderSpi。
如果我們需要擴充注冊自己的格式化處理方式,建議采用擴充MonetaryAmountFormatProviderSpi的方式。
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,實作面向服務的注冊與發現,完成服務提供與使用的解耦。加載服務的順序為按類名進行排序的順序;
Moneta的實作中也提供了一種實作PriorityAwareServiceProvider,它可以根據注解@Priority指定服務接口實作的優先級。
針對一些動态的資料,比如貨币類型的動态擴充以及貨币兌換匯率的變更等。Moneta提供了一套資料加載機制來支撐對應的功能。預設提供了四種加載更新政策:從fallback URL擷取,不擷取遠端的資料;啟動的時候從遠端擷取并且隻加載一次;首次使用的時候從遠端加載;定時擷取更新。針對不同的政策使用不同的加載資料的方式。分别對應如下代碼中NEVER、ONSTARTUP、LAZY、SCHEDULED對應的處理方式;
loadDataLocal方法通過觸發監聽器來完成資料的加載。而監聽器實際上調用的是newDataLoaded方法。
loadDataAsync和loadDataLocal類似,隻是放在另外的線程去異步執行:
loadDataRemote通過調用LoadableResource的loadRemote來加載資料。
LoadableResource加載資料的方式為;
定時執行的方案與上述類似,采用了JDK自帶的Timer做定時器,如下所示;
目前業務場景下需要支援v鑽、鼓勵金、v豆等多種貨币類型,而且随着業務的發展貨币類型的種類還會增長。我們需要擴充貨币類型而且還需要貨币類型資料的動态加載機制。按照如下步驟進行擴充:
1)javamoney.properties中添加如下配置;
2)META-INF.services路徑下添加檔案javax.money.spi.CurrencyProviderSpi,并且在檔案中添加如下内容;
3)java-money.defaults.VFC路徑下添加檔案currency.json,檔案内容如下;
4)添加類VFCurrencyProvider實作
CurrencyProviderSpi和LoaderService.LoaderListener,用于擴充貨币類型和實作擴充的貨币類型的資料加載。其中包含的資料解析類VFCurrencyReadingHandler,資料模型類VFCurrency等代碼省略。對應的實作關聯類圖為;
(圖2-8 貨币類型擴充主要關聯實作類圖)
關鍵實作為資料的加載,代碼如下;
随着貨币類型的增加,在充值等場景下對應的貨币兌換場景也會随之增加。我們需要擴充貨币兌換并需要貨币兌換匯率相關資料的動态加載機制。如貨币的擴充方式類似,按照如下步驟進行擴充:
javamoney.properties中添加如下配置;
META-INF.services路徑下添加檔案javax.money.convert.ExchangeRateProvider,并且在檔案中添加如下内容;
java-money.defaults.VFC路徑下添加檔案currencyExchangeRate.json,檔案内容如下;
添加類VFCExchangeRateProvider
繼承AbstractRateProvider并實作LoaderService.LoaderListener。對應的實作關聯類圖為;
(圖2-9 貨币金額擴充主要關聯實作類圖)
假設1人民币可以兌換100v豆,1人民币可以兌換1v鑽,目前場景下使用者充值100v豆對應支付了1v鑽,需要校驗支付金額和充值金額是否合法。可以使用如下方式校驗;
JavaMoney為金融場景下使用貨币提供了極大的便利。能夠支撐豐富的業務場景對貨币類型以及貨币金額的訴求。特别是Monetary、MonetaryConversions、MonetaryFormats作為貨币基礎能力、貨币兌換、貨币格式化等能力的入口,為相關的操作提供了便利。同時也提供了很好的擴充機制友善進行相關的改造來滿足自己的業務場景。
文中從使用場景出發引出JSR 354需要解決的主要問題。通過解析相關工程的包和子產品結構說明針對這些問題JSR 354及其實作是如果去劃分來解決這些問題的。然後從相關API來說明針對相應的貨币擴充,金額計算,貨币兌換、格式化等能力它是如何來支撐以及使用的。以及介紹了相關的擴充方式意見建議。接着總結了相關的SPI以及對應的資料加載機制。最後通過一個案例來說明針對特定場景如何擴充以及應用對應實作。
作者:vivo網際網路伺服器團隊-Hou Xiaobi
分享 vivo 網際網路技術幹貨與沙龍活動,推薦最新行業動态與熱門會議。