团队开发框架实战—异常处理
使用Exception的好处是省力,坏处是无法识别出究竟是系统异常还是业务上的错误,这有什么关系?需要分清系统异常和业务错误的原因是,你可能不想把系统内部的异常暴露给终端客户,比如给客户提示“未将对象引用设置到对象的实例”感觉如何,当然,这可能只是让客户摸不着头脑,还不是很严重,有一些异常会暴露系统的弱点,从而导致更易受攻击。
为每个业务错误创建一个自定义异常,好处是可以对异常精确定位,另外可以方便的为异常处理提供相关数据。这种方式的主要毛病是工作量很大,一般程序员都不会这么干。
现在来考虑我们一般是如何处理异常的?大部分时候,可能只是记录了一个日志,然后将该异常转换为客户端能识别的消息,客户端会把异常消息显示出来。更进一步,可能会识别出系统异常,给客户端提示一个默认消息,比如“系统忙,请稍后再试”,如果是业务错误,就直接显示给客户。
可以看到,只有在你真正需要进行特定异常处理的时候,创建业务错误对应的自定义异常才会有价值,如果你创建出来的自定义异常,仅仅记录了个日志,那就没有多大必要了。
现在的关键是你需要把系统异常和业务错误识别出来,以指示你是否应该把错误消息暴露给客户。我们可以创建一个自定义异常来表示通用的业务错误,我使用CustomException来表示这个异常,即自定义异常。
单元测试CustomExceptionTest的代码如下。
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using System.Text;
using Tdf.Excp;
using static Tdf.Excp.CustomException;
namespace Tests
{
/// <summary>
/// 应用程序异常测试
/// </summary>
[TestClass]
public class WarningTest
{
#region 常量
/// <summary>
/// 异常消息
/// </summary>
public const string Message = "A";
/// <summary>
/// 异常消息2
/// </summary>
public const string Message2 = "B";
/// <summary>
/// 异常消息3
/// </summary>
public const string Message3 = "C";
/// <summary>
/// 异常消息4
/// </summary>
public const string Message4 = "D";
#endregion
#region TestValidate_MessageIsNull(验证消息为空)
/// <summary>
/// 验证消息为空
/// </summary>
[TestMethod]
public void TestValidate_MessageIsNull()
{
CustomException warning = new CustomException(null, "P1");
Assert.AreEqual(string.Empty, warning.Message);
}
#endregion
#region TestCode(设置错误码)
/// <summary>
/// 设置错误码
/// </summary>
[TestMethod]
public void TestCode()
{
CustomException warning = new CustomException(Message, "P1");
Assert.AreEqual("P1", warning.Code);
}
#endregion
#region TestLogLevel(测试日志级别)
/// <summary>
/// 测试日志级别
/// </summary>
[TestMethod]
public void TestLogLevel()
{
CustomException warning = new CustomException(Message, "P1", LogLevel.Fatal);
Assert.AreEqual(LogLevel.Fatal, warning.Level);
}
#endregion
#region TestMessage_OnlyMessage(仅设置消息)
/// <summary>
/// 仅设置消息
/// </summary>
[TestMethod]
public void TestMessage_OnlyMessage()
{
CustomException warning = new CustomException(Message);
Assert.AreEqual(Message, warning.Message);
}
#endregion
#region TestMessage_OnlyException(仅设置异常)
/// <summary>
/// 仅设置异常
/// </summary>
[TestMethod]
public void TestMessage_OnlyException()
{
CustomException warning = new CustomException(GetException());
Assert.AreEqual(Message, warning.Message);
}
/// <summary>
/// 获取异常
/// </summary>
private Exception GetException()
{
return new Exception(Message);
}
#endregion
#region TestMessage_Message_Exception(设置错误消息和异常)
/// <summary>
/// 设置错误消息和异常
/// </summary>
[TestMethod]
public void TestMessage_Message_Exception()
{
CustomException warning = new CustomException(Message2, "P1", LogLevel.Fatal, GetException());
Assert.AreEqual(string.Format("{0}\r\n{1}", Message2, Message), warning.Message);
}
#endregion
#region TestMessage_2LayerException(设置2层异常)
/// <summary>
/// 设置2层异常
/// </summary>
[TestMethod]
public void TestMessage_2LayerException()
{
CustomException warning = new CustomException(Message3, "P1", LogLevel.Fatal, Get2LayerException());
Assert.AreEqual(string.Format("{0}\r\n{1}\r\n{2}", Message3, Message2, Message), warning.Message);
}
/// <summary>
/// 获取2层异常
/// </summary>
private Exception Get2LayerException()
{
return new Exception(Message2, new Exception(Message));
}
#endregion
#region TestMessage_Warning(设置Warning异常)
/// <summary>
/// 设置Warning异常
/// </summary>
[TestMethod]
public void TestMessage_Warning()
{
CustomException warning = new CustomException(GetWarning());
Assert.AreEqual(Message, warning.Message);
}
/// <summary>
/// 获取异常
/// </summary>
private CustomException GetWarning()
{
return new CustomException(Message);
}
#endregion
#region TestMessage_2LayerWarning(设置2层Warning异常)
/// <summary>
/// 设置2层Warning异常
/// </summary>
[TestMethod]
public void TestMessage_2LayerWarning()
{
CustomException warning = new CustomException(Message3, "", Get2LayerWarning());
Assert.AreEqual(string.Format("{0}\r\n{1}\r\n{2}", Message3, Message2, Message), warning.Message);
}
/// <summary>
/// 获取2层异常
/// </summary>
private CustomException Get2LayerWarning()
{
return new CustomException(Message2, "", new CustomException(Message));
}
#endregion
#region TestMessage_3LayerWarning(设置3层Warning异常)
/// <summary>
/// 设置3层Warning异常
/// </summary>
[TestMethod]
public void TestMessage_3LayerWarning()
{
CustomException warning = new CustomException(Message4, "", Get3LayerWarning());
Assert.AreEqual(string.Format("{0}\r\n{1}\r\n{2}\r\n{3}", Message4, Message3, Message2, Message), warning.Message);
}
/// <summary>
/// 获取3层异常
/// </summary>
private CustomException Get3LayerWarning()
{
return new CustomException(Message3, "", new Exception(Message2, new CustomException(Message)));
}
#endregion
#region 添加异常数据
/// <summary>
/// 添加异常数据
/// </summary>
[TestMethod]
public void TestAdd_1Layer()
{
CustomException warning = new CustomException(Message);
warning.Data.Add("key1", "value1");
warning.Data.Add("key2", "value2");
StringBuilder expected = new StringBuilder();
expected.AppendLine(Message);
expected.AppendLine("key1:value1");
expected.AppendLine("key2:value2");
Assert.AreEqual(expected.ToString(), warning.Message);
}
/// <summary>
/// 添加异常数据
/// </summary>
[TestMethod]
public void TestAdd_2Layer()
{
Exception exception = new Exception(Message);
exception.Data.Add("a", "a1");
exception.Data.Add("b", "b1");
CustomException warning = new CustomException(exception);
warning.Data.Add("key1", "value1");
warning.Data.Add("key2", "value2");
StringBuilder expected = new StringBuilder();
expected.AppendLine(Message);
expected.AppendLine("a:a1");
expected.AppendLine("b:b1");
expected.AppendLine("key1:value1");
expected.AppendLine("key2:value2");
Assert.AreEqual(expected.ToString(), warning.Message);
}
#endregion
}
}
CustomException的代码如下:
using System;
using System.Collections;
using System.Text;
namespace Tdf.Excp
{
public class CustomException : Exception
{
#region 构造方法
/// <summary>
/// 初始化应用程序异常
/// </summary>
/// <param name="message">错误消息</param>
public CustomException(string message)
: this( message, "" ) {
}
/// <summary>
/// 初始化应用程序异常
/// </summary>
/// <param name="message">错误消息</param>
/// <param name="code">错误码</param>
public CustomException(string message, string code)
: this( message, code, LogLevel.Warning ) {
}
/// <summary>
/// 初始化应用程序异常
/// </summary>
/// <param name="message">错误消息</param>
/// <param name="code">错误码</param>
/// <param name="level">日志级别</param>
public CustomException(string message, string code, LogLevel level)
: this( message, code, level, null ) {
}
/// <summary>
/// 初始化应用程序异常
/// </summary>
/// <param name="exception">异常</param>
public CustomException(Exception exception)
: this( "", "", LogLevel.Warning, exception ) {
}
/// <summary>
/// 初始化应用程序异常
/// </summary>
/// <param name="message">错误消息</param>
/// <param name="code">错误码</param>
/// <param name="exception">异常</param>
public CustomException(string message, string code, Exception exception)
: this( message, code, LogLevel.Warning, exception ) {
}
/// <summary>
/// 初始化应用程序异常
/// </summary>
/// <param name="message">错误消息</param>
/// <param name="code">错误码</param>
/// <param name="level">日志级别</param>
/// <param name="exception">异常</param>
public CustomException(string message, string code, LogLevel level, Exception exception)
: base( message ?? "", exception ) {
Code = code;
Level = level;
_message = GetMessage();
}
/// <summary>
/// 获取错误消息
/// </summary>
private string GetMessage()
{
var result = new StringBuilder();
AppendSelfMessage(result);
AppendInnerMessage(result, InnerException);
return result.ToString().TrimEnd(Environment.NewLine.ToCharArray());
}
/// <summary>
/// 添加本身消息
/// </summary>
private void AppendSelfMessage(StringBuilder result)
{
if (string.IsNullOrWhiteSpace(base.Message))
return;
result.AppendLine(base.Message);
}
/// <summary>
/// 添加内部异常消息
/// </summary>
private void AppendInnerMessage(StringBuilder result, Exception exception)
{
if (exception == null)
return;
if (exception is CustomException)
{
result.AppendLine(exception.Message);
return;
}
result.AppendLine(exception.Message);
result.Append(GetData(exception));
AppendInnerMessage(result, exception.InnerException);
}
/// <summary>
/// 获取添加的额外数据
/// </summary>
private string GetData(Exception ex)
{
var result = new StringBuilder();
foreach (DictionaryEntry data in ex.Data)
result.AppendFormat("{0}:{1}{2}", data.Key, data.Value, Environment.NewLine);
return result.ToString();
}
#endregion
#region Message(错误消息)
/// <summary>
/// 错误消息
/// </summary>
private readonly string _message;
/// <summary>
/// 错误消息
/// </summary>
public override string Message
{
get
{
if (Data.Count == 0)
return _message;
return _message + Environment.NewLine + GetData(this);
}
}
#endregion
#region TraceId(跟踪号)
/// <summary>
/// 跟踪号
/// </summary>
public string TraceId { get; set; }
#endregion
#region Code(错误码)
/// <summary>
/// 错误码
/// </summary>
public string Code { get; set; }
#endregion
#region Level(日志级别)
/// <summary>
/// 日志级别
/// </summary>
public LogLevel Level { get; set; }
public enum LogLevel
{
Debug, Info, Warning, Error, Fatal
}
#endregion
#region StackTrace(堆栈跟踪)
/// <summary>
/// 堆栈跟踪
/// </summary>
public override string StackTrace
{
get
{
if (!string.IsNullOrWhiteSpace(base.StackTrace))
return base.StackTrace;
if (base.InnerException == null)
return string.Empty;
return base.InnerException.StackTrace;
}
}
#endregion
}
}
需要注意的是,除了给CustomException添加了一些有用的属性以外,还重写了Message属性。这是因为当一个异常被抛出以后,其它代码可能会进行拦截,之后这些代码会抛出自己的异常,并把之前的异常包装在自己内部,所以你要访问之前的异常,就需要通过递归的方式访问InnerException,直到InnerException为null。所以大家会在后面看到CustomException类不仅被用来充当业务异常,还是一个获取异常全部消息的公共操作类。
最后,再补充一个重构小知识,观察CustomException的代码,多个构造方法中,只有参数最多的方法实现了功能,其它构造方法挨个调用,这称为链构造函数。这个手法对于重载方法都适用,不要在每个方法中重复实现代码,把实现代码放到参数最多的方法中,其它重载只是该方法提供了默认值的版本而已。