通过 AOP 改进异常处理






4.60/5 (6投票s)
使用 AOP 进行异常处理可以减少异常处理的代码量,从而提高应用程序的敏捷性和代码的可维护性。
目录
引言
使用 AOP 进行异常处理的概念已经存在一段时间了。参考文献中列出的两篇论文指出了这种方法的优点,例如将异常处理的代码量减少了 4 倍,更好地支持不同的异常行为配置等。关于这个主题的大多数文章都基于 Java 的 AspectJ。本文尝试使用 C# 和 Spring.Net AOP 提供一个具体的示例。
问题
假设我们正在编写一个数据访问对象(DAO)OrderDAO
,如下所示。想象一下Logger
是一个 Log4Net 的ILog
对象。当发生DbException
时,我们希望记录一些调试信息。在这种简单的情况下,我们记录参数的ToString
。
public class OrderDAO: IOrderDAO
{
...
public void UpdateOrder(Order order){
try{
Foo();
}catch(DbException e){
Logger.Debug(""+order);
throw;
}
}
public void GetOrder(int OrderID)
{
try{
Foo2();
}catch(DbException e){
Logger.Debug(""+OrderID);
throw;
}
}
/// <summary>
/// <throws>DbException</throws>
/// </summary>
private void Foo()
{
// throw instance of DbException
}
/// <summary>
/// <throws>DbException</throws>
/// </summary>
private void Foo2()
{
// throw instance of DbException
}
}
以当今的 IDE,我们可以很快地编写出这样的异常处理代码。一天结束时,我们感到很满足,因为我们处理了许多异常。
但是,等等。这段代码有几个问题。所有catch
块中的代码都很相似。它是“记录调试信息并重新抛出异常”这一关注点的体现。重复很少是好事。在这种传统方法中,ToString
可能无法返回关于参数对象的全部信息。如果对象很大,很难记录所有信息。如果我们向Order
类添加更多属性,我们很可能会忘记更新catch
块。然后我们就无法获得新的属性用于调试。有没有一种方法可以整合重复的异常处理代码?
整合 Aspect 中的重复异常处理
让我们看看 AOP 如何消除重复并为我们提供更详尽的调试信息。
IThrowsAdvice
在 Spring.Net AOP 模块中,IThrowsAdvice
的AfterThrowing
在被拦截的方法抛出异常后被调用。AfterThrowing
方法使我们能够访问抛出异常的target
和方法参数。我们的 advice 将实现此接口。抛出的异常在AfterThrowing
之后继续传播到上层调用堆栈。
LogArgumentsThrowsAdvice
这个 advice 所做的是遍历参数并调用ILog.Debug(object)
。这很简单。代码本身更能说明问题。
class LogArgumentsThrowsAdvice : IThrowsAdvice
{
private ILog Logger;
public LogArgumentsThrowsAdvice()
{
log4net.Config.XmlConfigurator.Configure();
Logger = LogManager.GetLogger(this.GetType().Name);
}
public void AfterThrowing(MethodInfo method, Object[] args,
Object target, Exception exception)
{
if (!(exception is DbException))
return;
if (args != null && args.Length > 0)
{
foreach (Object arg in args)
{
Logger.Debug(""+arg);
}
}
}
}
绑定 Aspect 和 Target
像往常一样,我们使用一个 aspect,ExceptionHandlingAspect
来组织 advice 和 pointcut。请注意,Spring.Net 没有Aspect
类这样的概念。这是一种组织 aspect、advices 和 pointcuts 的个人方式。ExceptionHandlingAspect
有一个LogArgumentsThrowsAdvice
类型的实例变量。下面的类图显示了它们之间的关系。

GetProxy
方法是进行拦截的地方。参数methodRE
是一个正则表达式,用于创建SdkRegularExpressionMethodPointcut
,Spring.Net 使用它来选择target
中要拦截的方法。pointcut 和 advice 被组合成一个类型为DefaultPointcutAdvisor
的 advisor。请注意,advisor 表示 aspect 的一个模块。advisor 被添加到ProxyFactory
。通过调用ProxyFactory
的GetProxy()
方法获得代理对象。
private LogArgumentsThrowsAdvice logArgumentsThrowsAdvice;
public static object GetProxy(object target, string methodRE)
{
SdkRegularExpressionMethodPointcut reMethodPointcut =
new SdkRegularExpressionMethodPointcut(methodRE);
ProxyFactory proxyFactory = new ProxyFactory(target);
DefaultPointcutAdvisor exceptionHandlingAdvisor =
new DefaultPointcutAdvisor(reMethodPointcut, logArgumentsThrowsAdvice);
proxyFactory.AddAdvisor(exceptionHandlingAdvisor);
return proxyFactory.GetProxy();
}
如果您熟悉 Spring.Net 配置文件,可以将上述逻辑抽象到配置文件中,让 Spring.Net IOC 容器为您创建代理。这样,您就可以在部署时配置异常如何被处理。
Aspect 运行
下图显示了 aspect 的运行。请注意advisor
的构造型。它拦截所有对被 advice 对象(例如IOrderDAO
)的调用(步骤 1.1)。如果方法调用符合 advisor 中 pointcut 的规范,它还会将调用重定向到其 advice(步骤 1.2)。

