這幾天BrnShop的開發工作比較多,所以這一篇文章來的晚了一些,還請大家見諒呀!還有通知大家一下BrnShop1.0.312版本已經發布,此版本添加了報表統計等新功能,需要源碼的園友可以點此下載。好了,我們現在進入今天的正題。關於BrnShop插件內容比較多,所以我分成兩篇文章來講解,今天先講第一部分內容:插件的工作機制。
對於任意一種插件機制來說,基本上只要解決以下三個方面的問題,這個插件機制就算成功了。這三個方面如下:
- 插件程序集的加載
- 視圖文件的路徑和編譯
- 插件的部署
首先是插件程序集的加載(請仔細閱讀,下面的坑比較多),最簡單的方式就是直接將插件程序集復制到網站根目錄下的bin文件夾(夠簡單吧!),但是我們不推薦使用這種方式,因為這種方式導致插件的程序集和視圖文件等內容分布在不同目錄,為以后的維護帶來不便。我們期望的是插件的所有內容都在同一個目錄中,以BrnShop的支付寶插件為例:

所有的插件文件都在“BrnShop.PayPlugin.Alipay”目錄中,這樣我們以后的刪除,擴展等就方便多了。好了現在第一個坑來了,那就是如何讓asp.net加載此目錄下的程序集文件?我們可能會想到使用web.config文件中probing節點來配置asp.net運行時探測程序集的目錄,但是很不幸它不管用(不信你可以試一下)。代碼如下:
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<probing privatePath="Plugins/bin/" />
</assemblyBinding>
</runtime>
既然自動加載不行,那我們就手動加載吧。手動加載需要使用一個方法:System.Web.Compilation.BuildManager.AddReferencedAssembly(Assembly assembly),此方法的的MSDN解釋如下圖:

