轉自 http://www.ibm.com/developerworks/cn/java/j-lombok/java
什麼時候以及如何爲自定義代碼生成擴展 Lomboknode
Alex Ruiz 在本文中介紹了 Project Lombok,探討了它的一些獨特的編程特點,包括註釋驅動代碼生成,以及簡潔、緊湊、可讀的代碼。而後,他會提示你們關注 Lombok 更有價值的用途:利用自定義 AST(Abstract Syntax Tree,抽象語法樹)轉換來對其進行擴展。擴展 Lombok 使得您能夠生成本身的項目或者域特定樣板代碼,可是,這也確實須要大量的工做。最後 Alex 提供了一些技巧,就是經過簡化流程的關鍵步驟,以及一個自由使用的 JavaBeans 自定義擴展。ios
即便對於保守的 Java™ 開發人員來講,冗長的語法也是 Java 語言編程的一個弱點。雖然有時可經過採用 Groovy 之類的新語言來避免冗長,可是,不少時候採用 Java 編程是最適合的,有時甚至就是這樣要求的。那麼您可能會想要嘗試 Project Lombok,它是個開源的、用於 Java 平臺的代碼生成庫。git
Lombok 能夠方便地減小 Java 應用程序中樣板文件的代碼量,這樣,您就不須要編碼大量的 Java 語法。可是使 Lombok 如此貼心的不僅是語法,它是一種獨特的代碼生成方法,可以開啓全部 Java 開發可能性。程序員
在 本文中,我將介紹 Project Lombok,並說明其優越之處,儘管並不完美,但豐富了 Java 開發人員的工具箱。我將爲你們提供對 Lombok 的概述,包括它的工做方式以及它最適用的場景,並簡單羅列其優缺點。接下來,我將爲你們介紹一個最有用,但也很複雜的 Lombok 用例:將其擴展爲一個自定義代碼基。這多是您本身的代碼或者現有的 Java 模板,它還不屬於 Lombok 庫的一部分。不管哪一種方式,文章的後續部分將側重於擴展 Lombok 的技巧與竅門,包括肯定是否值得在 Lombok API 上花費時間,或者是否可以爲您特定的應用程序更好地編寫樣本文件。編程
所包括的示例代碼(見 下載)擴展 Lombok 來生成 JavaBeans 樣板代碼。這在 Apache 2.0 環境下許可無償使用。安全
也 許選用 Lombok 而不是其餘代碼生成工具的主要緣由就是 Lombok 不只生成 Java 源或者比特代碼:它會經過在編譯階段修改其結構來轉換抽象語法樹(AST)。AST 表明已解析源代碼的樹,它由編譯器建立,與 XML 文件的 DOM 樹模型相似。經過修改(或轉換)AST,Lombok 可對源代碼進行修剪,來避免膨脹,這與純文本代碼生成不一樣。Lombok 所生成的代碼對於同一編譯單元的類是可見的,這不一樣於帶庫的直接字符編碼操做,好比 CGLib 或者 ASM。app
Lombok 支持多個觸發代碼生成的機制,包括了很是流行的 Java 註釋。利用 Java 註釋,開發人員可以修改已註釋的類,這是常規 Java 觸發流程所禁止的。eclipse
關於 Lombok 使用的例子,可參考清單 1 中的類:ide
public class Person { private String firstName; private String lastName; private int age; }
向代碼中增長 equals
、hashCode
、以及 toString
實施並不困難,只是單調乏味而容易出錯。您可採用 Eclipse 之類的現代 Java IDE 來自動生成主要的樣本代碼,可是,那只是部分解決方案。這是節省了時間與精力,但將以犧牲可讀性與可理解性爲代價,由於樣本代碼一般會嚮應用程序源增長干擾詞。
然而,Lombok 有一個很智能的方法來解決樣板代碼問題。以 清單 1 爲例,可經過爲 Person.java
類增長 @Data
註釋,來方便地生成所需的方法。圖 1 展現了 Lombok 在 Eclipse 中的代碼生成。在大綱視圖中,能夠看到在編譯類中展現了所生成的方法,同時源文件仍處於樣文件以外。
Lombok 支持流行的 Java 編譯器 javac 以及 Eclipse Compiler for Java(ECJ)。儘管這兩個編譯器產生相似的輸出,可是他們的實現卻徹底不一樣。結果是, Lombok 自帶兩套註釋處理程序(掛接到 Lombok 中的代碼以及包含的代碼生成邏輯):每一個編譯器一個。幸運的是,這是透明的,所以,做爲用戶,咱們僅需面對一套 Java 註釋。
Lombok 還提供與 Eclipse 的緊密集成:保存 Java 文件會自動觸發 Lombok 的代碼生成(沒有明顯的延遲)並更新 Eclipse 的大綱視圖來展現所生產的成員,如 圖 1 所示。
對於想要 瞭解內部狀況的開發人員,Lombok delombok
工具將爲您提供指導,可經過 Maven 或者 Ant 命令行訪問。Delombok 獲取經過 Lombok 轉換的代碼,並依據它來生成普通的 Java 源文件。「已被 delombok 處理過」 的代碼將會包含以前由 Lombok 所完成的轉換,格式爲普通文本。例如,若是將 delombok
應用到 圖 1 的代碼中,您將可以看到,equals
、hashCode
、以及 toString
已被實施。
在選擇 Lombok 並準備在項目中進行應用以前,您應當知道它有一些限制。其中兩個主要的方面是:
@SneakyThrows
轉換就是個明顯的例子。它容許不在方法定義中聲明所檢查的異常,而將其扔掉,如同它們是未經檢查的異常: // normally, we would need to declare that this method throws Exception @SneakyThrows public void doSomething() { throw new Exception(); }
@GenerateGetter
將可以比當前註釋 @Getter
更好地交流意圖。除了這些 Lombok 相關問題以外,還有一些有關 Eclipse 集成的問題。在大多數狀況下,這是因爲 Eclipse 不瞭解 Lombok 代碼生成狀況所形成的:
NullPointerException
。問題的緣由如今還不清楚。關閉並從新打開 Eclipse 一般就能解決此問題。getName
的代碼,Eclipse 調試工具會跳到字段 name
的註釋 @Getter
。除此以外,當 Lombok 出現時,Eclipse 調試工具會向日常同樣工做。總的說來,這些問題能夠繞過,並且從此其中大部分問題可能會被 Lombok 與 Eclipse dev 團隊所解決。可是,最好對所要應用的技術有所瞭解。這能夠隨時向工具箱中增長新的工具。
Lombok 生成大部分公共 Java 樣本代碼,包括 getters、setters、equals
、以及 hashCode
,僅舉幾個例子。這個頗有用,但有時您還須要生成本身的樣本代碼。例如,Lombok 還不支持一些公共編碼模式,好比 JavaBeans。在有些狀況下,您可能還須要生成指定給項目或者域的代碼。
關 於擴展的最佳用例就是在項目早期階段,利用新的代碼模式來進行原型設計與試驗。這些代碼模式會愈來愈成熟,所以,Lombok 會使其變動或者加強實施變得很簡單:僅需修改註釋處理程序(掛接到 Lombok 中來生成代碼的那部分代碼段)並編譯。全部基本代碼將被自動更新(除非在所生成代碼中的公共約定有變化,致使編譯出錯)一旦這些代碼模式肯定了,就能夠選 擇 delombok
代碼。所以,您就可使用常規 Java 源了。
爲擴展 Lombok,須要識別或者建立將觸發 Lombok 代碼生成的註釋。接下來,將須要爲所肯定的每一個註釋編寫註釋處理程序。註釋處理程序 是實現一對 Lombok 接口以及轉換邏輯的類 — aka 代碼生成。
如下部分包含了一些建議,從項目設置到測試,這些在建立本身的 AST 轉換時可能會頗有用。其中還包括了一些代碼示例,演示了用於支持 JavaBeans 的功能性 Lombok 擴展。後續文章將深刻介紹。
正 如我前面所提到的,Lombok 當前支持公共代碼模式,但並不能徹底涵蓋,包括 JavaBeans。爲了演示 Lombok 擴展,我編寫了一個用於生成 JavaBeans 全程(plumbing)代碼的很是簡單的項目。除了展現如何利用自定義註釋處理程序來爲 javac 與 ECJ 擴展 Lombok,本項目還打包了一些頗有用的工具(好比用於每一個編譯器的字段與方法構建程序),這些工具使得整個流程更清晰、更簡單。
我採用了 Eclipse 3.6(Helios)以及用於版本 0.10-BETA2 的 Lombok git
庫的快照。代碼包含了生成 JavaBean 「綁定」 setters 的。附加的 zip 文件(見 下載 部分)包含如下內容:
@GenerateBoundSetter
與 @GenerateJavaBean
PropertyChangeSupport
字段的生成)附加的代碼 具備完整的功能,並已得到 Apache 2.0 下的許可。可從 GitHub(見 參考資料)得到升級版本的代碼。此處有一個有關代碼功能的快速瀏覽可尋找靈感。
若是在清單 3 中採用個人觸發處理程序編寫代碼,Lombok 將會生成相似清單 4 中的代碼:
@GenerateJavaBean public class Person { @GenerateBoundSetter private String firstName; }
public class Person { public static final String PROP_FIRST_NAME = "firstName"; private String firstName; private PropertyChangeSupport propertySupport = new PropertyChangeSupport(this); public void addPropertyChangeListener(PropertyChangeListener listener) { propertySupport.addPropertyChangeListener(listener); } public void removePropertyChangeListener(PropertyChangeListener listener) { propertySupport.removePropertyChangeListener(listener); } public void setFirstName(String value) { String oldValue = firstName; firstName = value; propertySupport.firePropertyChange(PROP_FIRST_NAME, oldValue, firstName); } }
參閱包含在 示例代碼 中的 readme.txt 文件,來了解如何從示例代碼的構建文件生成 Eclipse 項目。
以 個人觀點看,任何 Lombok 擴展都須要同時支持 javac 與 ECJ,至少如今是這樣。Javac 是 Ant 與 Maven 之類的構建工具所默認採用的編譯器。然而,在寫這篇文章的時候,在與 Lombok 一塊兒使用時,Eclipse 能提供最流暢的編碼體驗。同時支持兩個編譯器,對於提升開發人員的生產效率是相當重要的。
Javac 與 ECJ 採用相似的 AST 結構。不幸的是,他們的部署徹底不一樣,這使得您不得不爲每一個註釋編寫兩個註釋處理程序,一個用於 javac 另外一個用於 ECJ。有個好消息是 Lombok 團隊已經開始了統一 AST API 的相關工做,這將最終實現了在採用兩個編譯器時,只須要爲每一個註釋編寫一個註釋處理程序(見 參考資料)。
接下來須要瞭解將要處理的事情,對此,最好是去查看源代碼。
Lombok 在 javac 與 ECJ 中採用非公共 API 來實現其智能的代碼生成技術。由於代碼將被插入到 Lombok 中,因此即便沒有相同的 API,也應當擁有相似的 API。
非 公共 API 的主要問題是缺乏文檔與可靠性。幸運的是,據 Lombok 團隊說,他們尚未遇到有關新版本 Eclipse(當 Java 7 發佈之後咱們就有機會看到)的兼容性問題。目前,缺少文檔是不得不處理的最大的問題。此外,即便有很好的文檔,學習兩個不一樣編譯器的 API 確實是個艱苦並耗時的任務。咱們須要的是一個有關 javac 與 ECJ 的 「快速而實用的指南」 — 其中一些超出了本文的範圍。
有 一個好消息是,Lombok 團隊已經完成了大量關於利用 javac 與 ECJ 生成 AST 節點的相關文檔工做。強烈建議您閱讀一下他們的代碼。他們提供了最通用的用例:好比變量聲明,方法實施等。閱讀 Lombok 的源代碼是學習 javac 與 ECJ 的 API 的最快捷的方法。清單 5 展現了 Lombok 所擁有的源代碼的示例:
/* final int PRIME = 31; */ { if (!fields.isEmpty() || callSuper) { statements.append(maker.VarDef(maker.Modifiers(Flags.FINAL), primeName, maker.TypeIdent(Javac.getCTCint(TypeTags.class, "INT")), maker.Literal(31))); } }
正如您所見,Lombok 團隊已經記錄了什麼塊產生什麼。下一次須要生成本地變量的聲明時,您能夠回到此源,並以此爲參考。
不要僅限於閱讀 Lombok 的 .java 文件。Lombok 開發人員已經提供了用於設置於構建項目以及用於測試註釋處理程序的指針。如下部分會介紹這些主題的更多細節。
若是嘗試在項目中自動化依賴管理,那麼就很難返回手動方式。Java 體系中有多個構建工具來提供依賴管理,包括 Ivy 與 Maven(見 參考資料)。然而,當建立 Lombok 擴展時,選擇範圍縮小爲一個,而且它是 Ivy。
選 擇 Ivy 的理由之一是全部必要的依賴,例如 javac,都位於 Maven 的中心庫中 — 這就排除了 Maven。另外一個理由是 Ivy 支持 Maven 庫中所沒有的管理依賴。能夠很方便地指定下載依賴的連接。這一配置須要自定義 ivysettings.xml 配置文件,這個比較簡單。
Ivy 位於 Ant 之上,提供對於構建的依賴管理。Lombok 團隊採用他們本身開發的 Ivy 的優化版本,ivyplusplus(見 參考資料)。這一 Ivy 擴展提供了一些有用的 Ant 目標(targets),好比從一系列依賴中建立 Eclipse 與 IntelliJ 項目文件。
要設置 Lombok 擴展項目須要以下文件:
您沒必要作重複的工做。爲節省時間與精力,可關注一下來自 Lombok 的構建文件,或者來自本文 附加資源 與其餘所需的內容。
正如前面所提到的,Lombok 的註釋不只是元數據,它還能很好地完成通訊任務。它們應當指出它們負責觸發一些類型的代碼生成。所以,我強烈建議您將全部 Lombok 相關的註釋放到 「Generate」 前面。在本文的 源代碼 中,我已對觸發 JavaBeans 相關源代碼 @GenerateBoundSetter
與 @GenerateJavaBean
的註釋命名。這一命名規則至少給不熟悉基本代碼的開發人員一個線索,即在構建環境中存在生成代碼的處理過程。
在擴展 Lombok 時,文檔很重要。文檔註釋處理程序將有益於 AST 轉換的維護者,而文檔註釋將有益於其用戶。
採用 javac 或 ECJ API 的代碼閱讀或瞭解起來並不繁瑣。即便其生成最簡單的 Java 代碼,也是複雜與耗時的。文檔記錄註釋處理程序會減輕您和您團隊的維護工做。關於文檔記錄問題,我發現如下內容頗有用:
/** * Instructs lombok to generate the necessary code to make an annotated Java * class a JavaBean. * <p> * For example, given this class: * * <pre> * @GenerateJavaBean * public class Person { * * } * </pre> * our lombok annotation handler (for both javac and eclipse) will generate * the AST nodes that correspond to this code: * * <pre> * public class Person { * * private PropertyChangeSupport propertySupport * = new PropertyChangeSupport(this); * * public void addPropertyChangeListener(PropertyChangeListener l) { * propertySupport.addPropertyChangeListener(l); * } * * public void removePropertyChangeListener(PropertyChangeListener l) { * propertySupport.removePropertyChangeListener(l); * } * } * </pre> * </p> * * @author Alex Ruiz */
// public void setFirstName(String value) { // final String oldValue = firstName; // firstName = value; // propertySupport.firePropertyChange(PROP_FIRST_NAME, oldValue, // firstName); // } JCVariableDecl fieldDecl = (JCVariableDecl) fieldNode.get(); long mods = toJavacModifier(accessLevel) | (fieldDecl.mods.flags & STATIC); TreeMaker treeMaker = fieldNode.getTreeMaker(); List<JCAnnotation> nonNulls = findAnnotations(fieldNode, NON_NULL_PATTERN); return newMethod().withModifiers(mods) .withName(setterName) .withReturnType(treeMaker.Type(voidType())) .withParameters(parameters(nonNulls, fieldNode)) .withBody(body(propertyNameFieldName, fieldNode)) .buildWith(fieldNode);
增長一個與咱們在註釋處理程序中所採用註釋相相似的類級別 Javadoc 註釋(在 清單 6 中),有助於註釋用戶知道並理解當他們使用這些註釋是所發生的狀況。
若是決定同時支持 javac 與 ECJ,這一提示將頗有用。當擁有兩套註釋處理程序時,任何錯誤修正、變動、或者增長都應當對兩套(或分支)同時應用。分支越相似,變動就會越快越安全。這種類似性必須同時出如今包級別與文件級別。
包級別一致性:越多越好,每一個分支(javac 與 ECJ)應當具備同等數量的類,採用相同的名字,如圖 2 所示:
文件級別一致性:由於這兩個分支可能或多或少具備相似數量的類,具備相似的名字,具備相同名字的每對文件中的註釋必須儘可能相似:字段、方法計數、方法名字等等,應當都基本相同。清單 8 展現了用於 javac 和 ECJ 的 generatePropertySupportField
方法。請注意,即便對於不一樣 AST API,這些方法的實現也是很是類似的。
// javac private void generatePropertyChangeSupportField(JavacNode typeNode) { if (fieldAlreadyExists(PROPERTY_SUPPORT_FIELD_NAME, typeNode)) return; JCExpression exprForThis = chainDots(typeNode.getTreeMaker(), typeNode, "this"); JCVariableDecl fieldDecl = newField().ofType(PropertyChangeSupport.class) .withName(PROPERTY_SUPPORT_FIELD_NAME) .withModifiers(PRIVATE | FINAL) .withArgs(exprForThis) .buildWith(typeNode); injectField(typeNode, fieldDecl); } // ECJ private void generatePropertyChangeSupportField(EclipseNode typeNode) { if (fieldAlreadyExists(PROPERTY_SUPPORT_FIELD_NAME, typeNode)) return; Expression exprForThis = referenceForThis(typeNode.get()); FieldDeclaration fieldDecl = newField().ofType(PropertyChangeSupport.class) .withName(PROPERTY_SUPPORT_FIELD_NAME) .withModifiers(PRIVATE | FINAL) .withArgs(exprForThis) .buildWith(typeNode); injectField(typeNode, fieldDecl); }
測試自定義 AST 轉換比您想象的更容易,這要感謝 Lombok 所提供的測試基礎設施。爲說明測試 AST 轉換有多容易,咱們來看一下清單 9 中的 JUnit 測試用例:
import static lombok.DirectoryRunner.Compiler.ECJ; import java.io.File; import lombok.*; import lombok.DirectoryRunner.Compiler; import lombok.DirectoryRunner.TestParams; import org.junit.runner.RunWith; /** * @author Alex Ruiz */ @RunWith(DirectoryRunner.class) public class TestWithEcj implements TestParams { @Override public Compiler getCompiler() { return ECJ; } @Override public boolean printErrors() { return true; } @Override public File getBeforeDirectory() { return new File("test/transform/resource/before"); } @Override public File getAfterDirectory() { return new File("test/transform/resource/after-ecj"); } @Override public File getMessagesDirectory() { return new File("test/transform/resource/messages-ecj"); } }
該測試工做或多或少有點相似下面的狀況:
getBeforeDirectory
指定的文件夾中的全部 Java 文件,採用由 getCompiler
與 Lombok 指定的編譯器。delombok
建立了已編譯類的文本表示。getAfterDirectory
指定的文件夾中的文件。這些文件包含所指望的已編譯類的內容。測試將這些文件的內容與在[第 2 步]中所獲取的文件進行對比。對比的文件必須具備相同的名字。getMessagesDirectory
中指定的文件夾中讀取文件。這些文件包含了所指望的編譯器消息(警告與錯誤)。測試將這些文件的內容與編譯過程當中所展現的實際值相對比,若是編譯 Java 文件則不須要消息文件,不存在所指望的消息。經過名字來匹配。例如,若是編譯 CompleteJavaBean.java
時有指望的編譯器消息,則包含此類消息的文件應當命名爲 CompleteJavaBean.java.messages
。如您所見,這是一個有很大不一樣但頗有效的測試註釋處理程序的方法:
我 所描述的測試在驗證註釋處理程序生成所指望的代碼過程當中頗有用。然而,還須要測試所生成代碼真的完成了您所指望的任務。要驗證所生成代碼特性的正確性,需 要編寫採用您的 AST 轉換的 Java 類,而後編寫測試來檢查所生成代碼的特性。要像代碼是您所編寫的那樣進行測試。
編譯並返回那些測試的最簡單方法是採用 Ant,這意味着利用 javac 來編譯。由於已經測試並瞭解了採用 ECJ 所生成代碼是正確的,因此沒必要在 Eclipse 內部(這會使設置嚴重複雜化)運行這些測試。
我已在本文示例代碼中(見 下載)包含了用於 javac 與 ECJ 註釋處理程序的測試。
Project Lombok 是簡化冗長 Java 代碼的有效工具。它經過以不尋常的智能方法使用 Java 註釋與編譯 API 來實現這一目的。與其餘工具同樣,它並不完美。實現獲益(代碼簡潔化)是要付代價的:Java 代碼失去了其 WYSIWYG 風格,並且,開發者失去了一些喜好的 IDE 功能。在向工具箱中增長 Lombok 以前必定要考慮好它的利弊,肯定所得是否大於所失。
如 果決定採用 Lombok,那就可能會但願對其進行擴展,來生成本身的樣板代碼。目前,雖然擴展 Lombok 並不簡單,但它是可行的。本文提供了一些關於擴展 Lombok 的指導,並描述瞭如何進行操做。花費時間與經從來進行 Lombok 擴展,仍是手工建立樣板代碼,這二者那個更划算您本身決定。