AOP在特來電的應用探索

摘要

​ 本文將探討AOP(面向切面編程)在特來電的應用,重點研究AOP在日誌記錄、參數校驗、權限校驗、異常處理等領域的實際應用。我們將首先對比目前實現AOP的兩種常見途徑:靜態編織方式和運行時處理方式,並分析它們的優缺點。然後,我們將對比主流的AOP技術,包括商業成本和學習成本等方面。最後,我們將介紹如何使用Fody 與 MethodBoundaryAspect來實現AOP,並提供相關代碼實例。

1. 引言

​ 在當今的軟件開發中,面向切面編程(AOP)作爲一種重要的編程範式,被廣泛應用於日誌記錄、參數校驗、權限校驗、異常處理等領域。通過AOP,我們可以將與核心業務邏輯無關的橫切關注點(cross-cutting concerns)與業務邏輯進行解耦,提高代碼的可維護性和可擴展性。本文將深入探索AOP在特來電的應用,並重點關注AOP在日誌記錄、參數校驗、權限校驗、異常處理等領域的實際應用。

2. AOP的實現途徑對比

2.1 IL靜態編織方式

​ 靜態編織方式是在編譯階段將切面織入目標代碼中,最常見的方式是使用註解或特殊的編譯器。這種方式的優點是在運行時沒有額外的性能開銷,並且可以在編譯時捕獲錯誤。然而,它的缺點是需要修改源代碼,可能會引入額外的複雜性,並且對於第三方庫或框架的集成可能會有一定的限制。

2.2 運行時處理方式

​ 運行時處理方式是在目標代碼執行期間通過動態代理或裝飾器來織入切面。這種方式的優點是可以在運行時根據需要動態添加和移除切面,而且不需要修改源代碼。然而,它的缺點是會引入一定的性能開銷,並且可能需要更多的配置和管理。

3. 主流AOP技術框架對比

3.1 靜態編織主流框架

​ PostSharp作爲一款商業收費的AOP框架,擁有極高的性能表現,因爲它直接在編譯期將切面代碼注入到目標類中,無需代理對象參與。同時,它提供了豐富的切面類型和模板,使用起來非常靈活。但作爲商業軟件,PostSharp需要進行授權許可,增加了使用成本。且其編譯時織入的特點使得擴展和修改都不太方便。Fody作爲一款輕量級的開源AOP框架,它的簡單和易用性在業界有口皆碑。通過特性配置織入切面,同樣可以享受無代理的高性能。但是,Fody只支持C#語言,且其靜態編織方式導致了擴展性和靈活性上的侷限。

3.2 運行時AOP主流框架

​ Unity Interception 與Unity容器無縫集成,爲依賴注入提供了AOP支持。但是它僅限於Unity體系,不夠通用。運行時代理也帶來一定性能損失。Spring.NET AOP深度集成到了Spring框架中,爲Spring生態系統提供了面向切面編程。但這也意味着它只能應用於Spring體系。如果需要通用的AOP解決方案,Spring.NET AOP就力不從心。Castle DynamicProxy以其強大的運行時代理功能著稱,既可以代理接口也可以代理類。這賦予了它極大的擴展靈活性。但是,運行時代理是以性能爲代價的。複雜的API也增加了學習難度。相比之下,Fody以輕量易用見長。它基於編譯期織入實現了無代理的高性能AOP,語法簡單,代碼侵入性也很低。且Fody是完全開源免費的。這種高性價比讓Fody成爲.NET生態中最理想的AOP解決方案。

4. AOP在特來電的應用

​ 綜合上述對比結果並結合業務需求,我們在SG服務的開發中引入了Fody來幫助團隊剝離切面關注點,專注核心業務開發。下面我們將介紹Fody的幾種典型應用實現方式。在使用Fody前,首先需要引入相關的包:

Install-Package Fody
Install-Package MethodBoundaryAspect.Fody

4.1 日誌記錄

