目录
- 一、概念
- 二、示例代码
- 三、切点
- 1、execution
- 2、within
- 3、this
- 4、target
- 5、args
- 6、@annotation
- 7、@within
- 8、@target
- 9、@args
- 10、组合切点表达式
- 11、在@Before注解中使用自定义的切入点表达式,以及切入点方法
- 12、获取指定类型的真实对象
- 四、通知
- 1、@Around注解的通知方法中调用proceed()方法之前的代码
- 2、@Before
- 3、连接点方法
- 4、@AfterReturning
- 5、@AfterThrowing
- 6、@After
- 7、@Around注解的通知方法中调用proceed()方法之后的代码
- 五、注解使用说明
- 六、参考资料
一、概念
关键名词:
- 目标对象:需要被增强的对象
- 连接点:需要被增强的方法
- 切面:添加@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;
}
五、注解使用说明
六、参考资料
- spring中文官网