MES项目简单总结(技术篇)—— “热替换”及客制化支持的实现(动态加载/卸载程序集)
接上篇:MES项目简单总结(技术篇)
开篇说明
MES产品框架采用WCF通信,服务端Service的更新 以及 客制化方案都以dll的方式实现,并且对Service的更新要在服务Runing的情况下进行(即“热替换”)。
采用动态加载/卸载dll的方式实现。
动态加载dll的两种方式
C#中实现动态加载dll有两种方式:Assembly和AppDomain。
1、 Assembly
如果直接使用Assembly.LoadFrom(fileName)的方式加载,在主程序运行过程中,无法更新dll文件。
所以,一般的做法时,在加载的时候,先加载到内存中,然后再从内存中加载。
示例代码:
1 byte[] bytes = File.ReadAllBytes("dll全路径"); 2 3 Assembly assembly = Assembly.Load(bytes); 4 5 类型名称 instance = (类型名称)assembly.CreateInstance("类型全名称");
2、 AppDomain
关于AppDomain的使用,可以参考MSDN:使用应用程序域
动态替换程序集的方法主要使用AppDomain的影像复制功能,可以通过设置AppDomainSetup的两个属性实现 :ShadowCopyFiles和ShadowCopyDirectories。参考MSDN: 影像复制
示例代码:
1 AppDomainSetup setup = new AppDomainSetup(); 2 setup.ApplicationName = "ApplicationLoader"; 3 setup.ApplicationBase = AppDomain.CurrentDomain.BaseDirectory; 4 setup.PrivateBinPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "private"); 5 setup.CachePath = setup.ApplicationBase; 6 setup.ShadowCopyFiles = "true"; //启用影像复制程序集 7 setup.ShadowCopyDirectories = setup.ApplicationBase; 8 AppDomain.CurrentDomain.SetShadowCopyFiles();
在线更新(热替换)
这里采用 AppDomain的方式。
1、建立一个调用相关信息类InvokerInfo,用来保存调用的相关信息。
代码:
1 public class InvokerInfo 2 { 3 //待调用的服务类型 4 /// <summary> 5 /// 待调用的服务类型 6 /// </summary> 7 public string TypeName { get; set; } 8 9 //服务所在应用程序域 10 /// <summary> 11 /// 服务所在应用程序域 12 /// </summary> 13 public AppDomain Domain { get; set; } 14 15 //服务调用器 16 /// <summary> 17 /// 服务调用器 18 /// </summary> 19 public MethodInvoker Invoker { get; set; } 20 21 //dll文件最后修改时间 22 /// <summary> 23 /// dll文件最后修改时间 24 /// </summary> 25 public DateTime LastWriteTime { get; set; } 26 27 //引用数 28 /// <summary> 29 /// 引用数 30 /// </summary> 31 public int Ref { get; set; } 32 }
LastWriteTime:影像复制的文件夹中dll的最后写入时间,用来判断是否更新,以便刷新缓存;
Ref:每次调用前Ref++,调用后Ref--。在卸载dll的时候,判断Ref是否等于0。如果Ref大于0,则不可以卸载。
2、建立方法调用器MethodInvoker,负责缓存指定dll中的指定类型中的所有方法,以及对方法的调用。该类需要继承自:MarshalByRefObject以实现跨程序域的操作。
代码:
1 //方法调用器 2 /// <summary> 3 /// 方法调用器 4 /// </summary> 5 public class MethodInvoker : MarshalByRefObject,IDisposable 6 { 7 //程序集名称 8 private readonly string _DllName; 9 //程序集中的类类型全名 10 private readonly string _TypeName; 11 //方法信息缓存列表 12 private readonly Dictionary<string, MethodInfo> _Methods = 13 new Dictionary<string, MethodInfo>(); 14 //程序集中的类类型实例 15 private object _TypeInstance; 16 17 //构造方法 18 /// <summary> 19 /// 构造方法 20 /// </summary> 21 /// <param name="dllName"></param> 22 /// <param name="typeName"></param> 23 public MethodInvoker(string dllName, string typeName) 24 { 25 this._DllName = dllName; 26 this._TypeName = typeName; 27 } 28 29 //加载程序集中的所有方法 30 /// <summary> 31 /// 加载程序集中的所有方法 32 /// </summary> 33 public void LoadAllMethods() 34 { 35 Assembly assembly = Assembly.LoadFrom(_DllName); 36 if (assembly == null) 37 throw new Exception("Can't find " + _DllName); 38 Type tp = assembly.GetType(_TypeName); 39 if (tp == null) 40 throw new Exception("Can't get type " + _TypeName + " from " + _DllName); 41 _TypeInstance = Activator.CreateInstance(tp); 42 if (_TypeInstance == null) 43 throw new Exception("Can't construct type " + _TypeName + " from " + _DllName); 44 45 MethodInfo[] typeMethod; 46 47 if (_Methods.Count == 0) 48 { 49 typeMethod = tp.GetMethods(BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.Instance); 50 51 for (int i = 0; i < typeMethod.Length; i++) 52 { 53 if (typeMethod[i].DeclaringType != typeof(object)) 54 { 55 MethodInfo method; 56 if (!_Methods.TryGetValue(typeMethod[i].Name, out method)) 57 _Methods.Add(typeMethod[i].Name, typeMethod[i]); 58 } 59 } 60 } 61 } 62 63 //调用程序集中的指定方法 64 /// <summary> 65 /// 调用程序集中的指定方法 66 /// </summary> 67 /// <param name="methodName">方法名称</param> 68 /// <param name="methodParams">参数数组</param> 69 /// <returns></returns> 70 public object InvokeMethod(string methodName, object[] methodParams) 71 { 72 MethodInfo method; 73 if (string.IsNullOrEmpty(methodName)) 74 throw new Exception("Method Name IsNullOrEmpty"); 75 76 _Methods.TryGetValue(methodName, out method); 77 78 if (method == null) 79 throw new Exception("Method can not be found"); 80 81 object result = method.Invoke(_TypeInstance, methodParams); 82 //这里可以对result进行包装 83 return result; 84 } 85 86 87 #region IDisposable 成员 88 89 public void Dispose() 90 { 91 _TypeInstance = null; 92 GC.Collect(); 93 GC.WaitForPendingFinalizers(); 94 GC.Collect(0); 95 } 96 #endregion 97 }
3、建立程序集加载器AssemblyLoader,负责所有dll的加载/卸载,并负责这些dll的缓存及刷新。
代码:
1 //程序集加载器 2 /// <summary> 3 /// 程序集加载器 4 /// </summary> 5 public class AssemblyLoader : IDisposable 6 { 7 private readonly object _lockThis = new object(); 8 private Dictionary<string, Queue<InvokerInfo>> _caches = 9 new Dictionary<string, Queue<InvokerInfo>>(); 10 11 //加载所有可用的程序集 12 /// <summary> 13 /// 加载所有可用的程序集 14 /// </summary> 15 /// <param name="dlls"></param> 16 public void LoadAssemblys(Dictionary<string, string> dlls) 17 { 18 foreach (KeyValuePair<string, string> kvp in dlls) 19 LoadAssembly(kvp.Key, kvp.Value); 20 } 21 //卸载程序集 22 /// <summary> 23 /// 卸载程序集 24 /// </summary> 25 /// <param name="dllName"></param> 26 public void Unload(string dllName) 27 { 28 InvokerInfo info = _caches[dllName].Dequeue(); 29 AppDomain.Unload(info.Domain); 30 } 31 //调用指定程序集中的指定方法 32 /// <summary> 33 /// 调用指定程序集中指定的方法 34 /// </summary> 35 /// <param name="dllName">程序集名称</param> 36 /// <param name="methodName">方法名称</param> 37 /// <param name="methodParams">参数数组</param> 38 /// <returns></returns> 39 public object InvokeMethod(string dllName, string methodName, object[] methodParams) 40 { 41 object result; 42 InvokerInfo info; 43 lock (_lockThis) 44 { 45 info = GetInvoker(dllName); 46 info.Ref++; 47 } 48 49 result = info.Invoker.InvokeMethod(methodName, methodParams); 50 51 lock (_lockThis) 52 { 53 info.Ref--; 54 TryToUnLoad(dllName, info); 55 } 56 57 return result; 58 } 59 60 //加载指定的程序集 61 /// <summary> 62 /// 加载指定的程序集 63 /// </summary> 64 /// <param name="dllName"></param> 65 /// <param name="typeName"></param> 66 private void LoadAssembly(string dllName, string typeName) 67 { 68 //Get object from cache 69 Queue<InvokerInfo> result; 70 _caches.TryGetValue(dllName, out result); 71 72 if (result == null || result.Count == 0) 73 { 74 //Get TimeStamp of file 75 FileInfo info = 76 new FileInfo( 77 AppDomain.CurrentDomain.BaseDirectory + @"ServiceDlls\" + dllName); 78 79 if (!info.Exists) 80 throw new Exception(AppDomain.CurrentDomain.BaseDirectory 81 + @"ServiceDlls\" + dllName + " not exist"); 82 83 CacheMethodInvoker(dllName, typeName, info.LastWriteTime); 84 } 85 } 86 //缓存指定的方法调用信息 87 /// <summary> 88 /// 缓存指定的方法调用信息 89 /// </summary> 90 /// <param name="dllName"></param> 91 /// <param name="typeName"></param> 92 /// <param name="lastWriteTime"></param> 93 /// <returns></returns> 94 private InvokerInfo CacheMethodInvoker(string dllName, string typeName, DateTime lastWriteTime) 95 { 96 MethodInvoker invoker; 97 98 var invokerInfo = new InvokerInfo(); 99 100 var setup = new AppDomainSetup 101 { 102 ShadowCopyFiles = "true", 103 ShadowCopyDirectories = AppDomain.CurrentDomain.BaseDirectory + @"ServiceDlls\", 104 ConfigurationFile = "DynamicLoadAssembly.exe.config", 105 ApplicationBase = AppDomain.CurrentDomain.BaseDirectory 106 }; 107 108 AppDomain domain = AppDomain.CreateDomain(dllName, null, setup); 109 110 domain.DoCallBack(delegate { LifetimeServices.LeaseTime = TimeSpan.Zero; }); 111 112 invokerInfo.Domain = domain; 113 invokerInfo.LastWriteTime = lastWriteTime; 114 invokerInfo.TypeName = typeName; 115 116 BindingFlags bindings = 117 BindingFlags.CreateInstance | BindingFlags.Instance | BindingFlags.Public; 118 object[] para = new object[] { setup.ShadowCopyDirectories + @"\" + dllName, typeName }; 119 try 120 { 121 invoker = (MethodInvoker)domain.CreateInstanceFromAndUnwrap( 122 Assembly.GetExecutingAssembly().CodeBase.Substring(8), 123 typeof(MethodInvoker).FullName, 124 true, bindings, null, para, null, 125 null, null); 126 } 127 catch (Exception ex) 128 { 129 throw new Exception( 130 "Can't create object which type is " + typeof(MethodInvoker).FullName + " from assembly " + 131 Assembly.GetExecutingAssembly().CodeBase + ",Error Message: " + ex.Message); 132 } 133 134 if (invoker == null) 135 throw new Exception( 136 "Can't find type " + typeof(MethodInvoker).FullName + " from " + 137 Assembly.GetExecutingAssembly().CodeBase); 138 139 try 140 { 141 invoker.LoadAllMethods(); 142 } 143 catch (Exception ex) 144 { 145 throw new Exception("Can't initialize object which type is " + typeof(MethodInvoker).FullName + 146 " from " + 147 Assembly.GetExecutingAssembly().CodeBase + ",Error Message: " + ex.Message); 148 } 149 150 invokerInfo.Invoker = invoker; 151 invokerInfo.Ref = 0; 152 153 if (_caches.Keys.Contains(dllName)) 154 { 155 _caches[dllName].Enqueue(invokerInfo); 156 } 157 else 158 { 159 Queue<InvokerInfo> queue = new Queue<InvokerInfo>(); 160 queue.Enqueue(invokerInfo); 161 _caches[dllName] = queue; 162 } 163 164 return invokerInfo; 165 } 166 //尝试卸载程序集 167 /// <summary> 168 /// 尝试卸载程序集 169 /// </summary> 170 /// <param name="dllName"></param> 171 /// <param name="currentInfo"></param> 172 private void TryToUnLoad(string dllName, InvokerInfo currentInfo) 173 { 174 InvokerInfo info = _caches[dllName].Peek(); 175 176 if (info == currentInfo) 177 return; 178 179 if (info.Ref == 0) 180 { 181 Unload(dllName); 182 } 183 } 184 //获取指定程序集的调用信息 185 /// <summary> 186 /// 获取指定程序集的调用信息 187 /// </summary> 188 /// <param name="dllName"></param> 189 /// <returns></returns> 190 private InvokerInfo GetInvoker(string dllName) 191 { 192 //Get object from cache 193 Queue<InvokerInfo> result; 194 _caches.TryGetValue(dllName, out result); 195 196 if (result == null) 197 { 198 throw new Exception(dllName + " not loaded"); 199 } 200 201 //Get TimeStamp of file 202 FileInfo info = 203 new FileInfo(AppDomain.CurrentDomain.BaseDirectory + @"ServiceDlls\" + dllName); 204 205 if (!info.Exists) 206 { 207 return result.ToArray()[result.Count - 1]; 208 } 209 210 if (info.LastWriteTime > result.ToArray()[result.Count - 1].LastWriteTime) 211 { 212 return CacheMethodInvoker(dllName, result.Peek().TypeName, info.LastWriteTime); 213 } 214 215 return result.ToArray()[result.Count - 1]; 216 } 217 218 #region IDisposable 成员 219 220 public void Dispose() 221 { 222 _caches.Clear(); 223 224 foreach (var o in _caches.Keys) 225 { 226 Unload(o); 227 } 228 } 229 230 #endregion 231 }
4、使用说明
a) 在主应用程序域通过单态模式定义一个AssemblyLoader,并通过该AssemblyLoader的LoadAssemblys()方法加载所有dll
b) 在LoadAssemblys()方法内部会调用LoadAssembly()方法逐个加载,真正使用AppDomain加载dll的方法是 CacheMethodInvoker()。
c) 在CacheMethodInvoker()方法中,声明AppDomainSetup,并设置其属性:
ShadowCopyFiles = "true";
ShadowCopyDirectories = AppDomain.CurrentDomain.BaseDirectory + @"ServiceDlls\"; //注:文件夹路径要以反斜杠结束
并使用AppDomain的CreateInstanceFromAndUnwrap方法创建MethodInvoker实例,并缓存下来
d) 外部通过AssemblyLoader中的InvokeMethod()方法进行调用。InvokeMethod()方法中会通过GetInvoker()方法获取一个InvokerInfo信息,首先会从缓存中获取,获取之后,比较其LastWriteTime和物理位置中的dll文件的LastWriteTime,如果有更新,就通过CacheMethodInvoker()方法刷新缓存,并返回,对新获取的InvokerInfo的Ref++;然后调用InvokerInfo中的MethodInvoker实例的InvokeMethod方法进行实际的方法调用,完成之后Ref--,并通过TryUnload()方法尝试卸载。
客制化支持
在以上方案的基础上实现客制化的支持。
1、定义请求类,分为基本请求类和需要流程支持的请求类:BaseRequest和WorkFlowRequest,WorkFlowRequest从BaseRequest继承。
2、 针对请求类,定义响应类,同样分为基本响应类和需要流程支持的响应类:BaseResponse和WorkFlowResponse。
3、根据模块定义不同的AssemblyLoader容器类,比如:WIPService。
4、在WIPService类中定义对两种请求进行处理的方法:
public static BaseResponse ExecuteBaseRequest(BaseRequest)
public static WorkFlowResponse ExecuteWorkFlowRequest(WorkFlowRequest)
5、在WCF的服务启动时加载指定名称的可以用来客制化的dll
6、客制化的dll中,通过执行WIPService.ExecuteBaseRequest()和WIPService.ExecuteWorkFlowRequest()方法完成操作。
最后,和本篇主题无关的是,目前的架构中,使用了NHibernate的Session来保证事务,即只支持单一数据库,且客制化代码中对数据库的操作也要使用框架中提供的数据操作方法进行操作。
测试源码下载
点击-->下载源码