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