AOP(Aspect Oriented Programming),即在不破坏封装的前提下,去额外扩展功能。下面是一些概念与区别。
面向过程编程-POP(Process-oriented programming),是一种以过程为中心的编程思想,分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现。面向对象编程-OOP(Object Oriented Programming),核心思想是将客观存在的不同事物抽象成相互独立的类,然后把与事物相关的属性和行为封装到类里,并通过继承和多态来定义类彼此间的关系,以“继承”关系为主线,我们称之为纵向。面向切面编程-AOP(Aspect Oriented Programming),核心思想是将业务逻辑中与类不相关的通用功能切面式的提取分离出来,让多个类共享一个行为,一旦这个行为发生改变,不必修改类,而只需要修改这个行为即可。在asp.net mvc 中 webapi 和 mvc 处理消息是两个不同的管道,Asp.net mvc 和 webapi 为我们提供的 ActionFilterAttribute 拦截器,通过 重写 OnActionExecutingAsync,来 拦截action的请求消息,当执行OnActionExecutingAsync完成以后才真正进入请求的action中,action运行完后又把控制权给了 OnActionExecutedAsync ,这个管道机制可以使我们用它来轻松实现权限认证、日志记录 ,跨域以及很多需要对全局或者部分请求做手脚的的功能。
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)] public abstract class ActionFilterAttribute : Attribute, IActionFilter, IFilterMetadata, IAsyncActionFilter, IAsyncResultFilter, IOrderedFilter, IResultFilter { public virtual void OnActionExecuted(ActionExecutedContext context); // Action执行之前 public virtual void OnActionExecuting(ActionExecutingContext context); // Action执行之后 public virtual void OnResultExecuted(ResultExecutedContext context); // Result执行之后 public virtual void OnResultExecuting(ResultExecutingContext context); // Result执行之后 }可以看出如上代码是经典的切面编程例子,那如果我们自己面向切面进行编程那该如何实现呢?
本小节的目的是:先创建一个基础的业务逻辑类CongService,然后创建一个拦截器类CustomInterceptor,在不改动CongService的逻辑的前提下通过CustomInterceptor来扩展其功能。 第一步:创建一个Console应用,并安装Castle.Core包
install-package Castle.Core第二步:创建业务逻辑类CongService, 注意里面包含虚方法和实方法
public class CongService { public void GreetWithoutInterceptor() { Console.WriteLine("Hello World - Without Interceptor"); } public virtual void GreetWithInterceptor() { Console.WriteLine("Hello World - With Interceptor"); } }第三步:创建一个拦截器类CustomInterceptor,希望扩展CongService中的方法的功能。
using Castle.DynamicProxy; using System; namespace Cong.AOP { public class CustomInterceptor : StandardInterceptor { protected override void PreProceed(IInvocation invocation) { Console.WriteLine($"执行前:{invocation.Method.Name}"); } protected override void PerformProceed(IInvocation invocation) { Console.WriteLine($"将执行:{invocation.Method.Name}"); base.PerformProceed(invocation); // 具体方法在这里执行 } protected override void PostProceed(IInvocation invocation) { Console.WriteLine($"执行后:{invocation.Method.Name}"); } } }第四步:在main方法里码一下如下代码:
using System; using Castle.DynamicProxy; namespace Cong.AOP { class Program { static void Main(string[] args) { ProxyGenerator proxyGenerator = new ProxyGenerator(); // 创建一个代理动态生成器 CustomInterceptor customInterceptor = new CustomInterceptor(); // 创建一个拦截器 var congServiceProxy = proxyGenerator.CreateClassProxy<CongService>(customInterceptor); congServiceProxy.GreetWithInterceptor(); Console.WriteLine("------------华丽的分割线------------"); congServiceProxy.GreetWithoutInterceptor(); Console.ReadLine(); } } }第五步:显示式结果: 可以看出,在执行方法congServiceProxy.GreetWithInterceptor()时,拦截器CustomInterceptor里的三个方法都被执行了,而我们并没有改动CongService里的任何逻辑。 注意执行实方法congServiceProxy.GreetWithoutInterceptor()时,拦截器CustomInterceptor里的三个方法却没有被执行,实方法在执行是没法替换,而虚方法相当于一个指针。
在Program.Main方法里,我们是创建了一个基于类的代理,但在实际开发过程中,我们通常是使用基于接口的代理,本小节将对业务逻辑类CongService稍作修改,并继承于接口ICongService,然后修改Program.Main使用基于接口的代理,我们将实现它。
第一步:修改业务逻辑类CongService,及创建一个接口ICongService,只有一个方法,让代码保持足够简单
using System; namespace Cong.AOP { public class CongService: ICongService { public void Greet() { Console.WriteLine("Hello World"); } } public interface ICongService { void Greet(); } }第二步:修改Program.Main方法,如下:
using System; using Castle.DynamicProxy; namespace Cong.AOP { class Program { static void Main(string[] args) { ProxyGenerator proxyGenerator = new ProxyGenerator(); CustomInterceptor customInterceptor = new CustomInterceptor(); CongService congService = new CongService(); var congServiceProxy = proxyGenerator.CreateInterfaceProxyWithTarget<ICongService>(congService, customInterceptor); congServiceProxy.Greet(); Console.ReadLine(); } } }第三步:显示式结果: 可以看出,在执行方法congServiceProxy.Greet()时,拦截器CustomInterceptor里的三个方法都被执行了,而我们并没有改动CongService里的任何逻辑;如果接口ICongService里有多个方法,那所有的方法执行时,都会被拦截器CustomInterceptor里的三个方法进行“拦截”。
在实际开发过程中,拦截器CustomInterceptor的功能是写死的,不满足业务的灵活需求,同时我们也不希望接口ICongService中所有的方法都被“拦截”。还记得在MVC框架中,我们只需要在Controller或Action上加上一些特性就可以实现权限、日志、路由等功能。接下来本小节将来实现类似的功能,为了保持简单,我们只创建一个特性LogInterceptorAttribute,期望在执行业务逻辑类CongService中的方法时,能自动先记录日志。
第一步:创建一个特性LogInterceptorAttribute
using System; namespace Cong.AOP { [AttributeUsage(AttributeTargets.Method)] public class LogInterceptorAttribute : Attribute { public void Execute() { Console.WriteLine($"执行-日志"); } } }第二步:修改业务逻辑类CongService,及创建一个接口ICongService,注意这里有两个方法,分别是Greet1和Greet2,在接口中方法Greet2上加了一个特性LogInterceptorAttribute,期望它能扩展业务逻辑类中Greet2的功能,即执行Greet2时先记录日志。
using System; namespace Cong.AOP { public class CongService: ICongService { public void Greet1() { Console.WriteLine("Hello World - 1"); } public void Greet2() { Console.WriteLine("Hello World - 2"); } } public interface ICongService { void Greet1(); [LogInterceptor] void Greet2(); } }第三步:修改拦截器类CustomInterceptor,注意PerformProceed方法
using Castle.DynamicProxy; using System.Reflection; namespace Cong.AOP { public class CustomInterceptor : StandardInterceptor { protected override void PreProceed(IInvocation invocation) { //Console.WriteLine($"执行前:{invocation.Method.Name}"); } protected override void PerformProceed(IInvocation invocation) { //Console.WriteLine($"将执行:{invocation.Method.Name}"); var methodInfo = invocation.Method; //反射得到具体方法的信息 if (methodInfo.IsDefined(typeof(LogInterceptorAttribute), true)) { LogInterceptorAttribute logInterceptorAttribute = methodInfo.GetCustomAttribute<LogInterceptorAttribute>(); logInterceptorAttribute.Execute(); // 在这里执行日志 } base.PerformProceed(invocation); } protected override void PostProceed(IInvocation invocation) { //Console.WriteLine($"执行后:{invocation.Method.Name}"); } } }第四步:保持Program.Main方法不变,如下:
using System; using Castle.DynamicProxy; namespace Cong.AOP { class Program { static void Main(string[] args) { ProxyGenerator proxyGenerator = new ProxyGenerator(); CustomInterceptor customInterceptor = new CustomInterceptor(); CongService congService = new CongService(); var congServiceProxy = proxyGenerator.CreateInterfaceProxyWithTarget<ICongService>(congService, customInterceptor); congServiceProxy.Greet1(); Console.WriteLine("------------华丽的分割线------------"); congServiceProxy.Greet2(); Console.ReadLine(); } } }第五步:显示式结果: 可以看出,只要我们在业务逻辑接口中的方法上加上特性logInterceptor就拥有日志的功能,没加特性的方法就没有日志功能。
在MVC框架中,我们是可以在Controller或Action上加上多个特性的,我们接下来实现这样一个效果,只要在某个方法上加上特性LogInterceptorAttribute和特性LoginInterceptorAttribute,那此方法就会拥有日志和登录这两个功能。
第一步:创建一个抽象特性BaseInterceptorAttribute和两个子特性LogInterceptorAttribute和特性LoginInterceptorAttribute
using System; namespace Cong.AOP { [AttributeUsage(AttributeTargets.Method)] public class LogInterceptorAttribute : BaseInterceptorAttribute { public override void Execute() { Console.WriteLine($"执行-日志"); } } [AttributeUsage(AttributeTargets.Method)] public class LoginInterceptorAttribute : BaseInterceptorAttribute { public override void Execute() { Console.WriteLine($"执行-登录"); } } [AttributeUsage(AttributeTargets.Method)] public abstract class BaseInterceptorAttribute : Attribute { public abstract void Execute(); } }第二步:保持业务逻辑类CongService不变,唯一变化的就是在方法Greet2上多加了一个特性LoginInterceptorAttribute,让它同时拥有两个特性LogInterceptorAttribute和特性LoginInterceptorAttribute。
using System; namespace Cong.AOP { public class CongService: ICongService { public void Greet1() { Console.WriteLine("Hello World - 1"); } public void Greet2() { Console.WriteLine("Hello World - 2"); } } public interface ICongService { void Greet1(); [LogInterceptor] [LoginInterceptor] void Greet2(); } }第三步:修改拦截器类CustomInterceptor,注意PerformProceed方法
using Castle.DynamicProxy; using System.Reflection; namespace Cong.AOP { public class CustomInterceptor : StandardInterceptor { protected override void PreProceed(IInvocation invocation) { //Console.WriteLine($"执行前:{invocation.Method.Name}"); } protected override void PerformProceed(IInvocation invocation) { //Console.WriteLine($"将执行:{invocation.Method.Name}"); var methodInfo = invocation.Method; //反射得到具体方法的信息 if (methodInfo.IsDefined(typeof(BaseInterceptorAttribute), true)) { foreach (var baseInterceptorAttribute in methodInfo.GetCustomAttributes<BaseInterceptorAttribute>()) { baseInterceptorAttribute.Execute(); // 在这里执行日志或者登录功能 } } base.PerformProceed(invocation); } protected override void PostProceed(IInvocation invocation) { //Console.WriteLine($"执行后:{invocation.Method.Name}"); } } }第四步:保持Program.Main方法不变,如下:
using System; using Castle.DynamicProxy; namespace Cong.AOP { class Program { static void Main(string[] args) { ProxyGenerator proxyGenerator = new ProxyGenerator(); CustomInterceptor customInterceptor = new CustomInterceptor(); CongService congService = new CongService(); var congServiceProxy = proxyGenerator.CreateInterfaceProxyWithTarget<ICongService>(congService, customInterceptor); congServiceProxy.Greet1(); Console.WriteLine("------------华丽的分割线------------"); congServiceProxy.Greet2(); Console.ReadLine(); } } }第五步:显示式结果:
可以看出,我们在业务逻辑接口中的方法上加上了特性LogInterceptorAttribute和特性LoginInterceptorAttribute就拥有日志和登录的功能。
在上一章节的日志特性LogInterceptorAttribute中,如果在业务逻辑接口中的某方法上加上此特性,那它就会在执行业务方法前去记录日志,但我们希望在执行业务方法的前后都记录日志,以便统计某业务方法共花了多少时间。也就是说到目前为止,这些特性只是完成的简单的串连顺序执行,并没有实现像中间件(俄罗斯套娃)那样的执行。那该怎么实现呢?我们这里需要配置管道。
在c#中只有委托才能配置管道,注意拦截器类CustomInterceptor的方法PerformProceed中的这行代码base.PerformProceed(invocation),可以看出我们实现的委托是Action(invocation),本小节我们来实现它。
第一步:修改此3个特性。
给方法LogInterceptorAttribute.Execute()传一个委托Action(invocation)参数,为什么要传入一个委托参数呢,因为我希望统计某业务方法CongService.Greet2()共花了多少时间,必需要在执行此业务方法CongService.Greet2()前后都加入日志,所以需要将此业务方法CongService.Greet2()以某种形式传入到方法LogInterceptorAttribute.Execute()之中,所以传入了一个委托参数。让方法LogInterceptorAttribute.Execute()返回个委托Action(invocation),为什么要返回呢,目的是为了组装管道。代码如下:
using Castle.DynamicProxy; using System; namespace Cong.AOP { [AttributeUsage(AttributeTargets.Method)] public class LogInterceptorAttribute : BaseInterceptorAttribute { public override Action<IInvocation> Execute(Action<IInvocation> action) { return invocation => { Console.WriteLine($"执行-日志-开始时间:{DateTime.Now}"); action.Invoke(invocation); Console.WriteLine($"执行-日志-结束时间:{DateTime.Now}"); }; } } [AttributeUsage(AttributeTargets.Method)] public class LoginInterceptorAttribute : BaseInterceptorAttribute { public override Action<IInvocation> Execute(Action<IInvocation> action) { return invocation => { Console.WriteLine($"执行-登录前"); action.Invoke(invocation); Console.WriteLine($"执行-登录后"); }; } } [AttributeUsage(AttributeTargets.Method)] public abstract class BaseInterceptorAttribute : Attribute { public abstract Action<IInvocation> Execute(Action<IInvocation> action); } }第二步:保持业务逻辑类CongService不变,方法Greet2上依然同时拥有两个特性LogInterceptorAttribute和特性LoginInterceptorAttribute。
using System; namespace Cong.AOP { public class CongService: ICongService { public void Greet1() { Console.WriteLine("Hello World - 1"); } public void Greet2() { Console.WriteLine("Hello World - 2"); } } public interface ICongService { void Greet1(); [LogInterceptor] [LoginInterceptor] void Greet2(); } }第三步:修改拦截器类CustomInterceptor,注意PerformProceed方法,这里是关键
using Castle.DynamicProxy; using System; using System.Linq; using System.Reflection; namespace Cong.AOP { public class CustomInterceptor : StandardInterceptor { protected override void PreProceed(IInvocation invocation) { //Console.WriteLine($"执行前:{invocation.Method.Name}"); } protected override void PerformProceed(IInvocation invocation) { //Console.WriteLine($"将执行:{invocation.Method.Name}"); var methodInfo = invocation.Method; //反射得到具体方法的信息 var action = new Action<IInvocation>(baseInvocation => base.PerformProceed(baseInvocation)); if (methodInfo.IsDefined(typeof(BaseInterceptorAttribute), true)) { foreach (var baseInterceptorAttribute in methodInfo.GetCustomAttributes<BaseInterceptorAttribute>().Reverse()) { action = baseInterceptorAttribute.Execute(action); // 组装管道 } } action.Invoke(invocation); //执行管道 } protected override void PostProceed(IInvocation invocation) { //Console.WriteLine($"执行后:{invocation.Method.Name}"); } } }第四步:保持Program.Main方法不变,如下:
using System; using Castle.DynamicProxy; namespace Cong.AOP { class Program { static void Main(string[] args) { ProxyGenerator proxyGenerator = new ProxyGenerator(); CustomInterceptor customInterceptor = new CustomInterceptor(); CongService congService = new CongService(); var congServiceProxy = proxyGenerator.CreateInterfaceProxyWithTarget<ICongService>(congService, customInterceptor); congServiceProxy.Greet1(); Console.WriteLine("------------华丽的分割线------------"); congServiceProxy.Greet2(); Console.ReadLine(); } } }第五步:显示式结果: 可以看出,我们执行业务方法Greet2的前后都记录了日志,统计某业务方法共花了多少时间,也在执行登录功能前后都作了处理。方法Greet2在最里层,登录功能在中间层,日志功能在最外层。
AOP 是目前软件开发中的一个热点,利用AOP可以对业务逻辑的各个部分隔离,从而使的业务逻辑各部分的耦合性降低,提高程序的可重用性,提高开发效率,主要功能:日志记录,性能统计,安全控制,事务处理,异常处理等。
它的核心思想是将业务逻辑中与类不相关的通用功能切面式的提取分离出来,让多个类共享一个行为,一旦这个行为发生改变,不必修改类,而只需要修改这个行为即可。
基于切面编程是属于23种设计模式中的代理模式,所做的操作都是在不改动业务逻辑的前提下进行,使用场景如下:
它是属于一种“补救”模式,特别是当你引用第三方库的时候它会很有用。如果你能直接改业务逻辑的话,那就不需要使用此模式,不然只会增加代码的复杂性。希望将多个方法中共用的逻辑抽出来灵活配置。[1]. https://docs.microsoft.com/zh-cn/aspnet/core/getting-started/?view=aspnetcore-3.1&tabs=windows [2] 理解POP,OOP,AOP之间的关系
.net 5.0将在今年11月以unify platform的平台面市,虽然.net 沉寂已久,但它有诸多优点,在容器化/微服务大行其道的今天,期待它能力挽狂澜,带领dotnet重回巅峰。