对于企业应用的开发者来说,异常处理是一件既简单又复杂的事情。说其简单,是因为相关的编程无外乎try/catch/finally+throw而已;说其复杂,是因为我们往往很难按照我们真正需要的策略来处理异常。我一直有这样的想法,理想的企业应用开发中应该尽量让框架来完成对异常的处理,最终的开发人员在大部分的情况下无需编写异常处理相关的任何代码。在这篇文章中我们将提供一个解决方案来让ASP.NET应用利用EntLib的异常处理模块来实现自动化的异常处理。

源代码: 
Sample1[通过重写Page的OnLoad和OnRaisePostBackEvent方法] 
Sample2[通过自动封装注册的EventHandler]

一、EntLib的异常处理方式 
二、实例演示 
三、通过重写Page的OnLoad和RaisePostBackEvent方法实现自动异常处理 
四、IPostBackDataHandler 
五、EventHandlerWraper 
六、对控件注册事件的自动封装 
七、AlertHandler

一、EntLib的异常处理方式

所谓异常,其本意就是超出预期的错误。既然如此,异常处理的策略就不可能一成不变,我们不可能在开发阶段就制定一个完备的异常处理策略来处理未来发生的所有异常。异常处理策略应该是可配置的,能够随时进行动态改变的。就此而言,微软的企业库(以下简称EntLib)的异常处理应用块(Exception Handling Application Block)是一个不错的异常处理框架,它运行我们通过配置文件来定义针对具体异常类型的处理策略。

针对EntLib的异常处理应用块采用非常简单的编程方式,我们只需要按照如下的方式捕捉抛出的异常,并通过调用ExceptionPolicy的HandleException根据指定的异常策略进行处理即可。对于ASP.NET应用来说,我们可以注册HttpApplication的Error事件的形式来进行统一的异常处理。但是在很多情况下,我们往往需要将异常控制在当前页面之内(比如当前页面被正常呈现,并通过执行一段JavaScript探出一个对话框显示错误消息),我们往往需要将下面这段相同的代码结构置于所有控件的注册事件之中。

   1: try
   2: {
   3:     //业务代码
   4: }
   5: catch(Exception ex)
   6: {
   7:     if(ExceptionPolicy.HandleException(ex,"exceptionPolcyName"))
   8:     {
   9:         throw;
  10:     }
  11: }

我个人不太能够容忍完全相同的代码到处出现,代码应该尽可能地重用,而不是重复。接下来我们就来讨论如何采用一些编程上的手段或者技巧来让开发人员无须编写任何的异常处理代码,而抛出的确却能按照我们预先指定的策略被处理。

二、实例演示

为了让读者对“自动化异常处理”有一个直观的认识,我们来做一个简单的实例演示。我们的异常处理策略很简单:如果后台代码抛出异常,异常的相关信息按照预定义的格式通过Alert的方式显示在当前页面中。如下所示的是异常处理策略在配置文件中的定义,该配置中定义了唯一个名为“default”的异常策略,该策略利用自定义的AlertHandler来显示异常信息。配置属性messageTemplate定义了一个模板用于控制显示消息的格式。

   1: <configuration>
   2:   ...
   3:   <exceptionHandling>
   4:     <exceptionPolicies>
   5:       <add name="default">
   6:         <exceptionTypes>
   7:           <add type="System.Exception, mscorlib" 
   8:                   postHandlingAction="None" name="Exception">
   9:             <exceptionHandlers>
  10:               <add name="Alert Handler" type="AutomaticExceptionHandling.AlertHandler, AutomaticExceptionHandling" 
  11:                    messageTemplate="[{ExceptionType}]{Message}"/>
  12:             </exceptionHandlers>
  13:           </add>          
  14:         </exceptionTypes>
  15:       </add>
  16:     </exceptionPolicies>
  17:   </exceptionHandling>     
  18: </configuration>

