透過 Reflection 建構通用型 SOAP 入口

透過 Reflection 建構通用型 SOAP 入口

 

今天收到一個任務是要建立一個可以讓外部系統溝通的 Web Service 接口,但由於要調用的對口Method 數量有太多,因此如果每個都去建立一個 Web Service 接口,那會有多少個要做,因此我們將所有的接口全部統一起來,只留一個 Method 來對所有的服務接口,如圖1. InvokeSer 示意圖中所定義的 InvokeSer。

圖1. InvokeSer 示意圖

在 InvokeSer 中有兩個參數分別如下:

  • Param 1:Service Identify
  • Param 2:Call method condition ( Json string / XML string )

Param1 主要定義的是 Service Identify,也就是要調用服務的定義識別,例如 LotCheckIn / LotCheckOut / InsertUser / CreateJob / ... ...等,這個定義識別只是一個識別,用來讓系統可以識別服務的唯一碼。

Param2 傳入的資料是真正要調用服務的時後,所要帶入的參數資料,一般會是 XML / JSON String / Text ...等,有時可能傳遞的 Method 會需要傳送一個以上的參數,這時候會強烈建議無論是一個或是多個參數,都要整合成一個參數值,無論是用 XML 去彙整又或者是用 JSON 去彙整都可以,要將多個參數資料轉換成一個參數值,轉換的方式如下圖2. Process Request Format 表格。

圖2. Process Request Format

我會將我要傳遞的資料存入到一個 DTO (Data Transform Object)物件中,並且將 DTO 物件轉換成 JSON 或是 XML 的字串,再將這個字串存入到 Condition 的格式中,如下範例:

{
 "Token":"GIaUgupqZumIQbVAANw7lbeNxBS2j3KTl9sUdS9NV8G0omjfUWM=",  
 "ConditionType":"S",
 "Condition":"{\"factor_id\" : \"bracket\", \"factor_name\" : \"支架\"}"
}

Condition : Json string 範例

<?xml version="1.0" encoding="UTF-8" ?>
<Param2>
 <Token>GIaUgupqZumIQbVAANw7lbeNxBS2j3KTl9sUdS9NV8G0omjfUWM=</Token>
 <ConditionType>S</ConditionType>
 <Condition>{"factor_id" : "bracket", "factor_name" : "支架"}</Condition>
</Param2>

Condition : XML string 範例

在上述的範例中,可以看到我要傳遞的參數有兩個資料,分別是 factor_id 與 factor_name 兩個值,但是我將兩個資料包含在了 Condition 的這個標籤中,並且將所有的資料都彙整成一個 XML 或是 JSON String,做為是 Param 2 的資料值,使用這樣的做法就可以將所有個參數都彙整成一個參數資料;附帶一提 Input 的參數資料可以這樣彙整,相對的如果我的 Out put 資料有一個以上,也可以用這樣的方式傳出。

好了,接下來就是我要如何透過 Param1 的定義,來調用不同的服務,一般人想到可能就是透由 switch 或是 if...else if ...else if 如此的串接下去,但如果是這樣的方式,不就每次要新增一個服務時就要重新再加入一次,另外如果有 100 個服務要做,那這個 switch 或是 if...else 就要寫不完,且對於維護上也不容易,因此可以透由一個小方法來完成我的這個定義,就是透由 Reflection 來建構,首先我們要先建立一張資料表,來取代這些 if...else 或是 switch 的定義,表結構如圖3,資料內容如圖4

圖3. 系統服務轉接配置表結構

圖4. 系統服務轉接配置資料範例

在圖4的範例中,裡面有一個「service_key」,這個欄位值就是用來儲存服務的識別值,也就是 Param1 的資料,從上述的圖4 的資料中可以看的出來,如果當 Param1 傳入的資料是 「plot.check.in.process」的資料時,基本上就可以對應到第三筆資料,從第三筆資料中就可以找到這個服務是要調用「FW.RMS_ManagerService.Service.lot_check_in_handler」這個類別的服務,也就是圖4 中「class_name」所儲存的資料,而真正要調用的 Method 是 method_name 所定義的 StartRun 的服務,如圖5. lot_check_in_handler 服務

圖5. lot_check_in_handler 服務

