BrnShop开源网上商城第三讲:插件的工作机制
这几天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框架之初确定的框架设计理念是:在不损失框架的扩展性,稳定性和性能的条件下,一切从简,直接,一目了然,返璞归真。最后奉上本人的框架设计理念(如不喜,请轻喷!):