AOP面向切面编程
AOP介绍
- AOP:Aspect Oriented Programming(面向切面编程),可以简单理解为面向特定方法编程
- 场景: 1. 统计业务方法的执行耗时 2. 记录系统的日志操作 3. 事务管理(底层就是AOP,Spring对其封装成了@Transactional) 4. 权限控制
- 优势: 1. 较少重复代码 2. 代码无侵入 3. 提高开发效率 4. 维护方便
AOP基础
- 需求:统计所有业务层方法的执行耗时
- 导入依赖:在pom.xml中引入AOP的依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
<dependency>
- 编写AOP程序:针对特定的方法根据业务需要进行编程
@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;
}
}
@Aspect:标识该类为 Aspect(切面类)@Around:生效的范围(环绕通知注解),可以在目标方法执行前后进行拦截和处理
execution:匹配方法执行连接点*:返回值类型(任意)com.norlcyan.service.impl.*:包路径下的所有类.*:所有方法-
(..):任意参数列表 3.pjp.proceed():可以看作是执行原始方法 -
可以控制是否执行目标方法
- 可以修改方法参数或返回值
4.
pjp.getSignature():获取方法的签名(方便定位具体哪个方法)
AOP核心概念
- 连接点:JoinPoint,可以被AOP控制的方法(暗含方法执行时的相关信息)
- 可以被控制不代表已经被控制了
- 切入点一定是连接点,连接点不一定是切入点
- 通知:Advice,指那些重复的逻辑,也就是共性功能(最终体现为一个方法)
- 在基础案例中,recordTime就是通知方法
- 切入点:PointCut,匹配连接点的条件,通知仅会在切入点方法执行时被应用
- 实际被控制的方法
- 简单理解为就是@Around注解中的execution参数指定的方法
- 切面:Aspect,描述通知与切入点的对应关系(通知+切入点)
- 目标对象:Target,通知所应用的对象
切面(Aspect)
├── 通知(Advice) ←→ 具体的增强逻辑方法
└── 切入点(PointCut) ←→ 匹配连接点的表达式
连接点(JoinPoint) ←→ 程序中可以被增强的点
↑ 包含 ↑
切入点(实际匹配的连接点) ←→ 被AOP实际控制的方法
目标对象(Target) ←→ 被代理和增强的原始对象

代理对象在Spring容器初始化时创建一次,而不是调用一次方法就创建一次代理对象
AOP进阶
通知类型
- @Around:环绕通知,此注解标注的通知方法在目标方法前、后都会被执行
- @Before:前置通知,此注解标注的通知方法在目标方法前被执行
- @After:后置通知,此注解标注的通知方法在目标方法后被执行,无论是否有异常都会执行
- @AfterReturning:返回后通知,此注解标注的通知方法在目标方法后被执行,有异常不会执行
- @AfterThrowing:异常后通知,此注解标注的通知方法发生异常后执行
注意:
- @Around环绕通知需要自己调用
ProceedingJoinPoint.proceed()来让原始方法执行,其他通知不需要考虑目标方法执行 - @Around环绕通知方法的返回值,必须指定为Object,来接收原始方法的返回值
示例:
@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主要根据方法的返回值、包名、类名、方法名、方法参数等信息来匹配语法为:
execution(访问修饰符? 返回值? 包名.类名.?方法名(方法参数) throws 异常?)
- 其中带
?的表示可以省略的部分 1. 访问修饰符:可以省略(比如public、protected) 2. 包名.类名:可以省略(不建议省略) 3. throws异常:可以省略(注意是方法上声明抛出的异常,不是实际抛出的异常) *:单个独立的任意符号,可以通配任意返回值、包名、类名、方法名、任意类型的一个参数,也可以通配包、类、方法名的一部分- 比如:
execution(* com.*.service.*.update*(*)) ..:多个连续的任意符号,可以通配任意层级的包,或任意类型、任意个数的参数- 比如:
execution(* com.norlcyan..DeptService.*(..))
示例:
@Before("execution(public void com.norlcyan.service.impl.DeptServiceImpl.delete(java.lang.Integer))")
public void before(JoinPoint joinPoint) {...};
另外,execution表达式还支持逻辑运算符:
@Before("execution(* com.norlcyan.service.impl.DeptServiceImpl.list(..)) ||" +
"execution(* com.norlcyan.service.impl.DeptServiceImpl.delete(..))"
)
public void before(JoinPoint joinPoint) {...};
上面表达式的意思是目标方法为list方法或delete方法
书写建议:
- 所有业务方法名在命名时尽量规范,方便切入点表达式快速匹配。如:findXxx,updateXxx
- 描述切入点方法通常基于接口描述,而不是直接描述实现类,增强拓展性
- 在满足业务需要的前提下,尽量缩小切入点的匹配范围。如:包名尽量不使用
..,使用*匹配单个包
@annotation
- @annotation切入点表达式,用于匹配标识有特定注解的方法
- 当execution表达式过于复杂,就可以考虑这种方式
@Before("@annotation(com.norlcyan.anno.LogOperation)")
public void before() {...};
@LogOperation// 匹配到该注解
@DeleteMapping
public Result delete(Integer id) {...}
特定注解可以是自定义注解,也可以是其他依赖提供的注解
连接点
- 在Spring中用JoinPoint抽象了连接点,用它可以获得方法执行时的相关信息,如目标类名、方法名、方法参数等
- 对于@Around通知,获取连接点信息只能使用ProceedingJoinPoint
- 对于其他四种通知,获取连接点信息只能使用JoinPoint,它是ProceedingJoinPoint的父类
@Around:
@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;
}
其他通知:
@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系统中的员工接口下的增、删、改操作以日志的形式记录到数据库表中
- 日志信息包括:操作人、操作时间、执行方法的全类名、执行方法名、方法运行时参数、返回值,方法执行时长
许可协议:
CC BY 4.0