AOP
- AOP(Aspect Oriented Programming):面向特定方法编程
1. 快速入门
- 导入依赖
spring-boot-starter-aop
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-aop -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
<version>3.2.0</version>
</dependency>
- 编写 AOP 类
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
@Component // 注入到 Spring 容器
@Aspect // AOP 类
@Slf4j
public class TimeAspect {
@Around("execution(* com.axr.service.*.*(..))") // 切入点表达式
public Object recordTime(ProceedingJoinPoint joinPoint) throws Throwable {
// 记录时间
long begin = System.currentTimeMillis();
// 执行方法
Object result = joinPoint.proceed();
// 记录时间
long end = System.currentTimeMillis();
log.info(joinPoint.getSignature() + "方法 {} 执行时间:{}ms", joinPoint.getSignature().getName(), end - begin);
return result;
}
}
当调用 com.axr.service包下的方法时:就会记录方法运行的时间
2. 核心概念
- 连接点: JoinPoint,连接点指的是可以被aop控制的方法。(如上文当中所有的业务方法)
- 通知:Advice,共性功能。
上文代码中是需要统计各个业务方法的执行耗时的,此时我们就需要在这些业务方法运行开始之前,先记录这个方法运行的开始时间,在每一个业务方法运行结束的时候,再来记录这个方法运行的结束时间。
但是在AOP面向切面编程当中,我们只需要将这部分重复的代码逻辑抽取出来单独定义。抽取出来的这一部分重复的逻辑,也就是共性的功能。
- 切入点:PointCut**,匹配连接点的条件,通知仅会在切入点方法执行时被应用。(Around(切入表达式))
- 切面: Aspect,描述通知与切入点的对应关系
- 目标对象:Target,通知所应用的对象
3. 通知
Spring中AOP的通知类型:
- @Around:环绕通知,此注解标注的通知方法在目标方法前、后都被执行
- @Before:前置通知,此注解标注的通知方法在目标方法前被执行
- @After :后置通知,此注解标注的通知方法在目标方法后被执行,无论是否有异常都会执行
- @AfterReturning : 返回后通知,此注解标注的通知方法在目标方法后被执行,有异常不会执行
- @AfterThrowing : 异常后通知,此注解标注的通知方法发生异常后执行
4. 通知顺序
- 不同的切面类当中,默认情况下通知的执行顺序是与切面类的类名字母排序是有关系的
- 可以在切面类上面加上@Order注解,来控制不同的切面类通知的执行顺序
5. 切入点表达式
1. @execution(…)
execution主要根据方法的返回值、包名、类名、方法名、方法参数等信息来匹配,语法为:
execution(访问修饰符? 返回值 包名.类名.?方法名(方法参数) throws 异常?)
其中带?
的表示可以省略的部分
-
访问修饰符:可省略(比如: public、protected)
-
包名.类名: 可省略
-
throws 异常:可省略(注意是方法上声明抛出的异常,不是实际抛出的异常)
@Before("execution(void com.itheima.service.impl.DeptServiceImpl.delete(java.lang.Integer))")
可以使用通配符描述切入点
*
:单个独立的任意符号,可以通配任意返回值、包名、类名、方法名、任意类型的一个参数,也可以通配包、类、方法名的一部分
execution(* com.*.service.*update*(*))
..
:多个连续的任意符号,可以通配任意层级的包,或任意类型、任意个数的参数
execution(* com.syy..DeptSerive.*(..))
2. @annotation
匹配标识有特定注解的方法
实现步骤:
-
编写自定义注解
-
在业务类要做为连接点的方法上添加自定义注解
首先,我们需要定义一个自定义的注解,让我们称其为 @CustomAnnotation
:
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CustomAnnotation {
String value() default "";
}
然后,我们创建一个业务类 BusinessClass
,其中包含带有 @CustomAnnotation
注解的方法:
public class BusinessClass {
@CustomAnnotation("someValue")
public void annotatedMethod() {
System.out.println("Annotated method is executed");
}
public void nonAnnotatedMethod() {
System.out.println("Non-annotated method is executed");
}
}
接下来,我们编写一个切面 CustomAspect
来拦截带有 @CustomAnnotation
注解的方法:
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
@Aspect
public class CustomAspect {
@Before("@annotation(customAnnotation)")
public void beforeAnnotatedMethod(JoinPoint joinPoint, CustomAnnotation customAnnotation) {
System.out.println("Before execution of method annotated with @CustomAnnotation");
System.out.println("Value: " + customAnnotation.value());
}
}
最后,我们创建一个 Main
类来测试这些代码:
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class Main {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
context.register(CustomAspect.class);
context.refresh();
BusinessClass businessClass = context.getBean(BusinessClass.class);
businessClass.annotatedMethod();
businessClass.nonAnnotatedMethod();
context.close();
}
}
在这个例子中,当我们调用 businessClass.annotatedMethod()
方法时,AspectJ 切面 CustomAspect
将会拦截这个调用,并在方法执行之前输出相应的信息。而对于 businessClass.nonAnnotatedMethod()
方法,由于没有被 @CustomAnnotation
注解修饰,所以不会被切面拦截
6. 连接点
在Spring中用JoinPoint抽象了连接点,用它可以获得方法执行时的相关信息,如目标类名、方法名、方法参数等。
-
对于@Around通知,获取连接点信息只能使用ProceedingJoinPoint类型
-
对于其他四种通知,获取连接点信息只能使用JoinPoint,它是ProceedingJoinPoint的父类型
案例如下:
首先,我们定义一个简单的服务类 MyService
:
package com.example;
public class MyService {
public void myMethod(String arg) {
System.out.println("Executing myMethod with argument: " + arg);
}
}
然后,我们创建一个切面 MyAspect
,在该切面中编写不同类型的通知以获取连接点信息:
package com.example;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
@Aspect
public class MyAspect {
@Before("execution(* com.example.MyService.*(..))")
public void beforeAdvice(JoinPoint joinPoint) {
System.out.println("Before advice:");
System.out.println("Target class: " + joinPoint.getTarget().getClass().getName());
System.out.println("Method name: " + joinPoint.getSignature().getName());
System.out.println("Arguments: ");
Object[] args = joinPoint.getArgs();
for (Object arg : args) {
System.out.println(arg);
}
}
@AfterReturning(pointcut = "execution(* com.example.MyService.*(..))", returning = "result")
public void afterReturningAdvice(JoinPoint joinPoint, Object result) {
System.out.println("After returning advice:");
System.out.println("Target class: " + joinPoint.getTarget().getClass().getName());
System.out.println("Method name: " + joinPoint.getSignature().getName());
System.out.println("Returned value: " + result);
}
@AfterThrowing(pointcut = "execution(* com.example.MyService.*(..))", throwing = "ex")
public void afterThrowingAdvice(JoinPoint joinPoint, Throwable ex) {
System.out.println("After throwing advice:");
System.out.println("Target class: " + joinPoint.getTarget().getClass().getName());
System.out.println("Method name: " + joinPoint.getSignature().getName());
System.out.println("Exception: " + ex.getMessage());
}
@After("execution(* com.example.MyService.*(..))")
public void afterAdvice(JoinPoint joinPoint) {
System.out.println("After advice:");
System.out.println("Target class: " + joinPoint.getTarget().getClass().getName());
System.out.println("Method name: " + joinPoint.getSignature().getName());
}
@Around("execution(* com.example.MyService.*(..))")
public Object aroundAdvice(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
System.out.println("Around advice - before proceeding:");
System.out.println("Target class: " + proceedingJoinPoint.getTarget().getClass().getName());
System.out.println("Method name: " + proceedingJoinPoint.getSignature().getName());
System.out.println("Arguments: ");
Object[] args = proceedingJoinPoint.getArgs();
for (Object arg : args) {
System.out.println(arg);
}
Object result = proceedingJoinPoint.proceed();
System.out.println("Around advice - after proceeding:");
System.out.println("Returned value: " + result);
return result;
}
}
最后,我们创建一个应用程序 MainApp
,使用Spring配置来启用AOP:
package com.example;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@Configuration
@ComponentScan(basePackages = "com.example")
public class MainApp {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MainApp.class);
MyService myService = context.getBean(MyService.class);
myService.myMethod("test");
context.close();
}
}
在这个例子中,我们定义了一个简单的 MyService
类,并在切面 MyAspect
中编写了不同类型的通知来获取连接点信息。然后,在 MainApp
类中启用了Spring的AOP功能,并使用 MyService
的实例来调用 myMethod()
方法。