无需写try/catch,也能正常处理异常
對于企業應用的開發者來說,異常處理是一件既簡單又復雜的事情。說其簡單,是因為相關的編程無外乎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,也能正常处理异常的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Android -- WebView进度
- 下一篇: Bzoj 3289: Mato的文件管理