65.9K
CodeProject 正在变化。 阅读更多。
Home

通过 AOP 改进异常处理

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.60/5 (6投票s)

2006 年 10 月 10 日

CPOL

6分钟阅读

viewsIcon

103560

downloadIcon

785

使用 AOP 进行异常处理可以减少异常处理的代码量,从而提高应用程序的敏捷性和代码的可维护性。

目录

  1. 引言
  2. 问题
  3. 整合 Aspect 中的重复异常处理
  4. 重构以改进异常处理
  5. 结论
  6. 参考文献

引言

使用 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 模块中,IThrowsAdviceAfterThrowing在被拦截的方法抛出异常后被调用。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。通过调用ProxyFactoryGetProxy()方法获得代理对象。

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,请修改示例项目中ExceptionHandlingAspectGetProxy。将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
© . All rights reserved.