Castle DynamicProxy基本用法(AOP)

本文介绍AOP编程的基本概念、Castle DynamicProxy(DP)的基本用法,使用第三方扩展实现对异步(async)的支持,结合Autofac演示如何实现AOP编程。html

AOP

百科中关于AOP的解释:git

AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,经过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发中的一个热点……是函数式编程的一种衍生范型。利用AOP能够对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度下降,提升程序的可重用性,同时提升了开发的效率。程序员

在AOP中,咱们关注横切点,将通用的处理流程提取出来,咱们会提供系统通用功能,并在各业务层中进行使用,例如日志模块、异常处理模块等。经过AOP编程实现更加灵活高效的开发体验。github

DynamicProxy的基本用法

动态代理是实现AOP的一种方式,即在开发过程当中咱们不须要处理切面中(日志等)的工做,而是在运行时,经过动态代理来自动完成。Castle DynamicProxy是一个实现动态代理的框架,被不少优秀的项目用来实现AOP编程,EF Core、Autofac等。编程

咱们来看两段代码,演示AOP的好处。在使用AOP以前:架构

public class ProductRepository : IProductRepository
{
    private readonly ILogger logger;
    
    public ProductRepository(ILogger logger)
    {
        this.logger = logger;
    }
        
    public void Update(Product product)
    {
        //执行更新操做
        //......

        //记录日志
        logger.WriteLog($"产品{product}已更新");
    }
}

在使用AOP以后:mvc

public class ProductRepository : IProductRepository
{
    public void Update(Product product)
    {
        //执行更新操做
        //......
    }
}

能够明显的看出,在使用以前咱们的ProductRepository依赖于ILogger,并在执行Update操做之后,要写出记录日志的代码;而在使用以后,将日志记录交给动态代理来处理,下降了很多的开发量,即便碰见略微马虎的程序员,也不耽误咱们日志的记录。框架

那该如何实现这样的操做呢?asp.net

  • 首先,引用Castle.Core
  • 而后,定义拦截器,实现IInterceptor接口
public class LoggerInterceptor : IInterceptor
{
    private readonly ILogger logger;

    public LoggerInterceptor(ILogger logger)
    {
        this.logger = logger;
    }

    public void Intercept(IInvocation invocation)
    {
        //获取执行信息
        var methodName = invocation.Method.Name;

        //调用业务方法
        invocation.Proceed();

        //记录日志
        this.logger.WriteLog($"{methodName} 已执行");
    }
}
  • 最后,添加调用代码
static void Main(string[] args)
{
    ILogger logger = new ConsoleLogger();
    Product product = new Product() { Name = "Book" };
    IProductRepository target = new ProductRepository();

    ProxyGenerator generator = new ProxyGenerator();

    IInterceptor loggerIntercept = new LoggerInterceptor(logger);
    IProductRepository proxy = generator.CreateInterfaceProxyWithTarget(target, loggerIntercept);
    
    proxy.Update(product);
}

至此,咱们已经完成了一个日志拦截器,其它业务须要用到日志记录的时候,也可经过建立动态代理的方式来进行AOP编程。异步

可是,调用起来仍是比较复杂,须要怎么改进呢?固然是使用依赖注入(DI)了。

Autofac的集成

Autofac集成了对DynamicProxy的支持,咱们须要引用Autofac.Extras.DynamicProxy,而后建立容器、注册服务、生成实例、调用方法,咱们来看下面的代码:

ContainerBuilder builder = new ContainerBuilder();
//注册拦截器
builder.RegisterType<LoggerInterceptor>().AsSelf();

//注册基础服务
builder.RegisterType<ConsoleLogger>().AsImplementedInterfaces();

//注册要拦截的服务
builder.RegisterType<ProductRepository>().AsImplementedInterfaces()
    .EnableInterfaceInterceptors()                  //启用接口拦截
    .InterceptedBy(typeof(LoggerInterceptor));      //指定拦截器

var container = builder.Build();

//解析服务
var productRepository = container.Resolve<IProductRepository>();

Product product = new Product() { Name = "Book" };
productRepository.Update(product);

对这段代码作一下说明:

  • 注册拦截器时,须要注册为AsSelf,由于服务拦截时使用的是拦截器的实例,这种注册方式能够保证容器可以解析到拦截器。
  • 开启拦截功能:注册要拦截的服务时,须要调用EnableInterfaceInterceptors方法,表示开启接口拦截;
  • 关联服务与拦截器:InterceptedBy方法传入拦截器,指定拦截器的方式有两种,一种是咱们代码中的写法,对服务是无入侵的,所以推荐这种用法。另外一种是经过Intercept特性来进行关联,例如咱们上面的代码能够写为ProductRepository类上添加特性[Intercept(typeof(LoggerInterceptor))]
  • 拦截器的注册,能够注册为类型拦截器,也能够注册为命名的拦截器,使用上会有一些差别,主要在拦截器的关联上,此部分能够参考Autofac官方文档。咱们示例用的是类型注册。
  • 拦截器只对公共的接口方法、类中的虚方法有效,使用时须要特别注意。

