C#進階系列——AOP?AOP!

發表於2016-04-05
原文:C#進階系列——AOP?AOP!

前言:今天大閱兵,可是苦逼的博主還得坐在電腦前寫部落格,為了弄清楚AOP,博主也是拼了。這篇打算寫寫AOP,說起AOP,其實博主接觸這個概念也才幾個月,瞭解後才知道,原來之前自己寫的好多程式碼原理就是基於AOP的,比如MVC的過濾器Filter,它裡面的異常捕捉可以通過FilterAttribute,IExceptionFilter去處理,這兩個物件的處理機制內部原理應該就是AOP,只不過之前沒有這個概念罷了。

一、AOP概念

老規矩,還是先看官方解釋:AOP(Aspect-Oriented Programming,面向切面的程式設計),它是可以通過預編譯方式和執行期動態代理實現在不修改原始碼的情況下給程式動態統一新增功能的一種技術。它是一種新的方法論,它是對傳統OOP程式設計的一種補充。OOP是關注將需求功能劃分為不同的並且相對獨立,封裝良好的類,並讓它們有著屬於自己的行為,依靠繼承和多型等來定義彼此的關係;AOP是希望能夠將通用需求功能從不相關的類當中分離出來,能夠使得很多類共享一個行為,一旦發生變化,不必修改很多類,而只需要修改這個行為即可。AOP是使用切面(aspect)將橫切關注點模組化,OOP是使用類將狀態和行為模組化。在OOP的世界中,程式都是通過類和介面組織的,使用它們實現程式的核心業務邏輯是十分合適。但是對於實現橫切關注點(跨越應用程式多個模組的功能需求)則十分吃力,比如日誌記錄,許可權驗證,異常攔截等。

博主的理解:AOP就是將公用功能提取出來,如果以後公用功能的需求發生變化,只需要改動公用的模組的程式碼即可,多個呼叫的地方則不需要改動。所謂面向切面,就是隻關注通用功能,而不關注業務邏輯。實現方式一般是通過攔截。比如,我們隨便一個Web專案基本都有的許可權驗證功能,進入每個頁面前都會校驗當前登入使用者是否有許可權檢視該介面,我們不可能說在每個頁面的初始化方法裡面都去寫這段驗證的程式碼,這個時候我們的AOP就派上用場了,AOP的機制是預先定義一組特性,使它具有攔截方法的功能,可以讓你在執行方法之前和之後做你想做的業務,而我們使用的時候只需要的對應的方法或者類定義上面加上某一個特性就好了。

二、使用AOP的優勢

博主覺得它的優勢主要表現在:

1、將通用功能從業務邏輯中抽離出來,可以省略大量重複程式碼,有利於程式碼的操作和維護。

2、在軟體設計時,抽出通用功能(切面),有利於軟體設計的模組化,降低軟體架構的複雜度。也就是說通用的功能都是一個單獨的模組,在專案的主業務裡面是看不到這些通用功能的設計程式碼的。

三、AOP的簡單應用

為了說明AOP的工作原理,博主打算先從一個簡單的例子開始,通過靜態攔截的方式來了解AOP是如何工作的。

1、靜態攔截

    public class Order
    {
        public int Id { set; get; }
        public string Name { set; get; }
        public int Count { set; get; }
        public double Price { set; get; }
        public string Desc { set; get; }
    }

    public interface IOrderProcessor
    {
        void Submit(Order order);
    }
    public class OrderProcessor : IOrderProcessor
    {
        public void Submit(Order order)
        {
            Console.WriteLine("提交訂單");
        }
    }

    public class OrderProcessorDecorator : IOrderProcessor
    {
        public IOrderProcessor OrderProcessor { get; set; }
        public OrderProcessorDecorator(IOrderProcessor orderprocessor)
        {
            OrderProcessor = orderprocessor;
        }
        public void Submit(Order order)
        {
            PreProceed(order);
            OrderProcessor.Submit(order);
            PostProceed(order);
        }
        public void PreProceed(Order order)
        {
            Console.WriteLine("提交訂單前,進行訂單資料校驗....");
            if (order.Price < 0)
            {
                Console.WriteLine("訂單總價有誤,請重新核對訂單。");
            }
        }

        public void PostProceed(Order order)
        {
            Console.WriteLine("提交帶單後,進行訂單日誌記錄......");
            Console.WriteLine(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + "提交訂單,訂單名稱:" + order.Name + ",訂單價格:" + order.Price);
        }
    }

