微软企业库5.0 学习之路——第三步、为项目加上异常处理(采用自定义扩展方式记录到数据库中)
在前一篇文章:[EntLib]微软企业库5.0 学习之路——第二步、使用VS2010+Data Access模块建立多数据库项目中我们搭建好了项目的整体多数据库环境,实现了项目的多数据库访问,而整个项目中最主要的异常处理却没有进行部署,今天我们就使用企业库中的Exception Handling+Logging模块为项目加上异常处理以及异常日志记录。
(注:关于Exception Handling和Logging模块的相关基本概念可以查看TerryLee的异常处理和日志检测这2篇文章)
首先说一下企业库Logging模块的个人感觉,个人感觉企业库的日志记录太繁琐了,而且要自定义也比较烦,无法通过简单的配置达到我自己的要求,企业库中的日志记录模块在可以记录许多信息如下:
Timestamp: 2010-6-12 3:16:39
Message: HandlingInstanceID: 669fed01-a758-434b-896e-a8e25ebf8c9b
An exception of type 'System.Exception' occurred and was caught.
----------------------------------------------------------------
06/12/2010 11:16:39
Type : System.Exception, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
Message : Test
Source : EntLibStudy.Helper
Help link :
Data : System.Collections.ListDictionaryInternal
TargetSite : System.String Test()
Stack Trace : 在 EntLibStudy.Helper.BasePage.Test() 位置 F:\EntLibStudy\Helper\BasePage.cs:行号 87
在 EntLibStudy.Helper.BasePage.<Page_Load>b__0() 位置 F:\EntLibStudy\Helper\BasePage.cs:行号 81
在 Microsoft.Practices.EnterpriseLibrary.ExceptionHandling.ExceptionManagerImpl.Process[TResult](Func`1 action, TResult defaultResult, String policyName) 位置以下省略N行。。。。。。
这些信息很多都不是我想要的,我想要的仅仅是异常的提示信息,异常发生的时间,以及异常发生的位置,好方便我们第一时间到异常发生的源头进行调试检查(可能企业库的这些异常信息更加有用,但是我个人认为很多时候都会干扰我们),所以我们仅仅需要其中的几条有用的信息就够了,比如Message,Timestamp、Stack Trace和Severity这4个就基本上够用了,所以我做了个处理,就是使用企业库中Logging模块提供的自定义CustomerTraceListener来实现我们需要的功能。
首先建立一个异常日志记录表(SQLite版)
CREATE TABLE [ExceptionLog] ( [Id] integer PRIMARY KEY AUTOINCREMENT NOT NULL, [Message] nvarchar(1024) NOT NULL, [LogDate] nvarchar(1024) NOT NULL, [ExceptionLevel] nvarchar(32) NOT NULL, [Exception] ntext NOT NULL )
我编写了一个类继承自CustomTraceListener,并重写了记录方法,具体代码如下:
using System; using System.Collections.Generic; using System.Data; using System.Data.Common; using System.Diagnostics; using System.Globalization; using System.Linq; using System.Text; using Microsoft.Practices.EnterpriseLibrary.Common.Configuration; using Microsoft.Practices.EnterpriseLibrary.Data; using Microsoft.Practices.EnterpriseLibrary.Logging; using Microsoft.Practices.EnterpriseLibrary.Logging.Configuration; using Microsoft.Practices.EnterpriseLibrary.Logging.Formatters; using Microsoft.Practices.EnterpriseLibrary.Logging.TraceListeners; namespace EntLibStudy.Helper.EntLibExtension.ExceptionExtension { [ConfigurationElementType(typeof(CustomTraceListenerData))] public class ExceptionCustomerListener : CustomTraceListener { string writeLogSQL = String.Empty; Database database; Exception ex; public ExceptionCustomerListener() : base() { database = DBHelper.CreateDataBase(); } public override void TraceData(TraceEventCache eventCache, string source, TraceEventType eventType, int id, object data) { if ((this.Filter == null) || this.Filter.ShouldTrace(eventCache, source, eventType, id, null, null, data, null)) { if (data is LogEntry) { LogEntry logEntry = data as LogEntry; ExecuteSQL(logEntry); } else if (data is string) { Write(data as string); } else { base.TraceData(eventCache, source, eventType, id, data); } } } public override void Write(string message) { ExecuteWriteLogSQL(TraceEventType.Information, DateTime.Now, message, database); } public override void WriteLine(string message) { Write(message); } /// <summary> ///执行SQL /// </summary> /// <param name="logEntry">日志对象</param> private void ExecuteSQL(LogEntry logEntry) { using (DbConnection connection = database.CreateConnection()) { try { connection.Open(); using (DbTransaction transaction = connection.BeginTransaction()) { try { ExecuteWriteLogSQL(logEntry, database, transaction); transaction.Commit(); } catch { transaction.Rollback(); throw; } } } finally { connection.Close(); } } } /// <summary> /// 执行写入日志数据库语句 /// </summary> /// <param name="severity">异常等级</param> /// <param name="message">消息</param> /// <param name="db">保存日志的数据库实例</param> private void ExecuteWriteLogSQL(TraceEventType severity, DateTime timeStamp, string message, Database db) { writeLogSQL = (string)this.Attributes["writeLogSQL"]; DbCommand cmd = db.GetSqlStringCommand(writeLogSQL); string exceptionMessage = Utils.GetBetweenString(message, "Message :", "Source :", 9); string exceptionInfo = Utils.GetBetweenString(message, "Stack Trace :", "Additional Info:", 13); db.AddInParameter(cmd, "@Message", DbType.String, exceptionMessage); db.AddInParameter(cmd, "@LogDate", DbType.DateTime, timeStamp); db.AddInParameter(cmd, "@Level", DbType.String, message); db.AddInParameter(cmd, "@Exception", DbType.String, exceptionInfo); db.ExecuteNonQuery(cmd); } /// <summary> /// 执行写入日志数据库语句 /// </summary> /// <param name="logEntry">日志对象</param> /// <param name="db">保存日志的数据库实例</param> /// <param name="transaction">事务对象</param> private void ExecuteWriteLogSQL(LogEntry logEntry, Database db, DbTransaction transaction) { writeLogSQL = (string)this.Attributes["writeLogSQL"]; DbCommand cmd = db.GetSqlStringCommand(writeLogSQL); string exceptionMessage = Utils.GetBetweenString(logEntry.Message, "Message :", "Source :", 9); string exceptionInfo = Utils.GetBetweenString(logEntry.Message, "Stack Trace :", "Additional Info:", 13); db.AddInParameter(cmd, "@Message", DbType.String, exceptionMessage); db.AddInParameter(cmd, "@LogDate", DbType.DateTime, logEntry.TimeStamp.ToLocalTime()); db.AddInParameter(cmd, "@Level", DbType.String, logEntry.LoggedSeverity); db.AddInParameter(cmd, "@Exception", DbType.String, exceptionInfo); db.ExecuteNonQuery(cmd, transaction); } } }
其中在类的初始化的时候获取配置文件的默认数据库对象,通过重写TraceData方法来调用ExecuteSQL方法来执行异常日志插入。
在ExecuteWriteLogSQL方法中有句代码:
writeLogSQL = (string)this.Attributes["writeLogSQL"];
这个代码就是从配置文件中Listener的Attributes中获取所配置的执行SQL语句(这里不同于Logging模块自带的数据库以存储过程的记录方式,而是使用配置的SQL语句的方式,因为本项目是面向多数据库的,并不是所有的数据库都有存储过程的,比如SQLite),下面看下具体的配置信息:
配置文件创建主要分为以下2步:
1、在企业库的配置工具添加一个Exception Handle模块,然后添加一个名为Exception Policy的策略,再为这个策略添加异常类型,默认我选择所有异常类型(All Exceptions),Post Handle Action为: NotifyRethow(对不理解Post Handle Action的处理方式的可以看下下面的解释)
PostHandlingAction 决定了在异常处理链完成后将发生什么活动。默认情况下,PostHandlingAction 被设置为 NotifyRethrow 。
None:应用程序块为此异常执行所有的处理程序,然后在 HandleException 方法的调用点上返回 false 给应用程序。应用程序检查此值以继续运行。
NotifyRethrow:应用程序块为此异常执行所有的处理程序,然后在 HandleException 方法的调用点上返回 true 给应用程序。应用程序检查到此值就重新抛出原始异常。
ThrowNewException:应用程序块为此异常执行所有的处理程序,然后在所有处理程序运行后抛出存在的异常。
2、为异常策略创建处理方式,我这边选择Loggin Exception Handler(在创建的同时配置工具会我们自动创建好Logging模块,并自动创建了一个日志分类:General,不过这个日志分类的默认Listener为event log,就是记录到系统的事件中),这时我们再创建一个CustomerTraceListener选择From File->自定义Listener所在DLL。
这边我碰到了一个问题就是添加了CustomerTraceListener,在对话框中我点击From File选择我编写的自定义Listener所在DLL,可惜没任何反应,不知道是不是要在DLL中做什么处理,所以我只能采用老办法:手写配置文件
首先看下Exception Handle模块的配置信息:
<exceptionHandling> <exceptionPolicies> <add name="ExceptionPolicy"> <exceptionTypes> <add name="All Exceptions" type="System.Exception, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" postHandlingAction="NotifyRethrow"> <exceptionHandlers> <add name="Logging Exception Handler" type="Microsoft.Practices.EnterpriseLibrary.ExceptionHandling.Logging.LoggingExceptionHandler, Microsoft.Practices.EnterpriseLibrary.ExceptionHandling.Logging, Version=5.0.414.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" logCategory="General" eventId="100" severity="Error" title="Enterprise Library Exception Handling" formatterType="Microsoft.Practices.EnterpriseLibrary.ExceptionHandling.TextExceptionFormatter, Microsoft.Practices.EnterpriseLibrary.ExceptionHandling" priority="0" /> </exceptionHandlers> </add> </exceptionTypes> </add> </exceptionPolicies> </exceptionHandling>
接下来是日志模块配置,在日志模块下我配置了3个Listener,其中Custom Trace Listener为我自定义的异常日志记录,Event Log Listener(系统日志记录)和Rolling Flat File Trace Listener(文本文件记录,按天回滚记录)为在日志分类General无法正常记录日志时的记录下日志分类General为何无法记录,因为异常日志默认保存到数据库中,但是如果数据库中存在问题,或者链接被关闭这时就无法正常记录异常,所以:
<loggingConfiguration name="" tracingEnabled="true" defaultCategory="General"> <listeners> <add listenerDataType="Microsoft.Practices.EnterpriseLibrary.Logging.Configuration.CustomTraceListenerData, Microsoft.Practices.EnterpriseLibrary.Logging" writeLogSQL="insert into ExceptionLog(Message,LogDate,ExceptionLevel,Exception) values(@Message,@LogDate,@Level,@Exception)" type="EntLibStudy.Helper.EntLibExtension.ExceptionExtension.ExceptionCustomerListener, EntLibStudy.Helper, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" traceOutputOptions="None" name="Custom Trace Listener" initializeData="" formatter="Text Formatter" /> <add name="Event Log Listener" type="Microsoft.Practices.EnterpriseLibrary.Logging.TraceListeners.FormattedEventLogTraceListener, Microsoft.Practices.EnterpriseLibrary.Logging, Version=5.0.414.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" listenerDataType="Microsoft.Practices.EnterpriseLibrary.Logging.Configuration.FormattedEventLogTraceListenerData, Microsoft.Practices.EnterpriseLibrary.Logging, Version=5.0.414.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" source="Enterprise Library Logging" formatter="Text Formatter" log="" machineName="." traceOutputOptions="None" /> <add name="Rolling Flat File Trace Listener" type="Microsoft.Practices.EnterpriseLibrary.Logging.TraceListeners.RollingFlatFileTraceListener, Microsoft.Practices.EnterpriseLibrary.Logging, Version=5.0.414.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" listenerDataType="Microsoft.Practices.EnterpriseLibrary.Logging.Configuration.RollingFlatFileTraceListenerData, Microsoft.Practices.EnterpriseLibrary.Logging, Version=5.0.414.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" fileName="rolling.log" formatter="Text Formatter" rollInterval="Day" /> </listeners> <formatters> <add type="Microsoft.Practices.EnterpriseLibrary.Logging.Formatters.TextFormatter, Microsoft.Practices.EnterpriseLibrary.Logging, Version=5.0.414.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" template="Timestamp: {timestamp}{newline} Message: {message}{newline} Category: {category}{newline} Priority: {priority}{newline} EventId: {eventid}{newline} Severity: {severity}{newline} Title:{title}{newline} Machine: {localMachine}{newline} App Domain: {localAppDomain}{newline} ProcessId: {localProcessId}{newline} Process Name: {localProcessName}{newline} Thread Name: {threadName}{newline} Win32 ThreadId:{win32ThreadId}{newline} Extended Properties: {dictionary({key} - {value}{newline})}" name="Text Formatter" /> </formatters> <categorySources> <add switchValue="All" name="General"> <listeners> <add name="Custom Trace Listener" /> </listeners> </add> </categorySources> <specialSources> <allEvents switchValue="All" name="All Events" /> <notProcessed switchValue="All" name="Unprocessed Category" /> <errors switchValue="All" name="Logging Errors & Warnings"> <listeners> <add name="Event Log Listener" /> <add name="Rolling Flat File Trace Listener" /> </listeners> </errors> </specialSources> </loggingConfiguration>
在配置完后我们就可以进行代码编写,在页面里进行异常控制。
在ASP.NET中,异常处理主要有4种,执行顺序为:Page_Error事件>ErrorPage属性>Application_Error事件> <customErrors>,我这边采用Page_Error,由于在本项目中我已经建立了BasePage,所有的页面都继承这个页面,所以我只需在这个页面中编写Page_Error事件:
protected void Page_Error(object sender, EventArgs e) { //获取最新的异常信息 var ex = Server.GetLastError(); //处理异常 HandleException(ex, "ExceptionPolicy"); //清空异常 Server.ClearError(); } /// <summary> /// 异常处理方法 /// </summary> /// <param name="ex">异常信息</param> /// <param name="policy">异常处理策略</param> protected void HandleException(Exception ex, string policy) { bool rethrow = false; var exManager = EnterpriseLibraryContainer.Current.GetInstance<ExceptionManager>(); rethrow = exManager.HandleException(ex, policy); if (rethrow) { this.RedirectPermanent("~/error.aspx"); } }
其中exManager.HandleException(ex, policy)为根据策略名处理异常,我这边使用的ExceptionPolicy,这个策略的处理方式为异常日志记录,它会帮我们调用到我们自定义的ExceptionCustomerListener 类,进行异常日志记录。
这样我们就完成了统一捕获系统中发生的异常了,本文也到此结束,欢迎大家指点!
--------------------------------文章扩展分割线-----------------------------------------------
当然企业库中的Exception Handle和Logging模块远不止这些,Exception Handle还提供了异常替换(将指定的异常替换成其他的异常信息),异常包装(将一个异常包装到另外一个异常当中)
Logging模块提供了许多的记录方式,如文本,XML,邮件,消息队列等等,所以我们可以根据我们的需求自由的选择。
本文仅仅就我的实际项目需求进行了简单的扩展,所以可能还有许多的不足,大家可根据自己的需求进行研究扩展,如果大家有好的异常记录处理办法可以提出来让我借鉴下。:)
PS:我在文中提到了企业库的Logging模块太繁琐,大家可以看下我的使用log4net完成程序异常日志记录(使用SQLite数据库记录和普通文本记录)这篇文章,如果仅仅是要进行系统的异常记录的话log4net是个不错的选择,配置又方便而且也轻便,单若想完整使用企业库的功能的话就还是使用Exception Handle+Logging这个组合了。
注意:
1、MSSQL数据库在DataBase目录下(需要自行附加数据库),SQLite数据库在Web目录的App_Data下,由于考虑到项目的大小,所以每个项目的BIN目录都已经删除,如出现无法生成项目请自行添加相关企业库的DLL。
2、由于微软企业库5.0 学习之路这个系列我是准备以一个小型项目的形式介绍企业库的各模块,所以源代码会根据系列文章的更新而更新,所以源代码不能保证与文章中所贴代码相同。
3、项目开发环境为:VS2010+SQL2005。
4、管理员帐户:admin
密码:admin
源代码下载地址:点我下载