結合ASP.NET Core,淺談AOP的實現
一、什麼是AOP?
AOP是程序開發的一種熱點,AOP是OOP的延續。
官方解釋,AOP爲Aspect Oriented Programming的縮寫,意爲:面向切面編程。是一種通過預編譯方式和運行期動態代理實現程序功能統一維護的技術。利用AOP可以對業務邏輯的各個部分進行分割、隔離,從而降低業務邏輯的各個部分之間的耦合度,提高程序的可維護性,提高了開發效率。
個人理解,AOP是一種面向方面(模塊)的編程,能夠對業務邏輯進行分割、隔離。就好比在程序開發中,每個人的各司其職,前端開發工程師只需要關注前端部分,如何調用後臺API、後端開發工程師只需要關注後端部分,提供API給前端、UI設計師只負責設計圖形界面、美工只負責UI的美化等。即一個應用程序,也好比一個公司,每個職位每個人都是相對獨立的,銷售就負責跑業務,實施負責對接客戶,HR做人力資源劃分,開發部門做產品開發,測試部門做產品測試,維護部門負責產品上線、系統維護等。使用AOP,即可以將公共代碼部分抽離出來,比如:每個方法都是需要記錄日誌,驗證權限,捕捉異常。我們可以將這些業務,封裝成獨立的模塊,將這部分的業務交給模塊執行。則程序開發人員做開發工程中,我們就可以只關注核心業務邏輯的編寫,不需要考慮日誌的記錄,權限認證和捕捉異常等。即將代碼模塊化。
二、爲什麼要用AOP?
現在有這麼一個業務場景:
場景一:
普通顧客lyq來到一家餐廳喫飯,服務員在每次點單前後,都要記錄一條log。
當我們拿到這個簡單的需求,我們肯定很快就想到這麼實現這個業務:
public class OrderMenu
{
public string Name { get; set; }
public decimal Price { get; set; }
public string Remarks { get; set; }
}
public class Customer
{
public string Name { get; set; }
public void PlaceOrder(OrderMenu order)
{
Console.WriteLine("點單開始");
Console.WriteLine($@"我需要一份{order.Name},價錢是{order.Price},備註:{order.Remarks}");
Console.WriteLine("點單結束");
}
}
看似很簡單的需求,看起來用不用AOP實現都一樣。但是在程序開發中,一成不變的需求往往是不存在的,在開發過程中,我們往往會在這樣的基礎上,增添需求。比如:顧客分VIP,VIP消費能夠累計積分,積分每滿1000分能夠兌換100元的餐券。這個時候,我們就要修改業務代碼。
我們給Customer類加一個Role屬性,用來記錄當前顧客時VIP還是普通客戶,修改如下:
public class Customer
{
public string Name { get; set; }
public string Role { get; set; }
public void PlaceOrder(OrderMenu order)
{
Console.WriteLine("點單開始");
if (Role == "VIP")
Console.WriteLine($@"我是VIP,我需要一份{order.Name},價錢是{order.Price},備註:{order.Remarks},點餐成功,積分累計加{order.Price}");
else
Console.WriteLine($@"我是普通顧客,我需要一份{order.Name},價錢是{order.Price},備註:{order.Remarks}");
Console.WriteLine("點單結束");
}
}
可是,客戶不會這麼溫柔啊。在這個的基礎上,客戶還要添加業務邏輯。每次點單時,要判斷廚房是否還有這個菜品,如果有,則正常下單,如果沒有,則提示顧客已經沒有這個菜品了,讓顧客重新點餐;每次點餐時,也要判斷菜品的價格是否合理,如果太低了(比如:豬肉漲價),會提示本次下單失敗等等。。。。。。
顯然,當我們豐滿這個業務的時候,我們都會修改這部分的代碼。久而久之,這個業務類就會越來越臃腫,越來越難維護。因此,纔有了本文開篇所說的使用AOP,將業務模塊和記錄Log模塊分割開來,業務模塊只專注業務部分,記錄Log模塊只做操作日誌處理。這樣,我們有利於項目後期的維護和擴展,當出現問題的時候,我們能夠快速的定位問題和解決問題。下面,來說一下如何實現AOP。
三、用靜態代理模式和裝飾着模式實現AOP
我們先定義一個代理接口IOrder:
public interface IOrder
{
void PlaceOrder(OrderMenu order);
}
然後我們要再添加一個代理類OrderMenuProxy,並且繼承這個IOrder的接口:
public class MenuOrderLogProxy : IOrder
{
private IOrder _order;
public MenuOrderLogProxy(IOrder order)
{
_order = order;
}
public void PlaceOrder(OrderMenu order)
{
Console.WriteLine("點單開始");
_order.PlaceOrder(order);
Console.WriteLine("點單結束");
}
}
然後,我們再修改一下業務類,讓他們也繼承這個IOrder的接口:
public class Customer : IOrder
{
public string Name { get; set; }
public string Role { get; set; }
public void PlaceOrder(OrderMenu order)
{
if (Role == "VIP")
{
Console.WriteLine($@"我是VIP,我需要一份{order.Name},價錢是{order.Price},備註:{order.Remarks},點餐成功,積分累計加20");
}
Console.WriteLine($@"我是普通顧客,我需要一份{order.Name},價錢是{order.Price},備註:{order.Remarks}");
}
}
OrderMenu order = new OrderMenu()
{
IsActive = true,
Name = "小炒牛肉",
Price = 25,
Remarks = "微辣"
};
IOrder customer1 = new Customer();
OrderMenuProxy orderProxy = new OrderMenuProxy(customer1);
orderProxy.PlaceOrder(order);
可以看出,AOP代碼的維護性更高,當用戶需要再修改需求的時候,我們只需要針對相對應的模塊做修改就可以了,不必將某一模塊的內容分散到各個業務模塊中去。業務模塊只要專注與業務方法的編寫,日誌模塊只關注怎麼日誌的記錄。
用戶畢竟是用戶,用戶突然腦洞大開,想在以上的基礎上,再加上一次異常捕獲模塊。如果還是使用靜態代理的方法,我們可能會將異常處理的方法放到OrderMenuProxy這個代理類中:
public class OrderMenuProxy : IOrder
{
private IOrder _order;
public OrderMenuProxy(IOrder order)
{
_order = order;
}
public void PlaceOrder(OrderMenu order)
{
try
{
Console.WriteLine("記錄異常開始.");
Console.WriteLine("點單開始");
if (order.Price > 0 && order.IsActive)
{
_order.PlaceOrder(order);
}
Console.WriteLine("點單結束");
}
catch (Exception ex)
{
Console.WriteLine("下單異常:" + ex.Message);
}
finally
{
Console.WriteLine("記錄異常結束.");
}
}
}
很顯然,日誌模塊和異常處理模塊耦合在一起了。如果後面隨着業務的增加,則這個代理類的方法將越來越複雜,這就違背開始我們的初衷,將整個業務的模塊分割開來。
看一下我們用裝飾者模式,如何解決這樣的問題,我們再定義一個OrderMenuExceptionProxy類:
public class OrderMenuExceptionProxy: IOrder
{
private IOrder _order;
public OrderMenuExceptionProxy(IOrder order)
{
_order = order;
}
public void PlaceOrder(OrderMenu order)
{
try
{
Console.WriteLine("記錄異常開始.");
_order.PlaceOrder(order);
}
catch (Exception ex)
{
Console.WriteLine("下單異常:" + ex.Message);
}
finally
{
Console.WriteLine("記錄異常結束.");
}
}
}
OrderMenu order = new OrderMenu()
{
IsActive = true,
Name = "小炒牛肉",
Price = 25,
Remarks = "微辣"
};
IOrder customer1 = new Customer();
IOrder orderMenuExceptionProxy = new OrderMenuExceptionProxy(customer1);
IOrder orderMenuLogProxy = new OrderMenuLogProxy(orderMenuExceptionProxy);
orderMenuLogProxy.PlaceOrder(order);
用裝飾者模式,能夠很好的實現我們想要的功能。可以將業務模塊,日誌模塊,異常處理模塊分割開來,降低了模塊與模塊之間的依賴性。
總結:靜態代理模式和裝飾者模式這兩種方法都能實現AOP,其實這兩種方法在使用的時候區別並不是很大。但是靜態代理模式講究的是將業務功能交給代理類去實現;而裝飾者模式講究的是組合方法,爲這個業務類添加各種裝飾品(功能)。
上述的兩種方法都是能夠實現AOP,但有一點很麻煩的是,當我們每次調用這個下單方法的時候,我們都要實例化每一個代理類。如果我們要調用100次這個方法方法,我們就要實例化100個代理類,這無疑會對我們的程序開發增加負擔,那我們如何減輕這負擔呢?我們可以使用動態代理的方法,實現AOP。
四、用動態代理的方法實現AOP
先引用Castle.Core的NuGet包。Castle.Core是第三方的開源框架,用於寫攔截器,實現AOP。何謂攔截器,就是每次再調用這個方法的時候,攔截器會將這個方法捕獲,然後讓我們的程序先去運行我們所指定的方法,再去執行我們所請求的方法。
我們分別定位兩個攔截器LogInterceptor,ExceptionInterceptor:
public class LogInterceptor : IInterceptor
{
public void Intercept(IInvocation invocation)
{
Console.WriteLine("點單開始");
invocation.Proceed();
Console.WriteLine("點單結束");
}
}
public class ExceptionInterceptor : IInterceptor
{
public void Intercept(IInvocation invocation)
{
try
{
Console.WriteLine("記錄異常開始。");
invocation.Proceed();
}
catch (Exception ex)
{
Console.WriteLine("下單異常:" + ex.Message);
}
finally
{
Console.WriteLine("記錄異常結束。");
}
}
}
然後我們重寫一下Customer下單的方法,將這個方法設置爲一個虛方法(virtual ):
public virtual void PlaceOrder(OrderMenu order)
{
if (Role == "VIP")
{
Console.WriteLine($@"我是VIP,我需要一份{order.Name},價錢是{order.Price},備註:{order.Remarks},點餐成功,積分累計加{order.Price}");
}
Console.WriteLine($@"我是普通顧客,我需要一份{order.Name},價錢是{order.Price},備註:{order.Remarks}");
}
接下來看怎麼用,能夠實現我們想要的功能:
var proxyGenerator = new ProxyGenerator();//創建一個代理生成器
var customerService = proxyGenerator.CreateClassProxy<Customer>(new ExceptionInterceptor(),new LogInterceptor());
customerService.Role = "VIP";
customerService.PlaceOrder(order);
五、ASP.NET Core 本身實現AOP的方式
1.Filter過濾器
ASP.NET CORE MVC一共有四種過濾器:分別是權限過濾器(AuthorizationFilter),資源過濾器(ResourceFilter),操作過濾器(ActionFilter),異常過濾器(ExceptionFilter).(優先級:權限>資源>操作>異常)
權限過濾器主要用作權限認證,每當用戶請求這個方法時,會判斷當前用戶是否有權限,如果沒有權限會返回401的錯誤。
資源過濾器:常用於獲取緩存資源,當用戶有權限訪問請求方法時,會用來讀取用戶存儲在服務器上的資源。
操作過濾器:針對請求方法前後所做的操作。
異常過濾器:針對的是程序在運行過程中拋出異常,捕獲異常,常用於全局配置
public class MyAuthorizeFilterAttribute : Attribute, IAuthorizationFilter
{
public void OnAuthorization(AuthorizationFilterContext context)
{
throw new NotImplementedException();
}
}
public class MyResourceFilter : Attribute, IResourceFilter
{
public void OnResourceExecuted(ResourceExecutedContext context)
{
throw new NotImplementedException();
}
public void OnResourceExecuting(ResourceExecutingContext context)
{
throw new NotImplementedException();
}
}
public class LogAFilterAttribute : ActionFilterAttribute
{
public override void OnActionExecuted(ActionExecutedContext context)
{
}
public override void OnActionExecuting(ActionExecutingContext context)
{
}
}
public class MyExceptionFilterAttribute : ExceptionFilterAttribute
{
public override void OnException(ExceptionContext context)
{
base.OnException(context);
}
}
然後,只要在請求的方法上貼上這個Attribute就好了。
[LogAFilterAttribute]
public IActionResult TestIndex()
{
HttpContext.Session.SetString("userName", "Tim");
var obj = HttpContext.Session.GetString("userName");
return Json($"已經設置好值了:{obj}");
}
2. MiddleWare中間件
中間件是組裝到應用程序管道中以處理請求和響應的軟件。 每個組件都可以選擇是否將請求傳遞給管道中的下一個組件,每一個組件都可以在調用管道中的下一個組件之前和之後執行工作。
中間件和Filter過濾器的講解,請詳細看下一篇文章。