程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> 基於反射機制的EMF模型比較

基於反射機制的EMF模型比較

編輯:關於JAVA

簡介:本文基於 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<CompareInfo>  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<CompareInfo> 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 是相同的,但是在程序語句中這是兩個完全不同的值。 為了滿足對值的復雜比較,可以設計一個字段比較器,把不同字段的比較工作交 給該字段對應的比較器去執行。這樣在應用中就可以更加靈活的設定比較規則。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved