加载程序集调用的方法说明,插件编程10bet手机官网:
分类:高并发

原文作者:
Shawn Patrick Walcheske

程序集加载

最近也研究了下插件编程,网上的例子太少,而且不适合初学者。这里专门做了个简单易懂的例子,供大家学习,程序如有问题还望跟帖予以指教。

译者:
电子科技大学 夏桅

程序集加载,CLR使用System.Reflection.Assembly.Load静态方法,当然这个方法我们自己也可以显式调用。

       程序界面运行如下:

[引言]

还有一个Assembly.LoadFrom方法加载指定路径名的程序集,实际上其内部是先通过AssemblyName.GetAssemblyName获取AssemblyName对象,然后调用Assembly.Load方法。

10bet手机官网 1

在.NET框架下的C#语言,和其他.NET语言一样提供了很多强大的特性和机制.其中一些是全新的,而有些则是从以前的语言和平台上照搬过来的.然而,这种巧妙的结合产生了一些有趣的方法可以用来解决我们的问题.这篇文章将讲述如何利用这些奇妙的特性,用插件(plug-ins)机制建立可扩展的解决方案.后面也将提供一个简要的例子,你甚至可以用这个东西来替换那些已经在很多系统中广泛使用的独立的程序.在一个系统中,可能有很多程序经常需要进行数据处理.可能其中有一个程序用于处理雇员的信息,而另一个用来管理客户关系.在大多数情况下,系统总是被设计为很多个独立的程序,他们之间很少有交互,经常使用复制代码的办法来共享.而实际上这样的情况可以把那些程序设计为插件,再用一个单一的程序来管理这些插件.这种设计可以让我们更好的在不同的解决方案中共享公用的方法,提供统一的感观.

此时load方法会在各个位置(前面03章讲过)查找程序集,如果已经加载了此程序集就返回已加载的程序集,如果没有加载就去加载找到的程序集,如果没有找到,就加载路径所给的那个程序集。(所以很清楚了解到不一定会加载所指定的那个程序集,而可能是另一个。在这里如果每次生成强命名程序集时更新版本号,才会使LoadFrom方法的行为符合预期)

//==============================================================

图片一是一个例子程序的截图.用户界面和其他常见的程序没有什么不同.整个窗体被垂直的分割为两块.左边的窗格是个树形菜单,用于显示插件列表,在每个插件的分支下面,列出了这个插件所管理的数据.而右边的窗格则用于编辑左边被选中的插件的数据.各个插件提供各自的编辑数据的界面.图片一展示了一个精巧的工作区.

LoadFrom方法允许传递一个Url作为实参,CLR会下载文件,把它安装到用户的下载缓存中,再从那儿加载文件。

首先,我们要定义一个插件接口,接口规范了插件内部类的程序结构,应实现的字段,属性,方法,事件。

[开始]

ReflectionOnlyLoadFrom函数也可以加载程序集,且禁止程序集中的任何代码执行。

using System; using System.Collections.Generic; using System.Text;

那么,主程序必须能够加载插件,然后和这些插件进行通信,这样才能实现我们的设计.所有这些的实现可以有很多不同的方法,仅取决于开发者选择的语言和平台.如果选择的是C#和.NET,那么反射(reflection)机制可以用来加载插件,并且其接口和抽象类可以用于和插件通信.

使用反射构建动态可扩展应用程序

