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則不用,只須要傳入類型便可。

參考文檔

相關文章
相關標籤/搜索