自学内容网 自学内容网

SpringAOP使用方法—学习文档

一、概念

关键名词:

  • 目标对象:需要被增强的对象
  • 连接点:需要被增强的方法
  • 切面:添加@Aspect的类,一个类里面可以有多个切点和多个通知
  • 切点:可以针对注解或者类路径进行查找
  • 通知:在切点上执行增强逻辑
  • 路径表达式:匹配连接点的表达式
  • 织入:将切面和目标对象连接起来

画图表示:

在这里插入图片描述

代码表示:

在这里插入图片描述

二、示例代码

情况说明:

在下面目录中的代码片段并不完整,缺失的内容请下载示例代码查看~

百度网盘:

链接:https://pan.baidu.com/s/1onHsCNHWIdfjmAUTg7q8TA?pwd=gyg1

提取码:gyg1

启动说明:

执行主启动类的main方法即可

在这里插入图片描述

启动日志:

spring启动完成,尝试调用TestController类的test方法……
===测试args,目标类名称:TestController、连接点(方法)名称:test
===测试within,目标类名称:TestController、连接点(方法)名称:test
+++测试@Around注解,针对目标类TestServiceImpl的方法test,在执行joinPoint.proceed()方法之前……
+++测试@Before注解,在目标类TestServiceImpl的方法test之前执行……
===测试@annotation,目标类名称:TestServiceImpl、连接点(方法)名称:test
===测试@args,目标类名称:TestServiceImpl、连接点(方法)名称:test
===测试@within,目标类名称:TestServiceImpl、连接点(方法)名称:test
===测试组合切点表达式,目标类名称:TestServiceImpl、连接点(方法)名称:test
===测试execution,目标类名称:TestServiceImpl、连接点(方法)名称:test
===测试直接获取相应对象,目标类名称:TestServiceImpl、连接点(方法)名称:test、类注解值:实现类、方法注解值:方法、方法参数名称:TestEntity、方法参数对应类注解值:实体类
===测试借助切点方法获取相应对象,目标类名称:TestServiceImpl、连接点(方法)名称:test、类注解值:实现类、方法注解值:方法、方法参数名称:TestEntity、方法参数对应类注解值:实体类
===测试通知中使用组合切入点方法,目标类名称:TestServiceImpl、连接点(方法)名称:test
===测试通知中使用组合自定义的切入点表达式,目标类名称:TestServiceImpl、连接点(方法)名称:test
===测试通知中使用组合 自定义的切入点表达式 + 切入点方法,目标类名称:TestServiceImpl、连接点(方法)名称:test
===测试target,目标类名称:TestServiceImpl、连接点(方法)名称:test
===测试this,目标类名称:TestServiceImpl、连接点(方法)名称:test
》》》执行测试方法……
+++测试@AfterReturning注解,在目标类TestServiceImpl的方法test执行正常返回时,方法执行结果:成功
+++测试@After注解,针对目标类TestServiceImpl的方法test,无论是否发生异常,一定会执行……
+++测试@Around注解,针对目标类TestServiceImpl的方法test,在执行joinPoint.proceed()方法之后……

三、切点

注意内容:

/**
 * 通配符说明:
 * .:包和包、包和方法之前的普通连接符号
 * ..:如果用在路径上,代表中间可有有多个任意路径(中间用.分隔的多个路径);如果用在方法参数上,代表不限制参数列表
 * *:指代任意单个单词,可以用在路径(包名、类名、方法名)、修饰符、返回值类型
 * +:匹配任何继承自指定的类,例如:execution(* com.example.Parent+.*(..)):匹配com.example.Parent类及其子类的任何方法。
 */

/**
 * 注意事项:
 * 1、如果在通知方法中使用JoinPoint类型参数,那就一定要放在第1个,否则会导致tomcat启动过程中出现空指针异常,从而启动失败;当然可以选择不放
 */

1、execution

// 测试:execution
// 用途:指定到方法级别
// 通配符:允许使用
// 格式:execution(修饰符 返回值类型 方法路径(参数类型列表))
// 注意:修饰符可以省略,省略代表不限制修饰符
//    @Pointcut("execution(public String com.atguigu.test.controller.TestController.test(..))") // 指定修饰符、返回值类型、方法全路径、任意参数类型
//    @Pointcut("execution(* com.atguigu.test.controller.TestController.test(..))") // 任意修饰符、任意返回值类型、方法全路径、任意参数类型
@Pointcut("execution(* com.atguigu.*.service..test(..))") // 任意修饰符、任意返回值类型、指定包下的所有方法
public void executionPointcut() {}

