如果對於Java開發時,經常須面對Getters、Setters、equals等樣版(Boilerplate)程式碼感到厭煩,Lombok是個便捷的方案,只要類別路徑包含JAR,在原始碼加入幾個標註,就可以自動生成對應的方法,無需額外的容器,就能夠立即使用。對於這麼方便的東西,身為好奇的開發者,當然要打開原始碼,來研究一下背後的原理。

編譯時期標註處理

談到Java最令人討厭的一件事,就是面對POJO(Plain Old Java Object)的那些Getter、Setter了。是的,整合開發工具(IDE)都提供自動產生Getter、Setter程式碼的功能,不過問題不只在於自動產生,閱讀原始碼時,這類樣版程式碼會形成一種干擾,在修改或增刪值域之後,就算有重構工具的輔助,清理這類原始碼也是個麻煩。

使用Lombok,我們可在值域上標註@Getter、@Setter,就能自動產生對應的方法;想指定某些值域來產生對應的toString方法,可用@ToString;要產生equals與hashCode,能用@EqualsAndHashCode;若是建構式,就用@AllArgsConstructor等標註。如果連這些標註都懶得加,只需在類別上標註@Data,就能搞定一切;若想要一個值物件,就標註@Value(就像不產生Setter的@Data),這通常被做為一個Immutable物件來使用。

同時,Lombok與整合開發工具的程式碼自動產生功能,並不相同,因為既有的原始碼不會增加任何內容。然而,引用被Lombok標註的類別時,在編譯時期就能呼叫自動產生的方法。也就是說,這一切都是在編譯時期處理好了,Lombok標註的類別在編譯時期,就產生了對應的方法,儲存在.class之中,而Lombok本身也附帶delombok,能將加過料的.class反組譯回.java檔案。

那麼,Lombok提供了自己的編譯器嗎?不是的,在javac編譯時,只要包含了Lombok的JAR檔案,就可以了(想整合整合開發工具,如Eclipse,可以透過JAR來安裝plugin),這是因為JAR檔案中,META-INF/services裏的javax.annotation.processing.Processor指定了Lombok的標註處理器(Annotation Processor)類別,而javac在完成原始碼的剖析之後,若發現標註處理器的部份,就會介入處理。

認識JSR269

自Java 5導入了標註(Annotation)之後,整個Java生態圈大量運用它,來簡化Java程式碼的撰寫、隱藏細節、突顯關切點,而這早就不是什麼新鮮事了。常見的標註應用是在執行時期,運用反射API取得標註資訊,載入類別生成實例,甚至是動態生成代理物件來做對應的處理。

就標註本身而言,其實預設是只將標註資訊儲存於.class檔案,可被編譯器或位元碼分析工具讀取;甚至Java本身提供的@SuppressWarnings、@Override標註,還被設定為RetentionPolicy.SOURCE,而且,標註資訊只用在編譯時期,.class不會留下標註資訊,執行時期也就無法讀取標註資訊;若要能於執行時期讀取,反而須特別搭配RetentionPolicy.RUNTIME,就這點來看,當初Java導入標註功能時,似乎比較傾向將標註用於編譯器等靜態工具。

那麼,如何在編譯時期讀取標註資訊?像AspectJ那樣改寫個編譯器?

在Java 5導入標註時,其實就提供了個標註處理工具(Annotation Processing Tool,APT),我們可以使用非標準的com.sun.mirror等API,來撰寫註解處理器,再透過apt工具程式,於靜態時期處理標註。

接著,在Java 6中,納入了JSR269:Pluggable Annotation Processing API,將標註處理器的API(套件為javax.annotation.processing、javax.lang.model等)標準化了,而Java 7將apt工具與原先的非標準API,標示為廢棄(deprecated),等到後續在Java 8中,正式移除apt與com.sun.mirror等相關API。

具體來說,現在開發者可繼承AbstractProcessor,使用@SupportedSourceVersion指定Java版本,@SupportedAnnotationTypes指定要處理的標註類型名稱,並定義process方法來處理標註,而在javac編譯時,若使用-processor或--processor-path指定標註處理器來源,或者在類別路徑包含的JAR中,META-INF裡面,存在如同上述的javax.annotation.processing.Processor設定,在編譯器剖析、生成語法樹之後,若原始碼出現了指定要處理的標註,就會載入標註處理器並執行process方法。

JSR269的應用

Lombok的標註處理器就運用了JSR269,主要是定義在lombok.javac.apt.LombokProcessor(https://goo.gl/h8QUs9),其中,運用了非標準的Java Compiler Tree API(om.sun.tools.javac等API)修改語法樹,之後,將修改過的結果交由編譯器分析、產生位元組碼,並儲存為.class。

運用Java Compiler Tree API本身來說,是比較複雜的,而且,它不是公開的標準API,直接修改語法樹來改變位元組碼的輸出,也像是在改變Java的語法,Lombok本身甚至有lombok.val、lombok.var型態,可以撰寫如下的程式碼:

var x = new User();

做為第三方工具程式庫,卻改變語言規則,不少開發者認為這是不明智的決定。事實上,這就與Java 10的var語法發生衝突,直接修改語法樹來改變位元組碼的輸出。在〈Don’t use Lombok〉(https://goo.gl/DYy9jt),也談到一些隱憂,例如,誤解或誤用Lombok的行為,或因四處呼叫產生的方法,在決定不使用Lombok時,反而造成許多問題,delombok出來的原始碼也變得醜陋,難以閱讀。

然而,我們還是可以使用JSR269,來實作編譯時期檢查工具,或者是程式碼產生器。

例如,在處理某個標註時,在某些條件下拋出例外,以中斷編譯過程,像是實作個@MyOverride來模仿標準的@Override行為;或者是在發現某些標註時,結合JavaPoet、JavaWriter之類的Java原始碼產生程式庫,自動生成相關的工具類別原始碼,若必要,也可以使用標準的Java Compiler API,來進一步對產生的原始碼進行編譯。

Google的AutoValue程式庫(https://goo.gl/j6T66h)就是基於JSR269,可自動為被標註類別,產生具有Getter、Setter等方法的原始碼;而對於值物件,也可以使用Immutables程式庫(https://goo.gl/WgRph3),它也是基於JSR269來產生Immutable物件建構器的原始碼。

改變位元組是好是壞,或許也是要看做法,例如,AspectJ也是改變位元組碼,不過是用來織入橫切主要流程的關切點,而被織入的流程是個可抽換的服務,不影響應用程式的主要功能,也不增加額外的方法,相對來說,就不會有預期之外的行為。

研究JSR269的實作

基本上,在某些專案上,適當地運用Lombok,還是有幫助的。至少對付POJO那些樣版方法時,使用@Data之類的標註,相對來說省事許多。全面採用Lombok,或許會有些隱憂,然而從對Java語言進行Hack的角度來看,Lombok是一套滿有趣、具有想像力的程式庫。

除了使用Lombok、AutoValue、Immutables之外,有機會的話,可以看看它們的原始碼是如何實作,對於靜態時期如何善用標準處理來輔助開發,會有不少的心得,在搭配執行時期的標註處理上,手邊可用的工具就會更加豐富了。

專欄作者

熱門新聞

Advertisement