天天看點

java script綜合項目_Rec:一個半月,一個Java項目的誕生

Rec是一個用來驗證和轉換資料檔案的Java應用。從第一行代碼到v1版本成形,僅僅經曆了一個半月的時間,作為一個開源項目,在很多方面都有着各種各樣的糾結。

需求

Rec的需求源自于我們團隊所做項目的特殊性:遺留系統遷移。在工作中,我們需要跟各種團隊打交道,每天處理各種來自ETL(Extract、Transform、Load)過程中的資料和程式問題,而整個ETL程式運作起來過于笨重,并且還要考慮準備後端資料和各種驗證問題,非常不友善。

其實在此之前,隻要有一些簡單的程式跑起來、能夠進行一些簡單的檢查,比如唯一性(uniqueness)、關聯關系等等,就可以在很大程度上減少我們在ETL過程中花費的時間。并且,這半年多來的實踐也證明了這一點。

最初,同僚的建議是寫一個腳本檔案來解決這個問題,這對于程式員來說當然不是什麼大問題。但随着使用次數的增加,我漸漸發現一套Python腳本并不能勝任:一方面,面對複雜的業務場景,很難有一套靈活的模式去比對所有的資料格式;另一方面,随着資料量的增長,性能也成了一個大問題。

于是我開始着手設計和實作Rec。

設計

Rec第一個可用版本的設計共花了七天的時間,基本上具備了我期望的各種能力:

可以自定義資料格式

能夠進行簡單的唯一性和關聯關系驗證

支援一些擴充的查詢文法:比如,可以驗證多字段組合的唯一性

性能上基本能夠勝任

Rec面向的資料檔案格式是類CSV的檔案,包括其他的一些使用分号(;)或者豎線(|)來做分隔符的檔案。出于習慣,檔案的Parser并沒有選取現成的庫,而是我自己按照Wikipedia和RFC4180的規範寫出來的,基本上能夠解析所有類似的檔案。而且還有一個意外的發現:用空格做分隔符的檔案(比如,某些日志)也是可以支援的。

對于每一條資料,Rec提供了兩部分元件,一部分是資料本身,另一部分是該資料的通路器(accessor)。通路器提供把字段名轉換成對應資料項下标的功能:跟Spring Batch中的FieldSetMapper很像,當然在其之上還多了一層文法糖。

一個典型的accessor format如下:

first name, last name, {5}, phone, …, job title,{3}

其中,“…”表示中間的字段全部可以忽略,{3}和{5}是占位符,表示在這些字段之間有如此多個字段也是可以忽略的。而由“…”分割成的兩部分也是有差異的:在其後的字段使用的是類似Python的負數下标;換句話說,我并不需要知道本來的資料有多少個字段,隻需要知道我要擷取的倒數第幾個是什麼就可以了。

Rec的驗證規則也是從簡設計。由于最初的需求隻有唯一性檢查和關聯關系檢查,是以第一個版本裡面就隻加入了這兩個功能,文法如下:

unique: Customer[id]

unique: Order[cust_id, prod_id]

exist: Order.prod_id, Product.id

每一行表示一個規則,冒号前面是規則的名字,後面是規則所需要驗證的資料查詢表達式。對于查詢表達式,這裡需要提一點,本來是設計了更多的功能,比如過濾群組合等等,在後面擴充的時候發現在文法上很難實作得更直覺而且友善使用,于是就決定改用嵌入腳本引擎的方式來解決。

另外Rec第一個版本釋出隻有Kotlin運作時的依賴,是以完整的Jar檔案隻有2MB。同時,隻要給對應的資料檔案提供.rec格式的描述檔案,再在同一目錄建立一個default.rule來加入各種檢驗規則,就可以運作、然後得到你想要的結果了。

擴充

Rec的第一個版本在某些方面是達到預期結果了的。但在那之後就發現了一些很重要的問題:首先,我們另一層的需求并沒有得到滿足:Rec能夠幫我們驗證并且找到有問題的資料,但是不能夠按需來選擇我們想要的内容;其次,在檢查資料的同時,我們也隐含地有內建和轉換資料的需求,Rec也不能夠滿足。

于是第一個星期以後我開始考慮對Rec進行擴充。首先是在同僚的建議下把亂成一坨的代碼分成多個module;其次考慮加入前面提到的過濾和格式轉換的功能。

第一個步驟勉強算是完成了,但是卡在了第二步上:對于轉換的規則,要不要和驗證的規則放在一起?如何對這兩種規則做區分?如何在過濾器中設計變量引用等細節?每一個問題都讓我糾結了很多,直到最後決定放棄這一步,直接通過引入腳本引擎來實作:從最初hack Kotlin編譯器的嵌入版,到決定用JavaScript,到放棄Nashorn轉而用Rhino,中間雖然輾轉幾次又遭遇了不少坑,但畢竟有成熟的社群經驗輔以指導,還是順利地走了下來。

Test Driven Development vs Test Driven Design

其實直到現在Rec的測試也隻有少量的一些。而且在拆分子產品的時候,因為測試代碼之間的依賴比較多,并沒有做拆分,是以基本上還是集中在一個子產品中。當然這也是很多時候我自己做項目時的一個習慣:并不會完全以TDD的方式來開發,而是把單元測試作為一個驗證設計思路的手段。因為很多時候思路轉變的太突然,不實作的話估計下一秒鐘就完全變了。而且,作為一個簡單的工具類程式,并不需要重度面向對象的設計,如何規劃和設計流暢易用的接口就成了必須考慮的一個問題。這個時候測試的設計性變得更明顯。

