什么是面向切面编程?
在软件开发中,分布于应用中多处的功能被成为横切关注点。
切面提供了取代继承和委托的另一种选择,而且在很多场景下更清晰简洁。在使用面向切面编程时,我们仍然在一个地方定义通用功能,但是我们可以通过声明的方式定义这个功能以及何种方式在何处应用,而无需修改受影响的类。横切关注点可以被模块化为特殊的类,这些类被成为切面。这样做有两个好处:首先,每个关注点现在都只集中于一处,而不是分散到多处代码中;其次,服务模块更简洁,因为它们只包含主要关注点(或核心功能)的代码,而次要关注点的代码被转移到切面中去了。
AOP常见的术语有通知(advice)、切点(pointcut)和连接点(join point)。
通知(Advice)
在AOP术语中,切面的工作被称为通知。
spring切面可以应用5种类型的通知:
1、Before:在方法被调用之前调用通知
2、After:在方法完成之后调用通知,无论方法执行是否成功。
3、After-returing:在方法成功执行之后调用通知。
4、After-throwing:在方法抛出异常后调用通知。
5、Around:通知包裹了被通知的方法,在被通知的方法调用之前和调用之后执行自定义的行为。
连接(Joinpoint):我们的应用可能需要对数以千计的时机应用通知,这些时机被称为连接点。
切点(Pointcut):如果通知定义了切面的“什么”和“何时”,那么切点就定义了“何处”。切点的定义会匹配通知索要织入的一个或多个连接点。我们通常使用明确的类和方法名称来指定这些切点,或是利用正则表达式定义匹配的类和方法名称模式来指定这些切点。有些AOP框架允许我们创建动态的切点,可以根据运行时的决策(比如方法的参数值)来决定是否应用通知。
切面(Aspect):切面是通知和切点的结合。通知和切点共同定义了关于切面的全部内容--它是什么,在何时和何处完成其功能。
引入(Introduction):引入允许我们向现有的类添加新方法或属性。例如,我们可以创建一个Auditable通知类,该类记录了对象最后一次修改时的状态。这很简单,只需一种方法,setLastModified(Date),和一个实例变量来保存这个状态。然后,这个新方法和实例变量就可以被引入到现有的类中。从而可以在无需修改这些现有的类的情况下,让它们具有新的行为和状态。
织入(Weaving):织入是将切面应用到目标对象来创建新的代理对象的过程。切面在指定的连接点被织入到目标对象中。在目标对象的声明周期里有多个点可以进行织入。
Spring对AOP的支持有哪些?
Spring提供了4种各具特色的AOP支持:
1、基于代理的经典AOP;
2、@AspectJ注解驱动的切面;
3、纯POJO切面;
4、注入式AspectJ切面(适合Spring各版本)
Spring在运行期通知对象
通过在代理类中包裹切面,Spring在运行期将切面织入到Spring管理的Bean中,如图所示,代理类封装了目标类,并拦截被通知的方法的调用,再将调用转发给真正的目标Bean
当拦截到方法调用时,在调用Bean方法之前,代理会执行切面逻辑。
直到应用需要被代理的Bean时,Spring才会创建代理对象。如果使用的是ApplicationContext,在ApplicationContext从BeanFactory中加载所有Bean时,Spring创建被代理的对象。因为Spring运行时才创建代理对象,所以我们不需要特殊的编译器来织入Spring AOP的切面。
Spring只支持方法连接点
正如前面所探讨过的,通过各种AOP实现可以支持多种连接点模型。因为Spring基于动态代理,所以Spring只支持方法连接点。这与其他一些AOP框架是不通的,例如AspectJ和JBoss,除了方法切点,它们还提供了字段和构造器接入点。Spring缺少对字段连接点的支持,无法让我们创建细粒度的通知,例如拦截对象字段的修改。而且Spring也不支持构造器连接点,我们无法在Bean创建时应用通知。
但是方法拦截可以满足绝大部分的需求。如果需要方法拦截之外的连接点拦截,我们可以利用Aspect来协助Spring AOP。
Spring AOP所支持的AspectJ切点指示器有:
1、arg():限制连接点匹配参数为指定类型的执行方法。
2、@arg():限制连接点匹配参数由指定注解标注的执行方法。
3、execution():用于匹配是连接点的执行方法。
4、this():限制连接点匹配AOP代理的Bean引用为指定类型的类
5、target():限制连接点匹配目标对象为指定类型的类
6、@target():限制连接点匹配特定的执行对象,这些对象对应的类要具备指定类型的注解
7、within():限制连接点匹配指定的类型。
8、@within():限制连接点匹配指定注解所标注的类型(当使用Spring AOP时,方法定义在由指定的注解所标注的类里)。
9、@annotation:限制匹配带有指定注解连接点。
在Spring中尝试使用AspectJ其他指示器时,将会抛出IllegalArgumentException异常。
execution指示器是我们在编写切点定义时最主要使用的指示器。在此基础上,我们使用其他指示器来限制所匹配的切点。
编写切点
使用AspectJ切点表达式来定位
使用within()指示器限制切点范围
使用Spring的bean()指示器
除了上图所示的指示器外,Spring2.5还引入了一个新的bean()指示器,该指示器允许我们在切点表达式中使用Bean的ID来标识Bean。bean()使用BeanID或Bean名称作为参数来限制切点只匹配特定的Bean。
例如,下面的切点希望在执行Instrument的play方法时应用通知,但限定Bean的ID为eddie。
在XML中声明切面
下面是AOP的配置元素:
1、<aop:advisor>:定义AOP通知器
2、<aop:after>:定义AOP后置通知(不管被通知的方法是否执行成功)
3、<aop:after-returning>:定义AOP after-returing通知
4、<aop:after-throwing>:定义after-throwing通知
5、<aop:around>:定义AOP环绕通知
6、<aop:aspect>:定义切面
7、<aop:aspectj-autoproxy>:启用@AspectJ注解驱动的切面
8、<aop:before>:定义AOP前置通知
9、<aop:config>:顶层AOP配置元素,大多数的<aop:*>元素必须包含在这个元素内
10、<aop:declare-parents>:为被通知的对象引入额外的接口,并透明地实现
11、<aop:pointcut>:定义切点
下面让我们具体的阐述Spring AOP。
程序Audience:定义一个观众类
声明前置和后置通知
这个的流程就如同调用了performer.perform()后这样执行:
你或许注意到所有通知元素中的pointcut属性的值都是一样的,这是因为所有的通知都是应用到相同的切点上。这似乎违反了DRY(不要重复自己)原则。如果将来想修改这个切点,那么需要同时修改4个地方。
为了避免重复定义切点,可以使用<aop:pointcut>元素定义一个命名切点。
声明环绕通知
前置通知和后置有一些限制。具体来说,如果不使用成员变量存储信息,那么在前置通知和后置通知之间共享信息非常麻烦。例如我们要在前置通知中记录开始时间并在某个后置通知中报告表演耗费的时长。但这样的话,我们必须在一个成员变量中保存开始时间。因为Audience是单例,如果像这样保存状态,它将存在线程安全问题。
下面是一个新的watchPerformance()方法。
为通知传递参数
现在我们要做的是Magician如何使用Spring AOP截听Volunteer的内心感应。
通过切面引入新功能
如果切面能够为现有的方法增加额外的功能,为什么不能为一个对象增加新的方法呢?实际上,利用被称为引入的AOP概念,切面可以为Spring Bean添加新方法。
下面为示例中所有表演者引入下面的contestant接口:
借助AOP引入,我们可以不需要为设计妥协或者侵入性的改变现有的实现。为了搞定它,我们需要使用<aop:declare-parents>元素:
types-matching:类型匹配Performer接口的哪些Bean会实现implement-interface指定的Contestant接口。我们可以用default-impl属性来通过它的全限定类名来显示指定Contestant的实现。或者我们还可以使用delegate-ref属性来标识。
注解切面
@Pointcut注解用于定义一个可以在@AspectJ切面内可重用的切点。@Pointcut注解是一个AspectJ切点表达式--这里标识该切点必须匹配Performer的perform()方法。切点的名称来源于注解所应用的方法名称。因此,该切点的名称为performance()。performance()方法的实际内容并不重要,在这里它事实上是空的。其实该方法本身只是一个标识,供@Pointcut注解依附。
该类仍然你可以像下面一样在Spring中进行装配:
因为Audience类本身包含了所有它所需要定义的切点和通知,所以我们不再需要在XML配置中声明切点和通知。最后一件需要做的事是让Spring将Audience应用为一个切面。我们需要在Spring上下文中声明一个自动代理Bean,该Bean知道如何把@AspectJ注解所标注的Bean转变为代理通知。
Spring在aop命名空间中提供了一个自定义的配置元素:
注解环绕通知
传递参数给所标注的通知
标注引入
value属性等同于<aop:declare-parents>的types-matching属性。它标识应该被引入接口的Bean的类型。
defaultImpl属性等同于<aop:declare-parents>的default-impl属性。它标识该类提供了所引入接口的实现。
由@DeclareParents注解所标注的static属性指定了将被引入的接口。
注入AspectJ切面
首先在AspectJ中创建一个裁判切面。JudgeAspect就是这样的一个切面。
下面是JudgeAspect所使用的一个CriticismEngine的实现
在展示如何实现注入之前,我们必须清楚AspectJ切面根本不需要Spring就可以织入进我们的应用中。但是如果想使用Spring的依赖注入为AspectJ切面注入协作者,那么就需要在Spring配置中把切面声明为一个Spring Bean。
很大程度上,<bean>的声明与我们在Spring中所看到的其他<bean>配置并没有太多的区别,但是最大的不通在于使用了factory-method属性。通常情况下,Spring Bean由Spring容器初始化,但是AspectJ切面是由AspectJ在运行期间创建的。等到Spring有几回为JudgeAspect注入CriticismEngine时,JudgeAspect已经被实例化了。
因为Spring无法负责创建JudgeAspect,那就不能在Spring中简单地将JudgeAspect声明为一个Bean。相反,我们需要一种方式为Spring获得已经由AspectJ创建的JudgeAspect实例的句柄,从而可以注入CriticismEngine。幸运的是,所有的AspectJ切面都提供了一个静态的aspectOf()方法,该方法返回切面的一个单利。所以为了获得切面的实例,我们必须使用factory-method来调用aspectOf()方法来代替调用JudgeAspect的构造器方法。
简而言之,Spring不能使用<bean>声明来创建一个JudgeAspect实例--它已经在运行时由AspectJ创建了。Spring通过aspectOf()工厂方法获得切面的引用,然后像<bean>元素规定的那样在该对象上执行依赖注入。
总结:
AOP是面向对象编程的一个强大补充。通过AspectJ,我们现在可以将之前分散在应用各处的行为放入可重用的模块中。我们显示地声明在何处如何应用该行为,这有效减少了代码冗余,并让我们的类关注自身的主要功能。
Spring提供了一个AOP框架,让我们把切面插入方法执行的周围。现在我们已经学会如何把通知织入前置、后置和环绕方法的调用中,以及为处理异常增加自定义的行为。
在Spring应用中如何使用切面,我们有几种选择。通过使用@AspectJ注解和简化的配置命名空间,在Spring中装配通知和切点变得非常简单。
最后,当SpringAOP不能满足需求时,我们必须转向更为强大的AspectJ。对于这些场景,我们了解了如何使用Spring为AspectJ切面注入依赖。
此时此刻,我们已经覆盖了Spring框架的基础知识。我们已经了解了如何配置Spring容器以及如何为Spring管理的对象应用切面。正如我们所看到的,这些核心技术为创建松散耦合的应用奠定了坚实的基础。