一、AOP简介
**AOP:**Aspect Oriented Programming(面向切面编程、面向方面编程),就是面向特定方法编程。
**使用场景:**当部分业务方法运行较慢,要定位到 耗时较长的方法,此时需要统计每个方法消耗时长。
**原始方法:**我们要计算一个方法运行时长就在方法开头和结尾计时再做差即可

但这样做需要给每个方法都单独写,太耗时了。**为了减少重复代码,提高开发效率,维护方便,**Sspring中提供了AOP。
二、AOP基本使用
我们现在的需求是统计所有业务层的执行耗时。
**step1:**在pom.xml引入AOP依赖
XML
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
**step2:**创建一个AOP类交给IOC容器管理,我们需要在AOP类的方法中实现下图功能

@Aspect标识当前是一个AOP类
@Component将该类交给IOC容器管理
@Around(指定执行AOP的方法)"execution(* com.itheima.service.impl.*.*(..))"
java
package com.itheima.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;
@Slf4j
@Aspect //表示当前是一个AOP类
@Component
public class RecordTimeAspect {
@Around("execution(* com.itheima.service.impl.*.*(..))")// 表示com.itheima.service.impl包下的所有类的所有方法
public Object recordTime(ProceedingJoinPoint pjp) throws Throwable {
//1.记录开始时间
long begin = System.currentTimeMillis();
//2.执行原始方法
Object result = pjp.proceed();// 因为原始方法返回结果可能各式各样,所以用Object接收
//3.记录结束时间
long end = System.currentTimeMillis();
log.info("方法 {} 执行耗时:{}", pjp.getSignature(), end - begin);
return result;
}
}
三、AOP核心概念
连接点: JoinPoint,可以被AOP控制的方法

**通知:**Advice,指那些重复的逻辑,也就是共性功能
eg:我们上面案例每个方法都要进行开始和结束时间的记录

**切入点:**PointCut,匹配连接点的条件,通知仅会在切入点方法执行时被应用
eg:@Around("execution(* com.itheima.service.impl.*.*(..))")切入点表达式

**切面:**Aspect,描述通知与切入点的对应关系

**目标对象:**Target,通知所应用的对象

AOP执行流程
AOP方法的@Around指定了目标对象,会基于动态代理技术为目标对象生成一个代理对象,代理对象中的方法会根据通知 里的内容进行生成,只需要把通知里的joinPoint.proceed() 换成调用目标对象的方法即可,最后将代理对象注入IOC容器,最后运行实际调用的代理对象的方法

四、通知类型
|-----------------|--------------------------------------|
| Spring AOP 通知类型 ||
| @Around | 环绕通知,此注解标注的通知方法在目标方法前、后都被执行 |
| @Before | 前置通知,此注解标注的通知方法在目标方法前被执行 |
| @After | 后置通知,此注解标注的通知方法在目标方法后被执行,无论是否有异常都会执行 |
| @AfterReturning | 返回后通知,此注解标注的通知方法在目标方法后被执行,有异常不会执行 |
| @AfterThrowing | 异常后通知,此注解标注的通知方法发生异常后执行 |
注意:
- @Around环绕通知需要自己调用ProceedingJoinPoint.proceed()来让原始方法执行,其他通知不需要考虑目标方法执行
- @Around方法的返回值,必须指定为Object来接受原始方法的返回值。
我们又发现,我们每次写切入点方法,都是重复的,因此Spring提供了**@pointCut**注解,该注解的作用是将公共的切入点表达式抽取出来,需要用到是引入该切入点表达式即可
java
package com.itheima.aop;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Slf4j
@Aspect
@Component
public class MyAspect1 {
//切入点方法(公共的切入点表达式)
@Pointcut("execution(* com.itheima.service.*.*(..))")
private void pt(){}
//前置通知
@Before("pt()")
public void before(){
log.info("before....");
}
@Around("pt()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
log.info("around...");
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....");
}
}
五、通知顺序
当有多个AOP执行时就需要分先后顺序,不同切面类中,默认按照切面类的类名字母排序:
-
目标方法前的通知方法:字母排名靠前的先执行
-
目标方法后的通知方法:字母排名靠前的后执行
如果我们想要控制通知的执行顺序有两种方法:
- 修改切面类的类名(不推荐)
- 使用Spring提供的@Order(数字):目标方法前的通知方法:数字小的先执行,目标方法后反之
java
@Slf4j
@Component
@Aspect
@Order(2) //切面类的执行顺序(前置通知:数字越小先执行; 后置通知:数字越小越后执行)
public class MyAspect2 {
//前置通知
@Before("execution(* com.itheima.service.*.*(..))")
public void before(){
log.info("MyAspect2 -> before ...");
}
//后置通知
@After("execution(* com.itheima.service.*.*(..))")
public void after(){
log.info("MyAspect2 -> after ...");
}
}
六、切入点表达式
**作用:**主要用来决定项目中的哪些方法需要加入通知
(一)execution
java
execution(访问修饰符? 返回值 包名.类名.?方法名(方法参数) throws 异常?)
其中带?的表示可以省略的部分
-
访问修饰符:可省略(比如: public、protected)
-
包名.类名: 可省略
-
throws 异常:可省略(注意是方法上声明抛出的异常,不是实际抛出的异常)
可以使用通配符描述切入点
-
*:单个独立的任意符号,可以通配任意返回值、包名、类名、方法名、任意类型的一个参数,也可以通配包、类、方法名的一部分 -
..:多个连续的任意符号,可以通配任意层级的包,或任意类型、任意个数的参数根据业务
注意: 根据业务需要,可以使用 且(&&)、或(||)、非(!) 来组合比较复杂的切入点表达式。就是简单的把两个切入表达式间加逻辑运算符
java
execution(* com.itheima.service.DeptService.list(..)) || execution(* com.itheima.service.DeptService.delete(..))
但是execution切入点表达式,如果要匹配多个无规则的方法,需要很多个表达式,非常麻烦,所以我们有引入了一个注释**@annotation**
(二)annotation注解
我们可以自定义一个注解
**step1:**编写自定义注解
@Target指定注解的作用返回
@Retention只当注解的运行时机