@Before("executionPointcut()")
public void executionAdvice(JoinPoint joinPoint) {
    String className = joinPoint.getTarget().getClass().getSimpleName();
    String methodName = joinPoint.getSignature().getName();
    System.out.printf("===测试execution,目标类名称:%s、连接点(方法)名称:%s\n",  className, methodName);
}

2、within

// 测试:within
// 用途:指定到类级别
// 通配符:允许使用
// 格式:within(类路径)
@Pointcut("within(com.atguigu.test.controller.TestController)") // 具体类
//    @Pointcut("within(com.atguigu.test.service.impl.*)") // 包中所有类
//    @Pointcut("within(com..test..*)") // 子包中所有类(包名称以com开头,中间包含test即可)
public void withinPointcut() {}

@Before("withinPointcut()")
public void withinAdvice(JoinPoint joinPoint) {
    String className = joinPoint.getTarget().getClass().getSimpleName();
    String methodName = joinPoint.getSignature().getName();
    System.out.printf("===测试within,目标类名称:%s、连接点(方法)名称:%s\n",  className, methodName);
}

3、this

// 测试:this
// 用途:指定接口,捕获接口实现类中方法被调用的情况
// 通配符:禁止使用
// 格式:this(接口路径)
// 注意:接口路径不允许使用通配符类型模式,必须使用类型名称
@Pointcut("this(com.atguigu.test.service.TestService)")
public void thisPointcut() {}

@Before("thisPointcut()")
public void thisAdvice(JoinPoint joinPoint) {
    String className = joinPoint.getTarget().getClass().getSimpleName();
    String methodName = joinPoint.getSignature().getName();
    System.out.printf("===测试this,目标类名称:%s、连接点(方法)名称:%s\n",  className, methodName);
}

4、target

// 测试:target
// 用途:指定接口,捕获接口实现类中方法被调用的情况
// 通配符:禁止使用
// 格式:target(接口路径)
// 注意:接口路径不允许使用通配符类型模式,必须使用类型名称
@Pointcut("this(com.atguigu.test.service.TestService)")
public void targetPointcut() {}

@Before("targetPointcut()")
public void targetAdvice(JoinPoint joinPoint) {
    String className = joinPoint.getTarget().getClass().getSimpleName();
    String methodName = joinPoint.getSignature().getName();
    System.out.printf("===测试target,目标类名称:%s、连接点(方法)名称:%s\n",  className, methodName);
}

5、args

// 测试:args
// 用途:指定参数类型
// 格式:args(参数类型列表)
@Pointcut("within(com.atguigu.test.controller.TestController) && args(String)")
public void argsPointcut() {}

@Before("argsPointcut()")
public void argsAdvice(JoinPoint joinPoint) {
    String className = joinPoint.getTarget().getClass().getSimpleName();
    String methodName = joinPoint.getSignature().getName();
    System.out.printf("===测试args,目标类名称:%s、连接点(方法)名称:%s\n",  className, methodName);
}

6、@annotation

// 测试:@annotation
// 用途:指定方法上的注解,添加该注解的方法都是连接点方法
// 通配符:禁止使用
// 格式:@annotation(注解路径)
// 注意:注解路径不允许使用通配符类型模式,必须使用类型名称
@Pointcut("@annotation(com.atguigu.test.annotation.TestAnnotation)")
public void atAnnotationPointcut() {}

@Before("atAnnotationPointcut()")
public void atAnnotationAdvice(JoinPoint joinPoint) {
    String className = joinPoint.getTarget().getClass().getSimpleName();
    String methodName = joinPoint.getSignature().getName();
    System.out.printf("===测试@annotation,目标类名称:%s、连接点(方法)名称:%s\n",  className, methodName);
}

7、@within

// 测试:@within
// 用途:指定类上的注解,添加该注解的类中所有方法都是连接点方法
// 通配符:禁止使用
// 格式:@within(注解路径)
// 注意:注解路径不允许使用通配符类型模式,必须使用类型名称
@Pointcut("@within(com.atguigu.test.annotation.TestAnnotation)")
public void atWithinPointcut() {}