DynamicProxy的基本原理

上面咱们说到动态代理只对公共接口方法、类中的虚方法生效,你是否想过为何?

其实,动态代理是在运行时为咱们动态生成了一个代理类,经过Generator生成的时候返回给咱们的是代理类的实例,而只有接口中的方法、类中的虚方法才能够在子类中被重写。

若是不使用动态代理,咱们的代理服务应该是什么样的呢?来看下面的代码,让咱们手工建立一个代理类:

如下是我对代理类的理解,请你们辩证的看待,若是存在不正确的地方,还望指出。

为接口使用代理:

public class ProductRepositoryProxy : IProductRepository
{
    private readonly ILogger logger;
    private readonly IProductRepository target;

    public ProductRepositoryProxy(ILogger logger, IProductRepository target)
    {
        this.logger = logger;
        this.target = target;
    }

    public void Update(Product product)
    {
        //调用IProductRepository的Update操做
        target.Update(product);

        //记录日志
        this.logger.WriteLog($"{nameof(Update)} 已执行");
    }
}

//使用代理类
IProductRepository target = new ProductRepository();
ILogger logger = new ConsoleLogger();
IProductRepository productRepository = new ProductRepositoryProxy(logger, target);

为类使用代理:

public class ProductRepository : IProductRepository
{
    //改写为虚方法
    public virtual void Update(Product product)
    {
        //执行更新操做
        //......
    }
}

public class ProductRepositoryProxy : ProductRepository
{
    private readonly ILogger logger;

    public ProductRepositoryProxy(ILogger logger)
    {
        this.logger = logger;
    }

    public override void Update(Product product)
    {
        //调用父类的Update操做
        base.Update(product);
        //记录日志
        this.logger.WriteLog($"{nameof(Update)} 已执行");
    }
}

//使用代理类
ILogger logger = new ConsoleLogger();
ProductRepository productRepository = new ProductRepositoryProxy(logger);

异步(async/await)的支持

若是你站在应用程序的角度来看,异步只是微软的一个语法糖,使用异步的方法返回结果为一个Task或Task 的对象,这对于DP来讲和一个int类型并没有差异,可是若是咱们想要在拦截中获取到真实的返回结果,就须要添加一些额外的处理。

Castle.Core.AsyncInterceptor是帮咱们处理异步拦截的框架,经过使用该框架能够下降异步处理的难度。

咱们本节仍然结合Autofac进行处理,首先对代码进行改造,将ProductRepository.Update方法改成异步的。

public class ProductRepository : IProductRepository
{
    public virtual Task<int> Update(Product product)
    {
        Console.WriteLine($"{nameof(Update)} Entry");

        //执行更新操做
        var task = Task.Run(() =>
        {
            //......
            Thread.Sleep(1000);

            Console.WriteLine($"{nameof(Update)} 更新操做已完成");
            //返回执行结果
            return 1;
        });

        //返回
        return task;
    }
}

接下来定义咱们的异步拦截器:

public class LoggerAsyncInterceptor : IAsyncInterceptor
{
    private readonly ILogger logger;

    public LoggerAsyncInterceptor(ILogger logger)
    {
        this.logger = logger;
    }

    /// <summary>
    /// 同步方法拦截时使用
    /// </summary>
    /// <param name="invocation"></param>
    public void InterceptSynchronous(IInvocation invocation)
    {
        throw new NotImplementedException(); 
    }

    /// <summary>
    /// 异步方法返回Task时使用
    /// </summary>
    /// <param name="invocation"></param>
    public void InterceptAsynchronous(IInvocation invocation)
    {
        throw new NotImplementedException();
    }

    /// <summary>
    /// 异步方法返回Task<T>时使用
    /// </summary>
    /// <typeparam name="TResult"></typeparam>
    /// <param name="invocation"></param>
    public void InterceptAsynchronous<TResult>(IInvocation invocation)
    {
        //调用业务方法
        invocation.ReturnValue = InternalInterceptAsynchronous<TResult>(invocation);
    }

