4.1 什么是面向切面编程
切面能够将横切关注点模块化,将其抽出来成为一个通用的功能。
4.1.1定义AOP术语
- 通知(Advice)
切面的工作成为通知。通知定义了切面是什么以及何时使用。除了描述切面要完成的工作,通知还解决了何时执行这个工作的问题。
Spring切面可以定义5种通知:- 前置通知(before):在目标方法被调用前调用该通知功能
- 后置通知(After):在目标方法被调用后调用该通知方法,不关心方法的输出是什么
- 返回通知(After-returning):在目标方法成功执行后调用通知
- 异常通知(After-throwing):在目标方法抛出异常后调用通知
- 环绕通知(Around):通知包裹了被通知的方法,在被通知的方法调用之前和调用之后执行自定义的行为。
- 连接点(Join point)
在程序执行过程中能够插入切面的一个点成为连接点,这个点可以是调用方法、抛出异常、甚至修改一个字段。切面代码通过这些连接点插入到应用的正常流程中,并添加新的行为。 - 切点(Pointcut)
切点的定义会匹配所要织入的一个或者多个连接点。通常是明确的类和方法名称,或者是用正则表达式定义所匹配的类和方法名。有些AOP框架可以创建动态的切点,可以在运行时候来决定是否进行应用的通知。 - 切面(Aspect)
切面是通知和切点的结合。通知和切点共同定义了切面的全部内容–它是什么,在何时和何处完成其功能。 - 引入(Introduction)
引入允许我们向现有的类添加新方法或者属性。 - 织入
织入是指把切面应用到目标对象并创建新的代理对象的过程。切面在指定的连接点被织入到目标对象中。在目标对象的生命周期有多个点可以进行织入:- 编译期:切面在目标类编译时被织入。这种方式需要特殊的编译器。AspectJ的织入编译器就是以这种方式织入切面的。
- 类加载期:切面在目标类加载到JVM时被织入。这种方式需要特殊的类加载器(ClassLoader),它可以在目标类被引入应用 之前增强该目标类的字节码。AspectJ5的加载时织入(load-time weaving,LTW)就支持以这种方式织入切面。
- 运行期:切面在应用运行的某个时刻被织入。一般情况下,在织入切面时,AOP容器会为目标对象动态地创建一个代理对象。 Spring AOP就是以这种方式织入切面的。
4.1.2 Spring对AOP的支持
Spring提供了4种类型的AOP支持:
- 基于代理的经典Spring AOP
- 纯POJO切面
- @AspectJ注解驱动的切面
- 注入式AspectJ切面(适用Spring各版本)
前三种都是Spring AOP实现的变体,Spring AOP构建在动态代理基础之上,因此,Spring对AOP的支持局限于方法拦截。
Spring借鉴了AspectJ的切面,编程模型和AspectJ几乎一致。如果AOP的需求超过了简单的方法调用(构造器,属性拦截),可以使用AspectJ来实现切面.
- Spring通知是Java写的,
- Spring创建的通知都是用Java类编写的。这样可以和使用和Java开发一样的IDE来开发切面,而且,定义通知应用的切点通常使用注解或者用xml编写,这两种语法对java开发者非常熟悉。
- AspectJ相反,他是通过Java语言拓展的方式实现的。
- 优点通过特有的AOP语言,可以获得更强大和细粒度的控制,还有更丰富的AOP工具集.
- 缺点是需要额外学习新的工具和语法
- Spring在运行时通知对象
通过在代理中包裹切面,Spring在运行期 把切面织入到Spring管理的bean中。
代理类封装了目标类,并拦截被通知方法的调用,再把调用转发给真正的目标bean,当代理拦截到方法调用时,在调用目标之前,会执行切面逻辑。
直到应用需要被代理的bean时,Spring才创建代理对象。如果使用的是ApplicationContext的话,在ApplicationContext从BeanFactory中加载所有bean的时候,Spring才会创建被代理的对象。因为Spring运行时才创建代理对象,所以我们不需要特殊的编译器来织入Spring AOP的切面。
- Spring只支持方法级别的连接点
因为Spring基于动态代理,所以只支持方法连接点,例如AspectJ和JBoss,除了支持方法切点,还支持字段和构造器接入点。- Spring缺少对字段连接点的支持,无法创建细粒度的通知,例如拦截对象字段的修改
- 不支持构造器连接点,无法在bean创建时应用通知。但是方法拦截可以满足绝大部分的需求了。如果需要方法拦截之外的连接点拦截功能,可以利用AspectJ来补充。
4.2 通过切点来选择连接点
Spring使用AspectJ的切点表达式语言来定义切点,且只支持AspectJ切点指示器(Point designator)的一个子集。
AspectJ指示器 | 描述 |
---|---|
arg() | 限制连接点匹配参数为指定类型的执行方法 |
@args() | 限制连接点匹配参数由指定注解标注的执行方法 |
executing() | 用于匹配是连接点的执行方法 |
this() | 限制连接点匹配AOP代理的bean引用为指定类型的类 |
target | 限制连接点匹配目标对象为指定类型的类 |
@target | 限制连接点匹配特定的执行对象,这些对象对应的类要具有指定类型的注解 |
within() | 限制连接点匹配指定的类型 |
@within() | 限制连接点匹配指定注解所标注的类型(当使用Spring AOP时,方 法定义在由指定的注解所标注的类里) |
@annotation | 限定匹配带有指定注解的连接点 |
Spring中使用了其他的AspectJ指示器,会抛出IllegalArgument-Exception异常。
上面的方法中,只有executing()是用来实际执行匹配的,其他的指示器都是用来限制匹配的。所以execution使我们编写切点定义的主要使用的指示器。execution的基础之上使用其他指示器来限制匹配的切点。
4.2.1 编写切点
定义一个Performance接口:
public interface Performance{
public void perfom();
}
下图是一个切点表达式,是编写perform()方法触发的通知。
返回类型的*代表不关系方法返回值的类型。类名使用了全限定名。参数列表的两个点代表无论该方法的入参是什么。
假设配置切点仅匹配concert,可以使用within()指示器来限制匹配。
这里使用了"&&“操作符形成"and"操作,同样可以使用”||"、"!"来进行更多的逻辑操作。
4.2.2 在切点中选择bean
切点表达式中还可以使用bean的ID来标识bean。
下面这个表达式只会匹配id为"woodstock"的bean的performance()方法。
同样可以匹配去除某一个指定的bean。
4.3 使用注解创建切面
Aspect5之前,编写AspectJ切面需要学习一种Java的拓展语言,Aspect5之后可以使用注解来把任意类转变为切面。
4.3.1 定义切面
现在有一个观众类Audience,是观看演出的切面:
Audience上使用了@Aspect注解,说明Audience不仅是一个POJO,还是一个切面。
Audience上有4个方法,定义了一个观众在观看演出时可能会做的事情
- 演出开始之前,观众需要就坐:takeSeats()
- 演出开始之前,观众需要手机静音:silenceCellPhones()
- 演出很精彩,结束之后,观众需要鼓掌:applause()
- 演出失败(发生异常),观众要求退票,demandRedund()
这4个方法都使用了通知注解来表明他们应该在什么时候调用,AspectJ提供了五个注解来定义通知。
注解 | 通知 |
---|---|
@After | 通知方法在目标方法返回或者抛出异常后调用 |
@AfterReturn | 通知方法会在目标方法返回后调用 |
@AfterThrowing | 通知方法会在目标方法抛出异常后调用 |
@Around | 通知方法会将目标方法封装起来 |
@Before | 通知方法会在目标方法调用之前执行 |
注解的执行顺序
try{
try{
//@Before
method.invoke(..);
}finally{
//@After
}
//@AfterReturning
}catch(){
//@AfterThrowing
}
这里的切点表达式重复了4遍,可以将它只定义一次,然后每次需要的时候引用他。这里可以使用@Pointcut注解在一个@AspectJ切面内定义可以重用的切点。
使用@Pointcut简化后的Audience类:
在这个类中,performance()方法使用了@Pointcut注解。为@Pointcut注解设置的值是一个切点表达式,这样就可以在任何的切点表达式中使用performance()。performance()方法内容不重要,实际上应该时空的,它只是一个标记,供@Pointcut注解依附。
Audience本质还是一个Java类,需要在容器中将它声明为一个bean,注意的是:仅仅将它声明为一个bean,它并不会被视为切面,它的注解不会被解析,也不会创建将其转换为切面的代理。
如果使用JavaConfig的简化,可以在配置类上的使用EnableAspectJ-AutoProxy注解启用自动代理功能。
如果使用XML配置bean,需要使用Spring AOP命名空间的<aop:aspectj-autoproxy>元素。
启用了自动代理之后,AspectJ自动代理会为使用@Aspect注解的bean创建一个代理。
Spring的AspectJ自动代理仅仅使用@AspectJ作为创建切面的直到,切面依然是基于代理的。本质上他还是Spring基于代理的切面。所以尽管使用的是@AspectJ注解,还是受限于代理方法的调用。如果想使用AspectJ其他的能力,还是需要使用AspectJ并且不依赖Spring来创建基于代理的切面。
4.3.2 创建环绕通知
环绕通知是最强的通知类型,可以将被通知的目标方法完全包装起来,实际上相当于在一个通知方法中同时编写前置通知和后置通知。
使用环绕通知重写Audience方法:
@Around注解表明watchPerformance()是一个环绕通知,他将原本四个分开的通知集合到了一个通知中。
ProceedingJoinPoint是它的参数,这个参数的proceed()方法就是被通知方法方法。如果不调用proceed()方法,这个通知实际上会阻塞被通知方法的调用。
4.3.3 处理通知中的参数
在切点表达式中声明参数,这个参数传入到通知方法中,表达式中的args(trackNumber)限定符表示传递给playTrack()方法的int类型参数也会传递到通知中。切点中的参数和切点方法中的参数名字是一样的,这样完成了从命名切点到通知方法的参数转义。
现在,可以在Spring配置中将TrackCounter和BlankDisc定义为bean,并启用AspectJ自动代理。
4.3.4 通过注解引入新功能
java不是动态语言,一旦类编译完成,很难为类添加新功能。
Spring AOP有一个概念叫做"引入",他可以为SpringBean添加新方法。
Spring中,切面实现了它包装的bean相同接口的代理,如果代理暴露新的接口,切面通知的bean也想实现了新的接口,即使底层实现类没有实现也无所谓。
首先,有这样一个Encoreable接口。
public interface Encoreable{
void performEncore();
}
/**
* Encoreable接口实现类
*/
@Component
public class DefaultEncoreable implements Encoreable{
@Override
public void performEncore() {
System.out.println("haha");
}
}
正常方式,我们可以让一个类实现该接口来增强功能,但是有时候可能这个类不是你可以修改的。
首选创建一个新的切面:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.DeclareParents;
@Aspect
public class EncoreableIntroducer {
@DeclareParents(value = "demo6.Performance+",defaultImpl = DefaultEncoreable.class)
public Encoreable encoreable;
}
Encoreable也是一个切面,但是它与一般的切面不同,它没有前置、后置或者环绕通知。
@DeclareParents注解可以将Encoreable接口引入到Performance Bean中。
@DeclareParents注解由三部分组成:
- value属性指定了那种类型的bean要引入该接口。(后面的"+"表示Performance的所有子类型,而非Performance本身)
- defaultImpl属性指定了为引入功能提供实现的类。上面例子中是DefaulEncoreable。
- @DeclareParents注解所标注的静态属性指明了要引入的接口,上面例子是Encoreable接口。
接着我们需要将EncoreableIntroducer声明为一个bean后就可以使用了。
@Test
public void test1(){
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(JavaConfig.class);
Performance performer = ctx.getBean("performer", Performance.class);
performer.perform();
Encoreable encoreable = (Encoreable) performer;
encoreable.performEncore();
}
这里的Perfonrmance和Emcoreable都是接口。
注解和自动代理能很方便的创建切面。但是有时没有源码,不能对类进行修改添加注解。
就可以在XML中声明切面。
4.4 在XML中声明切面
基于注解的配置>基于Java的配置>基于XML的配置。但是,如果你需要声明切面,但是又不能为通知类添加注解的时候,那么就必须转向XML配置了。
Spring AOP在XML中的命名空间
AOP配置元素 | 用途 |
---|---|
<aop:advisor> | 定义AOP通知器 |
<aop:after> | 定义AOP后置通知(不管被通知的方法是否执行成功) |
<aop:after-rerturning> | 定义AOP返回通知 |
<aop:afterthrowing> | 定义AOP异常通知 |
<aop:around> | 定义AOP环绕通知 |
<aop:aspect> | 定义一个切面 |
<aop:aspectj-autoproxy> | 启用@Aspect注解驱动的切面 |
<aop:before> | 定义一个AOP前置通知 |
<aop:config> | 顶层的AOP配置元素。大多数的<aop:*>元素必须包含在<aop:config>元素内 |
<aop:declare-parents> | 以透明的方式为被通知的对象引入额外的接口 |
<aop:pointcut> | 定义一个切点 |
把之前用@Aspect声明的切面Audience类,把他的所有注解全部移除
public void Audience{
public void silenceCellPhones(){
System.out.println("Silencing cell phone");
}
public void takeSeats(){
System.out.println("Taking seats");
}
public void demandRefund(){
System.out.println("Demanding a refund");
}
}
现在他就是个普通的java类,可以把他注册成为一个Spring应用上下文中的bean。我们现在可以在XML文件中将它声明成为一个切面。
4.4.1 声明前置和后置通知
大多数AOP配置元素必须在<aop:config>元素的上下文内使用,在<aop:config>元素内可以声明一个或者多个通知器、切面或者切点。上面例子中使用<aop:config>声明了一个简单的切面,使用ref元素引用了一个POJO bean,该bean实现了切面,也就是Audience。ref引用的bean提供了在切面中通知所调用的方法。
通知逻辑是如何织入到业务逻辑中的:
在这个例子中,所有的pointcut值都是相同的,在java配置中可以使用@Pointcut注解消除这种重复的值,在XML配置中则可以使用<aop:pointcut>元素。然后用pointcut-ref属性来引用这个命名切点。
这个<aop:pointcut>是在<aop:aspect>里面定义的,只能在当前这个<aop:aspect>里面引用,如果想在多个<aop:aspect>里面引用同一个<aop:aspect>,可以将其声明在<aop:config>元素下面,这样<aop:config>里面的<aop:aspect>都能引用它。
4.4.2 声明环绕通知
Audience前置通知是关闭手机,后置通知是鼓掌,如果我们还想知道其中花费了多长时间,就涉及到前置通知和后置通知的信息共享,虽然可以使用一个成员变量来记录时间,但是由于Audience是单例的,会存在线程安全问题。
环绕通知可以更方便的解决这个问题,使用环绕通知,可以将前置通知、后置通知集中在给一个方法内,这样就不需要成员变量来进行通信,避免了线程安全问题。
现在有个新的Audience类的watchPerformance()方法,他没有使用任何的注解。
这个方法中包含了之前四个通知方法的所有功能,在XML中我们使用<aop:around>元素来声明环绕通知。
像其他通知的XML元素一样,指定了一个切点和一个通知方法的名字。在这里,我们使用跟之前一样的切点,但是为该切点所设置的method属性值为watchPerformance()方法。
4.3.3 为通知传递参数
有这样一个类TrackCounter。
我们通过Spring XML配置TrackCounter bean和BlankDisc bean,并且将TrackCounter转化成为一个切面,用它来记录磁道播放次数。
4.4.4 通过切面引入新的功能
使用<aop:declare-parents>元素,可以实现和@DeclareParents注解一样引入新的功能。声明时和Java表达方式基本一致。
- types-matching属性匹配要通知的类型
- implement-interface属性指明增强的接口
- default-impl属性指明增强功能具体的实现类
4.5 注入AspectJ切面
Spring AOP只能在方法执行时进行通知,如果我们想使用除了方法切点之外的切点(例如构造器切点可以在对象创建时应用通知),可以使用AspectJ。
Spring的依赖注入可以将bean装配到AspectJ切面当中去。
这里有一个CriticAspect切面,它的功能是在表演结束之后为表演发表评论。
程序中的performance()切点配合perform()方法。当它配合afterReturning()通知一起使用,就可以让该切点在表演结束时候起作用。
在这个类中,CriticAspect并不是自己实现了发表评论的功能,而是他持有一个CriticismEngine对象,调用该对象的getCriticism()方法来实现发表评论的功能。为了防止CriticEngine和CriticismEngine二者之间的耦合,可以用依赖注入来消除耦合。
CriticismEngine是一个声明了简单getCriticism()方法的接口。下面是他的实现类:
这个实现类会在注入的评论池中随机选择一个评论,我们需要将它声明为一个Spring bean。
最后我们为CriticAspect装配CriticismEngineImpl。其实AspectJ切面不需要Spring就可以织入到应用中。但是如果想使用Spirng的依赖注入为AspectJ切面注入对象,就需要在Spring配置中将切面声明为一个bean。
在上面代码中,使用到了factory-method属性,通常情况,Spring bean是由Spring容器初始化的,但是AspectJ切面是由AspectJ在运行期创建的,所以Spring不能负责创建CriticAspect,这里需要使用AspectJ切面提供的aspectOf() 方法来返回该切面的一个单例。
4.6 小结
AOP是面向对象编程的一个强大补充。通过AspectJ,我们现在可以把之前分散在应用各处的行为放入可重用的模块中。我们显示地声明在何处如何应用该行为。这有效减少了代码冗余,并让我们的类关注自身的主要功能。
Spring提供了一个AOP框架,让我们把切面插入到方法执行的周围。现在我们已经学会如何把通知织入前置、后置和环绕方法的调用中,以及为处理异常增加自定义的行为。
关于在Spring应用中如何使用切面,我们可以有多种选择。通过使用@AspectJ注解和简化的配置命名空间,在Spring中装配通知和切点变得非常简单。最后,当Spring AOP不能满足需求时,我们必须转向更为强大的AspectJ。对于这些场景,我们了解了如何使用Spring为AspectJ切面注入依赖。