Spring实战(十)Spring AOP应用——为方法引入新功能、为对象引入新方法
切面最基本的元素是通知和切点,切点用于准确定位应该在什么地方应用切面的通知。
1、Spring借助AspectJ的切点表达式语言来定义Spring切面
在Spring中,要使用AspectJ的切点表达式语言来定义切点。
重要的一点是,Spring仅支持AspectJ切点指示器的一个子集,当尝试使用AspectJ其他指示器时,会抛出异常
arg() 限制连接点匹配参数为指定类型的执行方法
@args() 限制连接点匹配参数为指定注解标注的执行方法
execution() 用于匹配是连接点的执行方法
this() 限制连接点匹配AOP代理的bean引用为指定类型的类
target 限制连接点匹配目标对象为指定类型的类
@target() 限制连接点匹配特定的执行对象,这些对象对应的类要具有指定类型的注解
within() 限制连接点匹配指定的类型
@within 限制连接点匹配指定注解所标注的类型(当使用Spring AOP时,方法定义
在由指定的注解所注解的类中)
@annotation 限定匹配带有指定注解的连接点
2、编写切点——切点表达式
package concert
public interface Performance{
public void perform();
}
下面编写Performance的perform()方法的触发的通知,这个切点表达式能够设置当perform()方法执行时触发通知的调用:
execution( * concert.Performance.perform(..) )
其中方法表达式从“ * ”号开始,表示我们不关心方法返回值的类型;
然后,指定全限定类名和方法名;
方法参数列表中,用两个点号(..)表明切点要选择任意的perform()方法,而不关心该方法的入参是什么。
假设,我们要配置的切点仅匹配concert包,可以利用within()指示器:
execution( * concert.Performance.perform(..) ) && within(concert.*)
这里我们使用了“&&”操作,在其他切点表达式中我们也可以使用“||”、“!”操作。(or、not)
3、在切点中选择bean
Spring引入了一个新的bean()指示器,它允许我们在切点表达式中使用bean的ID or bean名称作为参数来限制切点,让它只匹配特定的bean。
execution( * concert.Performance.perform(..) ) and bean('woodstock')
这时,切面的通知会被织入到ID为woodstock的bean中。
还可以如下使用,切面的通知会被织入到所有ID不为woodstock的bean中:
execution( * concert.Performance.perform(..) ) and !bean('woodstock')
4、使用AspectJ注解创建切面
使用注解创建切面是AspectJ 5引入的关键特性,之前需要学习一种Java语言的扩展。
首先确定,上面定义的Performance接口,是切面中切点的目标对象,接下来我们定义一个Audience类,我们把观众作为演出的切面。
@Aspect
public class Audience {
@Before("execution(* concert.Performance.perform(..))")
public void silenceCellPhones(){
System.out.println("Silencing cell phones.");
}
@Before("execution(* concert.Performance.perform(..))")
public void takeSeats(){
System.out.println("Taking seats.");
}
@AfterReturning("execution(* concert.Performance.perform(..))")
public void applause(){
System.out.println("CLAP!!~~CLAP!!~~");
}
@AfterThrowing("execution(* concert.Performance.perform(..))")
public void demandRefund(){
System.out.println("Demanding a refund.");
}
}
这四个方法定义了一个观众在观看演出时可能会做的事情:
演出之前,观众就坐—将手机调至静音状态;
演出精彩—观众鼓掌;
演出很烂—观众要求退款。
(通知方法中的注解都给定了一个切点表达式作为它的值)
5、AspectJ提供的注解(声明通知方法)
@After 通知方法在目标方法返回or抛出异常后执行;
@AfterReturning 通知方法在目标方法返回后调用;
@AfterThrowing 通知方法在目标方法抛出异常后调用;
@Around 通知方法将目标方法封装起来;
@Before 通知方法在目标方法调用之前执行;
6、使用@Pointcut注解在一个@AspectJ切面内定义可以重用的切点。
在上一个切面Performance类中,注解中的切点表达式我们重复使用了四次,下面使用@Pointcut进行简化:
@Aspect
public class Audience {
@Pointcut("execution(* concert.Performance.perform(..))")
public void performance() {}
@Before("performance()")
public void silenceCellPhones(){
System.out.println("Silencing cell phones.");
}
@Before("performance()")
public void takeSeats(){
System.out.println("Taking seats.");
}
@AfterReturning("performance()")
public void applause(){
System.out.println("CLAP!!~~CLAP!!~~");
}
@AfterThrowing("performance()")
public void demandRefund(){
System.out.println("Demanding a refund.");
}
}
这个Audience类中的performance()方法的实际内容并不重要,在这里它实际上应该是空的,因为该方法本身只是一个标识,供@Pointcut注解依附。
到此为止,我们创建了切面吗?并没有,目前,Audience只会是Spring容器中的一个bean。(@AspectJ会自动创建为bean?)
这里即使我们使用了AspeJ注解,但它不会被是为切面,这些注解不会解析,也不会创建将其转换为切面的代理。
7、使用JavaConfig,XML开启自动代理
JavaConfig:
可以在配置类(使用JavaConfig有一个配置类(@Configuration)来开启各项功能)上使用@EnableAspeJAutoProxy启用自动代理功能;
@Configuration
@EnableAspectJAutoProxy
@ComponentScan
public class ConcertConfig{
@Bean
public Audience audience{
return new Audience();
}
}
XML:
使用Spring的aop命名空间中的<aop:aspectj-autoproxy>元素,开启自动代理。(先声明Spring的AOP命名空间)
不管是哪种方式,AspeJ自动代理都会为使用@Aspect所注解的bean创建一个代理,这个代理会围绕所有该切面所匹配的bean。
8、创建环绕通知@Around
环绕通知是最为强大的通知类型,可以使自己编写的逻辑将被通知的目标方法完全包装起来。
下面用一个环绕通知来代替之前的多个前置和后置通知:
@Aspect
public class AudienceAround {
@Pointcut("execution(* concert.Performance.perform(..))")
public void performance() {
}
@Around("performance()")
public void watchPerformance(ProceedingJoinPoint jp) {
try {
System.out.println("Silencing cell phone");
System.out.println("Taking seats");
jp.proceed();
System.out.println("CLAP~~CLAP~");
}catch(Throwable e){
System.out.println("Demanding a refund!");
}
}
}
这个位于一个方法中的通知所达到的效果,与之前的几个前置和后置通知是一样的。
注意到,这个watchPerformance方法接受了一个ProceedingJoinPoint类型的参数,jp对象必须要有,因为通过它的proceed()方法来调用被通知(增强)的方法。也就是说,这个通知阻塞了被通知的方法,我们必须手动调用。
9、若被通知的方法含有参数,切面能访问和使用被传递给被通知的方法的参数吗?
也就是说,我们的增强逻辑中,需要利用被增强的方法中的参数完成一些功能。例如:
@Aspect
public class TrackCounter{
@Pointcut("execution(* soundsystem.CompactDisc.playTrack(int))"&&"args(trackNumber)")
public void trackPlayed(int trackNumber) {}
@Before("trackPlayed(trackNumber)")
public void countTrack(int trackNumber){...}
...
}
这个切面中,使用@Pointcut注解定义命名的切点中声明了要提供给通知方法的参数。切点表达式中"args(trackNumber)"限定符表明了传递给playTrack()方法的int型参数也会传递到通知中去,参数的名称trackNumber也与切点方法签名中的参数相匹配。
下面的前置通知方法的注解@Before中与countTrack方法中的入参均有trackNumber,这样就完成了从命名切点到通知方法的参数转移。
形象点来说,我们靠AspectJ注解作为载体,把被通知方法中的参数转义到通知中。
目前为止,我们所使用的切面中,所包装的都是被通知对象的已有方法,这仅仅是切面能实现的功能之一。
10、如何通过编写切面,为被通知的对象(不是方法)引入全新的功能?
Java不是动态语言,一旦类编译完成了,我们就很难再为该类添加新功能。
我们之前做的都是为对象拥有的方法添加新功能,那我们为什么不能为对象增加新的方法呢?
使用Spring AOP,我们可以为bean引入新的方法,代理拦截调用并委托给实现该方法的其他对象。
11、通过@DeclareParens注解实现为对象引入新方法
现在我们来为之前的Performance接口的所有实现引入新接口Encoreable(观众要求返场表演):
package concert;
public interface Encoreable {
void performEncore();
}
借助于AOP的引入功能,我们可以不必在设计上妥协或者侵入性地改变现有的实现。
为了实现该功能,创建一个新的切面:
@Aspect
public class EncoreableIntroducer {
@DeclareParents(value="concert.Performance+",defaultImpl = DefaultEncoreable.class)
public static Encoreable encoreable;
}
这个切面并没有提供前置、后置或环绕通知,而是通过@DeclareParens注解,将Encoreable接口引入到Performance bean中:
- value属性指定了那种类型的bean要引入该接口。这里是指定为所有实现Performance的类型,“+”表示是Performance的所有子类型,而不是本身。
- defaultImpl属性指定了为引入功能提供实现的类。在这里,指定DefaultEncoreable提供实现。
- @DeclareParens注解所标注的静态属性表明了要引入的接口,这里我们引入Encoreable接口。
和其他切面一样,我们需要在Spring应用中将EncoreableIntroducer声明为一个bean。然后Spring的自动代理机制会获取到它的声明,当Spring发现一个bean使用@AspectJ注解时,Spring就会创建一个代理,单后将调用委托给被代理的bean或被引入的实现,这取决于调用的方法属于被代理的bean还是属于被引入的接口。
Spring的注解和自动代理提供了一种便利的方式创建切面,但是面向注解的切面声明有一个明显的劣势:你必须能够为通知类添加注解。为了做到这一点,必须要有源码!
若没有源码or不想将AspectJ注解放入你的代码中,可以使用Spring XML配置文件声明切面。