AOP介绍

  • AOP:Aspect Oriented Programming(面向切面编程),可以简单理解为面向特定方法编程
  • 场景:
    1. 统计业务方法的执行耗时
    2. 记录系统的日志操作
    3. 事务管理(底层就是AOP,Spring对其封装成了@Transactional)
    4. 权限控制
  • 优势:
    1. 较少重复代码
    2. 代码无侵入
    3. 提高开发效率
    4. 维护方便

AOP基础

  • 需求:统计所有业务层方法的执行耗时
  1. 导入依赖:在pom.xml中引入AOP的依赖
1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
<dependency>
  1. 编写AOP程序:针对特定的方法根据业务需要进行编程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Aspect
@Component
@Slf4j
public class RecordTimeAspect {
@Around("execution(* com.norlcyan.service.impl.*.*(..))")
public Object recordTime(ProceedingJoinPoint pjp) throws Throwable {
long beginTime = System.currentTimeMillis();

Object result = pjp.proceed();

long endTime = System.currentTimeMillis();
log.info("方法 {} 执行耗时: {}ms", pjp.getSignature() ,(endTime - beginTime));

return result;
}
}
  1. @Aspect:标识该类为 Aspect(切面类)

  2. @Around:生效的范围(环绕通知注解),可以在目标方法执行前后进行拦截和处理

    • execution:匹配方法执行连接点
    • *:返回值类型(任意)
    • com.norlcyan.service.impl.*:包路径下的所有类
    • .*:所有方法
    • (..):任意参数列表
  3. pjp.proceed():可以看作是执行原始方法

    • 可以控制是否执行目标方法
    • 可以修改方法参数或返回值
  4. pjp.getSignature():获取方法的签名(方便定位具体哪个方法)

AOP核心概念

  • 连接点:JoinPoint,可以被AOP控制的方法(暗含方法执行时的相关信息)
    • 可以被控制不代表已经被控制了
    • 切入点一定是连接点,连接点不一定是切入点
  • 通知:Advice,指那些重复的逻辑,也就是共性功能(最终体现为一个方法)
    • 在基础案例中,recordTime就是通知方法
  • 切入点:PointCut,匹配连接点的条件,通知仅会在切入点方法执行时被应用
    • 实际被控制的方法
    • 简单理解为就是@Around注解中的execution参数指定的方法
  • 切面:Aspect,描述通知与切入点的对应关系(通知+切入点)
  • 目标对象:Target,通知所应用的对象
1
2
3
4
5
6
7
8
9
切面(Aspect)
├── 通知(Advice) ←→ 具体的增强逻辑方法
└── 切入点(PointCut) ←→ 匹配连接点的表达式

连接点(JoinPoint) ←→ 程序中可以被增强的点
↑ 包含 ↑
切入点(实际匹配的连接点) ←→ 被AOP实际控制的方法

目标对象(Target) ←→ 被代理和增强的原始对象

后端Web进阶(AOP)

代理对象在Spring容器初始化时创建一次,而不是调用一次方法就创建一次代理对象

AOP进阶

通知类型

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

注意:

  1. @Around环绕通知需要自己调用ProceedingJoinPoint.proceed()来让原始方法执行,其他通知不需要考虑目标方法执行
  2. @Around环绕通知方法的返回值,必须指定为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
37
38
39
40
41
42
43
44
@Slf4j
@Aspect
@Component
public class MyAspect1 {

@Pointcut("execution(* com.itheima.service.impl.*.*(..))")
private void pt(){}

//前置通知 - 目标方法运行之前运行
@Before("pt()")
public void before(){
log.info("before ....");
}

//环绕通知 - 目标方法运行之前、后运行
@Around("pt()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
log.info("around ... before ....");

Object result = pjp.proceed();

log.info("around ... after ....");
return result;
}

//后置通知 - 目标方法运行之后运行, 无论是否出现异常都会执行
@After("pt()")
public void after(){
log.info("after ....");
}

//返回后通知 - 目标方法运行之后运行, 如果出现异常不会运行
@AfterReturning("pt()")
public void afterReturning(){
log.info("afterReturning ....");
}

//异常后通知 - 目标方法运行之后运行, 只有出现异常才会运行
@AfterThrowing("pt()")
public void afterThrowing(){
log.info("afterThrowing ....");
}

}

@PointCut:该注解的作用是将公共的切入点表达式抽取出来,许需要用到时引用该切入点表达式即可

该注解修饰的方法,它的权限修饰符根据如下规则即可:

  • private:仅能在当前切面类中引用该表达式
  • public:在其他外部的切面类中也可以引用该表达式