    private async Task<TResult> InternalInterceptAsynchronous<TResult>(IInvocation invocation)
    {
        //获取执行信息
        var methodName = invocation.Method.Name;

        invocation.Proceed();
        var task = (Task<TResult>)invocation.ReturnValue;
        TResult result = await task;

        //记录日志
        this.logger.WriteLog($"{methodName} 已执行,返回结果:{result}");

        return result;
    }
}

IAsyncInterceptor接口是异步拦截器接口,它提供了三个方法:

  • InterceptSynchronous:拦截同步执行的方法
  • InterceptAsynchronous:拦截返回结果为Task的方法
  • InterceptAsynchronous<TResult>:拦截返回结果为Task 的方法

在咱们上面的代码中,只实现了InterceptAsynchronous<TResult>方法。

因为IAsyncInterceptor接口和DP框架中的IInterceptor接口没有关联,因此咱们还须要一个同步拦截器,此处直接修改旧的同步拦截器:

public class LoggerInterceptor : IInterceptor
{
    private readonly LoggerAsyncInterceptor interceptor;
    public LoggerInterceptor(LoggerAsyncInterceptor interceptor)
    {
        this.interceptor = interceptor;
    }

    public void Intercept(IInvocation invocation)
    {
        this.interceptor.ToInterceptor().Intercept(invocation);
    }
}

从代码中能够看到,异步拦截器LoggerAsyncInterceptor具备名为ToInterceptor()的扩展方法,该方法能够将IAsyncInterceptor接口的对象转换为IInterceptor接口的对象。

接下来咱们修改DI的服务注册部分:

ContainerBuilder builder = new ContainerBuilder();
//注册拦截器
builder.RegisterType<LoggerInterceptor>().AsSelf();
builder.RegisterType<LoggerAsyncInterceptor>().AsSelf();

//注册基础服务
builder.RegisterType<ConsoleLogger>().AsImplementedInterfaces();

//注册要拦截的服务
builder.RegisterType<ProductRepository>().AsImplementedInterfaces()
    .EnableInterfaceInterceptors()                  //启用接口拦截
    .InterceptedBy(typeof(LoggerInterceptor));      //指定拦截器

var container = builder.Build();

以上即是经过IAsyncInterceptor实现异步拦截器的方式。除了使用这种方式,咱们也能够在在动态拦截器中判断返回结果手工处理,此处再也不赘述。

探讨:ASP.NET MVC中的切面编程

经过上面的介绍,咱们已经了解了AOP的基本用法,可是如何用在ASP.NET Core中呢?

  1. MVC控制器的注册是在Services中完成的,而Services自己不支持DP。这个问题能够经过整合Autofac从新注册控制器来完成,可是这样操做真的好吗?
  2. MVC中的控制器是继承自ControllerBase,Action方法是咱们自定义的,不是某个接口的实现,这对实现AOP来讲存在必定困难。这个问题能够经过将Action定义为虚方法来解决,可是这样真的符合咱们的编码习惯吗?

咱们知道,AOP的初衷就是对使用者保持黑盒,经过抽取切面进行编程,而这两个问题偏偏须要咱们对使用者进行修改,违背了SOLID原则。

那么,若是咱们要在MVC中使用AOP,有什么方法呢?其实MVC已经为咱们提供了两种实现AOP的方式:

  1. 中间件(Middleware),这是MVC中的大杀器,提供了日志、Cookie、受权等一系列内置的中间件,从中能够看出,MVC并不想咱们经过DP实现AOP,而是要在管道中作文章。
  2. 过滤器(Filter),Filter是 ASP.NET MVC的产物,曾经一度帮助咱们解决了异常、受权等逻辑,在Core时代咱们仍然能够采用这种方式。

这两种方式更符合咱们的编码习惯,也体现了MVC框架的特性。

综上,不建议在MVC中对Controller使用DP。若是采用NLayer架构,则能够在Application层、Domain层使用DP,来实现相似数据审计、SQL跟踪等处理。

虽然不推荐,但仍是给出代码,给本身多一条路:

  • MVC控制器注册为服务
services.AddMvc()
    .AddControllersAsServices();
  • 从新注册控制器,配置拦截
builder.RegisterType<ProductController>()
    .EnableClassInterceptors()
    .InterceptedBy(typeof(ControllerInterceptor));
  • 控制器中的Action定义为虚方法
[HttpPost]
public virtual Task<int> Update(Product product)
{
    return this.productRepository.Update(product);
}

补充内容

  • 2019年7月24日补充

在建立代理类时(不管是class或interface),都有两种写法:WithTarget和WithoutTarget,这两种写法有必定的区别,withTarget须要传入目标实例,而withoutTarget则不用,只须要传入类型便可。

参考文档

相关文章
相关标签/搜索