现在我们定义一个简单的页面来模式自动化异常处理,这个页面是一个用于进行除法预算的计算器。如下所示的该页面的后台代码,可以看出它没有直接继承自Page,而是继承自我们自定义的基类PageBase,所有异常处理的机制就实现在此。Page_Load方法收集以QueryString方式提供的操作数,并转化成整数进行除法预算,最后将运算结果显示在表示结果的文本框中。计算按钮的Click事件处理方法根据用户输入的操作数进行除法运算。两个方法中均没有一句与异常处理相关的代码。

   1: public partial class Default : PageBase
   2: {
   3:     protected void Page_Load(object sender, EventArgs e)
   4:     {
   5:         if (!this.IsPostBack)
   6:         {
   7:             string op1 = Request.QueryString["op1"];
   8:             string op2 = Request.QueryString["op2"];
   9:             if (!string.IsNullOrEmpty(op1) && !string.IsNullOrEmpty(op2))
  10:             {
  11:                 this.txtResult.Text = (int.Parse(op1) / int.Parse(op2)).ToString();
  12:             }
  13:         }
  14:     }
  15:  
  16:     protected void btnCal_Click(object sender, EventArgs e)
  17:     {
  18:         int op1 = int.Parse(this.txtOp1.Text);
  19:         int op2 = int.Parse(this.txtOp2.Text);
  20:         this.txtResult.Text = (op1 / op2).ToString();
  21:     }
  22: }

现在运行我们程序,可以想象如果在表示操作数的文本框中输入一个非整数字符,调用Int32的Parse方法时将会抛出一个FormatException异常,或者将被除数设置为0,则会抛出一个DivideByZeroException异常。如下面的代码片断所示,在这两种情况下相应的错误信息按照我们预定义的格式以Alert的形式显示出来。

三、通过重写Page的OnLoad和RaisePostBackEvent方法实现自动异常处理

我们知道ASP.NET应用中某个页面的后台代码基本上都是注册到页面及其控件的事件处理方法,除了第一次呈现页面的Load事件,其他事件均是通过PostBack的方式出发的。所以我最初的解决方案很直接:就是提供一个PageBase,在重写的OnLoad和RaisePostBackEvent方法中进行异常处理。PageBase的整个定义如下所示:

   1: public abstract class PageBase: Page
   2: {
   3:     public virtual string ExceptionPolicyName { get; set; }
   4:     public PageBase()
   5:     {
   6:         this.ExceptionPolicyName = "default";
   7:     }
   8:  
   9:     protected virtual string GetExceptionPolicyName()
  10:     {
  11:         ExceptionPolicyAttribute attribute = this.GetType().GetCustomAttributes(true)
  12:             .OfType<ExceptionPolicyAttribute>().FirstOrDefault();
  13:         if (null != attribute)
  14:         {
  15:             return attribute.ExceptionPolicyName;
  16:         }
  17:         else
  18:         {
  19:             return this.ExceptionPolicyName;
  20:         }
  21:     }
  22:  
  23:     protected override void OnLoad(EventArgs e)
  24:     {
  25:         this.InvokeAndHandleException(() => base.OnLoad(e));
  26:     }
  27:  
  28:     protected override void RaisePostBackEvent(IPostBackEventHandler sourceControl, string eventArgument)
  29:     {
  30:         this.InvokeAndHandleException(()=>base.RaisePostBackEvent(sourceControl, eventArgument));
  31:     }
  32:  
  33:     private void InvokeAndHandleException(Action action)
  34:     {
  35:         try
  36:         {
  37:             action();
  38:         }
  39:         catch (Exception ex)
  40:         {
  41:             string exceptionPolicyName = this.GetExceptionPolicyName();
  42:             if (ExceptionPolicy.HandleException(ex, exceptionPolicyName))
  43:             {
  44:                 throw;
  45:             }
  46:         }
  47:     }
  48: }

如上面的代码片断所示,在重写的OnLoad和RaisePostBackEvent方法中,我们采用与EntLib异常处理应用块的编程方式调用基类的同名方法。我们通过属性ExceptionPolicyName 指定了一个默认的异常处理策略名称(“default”,也正是配置文件中定义个策略名称)。如果某个页面需要采用其他的异常处理策略,可以在类型上面应用ExceptionPolicyAttribute特性来制定,该特性定义如下:

   1: [AttributeUsage( AttributeTargets.Class, AllowMultiple = false)]
   2: public class ExceptionPolicyAttribute: Attribute
   3: {
   4:     public string ExceptionPolicyName { get; private set; }
   5:     public ExceptionPolicyAttribute(string exceptionPolicyName)
   6:     {
   7:         Guard.ArgumentNotNullOrEmpty(exceptionPolicyName, "exceptionPolicyName");
   8:         this.ExceptionPolicyName = exceptionPolicyName;
   9:     }
  10: }