呼叫程式碼:

     static void Main(string[] args)
        {
            Order order = new Order() { Id = 1, Name = "lee", Count = 10, Price = 100.00, Desc = "訂單測試" };
            IOrderProcessor orderprocessor = new OrderProcessorDecorator(new OrderProcessor());
            orderprocessor.Submit(order);
            Console.ReadLine();
        }

得到結果:

上面我們模擬訂單提交的例子,在提交一個訂單前,我們需要做很多的準備工作,比如資料有效性校驗等;訂單提交完成之後,我們還需要做日誌記錄等。上面的程式碼很簡單,沒有任何複雜的邏輯,從上面的程式碼可以看出,我們通過靜態植入的方式手動在執行方法前和執行方法後讓它做一些我們需要的功能。AOP的實現原理應該也是如此,只不過它幫助我們做了方法攔截,幫我們省去了大量重複程式碼,我們要做的僅僅是寫好攔截前和攔截後需要處理的邏輯。

2、動態代理

瞭解了靜態攔截的例子,你是否對AOP有一個初步的認識了呢。下面我們就來到底AOP該如何使用。按照園子裡面很多牛人的說法,AOP的實現方式大致可以分為兩類:動態代理和IL 編織兩種方式。博主也不打算照本宣科,分別拿Demo來說話吧。下面就以兩種方式各選一個代表框架來說明。

動態代理方式,博主就以微軟企業庫(MS Enterprise Library)裡面的PIAB(Policy Injection Application Block)框架來作說明。

首先需要下載以下幾個dll,然後新增它們的引用。

然後定義對應的Handler

   public class User
    {
        public string Name { set; get; }
        public string PassWord { set; get; }
    }

    #region 1、定義特性方便使用
    public class LogHandlerAttribute : HandlerAttribute
    {
        public string LogInfo { set; get; }
        public int Order { get; set; }
        public override ICallHandler CreateHandler(IUnityContainer container)
        {
            return new LogHandler() { Order = this.Order, LogInfo = this.LogInfo };
        }
    }
    #endregion

    #region 2、註冊對需要的Handler攔截請求
    public class LogHandler : ICallHandler
    {
        public int Order { get; set; }
        public string LogInfo { set; get; }

        //這個方法就是攔截的方法,可以規定在執行方法之前和之後的攔截
        public IMethodReturn Invoke(IMethodInvocation input, GetNextHandlerDelegate getNext)
        {
            Console.WriteLine("LogInfo內容" + LogInfo);
            //0.解析引數
            var arrInputs = input.Inputs;
            if (arrInputs.Count > 0)
            {
                var oUserTest1 = arrInputs[0] as User;
            }
            //1.執行方法之前的攔截
            Console.WriteLine("方法執行前攔截到了");
            //2.執行方法
            var messagereturn = getNext()(input, getNext);

            //3.執行方法之後的攔截
            Console.WriteLine("方法執行後攔截到了");
            return messagereturn;
        }
    }
    #endregion

    #region 3、使用者定義介面和實現
    public interface IUserOperation
    {
        void Test(User oUser);
        void Test2(User oUser, User oUser2);
    }


    //這裡必須要繼承這個類MarshalByRefObject,否則報錯
    public class UserOperation : MarshalByRefObject, IUserOperation
    {
        private static UserOperation oUserOpertion = null;
        public UserOperation()
        {
            //oUserOpertion = PolicyInjection.Create<UserOperation>();
        }

        //定義單例模式將PolicyInjection.Create<UserOperation>()產生的這個物件傳出去,這樣就避免了在呼叫處寫這些東西
        public static UserOperation GetInstance()
        {
            if (oUserOpertion == null)
                oUserOpertion = PolicyInjection.Create<UserOperation>();

            return oUserOpertion;
        }
        //呼叫屬性也會攔截
        public string Name { set; get; }

        //[LogHandler],在方法上面加這個特性,只對此方法攔截
        [LogHandler(LogInfo = "Test的日誌為aaaaa")]
        public void Test(User oUser)
        {
            Console.WriteLine("Test方法執行了");
        }

        [LogHandler(LogInfo = "Test2的日誌為bbbbb")]
        public void Test2(User oUser, User oUser2)
        {
            Console.WriteLine("Test2方法執行了");
        }
    }
    #endregion