@Before("atWithinPointcut()")
public void atWithinAdvice(JoinPoint joinPoint) {
    String className = joinPoint.getTarget().getClass().getSimpleName();
    String methodName = joinPoint.getSignature().getName();
    System.out.printf("===测试@within,目标类名称:%s、连接点(方法)名称:%s\n",  className, methodName);
}

8、@target

// 测试:@target
// 警告:没搞懂怎么用,暂时放弃了

9、@args

// 测试:@args
// 用途:指定参数所属类上的注解,从而确定连接点方法
// 格式:@args(参数类型添加的注解路径)
@Pointcut("within(com.atguigu.test.service..*) && @args(com.atguigu.test.annotation.TestAnnotation, ..)")
public void atArgsPointcut() {}

@Before("atArgsPointcut()")
public void atArgsAdvice(JoinPoint joinPoint) {
    String className = joinPoint.getTarget().getClass().getSimpleName();
    String methodName = joinPoint.getSignature().getName();
    System.out.printf("===测试@args,目标类名称:%s、连接点(方法)名称:%s\n",  className, methodName);
}

10、组合切点表达式

// 测试:组合切点(Pointcut)表达式
// 格式:支持通过 &&、||、! 来组合 pointcut 表达式
@Pointcut("executionPointcut() || atAnnotationPointcut() || atWithinPointcut()")
public void combinationPointcut() {}

@Before("combinationPointcut()")
public void combinationAdvice(JoinPoint joinPoint) {
    String className = joinPoint.getTarget().getClass().getSimpleName();
    String methodName = joinPoint.getSignature().getName();
    System.out.printf("===测试组合切点表达式,目标类名称:%s、连接点(方法)名称:%s\n",  className, methodName);
}

11、在@Before注解中使用自定义的切入点表达式,以及切入点方法

// 说明:在@Before注解中使用切入点方法
@Before("executionPointcut() || atAnnotationPointcut() || atWithinPointcut()")
public void mix1PointcutAdvice(JoinPoint joinPoint) {
    String className = joinPoint.getTarget().getClass().getSimpleName();
    String methodName = joinPoint.getSignature().getName();
    System.out.printf("===测试通知中使用组合切入点方法,目标类名称:%s、连接点(方法)名称:%s\n",  className, methodName);
}


// 说明:在@Before注解中使用自定义的切入点表达式
@Before("execution(* com.atguigu.*.service..test(..)) || @annotation(com.atguigu.test.annotation.TestAnnotation)")
public void mix2PointcutAdvice(JoinPoint joinPoint) {
    String className = joinPoint.getTarget().getClass().getSimpleName();
    String methodName = joinPoint.getSignature().getName();
    System.out.printf("===测试通知中使用组合自定义的切入点表达式,目标类名称:%s、连接点(方法)名称:%s\n",  className, methodName);
}


// 说明:在@Before注解中使用自定义的切入点表达式 + 切入点方法
@Before("execution(* com.atguigu.*.service..test(..)) || atAnnotationPointcut() || atWithinPointcut()")
public void mix3PointcutAdvice(JoinPoint joinPoint) {
    String className = joinPoint.getTarget().getClass().getSimpleName();
    String methodName = joinPoint.getSignature().getName();
    System.out.printf("===测试通知中使用组合 自定义的切入点表达式 + 切入点方法,目标类名称:%s、连接点(方法)名称:%s\n",  className, methodName);
}

12、获取指定类型的真实对象

@Before(value = "getObjPointcut(classAnnotation, methodAnnotation, entity, entityAnnotation)")
public void getObjAdvice(JoinPoint joinPoint, TestAnnotation classAnnotation, TestAnnotation methodAnnotation, TestEntity entity, TestAnnotation entityAnnotation) {
    String className = joinPoint.getTarget().getClass().getSimpleName();
    String methodName = joinPoint.getSignature().getName();
    System.out.printf("===测试借助切点方法获取相应对象,目标类名称:%s、连接点(方法)名称:%s、类注解值:%s、方法注解值:%s、方法参数名称:%s、方法参数对应类注解值:%s\n",  className, methodName, classAnnotation.value(), methodAnnotation.value(), entity.getClass().getSimpleName(), entityAnnotation.value());
}

