异常处理是系统开发中的一个重要环节。好的异常处理流程,可以构建稳定的、可靠的系统,并且有很好的用户体验。而差的异常处理,可能造成系统的崩溃、带来安全隐患。有关异常处理的重要性,就不再赘述,下面还是叙述一下我对使用asp.net开发Web应用中异常处理的一些想法。
如果需要对异常进行分类,从异常处理的角度来说,应该只有两类:
可处理的异常
未处理异常
从字面的含义理解,“可处理的异常”就是开发人员可以预期的,并有手段进行处理的异常;而“未处理异常”是在开发过程中不能预期的异常。
在对一个异常进行分类时,需要根据不同的异常类型、不同的时间、不同的代码、甚至于不同的开发人员等因素综合考虑,不能一概而论。例如一个写文件的操作,可能会引发FileNotFoundException,如果说方法的调用者预期到了这种异常,他就可以针对这种异常编写相应的处理代码(例如创建一个新的文件),然后在继续操作。在这种情况下,FileNotFoundException就是一个“可处理的异常”。而同样的方法,不同的调用者没有预期到会出现此异常,也没有针对异常编写相应的处理代码,那么这个异常就会成为系统运行的一个不稳定的因素,这时,FileNotFoundException就是“未处理异常”。
“应用程序应该只处理自己能够理解的异常”——这是《.net设计规范》一书中提及的一段话,被我奉为异常处理的一个原则。
那到底什么才是“应用程序能够理解的异常”呢?
以asp.net 2.0中的MembershipUser.ChangePassword 方法为例,MSDN文档中对该方法可能出现的异常有这样的描述:
异常类型 条件
System.ArgumentException oldPassword 是空字符串。- 或 - newPassword 是空字符串。
System.ArgumentNullException oldPassword 为 空引用(在 Visual Basic 中为 Nothing)。- 或 - newPassword 为 空引用(在 Visual Basic 中为 Nothing)。
也就说,我们如果需要使用ChangePassword方法,就有可能会出现两种异常System.ArgumentException和System.ArgumentNullException,我们可以将该异常归结为“应用程序能够理解的异常”,编写相应的代码,在用户没有输入Old Password的时候,提示用户需要输入Old Password。
到目前为止,针对MembershipUser.ChangePassword 方法,System.ArgumentException和System.ArgumentNullException就是“应用程序能够理解的异常”。按照上面所描述的内容,可以编写这样修改密码的程序。
ChangePassword.aspx(省略校验两次新密码是否相同的代码)
2 <table>
3 <tr>
4 <td>Old Password:</td>
5 <td><asp:TextBox runat="server" ID="txtOldPassword" TextMode="password"></asp:TextBox></td>
6 </tr>
7 <tr>
8 <td>New Password:</td>
9 <td><asp:TextBox runat="server" ID="txtNewPassword" TextMode="password"></asp:TextBox></td>
10 </tr>
11 <tr>
12 <td>Confirm New Password:</td>
13 <td><asp:TextBox runat="server" ID="txtNewPassword2" TextMode="password"></asp:TextBox></td>
14 </tr>
15 <tr>
16 <td colspan="2" align="center">
17 <asp:Button runat="server" ID="btnChangePassword" Text="Change Password" OnClick="btnChangePassword_Click" />
18 </td>
19 </tr>
20 </table>
ChangePassword.aspx.cs
{
LabelMessage.Text = "";
try
{
MembershipUser user = Membership.GetUser(this.User.Identity.Name);
if (user.ChangePassword(txtOldPassword.Text, txtNewPassword.Text))
{
LabelMessage.Text = "修改密码成功";
}
else
{
LabelMessage.Text = "修改密码失败";
}
}
catch (ArgumentNullException ex)
{
LabelMessage.Text = ex.Message; //oldPassword 为 空引用(在 Visual Basic 中为 Nothing)。- 或 - newPassword 为 空引用(在 Visual Basic 中为 Nothing)。
}
catch (ArgumentException ex)
{
LabelMessage.Text = ex.Message; //oldPassword 是空字符串。- 或 - newPassword 是空字符串。
}
}
通过上面的程序逻辑的处理,可以得到下面的结果:
显然,这不是我们所期望的程序,为了有好的用户体验,我们需要增加Validator控件,在浏览器端对用户输入的内容进行非空校验。为此,程序修改为如下的形式:
ChangePassword.aspx(增加了必填校验控件)
<asp:ValidationSummary ID="ValidationSummary1" runat="server" />
<table>
<tr>
<td>Old Password:</td>
<td>
<asp:TextBox runat="server" ID="txtOldPassword" TextMode="password"></asp:TextBox>
<asp:RequiredFieldValidator runat="server" ID="rfvOldPassword" ControlToValidate="txtOldPassword" ErrorMessage="Please input the old password.">*</asp:RequiredFieldValidator>
</td>
</tr>
<tr>
<td>New Password:</td>
<td>
<asp:TextBox runat="server" ID="txtNewPassword" TextMode="password"></asp:TextBox>
<asp:RequiredFieldValidator runat="server" ID="rfvNewPassword" ControlToValidate="txtNewPassword" ErrorMessage="Please input the new password.">*</asp:RequiredFieldValidator>
</td>
</tr>
<tr>
<td>Confirm New Password:</td>
<td>
<asp:TextBox runat="server" ID="txtNewPassword2" TextMode="password"></asp:TextBox>
<asp:RequiredFieldValidator runat="server" ID="rfvNewPassword2" ControlToValidate="txtNewPassword2" ErrorMessage="Please confirm the new password.">*</asp:RequiredFieldValidator>
</td>
</tr>
<tr>
<td colspan="2" align="center">
<asp:Button runat="server" ID="btnChangePassword" Text="Change Password" OnClick="btnChangePassword_Click" />
</td>
</tr>
</table>
ChangePassword.aspx.cs(增加了对Validator控件的判断)
{
return;
}
try
{
LabelMessage.Text = "";
MembershipUser user = Membership.GetUser(this.User.Identity.Name);
if (user.ChangePassword(txtOldPassword.Text, txtNewPassword.Text))
{
LabelMessage.Text = "修改密码成功";
}
else
{
LabelMessage.Text = "修改密码失败";
}
}
catch (ArgumentNullException ex)
{
LabelMessage.Text = ex.Message; //oldPassword 为 空引用(在 Visual Basic 中为 Nothing)。- 或 - newPassword 为 空引用(在 Visual Basic 中为 Nothing)。
}
catch (ArgumentException ex)
{
LabelMessage.Text = ex.Message; //oldPassword 是空字符串。- 或 - newPassword 是空字符串。
}
如此一来,如果用户没有填写参数,可会有如下的提示信息。
在这种情况下,MembershipUser.ChangePassword 方法永远都不会抛出ArgumentNullException或者ArgumentException,上面的异常处理程序以及成为一种摆设,可有可无。如果说这时认为Change Password程序已经处理了“应用程序能够理解的异常”,并且进行了部署,那么,在接下来的一段时间里,你可能不断收到有关Change Password功能的BUG报告,大致内容如下:
是的,文档欺骗了我们。根据文档的描述,这样但odlPassword或者newPassword为空(包括空字符串)的时候,才可能抛出ArgumentNullException或ArgumentException,而我们已经在代码中进行了相应的处理,保证不会传递空参数,也就是说根据文档的描述,可以不用再捕获ArgumentNullException或者ArgumentException,但现在的结果是在输入的新密码长度不够的情况下,也可能导致System.ArgumentException。
Microsoft的官方文档都不能准确的描述某一个方法调用过程中可能出现的所有的异常情况。显然在我们的实际开发过程中很难针对某一个接口方法,描述所有可能出现的异常情况。也就是说,我们不能根据文档鉴定“应用程序能够理解的异常”。
那到底应该怎么定义“应用程序能够理解的异常”呢?
很简单,就是在代码开发或维护过程中,人员能够从文档查阅到的、凭经验想到的或者BUG中需要修复的,所有这些已知的异常,都属于“应用程序能够理解的异常”。而那些实际存在的异常,尚未被开发人员所觉察的,就是“未处理异常”,不管这个异常是什么类型,简单或者复杂。
正如上面所描述的情况,我们在实际的开发过程中,无法预期一切可能出现的异常,即使你有详尽的文档,也可能出现文档疏漏的情况。
既然无法预期所有的异常,那就针对开发过程中已知的异常编写异常处理的代码,所有“未处理异常”都记录到日志文件中,当作bug进行修改。按照这样的过程,经过一段时间的测试和运行,系统就可以稳定。
有些异常是可能出现的,但是因为一下判断或者实际调用的环境不同,永远也不可能在系统中出现,这时,对其进行的处理代码就可能变成冗余代码。
总结上面的描述,我认为异常处理应该是一个循序渐进的过程,“应用程序应该只处理自己能够理解的异常”是这个过程的原则。根据文档的全面性、代码编写者的经验,异常处理的过程可能颇为不同。
日志在系统中扮演非常重要的角色,使用日志记录所有“未处理异常”,然后在有针对地逐步将这些“未处理异常”转变为“可处理异常”,通过一段时间的测试和运行,系统就可以达到稳定的状态。