1. 程式人生 > 其它 >tp5檔案上傳實現縮圖+水印的功能(參考)

tp5檔案上傳實現縮圖+水印的功能(參考)

參考網址:https://www.cnblogs.com/youring2/p/10962573.html

本文介紹AOP程式設計的基本概念、Castle DynamicProxy(DP)的基本用法,使用第三方擴充套件實現對非同步(async)的支援,結合Autofac演示如何實現AOP程式設計。

AOP

百科中關於AOP的解釋:

AOP為Aspect Oriented Programming的縮寫,意為:面向切面程式設計,通過預編譯方式和執行期動態代理實現程式功能的統一維護的一種技術。AOP是OOP的延續,是軟體開發中的一個熱點……是函數語言程式設計的一種衍生範型。利用AOP可以對業務邏輯的各個部分進行隔離,從而使得業務邏輯各部分之間的耦合度降低,提高程式的可重用性,同時提高了開發的效率。

在AOP中,我們關注橫切點,將通用的處理流程提取出來,我們會提供系統通用功能,並在各業務層中進行使用,例如日誌模組、異常處理模組等。通過AOP程式設計實現更加靈活高效的開發體驗。

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之後:

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

可以明顯的看出,在使用之前我們的ProductRepository依賴於ILogger,並在執行Update操作以後,要寫出記錄日誌的程式碼;而在使用之後,將日誌記錄交給動態代理來處理,降低了不少的開發量,即使遇見略微馬虎的程式設計師,也不耽誤我們日誌的記錄。

那該如何實現這樣的操作呢?

  • 首先,引用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則不用,只需要傳入型別即可。

參考文件