简介:本文基于 EMF(Eclipse Modeling Framework)模型反射机制,实现 了一种 EMF 模型对象比较的方法,并展示如何使用该算法得出对象的匹配程度 。首先设定对象的待比较字段列表。对其中的每个字段,获取并比较对象的字段 值。在比较的过程中,该算法将组合数据类型(如自定义类、列表)的比较分解 为其子数据类型的比较。模型比较的结果是一个差异项列表,作为后续应用的基 础,可以被用于版本控制、模型导入 / 导出等场景中。
EMF 和 Ecore 简介
Eclipse Modeling Framework(EMF)是一个开放源代码的模型驱动应用程序 开发框架。它可以基于 XML Schema、UML 或带有模型特征注释的 Java 接口, 创建 Java 代码,实现图形化的数据编辑、操纵、读取和序列化。EMF 是 IBM WebSphere Studio 和 Eclipse 项目中很多工具的基础。
Ecore 元模型是 EMF 框架的核心,它描述 EMF 模型并且提供模型的运行时 支持,包括:模型修改通知,以默认的 XMI 序列化提供 EMF 的持久化支持,以 及通用于操作 EMF 对象的高效反射 API。本文正是运用 EMF 的反射 API 读取 EMF 对象的值,在此基础上完成 EMF 对象的比较。
图 1. Ecore 类型树
图 1 为 Ecore 的类型树。图中灰色填充背景表示在 EMF 框架中,该接口的 实现类为抽象类,黄色填充背景的接口有非抽象的实现类。对图中与本文相关的 类型介绍如下:
EAttribute:用来描述一个属性,它拥有一个名字和类型。EAttribute 描述 简单数据 , 它由一个 EDataType 来指定。
EClass:是 EMF 对象的元类型,用来描述建模模型。它以属性(EAttribute )和引用(EReference)描述建模类的字段(Field)。类似 Java 的 Object.getClass() 得到的 Class,调用 EObject 对象的 eClass() 方法可以 得到 EClass。
EDataType:用来描述一个属性的类型,这个属性必须是简单数据类型,包括 基本(primitive type)数据类型如:int,一个 Java 类型如 String,也可以 是一个数组。
EFacTory:为一个抽象工厂,它包含创建建模对象的方法。
EObject:由图 1 可见,EObject 为所有 EMF 建模对象的基类型 ( 或称超 类型 ),在 EMF 框架内类似于 java.lang.Object。为了区别于用户建模中的方 法名,EObject 接口中所定义的方法名都以”e”开头。如 eClass() 方法返回一 个 EMF 对象的元模型 (EClass)。
EPackage:在 Ecore 中,EPackage 包含关于模型类 (EClass) 和数据类型 (EDataType) 的信息,如何得到 EPackage 的实例和得到模型类的信息在后面将 详细介绍。
EReference:用来描述类之间的关联关系,EReference 有名称;一个描述包 含关系的布尔标志位(包含与否决定这两个类型之间的关系是聚合 (Aggregation )或者组合(Compostition));一个 reference( 目标 ) 类 型,用来指定关系的类型,由于关联关系是两个类型之间的关系,所以 EReference 总是指向组合数据类型。
EStructureFeature:是 EReference 和 EAttribute 的共同超类。在理解上 可以将其作为字段(Field)。因为 EStructuralFeature 的实现是抽象类,所 以得到的 EStructuralFeature 对象一定是 EAttribute 或者 EReference 类型 的实例。
EMF 的反射 API
反射的概念是由 Smith 在 1982 年首次提出的,主要是指程序可以访问、检 测和修改它本身状态或行为的一种能力。EMF 框架也提供了反射机制的接口及实 现。EMF 提供的反射机制是一个强大的工具。它使得代码更加灵活,这些代码可 以在运行时装配,而不是在编码阶段就将某些连接进行固定。对任何 EMF 对象 ,都可以使用反射 API 来存取它的数据。
图 2. EObject 接口
图 2 所示为 EObject 接口,其中 EMF 提供的反射方法有 eGet(),eSet() ,eUnset() 和 eIsSet(),由于 EObject 接口是所有 EMF 建模类型必须实现的 接口,所以所有 EMF 对象都可以使用反射 API 方法,这是本文阐述的实现比较 EMF 对象解决方案的基石。对这五个反射方法的解释如下:
Object eGet(EStructuralFeature)方法将返回 EStructuralFeature 表示属 性的值,等同于调用 Object eGet(EStructuralFeature,true) 方法。
Object eGet(EStructuralFeature,boolean)方法的 boolean 参数用来指定 是否在返回之前解析并加载代理(Proxies)的引用对象。关于 EMF 的代理 (Proxy) 请参阅参考文档扩展阅读。
eSet(EStructuralFeature,Object)方法将参数中的 Object 对象设置为指定 属性的新值。
eIsSet(EStructuralFeature)方法返回一个布尔值表示一个属性是否已经被 设置值。
eUnSet(EStructuralFeature)方法可以用来重置或取消一个属性的值。
用 EMF 反射实现对象按字段值比较
本章阐述如何得到 EStructuralFeature 对象,并通过 EMF 对象的反射方法 读取字段的值。对于取得的值,区别其是简单类型还是组合类型,之后分别进行 比较。
运用反射 API 读取对象的字段值
本文用 EObject 对象的 eGet(EStructuralFeature) 方法读取 EObject 对 象的值。首先,需要得到该方法的参数 EstructuralFeature。EMF 将模型类型 的字段相关描述信息集中放置到实现 EPackage 接口的类内部。EMF 代码生成工 具会为建模模型生成 EPackage 的子接口,由这个接口的静态变量 eINSTANCE 得到它的实例,将其作为 eGet 方法的参数即可得到相应的值。代码如清单 1 所示:
清单 1. 使用反射 API 的代码与非反射代码的示例
EStructuralFeature feature =ModelPackage.eINSTANCE.getNodeElement_Description(); Object remoteValue = remote.eGet(feature); Object localValue = local.eGet(feature);不使用反射的代码: Object removeValue = ((NodeElement)remote).getDescription (); Object localValue = ((NodeElement)local).getDescription ();
由清单 1 可见,如不使用反射 API 读取对象的值,首先需要将对象转型。 当需要取值的类型很多时,转型语句会随之增多,大大增加代码书写和维护的工 作量。尤其是这种取值方法必须已知被取值对象的类型,相对于使用反射 API 进行取值,这种方法是僵硬,且难于复用的。
判断简单数据类型或组合数据类型
因为要实现按值比较,所以首先要区别得到的字段值是简单数据类型还是组 合数据类型。
图 3. Eclass, EAttribute, EReference, EDataType 关系图
图 3 描述了 EClass, EAttribute, EReference 和 EDataType 的关系,可 知 EReference 类型的 eReferenceType 总是一个 EClass 的组合类型, EAttribute 的类型则总是一个简单类型。这有利于了解区分简单类型和组合类 型的方法,也便于理解 EMF 对建模模型的描述,从而更好的运用 EMF 反射 API 。于是,本文使用清单 2 代码所示的方法区分类型是简单还是组合,如下:
清单 2. 判断简单数据类型或组合数据类型
if (remote.eGet(feature) instanceof EList|| local.eGet (feature) instanceof EList) { // 集合数据类型,需要遍历其中每一个元素进行比较 } else if(remote.eGet(feature) instanceof EObject||local.eGet (feature) instanceof EObject) { // 组合数据类型 } else { // 简单数据类型 }
清单 2 代码通过 instanceof 操作符首先区分出列表类型,然后再区分出组 合数据类型,余下的作为简单数据类型处理。区分 EList 是因为它是一种集合 数据类型,需要进行遍历其中元素。比较 EList 的方法将在第 3.4 节中详细叙 述。
比较简单数据类型
通常,基本类型多以“==”操作符比较,但是“==”操作符在比较对象时是 按地址进行比较,所以此操作符并不适用于比较如 String 这样的简单数据类型 。按内容比较简单类型应选择 java.lang.Object 的 equals() 方法。由于 Java 自 5.0 版本新增加了自封箱(Autoboxing)特性,即 Java 编译器会在需 要时对所有基本数据类型做自封箱操作,比如将 int 自封箱为 Integer 对象, 将 boolean 自封箱为 Boolean 等。利用这个特性,只要将得到的值赋给 Object,由 Java 编译器去判断是否做自封箱处理,之后调用 equals 方法对两 个对象进行比较即可,省去了对基本数据类型的判断和使用“==”操作符比较的 繁琐步骤。比较简单数据的示例代码如清单 3:
清单 3. 用 equals 比较简单类型的数据
Object remoteValue = remote.eGet(feature); Object localValue = local.eGet(feature); if (remoteValue != null && localValue != null) { if (!remoteValue.equals(localValue)) { // remote value and local value are different } }
比较组合数据类型
对于组合数据类型,需要把其分解为简单数据类型比较。组合数据类型的字 段有可能仍是组合数据类型,所以需要进行递归分解。另外由于对象之间可能有 循环的关联关系,所以需要把已经比较过的对象放进备忘录,在每比较一个对象 之前先检查备忘录中是否已经记录了该对象,以避免程序陷入无限循环之中。
列表 (EList) 的比较算法可以简单概括为:求两个列表的差集,差集中的每 个元素都是一个比较的差异项;把两个列表的交集元素按组合类型或者简单类型 算法进行比较。清单 4 给出了比较列表的代码片断。
清单 4. 比较列表 EList
if (remote.eGet(feature) instanceof Elist || local.eGet (feature) instanceof Elist) { // if the value is a list compare((EList) remote.eGet(feature), (EList) local .eGet(feature), compareType, compareLog); } private void compare(List remote, List local, int compareType, List compareLog) { if (local == null || local.isEmpty()) { if (remote == null || remote.isEmpty()) return; for (int i = 0; i < remote.size(); i++) { EObject eo = (EObject) remote.get(i); compareLog.add(new CompareInfo (CompareInfo.REMOTE_ADD, compareType, eo, null)); } return; } for (int i = 0; i < remote.size(); i++) { EObject remoteObject = (EObject) remote.get(i); EObject localObject = findElementByID(local, remoteObject); if (localObject != null) { compareT((EObject) remoteObject, localObject, compareType,compareLog); } else { compareLog.add(new CompareInfo (CompareInfo.REMOTE_ADD, compareType, remoteObject, localObject)); } } for (int i = 0; i < local.size(); i++) { EObject localObject = (EObject) local.get(i); EObject remoteObject = findElementByID(remote, localObject); if (remoteObject == null) { //差集 local-remote 元 素 compareLog.add(new CompareInfo (CompareInfo.REMOTE_DELETE, compareType, remoteObject, localObject)); } } }
4. 一个完整的 EMF 模型比较器
下面针对一个本地模型与远端模型同步的场景,综合运用 EMF 反射 API 实 现一个完整的 EMF 模型比较器。所谓同步,是指将远端模型与本地模型做比较 ,得到两者的差异并显示给用户,最终由用户决定更新本地模型还是更新远端模 型的一系列动作。对照图 4 的类图,工具类 ModelComparePort 负责比较本地 模型和远端模型,每比较出一处不同,便生成一个 CompareInfo 对象记录此差 异项,最终得到一个差异项列表。CompareInfo 可以记录的差异类型包括:远端 / 本地模型增加新节点、远端 / 本地模型删除和修改节点。
图 4. 比较器类图
ModelComparePort 类在执行比较之前,函数 initALLStructuralFeature() 初始化一个 EStructuralFeature 列表,罗列所有需要参与比较的字段。如清单 5 所示。设定此列表的作用是控制比较的范围。
清单 5. 初始化需要比较的字段
private void initALLStructuralFeature() { // initiate the features that need to be compared ModelPackage pkg = ModelPackage.eINSTANCE; _featureList.add(pkg.getProjectModel_Scenarios()); _featureList.add(pkg.getProjectModel_ProjectNode()); ...... ...... _featureList.add(pkg.getStructureModel_Transactions()); }
比较两个对象差异的本质是比较两个可比对象相同字段值的差异。本例中的 被比较对象都具有 ID 字段,只有 ID 相同的对象才具有可比性。
当确定两个对象具有可比性之后,需要遍历字段列表 _featureList,获取并 比较两个对象中相同字段的值。因为需要比较的字段来自不同的建模类,并且本 例中这些字段被放置在一个列表内,所以需要判断某字段是否可以用于一个对象 的方法。清单 6 列出了完成这一功能的代码片断,函数 isSuitable() 递归遍 历一个类型及其所有超类型,判断参数 feature 是否适用于此对象。
清单 6. 判断一个 EStructuralFeature 是否适用于一个对象
public static boolean isSuitable(EObject eo, EStructuralFeature feature) { if (eo == null) return false; return isSuitable(eo.getClass(), feature); } private static boolean isSuitable(Class c, EStructuralFeature feature) { Class[] interfaces = c.getInterfaces(); for (int m = 0; m < interfaces.length; m++) { if (interfaces[m] == feature.getContainerClass() || isSuitable(interfaces[m], feature)) { return true; } } return false; }
如果一个 EStructuralFeature 适用于两个对象,则用 eGet() 方法获取两 个对象在该字段的值并进行比较。通过 EMF 反射 API 取值和根据得到值的不同 类型进行比较的细节同第 3 章。ModelComparePort 类的列表类型变量 _comparedNodes 是用作记录已经比较过节点的备忘录,在进行比较之前需要查 阅备忘录避免重复比较,在比较之后需要更新备忘录。比较器发现两个对象的不 同便生成一个 CompareInfo 对象,最终得到一个由 CompareInfo 对象组成的差 异项列表,关键代码如清单 7 所示:
清单 7. 比较两个 EObject 对象
public void compareT(EObject remote, EObject local, int compareType, List compareLog) { if (remote == null && local != null) { compareLog.add(new CompareInfo (CompareInfo.REMOTE_DELETE, compareType, remote, local)); return; } else if (remote != null && local == null) { compareLog.add(new CompareInfo(CompareInfo.REMOTE_ADD, compareType, remote, local)); return; } if (((MObject) local).getEditStatus().equals( EditStatus.DELETED_LITERAL)) { compareLog.add(new CompareInfo (CompareInfo.REMOTE_MODIFIED, compareType, remote, local)); return; } if (!_comparedNodes.contains(remote)) _comparedNodes.add(remote); else return; // compare all for (int i = 0; i < _featureList.size(); i++) { EStructuralFeature feature = _featureList.get (i); if (isSuitable(remote, feature)) { if (remote.eGet(feature) instanceof Elist || local.eGet(feature) instanceof Elist) { // if the value is a list compare((EList) remote.eGet(feature), (EList) local .eGet(feature), compareType, compareLog); } else if (remote.eGet(feature) instanceof Eobject || local.eGet(feature) instanceof Eobject) { // if the value is an Eobject compareT((EObject) remote.eGet(feature), (EObject) local .eGet(feature), compareType, compareLog); } else { // if the value is a simple object Object remoteValue = remote.eGet (feature); Object localValue = local.eGet(feature); if (remoteValue != null && localValue != null) { if (!remoteValue.equals(localValue)) { // the two remote value and local value are // different, record them compareLog.add(new CompareInfo( CompareInfo.REMOTE_MODIFIED, compareType, remote, local)); } } else if ((remoteValue == null && localValue != null) || (remoteValue != null && localValue == null))// 一个为null { compareLog.add(new CompareInfo( CompareInfo.REMOTE_MODIFIED, compareType, remote, local)); } } } } }
比较结果界面
图 5. 以树视图展示比较结果
图 5 中的树视图展示了比较器的比较结果(一个 CompareInfo 对象列表) ,其中红色 X 表示该节点发生删除操作,绿色 + 号表示增加操作,表格和一支 笔的图案表示修改操作,右下角带有上、下箭头的小角标表示发起操作的是远端 或者本地。比较器完成的工作是满足模型比较并展示需求的关键步骤,但是除了 比较器,还至少需要建模模型对本地修改状态的记录,和一个友好的界面来显示 比较结果。
后记
本文中提供的 EMF 比较器解决方案是根据字段的值得到差异性列表,得到字 段值之后的比较过程没有特殊的处理需求。在某些比较场景中,也许需要对两个 值的比较不只是调用 equals 方法那么简单,比如:也许业务需求在比较过程中 会认为空字符串和 null 是相同的,但是在程序语句中这是两个完全不同的值。 为了满足对值的复杂比较,可以设计一个字段比较器,把不同字段的比较工作交 给该字段对应的比较器去执行。这样在应用中就可以更加灵活的设定比较规则。
快乐要懂得分享,才能加倍的快乐