目录
1. AOP概述
2.Spring AOP快速入门
2.1引入AOP依赖
2.2编写AOP程序
3.Spring AOP核心概念
3.1切点(PointCut)
3.2连接点(Join Point)
3.3通知(Advice)
3.4切面(Aspect)
4.通知类型
5.@PointCut
6.切面优先级 @Order
1. AOP概述
AOP(Aspect Oriented Programming,面向切面编程)是一种编程范式,它旨在通过预编译方式和运行期间动态代理实现程序功能的统一维护。
AOP定义:
- AOP是OOP(面向对象编程)的延续,是软件开发中的一个热点技术。
- 它是Spring框架中的一个重要内容,也是函数式编程的一种衍生范型。
- AOP通过“切面”对业务逻辑的各个部分进行隔离,降低业务逻辑之间的耦合度,提高程序的可重用性和开发效率。
与OOP的区别:
OOP针对业务处理过程的实体及其属性和行为进行抽象封装,以获得更加清晰高效的逻辑单元划分。
AOP则是针对业务处理过程中的切面进行提取,它所面对的是处理过程中的某个步骤或阶段,以获得逻辑过程中各部分之间低耦合性的隔离效果。
应用场景:记录日志、性能监控、权限控制、缓存优化、事务管理(如声明式事务)
案例分析:
我们现在有⼀个项⽬ , 项⽬中开发了很多的业务功能
现在有⼀些业务的执行效率比较低, 耗时较长, 我们需要对接口进行优化.
第⼀步就需要定位出执行耗时比较长的业务方法, 再针对该业务方法来进行优化
如何定位呢? 我们就需要统计当前项目中每⼀个业务方法的执行耗时.
如何统计呢? 可以在业务方法运行前和运行后, 记录下方法的开始时间和结束时间, 两者之差就是这个方法的耗时.
这种方法是可以解决问题的, 但⼀个项⽬中会包含很多业务模块, 每个业务模块又有很多接口 , ⼀个接口又包含很多方法, 如果我们要在每个业务方法中都记录方法的耗时, 会增加特别多的工作量.
AOP就可以做到在不改动这些原始方法的基础上, 针对特定的方法进⾏功能的增强.
AOP的作用:在程序运行期间在不修改源代码的基础上对已有方法进行增强(无侵⼊性: 解耦)
2.Spring AOP快速入门
需求:统计系统各个方法的zhixing
2.1引入AOP依赖
在pom.xml文件中添加配置
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
2.2编写AOP程序
记录Controller中每个方法的执行时间
@Slf4j
@Aspect
@Component
public class TimeAspect {
/**
* 记录⽅法耗时
*/
@Around("execution(* com.example.demo.controller.*.*(..))")
public Object recordTime(ProceedingJoinPoint pjp) throws Throwable {
//记录⽅法执⾏开始时间
long begin = System.currentTimeMillis();
//执⾏原始⽅法
Object result = pjp.proceed();
//记录⽅法执⾏结束时间
long end = System.currentTimeMillis();
//记录⽅法执⾏耗时
log.info(pjp.getSignature() + "执⾏耗时 : {}ms", end - begin);
return result;
}
}
运行程序, 观察日志
对代码进行简单分析:
1.@Aspect:标识这是一个切面类
2.@Around:环绕通知,在目标方法的前后都会被执行.后面的表达式表示对哪些方法进行增强
3.ProceedingJoinPoint.proceed()让原始方法执行
整个代码划分为三部分
我们通过AOP入门程序完成了业务接口执行耗时的统计.
通过上面的程序,我们也可以感受到AOP面向切面编程的一些优势:
- 代码无侵入:不修改原始的业务方法,就可以对其进行了功能的增强或者是功能的改变
- 减少了重复代码
- 提高开发效率
- 维护方便
3.Spring AOP核心概念
3.1切点(PointCut)
切点(Pointcut),也称之为"切入点"
切点的作用就是提供一组规则(使用AspectJ pointcut expression language来描述),告诉程序对哪些方法来进行功能增强.
上面的表达式execution(* com.example.demo.controller.*.*(..))就是切点表达式.
3.2连接点(Join Point)
满足切点表达式规则的方法,就是连接点.也就是可以被AOP控制的方法以入门程序举例,所有com.example.demo.controller路径下的方法,都是连接点.
package com.example.demo.controller;
@RequestMapping("/book")
@RestController
public class BookController {
@RequestMapping("/addBook")
public Result addBook(BookInfo bookInfo) {
//...代码省略
}
@RequestMapping("/queryBookById")
public BookInfo queryBookById(Integer bookId){
//...代码省略
}
@RequestMapping("/updateBook")
public Result updateBook(BookInfo bookInfo) {
//...代码省略
}
}
上述BookController中的方法都是连接点
切点和连接点的关系
连接点是满足切点表达式的元素.切点可以看做是保存了众多连接点的一个集合
比如:
切点表达式:学校全体教师
连接点就是:张三,李四等各个老师
3.3通知(Advice)
通知就是具体要做的工作,指哪些重复的逻辑,也就是共性功能(最终体现为一个方法)比如上述程序中记录业务方法的耗时时间,就是通知.
在AOP面向切面编程当中,我们把这部分重复的代码逻辑抽取出来单独定义,这部分代码就是通知的内容.
3.4切面(Aspect)
切面(Aspect)=切点(Pointcut)+通知(Advice)
通过切面就能够描述当前AOP程序需要针对于哪些方法,在什么时候执行什么样的操作.
切面既包含了通知逻辑的定义,也包括了连接点的定义.
切面所在的类,我们一般称为切面类(被@Aspect注解标识的类)
4.通知类型
上面我们讲了什么是通知,接下来学习通知的类型.
Spring中AOP的通知类型有以下几种:
@Around:环绕通知,此注解标注的通知方法在目标方法前,后都被执行
@Before:前置通知,此注解标注的通知方法在目标方法前被执行
@After:后置通知,此注解标注的通知方法在目标方法后被执行,无论是否有异常都会执行
@AfterReturning:返回后通知,此注解标注的通知方法在目标方法后被执行,有异常不执行
@AfterThrowing:异常后通知,此注解标注的通知方法发生异常后执行
通过代码来测试这几个通知:
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 AspectDemo {
//前置通知
@Before("execution(* com.example.demo.controller.*.*(..))")
public void doBefore() {
log.info("执⾏ Before ⽅法");
}
//后置通知
@After("execution(* com.example.demo.controller.*.*(..))")
public void doAfter() {
log.info("执⾏ After ⽅法");
}
//返回后通知
@AfterReturning("execution(* com.example.demo.controller.*.*(..))")
public void doAfterReturning() {
log.info("执⾏ AfterReturning ⽅法");
}
//抛出异常后通知
@AfterThrowing("execution(* com.example.demo.controller.*.*(..))")
public void doAfterThrowing() {
log.info("执⾏ doAfterThrowing ⽅法");
}
//添加环绕通知
@Around("execution(* com.example.demo.controller.*.*(..))")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("Around ⽅法开始执⾏");
Object result = joinPoint.proceed();
log.info("Around ⽅法结束执⾏");
return result;
}
}
编写测试程序:
package com.example.demo.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RequestMapping("/test")
@RestController
public class TestController {
@RequestMapping("/t1")
public String t1() {
return "t1";
}
@RequestMapping("/t2")
public boolean t2() {
int a = 10 / 0;
return true;
}
}
运行程序,观察日志:
1.正常运行的情况
http://127.0.0.1:8080/test/t1
观察日志
程序正常运行的情况下,@AfterThrowing标识的通知方法不会执行
从上图可以看出,@Around标识的通知方法包含两部分,一个"前置逻辑",一个"后置逻辑",其中"前置逻辑"会先于@Before标识的通知方法执行,"后置逻辑"会晚于@After标识的通知方法执行
2.异常时的情况
http://127.0.0.1:8080/test/t2
观察日志:
程序发生异常的情况下:
@AfterReturning标识的通知方法不会执行,@AfterThrowing标识的通知方法执行了
@Around环绕通知中原始方法调用时有异常,通知中的环绕后的代码逻辑也不会在执行
注意事项:
@Around环绕通知需要调用ProceedingJoinPoint.proceed()来让原始方法执行,其他通知不需要考虑目标方法执行.
@Around环绕通知方法的返回值,必须指定为Object来接收原始方法的返回值,否则原始方法执行完毕,是获取不到返回值的.
一个切面类可以有多个切点.
5.@PointCut
上面代码存在一个问题,就是存在大量重复的切点表达式execution(* com.example.demo.controller.*.*(..)),
Spring提供@PointCut注解,把公共的切点表达式提取出来,需要用到时引用该切入点表达式即可.
上述代码就可以修改为:
@Slf4j
@Aspect
@Component
public class AspectDemo {
//定义切点(公共的切点表达式)
@Pointcut("execution(* com.example.demo.controller.*.*(..))")
private void pt(){}
//前置通知
@Before("pt()")
public void doBefore() {
//...代码省略
}
//后置通知
@After("pt()")
public void doAfter() {
//...代码省略
}
//返回后通知
@AfterReturning("pt()")
public void doAfterReturning() {
//...代码省略
}
//抛出异常后通知
@AfterThrowing("pt()")
public void doAfterThrowing() {
//...代码省略
}
//添加环绕通知
@Around("pt()")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
//...代码省略
}
}
注意:
当切点定义使用private修饰时,仅能在当前切面类中使用,当其他切面类也要使用当前切点定义时,就需要把private改为public.
引用方式为:全限定类名.方法名()
@Slf4j
@Aspect
@Component
public class AspectDemo2 {
//前置通知
@Before("com.example.demo.aspect.AspectDemo.pt()")
public void doBefore() {
log.info("执⾏ AspectDemo2 -> Before ⽅法");
}
}
6.切面优先级 @Order
当我们在一个项目中,定义了多个切面类并且这些切面类的多个切入点都匹配到了同一个目标方法
当目标方法运行的时候,这些切面类中的通知方法都会执行,那么这几个通知方法的执行顺序是什么样的呢?
我们还是通过程序来求证:
定义多个切面类:
为简单化,只写@Before和aAfter两个通知:
@Component
public class AspectDemo1 {
@Pointcut("execution(* com.example.demo.controller.*.*(..))")
private void pt(){}
//前置通知
@Before("pt()")
public void doBefore() {
log.info("执⾏ AspectDemo1 -> Before ⽅法");
}
//后置通知
@After("pt()")
public void doAfter() {
log.info("执⾏ AspectDemo1 -> After ⽅法");
}
}
@Component
public class AspectDemo2 {
@Pointcut("execution(* com.example.demo.controller.*.*(..))")
private void pt(){}
//前置通知
@Before("pt()")
public void doBefore() {
log.info("执⾏ AspectDemo2 -> Before ⽅法");
}
//后置通知
@After("pt()")
public void doAfter() {
log.info("执⾏ AspectDemo2 -> After ⽅法");
}
}
@Component
public class AspectDemo3 {
@Pointcut("execution(* com.example.demo.controller.*.*(..))")
private void pt(){}
//前置通知
@Before("pt()")
public void doBefore() {
log.info("执⾏ AspectDemo3 -> Before ⽅法");
}
//后置通知
@After("pt()")
public void doAfter() {
log.info("执⾏ AspectDemo3 -> After ⽅法");
}
}
运行程序,访问接口:
http://127.0.0.1:8080/test/t1
观察日志:
通过上述程序的运行结果,可以看出:
存在多个切面类时,默认按照切面类的类名字母排序:
@Before通知:字母排名靠前的先执行
@After通知:字母排名靠前的后执行
但这种方式不方便管理,我们的类名更多还是具备一定含义的.
Spring给我们提供了一个新的注解来控制这些切面通知的执行顺序:@Order
使用方式如下:
@Aspect
@Component
@Order(2)
public class AspectDemo1 {
//...代码省略
}
@Aspect
@Component
@Order(1)
public class AspectDemo2 {
//...代码省略
}
@Aspect
@Component
@Order(3)
public class AspectDemo3 {
//...代码省略
}
重新运行程序,访问接口:
http://127.0.0.1:8080/test/t1
观察日志:
通过上述程序的运行结果,得出结论:
@Order注解标识的切面类,执行顺序如下:
@Before通知:数字越小先执行
@After通知:数字越大先执行
@Order控制切面的优先级,先执行优先级较高的切面,再执行优先级较低的切面,最终执行目标方法。