如果不想要這麼麻煩,其實還有另外一個方式就是透過客製化特徵 Customized Attributes 或是介面樣板 Interface 一樣可以達到這個作用 (針對客製化特徵 Customized Attributes 會有另外章節來專門說明這個工具的用法,這裡不詳述),但是主要我是為了要便於管理,因此選擇採用 Database 來集中化管理。

OK,完成了以上的配置,那我究竟要如何去透過資料庫動態的去調用呢?看到這裡我想有人心裡已經有答案,其實說穿了就是反向注入的模型來達到這樣的機制,這機制要怎麼做呢?首先我會先建立一個類別是專門用來處理這個服務委派,我這裡稱他為「InvokeServiceHandler」。

特別說明一下,針對類別,我的命名方式喜歡以模擬人物化的方式來命名,例如這個服務就是「服務委派作業處理者」,因此在這裡我就已經賦予他一個責任是專責用來處理服務委派的作業,其他非服務委派的功能我便不會將它放入到這個類別中,這樣的作法是為了強逼自己要將物件進行單一原則,一個物件只讓它處理一個作業概念(Concept)

在這個「InvokeServiceHandler」的類別中,我加入了一個專門用來處理反向注入的方法是 StartRun ,這個方法中會要求帶入兩個參數,也就是要傳入 Param1 / Param2 的參數,呼叫方式可以參考圖1. InvokeSer 示意圖

  • pKey:Service Identify
  • pCondition:Call method condition ( Json string / XML string )

這個方法第一步基本上就是要抓取 Database 中「service_key」與 pKey 相吻合的資料,獲取到該資訊後,便可依據該資訊建立起對應的物件,如圖6. 動態調用類別

圖6. 動態調用類別

以圖6所示,首先我們會先看到一個下方程序
var AssemblyTemp = Assembly.GetExecutingAssembly();
這一段主要是用來獲取 Assembly,所謂 Assembly 就是編譯出來的那顆 dll 或是 .exe 檔案,那一個檔案就是所謂的 Assembly,一般我們如果要取得 Assembly 獲取方式主要是會指定我要獲取的檔案路徑,在讓程序動態將 Assembly 檔案讀取出來進而獲取到我們要的 Assembly;但是我們這次所做的服務,目前會調用的都是存在同一個 Assembly 檔案中的處理者類別,因此就不需要如此麻煩的去動態載入 dll 檔案,而是可以透由 Assembly.GetExecutingAssembly() 這個方法來獲取到當前這個類別所屬的 Assembly 物件。

接下來有看到一段程序如下
AssemblyTemp.GetTypes().SingleOrDefault
這一段程序可以透由 Assembly.GetTypes() 的方法去抓取到這個 Assembly 中所有的型別,Class 類別基本上就是屬於一種型別,因此透由 Assembly.GetTypes() 的方法,可以獲取到所有該 Assembly 下的所有類別資訊,並且透由 LINQ 的方式抓取到 Namespace + Class 名稱與數據表中「class_name」相符合的類別 (關於「class_name」請參考圖3或圖4的資料),透過這方法便可以獲取到對應服務處理類別的型別物件,注意,此時獲取到的只是類別,並沒有 Instance,也就是說它還沒有被 new 起來,因此如果現在直接去使用它,就會獲得一個 NullReferenceException 的錯誤。

沒有 Instance 的類別基本上便無法使用,但是在這樣的一個環節中,我們無法直接使用強行別的建構方式來實體化這個類別,進而轉換成物件,因此就出現了下面這一段程序
Activator.CreateInstance(CurrentType, null);
這一段程序主要是動態產生出一個實體化之後的物件類別,因此你需要指定你要建構的類別型別,並且如果該類別在建構子的地方是必須要帶入參數時,也必須要將建構子所需要帶入的資料傳入進去,如此便可以動態的將這個類別實體化,變成一個有 Instance 的物件。

獲取到被實體化的物件之後,基本上這個物件就是一個已經有配置記憶體的物件,只是被 Boxing 成一個 object 型別的實體,也就是說,如果你知道這個物件是什麼,你可以透過強行別轉換的方式強制將這個記憶體在轉換過去,如下列所示,可以直接將這個實體物件轉換成已知的物件或是介面
(IMachineInfo)Activator.CreateInstance(CurrentType, null);
但是目前我們的類別配置是在數據庫中,因此無法有一個唯一性的類別或介面,因此我們可以不需要進行轉換,但問題來了,如果不轉換的時候,我要如何去調用我要執行的 Method 呢?