四、IPostBackDataHandler

通过为具体Page定义基类并重写OnLoad和RaisePostBackEvent方法的方式貌似能够实现我们“自动化异常处理”的目标,而且针对我们提供的这个实例来说也是OK的。但是这却不是正确的解决方案,原因在于并非所有控件的事件都是在RaisePostBackEvent方法执行过程中触发的。ASP.NET提供了一组实现了IPostBackDataHandler接口的控件类型,它们会向PostBack的时候向服务端传递相应的数据,我们熟悉的ListControl(DropDownList、ListBox、RadioButtonList和CheckBoxList等)就属于此类。

   1: public interface IPostBackDataHandler
   2: {
   3:     bool LoadPostData(string postDataKey, NameValueCollection postCollection);
   4:     void RaisePostDataChangedEvent();
   5: }

当Page的ProcessRequest(这是对IHttpHandler方法的实现)被执行的的时候,会先于RaisePostBackEvent之前调用另一个方法RaiseChangedEvents。在RaiseChangedEvents方法执行过程中,如果目标类型实现了IPostBackDataHandler接口,会调用它们的RaisePostDataChangedEvent方法。很多表示输入数据改变的事件(比如ListControl的SelectedIndexChanged事件)就是被RaisePostDataChangedEvent方法触发的。如果可能,我们可以通过重写RaiseChangedEvents方法的方式来解决这个问题,不过很可惜,这个方法是一个内部方法。

五、EventHandlerWraper

要实现“自动化异常处理”的根本手段就是将页面和控件注册的事件处理方法置于一个try/catch块中执行,并采用EntLib的异常处理应用块的方式对抛出的异常进行处理。如果我们能够改变页面和控件注册的事件,使注册的事件处理器本身就具有异常处理的能力,我们“自动化异常处理”的目标也能够实现。为此我定义了如下一个用于封装EventHandler的EventHandlerWrapper,它将EventHandler的置于一个try/catch块中执行。对于EventHandlerWrapper的设计思想,在我两年前写的《如何编写没有Try/Catch的程序》一文中具有详细介绍。

   1: public class EventHandlerWrapper
   2: {
   3:     public object Target { get; private set; }
   4:     public MethodInfo Method { get; private set; }
   5:     public EventHandler Hander { get; private set; }
   6:     public string ExceptionPolicyName { get; private set; }
   7:  
   8:     public EventHandlerWrapper(EventHandler eventHandler, string exceptionPolicyName)
   9:     {
  10:         Guard.ArgumentNotNull(eventHandler, "eventHandler");
  11:         Guard.ArgumentNotNullOrEmpty(exceptionPolicyName, "exceptionPolicyName");
  12:  
  13:         this.Target = eventHandler.Target;
  14:         this.Method = eventHandler.Method;
  15:         this.ExceptionPolicyName = exceptionPolicyName;
  16:         this.Hander += Invoke;
  17:     }
  18:     public static implicit operator EventHandler(EventHandlerWrapper eventHandlerWrapper)
  19:     {
  20:         Guard.ArgumentNotNull(eventHandlerWrapper, "eventHandlerWrapper");
  21:         return eventHandlerWrapper.Hander;
  22:     }
  23:     private void Invoke(object sender, EventArgs args)
  24:     {
  25:         try
  26:         {
  27:             this.Method.Invoke(this.Target, new object[] { sender, args });
  28:         }
  29:         catch (TargetInvocationException ex)
  30:         {
  31:             if (ExceptionPolicy.HandleException(ex.InnerException, this.ExceptionPolicyName))
  32:             {
  33:                 throw;
  34:             }
  35:         }
  36:     }
  37: }

