关于spring.net的面向切面编程 (Aspect Oriented Programming with Spring.NET)-切入点(pointcut)API
本文翻译自Spring.NET官方文档Version 1.3.2。
受限于个人知识水平,有些地方翻译可能不准确,但是我还是希望我的这些微薄的努力能为他人提供帮助。
侵删。
让我们看看 Spring.NET 如何处理一些重要的关于切入点的概念。
一些概念
Spring.NET的切入点和通知是相互独立的,因此针对不同的通知类型可以使用相同的切入点。
Spring.Aop.IPointcut 接口是最核心的,是用来将通知定位到特定的类型或者方法,接口细节如下:
1 public interface IPointcut
2 {
3 ITypeFilter TypeFilter { get; }
4 IMethodMatcher MethodMatcher { get; }
5 }
将IPointcut 接口拆分成两个部分可以复用类型、方法匹配的功能和一些细粒度的操作(例如和其他方法匹配器(method matcher)的“union”操作)。
ITypeFilter 接口被用来将切入点限定到一系列的目标类型上。如果Matches() 方法全部返回true,那么所有的目标类型就都匹配上了:
1 public interface ITypeFilter
2 {
3 bool Matches(Type type);
4 }
IMethodMatcher 接口一般来说更加重要,完整的接口定义如下:
public interface IMethodMatcher
{
bool IsRuntime { get; }
bool Matches(MethodInfo method, Type targetType);
bool Matches(MethodInfo method, Type targetType, object[] args);
}
Matches(MethodInfo, Type)方法是用来测试这个切入点会不会匹配一个目标类中的特定方法。这种判断会在AOP代理生成的时候进行,而不是在每次方法调用的时候都进行一次。如果一个特定的方法在两个参数的匹配方法中匹配成功,并且IMethodMatcher 的IsRuntime 属性返回true,那么那个三个参数的方法就会在每一次 方法调用的时候被调用。这样,切入点就能在每次目标通知执行之前查看传入方法的参数。
大多IMethodMatchers 都是静态的,意味着他们的IsRuntime属性总是返回false。在这种情况下,三个参数的方法就不会被调用。所以如果有可能的话,尽量使用静态的切入点,因为AOP框架会在AOP代理产生的时候缓存这些切入点的判断结果。
关于切入点的一些操作
Spring.NET支持切入点的一些操作有:例如,union和intersection
union意味着方法中至少有一个符合切入点。
intersection意味着方法所有的切入点都符合。
union通常更有用。
切入点可以通过Spring.Aop.Support.Pointcuts 类中的静态方法结合,也可以使用在同一命名空间下的ComposablePointcut 类结合。
一些切入点的便捷应用
Spring.NET提供了一些便捷的切入点使用方式。一些可以直接使用,其他的可以作为各个应用特定的切入点的子类来应用。
静态切入点
静态切入点是以方法和目标类为基础,不考虑方法的传入参数。静态切入点足够应付,并且是最适合,大多的使用场景。这样,Spring.NET只要在一个方法第一次调用的时候判断一次切入点,在这个之后就不需要再每次调用的时候都进行判断了。
让我们考虑一些静态切入点在Spring.NET 中的使用场景:
使用正则表达式
一个常见的描述静态切入点使用的是正则表达式。包括Spring.NET,大部分的AOP框架都已经实现这个功能。Spring.Aop.Support.SdkRegularExpressionMethodPointcut是一个泛型的正则切入点,使用了.NET BCL中的正则类。
使用这个类,你可以先提供一系列的模式字符串(pattern Strings)。只要一个规则满足,这个切入点就会被判断为true(所以结果是这些切入点的union的操作结果)。匹配通过类的全名来判断,因此你可以使用这个切入点在任意的命名空间下的任何类中应用通知。
以下是一个使用场景:
1 <object id="settersAndAbsquatulatePointcut"
2 type="Spring.Aop.Support.SdkRegularExpressionMethodPointcut, Spring.Aop">
3 <property name="patterns">
4 <list>
5 <value>.*set.*</value>
6 <value>.*absquatulate</value>
7 </list>
8 </property>
9 </object>
为了方便,Spring为我们提供了RegularExpressionMethodPointcutAdvisor类来引用一个IAdvice接口实例,同时定义切入点规则(要记住IAdvice实例可以是一个拦截器,前置通知,异常通知等等)。这种简化的写法,把切入点和通知器写在同一个object标签里面,例如这样:
1 <object id="settersAndAbsquatulateAdvisor"
2 type="Spring.Aop.Support.RegularExpressionMethodPointcutAdvisor, Spring.Aop">
3 <property name="advice">
4 <ref local="objectNameOfAopAllianceInterceptor"/>
5 </property>
6 <property name="patterns">
7 <list>
8 <value>.*set.*</value>
9 <value>.*absquatulate</value>
10 </list>
11 </property>
12 </object>
RegularExpressionMethodPointcutAdvisor类可以作为任何的通知类型。如果你只有一个规则你可以使用这个属性名字规则然后为其特定一个值而不是定义所有的属性规则然后配上一个值列表。
你也可能从System.Text.RegularExpressions 命名空间下指定一个正则对象。内置的RegexConverter类将会提供解析。可以看6.4节, “Built-in TypeConverters”以找到更多的Spring内置类型转换器。正则对象在IoC容器中被创建成其他的任意对象。通过使用一个内部对象定义是一个很方便的方法来使定义和PointcutAdvisor声明更加接近。需要注意的是,如果在构造器中没有任何显式的指定的话,SdkRegularExpressionMethodPointcut类有一个默认配置属性来设置正则表达式的配置。
使用特性标签
切入点可以通过方法上面的特性(attribute)来指定。切入点关联的通知接下来就会通过解析特性标签来配置。AttributeMatchMethodPointCut类提供这种功能。以下例子中的切入点可以匹配所有的带有Spring.Attributes.CacheAttribute特性的标签方法:
1 <object id="cachePointcut" type="Spring.Aop.Support.AttributeMatchMethodPointcut, Spring.Aop">
2 <property name="Attribute" value="Spring.Attributes.CacheAttribute, Spring.Core"/>
3 </object>
就像下面展示的一样,这种方式也可以和DefaultPointcutAdvisor一起使用
1 <object id="cacheAspect" type="Spring.Aop.Support.DefaultPointcutAdvisor, Spring.Aop">
2 <property name="Pointcut">
3 <object type="Spring.Aop.Support.AttributeMatchMethodPointcut, Spring.Aop">
4 <property name="Attribute" value="Spring.Attributes.CacheAttribute, Spring.Core"/>
5 </object>
6 </property>
7 <property name="Advice" ref="aspNetCacheAdvice"/>
8 </object>
这里的aspNetCacheAdvice 是一个IMethodInterceptor 接口的实现,它缓存了方法的返回值。可以查阅SDK文档Spring.Aop.Advice.CacheAdvice 来获得更多与这个通知相关的信息。
为了方便,AttributeMatchMethodPointcutAdvisor 类定义另一种基于特性,更加简练的泛型DefaultPointcutAdvisor类通知器。以下是一个例子:
1 <object id="AspNetCacheAdvice" type="Spring.Aop.Support.AttributeMatchMethodPointcutAdvisor, Spring.Aop">
2 <property name="advice">
3 <object type="Aspect.AspNetCacheAdvice, Aspect"/>
4 </property>
5 <property name="attribute" value="Framework.AspNetCacheAttribute, Framework" />
6 </object>
动态切入点
动态切入点要比静态切入点更加耗费性能。他们要考虑方法的传入参数和静态的信息。这就意味着他们必须判断每一个方法的调用,这种判断结果是不能被缓存的,因为传入的参数多种多样。
一个常见的例子就是控制流切入点(control flow pointcut)。
控制流切入点
Spring.NET控制流切入点在概念上类似于AspectJ 的cflow切入点,尽管没有它那么强大。(当前无法指定一个切入点在其他切入点下执行)。一个控制流切入点肯定是动态的,这因为它要根据现在的每个方法调用时候的调用堆栈来判断。例如,一个类型A的方法A() 调用了类型B的方法B(),然后类型B的方法B()就在类型A的方法A()的控制流中执行。仅当类型B的方法B()被调用的时候,控制流切入点就能应用在类型A的方法A() ,而不是类型A的方法A() 在其他调用堆栈中执行。控制流切入点在Spring.Aop.Support.ControlFlowPointcut 类中详细定义。
当使用控制流切入点的时候,需要注意一些问题。在运行时,JIT编译器会内联一些方法,主要是为了提升性能,但是这会导致这个方法会在调用堆栈中消失。这是因为内联操作把被调用者的IL代码插入到调用者的IL代码中来有效地移除方法调用。这些信息可以从System.Diagnostics.StackTrace中获得,这样的话使用ControlFlowPointcut就会受到这些优化的影响而不匹配,这是因为这些方法被内联了。
总的来说,一个足够小的方法,可能只有几行代码(IL代码小于32 bytes)容易被内联。对于这个感兴趣的可以读David Notario的博客(JIT Optimizations I and JIT Optimizations II)。并且,当一个程序集按照发布模式的配置被编译的时候,元数据会告知CLR使用JIT优化。当按照调试模式的配置的时候,CLR会禁用(可能一些)JIT优化。根据以往经验来说,在调试模式下JIT是会关闭内联的。
一个保证你的控制流切入点不会被忽略的方法是使用System.Runtime.CompilerServices.MethodImplAttribute特性,然后将其赋值为MethodImplOptions.NoInlining。在这个简单的例子中,如果代码在发行模式中编译就不会匹配到“GetAge”方法。
1 public int GetAge(IPerson person)
2 {
3 return person.GetAge();
4 }
而且,使用上面的特性方法会在发行编译中不会进行内联。
1 [MethodImpl(MethodImplOptions.NoInlining)]
2 public int GetAge(IPerson person)
3 {
4 return person.GetAge();
5 }
自定义切入点
由于Spring.NET 中的切入点都是.NET 的基础类型,而不是基于语言的特征(language features )(例如在AspectJ中),因此无论对于静态切入点还是动态切入点,都可以声明自定义切入点。然而在AspectJ语法中, 现在却没有现成、成熟的自定义切入点的声明方式。在Spring.NET 中,自定义切入点就像一般实体模型一样可以被任意地关联调用。
Spring.NET 提供了有效的切入点的超类来支持你自定义切入点的实现。
因为静态切入点是最常见并且最有用的切入点类型,你只要继承StaticMethodMatcherPointcut,就像下面展示的那样,你只要实现一个抽象方法就行(尽管也可以重写其他方法来自定义行为):
1 public class TestStaticPointcut : StaticMethodMatcherPointcut {
2 public override bool Matches(MethodInfo method, Type targetType) {
3 // return true if custom criteria match
4 }
5 }