對于企業(yè)應(yīng)用的開發(fā)者來說,異常處理是一件既簡單又復(fù)雜的事情。說其簡單,是因為相關(guān)的編程無外乎try/catch/finally+throw而已;說其復(fù)雜,是因為我們往往很難按照我們真正需要的策略來處理異常。我一直有這樣的想法,理想的企業(yè)應(yīng)用開發(fā)中應(yīng)該盡量讓框架來完成對異常的處理,最終的開發(fā)人員在大部分的情況下無需編寫異常處理相關(guān)的任何代碼。在這篇文章中我們將提供一個解決方案來讓ASP.NET應(yīng)用利用EntLib的異常處理模塊來實現(xiàn)自動化的異常處理。
源代碼:
Sample1[通過重寫Page的OnLoad和OnRaisePostBackEvent方法]
Sample2[通過自動封裝注冊的EventHandler]
一、EntLib的異常處理方式
二、實例演示
三、通過重寫Page的OnLoad和RaisePostBackEvent方法實現(xiàn)自動異常處理
四、IPostBackDataHandler
五、EventHandlerWraper
六、對控件注冊事件的自動封裝
七、AlertHandler
一、EntLib的異常處理方式
所謂異常,其本意就是超出預(yù)期的錯誤。既然如此,異常處理的策略就不可能一成不變,我們不可能在開發(fā)階段就制定一個完備的異常處理策略來處理未來發(fā)生的所有異常。異常處理策略應(yīng)該是可配置的,能夠隨時進(jìn)行動態(tài)改變的。就此而言,微軟的企業(yè)庫(以下簡稱EntLib)的異常處理應(yīng)用塊(Exception Handling Application Block)是一個不錯的異常處理框架,它運行我們通過配置文件來定義針對具體異常類型的處理策略。
針對EntLib的異常處理應(yīng)用塊采用非常簡單的編程方式,我們只需要按照如下的方式捕捉拋出的一場,并通過調(diào)用ExceptionPolicy的HandleException根據(jù)指定的異常策略進(jìn)行處理即可。對于ASP.NET應(yīng)用來說,我們可以注冊HttpApplication的Error事件的形式來進(jìn)行統(tǒng)一的異常處理。但是在很多情況下,我們往往需要將異?刂圃诋(dāng)前頁面之內(nèi)(比如當(dāng)前頁面被正常呈現(xiàn),并通過執(zhí)行一段JavaScript探出一個對話框顯示錯誤消息),我們往往需要將下面這段相同的代碼結(jié)構(gòu)置于所有控件的注冊事件之中。
1: try
2: {
3: //業(yè)務(wù)代碼
4: }
5: catch(Exception ex)
6: {
7: if(ExceptionPolicy.HandleException(ex,"exceptionPolcyName"))
8: {
9: throw;
10: }
11: }
我個人不太能夠容忍完全相同的代碼到處出現(xiàn),代碼應(yīng)該盡可能地重用,而不是重復(fù)。接下來我們就來討論如何采用一些編程上的手段或者技巧來讓開發(fā)人員無須編寫任何的異常處理代碼,而拋出的確卻能按照我們預(yù)先指定的策略被處理。
二、實例演示
為了讓讀者對“自動化異常處理”有一個直觀的認(rèn)識,我們來做一個簡單的實例演示。我們的異常處理策略很簡單:如果后臺代碼拋出異常,異常的相關(guān)信息按照預(yù)定義的格式通過Alert的方式顯示在當(dāng)前頁面中。如下所示的是異常處理策略在配置文件中的定義,該配置中定義了唯一個名為“default”的異常策略,該策略利用自定義的AlertHandler來顯示一場信息。配置屬性messageTemplate定義了一個模板用于控制顯示消息的格式。
1:
2: ...
3:
4:
5:
6:
7: <add type="System.Exception, mscorlib"
8: postHandlingAction="None" name="Exception">
9:
10: <add name="Alert Handler" type="AutomaticExceptionHandling.AlertHandler, AutomaticExceptionHandling"
11: messageTemplate="[{ExceptionType}]{Message}"/>
12:
13:
14:
15:
16:
17:
18:
現(xiàn)在我們定義一個簡單的頁面來模式自動化異常處理,這個頁面是一個用于進(jìn)行除法預(yù)算的計算器。如下所示的該頁面的后臺代碼,可以看出它沒有直接繼承自Page,而是繼承自我們自定義的基類PageBase,所有異常處理的機(jī)制就實現(xiàn)在此。Page_Load方法收集以QueryString方式提供的操作數(shù),并轉(zhuǎn)化成整數(shù)進(jìn)行除法預(yù)算,最后將運算結(jié)果顯示在表示結(jié)果的文本框中。計算按鈕的Click事件處理方法根據(jù)用戶輸入的操作數(shù)進(jìn)行除法運算。兩個方法中均沒有一句與異常處理相關(guān)的代碼。
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: }
現(xiàn)在運行我們程序,可以想象如果在表示操作數(shù)的文本框中輸入一個非整數(shù)字符,調(diào)用Int32的Parse方法時將會拋出一個FormatException異常,或者將被除數(shù)設(shè)置為0,則會拋出一個DivideByZeroException異常。如下面的代碼片斷所示,在這兩種情況下相應(yīng)的錯誤信息按照我們預(yù)定義的格式以Alert的形式顯示出來。
三、通過重寫Page的OnLoad和RaisePostBackEvent方法實現(xiàn)自動異常處理
我們知道ASP.NET應(yīng)用中某個頁面的后臺代碼基本上都是注冊到頁面及其控件的事件處理方法,除了第一次呈現(xiàn)頁面的Load事件,其他事件均是通過PostBack的方式出發(fā)的。所以我最初的解決方案很直接:就是提供一個PageBase,在重寫的OnLoad和RaisePostBackEvent方法中進(jìn)行異常處理。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().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異常處理應(yīng)用塊的編程方式調(diào)用基類的同名方法。我們通過屬性ExceptionPolicyName 指定了一個默認(rèn)的異常處理策略名稱(“default”,也正是配置文件中定義個策略名稱)。如果某個頁面需要采用其他的異常處理策略,可以在類型上面應(yīng)用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方法的方式貌似能夠?qū)崿F(xiàn)我們“自動化異常處理”的目標(biāo),而且針對我們提供的這個實例來說也是OK的。但是這卻不是正確的解決方案,原因在于并非所有控件的事件都是在RaisePostBackEvent方法執(zhí)行過程中觸發(fā)的。ASP.NET提供了一組實現(xiàn)了IPostBackDataHandler接口的控件類型,它們會向PostBack的時候向服務(wù)端傳遞相應(yīng)的數(shù)據(jù),我們熟悉的ListControl(DropDownList、ListBox、RadioButtonList和CheckBoxList等)就屬于此類。
1: public interface IPostBackDataHandler
2: {
3: bool LoadPostData(string postDataKey, NameValueCollection postCollection);
4: void RaisePostDataChangedEvent();
5: }
當(dāng)Page的ProcessRequest(這是對IHttpHandler方法的實現(xiàn))的時候,會先于RaisePostBackEvent之前調(diào)用另一個方法RaiseChangedEvents。在RaisePostBackEvent方法執(zhí)行過程中,如果目標(biāo)類型實現(xiàn)了IPostBackDataHandler接口,會調(diào)用它們的RaisePostDataChangedEvent方法。很多表示輸入數(shù)據(jù)改變的事件(比如ListControl的SelectedIndexChanged事件)就是被RaisePostDataChangedEvent方法觸發(fā)的。如果可能,我們可以通過重寫RaiseChangedEvents方法的方式來解決這個問題,不過很可惜,這個方法是一個內(nèi)部方法。
五、EventHandlerWraper
要實現(xiàn)“自動化異常處理”的根本手段就是將頁面和控件注冊的事件處理方法置于一個try/catch塊中執(zhí)行,并采用EntLib的異常處理應(yīng)用塊的方式對拋出的異常進(jìn)行處理。如果我們能夠改變頁面和控件注冊的事件,使注冊的事件處理器本身就具有異常處理的能力,我們“自動化異常處理”的目標(biāo)也能夠?qū)崿F(xiàn)。為此我定義了如下一個用于封裝EventHandler的EventHandlerWrapper,它將EventHandler的置于一個try/catch塊中執(zhí)行。對于EventHandlerWrapper的設(shè)計思想,在我兩年前寫的《如何編寫沒有Try/Catch的程序》一文中具有詳細(xì)介紹。
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: }
由于我們?yōu)镋ventHandlerWrapper定義了一個針對EventHandler的隱式轉(zhuǎn)化符,一個EventHandlerWrapper對象能夠自動被轉(zhuǎn)化成EventHandler對象。我們現(xiàn)在的目標(biāo)就是:將包括頁面在內(nèi)的所有控件注冊的EventHandler替換成用于封裝它們的EventHandlerWrapper。我們知道所有控件的基類Control具有如下一個受保護(hù)的只讀屬性Events,所有注冊的EventHandler就包含在這里,而我們的目標(biāo)就是要改變所有控件該屬性中保存的EventHandler。
1: public class Control
2: {
3: protected EventHandlerList Events{get;}
4: }
其實要改變Events屬性中的EventHandler也并不是一件容易的事,因為其類型EventHandlerList 并不如它的名稱表現(xiàn)出來的那樣是一個可枚舉的列表,而是一個通過私有類型ListEntry維護(hù)的鏈表。要改變這些注冊的事件,我們不得不采用反射,而這會影響性能。不過對應(yīng)并非訪問量不高的企業(yè)應(yīng)用來說,我覺得這點性能損失是可以接受的。整個操作被定義在如下所示的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: }
六、對控件注冊事件的自動封裝
對包括頁面在內(nèi)的所有控件注冊時間的自動封裝同樣實現(xiàn)在作為具體頁面積累的PageBase中。具體的實現(xiàn)定義在WrapEventHandlers方法中,由于Control的Events屬性是受保護(hù)的,所以我們還得采用反射。該方法最終的重寫的OnInit方法中執(zhí)行。
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().FirstOrDefault();
44: if (null != attribute)
45: {
46: return attribute.ExceptionPolicyName;
47: }
48: else
49: {
50: return ExceptionPolicyName;
51: }
52: }
53: }
七、AlertHandler
我想有人對用于顯示錯誤消息對話框的AltertHandler的實現(xiàn)很感興趣,下面給出了它和對應(yīng)的AlertHandlerData的定義。從如下的代碼可以看出,AltertHandler僅僅是調(diào)用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("",
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 IEnumerableGetRegistrations(string namePrefix)
48: {
49: yield return new TypeRegistration(() => new AlertHandler(this.MessageTemplate))
50: {
51: Name = this.BuildName(namePrefix),
52: Lifetime = TypeRegistrationLifetime.Transient
53: };
54: }
55: }