个人觉得异常处理对于程序员来说是最为熟悉的同时也是最难掌握的。说它熟悉,因为仅仅就是try/catch/finally而已。说它难以掌握,则是因为很多开发人员却说不清楚try/catch/finally应该置于何处?什么情况下需要对异常进行日志记录?什么情况下需要对异常进行封装?什么情况下需要对异常进行替换?对于捕获的异常,在什么情况下需要将其再次抛出?什么情况下则不需要?
合理的异常处理应该是场景驱动的,在不同的场景下,采用的异常处理策略往往是不同的。异常处理的策略应该是可配置的,因为应用程序出现怎样的异常往往是不可预测的,现有异常策略的不足往往需要在真正出现某种异常的时候才会体现出来,所以我们需要一种动态可配置的异常处理策略维护方式。目前有一些开源的异常处理框架提供了这种可配置的、场景驱动的异常处理方式,EntLib的Exception Handling Application Block(以下简称EHAB)就是一个不错的选择。[源代码从这里下载][本文已经同步到《How ASP.NET MVC Works?》中]
目录 一、通过指定Handle-Error-Action响应请求 二、通过Error View显示错误消息 三、自动创建JsonResult响应Ajax请求
一、通过指定Handle-Error-Action响应请求
在正式介绍如何通过扩展实现与EntLib以实现自动化异常处理之前,我们不妨先来体验一下异常处理具有怎样的“自动化”特性。以用户登录场景为例,我们在通过Visual Studio的ASP.NET MVC项目模板创建的Web应用中定义了如下一个简单的数据类型LoginInfo封装用户登录需要输入的用户名和密码。
1: public class LoginInfo
2: {
3: [DisplayName("用户名")]
4: [Required(ErrorMessage="请输入{0}")]
5: public string UserName { get; set; }
6:
7: [DisplayName("密码")]
8: [Required(ErrorMessage = "请输入{0}")]
9: [DataType(DataType.Password)]
10: public string Password { get; set; }
11: }
然后我们定义了如下一个HomeController。基于HTTP-GET的Action方法Index将会呈现一个用户登录View,该View使用创建的LoginInfo对象作为其Model。真正的用户验证逻辑定义在另一个应用了HttpPostAttrubute特性的Index方法中:如果用户名不为Foo,抛出InvalidUserNameException异常;如果密码不是“password”,则抛出InvalidPasswordException异常。InvalidUserNameException和InvalidPasswordException是我们自定义的两种异常类型。
1: [ExceptionPolicy("defaultPolicy")]
2: public class HomeController : ExtendedController
3: {
4: public ActionResult Index()
5: {
6: return View(new LoginInfo());
7: }
8:
9: [HttpPost]
10: [HandleErrorAction("OnIndexError")]
11: public ActionResult Index(LoginInfo loginInfo)
12: {
13: if (string.Compare(loginInfo.UserName, "foo", true) != 0)
14: {
15: throw new InvalidUserNameException();
16: }
17:
18: if (loginInfo.Password != "password")
19: {
20: throw new InvalidPasswordException();
21: }
22: return View(loginInfo);
23: }
24:
25: [HttpPost]
26: public ActionResult OnIndexError(LoginInfo loginInfo)
27: {
28: return View(loginInfo);
29: }
30: }
上面定义的HomeController具有三点与自动化异常处理相关的地方:
- HomeController继承自自定义的基类ExtendedController,后者完成了对异常的自动化处理。
- HomeController类型上应用了自定义的ExceptionPolicyAttribute特性用于指定默认采用的异常处理策略名称(“defaultPolicy”)。
- 基于HTTP-POST的Index方法上应用了HandleErrorActionAttribute特性用于指定一个Handle-Error-Action名称,当异常在目标Action执行过程中抛出并通过EHAB处理后,指定的Action会被执行以实现对请求的响应。对于我们的例子来说,从Index方法抛出的异常被处理后会调用OnIndexError方法作为对当前请求的响应。
下面是代表登录页面的View的定义,这是一个Model类型为LoginInfo的强类型View。在该View中,作为Model的LoginInfo对象以编辑默认呈现在一个表单中,表单中提供了一个“登录”提交表单。除此之外,View中还具有个ValidationSummary。
1: @model LoginInfo
2: <html>
3: <head>
4: <title>用户登录</title>
5: <style type="text/css">
6: .validation-summary-errors{color:Red}
7: </style>
8: </head>
9: <body>
10: @using (Html.BeginForm())
11: {
12: @Html.ValidationSummary(true)
13: @Html.EditorForModel()
14: <input type="submit" value="登录" />
15: }
16: </body>
17: </html>
通过HomeController的定义我们知道两种不同类型的异常(InvalidUserNameException和InvalidPasswordException)分别在输入无效用户名和密码是被抛出来,而我们需要处理的就是这两种类型的异常。正对它们的异常处理策略定义在如下的配置中,策略名称就是通过应用在HomeController上的ExceptionPolicyAttribute特性指定的“defaultPolicy”。
1: <configuration>
2: <configSections>
3: <section name="exceptionHandling"
4: type="Microsoft.Practices.EnterpriseLibrary.ExceptionHandling.Configuration.ExceptionHandlingSettings, Microsoft.Practices.EnterpriseLibrary.ExceptionHandling" />
5: </configSections>
6: <exceptionHandling>
7: <exceptionPolicies>
8: <add name="defaultPolicy">
9: <exceptionTypes>
10: <add type="MvcApp.InvalidUserNameException, MvcApp" postHandlingAction="ThrowNewException" name="InvalidUserNameException">
11: <exceptionHandlers>
12: <add name ="ErrorMessageHandler" type="MvcApp.ErrorMessageHandler, MvcApp" errorMessage="用户名不存在"/>
13: </exceptionHandlers>
14: </add>
15:
16: <add type="MvcApp.InvalidPasswordException, MvcApp" postHandlingAction="ThrowNewException" name="InvalidPasswordException">
17: <exceptionHandlers>
18: <add name ="ErrorMessageHandler" type="MvcApp.ErrorMessageHandler, MvcApp" errorMessage="密码与用户名不匹配"/>
19: </exceptionHandlers>
20: </add>
21: </exceptionTypes>
22: </add>
23: </exceptionPolicies>
24: </exceptionHandling>
25: ...
26: </configuration>
通过上面的这样异常策略配置可以看到:我们使用一个自定义的名为ErrorMessageHandler的ExceptionHandler来处理抛出来的InvalidUserNameException和InvalidPasswordException异常,而ErrorMessageHandler仅仅是指定一个友好的错误消息,该消息一般会呈现给最终的用户。运行该程序后一个用于登录页面会呈现出来,当我们输入错误的用户名和密码的时候,相应的错误消息(在配置中通过ErrorMessageHandler设置的错误消息)会以如图7-16所示的效果显示出来,其实整个View是通过执行Action方法OnIndexError返回的ViewResult呈现出来的。
二、通过Error View显示错误消息
除了通过执行对应的Handle-Error-Action来呈现异常处理后的最终结果之外,还支持错误页面的错误呈现方法。简单起见,我们只是用名称为Error的View来作为最终的错误页面。为了演示基于错误页面的呈现方式,我们按照如下的方式重新定义了\Views\Shared\目录下的Error.cshtml。
1: @model ExtendedHandleErrorInfo
2: @{
3: Layout = null;
4: }
5: <!DOCTYPE html>
6: <html>
7: <head>
8: <meta name="viewport" content="width=device-width" />
9: <title>Error</title>
10: <style type="text/css">
11: h3 {color:Red;}
12: </style>
13: </head>
14: <body>
15: <h3>
16: @Html.DisplayFor(m=>m.ErrorMessage)
17: </h3>
18: <ul>
19: <li>Controller: @Html.DisplayFor(m => m.ControllerName)</li>
20: <li>Action: @Html.DisplayFor(m => m.ActionName)</li>
21: <li>Exception:
22: <ul>
23: <li>Message: @Html.DisplayFor(m => m.Exception.Message)</li>
24: <li>Type: @Model.Exception.GetType().FullName</li>
25: <li>StackTrace: @Html.DisplayFor(m => m.Exception.StackTrace)</li>
26: </ul>
27: </li>
28: </ul>
29: </body>
30: </html>
上面这个View的Model类型是具有如下定义的ExtendedHandleErrorInfo。它继承自HandleErrorInfo,只额外定义了一个表示错误消息的ErrorMessage属性。在上面的这个View中,我们将错误消息、异常类型和StackTrace和当前Controller/Action的名称呈现出来。
1: public class ExtendedHandleErrorInfo : HandleErrorInfo
2: {
3: public string ErrorMessage { get; private set; }
4: public ExtendedHandleErrorInfo(Exception exception, string controllerName, string actionName, string errorMessage)
5: : base(exception, controllerName, actionName)
6: {
7: this.ErrorMessage = errorMessage;
8: }
9: }
当利用EntLib的EHAB对从Index方法中抛出的异常进行处理后采用错误View的方式来响应请求,我们需要按照如下的方式将应用在该方法上的HandleErrorActionAttribute特性注释掉。
1: [ExceptionPolicy("defaultPolicy")]
2: public class HomeController : ExtendedController
3: {
4: //其他成员
5: [HttpPost]
6: //[HandleErrorAction("OnIndexError")]
7: public ActionResult Index(LoginInfo loginInfo)
8: {
9: //省略实现
10: }
11: }
再次运行该程序并分别输入错误的用户名和密码后,默认的错误View(Error.cshtml)将会以如下图所示地效果把处理后的异常结果呈现出来。
三、自动创建JsonResult响应Ajax请求
用于实施认证的Action方法Index可以通过普通的HTTP-POST的形式来调用,同样也可以通过Ajax请求的方式来调用。对于Ajax请求来说,我们最终会将通过EntLib处理后的异常封装成如下一个类型为ExceptionDetail的对象。如下面的代码片断所示,ExceptionDetail具有与Exception对应的属性设置。最终根据抛出异常对象创建的ExceptionDetail对象会被用于创建一个JsonResult对象对当前Ajax请求予以响应。
1: public class ExceptionDetail
2: {
3: public ExceptionDetail(Exception exception,string errorMessage=null)
4: {
5: this.HelpLink = exception.HelpLink;
6: this.Message = string.IsNullOrEmpty(errorMessage) ? exception.Message : errorMessage;
7: this.StackTrace = exception.StackTrace;
8: this.Type = exception.GetType().ToString();
9: if (exception.InnerException != null)
10: {
11: this.InnerException = new ExceptionDetail(exception.InnerException);
12: }
13: }
14:
15: public string HelpLink { get; set; }
16: public ExceptionDetail InnerException { get; set; }
17: public string Message { get; set; }
18: public string StackTrace { get; set; }
19: public string Type { get; set; }
20: }
当客户端接收到回复的Json对象后,可以通过检测其是否具有一个ExceptionType属性(对于一个ExceptionDetail对象来说,该属性不可能为Null)来判断是否发生异常。作为演示我们对Action方法Index对应的View进行了如下的改动。
1: @model LoginInfo
2: <html>
3: <head>
4: <title>用户登录</title>
5: <script type="text/javascript" src="@Url.Content("~/Scripts/jquery-1.6.2.js")"></script> 1:
2: <script type="text/javascript" src="@Url.Content("~/Scripts/jquery.unobtrusive-ajax.js")">
1: </script>
2: <script type="text/javascript">
3: function login(data) {
4: if (data.ExceptionType) {
5: alert(data.Message);
6: }
7: else {
8: alert("认证成功");
9: }
10: }
11:
</script>
6: </head>
7: <body>
8: @{
9: AjaxOptions options = new AjaxOptions{OnSuccess = "login"};
10: }
11: @using (Ajax.BeginForm(options))
12: {
13: @Html.EditorForModel()
14: <input type="submit" value="登录" />
15: }
16: </body>
17: </html>
如上面的代码片断所示,我们通过调用AjaxHelper的BuginForm生成了一个以Ajax形式提交的表单。表单成功提交(服务端因对抛出的异常进行处理而返回一个封装异常的Json对象,对于提交表单的Ajax请求来说依然属于成功提交)后会调用我们定义的回调函数login。在该JavaScript函数中,我们通过得到的对象是否具有一个ExceptionType属性来判断服务端是否抛出异常。如果抛出异常,在通过调用alert方法将错误消息显示出来,否则显示“认证成功”。我们再次运行我们的程序并分别输入不合法的用户名和密码,相应的错误消息会以对话框的形式显示出来,具体的显示效果如下图所示。
ASP.NET MVC集成EntLib实现“自动化”异常处理[实例篇] ASP.NET MVC集成EntLib实现“自动化”异常处理[实现篇] |