日誌記錄是應用中常見的橫切關注點之一。通過使用AOP,我們可以在方法執行前後自動記錄方法的輸入參數、返回值和執行時間等信息,從而方便進行故障排查和性能分析。

using MethodBoundaryAspect.Fody.Attributes;

[LogAspect]
public void MyMethod()
{
    // 方法邏輯
}

[Serializable]
public class LogAspect : OnMethodBoundaryAspect
{
    public override void OnEntry(MethodExecutionArgs args)
    {
        // 在方法執行前記錄日誌
        Log("Entering method: " + args.Method.Name);
    }

    public override void OnExit(MethodExecutionArgs args)
    {
        // 在方法執行後記錄日誌
        Log("Exiting method: " + args.Method.Name);
    }

    private void Log(string message)
    {
        // 記錄日誌的具體實現
        Console.WriteLine(message);
    }
}

4.2 參數校驗

參數校驗是確保方法接收到有效參數的重要環節。通過使用AOP,我們可以在方法執行前自動對輸入參數進行校驗,減少重複的校驗代碼,提高代碼的可讀性和可維護性。

using MethodBoundaryAspect.Fody.Attributes;

[ValidationAspect]
public void MyMethod(string param)
{
    // 方法邏輯
}

[Serializable]
public class ValidationAspect : OnMethodBoundaryAspect
{
    public override void OnEntry(MethodExecutionArgs args)
    {
        // 在方法執行前進行參數校驗
        if (args.Arguments[0] == null)
        {
            throw new ArgumentNullException("param", "Parameter cannot be null");
        }
    }
}

4.3 權限校驗

權限校驗是保護敏感操作和資源的一種重要手段。通過使用AOP,我們可以在方法執行前自動進行權限校驗,確保只有具有相應權限的用戶才能執行特定的操作。

using MethodBoundaryAspect.Fody.Attributes;

[AuthorizationAspect]
public void MyMethod()
{
    // 方法邏輯
}

[Serializable]
public class AuthorizationAspect : OnMethodBoundaryAspect
{
    public override void OnEntry(MethodExecutionArgs args)
    {
        // 在方法執行前進行權限校驗
        if (!IsUserAuthorized())
        {
            throw new UnauthorizedAccessException("User is not authorized to access this method");
        }
    }

    private bool IsUserAuthorized()
    {
        // 檢查用戶權限的具體實現邏輯
        // 返回 true 表示用戶具有權限,返回 false 表示用戶沒有權限
    }
}

4.4 異常處理

異常處理是保證應用穩定性和可靠性的重要環節。通過使用AOP,我們可以在方法執行過程中捕獲和處理異常,統一進行日誌記錄、錯誤信息提示或回滾操作,提高系統的容錯能力。

using MethodBoundaryAspect.Fody.Attributes;

[ExceptionHandlingAspect]
public void MyMethod()
{
    try
    {
        // 方法邏輯
    }
    catch (Exception ex)
    {
        HandleException(ex);
    }
}

[Serializable]
public class ExceptionHandlingAspect : OnMethodBoundaryAspect
{
    public override void OnException(MethodExecutionArgs args)
    {
        // 在方法拋出異常時進行異常處理
        HandleException(args.Exception);
    }

    private void HandleException(Exception exception)
    {
        // 異常處理的具體邏輯
        // 可以進行日誌記錄、錯誤信息提示等操作
    }
}

通過使用Fody + MethodBoundaryAspect,我們可以輕鬆實現上述AOP功能。只需在目標方法上添加相應的註解,即可自動織入切面邏輯,實現橫切關注點的解耦和複用。

4.5 編譯後前後代碼對比

