以下文章出自www.eclipse.org。本人仅作中文翻译,未作任何其它修改。
EMF概述
文档原出处:http://eclipse.org/emf/docs.php?doc=references/overview/EMF.html
最后修改时间: 2005年6月16日
本文为EMF及其代码生成模式提供了一个基本的概述。要了解EMF所有功能的详细描述,请参见《Eclipse Modeling Framework》(Addison Wesley, 2003),或者框架自身的Javadoc文档。
概论
EMF是一个Java框架与代码生成机制,用来构建基于结构化模型的工具或其它应用系统。它们可以带给你面向对象建模的思想,EMF帮助你快速地将你的模型转变为高效的、正确的、以及易用的定制Java代码,只需要一个很低的入门成本,EMF就可以为你提供这些益处。
那么,当我说“模型”时到底意味着什么?当谈论模型时,我们一般都会想到类图、协作图、状态类,等等。UML为这些图定义了标准的符号。联合使用各种UML图,可以详细说明一个应用系统的完整模型。这个模型可能纯粹只用作文档,或者通过适当的工具,它可以被用来作为输入内容生成一部分或全部应用系统代码。
要能够做到这些的建模工作一般都需要昂贵的面向对象的分析与设计工具(OOA/D),你可能对我们的结论有疑问,但是,EMF提供了一个低成本的入口。我们说这个的理由是一个EMF模型只需要你拥有在UML中建模所需知识的一小部分即可,主要是简单的类的定义,以及它们的属性与关系,针对这些,不需要使用一个全尺寸的图形化建模工具。
EMF使用XMI作为模型定义的规范形式,你有多种方法可以得到这种格式的模型:
· 直接使用XML或文本编辑器来创建XMI文档。
· 从建模工具,如Rose,中导出XMI文档。
· 带有模型属性的注解Java接口。
· 描述模型序列化格式的XML Schema。
第一种方法最直接,但一般只对XML高手有吸引力。若你已经使用了全尺寸的建模工具,则第二种方法是最可取的。第三种方法只需要有一个基本的Java开发环境就可以低成本地拥有EMF带来的好处、以及它的代码生成能力。在创建一个需要读写特定的XML文件格式的应用系统时,最后一种方法最适合。
一旦你指定一个EMF模型,EMF生成器就可以创建一个一致的Java实现类的集合,你可以编辑生成的类来添加方法与实例变量,只要需要还可以重新从模型中生成代码:你添加的部分在重新生成过程中都将被保留。若你添加的代码依赖于你在模型中修改的某些东西,你还需要更新代码来反映这些改变,其它情况下,你的代码是完全不受模型修改与重新生成的影响的。
另外,通过以下方法,就可以简单地提高你的生产力:使用EMF提供的几个其它的益处,如模型变动通知、持久化支持(包括默认的XMI、以及基于Schema的XML序列化),模型校验框架,以及非常有效的用来操纵EMF对象的反射API。最重要的是,EMF提供了与其它基于EMF的工具或应用进行互操作的基础。
EMF包括两个基本的框架,core框架与EMF.Edit。core框架通过为模型创建实现类,提供基本的代码生成与运行时支持。EMF.Edit基于core构建并进行了扩展,添加了对生成适配器类的支持,可以支持视图以及基于命令的(可以undo的)模型编辑操作。下面的章节描述core框架的主要功能。EMF.Edit将在另一篇文章中进行描述“The EMF.Edit Framework Overview”。指南“Generatin an EMF Model”详细介绍了如何运行EMF与EMF.Edit生成器。
EMF与OMG MOF的关系
如果你已经熟悉OMG的MOF,你肯定会困惑于EMF倒底与MOF有什么关系。实际上,EMF就是从作为MOF规范的一个实现开始的,通过实现大量使用EMF的工具积累的经验,我们又对它进行了发展。EMF可以被看作对于MOF的部分核心API的一个高效的Java实现。然而,为避免任何混淆,与MOF核心元模型类似的部分在EMF中称为Ecore。
在MOF2.0计划中,MOF模型的一个类似子集,称为(EMOF,Essential MOF),也被分离了出来。在Ecore与EMOF间只存在微小的,大部分是命名上的区别;无论如何,EMF都可以透明地读写EMOF的序列化存储。
定义一个EMF模型
为了有助于描述EMF,我们假定拥有一个简单的、只包含一个类的模型,如下图:
模型中展示了一个拥有两个属性的类:String类型的title,int类型的pages。
我们的如上图这么简单的模型定义,可以通过几种不同的方式提供给EMF代码生成器。
UML
若你拥有与EMF一起工作的建模工具,你可以简单地画出如上图所示的模型。
XMI
另外,我们可以直接用XMI文档来描述这个模型,就像下面所示:
<ecore:EPackage xmi:version="2.0" xmlns:xmi="http://www.omg.org/XMI" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:ecore="http://www.eclipse.org/emf/2002/Ecore" name="library "nsURI="http:///library.ecore" nsPrefix="library">
<eClassifiers xsi:type="ecore:EClass" name="Book">
<eStructuralFeatures xsi:type="ecore:EAttribute" name="title" eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EString"/>
<eStructuralFeatures xsi:type="ecore:EAttribute" name="pages" eType="ecore:EDataType http://www.eclipse.org/emf/2002/Ecore#//EInt"/>
</eClassifiers>
</ecore:EPackage>
XMI文档包含了类图中的所有信息.。在图中的每个类与属性都在XMI文档中有一个对应的类或属性定义。
Annotated Java
如果,你没有图形化建模工具,也没有兴趣手工输入所有的XMI语句,这是第三个描述你的模型的选项。因为EMF生成器是一个可以并入代码的生成器,通过提供部分Java接口(将模型信息通过注解提供),生成器可以将接口作为生成模型的元数据,并将其它的代码生成到最终实现代码中。
我们可以这样定义Book类:
/**
* @model
*/
public interface Book
{
**
* @model
*/
String getTitle();
/**
* @model
*/
int getPages();
}
通过这种方法,用Java接口中标准的get方法来标注属性与引用的形式,我们提供了模型的所有信息。@model标签用来向代码生成器表示那些接口,以及接口的那些部分对应到模型元素,并需要进行相应的代码生成。
对于我们的简单示例,所有模型信息都确实通过对接口进行Java反省操作而提供,所以不再需要任何其它附加的模型信息。在一般情况下,还可以在@model标签后添加模型元素的附加详细信息。针对此示例,若我们希望pages属性是只读的(这意味着不生成setter方法),我们就可以向注解中加入信息:
/**
* @model changeable="false"
*/
int getPages();
因为只有与默认值不一致的信息才需要被明确指定,所以注解可以保持简洁明了。
XML Schema
有时候,你可以通过XML Schema描述一个模型的实例序列化后看起来应该怎样来定义一个模型。这对于编写一个使用XML来整合现存的应用系统或遵循某个标准的的应用系统是很有帮助的。以下就是与我们的简单模型等值的Schema文档:
<xsd:schema targetNamespace="http:///library.ecore" xmlns="http:///library.ecore" lns:xsd="http://www.w3.org/2001/XMLSchema">
<xsd:complexType name="Book">
<xsd:sequence>
<xsd:element name="title" type="xsd:string"/>
<xsd:element name="pages" type="xsd:integer"/>
</xsd:sequence>
</xsd:complexType>
</xsd:schema>
这种方法与另外三种有些不同,主要因为EMF在最后的使用中,必然会加上某种序列化的约束,来确保与schema的一致性。作为结果,从Schema中创建的模型看起来就与使用其它方法生成的模型不一样。这些区别的细节将在概述后的章节中讨论。
在本文的剩余部分,为保持简洁明了,我们将统一使用UML图。所有我们所阐述的建模要领都可以用注解Java接口或直接使用XMI来表示,其中大部分也都存在有等价的XML Schema。不管提供了什么信息,对于EMF的代码生成来说都是一样的。
生成Java实现
对于模型中的每个类,都将会生成一个Java接口以及对应的实现类。在我们例子中,为Book生成的接口如下:
public interface Book extends EObject
{
String getTitle();
void setTitle(String value);
int getPages();
void setPages(int value);
}
每个生成的接口包含针对每个属性的getter与setter方法。
Book接口扩展了基础接口EObject。EObject是在EMF中等价于java.lang.Object的对象,也就是说,它是所有EMF类的基类。EObject以及它的实现类EObjectImpl(我们将在稍后讨论)提供了一些基础的轻量级的实现,来为Book引入EMF的通知与持久框架。在我们开始讨论EObject到底往混合中带来了什么之前,让我们继续来看一看EMF如何生成Book。
每个生成的实现类都包含定义在对应的接口中的getter与setter方法的实现,还加上其它一些EMF框架所需的方法。
类BookImpl将会包含有title与pages属性的存取实现。pages属性,如例子,拥有如下生成的实现代码:
public class BookImpl extends EObjectImpl implements Book
{
...
protected static final int PAGES_EDEFAULT = 0;
protected int pages = PAGES_EDEFAULT;
public int getPages()
{
return pages;
}
public void setPages(int newPages)
{
int oldPages = pages;
pages = newPages;
if (eNotificationRequired())
eNotify(new ENotificationImpl(this, Notification.SET, ..., oldPages, pages));
}
...
}
生成的get方法提供了理想的效率。它简单地返回表示属性的一个实例变量。
set方法,虽然有点复杂,但还是相当高效。在设置pages实例变量的值之外,set方法还需要向可能的观察者发送改变通知,这些观察者通过eNotity()来监听对象。在没有观察者时(例如,在一个批处理中),情况就可以得到优化,通知对象的构造、以及通知方法的调用,都由eNotificationRequired()方法来进行监控。eNotificationRequired()方法默认的实现只是简单地检查是否存在与对象相关的观察者(或适配器)。因此,若在没有观察者的情况下使用EMF对象,对eNotificationRequired的调用与一个null指针检查相当,它可以在JIT编译器中进行Inline处理。
对于其它类型的属性自动生成的访问方式,像String类型的title属性,有一些不同,但基本与上面列出的代码类似。
对于引用属性生成的访问器,特别是双向引用,就变得有点复杂。
单向引用
让我们用另一个与Book类有关联关系的类Writer来扩充示例模型。
在Book与Writer间的关联关系是,在本例中,一个单向的引用。从Book存取Writer的引用(角色)名字为author。
在EMF生成器中运行此模型,将会再生成一个新的接口Writer与实现类WriterImpl,并在Book接口中添加新的getter与setter方法。
Writer getAuthor();
void setAuthor(Writer value);
因为author引用是单向的,所以setAuthor()方法看起来还是一个简单的数据设置,与前面的setPages()类似:
public void setAuthor(Writer newAuthor)
{
Writer oldAuthor = author;
author = newAuthor;
if(eNotificationRequired())
eNotify(new ENotificationImpl(this, ...));
}
其中唯一的区别就是我们用一个对象指针代替了简单数据类型的字段。
因为我们在处理一个对象引用,所以,getAuther()方法会复杂一点。因为对于一些类型的引用,包括author类型,需要处理这种可能性:即被引用的对象(在本例中是Writer)可能被持久化在与源对象(在本例中是Book)不同的资源(文档)中。因为EMF的持久化框架使用懒惰加裁模式,一个对象指针(在本例中是author)在某一时间点可能只是对象的一个代理,而不是真正的引用此对象。综上所述,getAuthor()方法如下所示:
public Writer getAuthor()
{
if (author != null && author.eIsProxy())
{
Writer oldAuthor = author;
author = (Writer)eResolveProxy((InternalEObject)author);
if (author != oldAuthor)
{
if (eNotificationRequired())
eNotify(new ENotificationImpl(this, Notification.RESOLVE, ...));
}
}
return author;
}
代替简单地返回author实例变量,我们首先调用了从框架中继承来的方法eIsProxy()来检查引用是否是一个代理,若是,再调用eResolveProxy()来处理它。后者将会调用EcoreUtil.resolve(),一个静态工具方法来试图加载目标对象的文档,并使用代理URI来构建此对象。若成功,它将返回被解析的对象,若没有成功载入文档,则它还将再次返回代理。
双向引用
现在我们理解了代理解析对某些类型的引用的getter方式所起的影响,现在我们再来看一看双向关联关系如何影响setter方式。将单向关联改为如下:
现在关联是双向的了,通过不带箭头的关联线来进行表示。从Write访问Book的角色名为books。
当我们为模型重新生成代码时,getAuthor()方法不会有改变,但setAuthor()将变为如下所示:
public void setAuthor(Writer newAuthor)
{
if (newAuthor != author)
{
NotificationChain msgs = null;
if (author != null)
msgs = ((InternalEObject)author).eInverseRemove(this, ..., msgs);
if (newAuthor != null)
msgs = ((InternalEObject)newAuthor).eInverseAdd(this, ..., msgs);
msgs = basicSetAuthor(newAuthor, msgs);
if (msgs != null) msgs.dispatch();
}
else if (eNotificationRequired())
eNotify(new ENotificationImpl(this, ...)); // send "touch" notification
}
我们可以看到,当设置一个双向引用,如author,时,引用的另一端必须也同时被设置(通过调用eInverseAdd())。而且,我们也需要为另一端移除原先的author(通过调用eInverseRemove()),因为在模型中,author引用是单一的(也就是说,一本书只能有一个作者),Book不能拥有超过一个Writer引用。最后,我们通过调用另一个生成的方法basicSetAuthor()来设置新的author引用。此方法如下:
public NotificationChain basicSetAuthor(Writer newAuthor, NotificationChain msgs)
{
Writer oldAuthor = author;
author = newAuthor;
if (eNotificationRequired())
{
ENotificationImpl notification = new ENotificationImpl(this, ...);
if (msgs == null) msgs = notification; else msgs.add(notification);
}
return msgs;
}
这个方法与单向引用的set方法非常类似,除非msgs参数不为空,将会把notification加入到其中,代替原来直接触发此通知消息的做法。因为在双向引用的set操作中发生的正向的/反向的添加、以及移除操作,会有四个(在本例中是三个)不同的通知消息被生成。NotificationChain被用来集中所有这些单个的消息,直到所有状态改变都完成后再来触发它们。队列中的消息通过调用msgs.dispatch()进行发送。
多值引用
可以注意到在示例中books关联(从Writer到Book)是多值关联(0..*)。换句话说,一个作者可能写有多本书。EMF中的多值引用(上界大于1的引用)使用集合API来处理,所以在接口中只生成了getter方法:
public interface Writer extends EObject
{
...
EList getBooks();
}
注意到getBooks()返回一个替代java.util.List的EList。实际上,它们俩基本相同。EList是java.util.List在EMF中的子类,并在API中加入了两个move方法。除此之外,从客户端视角,你可以认为它就是一个JavaList。如,向books关联中添加一本书,只需简单地调用:
aWriter.getBooks().add(aBook);
或者,通过迭代器你可以如下所示做其它的事情:
for (Iterator iter = aWriter.getBooks().iterator(); iter.hasNext(); )
{
Book book = (Book)iter.next();
...
}
如上面所示,从客房端视角,操纵多值引用没有任何特殊之处。然而,因为books引用是双向引用的一部分(它是Book.author的另一端),我们还是要做如同在setAuthor()方法中所示的所有相对的握手处理。从WriterImpl中的getBooks()方法的实现代码可看到如何处理多值引用的情况:
public EList getBooks()
{
if (books == null)
{
books = new EObjectWithInverseResolvingEList(Book.class, this, LibraryPackage.WRITER__BOOKS, LibraryPackage.BOOK__AUTHOR);
}
return books;
}
getBooks()方法返回一个特殊的实现类EObjectWithInverseResolvingEList,通过向它提供为在添加或移除操作时顺利完成相对的握手处理所需的信息来构造它的实例。EMF实际上提供了20种不同的特殊的EList实现,来高效地实现所有类型的多值属性。对于单向关联关系(也就是说,没有反向)我们可以使用EObjectResolvingElist。若引用操作不需要代理解析,我们可以使用EObjectWithInverseEList或者EObjectEList等等。
所以对于我们的例子,实现对bookd引用的list对象使用参数LibraryPackage.BOOK_AUTHOR来进行创建(一个自动生成的静态int常量,表示相对的特性)。此参数在调用add()方法中对Book的eInverseAdd()方法进行调用时使用,类似于在Book的setAuthor()期间对Writer的eInverseAdd()调用的方式。下面是在BookImpl中eInverseAdd() 方法的实现:
public NotificationChain eInverseAdd(InternalEObject otherEnd, int featureID, Class baseClass, NotificationChain msgs)
{
if (featureID >= 0)
{
switch (eDerivedStructuralFeatureID(featureID, baseClass))
{
case LibraryPackage.BOOK__AUTHOR:
if (author != null)
msgs = ((InternalEObject)author).eInverseRemove(this, .., msgs);
return basicSetAuthor((Writer)otherEnd, msgs);
default:
...
}
}
...
}
它首先调用eInverseRemove()方法来移除任何原先的作者(如同Book的setAuthor()方法),然后,它调用basicSetAuthor()来实际设置引用。虽然在我们的例子中,只有一个双向引用,在eInverseAdd()中使用了switch结构可以对Book类的所有双向引用进行处理。
容器引用(复合聚合)
让我们再增加一个新类,Library,把它作为Book的容器。
容器引用通过在Library这端使用黑心菱形的箭头线来表示。此关联关系表示一个Library聚合,可以有0或更多本书。值聚合(包容)关联关系是特别重要的因为它表示了一个目标实例的父实例,或者称为拥有者,它也指明了在持久化处理时对象的物理位置。
容器从多个方面影响代码生成。首先,因为被包容的对象保证与它的容器处于同一资源中,就不再需要代理解析。因而,在LibraryImpl中生成的get方法将使用一个不需要解析的EList实现类:
public EList getBooks()
{
if (books == null)
{
books = new EObjectContainmentEList(Book.class, this, ...);
}
return books;
}
因为不需要完成代理解析,一个EObjectContainmentEList可以非常高效地实现contains()操作(就是说,与普通的线性增长的耗时,可在一个常量时间内完成操作)。这是非常重要的,因为在EMF引用列表中,不允许进行复制项目的操作,所以在add()操作中会调用contains()方法。
因为一个对象只能有一个容器,添加一个对象到容器关联中意味着必须将它从现在所处的容器中移出。例如,添加一本book到Library的books列表中将包括将此book从其它Library的books列表中移除的操作。它与那些相对端的重数是1的双向关联关系没有任何不同。让我们假定,若Writer类对于Book也拥有一个容器关联,称为ownedBooks。这时,一个给定的处于某个Writer的ownedBooks列表中的book实例,当它被加到一个Library的books引用时,它也将被从Writer的列表中移除。
要高效地实现此种操作,基类EObjectImol拥有一个EObject类型的实例变量(eContainer),用来存储包容它的容器。作为结果,一个容器引用隐含地一定是双向引用。要从Book访问Library,你可以写如下代码:
EObject container = book.eContainer();
if (container instanceof Library)
library = (Library)container;
若你想要避免向下造型,你可以将关联明确地改为双向:
并让EMF来为你生成一个良好的类型安全的get方法:
public Library getLibrary()
{
if (eContainerFeatureID != LibraryPackage.BOOK__LIBRARY) return null;
return (Library)eContainer;
}
注意到,在明确的get方法中使用从EObjectImpl中继承的eContainer变量,来代替像前面例子中一样生成一个实例变量。
枚举属性
到目前为止,我们已经看了EMF如何处理简单属性,以及各种类型的引用。另一种公共的属性类型是枚举。枚举属性使用Java类型安全的enum模式来实现。
我们增加一个枚举属性,category,到Book类:
重新生成实现类,Book接口现在包括针对category的getter与setter。
BookCategory getCategory();
void setCategory(BookCategory value);
在生成的接口中,category方法使用了类型安全的枚举类—BookCategory。它为枚举值定义了静态常量,以及其它的方法,如下:
public final class BookCategory extends AbstractEnumerator
{
public static final int MYSTERY = 0;
public static final int SCIENCE_FICTION = 1;
public static final int BIOGRAPHY = 2;
public static final BookCategory MYSTERY_LITERAL =
new BookCategory(MYSTERY, "Mystery");
public static final BookCategory SCIENCE_FICTION_LITERAL =
new BookCategory(SCIENCE_FICTION, "ScienceFiction");
public static final BookCategory BIOGRAPHY_LITERAL =
new BookCategory(BIOGRAPHY, "Biography");
public static final List VALUES = Collections.unmodifiableList(...));
public static BookCategory get(String name)
{
...
}
public static BookCategory get(int value)
{
...
}
private BookCategory(int value, String name)
{
super(value, name);
}
}
以下略…,因为在JDK5.0中,已提供了类似的Enum实现。要了解上面的代码,请参见相关的文档。
工厂与包
在模型接口与实现类之外,EMF还至少生成两个接口(以及它们的实现类):一个工厂与一个包。
工厂,如同名字的意思,用来创建模型中类的实例,而包则提供一些静态变量(如,生成的方法所使用的特性常量)以及便利方法来存取你模型的元数据。
下面是book示例中的工厂接口:
public interface LibraryFactory extends EFactory
{
LibraryFactory eINSTANCE = new LibraryFactoryImpl();
Book createBook();
Writer createWriter();
Library createLibrary();
LibraryPackage getLibraryPackage();
}
如上所示,生成的工厂为每模型中定义的类提供一个工厂方法(create),以及访问模型包的方法,一个指向自身的静态常量单值引用。
LibraryPackage接口提供了对模型元数据进行方便的访问:
public interface LibraryPackage extends EPackage
{
...
LibraryPackage eINSTANCE = LibraryPackageImpl.init();
static final int BOOK = 0;
static final int BOOK__TITLE = 0;
static final int BOOK__PAGES = 1;
static final int BOOK__CATEGORY = 2;
static final int BOOK__AUTHOR = 3;
...
static final int WRITER = 1;
static final int WRITER__NAME = 0;
...
EClass getBook();
EAttribute getBook_Title();
EAttribute getBook_Pages();
EAttribute getBook_Category();
EReference getBook_Author();
...
}
如上所示,元数据通过两种形式提供:int型常量,以及Ecore元对象。int常量提供了传递元信息的高效手段。你可以注意到生成的方法在实现中使用到了这些常量。稍后,当我们察看如何实现EMF适配器时,你将看到这些常量还为处理消息时,判断什么发生了改变时,来提供高效的手段。还有,与工厂类似,生成的包接口,也提供了一个指向自身的单值引用。
生成拥有超类的类
让我们在Book模型中来创建一个子类,SchoolBook,如下:
EMF代码生成器会如你所愿地处理单一继承,生成的接口扩展了超类接口。
public interface SchoolBook extends Book
实现类也扩展对应的超实现类。
public class SchoolBookImpl extends BookImpl implements SchoolBook
在Java中,支持接口的多重继承,但每个EMF实现类只能扩展其中一个基类的实现类。因此,若模型中有多重继承,我们需要决定将使用哪个类作为基类,其它的都只被当成接口的合并,并在继承后的实现类中提供所有的接口实现。
考虑如下图所示的模型:
我们让SchoolBook继承两个类:Book以及Asset。我们标志Book类作为实现类的基类。若我们重新生成代码,接口SchoolBook将会扩展两个接口:
public interface SchoolBook extends Book, Asset
实现类也与前面相同,只是现在包括了从接口合并进来的方法getValue()与方法setValue():
public class SchoolBookImpl extends BookImpl implements SchoolBook
{
public float getValue()
{
...
}
public void setValue(float newValue)
{
...
}
...
}
定制生成的实现类
你可以向生成的Java类中添加行卫(方法或实例变量),而不用担心一旦模型发生变动后重新生成代码会搞丢你加入的东西。例如,我们向类Book加入一个方法,isRecommended()。只要在Java接口Book中简单地加入新方法的声明即可。
public interface Book ...
{
boolean isRecommended();
...
}
以及它们在实现类中的实现:
public boolean isRecommended()
{
return getAuthor().getName().equals("William Shakespeare");
}
EMF生成器不会擦去这些修改,因为它不是一个自动生成的方法。每个EMF生成的方法都包括一个包含@generated标签的Javadoc注解,如下:
/**
* ...
* @generated
*/
public String getTitle()
{
return title;
}
不管怎样重新生成代码,EMF都不会去碰所有不包含此标签的方法(如isRecommended()方法)。实际上,若我们想要修改一个自动生成的方法,我们可以从它的注解中移除@generated标签。
/**
* ...
* @generated // (removed)
*/
public String getTitle()
{
// our custom implementation ...
}
现在,因为没有@generated标签,getTitle()方法被认为了用户的代码;若我们重新生成代码,生成器将会检测到冲突,并简单地放弃为此方法生成代码。
实际上,在放弃一个生成的方法前,生成器首先检查,在文件中是否存在另一个相同名字的,但加上Gen的,自动生成的方法。若它找到一个,则将会把生成的代码重定向到这个方法中。例如,若我们想扩展生成的getTitle()方法实现,来代替完全地放弃它,我们可以简单地改一下方法名字:
/**
* ...
* @generated
*/
public String getTitleGen()
{
return title;
}
并加入我们自己的覆盖方法:
public String getTitle()
{
String result = getTitleGen();
if (result == null)
result = ...
return result;
}
现在我们重新生成代码,生成器将会检测到与我们自己的getTitle()有冲突,但因为在类中存在带@generated标签的getTitleGen()方法,它将会重定向新生成的代码到此方法中,而不是简单地放弃生成新的代码。
EMF模型上的操作
除了属性与引用,你可以向模型中的类添加操作。若你这么做,则EMF生成器将会在接口中生成方法的声明,在实现类中生成一个方法框架。EMF不对行卫建模,所以实现要由用户自己写Java代码来提供。
可以像上面说的,移除@generated标签,然后加入自己的代码。另处,也可以将Java代码包括在模型中间。在Rose上,你可以在操作的属性对话框的Semantics页面中的文本框中输入相应的代码。这些代码将会作为操作的注解被加入到EMF模型中,可以被生成到方法体中。
使用生成的EMF类
创建并访问实体
使用生成的类,客户商程序可以创建并初始一个Book对象:
LibraryFactory factory = LibraryFactory.eINSTANCE;
Book book = factory.createBook();
Writer writer = factory.createWriter();
writer.setName("William Shakespeare");
book.setTitle("King Lear");
book.setAuthor(writer);
因为Book到Writer的关联关系(author)是双向关系,所以相反方向的引用(books)就自动初始化了。我们可以通过下面代码来验证:
System.out.println("Shakespeare books:");
for (Iterator iter = writer.getBooks().iterator(); iter.hasNext(); )
{
Book shakespeareBook = (Book)iter.next();
System.out.println(" title: " + shakespeareBook.getTitle());
}
运行它可以产生如下输入:
Shakespeare books:
title: King Lear
存储与载入资源
要创建一个包含上述模型的名字为mylibrary.xmi的文档,我们只要创建一个EMF资源,将book与writer放入资源,并调用save()存储:
// Create a resource set.
ResourceSet resourceSet = new ResourceSetImpl();
// Register the default resource factory -- only needed for stand-alone!
resourceSet.getResourceFactoryRegistry().getExtensionToFactoryMap().put( Resource.Factory.Registry.DEFAULT_EXTENSION, new XMIResourceFactoryImpl());
// Get the URI of the model file.
URI fileURI = URI.createFileURI(new File("mylibrary.xmi").getAbsolutePath());
// Create a resource for this file.
Resource resource = resourceSet.createResource(fileURI);
// Add the book and writer objects to the contents.
resource.getContents().add(book);
resource.getContents().add(writer);
// Save the contents of the resource to the file system.
try
{
resource.save(Collections.EMPTY_MAP);
}
catch (IOException e) {}
注意到一个资源集(接口ResourceSet)被用来创建EMF资源。EMF框架使用资源集来管理可能跨越文档引用的资源。使用registry(接口Resource.Factory.Registry),它针对给定的URI基于它的模式、文件扩展名、或者其它可能的条件来创建正确的资源类型。在这时,我们将XMI资源实现登记作为资源集的默认类型。在载入时,资源集也会管理跨越文档引用的载入请求。
运行程序将会产生文件mylibrary.xmi,其内容如下:
<xmi:XMI xmi:version="2.0" xmlns:xmi="http://www.omg.org/XMI" xmlns:library="http:///library.ecore">
<library:Book title="King Lear" author="/1"/>
<library:Writer name="William Shakespeare" books="/0"/>
</xmi:XMI>
要载入文档mylibrary.xmi,我们设置一个资源集,然后简单地请求载入资源,如下所示:
// Create a resource set.
ResourceSet resourceSet = new ResourceSetImpl();
// Register the default resource factory -- only needed for stand-alone!
resourceSet.getResourceFactoryRegistry().getExtensionToFactoryMap().put(Resource.Factory.Registry.DEFAULT_EXTENSION, new XMIResourceFactoryImpl());
// Register the package -- only needed for stand-alone!
LibraryPackage libraryPackage = LibraryPackage.eINSTANCE;
// Get the URI of the model file.
URI fileURI = URI.createFileURI(new File("mylibrary.xmi").getAbsolutePath());
// Demand load the resource for this file.
Resource resource = resourceSet.getResource(fileURI, true);
// Print the contents of the resource to System.out.
try
{
resource.save(System.out, Collections.EMPTY_MAP);
}
catch (IOException e) {}
再次,我们创建了一个资源集,在独立的情况下,登记一个默认的资源实现。同时,我们要确保我们的包被登记在包注册项(registry)中,资源将使用它来为将载入的模型获取适当的元数据与工厂。简单地访问自动生成的包接口的eINSTANCE字段,就可以保证它被注册。
这个例子还使用了save的第二种形式,提供一个OutputStream,来打印序列化到控制台上。
要将一个模型分隔到多个文档中,并在它们之间存在交叉引用是非常简单的。若我们要将books与writers分别序列化到不同的文档中,我们要做的就是创建第二个资源:
Resource anotherResource = resourceSet.createResource(anotherFileURI);
并将Writer添加进去:
resource.getContents().add(writer); // (replaced)
anotherResource.getContents().add(writer);
这样就可以产生两个资源,第个包含一个对象,并在两者间存关联关系。
注意,一个容器引用暗示着被容纳的对象要与它的容器在相同的资源中。因如,例如,假定我们创建一个Library的实例,并通过books容器引用来包含Book。这样将会自动地将Book从资源的内容中移除,它的道理就跟容器引用一样。若我们将Library添加到资源中,则book也会隐式地从属到资源中来,且它的内容也可以再次序列化到此资源中。
若你想要将你的对象以其它的格式序列化,你必须要提供你自己的序列化与分析的代码。创建你自己的资源类(作为ResourceImpl的子类)来实现你的序列化格式,然后将你的资源集在本地注册;若你想一直在模型中使用它,则使用全局工厂注册。
探测(adapting)EMF对象
在前面,当我们观察生成的EMF类的set方法时,我们看到当一个属性或引用被改变时,总会发送通知。例如,BookImpl.setPages()方法包括如下代码:
eNotify(newENotificationImpl(this, ..., oldPages, pages));
每个EObject能够维护一个观察者列表(也被称为适配器),当一个状态改变发生时,它将将会得到通知。框架的eNotify()方法迭代此列表,并向观察者传递通知。
一个观察者可以通过添加到eAdapter列表被附加到任意EObject上(例如,book):
Adapter bookObserver = ...
book.eAdapters().add(bookObserver);
更一般的情况,适配器通过使用一个适配器工厂来添加到EObject。除了它拉的观察者角色外,适配器还常被用来作为扩展它所附属的对象行卫的手段。客户端通常通过请求一个适配器工厂来为一个对象适配一个所需类型的扩展,来附加上这些扩展行卫。通常,它们看起来如下所示:
EObject someObject = ...;
AdapterFactory someAdapterFactory = ...;
Object requiredType = ...;
if(someAdapterFactory.isFactoryForType(requiredType))
{
Adapter theAdapter = someAdapterFactory.adapt(someObject, requiredType);
...
}
通常,requiredType表示由适配器所支持的一些接口。例如,参数可以是实现选中的适配器接口的java.lang.Class。返回的适配器也将被向下转型为所请求的接口:
MyAdapter theAdapter =
(MyAdapter)someAdapterFactory.adapt(someObject, MyAdapter.class);
适配器通常通过这种不需要子类的形式来扩展一个对象的行卫。
要在适配器中处理通知,我们需要覆盖eNotifyChanged()方法,此方法将会被eNotify()方法在每个已注册的适配器上进行回调。一个典型的适配器实现eNotifyChanged()来完成基于通知类型的针对此通知的动作。
某些情况,适配器被设计用来适配一个特定的类(如,Book),在这种情况下,notifyChanged()方法可能像下面所示:
public void notifyChanged(Notification notification)
{
Book book = (Book)notification.getNotifier();
switch (notification.getFeatureID(Book.class))
{
case LibraryPackage.BOOK__TITLE:
// book title changed
doSomething();
break;
caseLibraryPackage.BOOK__CATEGORY:
// book category changed
...
case ...
}
}
对notification.getFeatureID()的调用被传入参数Book.class来处理可能被适配的对象不是BookImpl类的实例的情况,如在一个多重继承情况下,且Book又不是其主要接口。在这种情况下,在通知中被传递的featureID将会可能是与其它类相关的一个数字,所以在使用switch中使用Book_常量前,我们要首先进行相应的调整。在单继承情况中,这个参数可以被忽略。
另一种常用的适配器类型是不界定到任何特定的类,而使用EMF反射API来完成它的功能。这时不使用通知的getFeatureID()方法,而是用getFeature代替,它可以返回真正的Ecore特性(即在元模型中表示此特性的对象)。
使用反射API
每个生成的类都可以通过使用定义在EObject接口中的反射API来进行操纵。
public interface EObject ...
{
..
Object eGet(EStructuralFeature feature);
void eSet(EStructuralFeature feature, Object newValue);
boolean eIsSet(EStructuralFeature feature);
void eUnset(EStructuralFeature feature);
}
使用反射API,我们可以如下来设置一个作者的名字:
writer.eSet(LibraryPackage.eINSTANCE.getWriter_Name(), "William Shakespeare");
或者得到名字:
String name = (String)writer.eGet(LibraryPackage.eINSTANCE.getWriter_Name());
注意到,被访问的特性由从library包的单实例中获取的元数据进行标示。
使用反射API会比直接使用生成的getName()或setName()方法稍微损失一些效率,但为模型提供了更一般化的访问。例如,反射方法可以被EMF.Edit框架用来实现可以在所有模型上使用的命令操作集(如,AddCommand、RemoveCommand、SetCommand)。详细内容请参见EMF.Edit框架。
除了eGet()与eSet(),反射API还包括两个其它相关的方法:eIsSet()与eUnset()。eIsSet()方法可以用来检查属性值是否已被设置,使用eUnset()可以清除属性的值。一般的XMI序列化,例如,使用eIsSet()来检查某个属性是否需要在资源保存操作时被序列化。
高级主题
代码生成控制标志
在模型特性上可以通过设置一些标志来控制为此特性生成代码的方式。典型的,默认情况下这些设置可以很好地工作,所以你不用总是去改变它们。
Unsettable (默认为 false)
一个被声明为unsettable的特性将明确地拥有unset或没有值的状态。例如,一个非unsettable的boolean型属性可以有二个值:true或者false。若它被声明为unsettabel,则它拥有三个值:true、false、或者unset。
unset的属性的get方法将返回默认值,但对于一个unsettable特性,这种情况与它被明确地设置为默认值有很大的不同。因为unset状态是在允许的值范围外的状态,我们需要生成附加的方法来将一个特性设置为unset状态,并可以检查它是否处于此状态。例如,若Book的pages属性被设为unsettable,则我们将得到另外的两个自动生成的方法:
boolean isSetPages();
void unsetPages();
原有的两个方法是:
int getPages();
void setPages(int value);
若特性值明确地被设置,则isSet方法返回true。unset方法将属性状态设置到unset状态。
若unsettable为false,将不会生成isSet与unset方法,但我们还是可以得到相应的反射API版本:eIsSet()以及eUnset()(每个EObject都必须要实现的方法)。对于非unsettable属性,若属性值不等于默认值,则eIsSet()返回true,eUnset()将属性值设置为默认值(类似于reset操作)。
ResolveProxies (默认为true)
ResolveProxies只能被设置到非容器引用。它意味着引用可能跨越文档,需要在get方法中加入代理检查与解析操作。
在确认不可能出现跨文档的场境下,你可能通过设置resolveProxies为false来优化生成的get方法工作模式。在这种情况下,生成的get方法将具有最佳性能。
Unique (默认为true)
unique只能被设置到多值属性,表示属性值中不能包含多个相当的对象。引用总是被作为unique对待。
Changeable (默认为true)
一个非Changeable属性将不会包含set方法,若你想要调用eSet()方法,则会抛出一个异常。在双向关系中将一个端点设置为非Changeable标志是一个好办法,它让客户端只能从另一端设置属性的值,而同时还能提供从任意一端的导航方法。将单向的引用或一个属性设置为非Changeable,则意味着必须通过用户自己的代码才可能更改属性的值。
Volatile (默认为false)
声明为volatile的特性将不会生成用来存储属性值的字段,且方法体也是空的,必须要由你自己来填充它。Volatile标志通常被用来表示一个特性的依赖于另一个特性,或需要手工地提供其它的存储与实现方式。
Derived (默认为false)
声明为Derived的特性值从其它特性计算得到,所以它不表示任何附加的对象状态。框架类,如EcoreUtil.Copier,在拷贝模型对象是不会处理这些特性。生成的代码不会受derived标志的影响,除了包的实现类,它负责初始化模型的元数据。Derived特性通常还被置为Volatile与Transient。
Transient (默认为false)
声明为Transient的特性表示其生命周期从来不会跨越应用运行的生命周期,从而不需要被持久化。序列化(如XMI)处理将不会处理被标志为Transient的特性的值。与Derived一样,在代码生成中,Transient也只对包的实现类的代码产生影响。
数据类型
在前面提到,所有模型中定义的类(例如,Book,Writer)都明确都从EMF基类EObject继承得到。然而,模型使用的类并非都必须是EObject。例如,假定我们想为模型加入一个java.util.Date类型的属性。在我们开始做之前,我们必须定义一个EMF DataType来表示这个外部类型。在UML中,我们使用一个datatype版类的类来实现此目标:
如同图中所示,一个数据类型是模型中一个简单的命名元素,作为一些java类在模型中的代理。真正的java类在一个javaclass版类属性中提供,它的名字就是java类的全质名。定义好了数据类型后,我们可以声明java.util.Date类型的属性。
重新生成代码,publicationDate属性将会出现在Book接口中:
import java.util.Date;
public interface Book extends EObject
{
...
Date getPublicationDate();
void setPublicationDate(Date value);
}
如上所示,Date类型的属性被处理得一样完美。事实上,所有属性,包括类型为String、int等等,都拥有一个数据类型作为它们的类型。对于标准的java类型,已经在Ecore模型中预定义好了对应的数据类型,所有当使用它们时,不需要首先进行定义。
一个数据类型的定义还会对生成的模型有另外的影响。因为数据类型表示一些任意的类,一般的序列化与分析器(如,默认的XMI序列化器)没有办法知道如何保存这种类型的属性值。是不是调用toString()?这是个合理的默认选择,但EMF框架并不这样要求,它将在工厂实现类中为每个模型中定义的数据类型生成两个(或更多)的方法。
/**
* @generated
*/
public Date createJavaDateFromString(EDataType eDataType, String initialValue)
{
return (Date)super.createFromString(eDataType, initialValue);
}
/**
* @generated
*/
public String convertJavaDateToString(EDataType eDataType, Object instanceValue)
{
return super.convertToString(eDataType, instanceValue);
}
默认情况下,这些方法简单地调用超类的,合理但低效的实现,默认的:convertToString()简单地对instanceValue调用toString();但createFromString会尝试使用java的反射机制来调用String的构造器,或者,调用静态的valueOf()方法,若此方法存在的话。典型的,你应该移除这些方法(通过移除@generated标签)并使用定制代码来实现它们。
/**
* @generated // (removed)
*/
public String convertJavaDateToString(EDataType eDataType, Object instanceValue)
{
return instanceValue.toString();
)
Ecore模型
下面是Ecore模型完整的类层次结构(有阴影的框表示抽象类):
在本文中,我们只介绍了表示EMF组件的一小部分:EClass(以及它们的属性、引用与操作)、EDataType、EEnum、EPackage、以及EFactory。
EMF中Ecore的实现本身是由EMF生成器生成的,是如同本文中前面提到的轻量、且高效的实现。