.Net Core(六) 面向切面编程

简介

面向切面编程(AOP,Aspect-Oriented Programming)是一种编程范式,旨在增强现有的面向对象编程(OOP,Object-Oriented Programming)范式。AOP 通过在程序执行过程中动态地将横切关注点(cross-cutting concerns)从它们所影响的对象中分离出来,并将其模块化,以便重用和管理。

在传统的面向对象编程中,程序功能被分解为对象及其方法,这些方法通常是面向单一功能的。但在实际应用中,一些功能可能会横跨多个对象和方法,这些功能通常被称为横切关注点,比如日志记录、安全性、事务管理等。这些横切关注点散布在整个应用程序中,导致了代码的重复和耦合性增加,难以维护和扩展。

AOP 通过将这些横切关注点从业务逻辑中分离出来,形成独立的切面(Aspect),然后将切面与主要的业务逻辑模块进行连接,实现了横切关注点的重用和集中管理。这样一来,切面可以在程序执行的特定点(例如方法调用前、方法调用后、方法异常时等)插入代码,从而实现特定的功能,而不需要修改原始业务逻辑代码。

术语

在 AOP(面向切面编程)中,有一些主要的术语,它们有助于理解和使用该编程范式:

  1. 切面(Aspect):切面是横切关注点的模块化单元。它包含了一组通知以及相应的切点表达式。通知定义了在何时、何地、如何处理横切关注点的逻辑,而切点表达式则定义了何处应用这些通知。

  2. 通知(Advice):通知是切面中具体的行为逻辑。在 AOP 中,通知定义了在程序执行过程中横切关注点被触发时应该执行的代码。常见的通知类型包括前置通知(Before advice)、后置通知(After advice)、环绕通知(Around advice)、异常通知(After-throwing advice)和返回通知(After-returning advice)。

  3. 连接点(Join Point):连接点是在程序执行过程中能够被切面通知的具体点。例如,方法调用、方法执行、对象创建等都是连接点。

  4. 切点(Pointcut):切点是连接点的集合,它定义了切面在何处应该被执行。通常使用切点表达式来描述切点,该表达式匹配连接点的模式。

  5. 目标对象(Target Object):目标对象是应用程序中真正具有业务逻辑的对象。在 AOP 中,切面通过通知来增强目标对象的功能。

  6. 代理(Proxy):代理是在运行时创建的对象,它包装了目标对象,并提供了额外的功能,例如应用切面中定义的通知。

  7. 织入(Weaving):织入是将切面与目标对象连接起来以创建新的代理对象的过程。织入可以在编译时、类加载时或运行时进行。在 AOP 中,织入是将切面的行为应用到目标对象的过程。

举例说明

假设你有一个电商网站,用户可以在该网站上购买商品。在这个网站中,用户的主要操作包括浏览商品、添加商品到购物车、填写订单信息和支付订单等。这些操作构成了用户的主要业务逻辑。

但是,除了用户的主要操作外,还有一些额外的功能需要处理,比如日志记录、安全验证、性能监控等。这些功能并不直接与用户的主要操作相关,但却是整个网站运行所必需的。

现在,我们使用面向切面编程(AOP)来描述这个情景:

  1. 切面(Aspect):切面是横切关注点的模块化单元。在我们的例子中,日志记录、安全验证、性能监控等是切面的一部分。

  2. 通知(Advice):通知是切面中具体的行为逻辑。在我们的例子中,通知就是针对特定切点(连接点)执行的额外代码,比如在方法调用前后记录日志、验证用户权限,或者监测方法执行时间等。

  3. 连接点(Join Point):在用户操作的过程中,比如浏览商品、添加商品到购物车、填写订单信息和支付订单等都是连接点。即,这些是切面可以介入的地方。

  4. 切点(Pointcut):切点是连接点的集合,它定义了切面应该在哪些连接点处进行织入。在这个例子中,浏览商品、添加商品到购物车、填写订单信息和支付订单等操作的集合就是切点。

  5. 目标对象(Target Object):在这个例子中,用户操作的过程就是目标对象。用户的主要业务逻辑是浏览商品、添加商品到购物车、填写订单信息和支付订单。

  6. 代理(Proxy):日志记录、安全验证、性能监控可以被看作是代理,它们为用户的操作提供了额外的功能,比如记录日志、验证用户身份、监控系统性能等。

  7. 织入(Weaving):织入是将日志记录、安全验证、性能监控的功能与用户操作的过程相连接的过程。这样,日志记录、安全验证、性能监控就在用户的操作过程中被执行了。

 不使用AOP代码

// HomeController.cs
public class HomeController : Controller
{
    private readonly ILogger<HomeController> _logger;

    public HomeController(ILogger<HomeController> logger)
    {
        _logger = logger;
    }

    public IActionResult Index()
    {
        _logger.LogInformation("User browsed products.");
        // 业务逻辑:浏览商品
        return View();
    }