由于我们为EventHandlerWrapper定义了一个针对EventHandler的隐式转化符,一个EventHandlerWrapper对象能够自动被转化成EventHandler对象。我们现在的目标就是:将包括页面在内的所有控件注册的EventHandler替换成用于封装它们的EventHandlerWrapper。我们知道所有控件的基类Control具有如下一个受保护的只读属性Events,所有注册的EventHandler就包含在这里,而我们的目标就是要改变所有控件该属性中保存的EventHandler。

   1: public class Control
   2: {
   3:     protected EventHandlerList Events{get;}
   4: }

其实要改变Events属性中的EventHandler也并不是一件容易的事,因为其类型EventHandlerList 并不如它的名称表现出来的那样是一个可枚举的列表,而是一个通过私有类型ListEntry维护的链表。要改变这些注册的事件,我们不得不采用反射,而这会影响性能。不过对应并非访问量不高的企业应用来说,我觉得这点性能损失是可以接受的。整个操作被定义在如下所示的EventHandlerWrapperUtil的Wrap方法中。

   1: private static class EventHandlerWrapperUtil
   2: {
   3:     private static Type listEntryType;
   4:     private static FieldInfo handler;
   5:     private static FieldInfo key;
   6:     private static FieldInfo next;
   7:  
   8:     static EventHandlerWrapperUtil()
   9:     {
  10:         listEntryType = Type.GetType("System.ComponentModel.EventHandlerList+ListEntry, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089");
  11:         BindingFlags bindingFlags = BindingFlags.Instance | BindingFlags.NonPublic;
  12:         handler = listEntryType.GetField("handler", bindingFlags);
  13:         key     = listEntryType.GetField("key", bindingFlags);
  14:         next    = listEntryType.GetField("next", bindingFlags);
  15:     }
  16:  
  17:     public static void Wrap(object listEntry, string exceptionPolicyName)
  18:     {
  19:         EventHandler eventHandler = handler.GetValue(listEntry) as EventHandler;
  20:         if (null != eventHandler)
  21:         {
  22:             EventHandlerWrapper eventHandlerWrapper = new EventHandlerWrapper(eventHandler, exceptionPolicyName);
  23:             handler.SetValue(listEntry, (EventHandler)eventHandlerWrapper);
  24:         }
  25:             object nextEntry = next.GetValue(listEntry);
  26:         if(null != nextEntry)
  27:         {
  28:             Wrap(nextEntry,exceptionPolicyName);
  29:         }
  30:     }            
  31: }

六、对控件注册事件的自动封装

对包括页面在内的所有控件注册时间的自动封装同样实现在作为具体页面积累的PageBase中。具体的实现定义在WrapEventHandlers方法中,由于Control的Events属性是受保护的,所以我们还得采用反射。该方法最终的重写的OnInit方法中执行。此外,由于EventHandlerWraper仅仅能够封装EventHandler,但是很多控件的事件却并非EventHandler类型,所以这是一个挺难解决的问题。

   1: public abstract class PageBase : Page
   2: {
   3:     private static PropertyInfo eventsProperty;
   4:     private static FieldInfo headField;
   5:  
   6:     public static string ExceptionPolicyName { get; set; }
   7:     static PageBase()
   8:     {
   9:         ExceptionPolicyName = "default";
  10:         eventsProperty = typeof(Control).GetProperty("Events", BindingFlags.Instance | BindingFlags.NonPublic);
  11:         headField = typeof(EventHandlerList).GetField("head", BindingFlags.Instance | BindingFlags.NonPublic);
  12:     }
  13:  
  14:     protected override void OnInit(EventArgs e)
  15:     {
  16:         base.OnInit(e);
  17:         Trace.Write("Begin to wrap events!");
  18:         this.WrapEventHandlers(this);
  19:         Trace.Write("Wrapping events ends!");
  20:     }
  21:  
  22:     protected virtual void WrapEventHandlers(Control control)
  23:     {
  24:         string exceptionPolicyName = this.GetExceptionPolicyName();
  25:         EventHandlerList events = eventsProperty.GetValue(control, null) as EventHandlerList;
  26:         if (null != events)
  27:         {
  28:             object head = headField.GetValue(events);
  29:             if (null != head)
  30:             {
  31:                 EventHandlerWrapperUtil.Wrap(head, exceptionPolicyName);
  32:             }
  33:         }
  34:         foreach (Control subControl in control.Controls)
  35:         {
  36:             WrapEventHandlers(subControl);
  37:         }
  38:     }
  39:  
  40:     protected virtual string GetExceptionPolicyName()
  41:     {
  42:         ExceptionPolicyAttribute attribute = this.GetType().GetCustomAttributes(true)
  43:             .OfType<ExceptionPolicyAttribute>().FirstOrDefault();
  44:         if (null != attribute)
  45:         {
  46:             return attribute.ExceptionPolicyName;
  47:         }
  48:         else
  49:         {
  50:             return ExceptionPolicyName;
  51:         }
  52:     }
  53: }

