AOP面向切面编程

AOP的概念

1
2
3
4
5
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>

面向切面编程:将很多重复的代码都归到一起,比如我想统计10个方法的执行时间,那么我就可以只写一份代码,套在10个方法上。

AOP核心概念

  • 连接点:JoinPoint,可以被AOP控制的方法(暗含方法执行时的相关信息)
  • 通知:Advice,指哪些重复的逻辑,也就是共性功能(最终体现为一个方法)
  • 切入点:Pointcut,匹配连接点的条件,通知仅会在切入点方法执行时被应用
  • 切面:Aspect,描述通知与切入点的对应关系(通知+切入点)
  • 目标对象:Target,通知所应用的对象
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    @Component   //加入容器
    @Aspect //标识为AOP
    public class TimeAspect {

    @Around("execution(* com.jaron.service.*.*(..))")//切入点表达式
    public Object recordTime(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {//这个方法称为通知
    long begin = System.currentTimeMillis();
    Object object = proceedingJoinPoint.proceed(); // 调用原始方法
    long end = System.currentTimeMillis();
    Log.info(proceedingJoinPoint.getSignature() + "执行耗时: " + (end - begin) + "ms");

    return object;
    }
    }

通知类型

  1. @Around:环绕通知,此注解标注的通知方法在目标方法前、后都被执行
  2. @Before:前置通知,此注解标注的通知方法在目标方法前被执行
  3. @After:后置通知,此注解标注的通知方法在目标方法后被执行,无论是否有异常都会执行
  4. @AfterReturning :返回后通知,此注解标注的通知方法在目标方法后被执行,有异常不会执行
  5. @AfterThrowing: 异常后通知,此注解标注的通知方法发生异常后执行

当我们想用多个通知时,并且切入点表达式一样,我们可以用@Pointcut进行抽取。

1
2
@Pointcut("execution(* com.jaron.service.*.*(..))")
public void pt(){}

切入点表达式

execution

execution 主要根据方法的返回值、包名、类名、方法名、方法参数等信息来匹配,语法为:

execution(访问修饰符 返回值 包名.类名.方法名(方法参数) throws 异常)

  • 访问修饰符:可省略(比如:public、protected)

  • 包名.类名:可省略

  • throws 异常:可省略(注意是方法上声明抛出的异常,不是实际抛出的异常)

• 可以使用通配符描述切入点

  • *:单个独立的任意符号,可以通配任意返回值、包名、类名、方法名、任意类型的一个参数,也可以通配包、类、方法名的一部分

execution(* com.*.service.*.update*(*))

  • ..:多个连续的任意符号,可以通配任意层级的包,或任意类型、任意个数的参数

execution(* com.savitar.. service.*(..))

@annotation

@annotation(注解全类名),通过在方法上自己加注解,就可以想匹配谁就匹配谁。

首先,定义一个自定义注解,例如 @LogExecutionTime:

1
2
3
4
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface LogExecutionTime {
}

创建一个切面类,使用 @Aspect 注解标记,并在其中定义拦截逻辑。例如,在方法执行前后记录日志:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Aspect
@Component
public class LoggingAspect {

private static final Logger logger = LoggerFactory.getLogger(LoggingAspect.class);

@Around("@annotation(LogExecutionTime)")//被LogExecutionTime注解的方法实现环绕通知
public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis();

Object proceed = joinPoint.proceed();

long executionTime = System.currentTimeMillis() - start;

logger.info(joinPoint.getSignature() + " executed in " + executionTime + "ms");
return proceed;
}
}

被LogExecutionTime注解的方法都会被拦截

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Service
public class ExampleService {

@LogExecutionTime
public void serve() {
// 模拟方法执行时间
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Service method executed");
}
}

连接点

  1. JoinPoint:这个接口提供了一系列方法来访问执行中的连接点的状态。一个连接点可以是一个方法的执行,一个异常的处理,一个字段的修改等。在Spring AOP中,连接点总是代表一个方法的执行。

  2. ProceedingJoinPoint:这是JoinPoint的一个子接口,它只用于环绕通知(Around Advice)。与JoinPoint相比,它多了一个方法proceed(),这个方法用于控制何时执行切入的方法。在环绕通知中,你可以在调用proceed()方法之前和之后添加额外的处理。

示例

假设我们有一个简单的服务类,我们想在其方法执行前后添加一些日志。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class SimpleService {
public void performAction() {
System.out.println("Performing Action!");
}
}

@Aspect
public class LoggingAspect {

// 使用 JoinPoint
@Before("execution(* SimpleService.performAction())")
public void logBefore(JoinPoint joinPoint) {
System.out.println("Before performing action");
}

// 使用 ProceedingJoinPoint
@Around("execution(* SimpleService.performAction())")
public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("Before proceeding");
Object result = joinPoint.proceed(); // 控制何时执行目标方法
System.out.println("After proceeding");
return result;
}
}