Java Emitter Templates(JET)概述
開發人員通常都使用一些工具來生成常用的代碼。Eclipse 用戶可能對一些標准的工 具非常熟悉,這些工具可以為選定的屬性生成 for(;;) 循環, main() 方法, 以及選定 屬性的訪問方法。將這些簡單而機械的任務變得自動化,可以加快編程的速度,並簡化編 程的過程。在某些情況中,例如為 J2EE 服務器生成部署代碼,自動生成代碼就可以節省 大量時間,並可以隱藏具體實現特有的一些復雜性,這樣就可以將程序部署到不同的 J2EE 服務器上。自動生成代碼的功能並不只是為開發大型工具的供應商提供的,在很多 項目中都可以使用這種功能來提高效率。Eclipse 的 JET 被包裝為 EMF 的一部分,可以 簡單而有效地向項目中添加自動生成的代碼。本文將介紹在各種環境中如何使用 JET 。
JET 是什麼?
JET 與 JSP 非常類似:二者使用相同的語法,實際上在後台都被編譯成 Java 程序; 二者都用來將呈現頁面與模型和控制器分離開來;二者都可以接受輸入的對象作為參數, 都可以在代碼中插入字符串值(表達式),可以直接使用 Java 代碼執行循環、聲明變量 或執行邏輯流程控制(腳本);二者都可以很好地表示所生成對象的結構,(Web 頁面、 Java 類或文件),而且可以支持用戶的詳細定制。
JET 與 JSP 在幾個關鍵的地方存在區別。在 JET 中,可以變換標記的結構來支持在 不同的語言中生成代碼。通常 JET 程序的輸入都是一個配置文件,而不是用戶的輸入( 當然也不禁止這樣使用)。而且對於一個給定的工作流來說,JET 通常只會執行一次。這 並不是技術上的限制;您可以看到 JET 有很多完全不同的用法。
開始
創建模板
要使用 JET,創建一個新 Java 項目 JETExample ,並將源文件夾設置為 src 。為了 讓 JET 啟用這個項目,請點擊鼠標右鍵,然後選擇 Add JET Nature。這樣就會在新項目 的根目錄下創建一個 templates 目錄。JET 的缺省配置使用項目的根目錄來保存編譯出 來的 Java 文件。要修改這種設置,打開該項目的 properties 窗口,選擇 JET Settings,並將 source container 設置為 src 。這樣在運行 JET 編譯器時,就會將編 譯出來的 JET Java 文件保存到這個正確的源文件夾中。
現在我們已經准備好創建第一個 JET 了。JET 編譯器會為每個 JET 都創建一個 Java 源文件,因此習慣上是將模板命名為 NewClass.javajet ,其中 NewClass 是要生成的類 名。雖然這種命名方式不是強制的,但是這樣可以避免產生混亂。
首先在模板目錄中創建一個新文件 GenDAO.javajet 。這樣系統會出現一個對話框, 警告您在這個新文件的第 1 行第 1 列處有編譯錯誤。如果您詳細地看以下警告信息,就 會發現它說 "The jet directive is missing"(沒有 jet 指令)。雖然這在技術上沒有 什麼錯誤,因為我們剛才只不過是創建了一個空文件,但是這個警告信息卻很容易產生混 亂並誤導我們的思路。單擊 'OK' 關閉警告對話框,然後單擊 'Cancel' 清除 New File 對話框(這個文件已經創建了)。為了防止再次出現這種問題,我們的首要問題是創建 jet 指令。
每個 JET 都必須以 jet 指令開始。這樣可以告訴 JET 編譯器編譯出來的 Java 模板 是什麼樣子(並不是模板生成了什麼內容,而是編譯生成的模板類是什麼樣子;請原諒, 這個術語有些容易讓人迷惑)。此處還要給出一些標准的 Java 類信息。例如,在下面這 個例子中使用了以下信息:
清單 1. 樣例 jet 聲明
<%@ jet
package="com.ibm.pdc.example.jet.gen"
class="GenDAO"
imports="java.util.* com.ibm.pdc.example.jet.model.*"
%>
清單 1 的內容是真正自解釋的。在編譯 JET 模板時,會創建一個 Java 文件 GenDAO ,並將其保存到 com.ibm.pdc.example.jet.gen 中,它將導入指定的包。重復一遍,這 只是說明模板像什麼樣子,而不是模板將要生成的內容 -- 後者稍後將會介紹。注意 JET 輸出結果的 Java 文件名是在 jet 的聲明中定義的,它並不局限於這個文件名。如果兩 個模板聲明了相同的類名,那麼它們就會相互影響到對方的變化,而不會產生任何警告信 息。如果您只是拷貝並粘貼模板文件,而沒有正確地修改所有的 jet 聲明,那就可能出 現這種情況。因為在模板目錄中創建新文件時會產生警告,而拷貝和粘貼是非常常見的, 因此要自己小心這個問題。
JSP 可以通過預先聲明的變量(例如會話、錯誤、上下文和請求)獲取信息, JET 與 此類似,也可以使用預先聲明的變量向模板傳遞信息。JET 只使用兩個隱式的變量: stringBuffer ,其類型為 StringBuffer (奇怪吧?),它用來在調用 generate() 時 構建輸出字符串;以及一個參數,出於方便起見,我們稱之為 argument ,它是 Object 類型。典型的 JET 模板的第一行會將其轉換為一個更適合的類,如清單 2 所示。
清單 2. JET 參數的初始化
<% GenDBModel genDBModel = (GenDBModel)argument; %>
package <%= genDBModel.getPackageName() %>;
正如您可以看到的一樣,JET 的缺省語法與 JSP 相同:使用 <%...%> 包括代 碼,使用 <%= ... %> 打印表達式的值。與 JSP 類似,正確地使用 <% ... % > 標簽就可以添加任何邏輯循環或結構,就像是在任何 Java 方法中一樣。例如:
清單 3. 腳本和表達式
Welcome <%= user.getName() %>!
<% if ( user.getDaysSinceLastVisit() > 5 ) { %>
Whew, thanks for coming back. We thought we'd lost you!
<% } else { %>
Back so soon? Don't you have anything better to do?
<% } %>
在定義完 JET 之後,保存文件並在包浏覽器中在這個文件上點擊鼠標右鍵,選擇 Compile Template。如果一切正常,就會在 com.ibm.pdc.example.jet.gen 包中創建一 個類 GenDAO 。其中只有一個方法 public String generate(Object argument) (參見 清單 4),這樣做的結果就是在 javajet 模板中定義的內容。
清單 4. 一個基本的 JET 編譯後的 Java 類,其功能是打印 "Hello <% =argument%>"
package com.ibm.pdc.example.jet.gen;
import java.util.*;
public class GenDAO
{
protected final String NL = System.getProperties().getProperty ("line.separator");
protected final String TEXT_1 = NL + "Hello, ";
protected final String TEXT_2 = NL + "\t ";
public String generate(Object argument)
{
StringBuffer stringBuffer = new StringBuffer();
stringBuffer.append(TEXT_1);
stringBuffer.append( argument );
stringBuffer.append(TEXT_2);
return stringBuffer.toString();
}
}
編寫好模板之後,您可能就會注意到一些公共的元素,這些元數會反復出現,例如所 有生成的代碼中都添加的版權信息。在 JSP 中,這是通過 include 聲明處理的。將所有 想要添加的內容都放到一個文件中,並將該文件命名為 'copyright.inc',然後在 javajet 模板中添加 <%@ include file="copyright.inc" %> 語句。所指定的包 含文件會被添加到編譯後的輸出結果中,因此它可以引用到現在為止已經聲明的任何變量 。擴展名 .inc 可以任意,只是不要采用以 jet 或 JET 結尾的名字,否則將試圖編譯包 含文件,這樣該文件的理解性自然很差。
定制 JET 編譯
如果只使用包含文件還不能滿足要求,您可能會想添加其他一些方法,或者對代碼生 成過程進行定制;最簡單的方法是創建一個新的 JET 骨架。骨架文件就是描述編譯後的 JET 模板樣子的一個模板。缺省的骨架如清單 5 所示。
清單 5. 缺省的 JET 骨架
public class CLASS
{
public String generate(Object argument)
{
return "";
}
}
所有的 import 語句都位於最開始, CLASS 會被替換為在 jet 聲明的 class 屬性中 設置的類名, generate() 方法的代碼會被替換為執行生成操作的代碼。因此,要修改編 譯後的模板代碼的樣子,我們只需要創建一個新的骨架文件並進行自己想要的定制即可, 但是仍然要在原來的地方保留基本的元素。
要創建一個定制的骨架,在 custom.skeleton 模板目錄中創建一個新文件,如清單 6 所示。
清單 6. 定制 JET 骨架
public class CLASS
{
private java.util.Date getDate() {
return new java.util.Date();
}
public String generate(Object argument) {
return "";
}
}
然後在想要使用這個定制骨架的任何 JET 模板中,向 javajet 文件中的 jet 聲明添 加 skeleton="custom.skeleton" 屬性。
或者,也可以使用它對基類進行擴充,例如 public class CLASS extends MyGenerator ,並在基類中添加所有必要的幫助器方法。這樣可能會更加整潔,因為它保 留了代碼的通用性,並可以簡化開發過程,因為 JET 編譯器並不能總是給出最正確的錯 誤消息。
定制骨架也可以用來修改方法名和 generate() 方法的參數列表,這樣非常挑剔的開 發人員就可以任意定制模板。說 JET 要將 generate() 的代碼替換為要生成的代碼,其 實有些不太准確。實際上,它只會替換在骨架中聲明的最後一個方法的代碼,因此如果粗 心地修改骨架的代碼,就很容易出錯,而且會讓您的同事迷惑不解。
使用 CodeGen
正如您可以看到的一樣,模板一旦編譯好之後,就是一個標准的 Java 類。要在程序 中使用這個類,只需要分發編譯後的模板類,而不需要分發 javajet 模板。或者,您可 能希望讓用戶可以修改模板,並在啟動時自動重新編譯模板。EMF 可以實現這種功能,任 何需要這種功能或對此感興趣的人都可以進入 plugins/org.eclipse.emf.codegen.ecore/templates 中,並修改 EMF 生成模型或編輯 器的方式。
如果您只是希望可以只分發編譯後的模板類,那麼編譯過程可以實現自動化。迄今為 止,我們只看到了如何使用 JET Eclipse 插件來編譯 JET 模板,但實際上我們可以編寫 一些腳本來實現這種功能,或者將生成代碼的工作作為一項 ANT 任務。
運行時編譯模板
要讓最終用戶可以定制模板(以及對模板的調試),可以選擇在運行時對模板進行編 譯。實現這種功能有幾種方法,首先我們使用一個非常有用的類 org.eclipse.emf.codegen.jet.JETEmitter ,它可以對細節進行抽象。常見的(但通常 是錯誤的)代碼非常簡單,如清單 7 所示。
清單 7. JETEmitter 的簡單用法(通常是錯誤的)
String uri = "platform:/templates/MyClass.javajet";
JETEmitter jetEmitter = new JETEmitter( uri );
String generated = jetEmitter.generate( new NullProgressMonitor(), new Object[]{argument} );
如果您試圖在一個標准的 main() 方法中運行這段代碼,就會發現第一個問題。 generate() 方法會觸發一個 NullPointerException 異常,因為 JETEmitter 假設自己 正被一個插件調用。在初始化過程中,它將調用 CodeGenPlugin.getPlugin ().getString() ,這個函數會失敗,因為 CodeGenPlugin.getPlugin() 為空。
解決這個問題有一個簡單的方法:將這段代碼放到一個插件中,這樣的確可以管用, 但卻不是完整的解決方法。現在 JETEmitter 的實現創建了一個隱藏項目 .JETEmitters ,其中包含了所生成的代碼。然而, JETEmitter 並不會將這個插件的 classpath 添加 到這個新項目中,因此,如果所生成的代碼引用了任何標准 Java 庫之外的對象,都將不 能成功編譯。2.0.0 版本初期似乎解決了這個問題,但是到 4 月初為止,這還沒有完全 實現。要解決這個問題,必須對 JETEmitter 類進行擴充,使其覆蓋 initialize() 方法 ,並將其加入您自己的 classpath 項中。Remko Popma 已經編寫了很好的一個例子 jp.azzurri.jet.article2.codegen.MyJETEmitter ,這個例子可以處理這個問題,在 JET 增加這種正確的特性之前都可以使用這種方法。修改後的代碼如清單 8 所示。
清單 8. 正確的 JETEmitter 調用
String base = Platform.getPlugin (PLUGIN_ID).getDescriptor().getInstallURL().toString();
String uri = base + "templates/GenTestCase.javajet";
MyJETEmitter jetEmitter = new MyJETEmitter( uri );
jetEmitter.addClasspathVariable( "JET_EXAMPLE", PLUGIN_ID);
String generated = jetEmitter.generate( new NullProgressMonitor(),
new Object[]{genClass} );
命令行
在命令行中編譯 JET 非常簡單,不會受到 classpath 問題的影響,這個問題會使編 譯一個 main() 方法都非常困難。在上面這種情況中,難點並不是將 javajet 編譯成 Java 代碼,而是將這個 Java 代碼編譯成 .class 。在命令行中,我們可以更好地控制 classpath,這樣可以分解每個步驟,最終再組合起來,就可以使整個工作順利而簡單。 唯一一個技巧是我們需要以一種 "無頭" 模式(沒有用戶界面)來運行 Eclipse,但即便 是這個問題也已經考慮到了。要編譯 JET,請查看一下 plugins/org.eclipse.emf.codegen_1.1.0/test 。這個目錄中包含了 Windows 和 Unix 使用的腳本,以及一個要驗證的 JET 例子。
作為一個 ANT 任務執行
有一個 ANT 任務 jetc ,它要麼可以采用一個 template 屬性,要麼對多個模板有一 個 fileset 屬性。一旦配置好 jetc 任務的 classpath 之後,模板的編譯就與標准的 Java 類一樣簡單。
定制 JET 以生成 JSP
最終,JET 使用 "<%" 和 "%>" 來標記模板,然而這與 JSP 使用的標記相同。 如果您希望生成 JSP 程序,那就只能修改定界符。這可以在模板開頭的 jet 聲明中使用 startTag 和 endTag 屬性實現,如清單 9 所示。在這種情況中,我使用 "[%" 和 "%]" 作為開始定界符和結束定界符。正如您可以看到的一樣, "[%= expression %]" 可以正 確處理,就像前面的 "<%= expression %>" 一樣。
清單 9. 修改標簽後的 JET 模板
<%@ jet
package="com.ibm.pdc.example.jet.gen"
class="JspGen"
imports="java.util.* "
startTag = "[%"
endTag = "%]"
%>
[% String argValue = (String)argument; %]
package [%= argValue %];
結束語
有一個不幸的事實:很多代碼都是通過拷貝/粘貼而實現重用的,不管是大型軟件還是 小型軟件都是如此。很多時候這個問題並沒有明顯的解決方案,即使面向對象語言也不能 解決問題。在重復出現相同的基本代碼模式而只對實現稍微進行了一些修改的情況中,將 通用的代碼放到一個模板中,然後使用 JET 來生成各種變化,這是一種很好的節省時間 和精力的辦法。JSP 早已采用了這種方法,因此 JET 可以從 JSP 的成功中借鑒很多東西 。JET 使用與 JSP 相同的基本布局和語義,但是允許更靈活的定制。為了實現更好的控 制,模板可以進行預編譯;為了實現更高的靈活性,也可以在運行時編譯和分發。
在本系列的下一篇文章中,我們將介紹如何為 Prime Time 生成代碼,這包括允許用 戶定制代碼,以及集成以域或方法甚至更細粒度級別的修改,從而允許重新生成代碼。我 們還會將它們都綁定到一個插件中,從而展示一種將生成的代碼集成到開發過程的方法。