Type t = ass.GetType("Lib1.Class1"); //namespace.classname
object obj1 = Activator.CreateInstance(t); //创建实例
MethodInfo mi = t.GetMethod("say"); //获得方法
object[] parm = new object[1] {"xuetao" };// 构造参数
object ret = mi.Invoke(obj1, parm); //调用方法 反射通常具有以下用途:① 使用Assembly定义和加载程序集,加载在程序集清单中列出的模块,以及从此程序集中查找类型并创建该类型的实例;② 使用Module了解如下的类似信息,如模块的程序集以及模块中的类等:③ 使用CoustructorInfo了解如下的类似信息,如构造函数的名称、参数、访问修饰符(如public或private)和实现详细信息(如abstract或virtua1)等;④ 使用MethodInfo来了解如下的类似信息,如方法的名称、返回类型、参数、访问修饰符(如public或private)和实现详细信息(如abstract或virtua1)等;⑤ 使用FieldInfo来了解如下的类似信息,如字段的名称、访问修饰符(如public或private)和实现详细信息(如static)等,并获取或设置字段值;⑥ 使用EventInfo来了解如下的类似信息,如事件的名称、事件处理程序数据类型、自定义属性、声明类型和反射类型等,并添加或移除事件处理程序:⑦ 使用PropertyInfo来了解如下的类似信息,如属性的名称、数据类型、声明类型、反射类型和只读或可写状态等,并获取或设置属性值;⑧ 使用ParameterInfo来了解如下的类似信息,如参数的名称、数据类型、参数是输入参数还是输出参数,以及参数在方法签名中的位置等。
3.总体设计思路
插件是一种遵循一定规范的应用程序接口编写出来的程序模块。当应用程序已经部署,但业务发生了变化,这样可以通过读取插件配置信息,载入新的应用构件,实现变化的业务。
对于应用系统的框架而言,扩展点是框架中预先定义的一些“点”。 在框架复用中应用构件的组装需要基于扩展点进行。构造性和演化性是软件的两个本质特征,作为一类重要的可复用软件制品。而基于扩展点可以组装不同的应用构 件以适应领域的变化性。则体现了框架对于软件演化特征的支持[3]。
本文涉及到几个概念,插件配置定义,接口定义,方法定义和调用参数定义和返回参数定义。在本插件平台中,配置文件描述插件配置定义,接口定义,方法定义。对于调用参数定义和返回参数定义则采用通用对象和动态对象组[4]来实现传入和返回参数。
插件平台的实现过程如图1所示。当平台运行初始化时,通过读取XML配置信息,装载DLL,通过C#的反射机制分析DLL里的全部实现类和方法。外部构件可以在平台容器中被实例化,并执行插件点的方法。实现的算法不再是编码硬绑定。
图1 PlugPlatform整个过程图
这样,应用程序在运行过程中动态绑定要实现的外部业务,当业务发生变化,也只是替换这些外部的动态库,不用重新对应用程序进行修改和编译,实现了耦合绑定。
4.具体实现
PlugPlatform平台包括四个部分:① 配置文件的获取和解析;② 通用参数和动态参数组处理;③ 插件平台装载DLL并执行外部方法;④ 异常处理。
4.1 配置文件的获取和解析
配置文件以XML Schema为基础,分为两种类型,一种是类配置文件,主要描述关于外部DLL中的类以及方法的内容。第二种配置文件是接口配置文件,主要描述关于外部DLL中的接口以及方法的内容。
类配置文件的XSD如图2所示。
图片看不清楚?请点击这里查看原图(大图)。
图2 类配置文件的schema图
按照此XSD形成的配置XML如图3所示。
图3 类配置文件的XML树
同理可以接口配置文件的XSD内容(如图4)和XML树(如图5)。
图片看不清楚?请点击这里查看原图(大图)。
图4 接口配置文件的schema图
图5 接口配置文件的XML树
在实现将XML树状结构的数据转换为二维MethodObject哈希表,MethodObject哈希表是一key/value的键值对,其中key通常可用来快速查找,value用于存储对应于key的值。MethodObject类数据结构如下:
public class MethodObject {
private ClassObject classobject = null;
private InterfaceObject interfaceobject = null;
private DllFileObject dllfileobject = null;
private string name = string.Empty;
private string simplename = string.Empty;
private string implementname = string.Empty;
public string MethodName {get { return this.name; } set { this.name = value; } }
public string SimpleName { get { return this.simplename; }set { this.simplename = value; } }
public string ImplementName {get { return this.implementname; }set { this.implementname = value; }}
public ClassObject ClassObject {get { return this.classobject; }set { this.classobject = value; }}
public InterfaceObject InterfaceObject {get {return this.interfaceobject; }set {this.interfaceobject = value; }}
public DllFileObject DLLFileObject{ get { return this.dllfileobject; } set { this.dllfileobject = value; }}
}
MethodObject哈希表中key为保证内容的唯一性而采用方法的全名。可以,这样形成的主键可以进行快速查找。value用来存储 MethodObject对象。同时MethodObject对象与ClassObject对象,InterfaceObject对象和DLLFile对 象都是多对一的关系,所以,一旦获得了MethodObject对象,就可以反推出ClassObject对象,InterfaceObject对象和 DLLFile对象。
图6 配置XML转MethodObject哈希表
根据XML Schema可以构建XML文档树,对XML文档树的节点进行分层遍历,然后采用递归算法,依次把XML文档树上最边上的叶子转化为方法对象哈希表,实现方式如图6所示。
4.2通用参数和动态参数组处理
对于外部的方法,要传入参数,同时也获得结果。这些都要用一些通用的数据结构来描述。参数必须可以支持任何类型,是一个通用性的参数。通过创建一个数据的通用类,可以保证支持任何数据类型。
由于传入和传出的参数有多有少,这就要求参数组能实现随意的自动增长和减少。通过设计一个动态自增长的参数数组就可以实现。设计模型如图7表示。
图7 DataValueObject类和DynamicArrayObject类的设计模型
动态参数组的增加数组对象方法如下:
public DynamicArrayObject addObject(DataValueObject obj) {
if (Objects == null) {
Objects = new DataValueObject[1];
Objects[0] = obj;
return this; }
else {
DataValueObject[] objectList = new DataValueObject[Length + 1];
for (int i = 0; i < Length; i++) { objectList[i] = Objects[i]; }
objectList[Length] = obj;
Objects = objectList;
return this;}
}
动态参数组的删除数组对象方法如下:
public DynamicArrayObject deleteObject(int idx) {
if (Objects == null) return this;
else {
if (idx >= 0 && idx < Length && Length > 1) {
DataValueObject[] objectList = new DataValueObject[Length - 1];
for (int i = 0; i < idx; i++) {objectList[i] = Objects[i];}
for (int i = idx + 1; i < Length; i++) {objectList[i - 1] = Objects[i]; }
Objects = objectList;
return this; }
else {
if (idx == 0 && Length <= 1) {
Objects = null;
return this; }
else return this; } }
}
4.3插件平台动态装载DLL并执行
PlugFramework是整个PlugPlatform平台的核心内容。主要实现检查和装载DLL文件,动态创建实例化对象,验证并执行外部方法。
图片看不清楚?请点击这里查看原图(大图)。
图8 PlugPlatform动态装载的全过程
PlugPlatform依据配置文件可以了解装载的外部DLL文件和要求执行的类或接口方法。整个装载和的调用过程如图8说明。
PlugFramework包含有类和接口,这些类和接口之间有继承、实现、关联关系。其类图如图9所示。
图9 PlugPlatform类图
PlugFramework各个类的具体详细描述如下:
序号
名称
实现功能
备注
1
Plus.PlusConfig
插件平台的配置信息类,可创建工厂类
类
2
Plus.PlugFactory
插件平台的工厂类,可以创建方法实现类
类
3
Plus.IAction
方法实现的接口
接口
4
Plus.Framework.PlugAction
方法实现的抽象祖先类
类
5
Plus.Framework.ClassAction
类对象的实现类
类
6
Plus.Framework.InterfaceAction
接口对象的实现类
类
7
Plus.Framework.AssemblyManager
Assembly的管理类,生成Assembly。
类
8
Plus.Framework.TypeManager
Type的管理类,可实现对Type、Object的生成和检查。包括动态方法的调用。
类
动态调用外部方法的核心代码如下所示:
public object InvokeClassMethod(String className, object[] objectArgs, String methodName, object[] methodArgs) {
Object[] newArgs = new Object[methodArgs.Length];
Object thisObject = new Object();
Type type = CreateType(className);
MethodInfo[] methods = type.GetMethods();
Object instance = CreateObject(type, objectArgs);
foreach (MethodInfo m in methods) {
if (m.Name == methodName) {
newArgs = ConvertArgsType(m, methodArgs);
try {
if (!m.IsStatic) thisObject = m.Invoke(instance, newArgs); //非静态方法,使用类实例调用
else thisObject = m.Invoke(null, newArgs);
return thisObject;}
catch (Exception e){throw new PlusException("不能动态调用方法,原因:" + e.Message, e); }}
}
return thisObject;
}
图10表示PlugPlatform实现全过程。下面分别对每个步骤做一个详细描述:
① 外部应用请求动态调用。
② PlusConfig类根据配置文件创建PlugFactory对象。
③ PlugFactory对象创建一个Action对象。
④ Action对象获得MethodObject对象组,逆向产生DllFileObject对象。
⑤ 根据DllFileObject对象中的DLL文件信息,Action对象通过AssemblyManager类获得Assembly对象。
⑥ Action对象使用Assembly对象创建TypeManager对象。
⑦ Action对象传递MethodObject对象给TypeManager对象。
⑧ TypeManager对象可依据MethodObject对象获得ClassObject对象。并使用ClassObject对象信息动态创建一个外部ClassObject对象的实例instance。
⑨ TypeManager对象使用instance和MethodObject对象信息调用instance的动态方法。instance把执行结果返回给Action对象。
⑩ Action对象把执行结果返回给外部应用。
图片看不清楚?请点击这里查看原图(大图)。
图10 PlugPlatform实现的顺序图
其中Plus工厂模式采用了Factory模式。对于Assembly的生成采用了Singleton模式。
4.4 异常处理
由于应用程序中有很多不可预料的问题,本平台在很多地方都有可能出现人为错误,如找不到配置文件;配置文件的格式不对,不能解析配置文件;类或接口名称写 错了,不能实例化类;方法名称写错了,不能执行方法等等。增加异常处理主要是增强其容错性,在这里就不做更多的说明。
5.应用实例
本例子程序主要有三个方面组成:XML配置文件、外部DLL文件和PlusPlatform调用代码。
5.1 XML配置文件
采用的XML配置文件有两个,一个是针对类对象的XML配置文件,一个是针对接口对象的配置文件。
其中类对象的XML配置文件:
接口对象的配置文件与类对象配置文件基本相同,只不过配置信息中由类换成了接口:
5.2 DLL文件内容
其编译的DLL文件为UserLibrary.dll,该dll文件包括两个类和一个接口,其内部代码为: public class UserTest1 {
public DynamicArrayObject testAction01(DynamicArrayObject outObject) {
DynamicArrayObject thisObject = new DynamicArrayObject();
//分解DynamicArrayObject
DataValueObject do1 = null;
string ls = null;
for (int i = 0; i < outObject.Length; i++) {
do1 = outObject.getObject(i);
ls += (String)do1.getDataValue(); }
DataValueObject do2 = new DataValueObject();
do2.setDataType(do1.getDataType()).setDataValue(ls);
//组装DynamicArrayObject,返回DynamicArrayObject
thisObject.addObject(do2);
return thisObject;
}
public DynamicArrayObject testAction02(DynamicArrayObject outObject) {
return outObject; }
}
public class UserTest2 : InterfaceTest1 {
public DynamicArrayObject testAction01(DynamicArrayObject outObject) {
DynamicArrayObject thisObject = new DynamicArrayObject();
//分解DynamicArrayObject
DataValueObject do1 = null;
string ls = null;
for (int i = 0; i < outObject.Length; i++) {
do1 = outObject.getObject(i);
ls += (String)do1.getDataValue(); }
DataValueObject do2 = new DataValueObject();
do2.setDataType(do1.getDataType()).setDataValue(ls);
//组装DynamicArrayObject,返回DynamicArrayObject
thisObject.addObject(do2);
return thisObject;
}
public DynamicArrayObject testAction02(DynamicArrayObject outObject){
return outObject;}
}
public interface InterfaceTest1 {
DynamicArrayObject testAction01(DynamicArrayObject outObject);
}
5.3 调用插件平台代码
调用代码也分为两类,一类是针对类对象处理的,代码如下:
DynamicArrayObject thisObject = new DynamicArrayObject();
DataValueObject do1 = new DataValueObject();
DataValueObject do2 = new DataValueObject();
do1.setDataType("string").setDataValue("类测试:第一个对象值.");
do2.setDataType("string").setDataValue("第二个对象值.");
thisObject.addObject(do1).addObject(do2);
string dllFile = Application.StartupPath + "\DllClassFile.xml";
PlugFactory factory = PlusConfig.BuildFactory(dllFile);
IAction action = factory.CreatAction();
DynamicArrayObject outputObject = action.Execute("UserLibrary.UserTest1.testAction01", thisObject);
另一类是针对接口对象处理,代码如下:
DynamicArrayObject thisObject = new DynamicArrayObject();
DataValueObject do1 = new DataValueObject();
DataValueObject do2 = new DataValueObject();
do1.setDataType("string").setDataValue("接口测试:第一个对象值.");
do2.setDataType("string").setDataValue("第二个对象值.");
thisObject.addObject(do1).addObject(do2);
string dllFile = Application.StartupPath + "\DllInterfaceFile.xml";
PlugFactory factory = PlusConfig.BuildFactory(dllFile);
IAction action = factory.CreatAction();
DynamicArrayObject outputObject = action.Execute("InterfaceTest1.testAction01", thisObject);
可以对返回的DynamicArrayObject做分解查看,满足设计要求。
6.结束语
反射机制结合动态数组很好地解决了应用软件的后期维护和升级。对于应用软件的变化,可不改动任何现有的程序,只要修改XML配置文件的相应对象名称和加载 新的对象即可,程序不需要任何的重编、重启和硬性改动,并保证了原应用系统的可复用性从而实现降低耦合度,实现复用的目标。
本模型在层与层之间借助类调用和接口实现,利用反射机制把调用者与实现者在编译期分离。运行期通过读配置文件动态加载实现类,并通过接口将实现 者强制转型,使其为调用者所用,完成调用者和实现者的解耦。但是,这个功能并不是完全完善,对于插件平台也有很多的改进性,如果能对类和接口配置文件更加 丰富,把插件平台升级为一个框架容器,该容器能把对象之间的依赖关系先行剥离,然后在适当时候由容器负责产生具体的实例再注射到调用者中,即控制权由应用 代码中转到了外部容器,控制权发生了转移。即所谓的控制反转模式,这种模式在java中已经有比较成熟的框架,如Spring等。相信凭借 Microsoft.Net庞大的技术框架平台,在C#上也会有这样的控制反转框架出现。
参考文献
[1](美)Karli Watson Christian Nagel等.康博,译.C#入门经典.北京:清华大学出版社,2006.
[2] MSDN Library .NET Framework开发员指南:在运行时了解类型信息.2003.
[3] 刘瑜 张世琨 王立福 杨芙清. 基于构件的软件框架与角色扩展形态研究. 软件学报,2004.14(8):1364-1370
[4] 段春笋 杜立新. C#动态数组设计原理. 电脑编程技巧与维护,2005.(7):24-25
[5] 何文海 谢建刚. 基于.NET平台的插件式应用框架开发. 电脑知识与技术:学术交流,2007.(8):755-756
[6] 冷山述 陆倜 武装. 用C#构造可复用软件体系结构. 航空计算技术,2003.(4):88-90,93
[7] 殷凯 谢文威. 在工厂方法模式中.NET反射技术应用的研究. 常州工学院学报,2006.(4):28-34
[8] 姚明 李家兰. 基于.NET的通用软件开发平台的研究与实现. 电脑知识与技术:学术交流,2007.(8):797-798