级别: 初级
Adrian Powell, Advisory I/T Specialist, IBM
2004 年 4 月 01 日
Eclipse Modeling Framework(EMF)是一个开放源代码的模型驱动应用程序开发框架。它可以基于 XML Schema、UML 或经过注释的 Java 中指定的模型,创建 Java 代码,实现图形化的数据编辑、操纵、读取和序列化。EMF 是 IBM WebSphere Studio 和 Eclipse 项目中很多工具的基础。本文将帮助您逐步了解创建模型、生成代码、使用生成的应用程序和定制编辑器的整个过程。
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 |
Topic |
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 类型也是支持的。有关 XML Schema 和 forum.xsd 文件的更多信息,请参阅 参考资料。
一旦完成了 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 (该属性不能被序列化)。
要获得完整的属性列表,请您参阅 参考资料中的 EMF user's guide。
惟一需要当心的是枚举类型。它被定义成一个 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() 时就会出现异常。
|