AOP
AOP即面向切面编程的简称。切面能帮助我们模块化横切关注点
定义切面术语
- 通知(advice)
描述了切面要完成的工作以及何时使用
Spring切面可以应用5中类型的通知:- 前置通知(Before):在目标方法被调用之前调用通知功能
- 后置通知(After):在目标方法完成之后调用通知,此时不会关心方法的输出是什么;
- 返回通知(After-returning):在目标方法成功执行之后调用通知;
- 异常通知(After-throwing):在目标方法抛出异常后调用通知;
- 环绕通知(Around):通知包裹了被通知的方法,在被通知的方法调用之前和调用之后执行自定义的行为
- 切点(pointcut)
定义了何处给予通知,切点的定义会匹配通知所要织入的一个或多个连接点 - 切面(Aspect)
由通知和切点组合而成,它是什么(例如它是一个用于安全防护或者日志记录的切面)在何时和何处完成其功能。 - 连接点(join point)
连接点是在应用执行过程中能够插入切面的一个点。简单来说,可以是你所运行方法的一个代码段。 - 引入(Introduction)
引入允许我们向现有的类添加新方法或属性。 - 织入(Weaving)
织入是把切面应用到目标对象并创建新的代理对象的过程,在目标对象的生命周期里有多个时间点可以进行织入:- 编译期:切面在目标类编译时被织入。这种方式需要特殊的编译器。AspectJ的织入编译器就是以这种方式织入切面的。
- 类加载器:切面在目标类加载到JVM时被织入。也需要特使的类加载器,使目标类被引入应用之前增强该目标类的字节码。AspectJ 5的类加载器就支持
- 运行期:切面在应用运行的某个时刻被织入。一般情况下,在织入切面是,AOP容器会为目标对象动态创建一个代理对象。Spring AOP就是以这种方式织入切面的。
Spring AOP
Spring 提供了4种类型的AOP支持
- 基于代理的经典Spring AOP
- 纯POJO切面;
- @AspectJ注解驱动的切面;
- 注入式AspectJ切面(适用于Spring各版本)
前三种都是Spring AOP实现的变体,Spring AOP构建在动态代理基础之上,因此,Spring对AOP的支持局限于方法拦截。
在具体学习之前,还是需要了解Spring AOP框架的一些关键知识:
- Spring通知是Java编写的
- Spring在运行时通知对象
- Spring只支持方法级别的连接点
由前面的知识已知,切面是由切点和通知组成,而引入和织入是针对切面和目标对象的。所以可以按照
切点构建->通知构建->切面构建->引入、织入
的顺序来学习Spring AOP
通过切点来选择连接点
Spring仅支持AspectJ切点指示器的一个子集 如下所示
AspectJ指示器 | 描述
————– | —
arg() | 限制连接点 匹配 参数为指定类型的执行方法
@args() | 限制连接点 匹配 参数由指定注解标注的执行方法
ececution() | 用于匹配是连接点的执行方法
this() | 限制连接点 匹配 AOP代理的bean应用为指定类型的类
target() | 限制连接点 匹配 目标对象为指定类型的类
@target() | 限制连接点 匹配 特定的执行对象,这些对象对应的类要具有指定类型的注解
within() | 限制连接点 匹配 指定的类型
@within() | 限制连接点 匹配 指定注解所标注的类型
@annotation() | 限制匹配 带有指定注解的连接点
当尝试使用其他AspectJ指示器时,会抛出IllegalArgument-Exception异常。
注意只有execution指示器时是实际执行匹配的,而其他都是限制匹配的
编写切点
为了阐述Spring中的切面,我们需要有个主题来定义切面的切点。为此,我们定义一个Performance接口:
package concert;
public interface Performance {
public void perform();
}
我们可以创建如下的切点
在切点中选择bean
Spring除了支持AspectJ切点指示器上的子集外,子集还引入了一个新的bean()指示器,
它允许我们在切点表达式中使用bean ID来表示bean。bean()使用bean ID或bean名称来限制切点只匹配特定的bean。如下例
execution(* concert.Performance.perform()) and bean('woodstock')
或只排除特定的bean
execution(* concert.Performance.perform()) and !bean('woodstock')
使用注解创建切面
切面可以被用于 织入 或 引入 ,下面分别来谈谈
用于织入(Weaving)
AspectJ提供了五个注解来定义通知在何时触发
注解 | 通知
————– | —
@After | 通知方法会在目标方法返回或抛出异常后调用
@AfterReturning | 通知方法会在目标方法返回后调用
@AfterThrowing | 通知方法会在目标方法抛出异常后调用
@Around | 通知方法会将目标方法封装起来
@Before | 通知方法会在目标方法之前调用
以下是个示例
package concert;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
@Aspect //声明这是个切面
public class Audience {
@Before("execution(** concert.Performance.perform(..))")
public void silenceCellPhones(){
System.out.println("Silencing cell phones");
}
@AfterReturning("execution(** concert.Performance.perform(..))")
public void applause(){
System.out.println("applause");
}
@AfterThrowing("execution(** concert.Performance.perform(..))")
public void demandRefund(){
System.out.println("bad");
}
}
发现了这个定义切点的指示器execution(** concert.Performance.perform(..))重复出现了三次,这一点也不优美。幸运的是Spring定义了@PointCut注解,用来简化上述情况,以下是使用@PointCut的示例
package concert;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.PointCut;
@Aspect //声明这是个切面
public class Audience {
@PointCut("execution(** concert.Performance.perform(..))")
pulic void performance(){} //该方法只是一个标识,里面的内容并不重要,应为空
@PointCut("execution(* soundsystem.CompactDisc.playTrack(int))&& args(trackNumber)")
pulic void trackPlayed(int trackNumber){} //该方法可以支持处理通知中的参数
@Before("performance()")
public void silenceCellPhones(){
System.out.println("Silencing cell phones");
}
@AfterReturning("performance()")
public void applause(){
System.out.println("applause");
}
@AfterThrowing("performance()")
public void demandRefund(){
System.out.println("bad");
}
/*切点定义中的参数与切点方法中的参数名称是一样的,这样就完成了从命名切点到通知方法的参数转移*/
@Before("trackPlayed(trackNumber)")
public void count(int trackNumber){
return trackNumber;
}
}
@Around注解会有点特殊,特别的为他举一个例子
package concert;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.PointCut;
@Aspect //声明这是个切面
public class Audience {
@PointCut("execution(** concert.Performance.perform(..))")
pulic void performance(){} //该方法只是一个标识,里面的内容并不重要,应为空
@Around("performance()")
Public void watchePerformance(ProceedingJoinPoint jp) {
tyr{
System.out.println("Silencing cell phones");
/*若不调用这个方法,被环绕的目标方法将会被阻塞,即不会调用目标方法*/
jp.proceed();
System.out.println("applause");
} catch (Throwable e){
System.out.println("bad");
}
}
}
用于引入(Introduction)
切面不仅可以实现它们所包装bean相同接口的代理,该代理还能暴露新接口,使切面所统治的bean看起来像是实现了新的接口,即便底层实现类并没有实现这些接口也无所谓。
我们首先创建一个新接口,它是我们将要引入并暴露的新接口
package concert;
public interface Encoreable {
void performEncore();
}
当你能访问目标对象的时候,你可以选择让该类实现这个接口以扩充新功能。
但大多数情况下,例如使用第三方实现并且没有源码的时候,你并不能访问或修改目标对象以实现新接口。但值得庆幸的是,你可以借助于AOP的引入功能,来实现新接口。
以下是实现:
package concert;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.DeclareParents;
@Aspect
public class EncoreableIntroducer {
@DeclareParents(value="concert.Performance+",
defaultImpl=DefaultEncoreable.class)
public static Encoreable encoreable;
}
@DeclareParents注解由三部分组成:
- value属性指定了哪些类型的bean要引入该接口(即value指定了目标类),标记符号后面的“+”表示是Performance的所有子类型,而不是Performance本身
- defaultImpl属性指定了为引入功能提供实现的类(即新接口的具体实现类)
- @DeclareParents注解所标注的静态属性指明了要引入的接口(即新接口)
让切面生效
因为Spring AOP构建在动态代理基础之上,而Spring并不是默认自动启动 自动代理功能,所以需要在配置文件中进行配置,以开启自动代理功能:
- 基于Java的配置文件(使用@EnableAspectJAutoProxy注解)
- 基于XML的配置文件(使用<aop:aspectj-autoproxy />元素)
同时给将该切面注册为bean。
Spring的自动代理机制将会获取到它们的声明,当Spring发现了一个bean使用了@Aspect注解时,Spring就会创建一个代理,然后将调用委托给被代理的bean或被引入的实现,这取决于调用的方法属于被代理的bean还是属于布尔诶引入的接口