七、AlertHandler

我想有人对用于显示错误消息对话框的AltertHandler的实现很感兴趣,下面给出了它和对应的AlertHandlerData的定义。从如下的代码可以看出,AltertHandler仅仅是调用Page的RaisePostBackEvent方法注册了一段显示错误消息的JavaScript脚本而已。

   1: [ConfigurationElementType(typeof(AlertHandlerData))]
   2: public class AlertHandler: IExceptionHandler
   3: {
   4:     public string MessageTemplate { get; private set; }
   5:     public AlertHandler(string messageTemplate)
   6:     {
   7:         this.MessageTemplate = messageTemplate;
   8:     }
   9:  
  10:     protected string FormatMessage(Exception exception)
  11:     {
  12:         Guard.ArgumentNotNull(exception, "exception");
  13:         string messageTemplate = string.IsNullOrEmpty(this.MessageTemplate) ? exception.Message : this.MessageTemplate;
  14:         return messageTemplate.Replace("{ExceptionType}", exception.GetType().Name)
  15:                                 .Replace("{HelpLink}", exception.HelpLink)
  16:                                 .Replace("{Message}", exception.Message)
  17:                                 .Replace("{Source}", exception.Source)
  18:                                 .Replace("{StackTrace}", exception.StackTrace);
  19:     }
  20:  
  21:     public Exception HandleException(Exception exception, Guid handlingInstanceId)
  22:     {
  23:         Page page = HttpContext.Current.Handler as Page;
  24:         if (null != page)
  25:         {
  26:  
  27:             string message = this.FormatMessage(exception);
  28:             string hiddenControl = "hiddenCurrentPageException";
  29:             page.ClientScript.RegisterHiddenField(hiddenControl, message);
  30:             string script = string.Format("<Script language=\"javascript\">var obj=document.forms[0].{0};alert(unescape(obj.value));</Script>", 
  31:                 new object[] { hiddenControl });
  32:             page.ClientScript.RegisterStartupScript(base.GetType(), "ExceptionHandling.AlertHandler", script);
  33:         }
  34:         return exception;
  35:     }
  36: }
  37:  
  38: public class AlertHandlerData : ExceptionHandlerData
  39: {
  40:     [ConfigurationProperty("messageTemplate", IsRequired = false, DefaultValue="")]
  41:     public string MessageTemplate
  42:     {
  43:         get { return (string)this["messageTemplate"]; }
  44:         set { this["messageTemplate"] = value; }
  45:     }
  46:  
  47:     public override IEnumerable<TypeRegistration> GetRegistrations(string namePrefix)
  48:     {
  49:         yield return new TypeRegistration<IExceptionHandler>(() => new AlertHandler(this.MessageTemplate))
  50:         {
  51:             Name = this.BuildName(namePrefix),
  52:             Lifetime = TypeRegistrationLifetime.Transient
  53:         };
  54:     }
  55: }
 
转载自:http://www.cnblogs.com/artech/archive/2012/10/28/automatic-exception-handling-aspnet.html
 
程序员的基础教程:菜鸟程序员

