讓用戶通過宏和插件向您的 .NET 應用程序添加功能

 Jason Clark

本文假設您熟悉 .NET 與 C#

下載本文的代碼: Plug-Ins.exe (135KB)

概述

大多數用戶應用程序都受益於可由其他開發人員擴展的能力。 擴展一個用戶已經很熟悉並針對它進行過培訓的現有應用程序往往比從頭開發來得簡單和有效。因此,可擴展性會使您的應用程序更加吸引人。 您可以通過支持插件和宏等功能來使應用程序具有可擴展性。 使用 .NET Framework 可以輕鬆實現這一點,即使核心應用程序不是 .NET Framework 應用程序。 在本文中,作者將描述 .NET Framework 的可擴展功能(包括晚期綁定和反射)及它們的使用方式,同時還介紹插件安全注意事項。

想像一下完美的文本編輯器是什麼樣子的。它啓動時間不超過兩秒,支持針對流行的編程語言的上下文着色和自動縮進,支持多文檔界面 (MDI) 以及很酷並且大受歡迎的選項卡式文檔排列方式。構想這種完美的文本編輯器的問題在於完美只存在旁觀者的眼中。 這些功能只是我對完美的文本編輯器的定義,其他人肯定會有不同的標準。也許完美的文本編輯器所能擁有的最重要的功能就是支持豐富的可擴展性,這樣開發人員就可以使用他們需要的功能來擴展該應用程序。