namespace IMsg {   //这是插件必须要实现的接口,也是主程序与插件通信的唯一接口,     //换句话说,主程序只认识插件里的这些方法     public interface IMsgPlug     {         void OnShowDlg();         string OnShowInfo();     } }

为了更好的理解主程序和插件之间的通信,可以先了解一下设计模式.设计模式最早由Erich Gamma提出[1],它利用架构和对象思想来实现通用的通信模型.不管组件是否具有不同的输入和输出,只要他们有相似的结构.设计模式可以帮助开发者利用广受证明的面向对象理论来解决问题.事实上它就是描述解决方案的语言,而不用管问题的具体细节或者编程语言的细节.设计模式策略的关键点在于如何把整个解决方案根据功能来分解,这种分解是通过把主程序的不同功能分开执行而完成的.这样主程序和子程序之间的通信可以通过设计良好的接口来完成.通过这种分解我们立即可以得到这两个好处:第一,软件项目被分成较小的不相干的单位,工作流程的设计可以更容易,而较小的代码片断意味着代码更容易建立和维护.第二个好处在于改变程序行为的时候并不会关系到主程序的运行,主程序不用关心子程序如何,他们之间只要有通用的通讯机制就足够了.

既然加载了程序集,那么就应该要有办法去使用程序集中定义的类,这种办法就是反射。

再者,需要实现接口在主程序内部的处理过程,包括载入插件,获取插件内部的被接口规范了的字段,属性,方法,事件。

[建立接口]

利用System.Reflection命名空间中包含的类型,可以写代码来反射元数据表,为所加载的程序集中所包含的元数据提供对象模型。

using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Text; using System.Windows.Forms; using System.IO; using System.Reflection; using System.Collections; using IMsg;

在C#程序中,接口是用来定义一个类的功能的.接口定义了预期的方法,属性,事件信息.为了使用接口,每个具体的函数必须严格按照接口的定义完成所描述的功能.列表一展示了上面例子程序的接口:IPlug.这个接口定义了四个方法:GetData,GetEditControl,Save和Print.这四个定义并没有描述具体是怎么完成的,但是他们保证了这个类支持IPlug接口,也就是保证支持这些方法的调用.

反射一些例子:

namespace MsgBoxMain {     public partial class FormMain : Form     {         public ArrayList plugins = new ArrayList();         public FormMain()         {             InitializeComponent();         }         //载入插件         private void LoadAllPlugins()         {             //获取指定插件目录下的所有文件的文件名             string[] files = Directory.GetFiles(Application.StartupPath + @"plugins");             //遍历该文件名集合             foreach (string file in files)             {   //检索出文件名以.dll结束的文件                 if (file.Substring(file.LastIndexOf(".")) == ".dll")                 {                     try                     {   //载入dll                         Assembly ab = Assembly.LoadFile(file);                         //获得载入的dll中的所有类                         Type[] tempTs = ab.GetTypes();                        //遍历该类集合                         foreach (Type tp in tempTs)                         {                             //如果某些类实现了预定义的IMsg.IMsgPlug接口,则认为该类适配与主程序(是主程序的插件)                             if (IsValidPlugin(tp))                             {   //实例化该类,并将对象装入动态数组plugins                                 plugins.Add(ab.CreateInstance(tp.FullName));                                 //将该类型名载入列表框内                                 ListItems.Items.Add(tp.Name.ToString());                             }                         }                     }                     catch(Exception ex)                     {                         MessageBox.Show(ex.ToString(), "加载插件出错", MessageBoxButtons.OK, MessageBoxIcon.Error);                     }                 }             }         }         //判断模块的类是否满足预定义接口         private bool IsValidPlugin(Type t)         {             bool ret = false;             Type[] interfaces = t.GetInterfaces();             foreach (Type theInterface in interfaces)             {                 if (theInterface.FullName == "IMsg.IMsgPlug")                 {                     ret = true;                     break;                 }             }             return ret;         }        //调用插件内的方法         private void button2_Click(object sender, EventArgs e)         {  
           //获取列表框内被选择的项             string itemStr = ListItems.SelectedItem.ToString();             if(ListItems.SelectedIndex >= 0)             {                 if (itemStr == "myConsole")                 {   //调用存储在动态数组plugins里面的插件对象的OnShowInfo方法                     string msgInfo = ((IMsgPlug)plugins[ListItems.SelectedIndex]).OnShowInfo();                     MessageBox.Show(msgInfo, "MYPlugin1", MessageBoxButtons.OK, MessageBoxIcon.Information);                 }                 else if (itemStr == "MYDlg")//调用存储在动态数组plugins里面的插件对象的OnShowDlg方法                 {                     ((IMsgPlug)plugins[ListItems.SelectedIndex]).OnShowDlg();                 }             }             else                 MessageBox.Show("请先选择列表框里的插件项", "Warning", MessageBoxButtons.OK, MessageBoxIcon.Warning);         }

[定制属性]

首先先建立一个用于反射的程序集,代码如下:

