基于反射机制的EMF模型比较

简介:本文基于 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 是相同的,但是在程序语句中这是两个完全不同的值。 为了满足对值的复杂比较,可以设计一个字段比较器,把不同字段的比较工作交 给该字段对应的比较器去执行。这样在应用中就可以更加灵活的设定比较规则。

快乐要懂得分享,才能加倍的快乐

基于反射机制的EMF模型比较

相关文章:

你感兴趣的文章:

标签云: