概念上其应该与应用的业务逻辑分离,但多数情况横切关注点会嵌入到应用的业务逻辑之中,而面向切面编程(AOP)就是要把横切关注点与业务逻辑进行解耦
。
切面必须完成的工作,并指定何时执行工作。
应用执行过程中,能插入切面,应用通知的时机。如调用方法时、抛出异常时、修改一个字段时等等
定义切面在"何处"应用通知,有助于缩小切面所通知的连接点的范围。切点定义会匹配通知所要织入的一个或多个连接点。指定切点可以使用明确的类和方法名或用正则表达式定义所匹配的类和方法名来指定切点,或创建动态的切点。
通知和切点组成
向现有类添加新方法或属性,在无需修改现有类的情况下,让它们具有新的行为和状态。
把切面应用到目标对象并创建新的代理对象的过程。
在目标对象的生命周期中,有多个点可以进行织入:
前三种都属于Spring AOP实现,构建在动态代理
的基础上,只支持方法拦截
。
Spring在运行初期把切面织入到Spring管理的bean中
代理类封装目标类,并拦截被通知方法的调用,执行切面逻辑,对目标方法进行增强,再把调用转发给真正的目标。
如果AOP需求超过了简单的方法调用(如构造器拦截或属性拦截),需要考虑第四种方式,将值注入到AspectJ驱动的切面中。
Spring只支持AspectJ切点指示器(pointcut designator)的一个子集。下表列出了Spring AOP所支持的AspectJ切点指示器。
AspectJ指示器 | 描述 |
---|---|
arg() | 限制连接点匹配参数为指定类型的执行方法 |
@args() | 限制连接点匹配参数由指定注解标注 |
execution() |
用于匹配是连接点的执行方法 |
this() | 限制连接点匹配AOP代理的bean引用为指定类型的类 |
target() | 限制连接点匹配目标对象为指定类型的类 |
@target() | 限制连接点匹配特定的执行对象,这些对象对应的类要具有指定类型的注解 |
within() | 限制连接点匹配指定的类型 |
@within() | 限制连接点匹配指定注解所标注的类型(当使用Spring AOP时,方法定义在由指定的注解所标注的类里) |
@annotation | 限定匹配带有指定注解的连接点 |
⚠️:在spring中尝试使用AspectJ其它指示器时,会抛出IllegalArgumentException
上述指示器,只有execution
是实际执行匹配的,而其它指示器都是用来限制匹配的。
首先,定义一个接口Performance代表任何类型的现场表演,其中包含perform()方法:
package com.note.demo.aop.interf
public interface Performance {
public void perform();
}
限制切点只匹配特定的bean
"execution(* concert.Performance.perform(..)) and bean('woodstock')"
此处限定的bean id为 woodstock
排除特定的bean
"execution(com.note.demo.aop.interf.Perform.perform(..)) and !bean('woodstock')"
@Aspect
@Component
public class MyAspect{
@Before("execution(* com.note.demo.aop.interf.Performance.perform(..))")
public void before() throws Throwable{
System.out.println("织入前处理");
}
@After("execution(* com.note.demo.aop.interf.Performance.perform(..))")
public void before() throws Throwable{
System.out.println("织入后处理");
}
}
简化上述代码,可以将公共的execution配置提取出来:
@Aspect
@Component
public class MyAspect{
@Pointcut("execution(* com.note.demo.aop.interf.Performance.perform(..))")
public void myPointcut(){}
@Before("myPointcut()")
public void before(){
System.out.println("织入前处理");
}
@After("myPointcut()")
public void after(){
System.out.println("织入后处理");
}
}
⚠️:声明的切面类也要注入到Spring中,否则不生效。
当然,若不使用@Component,还可以使用javaConfig的方式将其注入:
@Configuration
@EnableAspectJAutoProxy
public class AopConfig {
@Bean
public MyAspect myAspect(){
return new MyAspect();
}
}
使用@EnableAspectJAutoProxy
启用自动代理功能
Spring中定义通知的5个AspectJ注解
注解 | 通知 |
---|---|
@After | 通知方法会在目标方法返回或抛出异常后调用 |
@AfterReturning | 返回后 |
@AfterThrowing | 抛出异常后 |
@Around |
通知方法会将目标方法封装起来 |
@Before | 通知方法会在目标方法之前调用 |
其中,@Around格式比较特殊:
@Aspect
@Component
public class MyAspect{
@Pointcut("execution(* com.note.demo.aop.interf.Performance.perform(..)))")
public void myPointcut(){}
@Around("myPointcut()")
public void around(ProceedingJoinPoint point){
System.out.println("环绕织入-前");
point.proceed();
System.out.println("环绕织入-后");
}
}
其中ProceedingJoinPoint
参数是必须的,通知方法要将控制权交给被通知方法时,需要调用ProceedingJoinPoint的proceed()方法。
⚠️:你可以不调用proceed()方法,从而阻塞被通知方法的访问;也可以在通知中多次调用它,例如在实现重试逻辑时,可以这样使用,在被通知方法失败后,进行重复尝试。
另外,还可以增强指定注解标注的bean
//连接点所在类
package com.note.demo.aop.interf
public interface Performance {
public void perform();
}
@Component
public class PerformanceImpl implements Performance {
@Override
@AopLog
public String perform() {
System.out.println("连接点1");
return "连接点1";
}
}
//注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AopLog {
}
//切面类
@Aspect
@Component
public class MyAspect {
@Pointcut("@annotation(com.note.demo.aop.AopLog)")
public void logPointCut(){}
@Before("logPointCut()")
public void before(){
System.out.println("织入前处理");
}
}
⚠️:指定注解必须加在实现类的方法上,加在接口上不生效。
//被通知方法所在类
public interface Performance {
void count(Integer count);
}
@Component
public class PerformanceImpl implements Performance {
@Override
public void count(Integer count){
System.out.println("逻辑方法传入数值为:" + count);
}
}
//切面类
@Aspect
@Component
public class MyAspect {
@Pointcut("execution(* com.note.demo.aop.interf.Performance.count(Integer))" + " && args(count)")
public void countPointCut(Integer count){}
@Around("countPointCut(count)")
public void aroundWithParam(ProceedingJoinPoint point, Integer count) throws Throwable {
System.out.println("带参数-织入前处理" + "切面处理参数为 " + count);
point.proceed();
System.out.println("带参数-织入后处理 " + "切面处理参数为 " + count);
}
}
上述代码中,通过args(count)
限制了传入通知的参数,其名字和countPointCut(Integer count)
方法中参数名一致。
利用被称为引入
的AOP概念,切面可以为Spring bean添加新方法。当Spring发现一个bean使用了@Aspect注解时,Spring会创建一个代理,然后将调用委托给被代理的bean或被引入的实现。
现有一个接口Performance,其有多个实现类;还有另一个接口Encoreable,现在需要将此接口应用到Performance的多个实现类中,该如何做呢?
当然,我们可以修改所有的实现类,让它们都实现Encoreable接口。但是,这样做并非最佳方案:
下面,看看AOP的引入功能如何实现上述需求:
@Aspect
public class EncoreableIntroducer{
@DeclareParents(value="concert.Performance+", defaultImpl=DefaultWncoreable.class)
public static Encoreable encoreable;
}
@DeclareParents
注解是此处的关键,其由三部分组成:
+
号表示是Performance的所有子类型而不是其本身;若需要声明切面,但又不能为通知类添加注解时,可以使用XML配置。
Spring aop命名空间提供了多个元素用于在XML中声明切面:
AOP配置元素 | 用途 |
---|---|
aop:advisor | 定义aop通知器 |
aop:after | 定义aop后置通知(不管被通知的方法是否执行成功) |
aop:after-returning | 定义aop返回通知 |
aop:after-throwing | 定义aop异常通知 |
aop:around | 定义aop环绕通知 |
aop:aspect | 定义一个切面 |
aop:aspectj-autoproxy | 启用AspectJ注解驱动的切面 |
aop:before | 定义一个aop前置通知 |
aop:config | 顶层的aop配置元素。大多数的aop元素都要包含在其内 |
aop:declare-parents | 以透明的方式为被通知的对象引入额外的接口 |
aop:pointcut | 定义一个切点 |
此时,以下面类为例:
package com.note.demo;
public class Audience {
private Map counterMap = new HashMap<>();
public void silenceCellPhone() {
System.out.println("Silence cell phone");
}
public void takeSeats() {
System.out.println("Taking seats");
}
public void applause() {
System.out.println("CLAP, CLAP, CLAP!!!");
}
public void demandRefund() {
System.out.println("Demanding a refund");
}
public void watchPerformance(ProceedingJoinPoint point){
try{
System.out.println("Silencing cell phone");
System.out.println("Taking seats");
point.proceed();
System.out.println("CLAP, CLAP, CLAP!!!");
}catch(Throwable e){
System.out.println("Demanding a refund");
}
}
//统计各个节目表演的次数
public void counter(ProceedingJoinPoint point, Integer id) throws Throwable {
int currentCount = getProgramCount(id);
counterMap.put(id, currentCount + 1);
}
private int getProgramCount(int id){
if(counterMap.containsKey(id)){
return counterMap.get(id);
}else{
return 0;
}
}
}
当然,我们也可以将上述中重复的pointcut部分抽象成公共的模块:
在上面声明的aspect中,加入如下标签
就可以将watchPerformance(ProceedingJoinPoint point)
声明为环绕通知
按照上面声明aspect的方式,将其中pointcut和通知的内容改为如下:
就可以将countPrograms(int id)
方法的参数传递到通知中,从而使用切面来统计各个节目的表演次数。
⚠️:在一个aop:aspect标签中,不能声明多个aop:pointcut,可以抽象出一个aop:pointcut,其它的不能进行抽象,可以使用pointcut属性进行声明
eg:
和注解方式类似,为接口的实现类引入新功能,可以在上述aop:aspect注解方式内如下声明:
Spring aop是基于动态代理实现的,无法把通知应用于对象的创建过程。
AspectJ与之独立,可以织入到任何java应用中,包括Spring应用。
使用AspectJ声明切面如下:
public aspect AspectJAspect {
private Performance performance;
public void setPerformance(Performance performance) {
this.performance = performance;
}
pointcut performance() : execution(* perform(..));
}
可以借助Spring的IOC特性将AspectJ依赖的其它类注入到其中。若想这样做,需要将切面注入为Spring bean。
将AspectJ切面声明为bean,需要使用factory-method
属性:
Spring bean由Spring容器初始化;AspectJ切面由AspectJ在运行期创建。由于Sping不负责创建AspectJ切面,在切面需要被注入时,其可能已经被实例化了。
所有的AspectJ切面都提供了一个静态方法aspectOf()
,该方法返回切面的一个单例,我们通过factory-method
来调用aspectOf()
获取切面实例。