无需写try/catch,也能正常处理异常的更多相关文章

  1. 无需写try/catch,也能正常处理异常 (转)

    原文地址: http://www.cnblogs.com/artech/archive/2012/10/28/automatic-exception-handling-aspnet.html 对于企业 ...

  2. 【QT】无需写connect代码关联信号和槽函数

    对于一些简单的事件判别,如点击按钮. 无需写代码关联信号和槽函数. connect(ui->Btnshowhello,SIGNAL(clicked(bool)),this,SLOT(Btnsho ...

  3. 如何用Windbg找到被catch住的C++的异常

    博客搬到了fresky.github.io - Dawei XU,请各位看官挪步.最新的一篇是:如何用Windbg找到被catch住的C++的异常.

  4. Online Coding开发模式 (通过在线配置实现一个表模型的增删改查功能,无需写任何代码)

    JEECG 智能开发平台. 开发模式由代码生成器转变为Online Coding模式                      (通过在线配置实现一个表模型的增删改查功能,无需一行代码,支持用户自定义 ...

  5. c#winform将全局异常抛出,不用大量写try()catch()

    一.在program.cs处完善成如下,但是这样后只能抛出主线程(UI)的错误,所以请看第二步 /// 应用程序的主入口点. /// </summary> [STAThread] stat ...

  6. 厚溥教育1718部数据库连接作业答案,分装一个操作数据库而无需写SQL语句的函数

    <?php header("Content-type:text/html;charset=utf8"); //PHP操作数据库的函数 function phpsql($dbc ...

  7. [Java 开源项目]一款无需写任何代码,即可一键生成前后端代码的工具

    作者:HelloGitHub-小鱼干 JeecgBoot 是一款基于代码生成器的低代码开发平台,零代码开发.JeecgBoot 采用开发模式:Online Coding 模式-> 代码生成器模式 ...

  8. myeclipse与数据库进行连接(无需写代码进行验证)

    首先对SqlServer配置管理器进行设置. 1.打开SqlServer配置管理器 2.进入SQL配置管理器后,选中左侧“SQL Server网络配置”>>再选中“MSSQLSERVER的 ...

  9. 如何写出让java虚拟机发生内存溢出异常OutOfMemoryError的代码

    程序小白在写代码的过程中,经常会不经意间写出发生内存溢出异常的代码.很多时候这类异常如何产生的都傻傻弄不清楚,如果能故意写出让jvm发生内存溢出的代码,有时候看来也并非一件容易的事.最近通过学习< ...

随机推荐

  1. 运维命令:tcpdump

    tcpdump命令  tcpdump 命令是一款sniffer工具,它可以打印所有经过网络接口的数据包的头信息,也可以使用 -w 选项将数据包保存到文件中,方便以后分析. 常用参数: -a:尝试将网络 ...

  2. 2dx 3.0环境配置(mac)

    安装ant brew install ant ant默认的目录在 /usr/local/bin mvim ~/.profile,添加 export ANT_ROOT=/usr/local/bin . ...

  3. idea之jrebel热部署使用教程

    JRebel是一个J2EE热部署的工具.使用它可以减少浪费8-18%的开发时间在项目的构建和部署上.虽然Java也提供了HotSpot的JVM,但是如果你修改的类中有方法名称变动的话,HotSpot就 ...

  4. ZOJ 3609 Modular Inverse(拓展欧几里得求最小逆元)

    Modular Inverse Time Limit: 2 Seconds      Memory Limit: 65536 KB The modular modular multiplicative ...

  5. Go - coding之前的准备

    Go tool 的使用 Go的tool要求我们对于code有一定的结构化组织和管理,下面我们就来一介绍他们: --GoPath environment variable:  顾名思义,环境变量,指定了 ...

  6. 设计模式—三种工厂模式(JAVA)

    一:简单工厂: 有一个实际工厂,这个工厂只能造一类的产品,这一类产品就是一个产品接口,会有多个具体产品实现这个接口,例 如,一个手机厂,生产苹果手机,三星手机: 缺点:在工厂类中集中了所有实例的创建逻 ...

  7. 编译安装x264

    网上也有相应的教程,之所以在这里重申一遍,是因为我试了网上很多的编译方法,都出现了问题,为此将此编译安装方法记录下来. 首先是 获取x264的网站:http://www.videolan.org/de ...

  8. Cisco交换机配置VLAN与TRUNK

    0x00前言: 今日在学校里学习了如何搭建vlan和配置等等还有trunk. 由于快下课了.尽快写. 0x01准备: Cisco模拟器 0x02正文: 要求: VLAN 10 左边的IP:192.16 ...

  9. angularjs之ng-option

    ng-options一般有以下用法: 对于数组: label for value in array select as label for value in array label group by  ...

  10. leetcode451

    public class Solution { public string FrequencySort(string s) { var dic = new Dictionary<char, in ...