使用 Eclipse Modeling Framework 進行建模,第 2 部分

使用 Eclipse 的 Java Emitter Templates 生成代碼

級別: 中級

Adrian Powell
資深軟件開發人員, IBM
2004 年 6 月

Eclipse 的 Java Emitter Templates(JET) 是一個開放源代碼工具,可以在 Eclipse Modeling Framework(EMF)中生成代碼。 JET 與 JSP 非常類似,不同之處在於 JET 功能更強大,也更靈活,可以生成 Java、 SQL 和任何其他語言的代碼,包括 JSP。本文將介紹如何創建和配置 JET,並將其部署到各種環境中。

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 聲明中使用 startTagendTag 屬性實現,如清單 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 生成代碼,這包括允許用戶定製代碼,以及集成以域或方法甚至更細粒度級別的修改,從而允許重新生成代碼。我們還會將它們都綁定到一個插件中,從而展示一種將生成的代碼集成到開發過程的方法。

參考資料

關於作者
Adrian Powell 從剛加入 VisualAge for Java Enterprise Tooling 小組開始使用 IBM 的 Java 開發工具,在這兒他花費了兩年的時間來手工編寫一個代碼生成器。從那以後,他一直從事於 Eclipse 和 VisualAge for Java 中的工具和插件的開發,他現在幾乎爲 Eclipse 和 VisualAge for Java 的每一個版本都開發了這種工具和插件。Adrian 目前在 Vancouver Centre for IBM e-Business Innovation 工作,在這兒他正在開發替代軟件。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章