借助 Microsoft .NET Framework,編程人員便可輕松獲取由不同開發人員和公司構建的組件,並將這 些組件集成到自己的應用程序中。但僅當已知哪些組件是構建基礎時才能輕松實現上述過程。如果在構建 時對所需組件一無所知(對於加載項,通常會遇到這種情況),那麼事情就會變得更加困難。開發人員在 擴展其應用程序時經常會遇到問題。例如,應將加載項存儲在數據庫中還是磁盤上?開發人員應考慮已知 接口的加載項以獲得激活類型嗎?使用 AppDomain、AppDomainManager 和 AppDomainSetup 的最佳方法 是什麼?
我們將在 CLR 全面透徹解析的兩部分內容中解決上述問題及其他問題,方法是在基類庫 (BCL) 中引 入新的 System.AddIn 命名空間,我們將在 Visual Studio®(代號為“Orcas”)的下一 版本中推出該基類庫。您可在 Visual Studio“Orcas”的完全版本發布之前通過社區技術預 覽下載此基類庫。也可在 公共語言運行庫 (CLR) 加載項小組博客中找到這方面的信息。
在本系 列專欄中,我們將向您介紹有關將可擴展性添加到應用程序並隨時間推移逐漸將其版本化的開發生命周期 。本月專欄將涵蓋開發人員首次將可擴展性添加到其應用程序時所面臨的種種挑戰,以及 API 如何簡化 他們的工作等內容。下個月,我們將集中探討當宿主更換時仍保持加載項正常運行方面的長期問題:我們 將使用在本專欄中創建的示例應用程序,針對其下一版本對該應用程序做較大改動,然後向您介紹保持原 始加載項正常運行所需的各個步驟。
那麼現在就讓我們開始探討可擴展性方面的挑戰以及 API 的 優點吧。通過添加 System.AddIn 命名空間和提供有關構建托管加載項模型的指導,CLR 提供了一種解決 方案,不僅可解決編寫第一版應用程序時遇到的常見問題,還可為相關人員提供指導以確保應用程序將來 的兼容性。為更好地理解我們將要使用的術語,請參閱“可擴展性術語”提要欄。
可 擴展性術語
宿主 支持可擴展性的應用程序。宿主可以是客戶端,也可以是服務器(例如,Excel 、Word、Paint.NET、SQL Server 和 Exchange Server)。
可擴展性 操縱宿主應用程序對象或擴 展宿主功能的機制,有時將其稱為自動控制。通常,可通過作為宿主 SDK 一部分發布的對象模型實現上 述機制。
軟件開發工具包 (SDK) 一組公共接口 (API) 和相關文檔。允許宿主應用程序向加載項 提供服務,反之亦然。該 SDK 可由多種環境中的各方提供。例如,宿主可提供 SDK 以啟用特定於宿主的 加載項,而加載項可提供 SDK 以讓各種不同的宿主使用該加載項(例如,Windows Media® Player) ,此外第三方也可提供 SDK 以讓各種不同的宿主和加載項進行通信。
加載項(也稱為附件、擴展 件、插件或管理單元) 自定義項;由宿主加載的程序集。也是可自動執行宿主應用程序的某種事物(如 客戶端)。可向宿主(服務端)提供附加功能。加載項是自定義代碼,通常由第三方編寫,可通過宿主應 用程序並根據某個上下文環境(如宿主啟動或文檔加載)對其進行動態加載和將其激活。加載項通過宿主 公共 API(例如,對象模型或托管類庫)擴展宿主應用程序,加載項可通過 SDK 使用該 API。
管 道 使宿主和加載項能夠跨版本並通過安全協議進行通信的機制。
構建可擴展的宿主
在宿 主定義了其要公開的對象模型後,宿主與其加載項之間的交互會經歷下列三個主要階段:發現、激活和生 存期管理。發現階段包括查找可用加載項以及為宿主或用戶提供足夠的信息以選擇所要使用的加載項。激 活階段包括在適當的隔離安全沙箱中加載和啟動所選加載項。生存期管理是指在應用程序具有對某加載項 的引用期間,使該加載項及其與宿主交換的對象始終處於活動狀態,同時賦予宿主在必要時即刻關閉此加 載項的能力。除非另行指定,否則此處所有代碼均為 SampleExtensibleCalculator.csproj 中 Program.cs 文件的一部分,用戶可從 MSDN® 雜志的網站上下載這些代碼。
在更詳盡地探討有關在運行時使用加載項的內容之前,讓我們先來討論一下有關 SDK 以及定義宿主向 其加載項提供的對象模型方面的問題。我們的加載項模型可支持多種情況,但基本上可將這些情況歸於下 列三個類別之中。
在第一個類別中,加載項向宿主應用程序提供服務。例如,郵件服務器可成為 加載項的宿主,以實現病毒掃描、垃圾郵件過濾或 IP 保護;文字處理程序可能需要使用拼寫檢查加載項 ;而 Web 浏覽器則可能成為用於處理特定文件類型的加載項的宿主。計算器示例說明了這種情況。如果 查看一下在 SampleCalculatorAddIn.cs 中找到的加載項代碼,您便會發現該加載項向宿主提供了執行加 、減、乘、除運算的服務。
在第二個類別中,宿主向加載項提供了其自己的行為並讓加載項定義 其自動操作宿主的方式。在這種情況中,宿主向加載項提供了真正的服務。大多數 Microsoft® Office 加載項都屬於這一類別:在初始化加載項的基礎上,Office 應用程序還將其根對象傳遞給加載項 ,從而允許加載項實現對宿主的自動操作。
使用該模型,宿主便可讓第三方以史無前例的方式擴 展應用程序。我們見過用實時股票報價自動替換文本符號的 Microsoft Excel® 加載項,也見過將虛 擬鏈接添加到文檔中電話號碼的 Microsoft Word 加載項,添加該加載項後即可啟動 IP 語音 (VoIP) 程 序並調用電話號碼。有時,您還會看到將整個應用程序作為一個較大加載項構建到現有應用程序中的加載 項。此外,還有幾個 CRM(客戶關系管理)應用程序,其整個客戶端前端都包含在 Microsoft Outlook® 內部。
最後一個類別包括那些將宿主主要用於屏幕資源而非任何特定於宿主的功能 的加載項。搜索工具欄就是其中之一:同一工具欄可將其自身添加到浏覽器、窗口任務欄以及電子郵件客 戶端中,同時還可提供相同的功能,與其宿主無關。
在示例應用程序中,宿主向加載項提供了 SDK 以便將這些加載項作為服務使用;在這種情況下,宿主將使用加載項實現簡單的計算器功能。在我們 的模型中,實際上存三種有關對象模型的定義,這些定義通常非常類似。我們將這些組件定義為所謂的 “管道”:宿主視圖、加載項視圖以及跨越隔離邊界的合約。以後我們會詳盡探討管道方面的 內容,但現在讓我們來看一下宿主視圖。
下列代碼是宿主視圖的一部分,而不是核心宿主代碼, 因而用戶可在 hostCalculatorView.csproj 的 CalculatorView.cs 中找到這些代碼:
public abstract class Calculator { public abstract double Add(double a, double b); public abstract double Subtract(double a, double b); public abstract double Multiply(double a, double b); public abstract double Divide(double a, double b); }
如您所見,宿主視圖只是一個抽象基類,它定義了宿主想要從其加載項中獲得的功能。宿主將 直接編寫該視圖的程序,完全不理會自身與加載項之間的隔離邊界,也不理會加載項的視圖效果可能與宿 主的視圖效果截然不同這一因素。
發現
在應用程序中使用加載項的第一步是查找加載項: 應用程序需要通過某種方式找到可用的加載項,並獲取有關這些可用加載項的足夠信息以確定要加載和激 活的加載項。通常,宿主會根據各種應用程序來選擇執行該步驟的時間:在啟動應用程序時、在加載文檔 時,或在用戶請求時執行此步驟。
一些高層次發現能力還要求具有在指定位置中查找特定類型的 加載項的功能,從而向宿主或用戶提供足夠的信息,以便其確定要使用的加載項而不必事先激活這些加載 項;此外,該功能還可實現智能緩存,這樣便不必重新計算可用加載項列表,並可在安裝加載項時便即時 更新緩存。要實現上述目標,請按下列步驟進行操作。首先,定義要查找加載項的位置。在本例中,我們 希望將加載項和組件安裝到當前目錄中,但應用程序往往會選擇多個不同位置(例如,用戶的應用程序數 據目錄)。
String addInRoot = Environment.CurrentDirectory;
接下來,更新加載 項存儲區,以便使宿主能夠識別最新安裝的加載項和管道組件。AddInStore 類用於確定可互連的加載項 。將這些信息組合起來便構成了我們所謂的管道。
存儲區包含構成宿主及其加載項之間的管道的所有組件。可在運行時使用 AddInStore.Update API 更 新 AddInStore,也可在加載項安裝期間使用命令行工具對其進行更新,以提高發現啟動性能。下列代碼 用於檢查是否安裝了新的加載項。如果自上次調用該代碼後尚未安裝新的加載項或管道組件,則將快速返 回此代碼:
AddInStore.Update(addInRoot);
更新緩存後,宿主希望找到所有可用加 載項,這些加載項均可被連接到宿主所構建的視圖(抽象基類)中。該代碼在根目錄中查找計算機加載項 並存儲查找結果:
Collection<AddInToken> tokens = AddInStore.FindAddIns(typeof(Calculator), addInRoot);
AddInStore.FindAddIns 方法 可接受正在查找的加載項類型及其存儲路徑,並返回 AddInToken 的集合。請注意,此時並未加載任何加 載項程序集,也未執行任何加載項代碼。每個令牌都描述了唯一的加載項及該加載項所提供的四個屬性( 姓名、版本號、發布者和描述),從而允許應用程序/用戶確定要加載的加載項。 在計算器方案中,我們 創建了 ChooseCalculator 方法以將令牌顯示給用戶,從而允許他們選擇要使用的令牌:
AddInToken calcToken = ChooseCalculator(tokens);
激活、隔離、安全性和沙箱處 理
應用程序確定了要使用的加載項後,即需要激活這些加載項。從宿主的角度來看,宿主需要使 用實例來執行請求的抽象基類(即視圖)。但事實上,宿主需要的並不僅限於此:它真正需要的是將該加 載項與宿主和其他加載項相隔離,還需要通過特定權限集對此加載項進行沙箱處理。
隔離的加載 項程序集無法直接訪問其他程序集中的代碼或資源。.NET Framework 將 AppDomain 用作進程內的隔離單 元。對於那些不熟悉 AppDomain 的用戶,請將其視作小型進程。通常,宿主會在一個 AppDomain(一般 是啟動運行時所創建的默認 AppDomain)中運行其代碼,並希望各加載項都具有其自身的 AppDomain。通 過將加載項與宿主和其他加載項相隔離,用戶可獲得下列幾個重要優勢:
各加載項都位於其自身 的文件夾中,具有其自身的應用程序庫,並且加載其自身的程序集,而不會與宿主或其他加載項相沖突。 此外,這還使加載項能夠具有其自身的配置文件,該配置文件可用於特定於加載項的功能。
宿主 可捕獲一個加載項中的故障並關閉該加載項(可能需要重新啟動),而不會對宿主或宿主進程中其他加載 項產生任何影響。
加載項及其依賴項均可被卸載(有關詳細信息,請參閱本專欄中“生存期 管理”部分的內容)。
可對加載項進行沙箱處理,以使其成為特定的安全權限集(如上所述 )。
但請注意,當開發宿主和加載項本身時,由於您是直接根據構成宿主視圖和加載項視圖的抽 象基類進行編程的,因而不必擔心隔離邊界的約束。在此版本中,我們支持 AppDomain 級隔離,並可能 在將來版本中添加進程級隔離。有關加載項和 AppDomain 的詳細信息,請參閱 AppDomain's and Addin's(關於 AppDomain 和加載項)。
宿主需要考慮加載和激活加載項時的安全環境。宿 主應用程序可能在完全信任的環境中運行加載項,但也可能希望在權限受到一定限制的環境中運行加載項 。預先存在的安全性和 AppDomain 創建 API 的功能非常強大,利用它們可像對 AppDomain 進行沙箱處 理一樣調整各種不同的設置。在大多數情況下,宿主會希望為加載項提供 FullTrust、Intranet 或 Internet 安全設置,並根據框架來確定這些設置的含義。API 使用戶能夠輕松選擇上述三種安全設置的 一種,此外我們還向用戶提供了在必要時調整設置參數的功能。將以上功能匯集到一行代碼中,便得到了 下列代碼,這些代碼將在新的 AppDomain 中激活所選 AddInToken 並對 Internet 區域中的程序集進行 沙箱處理:
Calculator calculator = calcToken.Activate<Calculator>(AddInSecurityLevel.Internet);
這行代碼將向 宿主返回所選加載項的激活實例。那麼為實現該操作,後台究竟發生了些什麼呢?
AddIn 框架創 建了 AppDomain 並在該 AppDomain 中加載了適當的程序集。接下來,我們為在該 AppDomain 中運行的 代碼賦予對應於指定安全級別的安全權限(本例中的安全權限對應於 Internet 權限集)。然後,我們將 ApplicationBase 設置為實際加載項程序集的位置,從而使該加載項能夠找到其依賴項和資源。接著,我 們將 AppDomain 的配置文件設置為緊鄰加載項 DLL 的文件。最後,我們掛接所需管道組件,以便將宿主 連接到加載項。
這裡存在一個允許您為加載項指定自定義權限集的重載,還存在另一個允許您提供自己的 AppDomain 的重載。利用這兩個重載,您便可調整加載項的 AppDomain 和安全設置,甚至還可將具有相同生存期、 相同安全環境的加載項歸為一組,和/或獲得重新使用 AppDomain 所帶來的性能優勢。
圖 1 是采用了上文所述技術的端對端宿主。
Figure 1 從發現階段到激活階段的宿主加載項
static void Main() { // 在本示例中,我們預期將加載項 // 和組件安裝在當前目錄中 String addInRoot = Environment.CurrentDirectory; // 查看是否已經安裝了新的加載項 AddInStore.Rebuild(addInRoot); // 在根目錄中查找計算器加載項 // 並儲存結果 Collection<AddInToken> tokens = AddInStore.FindAddIns(typeof(Calculator), addInRoot); // 向用戶咨詢他們想要使用的加載項 AddInToken calcToken = ChooseCalculator(tokens); // 在已經於 Internet 區域中進行了沙箱處理的新 AppDomain 集中 // 激活選定 AddInToken Calculator calculator = calcToken.Activate<Calculator>(AddInSecurityLevel.Internet); // 運行讀取、計算、打印循環 RunCalculator(calculator); }
生存期管理
任何加載項模型的基本要素是用於管理其內部對象生存期的方法。COM 生存期管理完全以引用計數為 基礎,而 CLR 生存期管理卻基於垃圾收集器,並且 CLR 遠程采用發起者/超時方法。在我們的系統中, 我們需要穿過 AppDomain 或進程邊界,並且在這些方案中,CLR 的垃圾收集器已無法再提供足夠的支持 。因此,我們需要提供生存期管理系統,以使其對於宿主和加載項開發人員保持完全透明,並盡可能降低 管道(對象模型)開發人員的工作難度。我們提供了生存期管理解決方案,有關這方面的內容,我們將在 下個月的專欄中進行詳盡介紹。
宿主應用程序可能是一項長時間運行的服務,它具有內存限制,或需要具有在運行時出於某種原因而 卸載加載項的能力。在運行期間,僅允許在卸載了已加載程序集的 AppDomain 後才能卸載相應的程序集 :它不具有傳統的 DllUnload。這也是 AppDomain 邊界增值的另一種方法:允許用戶在不循環進程的情 況下便回收系統資源。
如果宿主希望立即關閉加載項,也無需擔心還要繼續跟蹤哪個 AppDomain 的哪一加載項。因為我們提 供了名為 AddInController 的類為您處理這方面的事務。只要擁有對要關閉加載項的引用,便可執行下 列一行代碼以完成該操作:
// 檢索 AddInController 中是否有我的加載項然後將其關閉 AddInController.GetAddInController(calculator).Shutdown();
除了賦予宿主快速關閉其加載項的能力之外,我們的模型還能控制宿主和加載項的行為,如同它們的 生存期管理完全受控於垃圾收集器一樣:如果它們要釋放某些內容,只需使引用為空或刪除引用即可。實 際上,我們在後台使用了引用計數系統和運行時遠程服務的組合來管理生存期。
加載項開發
從開發人員的角度來看,加載項系統的機制是隱藏的。但宿主的對象模型交互和/或加載項向宿主提供 可擴展性的方式應盡可能透明才好。使用我們的模型會使編寫加載項變得非常容易:我們部署了在加載項 程序集中定義加載項程序集條目的自定義屬性,並向宿主提供了有關加載項的描述性信息。應用該屬性後 ,所有加載項都必須執行其抽象基類中所定義的方法。
請注意,在加載項的示例代碼(可在 SampleCalculatorAddIn.cs 中找到這些代碼, SampleCalculatorAddIn.cs 是代碼下載中 SampleCalculatorAddIn.csproj 的一部分)中,加載項開發 人員無法看到管道代碼。例如,處理遠程控制和生存期管理的機制方面的難題便可迎刃而解(如圖 2 所 示)。
Figure 2 簡單的計算器加載項
namespace SampleCalculatorAddIn { [AddIn("Sample Calculator AddIn", Version="1.0.0.0")] public class SampleCalculatorAddIn : Calculator { public override double Add(double a, double b) { return a + b; } public override double Subtract(double a, double b) { return a-b; } public override double Multiply(double a, double b) { return a * b; } public override double Divide(double a, double b) { return a / b; } } }
如您所見,加載項模型幾乎不會給加載項開發人員帶來任何額外開銷。對於向宿主描述加載項,開發 人員只需使用自定義屬性,然後執行宿主要求其執行的功能(在本例中,即執行簡單的計算器算術運算) 。
總結
我們設計該模型的目的是實現跨版本可隔離組件的動態組合。我們關注的是,在引入能夠實現上述目 標的機制的同時,確保不增加加載項開發人員的開發難度,並盡可能簡化宿主的工作。
下個月,我們 將更詳盡地探討當宿主與加載項版本獨立時能夠實現兼容性的管道和模型使用方面的內容。屆時,歡迎訪 問我們的小組博客以獲取更多示例並告訴我們您的反饋意見。
將您想詢問的問題和提出的意見發送至 [email protected].
本文配套源碼:http://www.bianceng.net/dotnet/201212/828.htm