EMF 究竟是什麼?
Eclipse Modeling Framework(EMF)是一個開放源代碼的 框架,它的目標是實現模型驅動架構(Model-Driven Architecture)的開發。如果我們 當中的少數人有幸得到了某個 UML 模型,那麼這個框架就可以幫助我們將文檔變成代碼 。至於其他人,這個工具也使您又有一次機會向老板證實,把時間花在為解決方案建模上 是值得的。除了可以生成令人贊歎的 Java 代碼之外,EMF 還可以生成 Eclipse 插件, 以及圖形化的可定制編輯器。當您改變模型時(這種情況真的會出現),EMF 可以通過單 擊一個按鈕,就使代碼和模型保持同步。
EMF 生成的代碼也不是一種只配丟進垃 圾箱的解決方案。這種代碼支持標准的創建、獲取、更新和刪除操作,而且還支持元數約 束、復雜關系和繼承結構、屏蔽定義,以及一套屬性描述。生成的代碼還提供通知、參照 完整性和可定制的 XMI 持久性。您所需要做的全部工作就是創建一個對象模型,就像您 以前也想做的那樣。
EMF 是比較新的事物,但前景廣闊,對它持續支持的力度也 很強。它實現的是一項公共標准,即對象管理組織(Object Management Group)的元對 象工具(Meta-Object Facility,MOF)。現在 EMF 已經對 MOF 的第二版進行了增強。 更進一步看,EMF 還是 EMF:XSD 以及 Hyades 等 Eclipse 項目的基礎,大多數 IBM WebSphere Studio 產品也都使用它。EMF 第二版的開發已經開始,開發構建應該很快就 會出爐。第二版開發計劃中包括更好的 XML Schema 支持、更靈活的代碼生成方式以及模 型之間的映射機制。
讓工具自己說話
商業宣傳已經說得夠多了。現在讓我 們直接進入代碼中,看看 EMF 到底能做些什麼。下面的例子都是用 Eclipse 3.0M7 和 EMF 2.0.0,再加上與之匹配的 XSD 工具箱實現的。現在有四種獨立的 EMF 開發流程, 每一種都適用於不同版本的 Eclipse,所以一定要保證根據您的 Eclipse 版本選擇了正 確的 EMF 版本。
我們將以一個簡單的 Web 論壇為例,向您展示最重要的特性。 模型的根為 Forum ,下面包括一組 Member 和 Topic 。每一個 Topic 都具有一個 TopicCategory (枚舉類型), Member 和 Topic 通過 Post 類間接相關聯,這兩者之 間也存在直接關聯,因為 Member 可以創建 Topic 。
用 UML 和 Omondo 創建 EMF 模型
Omondo 的 UML 插件是在 Eclipse 中創建 UML 文檔的方便可靠的工具 。它看起來就像是 Rational Rose 受冷落的小兄弟,但除非是您需要特別強大的功能, 否則用它就可以工作得很好了。不過,該工具尚不支持 Eclipse 3,所以我采用 Eclipse 2.1 來創建 UML 類圖。
一開始,我們創建一個新的 Java 項目 UMLForum,以及 一個新包 com.ibm.example.forum 。再創建一個新的 EMF 類圖, forum.ucd ,存放在 src/com/ibm/example/forum 下。目錄中創建了兩個文件,forum.ecd 和 forum.ecore。 向類圖中增加一個新類,名為 Forum ,然後單擊 Finished。向 Forum 類中增加一條屬 性描述,類型為 EString (對於所有的簡單 Java 類都有相應的 Ecore 類),如圖 1 所示。對於屬性的特性,只選擇 changeable ,並將范圍設為從 0 到 1。
如果您 過一會改主意了,想使用其他的特性,可以打開 Properties 視圖,選擇其中的類或屬性 。
圖 1. 新建的 Forum 類及其屬性的性質
對於下 列接口重復上述步驟:
接口 屬性 類型 Member nickname EString Top ic title EString Post comment EString
為定義關聯,我們可以選中關聯按鈕,然後單擊關 聯的源( Forum )和目標( Member )。這樣將打開關聯屬性設置對話框。在其中將名 字設置為 members ,確保僅僅選擇了 changeable 和 containment,然後將上限設為 -1 。在第二個 Association End 選項卡中,取消選中的 Navigable,然後單擊 Ok。對 Forum 和 Topic 也執行相同的操作,屬性名稱從 members 改為 topics 。取消選中的 navigable,從而創建一個無方向的關聯,但我們想讓其他屬性都保持為雙向。
按 照下表所示完成關聯設置:
源 目標 關聯 名稱 特性 范圍 Member Topic 1st Association topicsCreated changeable 0 到 1 2nd Association creator changeable 0 到 1 Topic Post 1st Association posts Containment, changeable 0 到 - 1 2nd Association topic changeable 0 到 1 Member Post 1st Association posts changeable 0 到 - 1 2nd Association author changeable 0 到 1
最後,我們要定義一個枚舉類型,用於表示 topic 有多少 不同的類型。創建一個新的枚舉類型,名字叫做 TopicCategory 。Literal 中加入以下 的內容:
ANNOUNCEMENT , value = 0
GUEST_BOOK , value = 1
DISCUSSION , value = 2
然後,為 Topic 定義一個新屬性,叫做 category ,類型為 TopicCategory ,changeable,范圍 0-1。如果您願意的話,可以在 屬性標簽上對默認值進行修改,但我們將接受 ANNOUNCEMENT 的默認值。
圖 2. 完成後的 UML 類模型
一旦 您完成了圖 2 所示的 UML 類圖,下一步就是創建一個 EMF 模型。為此,需要先創建一 個新的 EMF 項目( File > New > Project... > Eclipse Modeling Framework > EMF Project),並用 com.ibm.example.forum 作為該項目的名稱(這 是插件名稱的基礎,因此我們遵從 Eclipse 插件的命名規范)。在下一個頁面上,選擇 Load from an EMF core model,然後單擊 Next。從文件系統中加載 ecore 文件,它將 自動填充 Generator 的模型名。在最後一個頁面上,單擊包旁邊的復選框,然後單擊 Finish。這樣就創建好了 EMF 模型,它的名字叫做 forum.genmodel。您可以從 使用生 成的 EMF 模型一節中了解到這個模型是什麼,以及如何使用它。
用 XML Schema
創建 EMF 模型
XML Schema(XSD)的表現能力不如 UML 或帶注釋 的 Java 代碼那麼強大,例如,它不能表達出雙向引用的關聯。但是由於默認的的序列化 方法要使用到您的方案,因此 XSD 對定制序列化來說是最快的方法。如果您希望為模型 生成非常詳細的 XML/XMI,那麼 XSD 就是必然的選擇。
清單 1. forum.xsd 的片 段
<xsd:simpleType name="TopicCategory">
<xsd:restriction base="xsd:NCName">
<xsd:enumeration value="Announcement"/>
<xsd:enumeration value="GuestBook"/>
<xsd:enumeration value="Discussion"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:complexType name="Post">
<xsd:sequence>
<xsd:element name="comment" type="xsd:string"/>
<xsd:element name="author" type="xsd:anyURI" ecore:reference="forum:Member"/>
<xsd:element name="topic" type="xsd:anyURI" ecore:reference="forum:Topic"/>
</xsd:sequence>
</xsd:complexType>
在清單 1 中,您可以看到枚舉是如何表示 的,也能從中了解到如何定義一個具有指向其他類型的元素和引用的類型。在 Forum 這 個例子中,我們僅僅使用了字符串屬性 "xsd:string" ,但是其他簡單 Java 類型也是支 持的。
一旦完成了 XSD,下一步就是創建 EMF 模型。方法與 UML 模型中類似, 先創建一個新的 EMF 項目( File > New > Project... > Eclipse Modeling Framework > EMF Project),項目名稱為 com.ibm.example.forum(這是插件名稱的 基礎,因此我們遵從 Eclipse 插件的命名規范)。在下一個頁面上選擇 Load from an XML Schema,然後單擊 Next。在文件系統中找出 XSD 文件並加載,然後 Generator 中 的模型名就會自動填充。在最後一個頁面上,單擊包旁邊的復選框,然後單擊 Finish。 這樣就創建了一個 EMF 模型,名字叫做 forum.genmodel。您可以從 使用生成的 EMF 模 型一節中了解到這個模型是什麼,以及如何使用它。
用帶注釋的 Java 代碼創建 EMF 模型
如果通過 Java 代碼定義 EMF 模型,我們可以用 Interface 列出每一 個類的屬性,以及類之間的關系。這樣得到的內容並不充足,無法定義我們想要的全部信 息,所以 EMF 使用了特殊的 JavaDoc 標簽。每一個屬性或類,如果是 EMF 模型的一部 分,就必須在其 JavaDoc 中包含一個 @model 標簽,也可以包含一個附加屬性列表。比 如說,如果要構造如上面圖 2 所示的一個對象模型,我們對 Forum 的定義看起來應該像 清單 2 的樣子。
清單 2. 帶注釋的 Forum.java
package com.ibm.example.forum;
import java.util.List;
/**
*
@model
*/
public interface Forum {
/**
*
@model type="Topic" containment="true"
*/
List getTopics();
/**
*
@model type="Member" containment="true"
*/
List getMembers();
/**
*
@model
*/
String getDescription();
}
清單 2 聲明了一個叫做 Forum 的對象,它具 有一條 String 類型的描述信息和兩個孩子,一個是 Topic 列表,還有一個是 Member 列表。這兩個孩子都包含在 Forum 之內。
對於簡單的屬性,如 描述信息 , @model 標簽就足夠了,但對於 list 而言,您也需要為其指明類型。containment 屬性 是可選的,但是如果某個對象是被包含的,那麼它就和其容器一起被序列化。為了簡化序 列化的過程,我們要保證所有的對象都是直接或者間接包含在 Forum 中的。其他一些有 用的可選屬性如下:
opposite (用於雙向屬性)。
default (屬性的默 認值)。
transient (該屬性不能被序列化)。
惟一需要當心的是枚舉類 型。它被定義成一個 Class,而不是其他模型類中的 Interface! 為了明確這一點,清 單 3 展示了 TopicCategory 枚舉類型是如何實現的。
清單 3. 枚舉類型 TopicCategory.java
package com.ibm.example.forum;
/**
* @model
*/
public
class TopicCategory{
/**
* @model name="Announcement"
*/
public static final int ANNOUNCEMENT = 0;
/**
* @model name="GuestBook"
*/
public static final int GUEST_BOOK = 1;
/**
* @model name="Discussion"
*/
public static final int DISCUSSION = 2;
}
最後,生成如下所示的三個接口 ,模型就完成了:
接口 方法 模型標簽 Member List getPosts() type="Post" opposite="author" List getTopicsCreated() type="Topic" opposite="creator" String getName() Topic List getPosts() type="Post" opposite="author" Member getCreator() opposite="topicsCreated" String getTitle() TopicCategory getCategory() Post Member getAuthor opposite="posts" Topic getTopic() opposite="posts" String getComment()
模型定義完成之時,可以生成 EMF 模型( File > New > Other > Eclipse Modeling Framework > EMF Models)。將父目錄設為 com.ibm.example.forum/src/model, File name設為 forum.genmodel。在下一個頁面上 ,選擇 Load from annotated Java,然後選中包“forum”旁邊的復選框。然 後單擊 Finish。這樣就創建了一個名為 forum.genmodel 的 EMF 模型。
使用生 成的 EMF 模型
現在您的工作空間中應該有一個生成好的 EMF 模型 forum.genmodel。這個模型中包含您輸入其中的所有信息。用默認的編輯器打開這個模型 (參見圖 3),再打開 Properties 視圖,然後檢查模型樹中每一個節點的屬性。前面輸 入的所有屬性都可以定制,但是也有一些用於定制代碼生成的屬性。為了驗證這一點,讓 我們試著修改 “Copyright Text”或“Generate Schema”之類的 屬性,看看會發生什麼事情。
圖 3. 在默認的編輯器中打開生成的 EMF 模型
如果 對模型描述(UML、XSD、帶注釋的 Java)進行了修改,也可以在 Package Explorer 中 用右鍵單擊該模型,然後選擇 Reload,這樣就能夠重新加載模型。這實現了用 EMF 生成 的模型與模型描述之間的同步。重新加載後將會改變您在生成的模型中修改過的屬性。
生成 Java 代碼
如果您對模型描述感到滿意,或者如果您僅僅是想看看所 有這一切到底是什麼意思,那麼現在就可以生成代碼了。在根節點上單擊鼠標右鍵,選擇 其中一個生成選項:Model、Edit、或 Editor code。Generate Model將在當前項目中創 建該 EMF 模型的 Java 實現代碼。其中會包含下列內容:
com.ibm.example.forum -- 創建該 Java 類的接口和工廠。
com.ibm.example.forum.impl -- com.ibm.example.forum 中定義的接口的具體 實現。
com.ibm.example.forum.util -- AdapterFactory。
Generate Editor Code將創建 com.ibm.example.forum.edit 項目。其中僅僅包含一個包, com.ibm.example.forum.provider ,用於控制每一個模型對象出現在編輯器中的方式。 Generate Editor Code將在 com.ibm.example.forum.editor 項目中創建一個插件編輯器 示例,其中包含了 com.ibm.example.forum.presentation。這些類提供了一系列簡單的 JFace 編輯器,可以與您的模型進行交互。
為了測試生成的插件,請依次進入 Run > Run... > Run Time Workbench > New。輸入一個描述性的名稱,然後在 plug-ins 選項卡中,選擇 launch with all workspace and enabled external plug- ins。再在 Common 頁下,單擊 Display in favorites menu > Run和 Launch in background。最後保存設置並運行。
這時將出現一個新的 Eclipse 工作台,您可 以在 Help > About Eclipse Platform > Plug-in Details下面驗證您的插件是否 可用,如圖 4 所示。
圖 4. Forum 的插件詳細信息
為了測試生成的插件,您可以創建一個新的 Simple 項目,名為“Forum Demo”,然後依次進入 New > Other... > Example EMF Model Creation Wizards > Forum Model。給文件取名叫做 sample.forum,然後選擇 Forum 作為 Model Object。這時會打開一個窗口,您可以在這裡向根中增加新的模型元素。其中包含 幾種視圖:Selection、Parent、List、Tree、Table 和 TreeTable。所有這些視圖都顯 示相同的數據,也和 Outline 視圖保持同步。雖然所有視圖都會在右鍵菜單選項中顯示 New Sibling/New Child,但是我發現,有些視圖在加入兄弟節點或子節點時不能正確響 應。如果您也遇到這種情況,可以使用 TableTree 視圖,或是在 Outline 視圖中創建新 的節點。圖 5 展示了所生成的插件編輯器。
圖 5. 所生成的插件編輯器
定制 生成的代碼
生成的代碼都很不錯,但是這只是真正應用程序的起點。為了滿足我 們的需要,我們必須對其進行調整和定制。我們可以改變所生成的模型類的實現,也可以 對編輯器進行擴展和定制。好在 EMF 沒有讓我們失望,我們可以按照自己的想法做任何 定制,當重新生成代碼時也不會丟掉這些內容。我們需要做的全部工作就是刪除 @generated JavaDoc 標簽,EMF 的 jmerge 將保證這些方法、屬性或類不被打擾。
為著重說明您能對代碼進行哪些修改,讓我們來看一個簡單的例子。在所生成編 輯器的 Table 視圖中,兩個字段都顯示出相同的的值。這一點並不是完全沒有用處。為 了改善一下,我們可以修改第二個字段,讓它在選中一個 Topic 的時候顯示 Author,然 後增加第三個字段,給出該 Topic 中的帖子數。
第一步,向 Table 視圖中額外 增加一個字段。這一步在 com.ibm.example.forum.editor 項目中實現,即 createPages() 方法中的 com.ibm.example.forum.presentation.ForumEditor 。把 @generated 標簽刪除,這樣就能持久保存我們的修改,然後定位到表浏覽窗口所在的位 置。按照清單 4 的內容對這段代碼進行修改。
清單 4. 修改後的 createPages ()
TableColumn selfColumn = new TableColumn(table, SWT.NONE);
layout.addColumnData(new ColumnWeightData(2, 100, true));
selfColumn.setText("Author");
selfColumn.setResizable(true);
TableColumn numberColumn = new TableColumn(table, SWT.NONE);
layout.addColumnData(new ColumnWeightData(4, 100, true));
numberColumn.setText("Number of Posts");
numberColumn.setResizable (true);
tableViewer.setColumnProperties(new String [] {"a", "b", "c"});
這樣就額外增加了一個字段,但是現在所有的三個字段都顯示相 同的數據。為了定制每一個字段中的數據,我們需要提供一些 ITableItemLabelProvider 的實現。打開 com.ibm.example.forum.provider.TopicItemProvider ,在實現列表中加 入 ITableItemLabelProvider 。我們需要增加兩個方法, getColumnText(Object, int) 和 getColumnImage(Object, int) ,如清單 5 所示。
清單 5. 加入 TopicItemProvider
public String getColumnText(Object obj, int index) {
if( index == 0 ){
return getText(obj);
}
else if( index == 1 ) {
return ((Topic)obj).getCreator ().getNickname();
} else if( index == 2 ) {
return " + ((Topic)obj).getPosts().size();
}
return "unknown";
}
public Object getColumnImage(Object obj, int index) {
return getImage( obj );
}
最後,我們需要注冊這個提供程序。實現方 法是編輯 com.ibm.example.forum.provider.ForumItemProviderAdapterFactory 的構造 函數,向支持的類型中增加 ITableItemLabelProvider ,如清單 6 所示。
清單 6. ForumItemProviderFactory 構造函數
public ForumItemProviderAdapterFactory() {
supportedTypes.add (ITableItemLabelProvider.class);
supportedTypes.add (IStructuredItemContentProvider.class);
supportedTypes.add (ITreeItemContentProvider.class);
supportedTypes.add (IItemPropertySource.class);
supportedTypes.add (IEditingDomainItemProvider.class);
supportedTypes.add (IItemLabelProvider.class);
}
現在我們再運行這個插件,打開表 視圖,就能看到圖 6。請注意,沒有實現的 ITableItemLabelProvider 元素將在所有的 字段中顯示相同的文本。
圖 6. 修改後的 Table 編輯器
在 Java 中操縱模型
生成的模型代碼看起來就像是 Java 代碼中增加了 一些有用的東西。系統還提供了一種靈活的定制反射 API,對工具很有用。您也許注意到 了,這就是 eGet() 和 eSet() 兩個方法。在大多數情況下,我們並不需要關心它,所以 我們還是看看我們感興趣的東西:如何創建、保存和加載模型。讓我們從頭開始:加載 EMF 模型。
清單 7. 加載 Forum
// Register the XMI resource factory for the .forummodel extension
Resource.Factory.Registry reg = Resource.Factory.Registry.INSTANCE;
Map m = reg.getExtensionToFactoryMap();
m.put("forummodel", new XMIResourceFactoryImpl());
ResourceSet resSet=new ResourceSetImpl();
Resource res = resSet.getResource(URI.createURI ("model/forum.forummodel"),true);
Forum forum = (Forum) res.getContents().get(0);
清單 7 展示了如何給文件關聯一個符合 XMI 格式的擴展名“forummodel”,然後用 EMF 的 ResourceSet 解析並加載 forum 模型。我們知道,Forum 是惟一的根元素,所以可以想象, res.getContents ().get(0) 將返回一個且僅有一個 Forum 對象。如果情況不是這樣,我們還可以從 getContents().iterator() 中取出一個 Iterator,然後分別檢查每一個元素。
我們還可以換一種方法,創建一個新的 Forum,然後用程序組裝起來,如清單 8 所示。
清單 8. 初始化 Forum
// initialize model and dependencies
ForumPackageImpl.init();
// retrieve the default Forum factory singleton
ForumFactory factory = ForumFactory.eINSTANCE;
Forum forum = factory.createForum();
forum.setDescription ("programmatic forum example");
Member adminMember = factory.createMember();
adminMember.setNickname("Administrator");
forum.getMembers().add( adminMember );
Topic noticeTopic = factory.createTopic();
noticeTopic.setTitle("Notices");
noticeTopic.setCategory(TopicCategory.ANNOUNCEMENT_LITERAL);
noticeTopic.setCreator(adminMember);
forum.getTopic().add( noticeTopic );
在這個例子中,我們首先初始化包,然後創建 ForumFactory,用它 生成所有的子對象。創建完畢之後,就可以像標准的 JavaBean 那樣訪問這些對象。然而 ,由於我們把 Topic 和 Memeber 之間的 creator/topicsCreated 關系聲明為雙向,當 我們調用 noticeTopic.setCreator(adminMember) 的時候, adminMember 的 topicsCreated 清單中就包括 noticeTopic 。
一旦我們創建並操縱了 EMF 模型 ,就很容易將其保存為我們選定的格式(參見清單 9)。
清單 9. 保存 Forum
URI fileURI = URI.createFileURI("model/forum.ecore");
Resource resource = new XMIResourceFactoryImpl().createResource (fileURI);
resource.getContents().add( forum );
try {
resource.save(Collections.EMPTY_MAP);
} catch (IOException e) {
e.printStackTrace();
}
在本例中,我們給 URI.createFileURI () 提供了希望保存成的文件名與目標格式。這個例子因為是保存為 XMI,所以使用了 XMIResourceFactoryImpl 。一旦創建完畢,所有的模型對象就如我們所願的持久保存起 來了。在這個例子中,除 Forum 之外的每一個對象都被另一個類包含,所以我們只需要 對包含所有孩子的 root 增加這條命令即可。如果某些對象沒有 包含 關系,那麼也必須 通過 resource.getContents().add() 顯式地將它們加進去。否則,當您調用 resource.save() 時就會出現異常。
結束語
Eclipse Modeling Framework 提供了進行模型驅動開發的工具。它包含了將您的開發精力集中在模型上而不是實現細節 上所必需的元素。其主要關注的領域是:生成模型時支持定制、通知、參照完整性以及其 他基本特性;生成可定制的模型編輯器;默認的序列化。正像例子中展示的那樣,生成的 過程既簡單又直接,所有的定制代碼都支持定制。序列化或圖形化編輯器等獨立的工具也 可以拉出來單獨使用,但所有的部件一起使用才能發揮完整的效力。EMF 已經在很多成功 的項目中得到應用,它正在蓬勃成長。