這時後,我們還要再度的利用到 Reflection 的機制,我們可以透過物件的 GetType() 的方法,將物件再萃取成類別的型別,透過型別物件可以提供出很多的方法與屬性其中就有一個 GetMethod() 的方法可以讓你指定你要獲取的方法名稱,如下列範例
ClassInstance.GetType().GetMethod(MappingInfo.method_name);
在上述的案例中,可以看到我透過 GetType() 的方式將 ClassInsteance 萃取出類別型別,再透過 GetMethod() 的方法,可以抓取出指定 Method 的型別,注意這裡也只是一個 Method 的型別,因此他不是一個具有實體物件的方法,所以他是無法被操作的。

如果我想要操作這個 Method 的話,就需要透過一些比較特別的方法去呼叫,這方法就是 Invoke,它是一個類似委派的呼叫,如下列範例所示
var ParamInfo = new object[] { pCondition };
return (string)MethodType.Invoke(ClassInstance, ParamInfo);
透過 Invoke 的方法,來執行我本身這個 Method,因此帶入的第一個參數需要具備有兩個條件

  • 必須要是有被實體化過的物件,也就是被配置過記憶體的物件
  • 這個實體化的物件必須要包含有我本身 (方法) 的方法,例如:MethodType 方法為「StartRun(string pCondition)」那第一個參數傳遞的實體物件就必須要包含有「StartRun(string pCondition)」這個 Method。

第二個參數是調用 Method 的時候要傳入的參數,以剛剛「StartRun(string pCondition)」的範例,它有一個參數是 pCondition,資料型別為 string,因此在透過 Invoke 呼叫的時候,就需要將這個參數的資料帶入,但是有可能你要調用的方法要傳入的參數不只一個時,你要怎麼做呢?
因此第二個參數所要帶入的資料是一個物件陣列 object[],你需要宣告一個物件陣列的物件,並依據要傳遞參數的順序依據存入到對應物件陣列 Index 的位置,如上述的範例 ParamInfo 所演示的範例一樣,最後再將 ParamInfo 資料作為第二個參數傳入到 Invoke 方法中。

透過 Invoke 調用 Method 時,如果該 Method 有回傳值時,也會在調用完 Invoke 後回傳回來,如此便可以獲取到你動態調用的方法回傳值。

接下來我們可以來做一個類別是可以讓 Invoke 調用的類別,在圖4中有一個 service_key 的資料為「material.on.machine.process」,它配置的類別是「FW.RMS_ManagerService.Service. material_on_machine_handler」,執行的方法為「StartRun」,於是我實作了這個類別與方法,很單純的回覆一個 「Hello this is test!!」的字串,並且要確定它是能夠接收到傳遞的參數,因此後面再加上傳遞進來的參數,如圖7. 實作material_on_machine_handler

圖7. 實作material_on_machine_handler

好了,萬事皆備,只欠東風,所以我們來踢這最後一腳,我們將它執行起來測試一下,Run 起來之後,就可以看到如圖8. InvokeSer 執行畫面,以及圖9. InvokeSer WSDL 畫面

圖8. InvokeSer 執行畫面

圖9. InvokeSer WSDL 畫面

好了我們嘗試執行 InvokeSer 的方法,點選 InvokeSer 會進入到執行畫面,如圖10. InvokeSer 作業畫面

圖10. InvokeSer 作業畫面

我在Param1中輸入了「material.on.machine.process」,然後再 Param2 中輸入了「哈喽,我是許小史」,沒有意外點選完叫用後,它會出現
Hello this is test!!
哈喽,我是許小史

如圖11. 叫用 InvokeSer 服務

圖11. 叫用 InvokeSer 服務

這樣就完成了我們通用型的 SOAP 服務,透過這個服務,我們日後就不需要再因為要加入新的 WSDL 方法而又要不斷的調整我們的 Controller,只需要到數據庫中,加入一筆對應要作業的資料便可完成了。