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

如何使用 AOP 改进 .NET 应用程序

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.93/5 (17投票s)

2020年5月12日

CPOL

6分钟阅读

viewsIcon

18568

了解如何配置依赖注入以按设计实现目标。

其理念是 AOP(面向切面编程)。这种技术广泛应用于 Java,并且能以很低的代价保持高标准的质量。今天,我们将学习如何在 .NET Core 项目中轻松使用它。在简要的理论解释之后,您将看到两个示例(所有代码都在我的 GitHub 个人资料中)。

什么是 AOP

让我们从维基百科的定义开始

引用

在计算领域,面向切面编程(AOP)是一种编程范例,旨在通过允许分离横切关注点来提高模块化。它通过在不修改现有代码本身的情况下为现有代码添加附加行为(称为“通知”)来实现……这使得非业务核心逻辑(如日志记录)的行为能够被添加到程序中,而不会使代码充斥着与核心功能无关的内容。 https://en.wikipedia.org/wiki/Aspect-oriented_programming

这个概念很简单,可以用一句话来概括。

不写代码就能完成事情。

这适用于所有必需但又不引入任何业务逻辑的代码。

下面是一些 AOP 如何改变我们代码的示例。第一个是关于日志记录。

public void SaveData(InputClass input)
{
  Stopwatch timer= new Stopwatch();
  timer.Start();
  logger.Info("enterd SaveData method");

  if(logger.LogLevel==LoggingLeve.Debug)
  {
      logger.Debug(MyJsonTool.ConvertToJson(input);
  }

  dataRepositoryInstance.Save(input);
  timer.End();
  logger.Info($"enterd SaveData method in {timer.ElapsedMilliseconds}ms");
}

如果我告诉你,所有这些代码都可以通过只写这个来产生相同的输出,你会有什么想法?

public virtual void SaveData(InputClass input)
{
  dataRepositoryInstance.Save(input);
}

所以,只需给方法添加一个 `virtual` 关键字就能完成所有工作,这太棒了!我们稍后会回到 `virtual` 关键字,以了解它与 AOP 的关系。

如果你不相信 AOP 的强大,那就看看数据获取的代码可以简化成这样

[Fetch("SELECT * FROM customers WHERE name=?")]
public List<MyDTO> GetByName(string name)
{
   return new List<MyDTO>();
}

我希望现在你已经确信 AOP 可以在许多场景下提供帮助,并且是一个强大的盟友。让我们看看它是如何工作的,以及如何将其集成到 .NET Core 应用程序中。

本文包含两个示例

  1. 使用 DinamyProxy 和 Autofact 拦截日志的简单案例
  2. 对 AOP 技术进行一次深入的探讨,展示如何实现一个 AOP 引擎

示例 1:自动控制器日志记录

在这个示例中,我们将配置一个拦截器来记录所有传入的请求。这可以扩展到我们应用程序的所有其他层,这里仅作为概念验证。

拦截器

拦截器的结构非常简单。在方法执行之前和之后进行日志记录。在这个示例中,我使用 GUID 来关联事件日志,但还可以进行许多改进。

public class AutofacModule : Module
  {
      protected override void Load(ContainerBuilder builder)
      {
          // Register the interceptor
          builder.Register(c => new CallLogger())
         .Named<IInterceptor>("log-calls");
     
          builder.Register(c => new ValuesService(c.Resolve<ILogger<ValuesService>>()))
              .As<IValuesService>()
              .InstancePerLifetimeScope()                
              .EnableInterfaceInterceptors();
      }
  }  
  
  public class CallLogger : IInterceptor
    {
        public void Intercept(IInvocation invocation)
        {
            var executionId = Guid.NewGuid().ToString();

            Debug.WriteLine("{0} - Calling method {1} with parameters {2}... ", 
                executionId,
               invocation.Method.Name,
               JsonConvert.SerializeObject(invocation.Arguments));

            invocation.Proceed();

            Debug.WriteLine("{0} - Done: result was {1}.", 
                executionId, 
               JsonConvert.SerializeObject( invocation.ReturnValue));
        }
    }

我们可以讨论到明天,关于将数据转储到日志中的愚蠢之处,或者我们可以改进这个系统,使用更好的日志记录系统和一种巧妙的方法来跟踪输入、时间和输出。

正如你所见,这里有一个方法执行的跟踪,带有时间信息。想象一下,开箱即用,在你所有的 ASP.NET Web API 应用程序的控制器中,或者在你业务逻辑的每个服务方法中。很棒吧?这可以节省大量的代码行。

示例 2:低代码查询实现

这个示例展示了如何通过添加一些注解来为方法添加默认行为。这个示例是从零开始实现的,不使用任何库,以突出其幕后工作原理。

要创建的基类是 `DispatcherProxy`。这个类实现了泛型类型的代理,它拦截方法调用并返回一个自定义对象。这就是我们需要用一个可工作的对象来替换一个空方法。

无论如何,要实现一个通用的引擎,我们需要更多。我创建了一个通用的属性,叫做 `AOPAttribute`,里面有很多创意。所有继承它的注解都需要实现 `Execute` 方法。使用这种模式,所有的实现都委托给了注解,而我们的代理引擎与许多实现完全解耦。

你可以在下面的代码片段中查看代码的相关部分。只用几行代码,我们就实现了一个非常强大的引擎,但这只是一个例子。你可以尽情想象有多少用例可以为你解决。

我讲得太快了吗?让我们一步一步来。

步骤 1:我们想要什么

首先,我们想实现一个允许自动实现方法的机制。在 C# 中,我们不能在类上使用 `DispatcherProxy`,只能在接口上使用,所以我们总是需要从一个包含所有方法声明的接口开始。但是,我们也想手动实现一些方法,所以我们也需要一个具体的类。现在是棘手的部分。如果让类继承接口,这是很自然的,我们将被迫实现所有方法,因为编译器是这样要求的。我采用的技巧是根本不考虑继承。类和接口之间的关系将在以后,在 DI 期间定义。

这是 `FruitRepository` 的代码片段。接口包含将自动实现的方法,而 `InitDB` 是手动实现的方法。

  public interface IFruitRepository
  {
      [Query("SELECT * FROM fruits where family={0}")]
      List<Fruit> GetFruits(string tree);

      [Query("SELECT * FROM fruits where family='Citrus'")]
      List<Fruit> GetCitrus(string tree);

      public void InitDB();
  }
  
    public  class FruitRepositoryImpl
    {
        public void InitDB()
        {
            using (var db = new FruitDB())
            {
                db.Database.EnsureCreated();

                var count = db.Fruits.Count();
                if (count == 0)
                {
                    db.Fruits.Add(new Fruit()
                    {
                        Name = "Lemon",
                        Family = "Citrus"
                    });

                   //... all the fruit of the world here

                    db.SaveChanges();
                }
            }
        }
    }

步骤 2:代理

现在我们需要创建一个代理,它将维护接口和实现类之间的关系,并根据注解提供方法。代码非常简单,请看下面的代码片段。

public class ProxyFacotory<T> : DispatchProxy
{
    private T _decorated;

    protected override object Invoke(MethodInfo targetMethod, object[] args)
    {        
        //Find an annotation on the interface
        var annotations = targetMethod.GetCustomAttributes(true);
        var aopAttr = annotations.FirstOrDefault
        (x => typeof(AOPAttribute).IsAssignableFrom(x.GetType())) as AOPAttribute;
        
        //in case the method has an AOP implementation, this is executed
        if (aopAttr != null)
        {
            return aopAttr.Execute(targetMethod, args, annotations);
        }

        //otherwise, the manual implementation on class is triggered
        var inherithedMethod=interfaceMethods.FirstOrDefault
                             (x => x.Name == targetMethod.Name);
        var result = inherithedMethod.Invoke(_decorated, args);
        return result;
    }

    public static T Create<T,TProxy>(TProxy instance) where T : class where TProxy:class
    {
        object proxy = Create<T, ProxyFacotory<TProxy>>();
        ((ProxyFacotory<TProxy>)proxy).SetParameters(instance);
        return (T)proxy;
    }

    private void SetParameters(T decorated)
    {
        _decorated = decorated;
    }
}

用法非常简单,并且使用了常规的 .NET Core 依赖注入。

//the proxy instance return an instance based on the concrete implementation
var instance=ProxyFacotory<IFruitRepository>.Create<IFruitRepository,FruitRepositoryImpl>
                                             (new FruitRepositoryImpl());
var serviceProvider = new ServiceCollection()
                .AddSingleton<IFruitRepository>(instance)
                .BuildServiceProvider();

步骤 3:注解

所有注解的基础是 `AOPAnnotation`。这是一个 `abstract` 类,包含一个 `Execute` 方法,该方法取代了常规的方法体。然后我们有 Query 注解,在我们的例子中,它使用开发人员传递的查询模板来获取数据。

public abstract class AOPAttribute: Attribute
  {
      public abstract object Execute
             (MethodInfo targetMethod, object[] args, object[] annotations);
  }
    
 public class QueryAttribute : AOPAttribute
  {
      public string Template { get; set; }
      public   QueryAttribute(string template)
      {
          this.Template = template;
      }

      public override object Execute
             (MethodInfo targetMethod, object[] args, object[] annotations)
      {
          using (var data = new FruitDB())
          {
              return data.Fruits
              .FromSqlRaw<Fruit>(this.Template,args ).ToList(); //Dataset can be 
                                                                //taken by target field
          }
      }
  }

步骤 4:看它如何工作

将所有这些放在一起非常简单。只需像我们手动编写的那样使用存储库。

  var fruitRepository = serviceProvider.GetService<IFruitRepository>();
  //This calls the manual method and fill the database
  fruitRepository.InitDB();
  //This uses a dynamic definition
  var fruits=fruitRepository.GetFruits("Citrus");

总结

AOP 是一个非常有趣的模式,因为它能够自动化代码编写。它非常强大,但有两个弱点

  1. 性能:大量使用反射和额外的处理步骤,可能会增加计算时间
  2. 失控:系统为你做的越多,你就越不知道如何修复

现代工具和框架有助于在不使用的情况下减少代码,所以它并不总是必要的。然而,当你设计一个框架或大型基础设施时,了解它非常重要,因为它可能是赢得战争的正确武器。例如,在我设计 RawCMS,开源无头 CMS 的架构时,它是一个很好的盟友。

关于性能或稳定性,只需记住 Java Spring Framework。它将其作为一切的基础,并且如今是企业应用程序的最佳选择之一。

所有源代码都在我的 GitHub 个人资料上

参考文献

历史

  • 2020年5月12日:初版
© . All rights reserved.