1 引言
在信息系统开发,用户业务功能变化预先不可知,故要提高系统后期的业务扩展。一般情况下用户需求发生变化,要重新编写代码,编译,生产部署包,然后再更新用户程序。这样的过程比较繁琐。
本文讨论生成后的应用系统与外部编译的业务库实现动态绑定,应用程序在运行过程中动态绑定要实现的外部业务,当业务发生变化,也只是替换这些外部的动态库,不用重新对应用程序进行修改和编译,实现了耦合绑定。同时,业务实例对象可以在程序运行时实现实例化,达到了封装效果。并且降低了调用代码和具体实现类代码的耦合,增强灵活性和可复用性,增加了软件的可维护性。
C#提供的反射机制,再结合自适应数据参数的传递,通过这个技术,我们可以将应用框架中的扩展点以插件式程序集的方式来动态加载、构建,从而实现可动态扩展的应用程序。
2 反射机制的基础知识
反射[1][2]是.NET中重要机制,通过反射,可以在运行时获得.NET中每一个类型(包括类、结构、委托、接口和枚举等)的成员,包括方法、属性、事件,以及构造函数等。还可以获得每个成员的名称、限定符和参数等。.NET的应用程序结构分为应用程序域、程序集、模块、类型和成员几个层次,公共语言运行库加载器管理应用程序域,这种管理包括将每个程序集加载到相应的应用程序域以及控制每个程序集中类型层次结构的内存布局。程序集包含模块,而模块包含类型,类型又包含成员,反射则提供了封装程序集、模块和类型的对象。我们可以使用反射动态地创建类型的实例,将类型绑定到现有对象或从现有对象中获取类型,然后调用类型的方法或访问其字段和属性。
反射通常具有以下用途:① 使用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配置文件:
<?xml version="1.0" encoding="utf-8" ?>
<PlugPlatformResource>
<DllFile name="UserLibrary.dll" filepath ="/" objectType ="class">
<classobject name="UserLibrary.UserTest1" >
<Methodobject>testAction01</Methodobject>
<Methodobject>testAction02</Methodobject>
<Methodobject>testAction03</Methodobject>
</classobject>
<classobject name="UserLibrary.UserTest2" >
<Methodobject>testAction01</Methodobject>
</classobject>
</DllFile>
</PlugPlatformResource>
接口对象的配置文件与类对象配置文件基本相同,只不过配置信息中由类换成了接口:
<?xml version="1.0" encoding="utf-8" ?>
<PlugPlatformResource>
<DllFile name="UserLibrary.dll" filepath ="/" objectType ="interface">
<interfaceobject name="InterfaceTest1" implement="UserLibrary.UserTest2" >
<Methodobject>testAction01</Methodobject>
</interfaceobject>
</DllFile>
</PlugPlatformResource>
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