最後我們來看呼叫的程式碼:

        static void Main(string[] args)
        {
            try
            {
                var oUserTest1 = new User() { Name = "test2222", PassWord = "yxj" };
                var oUserTest2 = new User() { Name = "test3333", PassWord = "yxj" };
                var oUser = UserOperation.GetInstance();
                oUser.Test(oUserTest1);
                oUser.Test2(oUserTest1,oUserTest2);
            }
            catch (Exception ex)
            {
                //throw;
            }
        }

得到結果如下:

我們來看執行Test()方法和Test2()方法時候的順序。

由於Test()和Test2()方法上面加了LogHander特性,這個特性裡面定義了AOP的Handler,在執行Test和Test2方法之前和之後都會進入Invoke()方法裡面。其實這就是AOP的意義所在,將切面的通用功能在統一的地方處理,在主要邏輯裡面直接用過特性使用即可。

3、IL編織

靜態織入的方式博主打算使用PostSharp來說明,一來這個使用起來簡單,二來專案中用過這種方式。

Postsharp從2.0版本就開始收費了。為了說明AOP的功能,博主下載了一個免費版本的安裝包,使用PostSharp與其它框架不太一樣的是一定要下載安裝包安裝,只引用類庫是不行的,因為上文說過,AOP框架需要為編譯器或執行時新增擴充套件。使用步驟如下:

(1)下載Postsharp安裝包,安裝。

(2)在需要使用AOP的專案中新增PostSharp.dll 這個dll的引用。

(3)定義攔截的方法:

    [Serializable]
    public class TestAop : PostSharp.Aspects.OnMethodBoundaryAspect
    {
     //發生異常時進入此方法
public override void OnException(MethodExecutionArgs args) { base.OnException(args); }
     //執行方法前執行此方法
public override void OnEntry(MethodExecutionArgs args) { base.OnEntry(args); }
     //執行方法後執行此方法
public override void OnExit(MethodExecutionArgs args) { base.OnExit(args); } }

注意這裡的TestAop這個類必須要是可序列化的,所以要加上[Serializable]特性

(4)在需要攔截功能的地方使用。

在類上面加特性攔截,此類下面的所有的方法都會具有攔截功能。

  [TestAop]public class Impc_TM_PLANT : Ifc_TM_PLANT
    {
        /// <summary>
        /// 獲取或設定服務介面。
        /// </summary>
        private Ic_TM_PLANTService service { get; set; }
        
        public IList<DTO_TM_PLANT> Find()
        {
            DTO_TM_PLANT otest = null;
            otest.NAME_C = "test";//異常,會進入OnException方法
        return service.FindAll(); 
     }
  }

方法上面加特性攔截,只會攔截此方法。

        [TestAop]
        public IList<DTO_TM_PLANT> Find()
        {
            DTO_TM_PLANT otest = null;
            otest.NAME_C = "test";
            return service.FindAll();
        }

有沒有感覺很簡單,很強大,其實這一簡單應用,解決我們常見的日誌、異常、許可權驗證等功能簡直太小菜一碟了。當然Postsharp可能還有許多更加高階的功能,有興趣可以深究下。

4、MVC裡面的Filter

   public class AOPFilterAttribute : ActionFilterAttribute, IExceptionFilter
    {

        public void OnException(ExceptionContext filterContext)
        {
            throw new System.NotImplementedException();
        }
        public override void OnActionExecuting(ActionExecutingContext filterContext)
        {
            
            base.OnActionExecuting(filterContext);
        }

        public override void OnActionExecuted(ActionExecutedContext filterContext)
        {
            base.OnActionExecuted(filterContext);
        }
    }

在controller裡面使用該特性:

     [AOPFilter]
        public JsonResult GetEditModel(string strType)
        {
            var lstRes = new List<List<DragElementProp>>();
            var lstResPage = new List<PageProperty>();

       //.........todo
return Json(new { lstDataAttr = lstRes, PageAttr = lstResPage, lstJsConnections = lstJsPlumbLines }, JsonRequestBehavior.AllowGet); }

除錯可知,在執行GetEditModel(string strType)方法之前,會先執行OnActionExecuting()方法,GetEditModel(string strType)之後,又會執行OnActionExecuted()方法。這在我們MVC裡面許可權驗證、錯誤頁導向、日誌記錄等常用功能都可以方便解決。

 

附上原始碼。有興趣可以下載看看。

 

相關文章