在.net core 中使用AOP

在.net core 中使用AOP

AOP优势

在这里就不介绍AOP是什么,不了解的同学可以自行百度。

使用AOP的优势有以下几点:

  • 将通用功能从业务逻辑中抽离出来,就可以省略大量重复代码,有利于代码的操作和维护。
  • 在软件设计时,抽出通用功能(切面),有利于软件设计的模块化,降低软件架构的复杂程度。也就是说通用的功能就是一个单独的模块,在项目的主业务里面是看不到这些通用功能的设计代码的。

目的

在不改变代码逻辑的情况下,缓存某些方法的结果

准备工作

本文是基于Autofac 实现的AOP

  • 创建.net core 3.1的项目
  • 引入 Autofac.Extensions.DependencyInjection 6.0.0
  • 引入 Autofac.Extras.DynamicProxy 5.0.0

定义特性

因为Autofac的拦截器只能作用与接口和类,无法指定具体的某一个方法。有这个特性即缓存,没有则跳过。该特性可以指定缓存的键,方便查找

using System;

namespace DesignPattern.Attributes
{
    [AttributeUsage(AttributeTargets.Method)]
    public class CacheAttribute : Attribute
    {
        public string Name { get; set; }

        public CacheAttribute(string name)
        {
            Name = name;
        }
    }
}

创建拦截器

需要实现 IInterceptor 接口,此处我创建了一个拦截器,用来实现方法缓存,即进入方法执行前,先判断该方法的结果是否已被缓存,如是,则直接返回缓存,否则继续执行方法,并将方法的结果缓存起来

public class CustomInterceptor : IInterceptor
{
    private readonly ILogger<CustomInterceptor> _logger;
    private readonly IMemoryCache _cache;

    public CustomInterceptor(ILogger<CustomInterceptor> logger, IMemoryCache cache)
    {
        _logger = logger;
        _cache = cache;
    }

    public void Intercept(IInvocation invocation)
    {
        try
        {
            var cacheAttr = invocation.Method.GetCustomAttributes<CacheAttribute>(true).FirstOrDefault();
            if (cacheAttr != null)
            {
                _logger.LogInformation($"拦截方法:{invocation.Method.DeclaringType?.Name}.{invocation.Method.Name}");

                var argument = invocation.Arguments.FirstOrDefault();   // 获取方法的参数
                var value = _cache.Get($"{cacheAttr.Name}-{argument}");
                if (value != null)
                    invocation.ReturnValue = value;
                else
                {
                    invocation.Proceed();
                    value = invocation.ReturnValue;
                    _cache.Set($"{cacheAttr.Name}-{argument}", value, TimeSpan.FromHours(1));
                }
            
                _logger.LogInformation($"{DateTime.Now:s},方法返回值:{JsonSerializer.Serialize(value)}");
            }
            else
            {
                invocation.Proceed();   // 方法执行中
            }
        }
        catch (Exception e)
        {
            _logger.LogInformation($"出错了:{e.Message}");
            invocation.ReturnValue = new {success = 1, message = e.Message};
        }
    }
}

注入拦截器

在ASP.NET Core 3.X托管方式发生了变化 并且需要不同的集成方式. 你不能在从 ConfigureServices 中返回 IServiceProvider, 也不能再将你的service provider factory加入到service collection.在这个方法中使用第三方依赖注入

下面是ASP.NET Core 3+ 和 .NET Core 3+ generic hosting support的集成方式:

public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup<Startup>(); })
        .UseServiceProviderFactory(new AutofacServiceProviderFactory()); // 

在你的Startup类中 (各版本ASP.NET Core基本一致) 你可以使用 ConfigureContainer 访问 Autofac container builder 并且直接使用Autofac注册东西.

public void ConfigureServices(IServiceCollection services)
{
    services.AddMemoryCache();
    // ......
}

//ConfigureContainer是你可以直接注册Autofac的东西。它在ConfigureServices之后运行,所以这里的内容将覆盖在ConfigureServices中进行的注册。
//不要在这里使用Build构建,ServiceProviderFactory会去做这件事
public void ConfigureContainer(ContainerBuilder builder)
{
    builder.RegisterType<CustomInterceptor>();
}

创建服务类

创建一个接口及它的实现类,模拟从数据库中读取数据

[Intercept(typeof(CustomInterceptor))]
public interface ICustomerService
{
    [Cache("test")]
    object Get(int id);

    object NoCache();
}

public class CustomerService : ICustomerService
{
    public object Get(int id)
    {
        var data = new {id, name = "cook", age = 18, time = DateTime.Now.ToString("s")};
        return data;
    }

    public object NoCache()
    {
        return new { time = DateTime.Now.ToString("s") };
    }
}

在这里,使用Intercept特性标记接口或者类使用哪一个拦截器,然后在Startup中注入服务

 public void ConfigureContainer(ContainerBuilder builder)
{
    builder.RegisterType<CustomInterceptor>();
    builder.RegisterType<CustomerService>().As<ICustomerService>().InstancePerLifetimeScope().EnableInterfaceInterceptors();
}

启用拦截器主要有两个方法 EnableInterfaceInterceptorsEnableClassInterceptors,分别对应接口和类,我这里使用的是接口,所以用的是EnableInterfaceInterceptors方法,当然还有其他的方式可以指定拦截器,如:InterceptedBy(typeof(CustomInterceptor))方法等,这里我不一一赘述,可以参考官方文档 https://autofaccn.readthedocs.io/zh/latest/advanced/interceptors.html

控制器中使用

[ApiController]
[Route("interceptor")]
public class InterceptorController : ControllerBase
{
    private readonly ICustomerService _customerService;

    public InterceptorController(ICustomerService customerService)
    {
        _customerService = customerService;
    }

    public IActionResult Index([FromQuery] int id)
    {
        var hasCache = _customerService.Get(id);
        var noCache = _customerService.NoCache();
        return Ok(hasCache);
    }
}

运行项目,并多次访问 localhost:5000/interceptor?id=2,得到的结果如下图

aop_01

可以看到,多次运行,只有第一次会执行Get方法,之后会从缓存中读取数据,不会执行Get方法

源码地址