通過調用這個方法可以手動加載指定的程序集(就是它的參數)到我們的程序中。在調用這個方法時有兩個坑需要注意,第一個坑是這個方法必須在Application_Start 事件發生前調用,針對這個坑我們可以使用微軟在.NET4.0中提供的PreApplicationStartMethodAttribute性質(此性質的具體介紹大家可以看這篇文章:http://haacked.com/archive/2010/05/16/three-hidden-extensibility-gems-in-asp-net-4.aspx/)來解決這個問題。代碼如下:
[assembly: PreApplicationStartMethod(typeof(BrnShop.Core.BSPPlugin), "Load")]
第二個坑是CLR會鎖定程序集文件,所以如果我們直接讀取此文件會導致異常,解決這個坑的方式是復制它的一個副本,然后不讀取原程序集,而是讀取程序集的副本並傳入AddReferencedAssembly方法中。具體代碼如下:
try
{
//復制程序集
File.Copy(dllFile.FullName, newDllFile.FullName, true);
}
catch
{
//在某些情況下會出現"正由另一進程使用,因此該進程無法訪問該文件"錯誤,所以先重命名再復制
File.Move(newDllFile.FullName, newDllFile.FullName + Guid.NewGuid().ToString("N") + ".locked");
File.Copy(dllFile.FullName, newDllFile.FullName, true);
}
//加載程序集的副本到應用程序中
Assembly assembly = Assembly.Load(AssemblyName.GetAssemblyName(newDllFile.FullName));
//將程序集添加到當前應用程序域
BuildManager.AddReferencedAssembly(assembly);
不過這時又產生一個坑:此副本復制到哪個目錄呢?我們期望這個副本能夠保存下來,不必每次應用程序啟動都要復制一次,熟悉asp.net編譯的園友們估計都會想到,這個目錄就是應用程序域的DynamicDirectory目錄,一般情況下這個目錄位於C:\Windows\Microsoft.NET\Framework\v4.0.30319\Temporary ASP.NET Files\root\15a300ab\6af0b19a類似的目錄,截圖如下:

不過這時又產生了一個坑(哎,步步有坑呀!):權限問題,就是只有在Full Trust級別下才有操作此目錄的權限,而在Medium Trust級別下我們沒有操作此目錄的權限。信任級別的MSDN解釋如下:

針對這個坑我們只能根據不同的級別復制到不同的目錄,Full Trust級別時復制到DynamicDirectory目錄,Medium Trust級別時復制到一個影子目錄(/Plugins/bin)代碼如下:
DirectoryInfo copyFolder;
//根據當前的信任級別設置復制目錄
if (WebHelper.GetTrustLevel() != AspNetHostingPermissionLevel.Unrestricted)//非完全信任級別
{
//shadowFolder就是"/Plugins/bin"影子目錄
copyFolder = shadowFolder;
}
else//完全信任級別
{
copyFolder = new DirectoryInfo(AppDomain.CurrentDomain.DynamicDirectory);
}
在跳過上面眾多坑后,我們終於長舒一口氣,來到勝利的彼岸:插件的程序集文件能夠正確加載了。下面附上插件加載的完整代碼:
using System;
using System.IO;
using System.Web;
using System.Reflection;
using System.Web.Compilation;
using System.Collections.Generic;
[assembly: PreApplicationStartMethod(typeof(BrnShop.Core.BSPPlugin), "Load")]
namespace BrnShop.Core
{
/// <summary>
/// BrnShop插件管理類
/// </summary>
public class BSPPlugin
{
private static object _locker = new object();//鎖對象
private static string _installedfilepath = "/App_Data/InstalledPlugin.config";//插件安裝文件
private static string _pluginfolderpath = "/Plugins";//插件目錄
private static string _shadowfolderpath = "/Plugins/bin";//插件影子目錄
private static List<PluginInfo> _oauthpluginlist = new List<PluginInfo>();//開放授權插件列表
private static List<PluginInfo> _paypluginlist = new List<PluginInfo>();//支付插件列表
private static List<PluginInfo> _shippluginlist = new List<PluginInfo>();//配送插件列表
private static List<PluginInfo> _uninstalledpluginlist = new List<PluginInfo>();//未安裝插件列表
/// <summary>
/// 開放授權插件列表
/// </summary>
public static List<PluginInfo> OAuthPluginList
{
get { return _oauthpluginlist; }
}
/// <summary>
/// 支付插件列表
/// </summary>
public static List<PluginInfo> PayPluginList
{
get { return _paypluginlist; }
}
/// <summary>
/// 配送插件列表
/// </summary>
public static List<PluginInfo> ShipPluginList
{
get { return _shippluginlist; }
}
/// <summary>
/// 未安裝插件列表
/// </summary>
public static List<PluginInfo> UnInstalledPluginList
{
get { return _uninstalledpluginlist; }
}
/// <summary>
/// 加載插件程序集到應用程序域中
/// </summary>
public static void Load()
{
try
{
//插件目錄
DirectoryInfo pluginFolder = new DirectoryInfo(IOHelper.GetMapPath(_pluginfolderpath));
if (!pluginFolder.Exists)
pluginFolder.Create();
//插件bin目錄
DirectoryInfo shadowFolder = new DirectoryInfo(IOHelper.GetMapPath(_shadowfolderpath));
if (!shadowFolder.Exists)
{
shadowFolder.Create();
}
else
{
//清空影子復制目錄中的dll文件
foreach (FileInfo fileInfo in shadowFolder.GetFiles())
{
fileInfo.Delete();
}
}
//獲得安裝的插件系統名稱列表
List<string> installedPluginSystemNameList = GetInstalledPluginSystemNameList();
//獲得全部插件
List<KeyValuePair<FileInfo, PluginInfo>> allPluginFileAndInfo = GetAllPluginFileAndInfo(pluginFolder);
foreach (KeyValuePair<FileInfo, PluginInfo> fileAndInfo in allPluginFileAndInfo)
{
FileInfo pluginFile = fileAndInfo.Key;
PluginInfo pluginInfo = fileAndInfo.Value;
if (String.IsNullOrWhiteSpace(pluginInfo.SystemName))
throw new BSPException(string.Format("插件'{0}'沒有\"systemName\", 請輸入一個唯一的\"systemName\"", pluginFile.FullName));
if (pluginInfo.Type < 0 || pluginInfo.Type > 2)
throw new BSPException(string.Format("插件'{0}'不屬於任何一種類型, 請輸入正確的的\"type\"", pluginFile.FullName));
//加載插件dll文件
FileInfo[] dllFiles = pluginFile.Directory.GetFiles("*.dll", SearchOption.TopDirectoryOnly);
foreach (FileInfo dllFile in dllFiles)
{
//部署dll文件
DeployDllFile(dllFile, shadowFolder);
}
if (IsInstalledlPlugin(pluginInfo.SystemName, installedPluginSystemNameList))//安裝的插件
{
//根據插件類型將插件添加到相應列表
switch (pluginInfo.Type)
{
case 0:
_oauthpluginlist.Add(pluginInfo);
break;
case 1:
_paypluginlist.Add(pluginInfo);
break;
case 2:
_shippluginlist.Add(pluginInfo);
break;
}
}
else//未安裝的插件
{
_uninstalledpluginlist.Add(pluginInfo);
}
}
}
catch (Exception ex)
{
throw new BSPException("加載BrnShop插件時出錯", ex);
}
}
/// <summary>
/// 安裝插件
/// </summary>
/// <param name="systemName">插件系統名稱</param>
public static void Install(string systemName)
{
lock (_locker)
{
if (string.IsNullOrWhiteSpace(systemName))
return;
//在未安裝的插件列表中獲得對應插件
PluginInfo pluginInfo = _uninstalledpluginlist.Find(x => x.SystemName.Equals(systemName, StringComparison.InvariantCultureIgnoreCase));
//當插件為空時直接返回
if (pluginInfo == null)
return;
//當插件不為空時將插件添加到相應列表
switch (pluginInfo.Type)
{
case 0:
_oauthpluginlist.Add(pluginInfo);
_oauthpluginlist.Sort((first, next) => first.DisplayOrder.CompareTo(next.DisplayOrder));
break;
case 1:
_paypluginlist.Add(pluginInfo);
_paypluginlist.Sort((first, next) => first.DisplayOrder.CompareTo(next.DisplayOrder));
break;
case 2:
_shippluginlist.Add(pluginInfo);
_shippluginlist.Sort((first, next) => first.DisplayOrder.CompareTo(next.DisplayOrder));
break;
}
//在未安裝的插件列表中移除對應插件
_uninstalledpluginlist.Remove(pluginInfo);
//將新安裝的插件保存到安裝的插件列表中
List<string> installedPluginSystemNameList = GetInstalledPluginSystemNameList();
installedPluginSystemNameList.Add(pluginInfo.SystemName);
SaveInstalledPluginSystemNameList(installedPluginSystemNameList);
}
}
/// <summary>
/// 卸載插件
/// </summary>
/// <param name="systemName">插件系統名稱</param>
public static void Uninstall(string systemName)
{
lock (_locker)
{
if (string.IsNullOrEmpty(systemName))
return;
PluginInfo pluginInfo = null;
Predicate<PluginInfo> condition = x => x.SystemName.Equals(systemName, StringComparison.InvariantCultureIgnoreCase);
pluginInfo = _oauthpluginlist.Find(condition);
if (pluginInfo == null)
pluginInfo = _paypluginlist.Find(condition);
if (pluginInfo == null)
pluginInfo = _shippluginlist.Find(condition);
//當插件為空時直接返回
if (pluginInfo == null)
return;
//根據插件類型移除對應插件
switch (pluginInfo.Type)
{
case 0:
_oauthpluginlist.Remove(pluginInfo);
break;
case 1:
_paypluginlist.Remove(pluginInfo);
break;
case 2:
_shippluginlist.Remove(pluginInfo);
break;
}
//將插件添加到未安裝插件列表
_uninstalledpluginlist.Add(pluginInfo);
//將卸載的插件從安裝的插件列表中移除
List<string> installedPluginSystemNameList = GetInstalledPluginSystemNameList();
installedPluginSystemNameList.Remove(pluginInfo.SystemName);
SaveInstalledPluginSystemNameList(installedPluginSystemNameList);
}
}
/// <summary>
/// 編輯插件信息
/// </summary>
/// <param name="systemName">插件系統名稱</param>
/// <param name="friendlyName">插件友好名稱</param>
/// <param name="description">插件描述</param>
/// <param name="displayOrder">插件排序</param>
public static void Edit(string systemName, string friendlyName, string description, int displayOrder)
{
lock (_locker)
{
bool isInstalled = true;//是否安裝
PluginInfo pluginInfo = null;
Predicate<PluginInfo> condition = x => x.SystemName.Equals(systemName, StringComparison.InvariantCultureIgnoreCase);
pluginInfo = _oauthpluginlist.Find(condition);
if (pluginInfo == null)
pluginInfo = _paypluginlist.Find(condition);
if (pluginInfo == null)
pluginInfo = _shippluginlist.Find(condition);
//當插件為空時直接返回
if (pluginInfo == null)
{
pluginInfo = _uninstalledpluginlist.Find(condition); ;
//當插件為空時直接返回
if (pluginInfo == null)
return;
else
isInstalled = false;
}
pluginInfo.FriendlyName = friendlyName;
pluginInfo.Description = description;
pluginInfo.DisplayOrder = displayOrder;
//將插件信息持久化到對應文件中
IOHelper.SerializeToXml(pluginInfo, IOHelper.GetMapPath("/Plugins/" + pluginInfo.Folder + "/PluginInfo.config"));
//插件列表重新排序
if (isInstalled)
{
switch (pluginInfo.Type)
{
case 0:
_oauthpluginlist.Sort((first, next) => first.DisplayOrder.CompareTo(next.DisplayOrder));
break;
case 1:
_paypluginlist.Sort((first, next) => first.DisplayOrder.CompareTo(next.DisplayOrder));
break;
case 2:
_shippluginlist.Sort((first, next) => first.DisplayOrder.CompareTo(next.DisplayOrder));
break;
}
}
else
{
_uninstalledpluginlist.Sort((first, next) => first.DisplayOrder.CompareTo(next.DisplayOrder));
}
}
}
/// <summary>
/// 獲得安裝的插件系統名稱列表
/// </summary>
private static List<string> GetInstalledPluginSystemNameList()
{
return (List<string>)IOHelper.DeserializeFromXML(typeof(List<string>), IOHelper.GetMapPath(_installedfilepath));
}
/// <summary>
/// 保存安裝的插件系統名稱列表
/// </summary>
/// <param name="installedPluginSystemNameList">安裝的插件系統名稱列表</param>
private static void SaveInstalledPluginSystemNameList(List<string> installedPluginSystemNameList)
{
IOHelper.SerializeToXml(installedPluginSystemNameList, IOHelper.GetMapPath(_installedfilepath));
}
/// <summary>
/// 獲得全部插件
/// </summary>
/// <param name="pluginFolder">插件目錄</param>
/// <returns></returns>
private static List<KeyValuePair<FileInfo, PluginInfo>> GetAllPluginFileAndInfo(DirectoryInfo pluginFolder)
{
List<KeyValuePair<FileInfo, PluginInfo>> list = new List<KeyValuePair<FileInfo, PluginInfo>>();
FileInfo[] PluginInfoes = pluginFolder.GetFiles("PluginInfo.config", SearchOption.AllDirectories);
Type pluginType = typeof(PluginInfo);
foreach (FileInfo file in PluginInfoes)
{
PluginInfo info = (PluginInfo)IOHelper.DeserializeFromXML(pluginType, file.FullName);
list.Add(new KeyValuePair<FileInfo, PluginInfo>(file, info));
}
list.Sort((firstPair, nextPair) => firstPair.Value.DisplayOrder.CompareTo(nextPair.Value.DisplayOrder));
return list;
}
/// <summary>
/// 判斷插件是否已經安裝
/// </summary>
/// <param name="systemName">插件系統名稱</param>
/// <param name="installedPluginSystemNameList">安裝的插件系統名稱列表</param>
/// <returns> </returns>
private static bool IsInstalledlPlugin(string systemName, List<string> installedPluginSystemNameList)
{
foreach (string name in installedPluginSystemNameList)
{
if (name.Equals(systemName, StringComparison.InvariantCultureIgnoreCase))
return true;
}
return false;
}
/// <summary>
/// 部署程序集
/// </summary>
/// <param name="dllFile">插件程序集文件</param>
/// <param name="shadowFolder">/Plugins/bin目錄</param>
private static void DeployDllFile(FileInfo dllFile, DirectoryInfo shadowFolder)
{
DirectoryInfo copyFolder;
//根據當前的信任級別設置復制目錄
if (WebHelper.GetTrustLevel() != AspNetHostingPermissionLevel.Unrestricted)//非完全信任級別
{
copyFolder = shadowFolder;
}
else//完全信任級別
{
copyFolder = new DirectoryInfo(AppDomain.CurrentDomain.DynamicDirectory);
}
FileInfo newDllFile = new FileInfo(copyFolder.FullName + "\\" + dllFile.Name);
try
{
File.Copy(dllFile.FullName, newDllFile.FullName, true);
}
catch
{
//在某些情況下會出現"正由另一進程使用,因此該進程無法訪問該文件"錯誤,所以先重命名再復制
File.Move(newDllFile.FullName, newDllFile.FullName + Guid.NewGuid().ToString("N") + ".locked");
File.Copy(dllFile.FullName, newDllFile.FullName, true);
}
Assembly assembly = Assembly.Load(AssemblyName.GetAssemblyName(newDllFile.FullName));
//將程序集添加到當前應用程序域
BuildManager.AddReferencedAssembly(assembly);
}
}
}
在解決了插件程序集的加載問題后我們再來解決視圖文件的路徑和編譯問題,由於插件目錄不在傳統View文件夾中,所以我們像普通視圖那樣返回插件路徑,我們需要使用根目錄路徑來返回視圖文件路徑,以支付寶插件為例:
/// <summary>
/// 配置
/// </summary>
[HttpGet]
[ChildActionOnly]
public ActionResult Config()
{
ConfigModel model = new ConfigModel();
model.Partner = PluginUtils.GetPluginSet().Partner;
model.Key = PluginUtils.GetPluginSet().Key;
model.Seller = PluginUtils.GetPluginSet().Seller;
model.AllowRecharge = PluginUtils.GetPluginSet().AllowRecharge;
//插件視圖文件路徑必須以"~"開頭
return View("~/Plugins/BrnShop.PayPlugin.Alipay/Views/AdminAlipay/Config.cshtml", model);
}
通過使用根目錄路徑,我們可以忽視一切路由匹配問題了。再來說說視圖的編譯問題,為了能夠正確指導asp.net編譯視圖文件,我們需要在“/Plugins”文件夾(此文件夾中的web.cong能夠覆蓋所有插件目錄)中添加一個web.config文件,並指定編譯要求如下:
<system.web.webPages.razor>
<host factoryType="System.Web.Mvc.MvcWebRazorHostFactory, System.Web.Mvc, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
<pages pageBaseType="System.Web.Mvc.WebViewPage">
<namespaces>
<add namespace="System.Web.Mvc" />
<add namespace="System.Web.Mvc.Ajax" />
<add namespace="System.Web.Mvc.Html" />
<add namespace="System.Web.Routing" />
<add namespace="System.Text" />
<add namespace="System.Data" />
<add namespace="System.Collections"/>
<add namespace="System.Collections.Generic"/>
<add namespace="BrnShop.Core" />
<add namespace="BrnShop.Services" />
<add namespace="BrnShop.Web.Framework" />
<add namespace="BrnShop.Web.Models" />
</namespaces>
</pages>
</system.web.webPages.razor>
這樣我們的插件視圖文件就能夠正確編譯了。
現在只剩下插件的部署問題了。如果是手動部署我們只需要將插件目錄及其文件復制到"/Plugins"目錄中就可以。如果是使用vs自動部署我們需要做以下幾步配置:
第一步配置插件程序集的輸出路徑,通過在項目上點擊右鍵選擇屬性進入,具體配置如下:

現在你生成一下解決方案就會發現插件程序集已經到"/Plugins"文件夾中。
第二步是篩選程序集,就是只輸出插件程序集,其它的程序集(包括系統自帶和引用的程序集)不輸出。具體配置如下如圖:

最后一步是輸出內容文件,例如視圖文件,具體配置如下圖:

到此BrnShop的插件能夠正常工作了。
PS:其實asp.net mvc插件的實現方式有許多種,大家可以google一下就會發現。而BrnShop之所以采用這種插件機制其實是服從於程序整體框架設計理念的。我們在設計BrnShop框架之初確定的框架設計理念是:在不損失框架的擴展性,穩定性和性能的條件下,一切從簡,直接,一目了然,返璞歸真。最后奉上本人的框架設計理念(如不喜,請輕噴!):
- 高手和大神的區別:高手能夠把簡單的問題復雜化,大神能夠把復雜的問題簡單化。
- 項目需要什么架構不是想出來的,而是在開發過程中為解決問題而引入的。不能因為存在或會某種設計模式就使用。

