無需寫try/catch,也能正常處理異常
對於企業應用的開發者來說,異常處理是一件既簡單又複雜的事情。說其簡單,是因為相關的編程無外乎try/catch/finally+throw而已;說其複雜,是因為我們往往很難按照我們真正需要的策略來處理異常。我一直有這樣的想法,理想的企業應用開發中應該盡量讓框架來完成對異常的處理,最終的開發人員在大部分的情況下無需編寫異常處理相關的任何代碼。在這篇文章中我們將提供一個解決方案來讓ASP.NET應用利用EntLib的異常處理模塊來實現自動化的異常處理。
源代碼:
Sample1[通過重寫Page的OnLoad和OnRaisePostBackEvent方法]
Sample2[通過自動封裝注冊的EventHandler]
一、EntLib的異常處理方式
二、實例演示
三、通過重寫Page的OnLoad和RaisePostBackEvent方法實現自動異常處理
四、IPostBackDataHandler
五、EventHandlerWraper
六、對控件注冊事件的自動封裝
七、AlertHandler
所謂異常,其本意就是超出預期的錯誤。既然如此,異常處理的策略就不可能一成不變,我們不可能在開發階段就製定一個完備的異常處理策略來處理未來發生的所有異常。異常處理策略應該是可配置的,能夠隨時進行動態改變的。就此而言,微軟的企業庫(以下簡稱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來顯示異常信息。
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 :
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的形式顯示出來。
我們知道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: }
微信公眾賬號:大內老A
微博:www.weibo.com/artech
如果你想及時得到個人撰寫文章以及著作的消息推送,或者想看看個人推薦的技術資料,可以掃描左邊二維碼(或者長按識別二維碼)關注個人公眾號(原來公眾帳號蔣金楠的自媒體將會停用)。
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁麵明顯位置給出原文連接,否則保留追究法律責任的權利。
最後更新:2017-10-25 16:05:19