概述
本系列文章的 前一篇介紹了有關 Eclipse 的 Java Emitter Templates (JET)和代碼生成的知識,在那篇文章中,您已經看到如何通過使用模板和代碼生成器 來節省時間,並實現模式級的代碼重用。然而在大部分情況中,這都還不夠。您需要能夠 將所生成的代碼插入現有的代碼中,或者允許以後的開發人員來定制所生成的代碼,而不 需要在重新生成代碼時重新編寫任何內容。理想情況下,代碼生成器的創建者希望可以支 持今後開發人員所有的需求:從修改方法的實現、修改各種方法簽名,到修改所生成類的 繼承結構。這是一個非常有趣的問題,目前還沒有很好的通用解決方案;但是有一個很好 的純 Java 的解決方案,稱為 JMerge。
JMerge 是 EMF 中包含的一個開放源代碼的工具,可以讓您定制所生成的模型和編輯 器,而重新生成的代碼不會損壞已經修改過的內容。如果描述了如何將新生成的代碼合並 到現有定制過的代碼中,那麼 JETEmitter 就可以支持 JMerge。本文通過一個例子來展 示其中的一些可用選項。
第一步
假設您已經添加了一個新項目,在這個項 目中需要為編寫的每個類都創建一個 JUnit 測試類,這樣必須要對編寫的每個方法都進 行測試。作為一個認真且高效的(或者比較懶的)程序員來說,您決定要編寫一個插件, 它接受一個 Java 類作為輸入,並生成 JUnit 測試例子的存根(stub)。您熱情高漲地 創建了 JET 和插件,現在想允許用戶定制所生成的測試類;然而在原有類的接口發生變 化時,仍然需要重新生成代碼。要實現這種功能,可以使用 JMerge。
從插件中調 用 JMerge 的代碼非常簡單(參見清單 1)。這會創建一個新的 JMerger 實例,以及一 個 URI merge.xml,設置要合並的來源和目標,並調用 merger.merge() 。然後合並的內 容就可以展開為 merger.getTargetCompilationUnit() 。
清單 1. 調用 JMerge
// ...
JMerger merger = getJMerger();
// set source
merger.setSourceCompilationUnit(
merger.createCompilationUnitForContents(generated));
// set target
merger.setTargetCompilationUnit(
merger.createCompilationUnitForInputStream(
new FileInputStream (target.getLocation().toFile())));
// merge source and target
merger.merge();
// extract merged contents
InputStream mergedContents = new ByteArrayInputStream(
merger.getTargetCompilationUnit().getContents().getBytes());
// overwrite the target with the merged contents
target.setContents (mergedContents, true, false, monitor);
// ...
// ...
private JMerger getJMerger() {
// build URI for merge document
String uri =
Platform.getPlugin (PLUGIN_ID).getDescriptor().getInstallURL().toString();
uri += "templates/merge.xml";
JMerger jmerger = new JMerger();
JControlModel controlModel = new JControlModel( uri );
jmerger.setControlModel( controlModel );
return jmerger;
}
要啟動這個過程,可以使用清單 2 這個簡單的 merge.xml。其中聲明了 <merge> 標簽,以及缺省的命名空間聲明。這段代碼最主要的部分在 merge:pull 元素中。此處,源類中每個方法的代碼都會被替換為目標類的對應方法的代碼。如果一個 方法在目標類不存在,就會被創建。如果一個方法只在源類中存在,而在目標類不存在, 就會被保留。
清單 2. 一個非常簡單的 merge.xml
<?xml version="1.0" encoding="UTF-8"?>
<merge:options xmlns:merge=
"http://www.eclipse.org/org/eclipse/emf/codegen/jmerge/Options">
<merge:pull
sourceGet="Method/getBody"
targetPut="Method/setBody"/>
</merge:options>
區分 生成的方法
這種簡單的方法有一個非常明顯的問題:每次修改源類並重新生成代 碼時,此前所做的修改就全部丟失了。我們需要增加某種機制來告訴 JMerge 有些方法已 經被修改過了,因此這些方法不應該被重寫。要實現這種功能,可以使用 <merge:dictionaryPattern> 元素。merge:dictionaryPattern 允許您使用正則表 達式來區分 Java 元素(參見清單 3)。
清單 3. 一個簡單的 dictionaryPattern
<merge:dictionaryPattern
name="generatedMember"
select="Member/getComment"
match=
"\s*@\s*(gen)erated\s*\n"/>
<merge:pull
targetMarkup=
"^gen$"
sourceGet="Method/getBody"
targetPut="Method/setBody"/>
dictionaryPattern 定義了一個正 則表達式,它可以匹配注釋中包含 " @generated " 的成員。select 屬性列出了要對這 個成員的哪些部分與在 match 屬性中給出的正則表達式進行比較。dictionaryPattern 是由字符串 gen 定義的,它就是 match 屬性值中圓括號中的內容。
merge:pull 元素多了一個附加屬性 targetMarkup 。這個屬性可以匹配 dictionaryPattern ,它必 須在應用合並規則之前對目標代碼進行匹配。此處,我們正在檢查的是目標代碼,而不是 源代碼,因此用戶可以定制這些代碼。當用戶刪除注釋中的 " @generated " 標簽時, dictionaryPattern 就不會與目標代碼匹配,因此就不會合並這個方法體。請參見清單 4 。
清單 4. 定制代碼
/**
* test case for getName
*
@generated
*/
public void testSimpleGetName() {
// because of the @generated tag,
// any code in this method will be overridden
}
/**
* test case for getName
*/
public void testSimpleSetName() {
// code in this method will not be regenerated
}
您或許會 注意到有些元素是不能定制的,任何試圖定制這些代碼的企圖都應該被制止。為了支持這 種功能,要定義另外一個 dictionaryPattern ,它負責在源代碼(而不是目標代碼)中 查找其他標記,例如 @unmodifiable 。然後再定義一條 pull 規則,來檢查 sourceMarkup ,而不是 targetMarkup ,這樣就能防止用戶刪除標簽或阻礙合並操作。 請參見清單5。
清單 5. 不可修改代碼的 merge.xml
<merge:dictionaryPattern
name="generatedUnmodifiableMembers"
select="Member/getComment"
match=
"\s*@\s*(unmod)ifiable\s*\n"/>
<merge:pull
sourceMarkup="^unmod$"
sourceGet="Member/getBody"
targetPut="Member/setBody"/>
細粒度的定制
在使用這種解決 方案一段時間之後,您將注意到有些方法在定制的代碼中具有一些通用的不可修改的代碼 (例如跟蹤和日志記錄代碼)。此時我們既不希望禁止生成代碼,也不希望全部生成整個 方法的代碼,而是希望能夠讓用戶定制一部分代碼。
要實現這種功能,可以將前 面的 pull 目標替用清單 6 來代替。
清單 6. 細粒度的定制代碼
<!-- if target is generated, transfer -->
<!-- change to sourceMarkup if the source is the standard -->
<merge:pull
targetMarkup="^gen$"
sourceGet="Method/getBody"
sourceTransfer=
"(\s*// \s*begin-user-code.*?//\s*end-user-code\s*)\n"
targetPut="Method/setBody"/>
這樣會只重寫字符串 " // begin- user-code " 之前和 " // end user-code " 之後的內容,因此就可以在定制代碼中保留 二者之間的內容。在上面的正則表達式中, "?" 表示在目標代碼中,除了要替換的內容 之外,其他內容全部保留。您可以實現與 JavaDoc 注釋類似的功能,這樣就可以拷貝注 釋,同時為用戶定制預留了空間。請參見清單 7。
清單 7. 細粒度的 JavaDoc 定 制
<!-- copy comments except between the begin-user-doc
and end-user-doc tags -->
<merge:pull
sourceMarkup="^gen$"
sourceGet="Member/getComment"
sourceTransfer="(\s*<!--\s*begin-user-doc.*?end-user-doc\s*-->\s*)\n"
targetMarkup="^gen$"
targetPut="Member/setComment"/>
要支持這種注釋,首先要修改開始 標簽和結束標簽,使其遵循 HTML 注釋語法,這樣它們就不會出現在所生成的 JavaDoc 中;然後修改 sourceGet 和 targetPut 屬性,以便使用 "Member/ getComment" 和 "Member/ setComment" 。JMerge 允許您在細粒度級別上存取 Java 代碼的不同部分。( 更多內容請參見 附錄 A)。
下一步
到現在為止,我們已經介紹了如何轉 換方法體,但是 JMerge 還可以處理域、初始化、異常、返回值、import 語句以及其他 Java 元素。它們也采用類似的基本思想,可能只需稍加修改即可。參考 plugins/org.eclipse.emf.codegen_1.1.0/test/merge.xml 就可以知道如何使用這些功 能(我使用的是 Eclipse 2.1,因此如果您使用的是其他版本的 Eclipse,那麼 ecore 插件的版本可能會不同)。這個例子非常簡單,其中並沒有使用 sourceTransfer 標記, 但是該例顯示了處理異常、標志和其他 Java 元素的方法。
更復雜的例子請參見 EMF 使用 JMerge 的方法: plugins/org.eclipse.emf.codegen.ecore_1.1.0/templates/emf-merge.xml 。從這個例 子中可以看出 EMF 只允許部分定制 JavaDoc,但是采用上面介紹的一些技巧,就可以為 方法體添加支持(這樣可以增強 JET 的功能)。
附錄 A:有效的目標選項
在 dictionaryPattern 和 pull 規則中,我們已經使用了 " Member/getComment " 和 " Member/getBody " 以及它們的 setter 方法,但是還有很多其他可用的選項。 JMerge 支持 org.eclipse.jdt.core.jdom.IDOM* 中定義的任何類的匹配和取代。所有可 用的選項如表 1 所示。
表 1. 有效的目標選項
類型 方法 注釋 CompilationUnit getHeader/setHeader getNa me/setName Field getInitializer/setInitializer 不包含 "=" getName/setName 變量名 getName/setName 類名 Import getName/setName 要麼是一個完全限定的 類型名,要麼是一個隨需應變的包 Initializer getName/setName getBody/setBody Member getComment/setComment getFlags/setFlags 例如: abstract, final, native 等。 Method addException addParameter getBody/setBody getName/setName getParameterNames/setParameterNames getParameterTypes/setParameterTypes getReturnType/setReturnType Package getName/setName Type addSuperInterface getName/setName getSuperclass/setSuperclass getSuperInterfaces/setSuperInterfaces
附錄 B:merge:pull 屬性
表2 給出了 merge:pull 元素的屬性。
表 2. merge:pull 屬性
屬性 條件 sourceGet 必需的。該值必須是 附錄 A中列出的一個 選項,例如 "Member/getBody"。 targetPut 必需的。該值 必須是 附錄 A中列出的一個選項,例如 "Member/setBody"。 sourceMarkup 可選的。用來在觸發 merge:pull 規則之前 過濾必須匹配源代碼的 dictionaryPatterns 。格式如 "^dictionaryName$",也可以使 用 "|" 將多個 dictionaryPatterns 合並在一行中。 targetMarkup 可選的。用來在觸發 merge:pull 規則之前 過濾必須匹配目標代碼的 dictionaryPatterns 。格式如 "^dictionaryName$",也可以 使用 "|" 將多個 dictionaryPatterns 合並在一行中。 sourceTransfer 可選的。一個正則表達式,指定要傳遞給 目標代碼的源代碼的數量。
本文配套源碼