aop

什么是AOP

AOP(Aspect Oriented Programming),面向切面编程,是一种编程范式,是OOP(Object Oriented Programming)的补充。OOP是面向对象编程,AOP是面向切面编程。

AOP的核心思想是将程序的业务逻辑和系统级服务分开,通过切面将系统级服务应用到业务逻辑中,从而实现业务逻辑和系统级服务的解耦。

AOP的核心概念

  1. 连接点(JoinPoint):可以被AOP控制的方法
  2. 通知(Advice):指哪些重复的逻辑,也就是共性功能
  3. 切入点(PointCut):匹配连接点的条件,通知仅会在切入点方法被执行时应用
  4. 切面(Aspect):通知和切入点的组合
  5. 目标对象(Target):被代理的对象

AOP的实现

动态代理是AOP最主流的实现方式,Spring AOP就是基于动态代理实现的。在管理bean对象的时候,Spring会为bean对象创建一个代理对象,当调用代理对象的方法时,会先调用代理对象的增强方法,再调用目标对象的方法。

AOP的应用场景

  1. 日志记录
  2. 权限控制
  3. 事务管理

例如,进行记录方法运行时间时,可以通过AOP实现,而不需要在每个方法中都写一段记录时间的代码。

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

@Around("execution(* com.example.demo.service.*.*(..))") // 切入点
public Object time(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis();

Object result = joinPoint.proceed();

long end = System.currentTimeMillis();

log.info("方法{}运行时间为{}ms", joinPoint.getSignature().getName(), end - start);
return result;
}
}

在上面的代码中,通过@Aspect注解声明一个切面,通过@Around注解声明一个环绕通知,通过execution表达式指定切入点,即需要增强的方法,通过ProceedingJoinPoint对象获取方法的信息,通过proceed方法调用目标方法,最后记录方法的运行时间。

AOP的优点

  1. 代码无侵入性,不需要修改原有代码
  2. 代码复用性,将公共代码抽取出来,可以在多个方法中复用
  3. 代码解耦,将系统级服务和业务逻辑分开,提高代码的可维护性

深入学习

通知类型

  1. 前置通知(Before):在目标方法执行前执行
  2. 后置通知(After):在目标方法执行后执行,无论目标方法是否抛出异常
  3. 环绕通知(Around):在目标方法执行前后执行
  4. 异常通知(AfterThrowing):在目标方法抛出异常后执行
  5. 返回通知(AfterReturning):在目标方法返回结果时执行,有异常时不执行

注意,@Around通知需要调用proceed方法,才能执行目标方法,否则目标方法不会执行。同时,proceed的返回值应该为Object类型,否则会报错。

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
26
27
28
29
30
31
32
33
34
35
36
@Slf4j
@Aspect
@Component
public class MyAspect {

@Pointcut("execution(* com.example.demo.service.*.*(..))")
public void pointCut() {} // 使用@Pointcut注解定义切入点

@Before("pointCut()")
public void before(JoinPoint joinPoint) {
log.info("before");
}

@After("pointCut()")
public void after(JoinPoint joinPoint) {
log.info("after");
}

@Around("pointCut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("around before");
Object result = joinPoint.proceed();
log.info("around after");
return result;
}

@AfterThrowing("pointCut()")
public void afterThrowing(JoinPoint joinPoint) {
log.info("afterThrowing");
}

@AfterReturning("pointCut()")
public void afterReturning(JoinPoint joinPoint) {
log.info("afterReturning");
}
}

通知顺序

当有多个切面的切入点都匹配到了目标方法时,多个切面都会执行

执行顺序

  1. 不同切面类中,默认按照切面类的类名的字典顺序执行
    • 目标方法前:字母靠前的切面类先执行
    • 目标方法后:字母靠后的切面类先执行
  2. 用@Order注解指定切面的执行顺序
    • 目标方法前:数字小的切面类先执行
    • 目标方法后:数字大的切面类先执行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Slf4j
@Aspect
@Component
@Order(1)
public class MyAspect1 {

@Pointcut("execution(* com.example.demo.service.*.*(..))")
public void pointCut() {}

@Before("pointCut()")
public void before(JoinPoint joinPoint) {
log.info("before1");
}

@After("pointCut()")
public void after(JoinPoint joinPoint) {
log.info("after1");
}
}

切入点表达式

两种常用的切入点表达式

execution表达式

execution表达式用于匹配方法的执行,语法为execution(访问修饰符? 返回值 包名.类名.?方法名(参数类型) throws 异常类型?)

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

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

execution(* com.*.service*(*)):匹配com包下的所有service开头的方法,且只有一个参数

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

execution(* com.example..DeptService.*(..)):匹配com.example包及其子包下的DeptService类的所有方法

annotation表达式

annotation表达式用于匹配带有指定注解的方法,语法为@annotation(注解类型)

自定义注解

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

使用自定义注解

1
2
3
4
5
6
7
8
9
10
11
12
13
@Slf4j
@Aspect
@Component
public class MyAspect {

@Pointcut("@annotation(com.example.demo.annotation.MyAnnotation)")
public void pointCut() {}

@Before("pointCut()")
public void before(JoinPoint joinPoint) {
log.info("before");
}
}

这样,只有带有@MyAnnotation注解的方法才会被增强

连接点

在spring中用JoinPoint抽象了连接点,可以通过JoinPoint获取方法的信息

对于@Around通知,可以通过ProceedingJoinPoint调用proceed方法执行目标方法

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

@Pointcut("execution(* com.example.demo.service.*.*(..))")
public void pointCut() {}

@Around("pointCut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("around before");
Object result = joinPoint.proceed();
log.info("around after");
return result;
}
}

对于其他通知,可以通过JoinPoint获取方法的信息,JointPoint是ProceedingJoinPoint的父类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Slf4j
@Aspect
@Component
public class MyAspect {

@Pointcut("execution(* com.example.demo.service.*.*(..))")
public void pointCut() {}

@Before("pointCut()")
public void before(JoinPoint joinPoint) {
log.info("before");
log.info("方法名:{}", joinPoint.getSignature().getName());
log.info("参数:{}", joinPoint.getArgs());
}
}
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
26
27
28
29
@Slf4j
@Aspect
@Component
public class MyAspect {

@Pointcut("execution(* com.example.demo.service.*.*(..))")
public void pointCut() {}

@Around("pointCut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
// 获取类名
String className = joinPoint.getTarget().getClass().getName();
log.info("类名:{}", className);

// 获取方法名
String methodName = joinPoint.getSignature().getName();
log.info("方法名:{}", methodName);

// 获取参数
Object[] args = joinPoint.getArgs();
log.info("参数:{}", args);

// 运行 获取返回值
Object result = joinPoint.proceed();
log.info("返回值:{}", result);

return result;
}
}