    public IActionResult AddToCart()
    {
        _logger.LogInformation("User added a product to the cart.");
        // 业务逻辑:添加商品到购物车
        return View();
    }

    public IActionResult Checkout()
    {
        _logger.LogInformation("User proceeded to checkout.");
        // 业务逻辑:填写订单信息
        return View();
    }

    public IActionResult Pay()
    {
        _logger.LogInformation("User paid for the order.");
        // 业务逻辑:支付订单
        return View();
    }
}

使用AOP代码

// LoggingInterceptor.cs
public class LoggingInterceptor : AbstractInterceptor
{
    private readonly ILogger<LoggingInterceptor> _logger;

    public LoggingInterceptor(ILogger<LoggingInterceptor> logger)
    {
        _logger = logger;
    }

    public async Task Invoke(AspectContext context, AspectDelegate next)
    {
        // 记录用户操作
        _logger.LogInformation($"User {context.ImplementationMethod.Name}");

        // 执行被拦截的方法
        await next(context);
    }
}

// HomeController.cs
[LoggingInterceptor]
public class HomeController : Controller
{
    public IActionResult Index()
    {
        // 业务逻辑:浏览商品
        return View();
    }

    public IActionResult AddToCart()
    {
        // 业务逻辑:添加商品到购物车
        return View();
    }

    public IActionResult Checkout()
    {
        // 业务逻辑:填写订单信息
        return View();
    }

    public IActionResult Pay()
    {
        // 业务逻辑:支付订单
        return View();
    }
}

在这个示例中,我们使用了 AOP 的方式来实现日志记录的功能。通过使用 AspectCore 框架,我们定义了一个名为 LoggingInterceptor 的拦截器,并在该拦截器中实现了日志记录的逻辑。在 HomeController 中,我们使用 [LoggingInterceptor] 属性将该拦截器应用到了控制器的所有方法上。这样,业务逻辑和日志记录的关注点被明确地分离开来,使代码更加清晰和可维护。

使用中间件

有时我会想中间件也可以实现这个日志记录和业务分离啊

我们创建一个自定义的日志记录中间件:

// LoggingMiddleware.cs
public class LoggingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<LoggingMiddleware> _logger;

    public LoggingMiddleware(RequestDelegate next, ILogger<LoggingMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }

    public async Task Invoke(HttpContext context)
    {
        // 记录用户操作
        _logger.LogInformation($"User requested: {context.Request.Path}");

        // 继续请求处理管道中的下一个中间件
        await _next(context);
    }
}

然后,在 Startup.cs 中注册和使用这个中间件:

// Startup.cs
public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        // 配置其他服务
    }

    public void Configure(IApplicationBuilder app)
    {
        // 配置其他中间件

        app.UseMiddleware<LoggingMiddleware>();

        // 配置其他中间件
    }
}

这样,我们就将日志记录的逻辑剥离到了一个中间件中,使得它与业务逻辑分离开来。在每次请求进入管道时,中间件都会记录用户操作的日志。

既然中间件可以实现,我们为什么还要AOP呢?

在中间件中实现日志记录可能会限制我们对日志记录逻辑的灵活控制。例如,我们可能希望根据不同的条件记录不同级别的日志,或者在特定情况下禁用日志记录。使用中间件可能无法满足这些需求。

优点

  1. 模块化:AOP允许将横切关注点(如日志记录、安全性、事务管理等)与核心业务逻辑分离,使得代码更易于理解、维护和重用。

  2. 可维护性:通过将横切关注点集中处理,AOP降低了代码重复性,减少了代码中的冗余,使得修改和维护更加容易。

  3. 关注分离:AOP通过将横切关注点从业务逻辑中分离出来,使得代码更加关注于核心业务逻辑,提高了代码的清晰度和可读性。

  4. 集中管理:AOP允许将各种横切关注点集中管理,便于统一控制和管理,提高了代码的可控性和可维护性。

  5. 横切关注点重用:AOP允许将一些横切关注点(如日志记录、事务管理等)在不同的模块中重用,提高了代码的灵活性和复用性。

缺点

  1. 学习曲线:AOP的概念相对传统的面向对象编程(OOP)来说比较抽象,可能需要一定时间来理解和掌握。

  2. 增加复杂性:使用AOP可能会增加代码的复杂性,特别是对于初学者来说,需要谨慎使用以避免引入不必要的复杂性。

  3. 运行时性能开销:一些AOP框架在运行时会增加一定的性能开销,尤其是在大型或高并发的应用中,可能会对性能产生一定影响。

  4. 调试困难:由于AOP会将一些关注点分离到不同的模块中,可能会增加调试代码的难度,特别是当一个横切关注点被多个连接点调用时。

  5. 潜在滥用:过度使用AOP可能导致代码的可读性和可维护性下降,特别是当横切关注点过多或不合理时,可能会导致代码变得混乱和难以理解。

 

posted @ 2024-03-04 14:27  咸鱼翻身?  阅读(195)  评论(0编辑  收藏  举报