大纲:
- AOP概念
- AOP实现方式与原理
- 代理模式
1. AOP概述
AOP是Spring框架的第⼆⼤核⼼
AOP-面向切面编程
什么是面向切面编程?
切⾯就是指某⼀类特定问题
什么是⾯向特定⽅法编程呢?
⽐如学习的"登录校验",就是⼀类特定问题.登录校验拦截器,就是对"登录校验"这类问题的统⼀处理.所以,拦截器也是AOP的⼀种应⽤.
AOP是⼀种思想,拦截器是AOP思想的⼀种实现.Spring框架实现了这种思想,提供了拦截器技术的相关接⼝.
同样的,统⼀数据返回格式和统⼀异常处理,也是AOP思想的⼀种实现
简单来说: AOP是⼀种思想,是对某⼀类事情的集中处理
什么是SpringAOP?
Spring AOP是AOP的一种实现方式
举个例子:
现在有⼀个项⽬,项⽬中开发了很多的业务功能,有⼀些业务的执⾏效率⽐较低,耗时较⻓,我们需要对接⼝进⾏优化,那么就需要定位出执⾏耗时⽐较⻓的业务⽅法,再针对该业务⽅法来进⾏优化
如何定位呢?
我们就需要统计当前项⽬中每⼀个业务⽅法的执⾏耗时
可以在业务⽅法运⾏前和运⾏后,记录下⽅法的开始时间和结束时间,两者之差就是这个⽅法的耗时.
AOP就可以做到在不改动这些原始⽅法的基础上,针对特定的⽅法进⾏功能的增强(解耦)
2. Spring AOP入门
需求统计图书系统各个接⼝⽅法的执⾏时间
2.1 引入AOP依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
2.2 编写AOP程序
@Aspect
@Slf4j
@Component
public class TimeAspect {
@Around("execution(* com.example.Bookdemo.controller.*.*(..))")
public Object timeCost(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis();
//执行目标方法
Object result = joinPoint.proceed();
long end = System.currentTimeMillis();
log.info(joinPoint+"消耗时间: "+(end-start)+"ms");
return result;
}
}
其中:
- @Aspect:标识这是⼀个切⾯类
- @Around: 环绕通知,在⽬标⽅法的前后都会被执⾏.后⾯的表达式表⽰对哪些⽅法进⾏增强.
- ProceedingJoinPoint.proceed() 让原始⽅法执⾏
通过上⾯的程序,我们也可以感受到AOP⾯向切⾯编程的⼀些优势:
- 代码⽆侵⼊:不修改原始的业务⽅法,就可以对原始的业务⽅法进⾏了功能的增强或者是功能的改变
- 减少了重复代码
- 提⾼开发效率
- 维护⽅便
3. Spring AOP详解
3.1 核心概念
3.1.1 切点((Pointcut))
Pointcut 的作⽤就是提供⼀组规则,告诉程序对哪些⽅法来进⾏功能增强.
切点表达式:
3.1.2 连接点(JoinPoint)
以上述代码为例, com.example.Bookdemo.controller路径下的方法都是连接点
切点和连接点的关系
连接点是满⾜切点表达式的元素.
切点可以看做是保存了众多连接点的⼀个集合.
⽐如: 切点表达式:全体教师 连接点就是:张三,李四等各个⽼师
3.1.3 通知(Advice)
通知就是具体要做的⼯作,指哪些重复的逻辑,也就是共性功能(最终体现为⼀个⽅法)
3.1.4 切面(Aspect)
切⾯(Aspect)=切点(Pointcut)+通知(Advice)
通过切⾯就能够描述当前AOP程序需要针对于哪些⽅法,在什么时候执⾏什么样的操作
切⾯所在的类,我们⼀般称为切⾯类(被@Aspect注解标识的类)
3.2 通知类型
类型 | 含义 |
@Around | 环绕通知,此注解标注的通知⽅法在⽬标⽅法前,后都被执⾏ |
@Before | 此注解标注的通知⽅法在⽬标⽅法前被执⾏ |
@After | 后置通知,此注解标注的通知⽅法在⽬标⽅法后被执⾏,⽆论是否有异常都会执⾏ |
@AfterReturning | 返回后通知,此注解标注的通知⽅法在⽬标⽅法后被执⾏,有异常不会执⾏ |
@AfterThrowing | 异常后通知,此注解标注的通知⽅法发⽣异常后执⾏ |
下面用代码看一下:
①正常:
@Component
@Aspect
@Slf4j
public class AspectDemo4 {
@Before("execution(* com.example.aopdemo.controller.*.*(..))")
public void doBefore(){
log.info("执行AspectDemo before...");
}
@After("execution(* com.example.aopdemo.controller.*.*(..))")
public void doAfter(){
log.info("执行AspectDemo doAfter...");
}
@AfterReturning("execution(* com.example.aopdemo.controller.*.*(..))")
public void doAfterReturning(){
log.info("执行AspectDemo doAfterReturning...");
}
@AfterThrowing("execution(* com.example.aopdemo.controller.*.*(..))")
public void doAfterThrowing(){
log.info("执行AspectDemo doAfterThrowing...");
}
@Around("execution(* com.example.aopdemo.controller.*.*(..))")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("执行AspectDemo doAround 前...");
Object result = joinPoint.proceed();
log.info("执行AspectDemo doAround 后...");
return result;
}
}
测试代码:
@RestController
public class TestController {
@RequestMapping("/t2")
public String t2(){
int a = 10/0;
return "t2";
}
}
程序正常运⾏的情况下, @AfterThrowing 标识的通知⽅法不会执⾏
②异常:
@ AfterReturning 标识的通知⽅法不会执⾏, @AfterThrowing 标识的通知⽅法执⾏了
通知中的环绕后的代码逻辑也不会在执⾏了
3.3 @PointCut
上⾯代码存在⼀个问题,就是存在⼤量重复的切点表达式
execution(* com.example.demo.controller.*.*(..)) , Spring提供了 表达式提取出来,需要⽤到时引⽤该切⼊点表达式即可.
@Aspect
@Component
@Slf4j
public class AspectDemo {
@Pointcut("execution(* com.example.aopdemo.controller.*.*(..))")
public void pt(){
}
@Before("pt()")
public void doBefore(){
log.info("执行AspectDemo before...");
}
@After("pt()")
public void doAfter(){
log.info("执行AspectDemo doAfter...");
}
@AfterReturning("pt()")
public void doAfterReturning(){
log.info("执行AspectDemo doAfterReturning...");
}
@AfterThrowing("pt()")
public void doAfterThrowing(){
log.info("执行AspectDemo doAfterThrowing...");
}
@Around("pt()")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("执行AspectDemo doAround 前...");
Object result = joinPoint.proceed();
log.info("执行AspectDemo doAround 后...");
return result;
}
}
当切点定义使⽤private修饰时,仅能在当前切⾯类中使⽤,当其他切⾯类也要使⽤当前切点定义时,就需要把private改为public
3.4 切⾯优先级@Order
当我们在⼀个项⽬中,定义了多个切⾯类时,并且这些切⾯类的多个切⼊点都匹配到了同⼀个⽬标⽅法. 当⽬标⽅法运⾏的时候,这些切⾯类中的通知⽅法都会执⾏,那么这⼏个通知⽅法的执⾏顺序是什么样的呢?
此时有AspectDemo1,AspectDemo2,AspectDemo3
经过验证可得,是按按照切⾯类的类名字⺟排序
Spring 给我们提供了⼀个新的注解,来控制这些切⾯通知的执⾏顺序:@Order
使用如下:
运行:
使用@Order时,数字越小,优先级越高
3.5 切点表达式
切点表达式常⻅有两种表达⽅式
1. execution(... ) :根据⽅法的签名来匹配
2. @annotation(... ):根据注解匹配
3.5.1 execution表达式
语法:
execution(< 访问修饰符 > < 返回类型 > < 包名 . 类名 . ⽅法 ( ⽅法参数 )> < 异常 >)
其中,访问修饰符和异常可以省略
* :匹配任意字符,只匹配⼀个元素(返回类型,包,类名,⽅法或者⽅法参数)
.. :匹配多个连续的任意符号,可以通配任意层级的包,或任意类型,任意个数的参数
3.5.2 @annotation
execution表达式更适⽤有规则的,如果我们要匹配多个⽆规则的⽅法呢,
⽐如:TestController中的t1() 和HelloController中的h2()这两个⽅法.
这个时候我们使⽤execution这种切点表达式来描述就不是很⽅便了.
实现自定义注解:
- 编写⾃定义注解
- 使⽤ @annotation 表达式来描述切点
- 在连接点的⽅法上添加⾃定义注解
3.5.2.1 自定义注解
创建一个注解类
@Target 标识了 Annotation 所修饰的对象范围,即该注解可以⽤在什么地⽅
@Retention 指Annotation被保留的时间⻓短,标明注解的⽣命周期
3.5.2.2 切面类
使⽤ @annotation 切点表达式定义切点,只对@MyAspect生效
@Aspect
@Component
@Slf4j
public class MyAspectDemo {
@Before("@annotation(com.example.aopdemo.aspect.MyAspect)")
public void doBefore(){
log.info("执行MyAspectDemo before...");
}
@After("@annotation(com.example.aopdemo.aspect.MyAspect)")
public void doAfter(){
log.info("执行MyAspectDemo After...");
}
}
3.5.2.3 添加自定义注解
测试一下:
t1:
t2:
切面未执行