0x00 前言
其实做插件相关的程序是我梦寐以求的东西,很早我就开始设想,但是那时候还不会C#,C++又比较晦涩难懂,微软的组件技术还停留在COM+的水平上,自己设计插件系统的学习成本实在太大。所以就一直憋着憋着。现在上手学C#才发现,反射是一个好用的东西,它他适合做组件化程序了。
于是我就开始做了一个简单的Demo。
1 class Program 2 { 3 static string[] _methods; 4 static string _PluginName; 5 static string _PluginScribe; 6 static bool t = true; 7 static int errorcount = 0; 8 static Assembly assemble; 9 static Type Package; 10 static object dll; 11 static object[] obj; 12 /* 13 * 对于FileName变量 14 * 15 * 如果是调试模式下,请直接将它的值设定为测试使用的程序集 16 * 如果是发行模式下,请赋值"" 17 * 18 * 对于count变量 19 * 20 * 如果是调试模式,请输入1 21 * 如果是发行模式,请设置为0 22 * 23 * 对于插件而言,每个插件都必须存在静态变量 24 * Methods PluginName PluginScribe 25 * Methods变量为string[]数组,需要保存当前插件内部所有的方法 26 * PluginName变量解释了当前插件的名称 27 * PluginScribe变量解释了当前插件的作用以及用途 28 */ 29 static void Main(string[] args) 30 { 31 Title = "NtToolPlatform";//设置标题 32 33 string FileName = @"F:\项目\ClassLibrary1\Package\bin\Debug\Package.dll"; 34 int count = 1;//计数器,每存在一个不是文件路径的参数则计数一次 35 36 foreach (string var in args) //遍历参数,寻找程序集的文件路径 37 { 38 if (System.IO.File.Exists(var) == true)//所有参数之中有文件存在并且后缀名为dll时不添加计数器 39 { 40 41 System.IO.FileInfo fi = new System.IO.FileInfo(var); 42 if (fi.Extension == "dll") 43 { 44 FileName = var; 45 } 46 } 47 else 48 { 49 count++; 50 } 51 }//遍历完成,判断是不是所有参数都不携带拓展插件 52 53 if (count == args.Length) 54 { 55 //证明没有拓展插件 56 System.Windows.Forms.MessageBox.Show("运行参数不正确", "错误", 57 System.Windows.Forms.MessageBoxButtons.OK, 58 System.Windows.Forms.MessageBoxIcon.Error); 59 //弹出错误提示框 60 } 61 else 62 { 63 //加载插件 64 assemble = Assembly.LoadFile(FileName); 65 //------------------------------------------- 66 //创建实例 67 //TODO :创建实例并不等于初始化 68 //------------------------------------------- 69 dll = assemble.CreateInstance("Package.Package"); 70 Package = dll.GetType();//获得元数据的类型 71 72 FieldInfo f = Package.GetField("Methods"); 73 FieldInfo d = Package.GetField("PluginName"); 74 FieldInfo scribe = Package.GetField("PluginScribe"); 75 76 obj = new object[2] 77 { 78 new Func<string>(() => { return ReadLine(); }), 79 new Action<string>((msg) =>{ Write(msg); } ) 80 };//将Action<string>委托装箱操作 81 //================================================= 82 //================================================= 83 if (f == null || d == null) 84 { 85 //如果f为null则表示这个插件是非法插件,无法调用 86 System.Windows.Forms.MessageBox.Show("该插件为非法插件无法调用", "错误", 87 System.Windows.Forms.MessageBoxButtons.OK, 88 System.Windows.Forms.MessageBoxIcon.Error); 89 } 90 else 91 { 92 //获得方法列表及相关的文本信息 93 _methods = (string[])f.GetValue(dll); 94 _PluginName = (string)d.GetValue(dll); 95 _PluginScribe = (string)scribe.GetValue(dll); 96 97 Start(); 98 99 while (t == true) 100 { 101 Write("Command\t>"); 102 string msg = ""; 103 104 msg = ReadLine(); 105 106 Check(msg);//判断 107 108 } 109 } 110 } 111 } 112 /// <summary> 113 /// 封装读取文本的命令 114 /// </summary> 115 /// <returns>返回读取到的文本</returns> 116 117 /// <summary> 118 /// 检查输入的文本是否为命令 119 /// </summary> 120 /// <param name="Command">传入输入的文本</param> 121 static void Check(string Command) 122 { 123 /* 124 * [插件名称]? 命令 导出所有方法及其解释文本 125 * [方法名称]? 命令 导出指定方法的参数信息及说明文本 126 * Back 命令 返回上一层 127 * Exit 命令 退出程序 128 */ 129 string s = Command.ToUpper(); 130 if (s == "EXIT") 131 { 132 //退出命令 133 t = false; 134 } 135 else if (Command == string.Empty) 136 { 137 //空白命令 138 Write("Command\t>"); 139 } 140 else if (s == _PluginName.ToUpper() + "?" || s == _PluginName.ToUpper()) 141 { 142 WriteLine(_PluginScribe); 143 } 144 else if (s == "CLS" || s == "CLEAR") 145 { 146 //清屏函数 147 Clear(); 148 } 149 else if (s == "?" || s == "?") 150 { 151 //帮助命令 152 Help(); 153 } 154 else 155 { 156 //判断是不是插件命令 157 MethodInfo mi = null; 158 bool l = false; 159 foreach (string c in _methods) 160 { 161 if (c.ToUpper() == s) 162 { 163 l = true; 164 mi = Package.GetMethod(c); 165 } 166 } 167 if (l == true) 168 { 169 mi.Invoke(dll,obj); 170 } 171 else 172 { 173 //如果是无法识别的命令 174 //添加一次错误计数 175 if (errorcount >= 2) 176 { 177 //如果连续错误大于两次则提示返回上一层菜单或者显示帮助 178 WriteLine("\n‘{0}‘不是命令\n输入?显示帮助", Command); 179 Reset(); 180 } 181 else 182 { 183 errorcount++; 184 WriteLine("‘{0}‘不是命令", Command); 185 } 186 } 187 } 188 } 189 /// <summary> 190 /// 控制台顶部提示文本 191 /// </summary> 192 static void Start() 193 { 194 WriteLine("NtToolPlatForm[版本 V1.0.0]\n作者 Luo (C)保留该作品的所有权利\n查看提示\n"); 195 } 196 /// <summary> 197 /// 清屏函数 198 /// </summary> 199 static void Clear() 200 { 201 Console.Clear(); 202 Start(); 203 errorcount = 0; 204 } 205 /// <summary> 206 /// 错误计数器重设 207 /// </summary> 208 static void Reset() 209 { 210 errorcount = 0; 211 } 212 static void Help() 213 { 214 WriteLine(""); 215 //主体 216 Write("[打开的程序集名称]\tPackage.Package\n[插件名称]\t{0}\n", _PluginName); 217 WriteLine("\t插件名称?\t\t导出所有方法及其解释文本\n"); 218 //平台命令 219 Write("[平台命令]\n"); 220 Write("\tcls\t\t\t清空屏幕缓冲区\n\texit\t\t\t退出命令\n\t?\\tt\t帮助命令\n\tBack\t\t\t返回上一层\n"); 221 WriteLine(""); 222 //插件命令 223 WriteLine("[插件命令]"); 224 WriteLine("\t方法名称?\t\t\t导出指定方法的参数信息及说明文本"); 225 WriteLine("\t方法名称\t\t\t进入对应方法操作界面"); 226 // 227 WriteLine(""); 228 }
这是一个控制台的代码,是实现主体,如果大家想要编译,请记得引用System.Windows.Forms这个命名空间,还要加入引用。因为控制台没有比较强势的提示工具,所以还是用Winform的MessageBox比较好一些。因为涉及外部调用,需要一个正确的打开方式,所以才设计这个。
平台用的是VS2015,所以我using static System.Console了。
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Net; using System.Threading.Tasks; namespace Package { /// <summary> /// Package类中保存着所有方法 /// </summary> public class Package { /// <summary> /// 保存当前插件所有方法 /// </summary> public static string[] Methods = new string[2] { "DnsGetHostAddress", "DnsGetHostName" }; /// <summary> /// 保存当前插件名字 /// </summary> public static string PluginName = "NetTool"; /// <summary> /// 保存插件的说明文本 /// </summary> public static string PluginScribe = "该插件为了方便开发者使用网络编程而设计,能够在非调试环境下查看自己的代码是否会发生错误"; /// <summary> /// 初始化Package类,并注册所有方法 /// </summary> public Package() { } /// <summary> /// 调用Dns.GetHostAddress方法 /// </summary> /// <remarks>对于Action的委托最好传入参数为WriteLine</remarks> public void DnsGetHostAddress(Func<string> Read, Action<string> Write) { Write.Invoke("\nDns.GetHostAddress(string hostNameOrAddress)\n\n请输入代表主机名字或者主机ip的文本:"); //获得所有的IP地址 try { IPAddress[] ip = Dns.GetHostAddresses(Read.Invoke()); Write.Invoke("\n[结果]\n"); foreach (IPAddress var in ip) { Write.Invoke(var.ToString() + "\n"); } } catch (Exception e) { Write.Invoke( "\n[错误代码]\t" + e.HResult.ToString() + "\n[错误提示]\t" + e.Message + "\n"); } //return temp; } /// <summary> /// 获得主机名 /// </summary> /// <param name="Method"></param> public void DnsGetHostName(Action<string> Method) { Method.Invoke(Dns.GetHostName()); } } }
这是插件部分的代码。一开始的实现就是这样子,不过还有很多需要考虑的问题。
0x01思考
对于一个插件系统而言,最重要的就是它们之间的约定,我做这个插件系统,主要想要实现的功能就是导入插件,每个插件具有不一样的方法,然后使用反射动态绑定。当我需要使用某个方法的时候我输入这个方法名称,然后让我设计的插件平台去搜索程序集内部,如果存在该方法则调用。虽然设想比较简单,但是实际操作的时候我发现之中需要很多的共通设计,也就是说,一个外部程序集,必须符合某些约定。这些约定多了就要设计规范。如果你想要做成一个庞大的插件系统的话,那更需要科学的设计。
所以,有些项目很适合锻炼一个人的统筹思维。
这是题外话,这里我们主要讨论四个东西:抽象类,静态类,接口以及单纯的约定,它们是我们主要考虑的主要约定实现方法。
抽象类大部分由虚函数构成,但是要注意,抽象类实际上还是可以实现方法的。例如:
public abstract class NtEngine { public virtual void Load() { } public virtual void Exports() { } public void A() { } } }
但是这样的方法是不能继承的,一般而言,你可以在虚函数里面写代码实现,然后继承的时候Override可以实现重写,也可以进行功能拓展。
这里要强调的主要是抽象类、接口、静态类。
0x02抽象类
抽象类因为不能实例化,所以必须引用一个实体或者被继承,因此抽象类适合作为插件系统的内核。
由这幅图大概就能表示我的想法了,抽象类以一个插件实体为核心,这样就可以找到实现方法的客体了。但是这里就有一个思考了,因为抽象类强调继承,他更希望自己是一个基类,其他类都是他的派生类。使用抽象类构造插件系统,那么这个插件程序集里面就必须继承并实现抽象类主体的方法,我就来搞笑一发吧。
VS里面一个项目只会生成一个DLL,如果你要把插件打包成单独的DLL需要自己新建一个项目,然后将共通的内容逐一导入。
这个思考还是小问题,最主要的是反射的时候,创建实例的时候命名空间的问题,假设你不知道这个插件内部有什么命名空间,这时候你该怎么寻找?抽象类是一个强调单继承的对象,如果不按照规范开发的话,很容易导致插件不能使用(具体是不是我还没试过)