一、AOP相关概念
- Aspect(切面): Aspect 声明类似于 Java 中的类声明,在 Aspect 中会包含着一些 Pointcut 以及相应的 Advice。
- Joint point(连接点):表示在程序中明确定义的点,典型的包括方法调用,对类成员的访问以及异常处理程序块的执行等等,它自身还可以嵌套其它 joint point。
- Pointcut(切点):表示一组 joint point,这些 joint point 或是通过逻辑关系组合起来,或是通过通配、正则表达式等方式集中起来,它定义了相应的 Advice 将要发生的地方。
- Advice(增强):Advice 定义了在 Pointcut 里面定义的程序点具体要做的操作,它通过 before、after 和 around 来区别是在每个 joint point 之前、之后还是代替执行的代码。
- Target(目标对象):织入 Advice 的目标对象.。
- Weaving(织入):将 Aspect 和其他对象连接起来, 并创建 Adviced object 的过程。
更通俗地讲:
- 重复代码就叫做关注点。
- 关注点形成的类,就叫做切面(类)。面向切面编程,就是指对很多功能都有的重复代码抽取,再在运行的时候往业务方法上动态植入"切面类代码"。
- 执行目标对象方法,动态植入切面代码就是切入点。可以通过切入点表达式,指定拦截那些类的那些方法,给指定的类在运行的时候植入切面类代码。
【以一个简单的例子来比喻一下 AOP 中 Aspect, Joint point, Pointcut 与 Advice之间的关系:
假设一下, 从前有一个叫爪哇的小县城, 在一个月黑风高的晚上, 这个县城中发生了命案. 作案的凶手十分狡猾, 现场没有留下什么有价值的线索. 不过万幸的是, 刚从隔壁回来的老王恰好在这时候无意中发现了凶手行凶的过程, 但是由于天色已晚, 加上凶手蒙着面, 老王并没有看清凶手的面目, 只知道凶手是个男性, 身高约七尺五寸. 爪哇县的县令根据老王的描述, 对守门的士兵下命令说: 凡是发现有身高七尺五寸的男性, 都要抓过来审问. 士兵当然不敢违背县令的命令, 只好把进出城的所有符合条件的人都抓了起来。
上面的一个小故事和 AOP 到底有什么对应关系?
首先我们知道, 在 Spring AOP 中 Joint point 指代的是所有方法的执行点, 而 point cut 是一个描述信息, 它修饰的是 Joint point, 通过 point cut, 我们就可以确定哪些 Joint point 可以被织入 Advice. 对应到我们在上面举的例子, 我们可以做一个简单的类比, Joint point 就相当于 爪哇的小县城里的百姓,pointcut 就相当于 老王所做的指控, 即凶手是个男性, 身高约七尺五寸, 而 Advice 则是施加在符合老王所描述的嫌疑人的动作: 抓过来审问.
为什么可以这样类比呢?
Joint point : 爪哇的小县城里的百姓: 因为根据定义, Joint point 是所有可能被织入 Advice 的候选的点, 在 Spring AOP中, 则可以认为所有方法执行点都是 Joint point. 而在我们上面的例子中, 命案发生在小县城中, 按理说在此县城中的所有人都有可能是嫌疑人.
Pointcut :男性, 身高约七尺五寸: 我们知道, 所有的方法(joint point) 都可以织入 Advice, 但是我们并不希望在所有方法上都织入 Advice, 而 Pointcut 的作用就是提供一组规则来匹配joinpoint, 给满足规则的 joinpoint 添加 Advice. 同理, 对于县令来说, 他再昏庸, 也知道不能把县城中的所有百姓都抓起来审问, 而是根据凶手是个男性, 身高约七尺五寸, 把符合条件的人抓起来. 在这里 凶手是个男性, 身高约七尺五寸 就是一个修饰谓语, 它限定了凶手的范围, 满足此修饰规则的百姓都是嫌疑人, 都需要抓起来审问.
Advice :抓过来审问, Advice 是一个动作, 即一段 Java 代码, 这段 Java 代码是作用于 point cut 所限定的那些 Joint point 上的. 同理, 对比到我们的例子中, 抓过来审问 这个动作就是对作用于那些满足 男性, 身高约七尺五寸 的爪哇的小县城里的百姓.
Aspect::Aspect 是 point cut 与 Advice 的组合, 因此在这里我们就可以类比: “根据老王的线索, 凡是发现有身高七尺五寸的男性, 都要抓过来审问” 这一整个动作可以被认为是一个 Aspect。】
AOP中的Joinpoint可以有多种类型:构造方法调用,字段的设置和获取,方法的调用,方法的执行,异常的处理执行,类的初始化。也就是说在AOP的概念中我们可以在上面的这些Joinpoint上织入我们自定义的Advice,但是在Spring中却没有实现上面所有的joinpoint,确切的说,Spring只支持方法执行类型的Joinpoint。
通知Advice的类型:
- before advice, 在 join point 前被执行的 advice. 虽然 before advice 是在 join point 前被执行, 但是它并不能够阻止 join point 的执行, 除非发生了异常(即我们在 before advice 代码中, 不能人为地决定是否继续执行 join point 中的代码)
- after return advice, 在一个 join point 正常返回后执行的 advice
- after throwing advice, 当一个 join point 抛出异常后执行的 advice
- after(final) advice, 无论一个 join point 是正常退出还是发生了异常, 都会被执行的 advice.
- around advice, 在 join point 前和 joint point 退出后都执行的 advice。这个是最常用的 advice。
在Spring AOP中目前只有执行方法这一个连接点(因为Spring基于动态代理,所以Spring只支持方法连接点。这与一些其他的AOP框架是不同的,例如AspectJ和JBoss,除了方法切点,它们还提供了字段和构造器接入点。Spring缺少对字段连接点的支持,无法让我们创建细粒度的通知,例如拦截对象字段的修改。而且它不支持构造器连接点,我们就无法在bean创建时应用通知。但是方法拦截可以满足绝大部分的需求。如果需要方法拦截之外的连接点拦截功能,那么我 们可以利用Aspect来补充Spring AOP的功能)。
二、实现AOP
1. 引入依赖
<!-- aop 依赖--> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-aop</artifactId> <version>4.3.23.RELEASE</version> </dependency> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjrt</artifactId> <version>1.9.2</version> </dependency> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.9.2</version> </dependency> <dependency> <groupId>cglib</groupId> <artifactId>cglib</artifactId> <version>3.3.0</version> <!-- 否则报错 Unable to process Jar entry [module-info.class] from Jar [jar:file:/E:/englishPath/respority/org/ow2/asm/asm/7.1/asm-7.1.jar!/] for annotations --> <exclusions> <exclusion> <groupId>org.ow2.asm</groupId> <artifactId>asm</artifactId> </exclusion> </exclusions> </dependency>
2. 业务类
import org.springframework.stereotype.Service; @Service public class BusinessService { public void testAop(){ System.out.println("aop business test"); } }
3. 切面
import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Pointcut; import org.springframework.stereotype.Component; /** * 1. 创建一个切片类(用@Aspect注解) * 2. 定义好切入点(用@Pointcut注解) * 3. 定义好通知类型 */ @Component //注入到ioc容器 @Aspect //声明式一个切片类 public class AopDemo { @Pointcut("execution(* *com.linhw.spring.service.BusinessService.testAop(..))") public void performance(){} @Around("performance()") public void watchPerformance(ProceedingJoinPoint jp) throws Throwable { //在方法之前执行 System.out.println(" before the method is invoked"); jp.proceed();//控制权交给目标方法 //在方法之后执行 System.out.println(" after the method is invoked"); } }
在配置类上添加 @EnableAspectJAutoProxy//启动AspectJ自动代理 。
可以发现在调用业务类的testAop前后会输出切面类的两个输出文本。
三、@Pointcut表达式语法
*: 匹配任意数量的字符
+:匹配制定数量的类及其子类
..:一般用于匹配任意数量的子包或参数
&&:与操作符
||:或操作符
!:非操作符
1. 方法描述匹配
execution( 方法修饰符 方法返回值 方法所属类 匹配方法名 ( 方法中的形参表 ) 方法申明抛出的异常 )
其中红色字体的部分时不能省略的,各部分都支持通配符 “*” 来匹配全部。
使用“execution(方法表达式)”匹配方法执行。
例:* cn.javass..IPointcutService.*() cn.javass包及所有子包下IPointcutService接口中的任何无参方法
2. 方法参数匹配
args():用于匹配当前执行的方法传入的参数为指定类型的执行方法;
@args:用于匹配当前执行的方法传入的参数持有指定注解的执行;
使用“args(参数类型列表)”匹配当前执行的方法传入的参数为指定类型的执行方法;注意是匹配传入的参数类型,不是匹配方法签名的参数类型;参数类型列表中的参数必须是类型全限定名,通配符不支持;args属于动态切入点,这种切入点开销非常大,非特殊情况最好不要使用;
例:args (java.io.Serializable,..) 任何一个以接受“传入参数类型为 java.io.Serializable” 开头,且其后可跟任意个任意类型的参数
使用“@args(注解列表)”匹配当前执行的方法传入的参数持有指定注解的执行;注解类型也必须是全限定类型名。
例:@args (cn.javass.spring.chapter6.Secure) 任何一个只接受一个参数的方法,且方法运行时传入的参数持有注解 cn.javass.spring.chapter6.Secure;动态切入点,类似于arg指示符。
3. 当前AOP代理对象类型匹配
this():用于匹配当前AOP代理对象类型的执行方法;注意是AOP代理对象的类型匹配,这样就可能包括引入接口也类型匹配;
使用“this(类型全限定名)”匹配当前AOP代理对象类型的执行方法;注意是AOP代理对象的类型匹配,这样就可能包括引入接口方法也可以匹配;注意this中使用的表达式必须是类型全限定名,不支持通配符。
例:this(cn.javass.spring.chapter6.service.IIntroductionService) 当前AOP对象实现了 IIntroductionService接口的任何方法也可能是引入接口
4. 目标类匹配
target():用于匹配当前目标对象类型的执行方法;注意是目标对象的类型匹配,这样就不包括引入接口也类型匹配;注意target中使用的表达式必须是类型全限定名,不支持通配符。
@target:用于匹配当前目标对象类型的执行方法,其中目标对象持有指定的注解;
within():用于匹配指定类型内的方法执行;
@within:用于匹配所以持有指定注解类型内的方法;
使用“target(类型全限定名)”匹配当前目标对象类型的执行方法;注意是目标对象的类型匹配,这样就不包括引入接口也类型匹配;注意target中使用的表达式必须是类型全限定名,不支持通配符。
例:target(cn.javass.spring.chapter6.service.IPointcutService) 当前目标对象(非AOP对象)实现了 IPointcutService接口的任何方法
使用“@target(注解类型)”匹配当前目标对象类型的执行方法,其中目标对象持有指定的注解;注解类型也必须是全限定类型名
例:@target (cn.javass.spring.chapter6.Secure) 任何目标对象持有Secure注解的类方法,必须是在目标对象上声明这个注解,在接口上声明的对它不起作用。
使用“within(类型表达式)”匹配指定类型内的方法执行。
例:within(cn.javass..*) cn.javass包及子包下的任何方法执行
使用“@within(注解类型)”匹配所以持有指定注解类型内的方法;注解类型也必须是全限定类型名。
例:@within (cn.javass.spring.chapter6.Secure) 任何目标对象对应的类型持有Secure注解的类方法,必须是在目标对象上声明这个注解,在接口上声明的对它不起作用。
5. 标有此注解的方法匹配
@annotation:用于匹配当前执行方法持有指定注解的方法;
使用“@annotation(注解类型)”匹配当前执行方法持有指定注解的方法;注解类型也必须是全限定类型名;
例:@annotation(cn.javass.spring.chapter6.Secure ) 当前执行方法上持有注解 cn.javass.spring.chapter6.Secure将被匹配
6. 匹配特定名称的bean对象
bean():Spring AOP扩展的,AspectJ没有对于指示符,用于匹配特定名称的Bean对象的执行方法;
使用“bean(Bean id或名字通配符)”匹配特定名称的Bean对象的执行方法;Spring AOP扩展的,在AspectJ中无相应概念。
例:bean(*Service) 匹配所有以Service命名(id或name)结尾的Bean。
AspectJ切入点支持的切入点指示符还有: call、get、set、preinitialization、staticinitialization、initialization、handler、adviceexecution、withincode、cflow、cflowbelow、if、@this、@withincode;但Spring AOP目前不支持这些指示符,使用这些指示符将抛出IllegalArgumentException异常。这些指示符Spring AOP可能会在以后进行扩展。
四、通知参数
如果想获取被被通知方法参数并传递给通知方法,该如何实现呢?接下来我们将介绍两种获取通知参数的方式。
1. 使用JoinPoint获取
Spring AOP提供使用org.aspectj.lang.JoinPoint类型获取连接点数据,任何通知方法的第一个参数都可以是JoinPoint(环绕通知是ProceedingJoinPoint,JoinPoint子类),当然第一个参数位置也可以是JoinPoint.StaticPart类型,这个只返回连接点的静态部分。
(1) JoinPoint:提供访问当前被通知方法的目标对象、代理对象、方法参数等数据。
package org.aspectj.lang; import org.aspectj.lang.reflect.SourceLocation; public interface JoinPoint { String toString(); //连接点所在位置的相关信息 String toShortString(); //连接点所在位置的简短相关信息 String toLongString(); //连接点所在位置的全部相关信息 Object getThis(); //返回AOP代理对象 Object getTarget(); //返回目标对象 Object[] getArgs(); //返回被通知方法参数列表 Signature getSignature(); //返回当前连接点签名 SourceLocation getSourceLocation();//返回连接点方法所在类文件中的位置 String getKind(); //连接点类型 StaticPart getStaticPart(); //返回连接点静态部分 }
(2) ProceedingJoinPoint:用于环绕通知,使用proceed()方法来执行目标方法。
public interface ProceedingJoinPoint extends JoinPoint { public Object proceed() throws Throwable; public Object proceed(Object[] args) throws Throwable; }
(3) JoinPoint.StaticPart:提供访问连接点的静态部分,如被通知方法签名、连接点类型等。
public interface StaticPart { Signature getSignature(); //返回当前连接点签名 String getKind(); //连接点类型 int getId(); //唯一标识 String toString(); //连接点所在位置的相关信息 String toShortString(); //连接点所在位置的简短相关信息 String toLongString(); //连接点所在位置的全部相关信息 }
示例:使用如下方式在通知方法上声明,必须是在第一个参数,然后使用jp.getArgs()就能获取到被通知方法参数:
@Before(value="execution(* sayBefore(*))") public void before(JoinPoint jp) {} @Before(value="execution(* sayBefore(*))") public void before(JoinPoint.StaticPart jp) {}
2. 自动获取:通过切入点表达式可以将相应的参数自动传递给通知方法,例如前边章节讲过的返回值和异常是如何传递给通知方法的。
在Spring AOP中,除了execution和bean指示符不能传递参数给通知方法,其他指示符都可以将匹配的相应参数或对象自动传递给通知方法。
示例:
@Before(value="execution(* test(*)) && args(param)", argNames="param") public void before1(String param) { System.out.println("===param:" + param); }
参考博客: