通用异常处理框架
异常捕捉、处理是每个项目中必不可少的一部分,利用反射和XML配置技术实现一个通用的、灵活的、可配置的、高度可自扩展的异常处理框架对项目的整体健壮性以及异常处理效率都是非常重要的。通用异常处理框架中需要提供配置信息的支持以及统一的异常处理类和异常日志记录类管理,并允许用户以插件形式扩展自定义的异常处理或日志记录方式。
一、问题的提出
由于异常处理在项目中的普遍性,我们会很自然的想到是否可以对异常处理模块进行提取为公用模块,加强项目间的复用,提高项目的开发进度。并且在异常处理中,因为没有良好的异常处理系统可能造成一些问题:
- 若异常处理方式不当,容易造成比较严重的性能问题。
- 在项目开发阶段,开发人员需要得到完整的异常错误信息方便分析BUG;在项目发布阶段,用户希望看到的是比较友好的错误信息。一个良好的异常处理系统应能够通过简单的配置方便的达到这样的效果
- 异常处理系统应该能够提供足够的信息方便开发人员对BUG异常的准确定位,减少查找BUG产生原因的时间。(该问题在自己的项目中有过深刻教训:因项目中异常信息的不准确,导致一个BUG消耗大量人力时间)
- 一个项目中各层对异常处理的方式不同,可能会不同的层编写不同的代码,造成代码使用麻烦以及复用性低,若需修改,可能改动较大。(例如PDM项目中,除UI层外,都是对异常进行包装后抛给上一层捕捉;而UI层需要处理记录异常,并反馈给用户;即便是UI层也应WebUI和WinUI采用的不同的方式实现,提供给项目的接口也都有一定区别,增加了编码的复杂程度)
- 异常报告信息没有统一管理。很多项目中报告给用户的错误消息,都是程序员在开发中自己编写的,对用户而言,常常不是有效的友好的提示信息。集中管理后可以由BA来进行错误信息的整理,改善用户体验。
- 异常记录方式可能会在不同环境有不同的处理,应该能通过配置文件简单的实现,而不必每次为不同的应用编写代码。
- 要把异常系统做成一个通用的框架,存在一个比较大的挑战:不同的项目可能因项目的整体构架不同,对异常处理方式会有不同的需求,如何保持通用异常处理框架的灵活性的同时实现复用以及可配置性。
二、解决思路
为了最大程度实现异常处理框架的通用性、可扩展性以及可配置性,采用配置文件结合动态加载插件的方式:框架提供接口,由不同项目根据自己的需要实现接口,完成对异常的处理,以及异常日志的记录;而框架再根据配置信息决定异常处理在不同的情况下的处理策略,并通过调用用户实现的接口来完成异常处理过程。
整个异常处理过程对项目调用高度封装,项目中不论任何地方,只需调用一个唯一的接口,框架就会根据配置信息执行需要的异常处理。
三、实践情况
整体结构
框架主要由ExManagement.Config、ExManagement.Interface、ExManagement.MessageHandler以及ExManagement构成。如图:三-1
- ExManagement.Config:用于从配置文件中获取配置信息,并将配置信息转换为一个配置信息实体对象,提供给框架的其他部分使用。
- ExManagement.Interface:提供异常处理类、异常日志记录类以及异常错误报告类的接口和基类,便于用户在框架的基础上进行自定义的扩展
- ExManagement.MessageHandler:对ExManagement.Interface中的IMessage接口实现,提供了WebUI和WinUI下的弹出错误信息提示的支持。也可以直接编写一个满足IMessage接口的包,以自己需要的方式弹出错误提示信息。
- ExManagement:是控制和管理框架按照配置信息进行处理的控制类。首先根据异常处理配置加载需要的异常处理类和日志记录类,再按照配置信息内容对异常进行处理。并提供唯一的接口供项目调用。
- ExManagement.Handler:默认的异常处理包,包含了一种对异常处理的具体实现。用户也可以继承ExManagement.Interface中的ExHandlerBase基类,实现自定义的异常处理类,并通过修改配置文件让框架调用。
- ExManagement.LogHandler:默认日志记录包,包含一种将异常日志记录到数据库的具体实现。用户也可以实现ExManagement.Interface中的ILogHandler接口,实现自定义的异常处理类,并通过修改配置文件让框架调用。
图三-1
ExManagement.Config包
该包主要由两部分组成:实现System.Configuration.IConfigurationSectionHandler接口的ExSectionHandler类和配置信息实体类ExManagerConfig。如图三-2。
ExManagerConfig类中又定义了3个嵌入类:ExHandlerConfig、LogHandlerConfig、LogHandlerConfigCollection以及三个枚举ExAlertType、ErrorCodeSource、ExReturnMode。ExManagerConfig,ExHandlerConfig、LogHandlerConfig分别对应配置文件中的 、 、 节点。ExManagerConfig是一个集合类,从System.Collections.CollectionBase继承,包含若干个ExHandlerConfig对象,并通过索引器访问包含的ExHandlerConfig对象,支持以int和string两种方式索引。ExHandlerConfig中除了包含对应节的属性外,还包含一个LogHandlerConfigCollection对象,是LogHandler类的集合类。
图三-2
ExSectionHandler类从配置文件中按照固定的格式获取到相应的配置信息,并将信息填充到ExManagerConfig。
配置文件
<configuration> <configSections> <section name="ExManager" type="ExManagement.Config.ExSectionHandler, ExManagement.Config" /> configSections>
<ExManager ErrorCodeSource="XML/DB" ConnectionString="" DataTable=""> <ExHandler Name="" Type="" ReturnMode="" AlertType="WebUI/WinUI/None"/> <ExHandler Name="" Type="" ReturnMode="" AlertType=""> <LogHandler Type="" ConnectionString="" DataTable=""/> <LogHandler Type="" ConnectionString="" DataTable=""/> ExHandler> ExManager> configuration> |
首先需要在Web.Config或者App.Config中添加这行
<section name="ExManager" type="ExManagement.Config.ExSectionHandler, ExManagement.Config" /> |
这句是指定用ExManagement.Config.ExSectionHandler类来处理ExManager配置节点
- 节点 包含ErrorCodeSource和ConnectionString两个属性。
- ErrorCodeSource属性 该属性是设置从何种数据源获取错误编号与错误信息的对应关系。其值只能是枚举ErrorCodeSource的值:XML或者DB。
- ConnectionString属性 该属性是设置数据源的位置。若ErrorCodeSource属性设置的是XML,则此处为XML文件的FullName;若ErrorCodeSource属性设置的是DB,则此处为数据库的连接字符串。
- DataTable属性 设置记录异常日志使用的表名。
- 节点 此节点为 节点的子节点,至少得有一个以上的子节点。包含Name、Type、ReturnMode以及AlertType属性。
- Name属性 为该异常处理器确定一个名称。在项目中调用异常处理方法时,需要指定这个名称。一般建议用层的名字或者项目的名字。
- Type属性 指定异常处理器的类,格式为Type=”ClassName, AssemblyName”。该类必须从ExHandlerBase继承。
- ReturnMode属性 异常处理器处理后返回给项目的值类型。对应枚举ExReturnMode的值,只能取Exception/ExceptionString/ErrorCode/ErrorString,分别含义是:抛出异常/返回异常详细描述/返回错误编号/返回错误信息。
- AlertType属性 报告异常信息的处理方式。对应枚举ExAlertType的值,只能取None/WebUI/WinUI。
- < LogHandler>节点 此节点为节点的子节点,节点可以有0到若干个< LogHandler>子节点。
- Type属性 用于指定日志记录处理器的类。格式为Type=”ClassName, AssemblyName”。该类必须实现IExLogHandler接口。
- ConnectionString属性 该属性是设置记录异常日志的数据源位置。
- DataTable属性 设置记录异常日志使用的表名。
ExManagement.Interface包
包含了IExLogHandler和I Message两个接口以及ExHandlerBase基类,可以通过实现这些接口来对框架进行扩展。如图三-3:
图三-3
以下是ExHandlerBase基类中加载所有异常日志对象的方法:
public void LoadLogHandler(ExManagerConfig.ExHandlerConfig config) { m_ExHandlerConfig = config; foreach (ExManagerConfig.LogHandlerConfig logConfig in config.LogConfigColletion) { object exLogHandler = System.Activator.CreateInstance(Type.GetType(logConfig.Type)); lstLogHandler.Add(exLogHandler); } } |
通过遍历配置文件中该节包含的所有子节点,并将配置中指定的LogHandler类反射实例化,将对象存入队列中。
ExManagement.MessageHandler包
该包包含两个IMessage接口的实现,分别完成对异常信息在WinUI和WebUI中的弹出提示功能。WinFormMessage和WebMessage分别引用了System.Web.Dll和System. Windows.Forms.Dll。如图三-4。
图三-4
ExManagement包
包含了ExManager类和ErrorInfo类,是框架中核心的业务流程控制模块。如图三-5
图三-5
ErrorInfo类:根据ErrorId查询对应的ErrorString。在这个类里,会根据在配置文件中节的值去指定的数据源(XML文件或者数据库)查询。
ExManager类,该类是一个单例类,会在第一次实例化时获取配置信息实体对象,并按照配置文件把所有指定的异常处理类实例化后并存入一个哈希表中。以后实例化该类都会重复使用之前实例化的该对象,避免反射造成的性能影响。通过调用该类的ProcessException()方法将捕捉到的异常对象,错误编号,用来处理异常的异常处理器名(建议为该层的名称)传递给框架,并按照参数从哈希表中取出对应的异常处理对象,调用接口对异常进行处理。
ExManagement.Handler包
只包含一个默认的异常处理器类:DefaultExHandler,它从ExHandlerBase基类继承。下面是该类里最重要的ProcessExeception方法。
public override string ProcessExeception(Exception ex, string strUserId, string strErrorCode) { // 记录异常信息 LogEx(ex, strUserId, strErrorCode);
switch(m_ExHandlerConfig.ReturnMode) { // 返回类型为错误编码 case ExManagement.ExReturnMode.ErrorCode: { return strErrorCode; } // 返回类型为详细错误信息(友好提示) case ExManagement.ExReturnMode.ErrorString: { return ExManagement.ExManager.GetErrorInfoByCode(strErrorCode); } // 返回类型为详细异常信息(Exception.Message) case ExManagement.ExReturnMode.ExceptionString: { return ex.Message; } // 将异常向上抛出 case ExManagement.ExReturnMode.Exception: { // 如果是该层自身引发的异常则包装后抛出 if(ex.GetBaseException() == ex) { throw new Exception(string.Format("{0}层发生异常:{1},{2}", m_ExHandlerConfig.Name, strErrorCode, ex.Message), ex); } // 若是捕捉到的是包装后的异常(即上层抛出的) else { throw ex; } } default: { return null; } } } |
在这个默认的异常处理类的ProcessExeception方法中首先调用该类中处理日志记录的方法,再根据配置中的ExReturnMode决定返回处理的结果方式,对异常进行处理。特别是当设置为返回方式为Exception,即抛出异常对象时是先判断该异常是否是最初发生的异常,还是已经处理包装过的异常,避免重复处理异常(不管异常是来自本层或者其他层)。
ExManagement.LogHandler包
该包只有一个默认的DefaultLogHandler类,实现了ILogHandler接口,它负责把异常信息记录到数据库中。
我的项目中使用的各层配置文件(示例)
BusinessLogicLayer
<ExManager ErrorCodeSource="DB" ConnectionString="Data Source=PDMDEV151.ITDEV.ZTE.COM.CN;user id=PDM;password=" DataTable="ErrorInfo"> <ExHandler Name="BusinessLogicLayer" Type="ExManagement.Handler.DefaultExHandler, ExManagement.Handler" ReturnMode="Exception" AlertType="None"/> <LogHandler Type="ExManagement.LogHandler.DefaultLogHandler, ExManagement.LogHandler" ConnectionString="Data Source=PDMDEV151.ITDEV.ZTE.COM.CN;user id=PDM;password=" DataTable ="ExceptionLog"/> ExHandler> ExManager> |
配置意义为:根据ErrorId到数据库ErrorInfo库中获取ErrorString,定义了一个ExHandler,名为BusinessLogicLayer的框架默认的异常处理类,异常返回方式为抛出异常对象,因为不是UI层,所以AlerType为None,该异常处理类用一个默认的LogHandler把异常日志记录到数据库ExceptionLog。
BusinessFacadeLayer
<ExManager ErrorCodeSource="DB" ConnectionString="Data Source=PDMDEV151.ITDEV.ZTE.COM.CN;user id=PDM;password=" DataBase="ErrorInfo"> <ExHandler Name="BusinessFacadeLayer" Type="ExManagement.Handler.DefaultExHandler, ExManagement.Handler" ReturnMode="Exception" AlertType="None"/> <LogHandler Type="ExManagement.LogHandler.DefaultLogHandler, ExManagement.LogHandler" ConnectionString="Data Source=PDMDEV151.ITDEV.ZTE.COM.CN;user id=PDM;password=" DataTable ="ExceptionLog"/> ExHandler> ExManager> |
该层配置除ExHandler的Name不同外,与BusinessFacadeLayer的配置基本一致。
WebUILayer
<ExManager ErrorCodeSource="DB" ConnectionString="Data Source=PDMDEV151.ITDEV.ZTE.COM.CN;user id=PDM;password=" DataBase="ErrorInfo"> <ExHandler Name="WebUILayer" Type="ExManagement.Handler.DefaultExHandler, ExManagement.Handler" ReturnMode="ExceptionString" AlertType="WebUI"/> <LogHandler Type="ExManagement.LogHandler.DefaultLogHandler, ExManagement.LogHandler" ConnectionString="Data Source=PDMDEV151.ITDEV.ZTE.COM.CN;user id=PDM;password=" DataTable ="ExceptionLog"/> ExHandler> ExManager> |
该层的异常处理器的ReturnMode方式为ExceptionString(即Exception.Message),弹出提示方式为WebUI。
WinUILayer
<ExManager ErrorCodeSource="DB" ConnectionString="Data Source=PDMDEV151.ITDEV.ZTE.COM.CN;user id=PDM;password=" DataBase="ErrorInfo"> <ExHandler Name="WinUILayer" Type="ExManagement.Handler.DefaultExHandler, ExManagement.Handler" ReturnMode="ExceptionString" AlertType="WinUI"/> <LogHandler Type="ExManagement.LogHandler.DefaultLogHandler, ExManagement.LogHandler" ConnectionString="Data Source=PDMDEV151.ITDEV.ZTE.COM.CN;user id=PDM;password=" DataTable ="ExceptionLog"/> ExHandler> ExManager> |
该层和WebUILayer的配置除AlertType为WinUI方式外,基本一致。
以上配置为开发调试方式时,若是在发布到测试或正式环境,只需要把WinUILayer和WebUILayer中的ReturnMode属性更改为ErrorString方式,即可让用户看到的是友好的错误信息。
补充:WinUI项目的异常日志记录器可以再增添一个本地异常Log文件方式,当发生异常时,可以根据用户提供的Log文件进行分析。
项目中调用
在写好配置文件之后,项目中引用ExManagement包。调用方法如下,项目中任何地方调用处理方式完全一致
// 返回值根据用户配置而不同,可以为ErrorId, ErrorString以及ExceptionString string strMessage; try { ; } catch(Exception ex) { // 参数ex, 异常对象 // 参数"BLL001", ErrorId, 即错误编号 // 参数"BusinessLogicLayer", ExHandlerName, 即异常处理器名称, 建议于层名称对应 // 参数UserId, 即当前用户Id ProcessExeception(ex, "BLL0001", "BusinessLogicLayer", UserId); } |
四、效果评价
可配置性。通过该异常处理框架可以方便的对异常处理进行需要的配置。可配置的内容:
可以配置多个异常日志记录以不同的方式记录在不同的位置;
异常处理方式可以有多种:抛出包装后异常对象、 返回详细的异常信息(调试用)、返回错误提示信息(发布后给用户看)以及错误编号;
灵活性。建立在可配置性的基础上,可以组合出多种异常处理方案,以满足不同项目的特殊需要。
开放的可扩展性。用户可以自行实现框架提供的接口,自行扩展异常处理以及异常日志记录的类,以插件形式供框架调用,以实现最大可能的灵活性。
性能。因为使用了不少反射技术,在性能上有一定损耗,但使用了单例模式来弥补,只在项目第一出现异常的时候反射加载对象,以后再次调用时则直接使用该对象,对效率基本没有任何损耗了。
而多数情况下,以框架提供的默认解决方案已经能够满足普通项目的需要,提供一个功能比较完整的,健壮的异常处理机制:
1) 方便和简化了开发人员及时定位和发现异常原因;
2) 对系统运行状况提供了强有力的数据支持,并使错误信息统一的方式管理,可以改善用户体验;
3) 当项目在用户使用中发现运行错误时,可以记下系统反馈的异常记录编号后于项目开发人员联系,而开发人员可以根据记录编号得到异常发生的详细信息进行分析。有助于缩短项目异常反馈时间。
五、推广建议
该异常处理框架基本适合所有.Net项目,因为可以灵活的配置以适应不同项目的具体需要。
在一个项目推广中,只需要有一个人比较深入的了解该异常处理框架的原理以及如何进行配置和自定义扩展开发,掌握时间大概只需要半天到一天时间。而项目中其他人员无须知道该框架的运行机制,他们只需要在每个捕捉异常的地方用同样的、唯一的方法调用框架即可。
因为该框架对于项目而言是高度聚合,低耦合的,对于项目而言不需要知道异常究竟会被如何处理,减少对项目的依赖。因此对于现有异常处理系统存在不足以及新项目是应该大力推荐使用该框架进行异常处理的,并且对现有项目的改造工作不大;当然也可以选择对项目已有部分不改动,新开发部分进行使用该框架进行异常处理也是完全可以的。
异常处理框架本身没有做任何自身的异常情况处理,所以在采用框架的时候需要先按照预想的配置在模拟环境中进行调试,确认能够正常运行之后再加入到正式项目中去,避免在正式环境中出现框架本身异常无法判断的情况。
当然因为异常处理的可能方案比较多,该框架的第一个版本可能会有遗漏的可能,但因为框架本身的良好扩展性,多数的特殊情况应该可以用户自行扩展解决。若有无法解决的可以和我联系,对框架本身代码做调整,以求完善该框架。
另外,该异常处理框架若与最近讨论比较热门的AOP(Aspect Oriented Programming面向方面编程)思想结合可以最大程度使系统的业务代码和系统异常处理代码完全分离,并提供更为准确的异常信息。因AOP技术目前在发展阶段,并需要完全的纯OOP项目中实施,暂不对此展开讨论。