可擴展的文本編輯器可能支持創建自定義工具欄、菜單、宏,甚至是自定義文檔類型。 它應該允許我編寫能掛接到編輯進程的插件,以便添加自動完成、拼寫檢查及其他諸如此類的美妙功能。 最後,完美的文本編輯器應該能讓我用任何語言編寫自己的插件(我個人的首選是 C#)。

誠然,我希望所用的每個應用程序都能按這種方式來擴展。 如果在某些地方編寫少量代碼就可以自定義自己喜歡的應用程序,那就再好不過。即使我做不到,我也知道其他人能夠做到,我再通過下載來從 Internet 利用他們的擴展。這就是我開展此項活動以讓所有開發人員都來編寫可擴展應用程序的初衷。

理想的可擴展應用程序

許多應用程序都可以使用可插入代碼來修改。 實際上,整個 Microsoft Office 應用程序 套件都可以如此廣泛地進行自定義,以致人們能夠使用 Office 作爲平臺來編寫完整的自定義應用程序。 然而,即便有這麼完備的可自定義能力,我還是爲 Microsoft Word(一個我幾乎天天使用的應用程序)編寫了我的第一個插件。

原因很簡單。 Microsoft Office 的所有功能並不能完全符合我的標準,包括:

簡單性。 我想以已經很熟悉的非常簡單的軟件工具來操作我的可插入應用程序。

權限。 我想讓我的插件有權訪問應用程序中內置的某些對象和功能子集。 這種權限應該是自然而然的,如同我選擇的編程語言的一部分。

編程語言。 有時我想使用特別選擇的編程語言。

能力。 除了訪問應用程序的文檔對象模型 (DOM) 外,我還要一個豐富的 API。

安全性。 我需要能夠下載其他人編寫並且可以通過 Internet 下載的插件。 我希望執行有潛在威脅或錯誤百出的組件而不必考慮系統的安全。

所列的簡短但近乎苛求。 實際上,在 Microsoft .NET Framework 發行之前,這些標準對普通應用程序而言太過嚴格,是無法做到的。 但現在,我可以向您展示如何使用 .NET Framework 來將所有這些可擴展性功能添加到您的託管和非託管應用程序中。

.NET Framework 可擴展性功能

可擴展性構建在晚期綁定之上,它是指在運行時而非編譯時(更典型的情況)發現和執行代碼的能力。 在這幾年中有許多技術創新對晚期綁定做出了重大貢獻,其中包括 DLL 和 COM API。 .NET Framework 將晚期綁定的簡單性提高到一個全新的層次。 爲加深理解,我們來看一個非常簡單的代碼示例。

圖 1 顯示了使用反射在託管對象中執行晚期綁定是如何的簡單。 如果您在 LateBinder.exe 內構建 圖 1 中的代碼並運行它,則可以將任何程序集(比如從圖 2 中的代碼構建的程序集)的名稱作爲命令行參數傳遞給給它。 LateBinder.exe 會反射程序集並在該程序集中創建從 Form 派生的類的實例,並使它們成爲它自己的 MDI 子類。 .NET Framework 中的反射使晚期綁定大大簡化。

反射是 .NET Framework 的基本工具之一,它促進了可擴展性應用程序的開發。 它是我這裏提到的可使應用程序可擴展的四種功能之一。

公共類型系統 使用 .NET Framework 一段時間之後,您可能就會開始認爲公共類型系統 (CTS) 理所當然了。不過,它的確是使該平臺中可擴展性變得如此簡單的原因之一。 CTS 定義了所有託管語言都必須遵循的部分面向對象特徵,例如對派生的規則、命名空間、對象類型、接口和基元類型。 CTS 的這些基本規定是針對公共語言運行庫 (CLR) 運行的代碼設置的。

反射 反射是在運行時發現信息(例如,程序集實現的類型或類型定義的方法等信息)的能力。 之所以反射成爲可能,是因爲所有託管代碼都是通過嵌入到程序集中的數據結構(稱爲元數據)自描述的。

Fusion .NET Framework 使用 Fusion 來將程序集加載到託管進程 (AppDomain) 中。 Fusion 有助於實現一些高級功能,如強命名和簡化 DLL 的搜索規則。

代碼訪問安全性 代碼訪問安全性 (CAS) 是 .NET Framework 的一個功能,可以簡化部分受信任的代碼的執行。 簡而言之,您可以使用 Microsoft .NET Framework 的功能來限制晚期綁定代碼可以訪問的內容,這樣就不用擔心插件破壞用戶的系統。

這就是.NET Framework 使可擴展性變成現實的四個功能。 然而,由於這些功能是如此的酷,所以一篇文章介紹一個功能不可能將可擴展性講得非常透徹。 因此,最好的做法是從一個任務出發引入這個話題。

可擴展性入門

不管您的應用程序有什麼用途,只要它是可擴展的,就必須執行三個基本任務: 發現、加載和激活擴展。發現是查找您的應用程序在運行時綁定的插件和其他代碼的過程。 加載是將代碼(打包爲程序集)放入進程或 AppDomain 以便激活並使用由程序集定義的類型的過程。 激活是創建晚期綁定對象的實例並調用它們的方法的過程。

這三個階段的每一個都包含着許多 .NET 技術和應用程序設計注意事項。 雖然技術上能夠做到,但 .NET Framework 並沒有定義一種特殊的方式來實現可擴展性,所以您可以有許多選擇。

加載: 在運行時綁定到代碼

從邏輯上講,可擴展性應用程序在加載代碼之前要先發現它。 但反射必須加載代碼才能發現與它有關的內容,所以實際上發現過程可能在加載代碼之後。 我們來看一下這是什麼意思。

反射可以用來實現晚期綁定。 大多數反射類都可以在 System.Reflection 命名空間中找到。 三種最重要的類是 System.AppDomain、System.Type 和 System.Reflection.Assembly。後面我將會介紹 System.Type。 爲了理解 AppDomain 和 Assembly 類,我們簡單看一下託管進程。

CLR 在 Win32 進程中運行託管代碼,粒度比在非託管應用程序中找到的更細。 例如,一個託管進程可以包含多個 AppDomain,您可以認爲後者是一種子進程或輕量級的應用程序容器。

程序集是 DLL 和 EXE 的託管版本,它們包含可重用的對象類型(如類庫類型)以及應用程序代碼。 另外,應用程序的任何擴展或插件也應該存在於程序集中(與 DLL 非常類似)。 程序集也要加載到託管進程內的一個或多個 AppDomain 中。

每個託管進程至少有一個默認 AppDomain,同時包含某些共享資源,例如託管堆 (heep)、託管線程池和執行引擎本身。除了這些邏輯組件之外,託管進程還可以創建任意數量的 AppDomain。請參見 圖 3,它顯示了一個包含兩個 AppDomain 的託管線程。在後面有關插件發現的話題中,AppDomain 顯得極爲重要。

fig03

圖 3 託管進程

現在我們回到 AppDomain 和 Assembly 類型。 您可以使用 Assembly.Load 靜態方法將程序集加載到當前的 AppDomain 中。 Assembly.Load 方法將引用返回給 Assembly 對象。這個方法在運行時將代碼綁定到您的應用程序,方法是加載駐留代碼的程序集。

Assembly.Load 通過名稱(不帶擴展名)從 AppBaseDir 目錄加載程序集(AppDomain 就是從這個目錄祕密加載已部署程序集的)。 默認情況下,Assembly.Load 從加載 EXE 的目錄中尋找常規可執行程序。 當加載早期綁定的程序集時,Assembly.Load 遵循的程序集綁定規則與 CLR 使用的一樣。

可以通過獲取 AppDomain 對象的一個引用並調用 AppDomain 的 AppendPrivatePath 和 ClearPrivatePath 實例方法來調整 AppDomain 的 AppBaseDir。 如果您使用 Assembly.Load 加載插件程序集,則可能需要操作 AppDomain 的 AppBaseDir。 這是因爲維護主應用程序目錄的子目錄下的插件非常有用。具體原因我很快就會解釋。

Assembly 類也實現了一個稱爲 LoadFrom 的方法,它與 Load 的不同之處在於,它帶有程序集文件的完整名稱(包括路徑和擴展名)。 LoadFrom 只是簡單地加載您指向的文件,而不是按照綁定和發現規則尋找程序集。 這在加載用作插件的程序集時十分有用。 不過要注意,與 Load 相比,LoadFrom 對性能產生了不利的影響,而且它缺少版本綁定智能,這使得除插件方案外的幾乎所有方案都行不通。

一旦獲取了 Assembly 對象的引用,您就可以發現包含在程序集中的類型,並創建這些類型的實例和調用方法。 接下來的兩節中我將介紹這些操作。

發現: 在運行時發現代碼

當編譯應用程序時,您必須顯式或隱式地告訴編譯器應用程序在運行時綁定和使用的代碼。 這些代碼出現的形式是可廣泛重用的類庫類型(例如 Object、FileStream 和 ArrayList)以及包裝在 helper 程序集中特定於應用程序的類型。編譯器存儲在程序集清單中實現這些類型的程序集的引用,而 CLR 在運行時引用該清單以加載必要的程序集。 這就是典型的託管代碼綁定過程。

正如前面所提到的,晚期綁定也使用程序集形式的代碼;然而,編譯器並不直接涉及綁定過程。 相反,代碼必須在運行時發現它需要加載哪些程序集。發現過程可以通過各種方法實現。 事實上,.NET Framework 並沒有指定一個發現方法,不過它確實爲您提供了實現自己的技術的重要工具。

兩種發現機制都值得一提。 第一種也是最簡單的一種(從軟件角度看)就是維護一個 XML 文件,它記錄應用程序在運行時應該綁定的代碼。 第二種是比較高級的基於反射的方法,它可以使應用程序的可用性更高。 我們首先來看 XML 方法。

XML 方法只需代碼解析 XML 文件並使用其中的信息即可加載程序集。 以下示例展示了一種可能的 XML 格式:

<PluginAssembly name="MyPlugin.dll">
   <Type name="SomeType"/>
   <Type name="AnotherType"/>
</PluginAssembly>
<PluginAssembly name="MorePlugins.dll">
</PluginAssembly>

這個 XML 包含使用反射實現加載過程所需要的最少信息;也就是想要激活的程序集的名稱和程序集中一種或多種類型的名稱。 代碼只需找出 XML 中程序集的名稱,然後使用如 圖 1 所示的代碼將程序集加載到 AppDomain 中。

XML 方法易於實現,但產生的應用程序可用性不高。 例如,我不是特別喜歡這樣的文本編輯器:必須編輯某種 .config 文件才能擴展應用程序。 然而,對於服務器端應用程序,這種方法可能比使用反射更合適。

基於反射的發現方法可以使用相對於應用程序目錄的已知位置來自動操作。例如,我爲本文編寫的示例應用程序在一個名爲“Plugins”的子目錄中搜索可能的可擴展性程序集(要下載完整的源代碼,請轉到本文頂部的鏈接)。這種特殊的方法比較有用,因爲用戶只需要將程序集文件複製到文件系統,應用程序在啓動時就會綁定新的代碼。

有兩個原因造成這種方法的實現比 XML 方法困難。 首先,必須制訂加載程序集應該遵循的標準。 其次,必須反射程序集以發現它是否符合標準,以及是否應該加載到 AppDomain 中。

有三個有用的標準可用於在作爲插件的程序集中爲代碼確定一種類型。 當將反射方法用於發現時,應該採用這些標準中的一個。

接口標準 代碼可以使用反射搜索整個程序集以發現實現已知接口類型的所有類型。

基類標準 代碼再次使用反射搜索整個程序集以發現從一個已知基類派生的所有類型。

自定義屬性標準 最後,可以使用自定義屬性來將一種類型標記爲應用程序的插件。

我馬上就會向您展示如何使用反射來發現晚期綁定程序集中的某種類型是否實現一個接口、是否從一個基類派生,或者是否屬性化。 但首先我們來看一下使用這些綁定標準的設計折衷方案。

接口和基類標準比自定義屬性方法(或基於反射的晚期綁定的其他任何方法)更加有用,這有兩個原因。首先,相對於自定義屬性而言,插件開發人員很可能對接口或基類比較熟悉。 更重要的是,可以使用接口和基類以早期綁定方式調用晚期綁定代碼。例如,當您將接口作爲綁定到一個對象類型的標準進行使用時,則可以通過該接口類型的引用來使用對象的實例。這樣就可以避免需要使用反射來調用方法和訪問實例的其他成員,從而提高可擴展性應用程序的性能和代碼能力。

要想在運行時發現一個類型的相關信息,可以使用反射類型 System.Type。從 Type 派生的類型的每個實例都引用系統中的一個類型。 Type 實例可以用於在運行時發現關於該類型的一些事實,比如它的基類或它實現的接口。

要想獲取由一個程序集實現的一組類型,只需調用 Assembly 對象的 GetTypes 實例方法即可。 GetTypes 方法返回一個 Type 派生的對象的引用數組,程序集中定義的每個類型對應一個引用。

要想確定一個類型是實現一個已知接口還是從某個基類派生,可以使用 Type 對象的 IsAssignableFrom 方法。 以下代碼片段展示瞭如何測試 someType 變量所表示的類型是否實現了 IPlugin 接口:

Boolean ImplementsPluginInterface(Type someType){
   return typeof(IPlugin).IsAssignableFrom(someType);
} 

這個原則也適用於測試基類型。

對於發現要在應用程序邏輯中插入哪些類型,加載程序集以反射該程序集中的類型是很有效的。 但如果沒有一種類型與您的標準相匹配,會怎麼樣呢? 簡單地說,就是無法卸載在 AppDomain 中已經存在的程序集。這就是基於反射的發現方法比 XML 方法稍難實現的第二個原因。

對於某些客戶端應用程序,將代碼反射的所有程序集(甚至不符合插件綁定標準的程序集)都加載到進程中也許還可以接受。 但對於可擴展的服務器應用程序,沒有足夠的空間來將隨後不能卸載的任何程序集都加載進來。這個問題的解決辦法分爲兩個階段。 首先,只要卸載加載程序集的整個 AppDomain,就可以卸載該程序集。其次,可以通過編程方式創建一個臨時的 AppDomain,其唯一目的是加載程序集來進行反射,以便發現是否需要將程序集中的類型作爲插件使用。一旦發現階段結束,就可以卸載這個臨時 AppDomain 及其所有程序集。

這種解決辦法聽起來好像很複雜,但其實要實現的只是一段非常簡單的代碼。 本文使用的 PluginManager 類採用了這種方法,它要實現的代碼不超過 100 行。如果您想在自己的可擴展性應用程序中採用這種方法,您會發現 PluginManager 的源代碼非常有用(請參見下載文件)。

激活: 創建實例和調用方法

一旦確定了要在應用程序中插入哪些類型,接下來就需要創建對象的實例。 這完全可以通過反射來實現;不過您會發現反射速度慢、不實用,而且難以維護。另外,您還應該選擇使用反射來創建插件對象的實例,將對象強制轉換爲一種已知接口或基類,然後在其整個剩餘生命週期內根據已知類型使用這些對象。請記住,接口和基類標準很適合採用這種方法發現插件類型。

由於有了 CTS,託管代碼纔是面向對象且類型安全的。晚期綁定代碼面臨的一個挑戰是在編譯時不一定知道對象的類型。 但由於有了 CTS,所以您可以將任何對象看作是 Object 基類,然後將其強制轉換爲應用程序和可插入代碼已知的某個基類或接口。 如果對象無法進行強制轉換,則會引發 InvalidCastException 異常,應用程序可以捕獲這個異常並進行相應的處理。

然而,在進行任何這樣的操作之前,必須創建要綁定到的對象的實例。 與早期綁定對象不同,您不能簡單地使用 new 關鍵字來創建實例,因爲編譯器需要與 new 一起使用的類型名稱,而對於晚期綁定類型,這顯然是不知道的。 解決辦法是採用靜態方法 Activator.CreateInstance。這個方法將創建對象的實例(假定引用對象的 Type 派生的實例)和一個可選的 Object 引用數組(用作構造函數參數)。 以下代碼使用 CreateInstance 創建一個對象並返回一個已知接口:

IPlugIn CreatePlugInObject(Type type){
   return (IPlugIn) Activator.CreateInstance(type);
}

一旦擁有一個對象並將其強制轉換爲一種已知類型,就可以通過引用變量調用該對象的方法,就像調用其他任何對象的方法一樣。 此時,晚期綁定代碼中的對象就與應用程序的其他部分無縫集成在一起了。

保護可擴展性

如果您的應用程序將廣泛分發,而且任何人都可以編寫插件,則必須考慮安全性。 幸運的是,.NET Framework 使保護代碼變得非常容易。 讓我們看一下這是如何做到的。

再次想像一下可擴展文本編輯器。 在理想的情況下,用戶應該能夠從 Internet 下載插件並安全地將其插入到應用程序中,即使該插件是由非受信任的第三方設計的。而且如果它不是受信任的,則應該在部分受信任的狀態下執行,在這種狀態下,它無權訪問像文件系統或註冊表這樣的系統資源。

.NET Framework 可以通過 CAS 執行部分受信任的代碼。 因爲託管代碼是實時編譯的,所以 CLR 有能力斷言部分受信任的託管代碼不執行它缺少權限的操作。 如果部分受信任的代碼試圖執行一個不被允許的操作,CLR 就會引發 SecurityException 異常,應用程序可以捕獲該異常。

在某些情況下,代碼默認爲部分受信任,比如分佈在 Internet 中或嵌入 HTML 文檔內的控件。 然而,您也可以利用 CAS,這樣用戶就可以安全地使用來自第三方的擴展程序集。在所有情況下,CLR 將一個程序集視爲一個安全單元,這意味着應用程序可以包含多個程序集,授予其中每個程序集的安全權限都可能有所不同。這對插件來說非常適合。

另外,.NET Framework 提供了許多功能,並且有許多方法,您可以使用這些方法來創建部分受信任的插件。 以下步驟採用最簡單也是最安全的方法。首先,爲應用程序的插件創建兩個子目錄。 一個存放完全受信任的插件,另一個存放部分受信任的插件。然後通過編程方式調整本地安全策略,將代碼組與部分受信任插件對應的子目錄相關聯。 最後,授予代碼組中的代碼 Internet 權限,也就是說使它擁有一個權限子集,將即使可能有惡意的代碼也視爲是安全的。

一旦生成了這種代碼組,CLR 就會自動將降低的權限與從部分受信任子目錄加載的程序集相關聯。 除了要調整本地安全策略(我馬上就會向您介紹如何來做)外,插件的安全性會自動工作。

調整安全策略

.NET Framework 安全策略引擎非常靈活,可以調整。 在實際情況中,可以使用隨 Framework 安裝的 .NET Framework Configuration 控制面板 applet 手動調整安全策略(參見 圖 4)。 此外,也可以通過編程方式調整策略。要編寫修改安全策略的代碼,必須從 SecurityManager 類型開始。 這個有用的類型可以幫助您訪問 .NET Framework 安裝的三種策略級別: Enterprise、Machine 和 User。 我建議您將插件的自定義代碼組添加到 Machine 策略級別中。要發現 machine PolicyLevel 對象,可以使用如 圖 5 所示的代碼。

fig04

圖 4 安全配置

代碼組是以邏輯層次結構排列的。 一旦獲取了 PolicyLevel 對象,就可以從 PolicyLevel.RootCodeGroup 屬性返回的代碼組開始遍歷代碼組的層次結構。 默認情況下根代碼組的名稱爲 ALL_CODE,它代表所有託管代碼。 您應該創建自定義的代碼組,作爲 ALL_CODE 代碼組的子組。

圖 6 中的代碼爲通過特定的 URL 加載的任何代碼創建一個自定義代碼組。 該代碼組具有 Internet 權限,並設置了 PolicyStatementAttribute.Exclusive 和 PolicyStatementAttribute.LevelFinal 位來指示與這個代碼組匹配的代碼只具有這些權限。 URL 可以是 HTTP、HTTPS 或 FILE URL。 要將這個新代碼組與文件系統中的目錄相關聯,可以使用具有如下結構的文件 URL: file://d:/programs/extensible-app/partially-trusted。

.NET Framework 的 CAS 功能非常靈活,但可能需要一段時間才能習慣使用。通過閱讀本文以及我在下載文件中提供的示例應用程序代碼,您應該能夠獲取創建插件體系結構所需的信息。 不過,我強烈建議您使用 .NET Framework Configuration 控制面板 applet 來嘗試對系統策略進行更改,只有這樣才能熟悉概念。如果更改太多,可以隨時恢復策略默認值。

可擴展應用程序設計

本文附帶的 ExtensibleApp.exe 示例應用程序是一個支持自定義插件的例子。 其實,該應用程序只不過是一個 shell,它僅僅顯示一個 MDI 窗口並允許用戶安裝插件。如果您正在使用該示例中的代碼作爲編寫自己的可擴展應用程序的學習工具,則應該特別注意 PluginManager.cs 中的代碼。該模塊包含 PluginManager 可重用類,它爲示例應用程序處理所有非特定於應用程序的插件邏輯。

fig07

圖 7 插件安裝前

如果您構建並運行 ExtensibleApp.exe 示例,則會發現它允許您選擇 DLL 作爲插件安裝到應用程序中。該示例包含兩個插件對象:PluginProject1.dll 和 PluginProject2.dll。 它們利用應用程序本身公開的 API 來創建工具欄和菜單,並在應用程序中添加一個文檔類型。 圖 7 顯示插入自定義代碼之前的應用程序,圖 8 顯示插入之後的應用程序。

fig08

圖 8 運行插件

該應用程序用到了本文前面討論的技術和技巧。 另外,它還展示了一些設計方法,這些方法在使應用程序可擴展時應該加以考慮。 讓我們看一下其中一些注意事項。

版本控制

版本控制是應用程序生命週期的一個重要方面。 它也對可擴展應用程序有重要的影響。可擴展應用程序需要定義用來發現和使用插件的接口、基類型或屬性類型。 如果您將這些類型包括在常規應用程序 EXE 或 DLL 程序集中,則它們的版本會由應用程序的其他部分控制,對於不按相同的時間表進行版本控制的插件程序集來說,這可能會產生綁定衝突。但有一個辦法可以解決這個問題。

解決辦法就是,應該將用來發現或綁定到插件的任何類型都定義在它自己的程序集中,在該程序集中只有其他由插件產生的類型存在。 還應該避免將任何代碼(至少不應該太多)都放在該程序集中,因爲需要儘可能少地對該程序集進行版本控制。同時,也可以根據需要對應用程序的其他部分進行更改和版本控制。 應用程序和插件將共享很少進行版本控制的粘連程序集。

這就帶來了一個與插件所使用的基類型有關的問題。 與接口不同,基類一般包含相同的代碼。 這是一個棘手的問題,它也是我們更喜歡選擇通過接口調用晚期綁定對象的主要原因之一。

然而,您應該知道,如果需要對基類和接口程序集中的代碼進行版本控制,.NET Framework 提供的靈活性可以滿足您將綁定從舊版本程序集重定向到新版本的需要。 但這需要對綁定策略作出更改,所作的更改必須輸入到應用程序的 app.config 文件或整個系統的全局程序集緩存 (GAC) 中。最好避免這種可能性,因此要管理好插件接口和基類以便儘可能少地對它們進行版本控制(如果曾經這樣做過)。

健壯性

請記住,在編寫可擴展應用程序時,雖然您的代碼能夠得到嚴格的質量控制,但插件代碼卻很可能沒有。因此,通過接口或基類引用調用晚期綁定對象的任何代碼都應該會遇到這些無法預料的問題。即使開發人員本意不想造成破壞,但部分受信任插件還是可能引發安全異常。同樣,部分受信任插件和完全受信任插件都可能有錯誤存在,因爲插件作者對您的應用程序內部的瞭解和您是不一樣的。

如果您設計的可擴展應用程序只支持由您自己或您的團隊編寫的插件,則可以不考慮這個問題。 但是許多可擴展應用程序都希望當插件對象出現異常情況時能夠儘可能地正常恢復。

託管代碼的一個強大之處在於,當一個對象確實運行失敗時,它會以一種定義良好的方式失敗 — 也就是說該對象會引發一個異常。 所以如果您一貫堅持註冊未處理的異常處理程序,並處理調用插件代碼應該會產生的異常,則即使插件失敗也可以給用戶提供一致的體驗。

如果您使用接口調用插件對象,則可以使用一種高級技術,也就是將所有晚期綁定對象包裝在一個實現了相同接口的代理對象中。這個常規目的的代理對象會將所有調用傳遞到基礎插件,但也會將與日誌記錄失敗一致的異常處理程序與調用一起包裝,同時警告用戶插件出現異常及其他這樣的情況。 對最終的插件健壯性而言,這是一個很好的主意,但對於許多應用程序來說,可能並不需要如此程度的穩定性。

安全注意事項

只要您制訂策略來限制所加載的程序集的權限,.NET Framework 就可以維護部分受信任的插件的安全性。然而,這雖然對保護系統起了很大作用,但是不能自動保護應用程序的狀態。部分受信任的對象與應用程序中的對象共享一個託管堆,並且需要考慮如何限制它們訪問您的內部應用程序對象。 以下是一些要點。

如果沒有必要,就不要將應用程序中的對象類型指定爲公共對象。 如果類型爲內部類型(默認情況),則可以限制應用程序中的部分受信任代碼對其進行訪問。然而,很容易在不經意間就將公共類型引入應用程序。 例如,當您將從 Form 派生的類型添加到項目中時,Visual Studio 嚮導會生成公共類。 這對大多數應用程序來說都是不必要的,所以應該將這些類型中的 public 關鍵字刪除,當您覺得有必要時再添加上去。

同樣,如果沒有必要,不應該使公共類型的成員爲公共或受保護成員。即使對於不可擴展的應用程序,在您覺得有必要提高它們的可訪問性之前,成員也應該是私有的。 因而,如果內部可訪問性能夠滿足要求,就不要提升。只有當您打算在程序集外公開成員時才使用受保護成員和公共成員。

您應該關注類庫類型是否有不好的或不完整的安全策略。例如,System.Windows.Forms.MainMenu 類公開一個稱爲 GetForm 的方法,它返回菜單所在的窗體。這通常是應用程序的主窗體。 即使您不打算將對應用程序主窗體的引用傳遞給部分受信任的插件,您也可能無意中讓插件直接訪問應用程序中從 Menu 派生的對象,從而允許插件訪問應用程序主窗體。 CLR 類庫開發人員考慮了類似的安全問題。 例如,Form.Parent 在返回對父窗體的引用之前要求它的調用者具有一個安全權限。 例如,以 Internet 權限運行的部分受信任代碼在默認情況下無法訪問該屬性。

正如您可以看到的,部分受信任的插件可能無法訪問一般文件系統或註冊表,但您仍然要提防有惡意的插件執行某些事情,比如關閉您的應用程序。 對於客戶端應用程序,此類問題通常無關緊要。 但對於服務器應用程序卻是很重大的問題。

在最後一節中,我將簡要討論可擴展應用程序的一些高級可能性。 這些主題範圍太廣,很難詳細介紹,大多數應用程序也可能不需要用到,但瞭解這些可能性是有幫助的。

可卸載的插件程序集

在本文前面介紹插件的章節中,您可能還記得有關卸載不想要的程序集的問題。 解決這個問題的辦法是在臨時的 AppDomain 中測試程序集是否有用,以便在需要時將它們卸載。 然後,如果發現有程序集您確實需要用到,可以將它們加載到您的主 AppDomain 中。但是,如果不想讓所有插件無限期地存在,該怎麼辦呢?

對於客戶端應用程序,以下做法是可以接受的:加載插件程序集,使用它們的類型直到它們不再需要使用,然後在應用程序剩餘的生命週期內不用再去管它。 然而,在服務器端的要求要嚴格得多。服務器端應用程序必須無限期運行,而且不能耗盡諸如進程地址空間等重要資源,所以需要您加載和卸載包含插件類型的程序集。

爲做到這一點,您需要在一個臨時應用程序域中尋找程序集,然後專門到另一個 AppDomain 中使用該插件類型。 這給應用程序的設計增加了一定的複雜性,但也很好地將插件與核心應用程序邏輯分開。

It is possible to instantiate and use objects entirely from within a separate AppDomain, and the feature of the .NET Framework that implements this is called Remoting.完全在一個獨立的 AppDomain 中實例化和使用對象是可以做到的,而在 .NET Framework 中實現它的功能稱爲遠程處理 (Remoting)。遠程處理用於跨進程和跨網絡訪問對象,但也可以用來訪問不同 AppDomain 中的對象(即使它們位於相同的 Windows 進程)。我無法在這裏完整地講述遠程處理,但您可以在 以前出版的 MSDN Magazine 中找到詳細的介紹,也可以在我的示例 PluginManager 類型中找到一些簡單的遠程處理代碼。

非託管應用程序中的託管插件

有了這麼多強大的可擴展功能可供您使用,但如果您的應用程序採用非託管語言(如 Visual Basic 6.0 或 C++)編寫的舊式應用程序,可能會覺得非常失望。 而現在可以不必失望了。 CLR 本身是一個 COM 對象,它可以宿主在由可以編寫 COM 客戶端的語言編寫的任何 Win32 進程中。

這意味着您可以編寫插件託管代碼(用 C# 或 Visual Basic .NET),然後通過非託管代碼加載運行庫和粘連代碼,再由該代碼以您喜歡的方式加載可與您的非託管應用程序進行交互的插件。 同時,COM Interop 允許您在託管和非託管代碼之間來回無縫地傳遞 COM 接口。

實際上,有三種方式可以宿主非託管程序集中的託管代碼和與其進行交互。 可以使用 COM Interop。 CLR 允許您採用 C#、Visual Basic .NET 和其他託管語言創建 COM 服務器。 您可以在任何非託管應用程序中綁定和使用這些託管對象。 也可以使用帶託管擴展的 C++ (MC++),它能夠將託管和非託管代碼自然地混合在單個進程中。 如果您的應用程序是用 C++ 編寫的,您會發現託管 C++ 包含豐富的可擴展性功能。 另外,還可以直接宿主 CLR。 CLR 本身是一個可以宿主的 COM 對象。 .NET Framework SDK 帶有一個名爲 MSCorEE.h 的 C++ 頭文件,它包含將運行庫作爲 COM 對象使用所需的定義。

再次強調,一旦託管代碼綁定到非託管應用程序中,託管代碼就可以使用本文所介紹的技術來實現應用程序的可擴展性。

小結

.NET Framework 爲代碼反射、後期綁定和代碼安全性提供了一些非常靈活的功能。. 這些功能可以以各種方式混合和搭配使用來實現可擴展應用程序。如果一個可靠的應用程序也是可擴展的,則可能有更長的壽命,也可能獲得進一步的發展,這可以促使更多插件的創建並且更廣泛地爲人們所接受。所以請認真研究這些可擴展性功能。 我想結果您會喜歡的。

相關文章請參閱:
.NET Framework: Building, Packaging, Deploying, and Administering Applications and Types
.NET Framework: Building, Packaging, Deploying, and Administering Applications and Types—Part 2
背景資料請參閱:
.NET Framework Security by Brian A. LaMacchia, Sebastian Lange, Matthew Lyons, Rudi Martin, and Kevin T. Price (Addison-Wesley, 2002)

Jason Clark provides training and consulting for Microsoft and Wintellect (http://www.wintellect.com) and is a former developer on the Windows NT and Windows 2000 Server team.Jason Clark 爲 Microsoft 和 Wintellect (http://www.wintellect.com) 提供培訓和諮詢,他以前是 Windows NT 和 Windows 2000 Server 團隊的開發人員。 他與人合著了 Programming Server-side Applications for Microsoft Windows 2000 (Microsoft Press, 2000)。 您可以通過 [email protected]與 Jason 聯繫。

轉到原英文頁面

發佈了25 篇原創文章 · 獲贊 0 · 訪問量 3萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章