实用指南:Spring AOP面向切面的底层原理、注解、切入点表达式、连接点获取方法名参数值等
Spring AOP注解原理与实例
AOP注解
- @Before():不管方法是否执行成功,都会执行
- @AfterReturning():方法执行成功,返回结果才会执行
- @AfterThrowing():方法执行失败,抛出异常才会执行
- @After():不管方法是否执行成功,都会执行
执行顺序
- @Before注解:先于方法之前,执行,最先执行。
- 被代理类的方法执行
- @AfterReturning:方法返回结果之后,执行
- @AfterThrowing:方法返回结果之后,执行。与@AfterReturning互斥,同一时间只能执行一个,要么成功,要么时报。
- @After注解:方法结果返回之后,执行,最后执行。
通知方法执行顺序
- 正常链路:前置通知->目标方法->返回通知->后置通知
@Before->目标方法->@AfterReturning
->@After - 异常链路:前置通知->目标方法->异常通知->后置通知
@Before->目标方法->@AfterThrowing
->@After
切入点表达式
execution(方法全签名)
@Before(“execution(方法全签名)”)
- 方法的全签名:
方法返回值类型
+被代理对象的接口类名路径
+方法名(参数类型)
; - 示例:
public int com.ssg.aop.service.MathCalculator.add(int,int) throws Exception
完整写法
@Component
@Aspect
public class MathCalculatorAop
{
@Before("execution(public int com.ssg.aop.service.MathCalculator.add(int,int))")
public void startLog(){
System.out.println("[AOP 切面]:@Before 方法之前,MathCalculatorAop.startLog()");
}
}
简写
- 只需要保留,
方法返回值类型
,参数类型
即可。 - 方法名称,可以通过
*
号代替。 *
号,表示所有方法都会执行。(..)
,参数类型写两个点,表示多个参数,任意类型。- 最简单的写法:
* * (..)
,匹配任意方法,不能这样写!
@Component
@Aspect
public class MathCalculatorAop
{
@AfterReturning("execution(int add(int,int))")
public void returnLog(){
System.out.println("[AOP 切面]:@AfterReturning 方法返回值");
}
@AfterThrowing("execution(int *(int,int))")
public void throwLog(){
System.out.println("[AOP 切面]:@AfterThrowing 方法异常");
}
}
args(方法参数类型):
- 同时存在两个@Before注解,@Before(“args()”)切入点表达式优先级高于@Before(“execution()”) ,最终
@Before("args")先执行
。
package com.ssg.aop.aspect;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Component
@Aspect
public class MathCalculatorAop
{
@Before("execution(int add(int,int))")
public void startLog(){
System.out.println("[AOP 切面]:@Before,前置");
}
/**
* args(方法参数类型)
* args(int,int):当参数类型为int,int时,切面方法执行
*/
@Before("args(int,int)")
public void argsLog(){
System.out.println("[AOP 切面] [切入点表达式]:args");
}
}
结果预览:
[AOP 切面] [切入点表达式]:args
[AOP 切面]:@Before,前置
[AOP 切面]:@AfterReturning,返回
[AOP 切面]:@After,后置
步骤
- 导入AOP依赖。
- 编写切面Aspect。
- 编写通知方法。
- 指定切入点表达式。
- 测试AOP动态织入。
1. 导入AOP依赖。
- pom文件导入依赖。
org.springframework.boot
spring-boot-starter-aop
2. 编写Aspect切面类、3.编写通知方法、4.指定切入点表达式
- 注意:需要导入@Component交由容器进行管理,否则不生效
package com.ssg.aop.aspect;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Component
@Aspect
public class MathCalculatorAop
{
@Before("execution(public int com.ssg.aop.service.MathCalculator.add(int,int))")
public void startLog(){
System.out.println("[AOP 切面]:@Before 方法之前,MathCalculatorAop.startLog()");
}
@AfterReturning("execution(int add(int,int))")
public void returnLog(){
System.out.println("[AOP 切面]:@AfterReturning 方法返回值");
}
@AfterThrowing("execution(int *(int,int))")
public void throwLog(){
System.out.println("[AOP 切面]:@AfterThrowing 方法异常");
}
@After("execution(int add(int,int))")
public void endLog(){
System.out.println("[AOP 切面]:@After 方法之后");
}
}
5.测试AOP动态织入
package com.ssg.aop;
import com.ssg.aop.service.MathCalculator;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
public class AopTest
{
@Autowired
MathCalculator mathCalculator;
@Test
public void test01(){
System.out.println("mathCalculator = " + mathCalculator);
int add = mathCalculator.add(1, 2);
System.out.println("@Test = " + add);
}
}
- 结果:
mathCalculator = com.ssg.aop.service.impl.MathCalculatorImpl@5b000fe6
[AOP 切面]:@Before 方法之前,MathCalculatorAop.startLog()
方法执行结果 = 3
[AOP 切面]:@AfterReturning 方法返回值
[AOP 切面]:@After 方法之后
@Test = 3
JoinPoint连接点
获取方法名称和参数值
@Component
@Aspect
public class MathCalculatorAop
{
@Before("execution(int add(int,int))")
public void startLog3(JoinPoint joinPoint){
// 顶级类型
// 强制类型转换,默认的得到的对象是最顶级的,我们需要用到它的子类中的部分方法
Signature signature = joinPoint.getSignature();
// 类型的全签名
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
// 方法名
String name = methodSignature.getName();
// 目标方法传递的参数值
Object[] args = joinPoint.getArgs();
System.out.println("[AOP 切面]:@Before3,前置 连接点 ,方法名:" + name + "参数值:" + Arrays.toString(args));
}
}
获取返回值
package com.ssg.aop.aspect;
import jdk.jshell.MethodSnippet;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import java.util.Arrays;
@Component
@Aspect
public class MathCalculatorAop
{
/**
* @AfterReturning 返回通知注解,存在一个returning参数,用于接收返回值。
* 1. 在切面返回值通知方法中,添加一个参数:Object result
*/
@AfterReturning(value = "execution(int add(int,int))", returning = "result")
public void returnLog2(JoinPoint joinPoint,Object result){
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
String name = signature.getName();
System.out.println("[AOP 切面]:["+name+"] @AfterReturning,返回值" + result);
}
}
获取异常信息
package com.ssg.aop.aspect;
import jdk.jshell.MethodSnippet;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import java.util.Arrays;
@Component
@Aspect
public class MathCalculatorAop
{
/**
* @AfterThrowing 异常通知注解,存在一个throwing参数,用于接受返回的异常信息。
* 1. 在切面异常通知方法中,添加一个参数:Exception e
* 2. throwing = "e",获取目标方法抛出异常
*/
@AfterThrowing(value = "execution(int *(int,int))", throwing = "e")
public void throwLog2(JoinPoint joinPoint, Exception e) {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
String name = signature.getName();
System.out.println("[AOP 切面]:[" + name + "] @AfterThrowing,异常信息:" + e.getMessage());
}
@Pointcut 简化切入点表达式
- @Pointcut用于简化切入点表达式
- 其放置在方法上面,方法名称作为其他AOP注解的引用。
- 针对需要返回值的场景,需要在AOP注解中,使用参数
pointcut=""
来引用简化的切入点表达式。
常规写法,不要求返回值参数等
package com.ssg.aop.aspect;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import java.util.Arrays;
@Component
@Aspect
public class MathCalculatorAopPointcut
{
@Pointcut("execution(public int com.ssg.aop.service.impl.SummerServiceImpl.add(int,int))")
public void pointCutExecution(){
}
@Pointcut("args(int,int)")
public void pointCutArgs(){
}
/**
* args(方法参数类型)
* args(int,int):当参数类型为int,int时,切面方法执行
* 同一时间只能存在一个@Before注解,后者覆盖前者。
*/
@Before("pointCutArgs()")
public void argsLog() {
System.out.println("[AOP 切面] [切入点表达式]:args");
}
@Before("pointCutExecution()")
public void startLog(JoinPoint joinPoint) {
// 顶级类型
// 强制类型转换,默认的得到的对象是最顶级的,我们需要用到它的子类中的部分方法
Signature signature = joinPoint.getSignature();
// 类型的全签名
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
// 方法名
String name = methodSignature.getName();
// 目标方法传递的参数值
Object[] args = joinPoint.getArgs();
System.out.println("[AOP 切面]:@Before3,前置 连接点 ,方法名:" + name + "参数值:" + Arrays.toString(args));
}
}
需要返回值参数、异常信息等
@AfterReturning(pointcut = "", returning = "")
@AfterReturning(pointcut = "pointCutExecution()", returning = "result")
package com.ssg.aop.aspect;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import java.util.Arrays;
@Component
@Aspect
public class MathCalculatorAopPointcut
{
@Pointcut("execution(public int com.ssg.aop.service.impl.SummerServiceImpl.add(int,int))")
public void pointCutExecution(){
}
@Pointcut("args(int,int)")
public void pointCutArgs(){
}
/**
* @AfterReturning 返回通知注解,存在一个returning参数,用于接收返回值。
* 1. 在切面返回值通知方法中,添加一个参数:Object result
* 2. returning = "result",获取目标方法返回值
*/
@AfterReturning(pointcut = "pointCutExecution()", returning = "result")
public void returnLog(JoinPoint joinPoint, Object result) {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
String name = signature.getName();
System.out.println("[AOP 切面]:[" + name + "] @AfterReturning,返回值" + result);
}
/**
* @AfterThrowing 异常通知注解,存在一个throwing参数,用于接受返回的异常信息。
* 1. 在切面异常通知方法中,添加一个参数:Exception e
* 2. throwing = "e",获取目标方法抛出异常
*/
@AfterThrowing(pointcut = "pointCutExecution()", throwing = "e")
public void throwLog(JoinPoint joinPoint, Exception e) {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
String name = signature.getName();
System.out.println("[AOP 切面]:[" + name + "] @AfterThrowing,异常信息:" + e.getMessage());
}
@After("pointCutExecution()")
public void endLog() {
System.out.println("[AOP 切面]:@After,后置");
}
}
多切面执行顺序
- 存在一种情况,对同一个目标类,有多个AOP切面类,如何确定他们的执行顺序呢?
- 默认情况下,根据切面类的字母顺序A-Z执行。
- 或者使用@Order(0)注解,数字越小的越先执行。
- 先执行的切面代理对象会将后执行的切面代理对象包裹。先切面对象执行完前置后,等后切面对象执行完全部通知方法后,再执行后置通知。
@Component
@Aspect
@Order(1)
public class MathCalculatorAop
{
}
环绕通知
@Around 是 Spring-AOP(或 AspectJ)里功能最强大的通知类型,它把“目标方法”整个包裹起来,让你在方法执行前、执行后、甚至代替原方法去做任何事情。
环绕通知
@Around
,相当于将@Before
前置通知、@AfterReturning
返回通知、@AfterThrowing
异常通知、@After
后置通知,集成到了一起。一个更比4个强。
注意:在写环绕同通知的异常通知时,因为我们捕获了通知,最后都需要再重新抛出异常
防止存在多个切面类时,环绕通知不抛出异常,他的外层切面类对象,会将其当作返回通知进行处理,导致存在异常情况。实际异常但却返回。
环绕通知需要手动抛出异常。
环绕通知固定写法
- 方法执行:
joinPoint.proceed(args);
,可以不加参数。 - try-cache:必须手动抛出异常。切记切记。
package com.ssg.aop.aspect;
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;
import java.util.Arrays;
@Component
@Aspect
public class MathCalculatorAopAround
{
@Pointcut("execution(int com.ssg.aop.service.*.add(int,int))")
public void pointCut() {
}
/**
* 环绕通知
* @param joinPoint
* @return
* @throws Throwable 环绕通知需要手动抛出异常
*/
@Around("pointCut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
// 获取参数信息
Object[] args = joinPoint.getArgs();
System.out.println("环绕通知 - 前置通知 - 参数信息:" + Arrays.toString(args));
Object proceed = null;
try {
proceed = joinPoint.proceed(args);
System.out.println("环绕通知 - 返回通知 - 返回信息:" + proceed);
} catch (Exception e) {
System.out.println("环绕通知 - 异常通知 - 异常信息" + e.getMessage());
// 注意:在写环绕同通知的异常通知时,因为我们捕获了通知,最后都需要再重新抛出异常
// 防止存在多个切面类时,环绕通知不抛出异常,他的外层切面类对象,会将其当作返回通知进行处理,导致存在异常情况。实际异常但却返回。
// 环绕通知需要手动抛出异常。
throw e;
} finally {
System.out.println("环绕通知 - 后置通知 - ");
}
return proceed;
}
}
图解:
多个切面类为什么一定要求环绕切面类要重新抛出异常?
- 当我们的环绕切面类,执行方法抛出异常时。
- 如果不抛出异常,则其外层切面类,会将其当作返回通知进行处理。
- 也就导致实际异常,结果做了返回处理。
底层原理
- 增强器链:切面中的所有通知方法其实是增强器,他们被组织成一个链路放到集合中,目标方法真正执行前后,会去增强器链中执行哪些需要提前执行的方法。
AOP的底层原理是什么?
- Spring会为每个被切面切入的组件创建代理对象(Spring CGLIB 创建的代理对象,无视接口,没有接口也能创建代理对象)。
- 代理对象中保存了,切面类里面所有通知方法构成的增强器链。
- 目标方法执行时,会先执行增强器链中拿到需要提前执行的通知方法去执行。
代理对象信息
不使用切面的情况下
- 由容器直接注入mathCalculator目标对象,结果输出为目标对象的信息。
使用切面的情况下
- 由容器直接注入mathCalculator目标对象。
- 由于使用了切面类,对目标方法进行了代理。
- 结果输出为代理对象的信息。
- 代理对象执行了增强器链相关操作。