step2:在业务类 要做为连接点的方法上添加自定义注解

七、连接点
Spring中用JoinPoint抽象了连接点, 用它可以获得方法执行时的相关信息,如:目标类名、方法名、方法参数等。
对于@Around 通知,获取连接点信息只能使用 ProceedingJoinPoint
对于其他四种通知,获取连接点信息只能使用JoinPoint,他是 ProceedingJoinPoint 的父类
八、案例
(一)需求
需求:将案例(Tlias智能学习辅助系统)中增、删、改相关接口的操作日志记录到数据库表中
操作日志信息包含:
操作人、操作时间、执行方法的全类名、执行方法名、方法运行时参数、返回值、方法执行时长
采用哪种通知类型?
@Around环绕通知
切入点表达式该怎么写?
匹配增删改查方法,由于方法名定义的比较规范,分别为save、delete、update。
**step1:**我们需要一个存储操作日志的数据库表
sql
-- 操作日志表
create table operate_log(
id int unsigned primary key auto_increment comment 'ID',
operate_emp_id int unsigned comment '操作人ID',
operate_time datetime comment '操作时间',
class_name varchar(100) comment '操作的类名',
method_name varchar(100) comment '操作的方法名',
method_params varchar(2000) comment '方法参数',
return_value varchar(2000) comment '返回值',
cost_time bigint unsigned comment '方法执行耗时, 单位:ms'
) comment '操作日志表';
**step2:**有了数据库表,我们就需要一个类来封装它
java
package com.sjy.pojo;
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class OperateLog {
private Integer id; //ID
private Integer operateEmpId; //操作人ID
private LocalDateTime operateTime; //操作时间
private String className; //操作类名
private String methodName; //操作方法名
private String methodParams; //操作方法参数
private String returnValue; //操作方法返回值
private Long costTime; //操作耗时
}
**step3:**由于我们只需要增删改查操作进行日志输出,需要较为复杂的execution切入点表达式,所以我们可以使用注解的方式。我们来自定义一个注解。
java
package com.sjy.anno;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Log {
}
step4:接下来要为增删改操作添加记录日志的操作,我们就需要定义一个AOP类。定义一个aroud方法连接点由注释决定,通知内容进行操作的信息获取并存入数据库
java
package com.sjy.aop;
import com.sjy.mapper.OperateLogMapper;
import com.sjy.pojo.OperateLog;
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.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.util.Arrays;
@Slf4j
@Aspect
@Component
public class OperationAspect {
@Autowired
private OperateLogMapper operateLogMapper;
// 环绕通知
@Around("@annotation(com.sjy.anno.Log)")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
// 记录开始时间
long startTime = System.currentTimeMillis();
// 执行方法
Object result = joinPoint.proceed();
// 当前时间
long endTime = System.currentTimeMillis();
// 耗时
long costTime = endTime - startTime;
// 构建日志对象
OperateLog olog = new OperateLog();
olog.setOperateEmpId(getCurrentUserId()); // 需要实现 getCurrentUserId 方法
olog.setOperateTime(LocalDateTime.now());
olog.setClassName(joinPoint.getTarget().getClass().getName());
olog.setMethodName(joinPoint.getSignature().getName());
olog.setMethodParams(Arrays.toString(joinPoint.getArgs()));
olog.setReturnValue(result.toString());
olog.setCostTime(costTime);
// 插入日志
operateLogMapper.insert(olog);
return result;
}
private Integer getCurrentUserId() {
return 1;
}
}