原文地址: http://www.cnblogs.com/artech/archive/2012/10/28/automatic-exception-handling-aspnet.htmljavascript
對於企業應用的開發者來講,異常處理是一件既簡單又複雜的事情。說其簡單,是由於相關的編程無外乎try/catch/finally+throw而已;說其複雜,是由於咱們每每很難按照咱們真正須要的策略來處理異常。我一直有這樣的想法,理想的企業應用開發中應該儘可能讓框架來完成對異常的處理,最終的開發人員在大部分的狀況下無需編寫異常處理相關的任何代碼。在這篇文章中咱們將提供一個解決方案來讓ASP.NET應用利用EntLib的異常處理模塊來實現自動化的異常處理。html
源代碼: Sample1[經過重寫Page的OnLoad和OnRaisePostBackEvent方法] Sample2[經過自動封裝註冊的EventHandler]java
1、EntLib的異常處理方式 2、實例演示 3、經過重寫Page的OnLoad和RaisePostBackEvent方法實現自動異常處理 4、IPostBackDataHandler 5、EventHandlerWraper 6、對控件註冊事件的自動封裝 7、AlertHandler編程
所謂異常,其本意就是超出預期的錯誤。既然如此,異常處理的策略就不可能一成不變,咱們不可能在開發階段就制定一個完備的異常處理策略來處理將來發生的全部異常。異常處理策略應該是可配置的,可以隨時進行動態改變的。就此而言,微軟的企業庫(如下簡稱EntLib)的異常處理應用塊(Exception Handling Application Block)是一個不錯的異常處理框架,它運行咱們經過配置文件來定義針對具體異常類型的處理策略。app
針對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: }
我我的不太可以容忍徹底相同的代碼處處出現,代碼應該儘量地重用,而不是重複。接下來咱們就來討論如何採用一些編程上的手段或者技巧來讓開發人員無須編寫任何的異常處理代碼,而拋出的確卻能按照咱們預先指定的策略被處理。ide
爲了讓讀者對「自動化異常處理」有一個直觀的認識,咱們來作一個簡單的實例演示。咱們的異常處理策略很簡單:若是後臺代碼拋出異常,異常的相關信息按照預約義的格式經過Alert的方式顯示在當前頁面中。以下所示的是異常處理策略在配置文件中的定義,該配置中定義了惟一個名爲「default」的異常策略,該策略利用自定義的AlertHandler來顯示異常信息。配置屬性messageTemplate定義了一個模板用於控制顯示消息的格式。post
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的形式顯示出來。ui
咱們知道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: }
經過爲具體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方法的方式來解決這個問題,不過很惋惜,這個方法是一個內部方法。
要實現「自動化異常處理」的根本手段就是將頁面和控件註冊的事件處理方法置於一個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: }
我想有人對用於顯示錯誤消息對話框的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: }