C# 实现AOP 的几种常见方式

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介:

AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方式和运行期动态代理实现程序功能的中统一处理业务逻辑的一种技术,比较常见的场景是:日志记录,错误捕获、性能监控等

AOP的本质是通过代理对象来间接执行真实对象,在代理类中往往会添加装饰一些额外的业务代码,比如如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
     class  RealA
     {
         public  virtual  string  Pro {  get set ; }
 
         public  virtual  void  ShowHello( string  name)
         {
             Console.WriteLine($ "Hello!{name},Welcome!" );
         }
     }
 
 
//调用:
 
             var  a =  new  RealA();
             a.Pro =  "测试" ;
             a.ShowHello( "梦在旅途" );

这段代码很简单,只是NEW一个对象,然后设置属性及调用方法,但如果我想在设置属性前后及调用方法前后或报错都能收集日志信息,该如何做呢?可能大家会想到,在设置属性及调用方法前后都加上记录日志的代码不就可以了,虽然这样是可以,但如果很多地方都要用到这个类的时候,那重复的代码是否太多了一些吧,所以我们应该使用代理模式或装饰模式,将原有的真实类RealA委托给代理类ProxyRealA来执行,代理类中在设置属性及调用方法时,再添加记录日志的代码就可以了,这样可以保证代码的干净整洁,也便于代码的后期维护。(注意,在C#中若需被子类重写,父类必需是虚方法或虚属性virtual)

如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
     class  ProxyRealA : RealA
     {
 
         public  override  string  Pro
         {
             get
             {
                 return  base .Pro;
             }
             set
             {
                 ShowLog( "设置Pro属性前日志信息" );
                 base .Pro = value;
                 ShowLog($ "设置Pro属性后日志信息:{value}" );
             }
         }
 
         public  override  void  ShowHello( string  name)
         {
             try
             {
                 ShowLog( "ShowHello执行前日志信息" );
                 base .ShowHello(name);
                 ShowLog( "ShowHello执行后日志信息" );
             }
             catch (Exception ex)
             {
                 ShowLog($ "ShowHello执行出错日志信息:{ex.Message}" );
             }
         }
 
         private  void  ShowLog( string  log)
         {
             Console.WriteLine($ "{DateTime.Now.ToString()}-{log}" );
         }
     }
 
 
//调用:
             var  aa =  new  ProxyRealA();
             aa.Pro =  "测试2" ;
             aa.ShowHello( "zuowenjun.cn" );

这段代码同样很简单,就是ProxyRealA继承自RealA类,即可看成是ProxyRealA代理RealA,由ProxyRealA提供各种属性及方法调用。这样在ProxyRealA类内部属性及方法执行前后都有统一记录日志的代码,不论在哪里用这个RealA类,都可以直接用ProxyRealA类代替,因为里氏替换原则,父类可以被子类替换,而且后续若想更改日志记录代码方式,只需要在ProxyRealA中更改就行了,这样所有用到的ProxyRealA类的日志都会改变,是不是很爽。上述执行结果如下图示:

 以上通过定义代理类的方式能够实现在方法中统一进行各种执行点的拦截代码逻辑处理,拦截点(或者称为:横切面,切面点)一般主要为:执行前,执行后,发生错误,虽然解决了之前直接调用真实类RealA时,需要重复增加各种逻辑代码的问题,但随之而来的新问题又来了,那就是当一个系统中的类非常多的时候,如果我们针对每个类都定义一个代理类,那么系统的类的个数会成倍增加,而且不同的代理类中可能某些拦截业务逻辑代码都是相同的,这种情况同样是不能允许的,那有没有什么好的办法呢?答案是肯定的,以下是我结合网上资源及个人总结的如下几种常见的实现AOP的方式,各位可以参考学习。

第一种:静态织入,即:在编译时,就将各种涉及AOP拦截的代码注入到符合一定规则的类中,编译后的代码与我们直接在RealA调用属性或方法前后增加代码是相同的,只是这个工作交由编译器来完成。

PostSharp:PostSharp的Aspect是使用Attribute实现的,我们只需事先通过继承自OnMethodBoundaryAspect,然后重写几个常见的方法即可,如:OnEntry,OnExit等,最后只需要在需要进行AOP拦截的属性或方法上加上AOP拦截特性类即可。由于PostSharp是静态织入的,所以相比其它的通过反射或EMIT反射来说效率是最高的,但PostSharp是收费版本的,而且网上的教程比较多,我就不在此重复说明了,大家可以参见:使用PostSharp在.NET平台上实现AOP

第二种:EMIT反射,即:通过Emit反射动态生成代理类,如下Castle.DynamicProxy的AOP实现方式,代码也还是比较简单的,效率相对第一种要慢一点,但对于普通的反射来说又高一些,代码实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
using  Castle.Core.Interceptor;
using  Castle.DynamicProxy;
using  NLog;
using  NLog.Config;
using  NLog.Win32.Targets;
using  System;
using  System.Collections.Generic;
using  System.Linq;
using  System.Text;
using  System.Threading.Tasks;
 
namespace  ConsoleApp
{
     class  Program
     {
         static  void  Main( string [] args)
         {
             ProxyGenerator generator =  new  ProxyGenerator();
             var  test = generator.CreateClassProxy<TestA>( new  TestInterceptor());
             Console.WriteLine($ "GetResult:{test.GetResult(Console.ReadLine())}" );
             test.GetResult2( "test" );
             Console.ReadKey();
         }
     }
 
     public  class  TestInterceptor : StandardInterceptor
     {
         private  static  NLog.Logger logger;
 
         protected  override  void  PreProceed(IInvocation invocation)
         {
             Console.WriteLine(invocation.Method.Name +  "执行前,入参:"  string .Join( "," , invocation.Arguments));
         }
 
         protected  override  void  PerformProceed(IInvocation invocation)
         {
             Console.WriteLine(invocation.Method.Name +  "执行中" );
             try
             {
                 base .PerformProceed(invocation);
             }
             catch  (Exception ex)
             {
                 HandleException(ex);
             }
         }
 
         protected  override  void  PostProceed(IInvocation invocation)
         {
             Console.WriteLine(invocation.Method.Name +  "执行后,返回值:"  + invocation.ReturnValue);
         }
 
         private  void  HandleException(Exception ex)
         {
             if  (logger ==  null )
             {
                 LoggingConfiguration config =  new  LoggingConfiguration();
 
                 ColoredConsoleTarget consoleTarget =  new  ColoredConsoleTarget();
                 consoleTarget.Layout =  "${date:format=HH\\:MM\\:ss} ${logger} ${message}" ;
                 config.AddTarget( "console" , consoleTarget);
 
                 LoggingRule rule1 =  new  LoggingRule( "*" , LogLevel.Debug, consoleTarget);
                 config.LoggingRules.Add(rule1);
                 LogManager.Configuration = config;
 
                 logger = LogManager.GetCurrentClassLogger();  //new NLog.LogFactory().GetCurrentClassLogger();
             }
             logger.ErrorException( "error" ,ex);
         }
     }
 
     public  class  TestA
     {
         public  virtual  string  GetResult( string  msg)
         {
             string  str = $ "{DateTime.Now.ToString(" yyyy-mm-dd HH:mm:ss ")}---{msg}" ;
             return  str;
         }
 
         public  virtual  string  GetResult2( string  msg)
         {
             throw  new  Exception( "throw Exception!" );
         }
     }
}

简要说明一下代码原理,先创建ProxyGenerator类实例,从名字就看得出来,是代理类生成器,然后实例化一个基于继承自StandardInterceptor的TestInterceptor,这个TestInterceptor是一个自定义的拦截器,最后通过generator.CreateClassProxy<TestA>(new TestInterceptor())动态创建了一个继承自TestA的动态代理类,这个代理类只有在运行时才会生成的,后面就可以如代码所示,直接用动态代理类对象实例Test操作TestA的所有属性与方法,当然这里需要注意,若需要被动态代理类所代理并拦截,则父类的属性或方法必需是virtual,这点与我上面说的直接写一个代理类相同。

上述代码运行效果如下:

 第三种:普通反射+利用Remoting的远程访问对象时的直实代理类来实现,代码如下,这个可能相比以上两种稍微复杂一点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
using  System;
using  System.Collections.Generic;
using  System.Linq;
using  System.Runtime.Remoting.Activation;
using  System.Runtime.Remoting.Messaging;
using  System.Runtime.Remoting.Proxies;
using  System.Text;
using  System.Threading.Tasks;
 
namespace  ConsoleApp
{
     class  Program
     {
         static  void  Main( string [] args)
         {
 
             var  A =  new  AopClass();
             A.Hello();
 
             var  aop =  new  AopClassSub( "梦在旅途" );
             aop.Pro =  "test" ;
             aop.Output( "hlf" );
             aop.ShowMsg();
             Console.ReadKey();
 
         }
     }
 
 
     [AopAttribute]
     public  class  AopClass : ContextBoundObject
     {
         public  string  Hello()
         {
             return  "welcome" ;
         }
 
     }
 
 
     public  class  AopClassSub : AopClass
     {
         public  string  Pro =  null ;
         private  string  Msg =  null ;
 
         public  AopClassSub( string  msg)
         {
             Msg = msg;
         }
 
         public  void  Output( string  name)
         {
             Console.WriteLine(name +  ",你好!-->P:"  + Pro);
         }
 
         public  void  ShowMsg()
         {
             Console.WriteLine($ "构造函数传的Msg参数内容是:{Msg}" );
         }
     }
 
 
 
     public  class  AopAttribute : ProxyAttribute
     {
         public  override  MarshalByRefObject CreateInstance(Type serverType)
         {
             AopProxy realProxy =  new  AopProxy(serverType);
             return  realProxy.GetTransparentProxy()  as  MarshalByRefObject;
         }
     }
 
     public  class  AopProxy : RealProxy
     {
         public  AopProxy(Type serverType)
             base (serverType) { }
 
         public  override  IMessage Invoke(IMessage msg)
         {
             if  (msg  is  IConstructionCallMessage)
             {
                 IConstructionCallMessage constructCallMsg = msg  as  IConstructionCallMessage;
                 IConstructionReturnMessage constructionReturnMessage =  this .InitializeServerObject((IConstructionCallMessage)msg);
                 RealProxy.SetStubData( this , constructionReturnMessage.ReturnValue);
                 Console.WriteLine( "Call constructor" );
                 return  constructionReturnMessage;
             }
             else
             {
                 IMethodCallMessage callMsg = msg  as  IMethodCallMessage;
                 IMessage message;
                 try
                 {
                     Console.WriteLine(callMsg.MethodName +  "执行前。。。" );
                     object [] args = callMsg.Args;
                     object  o = callMsg.MethodBase.Invoke(GetUnwrappedServer(), args);
                     Console.WriteLine(callMsg.MethodName +  "执行后。。。" );
                     message =  new  ReturnMessage(o, args, args.Length, callMsg.LogicalCallContext, callMsg);
                 }
                 catch  (Exception e)
                 {
                     message =  new  ReturnMessage(e, callMsg);
                 }
                 Console.WriteLine(message.Properties[ "__Return" ]);
                 return  message;
             }
         }
     }
 
}

以上代码实现步骤说明:

1.这里定义的一个真实类AopClass必需继承自ContextBoundObject类,而ContextBoundObject类又直接继承自MarshalByRefObject类,表明该类是上下文绑定对象,允许在支持远程处理的应用程序中跨应用程序域边界访问对象,说白了就是可以获取这个真实类的所有信息,以便可以被生成动态代理。

2.定义继承自ProxyAttribute的代理特性标识类AopAttribute,以表明哪些类可以被代理,同时注意重写CreateInstance方法,在CreateInstance方法里实现通过委托与生成透明代理类的过程,realProxy.GetTransparentProxy() 非常重要,目的就是根据定义的AopProxy代理类获取生成透明代理类对象实例。

3.实现通用的AopProxy代理类,代理类必需继承自RealProxy类,在这个代理类里面重写Invoke方法,该方法是统一执行被代理的真实类的所有方法、属性、字段的出入口,我们只需要在该方法中根据传入的IMessage进行判断并实现相应的拦截代码即可。

4.最后在需要进行Aop拦截的类上标注AopAttribute即可(注意:被标识的类必需是如第1条说明的继承自ContextBoundObject类),在实际调用的过程中是感知不到任何的变化。且AopAttribute可以被子类继承,也就意味着所有子类都可以被代理并拦截。

如上代码运行效果如下:

这里顺便分享微软官方如果利用RealProxy类实现AOP的,详见地址:https://msdn.microsoft.com/zh-cn/library/dn574804.aspx

 第四种:反射+ 通过定义统一的出入口,并运用一些特性实现AOP的效果,比如:常见的MVC、WEB API中的过滤器特性 ,我这里根据MVC的思路,实现了类似的MVC过滤器的AOP效果,只是中间用到了反射,可能性能不佳,但效果还是成功实现了各种拦截,正如MVC一样,既支持过滤器特性,也支持Controller中的Action执行前,执行后,错误等方法实现拦截

 实现思路如下:

A.过滤器及Controller特定方法拦截实现原理:

1.获取程序集中所有继承自Controller的类型; 

2.根据Controller的名称找到第1步中的对应的Controller的类型:FindControllerType

3.根据找到的Controller类型及Action的名称找到对应的方法:FindAction

4.创建Controller类型的实例;

5.根据Action方法找到定义在方法上的所有过滤器特性(包含:执行前、执行后、错误)

6.执行Controller中的OnActionExecuting方法,随后执行执行前的过滤器特性列表,如:ActionExecutingFilter

7.执行Action方法,获得结果;

8.执行Controller中的OnActionExecuted方法,随后执行执行后的过滤器特性列表,如:ActionExecutedFilter

9.通过try catch在catch中执行Controller中的OnActionError方法,随后执行错误过滤器特性列表,如:ActionErrorFilter

10.最后返回结果;

B.实现执行路由配置效果原理:

1.增加可设置路由模板列表方法:AddExecRouteTemplate,在方法中验证controller、action,并获取模板中的占位符数组,最后保存到类全局对象中routeTemplates; 

2.增加根据执行路由执行对应的Controller中的Action方法的效果: Run,在该方法中主要遍历所有路由模板,然后与实行执行的请求路由信息通过正则匹配,若匹配OK,并能正确找到Controller及Action,则说明正确,并最终统一调用:Process方法,执行A中的所有步骤最终返回结果。

需要说明该模拟MVC方案并没有实现Action方法参数的的绑定功能,因为ModelBinding本身就是比较复杂的机制,所以这里只是为了搞清楚AOP的实现原理,故不作这方面的研究,大家如果有空可以实现,最终实现MVC不仅是ASP.NET MVC,还可以是 Console MVC,甚至是Winform MVC等。

以下是实现的全部代码,代码中我已进行了一些基本的优化,可以直接使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82