持久化(Persistence)
EMF拥有一个强大的模型持久化框架。通过一个高度可定制资源实现(resource implementation)来支持XML序列化。
EMF不要求持久化是基于XML或者是基于流(stream)。这个框架提供的API灵活到足以支持任何种类的存储,甚至在不同类型的存储中保存对象,其中包含引用。
持久化框架的概述
EMF中持久化的基本单元叫做资源(resource),它是一个或多个与其内容一起持久化的对象的容器。
EMF对象由Resource接口来进行持久化,方法是将对象添加到资源的内容列表中,然后调用save()方法,例子如下:
PurchaseOrder po = ...
Resource resource = ...
resource.getContents().add(po);
resource.save(null);
这个例子中,save()方法的参数,一个Map,如果指定了保存操作的选项,那么这个参数将非空(non-null)。EMF的XML资源支持的选项详细介绍在15.3.3。
持久化的逆操作,从持久化形式中在内存中重建活动(actiive)的对象。使用load()方法从资源的内容列表中访问对象:
Resource resource = ...
resource.load(null);
PurchaseOrder po = (PurchaseOrder)resource.getContents().get(0);
Resource接口的详细说明在15.2.3。
上面的代码带来了以下问题:XML序列化写到哪里?从哪里读入?开始的时候如何获取资源?
为了管理不同资源中对象之间的引用,EMF持久化框架包含了另一种接口,称作ResourceSet,作用相当于资源的容器。getResources()方法返回的是资源的列表,以一个集合(set)的形式。
通常而言,资源是由资源集合创建的,或者是加载的。如下:
ResourceSet resourceSet = new ResourceSetImpl();
URI uri = URI.createURI("file:/c:/data/out.epo2");
Resource resource = resourceSet.createResource(uri);
这里我们创建了一个资源集合,调用createResource()来创建特定的资源。这个方法的参数是URI,它被用来指定资源,在资源集合中识别出这个资源。然后,我们调用save()和load()方法,资源可以使用这个URI来决定写出和读入的位置。这里的URI是文件模式(file-scheme),其他支持的URI类型详见15.2.1。
EMF不会向资源集合提供工厂。由用户来决定实例化合适的实现。ResourceSetImpl是一个功能灵活的实现,通常应该是足够的。如果有必要的话,它也可以被扩展以及定制。
传给createResource()的URI还有另外的用途。资源集合维持(maintain)资源工厂的注册表。为了创建一个资源,需要查询(consult)它的注册表,以根据特定的URI来获取一个合适的工厂。这个工厂实际上创建了对应的资源,并且决定对象如何被持久化。
注册表根据模式(scheme)或者文件扩展名(file extension)来选择资源工厂。我们可以对.epo2文件扩展名注册EMF的默认XMI资源工厂:
resourceSet.getResourceFactoryRegistry().getExtensionToFactoryMap().
put("epo2", new XMIResourceFactoryImpl());
当使用Eclipse里生成的模型时,这种注册不是必要的,因为相似的注册会通过模型插件(model plug-in)的清单文件(manifest file)中指定的扩展名来自动地执行。然而,当独立运行时,注册必须被明确地执行。否则,createResource()将无法创建资源并且会返回null。详见15.2.4的Resource的嵌套工厂和Registry接口以及15.2.5的ResourceSet
现在已经有了对持久化框架的足够理解,来编写简单但有效的程序来保存采购清单:
PurchaseOrder po = createPurchaseOrder();
ResourceSet resourceSet = new ResourceSetImpl();
resourceSet.getResourceFactoryRegistry().getExtensionToFactoryMap().
put("epo2", new XMIResourceFactoryImpl());
URI uri = URI.createURI("file:/c:/data/out.epo2");
Resource resource = resourceSet.createResource(uri);
resource.getContents().add(po);
try
{
resource.save(null);
System.out.println("saved");
}
catch (IOException e)
{
System.out.println("failed to write " + uri);
}
这里的createPurchaseOrder()返回的是我们想要持久化的采购清单。我们可以设想它创建了PurchaseOrder的实例并且向其中加入一组Items,在out.epo2的最终的序列化结果如下:
<?xml version="1.0" encoding="ASCII"?>
<epo2:PurchaseOrder xmi:version="2.0"
xmlns:xmi="http://www.omg.org/XMI"
xmlns:epo2="http://www.example.com/epo2.ecore"
comment="rush order">
<items productName="apple" quantity="10" partNum="3313"/>
<items productName="orange" quantity="10" partNum="9145"/>
</epo2:PurchaseOrder>
从其持久化形式中加载采购清单的程序如下:
EPO2Package epo2Package = EPO2Package.eINSTANCE;
ResourceSet resourceSet = new ResourceSetImpl();
resourceSet.getResourceFactoryRegistry().getExtensionToFactoryMap().
put("epo2", new XMIResourceFactoryImpl());
URI uri = URI.createURI("file:/c:/data/out.epo2");
Resource resource = resourceSet.createResource(uri);
try
{
resource.load(null);
PurchaseOrder po = (PurchaseOrder)resource.getContents().get(0);
System.out.println("loaded: " + po);
}
catch (IOException e)
{
System.out.println("failed to read " + uri);
}
程序的第一行,访问采购清单模型的包,另一种作用是注册包。生成的包首次被访问时,在全局包注册表中自动地注册。资源取决于能够找到注册的包,以反射式地实例化其描述的类。然而,类似于资源工厂注册表,这种显式(explicit)注册的过程仅当独立运行时被要求,在Eclipse下运行时通过扩展指针(extension point)来自动地完成。
EMF持久化API
EMF持久化框架以四个基础接口为中心:Resource,ResourceSet, Resource.Factory, URIConverter。
URI
URI是一个字符串,包含三个基本的部分:scheme、scheme-specific部分、可选片段。
URIConverter
这个接口用来规范化 URIs:将一个URI转换为另一个访问资源的URI。
Resource
前面已经说过,EMF中持久化的基本单元叫做资源(resource)。尽管资源直接关系到EObjects,但是不像从第5章描述的元模型中生成的各种Ecore接口,Resource接口事实上不会来描述模型化的类。然而,它表现的像重要的方式之一:
public interface Resource extends Notifier
{
int RESOURCE__RESOURCE_SET = 0;
int RESOURCE__URI = 1;
int RESOURCE__CONTENTS = 2;
...
ResourceSet getResourceSet();
URI getURI();
void setURI(URI uri);
EList getContents();
...
}
首先,资源是通知器(notifier):多个适配器(adapter)可以被附加于一个资源,当资源改变时,将会给适配器们发送通知。可以看到,接口定义了“虚拟特征(virtual features)”,比如resourceSet,URI和内容,它们都被使用于通知中。
其次,getContents()返回的列表,描绘了资源的内容,表现的十分像一个模型化的双向的引用,相反的引用由EObject中的eResource()方法实现。需要注意的是,这种关系实际上并不是真正的模式化,而是概念上的,情况如下图:
值得强调的是,当内容虚拟特征(contents virtual feature)期望getContents()访问器时,其相反的访问器的名字,eResource(),毫无疑问是非常规的(unconventional)。这是因为它反而遵循EObject中所有其他方法使用的模式,被选择以避免与在模型化子类中生成的访问器的名称相冲突。还值得注意的是,对于eResource()没有相应的设置(set)方法,所以这个虚拟特征只能从Resource end来设置,通过将对象加入到内容列表。
在URI这一节,资源默认地包含其内容列表中每个对象所包含的整个对象树。这影响到eResource()的返回值。
内容虚拟特征表现的像一个普通双向特征,相反的是multiplicity-one。这就是说,如果一个对象已经存在于一个资源的内容中,将其添加到另一个则会自动地从第一个中移除。
此外,内容虚拟特征展示了遏制(contaiment)语义,但仅涉及不是代理解析(proxy resolving)的模型化的遏制引用。换句话说,如果一个对象已经存在于任一模式化的非代理解析的遏制引用,把它添加到一个资源的内容中则会自动的将其从它的容器中移除。回顾第10章中遏制语义默认不是代理解析。因此,它就表现出默认阻止跨资源(cross-resource)遏制:一个对象只可以被直接放置在一个容器对象中,或者直接放置在一个资源内容中,而不能两者都放置。
“遏制代理”详见13.8.2。
一个资源通常被一个资源集合所包含,getResourceSet()来获取。资源在集合中通过URI来唯一标识。通常在资源创建时指定URI,但是只能分别通过getURI()和setURI来访问和设置。URI通常决定资源的持久化形式将要存贮的位置。
save()和load()方法将资源从活动的内存形式复制到持久化存储,反之亦然。每个都有两种形式:
void save(Map options) throws IOException;
void load(Map options) throws IOException;
void save(OutputStream outputStream, Map options) throws IOException;
void load(InputStream inputStream, Map options) throws IOException;
EMF为资源提供了一个基础类,ResourceImpl,它通过从资源集合中获取一个URIConverter来实现每个单参数的方法,这个URIConverter用来打开资源的URI的输出或输入流,还能将流传到对应的双参数方法。因此,特定子类的作用是提供有意义的,特定于存储(storage-specific)的保存和加载实现。
通过调用unload()方法可以卸载资源。卸载会将资源中所有的对象变为代理(proxy),这样的结果是,下次这些对象中之一从其他资源通过跨引用(cross-reference)被访问时,资源的请求重新加载(通过EcoreUtil.resolve())。这就可以把一个资源更新,比如自上次加载以来,底层文件( underlying file )发生了变化。isLoaded() 方法表明资源当前是否被加载。
资源可以通过isModified(),来跟踪对其完整内容的任何更改,并且返回自从最近保存和加载后的变更。默认情况下,这个特性是无效的,因为它的实现昂贵而且对撤销命令是无效的,由于撤销看起来像变更。适当时候,setTrackingModification() 可以使其生效。
URI Fragments,是资源基于ResourceImpl使用类似XPath的方式来定位对象。Resource接口定义getEObject() 方法,可以根据给出的段路径(fragment path)来检索对象:
Resource resource = ...
Item item = (Item)resource.getEObject("//@orders.0/@items.2");
相反,也可以获取段路径:
Item item = ...
String fragment = resource.getURIFragment(item);
Resource.Factory和Resource.Factory.Registry
嵌套在Resource接口中的是Factory接口,它定义了资源如何被创建:
public interface Resource extends Notifier
{
...
interface Factory
{
Resource createResource(URI uri);
interface Descriptor
{
Factory createFactory();
}
interface Registry
{
Factory getFactory(URI uri);
Map getProtocolToFactoryMap();
Map getExtensionToFactoryMap();
String DEFAULT_EXTENSION = "*";
Registry INSTANCE = new ResourceFactoryRegistryImpl();
}
}
}
正如这个接口建议的那样,资源工厂只是简单地知道如何去创建资源的一种类型。通过使用合适的资源工厂,就可以确定被产生的和被使用的持久化形式。
嵌套在Factory中的是Registry接口,它基于特定URI,提供了选择合适资源工厂的机制。资源工厂注册表维护着两个可以注册资源工厂的map。这些map被getFactory方法咨询,以获取适合于给定URI的工厂。
ResourceFactoryRegistryImpl(),按照以下步骤来获取资源工厂:
检查getProtocolToFactoryMap()返回的map,使用URI的模式作为关键字。
如果没有找到任何东西,检查getExtensionToFactoryMap()返回的map,使用URI的文件扩展名作为关键字。
如果仍然没有找到,就检查getExtensionToFactoryMap()返回的map的默认值,使用DEFAULT_EXTENSION*作为关键字。
如果没有找到相应的匹配,调用受保护的(protected)delegatedGetFactory()方法。默认情况下,它没有做任何事,但可以被重写(override)。
资源集合维护(maintain)着资源工厂注册表,当其需要创建资源时,就要咨询(consult)来获取工厂。此外,有一个可用的全局注册表Resource.Factory.Registry.INSTANCE,它可以注册系统中的所有资源集合。默认资源集合中注册表的实现重写了delegatedGetFactory() 来委托给全局注册表。所以仅当根据URI的模式,在局部没有找到合适的工厂时,才会根据文件扩展名(file extension)来咨询全局注册表。
在持久化框架概述中说道,在Eclipse环境下运行时,资源工厂可以通过Eclipse的扩展指针机制,来被自动地注册。这样的注册是在全局注册表中完成的。
org.eclipse.emf.ecore.xmi 插件清单文件包含以下扩展:
<extension point="org.eclipse.emf.ecore.extension_parser">
<parser type="*"
class="org.eclipse.emf.ecore.xmi.impl.XMIResourceFactoryImpl"/>
</extension>
它默认注册了XMIResoureFactoryImpl资源工厂实现。这就是为什么EMF的XMI序列化作为默认持久化格式,以及为什么在Eclipse下运行时没有要求显式的注册。
如果你创建了自己的资源实现类,你需要为其创建工厂然后进行注册,以相似的方式,在你自己的插件(plug-in)中。例如,设想你已经创建了自己的资源实现类,EPO2ResourceImpl,你想要使用它来持久化.epo2资源。为此,需要创建资源工厂:
public class EPO2ResourceFactoryImpl implements Resource.Factory
{
public Resource createResource(URI uri)
{
return new EPO2ResourceImpl(uri);
}
}
然后,你可以向plugin.xml文件中添加以下扩展:
<extension point="org.eclipse.emf.ecore.extension_parser">
<parser type="epo2"
class="com.example.epo2.util.EPO2ResourceFactoryImpl"/>
</extension>
事实上,这就是在为生成器中的包选择“资源类型(Resource Type)”时发生的事情。从以XML Schema描述的模型开始尤为重要,因为默认XML资源将不能读和写符合模式的序列化。如果注册不能完成或者你试图加载没有合适文件扩展名(file extension)的资源,Resource.load()将会抛出ClassNotFoundException。对于独立的应用程序,需要在代码进行以下等价的注册:
Resource.Factory.Registry.INSTANCE.getExtensionToFactoryMap().put(
"epo2", new EPO2ResourceFactoryImpl());
尽管等价,这两个资源工厂注册表(registration)还是有细微差别的。后者,显式注册,工厂的实例被创建并且被注册表中的一个map直接注册。然而,前者,基于扩展名(extension-based)的注册表,实际注册的是描述符(Descriptor)。
描述符,另一个嵌套在Resource.Factory中的接口,在创建资源工厂时提供了额外的间接层(indirection)的级别(level)。在完成之前所述的4步枚举型之后,如果注册获取了描述符而不是资源工厂,它将调用createFactory()并返回结果。这项功能使EMF能够推迟加载特定的资源工厂类直到这个类被真正需要时。这对基于扩展名的注册表很重要,其发生在平台启动时。
ResourceSet
ResourceSet是持久化API中最高级别的接口,作用相当于资源的容器,以便允许它们之间的引用。它提供了访问这些资源的途径,从模型或者一组相关联的模型中,定义了对象的上下文(context)。此外,它维护这URI转换器,资源工厂注册(resource factory registry),以及它自己使用的和用于资源的包注册(package registry)
类似于Resource,ResourceSet并不表示模型化的类,但是它实现Notifier接口:
public interface ResourceSet extends Notifier
{
int RESOURCE_SET__RESOURCES = 0;
EList getResources();
...
}
由getResource()返回的资源的列表,构成了它仅有的虚拟特征,表现得像个遏制引用(containment reference)。换句话说,资源每次只能在一个资源集合中。
ResourceSet为创建新资源提供了用户级别(user-level)的接口。在URI那一节,我们使用最简单的方法来实现这个目的,createResource(),它授权(delegate)给集合的资源工厂注册返回的资源工厂。回顾这个注册,是getResourceFactoryRegistry()返回的,当它不能定位适当的资源工厂时,就授权给全局资源工厂。createResource()方法在内存中创建资源,并且将其添加到资源集合中,毫不费力就能读入和写出持久化表示。在它盲目地创建资源前,也不会检查集合是否已经包含给出的URI的资源,这就有可能使集合处于无效状态。
一个更强大的方法,getResource(),首先规范化给出的URI,检查资源集合中是否有资源拥有与其匹配的规范化URI,如果有,返回这个资源。如果这个资源没有加载,它将会被要求加载(demand-loaded),如果此方法的第二个布尔参数为真的话。如果资源没有找到并且第二个参数为真,资源将会被要求创建(demand-created)和要求加载。使用getURIConverter()返回的URI 转换器来规范化URI,使用getLoadOptions()返回的map中指定的选项来完成要求的加载。
注意的是,资源的持久化形式不会被要求创建。如果因为持久化形式无法找到而导致要求的加载失败,getResource()会抛出异常。
所以,在持久化框架的概述中最后的例子可以被简化。在设置资源集合和创建URI之后,两行就足以创建资源,加载,并且访问其中的对象:
...
Resource resource = resourceSet.getResource(uri, true);
PurchaseOrder po = (PurchaseOrder)resource.getContents().get(0);
System.out.println("loaded: " + po);
ResourceSet中的另一种简便方法,getEObject(),允许我们进一步简化,替换获取资源和使用单个声明检索其中对象的两行:
...
PurchaseOrder po =
(PurchaseOrder)resourceSet.getEObject(uri.appendFragment("/"),true);
...
getEObject()方法简单地调用getResource()来获取资源,必要的话进行要求的创建和要求的加载,然后使用指定URI的分段(fragment)来定位和返回资源里面的对象。
包注册,提供了一种手段来定位描述任一特殊包的元数据。有个全局包注册,EPackage.Registry.INSTANCE,在其中生成的包会被自动地注册。在其他情况下,特定语境(context-specific)的包注册会更合适些。
事实上,正是资源集合提供了语境(context)。ResourceSet定义了getPackageRegistry()方法,其返回局部的(local)注册。这个局部注册只在局部不能找到包时,授权给全局注册。因此,加载到给定资源集合的资源已经检索的包,可以在不污染全局注册的情况下被注册。甚至有可能,针对同一个命名空间URI,仅仅通过局部注册一个不同的包,来重写一个全局注册表。
资源集合的主要目的是支持不同资源中对象之间的引用。特别的事,它们对要求的加载(demand-loading)和代理解析(proxy resolution)至关重要。例如,考虑资源如下序列化:
<?xml version="1.0" encoding="ASCII"?>
<epo2:Supplier ... >
<orders>
<previousOrder href="sample.epo2#//@orders.1"/>
</orders>
</epo2:Supplier>
可以看出资源中的Supplier包含一个PurchaseOrder引用了另一个资源中Supplier中序列化的PurchaseOrder。href属性表示的是跨文档(cross-document)的引用,属性的值是标识其他资源中引用的URI*。
当加载这个资源时,代理被创建作为previousOrder引用的目标,指定对象的URI(例如:platform:/resource/project/sample.epo2#//@orders.1)。如果想要访问采购清单的上个清单,可以调用 getPreviousOrder(),EcoreUtil.resolve()被调用以便解析代理(resolve proxy)。