通知顺序

  • 当有多个切面的切入点都匹配到了目标方法,目标方法运行时,多个通知方法都会被执行
  • 执行顺序:
    • 不同切面类中,默认按照切面类的类名字母排序:
      • 目标方法的通知方法:字母排名靠前的执行
      • 目标方法的通知方法:字母排名考前的执行
    • @Order(数字)加在切面类上来控制顺序
      • 目标方法前的通知方法:数字小的先执行
      • 目标方法后的通知方法:数字小的后执行

切入点表达式

  • 介绍:描述切入点方法的一种表达式
  • 作用:用来决定项目中的哪些方法需要加入通知
  • 常见形式:
    1. execution(...):根据方法的签名来匹配
    2. @annotation(...):根据注解匹配

execution

  • execution主要根据方法的返回值、包名、类名、方法名、方法参数等信息来匹配语法为:
1
execution(访问修饰符? 返回值? 包名.类名.?方法名(方法参数) throws 异常?)
  • 其中带?的表示可以省略的部分
    1. 访问修饰符:可以省略(比如public、protected)
    2. 包名.类名:可以省略(不建议省略)
    3. throws异常:可以省略(注意是方法上声明抛出的异常,不是实际抛出的异常
  • *:单个独立的任意符号,可以通配任意返回值、包名、类名、方法名、任意类型的一个参数,也可以通配包、类、方法名的一部分
    • 比如:execution(* com.*.service.*.update*(*))
  • ..:多个连续的任意符号,可以通配任意层级的包,或任意类型任意个数的参数
    • 比如:execution(* com.norlcyan..DeptService.*(..))

示例:

1
2
@Before("execution(public void com.norlcyan.service.impl.DeptServiceImpl.delete(java.lang.Integer))")
public void before(JoinPoint joinPoint) {...};

另外,execution表达式还支持逻辑运算符

1
2
3
4
@Before("execution(* com.norlcyan.service.impl.DeptServiceImpl.list(..)) ||" +
"execution(* com.norlcyan.service.impl.DeptServiceImpl.delete(..))"
)
public void before(JoinPoint joinPoint) {...};

上面表达式的意思是目标方法为list方法或delete方法

书写建议:

  1. 所有业务方法名在命名时尽量规范,方便切入点表达式快速匹配。如:findXxx,updateXxx
  2. 描述切入点方法通常基于接口描述,而不是直接描述实现类,增强拓展性
  3. 在满足业务需要的前提下,尽量缩小切入点的匹配范围。如:包名尽量不使用..,使用*匹配单个包

@annotation

  • @annotation切入点表达式,用于匹配标识有特定注解的方法
  • 当execution表达式过于复杂,就可以考虑这种方式
1
2
@Before("@annotation(com.norlcyan.anno.LogOperation)")
public void before() {...};
1
2
3
@LogOperation	// 匹配到该注解
@DeleteMapping
public Result delete(Integer id) {...}

特定注解可以是自定义注解,也可以是其他依赖提供的注解

连接点

  • 在Spring中用JoinPoint抽象了连接点,用它可以获得方法执行时的相关信息,如目标类名、方法名、方法参数等
    • 对于@Around通知,获取连接点信息只能使用ProceedingJoinPoint
    • 对于其他四种通知,获取连接点信息只能使用JoinPoint,它是ProceedingJoinPoint的父类

@Around:

1
2
3
4
5
6
7
8
9
10
@Around("execution(* com.norlcyan.service.DeptService.*(..))")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
String className = joinPoint.getTarget().getClass().getName(); //获取目标类名
Signature signature = joinPoint.getSignature(); //获取目标方法签名
String methodName = joinPoint.getSignature().getName(); //获取目标方法名
Object[] args = joinPoint.getArgs(); //获取目标方法运行参数
Object res = joinPoint.proceed(); //执行原始方法,获取返回值(环绕通知)
return res;
}

其他通知:

1
2
3
4
5
6
7
8
@Before("execution(* com.itheima.service.DeptService.*(..))")
public void before(JoinPoint joinPoint) {
String className = joinPoint.getTarget().getClass().getName(); //获取目标类名
Signature signature = joinPoint.getSignature(); //获取目标方法签名
String methodName = joinPoint.getSignature().getName(); //获取目标方法名
Object[] args = joinPoint.getArgs(); //获取目标方法运行参数
}

案例

  • 需求:将Tlias系统中的员工接口下的增、删、改操作以日志的形式记录到数据库表中
  • 日志信息包括:操作人、操作时间、执行方法的全类名、执行方法名、方法运行时参数、返回值,方法执行时长