這裏我們將通過對比使用Fody編譯前後的代碼來大致瞭解程序的行爲。首先在上面日誌切面示例的基礎上我們做了一些小的更改,代碼如下:

    [Serializable]
    public class LogAspectAttribute : OnMethodBoundaryAspect
    {
        public FlowBehavior Behavior { get; set; }
        public LogAspectAttribute(FlowBehavior flowBehavior)
        {
            Behavior = flowBehavior;
        }

        public override void OnEntry(MethodExecutionArgs args)
        {
            // 設置程序異常時的執行行爲
            args.FlowBehavior = Behavior;

            // 在方法執行前記錄日誌
            Log("Entering method: " + args.Method.Name);
            Console.WriteLine($"參數:{string.Join(',', args.Arguments)}");
        }

        public override void OnExit(MethodExecutionArgs args)
        {
            // 在方法執行後記錄日誌
            Log("Exiting method: " + args.Method.Name);
        }

        public override void OnException(MethodExecutionArgs arg)
        {
            // 在方法發生異常後執行
            Console.WriteLine($"發生異常:{arg.Exception}");
        }

        private void Log(string message)
        {
            // 記錄日誌的具體實現
            Console.WriteLine(message);
        }
    }

可以注意到上述代碼多了一個FlowBehavior屬性。OnMethodBoundaryAspectAttribute特性提供該屬性來控制異常發生時程序的流程行爲,即:

    public enum FlowBehavior
    {
        Default,
        Continue,
        RethrowException,
        Return
    }

接着,我們新建一個類來模擬我們的業務,並將上述特性應用在該類上(注意:如果應用在類上,則類中所有方法都將被自動應用該特性):

    [LogAspect(MethodBoundaryAspect.Fody.Attributes.FlowBehavior.Continue)]
    public class Test
    {
        // 模擬登錄
        public void Login(string msg)
        {
            Console.WriteLine($"Login Success: {msg}");
            
            // 下面代碼模擬一個異常
            int x = 0;
            int y = 1;
            int z = y / x;
        }
    }

編譯成功後,我們通過反編譯工具來看看生成的代碼:

[LogAspect(FlowBehavior.Continue)]
public class Test
{
	[DebuggerStepThrough]
	public void Login(string msg)
	{
		//Discarded unreachable code: IL_0088
		object[] x = new object[1] { msg };
		MethodExecutionArgs z = new MethodExecutionArgs();
		z.Arguments = x;
		MethodBase __var_3 = (z.Method = MethodInfos._methodInfo_E4323935D8604508164216EE96DF4799411096CB20F814182C1BBD7FF582F164);
		Test y = (Test)(z.Instance = this);
		LogAspectAttribute __var_4 = new LogAspectAttribute(FlowBehavior.Continue);
		__var_4.OnEntry(z);
		FlowBehavior __var_5 = z.FlowBehavior;
		if (__var_5 == FlowBehavior.Return)
		{
			return;
		}
		try
		{
			$_executor_Login(msg);
		}
		catch (Exception __var_6)
		{
			Exception __var_7 = (z.Exception = __var_6);
			__var_4.OnException(z);
			FlowBehavior __var_8 = z.FlowBehavior;
			if (__var_8 == FlowBehavior.Continue || __var_8 == FlowBehavior.Return)
			{
				return;
			}
			throw;
		}
		__var_4.OnExit(z);
	}

	[MethodImpl(MethodImplOptions.AggressiveInlining)]
	private void $_executor_Login(string msg)
	{
		Console.WriteLine("Login Success: " + msg);
		int x = 0;
		int y = 1;
		int z = y / x;
	}
}

可以看生成後的代碼相當簡單,也很容易理解。觀察編譯後的二進制文件目錄,我們可以發現僅僅多了一個MethodBoundaryAspect.dll 庫依賴,並且通過反編譯該庫可以知道,裏面基本上都是一些特性類,安全可靠。 這也是我們選擇Fody的重要原因之一。

5. 結論

本文探討了Fody的應用,通過選擇Fody與MethodBoundaryAspect作爲AOP框架,我們可以在不修改源代碼的情況下,實現橫切關注點的解耦和複用,提高代碼的可維護性和可擴展性。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章