@Before(value = "@within(classAnnotation) && @annotation(methodAnnotation) && args(entity, ..) && @args(entityAnnotation, ..)")
public void getObj2Advice(JoinPoint joinPoint, TestAnnotation classAnnotation, TestAnnotation methodAnnotation, TestEntity entity, TestAnnotation entityAnnotation) {
    String className = joinPoint.getTarget().getClass().getSimpleName();
    String methodName = joinPoint.getSignature().getName();
    System.out.printf("===测试直接获取相应对象,目标类名称:%s、连接点(方法)名称:%s、类注解值:%s、方法注解值:%s、方法参数名称:%s、方法参数对应类注解值:%s\n",  className, methodName, classAnnotation.value(), methodAnnotation.value(), entity.getClass().getSimpleName(), entityAnnotation.value());
}

四、通知

注意内容:

/**
 * 通配符说明:
 * .:包和包、包和方法之前的普通连接符号
 * ..:如果用在路径上,代表中间可有有多个任意路径(中间用.分隔的多个路径);如果用在方法参数上,代表不限制参数列表
 * *:指代任意单个单词,可以用在路径(包名、类名、方法名)、修饰符、返回值类型
 * +:匹配任何继承自指定的类,例如:execution(* com.example.Parent+.*(..)):匹配com.example.Parent类及其子类的任何方法。
 */

/**
 * 注意事项:
 * 1、如果在通知方法中使用JoinPoint类型参数,那就一定要放在第1个,否则会导致tomcat启动过程中出现空指针异常,从而启动失败;当然可以选择不放
 * 2、每一种类型的通知都列出了1个,其实针对同一个切点方法,也可以添加多次同类型通知,这些通知都会执行
 * 3、始终使用符合你要求的最不强大的 advice 形式。例如,如果 before advice 足以满足你的需要,就不要使用 around advice。
 * 4、执行“连接点方法”和“通知方法”都不报错,执行通知的顺序:
 *    @Around注解方法中调用proceed()方法之前
 *    @Before
 *    连接点方法
 *    @AfterReturning
 *    @After
 *    @Around注解方法中调用proceed()方法之后
 * 5、执行“连接点方法”报错,但是执行“通知方法”不报错,执行通知的顺序:
 *    @Around注解方法中调用proceed()方法之前
 *    @Before
 *    连接点方法(报错),往上抛出异常
 *    @AfterThrowing
 *    @After
 *    @Around注解方法,往上抛出异常
 * 6、执行“连接点方法”不报错,但是执行“通知方法”报错,执行通知的顺序:
 *    任意一个通知方法出现异常,将会直接抛出异常,并且不会执行后面的所有通知方法
 */

切点:

// 切点
@Pointcut("execution(* com.atguigu.*.service..test(..))") // 任意修饰符、任意返回值类型、指定包下的所有方法
public void pointcut() {}

1、@Around注解的通知方法中调用proceed()方法之前的代码

