在系统中有些功能我们需要应用到程序的多个地方,但是我们又不想在每个点都明确的调用他。日志、安全和事务管理的确很重要,但他们是否是应用对象主动参与的行为呢?如果让应用对象只关注于自己所针对的业务领域问题,而其他方面由其他应用对象来处理,会不会更好?
在软件开发中,散布于应用中多处的功能被称为横切关注点。通常来讲,这些横切关注点从概念上是与应用的业务逻辑相分离的(但往往会直接嵌入到应用的业务逻辑中)。把这些横切关注点和业务逻辑相分离正式面向切面编程(AOP)所要解决的问题。
解释面向切面编程
如下图所示,我们将一个个横切关注点理解为模块,这些模块可以被应用到多处。例如日志管理是一个模块,应用中的各个方法都会涉及到日志管理,所以日志管理这个模块会被多次应用,为了不与某个单独的方法耦合,我们只是将其横切于各个方法之上,同理我们可以有多个模块同时横切多个内容。可能会想到这些重用的功能是否可以通过继承或者是委托来实现,实际上是可以的,但是使用继承会导致一个脆弱的类,使用委托可能需要对委托对象进行复杂的调用。切面提供了取代继承和委托的一种可选方案,在使用面向切面编程的时候,我们仍然在一个地方定义一个通用的功能,但是可以通过声明的方式定义这个功能要以何种方式在何处应用,而无需修改受影响的类。横切关注点可以被模块化为特殊的类,这些类被称为切面。
在AOP中有很多术语,最好先来解释一下各种术语。
- 通知(Advice)
我们需要让模块做什么事情,比如说日志模块,他的工作是跟日志记录相关的,也就是他要完成度工作,在AOP术语中,他要完成的功能被称为通知。通知定义了切面是什么以及何时被使用这两个问题,也就是在什么时候才要执行我这个功能。Spring切面有五种类型的通知:前置通知、后置通知、返回通知、异常通知、环绕通知。
- 切点(Poincut)
切点定义了何处,即在哪里插入方法,我们通常使用名且的类和方法名称或者是应用正则表达式定义所匹配的类或者方法名称来指定这些切点。
- 切面(Aspect)
切面是通知和切点的集合,通知和切点共同定义了切面的全部内容,他是什么在何时何处完成功能。
- 引入(Introduction)
引入允许我们向现有的类添加新方法或者属性。
- 织入(Weaving)
织入就是把切面应用到目标对象并创建新的代理对象的过程。在目标对象的生命周期里有多个点可以织入,编译期、类加载期、运行期(Spring AOP就是以这种方式织入切面的)
切点相关
正如之前提到过的,切点用于准确定位应该在什么地方应用切面的通知,通知和切点是切面的最基本元素,因此了解如何编写切点非常重要。Spring是借助AspectJ的切点表达式来定义Spring切点的,我们主要使用executi指示器来编写切点定义,在此基础上使用其他指示器来限制匹配的切点。我们来定义一个接口实例:
public interface Performance {
void perform();
}
我们希望在执行perform()方法的时候触发通知,我们可以编写一个切点表示来设置当perform方法执行的时候触发通知的调用。我们使用exection指示器来选择perform方法,execution(* concert.Performance.perform(..)),从*开始,表示返回类型可以是任意类型,concert.Performance.perform指定了全限定的类名和方法名,在方法参数中使用两个点(..)表示任意参数。这个表达式的完整意思是只要存在concert.Performance.perform这个方法,不管他的返回值是什么参数有什么不同,都会触发通知的调用。
切面相关
使用注解创建切面
定义切面
接着上述的场景,在一场演出中,如果没有观众的话那就不能称为演出,但是对于演出本身来说,关注不是他所要关心的功能。因此我们可以将观众定义为一个切面,并将其应用到演出上是比较明智的做法。
/**
* 该注解表明Audience不仅仅是POJO还是一个切面
*/
@AspectJ
public class Audience{
//在表演之前通知--手机静音
@Before("execution(* concert.Performance.perform(..))")
public void silenceCellPhones(){
System.out.println("Silence Cell Phones");
}
//在表演之前通知--观众就坐
@Before("execution(* concert.Performance.perform(..))")
public void takeSeats(){
System.out.println("Take Seats");
}
//在表演之后--观众喝彩
@AfterReturning("execution(* concert.Performance.perform(..))")
public void applause(){
System.out.println("applause");
}
//在表演失败之后通知--退款
@AfterThrowing("execution(* concert.Performance.perform(..))")
public void demanRefund(){
System.out.println("deman Refund");
}
}
但是我们发现同样的切点我们重复了四次,我们是否可以只定义一次切点然后在需要的时候引用呢,答案是肯定的,我们可以使用@Pointcut注解在一个切面内定义可重用的切点。需要注意的Audienc这个类仍然是一个POJO,我们可以像使用其他Java类那样调用他的方法,他的方法也能够进行独立的测试。
//定义切点
@Pointcut("execution(* concert.Performance.perform(..))")
public void performance(){}//performance此时就是切点的名字
//在表演之前通知--手机静音
@Before("performance()")
public void silenceCellPhones(){
System.out.println("Silence Cell Phones");
}
像其他的Java类一样,他可以装配为Spring中的bean
@Bean
public Audience audience(){
return new Audience();
}
但是如果就此止步的话,他也知识Spring容器中的一个bean,即便是使用了@AspectJ注解,也不会被视为切面,这些注解不会解析也不会创建将其转换为切面的代理。这时候我们需要开启@AspectJ自动代理功能。在JavaConfgi中,可以在类级别上使用@EnableAspectJAutoProxy注解,同理可以在XML中声明。需要引入aop命名空间,使用<aop:aspectj-autoproxy>元素,然后配置bean就可以了。不管使用哪一种方式,AspectJ自动代理都会为使用@Aspect注解的bean创建一个代理,这个代理会围绕所有该切面的切点匹配的bean。
创建环绕通知
单独拿环绕通知来举例是因为这种通知与前面实例的通知都是不太相同的,首先我们来重写一下Audiencen切面,使用环绕通知来实现效果。
@AspectJ
public class Audience{
@Pointcut("execution(* concert.Performance.perform(..))")
public void performance(){}
@Around("performance()")
public void watchPerformance(ProceedingJoinPoint jp){
try{
System.out.println("Silence Cell Phones");//相当于前置通知
System.out.println("Take Seats");//前置通知
jp.proceed();//执行被通知的方法
System.out.println("applause");//后置通知
}catch(Exception e){
System.out.println("deman Refund");//后置异常通知
}
}
}
在这样的一个通知中直接实现了之前的四个通知方法,还有一点不同的是在环绕通知中还有一个参数ProceedingJoinPoint,这个对象是必须的,我们需要在通知中通过他来调用被通知的方法,当我们调用ProceedingJoinPoint的proceed方法时,就是把执行的控制权交给被通知的方法,实际上如果没有调用proceed方法的话,会造成会被通知方法的阻塞。
处理通知中的参数
到目前为止,我们的切面非常简单没有任何参数。如果在切点表达式中声明参数的话,这个参数会传入到通知的方法中去,在切点中定义的参数与切点方法中的参数名称是相同的,这样就完成了从命名切点到通知方法的参数转移。
@Pointcut("execution(* com.package.method(int))") and args(intName)
public void argsMethod(int intName){}
@Before("argsMethod(intName)")
public void testMethod(int intName){
"Use the args";
}
通过注解引入新功能
我们可以对象的已有方法上添加新功能,但是能否在已有的对象上添加新方法呢?实际上,可以利用被称为引入的AOP概念,切面可以为Spring bean添加新的方法。在Spring中,切面只是实现了他们所包装的bean相同接口的代理,如果除了实现这些接口,代理也能暴露新的接口的话,是不是切面所通知的bean看起来像是实现了新的接口呢,既是底层的实现类并没有真正实现这些接口。
假设我们有个新的接口名字叫Encoreable,暂且不管这个接口是干嘛的,我们现在想要将这个接口应用到Performance实现中,我们可以使用AOP的引入功能,不必在设计上妥协或者侵入性地改变现有的实现。为此我们需要创建一个切面:
@AspectJ
public Introducer{
/**
*我们解释一下@DeclareParents这个注解由三部分组成
*value属性指定了哪种类型的bean要引入接口后面的"+"表示Performance的子类型不包含本身
*defaultImpl属性指定了具体引入的实现类
*第三部分就是被注解的这个静态属性代表了要引入的接口是什么
*/
@DeclareParents(value="concert.Performance+",defaultImpl="DefaultEncoreable.class")
public static Encoreable encoreable;
}
和其他切面一样,我们需要在Spring中将Introducer声明为一个bean,Spring就会创建一个代理,然后将调用委托给被代理的bean或被引入的实现,取决于调用的方法术语被代理的bean还是被引入的接口。到此已经介绍了基于注解的切面,虽然这是一种很方便的方式来创建代理,但是面向注解的切面声明有一个明显的劣势,你必须能够为通知的类添加注解,也就是必须要有源码。,如果没有源码的话或者不想使用AspectJ注解,还可以使用基于XML配置文件的方式声明切面。
在XML中声明切面
在Spring aop命名空间中提供了很多元素用来在XML中声明切面。
<!--将Audience声明为一个bean-->
<bean id="audience" class="com.Audience"/>
<!--启动@AspectJ注解驱动的切面-->
<aop:aspect-autoproxy>
<!--配置切面-->
<!--顶层的aop配置元素-->
<aop:config>
<!--声明一个切面-->
<aop:aspect ref="audience">
<!--定义一个切点-->
<aop:pointcut id="performance" expression="expression(* concert.Performance.perform(..))"/>
<!--定义前置通知-->
<aop:before pointcut-ref="performance" method="silenceCellPhones"/>
<!--定义后置通知(不管被通知的方法是否成功执行)-->
<aop:after pointcut-ref="performance" method="applause"/>
<!--定义后置通知-->
<aop:after-returning pointcut-ref="performance" method="applause"/>
<!--定义异常通知-->
<aop:after-throwing pointcut-ref="performance" method="demanRefund"/>
<!--定义环绕通知-->
<aop:around pointcut-ref="performance" method="watchPerformance"/>
<!--引入功能-->
<aop:declare-parents
type-matching="concert.Performance+"
implement-interface="concert.Encoreable"
default-impl="concert.DefaultEncoreable"/>
</aop:aspect>
</aop:config>
基本上XML配置中的aop元素都与注解都能够相对应。
结束
AOP是面向对象编程的一个强大补充,在Spring中提供了AOP框架,可以通过XML和注解的方式配置切点和切面。