(一)插件技术原理及其特性 插件是一种遵循统一的预定义接口规范编写出来的程序,应用程序在运行时通过接口规范对插件进行调用,以扩展应用程序的功 能。插件在英文中通常称为plug-in、plugin或者plug in。插件最典型的例子是Microsoft的ActiveX控件和COM(Component Object Model,部件对象模型)实际上ActiveX控件不过是一个更高继承层次的COM而已。此外还有Photoshop的滤镜(Filter)也是一种比 较常见的插件。 关于ActiveX和COM 在Microsoft的.Net Framework 推出的之前(大约是2003年之前吧),ActiveX和COM可是炙手可热的技术啊!在那个年代,一个顶尖的VC++高手的标志是什么?是会COM编 程!不知道IUnknown接口和QueryInterface函数,你怎么可能通过Microsoft的MCSD认证考试? 现在当然不同了,我曾经见过不少断言COM和ActiveX已经消亡或终将消亡的文章。但是不管怎么说,个人认为,ActiveX和COM代表了插件技术的最高境界,通过对ActiveX和COM的研究,我们可以对插件有更深刻的认识。 关于ActiveX控件和COM技术的详细介绍,有兴趣的朋友不妨去“百度一下”,相信能够获得很多相关信息的。 插件技术过时了吗? COM技术的逐渐淡出,使不少程序员产生了困惑:插件技术已经过时了吗? NO!至少我不这样认为!毕竟,没有了插件技术,我们还有什么更好的方法为应用程序提供运行时的功能扩展呢? COM的没落自然有其原因,例如编程实在是太复杂而难以掌握,还有就是在这个病毒和木马肆虐的年代,其安全性也令人堪忧。但至少我们可以看到,插件技术的成功应用还是有的:比如PhotoShop的滤镜,比如各大主流工控软件的功能扩展。 对于插件的理解,我们应该注意以下几点: 一、插件是遵循统一的预定义接口规范编写的。 下面我以微软的ActiveX Control Test Container为大家解释一下插件的接口定义。 1、打开VS.NET 2005; 2、点击主菜单的“工具”菜单; 3、如果在“工具”菜单中未找到“Activex Test Container”,则点击“工具”-〉“外部工具”,在“外部工具”对话框的“菜单内容”列表框中,选中“Activex Test Container",然后点击右上角的"添加“按钮,再点击左下角的”确定按钮“,此步骤将在在“工具”菜单中添加“Activex Test Container”菜单项; 4、点击主菜单的“工具->Activex Test Container”菜单项,运行”Activex Test Container" ; 5、在“Activex Test Container”程序中点击“编辑->插入新控件”菜单; 6、在“插入控件”对话框中找到“日历控件 11.0" 控件,点击确定; 7、点击“控件”-〉“调用”方法,弹出“调用方法”对话框,按下图所示的方法将日历控件的背景色设置为红色; 在这里,“BackColor”是一个预定义的通用的Activex控件接口函数,用于设置控件的背景颜色。 二、应用程序是在运行时调用插件以实现功能扩展的 插件最吸引人的地方当然就是其所实现“运行时(run-time)"功能扩展。这意味着软件开发者可以通过公布插件的预定义接口规范,从而允许第三方的软件开发者通过开发插件对软件的功能进行扩展,而无需对整个程序代码进行重新编译。 运行时(run-time)是相对于编译时(assembly-time)而言的。一般来说,软件开发者对软件功能更新时,是在源代码级别进行更新,然后 对整个程序或部分动态链接库(DLL)进行重新编译,进而发布应用程序的新版本,这就是编译时(assembly-time)的软件更新。 三、插件技术的优缺点 运行时的软件功能扩展其优点是显而易见的: 1、对软件的开发者而言,只需对主程序和某些常用插件进行更新和维护,然后通过公布插件接口吸引第三方的软件开发者对主程序的功能进行扩展,这是一种“我为人人,人人为我”的双赢策略; 2、对最终用户而言,可以通过有选择地购买第三方提供的插件实现自己所需要的功能,从而实现最佳性价比组合,以节省不必要的开支。 但是,运行时的软件功能扩展也有其弊端: 1、为实现运行时的软件扩展,程序开发者必须编写更多、更复杂的代码,从而会对程序的执行效率产生一定的影响。关于这一点,我会在第二讲中详细论述; 2、由于插件是在运行时加载的,因此第三方插件可能对用户造成危害。这种危害通常可以分为两类: (1)由于插件开发者的技术水平原因导致的插件BUG,这种BUG可能导致内存泄露、死机、数据丢失等等故障,从而影响到用户对软件的使用; (2)插件开发者恶意开发类似于病毒和木马的插件,窃取或毁坏用户数据,使用户遭受不必要的损失; 为了避免此类缺点,软件开发者可能需要付出额外的代价,如需要对第三方插件进行检验和认证,或者干脆不对外提供插件开发接口,仅由自己提供插件。 (二)用C#编写插件 为了使大家对插件有更深入的了解,让我们先重温一下通常情况下创建和调用DLL的过程。 每一个程序员都知道,我们应当将某些类或者模块编译为DLL,然后在主程序中调用,关于这样做的目的和好处,我就不再啰嗦了。 假设我创建了一个名为TirayComm.dll的类库,并编写了一个UDP类用于UDP数据传输: namespace Tiray.Net { public class UDP { ... //port--本地侦听端口 //ttl时间,以毫秒为单位 public UDP(int port,short ttl) { ... } //初始化 public void Init() { ... } //发送数据 //data--待发送数据 //remoteIP--远端IP地址 //port--远端UDP端口 public void Send(byte[] data,string remoteIP,int port) { ... } //关闭 public void Close() { ... } } } 我在主程序中创建UDP类的实例和调用UDP类方法的代码如下: using Tiray.Net ... Tiray.Net.UDP udp=new UDP(25000,2000); udp.Int(); ... 别忘了要先在项目中添加对TirayComm.dll的引用。 我先提醒大家一下,上面的步骤都是在你的IDE中完成的,也就是说是在你编写程序代码的过程中完成的。 下面我用我以前开发的一个软件项目为大家详细解释一下用C#实现插件的一些技术细节。 我曾经接到过一个SP的软件项目,为一家电台开发一个听众短信互动平台。在需求分析过程中,我意识到电台的短信互动平台需要非常灵活的功能扩展,因为一个 电台往往有好几个频率,每个频率平均每个小时都是一个不同的节目,每一个节目对短信平台的要求都可能不同:有的节目仅仅要求短信平台将听众的短信显示给主 持人;有的节目要求短信平台能够自动回复某些信息;有的节目如短信答题等要求短信平台能自动判断用户的答案是否正确,并能提供抽奖功能...... 而且最糟糕的是即使同一个节目,其内容也随时有可能更换或改版,然后对短信平台提出新的要求。 这时候我开始有了编写插件的想法。我希望当主持人提出一个新的短信应用要求时,我只需要编写一个插件来实现相关的应用逻辑,然后将插件安装到短信平台的特 定目录下,就可以实现相应的功能,而无须对整个短信平台进行升级。同样,当主持人不再需要某个短信应用的时候,我只须简单地从特定目录中将相应的插件删除 即可。 下面是我编写的插件的基类,请注意这是一个抽象类。从插件的意义说,其中的两个公共抽象方法和一个事件就是插件的接口规范定义。我没有用 interface关键字来定义插件的接口,是因为我还有一些与短信网关有关的代码需要在这个类中实现,而且我也不想考虑在插件中实现多重继承的问题。实 际上,也可以使用interface关键字来定义插件的接口。关于抽象类和接口的有关内容,大家可以到MSDN里寻找,我就不多说了。 namespace Tiray.SMS { public abstract class Plugin { //插件的名称 protected String pluginName; ... //OnReceive是接收到中国移动短信网关类CMPP30和中国联通短信网关类SGIP短信后的事件处理函数 //destNumber--SP端的号码,如1066123456 //phoneNumber--手机号码,如13812345678 //Message--短消息 public abstract void OnReceive(String destNumber,String phoneNumber,String message); //显示插件属性,用户可以调用此方方法察看和设置插件属性 public abstract void ShowProperty(); ... //从内存中销毁插件实例时的处理函数。 public abstract void Finalize(); ... //插件的事件 //通常是一个发送短消息请求 public event PluginEventHandler PluginEvent; } //插件事件定义 public delegate void PluginEventHandler(object sender, PluginEventArgs e); } 下面是一个为交通类节目写的交通违法查询的短信插件实现。这个插件的作用是当用户发送一个车牌号到短信互动平台时,可以返回该车牌号在一段时间内的交通违法记录。这个插件被编译为一个名为Illegal.dll的类库,然后发布到主程序的“Plugin”子目录下。 using Tiray.SMS; namespace Tiray.SMS.RadioService { //交通违法查询短信插件 public class IllegalQuery:Plugin { ... public IllegalQuery() { ... pluginName="交通违法行为查询"; ... } public override void ShowProperty() { ... } public override void Finalize() { ... } public override void OnReceive(String destNumber,String phoneNumber,String message) { ... //对接收到的车牌号进行检验和处理 string carNumber=CheckCarNumber(string message); if(carNumber!=string.Empty) { ... //查询违法记录并发送到用户手机 QueryAndSend(carNumber,phoneNumber); ... } ... } } } 从上面的代码来看,我只是实现了一个很普通的类继承,似乎看不出插件有什么与众不同的地方。下面来看一看在主程序中是如何调用插件的。 ... using Tiray.SMS; using System.Reflection; ... ... protected Hashtable n_htPlugin=null; ... protected void InitPlugin() { m_htPlugin=new Hashtable(); string dir=Directory.GetCurrentDirectory()+@"/plugin"; string[] files=Directory.GetFiles(strDir,"*.DLL"); foreach(string file in files) { Tiray.SMS.Plugin plugin=null; try { System.Reflection.Assembly asm=System.Reflection.Assembly.LoadFile(files); Type[] types=asm.GetTypes(); foreach(Type type in types) { if(type.BaseType.FullName=="Tiray.SMS.Plugin") { plugin=asm.CreateInstance(type.FullName) as Plugin; break; } } } catch(Exception ex) { ... plugin=null; ... } if(plugin!=null) { plugin.PluginEvent+=new PluginEventHandler(OnPlugin); m_htPlugin.Add(plugin.Name,plugin); } } } //来自插件的消息 //通常是一个发送短消息请求 protected void OnPlugin(object sender,PluginEventArgs e) { ... } } 请注意插件实例是使用Sytem.Reflection.Assembly类的CreateInstance方法 创建的。对比前面我提到过的UDP类的实例创建方法,我并没有在主程序中添加对Illegal.DLL的引用,程序中也没有直接对 IllegalQuery类进行声明的代码。 在上一讲中,我提到过,插件的一个优点就是运行时的功能扩展,通过上面的代码,大家应该对插件的这一特性有一些了解了吧。 上面的只是一个很简单的例子,但是大家也已经看到,为了实现插件,我不得不在主程序中添加了更多的代码,如我创建了一个Hashtable用于在内存中保 存每一个插件实例;我使用了比常规的类实例创建方法更多的代码来创建插件实例;虽然我没有列出插件事件处理函数的代码,但是可以想象,我一定要使用更多的 代码来判断当前的事件是由哪一个插件引发的。实际上,插件的实现还需要考虑一些更复杂的问题,在第三讲中,我会对插件进行更深入的探讨。 现在,让我们回顾一下插件的两个基本特性及实现方法: 统一的调用接口 这是用abstract关键字定义一个抽象类或者用interface关键字定义一个接口来实现的; 运行时加载 这是使用System.Reflection.Assembly类的相关方法来实现的。 |