// Around通知:在方法运行之前和之后进行工作,并决定何时、如何、甚至是否真正运行该方法
// 说明:1、该方法的第一个参数必须是 ProceedingJoinPoint 类型,从而调用proceed()以执行连接点方法。
//      2、如果不为proceed()方法传递参数,那么该方法底层在调用连接点方法时将会传原始参数给它;
//        如果调用重载的 proceed() 方法,它接受一个参数数组(Object[]),那么该方法底层在调用连接点方法时将使用参数数组(Object[])。
//      3、如果你将 around advice 方法的返回类型声明为 void,那么将总是返回给调用者 null,有效地忽略了任何调用 proceed() 的结果。
//         因此,我们建议 around advice 方法声明一个 Object 的返回类型。该advice方法通常应该返回调用 proceed() 所返回的值,
//         即使底层方法的返回类型为 void。然而,advice可以根据使用情况选择性地返回一个缓存的值、一个封装的值或一些其他的值。
@Around("pointcut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
    // 测试下面算术异常的影响,将导致所有通知方法都不会执行
//        int a = 10 / 0;
    String className = joinPoint.getTarget().getClass().getSimpleName();
    String methodName = joinPoint.getSignature().getName();
    System.out.printf("+++测试@Around注解,针对目标类%s的方法%s,在执行joinPoint.proceed()方法之前……\n",  className, methodName);

2、@Before

// Before通知:在切点匹配的方法执行之前
@Before("pointcut()")
public void before(JoinPoint joinPoint) {
    // JoinPoint对象方法:https://eclipse.dev/aspectj/doc/released/runtime-api/org/aspectj/lang/JoinPoint.html
    String className = joinPoint.getTarget().getClass().getSimpleName();
    String methodName = joinPoint.getSignature().getName();
    System.out.printf("+++测试@Before注解,在目标类%s的方法%s之前执行……\n",  className, methodName);
}

3、连接点方法

// Around通知:在方法运行之前和之后进行工作,并决定何时、如何、甚至是否真正运行该方法
@Around("pointcut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
    ………………
    Object returnValue = joinPoint.proceed();

4、@AfterReturning

// AfterThrowing通知:当切点匹配的方法执行出现异常时
// 说明:1、可以通过异常类型来匹配通知方法,比如我们这里只处理空指针异常(如果不需要指定特殊异常类型,可以使用 Throwable 作为异常类型)
//      2、只接收来自连接点(用户声明的目标方法)本身的异常,而不是来自其他通知方法,比如:添加注解 @After/@AfterReturning 方法的情况
//      3、当一个连接点方法的执行抛出异常退出时,该异常将作为相应的参数值传递给对应处理异常的通知方法
@AfterThrowing(value = "pointcut()", throwing = "ex")
public void executionAdvice(JoinPoint joinPoint, NullPointerException ex) {
    String className = joinPoint.getTarget().getClass().getSimpleName();
    String methodName = joinPoint.getSignature().getName();
    System.out.printf("+++测试@AfterThrowing注解,在目标类%s的方法%s出现空指针异常时执行,异常信息:%s\n", className, methodName, ex.getMessage());
}

5、@AfterThrowing

// AfterThrowing通知:当切点匹配的方法执行出现异常时执行
// 说明:1、可以通过异常类型来匹配通知方法,比如我们这里只处理算术异常(如果不需要指定特殊异常类型,可以使用 Throwable 作为异常类型)
//      2、只接收来自连接点(用户声明的目标方法)本身的异常,,而不是来自附带的 @After / @AfterReturning 方法。
//      3、当一个连接点方法的执行抛出异常退出时,该异常将作为相应的参数值传递给对应处理异常的通知方法
@AfterThrowing(value = "pointcut()", throwing = "ex")
public void executionAdvice(JoinPoint joinPoint, ArithmeticException ex) {
    String className = joinPoint.getTarget().getClass().getSimpleName();
    String methodName = joinPoint.getSignature().getName();
    System.out.printf("+++测试@AfterThrowing注解,在目标类%s的方法%s出现算术异常时执行,异常信息:%s\n", className, methodName, ex.getMessage());
}

6、@After

// After通知:当一个匹配的方法执行退出时,一定会执行该通知
// 将对任何结果、正常返回或从连接点(用户声明的目标方法)抛出的异常进行调用,通常被用于释放资源和类似的目的。
// 类似于try-catch语句中的finally块,这与 @AfterReturning 不同,后者只适用于成功的正常返回。
@After("pointcut()")
public void after(JoinPoint joinPoint) {
    String className = joinPoint.getTarget().getClass().getSimpleName();
    String methodName = joinPoint.getSignature().getName();
    System.out.printf("+++测试@After注解,针对目标类%s的方法%s,无论是否发生异常,一定会执行……\n",  className, methodName);
}

7、@Around注解的通知方法中调用proceed()方法之后的代码

// Around通知:在方法运行之前和之后进行工作,并决定何时、如何、甚至是否真正运行该方法
@Around("pointcut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
    ………………
    // 测试@After通知在执行joinPoint.proceed()方法中执行,而不会受到下面算术异常的影响
//        int a = 10 / 0;
    System.out.printf("+++测试@Around注解,针对目标类%s的方法%s,在执行joinPoint.proceed()方法之后……\n",  className, methodName);
    return returnValue;
}

五、注解使用说明

在这里插入图片描述

六、参考资料


原文地址:https://blog.csdn.net/qq_42449963/article/details/142866334

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!