在软件开发中,散布于应用中多处的功能被称为横切关注点。把这些横切关注点与业务逻辑相分离正是面向切面编程(AOP)所要解决的问题。
依赖注入(DI)有助于应用对象之间的解耦,而AOP可以实现横切关注点与它们所影响的对象之间的解耦。
在使用面向切面编程时,我们仍然在一个地方定义通用功能,但是可以通过声明的方式定义这个功能要以何种方式在何处应用,而无需修改受影响的类。
横切关注点可以被模块化为特殊的类,这些类被称为切面(aspect)。
描述切面的常用术语有通知(advice)、切点(pointcut)、连接点(join point)。
在AOP术语中,切面的工作被称为通知。通知定义了切面是什么以及何时使用。除了描述切面要完成的工作,通知还解决了何时执行这个工作的问题。
Spring切面可以应用5种类型的通知:
连接点
连接点是在应用执行过程中能够插入切面的一个点。这个点可以是调用方法时、抛出异常时、甚至修改一个字段时。切面代码可以利用这些点插入到应用的正常流程之中,并添加新的行为。
切点(Poincut)
如果说通知定义了切面的“什么”和“何时”的话,那么切点就定义了“何处”。切点的定义会匹配所要织入的一个或多个连接点。我们通常使用明明确的类和方法名称,或是利用正则表达式定义所匹配的类和方法名称来指定这些切点。
切面(Aspect)
切面是通知和切点的结合。通知和切点共同定义了切面的全部内容——它是什么、在何时何处完成其功能。
引入(Introduction)
引入允许我们向现有的类添加新方法或属性。
织入(Weaving)
织入是把切面应用到目标对象并创建新的代理对象的过程。切面在指定的连接点呗织入到目标对象中。在目标对象的生命周期里有多个点可以进行织入:
Spring对AOP的支持
创建切点来定义切面所织入的连接点是AOP框架的基本功能。
Spring提供了4中类型的AOP支持:
前三种都是Spring AOP实现的变体,Spring AOP构建在动态代理基础之上,因此,Spring对AOP的支持局限于方法拦截。
Spring所创建的通知都是用标准的Java类编写的。
通过在代理类中包裹切面,Spring在运行期把切面织入到Spring管理的bean中。Spring的切面包裹了目标对象的代理类实现。代理类处理方法的调用,执行额外的切面逻辑,并调用目标方法。直到应用需要被代理的bean时,Spring才创建代理对象。因为Spring运行时才创建代理对象,因此我们不需要特殊的编译器来织入Spring AOP的切面。
Spring只支持方法级别的连接点。
在Spring AOP中,要使用AspectJ的切点表达式语言来定义切点。关于Spring AOP的AspectJ切点,最重要的一点就是Spring仅支持AspectJ切点指示器的一个子集。因为Spring是基于代理的,而某些切点表达式与基于代理的AOP无关。
下图为Spring AOP所支持的AspectJ切点指示器:
execution指示器执行匹配,其他指示器来限制匹配的切点。
除此之外,Spring还引入一个新的bean()指示器,它允许我们在切点表达式中使用bean的ID来标示bean。
例如,我们要使用AspectJ切点表达式来选择一个名为Performance类中的perform()方法:
execution(* concert.Perfoemance.perform(..))
/**我们使用execution()指示器来选择Performance的perform()方法。方法表达式以“ * ”号开始,表明我们不关心方法返回值的类型。然后,指定了全限定类名和方法名。对于参数列表,我们使用两个点号(..)表明切点要选择任意的perform()方法,无论该方法的入参是什么。**/
/**如果我们需要配置的切点仅匹配concert包,我们可以使用within()指示器来限制。**/
execution(* concert.Perfoemance.perform(..)) && within(concert.*)
因为“&”在XML中有特殊含义,所以Spring的XML配置里面描述切点时,我们可以使用and、or、not来代替“&&”、“||”、“!”。
execution(* concert.Performance.perform(..)) and bean('woodstock')
如果我们把Performance类中perform()方法看成是一个表演的话,那么下面这个切面定义的是在表演之前、表演之后、以及表演失败之后观众的反映(即程序的输出)。
@Aspect //表明Audience是一个切面
public class Audience {
//表演之前
@Before("execution(** concert.Performance.perform(..))")
public void silenceCellPhone() {
System.out.println("将手机调至静音状态");
}
//表演之前
@Before("execution(** concert.Performance.perform(..))")
public void takeSeats() {
System.out.println("就坐");
}
//表演之后
@AfterReturning("execution(** concert.Performance.perform(..))")
public void applause() {
System.out.println("鼓掌喝彩");
}
//表演之后
@AfterThrowing("execution(** concert.Performance.perform(..))")
public void demandrefund() {
System.out.println("要求退款");
}
}
此外,@Pointcut注解能够在一个@Aspect切面内定义可重用的切点
@Aspect //表明Audience是一个切面
public class Audience {
//定义命名的切点
@Pointcut("execution(** concert.Performance.perform(..))")
public void performance() {}
//表演之前
@Before("performance()")
public void silenceCellPhone() {
System.out.println("将手机调至静音状态");
}
//表演之前
@Before("performance()")
public void takeSeats() {
System.out.println("就坐");
}
//表演之后
@AfterReturning("performance()")
public void applause() {
System.out.println("鼓掌喝彩");
}
//表演之后
@AfterThrowing("performance()")
public void demandrefund() {
System.out.println("要求退款");
}
}
在这里,Audience仍然是一个Java类,只不过它通过注解表明会作为切面使用而已。同其他类一样,可以装配为Spring中bean。
除此之外,你还需要对AspectJ注解进行配置,不然这些代码不会生效。如果使用JavaConfig的话,可以在配置类的类级别上通过使用@EnableAspectJAutoProxy注解启用自动代理:
@Configuration
@EnableAspectJAutoProxy //启用AspectJ自动代理
@Component
public class ConcertConfg{
//声明Audience bean
@Bean
public Audience audience() {
return new Audience();
}
}
如果使用xml装配bean的话,那么需要使用Spring aop命名空间的
元素:
...
记得加命名空间哦>
"concert" />
"concert.Audience" />
需要记住的是,Spring的AspectJ自动代理仅仅使用@AspectJ作为创建切面的指导,切面依然是基于代理的。在本质上,它依然是Spring基于代理的切面。这一点非常重要,因为这意味着尽管使用的是@AspectJ注解,但我们仍然限于代理方法的调用。如果想利用AspectJ的所有能力,我们必须在运行时使用AspectJ并且不依赖Spring来创建基于代理的切面。
环绕通知是最为强大的通知类型。它能够让你所编写的逻辑将被通知的目标方法完全包装起来。实际上就像在一个通知方法汇总同时编写前置通知和后置通知。
@Aspect //表明Audience是一个切面
public class Audience {
//定义命名的切点
@Pointcut("execution(** concert.Performance.perform(..))")
public void performance() {}
@Around("performance()")
public void watchPerformance(ProceedingJoinPoint jp) {
try {
System.out.println("将手机调至静音状态");
System.out.println("就坐");
jp.proceed();//调用被通知的方法
System.out.println("鼓掌喝彩");
}catch (Throwable e) {
System.out.println("要求退款");
}
}
}
ProceedingJoinPoint 对象参数是必须的。通知中通过他来调用被通知的方法。
处理通知中的参数
@Aspect //表明Audience是一个切面
public class Audience {
//定义命名的切点
@Pointcut("execution(** concert.Performance.perform(String))"
+"&& args(songName)")
public void performance(String songName) {}
@Before("performance(songName)")
public void watchPerformance(String songName) {
System.out.println("演唱的歌曲是"+songName);
}
}
这样就可以将方法中的参数传达到通知中。
通过注解引入新功能
当引入接口的方法被调用时,代理会把此调用委托给实现了新接口的某个其他对象。也就是说,一个bean的实现被拆分到多个类中。
为了实现该功能,我们需要创建一个新的切面:
@Aspect
public class EncoreableIntroducer {
@DeclareParents(value="concert.Performance+",
defaultImpl=DefaultEncoreable.class)
public static Encoreable encoreable;
}
通过@DeclareParents注解,将Encoreable 接口引入到Performance bean中。
@DeclareParents注解由三部分组成:
在Spring的aop命名空间中,提供了多个元素用来在XML中声明切面
去掉Audience所有的Aspect注解
public class Audience {
public void silenceCellPhone() {
System.out.println("将手机调至静音状态");
}
public void takeSeats() {
System.out.println("就坐");
}
public void applause() {
System.out.println("鼓掌喝彩");
}
public void demandrefund() {
System.out.println("要求退款");
}
}
声明前置通知和后置通知
<aop:config>
<aop:aspect ref="audience">
<aop:before
pointcut="execution(** concert.Performance.perform(..))"
method="silenceCellPhone" />
<aop:before
pointcut="execution(** concert.Performance.perform(..))"
method="takeSeats" />
<aop:after-returning
pointcut="execution(** concert.Performance.perform(..))"
method="applause" />
<aop:after-throwing
pointcut="execution(** concert.Performance.perform(..))"
method="demandrefund" />
aop:aspect>
aop:config>
使用
定义切点
<aop:config>
<aop:aspect ref="audience">
<aop:pointcut
id="performance"
expression="execution(** concert.Performance.perform(..))" />
<aop:before
pointcut-ref="performance"
method="silenceCellPhone" />
<aop:before
pointcut-ref="performance"
method="takeSeats" />
<aop:after-returning
pointcut-ref="performance"
method="applause" />
<aop:after-throwing
pointcut-ref="performance"
method="demandrefund" />
aop:aspect>
aop:config>
声明环绕通知
public class Audience {
public void watchPerformance(ProceedingJoinPoint jp) {
try {
System.out.println("将手机调至静音状态");
System.out.println("就坐");
jp.proceed();//调用被通知的方法
System.out.println("鼓掌喝彩");
}catch (Throwable e) {
System.out.println("要求退款");
}
}
}
<aop:config>
<aop:aspect ref="audience">
<aop:pointcut
id="performance"
expression="execution(** concert.Performance.perform(..))" />
<aop:around
pointcut-ref="performance"
method="watchPerformance" />
aop:aspect>
aop:config>
为通知传递参数
public class Audience {
//要声明为前置通知的方法
public void watchPerformance(String songName) {
System.out.println("演唱的歌曲是"+songName);
}
}
<aop:config>
<aop:aspect ref="audience">
<aop:pointcut
id="performance"
expression="execution(** concert.Performance.perform(String))
and args(songName)" />
<aop:before
pointcut-ref="performance"
method="watchPerformance" />
aop:aspect>
aop:config>
通过切面引入新的功能
<aop:aspect>
<aop:declare-parents
types-matching="concert.Performance+"
implement-interface="concert.Encoreable"
default-impl="concert.DefaultEncoteable"
/>
</aop:aspect>