新的 OrderDAO
现在异常处理代码已整合到一个 advice 中,OrderDAO
也更整洁了。
public class OrderDAO: IOrderDAO
{
...
public void UpdateOrder(Order o)
{
Foo();
}
public void GetOrder(int OrderID)
{
Foo2();
}
}
重构以改进异常处理
现在我们可以轻松地改进应用程序中的异常处理了。让我们从查看如何改进对调试的支持开始。
ToString
的返回值通常没有足够的调试信息。大多数时候,重要的调试信息都在未记录的内容之中。好吧,如果我们持久化整个参数对象呢?由于我们将异常处理代码整合到了一个 advice 中,我们需要做的是创建一个新的 advice 并应用它。酷吧?
引入 SerializeArgumentsThrowsAdvice
我们都对代码有着特殊的感情。让我们来看看代码。
/// <summary>
/// SerializeArgumentsThrowsAdvice is an around advice which
/// persists debug information.
///
/// </summary>
class SerializeArgumentsThrowsAdvice : IThrowsAdvice
{
private BinaryFormatter binaryFormatter = new BinaryFormatter();
/// <summary>
/// Persists the target and arguments, if any, to file system.
/// </summary>
public void AfterThrowing(MethodInfo method, Object[] args,
Object target, Exception exception)
{
//this.Serialize(target);
if(!(exception is DbException))
return;
if (args != null && args.Length > 0)
{
foreach (Object arg in args)
{
Serialize(arg);
}
}
}
protected virtual void Serialize(object obj)
{
if (obj == null)
return;
FileStream fileStream = null;
try
{
String fileName = "" + obj.GetType() + "_" + obj.GetHashCode() + ".dat";
fileStream = File.Create(fileName);
binaryFormatter.Serialize(fileStream, obj);
}
catch (Exception e)
{
System.Console.WriteLine(e.StackTrace);
}
finally
{
if (fileStream != null)
{
fileStream.Close();
}
}
}
}
它与LogArgumentsThrowsAdvice
非常相似,只是我们调用了Serialize
而不是。假设参数被标记为 Serializable,Serialize(object)
使用System.Runtime.Serialization.Formatters.Binary
命名空间中的BinaryFormatter
将对象持久化到文件系统。
要应用这个新的 advice,请修改示例项目中ExceptionHandlingAspect
的GetProxy
。将logArgumentsException
替换为serializeArgumentsThrowsAdvice
。或者,您可以重写GetProxy
,使其可以从 Spring.Net 配置文件进行配置。
下图所示的序列化 Order 对象是由SerializeArgumentsThrowsAdvice
对象创建的,该对象拦截Foo.UpdateOrder(Order o)
方法。现在,只需反序列化并将它们带回活动的 .NET CLR,我们就可以检查Order
参数的内部以调试问题。

序列化参数
您可能会争辩说,如果参数无法序列化怎么办?正如 advice 的名称所示,它序列化参数。您可能需要根据自己的需求开发和使用适当的 advice。例如,如果参数的属性和target
满足您的需求,您可以使用 .NET 反射来持久化属性,或者使用替代的序列化器,例如AltSerializer。或者,您可以在设计中强制实现ToString
,并实现一个持久化ToString
的 advice。
其他异常处理策略
Enterprise Library 包含一个异常处理块,您可以在其中使用一个友好的用户界面来配置异常处理策略。因此,如果您愿意,可以有一个 advice 将异常处理委托给异常处理块。
ThrowsAdvice 中的异常
在 advice 中处理异常时要小心。您不希望抛出源自 advice 内部的异常。它们会掩盖target
抛出的原始异常。在示例代码中,我只是吞噬了异常并将信息打印到控制台。
结论
使用 AOP 进行异常处理可以消除重复的异常处理代码,从而减少异常处理的代码量并提高代码的可维护性。统一的异常处理提高了设计的敏捷性。我们可以通过几行代码和配置更改轻松地应用不同的异常处理。我们还展示了如何轻松重构异常处理 aspect 以提供更详尽的调试信息,例如抛出异常的方法的序列化参数对象!
参考文献
- Cristina Lopes, Jim Hugunin, Mik Kersten, Martin Lippert, Erik Hilsdale Gregor Kiczales: Using AspectJ For Programming The Detection and Handling of Exceptions
- M. Lippert and C. Lopes: A Study on Exception Detection and Handling Using Aspect-Oriented Programming
- AltSerializer
- Spring.Net 文档。我认为第 17 章是 AOP 入门中最好的。第 9 章全部关于 Spring.Net 中的 AOP。
- 面向方面编程 / 面向方面软件设计 by Mark Clifton