另外,對于Parser這種東西,測試是必不可少的,但是要TDD一個Parser出來,基本上就是在給自己找活幹了。是以這種時候,我會先加一些基本的case,來確定能夠正常的實作功能,然後再引入一些比較corner的case來確定實際的可用性。對我來說,這是完全沒有問題的:當然後面的實踐驗證了這一點,Rec沒在解析檔案方面出現過任何問題。

Kotlin vs Java(Script)

最初采用Kotlin就是因為它有很多優點,而且這些優點也确實影響了Rec的設計,但是因為各種原因,還是被替換了兩次。首先遲遲不釋出的1.1版本和編碼相容性的諸多問題,導緻我決定用原生Java換掉Kotlin。當然,這也導緻了不得不強行舍棄很多好用的編譯期檢查和文法糖,以及一個用來做bean mapping的元件。

至于采用JavaScript,則是另外一個問題。

衆所周知,JSR223定義了一套JVM平台的腳本引擎規範,但是作為一個強靜态類型的編譯型語言,Kotlin想要契合這套規範還是很困難的,于是無論是官方的實作還是Rec的解決方法,都不是很好:

首先你要啟動一個JVM來執行這個腳本的動作;在這個動作裡面,啟動第二個JVM要調用Kotlin的編譯器來将該腳本編譯成class;然後這個編譯器會再去利用自定義的classloader來加載和執行這個class檔案。當所有的功能都集中在一個Jar檔案裡面的時候,每次都要選擇指定classpath等選項,實作非常複雜。而且,由于第二次執行的Kotlin編譯器是識别不到你已引入的kotlin-reflect類庫的(因為已經統一包裝到rec的jar包裡面去了),就會導緻腳本中bean mapper的一些功能根本不能使用。萬般無奈,選擇采用更成熟的JS引擎。

當然選擇JS帶來的一個好處就是,有更多人可以拿來就用了,而且,最新的Rhino提供了CommonJS擴充,能夠順手require所需的JS檔案,在複用和子產品化方面也能夠有不少提升。

技術抉擇

除了部分Parser相關的代碼外,Rec采用的基本都是不可變的資料結構:一方面是因為使用Kotlin;另一方面,在整個模型裡面并沒有特别的需求會涉及更改資料。唯一的擔心是記憶體占用,但是後來發現這部分擔心也是不必要的,因為所有記憶體的瓶頸隻在資料檔案的Parser上,項目中的資料條目動辄數十個資料項,幾十萬條資料,再加上每次parse都會把一個字元串分割成多個,最後再合并到一個大的集合裡面,在最開始設計的時候沒有考慮這一點,輕輕松松就爆了JVM堆。這也是後期需要着重優化的一個方面。

另外一個點是關于異常處理。對于Java應用來說這是個巨坑:異常本身并沒有問題,但是由于checked和unchecked的區分以及衆多設計哲學的不同,是以就成了争議點所在。在這裡我參考了Joe Duffy的做法。對于嚴重的不可重試的錯誤,比如檔案找不到,空指針異常,下标錯誤等,直接讓程式die(沒錯,就是PHP中的那個die),至于資料格式錯誤等問題,更多的做法是做一條記錄然後選擇繼續。當然這一套東西并不依賴Java的異常系統,隻是作為一個設計原則來應用,畢竟這不是一個App server,并不需要高可用的保障,相反這種fail fast的直接回報更有助于發現和解決問題。

在類型系統上,最初實作Rec的語言是Kotlin,它提供了一套比Java略微進階一些的類型系統。當然主要的點還是在于nullable:在功能上,nullable與Java 8的Optional類似,用來容納可以為空的值,同時能夠有效避免空指針異常;在實作上,比Java略微高出了一點的是,非nullable的對象必須被初始化并且不容許為null。這直接解決了Optional對象為空的尴尬問題。

當然,由于運作時的依賴還是無法避免地使用JVM,而且沒有自定義值類型的支援,在使用Kotlin,特别是跟Java标準庫和其他架構結合使用的時候,還是會遇到空指針的坑。但是在這一點上,Kotlin給我們開了個好頭,比如在後面convert到Java的過程中,我也盡量保證各種對象都是final并且被非空初始化了的。

結語

當然也許很多人會說,Unix那套工具用的很順手的話,上面說的這些都不是問題,其實Rec本來的思路也是來自于它們:accessor來自于awk的列操作模式,scripting中的過濾器來自于sed和grep,鍊式調用源于管道。Rec也隻是在這些思路之上加了一些友善的操作而已。但是對于我個人來說,這種折騰其實是在檢驗我自己的理論和思考,更别說還提升了項目的生産力。也許哪一天實在受不了了,還可以拿C++和Lua重寫了呢。畢竟,生命不息,折騰不止。

硬廣的最後,歡迎來貢獻文檔、issue、PR、星星和轉發分享:rec-framework/rec-core

文/ThoughtWorks劉清 (更多精彩洞見,請關注微信公衆号:思特沃克)