        private void button1_Click(object sender, EventArgs e)         {             try             {   //载入插件                 LoadAllPlugins();             }             catch (Exception ex)             {                 MessageBox.Show(ex.ToString());             }         }     } }

在查看代码之前,讨论总是先得转移到属性定制上面.属性定制是.NET提供的一个非常棒的新特性之一,属性对于所有的编程语言都是一种通用的结构.举个例子,一个函数用于标识可访问权限的public,private,或者protect标志就是这个函数的一个属性.属性定制之所以如此让人兴奋,那是因为编程人员将不再只能从语言本身提供的有限的属性集中选择.一个定制的属性其实也是一个类,它从System.Attribute继承,它的代码被允许是自我描述的.属性定制可以应用于绝大多数结构中,包括C#里面的类,方法,事件,域和属性等等.示例代码片断定义了两个定制的属性:PlugDisplayNameAttribute和PlugDescriptionAttribute,所有的插件内部的类必须支持这两个属性.列表二是用于定义PlugDisplayNameAttribute的类.这个属性用于显示插件节点的内容.在程序运行的时候,主程序将可以利用反射(reflection)来取得属性值.

复制代码

最后,编写插件类库,实现接口的属性,方法,事件。

[插件(Plug-Ins)]

namespace HelloWorld

//程序集(dll)内部的一个类1,该类实现了插件接口

上面的示例程序包括了两个插件的执行.这些插件在EmployeePlug.cs和CustomerPlug.cs中定义.列表三展示了EmployeePlug类的部分定义.下面是一些关键点.

{

using System; using System.Collections.Generic; using System.Text; using IMsg;

1.这个类实现了IPlug接口.由于主程序根本不会知道插件内部的类是如何定义的,这非常重要,主程序需要使用IPlug接口和各个插件通信.这种设计利用了面向对象概念里面的"多态性".多态性允许运行时,可以通过指向基类的引用,来调用实现派生类中的方法.
2.这个类被两个属性标识,这样主程序可以判断这个插件是不是有效的.在C#中,要给一个类标识一个属性,你得在类的定义之前声明属性,内容附在括号内.
3.简明起见,例子只是使用了直接写入代码的数据.而如果这个插件是个正式的产品,那么数据总是应该放在数据库中或者文件中,各自所有的数据都应该仅仅由插件本身来管理.EmployeePlug类的数据在这里用EmployeeData对象来存储,那也是一个类型并且实现了IPlugData接口.IPlugData接口在IPlugData.cs中定义,它提供了最基础的数据交换功能,用于主程序和插件之间的通讯.所有支持IPlugData接口的对象在下层数据变化的时候将提供一个通知.这个通知实际上就是DataChanged事件的发生.
4.当主程序需要显示某个插件所含数据列表的时候,它会调用GetData方法.这个方法返回IPlugData对象的一个数组.这样主程序就可以对数组中的每个对象使用ToString方法得到数据以建立树的各个节点.ToString方法是EmployeeData类的一个重载,用于显示雇员的名字.
5.IPlug接口也定义了Save和Print方法.定义这两个方法的目的在于当有需要打印或者保存数据的时候,要通知一个插件.EmployeePlug类就是用于实现打印和保存数据的功能的.在使用Save方法的时候,需要保存数据的位置将会在方法调用的时候提供.这里假设主程序会向用户查询路径等信息.路径信息的查询是主程序提供给各个插件的服务.对于Print方法,主程序将把选项和内容传递到System.Drawing.Printing.PrintDocument类的实例.这两种情况下,和用户的交互操作都是一致的由主程序提供的.

   public class Man

namespace MYPlugin1 {  //实现插件接口     public class myConsole : IMsgPlug     {           public void OnShowDlg() {}         public string OnShowInfo()         {             return "调用了插件1的OnShowInfo方法!";         }     } }

[反射(Reflection)]

   {

//程序集(dll)内部的一个类2,该类实现了也插件接口

在一个插件定义好之后,下一步要做的就是查看主程序是怎么加载插件的.为了实现这个目标,主程序使用了反射机制.反射是.NET中用于运行时查看类型信息的.在反射机制的帮助下,类型信息将被加载和查看.这样就可以通过检查这个类型以判断插件是否有效.如果类型通过了检查,那么插件就可以被添加到主程序的界面中,就可以被用户操作.

       public string _name;

using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Text; using System.Windows.Forms; using IMsg;

示例程序使用了.NET框架的三个内置类来使用反射:System.Reflection.Assembly,System.Type,和System.Activator.

       public Man(String name) {

namespace MYPlugin1 {   //实现插件接口     public partial class MYDlg : Form, IMsgPlug     {         public MYDlg()         {             InitializeComponent();         }

System.Reflection.Assembly类描述了.NET的程序集.在.NET中,程序集是配置单元.对于一个典型的Windows程序,程序集被配置为单一的Win32可执行文件,并且带有特定的附加信息,使之适应.NET运行环境.程序集也可以配置为Win32的DLL(动态链接库),同样需要带有.NET需要的附加信息.System.Reflection.Assembly类可以在运行的时候取得程序集的信息.这些信息包括程序集包含的类型信息.

           this._name = name;

        //=============================================         public string OnShowInfo() { return null; }         public void OnShowDlg()         {             this.Show();         }         //=============================================     } }

System.Type类描述了类型定义.一个类型声明可以是一个类,接口,数组,结构体,或者枚举.在加载了一个类之后,System.Type类可以被用于枚举该类支持的方法,属性,事件和接口.

       }

总结: 通过反射以及接口使C#下的插件编程变得容易,我们在接口中定义的字段,属性,方法,事件是在插件里面被实现的,是在主程序里被解析的。主程序通过公共的接口访问插件

System.Activator类用于创建一个类的实例.

       public void ShowName() {

 

[加载插件]

           Console.WriteLine(this._name);

出处:

列表四展示了LoadPlugs方法.LoadPlugs方法在HostForm.cs中定义,是HostForm类的一个private的非静态方法.LoadPlugs方法使用.NET的反射机制来加载可用的插件文件,并且验证它们是否符合被主程序使用的要求,然后把它们添加到主程序的树形显示区中.这个方法包含了下面几个步骤:

       }

1.通过使用System.IO.Directory类,我们的代码可以用通配符来查找所有的以.plug为扩展名的文件.而Directory类的静态方法GetFiles能够返回一个System.String类型的数组,以得到每个符合要求的文件的物理路径.
2.在得到路径字符串数组之后,就可以开始把文件加载到System.Reflection.Assembly实例中了.建立Asdsembly对象的代码使用了try/catch代码块,这样如果某个文件并不是一个有效地.NET程序集,就会抛出异常,程序此时将弹出一个MessageBox对话框,告诉用户无法加载该文件.循环一直进行直到所有文件都已遍历完成.
3.在一个程序集加载之后,代码将遍历所有可访问到的类型信息,检查是否支持了HostCommon.IPlug接口.
4.如果所有类型都支持HostCommon.IPlug接口,那么代码继续验证这些类型,检查是否支持那些已预先为插件定义好的属性.如果没有支持,那么一个HostCommon.PlugNotValidException类型的异常将会被抛出,同样,主程序将会弹出一个MessageBox,告诉用户出错的具体信息.循环一直进行直到所有文件都已遍历完成.
5.最后,如果这些类型支持HostCommon.IPlug接口,也已定义了所有需要定义的属性,那么它将被包装为一个PlugTreeNode实例.这个实例就会被添加到主程序的树形显示区.

   }

[实现]

}

主程序框架被设计为两个程序集.第一个程序集是Host.exe,它提供了主程序的Windows窗体界面.第二个程序集是HostCommon.dll,它提供了主程序和插件之间进行通信所需的所有类型定义.比如,IPlug接口就是在HostCommon.dll里面配置的,这样它可以被主程序和插件等价的访问.这两个程序集在一个文件夹内,同样的,附加的作为插件的程序集也需要被配置在一起.那些程序集被配置在plugs文件夹内(主程序目录的一个子文件夹).EmployeePlug类在Employee.plug程序集中定义,而CustomerPlug类在Customer.plug程序集中定义.这个例子指定插件文件以.plug为扩展名.事实上这些插件就是个普通的.NET类库文件,只是通常库文件使用.dll扩展名,这里用.plug罢了.特殊的扩展名对于程序运行是完全没有影响的,但是它可以让用户更明确的知道这是个插件文件.

namespace HelloWorld

[设计的比较]

{

并不是一定要像例子程序这样设计才算正确的.比如,在开发一个带有插件的C#程序时,并不一定需要使用属性.例子里使用了两个自定义的属性,其实也可以新定义两个IPlug接口的参数来实现.这里选择用属性,是因为插件的名字和它的描述在本质上确实就是一个事物的属性,符合规范.当然了,使用属性会造成主程序需要更多的关于反射的代码.对于不同的需求,设计者总是需要做出合理的决定.

   public class Troy:Man

[总结]

   {

示例程序被设计为尽量的简单,以帮助理解主程序和插件之间的通信.在实际做产品的时候,可以做很多的改进以满足实用要求.比如:

       private string _jobName;

1.通过对IPlug接口增加更多的方法,属性,事件,可以增加主程序和插件之间的通信点.两者间的更多的交互操作使得插件可以做更多的事情.
2.可以允许用户主动选择需要加载的插件.

public Troy(string name,string jobName):base(name) {

[源代码]
示例程序的完整的源代码可以在这里下载.
ftp://ftp.cuj.com/pub/2003/2101/walchesk.zip

this._jobName = jobName;

[备注]
[1] Erich Gamma et al. Design Patterns (Addison-Wesley, 1995).

}

图片一:

public void ShowJobName() {

列表一:The IPlug interface

Console.WriteLine(this._jobName);

public interface IPlug
{
  IPlugData[] GetData();
  PlugDataEditControl GetEditControl(IPlugData Data);
  bool Save(string Path);
  bool Print(PrintDocument Document);
}

}

列表二:The PlugDisplayNameAttribute class definition

}

[AttributeUsage(AttributeTargets.Class)]
public class PlugDisplayNameAttribute : System.Attribute
{
  private string _displayName;

}

  public PlugDisplayNameAttribute(string DisplayName) : base()
  {
    _displayName=DisplayName;
    return;
  }

复制代码

  public override string ToString()
  {
    return _displayName;
  }

然后生成了一个叫HelloWorld.dll的文件,然后开始玩反射

列表三:A partial listing of the EmployeePlug class definition

复制代码

[PlugDisplayName("Employees")]
[PlugDescription("This plug is for managing employee data")]
public class EmployeePlug : System.Object, IPlug
{
  public IPlugData[] GetData()
  {
     IPlugData[] data = new EmployeeData[]
      {
        new EmployeeData("Jerry", "Seinfeld")
        ,new EmployeeData("Bill", "Cosby")
        ,new EmployeeData("Martin", "Lawrence")
      };

      //首先加载程序集,获取程序集对象

    return data;
  }

Assembly myAssembly=Assembly.LoadFrom("D:\HelloWorld.dll");

  public PlugDataEditControl GetEditControl(IPlugData Data)
  {
    return new EmployeeControl((EmployeeData)Data);
  }

//玩程序集中定义的公共类型

  public bool Save(string Path)
  {
    //implementation not shown
  }

foreach (Type type in myAssembly.ExportedTypes) {

  public bool Print(PrintDocument Document)
  {
    //implementation not shown
  }
}

//打印类型全名

列表四:The method LoadPlugs

Console.WriteLine("类型全名:"+type.FullName);

private void LoadPlugs()
{
  string[] files = Directory.GetFiles("Plugs", "*.plug");

Console.WriteLine(type.FullName + "的基类:" + type.BaseType.FullName);

  foreach(string f in files)
  {

//判定类型是否为String(当然这是不可能的,因为只有Man和Troy)

    try
    {
      Assembly a = Assembly.LoadFrom(f);
      System.Type[] types = a.GetTypes();
      foreach(System.Type type in types)
      {
        if(type.GetInterface("IPlug")!=null)
        {
          if(type.GetCustomAttributes(typeof(PlugDisplayNameAttribute),
  false).Length!=1)
            throw new PlugNotValidException(type,
              "PlugDisplayNameAttribute is not supported");
          if(type.GetCustomAttributes(typeof(PlugDescriptionAttribute),
  false).Length!=1)
            throw new PlugNotValidException(type,
              "PlugDescriptionAttribute is not supported");

if (type == typeof(String)) {

          _tree.Nodes.Add(new PlugTreeNode(type));
        }
      }
    }
    catch(Exception e)
    {
      MessageBox.Show(e.Message);
    }
  }

Console.WriteLine("有个String类型");

  return;
}

}

[关于作者]
Shawn Patrick Walcheske是美国Arizona州Phoenix市的一名软件开发工程师.他同时是Microsoft Certified Solution Developer和Sun Certified Programmer for the Java 2 Platform.你可以在这里联系到他, questions@walcheske.com.

//Type对象是轻量型的类型引用,更全面的信息在TypeInfo对象(获取TypeInfo对象会强迫CLR确保已加载类型的定义程序集,从而对类型进行解析。(代价高昂)),

[译者注]
以前就考虑过在.NET里面如何实现插件机制,做来做去总是觉得设计上不够好.而昨天在网上无意中发现了这篇文章,写的实在是太棒了,所以看完之后,决定把它翻译过来,前后一共花了大概10个小时吧.翻译的可能不太好,请见谅.文中有什么错误,请不吝指正.
我的E-Mail: sunmast@vip.163.com

//如下转换

  <> <>

TypeInfo typeInfo = IntrospectionExtensions.GetTypeInfo(type);

//也可以反着转

Type tmpType = typeInfo.AsType();

//泛型类型的Type

Type openType = typeof(Dictionary<,>);//开放类型

Type closedType= openType.MakeGenericType(typeof(int), type);//闭合类型

//实例化

Object obj= Activator.CreateInstance(closedType);

Console.WriteLine(obj.GetType());

}

复制代码

反射的性能

反射是相当强大的机制,但是也有其缺点:

反射造成编译时无法保证类型安全性,因为它是在运行时才依靠字符串来对类进行实例化等操作。

反射的速度很慢,因为是在运行时靠字符串去标识成员,发现它们,使用它们。整个过程中都是用字符串来搜索。

设计支持加载项的应用程序

构建可扩展应用程序时,一般使用接口而不是基类,因为接口允许加载项开发人员选择自己的基类。

为宿主接口类的方法定义参数和返回类时,尝试使用MSCorLib.dll定义的接口和类型。因为CLR只加载一个MSCorLib.dl,所以不会出现类型版本不匹配的情况,且有助于减少应用程序对内存的需求。

反射与类型的成员

System.Reflection.MemberInfo封装了所有类型成员都通用的一组属性。它的一些派生类如MethodInfo则封装了与特定类型成员相关的更多属性。

直接上代码简单易懂:

复制代码

class Program

   {

       static void Main(string[] args)

       {

           Type type = typeof(Troy);

           Object obj = Activator.CreateInstance(type);

           MethodInfo[] arrMethod= type.GetMethods();

           foreach (var methodInfo in arrMethod) {

               if (methodInfo.GetParameters().Length == 0)

               {

                   methodInfo.Invoke(obj, null);

               }

           }

           Console.Read();

       }

   }

   public class Troy{

       public string name;

       public Troy() {

           name = "Troy";

       }

       public void Show() {

           Console.WriteLine(name);

       }

   }

复制代码

对于FieldInfo(字段)和PropertyInfo(属性)可以用GetValue和SetValue来获取和设置实例的值,

对于MethodInfo(方法)和ConstructorInfo(构造器)则可以用Invoke来调用,

对于EventInfo(事件)可以用AddEventHandler和RemoveHandler来增加事件回调函数和减少回调函数。

上述方法其实很麻烦,如果用dynamic方法那么就会和一般的写程序一样简单了。

本文由10bet手机官网发布于高并发,转载请注明出处:加载程序集调用的方法说明,插件编程10bet手机官网:

上一篇:设置系统时间,进程间通信之二传递复杂数据类型 下一篇:开发网络防火墙技术分析,Socket编程实现网络封包监视
猜你喜欢
热门排行
精彩图文