AOP in .NET


AOP in .NET

AOP是所有現代OOP語言開發框架中的基礎功能,Spring框架中有着廣泛應用。雖然AOP早已不是什么新技術,可知其然還要其所以然。本文將基於.NET環境探討實現AOP的底層原理。

本文為讀書筆記

文中部分代碼樣例摘自Matthew D. Groves的《AOP in .NET》,推薦大家購買閱讀。

中間件與過濾器原理截圖摘自微軟官方文檔,請查看文中鏈接。

本文主要包含以下內容:

  1. 基礎概念

  2. ASP.NET Core框架內置的AOP

    1. 中間件
    2. 過濾器
  3. AOP in .NET

    1. 編譯時/運行時織入

    2. 代理模式

    3. 手動編寫動態代理代碼

    4. Castle DynamicProxy

    5. Autofac + Castle.DynamicProxy

下載文中樣例代碼請訪問 https://github.com/wswind/Learn-AOP

基礎概念

面向對象編程通過類的繼承機制來復用代碼,這在大多數情況下這很有用。但是隨着軟件系統的越來復雜,出現了一些通過OOP處理起來相當費力的關注點,比如:日志記錄,權限控制,緩存,數據庫事務提交等等。它們的處理邏輯分散於各個模塊,各個類方法之中,這違反了DRY原則(Don't Repeat Yourself)以及關注度點分離原則(Separation of Concerns),不利於后期的代碼維護。所謂AOP(面向切面編程),就是將這些關注點,看作一個個切面,捕獲這些切面並將其處理程序模塊化的過程。

以一個簡單的日志記錄切面處理為例。如果不應用AOP,日志處理的代碼邏輯分散於模塊的各個方法中,如下圖

要實現AOP,關鍵在於捕捉切面,然后將切面織入(“weaving”)到業務模塊中。

如下圖代碼中,我們將分散的日志處理代碼模塊化成了一個統一的切面處理程序:LogAspect。然后將其織入到BusinessModule1中,這就實現了日志處理的AOP。

ASP.NET Core框架內置的AOP機制

在.ASP.NET Core框架中,微軟內置了一些處理AOP邏輯的機制。雖然這與傳統意義上的AOP不同,但是這里還是簡單提一下。

中間件機制

https://docs.microsoft.com/en-us/aspnet/core/fundamentals/middleware/

https://docs.microsoft.com/en-us/aspnet/core/fundamentals/middleware/write

ASP.NET Core框架本身就是由一系列中間件組成的,它本身內置的異常處理,路由轉發,權限控制,也就是在上述圖中的請求管道中實現的。所以我們也完全可以基於中間件機制,實現AOP。

以異常處理為例,我可以將try catch加入到next方法的前后,以捕獲后續運行過程中未處理的異常,並進行統一處理。代碼如下:

public class ExceptionHandlerMiddleware
{
    private readonly RequestDelegate _next;
    public ExceptionHandlerMiddleware(RequestDelegate next )
    {
        _next = next;
    }

    public async Task Invoke(HttpContext context, IHostingEnvironment env,ILogger<ExceptionHandlerMiddleware> logger)
    {
        try
        {
            await _next(context);
        }
        catch (Exception ex)
        {
            logger.LogError(new EventId(ex.HResult), ex, ex.Message);
            await context.HandleExceptionAsync(ex, env.IsDevelopment());
        }
    }
}

過濾器機制

https://docs.microsoft.com/en-us/aspnet/core/mvc/controllers/filters

過濾器本質上是由路由中間件(Routing Middleware)的請求管道實現的,如下圖所示。

開發者通過定義並注冊相應的過濾器,就能基於這個請求管道,來處理對應的關注點,如權限控制,結果轉換,日志記錄等等。Asp.NET Core 的過濾器執行順序如下圖:

我們可以基於中間件或者過濾器機制,完成簡單的開發。可惜的是,這些並不是語言級別的aop。asp.net core是一個開發框架,它為了方便你開發,給你內置了一些條條框框,你照着做確實能夠解決大部分問題。

但是脫離了它,該如何自己借助語言特性實現AOP呢?下面我們開始真正進入主題。

AOP in .NET

編譯時/運行時織入

在基礎概念中,我們已經簡單的說明了什么是AOP的織入。實現織入的方式分為兩種:編譯時織入、運行時織入。

當你使用C#創建.NET項目時,該項目將被編譯為CIL(也稱為MSIL,IL和bytecode)作為程序集(DLL或EXE文件)。 下圖說明了這個過程。然后,公共語言運行時(CLR)可以將CIL轉換成真實的機器指令(通過即時編譯過程,JIT)。

《aop in .net》

所謂編譯時織入,就是在編譯過程中修改產生的CIL文件,來達到織入的效果,如下圖所示。編譯時織入主要通過PostSharp實現。

運行時織入則是在程序運行時來完成的織入,一般是通過DynamicProxy(動態代理)程序(Castle.Core)配合IoC容器(Autofac,StructureMap等)來實現的。
在IoC容器解析服務實例(Service Instance)時,動態代理程序會基於服務實例創建動態代理對象,並在動態代理對象方法中,織入攔截器(interceptor)的執行邏輯,以此完成動態織入。
這里的攔截器就是我們處理切面邏輯的地方,我們會在后面通過代碼樣例詳細講解這種動態代理模式的實現原理。

DynamicProxy與PostSharp這兩種織入模式各有利弊:

  1. PostSharp是在編譯時進行的,DynamicProxy在運行時進行。所以一個會增加編譯時間,一個會降低運行效率。
  2. 由於PostSharp需要安裝額外的編譯程序,這意味着沒有安裝PostSharp的機器,無法正確編譯你開發的程序。這不利於應用在開源項目中,也不利於部署CI/CD的自動化編譯服務。
  3. PostSharp為收費的商業項目,需要付費使用。而運行時織入所需的Castle.Core以及IoC框架,都是開源免費的。
  4. DynamicProxy必須使用IoC容器,對於UI對象或領域對象,並不適合或不可能通過容器獲取實例。PostSharp沒有這個問題。
  5. DynamicProxy比PostSharp更易於進行單元測試。
  6. DynamicProxy在運行時執行,因此在編譯完成后,你仍可以通過修改配置文件來修改切面配置。PostSharp做不到這一點。
  7. DynamicProxy的攔截器被附加到類的所有方法中,而PostSharp能夠更精准的攔截。
  8. PostSharp能夠在static方法、private方法、屬性中織入AOP,而DynamicProxy做不到這一點。

你可以根據自己的需要選擇合適的織入方式,不過由於PostSharp為商業付費項目,我后面不再對其進行過多講解,需要的朋友可自行閱讀《AOP in .NET》中的相關內容,或查閱PostSharp官網。

本文后面將主要通過代碼樣例講述如何基於動態代理實現運行時織入。

代理模式

回顧之前基礎概念一節中的例子。我們需要在Mehtod1的執行前后,分別調用LogAspect的BeginMethod以及EndMethod方法來處理日志記錄邏輯。

我們現在通過運用一個簡單的代理模式模擬這個過程:

定義一個接口 IBusinessModule,並實現它

public interface IBusinessModule
{
    void Method1();
}

public class BusinessModule : IBusinessModule
{
    public void Method1()
    {
        Console.WriteLine("Method1");
    }
}

我現在需要在Method1方法調用前后,添加日志記錄。在不改變BusinessModule原有代碼的情況下,我們可以添加一個代理中間層來實現。代理類調用Method1,並在調用前后來打印日志。

public class BusinessModuleProxy : IBusinessModule
{
    BusinessModule _realObject;
    public BusinessModuleProxy()
    {
        _realObject = new BusinessModule();
    }
    public void Method1()
    {
        Console.WriteLine("BusinessModuleProxy before");
        _realObject.Method1();
        Console.WriteLine("BusinessModuleProxy after");
    }
}

在執行時,我們通過調用代理類來執行Method1,輸出便可以實現日志的輸出

class Program
{
    static void Main(string[] args)
    {
        IBusinessModule module = new BusinessModuleProxy();
        module.Method1();
    }
}

越是簡單的東西越接近事物的本質,代理模式就是后面一切運行時織入實現的根本。

其實如果你在實際開發過程中,如果你的程序較小,對AOP的需要沒有那么迫切,你也完全可以考慮通過IoC容器 + 代理模式(將對象的創建改為DI)來替代后面即將講的重型AOP實現。因為引入動態代理實現重型AOP會降低你的程序運行速度。

手動編寫動態代理代碼

上個例子中的代理模式雖然很有用,但是如果你需要為多個類的多個接口編寫切面處理程序,你就需要為每個接口編寫一個代理類,這是一個不小的工作量,也不易於代碼的維護。因此我們需要使用動態代理技術來動態生成代理類。

雖然我們能夠通過Castle的DynamicProxy工具來實現動態代理,但是為了了解底層原理,我們還是先手動編寫動態代理代碼。

為了更好的展示動態代理類的構建,我們對上面的例子進行一些調整。
我們不再自行定義代理類,而是需要通過IL生成器(ILGenerator)來生成它。

BusinessModule之前的例子很類似,但是也有些不同,Method1方法加入了參數,這主要是為了便於演示IL生成器的用法。

public interface IBusinessModule
{
    void Method1(string message);
}
public class BusinessModule : IBusinessModule
{
    public void Method1(string message)
    {
        Console.WriteLine("Method1: {0}", message);
    }
}

我們希望通過IL生成器構造以下的代理類。和之前不同的是,這個代理類的構造函數傳入了BusinessModule對象實例而不是通過new方法自己創建(這有些類似裝飾器模式)。
之所以這樣做,是為了簡化IL生成器的代碼量(這個代碼真的不是很好寫)。
代理類定義如下,需要說明的是,這個類只是一個偽代碼,用於講解IL生成器的邏輯。在運行中不會被調用。

public class BusinessModuleProxy
{
    BusinessModule _realObject;

    public BusinessModuleProxy(BusinessModule svc)
    {
        _realObject = svc;
    }
    public void Method1(string message)
    {
        Console.WriteLine("Method1 before!");
        _realObject.Method1(message);
        Console.WriteLine("Method1 after!");
    }
}

手動創建創建代理類的CreateDynamicProxyType方法代碼如下(你可以在文章開頭提到的github倉庫中下載)。

static Type CreateDynamicProxyType()
{
    var assemblyName = new AssemblyName("MyProxies");
    var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(assemblyName,
                                                AssemblyBuilderAccess.Run);      
    var modBuilder = assemblyBuilder.DefineDynamicModule("MyProxies");

    var typeBuilder = modBuilder.DefineType(
        "BusinessModuleProxy",
        TypeAttributes.Public | TypeAttributes.Class,
        typeof(object),
        new[] { typeof(IBusinessModule) });
    
    var fieldBuilder = typeBuilder.DefineField(
        "_realObject",
        typeof (BusinessModule),
        FieldAttributes.Private);
    var constructorBuilder = typeBuilder.DefineConstructor(
        MethodAttributes.Public,
        CallingConventions.HasThis,
        new[] {typeof (BusinessModule)});
        var contructorIl = constructorBuilder.GetILGenerator();
    contructorIl.Emit(OpCodes.Ldarg_0);
    contructorIl.Emit(OpCodes.Ldarg_1);
    contructorIl.Emit(OpCodes.Stfld, fieldBuilder);
    contructorIl.Emit(OpCodes.Ret);
    var methodBuilder = typeBuilder.DefineMethod("Method1",
                        MethodAttributes.Public | MethodAttributes.Virtual,
                        typeof (void),
                        new[] {typeof (string)});
                        typeBuilder.DefineMethodOverride(methodBuilder,
                        typeof (IBusinessModule).GetMethod("Method1"));
                        var method1 = methodBuilder.GetILGenerator();

    //Console.Writeline
    method1.Emit(OpCodes.Ldstr, "Method1 before!");
    method1.Emit(OpCodes.Call, typeof (Console).GetMethod("WriteLine", new[] {typeof (string)}));
    //load arg0 (this)
    method1.Emit(OpCodes.Ldarg_0);
    //load _realObject
    method1.Emit(OpCodes.Ldfld, fieldBuilder);
    //load argument1
    method1.Emit(OpCodes.Ldarg_1);
    //call Method1
    method1.Emit(OpCodes.Call,fieldBuilder.FieldType.GetMethod("Method1"));
    //Console.Writeline
    method1.Emit(OpCodes.Ldstr, "Method1 after!");
    method1.Emit(OpCodes.Call, typeof (Console).GetMethod("WriteLine", new[] {typeof (string)}));
    method1.Emit(OpCodes.Ret);
    return  typeBuilder.CreateType();

}

CreateDynamicProxyType方法構造出的類型,其實就是偽代碼展示過的BusinessModuleProxy。通過ILGenerator.Emit方法,我們插入了控制台提示。

Main函數調用代碼如下:

static void Main(string[] args)
{
    var type = CreateDynamicProxyType();
    var dynamicProxy = (IBusinessModule)Activator.CreateInstance(
    type, new object[] { new BusinessModule() });
    dynamicProxy.Method1("Hello DynamicProxy!");
}

執行結果展示:

Method1 before!
Method1: Hello DynamicProxy!
Method1 after!

雖然我們在實際開發中,不會自己手動這樣構造程序集來構造代理類。但是這個例子展示了運行時織入的動態代理原理。和之前的編譯時織入類似,它也是對程序集的IL進行了修改。只不過它修改的時機是在對象實例創建時進行的。

希望這個例子能夠幫助你理解動態代理的底層原理。

Castle DynamicProxy

在實際開發中,我們往往通過Castle.Core來實現DynamicProxy。Castle.Core是一個開源且被廣泛使用的動態代理組件,你可以通過nuget安裝並使用它。

IInterceptor是Castle.Core定義的攔截器接口。我們首先定義一個簡單的攔截器,在方法執行的前后,在控制台打印消息。

public class MyInterceptorAspect : IInterceptor
{
    public void Intercept(IInvocation invocation)
    {
        Console.WriteLine("Interceptor before");
        invocation.Proceed();
        Console.WriteLine("Interceptor after");
    }
}

在定義一個消息發送類,用於打印消息。

public class MessageClient
{
    public virtual void Send(string msg)
    {
        Console.WriteLine("Sending: {0}", msg);
    }
}

我們希望在Send方法調用前后,織入上面的攔截器。則可在Main函數中添加以下代碼

var proxyGenerator = new ProxyGenerator();
var svc = proxyGenerator.CreateClassProxy<MessageClient>(new MyInterceptorAspect());
svc.Send("hi");

控制台結果如下

Interceptor before
Sending: hi
Interceptor after

我們可以看到,使用Castle.Core織入非常簡單。不過也有一點需要額外注意:

Send必須是虛方法,這是因為CreateClassProxy返回的類型,並不是MessageClient,它是以MessageClient為父類的動態代理類,如果你看懂了上一節的內容,這里應該很好理解。所以,所有需要攔截的方法,都需要聲明為虛方法,這樣才能使攔截生效。如果你使用過NHibernate或者EntityFramework的.NET Framework版本,這個要求你應該很熟悉。

不過虛方法要求是因為MessageClient是一個具體類(concrete class)。如果通過接口進行攔截,我們可以使用CreateInterfaceProxyWithTarget方法,而避免必須要求為虛方法的限制。下面我們來通過代碼演示:

我們定義一個HelloClient,它繼承了IHelloClient接口

public class HelloClient : IHelloClient
{
    public void Hello()
    {
        Console.WriteLine("Hello");
    }
}

public interface IHelloClient
{
    void Hello();
}

通過CreateInterfaceProxyWithTarget即可完成MyInterceptorAspect接口攔截。通過接口攔截不再要求Hello方法為虛方法。

var svc2 = proxyGenerator.CreateInterfaceProxyWithTarget<IHelloClient>(new HelloClient(), new MyInterceptorAspect());
svc2.Hello();

Castle.Core是一個很有用的動態代理插件,很多開源組件都使用了它,學習與掌握它的基本使用是很有必要的。

Autofac + Castle.DynamicProxy

通過IoC容器配合動態代理,是實際開發中,最常用的方式。這里使用autofac來進行演示。

autofac攔截器的詳細文檔請瀏覽:https://autofac.readthedocs.io/en/latest/advanced/interceptors.html

和之前一樣,我創建了一個攔截器,攔截特定方法的執行,並在執行前后進行控制台打印。

另外,我定義了一個自定義屬性(Attribute)來設置方法是否需要使用日志,如果開啟了,才進行日志打印。

通過自定義屬性對方法進行聲明,從而影響AOP攔截器的方式,可以使代碼更加直觀,簡化代碼邏輯。

攔截器CallLogger代碼如下:

public class CallLogger : IInterceptor
{
    TextWriter _output;

    public CallLogger(TextWriter output)
    {
        _output = output;
    }

    public void Intercept(IInvocation invocation)
    {
        _output.WriteLine("Calling method '{0}' with parameters '{1}'... ",
            invocation.Method.Name,
            string.Join(", ", invocation.Arguments.Select(a => (a ?? "").ToString()).ToArray()));

        //校驗方法是否需要開啟了Logger
        bool isEnabled = AttributeHelper.IsLoggerEnabled(invocation.Method);

        //方法執行前
        if (isEnabled)
        {
            _output.WriteLine("Logger is Enabled");
        }
        //被攔截的方法執行
        invocation.Proceed();

        //方法執行后
        if (isEnabled)
        {
            _output.WriteLine("Done: result was '{0}'.", invocation.ReturnValue);
        }
    }
}

要攔截的接口ISomeType及其實現類定義如下,[Intercept]標簽將接口與攔截器進行了關聯。

[Intercept(typeof(CallLogger))] 
public interface ISomeType
{
    [Custom(StartLog = true)]
    string Show(string input);
}

public class SomeType : ISomeType
{
    //di called interface ,the attribute should be at interface
    public string Show(string input)
    {
        Console.WriteLine($"showdemo");
        return "resultdemo";
    }
}

代碼中[Custom(StartLog = true)]是我自定義的標簽,用於設定日志開關。

CustomAttribute定義代碼如下

[AttributeUsage(AttributeTargets.Method)]
public class CustomAttribute : Attribute
{
    public bool StartLog { get; set; }
}

我編寫了一個幫助類來處理這個Attribute

public static class AttributeHelper
{
    public static bool IsLoggerEnabled(MethodInfo type)
    {
        return GetStartLog(type);
    }
    
    public static bool HasCustomAttribute(MemberInfo methodInfo)
    {
       return methodInfo.IsDefined(typeof(CustomAttribute), true);
    }

    private static bool GetStartLog(MethodInfo methodInfo)
    {
        var attrs = methodInfo.GetCustomAttributes(true).OfType<CustomAttribute>().ToArray();
        if (attrs.Any())
        {
            CustomAttribute customAttribute = attrs.First();
            return customAttribute.StartLog;
        }
        return false;
    }
}

通過控制台的Main函數進行代碼調用

static void Main(string[] args)
{
    // create builder
    var builder = new ContainerBuilder();
	// 注冊接口及其實現類
    builder.RegisterType<SomeType>()
        .As<ISomeType>()
        .EnableInterfaceInterceptors();
    // 注冊攔截器
    builder.Register(c => new CallLogger(Console.Out));
    // 創建容器
    var container = builder.Build();
    // 解析服務
    var willBeIntercepted = container.Resolve<ISomeType>();
    // 執行
    willBeIntercepted.Show("this is a test");
}   

輸出結果如下:

Calling method 'Show' with parameters 'this is a test'...
Logger is Enabled
showdemo
Done: result was 'resultdemo'.

關於異步方法的攔截,這一補充一點:

Castle.Core目前沒有原生支持異步方法的攔截,你需要在攔截器對異步方法進行一些額外的處理。

Autofac對這個問題也沒有內置支持:https://autofac.readthedocs.io/en/latest/advanced/interceptors.html#asynchronous-method-interception

你可以通過Task.ContinueWith()來處理異步情況,或者通過第三方的幫助庫來實現異步方法的攔截:https://github.com/JSkimming/Castle.Core.AsyncInterceptor

對於Autofac的異步攔截器的代碼樣例,可查看:https://github.com/wswind/aop-learn/tree/master/AutofacAsyncInterceptor

另外:Structuremap(sunsetted) 是有異步攔截器支持的,可查看拓展閱讀中鏈接。

最后,希望本文對你有幫助。如果本文有錯誤歡迎在評論中指出。

拓展閱讀:

編譯時織入除了postsharp還可以看看: https://github.com/Fody/Fody

裝飾模式 https://www.tutorialspoint.com/design_pattern/decorator_pattern.htm

代理模式 https://www.tutorialspoint.com/design_pattern/proxy_pattern.htm

代理模式與裝飾模式的區別 https://stackoverflow.com/questions/18618779/differences-between-proxy-and-decorator-pattern

.NET Core 默認IoC容器結合適配器模式 https://medium.com/@willie.tetlow/net-core-dependency-injection-decorator-workaround-664cd3ec1246>

Simple .NET Aspect-Oriented Programming :https://github.com/TylerBrinks/Snap

Structuremap Interception and Decorators: https://structuremap.github.io/interception-and-decorators

StructuremapAspect Oriented Programming with StructureMap.DynamicInterception: https://structuremap.github.io/dynamic-interception/

Castle.Core 異步攔截器文檔: https://github.com/castleproject/Core/blob/master/docs/dynamicproxy-async-interception.md


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM